@claude-transcripts/cli 0.3.3 → 0.4.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-fptk6bgx.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) {
@@ -4912,6 +5230,7 @@ async function runImport(argv) {
4912
5230
 
4913
5231
  // packages/cli/src/commands/install.ts
4914
5232
  import { existsSync as existsSync8, readFileSync as readFileSync12 } from "fs";
5233
+ import { join as join9 } from "path";
4915
5234
 
4916
5235
  // packages/cli/src/lib/assets.ts
4917
5236
  import {
@@ -5515,8 +5834,27 @@ async function provisionGarage(env, bucket, instanceEnvPath) {
5515
5834
  S3_SECRET_KEY: secretKey
5516
5835
  });
5517
5836
  steps.push({ name: "garage:key", ok: true, detail: `created ${accessKey}` });
5837
+ const stale = await removeStaleKeys(api, accessKey);
5838
+ if (stale)
5839
+ steps.push(stale);
5518
5840
  return { steps, env: next };
5519
5841
  }
5842
+ async function removeStaleKeys(api, keep) {
5843
+ const list = await api("GET", "/v2/ListKeys");
5844
+ if (!list.ok || !Array.isArray(list.json)) {
5845
+ return { name: "garage:stale-keys", ok: false, detail: `list failed (HTTP ${list.status})` };
5846
+ }
5847
+ const stale = list.json.filter((k) => k?.name === KEY_NAME && typeof k.id === "string" && k.id !== keep).map((k) => k.id);
5848
+ if (stale.length === 0)
5849
+ return null;
5850
+ const failed = [];
5851
+ for (const id of stale) {
5852
+ const del = await api("POST", `/v2/DeleteKey?id=${encodeURIComponent(id)}`);
5853
+ if (!del.ok)
5854
+ failed.push(`${id} (HTTP ${del.status})`);
5855
+ }
5856
+ 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(", ")}` };
5857
+ }
5520
5858
 
5521
5859
  // packages/cli/src/commands/statusline.ts
5522
5860
  import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
@@ -5922,7 +6260,7 @@ async function runInstall(argv) {
5922
6260
  install: done.`);
5923
6261
  console.log(` UI http://127.0.0.1:${env.WEBAPI_PORT}/app`);
5924
6262
  console.log(` API http://127.0.0.1:${env.WEBAPI_PORT}/api/docs`);
5925
- console.log(` data docker volumes (survive uninstall unless --purge)`);
6263
+ console.log(` data ${join9(paths.deployDir, "data")} (survives uninstall unless --purge)`);
5926
6264
  console.log(" remove claude-transcripts uninstall");
5927
6265
  return 0;
5928
6266
  }
@@ -6336,15 +6674,15 @@ async function runSessions(argv) {
6336
6674
  // packages/cli/src/commands/setup.ts
6337
6675
  import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
6338
6676
  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");
6677
+ import { dirname as dirname7, join as join10 } from "path";
6678
+ var REPO_ROOT = join10(import.meta.dir, "..", "..", "..", "..");
6679
+ var HOOK_DIR = join10(REPO_ROOT, "hooks");
6680
+ var HOOK_CONFIG_PATH = process.env.CT_HOOK_CONFIG ?? join10(homedir3(), ".config", "claude-transcripts", "config.json");
6343
6681
  var GLOBAL_SETTINGS = installPaths().claudeSettings;
6344
- var PROJECT_SETTINGS = join9(process.cwd(), ".claude", "settings.json");
6682
+ var PROJECT_SETTINGS = join10(process.cwd(), ".claude", "settings.json");
6345
6683
  function loadRepoConfig() {
6346
- const live = join9(REPO_ROOT, "config", "config.json");
6347
- const template = join9(REPO_ROOT, "config", "config.template.json");
6684
+ const live = join10(REPO_ROOT, "config", "config.json");
6685
+ const template = join10(REPO_ROOT, "config", "config.template.json");
6348
6686
  return JSON.parse(readFileSync13(existsSync10(live) ? live : template, "utf8"));
6349
6687
  }
6350
6688
  function couchHeaders(auth) {
@@ -6404,7 +6742,7 @@ function readSettings3(path) {
6404
6742
  function globalRegistrationBlockedBy(settingsPath = GLOBAL_SETTINGS) {
6405
6743
  return pluginRegistration(settingsPath, readSettings3(settingsPath));
6406
6744
  }
6407
- var DISPATCH_PATH = join9(HOOK_DIR, "scripts", "dispatch.ts");
6745
+ var DISPATCH_PATH = join10(HOOK_DIR, "scripts", "dispatch.ts");
6408
6746
  function dispatchCommand() {
6409
6747
  return hookLaunch(`bun run "${DISPATCH_PATH}"`);
6410
6748
  }
@@ -6415,7 +6753,7 @@ function isRegistered(path) {
6415
6753
  }
6416
6754
  function registerGlobal(dryRun) {
6417
6755
  const cmd = dispatchCommand();
6418
- const generated = JSON.parse(readFileSync13(join9(HOOK_DIR, "hooks", "hooks.json"), "utf8"));
6756
+ const generated = JSON.parse(readFileSync13(join10(HOOK_DIR, "hooks", "hooks.json"), "utf8"));
6419
6757
  const settings = readSettings3(GLOBAL_SETTINGS);
6420
6758
  const hooks = settings.hooks ?? {};
6421
6759
  for (const [event, groups] of Object.entries(generated.hooks)) {
@@ -6655,12 +6993,30 @@ async function runTurns(argv) {
6655
6993
  }
6656
6994
 
6657
6995
  // packages/cli/src/commands/uninstall.ts
6658
- import { existsSync as existsSync12, rmSync } from "fs";
6996
+ import { existsSync as existsSync12, readdirSync, rmSync } from "fs";
6997
+ import { join as join11 } from "path";
6998
+ var DATA_SUBDIR = "data";
6999
+ function removeDeployDir(deployDir, purge) {
7000
+ if (purge) {
7001
+ rmSync(deployDir, { recursive: true, force: true });
7002
+ return;
7003
+ }
7004
+ let entries;
7005
+ try {
7006
+ entries = readdirSync(deployDir);
7007
+ } catch {
7008
+ return;
7009
+ }
7010
+ for (const entry of entries) {
7011
+ if (entry !== DATA_SUBDIR)
7012
+ rmSync(join11(deployDir, entry), { recursive: true, force: true });
7013
+ }
7014
+ }
6659
7015
  async function confirmPurge(auto) {
6660
7016
  if (auto)
6661
7017
  return true;
6662
7018
  process.stdout.write(`
6663
- uninstall: --purge DELETES ALL RECORDED HISTORY (CouchDB + S3 volumes).
7019
+ uninstall: --purge DELETES ALL RECORDED HISTORY (CouchDB + S3 data).
6664
7020
  ` + "Type 'delete my history' to confirm: ");
6665
7021
  const line = (await new Response(Bun.stdin.stream()).text()).trim();
6666
7022
  return line === "delete my history";
@@ -6673,10 +7029,11 @@ async function runUninstall(argv) {
6673
7029
  console.error("uninstall: not confirmed \u2014 nothing was removed.");
6674
7030
  return 1;
6675
7031
  }
6676
- console.log("uninstall: deregistering the hook");
7032
+ console.log("uninstall: deregistering the hook and the statusline");
6677
7033
  await runHook(["uninstall"]);
7034
+ await runStatusline(["uninstall"]);
6678
7035
  if (existsSync12(paths.instanceEnv)) {
6679
- console.log(`uninstall: stopping the stack${purge ? " and deleting volumes" : ""}`);
7036
+ console.log("uninstall: stopping the stack");
6680
7037
  try {
6681
7038
  const env = loadOrCreateInstanceEnv(paths.instanceEnv);
6682
7039
  await composeDown({ paths, env, app: true }, purge);
@@ -6684,17 +7041,28 @@ async function runUninstall(argv) {
6684
7041
  console.warn(`uninstall: could not stop the stack (continuing): ${err.message}`);
6685
7042
  }
6686
7043
  }
6687
- for (const target of [paths.hookConfig, paths.instanceEnv, paths.deployDir, paths.versionFile]) {
7044
+ const targets = [paths.instanceEnv, paths.versionFile, ...purge ? [paths.hookConfig] : []];
7045
+ for (const target of targets) {
6688
7046
  try {
6689
7047
  rmSync(target, { recursive: true, force: true });
6690
7048
  } catch {}
6691
7049
  }
7050
+ const dataDir = join11(paths.deployDir, DATA_SUBDIR);
7051
+ try {
7052
+ removeDeployDir(paths.deployDir, purge);
7053
+ } catch (err) {
7054
+ console.error(`uninstall: could not remove ${paths.deployDir}: ${err.message}`);
7055
+ if (purge)
7056
+ console.error(`uninstall: delete ${dataDir} by hand to finish the purge`);
7057
+ return 1;
7058
+ }
6692
7059
  console.log("uninstall: removed generated config and deployment assets");
6693
7060
  if (purge) {
6694
7061
  rmSync(paths.appConfig, { force: true });
6695
7062
  console.log("uninstall: purged history and app config");
6696
7063
  } else {
6697
- console.log(`uninstall: history kept in Docker volumes; ${paths.appConfig} left in place`);
7064
+ console.log(`uninstall: history kept in ${dataDir}`);
7065
+ console.log(`uninstall: ${paths.hookConfig} and ${paths.appConfig} left in place`);
6698
7066
  console.log("uninstall: re-run with --purge to delete the data too");
6699
7067
  }
6700
7068
  console.log(`uninstall: the binary itself is still at ${paths.binDir}/claude-transcripts`);
@@ -6718,7 +7086,8 @@ var COMMANDS = {
6718
7086
  doctor: runDoctor,
6719
7087
  sessions: runSessions,
6720
7088
  search: runSearch,
6721
- turns: runTurns
7089
+ turns: runTurns,
7090
+ completions: runCompletions
6722
7091
  };
6723
7092
 
6724
- export { CLI_SPEC, isFlag, cliCommandsByGroup, cliUsage, cliArgHint, cliArgLabel, validateCliArgs, parseFlags, VERSION, COMMANDS };
7093
+ 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-fptk6bgx.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-a93wk4rv.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.4.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": [