@claude-transcripts/cli 0.3.3 → 0.5.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.
package/README.md CHANGED
@@ -109,6 +109,7 @@ upgrades in place: it's idempotent and keeps your history.
109
109
  | `doctor [options]` | Smoke-test the write/read/search path end-to-end |
110
110
  | `hook [action] [options]` | The Claude Code hook, and its registration |
111
111
  | `statusline [action] [options]` | The Claude Code statusline indicator (recording / off), and its registration |
112
+ | `completions <shell>` | Print a shell completion script to eval or source from your shell's rc |
112
113
 
113
114
  **Global options** (every command)
114
115
 
@@ -1,13 +1,13 @@
1
1
  // @bun
2
2
  import {
3
- CLI_SPEC,
4
3
  isFlag,
5
4
  cliCommandsByGroup,
6
5
  cliUsage,
7
6
  cliArgHint,
8
7
  cliArgLabel,
8
+ CLI_SPEC,
9
9
  COMMANDS
10
- } from "./cli-d7a6svha.js";
10
+ } from "./cli-byvjee1b.js";
11
11
  import {
12
12
  __toESM,
13
13
  __commonJS,
@@ -1,4 +1,385 @@
1
1
  // @bun
2
+ // packages/shared/src/model/cli-project.ts
3
+ var isFlag = (a) => a.name.startsWith("--");
4
+ function cliArgType(a) {
5
+ if (a.type)
6
+ return a.type;
7
+ if (a.choices)
8
+ return "string";
9
+ if (typeof a.default === "number")
10
+ return "number";
11
+ if (typeof a.default === "string")
12
+ return "string";
13
+ return isFlag(a) ? "boolean" : "string";
14
+ }
15
+ function cliCommandsByGroup(spec) {
16
+ return spec.groups.map((g) => ({ ...g, commands: spec.commands.filter((c) => c.group === g.key) })).filter((g) => g.commands.length > 0);
17
+ }
18
+ function cliUsage(cmd) {
19
+ const parts = (cmd.args ?? []).filter((a) => !isFlag(a)).map((a) => a.required ? `<${a.name}>` : `[${a.name}]`);
20
+ const hasFlags = (cmd.args ?? []).some(isFlag);
21
+ return [cmd.name, ...parts, ...hasFlags ? ["[options]"] : []].join(" ");
22
+ }
23
+ function cliArgHint(a) {
24
+ const bits = [];
25
+ if (a.choices)
26
+ bits.push(a.choices.join(" | "));
27
+ if (a.default !== undefined && a.default !== false)
28
+ bits.push(`default ${a.default}`);
29
+ return bits.length ? `(${bits.join("; ")})` : "";
30
+ }
31
+ function cliArgLabel(a) {
32
+ if (!isFlag(a))
33
+ return a.name;
34
+ switch (cliArgType(a)) {
35
+ case "number":
36
+ return `${a.name} <n>`;
37
+ case "string":
38
+ return `${a.name} <value>`;
39
+ default:
40
+ return a.name;
41
+ }
42
+ }
43
+ function validateCliArgs(spec, cmd, parsed) {
44
+ const errors = [];
45
+ const args = cmd.args ?? [];
46
+ const known = new Map;
47
+ for (const a of [...spec.globalArgs, ...args])
48
+ if (isFlag(a))
49
+ known.set(a.name.slice(2), a);
50
+ for (const [key, value] of Object.entries(parsed.options)) {
51
+ const def = known.get(key);
52
+ if (!def) {
53
+ errors.push(`unknown option --${key}`);
54
+ continue;
55
+ }
56
+ errors.push(...checkValue(def, value));
57
+ }
58
+ const positionals = args.filter((a) => !isFlag(a));
59
+ positionals.forEach((def, i) => {
60
+ const value = parsed.positionals[i];
61
+ if (value === undefined) {
62
+ if (def.required)
63
+ errors.push(`missing required argument <${def.name}>`);
64
+ return;
65
+ }
66
+ errors.push(...checkValue(def, value));
67
+ });
68
+ return errors;
69
+ }
70
+ function checkValue(def, value) {
71
+ const type = cliArgType(def);
72
+ const label = isFlag(def) ? def.name : `<${def.name}>`;
73
+ if (typeof value === "boolean") {
74
+ return type === "boolean" ? [] : [`${label} needs a value`];
75
+ }
76
+ if (type === "boolean")
77
+ return [`${label} takes no value (got "${value}")`];
78
+ if (def.choices && !def.choices.includes(value)) {
79
+ return [`${label} must be one of ${def.choices.join(" | ")} (got "${value}")`];
80
+ }
81
+ if (type === "number" && !/^-?\d+(\.\d+)?$/.test(value)) {
82
+ return [`${label} must be a number (got "${value}")`];
83
+ }
84
+ return [];
85
+ }
86
+ var COMPLETION_SHELLS = ["bash", "zsh", "fish"];
87
+ function toCompletions(spec, shell, bin) {
88
+ const model = completionModel(spec);
89
+ const fn = `_${bin.replace(/[^A-Za-z0-9]/g, "_")}`;
90
+ switch (shell) {
91
+ case "bash":
92
+ return bashCompletions(model, bin, fn);
93
+ case "zsh":
94
+ return zshCompletions(model, bin, fn);
95
+ case "fish":
96
+ return fishCompletions(model, bin, fn);
97
+ }
98
+ }
99
+ var takesValue = (a) => isFlag(a) && cliArgType(a) !== "boolean";
100
+ function completionModel(spec) {
101
+ const flags = new Map;
102
+ const valued = [];
103
+ const choices = [];
104
+ for (const c of spec.commands) {
105
+ const all = [...(c.args ?? []).filter(isFlag), ...spec.globalArgs.filter(isFlag)];
106
+ flags.set(c.name, all);
107
+ for (const a of all) {
108
+ if (!takesValue(a))
109
+ continue;
110
+ valued.push(`${c.name}:${a.name}`);
111
+ if (a.choices)
112
+ choices.push([`${c.name}:${a.name}`, a.choices]);
113
+ }
114
+ (c.args ?? []).filter((a) => !isFlag(a)).forEach((a, i) => {
115
+ if (a.choices)
116
+ choices.push([`${c.name}:${i + 1}`, a.choices]);
117
+ });
118
+ }
119
+ const leading = spec.globalArgs.filter((a) => isFlag(a) && !takesValue(a));
120
+ return { commands: spec.commands, leading, flags, valued, choices };
121
+ }
122
+ var header = (shell, bin) => `# ${shell} completion for ${bin} \u2014 generated from CLI_SPEC; do not edit.`;
123
+ var names = (args) => args.map((a) => a.name).join(" ");
124
+ function bashCompletions(m, bin, fn) {
125
+ const flagCases = m.commands.map((c) => ` ${c.name}) printf '%s' "${names(m.flags.get(c.name) ?? [])}" ;;`).join(`
126
+ `);
127
+ const choiceCases = m.choices.map(([k, v]) => ` ${k}) printf '%s' "${v.join(" ")}" ;;`).join(`
128
+ `);
129
+ return `${header("bash", bin)}
130
+ # Load it with: eval "$(${bin} completions bash)" (e.g. in ~/.bashrc)
131
+ # Works with bash 3.2+ and needs no bash-completion package.
132
+
133
+ # Does flag \`cmd:--flag\` ($1) take a value?
134
+ ${fn}_valued() {
135
+ case "$1" in
136
+ ${m.valued.length ? `${m.valued.join("|")}) return 0 ;;` : ""}
137
+ esac
138
+ return 1
139
+ }
140
+
141
+ # The values to offer for $1 \u2014 \`cmd:--flag\` or \`cmd:<positional index>\`.
142
+ ${fn}_choices() {
143
+ case "$1" in
144
+ ${choiceCases}
145
+ esac
146
+ }
147
+
148
+ # The flags command $1 accepts, its own and the global ones.
149
+ ${fn}_flags() {
150
+ case "$1" in
151
+ ${flagCases}
152
+ esac
153
+ }
154
+
155
+ ${fn}() {
156
+ # Split the line ourselves: COMP_WORDS also breaks at ":" and "=", which would turn
157
+ # \`--webapi http://x\` into four words and miscount everything after it.
158
+ local line="\${COMP_LINE:0:COMP_POINT}" cur cmd npos=0 flag="" i n w
159
+ local -a words
160
+ read -ra words <<<"$line"
161
+ [[ $line == *[[:space:]] || \${#words[@]} -eq 0 ]] && words+=("")
162
+ n=\${#words[@]}
163
+ cur="\${words[n - 1]}"
164
+ COMPREPLY=()
165
+ # \`--flag=value\`: bash replaces only the part after "=", which compgen can't target.
166
+ [[ $cur == *=* ]] && return 0
167
+
168
+ if ((n == 2)); then
169
+ if [[ $cur == -* ]]; then
170
+ COMPREPLY=($(compgen -W "${names(m.leading)}" -- "$cur"))
171
+ else
172
+ COMPREPLY=($(compgen -W "${m.commands.map((c) => c.name).join(" ")}" -- "$cur"))
173
+ fi
174
+ return 0
175
+ fi
176
+
177
+ # Only the first word is the command; after a leading flag nothing runs a command.
178
+ cmd="\${words[1]}"
179
+ [[ $cmd == -* ]] && return 0
180
+
181
+ # Count positionals the way parseFlags reads them: a bare --flag takes the next word
182
+ # as its value unless that word is another --flag.
183
+ for ((i = 2; i < n - 1; i++)); do
184
+ w="\${words[i]}"
185
+ if [[ -n $flag && $w != --* ]]; then
186
+ flag=""
187
+ continue
188
+ fi
189
+ flag=""
190
+ case "$w" in
191
+ --*=*) ;;
192
+ --*) flag="$w" ;;
193
+ *) npos=$((npos + 1)) ;;
194
+ esac
195
+ done
196
+
197
+ if [[ -n $flag ]] && ${fn}_valued "$cmd:$flag"; then
198
+ COMPREPLY=($(compgen -W "$(${fn}_choices "$cmd:$flag")" -- "$cur"))
199
+ elif [[ $cur == -* || -n $flag ]]; then
200
+ COMPREPLY=($(compgen -W "$(${fn}_flags "$cmd")" -- "$cur"))
201
+ else
202
+ COMPREPLY=($(compgen -W "$(${fn}_choices "$cmd:$((npos + 1))")" -- "$cur"))
203
+ fi
204
+ return 0
205
+ }
206
+
207
+ complete -F ${fn} ${bin}
208
+ `;
209
+ }
210
+ var zshQ = (s) => `'${s.replace(/'/g, "'\\''")}'`;
211
+ var zshEntry = (name, desc = "") => zshQ(`${name.replace(/:/g, "\\:")}:${desc}`);
212
+ function zshCompletions(m, bin, fn) {
213
+ const entries = (args) => args.map((a) => zshEntry(a.name, a.description)).join(" ");
214
+ const flagCases = m.commands.map((c) => ` (${c.name}) reply=(${entries(m.flags.get(c.name) ?? [])}) ;;`).join(`
215
+ `);
216
+ const choiceCases = m.choices.map(([k, v]) => ` (${k}) reply=(${v.join(" ")}) ;;`).join(`
217
+ `);
218
+ const commands = m.commands.map((c) => ` ${zshEntry(c.name, c.summary)}`).join(`
219
+ `);
220
+ return `#compdef ${bin}
221
+ ${header("zsh", bin)}
222
+ # Load it with: eval "$(${bin} completions zsh)" (after compinit, e.g. in ~/.zshrc)
223
+ # or save it as \`${fn}\` in a directory on $fpath.
224
+
225
+ ${fn}_valued() {
226
+ case $1 in
227
+ (${m.valued.join("|")}) return 0 ;;
228
+ esac
229
+ return 1
230
+ }
231
+
232
+ ${fn}_choices() {
233
+ reply=()
234
+ case $1 in
235
+ ${choiceCases}
236
+ esac
237
+ }
238
+
239
+ ${fn}_flags() {
240
+ reply=()
241
+ case $1 in
242
+ ${flagCases}
243
+ esac
244
+ }
245
+
246
+ ${fn}() {
247
+ local cur=$words[CURRENT] cmd=$words[2] npos=0 flag="" i w
248
+ local -a reply commands
249
+ commands=(
250
+ ${commands}
251
+ )
252
+
253
+ if (( CURRENT == 2 )); then
254
+ if [[ $cur == -* ]]; then
255
+ reply=(${entries(m.leading)})
256
+ _describe -t options option reply
257
+ else
258
+ _describe -t commands '${bin} command' commands
259
+ fi
260
+ return
261
+ fi
262
+
263
+ # Only the first word is the command; after a leading flag nothing runs a command.
264
+ [[ $cmd == -* ]] && return 1
265
+
266
+ # Count positionals the way parseFlags reads them: a bare --flag takes the next word
267
+ # as its value unless that word is another --flag.
268
+ for (( i = 3; i < CURRENT; i++ )); do
269
+ w=$words[i]
270
+ if [[ -n $flag && $w != --* ]]; then
271
+ flag=""
272
+ continue
273
+ fi
274
+ flag=""
275
+ case $w in
276
+ (--*=*) ;;
277
+ (--*) flag=$w ;;
278
+ (*) npos=$((npos + 1)) ;;
279
+ esac
280
+ done
281
+
282
+ if [[ $cur == --*=* ]]; then
283
+ flag=\${cur%%=*}
284
+ ${fn}_valued "$cmd:$flag" || return 1
285
+ compset -P '*='
286
+ fi
287
+
288
+ if [[ -n $flag ]] && ${fn}_valued "$cmd:$flag"; then
289
+ ${fn}_choices "$cmd:$flag"
290
+ if (( $#reply )); then compadd -a reply; else _message value; fi
291
+ elif [[ $cur == -* || -n $flag ]]; then
292
+ ${fn}_flags "$cmd"
293
+ _describe -t options option reply
294
+ else
295
+ ${fn}_choices "$cmd:$((npos + 1))"
296
+ (( $#reply )) && compadd -a reply
297
+ fi
298
+ }
299
+
300
+ if [[ $zsh_eval_context[-1] == loadautofunc ]]; then
301
+ ${fn} "$@"
302
+ else
303
+ compdef ${fn} ${bin}
304
+ fi
305
+ `;
306
+ }
307
+ var fishQ = (s) => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
308
+ function fishCompletions(m, bin, fn) {
309
+ const c = `complete -c ${bin}`;
310
+ const flagLine = (cond, a, cmd) => {
311
+ const parts = [c, "-n", fishQ(cond), "-l", a.name.slice(2)];
312
+ if (takesValue(a)) {
313
+ const ch = m.choices.find(([k]) => k === `${cmd}:${a.name}`)?.[1];
314
+ parts.push(ch ? `-xa ${fishQ(ch.join(" "))}` : "-x");
315
+ }
316
+ if (a.description)
317
+ parts.push("-d", fishQ(a.description));
318
+ return parts.join(" ");
319
+ };
320
+ const lines = [];
321
+ for (const cmd of m.commands) {
322
+ lines.push(`${c} -n ${fn}_at_command -a ${cmd.name} -d ${fishQ(cmd.summary)}`);
323
+ }
324
+ for (const a of m.leading)
325
+ lines.push(flagLine(`${fn}_at_command`, a));
326
+ for (const cmd of m.commands) {
327
+ lines.push("");
328
+ for (const [key, values] of m.choices) {
329
+ const [name, pos] = key.split(":");
330
+ if (name !== cmd.name || pos?.startsWith("--"))
331
+ continue;
332
+ lines.push(`${c} -n '${fn}_at_positional ${cmd.name} ${pos}' -a ${fishQ(values.join(" "))}`);
333
+ }
334
+ for (const a of m.flags.get(cmd.name) ?? [])
335
+ lines.push(flagLine(`${fn}_in ${cmd.name}`, a, cmd.name));
336
+ }
337
+ return `${header("fish", bin)}
338
+ # Load it with: ${bin} completions fish | source (e.g. in ~/.config/fish/config.fish)
339
+ # or save it as ~/.config/fish/completions/${bin}.fish
340
+
341
+ # Nothing typed after the binary yet: the next word is the command.
342
+ function ${fn}_at_command
343
+ test (count (commandline -opc)) -eq 1
344
+ end
345
+
346
+ # The command (the first word, and only the first) is $argv[1].
347
+ function ${fn}_in
348
+ set -l t (commandline -opc)
349
+ test "$t[2]" = "$argv[1]"
350
+ end
351
+
352
+ # The word being completed is positional $argv[2] of command $argv[1]. Positionals
353
+ # are counted the way parseFlags reads them: a bare --flag takes the next word as its
354
+ # value unless that word is another --flag.
355
+ function ${fn}_at_positional
356
+ set -l t (commandline -opc)
357
+ test "$t[2]" = "$argv[1]"; or return 1
358
+ set -l npos 0
359
+ set -l flag 0
360
+ for w in $t[3..-1]
361
+ if test $flag = 1; and not string match -q -- '--*' $w
362
+ set flag 0
363
+ continue
364
+ end
365
+ set flag 0
366
+ switch $w
367
+ case '--*=*'
368
+ case '--*'
369
+ set flag 1
370
+ case '*'
371
+ set npos (math $npos + 1)
372
+ end
373
+ end
374
+ test $flag = 0; and test $npos -eq (math $argv[2] - 1)
375
+ end
376
+
377
+ ${c} -f
378
+ ${lines.join(`
379
+ `)}
380
+ `;
381
+ }
382
+
2
383
  // packages/shared/src/model/cli.ts
3
384
  var WEBAPI = {
4
385
  name: "--webapi",
@@ -318,95 +699,24 @@ var CLI_SPEC = {
318
699
  { name: "--dry-run", description: "with `install`: show the change without writing" }
319
700
  ],
320
701
  examples: ["statusline install", "statusline status"]
702
+ },
703
+ {
704
+ name: "completions",
705
+ group: "admin",
706
+ summary: "Print a shell completion script to eval or source from your shell's rc",
707
+ args: [
708
+ {
709
+ name: "shell",
710
+ required: true,
711
+ choices: COMPLETION_SHELLS,
712
+ description: "the shell to complete for"
713
+ }
714
+ ],
715
+ examples: ["completions bash", "completions fish | source"]
321
716
  }
322
717
  ]
323
718
  };
324
719
 
325
- // packages/shared/src/model/cli-project.ts
326
- var isFlag = (a) => a.name.startsWith("--");
327
- function cliArgType(a) {
328
- if (a.type)
329
- return a.type;
330
- if (a.choices)
331
- return "string";
332
- if (typeof a.default === "number")
333
- return "number";
334
- if (typeof a.default === "string")
335
- return "string";
336
- return isFlag(a) ? "boolean" : "string";
337
- }
338
- function cliCommandsByGroup(spec) {
339
- return spec.groups.map((g) => ({ ...g, commands: spec.commands.filter((c) => c.group === g.key) })).filter((g) => g.commands.length > 0);
340
- }
341
- function cliUsage(cmd) {
342
- const parts = (cmd.args ?? []).filter((a) => !isFlag(a)).map((a) => a.required ? `<${a.name}>` : `[${a.name}]`);
343
- const hasFlags = (cmd.args ?? []).some(isFlag);
344
- return [cmd.name, ...parts, ...hasFlags ? ["[options]"] : []].join(" ");
345
- }
346
- function cliArgHint(a) {
347
- const bits = [];
348
- if (a.choices)
349
- bits.push(a.choices.join(" | "));
350
- if (a.default !== undefined && a.default !== false)
351
- bits.push(`default ${a.default}`);
352
- return bits.length ? `(${bits.join("; ")})` : "";
353
- }
354
- function cliArgLabel(a) {
355
- if (!isFlag(a))
356
- return a.name;
357
- switch (cliArgType(a)) {
358
- case "number":
359
- return `${a.name} <n>`;
360
- case "string":
361
- return `${a.name} <value>`;
362
- default:
363
- return a.name;
364
- }
365
- }
366
- function validateCliArgs(spec, cmd, parsed) {
367
- const errors = [];
368
- const args = cmd.args ?? [];
369
- const known = new Map;
370
- for (const a of [...spec.globalArgs, ...args])
371
- if (isFlag(a))
372
- known.set(a.name.slice(2), a);
373
- for (const [key, value] of Object.entries(parsed.options)) {
374
- const def = known.get(key);
375
- if (!def) {
376
- errors.push(`unknown option --${key}`);
377
- continue;
378
- }
379
- errors.push(...checkValue(def, value));
380
- }
381
- const positionals = args.filter((a) => !isFlag(a));
382
- positionals.forEach((def, i) => {
383
- const value = parsed.positionals[i];
384
- if (value === undefined) {
385
- if (def.required)
386
- errors.push(`missing required argument <${def.name}>`);
387
- return;
388
- }
389
- errors.push(...checkValue(def, value));
390
- });
391
- return errors;
392
- }
393
- function checkValue(def, value) {
394
- const type = cliArgType(def);
395
- const label = isFlag(def) ? def.name : `<${def.name}>`;
396
- if (typeof value === "boolean") {
397
- return type === "boolean" ? [] : [`${label} needs a value`];
398
- }
399
- if (type === "boolean")
400
- return [`${label} takes no value (got "${value}")`];
401
- if (def.choices && !def.choices.includes(value)) {
402
- return [`${label} must be one of ${def.choices.join(" | ")} (got "${value}")`];
403
- }
404
- if (type === "number" && !/^-?\d+(\.\d+)?$/.test(value)) {
405
- return [`${label} must be a number (got "${value}")`];
406
- }
407
- return [];
408
- }
409
-
410
720
  // packages/shared/src/couch-url.ts
411
721
  var DEFAULT_COUCHDB_HOST = "127.0.0.1";
412
722
  var DEFAULT_COUCHDB_PORT = "7652";
@@ -2854,6 +3164,14 @@ async function runBackfill(argv) {
2854
3164
  return failed > 0 ? 1 : 0;
2855
3165
  }
2856
3166
 
3167
+ // packages/cli/src/commands/completions.ts
3168
+ var BIN = "claude-transcripts";
3169
+ async function runCompletions(argv) {
3170
+ const shell = parseFlags(argv).positionals[0];
3171
+ process.stdout.write(toCompletions(CLI_SPEC, shell, BIN));
3172
+ return 0;
3173
+ }
3174
+
2857
3175
  // packages/cli/src/commands/doctor.ts
2858
3176
  import { hostname as hostname2 } from "os";
2859
3177
  function buildTranscript(sessionId, cwd, ts) {
@@ -3567,13 +3885,189 @@ async function runExport(argv) {
3567
3885
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
3568
3886
  import { dirname as dirname2 } from "path";
3569
3887
 
3888
+ // packages/cli/src/lib/statusline.ts
3889
+ import { statSync } from "fs";
3890
+ var STALL_AFTER_MS = 60000;
3891
+ var HOOK_SILENT_AFTER_MS = 5 * 60000;
3892
+ function transcriptMtimeMs(path) {
3893
+ if (typeof path !== "string" || !path)
3894
+ return null;
3895
+ try {
3896
+ return statSync(path).mtimeMs;
3897
+ } catch {
3898
+ return null;
3899
+ }
3900
+ }
3901
+ function hookSilent(lastActivityMs, transcriptMs) {
3902
+ if (!transcriptMs || lastActivityMs <= 0)
3903
+ return false;
3904
+ return transcriptMs - lastActivityMs > HOOK_SILENT_AFTER_MS;
3905
+ }
3906
+ function lastWrite(ms, now) {
3907
+ return ms > 0 ? `last write ${ago(ms, now)}` : "no write landed";
3908
+ }
3909
+ function ago(ms, now) {
3910
+ const s = Math.max(0, Math.round((now - ms) / 1000));
3911
+ if (s < 60)
3912
+ return `${s}s ago`;
3913
+ const m = Math.round(s / 60);
3914
+ if (m < 60)
3915
+ return `${m}m ago`;
3916
+ return `${Math.round(m / 60)}h ago`;
3917
+ }
3918
+ function versionLabel(version = VERSION) {
3919
+ return `ct@${version === DEV_VERSION ? "dev" : version}`;
3920
+ }
3921
+ function whereLabel(t) {
3922
+ const host = t.couchUrl.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
3923
+ return `${t.sessionsDb}@${host}`;
3924
+ }
3925
+ function storeState(s, now) {
3926
+ if (s.lastFailureMs > s.lastWriteMs && now - s.lastWriteMs > STALL_AFTER_MS)
3927
+ return "failing";
3928
+ return s.lastWriteMs > 0 ? "healthy" : "idle";
3929
+ }
3930
+ var GLYPH = { rec: "\u25CF", stalled: "\u25D0", ready: "\u25CC", off: "\u25CB" };
3931
+ function sessionLink(webapiUrl, sessionId) {
3932
+ return webapiUrl ? `${webapiUrl.replace(/\/$/, "")}/app/sessions/${sessionId}` : null;
3933
+ }
3934
+ function renderStatusline(state, now = Date.now(), opts = {}) {
3935
+ return formatStatusline(statuslineLine(state, now), opts);
3936
+ }
3937
+ function statuslineLine(state, now = Date.now()) {
3938
+ const version = versionLabel(state.version);
3939
+ if (!state.configured)
3940
+ return { state: "off", version, note: "no instance configured" };
3941
+ const t = state.targets;
3942
+ if (!t)
3943
+ return { state: "off", version, note: "not recording this session" };
3944
+ const c = state.counts;
3945
+ const counted = c ? { events: c.events, tools: Object.values(c.tools).reduce((a, b) => a + b, 0) } : { events: 0 };
3946
+ const line = (state, rest) => ({
3947
+ state,
3948
+ version,
3949
+ ...counted,
3950
+ ...rest
3951
+ });
3952
+ const stores = t.stores?.length ? t.stores : null;
3953
+ if (!stores) {
3954
+ const where = whereLabel(t);
3955
+ const age = lastWrite(t.lastWriteMs, now);
3956
+ if (hookSilent(Math.max(t.lastWriteMs, t.lastFailureMs), state.transcriptMtimeMs)) {
3957
+ return line("stalled", { note: "hook silent", age, where });
3958
+ }
3959
+ if (t.lastFailureMs > t.lastWriteMs && now - t.lastWriteMs > STALL_AFTER_MS) {
3960
+ return line("stalled", { age, where });
3961
+ }
3962
+ if (t.lastWriteMs <= 0)
3963
+ return line("ready", { age: "no write yet", where });
3964
+ return line("rec", { age: ago(t.lastWriteMs, now), where });
3965
+ }
3966
+ const direct = stores.find((s) => s.kind === "direct") ?? stores[0];
3967
+ const headline = direct ?? { label: whereLabel(t), lastWriteMs: 0, lastFailureMs: 0 };
3968
+ const lastActivity = Math.max(...stores.flatMap((s) => [s.lastWriteMs, s.lastFailureMs]));
3969
+ if (hookSilent(lastActivity, state.transcriptMtimeMs)) {
3970
+ const newest = stores.reduce((a, b) => b.lastWriteMs > a.lastWriteMs ? b : a, headline);
3971
+ return line("stalled", {
3972
+ note: "hook silent",
3973
+ age: lastWrite(newest.lastWriteMs, now),
3974
+ where: newest.label
3975
+ });
3976
+ }
3977
+ if (direct && storeState(direct, now) === "healthy") {
3978
+ return line("rec", { age: ago(direct.lastWriteMs, now), where: direct.label });
3979
+ }
3980
+ const healthyMirrors = stores.filter((s) => s !== direct && storeState(s, now) === "healthy");
3981
+ const first = healthyMirrors[0];
3982
+ if (first) {
3983
+ return line("rec", {
3984
+ qualifier: "(mirror)",
3985
+ age: ago(first.lastWriteMs, now),
3986
+ where: first.label,
3987
+ ...healthyMirrors.length > 1 ? { more: `+${healthyMirrors.length - 1}` } : {}
3988
+ });
3989
+ }
3990
+ if (stores.some((s) => storeState(s, now) === "failing")) {
3991
+ const best = Math.max(...stores.map((s) => s.lastWriteMs));
3992
+ return line("stalled", { age: lastWrite(best, now), where: headline.label });
3993
+ }
3994
+ return line("ready", { age: "no write yet", where: headline.label });
3995
+ }
3996
+ var SGR = {
3997
+ reset: "\x1B[0m",
3998
+ dim: "\x1B[2m",
3999
+ green: "\x1B[32m",
4000
+ yellow: "\x1B[33m",
4001
+ cyan: "\x1B[36m"
4002
+ };
4003
+ var STATE_COLOR = {
4004
+ rec: SGR.green,
4005
+ stalled: SGR.yellow,
4006
+ ready: SGR.cyan,
4007
+ off: SGR.dim
4008
+ };
4009
+ var SHRINK = [
4010
+ ({ tools: _, ...l }) => l,
4011
+ (l) => l.where ? { ...l, where: shortWhere(l.where) } : l,
4012
+ ({ events: _, ...l }) => l,
4013
+ ({ age: _, ...l }) => l,
4014
+ ({ where: _, more: __, ...l }) => l
4015
+ ];
4016
+ function shortWhere(where) {
4017
+ const at = where.indexOf("@");
4018
+ if (at > 0)
4019
+ return where.slice(0, at);
4020
+ const host = where.startsWith("[") ? where.slice(0, where.indexOf("]") + 1) : where.split(":")[0];
4021
+ if (!host || host.startsWith("[") || /^\d+(\.\d+){3}$/.test(host))
4022
+ return host || where;
4023
+ return host.split(".")[0] || host;
4024
+ }
4025
+ function formatStatusline(line, opts = {}) {
4026
+ let l = line;
4027
+ if (opts.columns && opts.columns > 0) {
4028
+ for (const shrink of SHRINK) {
4029
+ if ([...draw(l, {})].length <= opts.columns)
4030
+ break;
4031
+ l = shrink(l);
4032
+ }
4033
+ }
4034
+ return draw(l, opts);
4035
+ }
4036
+ function draw(l, opts) {
4037
+ const paint = (code, s) => opts.color ? `${code}${s}${SGR.reset}` : s;
4038
+ const dim = (s) => paint(SGR.dim, s);
4039
+ const color = STATE_COLOR[l.state];
4040
+ const head = [
4041
+ paint(color, `${GLYPH[l.state]}`),
4042
+ l.version,
4043
+ paint(color, l.state),
4044
+ ...l.qualifier ? [paint(SGR.yellow, l.qualifier)] : []
4045
+ ].join(" ");
4046
+ const counts = l.events === undefined ? undefined : l.tools === undefined ? `${l.events} ev` : `${l.events} ev \xB7 ${l.tools} tools`;
4047
+ const details = [
4048
+ l.note && (l.note === "hook silent" ? paint(SGR.yellow, l.note) : l.note),
4049
+ counts && dim(counts),
4050
+ l.age && dim(l.age)
4051
+ ].filter(Boolean);
4052
+ const sep = dim(" \xB7 ");
4053
+ let out = [head, ...details].join(sep);
4054
+ if (l.where) {
4055
+ const label = opts.link ? osc8(opts.link, l.where) : l.where;
4056
+ out += `${dim(" \u2192")} ${label}${l.more ? ` ${l.more}` : ""}`;
4057
+ }
4058
+ return out;
4059
+ }
4060
+ function osc8(url, text) {
4061
+ return `\x1B]8;;${url}\x1B\\${text}\x1B]8;;\x1B\\`;
4062
+ }
4063
+
3570
4064
  // packages/cli/src/hook/runtime.ts
3571
4065
  import {
3572
4066
  closeSync,
3573
4067
  openSync,
3574
4068
  readFileSync as readFileSync4,
3575
4069
  readSync,
3576
- statSync,
4070
+ statSync as statSync2,
3577
4071
  unlinkSync,
3578
4072
  writeFileSync as writeFileSync2
3579
4073
  } from "fs";
@@ -3960,7 +4454,7 @@ function makeChunkState(sessionId) {
3960
4454
  return true;
3961
4455
  } catch {
3962
4456
  try {
3963
- if (Date.now() - statSync(lockFile).mtimeMs > STALE_LOCK_MS) {
4457
+ if (Date.now() - statSync2(lockFile).mtimeMs > STALE_LOCK_MS) {
3964
4458
  unlinkSync(lockFile);
3965
4459
  closeSync(openSync(lockFile, "wx"));
3966
4460
  return true;
@@ -3975,7 +4469,7 @@ function makeChunkState(sessionId) {
3975
4469
  } catch {}
3976
4470
  },
3977
4471
  readTail(path, offset) {
3978
- const size = statSync(path).size;
4472
+ const size = statSync2(path).size;
3979
4473
  if (size <= offset)
3980
4474
  return "";
3981
4475
  const fd = openSync(path, "r");
@@ -4051,20 +4545,19 @@ function commonFields(ctx) {
4051
4545
  }
4052
4546
 
4053
4547
  // packages/cli/src/hook/announce.ts
4054
- function sessionLink(webapiUrl, sessionId) {
4055
- return webapiUrl ? `${webapiUrl.replace(/\/$/, "")}/app/sessions/${sessionId}` : null;
4056
- }
4057
4548
  function recordingBanner(targets, sessionId) {
4058
- const where = [`couchdb://${targets.couchUrl.replace(/^https?:\/\//, "")}/${targets.sessionsDb}`];
4549
+ const also = [];
4059
4550
  if (targets.bucket)
4060
- where.push(`s3://${targets.bucket}`);
4551
+ also.push(`s3://${targets.bucket}`);
4061
4552
  if (targets.mirrors.length) {
4062
- where.push(`mirrors: ${targets.mirrors.map(hostOf).join(", ")}`);
4553
+ also.push(`mirrors: ${targets.mirrors.map(hostOf).join(", ")}`);
4063
4554
  }
4555
+ const head = `\u25CF Claude Transcripts recording \u2192 ${whereLabel(targets)}${also.length ? ` (+ ${also.join(", ")})` : ""}`;
4064
4556
  const link = sessionLink(targets.webapiUrl, sessionId);
4065
- return `Claude Transcripts \u2014 recording to ${where.join(" + ")}${link ? ` \xB7 ${link}` : ""}`;
4557
+ return link ? `${head}
4558
+ ${link}` : head;
4066
4559
  }
4067
- var NOT_RECORDING_BANNER = "Claude Transcripts \u2014 not recording (no instance configured). Run `claude-transcripts install`.";
4560
+ var NOT_RECORDING_BANNER = "\u25CB Claude Transcripts not recording \u2014 no instance configured. Run `claude-transcripts install`.";
4068
4561
  function sessionStartEnvelope(out) {
4069
4562
  if (!out.systemMessage && !out.additionalContext)
4070
4563
  return null;
@@ -4088,7 +4581,7 @@ function emitSessionStart(out) {
4088
4581
  }
4089
4582
 
4090
4583
  // packages/cli/src/hook/handlers.ts
4091
- import { readFileSync as readFileSync5, statSync as statSync2 } from "fs";
4584
+ import { readFileSync as readFileSync5, statSync as statSync3 } from "fs";
4092
4585
 
4093
4586
  // packages/cli/src/hook/recall.ts
4094
4587
  var FACTS_TIMEOUT_MS = 2000;
@@ -4162,7 +4655,7 @@ var seedSessionStart = async (ctx) => {
4162
4655
  };
4163
4656
  function fileSize(path) {
4164
4657
  try {
4165
- return path ? statSync2(path).size : null;
4658
+ return path ? statSync3(path).size : null;
4166
4659
  } catch {
4167
4660
  return null;
4168
4661
  }
@@ -4671,7 +5164,7 @@ async function runHook(argv) {
4671
5164
  }
4672
5165
 
4673
5166
  // packages/cli/src/commands/import.ts
4674
- import { existsSync as existsSync4, statSync as statSync3 } from "fs";
5167
+ import { existsSync as existsSync4, statSync as statSync4 } from "fs";
4675
5168
  import { join as join7 } from "path";
4676
5169
  var BATCH = 500;
4677
5170
  function num(n) {
@@ -4890,7 +5383,7 @@ async function runImport(argv) {
4890
5383
  const path = join7(dir, rel);
4891
5384
  await putStream(`/api/ingest/${sessionId}/transcript`, path, "application/x-ndjson");
4892
5385
  blobs++;
4893
- blobBytes += statSync3(path).size;
5386
+ blobBytes += statSync4(path).size;
4894
5387
  }
4895
5388
  console.log(` blobs ${blobs} restored, ${formatBytes(blobBytes)}${skipped ? ` (${skipped} skipped)` : ""}`);
4896
5389
  } else {
@@ -4912,6 +5405,7 @@ async function runImport(argv) {
4912
5405
 
4913
5406
  // packages/cli/src/commands/install.ts
4914
5407
  import { existsSync as existsSync8, readFileSync as readFileSync12 } from "fs";
5408
+ import { join as join9 } from "path";
4915
5409
 
4916
5410
  // packages/cli/src/lib/assets.ts
4917
5411
  import {
@@ -5515,102 +6009,31 @@ async function provisionGarage(env, bucket, instanceEnvPath) {
5515
6009
  S3_SECRET_KEY: secretKey
5516
6010
  });
5517
6011
  steps.push({ name: "garage:key", ok: true, detail: `created ${accessKey}` });
6012
+ const stale = await removeStaleKeys(api, accessKey);
6013
+ if (stale)
6014
+ steps.push(stale);
5518
6015
  return { steps, env: next };
5519
6016
  }
5520
-
5521
- // packages/cli/src/commands/statusline.ts
5522
- import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
5523
- import { dirname as dirname6 } from "path";
5524
-
5525
- // packages/cli/src/lib/statusline.ts
5526
- import { statSync as statSync4 } from "fs";
5527
- var STALL_AFTER_MS = 60000;
5528
- var HOOK_SILENT_AFTER_MS = 5 * 60000;
5529
- function transcriptMtimeMs(path) {
5530
- if (typeof path !== "string" || !path)
5531
- return null;
5532
- try {
5533
- return statSync4(path).mtimeMs;
5534
- } catch {
5535
- return null;
6017
+ async function removeStaleKeys(api, keep) {
6018
+ const list = await api("GET", "/v2/ListKeys");
6019
+ if (!list.ok || !Array.isArray(list.json)) {
6020
+ return { name: "garage:stale-keys", ok: false, detail: `list failed (HTTP ${list.status})` };
5536
6021
  }
5537
- }
5538
- function hookSilent(lastActivityMs, transcriptMs) {
5539
- if (!transcriptMs || lastActivityMs <= 0)
5540
- return false;
5541
- return transcriptMs - lastActivityMs > HOOK_SILENT_AFTER_MS;
5542
- }
5543
- function lastWrite(ms, now) {
5544
- return ms > 0 ? `last write ${ago(ms, now)}` : "no write landed";
5545
- }
5546
- function ago(ms, now) {
5547
- const s = Math.max(0, Math.round((now - ms) / 1000));
5548
- if (s < 60)
5549
- return `${s}s ago`;
5550
- const m = Math.round(s / 60);
5551
- if (m < 60)
5552
- return `${m}m ago`;
5553
- return `${Math.round(m / 60)}h ago`;
5554
- }
5555
- function versionLabel(version = VERSION) {
5556
- return `ct@${version === DEV_VERSION ? "dev" : version}`;
5557
- }
5558
- function whereLabel(t) {
5559
- const host = t.couchUrl.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
5560
- return `${t.sessionsDb}@${host}`;
5561
- }
5562
- function storeState(s, now) {
5563
- if (s.lastFailureMs > s.lastWriteMs && now - s.lastWriteMs > STALL_AFTER_MS)
5564
- return "failing";
5565
- return s.lastWriteMs > 0 ? "healthy" : "idle";
5566
- }
5567
- function renderStatusline(state, now = Date.now()) {
5568
- const ct = versionLabel(state.version);
5569
- if (!state.configured)
5570
- return `\u25CB ${ct} off \xB7 no instance configured`;
5571
- const t = state.targets;
5572
- if (!t)
5573
- return `\u25CB ${ct} off \xB7 not recording this session`;
5574
- const c = state.counts;
5575
- const toolCalls = c ? Object.values(c.tools).reduce((a, b) => a + b, 0) : 0;
5576
- const counts = c ? `${c.events} ev \xB7 ${toolCalls} tools` : "0 ev";
5577
- const stores = t.stores?.length ? t.stores : null;
5578
- if (!stores) {
5579
- const last = lastWrite(t.lastWriteMs, now);
5580
- if (hookSilent(Math.max(t.lastWriteMs, t.lastFailureMs), state.transcriptMtimeMs)) {
5581
- return `\u25D0 ${ct} stalled \xB7 hook silent \xB7 ${counts} \xB7 ${last} \u2192 ${whereLabel(t)}`;
5582
- }
5583
- if (t.lastFailureMs > t.lastWriteMs && now - t.lastWriteMs > STALL_AFTER_MS) {
5584
- return `\u25D0 ${ct} stalled \xB7 ${counts} \xB7 ${last} \u2192 ${whereLabel(t)}`;
5585
- }
5586
- if (t.lastWriteMs <= 0)
5587
- return `\u25CC ${ct} ready \xB7 ${counts} \xB7 no write yet \u2192 ${whereLabel(t)}`;
5588
- return `\u25CF ${ct} rec \xB7 ${counts} \xB7 ${ago(t.lastWriteMs, now)} \u2192 ${whereLabel(t)}`;
5589
- }
5590
- const direct = stores.find((s) => s.kind === "direct") ?? stores[0];
5591
- const headline = direct ?? { label: whereLabel(t), lastWriteMs: 0, lastFailureMs: 0 };
5592
- const lastActivity = Math.max(...stores.flatMap((s) => [s.lastWriteMs, s.lastFailureMs]));
5593
- if (hookSilent(lastActivity, state.transcriptMtimeMs)) {
5594
- const newest = stores.reduce((a, b) => b.lastWriteMs > a.lastWriteMs ? b : a, headline);
5595
- return `\u25D0 ${ct} stalled \xB7 hook silent \xB7 ${counts} \xB7 ${lastWrite(newest.lastWriteMs, now)} \u2192 ${newest.label}`;
5596
- }
5597
- if (direct && storeState(direct, now) === "healthy") {
5598
- return `\u25CF ${ct} rec \xB7 ${counts} \xB7 ${ago(direct.lastWriteMs, now)} \u2192 ${direct.label}`;
5599
- }
5600
- const healthyMirrors = stores.filter((s) => s !== direct && storeState(s, now) === "healthy");
5601
- const first = healthyMirrors[0];
5602
- if (first) {
5603
- const more = healthyMirrors.length > 1 ? ` +${healthyMirrors.length - 1}` : "";
5604
- return `\u25CF ${ct} rec (mirror) \xB7 ${counts} \xB7 ${ago(first.lastWriteMs, now)} \u2192 ${first.label}${more}`;
5605
- }
5606
- if (stores.some((s) => storeState(s, now) === "failing")) {
5607
- const best = Math.max(...stores.map((s) => s.lastWriteMs));
5608
- return `\u25D0 ${ct} stalled \xB7 ${counts} \xB7 ${lastWrite(best, now)} \u2192 ${headline.label}`;
6022
+ const stale = list.json.filter((k) => k?.name === KEY_NAME && typeof k.id === "string" && k.id !== keep).map((k) => k.id);
6023
+ if (stale.length === 0)
6024
+ return null;
6025
+ const failed = [];
6026
+ for (const id of stale) {
6027
+ const del = await api("POST", `/v2/DeleteKey?id=${encodeURIComponent(id)}`);
6028
+ if (!del.ok)
6029
+ failed.push(`${id} (HTTP ${del.status})`);
5609
6030
  }
5610
- return `\u25CC ${ct} ready \xB7 ${counts} \xB7 no write yet \u2192 ${headline.label}`;
6031
+ return failed.length ? { name: "garage:stale-keys", ok: false, detail: `could not delete ${failed.join(", ")}` } : { name: "garage:stale-keys", ok: true, detail: `deleted ${stale.join(", ")}` };
5611
6032
  }
5612
6033
 
5613
6034
  // packages/cli/src/commands/statusline.ts
6035
+ import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
6036
+ import { dirname as dirname6 } from "path";
5614
6037
  function readSettings2(path) {
5615
6038
  try {
5616
6039
  return JSON.parse(readFileSync11(path, "utf8"));
@@ -5639,6 +6062,10 @@ async function render() {
5639
6062
  targets,
5640
6063
  counts: id ? makeCounts(id).read() : null,
5641
6064
  transcriptMtimeMs: targets ? transcriptMtimeMs(input.transcript_path) : null
6065
+ }, Date.now(), {
6066
+ color: !process.env.NO_COLOR,
6067
+ link: targets && id ? sessionLink(targets.webapiUrl, id) : null,
6068
+ columns: Number(process.env.COLUMNS) || undefined
5642
6069
  });
5643
6070
  process.stdout.write(`${line}
5644
6071
  `);
@@ -5922,7 +6349,7 @@ async function runInstall(argv) {
5922
6349
  install: done.`);
5923
6350
  console.log(` UI http://127.0.0.1:${env.WEBAPI_PORT}/app`);
5924
6351
  console.log(` API http://127.0.0.1:${env.WEBAPI_PORT}/api/docs`);
5925
- console.log(` data docker volumes (survive uninstall unless --purge)`);
6352
+ console.log(` data ${join9(paths.deployDir, "data")} (survives uninstall unless --purge)`);
5926
6353
  console.log(" remove claude-transcripts uninstall");
5927
6354
  return 0;
5928
6355
  }
@@ -6336,15 +6763,15 @@ async function runSessions(argv) {
6336
6763
  // packages/cli/src/commands/setup.ts
6337
6764
  import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
6338
6765
  import { homedir as homedir3 } from "os";
6339
- import { dirname as dirname7, join as join9 } from "path";
6340
- var REPO_ROOT = join9(import.meta.dir, "..", "..", "..", "..");
6341
- var HOOK_DIR = join9(REPO_ROOT, "hooks");
6342
- var HOOK_CONFIG_PATH = process.env.CT_HOOK_CONFIG ?? join9(homedir3(), ".config", "claude-transcripts", "config.json");
6766
+ import { dirname as dirname7, join as join10 } from "path";
6767
+ var REPO_ROOT = join10(import.meta.dir, "..", "..", "..", "..");
6768
+ var HOOK_DIR = join10(REPO_ROOT, "hooks");
6769
+ var HOOK_CONFIG_PATH = process.env.CT_HOOK_CONFIG ?? join10(homedir3(), ".config", "claude-transcripts", "config.json");
6343
6770
  var GLOBAL_SETTINGS = installPaths().claudeSettings;
6344
- var PROJECT_SETTINGS = join9(process.cwd(), ".claude", "settings.json");
6771
+ var PROJECT_SETTINGS = join10(process.cwd(), ".claude", "settings.json");
6345
6772
  function loadRepoConfig() {
6346
- const live = join9(REPO_ROOT, "config", "config.json");
6347
- const template = join9(REPO_ROOT, "config", "config.template.json");
6773
+ const live = join10(REPO_ROOT, "config", "config.json");
6774
+ const template = join10(REPO_ROOT, "config", "config.template.json");
6348
6775
  return JSON.parse(readFileSync13(existsSync10(live) ? live : template, "utf8"));
6349
6776
  }
6350
6777
  function couchHeaders(auth) {
@@ -6404,7 +6831,7 @@ function readSettings3(path) {
6404
6831
  function globalRegistrationBlockedBy(settingsPath = GLOBAL_SETTINGS) {
6405
6832
  return pluginRegistration(settingsPath, readSettings3(settingsPath));
6406
6833
  }
6407
- var DISPATCH_PATH = join9(HOOK_DIR, "scripts", "dispatch.ts");
6834
+ var DISPATCH_PATH = join10(HOOK_DIR, "scripts", "dispatch.ts");
6408
6835
  function dispatchCommand() {
6409
6836
  return hookLaunch(`bun run "${DISPATCH_PATH}"`);
6410
6837
  }
@@ -6415,7 +6842,7 @@ function isRegistered(path) {
6415
6842
  }
6416
6843
  function registerGlobal(dryRun) {
6417
6844
  const cmd = dispatchCommand();
6418
- const generated = JSON.parse(readFileSync13(join9(HOOK_DIR, "hooks", "hooks.json"), "utf8"));
6845
+ const generated = JSON.parse(readFileSync13(join10(HOOK_DIR, "hooks", "hooks.json"), "utf8"));
6419
6846
  const settings = readSettings3(GLOBAL_SETTINGS);
6420
6847
  const hooks = settings.hooks ?? {};
6421
6848
  for (const [event, groups] of Object.entries(generated.hooks)) {
@@ -6655,12 +7082,30 @@ async function runTurns(argv) {
6655
7082
  }
6656
7083
 
6657
7084
  // packages/cli/src/commands/uninstall.ts
6658
- import { existsSync as existsSync12, rmSync } from "fs";
7085
+ import { existsSync as existsSync12, readdirSync, rmSync } from "fs";
7086
+ import { join as join11 } from "path";
7087
+ var DATA_SUBDIR = "data";
7088
+ function removeDeployDir(deployDir, purge) {
7089
+ if (purge) {
7090
+ rmSync(deployDir, { recursive: true, force: true });
7091
+ return;
7092
+ }
7093
+ let entries;
7094
+ try {
7095
+ entries = readdirSync(deployDir);
7096
+ } catch {
7097
+ return;
7098
+ }
7099
+ for (const entry of entries) {
7100
+ if (entry !== DATA_SUBDIR)
7101
+ rmSync(join11(deployDir, entry), { recursive: true, force: true });
7102
+ }
7103
+ }
6659
7104
  async function confirmPurge(auto) {
6660
7105
  if (auto)
6661
7106
  return true;
6662
7107
  process.stdout.write(`
6663
- uninstall: --purge DELETES ALL RECORDED HISTORY (CouchDB + S3 volumes).
7108
+ uninstall: --purge DELETES ALL RECORDED HISTORY (CouchDB + S3 data).
6664
7109
  ` + "Type 'delete my history' to confirm: ");
6665
7110
  const line = (await new Response(Bun.stdin.stream()).text()).trim();
6666
7111
  return line === "delete my history";
@@ -6673,10 +7118,11 @@ async function runUninstall(argv) {
6673
7118
  console.error("uninstall: not confirmed \u2014 nothing was removed.");
6674
7119
  return 1;
6675
7120
  }
6676
- console.log("uninstall: deregistering the hook");
7121
+ console.log("uninstall: deregistering the hook and the statusline");
6677
7122
  await runHook(["uninstall"]);
7123
+ await runStatusline(["uninstall"]);
6678
7124
  if (existsSync12(paths.instanceEnv)) {
6679
- console.log(`uninstall: stopping the stack${purge ? " and deleting volumes" : ""}`);
7125
+ console.log("uninstall: stopping the stack");
6680
7126
  try {
6681
7127
  const env = loadOrCreateInstanceEnv(paths.instanceEnv);
6682
7128
  await composeDown({ paths, env, app: true }, purge);
@@ -6684,17 +7130,28 @@ async function runUninstall(argv) {
6684
7130
  console.warn(`uninstall: could not stop the stack (continuing): ${err.message}`);
6685
7131
  }
6686
7132
  }
6687
- for (const target of [paths.hookConfig, paths.instanceEnv, paths.deployDir, paths.versionFile]) {
7133
+ const targets = [paths.instanceEnv, paths.versionFile, ...purge ? [paths.hookConfig] : []];
7134
+ for (const target of targets) {
6688
7135
  try {
6689
7136
  rmSync(target, { recursive: true, force: true });
6690
7137
  } catch {}
6691
7138
  }
7139
+ const dataDir = join11(paths.deployDir, DATA_SUBDIR);
7140
+ try {
7141
+ removeDeployDir(paths.deployDir, purge);
7142
+ } catch (err) {
7143
+ console.error(`uninstall: could not remove ${paths.deployDir}: ${err.message}`);
7144
+ if (purge)
7145
+ console.error(`uninstall: delete ${dataDir} by hand to finish the purge`);
7146
+ return 1;
7147
+ }
6692
7148
  console.log("uninstall: removed generated config and deployment assets");
6693
7149
  if (purge) {
6694
7150
  rmSync(paths.appConfig, { force: true });
6695
7151
  console.log("uninstall: purged history and app config");
6696
7152
  } else {
6697
- console.log(`uninstall: history kept in Docker volumes; ${paths.appConfig} left in place`);
7153
+ console.log(`uninstall: history kept in ${dataDir}`);
7154
+ console.log(`uninstall: ${paths.hookConfig} and ${paths.appConfig} left in place`);
6698
7155
  console.log("uninstall: re-run with --purge to delete the data too");
6699
7156
  }
6700
7157
  console.log(`uninstall: the binary itself is still at ${paths.binDir}/claude-transcripts`);
@@ -6718,7 +7175,8 @@ var COMMANDS = {
6718
7175
  doctor: runDoctor,
6719
7176
  sessions: runSessions,
6720
7177
  search: runSearch,
6721
- turns: runTurns
7178
+ turns: runTurns,
7179
+ completions: runCompletions
6722
7180
  };
6723
7181
 
6724
- export { CLI_SPEC, isFlag, cliCommandsByGroup, cliUsage, cliArgHint, cliArgLabel, validateCliArgs, parseFlags, VERSION, COMMANDS };
7182
+ export { isFlag, cliCommandsByGroup, cliUsage, cliArgHint, cliArgLabel, validateCliArgs, CLI_SPEC, parseFlags, VERSION, COMMANDS };
package/dist/cli.js CHANGED
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
  import {
4
- CLI_SPEC,
5
4
  cliUsage,
6
5
  validateCliArgs,
6
+ CLI_SPEC,
7
7
  parseFlags,
8
8
  VERSION,
9
9
  COMMANDS
10
- } from "./cli-d7a6svha.js";
10
+ } from "./cli-byvjee1b.js";
11
11
 
12
12
  // packages/cli/src/cli.tsx
13
13
  var [, , first, ...rest] = process.argv;
14
14
  var USAGE_ERROR = 2;
15
15
  async function showHelp(props, stream = process.stdout) {
16
- const { renderHelp } = await import("./app-vcj5svg7.js");
16
+ const { renderHelp } = await import("./app-2rqkxqp9.js");
17
17
  renderHelp(props, stream);
18
18
  }
19
19
  var argv = first === undefined ? [] : [first, ...rest];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-transcripts/cli",
3
- "version": "0.3.3",
3
+ "version": "0.5.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": [