@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 +1 -0
- package/dist/{app-vcj5svg7.js → app-2rqkxqp9.js} +2 -2
- package/dist/{cli-d7a6svha.js → cli-byvjee1b.js} +665 -207
- package/dist/cli.js +3 -3
- package/package.json +1 -1
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,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() -
|
|
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 =
|
|
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
|
|
4549
|
+
const also = [];
|
|
4059
4550
|
if (targets.bucket)
|
|
4060
|
-
|
|
4551
|
+
also.push(`s3://${targets.bucket}`);
|
|
4061
4552
|
if (targets.mirrors.length) {
|
|
4062
|
-
|
|
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
|
|
4557
|
+
return link ? `${head}
|
|
4558
|
+
${link}` : head;
|
|
4066
4559
|
}
|
|
4067
|
-
var NOT_RECORDING_BANNER = "Claude Transcripts
|
|
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
|
|
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 ?
|
|
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
|
|
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 +=
|
|
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
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
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
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
}
|
|
5543
|
-
|
|
5544
|
-
|
|
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
|
|
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
|
|
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
|
|
6340
|
-
var REPO_ROOT =
|
|
6341
|
-
var HOOK_DIR =
|
|
6342
|
-
var HOOK_CONFIG_PATH = process.env.CT_HOOK_CONFIG ??
|
|
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 =
|
|
6771
|
+
var PROJECT_SETTINGS = join10(process.cwd(), ".claude", "settings.json");
|
|
6345
6772
|
function loadRepoConfig() {
|
|
6346
|
-
const live =
|
|
6347
|
-
const template =
|
|
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 =
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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-
|
|
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-
|
|
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