> ## Documentation Index
> Fetch the complete documentation index at: https://docs.timeless.day/llms.txt
> Use this file to discover all available pages before exploring further.

# Commands

> The timeless command surface: meetings, bot, events, playbooks, and the desktop app.

Every command group prints help on a bare invocation (`timeless meetings`,
`timeless bot`, …) and accepts `--json`. Because the CLI is agentic-first, output is
JSON automatically when piped; pass `--json` to force it anywhere. `timeless <group> --help` is always authoritative for the full flag list.

## Skills

Write the embedded skill bundle into your coding agent's discovery layout so it
knows how to drive the CLI. This is the CLI-side step of [install](/cli/install) —
the desktop app installs the binary and owns login; you (or your agent) install the
skill.

```bash theme={null}
timeless skills install                                  # the configured harness
timeless skills install --harness codex                  # a specific one
timeless skills install --harness claude-code --harness codex   # repeatable
```

`--harness` accepts `claude-code`, `codex`, or `cursor`, is repeatable to wire
several at once, and defaults to the harness in your config. The result lists every
file written per harness.

<Note>
  `skills install` **writes files only — it seeds no config.** Setting up the
  `playbooks:` map is `timeless playbooks init`. If `playbooks list` comes back empty,
  `skills install` will not fix it; the two are separate steps of onboarding.
</Note>

There is no `timeless setup` command in the desktop-app model — the app handles
install and login; `skills install` handles the agent wiring, and
`playbooks init` seeds the default playbooks.

## Auth

The CLI authenticates with **OAuth** (a loopback PKCE flow), not an API token — unlike
the [REST API](/api-reference/introduction), which uses a `Bearer` token. You never
paste a token into the CLI: you sign in through the browser once, and the token is
stored in your OS keychain (with an encrypted file fallback under the config
directory). Every authenticated command resolves its token from that store.

```bash theme={null}
timeless login                     # one OAuth click in the browser
timeless login --session-token     # non-interactive: exchange a session token read from stdin
timeless status --json             # {"authenticated": true, "base_url": "…/v1"}
timeless status --wait 10m --json  # block-poll until a sign-in lands
timeless logout                    # clears the stored token; safe to run repeatedly
```

`status` is a state report, not an identity lookup — there is no `whoami` endpoint. It
exits **non-zero** when you are not logged in, even though it still prints a clean JSON
document, so judge the connection by the exit code. Use `--wait` instead of a `sleep`
loop when something else is performing the sign-in — a human in the desktop app window,
or the app provisioning a token; it exits `0` the moment the token is valid, or non-zero
at the deadline.

`--wait` is not a check. Reach for the plain `status --json` to find out whether you are
authenticated, and add `--wait` only once a sign-in is already under way — otherwise you
block for the full budget to learn something the instant check would have told you.

The desktop app and the bundled CLI **share one token**, so signing in once covers both.
If the app is already signed in, it can provision the CLI's token from that session with
no browser at all. That session is the whole input: if the app is *not* signed in there is
nothing to provision from, and the trigger still reports `emitted: true` while the wait
after it simply expires. `app authenticate` returns immediately, so pair it with a short
`--wait`:

```bash theme={null}
timeless app authenticate          # provision from the app's existing login
timeless status --wait 2m --json   # exit 0 the moment the token lands
```

Conversely, `timeless login` writes to the same shared store the app reads, so the app
can refresh the token going forward.

<Note>
  When any command returns a re-login error, the fix is always `timeless login`. A `401`
  from the API surfaces this way.
</Note>

## Meetings

Read meetings, transcripts, and recordings.

```bash theme={null}
timeless meetings list                    # paginated list of recorded meetings
timeless meetings upcoming                # upcoming calendar events (evt_ IDs)
timeless meetings get <meeting-id>        # one meeting's details
timeless meetings transcript <meeting-id> # timestamped transcript with speakers
timeless meetings recording <meeting-id>  # signed download URL (or null)
```

`list` supports `--status` (`completed|processing|scheduled|failed`), `--start-date`
/ `--end-date` (`YYYY-MM-DD`), `--search`, `--participant`, `--company`, `--room`,
`--limit` (1–100), and `--cursor` for pagination. Paginate by passing the previous
response's `next_cursor` back as `--cursor` while `has_more` is `true`.

`upcoming` lists not-yet-started events from your connected calendar and accepts
`--within-hours` (only events starting within that window), `--limit` (1–100), and
`--cursor`.

<Note>
  `upcoming` returns calendar events whose IDs are `evt_` IDs — exactly what
  `timeless bot schedule <event-id>` expects. Already-recorded meetings carry `mtg_`
  IDs and are returned by `meetings list` instead.
</Note>

## Bot

Control the AI note-taker. `<event-id>` is a **calendar event** ID, not a meeting
ID. Scheduling and cancelling are asynchronous.

```bash theme={null}
timeless bot schedule <event-id>      # ask the bot to join a calendar event
timeless bot cancel <event-id>        # cancel a scheduled bot
timeless bot automation get           # read the auto-join policy
timeless bot automation set <level>   # JOIN_ALL_EVENTS | JOIN_AS_HOST | MANUAL
```

## Events

Drain the delivery queue directly — the building block for custom recap delivery.

```bash theme={null}
timeless events pull --json             # lease pending events and print them
timeless events pull --json --limit 50  # cap how many are leased (1–100)
```

Each event is `{ "id", "type", "created_at", "payload": { … } }`. `event.type` is an
open set — dedupe by the stable `event.id` and skip types you do not handle. Two types
reach the queue today: `meeting.transcript_ready` and `meeting.initial_summary_ready`.

<Note>
  `events pull` is **read-only**: it can display events but never consume them.
  Delivery uses an SQS-style visibility timeout, so a leased event reappears once the
  timeout lapses — inspecting an event, any number of times, never costs it its
  attempt. (`--ack` was removed and now errors; only `playbooks run --event-type`
  consumes events.)
</Note>

## Playbooks run

Turn events into artifacts. This is the **sole entrypoint that executes a playbook**, in
two mutually exclusive forms:

```bash theme={null}
timeless playbooks run --event-type <type>              # event-driven work
timeless playbooks run --event-type <type> --limit 10   # cap events leased per pull (1–100)
timeless playbooks run --event-type <type> --max-age 0  # this pass: process stale events too
timeless playbooks run <name>                           # a scheduled playbook
```

`--event-type` leases pending events of that type and runs **every** enabled playbook
whose `on.event` matches, one attempt each, each writing its own artifact — so three
playbooks on one type turn one meeting into three deliverables. A `<name>` may only be
used for a **scheduled** playbook, which renders its prompt once with time variables
and writes a single artifact without touching the queue. Naming an event-driven
playbook is refused, because two invocations racing for one event is exactly what
`--event-type` prevents.

`--limit` and `--max-age` are event-form only and inert with a playbook name.
`--max-age` overrides [`events.max_age`](/cli/config#freshness-window) for one pass —
`0` disables expiry, `168h` widens it to a week — which is how you deliberately work
through a backlog.

Artifacts are written to `~/.local/share/timeless/artifacts`; the desktop app surfaces
each one in-app. `run` never opens a browser.

The result reports counts at **two levels**, which is the easy thing to get wrong.
`pulled`, `delivered`, `deferred`, and `expired` are **per event**; `attempts`,
`handled`, and `lost` are **per (event, playbook) pair**. One event with three
playbooks gives `delivered: 1` and `attempts: 3`, so `delivered == handled + lost` is
false — compare outcomes against `attempts`. `handled` is the success count; `lost`
entries are listed in `losses[]` with a reason. Exit status is non-zero when `lost > 0`
or `deferred > 0`.

<Warning>
  Delivery is **at-most-once**: each event is acknowledged once, *before* any attempt.
  An event gets one round of attempts, and one that produces nothing is reported in
  `losses` rather than retried. Do not expect a failed run to be redelivered.
</Warning>

<Note>
  `expired` is **not** a failure. An event whose meeting is older than `events.max_age`
  is acknowledged and skipped — no playbook runs, no artifact is written, and the pass
  still exits 0. That is what stops a week away from producing a burst of stale recaps.
  `deferred` does make the exit non-zero, but it is not a lost deliverable: the lease
  succeeded and the acknowledgement did not, so the event was never attempted and is
  still queued for a later run. Reporting one as a loss would be a false alarm.
</Note>

## App

Drive the Timeless desktop app, which owns install readiness and authentication.
These commands talk to the running app over its control channel.

```bash theme={null}
timeless app ready --json            # exit 0 once the app is installed and configured
timeless app ready --wait 5m --json  # block-poll until ready (or the timeout elapses)
timeless app authenticate            # ask the app to authenticate / own token refresh
timeless app sync                    # ask the app to process its due triggers now
```

`app ready` is the install gate: it exits 0 only when the app is installed, running, and
past first-run config. Branch on its `ready` field — the sibling `app_installed` is a
supplementary on-disk check that reads `false` on every platform where bundle detection
isn't implemented (currently everything but macOS), so it never means "not installed".

`authenticate` and `sync` are fire-and-forget triggers, so `emitted: true` means the
trigger fired, not that the app acted on it. Follow `app authenticate` with
`timeless status --wait 2m`.

## Playbooks

A playbook is keyed by **name** and carries its own trigger in an `on:` block — either
an event binding or a schedule (see [Playbooks](/cli/playbooks)). Seed the embedded
defaults and inspect what's configured:

```bash theme={null}
timeless playbooks init --json           # write the default playbooks into config.yaml
timeless playbooks init --harness codex  # …and record codex as the global default
timeless playbooks list --json           # show configured playbooks and each trigger
timeless playbooks disable <name>        # stop it running on future triggers
timeless playbooks enable <name>         # let it run again
```

`playbooks init` is idempotent and never overwrites a prompt you have edited. On a
fresh machine it installs `transcript_notes`, bound to `meeting.transcript_ready`. It
is a pure config write — no binary resolution, no network. Its `--harness` flag doubles
as "set the global default engine", so seeding and choosing an engine is one step; that
also means `changed: true` with an empty `added` is normal (the file was rewritten only
to record the harness).

`list` reports each playbook's name, harness, prompt, and trigger as `event=<type>` or
`schedule=interval:…` / `schedule=at:…`.

`disable` / `enable` write one playbook's `enabled` key. Nothing is deleted, so `enable`
restores it exactly — never delete a playbook to stop it. Both are idempotent and
report `{"playbook": …, "enabled": …, "changed": …}`; `changed: false` is a success, not
a no-op you need to retry. They govern **future** runs and do not abort a pass already
in flight, so one more recap can still land right after.

<Note>
  `playbooks list` does **not** report enabled state — its fields are name, trigger,
  harness, and prompt only. Trust `disable`'s own `changed` field, or read
  `config.yaml`, to confirm a playbook is parked.
</Note>

<Note>
  There is no separate scheduling command and no `timeless schedule`: each playbook's
  cadence lives inline in its `on:` block, and the desktop app's resident timer reads the
  `playbooks:` map directly. The recurring work is the app's — it runs while the machine
  is on and you're signed in, so you never install a scheduler yourself.
</Note>

## Config

Read and write the scalar keys in `config.yaml` (see [Configuration](/cli/config)):

```bash theme={null}
timeless config list --json          # every key and its effective value
timeless config get <key>            # one key's effective value
timeless config set <key> <value>    # persist a key
```

There are exactly seven settable keys, and `config list` is the authority:
`events.max_age`, `harness`, `harness_model`, `output`, and the three `update.*` keys —
which are **inert**, accepted and validated but wired to nothing (see
[Configuration](/cli/config#update-keys-are-inert)). `config get` returns the
**effective** value (env + file + defaults), not the file's text. `base_url` is
env-driven via `TIMELESS_BASE_URL`, and `harness_path` is hand-edited only; neither is
settable here. Playbooks and logging are nested, not scalar — edit them in `config.yaml`,
or seed the playbooks with [`playbooks init`](/cli/playbooks).

## Harness

Report which coding agents this machine can actually launch, and how.

```bash theme={null}
timeless harness list --json                        # every supported agent, configured first
timeless harness list --artifact /abs/recap.html    # scope it to one artifact
```

**You do not normally need this.** It exists for the desktop app's artifact viewer,
whose "Edit with AI" menu has to know what will launch here. It is read-only — no config
write, no subprocess, no network. If you just want the configured harness, `config get
harness` is the direct answer.

Always three entries in a stable order, including agents that are not installed, so a
caller can tell "unsupported" from "supported but missing". Gate on `available` (the OR
of `app_available`, a desktop app, and `cli_available`, a terminal session) — those two
differ in kind per harness. All three unavailable still exits 0 with a complete
document, so check for an `available: true` rather than reading exit 0 as "there is
something to show".

## Version

```bash theme={null}
timeless version --json   # {"version": "…", "commit": "…", "date": "…"}
```

`timeless --version` is equivalent. The CLI does not update itself — the desktop app's
updater owns that.
