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). Facts only: which side is live is not \
computable from the jsonl, so 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\
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), ts_utc, ts_local, text (FULL; never \
clipped), image_ids:[…]}. 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}."
)]
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 sums), 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. Under `--turn`/time \
windowing every figure windows EXCEPT `lines` and the `types` census, which stay \
file facts (physical line count / per-type line counts), 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, compactions, first_utc, first_local, last_utc, last_local, \
tokens:{<model>:{input, output, cache_read, cache_creation}}, tools:{<name>:count}, \
skipped_lines}. The summary adds the scope totals ({sessions, line_types, tokens, tools, \
turns, \
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
}
}