use super::*;
#[derive(Debug, Parser)]
#[command(
name = "csift",
version,
about = "ripgrep for Claude Code session transcripts",
long_about = "csift - \"ripgrep for Claude Code session transcripts\".\n\n\
A fast Rust CLI to list and regex-search Claude Code session `.jsonl` files \
under ~/.claude/projects/<encoded>/. Built for an LLM consumer (a CC agent \
searching or recovering its own / a peer session): output is clean, \
token-efficient and regex-driven. Human/LLM-readable text by default; pass \
`--format json` for machine use.\n\n\
Pure regex/ripgrep only: no BM25, no embeddings, no semantic search, by design.\n\n\
SUBCOMMANDS\n \
list fast \"which session is this?\" index (first/last user + last agent)\n \
search regex over transcripts, returning the complete round-trip exchange per hit\n \
show FETCH the record(s) you name: `--line` / `--turn` / `--uuid` of ONE transcript\n \
(`--turn -3..` = the last 3 turns, the live-tail peek), rendered full or `--raw` bytes\n \
stats one-scan aggregates per session: tokens by model, tool calls, turns, span\n \
agents list a session's subagents (kind, start/completion, status) + time-window filter\n \
whoami identify the calling CC session via $CLAUDE_CODE_SESSION_ID\n \
files which files/dirs a session modified, when (Edit/Write/Notebook + heuristic Bash)\n \
recover reconstruct a file's history from the transcript: segmented diff-patches,\n \
point-in-time partial snapshot, or coverage scoping\n \
plan locate the Plan-Mode plan file bound to a session (recover --file @plan dumps it)\n \
verbatim RESTORE the verbatim user/assistant back-and-forth a compaction summary\n \
CLIPPED, within a char budget (live-tail peek is `show --turn`)\n \
image list + extract the inline images a session carries (pastes/screenshots)\n\n\
list/search/stats/files/recover/plan/image span each session's subagent transcripts by \
default (built-in Task/Agent-tool, OMC, and Workflow agents); pass `--no-subagents` \
to restrict to top-level sessions. `verbatim` is the exception among the file-operating \
commands: it defaults to the TOP-LEVEL thread only (single-thread recovery), \
opts INTO spanning via `--subagents`, and REQUIRES a target. (`agents` LISTS \
subagents as its targets, not as inputs to span, so it rejects both \
span flags.)\n\n\
A target is EITHER a real filesystem cwd (it gets path-encoded) OR an \
already-encoded `-Users-...` projects-dir token; with no target, every \
project under ~/.claude/projects is scanned.\n\n\
TARGETING a session/agent (always positional): `@<uuid>` (or its \
leading-hex prefix, e.g. `@13d9645a`) scopes to one session; `@<agent-id>` to one \
subagent + its subtree (a bare hex ≥12 OR a teammate `aName-<hex>` id; exactly what \
`csift agents` prints); `@main` is the CALLING top-level session (read from \
$CLAUDE_CODE_SESSION_ID); a `*.jsonl` path scopes to that one transcript. A BARE uuid \
(no `@`) is NOT special. `--sessions-from <FILE|->` scopes to a LIST of ids (one per \
line, e.g. piped from `search -l`) on every multi-target subcommand.\n\n\
@trap:<marker> answers \"which subagent am I?\" on EVERY subcommand (including `whoami`, which \
reports your bare hex + parent uuid). CC withholds a subagent's OWN id from its Bash \
environment, so a running subagent cannot name itself via env. Instead you INVENT a marker \
and embed it LITERALLY in this very csift command; csift finds the transcript whose Bash \
`csift` command carries that marker and scopes to that agent's subtree. TIMING: a \
subagent's transcript flushes per content block, so its command is on disk at dispatch \
and a first try resolves; the MAIN conversation's record is an async flush of the \
completed assistant message landing ~1-3.4s after dispatch, so a top-level FIRST use \
races it and normally misses; from the main thread use `@main` instead (env-based, no \
race), or re-run the SAME command with the SAME marker. \
DISCIPLINE (enforced): invent the marker ONE-SHOT, by you, right now: an imaginative, \
literary, random, CONTEXT-INDEPENDENT token of EXACTLY 3 CamelCase words + 4 random digits, \
shaped like `@trap:JollyShinyBrook4283`. That exact literal is the RESERVED doc \
example, refused by csift (a copied example self-collides into ambiguity), so invent your \
OWN. NEVER generate it with a script (a generator would be \
another `csift`-ish command carrying the marker → ambiguity); never build it from a \
shell variable or string concatenation (it must appear VERBATIM in the recorded \
command); never reuse a previous one. csift REJECTS lazy markers: not EXACTLY 3 words, \
single-letter or ALLCAPS \"words\" (`HTML`/`USB`…), not exactly 4 trailing digits, or \
trivial digits (1111 / 1234 / 9876 / 1357 / 2468).",
after_help = "EXAMPLES\n \
csift list # index every session, all projects\n \
csift list . # just this cwd's project\n \
csift search \"carry\" # smart-case regex, all projects\n \
csift search \"\" -t user --since 2h . # pure filter: user turns, last 2h, here\n \
csift agents @<uuid> --since 2h # subagents TRIGGERED in the last 2h\n \
csift whoami # who am I (this CC session)?\n \
csift files @<uuid> --by file # which files this session modified, when\n \
csift recover @<uuid> --file /abs/app.py # segmented diff-patch history of a file\n \
csift recover . --file /abs/app.py --at @turn:42 # partial snapshot as the LLM saw it at turn 42\n \
csift verbatim . --budget 40000 # restore the verbatim back-and-forth a summary clipped\n \
csift plan @<uuid> # locate the plan file bound to a session\n \
csift image @<uuid> --out /tmp/imgs # extract every pasted image to a dir\n \
csift show @<uuid> --line 46550 # fetch the exact record a hit cited, in full\n \
csift show @<uuid> --line 46550 --raw # …or its verbatim raw jsonl bytes\n \
csift stats @main --no-subagents # this session's token/tool/turn aggregates\n \
csift search \"panic\" -l | csift stats --sessions-from - # aggregate exactly the sessions that matched\n \
csift search \"\" @<uuid> -t agent -T agent.thinking # the agent role minus its thinking\n\n\
THE RULES EVERY COMMAND FOLLOWS\n \
Exit codes An address you NAMED that doesn't resolve (a line, a turn, a uuid, a\n \
pinned id, a file) is a hard error (exit != 0). A filter that matches nothing is an\n \
honest empty (exit 0); a zero-match search additionally prints a diagnosis on stderr\n \
explaining what was searched and, under a label filter, where the pattern DOES occur.\n \
Ranges Every range flag shares one grammar: N | A..B | N.. | ..N | .. | -k,\n \
endpoints inclusive; `-k` counts from the end, so `-3..` means \"the last 3\". Turns\n \
are 0-based logical indices (the `tN` search prints); lines are 1-based physical\n \
jsonl lines (the `Lnnnn`). Different axes: read both from output, don't compute.\n \
Subagents Most commands include a session's subagent transcripts automatically;\n \
`--no-subagents` restricts. `verbatim` is the one opt-in (`--subagents`, because its\n \
budget multiplies per session), and `agents` lists subagents instead of spanning.\n \
Caps No silent truncation, ever: every cap prints what was dropped and how\n \
to get the rest. `list` caps an unscoped run at 50 rows; `show` caps at 200 record\n \
units and prints the exact continuation command; `--max-count 0` = uncapped, always.\n \
Time Every timestamp prints in YOUR local timezone with the offset inline,\n \
like `2026-07-11 15:33:37 AEST(UTC+10)`, derived per instant (DST-correct; fractional\n \
zones render like `IST(UTC+05:30)`). Raw UTC lives in JSON `ts_utc`, never in text.\n\n\
JSON OUTPUT (--format json)\n \
Every command emits the same envelope: one `{\"kind\":\"header\",…}` line, then\n \
kind-tagged rows, then one `{\"kind\":\"summary\",…}` line, even for zero matches.\n \
Select rows with `jq 'select(.kind==\"…\")'`; the summary is always `tail -1`. Do\n \
the select BEFORE projecting fields; a field template applied to the mixed stream\n \
stamps the header/summary rows as all-null rows. Rows\n \
carry the id trio (`session_id` / `is_subagent` / `parent_session_id`), INCLUDING\n \
every search hit inside `.hits[]` (v0.6.5), so bare `.hits[]` flattening carries\n \
real ids; each hit also carries `refetch`: a ready-to-run `csift show` command for\n \
that exact record (still the preferred single-record path). Full\n \
row schemas live in each subcommand's --help.\n\n\
PITFALLS WORTH KNOWING UP FRONT\n \
- An empty pattern (\"\") matches EVERYTHING: it is the base for -t/time/turn\n \
filters and the census on-ramp (`csift search \"\" <target> --count-by label`).\n \
- `search -c` counts round-trip EXCHANGES, not records or lines; per-record\n \
counts are `--count-by <axis>`; `stats`' tool tallies are per CALL. Three units\n \
(exchange, record, call), so `--count-by tool` reading ≈2× a `stats` tally\n \
(use + result carrier) is a unit difference, not a discrepancy.\n \
- `search -l` prints owning SESSION uuids (what `--sessions-from -` consumes);\n \
the JSON summary's `transcript_ids` is the finer per-transcript list. Two\n \
different questions, two different answers.\n \
- Line numbers are per FILE: fetch a subagent's line at the subagent's own id,\n \
never at the parent uuid. The printed `refetch` command already has this right.\n \
- Reading recent turns is `show <target> --turn -3..`; `verbatim` is only for\n \
turns a compaction summary already clipped (it tells you when nothing was).\n \
- A pending AskUserQuestion / ExitPlanMode / MCP prompt is invisible in the\n \
transcript until answered. csift merges pending ones from a hook-written sidecar\n \
when present, and `list` reports `sidecar_present`, so you can tell \"nothing\n \
pending\" apart from \"no hook installed\". A sidecar pending whose elicitation the\n \
NATIVE transcript already shows closed (e.g. a REJECTED plan: Claude Code fires\n \
no PostToolUse for a rejection, so the hook never got to write its resolved\n \
marker) is auto-dropped: the native record outranks the sidecar.\n \
- Text-mode excerpts keep a record's LITERAL newlines (a multiline Bash command\n \
renders as-is), so piping text output through `head -N` can cut mid-record and\n \
hide the overflow pointers. The line-safe machine form is `--format json` (one\n \
object per line); the honest caps are `--max-count` and the built-in drop reports.\n\n\
WHAT csift WILL NOT DO\n \
Semantic/BM25 search (regex is the tool; broaden the pattern, or census first);\n \
ad-hoc aggregation languages (the closed `--count-by` axes, `stats`, and\n \
`files --by` are the built-ins; anything else: `--raw | jq`); diffs (fetch both\n \
sides with `show` / `recover --at`, diff outside); writing or terminating\n \
anything. csift only reads.\n\n\
RETENTION\n \
Claude Code deletes transcripts older than `cleanupPeriodDays` (default 30!).\n \
Check `jq '.cleanupPeriodDays // 30' ~/.claude/settings.json` and consider raising\n \
it; csift can only read what survives.\n\n\
Run `csift <subcommand> --help` for per-subcommand flags, JSON schemas + examples."
)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
#[arg(long = "claude-home", value_name = "DIR", global = true)]
pub claude_home: Option<PathBuf>,
}
#[derive(Debug, Subcommand)]
pub enum Command {
List(ListArgs),
Search(SearchArgs),
Show(ShowArgs),
Stats(StatsArgs),
Whoami(WhoamiArgs),
Agents(AgentsArgs),
Files(FilesArgs),
Recover(RecoverArgs),
Plan(PlanArgs),
Verbatim(VerbatimArgs),
Image(ImageArgs),
#[command(hide = true)]
Turns(TurnsRenamedArgs),
}
#[derive(Args, Debug)]
pub struct TurnsRenamedArgs {
#[arg(trailing_var_arg = true, allow_hyphen_values = true, num_args = 0.., hide = true)]
pub rest: Vec<std::ffi::OsString>,
}