@phnx-labs/sessions-cli
v0.8.3
Published
High-performance session index: search, list, and preview agent transcripts
Readme
sessions
High-performance session index for agent transcripts. The installed binary is sessions.
It reads the existing SQLite index at ~/.agents/.history/sessions/sessions.db
(override with $SESSIONS_DB). It reads the index directly — no daemon boot, no
transcript-directory walk.
npm i -g @phnx-labs/sessions-cli
sessions # browse (TTY): this dir, last 30 days, preview pane, hotkeys
sessions --all # every directory, all time
sessions "auth middleware" --json --limit 5
sessions a1b2c3d4 --json
sessions --limit 20 --no-interactive # print the list instead of the browserCommand reference
scripts/generate-cli-docs.sh # refresh docs/
scripts/generate-cli-docs.sh --check # fail on stale docs
scripts/generate-cli-docs.sh --out-dir .agents/scratch/cli-docs # previewRun after bun install --frozen-lockfile. The script works from any directory;
--out-dir is relative to your current directory. Open command-reference.html
from the output folder to browse the tree and search commands.
CI uploads a sessions-cli-docs artifact on pull requests and pushes to main
or release/**. It contains the HTML reference plus Markdown and JSON indexes.
Tests and releases check the committed files; commit regenerated docs/ with
command changes. Release checks also run with --skip-tests / --skip-build.
Browse the generated HTML command reference or
Markdown command index. Regenerate after changing command
help with scripts/generate-cli-docs.sh; the test script checks that these files stay current.
Generation requires Bun 1.4 or later because the program imports node:sqlite.
Interactive browser
In a terminal, a bare sessions opens the session browser agents sessions had:
one filter state shown in the header (device:all · agent:all · team:all · this repo ·
window:30d), rows with id, agent, version, machine (when the list spans more than
one), project, title, ticket/PR, worktree and age, and a preview pane with the
highlighted session's card (agent and model, cwd, branch, timing, ticket, then
Asked / Doing / Made / Health / Cost / Latest / Details).
| Key | Does |
|---|---|
| s | search the loaded rows: the visible fields, plus the full transcript text through the index |
| a / d / t | cycle agent / machine / team over what the list contains |
| p | this directory's subtree ⇄ every directory |
| w | window: all → 1d → 7d → 30d |
| c | include team-spawned sessions (teammates, headless title generators, Codex sub-agent threads) |
| b / * | only bookmarked sessions / bookmark the highlighted one |
| tab | hide or show the preview |
| y | copy the sessions … command that reproduces the view (printed when no clipboard tool) |
| ⏎ | resume: runs agents run <agent> --resume <id> (agents-cli owns resume), plus --device <host> for a --host row, or prints that command when agents is not installed |
| esc | leave search (the filter stays), clear the filter, then quit |
A positional query opens the browser on the sessions whose text matches, all time.
--all, --teams, --in-team <name>, --bookmarks, --machine <id>, --since,
--agent, --path, --skill, --plugin and --routine seed the same state. The browser is skipped — and the plain
list printed instead — with --json, --no-interactive, --tree/--flat, a direct
id lookup, or whenever stdout/stdin is not a TTY, so machine callers are unaffected.
The browser and the printed list hide team-origin sessions unless --teams: Claude
SDK spawns the index flags, and Codex sub-agent threads (their transcript's
session_meta says source: subagent). --json, and the read a --host caller
runs on its peer, return every row, with isTeamOrigin set on those sessions.
A row's title skips context a SessionStart hook or the harness injected (## Team &
Agents, Codex's /root role) for the first genuine user turn. Ids that share
their first 8 characters (Codex's time-ordered ids) are widened until each listed
row reads distinctly; --json keeps the index's shortId.
Flags agents sessions had that belong elsewhere now (--active and its status
filters, --roots, --print-cmd, --cloud,
--browser, --computer) exit 2 and name where that lives instead of being read
as search text.
Filter and sort the listing (composable, and honored with or without a query):
sessions --agent claude # by harness (or the shorthand: --claude)
sessions --agent [email protected] # harness pinned to a version
sessions --project my-app # by project NAME (-p)
sessions . # by PATH: sessions started in this dir (or under it)
sessions ~/src/app --since 7d # a bare dir is a path filter; composes with others
sessions --path /abs/dir # explicit form (alias: --cwd)
sessions --since 7d --until 1d # active window: 2h/7d/4w/1mo/1y or an ISO date
sessions --sort cost # order by cost (also: duration, recent)
sessions --tree # group the printed list by project (--flat is the default)
sessions --teams # include team-spawned sessions (hidden from the list by default)
sessions --in-team auth-team # one team: the session that spawned it and its teammates
sessions --bookmarks # only bookmarked sessions
sessions --machine yosemite-m2 # only sessions the index recorded on that machine
sessions --skill design # sessions that invoked a skill: design, or any <plugin>:design
sessions --plugin rush # sessions that used any skill or command the rush plugin owns
sessions --routines # transcripts routine runs archived (alias: --routine)
sessions --routine nightly-review # one routine's: exact, unique substring, or one-typo name
sessions 2026-10-06T12-00-00-000Z # every transcript one routine run archived (a run id or its prefix)--routines / --routine [name] read the transcripts agents-cli archived from
routine runs (origin routine), across every directory and all time; every other
filter still narrows them. Without --limit they return up to 5000 rows (an
ordinary read keeps 20, at most 500), and an explicit --limit is honoured, also
in the browser, whose routine pool otherwise holds 1000. A name resolves against
the routines with an indexed transcript (exact, then one containing match, then
one unambiguous typo); an unknown or ambiguous name exits 1 and lists them. With
no name a terminal asks which routine, then browses its transcripts; piped or
--json, every routine transcript is returned. Rows carry routineName and
routineRunId. A run id names every transcript the run archived, so a
one-session verb (render, trace, timeline, preview, artifacts,
--resolve) refuses a run with several and lists their session ids; any id
prefix several sessions share is refused the same way. Those verbs take the
listing's filters to pick one (sessions trace <run-id> --agent [email protected],
--project, --path, --since/--until, --routine), applied before the
ambiguity check and never capped by --limit; a full session id ignores them
except --routine/--skill/--plugin. sessions render <run-id> without
--json renders every transcript of the run into one document, and
sessions export <run-id> bundles them; the same filters narrow the run first and
-n caps it. Routines that never wrote
an agent transcript (command-only, deleted jobs) are not in the index: their run
history is agents routines runs <name>.
--skill and --plugin read the per-session usage agents-cli indexes
(session_resource_usage), case-insensitively. Both compose with every other
filter and a full-text query, apply before --limit, ride to --host peers, and
narrow <id> and --resolve to that session only when it used them. An index without the usage
table is refused rather than read as empty.
Bookmark from a script with sessions bookmark. It writes the same
~/.agents/.history/bookmarks.json the browser's * does, so each sees the
other's marks:
sessions bookmark a1b2c3d4 0f9e8d7c # set (not toggle); --remove clears
sessions bookmark --json # no ids (or --list): {"bookmarks": [sorted full ids]}
sessions bookmark a1b2 nope --json # {"results": [{query, id, bookmark} | {query, error}]}, exit 1A selector is a full id, short id, or unique prefix in the index. An ambiguous
one is an error, never the first match. A complete id the index has not seen
(a UUID, session_<UUID>, ses_<26 chars>) is kept as given. In a batch the
valid ids are applied even when another fails; any failure exits 1 after the
whole JSON is written. Bookmarks are per-machine: --host is refused, and
sessions --host <h> --bookmarks filters the peer's rows by this machine's
bookmarks.
--path/--cwd (and a bare positional with a path sigil — ., .., ~,
~/src/app, ./x, ../x, or an absolute /path) match sessions whose cwd is
that directory or under it. --project is the registered project name;
--path is where the session actually ran. A plain word (sessions auth, even
sessions src where src/ exists) stays an FTS query — only a sigil, or the
explicit --path/--cwd flag, means a directory. The path is normalized to an
absolute path once, so it composes with every filter, feeds the picker in a TTY,
and rides to every --host.
Read other machines' indexes over SSH with --host (short -H; -D/--device
is an alias). A target is an OpenSSH alias, user@host, or ssh://user@host;
any other scheme is refused. Repeat the flag or comma-separate to read several:
sessions "auth middleware" --host yosemite-s0 --json # one host
sessions --limit 20 -H muqsit@box # a bare listing, remotely
sessions --host yosemite-s0,yosemite-s1 # merge two hosts
sessions -H yosemite-s0 -H box --json --limit 50 # same, repeatedWith one host, sessions SSHes there, runs the remote sessions with --local,
and streams its stdout back (so --json is byte-for-byte); it exits with the
remote's code. One connection per host, no multiplexing.
The one-session reads follow the same host: preview, render (and
sessions <id> --markdown), artifacts, trace and timeline with one
--host run on that host and print its answer, never this machine's copy of the
same id. render -o <file> --host <h> writes the peer's document to the local
file. An unreachable host fails the read (ssh's exit 255); several hosts, or a
--host with no target in it (--host ,), are refused. --local keeps the read
on this machine even with --host. share and tail work on this machine only and refuse --host: pull
the session first with sessions import --from-host <h> <id>, or follow it there
with ssh <h> sessions tail <id>.
sessions render a1b2c3d4 --host yosemite-s1 # the transcript on yosemite-s1
sessions artifacts a1b2c3d4 report.md -H yosemite-s1 # a file that session wrote thereWith several hosts, each peer is read with --json --local (so no peer recurses back over SSH), its
rows are tagged with the host they came from, and the results are merged
newest-first and capped to --limit. An unreachable or failing peer is reported
as a dim warning rather than aborting the whole read. In a TTY the merged rows
feed the same picker; with --json/--no-interactive they print as one list.
There is no all/fleet keyword: the read engine keeps no device registry, so
name the hosts explicitly. To fan
across the whole fleet, feed it from agents-cli:
sessions --host "$(agents devices list --json | jq -r '.[]|select(.tailscale.online).name' | paste -sd,)"Hidden fast paths (no commander tree):
sessions __query --json --limit 5 -- auth
sessions __serve # request JSON on fd 3, response on fd 4Read one session's content:
sessions render a1b2c3d4 -o s.md # a redacted document: preview card + conversation (owner-only file)
sessions render a1b2c3d4 d4c3b2a1 # several sessions, one document; --reasoning omit|fold|include
sessions a1b2c3d4 --markdown # just the conversation as Markdown
sessions render a1b2c3d4 --json # { session, events } JSON
sessions render a1b2c3d4 --include user,assistant # by role; also --exclude, --first N, --last N
sessions share a1b2c3d4 # publish a redacted page through `artifacts share`
sessions --resolve a1b2c3d4 --json # one session's safe metadata (id, unique prefix, label, or keywords)
sessions tail # stream the latest Claude/Codex session as it is written
sessions trace a1b2c3d4 # compact tool-call trajectory
sessions timeline a1b2c3d4 # narration-anchored step list
sessions artifacts a1b2c3d4 # files the session wrote (--json for a list)
sessions artifacts a1b2c3d4 plan.md # print one written file (by path, basename, or suffix)
sessions preview a1b2c3d4 # the picker's preview card, non-interactiverender output is redacted (secrets + home paths) unless --no-redact (local only).
Search tool calls across sessions (the index's tool tables; read-only):
sessions --include tools --query 'program:git input:merge' --since 7d
sessions --include tools --query 'program:git input:merge' --query 'program:gh output:CONFLICT' --json
sessions --include tools --query program:gh --count --json # every indexed gh call siteMove sessions between machines as a portable bundle (the agents-cli format):
sessions export --since 7d -o week.bundle
sessions export --since 7d --stdout | ssh boxB 'sessions import -'
sessions import --from-host boxA --since 1dOr back them up off-box and restore them on any machine:
sessions export --since 30d --to-r2 # managed Phoenix store when signed in ('agents auth login')
sessions import --from-r2 # on a fresh box: recovers your per-account key
sessions export --since 30d --to-r2 --byo # your own bucket from the r2.backups secrets bundleManaged backups are always sealed with AES-256-GCM under a per-account key that is
escrowed with the store, so a fresh machine can restore. --byo (or
AGENTS_SESSIONS_BACKEND=byo) uses the r2.backups bundle, resolved by the
installed secrets CLI; add R2_SYNC_ENC_KEY to it so neither Phoenix nor the
storage provider can read the objects. To back up to a staging or self-hosted
managed store (a backup Worker deployed with agents sessions backup-setup --domain
<host>), set AGENTS_SESSIONS_URL=https://<host>; your Phoenix sign-in is sent
to it, so use only a store you run or trust.
sessions reads and moves sessions; it does not resume, inject, or stop them —
the browser's Enter hands resume to agents run --resume.
Library
The transcript reader is also importable in-process, so you can parse and render a transcript without shelling out to the bin:
import { parseSession, renderConversationMarkdown, buildTrajectory }
from '@phnx-labs/sessions-cli/reader';
const events = parseSession('/path/to/transcript.jsonl');
const markdown = renderConversationMarkdown(events, meta);The reader subpath exposes the parse → render → timeline → trajectory → state
pipeline (parsers for every harness, the SessionEvent/SessionMeta types, and
the render / analysis helpers) with TypeScript declarations. It is a complete
superset of the reader agents-cli carries — including insight facets
(computeInsightFacets), artifact discovery (discoverArtifacts), bounded
tail/head reads (readSessionTail / readSessionHead), the streaming line
renderer (makeStreamRenderer), and the HTML document renderers
(renderSessionHtmlDocument, renderTrajectoryHtml). It is pure and read-only —
no process spawn, no network, no writes.
The backup subpath (@phnx-labs/sessions-cli/backup) carries the off-box
backup library behind --to-r2 / --from-r2: the managed and bring-your-own
clients, key escrow, uploadBackup / pullBackup, and the managed store's Worker
source (renderSessionsWorkerScript) that agents sessions backup-setup deploys. Unlike
reader, it talks to the network and caches a key.
See PHNX-4012 and PHNX-4118.
