# Bout — Agent Integration

Bout is a native macOS menu bar countdown timer. This skill teaches an agent to manage timers, query status, and analyze focus data on the user's behalf.

## Three Integration Paths

1. **URL scheme** — `open "bout://..."` from shell. Fire-and-forget. Works from any context.
2. **Live state file** — Read `timers.json` to get active timers, remaining time, paused state. The source of truth.
3. **Apple Shortcuts** — App Intents for Siri and Shortcuts.app. `GetTimerStatusIntent` returns structured text.

---

## URL Scheme Commands

All commands: `open "bout://..."` — Bout handles them in the background.

### Start
```bash
open "bout://start?duration=25m&label=deep+work"    # natural language duration
open "bout://start?minutes=90&label=design+review"   # integer minutes
open "bout://start?until=15:00&label=standup"         # clock time
```

Duration formats: `25m`, `1h30m`, `90s`, `2d`, `1.5h`. Math: `20m*2`, `7d/7`. Bare numbers = minutes (configurable).

### Stop / Pause / Resume
```bash
open "bout://stop"                    # most urgent timer
open "bout://stop?label=eggs"         # by name (case-insensitive)
open "bout://stop?all=true"           # all timers

open "bout://pause"                   # most urgent
open "bout://pause?label=deploy"      # by name
open "bout://pause?all=true"          # all

open "bout://resume"                  # first paused
open "bout://resume?label=deploy"     # by name
open "bout://resume?all=true"         # all
```

**Addressing:** no qualifier = most urgent running timer. `label=X` = case-insensitive name match. `all=true` = every timer.

### Status
```bash
open "bout://status"   # opens the popover UI (visual only, no data returned)
```

For programmatic status, read the live state file instead (see below).

---

## Reading Live Timer State

The live state file is the source of truth for what's currently running.

**Path:** `~/Library/Application Support/Bout/timers.json`
**Sandboxed path:** `~/Library/Containers/dev.bout.app/Data/Library/Application Support/Bout/timers.json`

```json
{
  "version": 1,
  "timers": [
    {
      "id": "A1B2C3D4-...",
      "endDate": "2026-04-08T15:30:00Z",
      "totalSeconds": 1500,
      "label": "focus"
    },
    {
      "id": "E5F6G7H8-...",
      "totalSeconds": 600,
      "label": "eggs",
      "pausedRemainingSeconds": 180
    }
  ]
}
```

**Deriving state:**
- `endDate` present, in future → **running**. Remaining = `endDate - now`.
- `endDate` present, in past → **expired** (user hasn't dismissed).
- `pausedRemainingSeconds` present → **paused**. That many seconds remain when resumed.
- File empty or missing → no active timers.

```bash
# Quick check: any timers running?
cat ~/Library/Application\ Support/Bout/timers.json | python3 -c "
import json, sys
from datetime import datetime, timezone
data = json.load(sys.stdin)
for t in data.get('timers', []):
    label = t.get('label', 'unlabeled')
    if 'pausedRemainingSeconds' in t:
        print(f'{label}: paused, {t[\"pausedRemainingSeconds\"]//60}m remaining')
    elif 'endDate' in t:
        end = datetime.fromisoformat(t['endDate'].replace('Z','+00:00'))
        remaining = (end - datetime.now(timezone.utc)).total_seconds()
        if remaining > 0:
            print(f'{label}: {int(remaining//60)}m {int(remaining%60)}s remaining')
        else:
            print(f'{label}: expired')
"
```

---

## Event Log (Analytics)

Append-only JSONL at `~/Library/Application Support/Bout/events.jsonl`.

**7 event types:** `start`, `stop`, `expired`, `dismiss`, `pause`, `resume`, `adjust`

Every line is self-contained — denormalized with `label`, `duration`, `timer` UUID. No joins needed.

**Key fields:** `event`, `timestamp` (ISO8601 with tz), `timer` (UUID), `duration` (seconds), `label`, `elapsed`, `remaining`, `source` (popover/preset/url/shortcut), `scheduledEnd`.

### Useful Queries
```bash
LOG=~/Library/Application\ Support/Bout/events.jsonl

# Completed bouts today
jq -s "[.[] | select(.event==\"expired\" and (.timestamp|startswith(\"$(date +%Y-%m-%d)\")))] | length" "$LOG"

# Total focus hours
jq -s '[.[] | select(.event=="expired") | .elapsed] | add / 3600' "$LOG"

# Hours per label
jq -s 'group_by(.label) | map({label:.[0].label, hours:([.[].elapsed]|add/3600|.*10|round/10)})' \
  <(jq 'select(.event=="expired")' "$LOG")

# Completion rate
jq -s '(map(select(.event=="expired"))|length) as $d |
  (map(select(.event=="stop"))|length) as $s |
  {completed:$d, stopped:$s, pct:($d/($d+$s)*100|round)}' "$LOG"

# How bouts are started
jq 'select(.event=="start") | .source' "$LOG" | sort | uniq -c | sort -rn

# All events for one timer
jq 'select(.timer=="A1B2C3D4-...")' "$LOG"
```

---

## Apple Shortcuts / Siri

Available intents (work via Shortcuts.app, Siri, and programmatically):

| Intent | Parameters | Returns |
|--------|-----------|---------|
| Start Timer | minutes (Int), label (String?) | Confirmation |
| Stop Timer | timer entity (optional) | Confirmation |
| Stop All Timers | — | Confirmation |
| Pause Timer | timer entity (optional) | Confirmation |
| Resume Timer | timer entity (optional) | Confirmation |
| **Get Timer Status** | timer entity (optional) | **Structured text with remaining time** |

`Get Timer Status` is the richest programmatic query — returns e.g. `"deep work: 23m remaining. eggs: paused, 5m remaining"`.

Siri phrases: "Start a 25 minute bout", "How much time is left in Bout", "Stop my timer".

---

## Building a CLI Wrapper

If the agent needs a proper CLI that returns data to stdout, it can create a thin wrapper:

```bash
#!/bin/bash
# bout-cli: thin wrapper around Bout's state file and URL scheme
TIMERS=~/Library/Application\ Support/Bout/timers.json
LOG=~/Library/Application\ Support/Bout/events.jsonl

case "$1" in
  start)  open "bout://start?duration=$2&label=${3// /+}" ;;
  stop)   open "bout://stop${2:+?label=${2// /+}}" ;;
  pause)  open "bout://pause${2:+?label=${2// /+}}" ;;
  resume) open "bout://resume${2:+?label=${2// /+}}" ;;
  status)
    python3 -c "
import json,sys
from datetime import datetime,timezone
data=json.load(open('$TIMERS'))
for t in data.get('timers',[]):
  l=t.get('label','unlabeled')
  if 'pausedRemainingSeconds' in t:
    print(f'{l}: paused, {t[\"pausedRemainingSeconds\"]//60}m left')
  elif 'endDate' in t:
    r=(datetime.fromisoformat(t['endDate'].replace('Z','+00:00'))-datetime.now(timezone.utc)).total_seconds()
    print(f'{l}: {int(r//60)}m {int(r%60)}s left' if r>0 else f'{l}: expired')
" ;;
  log)    shift; jq "$@" "$LOG" ;;
  *)      echo "usage: bout-cli {start|stop|pause|resume|status|log} [args]" ;;
esac
```

Usage: `bout-cli start 25m "deep work"`, `bout-cli status`, `bout-cli log 'select(.event=="expired")'`

---

## Notes

- **URL-encode labels:** spaces as `+` or `%20` in URL scheme commands.
- **Duration is always seconds** in the event log. Divide by 60 for minutes, 3600 for hours.
- **Nil fields are omitted**, not null. A timer with no label has no `"label"` key.
- **Timestamps are local ISO8601 with timezone offset.** Display directly, no conversion needed.
- **The `timer` UUID** links all events for one timer instance: start → pause → resume → expired → dismiss.
- **The state file updates on every transition** (start/stop/pause/resume/expire/dismiss), not on tick. It's always current within one state change.
