use super::*;
#[derive(Args, Debug)]
#[command(
about = "Fetch specific record(s) of ONE transcript by line / turn index / record uuid \
, rendered full, or verbatim raw jsonl with `--raw`; `--branch-points` reports \
where the conversation forked",
long_about = "Fetch the record(s) at specific 1-based jsonl line number(s) (the `Lnnnn` \
every csift surface prints) and/or record uuid(s), from exactly ONE transcript. \
Default output renders each record FULL (label + timestamp + complete text: the \
permission-friendly alternative to `Read`-ing the raw jsonl). `--raw` emits the \
VERBATIM raw jsonl line(s) instead: the escape hatch for inspecting fields csift \
does not render (usage tokens, stop_reason, model, …). `--branch-points` instead \
reports the transcript's conversation FORK facts: every record with more than one \
conversation child (a later parentUuid re-attach: a rewind, a retry, or a \
parallel lane), ranked by the widest inter-child time gap (a rewind usually shows \
a wide gap, a parallel lane near zero). Each fork names the LIVE child and gives \
every other child the conversation chain's verdict; beyond what that chain \
resolves csift does not guess.",
after_help = "EXAMPLES\n \
csift show @<uuid> --line 46550 # the record at line 46550, full\n \
csift show @<uuid> --line 87,495..500,992 # several lines + ranges\n \
csift show @<agent-id> --line 88 # a SUBAGENT transcript (id from `csift agents`)\n \
csift show @<uuid> --uuid <record-uuid> # by record uuid\n \
csift show @<uuid> --line 46550 --raw # the verbatim raw jsonl line\n \
csift show @<uuid> --branch-points # where did this conversation fork (rewind / retry / parallel)?\n\n\
TARGET, exactly ONE transcript\n \
`@<uuid>` / `@<uuid-prefix>` → that top-level transcript (never spans subagents); \
`@<agent-id>` (from `csift agents`) → that subagent transcript; a `*.jsonl` path → \
that file. A target resolving to more or fewer than one transcript is a hard error; \
line numbers address one file.\n\n\
ADDRESSING + EXIT\n \
`--line` / `--turn` tokens take the shared range grammar: `N` · `A..B` · `N..` (to \
the end) · `..N` · `-k` from the end (`--line -20..` = the last 20 lines, `--turn -3..` = \
the last 3 turns); 1-based for lines, 0-based for turns, inclusive, repeatable / \
comma-joined. An explicitly named line/uuid that resolves to no record is a HARD \
ERROR (exit non-zero); a range CLAMPS to the file but erroring if it yields nothing. \
A pending-elicitation record merged from the sidecar has no physical line; address \
it by `--uuid` (it renders `(elicitation sidecar)` in place of `Lnnnn`).\n\n\
RECORDS OUTSIDE THE SURVIVING CONVERSATION\n \
`--turn` speaks the SAME numbering `search` prints, and that numbering follows \
Claude Code's own conversation chain: a record the chain no longer reaches - a \
recalled draft, a turn the operator REWOUND past, anything under one - belongs to \
no numbered turn. An ADDRESS still renders it (the refetch law), as its own \
annotated unit headed `draft (superseded …)` or `rewound turn …` instead of a \
fabricated t<N>, and it states its distance from the message that replaced it: \
`differs from the sent message in N chars (P% of the final)` - insertions plus \
deletions of a shortest CHARACTER edit script, never a length difference, so P can \
exceed 100 when the draft was the longer text; a rewound turn leads with `rewound: \
the conversation continued from L<n> instead`. JSON record rows add \
superseding_line, superseding_uuid, diff_chars, diff_pct and diff_exact (false \
when a bound stopped the walk, in which case both numbers read \"more than\") - all \
five null on any other row - plus survival (live | pre-cut | abandoned), \
abandoned_root_line and replay_copy_of on every row.\n\n\
RAW MODE\n \
`--raw` prints the exact bytes of each addressed jsonl line (even a malformed / \
torn line: that is the point). It is mutually exclusive with `--format json` (raw \
IS the file's own JSON) and reads the transcript file only (no sidecar merge).\n\n\
JSON SCHEMA (per --format json)\n \
Envelope: {kind:\"header\", command:\"show\", session_id, is_subagent, \
parent_session_id, path} → one {kind:\"record\", …} row per fetched record → \
{kind:\"summary\", …}. Record rows carry {session_id, is_subagent, parent_session_id, \
turn_index, line (null for a sidecar-merged record), uuid, label, labels:[…], \
tool_name, from, to, pairing (paired | pending | orphan | null), tool_use_id, \
source (\"elicitation-sidecar\" | null), survival (live | pre-cut | abandoned), \
abandoned_root_line, replay_copy_of, ts_utc, ts_local, text, body, \
image_ids:[…], mode, compact_metadata}. `text` is one line: whitespace runs are \
collapsed to single spaces, and it is FULL under a `--line`/`--uuid` address (which \
lifts the excerpt cap) but CLIPPED to ~400 chars with the explicit `… (+N chars)` \
marker under `--turn`, which is a window rather than an address. `body` carries the \
same rendered text with its NEWLINES intact - so a markdown table or a paragraph \
break survives - and is non-null under exactly the addresses that lift the cap \
(`--line` / `--uuid`), null under `--turn`. It is the RENDERED form, so a record \
whose text the opener renderer already flattened (a user message) carries no \
newline there either; `--raw` stays the path to the bytes. `mode` and `compact_metadata` are \
the compaction pair's own fields and null on every other record: `mode` is \
`compact` | `summarize-from-here` | `summarize-up-to-here` (a `/rewind` summarize \
IS a compaction; the boundary learns its mode from the summary that follows it, and \
stays null when the fetch did not include one), and `compact_metadata` is the \
boundary's own object verbatim, including the `preservedMessages` uuid lists the \
one-line excerpt only counts. The summary is {records, dropped_by_cap, refetch_remainder \
(the ready-to-run continuation command when the cap dropped units, else null), \
non_record_lines, skipped_lines, with_elicitation_sidecar}.\n\n \
Under `--branch-points` the rows are {kind:\"branch-point\", uuid, line, \
parent_line, parent_type, live_child_line, children:[…], widest_gap_seconds} and \
the summary is {branch_points, conversation_records, skipped_lines}. `line` and \
`parent_line` are the SAME value - on this row the record IS the fork parent - and \
both are null when the parent uuid names no line in the file. `live_child_line` is \
the line of the child on the chain, and it is null unless EXACTLY one child is \
live: null with zero live children means the whole fork sits off the surviving \
conversation, and null with two (the chain's membership rules keep \
same-message.id assistant siblings) means the text named both. Each child is \
{line, uuid, record_type, survival (live | pre-cut | abandoned), verdict (live | \
rewound | draft | abandoned | pre-cut), ts_utc, ts_local}."
)]
pub struct ShowArgs {
#[arg(value_name = "TARGET", value_parser = parse_project_target, allow_hyphen_values = true, required = true, num_args = 1..)]
pub target: Vec<std::path::PathBuf>,
#[arg(
long,
value_name = "SPEC",
value_delimiter = ',',
allow_hyphen_values = true
)]
pub line: Vec<String>,
#[arg(long, value_name = "UUID", value_delimiter = ',')]
pub uuid: Vec<String>,
#[arg(
long,
value_name = "N|A..B|N..|-k",
allow_hyphen_values = true,
conflicts_with_all = ["line", "uuid"]
)]
pub turn: Option<String>,
#[arg(long, value_name = "N")]
pub max_count: Option<usize>,
#[arg(long)]
pub raw: bool,
#[arg(long = "branch-points", conflicts_with_all = ["line", "uuid", "turn", "raw"])]
pub branch_points: bool,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
pub format: OutputFormat,
#[arg(long = "no-subagents", hide = true)]
pub no_subagents: bool,
#[arg(long = "subagents", hide = true)]
pub subagents: bool,
}
impl ShowArgs {
#[must_use]
pub fn span_flag_error(&self) -> Option<&'static str> {
if self.no_subagents || self.subagents {
Some(
"`show` has no subagent-span flag: it fetches record(s) from exactly ONE \
transcript and never spans a session's subagents (line numbers are \
per-FILE). Drop the span flag. To read a subagent transcript, target it \
directly: `csift show @<agent-id> --line N` (the id `agents`/`search` \
print); to search across a session AND its subagents use `csift search`.",
)
} else {
None
}
}
}
#[derive(Args, Debug)]
#[command(
about = "One-scan aggregates per session: records, turns, tool calls by name, tokens \
by model, time span, compactions",
long_about = "One scan, one fixed rich shape: the aggregation questions that \
otherwise force hand-rolled jsonl parsing: token burn per model \
(message.usage, counted ONCE per API message: Claude Code repeats the identical \
usage object on every per-block record of one message, so sums dedupe by \
message.id per transcript - a per-record sum over-reads 2-3.5x), narration-block \
counts per model (agent.thinking.narration: API-issued summaries; the token \
split is not derivable, so only blocks are counted), tool-call counts by name (per CALL: one per invocation; \
`search --count-by tool` counts RECORDS, use + result carrier, so it reads ≈2× \
these tallies: a unit difference, not a discrepancy), turn count, first/last \
timestamps + duration, compaction count, malformed-line count, and a whole-file \
LINE-TYPE census (every physical line counted by its top-level `type`: `user`, \
`assistant`, `attachment`, `file-history-snapshot`, `system`, …) - the answer to \
\"what else is in this jsonl besides the conversation\", since non-record lines \
are the majority of many transcripts' bytes and no other surface parses them. \
Spans subagents \
by default (each transcript is its own row; the scope TOTAL block sums them). \
`--since`/`--until` bound the counted records by timestamp. `turns` counts the \
LIVE turns - the numbering that follows Claude Code's own conversation chain, the \
same one `search` prints as `tN` and `show --turn` addresses - so a `stats` turn \
count equals the row count of `search --count-by turn` over the same scope, and a \
`--turn` window resolves against that numbering. What LEFT the numbering is \
disclosed beside it, on one line printed only when there is something to say: \
`chain abandoned turns N (M rewound) - replay copy lines K`. An ABANDONED turn is \
an opener the chain no longer reaches (a recalled draft, or a turn the operator \
rewound past); the REWOUND ones are the subset that drew a reply before the \
rewind; a REPLAY COPY is a line whose uuid a later line also carries, which a \
compaction re-anchor appends. Under `--turn`/time windowing every figure windows \
EXCEPT `lines`, the `types` census and those three chain totals, which stay file \
facts (physical line count / per-type line counts / whole-chain counts - an \
abandoned opener carries no turn index to window on), not window facts.",
after_help = "EXAMPLES\n \
csift stats @<uuid> # one session + its subagents\n \
csift stats @<uuid> --no-subagents # just the top-level thread\n \
csift stats . --since 1d # this project, last 24h\n \
csift stats @<uuid> --format json | tail -1 | jq .tokens # scope token totals\n\n\
JSON SCHEMA (per --format json)\n \
Envelope: header → one {kind:\"session\", …} row per session → summary. Session \
rows carry {session_id, is_subagent, parent_session_id, lines, line_types:{<type>:count}, \
user_records, \
assistant_records, turns, abandoned_turns, rewound_turns, replay_copies, \
compactions, first_utc, first_local, last_utc, last_local, \
tokens:{<model>:{input, output, cache_read, cache_creation}}, \
narration_blocks:{<model>:count}, unknown_thinking_tags, tools:{<name>:count}, \
skipped_lines}. The summary adds the scope totals ({sessions, line_types, tokens, \
narration_blocks, unknown_thinking_tags, tools, turns, abandoned_turns, \
rewound_turns, replay_copies, \
dropped_by_cap, skipped_lines}): `tail -1 | jq .tokens` is the one-liner for total \
burn. `skipped_lines` here is a FULL-SCAN census (stats parses every line): the \
corruption-census authority for \"does this transcript carry a torn/corrupt line \
anywhere\"; `list`'s same-named field covers only the head/tail lines list reads."
)]
pub struct StatsArgs {
#[arg(
value_name = "PATH",
value_parser = parse_project_target,
allow_hyphen_values = true
)]
pub paths: Vec<std::path::PathBuf>,
#[arg(long = "sessions-from", value_name = "FILE|-")]
pub sessions_from: Option<std::path::PathBuf>,
#[arg(long, value_name = "WHEN")]
pub since: Option<String>,
#[arg(long, value_name = "WHEN")]
pub until: Option<String>,
#[arg(
long = "turn",
value_name = "N|A..B|N..|-k",
allow_hyphen_values = true
)]
pub turn_range: Option<String>,
#[arg(long, value_name = "N")]
pub max_count: Option<usize>,
#[arg(long = "no-subagents")]
pub no_subagents: bool,
#[arg(long = "subagents", conflicts_with = "no_subagents")]
pub subagents: bool,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
pub format: OutputFormat,
}
impl StatsArgs {
#[must_use]
pub fn want_subagents(&self) -> bool {
self.subagents || !self.no_subagents
}
}