@claude-transcripts/cli 0.2.0 → 0.3.1

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 +81 -18
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -39113,6 +39113,10 @@ async function errorDetail(res) {
39113
39113
  return "";
39114
39114
  }
39115
39115
  }
39116
+ async function exists(path) {
39117
+ const res = await fetch(`${base2()}${path}`);
39118
+ return res.ok;
39119
+ }
39116
39120
  async function getOrNull(path) {
39117
39121
  const res = await fetch(`${base2()}${path}`);
39118
39122
  if (!res.ok)
@@ -39327,6 +39331,9 @@ class DryRunSink {
39327
39331
  async existingSession() {
39328
39332
  return null;
39329
39333
  }
39334
+ async hasTranscriptBlob() {
39335
+ return true;
39336
+ }
39330
39337
  async hasTurns() {
39331
39338
  return false;
39332
39339
  }
@@ -39360,6 +39367,9 @@ class WebapiSink {
39360
39367
  const res = await getOrNull(`/api/sessions/${encodeURIComponent(sessionId)}/turns?limit=1`);
39361
39368
  return (res?.turns?.length ?? 0) > 0;
39362
39369
  }
39370
+ async hasTranscriptBlob(sessionId) {
39371
+ return exists(`/api/s3/sessions/${encodeURIComponent(sessionId)}/transcript.jsonl`);
39372
+ }
39363
39373
  async resetSession(sessionId) {
39364
39374
  const res = await resetSession(sessionId);
39365
39375
  return res.deleted;
@@ -39394,8 +39404,9 @@ function planReingest(existing, opts) {
39394
39404
  if (opts.repair) {
39395
39405
  if (existing.status === "running")
39396
39406
  return { action: "skip", reason: "running" };
39397
- if (opts.hasTurns)
39398
- return { action: "skip", reason: "has-turns" };
39407
+ if (opts.hasTurns) {
39408
+ return opts.hasBlob === false ? { action: "repair-blob" } : { action: "skip", reason: "has-turns" };
39409
+ }
39399
39410
  return { action: "repair" };
39400
39411
  }
39401
39412
  if (!opts.force)
@@ -39463,7 +39474,8 @@ async function runBackfill(argv) {
39463
39474
  }
39464
39475
  const existing = dryRun ? null : await sink.existingSession(t.sessionId);
39465
39476
  const hasTurns = repair && existing ? await sink.hasTurns(t.sessionId) : false;
39466
- const plan = planReingest(existing, { force, replaceLive, repair, hasTurns });
39477
+ const hasBlob = hasTurns ? await sink.hasTranscriptBlob(t.sessionId) : undefined;
39478
+ const plan = planReingest(existing, { force, replaceLive, repair, hasTurns, hasBlob });
39467
39479
  if (plan.action === "skip") {
39468
39480
  if (plan.reason === "live-record") {
39469
39481
  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.");
@@ -39473,11 +39485,18 @@ async function runBackfill(argv) {
39473
39485
  console.log(` \xB7 ${t.sessionId}: skipped \u2014 still running`);
39474
39486
  }
39475
39487
  if (plan.reason === "has-turns") {
39476
- console.log(` \xB7 ${t.sessionId}: skipped \u2014 already has turn content`);
39488
+ console.log(` \xB7 ${t.sessionId}: skipped \u2014 already has turn content and a transcript`);
39477
39489
  }
39478
39490
  skipped++;
39479
39491
  continue;
39480
39492
  }
39493
+ if (plan.action === "repair-blob") {
39494
+ const jsonl = await readTranscript(t.path);
39495
+ await sink.putTranscript(t.sessionId, new TextEncoder().encode(jsonl));
39496
+ console.log(` + ${t.sessionId}: re-uploaded the transcript (chunks left alone)`);
39497
+ repaired++;
39498
+ continue;
39499
+ }
39481
39500
  if (plan.action === "repair") {
39482
39501
  const jsonl = await readTranscript(t.path);
39483
39502
  const facts = deriveSessionFacts(jsonl, { hostname: host, sessionIdHint: t.sessionId });
@@ -40293,6 +40312,9 @@ function makeMirrorCouch(target, onWrite) {
40293
40312
  },
40294
40313
  async putDoc(_db, id, doc, timeoutMs) {
40295
40314
  await send({ ...doc, _id: id }, timeoutMs ?? fallback);
40315
+ },
40316
+ async upsertDoc(_db, id, doc, timeoutMs) {
40317
+ await send({ ...doc, _id: id }, timeoutMs ?? fallback);
40296
40318
  }
40297
40319
  };
40298
40320
  }
@@ -40328,6 +40350,9 @@ function fanOutCouch(clients) {
40328
40350
  },
40329
40351
  async putDoc(db, id, doc, timeoutMs) {
40330
40352
  await Promise.allSettled(clients.map((c) => c.putDoc(db, id, doc, timeoutMs)));
40353
+ },
40354
+ async upsertDoc(db, id, doc, timeoutMs) {
40355
+ await Promise.allSettled(clients.map((c) => c.upsertDoc(db, id, doc, timeoutMs)));
40331
40356
  }
40332
40357
  };
40333
40358
  }
@@ -40403,6 +40428,39 @@ function makeDirectCouch(config, onWrite) {
40403
40428
  } catch {
40404
40429
  onWrite?.(false);
40405
40430
  }
40431
+ },
40432
+ async upsertDoc(db, id, doc, timeoutMs = 5000) {
40433
+ const url = `${root}/${db}/${encodeURIComponent(id)}`;
40434
+ const put = (body) => fetch(url, {
40435
+ method: "PUT",
40436
+ headers,
40437
+ body: JSON.stringify(body),
40438
+ signal: AbortSignal.timeout(timeoutMs)
40439
+ });
40440
+ try {
40441
+ const res = await put(doc);
40442
+ if (res.status !== 409) {
40443
+ onWrite?.(res.ok);
40444
+ return;
40445
+ }
40446
+ const current = await fetch(url, {
40447
+ headers,
40448
+ signal: AbortSignal.timeout(timeoutMs)
40449
+ });
40450
+ if (!current.ok) {
40451
+ onWrite?.(false);
40452
+ return;
40453
+ }
40454
+ const { _rev } = await current.json();
40455
+ if (!_rev) {
40456
+ onWrite?.(false);
40457
+ return;
40458
+ }
40459
+ const retry = await put({ ...doc, _rev });
40460
+ onWrite?.(retry.ok);
40461
+ } catch {
40462
+ onWrite?.(false);
40463
+ }
40406
40464
  }
40407
40465
  };
40408
40466
  }
@@ -40913,11 +40971,10 @@ var writeSummary = async (ctx) => {
40913
40971
  system_checks: {},
40914
40972
  source: "live"
40915
40973
  };
40916
- await ctx.couch.putDoc(ctx.sessionsDb, `summary:${ctx.sessionId}`, doc, 30000);
40974
+ await ctx.couch.upsertDoc(ctx.sessionsDb, `summary:${ctx.sessionId}`, doc, 30000);
40917
40975
  if (ctx.blob.enabled && ctx.sessionsBucket) {
40918
40976
  await ctx.blob.put(ctx.sessionsBucket, `${ctx.sessionId}/summary.json`, JSON.stringify(doc), "application/json");
40919
40977
  }
40920
- ctx.counts.clear();
40921
40978
  ctx.targets.clear();
40922
40979
  };
40923
40980
  var uploadBlobs = async (ctx) => {
@@ -42121,6 +42178,9 @@ function ago(ms, now) {
42121
42178
  return `${m}m ago`;
42122
42179
  return `${Math.round(m / 60)}h ago`;
42123
42180
  }
42181
+ function versionLabel(version = VERSION) {
42182
+ return `ct@${version === DEV_VERSION ? "dev" : version}`;
42183
+ }
42124
42184
  function whereLabel(t) {
42125
42185
  const host = t.couchUrl.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
42126
42186
  return `${t.sessionsDb}@${host}`;
@@ -42131,11 +42191,12 @@ function storeState(s, now) {
42131
42191
  return s.lastWriteMs > 0 ? "healthy" : "idle";
42132
42192
  }
42133
42193
  function renderStatusline(state, now = Date.now()) {
42194
+ const ct = versionLabel(state.version);
42134
42195
  if (!state.configured)
42135
- return "\u25CB ct off \xB7 no instance configured";
42196
+ return `\u25CB ${ct} off \xB7 no instance configured`;
42136
42197
  const t = state.targets;
42137
42198
  if (!t)
42138
- return "\u25CB ct off \xB7 not recording this session";
42199
+ return `\u25CB ${ct} off \xB7 not recording this session`;
42139
42200
  const c = state.counts;
42140
42201
  const toolCalls = c ? Object.values(c.tools).reduce((a, b) => a + b, 0) : 0;
42141
42202
  const counts = c ? `${c.events} ev \xB7 ${toolCalls} tools` : "0 ev";
@@ -42145,29 +42206,29 @@ function renderStatusline(state, now = Date.now()) {
42145
42206
  const failing = t.lastFailureMs > t.lastWriteMs && now - t.lastWriteMs > STALL_AFTER_MS;
42146
42207
  if (failing) {
42147
42208
  const last = written ? `last write ${ago(t.lastWriteMs, now)}` : "no write landed";
42148
- return `\u25D0 ct stalled \xB7 ${counts} \xB7 ${last} \u2192 ${whereLabel(t)}`;
42209
+ return `\u25D0 ${ct} stalled \xB7 ${counts} \xB7 ${last} \u2192 ${whereLabel(t)}`;
42149
42210
  }
42150
42211
  if (!written)
42151
- return `\u25CC ct ready \xB7 ${counts} \xB7 no write yet \u2192 ${whereLabel(t)}`;
42152
- return `\u25CF ct rec \xB7 ${counts} \xB7 ${ago(t.lastWriteMs, now)} \u2192 ${whereLabel(t)}`;
42212
+ return `\u25CC ${ct} ready \xB7 ${counts} \xB7 no write yet \u2192 ${whereLabel(t)}`;
42213
+ return `\u25CF ${ct} rec \xB7 ${counts} \xB7 ${ago(t.lastWriteMs, now)} \u2192 ${whereLabel(t)}`;
42153
42214
  }
42154
42215
  const direct = stores.find((s) => s.kind === "direct") ?? stores[0];
42155
42216
  const headline = direct ?? { label: whereLabel(t), lastWriteMs: 0, lastFailureMs: 0 };
42156
42217
  if (direct && storeState(direct, now) === "healthy") {
42157
- return `\u25CF ct rec \xB7 ${counts} \xB7 ${ago(direct.lastWriteMs, now)} \u2192 ${direct.label}`;
42218
+ return `\u25CF ${ct} rec \xB7 ${counts} \xB7 ${ago(direct.lastWriteMs, now)} \u2192 ${direct.label}`;
42158
42219
  }
42159
42220
  const healthyMirrors = stores.filter((s) => s !== direct && storeState(s, now) === "healthy");
42160
42221
  const first = healthyMirrors[0];
42161
42222
  if (first) {
42162
42223
  const more = healthyMirrors.length > 1 ? ` +${healthyMirrors.length - 1}` : "";
42163
- return `\u25CF ct rec (mirror) \xB7 ${counts} \xB7 ${ago(first.lastWriteMs, now)} \u2192 ${first.label}${more}`;
42224
+ return `\u25CF ${ct} rec (mirror) \xB7 ${counts} \xB7 ${ago(first.lastWriteMs, now)} \u2192 ${first.label}${more}`;
42164
42225
  }
42165
42226
  if (stores.some((s) => storeState(s, now) === "failing")) {
42166
42227
  const best = Math.max(...stores.map((s) => s.lastWriteMs));
42167
42228
  const last = best > 0 ? `last write ${ago(best, now)}` : "no write landed";
42168
- return `\u25D0 ct stalled \xB7 ${counts} \xB7 ${last} \u2192 ${headline.label}`;
42229
+ return `\u25D0 ${ct} stalled \xB7 ${counts} \xB7 ${last} \u2192 ${headline.label}`;
42169
42230
  }
42170
- return `\u25CC ct ready \xB7 ${counts} \xB7 no write yet \u2192 ${headline.label}`;
42231
+ return `\u25CC ${ct} ready \xB7 ${counts} \xB7 no write yet \u2192 ${headline.label}`;
42171
42232
  }
42172
42233
 
42173
42234
  // packages/cli/src/commands/statusline.ts
@@ -42201,7 +42262,7 @@ async function render2() {
42201
42262
  process.stdout.write(`${line}
42202
42263
  `);
42203
42264
  } catch {
42204
- process.stdout.write(`\u25CB ct off
42265
+ process.stdout.write(`\u25CB ${versionLabel()} off
42205
42266
  `);
42206
42267
  }
42207
42268
  return 0;
@@ -42775,7 +42836,7 @@ function tools(counts) {
42775
42836
  function summaryLine(s) {
42776
42837
  return row([
42777
42838
  [s.sessionId.slice(0, 8), 8],
42778
- [when(s.timestamp), 16],
42839
+ [when(s.startTimestamp ?? s.timestamp), 16],
42779
42840
  [s.status, 10],
42780
42841
  [project2(s.cwd), 18],
42781
42842
  [num3(s.promptCount), 7],
@@ -42825,7 +42886,9 @@ async function showDetail(id, limit, json) {
42825
42886
  return 0;
42826
42887
  }
42827
42888
  console.log(`session ${s.sessionId} [${s.status}]`);
42828
- console.log(` started ${s.timestamp ?? "\u2014"}`);
42889
+ console.log(` started ${s.startTimestamp ?? s.timestamp ?? "\u2014"}`);
42890
+ if (s.status === "ended")
42891
+ console.log(` ended ${s.timestamp ?? "\u2014"}`);
42829
42892
  console.log(` project ${s.cwd || "\u2014"}`);
42830
42893
  console.log(` model ${s.model ?? "\u2014"}`);
42831
42894
  console.log(` hostname ${s.hostname || "\u2014"}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-transcripts/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
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": [
@@ -34,7 +34,7 @@
34
34
  "LICENSE"
35
35
  ],
36
36
  "engines": {
37
- "bun": ">=1.1.0"
37
+ "bun": ">=1.4.0"
38
38
  },
39
39
  "publishConfig": {
40
40
  "access": "public"