@claude-transcripts/cli 0.3.0 → 0.3.2

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 +231 -141
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -29928,14 +29928,17 @@ var CLI_SPEC = {
29928
29928
  },
29929
29929
  {
29930
29930
  name: "--repair",
29931
- description: "add missing chunk docs to adopted sessions; leaves summary + events alone"
29931
+ description: "add what an interrupted write left out of an adopted session: missing chunk docs, or a transcript that never reached S3; leaves summary + events alone"
29932
29932
  },
29933
29933
  {
29934
29934
  name: "--session",
29935
29935
  type: "string",
29936
29936
  description: "with --force: re-process only this session"
29937
29937
  },
29938
- { name: "--dry-run", description: "preview without writing" }
29938
+ {
29939
+ name: "--dry-run",
29940
+ description: "preview without writing (reads the store, so skips are shown)"
29941
+ }
29939
29942
  ],
29940
29943
  examples: ["backfill --dry-run", "backfill", "backfill --force --session 3f9a2c1e"]
29941
29944
  },
@@ -38772,6 +38775,128 @@ var import_react28 = __toESM(require_react(), 1);
38772
38775
  // packages/cli/src/commands/backfill.ts
38773
38776
  import { hostname } from "os";
38774
38777
 
38778
+ // packages/cli/src/api/http.ts
38779
+ import { readFileSync as readFileSync2 } from "fs";
38780
+
38781
+ // packages/cli/src/lib/paths.ts
38782
+ import { homedir } from "os";
38783
+ import { join as join2 } from "path";
38784
+ function xdg(envVar, fallback) {
38785
+ const v = process.env[envVar];
38786
+ return v?.trim() ? v : fallback;
38787
+ }
38788
+ function installPaths() {
38789
+ const home = homedir();
38790
+ const ctHome = process.env.CT_HOME;
38791
+ const configDir = ctHome ? join2(ctHome, "config") : join2(xdg("XDG_CONFIG_HOME", join2(home, ".config")), "claude-transcripts");
38792
+ const dataDir = ctHome ? join2(ctHome, "data") : join2(xdg("XDG_DATA_HOME", join2(home, ".local", "share")), "claude-transcripts");
38793
+ const binDir = ctHome ? join2(ctHome, "bin") : join2(home, ".local", "bin");
38794
+ const claudeSettings = ctHome ? join2(ctHome, "claude", "settings.json") : join2(home, ".claude", "settings.json");
38795
+ return {
38796
+ configDir,
38797
+ dataDir,
38798
+ binDir,
38799
+ claudeSettings,
38800
+ hookConfig: process.env.CT_HOOK_CONFIG ?? join2(configDir, "config.json"),
38801
+ instanceEnv: join2(configDir, "instance.env"),
38802
+ appConfig: join2(configDir, "app.json"),
38803
+ deployDir: join2(dataDir, "deploy"),
38804
+ versionFile: join2(dataDir, "version")
38805
+ };
38806
+ }
38807
+
38808
+ // packages/cli/src/api/http.ts
38809
+ var DEFAULT_WEBAPI_PORT = "7650";
38810
+ var resolved = null;
38811
+ function base2() {
38812
+ resolved ??= resolveWebapiUrl();
38813
+ return resolved;
38814
+ }
38815
+ function resolveWebapiUrl() {
38816
+ if (process.env.CT_WEBAPI_URL)
38817
+ return process.env.CT_WEBAPI_URL.replace(/\/$/, "");
38818
+ const host = process.env.WEBAPI_HOST || "127.0.0.1";
38819
+ if (process.env.WEBAPI_PORT)
38820
+ return `http://${host}:${process.env.WEBAPI_PORT}`;
38821
+ const fromInstance = instanceWebapiUrl();
38822
+ if (fromInstance)
38823
+ return fromInstance;
38824
+ return `http://${host}:${DEFAULT_WEBAPI_PORT}`;
38825
+ }
38826
+ function instanceWebapiUrl() {
38827
+ try {
38828
+ const raw = readFileSync2(installPaths().instanceEnv, "utf8");
38829
+ const port = /^WEBAPI_PORT=(\d+)\s*$/m.exec(raw)?.[1];
38830
+ if (!port)
38831
+ return null;
38832
+ const host = /^WEBAPI_HOST=(\S+)\s*$/m.exec(raw)?.[1] ?? "127.0.0.1";
38833
+ return `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}`;
38834
+ } catch {
38835
+ return null;
38836
+ }
38837
+ }
38838
+ function setWebapiUrl(url) {
38839
+ resolved = url.replace(/\/$/, "");
38840
+ }
38841
+ function webapiUrl() {
38842
+ return base2();
38843
+ }
38844
+ async function customFetch(url, init) {
38845
+ const res = await fetch(`${base2()}${url}`, init);
38846
+ if (!res.ok) {
38847
+ throw new Error(`${init?.method ?? "GET"} ${url} \u2192 ${res.status} ${res.statusText}${await errorDetail(res)}`);
38848
+ }
38849
+ const text = await res.text();
38850
+ return text ? JSON.parse(text) : undefined;
38851
+ }
38852
+ async function errorDetail(res) {
38853
+ try {
38854
+ const text = (await res.text()).trim();
38855
+ if (!text)
38856
+ return "";
38857
+ try {
38858
+ const body = JSON.parse(text);
38859
+ const msg = body.error ?? body.reason;
38860
+ if (typeof msg === "string" && msg)
38861
+ return `: ${msg}`;
38862
+ } catch {}
38863
+ return `: ${text.slice(0, 300)}`;
38864
+ } catch {
38865
+ return "";
38866
+ }
38867
+ }
38868
+ async function exists(path) {
38869
+ const res = await fetch(`${base2()}${path}`);
38870
+ return res.ok;
38871
+ }
38872
+ async function getOrNull(path) {
38873
+ const res = await fetch(`${base2()}${path}`);
38874
+ if (!res.ok)
38875
+ return null;
38876
+ return await res.json();
38877
+ }
38878
+ async function putStream(path, filePath, contentType) {
38879
+ const res = await fetch(`${base2()}${path}`, {
38880
+ method: "PUT",
38881
+ headers: { "content-type": contentType },
38882
+ body: Bun.file(filePath).stream(),
38883
+ duplex: "half"
38884
+ });
38885
+ if (!res.ok) {
38886
+ throw new Error(`PUT ${path} \u2192 ${res.status} ${res.statusText}${await errorDetail(res)}`);
38887
+ }
38888
+ }
38889
+ async function putRaw(path, body, contentType) {
38890
+ const res = await fetch(`${base2()}${path}`, {
38891
+ method: "PUT",
38892
+ headers: { "content-type": contentType },
38893
+ body
38894
+ });
38895
+ if (!res.ok) {
38896
+ throw new Error(`PUT ${path} \u2192 ${res.status} ${res.statusText}${await errorDetail(res)}`);
38897
+ }
38898
+ }
38899
+
38775
38900
  // packages/cli/src/lib/args.ts
38776
38901
  function parseFlags(argv) {
38777
38902
  const positionals = [];
@@ -38807,10 +38932,10 @@ function strOpt(opts, key) {
38807
38932
 
38808
38933
  // packages/cli/src/lib/claude-fs.ts
38809
38934
  import { readdir, readFile } from "fs/promises";
38810
- import { homedir } from "os";
38811
- import { join as join2 } from "path";
38935
+ import { homedir as homedir2 } from "os";
38936
+ import { join as join3 } from "path";
38812
38937
  function defaultProjectsDir() {
38813
- return join2(homedir(), ".claude", "projects");
38938
+ return join3(homedir2(), ".claude", "projects");
38814
38939
  }
38815
38940
  async function discoverTranscripts(root) {
38816
38941
  const out = [];
@@ -38818,12 +38943,12 @@ async function discoverTranscripts(root) {
38818
38943
  for (const proj of projects) {
38819
38944
  if (!proj.isDirectory())
38820
38945
  continue;
38821
- const dir = join2(root, proj.name);
38946
+ const dir = join3(root, proj.name);
38822
38947
  const files = await readdir(dir).catch(() => []);
38823
38948
  for (const f of files) {
38824
38949
  if (!f.endsWith(".jsonl"))
38825
38950
  continue;
38826
- out.push({ path: join2(dir, f), project: proj.name, sessionId: f.replace(/\.jsonl$/, "") });
38951
+ out.push({ path: join3(dir, f), project: proj.name, sessionId: f.replace(/\.jsonl$/, "") });
38827
38952
  }
38828
38953
  }
38829
38954
  return out.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
@@ -39023,124 +39148,6 @@ function safeStringify(v) {
39023
39148
  }
39024
39149
  }
39025
39150
 
39026
- // packages/cli/src/api/http.ts
39027
- import { readFileSync as readFileSync2 } from "fs";
39028
-
39029
- // packages/cli/src/lib/paths.ts
39030
- import { homedir as homedir2 } from "os";
39031
- import { join as join3 } from "path";
39032
- function xdg(envVar, fallback) {
39033
- const v = process.env[envVar];
39034
- return v?.trim() ? v : fallback;
39035
- }
39036
- function installPaths() {
39037
- const home = homedir2();
39038
- const ctHome = process.env.CT_HOME;
39039
- const configDir = ctHome ? join3(ctHome, "config") : join3(xdg("XDG_CONFIG_HOME", join3(home, ".config")), "claude-transcripts");
39040
- const dataDir = ctHome ? join3(ctHome, "data") : join3(xdg("XDG_DATA_HOME", join3(home, ".local", "share")), "claude-transcripts");
39041
- const binDir = ctHome ? join3(ctHome, "bin") : join3(home, ".local", "bin");
39042
- const claudeSettings = ctHome ? join3(ctHome, "claude", "settings.json") : join3(home, ".claude", "settings.json");
39043
- return {
39044
- configDir,
39045
- dataDir,
39046
- binDir,
39047
- claudeSettings,
39048
- hookConfig: process.env.CT_HOOK_CONFIG ?? join3(configDir, "config.json"),
39049
- instanceEnv: join3(configDir, "instance.env"),
39050
- appConfig: join3(configDir, "app.json"),
39051
- deployDir: join3(dataDir, "deploy"),
39052
- versionFile: join3(dataDir, "version")
39053
- };
39054
- }
39055
-
39056
- // packages/cli/src/api/http.ts
39057
- var DEFAULT_WEBAPI_PORT = "7650";
39058
- var resolved = null;
39059
- function base2() {
39060
- resolved ??= resolveWebapiUrl();
39061
- return resolved;
39062
- }
39063
- function resolveWebapiUrl() {
39064
- if (process.env.CT_WEBAPI_URL)
39065
- return process.env.CT_WEBAPI_URL.replace(/\/$/, "");
39066
- const host = process.env.WEBAPI_HOST || "127.0.0.1";
39067
- if (process.env.WEBAPI_PORT)
39068
- return `http://${host}:${process.env.WEBAPI_PORT}`;
39069
- const fromInstance = instanceWebapiUrl();
39070
- if (fromInstance)
39071
- return fromInstance;
39072
- return `http://${host}:${DEFAULT_WEBAPI_PORT}`;
39073
- }
39074
- function instanceWebapiUrl() {
39075
- try {
39076
- const raw = readFileSync2(installPaths().instanceEnv, "utf8");
39077
- const port = /^WEBAPI_PORT=(\d+)\s*$/m.exec(raw)?.[1];
39078
- if (!port)
39079
- return null;
39080
- const host = /^WEBAPI_HOST=(\S+)\s*$/m.exec(raw)?.[1] ?? "127.0.0.1";
39081
- return `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}`;
39082
- } catch {
39083
- return null;
39084
- }
39085
- }
39086
- function setWebapiUrl(url) {
39087
- resolved = url.replace(/\/$/, "");
39088
- }
39089
- function webapiUrl() {
39090
- return base2();
39091
- }
39092
- async function customFetch(url, init) {
39093
- const res = await fetch(`${base2()}${url}`, init);
39094
- if (!res.ok) {
39095
- throw new Error(`${init?.method ?? "GET"} ${url} \u2192 ${res.status} ${res.statusText}${await errorDetail(res)}`);
39096
- }
39097
- const text = await res.text();
39098
- return text ? JSON.parse(text) : undefined;
39099
- }
39100
- async function errorDetail(res) {
39101
- try {
39102
- const text = (await res.text()).trim();
39103
- if (!text)
39104
- return "";
39105
- try {
39106
- const body = JSON.parse(text);
39107
- const msg = body.error ?? body.reason;
39108
- if (typeof msg === "string" && msg)
39109
- return `: ${msg}`;
39110
- } catch {}
39111
- return `: ${text.slice(0, 300)}`;
39112
- } catch {
39113
- return "";
39114
- }
39115
- }
39116
- async function getOrNull(path) {
39117
- const res = await fetch(`${base2()}${path}`);
39118
- if (!res.ok)
39119
- return null;
39120
- return await res.json();
39121
- }
39122
- async function putStream(path, filePath, contentType) {
39123
- const res = await fetch(`${base2()}${path}`, {
39124
- method: "PUT",
39125
- headers: { "content-type": contentType },
39126
- body: Bun.file(filePath).stream(),
39127
- duplex: "half"
39128
- });
39129
- if (!res.ok) {
39130
- throw new Error(`PUT ${path} \u2192 ${res.status} ${res.statusText}${await errorDetail(res)}`);
39131
- }
39132
- }
39133
- async function putRaw(path, body, contentType) {
39134
- const res = await fetch(`${base2()}${path}`, {
39135
- method: "PUT",
39136
- headers: { "content-type": contentType },
39137
- body
39138
- });
39139
- if (!res.ok) {
39140
- throw new Error(`PUT ${path} \u2192 ${res.status} ${res.statusText}${await errorDetail(res)}`);
39141
- }
39142
- }
39143
-
39144
39151
  // packages/cli/src/api/generated.ts
39145
39152
  var getListSessionsUrl = (params) => {
39146
39153
  const normalizedParams = new URLSearchParams;
@@ -39324,11 +39331,41 @@ var migrateDown = async (migrateDownBody, options) => {
39324
39331
  // packages/cli/src/lib/sink.ts
39325
39332
  class DryRunSink {
39326
39333
  label = "dry-run";
39327
- async existingSession() {
39328
- return null;
39334
+ blind = false;
39335
+ reader;
39336
+ constructor(reader) {
39337
+ this.reader = reader;
39329
39338
  }
39330
- async hasTurns() {
39331
- return false;
39339
+ reads() {
39340
+ if (this.reader === undefined) {
39341
+ try {
39342
+ this.reader = new WebapiSink;
39343
+ } catch {
39344
+ this.reader = null;
39345
+ this.blind = true;
39346
+ }
39347
+ }
39348
+ return this.reader;
39349
+ }
39350
+ async ask(run, whenBlind) {
39351
+ const r = this.reads();
39352
+ if (!r)
39353
+ return whenBlind;
39354
+ try {
39355
+ return await run(r);
39356
+ } catch {
39357
+ this.blind = true;
39358
+ return whenBlind;
39359
+ }
39360
+ }
39361
+ async existingSession(sessionId) {
39362
+ return this.ask((r) => r.existingSession(sessionId), null);
39363
+ }
39364
+ async hasTranscriptBlob(sessionId) {
39365
+ return this.ask((r) => r.hasTranscriptBlob(sessionId), true);
39366
+ }
39367
+ async hasTurns(sessionId) {
39368
+ return this.ask((r) => r.hasTurns(sessionId), false);
39332
39369
  }
39333
39370
  async resetSession(sessionId) {
39334
39371
  console.log(` [dry-run] DELETE ${sessionId} (summary + event + chunk docs)`);
@@ -39360,6 +39397,9 @@ class WebapiSink {
39360
39397
  const res = await getOrNull(`/api/sessions/${encodeURIComponent(sessionId)}/turns?limit=1`);
39361
39398
  return (res?.turns?.length ?? 0) > 0;
39362
39399
  }
39400
+ async hasTranscriptBlob(sessionId) {
39401
+ return exists(`/api/s3/sessions/${encodeURIComponent(sessionId)}/transcript.jsonl`);
39402
+ }
39363
39403
  async resetSession(sessionId) {
39364
39404
  const res = await resetSession(sessionId);
39365
39405
  return res.deleted;
@@ -39380,11 +39420,9 @@ class WebapiSink {
39380
39420
  }
39381
39421
  }
39382
39422
  function makeSink(opts) {
39383
- if (opts.dryRun)
39384
- return new DryRunSink;
39385
39423
  if (opts.webapiUrl)
39386
39424
  setWebapiUrl(opts.webapiUrl);
39387
- return new WebapiSink;
39425
+ return opts.dryRun ? new DryRunSink : new WebapiSink;
39388
39426
  }
39389
39427
 
39390
39428
  // packages/cli/src/commands/backfill.ts
@@ -39394,8 +39432,9 @@ function planReingest(existing, opts) {
39394
39432
  if (opts.repair) {
39395
39433
  if (existing.status === "running")
39396
39434
  return { action: "skip", reason: "running" };
39397
- if (opts.hasTurns)
39398
- return { action: "skip", reason: "has-turns" };
39435
+ if (opts.hasTurns) {
39436
+ return opts.hasBlob === false ? { action: "repair-blob" } : { action: "skip", reason: "has-turns" };
39437
+ }
39399
39438
  return { action: "repair" };
39400
39439
  }
39401
39440
  if (!opts.force)
@@ -39461,9 +39500,10 @@ async function runBackfill(argv) {
39461
39500
  skipped++;
39462
39501
  continue;
39463
39502
  }
39464
- const existing = dryRun ? null : await sink.existingSession(t.sessionId);
39503
+ const existing = await sink.existingSession(t.sessionId);
39465
39504
  const hasTurns = repair && existing ? await sink.hasTurns(t.sessionId) : false;
39466
- const plan = planReingest(existing, { force, replaceLive, repair, hasTurns });
39505
+ const hasBlob = hasTurns ? await sink.hasTranscriptBlob(t.sessionId) : undefined;
39506
+ const plan = planReingest(existing, { force, replaceLive, repair, hasTurns, hasBlob });
39467
39507
  if (plan.action === "skip") {
39468
39508
  if (plan.reason === "live-record") {
39469
39509
  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 +39513,18 @@ async function runBackfill(argv) {
39473
39513
  console.log(` \xB7 ${t.sessionId}: skipped \u2014 still running`);
39474
39514
  }
39475
39515
  if (plan.reason === "has-turns") {
39476
- console.log(` \xB7 ${t.sessionId}: skipped \u2014 already has turn content`);
39516
+ console.log(` \xB7 ${t.sessionId}: skipped \u2014 already has turn content and a transcript`);
39477
39517
  }
39478
39518
  skipped++;
39479
39519
  continue;
39480
39520
  }
39521
+ if (plan.action === "repair-blob") {
39522
+ const jsonl = await readTranscript(t.path);
39523
+ await sink.putTranscript(t.sessionId, new TextEncoder().encode(jsonl));
39524
+ console.log(` + ${t.sessionId}: re-uploaded the transcript (chunks left alone)`);
39525
+ repaired++;
39526
+ continue;
39527
+ }
39481
39528
  if (plan.action === "repair") {
39482
39529
  const jsonl = await readTranscript(t.path);
39483
39530
  const facts = deriveSessionFacts(jsonl, { hostname: host, sessionIdHint: t.sessionId });
@@ -39507,6 +39554,9 @@ async function runBackfill(argv) {
39507
39554
  console.error(` ! ${t.sessionId}: ${err.message}`);
39508
39555
  }
39509
39556
  }
39557
+ if (sink instanceof DryRunSink && sink.blind) {
39558
+ console.warn(`backfill: WARNING \u2014 could not read ${webapiUrl()}, so this preview assumed nothing ` + "is stored yet. A real run will skip whatever is already there; re-run the " + "preview once the webapi is reachable to see the true plan.");
39559
+ }
39510
39560
  console.log(`backfill: ${written} backfilled${repaired ? `, ${repaired} repaired` : ""}` + `${reprocessed ? ` (${reprocessed} re-processed)` : ""}, ` + `${skipped} skipped${refusedLive ? ` (${refusedLive} live, not replaced)` : ""}, ` + `${failed} failed`);
39511
39561
  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).");
39512
39562
  if (reprocessed && !dryRun) {
@@ -40293,6 +40343,9 @@ function makeMirrorCouch(target, onWrite) {
40293
40343
  },
40294
40344
  async putDoc(_db, id, doc, timeoutMs) {
40295
40345
  await send({ ...doc, _id: id }, timeoutMs ?? fallback);
40346
+ },
40347
+ async upsertDoc(_db, id, doc, timeoutMs) {
40348
+ await send({ ...doc, _id: id }, timeoutMs ?? fallback);
40296
40349
  }
40297
40350
  };
40298
40351
  }
@@ -40328,6 +40381,9 @@ function fanOutCouch(clients) {
40328
40381
  },
40329
40382
  async putDoc(db, id, doc, timeoutMs) {
40330
40383
  await Promise.allSettled(clients.map((c) => c.putDoc(db, id, doc, timeoutMs)));
40384
+ },
40385
+ async upsertDoc(db, id, doc, timeoutMs) {
40386
+ await Promise.allSettled(clients.map((c) => c.upsertDoc(db, id, doc, timeoutMs)));
40331
40387
  }
40332
40388
  };
40333
40389
  }
@@ -40403,6 +40459,39 @@ function makeDirectCouch(config, onWrite) {
40403
40459
  } catch {
40404
40460
  onWrite?.(false);
40405
40461
  }
40462
+ },
40463
+ async upsertDoc(db, id, doc, timeoutMs = 5000) {
40464
+ const url = `${root}/${db}/${encodeURIComponent(id)}`;
40465
+ const put = (body) => fetch(url, {
40466
+ method: "PUT",
40467
+ headers,
40468
+ body: JSON.stringify(body),
40469
+ signal: AbortSignal.timeout(timeoutMs)
40470
+ });
40471
+ try {
40472
+ const res = await put(doc);
40473
+ if (res.status !== 409) {
40474
+ onWrite?.(res.ok);
40475
+ return;
40476
+ }
40477
+ const current = await fetch(url, {
40478
+ headers,
40479
+ signal: AbortSignal.timeout(timeoutMs)
40480
+ });
40481
+ if (!current.ok) {
40482
+ onWrite?.(false);
40483
+ return;
40484
+ }
40485
+ const { _rev } = await current.json();
40486
+ if (!_rev) {
40487
+ onWrite?.(false);
40488
+ return;
40489
+ }
40490
+ const retry = await put({ ...doc, _rev });
40491
+ onWrite?.(retry.ok);
40492
+ } catch {
40493
+ onWrite?.(false);
40494
+ }
40406
40495
  }
40407
40496
  };
40408
40497
  }
@@ -40913,11 +41002,10 @@ var writeSummary = async (ctx) => {
40913
41002
  system_checks: {},
40914
41003
  source: "live"
40915
41004
  };
40916
- await ctx.couch.putDoc(ctx.sessionsDb, `summary:${ctx.sessionId}`, doc, 30000);
41005
+ await ctx.couch.upsertDoc(ctx.sessionsDb, `summary:${ctx.sessionId}`, doc, 30000);
40917
41006
  if (ctx.blob.enabled && ctx.sessionsBucket) {
40918
41007
  await ctx.blob.put(ctx.sessionsBucket, `${ctx.sessionId}/summary.json`, JSON.stringify(doc), "application/json");
40919
41008
  }
40920
- ctx.counts.clear();
40921
41009
  ctx.targets.clear();
40922
41010
  };
40923
41011
  var uploadBlobs = async (ctx) => {
@@ -42779,7 +42867,7 @@ function tools(counts) {
42779
42867
  function summaryLine(s) {
42780
42868
  return row([
42781
42869
  [s.sessionId.slice(0, 8), 8],
42782
- [when(s.timestamp), 16],
42870
+ [when(s.startTimestamp ?? s.timestamp), 16],
42783
42871
  [s.status, 10],
42784
42872
  [project2(s.cwd), 18],
42785
42873
  [num3(s.promptCount), 7],
@@ -42829,7 +42917,9 @@ async function showDetail(id, limit, json) {
42829
42917
  return 0;
42830
42918
  }
42831
42919
  console.log(`session ${s.sessionId} [${s.status}]`);
42832
- console.log(` started ${s.timestamp ?? "\u2014"}`);
42920
+ console.log(` started ${s.startTimestamp ?? s.timestamp ?? "\u2014"}`);
42921
+ if (s.status === "ended")
42922
+ console.log(` ended ${s.timestamp ?? "\u2014"}`);
42833
42923
  console.log(` project ${s.cwd || "\u2014"}`);
42834
42924
  console.log(` model ${s.model ?? "\u2014"}`);
42835
42925
  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.3.0",
3
+ "version": "0.3.2",
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": [