@claude-transcripts/cli 0.0.16 → 0.1.0

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 (2) hide show
  1. package/dist/cli.js +451 -174
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -29899,7 +29899,18 @@ var CLI_SPEC = {
29899
29899
  description: "entries per chunk doc"
29900
29900
  },
29901
29901
  { name: "--no-content", description: "byte-range chunks only (no turn content)" },
29902
- { name: "--force", description: "re-process sessions already adopted" },
29902
+ {
29903
+ name: "--force",
29904
+ description: "re-process adopted sessions (deletes their derived docs, then rebuilds)"
29905
+ },
29906
+ {
29907
+ name: "--replace-live",
29908
+ description: "with --force: also replace live-recorded sessions (loses hook provenance)"
29909
+ },
29910
+ {
29911
+ name: "--repair",
29912
+ description: "add missing chunk docs to adopted sessions; leaves summary + events alone"
29913
+ },
29903
29914
  {
29904
29915
  name: "--session",
29905
29916
  type: "string",
@@ -29978,7 +29989,11 @@ var CLI_SPEC = {
29978
29989
  default: "status",
29979
29990
  description: "`run` reads one event payload from stdin (Claude Code calls this)"
29980
29991
  },
29981
- { name: "--dry-run", description: "with `install`: show the change without writing" }
29992
+ { name: "--dry-run", description: "with `install`: show the change without writing" },
29993
+ {
29994
+ name: "--force",
29995
+ description: "with `install`: register even if the plugin already does"
29996
+ }
29982
29997
  ],
29983
29998
  examples: ["hook status", "hook install --dry-run"]
29984
29999
  },
@@ -39157,9 +39172,11 @@ async function errorDetail(res) {
39157
39172
  return "";
39158
39173
  }
39159
39174
  }
39160
- async function exists(path) {
39175
+ async function getOrNull(path) {
39161
39176
  const res = await fetch(`${base2()}${path}`);
39162
- return res.ok;
39177
+ if (!res.ok)
39178
+ return null;
39179
+ return await res.json();
39163
39180
  }
39164
39181
  async function putStream(path, filePath, contentType) {
39165
39182
  const res = await fetch(`${base2()}${path}`, {
@@ -39366,7 +39383,10 @@ var migrateDown = async (migrateDownBody, options) => {
39366
39383
  // packages/cli/src/lib/sink.ts
39367
39384
  class DryRunSink {
39368
39385
  label = "dry-run";
39369
- async hasSummary() {
39386
+ async existingSession() {
39387
+ return null;
39388
+ }
39389
+ async hasTurns() {
39370
39390
  return false;
39371
39391
  }
39372
39392
  async resetSession(sessionId) {
@@ -39392,8 +39412,12 @@ class DryRunSink {
39392
39412
 
39393
39413
  class WebapiSink {
39394
39414
  label = webapiUrl();
39395
- async hasSummary(sessionId) {
39396
- return exists(`/api/sessions/${encodeURIComponent(sessionId)}`);
39415
+ async existingSession(sessionId) {
39416
+ return getOrNull(`/api/sessions/${encodeURIComponent(sessionId)}`);
39417
+ }
39418
+ async hasTurns(sessionId) {
39419
+ const res = await getOrNull(`/api/sessions/${encodeURIComponent(sessionId)}/turns?limit=1`);
39420
+ return (res?.turns?.length ?? 0) > 0;
39397
39421
  }
39398
39422
  async resetSession(sessionId) {
39399
39423
  const res = await resetSession(sessionId);
@@ -39423,6 +39447,23 @@ function makeSink(opts) {
39423
39447
  }
39424
39448
 
39425
39449
  // packages/cli/src/commands/backfill.ts
39450
+ function planReingest(existing, opts) {
39451
+ if (!existing)
39452
+ return { action: "adopt" };
39453
+ if (opts.repair) {
39454
+ if (existing.status === "running")
39455
+ return { action: "skip", reason: "running" };
39456
+ if (opts.hasTurns)
39457
+ return { action: "skip", reason: "has-turns" };
39458
+ return { action: "repair" };
39459
+ }
39460
+ if (!opts.force)
39461
+ return { action: "skip", reason: "already-adopted" };
39462
+ if (existing.source === "live" && !opts.replaceLive) {
39463
+ return { action: "skip", reason: "live-record" };
39464
+ }
39465
+ return { action: "adopt" };
39466
+ }
39426
39467
  async function runBackfill(argv) {
39427
39468
  const { options } = parseFlags(argv);
39428
39469
  const dryRun = options["dry-run"] === true;
@@ -39434,6 +39475,8 @@ async function runBackfill(argv) {
39434
39475
  const maxEntriesPerChunk = chunkSizeOpt ? Number(chunkSizeOpt) : undefined;
39435
39476
  const withContent = options["no-content"] !== true;
39436
39477
  const force = options.force === true;
39478
+ const replaceLive = options["replace-live"] === true;
39479
+ const repair = options.repair === true;
39437
39480
  const onlySession = strOpt(options, "session");
39438
39481
  const only = new Set(onlySession ? [onlySession] : []);
39439
39482
  const sink = makeSink({ dryRun, webapiUrl: strOpt(options, "webapi") });
@@ -39442,30 +39485,69 @@ async function runBackfill(argv) {
39442
39485
  console.error("backfill: adopted anyway and existing ones are skipped).");
39443
39486
  return 2;
39444
39487
  }
39488
+ if (repair && (force || replaceLive)) {
39489
+ console.error("backfill: --repair cannot be combined with --force or --replace-live \u2014");
39490
+ console.error("backfill: repair adds what is missing, those replace what is there.");
39491
+ return 2;
39492
+ }
39493
+ if (replaceLive && !force) {
39494
+ console.error("backfill: --replace-live only applies with --force (it widens what --force");
39495
+ console.error("backfill: may overwrite; on its own it would widen nothing).");
39496
+ return 2;
39497
+ }
39445
39498
  const who = `host=${host}${actor ? `, actor=${actor}` : ""}`;
39446
39499
  console.log(`backfill: scanning ${root} \u2192 ${sink.label}${dryRun ? " (dry-run)" : ""} [${who}]`);
39447
39500
  const found = await discoverTranscripts(root);
39448
39501
  console.log(`backfill: ${found.length} transcript(s) found`);
39502
+ if (repair) {
39503
+ console.log("backfill: --repair \u2014 adding missing chunk docs only; summaries and event markers " + "are left untouched");
39504
+ }
39449
39505
  if (force) {
39450
39506
  const scope = only.size ? `${only.size} named session(s)` : "every already-adopted session";
39451
- console.log(`backfill: --force \u2014 re-processing ${scope} (derived docs are replaced)`);
39507
+ console.log(`backfill: --force \u2014 re-processing ${scope} (derived docs are deleted, then rebuilt)`);
39508
+ console.log(replaceLive ? "backfill: --replace-live \u2014 live-recorded sessions WILL be replaced by reconstructions" : "backfill: live-recorded sessions are left alone (--replace-live overrides)");
39452
39509
  }
39453
39510
  let written = 0;
39454
39511
  let skipped = 0;
39455
39512
  let failed = 0;
39456
39513
  let sidechains = 0;
39457
39514
  let reprocessed = 0;
39515
+ let refusedLive = 0;
39516
+ let repaired = 0;
39458
39517
  for (const t of found) {
39459
39518
  try {
39460
39519
  if (only.size && !only.has(t.sessionId)) {
39461
39520
  skipped++;
39462
39521
  continue;
39463
39522
  }
39464
- if (!dryRun && await sink.hasSummary(t.sessionId)) {
39465
- if (!force) {
39466
- skipped++;
39467
- continue;
39523
+ const existing = dryRun ? null : await sink.existingSession(t.sessionId);
39524
+ const hasTurns = repair && existing ? await sink.hasTurns(t.sessionId) : false;
39525
+ const plan = planReingest(existing, { force, replaceLive, repair, hasTurns });
39526
+ if (plan.action === "skip") {
39527
+ if (plan.reason === "live-record") {
39528
+ console.warn(` ! ${t.sessionId}: skipped \u2014 recorded live. --force would delete that record and ` + "rebuild it from the transcript, losing provenance only the hook had. Pass " + "--replace-live to do that deliberately.");
39529
+ refusedLive++;
39530
+ }
39531
+ if (plan.reason === "running") {
39532
+ console.log(` \xB7 ${t.sessionId}: skipped \u2014 still running`);
39533
+ }
39534
+ if (plan.reason === "has-turns") {
39535
+ console.log(` \xB7 ${t.sessionId}: skipped \u2014 already has turn content`);
39468
39536
  }
39537
+ skipped++;
39538
+ continue;
39539
+ }
39540
+ if (plan.action === "repair") {
39541
+ const jsonl2 = await readTranscript(t.path);
39542
+ const facts2 = deriveSessionFacts(jsonl2, { hostname: host, sessionIdHint: t.sessionId });
39543
+ const chunks = buildChunkDocs(jsonl2, facts2, "backfill", maxEntriesPerChunk, withContent);
39544
+ await sink.putChunks(chunks);
39545
+ await sink.putTranscript(t.sessionId, new TextEncoder().encode(jsonl2));
39546
+ console.log(` + ${t.sessionId}: added ${chunks.length} chunk doc(s)`);
39547
+ repaired++;
39548
+ continue;
39549
+ }
39550
+ if (existing) {
39469
39551
  const gone = await sink.resetSession(t.sessionId);
39470
39552
  console.log(` ~ ${t.sessionId}: cleared ${gone.summary} summary, ${gone.events} event(s), ` + `${gone.chunks} chunk(s)`);
39471
39553
  reprocessed++;
@@ -39484,7 +39566,7 @@ async function runBackfill(argv) {
39484
39566
  console.error(` ! ${t.sessionId}: ${err.message}`);
39485
39567
  }
39486
39568
  }
39487
- console.log(`backfill: ${written} backfilled${reprocessed ? ` (${reprocessed} re-processed)` : ""}, ` + `${skipped} skipped, ${failed} failed`);
39569
+ console.log(`backfill: ${written} backfilled${repaired ? `, ${repaired} repaired` : ""}` + `${reprocessed ? ` (${reprocessed} re-processed)` : ""}, ` + `${skipped} skipped${refusedLive ? ` (${refusedLive} live, not replaced)` : ""}, ` + `${failed} failed`);
39488
39570
  console.log("backfill: NOTE \u2014 summary + per-event markers + chunk docs reconstructed" + `${sidechains ? `; subagent sub-transcripts (${sidechains} session(s) have them) still TODO` : ""}` + " (see docs/operate/tools.md).");
39489
39571
  if (reprocessed && !dryRun) {
39490
39572
  console.log("backfill: run `claude-transcripts reindex` \u2014 search still holds the old turns.");
@@ -40202,97 +40284,8 @@ async function runExport(argv) {
40202
40284
  }
40203
40285
 
40204
40286
  // packages/cli/src/commands/hook.ts
40205
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
40206
- import { dirname } from "path";
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
-
40244
- // packages/cli/src/hook/handlers.ts
40245
- import { readFileSync as readFileSync6 } from "fs";
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
- }
40287
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
40288
+ import { dirname as dirname2 } from "path";
40296
40289
 
40297
40290
  // packages/cli/src/hook/runtime.ts
40298
40291
  import {
@@ -40329,7 +40322,7 @@ function ingestRoute(doc) {
40329
40322
  return null;
40330
40323
  }
40331
40324
  }
40332
- function makeMirrorCouch(target) {
40325
+ function makeMirrorCouch(target, onWrite) {
40333
40326
  const base3 = baseUrl(target);
40334
40327
  const headers = { "Content-Type": "application/json", ...authHeaders(target) };
40335
40328
  const fallback = target.timeoutMs ?? DEFAULT_TIMEOUT_MS;
@@ -40338,13 +40331,16 @@ function makeMirrorCouch(target) {
40338
40331
  if (!route)
40339
40332
  return;
40340
40333
  try {
40341
- await fetch(`${base3}${route.path}`, {
40334
+ const res = await fetch(`${base3}${route.path}`, {
40342
40335
  method: "POST",
40343
40336
  headers,
40344
40337
  body: JSON.stringify(route.body),
40345
40338
  signal: AbortSignal.timeout(timeoutMs)
40346
40339
  });
40347
- } catch {}
40340
+ onWrite?.(res.ok);
40341
+ } catch {
40342
+ onWrite?.(false);
40343
+ }
40348
40344
  };
40349
40345
  return {
40350
40346
  async postDoc(_db, doc) {
@@ -40404,9 +40400,29 @@ function fanOutBlob(clients) {
40404
40400
  }
40405
40401
 
40406
40402
  // packages/cli/src/hook/runtime.ts
40403
+ var DEFAULT_CHUNK = { maxEntriesPerChunk: 200, flushIntervalMs: 15000 };
40404
+ function normalizeHookConfig(raw) {
40405
+ const chunk2 = raw.system?.logging?.chunk;
40406
+ return {
40407
+ ...raw,
40408
+ system: {
40409
+ ...raw.system,
40410
+ logging: {
40411
+ ...raw.system?.logging,
40412
+ chunk: {
40413
+ maxEntriesPerChunk: chunk2?.maxEntriesPerChunk ?? DEFAULT_CHUNK.maxEntriesPerChunk,
40414
+ flushIntervalMs: chunk2?.flushIntervalMs ?? DEFAULT_CHUNK.flushIntervalMs
40415
+ }
40416
+ }
40417
+ }
40418
+ };
40419
+ }
40407
40420
  function loadHookConfig(path) {
40408
40421
  try {
40409
- return JSON.parse(readFileSync5(path, "utf8"));
40422
+ const raw = JSON.parse(readFileSync5(path, "utf8"));
40423
+ if (!raw?.couch?.url)
40424
+ return null;
40425
+ return normalizeHookConfig(raw);
40410
40426
  } catch {
40411
40427
  return null;
40412
40428
  }
@@ -40447,7 +40463,10 @@ function makeDirectCouch(config, onWrite) {
40447
40463
  }
40448
40464
  function makeCouch(config, onWrite) {
40449
40465
  const mirrors = config.mirrors ?? [];
40450
- return fanOutCouch([makeDirectCouch(config, onWrite), ...mirrors.map((m) => makeMirrorCouch(m))]);
40466
+ return fanOutCouch([
40467
+ makeDirectCouch(config, onWrite && ((ok) => onWrite(ok, 0))),
40468
+ ...mirrors.map((m, i) => makeMirrorCouch(m, onWrite && ((ok) => onWrite(ok, i + 1))))
40469
+ ]);
40451
40470
  }
40452
40471
  function makeDirectBlob(config) {
40453
40472
  const cfg = config.blob;
@@ -40527,15 +40546,25 @@ function makeTargets(sessionId) {
40527
40546
  return {
40528
40547
  read,
40529
40548
  write,
40530
- markWrite(ok) {
40549
+ markWrite(ok, storeIndex = 0) {
40531
40550
  const t = read();
40532
40551
  if (!t)
40533
40552
  return;
40553
+ const now2 = Date.now();
40554
+ const store = t.stores?.[storeIndex];
40555
+ if (store) {
40556
+ if (ok) {
40557
+ store.lastWriteMs = now2;
40558
+ store.lastFailureMs = 0;
40559
+ } else {
40560
+ store.lastFailureMs = now2;
40561
+ }
40562
+ }
40534
40563
  if (ok) {
40535
- t.lastWriteMs = Date.now();
40564
+ t.lastWriteMs = now2;
40536
40565
  t.lastFailureMs = 0;
40537
- } else {
40538
- t.lastFailureMs = Date.now();
40566
+ } else if (!t.stores?.some((x) => x.lastWriteMs > x.lastFailureMs)) {
40567
+ t.lastFailureMs = now2;
40539
40568
  }
40540
40569
  write(t);
40541
40570
  },
@@ -40556,6 +40585,9 @@ function redactUrl(url) {
40556
40585
  return url;
40557
40586
  }
40558
40587
  }
40588
+ function hostOf(url) {
40589
+ return redactUrl(url).replace(/^https?:\/\//, "").replace(/\/.*$/, "");
40590
+ }
40559
40591
  function resolveTargets(config, webapiUrl2) {
40560
40592
  return {
40561
40593
  couchUrl: redactUrl(config.couch.url),
@@ -40564,6 +40596,20 @@ function resolveTargets(config, webapiUrl2) {
40564
40596
  webapiUrl: webapiUrl2,
40565
40597
  features: Object.entries(config.features ?? {}).filter(([, on]) => on).map(([k]) => k),
40566
40598
  mirrors: (config.mirrors ?? []).map((m) => redactUrl(m.url)),
40599
+ stores: [
40600
+ {
40601
+ label: `${config.couch.databases.sessions ?? DEFAULT_SESSIONS_DB}@${hostOf(config.couch.url)}`,
40602
+ kind: "direct",
40603
+ lastWriteMs: 0,
40604
+ lastFailureMs: 0
40605
+ },
40606
+ ...(config.mirrors ?? []).map((m) => ({
40607
+ label: hostOf(m.url),
40608
+ kind: "mirror",
40609
+ lastWriteMs: 0,
40610
+ lastFailureMs: 0
40611
+ }))
40612
+ ],
40567
40613
  lastWriteMs: 0,
40568
40614
  lastFailureMs: 0
40569
40615
  };
@@ -40647,7 +40693,7 @@ function buildContext(payload, config) {
40647
40693
  transcriptPath: payload?.transcript_path,
40648
40694
  payload,
40649
40695
  config,
40650
- couch: makeCouch(config, (ok) => targets.markWrite(ok)),
40696
+ couch: makeCouch(config, (ok, storeIndex) => targets.markWrite(ok, storeIndex)),
40651
40697
  blob: makeBlob(config),
40652
40698
  counts: makeCounts(sessionId),
40653
40699
  targets,
@@ -40665,6 +40711,96 @@ function commonFields(ctx) {
40665
40711
  };
40666
40712
  }
40667
40713
 
40714
+ // packages/cli/src/hook/announce.ts
40715
+ function sessionLink(webapiUrl2, sessionId) {
40716
+ return webapiUrl2 ? `${webapiUrl2.replace(/\/$/, "")}/app/sessions/${sessionId}` : null;
40717
+ }
40718
+ function recordingBanner(targets, sessionId) {
40719
+ const where = [`couchdb://${targets.couchUrl.replace(/^https?:\/\//, "")}/${targets.sessionsDb}`];
40720
+ if (targets.bucket)
40721
+ where.push(`s3://${targets.bucket}`);
40722
+ if (targets.mirrors.length) {
40723
+ where.push(`mirrors: ${targets.mirrors.map(hostOf).join(", ")}`);
40724
+ }
40725
+ const link2 = sessionLink(targets.webapiUrl, sessionId);
40726
+ return `Claude Transcripts \u2014 recording to ${where.join(" + ")}${link2 ? ` \xB7 ${link2}` : ""}`;
40727
+ }
40728
+ var NOT_RECORDING_BANNER = "Claude Transcripts \u2014 not recording (no instance configured). Run `claude-transcripts install`.";
40729
+ function sessionStartEnvelope(out) {
40730
+ if (!out.systemMessage && !out.additionalContext)
40731
+ return null;
40732
+ return {
40733
+ ...out.systemMessage ? { systemMessage: out.systemMessage } : {},
40734
+ ...out.additionalContext ? {
40735
+ hookSpecificOutput: {
40736
+ hookEventName: "SessionStart",
40737
+ additionalContext: out.additionalContext
40738
+ }
40739
+ } : {}
40740
+ };
40741
+ }
40742
+ function emitSessionStart(out) {
40743
+ try {
40744
+ const envelope = sessionStartEnvelope(out);
40745
+ if (envelope)
40746
+ process.stdout.write(`${JSON.stringify(envelope)}
40747
+ `);
40748
+ } catch {}
40749
+ }
40750
+
40751
+ // packages/cli/src/hook/handlers.ts
40752
+ import { readFileSync as readFileSync6 } from "fs";
40753
+
40754
+ // packages/cli/src/hook/recall.ts
40755
+ var FACTS_TIMEOUT_MS = 2000;
40756
+ function isExcluded(cwd2, globs) {
40757
+ for (const g of globs) {
40758
+ try {
40759
+ if (new Bun.Glob(g).match(cwd2))
40760
+ return true;
40761
+ } catch {}
40762
+ }
40763
+ return false;
40764
+ }
40765
+ async function corpusFacts(webapiUrl2, policy, cwd2, hostname5) {
40766
+ const q = new URLSearchParams({ limit: "1" });
40767
+ if (policy.scope === "project")
40768
+ q.set("cwd", cwd2);
40769
+ else if (policy.scope === "host")
40770
+ q.set("hostname", hostname5);
40771
+ try {
40772
+ const res = await fetch(`${webapiUrl2}/api/sessions?${q}`, {
40773
+ signal: AbortSignal.timeout(FACTS_TIMEOUT_MS)
40774
+ });
40775
+ if (!res.ok)
40776
+ return null;
40777
+ const body = await res.json();
40778
+ return {
40779
+ sessionCount: body.totalCount ?? 0,
40780
+ mostRecent: body.sessions?.[0]?.timestamp
40781
+ };
40782
+ } catch {
40783
+ return null;
40784
+ }
40785
+ }
40786
+ async function buildPrimer(ctx) {
40787
+ const policy = resolveRecall(ctx.config.recall, process.env);
40788
+ if (policy.mode === "off" || !policy.primer.onSessionStart)
40789
+ return null;
40790
+ if (!ctx.cwd || isExcluded(ctx.cwd, policy.excludeCwdGlobs))
40791
+ return null;
40792
+ let webapi;
40793
+ try {
40794
+ webapi = resolveWebapiUrl();
40795
+ } catch {
40796
+ return null;
40797
+ }
40798
+ const facts = await corpusFacts(webapi, policy, ctx.cwd, ctx.hostname);
40799
+ if (!facts)
40800
+ return null;
40801
+ return recallPrimer(policy, facts, { cwd: ctx.cwd, hostname: ctx.hostname });
40802
+ }
40803
+
40668
40804
  // packages/cli/src/hook/handlers.ts
40669
40805
  var seedSessionStart = async (ctx) => {
40670
40806
  const source = ctx.payload?.source;
@@ -40873,6 +41009,10 @@ var LONG_TIMEOUT_EVENTS = new Set(["SessionStart", "SessionEnd"]);
40873
41009
  function hookTimeout(event) {
40874
41010
  return LONG_TIMEOUT_EVENTS.has(event) ? 180 : 5;
40875
41011
  }
41012
+ var SYNCHRONOUS_EVENTS = new Set(["SessionStart", "SessionEnd", "UserPromptSubmit"]);
41013
+ function hookAsync(event) {
41014
+ return !SYNCHRONOUS_EVENTS.has(event);
41015
+ }
40876
41016
  async function dispatch(raw, configPath) {
40877
41017
  let payload;
40878
41018
  try {
@@ -40887,7 +41027,13 @@ async function dispatch(raw, configPath) {
40887
41027
  }
40888
41028
  return { ran: false, actions: [] };
40889
41029
  }
40890
- const ctx = buildContext(payload, config);
41030
+ let ctx;
41031
+ try {
41032
+ ctx = buildContext(payload, config);
41033
+ } catch (err) {
41034
+ console.error("[hook] could not build context (ignored):", err);
41035
+ return { ran: false, actions: [] };
41036
+ }
40891
41037
  if (!ctx)
40892
41038
  return { ran: false, actions: [] };
40893
41039
  const actions2 = hookBindings()[ctx.event] ?? [];
@@ -40906,10 +41052,62 @@ async function dispatch(raw, configPath) {
40906
41052
  return { ran: true, event: ctx.event, actions: actions2 };
40907
41053
  }
40908
41054
 
41055
+ // packages/cli/src/lib/plugin.ts
41056
+ import { readFileSync as readFileSync7 } from "fs";
41057
+ import { dirname, join as join7 } from "path";
41058
+ var PLUGIN_NAME = "claude-transcripts";
41059
+ function readJson(path) {
41060
+ try {
41061
+ const parsed = JSON.parse(readFileSync7(path, "utf8"));
41062
+ return parsed && typeof parsed === "object" ? parsed : null;
41063
+ } catch {
41064
+ return null;
41065
+ }
41066
+ }
41067
+ function pluginsDir(settingsPath) {
41068
+ return join7(dirname(settingsPath), "plugins");
41069
+ }
41070
+ function pluginEvents(settingsPath, key) {
41071
+ const installed = readJson(join7(pluginsDir(settingsPath), "installed_plugins.json"));
41072
+ const entry = installed?.plugins?.[key];
41073
+ const first = Array.isArray(entry) ? entry[0] : undefined;
41074
+ const installPath = typeof first?.installPath === "string" ? first.installPath : undefined;
41075
+ if (!installPath)
41076
+ return;
41077
+ const hooks2 = readJson(join7(installPath, "hooks", "hooks.json"));
41078
+ const events = hooks2?.hooks;
41079
+ if (!events || typeof events !== "object")
41080
+ return;
41081
+ return Object.keys(events);
41082
+ }
41083
+ function pluginVersion(settingsPath, key) {
41084
+ const installed = readJson(join7(pluginsDir(settingsPath), "installed_plugins.json"));
41085
+ const entry = installed?.plugins?.[key];
41086
+ const first = Array.isArray(entry) ? entry[0] : undefined;
41087
+ return typeof first?.version === "string" ? first.version : undefined;
41088
+ }
41089
+ function pluginRegistration(settingsPath, settings) {
41090
+ const enabled = settings.enabledPlugins;
41091
+ if (!enabled || typeof enabled !== "object")
41092
+ return null;
41093
+ for (const [key, on] of Object.entries(enabled)) {
41094
+ if (on !== true)
41095
+ continue;
41096
+ if (key.split("@")[0] !== PLUGIN_NAME)
41097
+ continue;
41098
+ return {
41099
+ key,
41100
+ version: pluginVersion(settingsPath, key),
41101
+ events: pluginEvents(settingsPath, key)
41102
+ };
41103
+ }
41104
+ return null;
41105
+ }
41106
+
40909
41107
  // packages/cli/src/commands/hook.ts
40910
41108
  function readSettings(path) {
40911
41109
  try {
40912
- return JSON.parse(readFileSync7(path, "utf8"));
41110
+ return JSON.parse(readFileSync8(path, "utf8"));
40913
41111
  } catch {
40914
41112
  return {};
40915
41113
  }
@@ -40948,8 +41146,19 @@ function install(argv) {
40948
41146
  const settings = readSettings(paths.claudeSettings);
40949
41147
  const hooks2 = settings.hooks ?? {};
40950
41148
  const events = Object.keys(hookBindings());
41149
+ const plugin = pluginRegistration(paths.claudeSettings, settings);
41150
+ if (plugin && options.force !== true) {
41151
+ console.error(`hook: the plugin ${plugin.key} already registers these events.`);
41152
+ console.error("hook: registering here as well would write every event twice.");
41153
+ console.error("hook: pick one route \u2014");
41154
+ console.error(`hook: keep the plugin \u2192 nothing to do (\`hook status\` explains it)`);
41155
+ console.error(`hook: use this binary \u2192 disable the plugin, then re-run`);
41156
+ console.error("hook: --force registers anyway.");
41157
+ return 1;
41158
+ }
40951
41159
  let added = 0;
40952
41160
  let replaced = 0;
41161
+ let corrected = 0;
40953
41162
  for (const event of events) {
40954
41163
  const existing = hooks2[event] ?? [];
40955
41164
  const cleaned = [];
@@ -40966,26 +41175,48 @@ function install(argv) {
40966
41175
  if (kept.length)
40967
41176
  cleaned.push({ ...g, hooks: kept });
40968
41177
  }
41178
+ const timeout = hookTimeout(event);
41179
+ const wantAsync = hookAsync(event);
40969
41180
  if (cleaned.some((g) => g.hooks?.some((h) => h.command === command))) {
41181
+ for (const g of cleaned) {
41182
+ for (const h of g.hooks ?? []) {
41183
+ if (h.command !== command)
41184
+ continue;
41185
+ if (h.timeout !== timeout) {
41186
+ h.timeout = timeout;
41187
+ corrected++;
41188
+ } else if (wantAsync ? h.async !== true : h.async !== undefined) {
41189
+ corrected++;
41190
+ }
41191
+ if (wantAsync)
41192
+ h.async = true;
41193
+ else
41194
+ delete h.async;
41195
+ }
41196
+ }
40970
41197
  hooks2[event] = cleaned;
40971
41198
  continue;
40972
41199
  }
40973
41200
  hooks2[event] = [
40974
41201
  ...cleaned,
40975
- { hooks: [{ type: "command", command, timeout: hookTimeout(event) }] }
41202
+ { hooks: [{ type: "command", command, timeout, ...wantAsync ? { async: true } : {} }] }
40976
41203
  ];
40977
41204
  added++;
40978
41205
  }
40979
41206
  if (options["dry-run"] === true) {
40980
- console.log(`hook: would register ${added} event(s)` + `${replaced ? `, replacing ${replaced} stale one(s)` : ""} in ${paths.claudeSettings}`);
41207
+ console.log(`hook: would register ${added} event(s)` + `${replaced ? `, replacing ${replaced} stale one(s)` : ""}` + `${corrected ? `, correcting ${corrected} existing one(s)` : ""} in ${paths.claudeSettings}`);
40981
41208
  return 0;
40982
41209
  }
40983
- mkdirSync3(dirname(paths.claudeSettings), { recursive: true });
41210
+ mkdirSync3(dirname2(paths.claudeSettings), { recursive: true });
40984
41211
  writeFileSync3(paths.claudeSettings, `${JSON.stringify({ ...settings, hooks: hooks2 }, null, 2)}
40985
41212
  `);
40986
- const suffix = replaced ? ` (replaced ${replaced} stale registration(s))` : "";
41213
+ const notes = [
41214
+ replaced ? `replaced ${replaced} stale registration(s)` : "",
41215
+ corrected ? `corrected ${corrected} existing registration(s)` : ""
41216
+ ].filter(Boolean);
41217
+ const suffix = notes.length ? ` (${notes.join(", ")})` : "";
40987
41218
  console.log(added ? `hook: registered ${added} event(s)${suffix} \u2192 ${paths.claudeSettings}` : `hook: already registered (${events.length} events)${suffix} \u2192 ${paths.claudeSettings}`);
40988
- if (added)
41219
+ if (added || corrected)
40989
41220
  console.log("hook: restart any open Claude Code session for it to take effect.");
40990
41221
  return 0;
40991
41222
  }
@@ -41011,27 +41242,48 @@ function uninstall() {
41011
41242
  writeFileSync3(paths.claudeSettings, `${JSON.stringify({ ...settings, hooks: hooks2 }, null, 2)}
41012
41243
  `);
41013
41244
  console.log(`hook: removed ${removed} registration(s) from ${paths.claudeSettings}`);
41245
+ const plugin = pluginRegistration(paths.claudeSettings, settings);
41246
+ if (plugin)
41247
+ console.log(`hook: still recording via the plugin ${plugin.key}.`);
41014
41248
  return 0;
41015
41249
  }
41016
41250
  function configuredMirrors(path) {
41017
41251
  try {
41018
- const parsed = JSON.parse(readFileSync7(path, "utf8"));
41252
+ const parsed = JSON.parse(readFileSync8(path, "utf8"));
41019
41253
  return (parsed.mirrors ?? []).map((m) => m?.url ?? "(no url)");
41020
41254
  } catch {
41021
41255
  return [];
41022
41256
  }
41023
41257
  }
41258
+ function pluginSummary(plugin) {
41259
+ const version = plugin.version ? ` v${plugin.version}` : "";
41260
+ const count = plugin.events?.length;
41261
+ return `${plugin.key}${version}${count ? ` \u2014 registers ${count} event(s)` : ""}`;
41262
+ }
41024
41263
  function status() {
41025
41264
  const paths = installPaths();
41026
41265
  const settings = readSettings(paths.claudeSettings);
41027
41266
  const found = findRegistered(settings);
41267
+ const plugin = pluginRegistration(paths.claudeSettings, settings);
41028
41268
  console.log(`hook: settings ${paths.claudeSettings}`);
41029
41269
  console.log(`hook: config ${paths.hookConfig} ${existsSync4(paths.hookConfig) ? "" : "(MISSING \u2014 run `install`)"}`);
41030
41270
  console.log(`hook: command ${hookCommand()}`);
41271
+ if (plugin)
41272
+ console.log(`hook: plugin ${pluginSummary(plugin)}`);
41031
41273
  const mirrors = configuredMirrors(paths.hookConfig);
41032
41274
  for (const url of mirrors)
41033
41275
  console.log(`hook: mirror ${url}`);
41276
+ if (plugin && found.length) {
41277
+ console.log(`hook: DOUBLE-REGISTERED \u2014 ${found.length} event(s) here AND via the plugin.`);
41278
+ console.log("hook: every event is being written twice. Run `hook uninstall` to keep");
41279
+ console.log("hook: the plugin's registration, or disable the plugin to keep this one.");
41280
+ return 1;
41281
+ }
41034
41282
  if (!found.length) {
41283
+ if (plugin) {
41284
+ console.log("hook: registered by the plugin (nothing in settings.json \u2014 correct).");
41285
+ return 0;
41286
+ }
41035
41287
  console.log("hook: not registered.");
41036
41288
  return 1;
41037
41289
  }
@@ -41066,7 +41318,7 @@ async function runHook(argv) {
41066
41318
 
41067
41319
  // packages/cli/src/commands/import.ts
41068
41320
  import { existsSync as existsSync5, statSync as statSync2 } from "fs";
41069
- import { join as join7 } from "path";
41321
+ import { join as join8 } from "path";
41070
41322
  var BATCH = 500;
41071
41323
  function num(n) {
41072
41324
  return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@@ -41136,7 +41388,7 @@ function checkSchema(bundleVersion, target, registry2 = REAL_REGISTRY) {
41136
41388
  async function verifyChecksums(dir, manifest) {
41137
41389
  const bad = [];
41138
41390
  for (const [rel, expected] of Object.entries(manifest.checksums)) {
41139
- const path = join7(dir, rel);
41391
+ const path = join8(dir, rel);
41140
41392
  if (!existsSync5(path)) {
41141
41393
  bad.push(`${rel} (missing)`);
41142
41394
  continue;
@@ -41281,7 +41533,7 @@ async function runImport(argv) {
41281
41533
  skipped++;
41282
41534
  continue;
41283
41535
  }
41284
- const path = join7(dir, rel);
41536
+ const path = join8(dir, rel);
41285
41537
  await putStream(`/api/ingest/${sessionId}/transcript`, path, "application/x-ndjson");
41286
41538
  blobs++;
41287
41539
  blobBytes += statSync2(path).size;
@@ -41305,41 +41557,41 @@ async function runImport(argv) {
41305
41557
  }
41306
41558
 
41307
41559
  // packages/cli/src/commands/install.ts
41308
- import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
41560
+ import { existsSync as existsSync9, readFileSync as readFileSync13 } from "fs";
41309
41561
 
41310
41562
  // packages/cli/src/lib/assets.ts
41311
41563
  import {
41312
41564
  existsSync as existsSync6,
41313
41565
  lstatSync,
41314
41566
  mkdirSync as mkdirSync4,
41315
- readFileSync as readFileSync8,
41567
+ readFileSync as readFileSync9,
41316
41568
  symlinkSync,
41317
41569
  unlinkSync as unlinkSync2,
41318
41570
  writeFileSync as writeFileSync4
41319
41571
  } from "fs";
41320
- import { dirname as dirname2, join as join8 } from "path";
41572
+ import { dirname as dirname3, join as join9 } from "path";
41321
41573
  function writeAssets(paths) {
41322
41574
  const out = [];
41323
41575
  for (const asset of Object.values(EMBEDDED_ASSETS)) {
41324
41576
  const isUserOwned = asset.path.startsWith("config/");
41325
- const target = isUserOwned ? paths.appConfig : join8(paths.dataDir, asset.path);
41577
+ const target = isUserOwned ? paths.appConfig : join9(paths.dataDir, asset.path);
41326
41578
  if (isUserOwned && existsSync6(target)) {
41327
41579
  out.push({ path: target, changed: false });
41328
41580
  continue;
41329
41581
  }
41330
- const previous = existsSync6(target) ? readFileSync8(target, "utf8") : null;
41582
+ const previous = existsSync6(target) ? readFileSync9(target, "utf8") : null;
41331
41583
  if (previous === asset.content) {
41332
41584
  out.push({ path: target, changed: false });
41333
41585
  continue;
41334
41586
  }
41335
- mkdirSync4(dirname2(target), { recursive: true });
41587
+ mkdirSync4(dirname3(target), { recursive: true });
41336
41588
  writeFileSync4(target, asset.content);
41337
41589
  out.push({ path: target, changed: true });
41338
41590
  }
41339
41591
  return out;
41340
41592
  }
41341
41593
  function linkInstanceEnv(paths) {
41342
- const link2 = join8(paths.dataDir, ".env");
41594
+ const link2 = join9(paths.dataDir, ".env");
41343
41595
  try {
41344
41596
  if (lstatSync(link2, { throwIfNoEntry: false }))
41345
41597
  unlinkSync2(link2);
@@ -41348,14 +41600,14 @@ function linkInstanceEnv(paths) {
41348
41600
  symlinkSync(paths.instanceEnv, link2);
41349
41601
  }
41350
41602
  function writeVersionStamp(paths, version) {
41351
- mkdirSync4(dirname2(paths.versionFile), { recursive: true });
41603
+ mkdirSync4(dirname3(paths.versionFile), { recursive: true });
41352
41604
  writeFileSync4(paths.versionFile, `${version}
41353
41605
  `);
41354
41606
  }
41355
41607
  function composeFiles(paths, upstream) {
41356
- const files = [join8(paths.dataDir, EMBEDDED_ASSETS.compose.path)];
41608
+ const files = [join9(paths.dataDir, EMBEDDED_ASSETS.compose.path)];
41357
41609
  if (upstream)
41358
- files.push(join8(paths.dataDir, EMBEDDED_ASSETS.composeUpstream.path));
41610
+ files.push(join9(paths.dataDir, EMBEDDED_ASSETS.composeUpstream.path));
41359
41611
  return files;
41360
41612
  }
41361
41613
 
@@ -41420,13 +41672,13 @@ function healthTargets(env4) {
41420
41672
  }
41421
41673
 
41422
41674
  // packages/cli/src/lib/hook-config.ts
41423
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
41424
- import { dirname as dirname4 } from "path";
41675
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
41676
+ import { dirname as dirname5 } from "path";
41425
41677
 
41426
41678
  // packages/cli/src/lib/instance-env.ts
41427
41679
  import { randomBytes } from "crypto";
41428
- import { chmodSync as chmodSync2, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
41429
- import { dirname as dirname3 } from "path";
41680
+ import { chmodSync as chmodSync2, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
41681
+ import { dirname as dirname4 } from "path";
41430
41682
  var DEFAULT_PORT_BASE = 7650;
41431
41683
  var PORT_KEYS = [
41432
41684
  "WEBAPI_PORT",
@@ -41508,7 +41760,7 @@ var HEADER = [
41508
41760
  var INSTALLER_OWNED = ["APP_TAG"];
41509
41761
  function loadOrCreateInstanceEnv(path, opts = {}) {
41510
41762
  const generated = buildInstanceEnv(opts);
41511
- const existing = existsSync7(path) ? parseEnv(readFileSync9(path, "utf8")) : {};
41763
+ const existing = existsSync7(path) ? parseEnv(readFileSync10(path, "utf8")) : {};
41512
41764
  const merged = { ...generated, ...existing };
41513
41765
  for (const key of INSTALLER_OWNED) {
41514
41766
  if (generated[key] !== undefined)
@@ -41522,12 +41774,12 @@ function loadOrCreateInstanceEnv(path, opts = {}) {
41522
41774
  return merged;
41523
41775
  }
41524
41776
  function writeInstanceEnv(path, env4) {
41525
- mkdirSync5(dirname3(path), { recursive: true });
41777
+ mkdirSync5(dirname4(path), { recursive: true });
41526
41778
  writeFileSync5(path, formatEnv(env4, HEADER));
41527
41779
  chmodSync2(path, 384);
41528
41780
  }
41529
41781
  function updateInstanceEnv(path, patch) {
41530
- const env4 = existsSync7(path) ? parseEnv(readFileSync9(path, "utf8")) : {};
41782
+ const env4 = existsSync7(path) ? parseEnv(readFileSync10(path, "utf8")) : {};
41531
41783
  const next = { ...env4, ...patch };
41532
41784
  writeInstanceEnv(path, next);
41533
41785
  return next;
@@ -41692,7 +41944,7 @@ function buildHookConfig(app, env4) {
41692
41944
  var PRESERVED_KEYS = ["mirrors"];
41693
41945
  function readExisting(path) {
41694
41946
  try {
41695
- const parsed = JSON.parse(readFileSync10(path, "utf8"));
41947
+ const parsed = JSON.parse(readFileSync11(path, "utf8"));
41696
41948
  return parsed && typeof parsed === "object" ? parsed : {};
41697
41949
  } catch {
41698
41950
  return {};
@@ -41708,7 +41960,7 @@ function mergePreserved(next, existing) {
41708
41960
  }
41709
41961
  function writeHookConfig(path, config) {
41710
41962
  const merged = mergePreserved(config ?? {}, readExisting(path));
41711
- mkdirSync6(dirname4(path), { recursive: true });
41963
+ mkdirSync6(dirname5(path), { recursive: true });
41712
41964
  writeFileSync6(path, `${JSON.stringify(merged, null, 2)}
41713
41965
  `);
41714
41966
  chmodSync3(path, 384);
@@ -41872,8 +42124,8 @@ async function portsCheck(ports) {
41872
42124
  }
41873
42125
  async function claudeCheck(settingsPath) {
41874
42126
  const dir = settingsPath.replace(/\/[^/]+$/, "");
41875
- const exists2 = await Bun.file(settingsPath).exists().catch(() => false);
41876
- if (exists2)
42127
+ const exists = await Bun.file(settingsPath).exists().catch(() => false);
42128
+ if (exists)
41877
42129
  return { name: "claude code", status: "ok", detail: `settings at ${settingsPath}` };
41878
42130
  return {
41879
42131
  name: "claude code",
@@ -41910,8 +42162,8 @@ var DEV_VERSION = "0.0.0-dev";
41910
42162
  var VERSION = process.env.CT_VERSION ?? DEV_VERSION;
41911
42163
 
41912
42164
  // 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";
42165
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
42166
+ import { dirname as dirname6 } from "path";
41915
42167
 
41916
42168
  // packages/cli/src/lib/statusline.ts
41917
42169
  var STALL_AFTER_MS = 60000;
@@ -41928,6 +42180,11 @@ function whereLabel(t) {
41928
42180
  const host = t.couchUrl.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
41929
42181
  return `${t.sessionsDb}@${host}`;
41930
42182
  }
42183
+ function storeState(s, now2) {
42184
+ if (s.lastFailureMs > s.lastWriteMs && now2 - s.lastWriteMs > STALL_AFTER_MS)
42185
+ return "failing";
42186
+ return s.lastWriteMs > 0 ? "healthy" : "idle";
42187
+ }
41931
42188
  function renderStatusline(state, now2 = Date.now()) {
41932
42189
  if (!state.configured)
41933
42190
  return "\u25CB ct off \xB7 no instance configured";
@@ -41937,21 +42194,41 @@ function renderStatusline(state, now2 = Date.now()) {
41937
42194
  const c = state.counts;
41938
42195
  const toolCalls = c ? Object.values(c.tools).reduce((a, b) => a + b, 0) : 0;
41939
42196
  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)}`;
42197
+ const stores = t.stores?.length ? t.stores : null;
42198
+ if (!stores) {
42199
+ const written = t.lastWriteMs > 0;
42200
+ const failing = t.lastFailureMs > t.lastWriteMs && now2 - t.lastWriteMs > STALL_AFTER_MS;
42201
+ if (failing) {
42202
+ const last2 = written ? `last write ${ago(t.lastWriteMs, now2)}` : "no write landed";
42203
+ return `\u25D0 ct stalled \xB7 ${counts} \xB7 ${last2} \u2192 ${whereLabel(t)}`;
42204
+ }
42205
+ if (!written)
42206
+ return `\u25CC ct ready \xB7 ${counts} \xB7 no write yet \u2192 ${whereLabel(t)}`;
42207
+ return `\u25CF ct rec \xB7 ${counts} \xB7 ${ago(t.lastWriteMs, now2)} \u2192 ${whereLabel(t)}`;
42208
+ }
42209
+ const direct = stores.find((s) => s.kind === "direct") ?? stores[0];
42210
+ const headline = direct ?? { label: whereLabel(t), lastWriteMs: 0, lastFailureMs: 0 };
42211
+ if (direct && storeState(direct, now2) === "healthy") {
42212
+ return `\u25CF ct rec \xB7 ${counts} \xB7 ${ago(direct.lastWriteMs, now2)} \u2192 ${direct.label}`;
42213
+ }
42214
+ const healthyMirrors = stores.filter((s) => s !== direct && storeState(s, now2) === "healthy");
42215
+ const first = healthyMirrors[0];
42216
+ if (first) {
42217
+ const more = healthyMirrors.length > 1 ? ` +${healthyMirrors.length - 1}` : "";
42218
+ return `\u25CF ct rec (mirror) \xB7 ${counts} \xB7 ${ago(first.lastWriteMs, now2)} \u2192 ${first.label}${more}`;
42219
+ }
42220
+ if (stores.some((s) => storeState(s, now2) === "failing")) {
42221
+ const best = Math.max(...stores.map((s) => s.lastWriteMs));
42222
+ const last2 = best > 0 ? `last write ${ago(best, now2)}` : "no write landed";
42223
+ return `\u25D0 ct stalled \xB7 ${counts} \xB7 ${last2} \u2192 ${headline.label}`;
42224
+ }
42225
+ return `\u25CC ct ready \xB7 ${counts} \xB7 no write yet \u2192 ${headline.label}`;
41949
42226
  }
41950
42227
 
41951
42228
  // packages/cli/src/commands/statusline.ts
41952
42229
  function readSettings2(path) {
41953
42230
  try {
41954
- return JSON.parse(readFileSync11(path, "utf8"));
42231
+ return JSON.parse(readFileSync12(path, "utf8"));
41955
42232
  } catch {
41956
42233
  return {};
41957
42234
  }
@@ -42008,7 +42285,7 @@ function install2(argv) {
42008
42285
  console.log(` ${JSON.stringify(next)}`);
42009
42286
  return 0;
42010
42287
  }
42011
- mkdirSync7(dirname5(paths.claudeSettings), { recursive: true });
42288
+ mkdirSync7(dirname6(paths.claudeSettings), { recursive: true });
42012
42289
  writeFileSync7(paths.claudeSettings, `${JSON.stringify({ ...settings, statusLine: next }, null, 2)}
42013
42290
  `);
42014
42291
  console.log(`statusline: registered \u2192 ${paths.claudeSettings}`);
@@ -42128,7 +42405,7 @@ async function runInstall(argv) {
42128
42405
  const noPrune = options["no-prune"] === true;
42129
42406
  const portBaseOpt = strOpt(options, "port-base");
42130
42407
  const paths = installPaths();
42131
- const priorAppTag = existsSync9(paths.instanceEnv) ? /^APP_TAG=(\S+)\s*$/m.exec(readFileSync12(paths.instanceEnv, "utf8"))?.[1] : undefined;
42408
+ const priorAppTag = existsSync9(paths.instanceEnv) ? /^APP_TAG=(\S+)\s*$/m.exec(readFileSync13(paths.instanceEnv, "utf8"))?.[1] : undefined;
42132
42409
  const TOTAL = 8;
42133
42410
  console.log(`claude-transcripts install (${VERSION})`);
42134
42411
  console.log(` config ${paths.configDir}`);
@@ -42615,18 +42892,18 @@ async function runSessions(argv) {
42615
42892
  }
42616
42893
 
42617
42894
  // packages/cli/src/commands/setup.ts
42618
- import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
42895
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "fs";
42619
42896
  import { homedir as homedir3 } from "os";
42620
- import { dirname as dirname6, join as join9 } from "path";
42621
- var REPO_ROOT = join9(import.meta.dir, "..", "..", "..", "..");
42622
- var HOOK_DIR = join9(REPO_ROOT, "hooks");
42623
- var HOOK_CONFIG_PATH = process.env.CT_HOOK_CONFIG ?? join9(homedir3(), ".config", "claude-transcripts", "config.json");
42624
- var GLOBAL_SETTINGS = join9(homedir3(), ".claude", "settings.json");
42625
- var PROJECT_SETTINGS = join9(process.cwd(), ".claude", "settings.json");
42897
+ import { dirname as dirname7, join as join10 } from "path";
42898
+ var REPO_ROOT = join10(import.meta.dir, "..", "..", "..", "..");
42899
+ var HOOK_DIR = join10(REPO_ROOT, "hooks");
42900
+ var HOOK_CONFIG_PATH = process.env.CT_HOOK_CONFIG ?? join10(homedir3(), ".config", "claude-transcripts", "config.json");
42901
+ var GLOBAL_SETTINGS = join10(homedir3(), ".claude", "settings.json");
42902
+ var PROJECT_SETTINGS = join10(process.cwd(), ".claude", "settings.json");
42626
42903
  function loadRepoConfig() {
42627
- const live = join9(REPO_ROOT, "config", "config.json");
42628
- const template2 = join9(REPO_ROOT, "config", "config.template.json");
42629
- return JSON.parse(readFileSync13(existsSync11(live) ? live : template2, "utf8"));
42904
+ const live = join10(REPO_ROOT, "config", "config.json");
42905
+ const template2 = join10(REPO_ROOT, "config", "config.template.json");
42906
+ return JSON.parse(readFileSync14(existsSync11(live) ? live : template2, "utf8"));
42630
42907
  }
42631
42908
  function buildHookConfig2(cfg) {
42632
42909
  const env4 = process.env;
@@ -42693,13 +42970,13 @@ async function probeGarage(blob) {
42693
42970
  }
42694
42971
  function readSettings3(path) {
42695
42972
  try {
42696
- return JSON.parse(readFileSync13(path, "utf8"));
42973
+ return JSON.parse(readFileSync14(path, "utf8"));
42697
42974
  } catch {
42698
42975
  return {};
42699
42976
  }
42700
42977
  }
42701
42978
  function dispatchCommand() {
42702
- return `bun run ${join9(HOOK_DIR, "scripts", "dispatch.ts")}`;
42979
+ return `bun run ${join10(HOOK_DIR, "scripts", "dispatch.ts")}`;
42703
42980
  }
42704
42981
  function isRegistered(path) {
42705
42982
  const cmd = dispatchCommand();
@@ -42708,7 +42985,7 @@ function isRegistered(path) {
42708
42985
  }
42709
42986
  function registerGlobal(dryRun) {
42710
42987
  const cmd = dispatchCommand();
42711
- const generated = JSON.parse(readFileSync13(join9(HOOK_DIR, "hooks", "hooks.json"), "utf8"));
42988
+ const generated = JSON.parse(readFileSync14(join10(HOOK_DIR, "hooks", "hooks.json"), "utf8"));
42712
42989
  const settings = readSettings3(GLOBAL_SETTINGS);
42713
42990
  const hooks2 = settings.hooks ?? {};
42714
42991
  for (const [event, groups] of Object.entries(generated.hooks)) {
@@ -42723,7 +43000,7 @@ function registerGlobal(dryRun) {
42723
43000
  console.log(` [check] would register ${Object.keys(generated.hooks).length} events in ${GLOBAL_SETTINGS}`);
42724
43001
  return;
42725
43002
  }
42726
- mkdirSync8(dirname6(GLOBAL_SETTINGS), { recursive: true });
43003
+ mkdirSync8(dirname7(GLOBAL_SETTINGS), { recursive: true });
42727
43004
  writeFileSync8(GLOBAL_SETTINGS, `${JSON.stringify({ ...settings, hooks: hooks2 }, null, 2)}
42728
43005
  `);
42729
43006
  console.log(` registered ${Object.keys(generated.hooks).length} events (system-wide) in ${GLOBAL_SETTINGS}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-transcripts/cli",
3
- "version": "0.0.16",
3
+ "version": "0.1.0",
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": [