@claude-transcripts/cli 0.0.14 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.js +720 -45
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -90,6 +90,7 @@ upgrades in place: it's idempotent and keeps your history.
90
90
  |---|---|
91
91
  | `sessions [id] [options]` | List / inspect sessions (via the webapi) |
92
92
  | `search <query> [options]` | Search the corpus |
93
+ | `turns [session] [options]` | Speaker-split turns: one session, or one speaker across all sessions |
93
94
  | `backfill [options]` | Adopt on-disk ~/.claude transcripts as first-class history |
94
95
 
95
96
  **Portability**
@@ -107,6 +108,7 @@ upgrades in place: it's idempotent and keeps your history.
107
108
  | `reindex` | Rebuild the search indexes from CouchDB |
108
109
  | `doctor [options]` | Smoke-test the write/read/search path end-to-end |
109
110
  | `hook [action] [options]` | The Claude Code hook, and its registration |
111
+ | `statusline [action] [options]` | The Claude Code statusline indicator (recording / off), and its registration |
110
112
 
111
113
  **Global options** (every command)
112
114
 
package/dist/cli.js CHANGED
@@ -29659,6 +29659,16 @@ var ACTIONS = [
29659
29659
  summary: "Reset counters + seed the chunk offset at start",
29660
29660
  implemented: false
29661
29661
  },
29662
+ {
29663
+ key: "announce-recording",
29664
+ summary: "Tell the user, in the transcript, whether and where this session is recorded",
29665
+ implemented: false
29666
+ },
29667
+ {
29668
+ key: "inject-recall-policy",
29669
+ summary: "Prime the session with the recall policy + how much history this cwd has",
29670
+ implemented: false
29671
+ },
29662
29672
  { key: "enrich-metadata", summary: "Post additional session metadata", implemented: false },
29663
29673
  {
29664
29674
  key: "extract-feature",
@@ -29679,7 +29689,15 @@ var ACTIONS = [
29679
29689
  }
29680
29690
  ];
29681
29691
  var BINDINGS = [
29682
- { event: "SessionStart", actions: ["seed-session-start", "write-event-marker"] },
29692
+ {
29693
+ event: "SessionStart",
29694
+ actions: [
29695
+ "seed-session-start",
29696
+ "write-event-marker",
29697
+ "announce-recording",
29698
+ "inject-recall-policy"
29699
+ ]
29700
+ },
29683
29701
  {
29684
29702
  event: "UserPromptSubmit",
29685
29703
  actions: ["write-event-marker", "update-counts", "flush-transcript-chunk"]
@@ -29706,6 +29724,10 @@ var WEBAPI = {
29706
29724
  type: "string",
29707
29725
  description: "webapi base URL (default: $CT_WEBAPI_URL)"
29708
29726
  };
29727
+ var JSON_OUT = {
29728
+ name: "--json",
29729
+ description: "print the webapi response as JSON instead of a table"
29730
+ };
29709
29731
  var CLI_SPEC = {
29710
29732
  groups: [
29711
29733
  { key: "lifecycle", title: "Lifecycle" },
@@ -29732,6 +29754,7 @@ var CLI_SPEC = {
29732
29754
  },
29733
29755
  { name: "--meili-key", description: "generate a Meilisearch master key" },
29734
29756
  { name: "--no-hook", description: "skip Claude Code registration" },
29757
+ { name: "--no-statusline", description: "register the hook but not the statusline" },
29735
29758
  { name: "--no-app", description: "skip the app container (run the webapi yourself)" },
29736
29759
  { name: "--no-prune", description: "keep superseded app images" },
29737
29760
  { name: "--skip-preflight", description: "continue past failed preflight checks" },
@@ -29792,9 +29815,10 @@ var CLI_SPEC = {
29792
29815
  name: "--limit",
29793
29816
  type: "number",
29794
29817
  description: "rows to list (default 50) / transcript entries to preview (default 30)"
29795
- }
29818
+ },
29819
+ JSON_OUT
29796
29820
  ],
29797
- examples: ["sessions", "sessions --limit 10", "sessions 3f9a2c1e --limit 80"]
29821
+ examples: ["sessions", "sessions --limit 10", "sessions 3f9a2c1e --limit 80 --json"]
29798
29822
  },
29799
29823
  {
29800
29824
  name: "search",
@@ -29815,9 +29839,42 @@ var CLI_SPEC = {
29815
29839
  name: "--source",
29816
29840
  type: "string",
29817
29841
  description: "only this provenance (live | backfill | \u2026)"
29818
- }
29842
+ },
29843
+ JSON_OUT
29819
29844
  ],
29820
- examples: ['search "retry policy"', "search deploy --cwd ~/proj --limit 5"]
29845
+ examples: ['search "retry policy"', "search deploy --cwd ~/proj --limit 5 --json"]
29846
+ },
29847
+ {
29848
+ name: "turns",
29849
+ group: "daily",
29850
+ summary: "Speaker-split turns: one session, or one speaker across all sessions",
29851
+ args: [
29852
+ { name: "session", description: "session id \u2014 that session's turns (omit: all sessions)" },
29853
+ {
29854
+ name: "--role",
29855
+ type: "string",
29856
+ choices: ["user", "assistant", "tool_result", "system", "other"],
29857
+ description: "only this speaker"
29858
+ },
29859
+ {
29860
+ name: "--from",
29861
+ type: "string",
29862
+ description: "all sessions: only turns at/after this ISO instant"
29863
+ },
29864
+ {
29865
+ name: "--to",
29866
+ type: "string",
29867
+ description: "all sessions: only turns at/before this ISO instant"
29868
+ },
29869
+ { name: "--limit", type: "number", default: 50, description: "turns to show" },
29870
+ { name: "--skip", type: "number", description: "skip this many (paging)" },
29871
+ JSON_OUT
29872
+ ],
29873
+ examples: [
29874
+ "turns --role user --limit 200",
29875
+ "turns --role user --from 2026-08-01 --json",
29876
+ "turns 3f9a2c1e --role assistant"
29877
+ ]
29821
29878
  },
29822
29879
  {
29823
29880
  name: "backfill",
@@ -29924,6 +29981,21 @@ var CLI_SPEC = {
29924
29981
  { name: "--dry-run", description: "with `install`: show the change without writing" }
29925
29982
  ],
29926
29983
  examples: ["hook status", "hook install --dry-run"]
29984
+ },
29985
+ {
29986
+ name: "statusline",
29987
+ group: "admin",
29988
+ summary: "The Claude Code statusline indicator (recording / off), and its registration",
29989
+ args: [
29990
+ {
29991
+ name: "action",
29992
+ choices: ["render", "install", "uninstall", "status"],
29993
+ default: "status",
29994
+ description: "`render` reads Claude Code's statusline JSON from stdin and prints one line (no network)"
29995
+ },
29996
+ { name: "--dry-run", description: "with `install`: show the change without writing" }
29997
+ ],
29998
+ examples: ["statusline install", "statusline status"]
29927
29999
  }
29928
30000
  ]
29929
30001
  };
@@ -30248,6 +30320,97 @@ var HOOK_TYPES = [
30248
30320
  }
30249
30321
  ];
30250
30322
 
30323
+ // packages/shared/src/model/recall.ts
30324
+ var RECALL_MODES = ["off", "suggest", "auto"];
30325
+ var RECALL_SCOPES = ["project", "host", "all"];
30326
+ var DEFAULT_RECALL = {
30327
+ mode: "auto",
30328
+ scope: "project",
30329
+ maxResults: 5,
30330
+ maxSnippetChars: 400,
30331
+ triggers: { priorWorkQuestion: true, repeatedError: true, beforeRederiving: true },
30332
+ excludeCwdGlobs: [],
30333
+ primer: { onSessionStart: true, maxTokens: 200 }
30334
+ };
30335
+ var RECALL_ENV = {
30336
+ mode: "CLAUDE_PLUGIN_OPTION_RECALL_MODE",
30337
+ scope: "CLAUDE_PLUGIN_OPTION_RECALL_SCOPE",
30338
+ maxResults: "CLAUDE_PLUGIN_OPTION_MAX_RESULTS"
30339
+ };
30340
+ function resolveRecall(fromConfig, env = {}) {
30341
+ const c = fromConfig ?? {};
30342
+ const merged = {
30343
+ ...DEFAULT_RECALL,
30344
+ ...stripUndefined(c),
30345
+ triggers: { ...DEFAULT_RECALL.triggers, ...stripUndefined(c.triggers ?? {}) },
30346
+ primer: { ...DEFAULT_RECALL.primer, ...stripUndefined(c.primer ?? {}) },
30347
+ excludeCwdGlobs: c.excludeCwdGlobs ?? DEFAULT_RECALL.excludeCwdGlobs
30348
+ };
30349
+ const mode = env[RECALL_ENV.mode];
30350
+ if (mode && RECALL_MODES.includes(mode))
30351
+ merged.mode = mode;
30352
+ const scope = env[RECALL_ENV.scope];
30353
+ if (scope && RECALL_SCOPES.includes(scope))
30354
+ merged.scope = scope;
30355
+ const max = Number(env[RECALL_ENV.maxResults]);
30356
+ if (Number.isInteger(max) && max > 0)
30357
+ merged.maxResults = max;
30358
+ return merged;
30359
+ }
30360
+ function stripUndefined(o) {
30361
+ const out = {};
30362
+ for (const [k, v] of Object.entries(o))
30363
+ if (v !== undefined)
30364
+ out[k] = v;
30365
+ return out;
30366
+ }
30367
+ function scopeFlag(scope, cwd, hostname) {
30368
+ switch (scope) {
30369
+ case "project":
30370
+ return `--cwd "${cwd}"`;
30371
+ case "host":
30372
+ return `--hostname "${hostname}"`;
30373
+ default:
30374
+ return "";
30375
+ }
30376
+ }
30377
+ function daysAgo(iso, now) {
30378
+ if (!iso)
30379
+ return "";
30380
+ const d = Math.floor((now - Date.parse(iso)) / 86400000);
30381
+ if (!Number.isFinite(d))
30382
+ return "";
30383
+ return d <= 0 ? "today" : d === 1 ? "yesterday" : `${d} days ago`;
30384
+ }
30385
+ function recallPrimer(policy, facts, where, now = Date.now()) {
30386
+ if (policy.mode === "off" || !policy.primer.onSessionStart)
30387
+ return null;
30388
+ if (facts.sessionCount <= 0)
30389
+ return null;
30390
+ const scopeWord = { project: "this project", host: "this host", all: "all projects" }[policy.scope];
30391
+ const recent = daysAgo(facts.mostRecent, now);
30392
+ const flag = scopeFlag(policy.scope, where.cwd, where.hostname);
30393
+ const when = [];
30394
+ if (policy.triggers.priorWorkQuestion)
30395
+ when.push("a question about why existing code or a decision is the way it is");
30396
+ if (policy.triggers.repeatedError)
30397
+ when.push("an error that looks familiar");
30398
+ if (policy.triggers.beforeRederiving)
30399
+ when.push("re-deriving something that looks like prior work here");
30400
+ const action = policy.mode === "auto" ? "search history first" : "offer to search history, and search if the user agrees";
30401
+ return [
30402
+ `Session history for ${scopeWord} is available via \`claude-transcripts search "<query>" ${flag} --json --limit ${policy.maxResults}\`.`,
30403
+ `${facts.sessionCount} recorded session${facts.sessionCount === 1 ? "" : "s"} in scope${recent ? `, most recent ${recent}` : ""}.`,
30404
+ when.length ? `Before answering ${joinOr(when)}, ${action}.` : "",
30405
+ `Cite the session id and date. Snippets only, never a whole transcript; max ${policy.maxResults} results. See the \`recall\` skill.`
30406
+ ].filter(Boolean).join(" ");
30407
+ }
30408
+ function joinOr(items) {
30409
+ if (items.length <= 1)
30410
+ return items[0] ?? "";
30411
+ return `${items.slice(0, -1).join(", ")}, or ${items[items.length - 1]}`;
30412
+ }
30413
+
30251
30414
  // packages/shared/src/model/routes.ts
30252
30415
  var ROUTES = [
30253
30416
  { path: "/", serves: "machine-readable app manifest (agent entrypoint)" },
@@ -30685,6 +30848,7 @@ function buildAppModel(config, env = {}) {
30685
30848
  features: config.features,
30686
30849
  servicesMenu: buildServicesMenu(services, config.servicesMenu),
30687
30850
  system: config.system,
30851
+ recall: resolveRecall(config.recall, env),
30688
30852
  cliSpec: CLI_SPEC
30689
30853
  };
30690
30854
  }
@@ -39061,6 +39225,38 @@ var getSessionTranscript = async (id, params, options) => {
39061
39225
  method: "GET"
39062
39226
  });
39063
39227
  };
39228
+ var getGetSessionTurnsUrl = (id, params) => {
39229
+ const normalizedParams = new URLSearchParams;
39230
+ Object.entries(params || {}).forEach(([key, value]) => {
39231
+ if (value !== undefined) {
39232
+ normalizedParams.append(key, value === null ? "null" : value.toString());
39233
+ }
39234
+ });
39235
+ const stringifiedParams = normalizedParams.toString();
39236
+ return stringifiedParams.length > 0 ? `/api/sessions/${id}/turns?${stringifiedParams}` : `/api/sessions/${id}/turns`;
39237
+ };
39238
+ var getSessionTurns = async (id, params, options) => {
39239
+ return customFetch(getGetSessionTurnsUrl(id, params), {
39240
+ ...options,
39241
+ method: "GET"
39242
+ });
39243
+ };
39244
+ var getGetTurnsUrl = (params) => {
39245
+ const normalizedParams = new URLSearchParams;
39246
+ Object.entries(params || {}).forEach(([key, value]) => {
39247
+ if (value !== undefined) {
39248
+ normalizedParams.append(key, value === null ? "null" : value.toString());
39249
+ }
39250
+ });
39251
+ const stringifiedParams = normalizedParams.toString();
39252
+ return stringifiedParams.length > 0 ? `/api/turns?${stringifiedParams}` : `/api/turns`;
39253
+ };
39254
+ var getTurns = async (params, options) => {
39255
+ return customFetch(getGetTurnsUrl(params), {
39256
+ ...options,
39257
+ method: "GET"
39258
+ });
39259
+ };
39064
39260
  var getIngestSummaryUrl = () => {
39065
39261
  return `/api/ingest/summary`;
39066
39262
  };
@@ -39738,7 +39934,20 @@ api_bind_addr = "[::]:3903"
39738
39934
  "garageWebui": "http://127.0.0.1:7655/",
39739
39935
  "meilisearch": "http://127.0.0.1:7656/"
39740
39936
  },
39741
- "userSettings": {}
39937
+ "userSettings": {},
39938
+ "recall": {
39939
+ "mode": "auto",
39940
+ "scope": "project",
39941
+ "maxResults": 5,
39942
+ "maxSnippetChars": 400,
39943
+ "triggers": {
39944
+ "priorWorkQuestion": true,
39945
+ "repeatedError": true,
39946
+ "beforeRederiving": true
39947
+ },
39948
+ "excludeCwdGlobs": [],
39949
+ "primer": { "onSessionStart": true, "maxTokens": 200 }
39950
+ }
39742
39951
  }
39743
39952
  `
39744
39953
  }
@@ -39996,9 +40205,95 @@ async function runExport(argv) {
39996
40205
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
39997
40206
  import { dirname } from "path";
39998
40207
 
40208
+ // packages/cli/src/hook/announce.ts
40209
+ function sessionLink(webapiUrl2, sessionId) {
40210
+ return webapiUrl2 ? `${webapiUrl2.replace(/\/$/, "")}/app/sessions/${sessionId}` : null;
40211
+ }
40212
+ function recordingBanner(targets, sessionId) {
40213
+ const where = [`couchdb://${targets.couchUrl.replace(/^https?:\/\//, "")}/${targets.sessionsDb}`];
40214
+ if (targets.bucket)
40215
+ where.push(`s3://${targets.bucket}`);
40216
+ if (targets.mirrors.length)
40217
+ where.push(`${targets.mirrors.length} mirror(s)`);
40218
+ const link2 = sessionLink(targets.webapiUrl, sessionId);
40219
+ return `Claude Transcripts \u2014 recording to ${where.join(" + ")}${link2 ? ` \xB7 ${link2}` : ""}`;
40220
+ }
40221
+ var NOT_RECORDING_BANNER = "Claude Transcripts \u2014 not recording (no instance configured). Run `claude-transcripts install`.";
40222
+ function sessionStartEnvelope(out) {
40223
+ if (!out.systemMessage && !out.additionalContext)
40224
+ return null;
40225
+ return {
40226
+ ...out.systemMessage ? { systemMessage: out.systemMessage } : {},
40227
+ ...out.additionalContext ? {
40228
+ hookSpecificOutput: {
40229
+ hookEventName: "SessionStart",
40230
+ additionalContext: out.additionalContext
40231
+ }
40232
+ } : {}
40233
+ };
40234
+ }
40235
+ function emitSessionStart(out) {
40236
+ try {
40237
+ const envelope = sessionStartEnvelope(out);
40238
+ if (envelope)
40239
+ process.stdout.write(`${JSON.stringify(envelope)}
40240
+ `);
40241
+ } catch {}
40242
+ }
40243
+
39999
40244
  // packages/cli/src/hook/handlers.ts
40000
40245
  import { readFileSync as readFileSync6 } from "fs";
40001
40246
 
40247
+ // packages/cli/src/hook/recall.ts
40248
+ var FACTS_TIMEOUT_MS = 2000;
40249
+ function isExcluded(cwd2, globs) {
40250
+ for (const g of globs) {
40251
+ try {
40252
+ if (new Bun.Glob(g).match(cwd2))
40253
+ return true;
40254
+ } catch {}
40255
+ }
40256
+ return false;
40257
+ }
40258
+ async function corpusFacts(webapiUrl2, policy, cwd2, hostname4) {
40259
+ const q = new URLSearchParams({ limit: "1" });
40260
+ if (policy.scope === "project")
40261
+ q.set("cwd", cwd2);
40262
+ else if (policy.scope === "host")
40263
+ q.set("hostname", hostname4);
40264
+ try {
40265
+ const res = await fetch(`${webapiUrl2}/api/sessions?${q}`, {
40266
+ signal: AbortSignal.timeout(FACTS_TIMEOUT_MS)
40267
+ });
40268
+ if (!res.ok)
40269
+ return null;
40270
+ const body = await res.json();
40271
+ return {
40272
+ sessionCount: body.totalCount ?? 0,
40273
+ mostRecent: body.sessions?.[0]?.timestamp
40274
+ };
40275
+ } catch {
40276
+ return null;
40277
+ }
40278
+ }
40279
+ async function buildPrimer(ctx) {
40280
+ const policy = resolveRecall(ctx.config.recall, process.env);
40281
+ if (policy.mode === "off" || !policy.primer.onSessionStart)
40282
+ return null;
40283
+ if (!ctx.cwd || isExcluded(ctx.cwd, policy.excludeCwdGlobs))
40284
+ return null;
40285
+ let webapi;
40286
+ try {
40287
+ webapi = resolveWebapiUrl();
40288
+ } catch {
40289
+ return null;
40290
+ }
40291
+ const facts = await corpusFacts(webapi, policy, ctx.cwd, ctx.hostname);
40292
+ if (!facts)
40293
+ return null;
40294
+ return recallPrimer(policy, facts, { cwd: ctx.cwd, hostname: ctx.hostname });
40295
+ }
40296
+
40002
40297
  // packages/cli/src/hook/runtime.ts
40003
40298
  import {
40004
40299
  closeSync,
@@ -40116,7 +40411,7 @@ function loadHookConfig(path) {
40116
40411
  return null;
40117
40412
  }
40118
40413
  }
40119
- function makeDirectCouch(config) {
40414
+ function makeDirectCouch(config, onWrite) {
40120
40415
  const root = config.couch.url;
40121
40416
  const headers = { "Content-Type": "application/json" };
40122
40417
  if (config.couch.auth)
@@ -40124,29 +40419,35 @@ function makeDirectCouch(config) {
40124
40419
  return {
40125
40420
  async postDoc(db, doc) {
40126
40421
  try {
40127
- await fetch(`${root}/${db}`, {
40422
+ const res = await fetch(`${root}/${db}`, {
40128
40423
  method: "POST",
40129
40424
  headers,
40130
40425
  body: JSON.stringify(doc),
40131
40426
  signal: AbortSignal.timeout(5000)
40132
40427
  });
40133
- } catch {}
40428
+ onWrite?.(res.ok);
40429
+ } catch {
40430
+ onWrite?.(false);
40431
+ }
40134
40432
  },
40135
40433
  async putDoc(db, id, doc, timeoutMs = 5000) {
40136
40434
  try {
40137
- await fetch(`${root}/${db}/${encodeURIComponent(id)}`, {
40435
+ const res = await fetch(`${root}/${db}/${encodeURIComponent(id)}`, {
40138
40436
  method: "PUT",
40139
40437
  headers,
40140
40438
  body: JSON.stringify(doc),
40141
40439
  signal: AbortSignal.timeout(timeoutMs)
40142
40440
  });
40143
- } catch {}
40441
+ onWrite?.(res.ok);
40442
+ } catch {
40443
+ onWrite?.(false);
40444
+ }
40144
40445
  }
40145
40446
  };
40146
40447
  }
40147
- function makeCouch(config) {
40448
+ function makeCouch(config, onWrite) {
40148
40449
  const mirrors = config.mirrors ?? [];
40149
- return fanOutCouch([makeDirectCouch(config), ...mirrors.map((m) => makeMirrorCouch(m))]);
40450
+ return fanOutCouch([makeDirectCouch(config, onWrite), ...mirrors.map((m) => makeMirrorCouch(m))]);
40150
40451
  }
40151
40452
  function makeDirectBlob(config) {
40152
40453
  const cfg = config.blob;
@@ -40206,6 +40507,67 @@ function makeCounts(sessionId) {
40206
40507
  }
40207
40508
  };
40208
40509
  }
40510
+ function targetsFile(sessionId) {
40511
+ return `/tmp/claude-transcripts-${sessionId}.targets`;
40512
+ }
40513
+ function makeTargets(sessionId) {
40514
+ const file = targetsFile(sessionId);
40515
+ const read = () => {
40516
+ try {
40517
+ return JSON.parse(readFileSync5(file, "utf8"));
40518
+ } catch {
40519
+ return null;
40520
+ }
40521
+ };
40522
+ const write = (t) => {
40523
+ try {
40524
+ writeFileSync2(file, JSON.stringify(t));
40525
+ } catch {}
40526
+ };
40527
+ return {
40528
+ read,
40529
+ write,
40530
+ markWrite(ok) {
40531
+ const t = read();
40532
+ if (!t)
40533
+ return;
40534
+ if (ok) {
40535
+ t.lastWriteMs = Date.now();
40536
+ t.lastFailureMs = 0;
40537
+ } else {
40538
+ t.lastFailureMs = Date.now();
40539
+ }
40540
+ write(t);
40541
+ },
40542
+ clear() {
40543
+ try {
40544
+ unlinkSync(file);
40545
+ } catch {}
40546
+ }
40547
+ };
40548
+ }
40549
+ function redactUrl(url) {
40550
+ try {
40551
+ const u = new URL(url);
40552
+ u.username = "";
40553
+ u.password = "";
40554
+ return u.toString().replace(/\/$/, "");
40555
+ } catch {
40556
+ return url;
40557
+ }
40558
+ }
40559
+ function resolveTargets(config, webapiUrl2) {
40560
+ return {
40561
+ couchUrl: redactUrl(config.couch.url),
40562
+ sessionsDb: config.couch.databases.sessions ?? DEFAULT_SESSIONS_DB,
40563
+ bucket: config.blob?.accessKey ? config.blob.buckets.sessions : undefined,
40564
+ webapiUrl: webapiUrl2,
40565
+ features: Object.entries(config.features ?? {}).filter(([, on]) => on).map(([k]) => k),
40566
+ mirrors: (config.mirrors ?? []).map((m) => redactUrl(m.url)),
40567
+ lastWriteMs: 0,
40568
+ lastFailureMs: 0
40569
+ };
40570
+ }
40209
40571
  var STALE_LOCK_MS = 30000;
40210
40572
  function makeChunkState(sessionId) {
40211
40573
  const stateFile = `/tmp/claude-transcripts-${sessionId}.chunkstate`;
@@ -40268,12 +40630,15 @@ function makeChunkState(sessionId) {
40268
40630
  }
40269
40631
  };
40270
40632
  }
40633
+ var DEFAULT_SESSIONS_DB = "claude-transcripts-sessions";
40271
40634
  function buildContext(payload, config) {
40272
40635
  const event = payload?.hook_event_name;
40273
40636
  const sessionId = payload?.session_id;
40274
40637
  if (!event || !sessionId)
40275
40638
  return null;
40639
+ const targets = makeTargets(sessionId);
40276
40640
  return {
40641
+ output: {},
40277
40642
  event,
40278
40643
  sessionId,
40279
40644
  cwd: payload?.cwd ?? "",
@@ -40282,10 +40647,11 @@ function buildContext(payload, config) {
40282
40647
  transcriptPath: payload?.transcript_path,
40283
40648
  payload,
40284
40649
  config,
40285
- couch: makeCouch(config),
40650
+ couch: makeCouch(config, (ok) => targets.markWrite(ok)),
40286
40651
  blob: makeBlob(config),
40287
40652
  counts: makeCounts(sessionId),
40288
- sessionsDb: config.couch.databases.sessions ?? "claude-transcripts-sessions",
40653
+ targets,
40654
+ sessionsDb: config.couch.databases.sessions ?? DEFAULT_SESSIONS_DB,
40289
40655
  sessionsBucket: config.blob?.buckets.sessions
40290
40656
  };
40291
40657
  }
@@ -40306,6 +40672,23 @@ var seedSessionStart = async (ctx) => {
40306
40672
  ctx.counts.reset();
40307
40673
  makeChunkState(ctx.sessionId).seed();
40308
40674
  }
40675
+ ctx.targets.write(resolveTargets(ctx.config, safeWebapiUrl()));
40676
+ };
40677
+ function safeWebapiUrl() {
40678
+ try {
40679
+ return resolveWebapiUrl();
40680
+ } catch {
40681
+ return;
40682
+ }
40683
+ }
40684
+ var announceRecording = async (ctx) => {
40685
+ const targets = ctx.targets.read() ?? resolveTargets(ctx.config, safeWebapiUrl());
40686
+ ctx.output.systemMessage = recordingBanner(targets, ctx.sessionId);
40687
+ };
40688
+ var injectRecallPolicy = async (ctx) => {
40689
+ const primer = await buildPrimer(ctx);
40690
+ if (primer)
40691
+ ctx.output.additionalContext = primer;
40309
40692
  };
40310
40693
  var updateCounts = async (ctx) => {
40311
40694
  ctx.counts.inc("events");
@@ -40454,6 +40837,7 @@ var writeSummary = async (ctx) => {
40454
40837
  await ctx.blob.put(ctx.sessionsBucket, `${ctx.sessionId}/summary.json`, JSON.stringify(doc), "application/json");
40455
40838
  }
40456
40839
  ctx.counts.clear();
40840
+ ctx.targets.clear();
40457
40841
  };
40458
40842
  var uploadBlobs = async (ctx) => {
40459
40843
  if (!ctx.blob.enabled || !ctx.sessionsBucket || !ctx.transcriptPath)
@@ -40468,6 +40852,8 @@ var uploadBlobs = async (ctx) => {
40468
40852
  };
40469
40853
  var HANDLERS = {
40470
40854
  "seed-session-start": seedSessionStart,
40855
+ "announce-recording": announceRecording,
40856
+ "inject-recall-policy": injectRecallPolicy,
40471
40857
  "update-counts": updateCounts,
40472
40858
  "write-event-marker": writeEventMarker,
40473
40859
  "flush-transcript-chunk": flushTranscriptChunk,
@@ -40495,8 +40881,12 @@ async function dispatch(raw, configPath) {
40495
40881
  return { ran: false, actions: [] };
40496
40882
  }
40497
40883
  const config = loadHookConfig(configPath);
40498
- if (!config)
40884
+ if (!config) {
40885
+ if (payload?.hook_event_name === "SessionStart") {
40886
+ emitSessionStart({ systemMessage: NOT_RECORDING_BANNER });
40887
+ }
40499
40888
  return { ran: false, actions: [] };
40889
+ }
40500
40890
  const ctx = buildContext(payload, config);
40501
40891
  if (!ctx)
40502
40892
  return { ran: false, actions: [] };
@@ -40511,6 +40901,8 @@ async function dispatch(raw, configPath) {
40511
40901
  console.error(`[hook] action ${key} failed (ignored):`, err);
40512
40902
  }
40513
40903
  }));
40904
+ if (ctx.event === "SessionStart")
40905
+ emitSessionStart(ctx.output);
40514
40906
  return { ran: true, event: ctx.event, actions: actions2 };
40515
40907
  }
40516
40908
 
@@ -40523,12 +40915,14 @@ function readSettings(path) {
40523
40915
  }
40524
40916
  }
40525
40917
  function hookCommand() {
40918
+ return `${cliInvocation()} hook run`;
40919
+ }
40920
+ function cliInvocation() {
40526
40921
  const exec3 = process.execPath;
40527
40922
  const isCompiled = !/\/bun$/.test(exec3);
40528
40923
  if (isCompiled)
40529
- return `${exec3} hook run`;
40530
- const entry = Bun.main;
40531
- return `${exec3} run ${entry} hook run`;
40924
+ return exec3;
40925
+ return `${exec3} run ${Bun.main}`;
40532
40926
  }
40533
40927
  function isOurCommand(command) {
40534
40928
  if (!command?.includes("hook run"))
@@ -40911,7 +41305,7 @@ async function runImport(argv) {
40911
41305
  }
40912
41306
 
40913
41307
  // packages/cli/src/commands/install.ts
40914
- import { existsSync as existsSync8, readFileSync as readFileSync11 } from "fs";
41308
+ import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
40915
41309
 
40916
41310
  // packages/cli/src/lib/assets.ts
40917
41311
  import {
@@ -41291,7 +41685,8 @@ function buildHookConfig(app, env4) {
41291
41685
  buckets: app.s3.buckets
41292
41686
  } : undefined,
41293
41687
  features: app.features,
41294
- system: app.system
41688
+ system: app.system,
41689
+ ...app.recall ? { recall: app.recall } : {}
41295
41690
  };
41296
41691
  }
41297
41692
  var PRESERVED_KEYS = ["mirrors"];
@@ -41514,6 +41909,163 @@ var hasFailures = (checks) => checks.some((c) => c.status === "fail");
41514
41909
  var DEV_VERSION = "0.0.0-dev";
41515
41910
  var VERSION = process.env.CT_VERSION ?? DEV_VERSION;
41516
41911
 
41912
+ // packages/cli/src/commands/statusline.ts
41913
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
41914
+ import { dirname as dirname5 } from "path";
41915
+
41916
+ // packages/cli/src/lib/statusline.ts
41917
+ var STALL_AFTER_MS = 60000;
41918
+ function ago(ms, now2) {
41919
+ const s = Math.max(0, Math.round((now2 - ms) / 1000));
41920
+ if (s < 60)
41921
+ return `${s}s ago`;
41922
+ const m = Math.round(s / 60);
41923
+ if (m < 60)
41924
+ return `${m}m ago`;
41925
+ return `${Math.round(m / 60)}h ago`;
41926
+ }
41927
+ function whereLabel(t) {
41928
+ const host = t.couchUrl.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
41929
+ return `${t.sessionsDb}@${host}`;
41930
+ }
41931
+ function renderStatusline(state, now2 = Date.now()) {
41932
+ if (!state.configured)
41933
+ return "\u25CB ct off \xB7 no instance configured";
41934
+ const t = state.targets;
41935
+ if (!t)
41936
+ return "\u25CB ct off \xB7 not recording this session";
41937
+ const c = state.counts;
41938
+ const toolCalls = c ? Object.values(c.tools).reduce((a, b) => a + b, 0) : 0;
41939
+ const counts = c ? `${c.events} ev \xB7 ${toolCalls} tools` : "0 ev";
41940
+ const written = t.lastWriteMs > 0;
41941
+ const failing = t.lastFailureMs > t.lastWriteMs && now2 - t.lastWriteMs > STALL_AFTER_MS;
41942
+ if (failing) {
41943
+ const last2 = written ? `last write ${ago(t.lastWriteMs, now2)}` : "no write landed";
41944
+ return `\u25D0 ct stalled \xB7 ${counts} \xB7 ${last2} \u2192 ${whereLabel(t)}`;
41945
+ }
41946
+ if (!written)
41947
+ return `\u25CC ct ready \xB7 ${counts} \xB7 no write yet \u2192 ${whereLabel(t)}`;
41948
+ return `\u25CF ct rec \xB7 ${counts} \xB7 ${ago(t.lastWriteMs, now2)} \u2192 ${whereLabel(t)}`;
41949
+ }
41950
+
41951
+ // packages/cli/src/commands/statusline.ts
41952
+ function readSettings2(path) {
41953
+ try {
41954
+ return JSON.parse(readFileSync11(path, "utf8"));
41955
+ } catch {
41956
+ return {};
41957
+ }
41958
+ }
41959
+ function statuslineCommand() {
41960
+ return `${cliInvocation()} statusline render`;
41961
+ }
41962
+ function isOurStatusline(s) {
41963
+ const c = s?.command ?? "";
41964
+ return c.includes("claude-transcripts-statusline") || c.includes("statusline render");
41965
+ }
41966
+ async function render2() {
41967
+ try {
41968
+ let input = {};
41969
+ try {
41970
+ input = JSON.parse(await Bun.stdin.text());
41971
+ } catch {}
41972
+ const configured = existsSync8(installPaths().hookConfig);
41973
+ const id = input.session_id;
41974
+ const line = renderStatusline({
41975
+ configured,
41976
+ targets: id ? makeTargets(id).read() : null,
41977
+ counts: id ? makeCounts(id).read() : null
41978
+ });
41979
+ process.stdout.write(`${line}
41980
+ `);
41981
+ } catch {
41982
+ process.stdout.write(`\u25CB ct off
41983
+ `);
41984
+ }
41985
+ return 0;
41986
+ }
41987
+ function install2(argv) {
41988
+ const { options } = parseFlags(argv);
41989
+ const paths = installPaths();
41990
+ const settings = readSettings2(paths.claudeSettings);
41991
+ const command = statuslineCommand();
41992
+ const next = { type: "command", command };
41993
+ const existing = settings.statusLine;
41994
+ if (existing && !isOurStatusline(existing)) {
41995
+ console.log(`statusline: ${paths.claudeSettings} already has a statusLine that isn't ours:`);
41996
+ console.log(` ${JSON.stringify(existing)}`);
41997
+ console.log("statusline: not overwriting it. To show the indicator alongside your own,");
41998
+ console.log("statusline: have your command append the output of:");
41999
+ console.log(` ${command}`);
42000
+ return 1;
42001
+ }
42002
+ if (existing?.command === command) {
42003
+ console.log(`statusline: already registered \u2192 ${paths.claudeSettings}`);
42004
+ return 0;
42005
+ }
42006
+ if (options["dry-run"] === true) {
42007
+ console.log(`statusline: would set statusLine in ${paths.claudeSettings} to`);
42008
+ console.log(` ${JSON.stringify(next)}`);
42009
+ return 0;
42010
+ }
42011
+ mkdirSync7(dirname5(paths.claudeSettings), { recursive: true });
42012
+ writeFileSync7(paths.claudeSettings, `${JSON.stringify({ ...settings, statusLine: next }, null, 2)}
42013
+ `);
42014
+ console.log(`statusline: registered \u2192 ${paths.claudeSettings}`);
42015
+ console.log("statusline: restart any open Claude Code session for it to take effect.");
42016
+ return 0;
42017
+ }
42018
+ function uninstall2() {
42019
+ const paths = installPaths();
42020
+ const settings = readSettings2(paths.claudeSettings);
42021
+ if (!settings.statusLine) {
42022
+ console.log("statusline: nothing registered.");
42023
+ return 0;
42024
+ }
42025
+ if (!isOurStatusline(settings.statusLine)) {
42026
+ console.log("statusline: the registered statusLine isn't ours; leaving it alone.");
42027
+ return 1;
42028
+ }
42029
+ const { statusLine: _drop, ...rest2 } = settings;
42030
+ writeFileSync7(paths.claudeSettings, `${JSON.stringify(rest2, null, 2)}
42031
+ `);
42032
+ console.log(`statusline: removed from ${paths.claudeSettings}`);
42033
+ return 0;
42034
+ }
42035
+ function status2() {
42036
+ const paths = installPaths();
42037
+ const settings = readSettings2(paths.claudeSettings);
42038
+ console.log(`statusline: settings ${paths.claudeSettings}`);
42039
+ console.log(`statusline: command ${statuslineCommand()}`);
42040
+ const s = settings.statusLine;
42041
+ if (!s) {
42042
+ console.log("statusline: not registered (run `statusline install`).");
42043
+ return 1;
42044
+ }
42045
+ if (!isOurStatusline(s)) {
42046
+ console.log(`statusline: another statusLine is registered: ${JSON.stringify(s)}`);
42047
+ return 1;
42048
+ }
42049
+ console.log("statusline: registered.");
42050
+ return 0;
42051
+ }
42052
+ async function runStatusline(argv) {
42053
+ const [sub = "status", ...rest2] = argv;
42054
+ switch (sub) {
42055
+ case "render":
42056
+ return render2();
42057
+ case "install":
42058
+ return install2(rest2);
42059
+ case "uninstall":
42060
+ return uninstall2();
42061
+ case "status":
42062
+ return status2();
42063
+ default:
42064
+ console.error(`statusline: unknown subcommand "${sub}" (render | install | uninstall | status)`);
42065
+ return 2;
42066
+ }
42067
+ }
42068
+
41517
42069
  // packages/cli/src/commands/install.ts
41518
42070
  function step(n, total, title) {
41519
42071
  console.log(`
@@ -41570,12 +42122,13 @@ async function setUpSearch(webapiUrl2) {
41570
42122
  async function runInstall(argv) {
41571
42123
  const { options } = parseFlags(argv);
41572
42124
  const noHook = options["no-hook"] === true;
42125
+ const noStatusline = options["no-statusline"] === true;
41573
42126
  const noApp = options["no-app"] === true;
41574
42127
  const meiliKey = options["meili-key"] === true;
41575
42128
  const noPrune = options["no-prune"] === true;
41576
42129
  const portBaseOpt = strOpt(options, "port-base");
41577
42130
  const paths = installPaths();
41578
- const priorAppTag = existsSync8(paths.instanceEnv) ? /^APP_TAG=(\S+)\s*$/m.exec(readFileSync11(paths.instanceEnv, "utf8"))?.[1] : undefined;
42131
+ const priorAppTag = existsSync9(paths.instanceEnv) ? /^APP_TAG=(\S+)\s*$/m.exec(readFileSync12(paths.instanceEnv, "utf8"))?.[1] : undefined;
41579
42132
  const TOTAL = 8;
41580
42133
  console.log(`claude-transcripts install (${VERSION})`);
41581
42134
  console.log(` config ${paths.configDir}`);
@@ -41603,7 +42156,7 @@ async function runInstall(argv) {
41603
42156
  portBase = found;
41604
42157
  }
41605
42158
  step(2, TOTAL, "Configuration");
41606
- const firstRun = !existsSync8(paths.instanceEnv);
42159
+ const firstRun = !existsSync9(paths.instanceEnv);
41607
42160
  let env4 = loadOrCreateInstanceEnv(paths.instanceEnv, {
41608
42161
  portBase,
41609
42162
  appTag: VERSION === DEV_VERSION ? "main" : VERSION,
@@ -41687,6 +42240,10 @@ async function runInstall(argv) {
41687
42240
  console.log(" --no-hook: skipped registration");
41688
42241
  } else {
41689
42242
  await runHook(["install"]);
42243
+ if (noStatusline)
42244
+ console.log(" --no-statusline: skipped the statusline indicator");
42245
+ else
42246
+ await runStatusline(["install"]);
41690
42247
  }
41691
42248
  step(7, TOTAL, "Search");
41692
42249
  if (noApp) {
@@ -41761,10 +42318,10 @@ async function runMigrate(argv) {
41761
42318
  }
41762
42319
 
41763
42320
  // packages/cli/src/commands/provision.ts
41764
- import { existsSync as existsSync9 } from "fs";
42321
+ import { existsSync as existsSync10 } from "fs";
41765
42322
  async function runProvision(_argv) {
41766
42323
  const paths = installPaths();
41767
- if (!existsSync9(paths.instanceEnv)) {
42324
+ if (!existsSync10(paths.instanceEnv)) {
41768
42325
  console.error("provision: this machine has no instance \u2014 run `claude-transcripts install` first.");
41769
42326
  return 1;
41770
42327
  }
@@ -41894,6 +42451,10 @@ async function runSearch(argv) {
41894
42451
  ...strOpt(options, "hostname") ? { hostname: strOpt(options, "hostname") } : {},
41895
42452
  ...strOpt(options, "source") ? { source: strOpt(options, "source") } : {}
41896
42453
  });
42454
+ if (options.json === true) {
42455
+ console.log(JSON.stringify(res, null, 2));
42456
+ return res.enabled ? 0 : 1;
42457
+ }
41897
42458
  if (!res.enabled) {
41898
42459
  console.error("search: search is not enabled on this instance.");
41899
42460
  console.error("search: turn on `features.meilisearch` in config/config.json, bring up the stack,");
@@ -41975,8 +42536,12 @@ function summaryLine(s) {
41975
42536
  [s.tokenUsage ? num3(s.tokenUsage.total) : "\u2014", 9]
41976
42537
  ], RIGHT_FROM);
41977
42538
  }
41978
- async function showList(limit) {
42539
+ async function showList(limit, json) {
41979
42540
  const res = await listSessions({ limit });
42541
+ if (json) {
42542
+ console.log(JSON.stringify(res, null, 2));
42543
+ return 0;
42544
+ }
41980
42545
  console.log(`sessions: ${num3(res.totalCount)} total (${webapiUrl()})`);
41981
42546
  if (res.sessions.length === 0) {
41982
42547
  console.log("sessions: none recorded yet");
@@ -42001,8 +42566,13 @@ function entryLine(entry, i) {
42001
42566
  const preview = (text && tools2 ? `${text} ${tools2}` : text || tools2).replace(/\s+/g, " ").trim().slice(0, 100);
42002
42567
  return ` ${padL(`#${i}`, 4)} ${pad2(entry.role, 12)} ${preview}`;
42003
42568
  }
42004
- async function showDetail(id, limit) {
42569
+ async function showDetail(id, limit, json) {
42005
42570
  const s = await getSession(id);
42571
+ if (json) {
42572
+ const transcript = s.hasTranscript ? await getSessionTranscript(id, { limit }) : null;
42573
+ console.log(JSON.stringify({ session: s, transcript }, null, 2));
42574
+ return 0;
42575
+ }
42006
42576
  console.log(`session ${s.sessionId} [${s.status}]`);
42007
42577
  console.log(` started ${s.timestamp ?? "\u2014"}`);
42008
42578
  console.log(` project ${s.cwd || "\u2014"}`);
@@ -42034,8 +42604,9 @@ async function runSessions(argv) {
42034
42604
  const limitOpt = strOpt(options, "limit");
42035
42605
  const limit = limitOpt ? Number(limitOpt) : positionals[0] ? 30 : 50;
42036
42606
  const id = positionals[0];
42607
+ const json = options.json === true;
42037
42608
  try {
42038
- return id ? await showDetail(id, limit) : await showList(limit);
42609
+ return id ? await showDetail(id, limit, json) : await showList(limit, json);
42039
42610
  } catch (err) {
42040
42611
  console.error(`sessions: failed \u2014 ${err.message}`);
42041
42612
  console.error(`sessions: is the webapi reachable at ${webapiUrl()}? (set --webapi or $CT_WEBAPI_URL)`);
@@ -42044,9 +42615,9 @@ async function runSessions(argv) {
42044
42615
  }
42045
42616
 
42046
42617
  // packages/cli/src/commands/setup.ts
42047
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
42618
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
42048
42619
  import { homedir as homedir3 } from "os";
42049
- import { dirname as dirname5, join as join9 } from "path";
42620
+ import { dirname as dirname6, join as join9 } from "path";
42050
42621
  var REPO_ROOT = join9(import.meta.dir, "..", "..", "..", "..");
42051
42622
  var HOOK_DIR = join9(REPO_ROOT, "hooks");
42052
42623
  var HOOK_CONFIG_PATH = process.env.CT_HOOK_CONFIG ?? join9(homedir3(), ".config", "claude-transcripts", "config.json");
@@ -42055,7 +42626,7 @@ var PROJECT_SETTINGS = join9(process.cwd(), ".claude", "settings.json");
42055
42626
  function loadRepoConfig() {
42056
42627
  const live = join9(REPO_ROOT, "config", "config.json");
42057
42628
  const template2 = join9(REPO_ROOT, "config", "config.template.json");
42058
- return JSON.parse(readFileSync12(existsSync10(live) ? live : template2, "utf8"));
42629
+ return JSON.parse(readFileSync13(existsSync11(live) ? live : template2, "utf8"));
42059
42630
  }
42060
42631
  function buildHookConfig2(cfg) {
42061
42632
  const env4 = process.env;
@@ -42120,9 +42691,9 @@ async function probeGarage(blob) {
42120
42691
  console.warn(` garage bucket ${bucket}: NOT reachable/created \u2014 create it + a key on the Garage cluster (admin), then set S3_* in .env. (${err.message})`);
42121
42692
  }
42122
42693
  }
42123
- function readSettings2(path) {
42694
+ function readSettings3(path) {
42124
42695
  try {
42125
- return JSON.parse(readFileSync12(path, "utf8"));
42696
+ return JSON.parse(readFileSync13(path, "utf8"));
42126
42697
  } catch {
42127
42698
  return {};
42128
42699
  }
@@ -42132,13 +42703,13 @@ function dispatchCommand() {
42132
42703
  }
42133
42704
  function isRegistered(path) {
42134
42705
  const cmd = dispatchCommand();
42135
- const hooks2 = readSettings2(path).hooks ?? {};
42706
+ const hooks2 = readSettings3(path).hooks ?? {};
42136
42707
  return Object.values(hooks2).some((groups) => groups.some((g) => g.hooks.some((h) => h.command === cmd)));
42137
42708
  }
42138
42709
  function registerGlobal(dryRun) {
42139
42710
  const cmd = dispatchCommand();
42140
- const generated = JSON.parse(readFileSync12(join9(HOOK_DIR, "hooks", "hooks.json"), "utf8"));
42141
- const settings = readSettings2(GLOBAL_SETTINGS);
42711
+ const generated = JSON.parse(readFileSync13(join9(HOOK_DIR, "hooks", "hooks.json"), "utf8"));
42712
+ const settings = readSettings3(GLOBAL_SETTINGS);
42142
42713
  const hooks2 = settings.hooks ?? {};
42143
42714
  for (const [event, groups] of Object.entries(generated.hooks)) {
42144
42715
  const timeout = groups[0]?.hooks[0]?.timeout;
@@ -42152,8 +42723,8 @@ function registerGlobal(dryRun) {
42152
42723
  console.log(` [check] would register ${Object.keys(generated.hooks).length} events in ${GLOBAL_SETTINGS}`);
42153
42724
  return;
42154
42725
  }
42155
- mkdirSync7(dirname5(GLOBAL_SETTINGS), { recursive: true });
42156
- writeFileSync7(GLOBAL_SETTINGS, `${JSON.stringify({ ...settings, hooks: hooks2 }, null, 2)}
42726
+ mkdirSync8(dirname6(GLOBAL_SETTINGS), { recursive: true });
42727
+ writeFileSync8(GLOBAL_SETTINGS, `${JSON.stringify({ ...settings, hooks: hooks2 }, null, 2)}
42157
42728
  `);
42158
42729
  console.log(` registered ${Object.keys(generated.hooks).length} events (system-wide) in ${GLOBAL_SETTINGS}`);
42159
42730
  }
@@ -42162,7 +42733,7 @@ function registerProject() {
42162
42733
  }
42163
42734
  function reportExisting() {
42164
42735
  const g = isRegistered(GLOBAL_SETTINGS);
42165
- const p = existsSync10(PROJECT_SETTINGS) && isRegistered(PROJECT_SETTINGS);
42736
+ const p = existsSync11(PROJECT_SETTINGS) && isRegistered(PROJECT_SETTINGS);
42166
42737
  console.log(` existing registration \u2192 global: ${g ? "yes" : "no"}, this project: ${p ? "yes" : "no"}`);
42167
42738
  }
42168
42739
  async function reportSearch() {
@@ -42193,7 +42764,7 @@ async function runSetup(argv) {
42193
42764
  reportExisting();
42194
42765
  if (check2) {
42195
42766
  console.log("setup --check (read-only):");
42196
- console.log(` hook config: ${existsSync10(HOOK_CONFIG_PATH) ? "present" : "MISSING"} (${HOOK_CONFIG_PATH})`);
42767
+ console.log(` hook config: ${existsSync11(HOOK_CONFIG_PATH) ? "present" : "MISSING"} (${HOOK_CONFIG_PATH})`);
42197
42768
  console.log(` couch: ${couch.url}`);
42198
42769
  await provisionCouch2(couch, true);
42199
42770
  await probeGarage(hookConfig.blob);
@@ -42216,12 +42787,12 @@ async function runSetup(argv) {
42216
42787
  }
42217
42788
 
42218
42789
  // packages/cli/src/commands/stack.ts
42219
- import { existsSync as existsSync11 } from "fs";
42790
+ import { existsSync as existsSync12 } from "fs";
42220
42791
  async function runStack(argv) {
42221
42792
  const { positionals, options } = parseFlags(argv);
42222
42793
  const cmd = positionals[0] ?? "ps";
42223
42794
  const paths = installPaths();
42224
- if (!existsSync11(paths.instanceEnv)) {
42795
+ if (!existsSync12(paths.instanceEnv)) {
42225
42796
  console.error("stack: this machine has no instance \u2014 run `claude-transcripts install` first.");
42226
42797
  return 1;
42227
42798
  }
@@ -42255,8 +42826,110 @@ async function runStack(argv) {
42255
42826
  }
42256
42827
  }
42257
42828
 
42829
+ // packages/cli/src/commands/turns.ts
42830
+ var ROLE_W2 = 11;
42831
+ function textWidth() {
42832
+ const cols = process.stdout.columns ?? 0;
42833
+ const before2 = 8 + 2 + 16 + 2 + ROLE_W2 + 2 + 18 + 2;
42834
+ return Math.max(40, (cols > 0 ? cols : 120) - before2);
42835
+ }
42836
+ function oneLine(text, width) {
42837
+ const t = text.replace(/\s+/g, " ").trim();
42838
+ return t.length > width ? `${t.slice(0, width - 1)}\u2026` : t;
42839
+ }
42840
+ function crossTurnLine(t, width = textWidth()) {
42841
+ return row([
42842
+ [t.sessionId.slice(0, 8), 8],
42843
+ [when(t.timestamp), 16],
42844
+ [t.role, ROLE_W2],
42845
+ [project2(t.cwd), 18],
42846
+ [oneLine(t.text, width), width]
42847
+ ]);
42848
+ }
42849
+ function sessionTurnLine(t, i, width = textWidth()) {
42850
+ const tools2 = (t.toolUses ?? []).map((u) => `\u2699 ${u.name}`).join(" ");
42851
+ const text = t.text ?? "";
42852
+ return row([
42853
+ [`#${i}`, 5],
42854
+ [when(t.timestamp), 16],
42855
+ [t.role, ROLE_W2],
42856
+ [oneLine(text && tools2 ? `${text} ${tools2}` : text || tools2, width), width]
42857
+ ]);
42858
+ }
42859
+ async function runTurns(argv) {
42860
+ const { positionals, options } = parseFlags(argv);
42861
+ const webapiOverride = strOpt(options, "webapi");
42862
+ if (webapiOverride)
42863
+ setWebapiUrl(webapiOverride);
42864
+ const json = options.json === true;
42865
+ const role = strOpt(options, "role");
42866
+ const limitOpt = strOpt(options, "limit");
42867
+ const limit = limitOpt ? Number(limitOpt) : 50;
42868
+ const id = positionals[0];
42869
+ try {
42870
+ if (id) {
42871
+ const offsetOpt = strOpt(options, "skip");
42872
+ const res2 = await getSessionTurns(id, {
42873
+ ...role ? { role } : {},
42874
+ limit,
42875
+ ...offsetOpt ? { offset: Number(offsetOpt) } : {}
42876
+ });
42877
+ if (json) {
42878
+ console.log(JSON.stringify(res2, null, 2));
42879
+ return 0;
42880
+ }
42881
+ console.log(`turns: session ${id.slice(0, 8)} \u2014 ${num3(res2.turns.length)} of ${num3(res2.totalCount)}${role ? ` (${role})` : ""} (${webapiUrl()})`);
42882
+ const width2 = textWidth();
42883
+ console.log(row([
42884
+ ["#", 5],
42885
+ ["WHEN", 16],
42886
+ ["WHO", ROLE_W2],
42887
+ ["TEXT", width2]
42888
+ ]));
42889
+ for (const [i, t] of res2.turns.entries())
42890
+ console.log(sessionTurnLine(t, i, width2));
42891
+ return 0;
42892
+ }
42893
+ const skipOpt = strOpt(options, "skip");
42894
+ const res = await getTurns({
42895
+ ...role ? { role } : {},
42896
+ ...strOpt(options, "from") ? { from: strOpt(options, "from") } : {},
42897
+ ...strOpt(options, "to") ? { to: strOpt(options, "to") } : {},
42898
+ limit,
42899
+ ...skipOpt ? { skip: Number(skipOpt) } : {}
42900
+ });
42901
+ if (json) {
42902
+ console.log(JSON.stringify(res, null, 2));
42903
+ return 0;
42904
+ }
42905
+ console.log(`turns: ${num3(res.turns.length)}${res.hasMore ? "+" : ""}${role ? ` (${role})` : ""}, all sessions, time order (${webapiUrl()})`);
42906
+ if (res.turns.length === 0) {
42907
+ console.log("turns: none \u2014 sessions logged without couchFullContentChunks have no turns");
42908
+ return 0;
42909
+ }
42910
+ const width = textWidth();
42911
+ console.log(row([
42912
+ ["SESSION", 8],
42913
+ ["WHEN", 16],
42914
+ ["WHO", ROLE_W2],
42915
+ ["PROJECT", 18],
42916
+ ["TEXT", width]
42917
+ ]));
42918
+ for (const t of res.turns)
42919
+ console.log(crossTurnLine(t, width));
42920
+ if (res.hasMore)
42921
+ console.log(`
42922
+ \u2026 more \u2014 raise --limit, or page with --skip`);
42923
+ return 0;
42924
+ } catch (err) {
42925
+ console.error(`turns: failed \u2014 ${err.message}`);
42926
+ console.error(`turns: is the webapi reachable at ${webapiUrl()}? (set --webapi or $CT_WEBAPI_URL)`);
42927
+ return 1;
42928
+ }
42929
+ }
42930
+
42258
42931
  // packages/cli/src/commands/uninstall.ts
42259
- import { existsSync as existsSync12, rmSync } from "fs";
42932
+ import { existsSync as existsSync13, rmSync } from "fs";
42260
42933
  async function confirmPurge(auto) {
42261
42934
  if (auto)
42262
42935
  return true;
@@ -42276,7 +42949,7 @@ async function runUninstall(argv) {
42276
42949
  }
42277
42950
  console.log("uninstall: deregistering the hook");
42278
42951
  await runHook(["uninstall"]);
42279
- if (existsSync12(paths.instanceEnv)) {
42952
+ if (existsSync13(paths.instanceEnv)) {
42280
42953
  console.log(`uninstall: stopping the stack${purge ? " and deleting volumes" : ""}`);
42281
42954
  try {
42282
42955
  const env4 = loadOrCreateInstanceEnv(paths.instanceEnv);
@@ -42309,6 +42982,7 @@ var COMMANDS = {
42309
42982
  stack: runStack,
42310
42983
  provision: runProvision,
42311
42984
  hook: runHook,
42985
+ statusline: runStatusline,
42312
42986
  export: runExport,
42313
42987
  import: runImport,
42314
42988
  setup: runSetup,
@@ -42317,7 +42991,8 @@ var COMMANDS = {
42317
42991
  reindex: runReindex,
42318
42992
  doctor: runDoctor,
42319
42993
  sessions: runSessions,
42320
- search: runSearch
42994
+ search: runSearch,
42995
+ turns: runTurns
42321
42996
  };
42322
42997
 
42323
42998
  // packages/cli/src/app.tsx
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-transcripts/cli",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "type": "module",
5
5
  "description": "Claude Transcripts CLI — browse, search, back up, and administer your self-hosted Claude Code session history.",
6
6
  "keywords": [