claude-code-kanban 4.29.0 → 4.30.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 +3 -2
- package/cli.js +383 -70
- package/install.js +7 -8
- package/lib/approvals.js +2 -2
- package/lib/contain.js +2 -2
- package/lib/dispatch-groups.js +28 -11
- package/lib/dispatch.js +1 -0
- package/lib/file-url.js +1 -1
- package/lib/inline-assets.js +2 -2
- package/lib/linked-docs.js +56 -0
- package/lib/net-guard.js +1 -1
- package/lib/open-editor.js +5 -3
- package/lib/parent-cache.js +58 -0
- package/lib/parsers.js +175 -168
- package/lib/session-cache.js +34 -0
- package/lib/session-events.js +2 -1
- package/lib/task-counts.js +90 -0
- package/lib/terminal.js +9 -4
- package/package.json +2 -1
- package/plugin/plugins/claude-code-kanban/scripts/postman.js +3 -0
- package/plugin/plugins/claude-code-kanban/skills/kanban/SKILL.md +16 -46
- package/public/app.js +563 -313
- package/public/index.html +46 -23
- package/public/project-match.js +27 -0
- package/public/style.css +306 -94
- package/public/sw.js +1 -1
- package/server.js +167 -74
- package/skill-guides/dispatch.md +5 -4
package/cli.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
3
|
const { getClaudeDir, displayPath } = require('./lib/claude-dir');
|
|
4
4
|
const { isGroupName, suggestGroupName } = require('./lib/dispatch-groups');
|
|
5
|
-
|
|
6
5
|
// Help is auto-generated from this table — keep flags/usage in sync with `run` behavior.
|
|
7
6
|
const COMMANDS = {
|
|
8
7
|
'preview-doc': {
|
|
@@ -11,19 +10,31 @@ const COMMANDS = {
|
|
|
11
10
|
flags: {
|
|
12
11
|
'--session <id>': 'Switch focused session in the browser (does not link the file)',
|
|
13
12
|
},
|
|
13
|
+
notes: 'HTML renders in a sandboxed iframe; local stylesheets, scripts and images are inlined. Relative paths resolve against the current dir.',
|
|
14
|
+
examples: [
|
|
15
|
+
'claude-code-kanban preview-doc ./notes.md --session $CLAUDE_SESSION_ID',
|
|
16
|
+
],
|
|
14
17
|
run: runPreviewCli,
|
|
15
18
|
},
|
|
16
19
|
'link-doc': {
|
|
17
20
|
summary: 'Link a file to a session in the sidebar without opening the preview modal',
|
|
18
|
-
usage: 'claude-code-kanban link-doc <file> --session <id> [--unlink]',
|
|
21
|
+
usage: 'claude-code-kanban link-doc <file> --session <id> [--unlink] | link-doc --list --session <id> [--json]',
|
|
19
22
|
flags: {
|
|
20
|
-
'
|
|
21
|
-
'--
|
|
23
|
+
'<file>': 'Any file type; one the preview cannot render opens in the editor',
|
|
24
|
+
'--session <id>': 'Session to link the file to (required unless $PREVIEW_SESSION is set); full id or unique prefix',
|
|
25
|
+
'--unlink': 'Remove the link instead of adding it (the file need not exist)',
|
|
26
|
+
'--list': 'Print the docs the server holds for the session',
|
|
27
|
+
'--json': 'With --list: output JSON',
|
|
22
28
|
},
|
|
29
|
+
notes: 'The server keeps the link, so it shows when a browser tab opens later.',
|
|
30
|
+
examples: [
|
|
31
|
+
'claude-code-kanban link-doc ./design.md --session $CLAUDE_SESSION_ID',
|
|
32
|
+
'claude-code-kanban link-doc --list --session $CLAUDE_SESSION_ID',
|
|
33
|
+
],
|
|
23
34
|
run: runLinkDocCli,
|
|
24
35
|
},
|
|
25
36
|
session: {
|
|
26
|
-
summary: 'List
|
|
37
|
+
summary: 'List, search, open and inspect Claude Code sessions',
|
|
27
38
|
verbs: {
|
|
28
39
|
list: {
|
|
29
40
|
summary: 'List sessions (pinned/sticky always included)',
|
|
@@ -31,19 +42,37 @@ const COMMANDS = {
|
|
|
31
42
|
flags: {
|
|
32
43
|
'--active': 'Only sessions with recent activity (sidebar-style filter)',
|
|
33
44
|
'--days <n>': 'Only sessions modified within the last N days (fractional ok, e.g. 0.5)',
|
|
34
|
-
'--project <name>': 'Filter by project
|
|
45
|
+
'--project <name>': 'Filter by project: an absolute path selects one project, other text matches a part of the path',
|
|
35
46
|
'--limit <n|all>': 'Max rows to display (default: 10). Use "all" for no cap.',
|
|
36
47
|
'--no-pins': 'Disable always-include and sticky-first ordering for pinned sessions',
|
|
37
48
|
'--json': 'Output JSON instead of a table',
|
|
38
49
|
},
|
|
50
|
+
examples: [
|
|
51
|
+
'claude-code-kanban session list --active',
|
|
52
|
+
'claude-code-kanban session list --days 0.5 --limit all --project my-repo',
|
|
53
|
+
],
|
|
39
54
|
run: runSessionListCli,
|
|
40
55
|
},
|
|
56
|
+
search: {
|
|
57
|
+
summary: 'Find sessions whose name or id contains the text, from any transcript',
|
|
58
|
+
usage: 'claude-code-kanban session search <text> [--limit <n>] [--json]',
|
|
59
|
+
flags: {
|
|
60
|
+
'<text>': 'At least 3 characters; matched against the session name and id',
|
|
61
|
+
'--limit <n>': 'Max rows (default and max: 20)',
|
|
62
|
+
'--json': 'Output JSON instead of a table',
|
|
63
|
+
},
|
|
64
|
+
examples: [
|
|
65
|
+
'claude-code-kanban session search login-redirect',
|
|
66
|
+
],
|
|
67
|
+
run: runSessionSearchCli,
|
|
68
|
+
},
|
|
41
69
|
open: {
|
|
42
70
|
summary: 'Focus a session in the browser (Active tab)',
|
|
43
71
|
usage: 'claude-code-kanban session open <id>',
|
|
44
72
|
flags: {
|
|
45
73
|
'<id>': 'Full session id, or a unique prefix',
|
|
46
74
|
},
|
|
75
|
+
examples: ['claude-code-kanban session open $CLAUDE_SESSION_ID'],
|
|
47
76
|
run: runSessionOpenCli,
|
|
48
77
|
},
|
|
49
78
|
view: {
|
|
@@ -53,8 +82,30 @@ const COMMANDS = {
|
|
|
53
82
|
'<id>': 'Full session id, or a unique prefix',
|
|
54
83
|
'--json': 'Output JSON instead of formatted sections',
|
|
55
84
|
},
|
|
85
|
+
examples: ['claude-code-kanban session view $CLAUDE_SESSION_ID'],
|
|
56
86
|
run: runSessionViewCli,
|
|
57
87
|
},
|
|
88
|
+
plan: {
|
|
89
|
+
summary: 'Print the plan saved for a session (plan mode)',
|
|
90
|
+
usage: 'claude-code-kanban session plan <id> [--json]',
|
|
91
|
+
flags: {
|
|
92
|
+
'<id>': 'Full session id, or a unique prefix',
|
|
93
|
+
'--json': 'Output JSON ({content, slug}); content is null when there is no plan',
|
|
94
|
+
},
|
|
95
|
+
examples: ['claude-code-kanban session plan 3fa9c1'],
|
|
96
|
+
run: runSessionPlanCli,
|
|
97
|
+
},
|
|
98
|
+
agents: {
|
|
99
|
+
summary: 'List the subagents a session has run, and whether it waits for the user',
|
|
100
|
+
usage: 'claude-code-kanban session agents <id> [--json]',
|
|
101
|
+
flags: {
|
|
102
|
+
'<id>': 'Full session id, or a unique prefix',
|
|
103
|
+
'--json': 'Output JSON ({agents, waitingForUser})',
|
|
104
|
+
},
|
|
105
|
+
notes: 'Needs the cck hooks in this config dir (claude-code-kanban --install); without them the list is empty.',
|
|
106
|
+
examples: ['claude-code-kanban session agents $CLAUDE_SESSION_ID'],
|
|
107
|
+
run: runSessionAgentsCli,
|
|
108
|
+
},
|
|
58
109
|
pin: {
|
|
59
110
|
summary: 'Pin (or unpin) a session in the sidebar of connected browser tabs',
|
|
60
111
|
usage: 'claude-code-kanban session pin <id> [--sticky] [--unpin]',
|
|
@@ -63,6 +114,10 @@ const COMMANDS = {
|
|
|
63
114
|
'--sticky': 'Set sticky state (always shown, top of list)',
|
|
64
115
|
'--unpin': 'Clear pin/sticky state',
|
|
65
116
|
},
|
|
117
|
+
examples: [
|
|
118
|
+
'claude-code-kanban session pin $CLAUDE_SESSION_ID --sticky',
|
|
119
|
+
'claude-code-kanban session pin $CLAUDE_SESSION_ID --unpin',
|
|
120
|
+
],
|
|
66
121
|
run: runSessionPinCli,
|
|
67
122
|
},
|
|
68
123
|
pins: {
|
|
@@ -82,10 +137,46 @@ const COMMANDS = {
|
|
|
82
137
|
'--limit <n>': 'Number of messages (default: 10, max: 50)',
|
|
83
138
|
'--json': 'Output JSON instead of formatted lines',
|
|
84
139
|
},
|
|
140
|
+
examples: ['claude-code-kanban session peek 3fa9c1 --limit 20'],
|
|
85
141
|
run: runSessionPeekCli,
|
|
86
142
|
},
|
|
87
143
|
},
|
|
88
144
|
},
|
|
145
|
+
task: {
|
|
146
|
+
summary: 'Read the tasks on the board',
|
|
147
|
+
verbs: {
|
|
148
|
+
list: {
|
|
149
|
+
summary: 'List the tasks of a session, a project, or every session',
|
|
150
|
+
usage: 'claude-code-kanban task list (<session> | --project <path> | --all) [--status <s>] [--json]',
|
|
151
|
+
flags: {
|
|
152
|
+
'<session>': 'Full session id, or a unique prefix',
|
|
153
|
+
'--project <path>': 'Every task of the sessions in this project (absolute path, as in `project list`)',
|
|
154
|
+
'--all': 'Every task on the board',
|
|
155
|
+
'--status <s>': 'Only tasks in this status: pending, in_progress or completed',
|
|
156
|
+
'--json': 'Output JSON instead of a table',
|
|
157
|
+
},
|
|
158
|
+
examples: [
|
|
159
|
+
'claude-code-kanban task list $CLAUDE_SESSION_ID',
|
|
160
|
+
'claude-code-kanban task list --all --status in_progress',
|
|
161
|
+
],
|
|
162
|
+
run: runTaskListCli,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
project: {
|
|
167
|
+
summary: 'Read the projects the board knows',
|
|
168
|
+
verbs: {
|
|
169
|
+
list: {
|
|
170
|
+
summary: 'List known project paths, newest activity first',
|
|
171
|
+
usage: 'claude-code-kanban project list [--json]',
|
|
172
|
+
flags: {
|
|
173
|
+
'--json': 'Output JSON instead of a table',
|
|
174
|
+
},
|
|
175
|
+
notes: 'These are the folders `dispatch start --cwd` accepts.',
|
|
176
|
+
run: runProjectListCli,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
89
180
|
dispatch: {
|
|
90
181
|
summary: 'Start a Claude Code session for a task in cck and collect its report',
|
|
91
182
|
verbs: {
|
|
@@ -104,6 +195,10 @@ const COMMANDS = {
|
|
|
104
195
|
'--worktree [name]': 'Run in a new git worktree',
|
|
105
196
|
'--json': 'Output JSON',
|
|
106
197
|
},
|
|
198
|
+
notes: 'Needs the terminal token, so it runs on the machine of the cck server. Run `claude-code-kanban skills get dispatch` for how to write the spec.',
|
|
199
|
+
examples: [
|
|
200
|
+
'claude-code-kanban dispatch start --cwd . --spec-file spec.md --name fix-login-redirect --group auth-refactor --peer my-peer --report --json',
|
|
201
|
+
],
|
|
107
202
|
run: runDispatchStartCli,
|
|
108
203
|
},
|
|
109
204
|
done: {
|
|
@@ -116,6 +211,9 @@ const COMMANDS = {
|
|
|
116
211
|
'--summary <text>': 'What changed, what was found, what remains',
|
|
117
212
|
'--summary-file <path>': 'Read the summary from a file',
|
|
118
213
|
},
|
|
214
|
+
examples: [
|
|
215
|
+
'claude-code-kanban dispatch done d_1a2b3c --cap <cap> --outcome succeeded --summary-file summary.md',
|
|
216
|
+
],
|
|
119
217
|
run: runDispatchDoneCli,
|
|
120
218
|
},
|
|
121
219
|
wait: {
|
|
@@ -126,6 +224,8 @@ const COMMANDS = {
|
|
|
126
224
|
'--timeout <dur>': 'How long to wait, e.g. 90s, 15m, 1h (default: 10m)',
|
|
127
225
|
'--json': 'Output JSON',
|
|
128
226
|
},
|
|
227
|
+
notes: 'Returns as soon as any watched dispatch settles, with settled, running and timeout.',
|
|
228
|
+
examples: ['claude-code-kanban dispatch wait --timeout 15m --json'],
|
|
129
229
|
run: runDispatchWaitCli,
|
|
130
230
|
},
|
|
131
231
|
list: {
|
|
@@ -146,12 +246,18 @@ const COMMANDS = {
|
|
|
146
246
|
summary: 'Print the guide for a skill',
|
|
147
247
|
usage: 'claude-code-kanban skills get <name>',
|
|
148
248
|
flags: { '<name>': 'Skill name, e.g. dispatch' },
|
|
249
|
+
examples: ['claude-code-kanban skills get dispatch'],
|
|
149
250
|
run: runSkillsGetCli,
|
|
150
251
|
},
|
|
151
252
|
},
|
|
152
253
|
},
|
|
153
254
|
};
|
|
154
255
|
|
|
256
|
+
for (const [noun, cmd] of Object.entries(COMMANDS)) {
|
|
257
|
+
cmd.name = noun;
|
|
258
|
+
for (const [verb, v] of Object.entries(cmd.verbs || {})) v.name = `${noun} ${verb}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
155
261
|
function runCli(argv) {
|
|
156
262
|
if (argv.includes('--version') || argv.includes('-v')) {
|
|
157
263
|
console.log(require('./package.json').version);
|
|
@@ -160,7 +266,10 @@ function runCli(argv) {
|
|
|
160
266
|
const cli = resolveCliCommand(argv);
|
|
161
267
|
if (cli.kind === 'server') return false;
|
|
162
268
|
if (cli.kind === 'help') {
|
|
163
|
-
|
|
269
|
+
const noun = cli.target && Object.hasOwn(COMMANDS, cli.target) ? COMMANDS[cli.target] : null;
|
|
270
|
+
const verb = noun?.verbs && cli.verb && Object.hasOwn(noun.verbs, cli.verb) ? noun.verbs[cli.verb] : null;
|
|
271
|
+
if (verb) printLeafHelp(verb);
|
|
272
|
+
else if (noun) printNounHelp(cli.target);
|
|
164
273
|
else printTopHelp();
|
|
165
274
|
process.exit(0);
|
|
166
275
|
}
|
|
@@ -176,11 +285,11 @@ function runCli(argv) {
|
|
|
176
285
|
}
|
|
177
286
|
if (cli.kind === 'noun') {
|
|
178
287
|
printNounHelp(cli.noun);
|
|
179
|
-
process.exit(0);
|
|
288
|
+
process.exit(argv.includes('--help') || argv.includes('-h') ? 0 : 1);
|
|
180
289
|
}
|
|
181
290
|
if (cli.kind === 'leaf') {
|
|
182
291
|
if (cli.args.includes('--help') || cli.args.includes('-h')) {
|
|
183
|
-
printLeafHelp(cli.
|
|
292
|
+
printLeafHelp(cli.entry);
|
|
184
293
|
process.exit(0);
|
|
185
294
|
}
|
|
186
295
|
cli.entry.run(cli.args)
|
|
@@ -195,7 +304,7 @@ function resolveCliCommand(argv) {
|
|
|
195
304
|
const noun = argv[2] && !argv[2].startsWith('-') ? argv[2] : null;
|
|
196
305
|
const hasHelp = (a) => a.includes('--help') || a.includes('-h');
|
|
197
306
|
if (!noun) return hasHelp(argv) ? { kind: 'help' } : { kind: 'server' };
|
|
198
|
-
if (noun === 'help') return { kind: 'help', target: argv[3] };
|
|
307
|
+
if (noun === 'help') return { kind: 'help', target: argv[3], verb: argv[4] };
|
|
199
308
|
if (!Object.hasOwn(COMMANDS, noun)) return { kind: 'unknown-noun', noun };
|
|
200
309
|
const entry = COMMANDS[noun];
|
|
201
310
|
if (!entry.verbs) return { kind: 'leaf', name: noun, entry, args: argv.slice(3) };
|
|
@@ -209,12 +318,8 @@ function printTopHelp() {
|
|
|
209
318
|
console.log('Usage: claude-code-kanban <command> [args] [--flags]\n');
|
|
210
319
|
console.log('Commands:');
|
|
211
320
|
for (const [name, cmd] of Object.entries(COMMANDS)) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
for (const [vName, v] of Object.entries(cmd.verbs)) {
|
|
215
|
-
console.log(` ${`${name} ${vName}`.padEnd(18)}${v.summary}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
321
|
+
const verbs = cmd.verbs ? ` (${Object.keys(cmd.verbs).join(', ')})` : '';
|
|
322
|
+
console.log(` ${name.padEnd(20)}${cmd.summary}${verbs}`);
|
|
218
323
|
}
|
|
219
324
|
console.log(` ${'help'.padEnd(20)}Show help for a command (claude-code-kanban help <command>)`);
|
|
220
325
|
console.log('\nFlags:');
|
|
@@ -226,6 +331,11 @@ function printTopHelp() {
|
|
|
226
331
|
console.log(' --open Open browser on start');
|
|
227
332
|
console.log(' --install, --uninstall Install or remove the plugin, context spy, and statusline');
|
|
228
333
|
console.log(' --plugin-only With --install: refresh only the plugin, skip context spy and statusline');
|
|
334
|
+
console.log('\nEnvironment:');
|
|
335
|
+
console.log(' CCK_URL Server base URL, e.g. http://127.0.0.1:4795 (wins over PORT)');
|
|
336
|
+
console.log(' PORT Server port (default: the one this config dir\'s server reports, else 3541)');
|
|
337
|
+
console.log(' CLAUDE_CONFIG_DIR Claude config dir whose board to use');
|
|
338
|
+
console.log('\nRun `claude-code-kanban help <command>` for its subcommands, and `help <command> <subcommand>` for flags and examples.');
|
|
229
339
|
}
|
|
230
340
|
|
|
231
341
|
function printNounHelp(noun) {
|
|
@@ -237,23 +347,33 @@ function printNounHelp(noun) {
|
|
|
237
347
|
for (const [vName, v] of Object.entries(entry.verbs)) {
|
|
238
348
|
console.log(` ${vName.padEnd(12)}${v.summary}`);
|
|
239
349
|
}
|
|
240
|
-
console.log(`\nRun \`claude-code-kanban ${noun} <subcommand
|
|
350
|
+
console.log(`\nRun \`claude-code-kanban help ${noun} <subcommand>\` for flags and examples.`);
|
|
241
351
|
} else {
|
|
242
|
-
printLeafHelp(
|
|
352
|
+
printLeafHelp(entry);
|
|
243
353
|
}
|
|
244
354
|
}
|
|
245
355
|
|
|
246
|
-
function printLeafHelp(
|
|
356
|
+
function printLeafHelp(entry) {
|
|
247
357
|
console.log(`${entry.summary}\n`);
|
|
248
358
|
console.log(`Usage: ${entry.usage}`);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
359
|
+
const flags = { ...entry.flags, '--help, -h': 'Show this help' };
|
|
360
|
+
const pad = Math.max(...Object.keys(flags).map(f => f.length));
|
|
361
|
+
console.log('\nFlags:');
|
|
362
|
+
for (const [flag, desc] of Object.entries(flags)) {
|
|
363
|
+
console.log(` ${flag.padEnd(pad + 2)}${desc}`);
|
|
255
364
|
}
|
|
256
|
-
console.log(
|
|
365
|
+
if (entry.notes) console.log(`\n${entry.notes}`);
|
|
366
|
+
if (entry.examples?.length) {
|
|
367
|
+
console.log('\nExamples:');
|
|
368
|
+
for (const ex of entry.examples) console.log(` ${ex}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// For a bad value: the leaf help would bury the one line that says what is wrong.
|
|
373
|
+
function usageError(entry, message) {
|
|
374
|
+
console.error(message);
|
|
375
|
+
console.error(`Run \`claude-code-kanban help ${entry.name}\` for usage.`);
|
|
376
|
+
return 1;
|
|
257
377
|
}
|
|
258
378
|
|
|
259
379
|
function getArgValue(args, name) {
|
|
@@ -266,10 +386,24 @@ function getArgValue(args, name) {
|
|
|
266
386
|
|
|
267
387
|
// The hub runs one cck per config dir, each on its own port, so 3541 can be another dir's board.
|
|
268
388
|
// The server's beacon in this config dir names the right one; PORT still wins when set.
|
|
389
|
+
// A beacon left by a dead server means this dir's board is down: 3541 would be someone else's.
|
|
390
|
+
// Returns null in that case.
|
|
269
391
|
function cliPort() {
|
|
270
392
|
if (process.env.PORT) return process.env.PORT;
|
|
271
|
-
const
|
|
272
|
-
|
|
393
|
+
const beacon = readCckJson('server.json');
|
|
394
|
+
if (!beacon) return 3541;
|
|
395
|
+
return beacon.port && beacon.pid && isPidAlive(beacon.pid) ? beacon.port : null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function cliBaseUrl() {
|
|
399
|
+
if (process.env.CCK_URL) return process.env.CCK_URL.replace(/\/+$/, '');
|
|
400
|
+
const port = cliPort();
|
|
401
|
+
return port === null ? null : `http://127.0.0.1:${port}`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function cliTargetPort() {
|
|
405
|
+
if (!process.env.CCK_URL) return cliPort();
|
|
406
|
+
try { return new URL(process.env.CCK_URL).port; } catch (_) { return ''; }
|
|
273
407
|
}
|
|
274
408
|
|
|
275
409
|
function readCckJson(name) {
|
|
@@ -281,14 +415,21 @@ function isPidAlive(pid) {
|
|
|
281
415
|
try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; }
|
|
282
416
|
}
|
|
283
417
|
function unreachable() {
|
|
284
|
-
|
|
418
|
+
const dir = displayPath(getClaudeDir());
|
|
419
|
+
const start = 'Start it first with "claude-code-kanban".';
|
|
420
|
+
if (process.env.CCK_URL) return `Cannot reach cck server for ${dir} at ${cliBaseUrl()} (CCK_URL). ${start}`;
|
|
421
|
+
const port = cliPort();
|
|
422
|
+
if (port === null) return `Cannot reach cck server for ${dir}: its server.json names a server that is no longer running. ${start}`;
|
|
423
|
+
return `Cannot reach cck server for ${dir} on port ${port}. ${start}`;
|
|
285
424
|
}
|
|
286
425
|
|
|
287
426
|
class CliUnreachable extends Error { constructor() { super(unreachable()); this.code = 'unreachable'; } }
|
|
288
427
|
|
|
289
428
|
async function cliFetch(urlPath, init) {
|
|
429
|
+
const base = cliBaseUrl();
|
|
430
|
+
if (!base) throw new CliUnreachable();
|
|
290
431
|
try {
|
|
291
|
-
return await fetch(
|
|
432
|
+
return await fetch(`${base}${urlPath}`, init);
|
|
292
433
|
} catch (e) {
|
|
293
434
|
if (e.cause?.code === 'ECONNREFUSED' || /fetch failed/i.test(e.message)) throw new CliUnreachable();
|
|
294
435
|
throw e;
|
|
@@ -319,7 +460,7 @@ function reportCliError(e) {
|
|
|
319
460
|
async function runPreviewCli(args) {
|
|
320
461
|
const filePathArg = args.find(a => !a.startsWith('--'));
|
|
321
462
|
if (!filePathArg) {
|
|
322
|
-
printLeafHelp(
|
|
463
|
+
printLeafHelp(COMMANDS['preview-doc']);
|
|
323
464
|
return 1;
|
|
324
465
|
}
|
|
325
466
|
const sessionId = getArgValue(args, 'session') || process.env.PREVIEW_SESSION || null;
|
|
@@ -332,24 +473,38 @@ async function runPreviewCli(args) {
|
|
|
332
473
|
}
|
|
333
474
|
|
|
334
475
|
async function runLinkDocCli(args) {
|
|
335
|
-
const
|
|
476
|
+
const entry = COMMANDS['link-doc'];
|
|
477
|
+
const [filePathArg] = positionals(args, ['--session']);
|
|
336
478
|
const sessionArg = getArgValue(args, 'session') || process.env.PREVIEW_SESSION || null;
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
if (!sessionArg) {
|
|
342
|
-
console.error('--session is required: linked docs are stored per session.');
|
|
479
|
+
const list = args.includes('--list');
|
|
480
|
+
if (!filePathArg && !list) {
|
|
481
|
+
printLeafHelp(entry);
|
|
343
482
|
return 1;
|
|
344
483
|
}
|
|
484
|
+
if (!sessionArg) return usageError(entry, '--session is required: linked docs are stored per session.');
|
|
345
485
|
const unlink = args.includes('--unlink');
|
|
346
486
|
// Resolved here because the browser keys linked docs by full id, so a prefix won't match.
|
|
347
487
|
const resolved = await resolveSessionByIdOrPrefix(sessionArg);
|
|
348
488
|
if (!resolved) return 1;
|
|
489
|
+
if (list) return printLinkedDocs(resolved.id, args.includes('--json'));
|
|
349
490
|
const abs = path.resolve(filePathArg);
|
|
350
491
|
try {
|
|
351
|
-
|
|
492
|
+
const out = await cliPostJson('/api/document/link', { path: abs, sessionId: resolved.id, unlink }, 'Link');
|
|
493
|
+
if (!out) return 1;
|
|
352
494
|
console.log(`Document ${unlink ? 'unlinked from' : 'linked to'} session ${resolved.id.slice(0, 8)}: ${abs}`);
|
|
495
|
+
if (!unlink && out.tabs === 0) console.log('No browser tab is open; the board shows it when one opens.');
|
|
496
|
+
return 0;
|
|
497
|
+
} catch (e) { reportCliError(e); return 1; }
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function printLinkedDocs(sessionId, asJson) {
|
|
501
|
+
try {
|
|
502
|
+
const res = await cliFetch(`/api/document/links?session=${encodeURIComponent(sessionId)}`);
|
|
503
|
+
if (!res.ok) throw new Error(`Failed to fetch linked docs (${res.status})`);
|
|
504
|
+
const paths = (await res.json())[sessionId] || [];
|
|
505
|
+
if (asJson) console.log(JSON.stringify(paths, null, 2));
|
|
506
|
+
else if (!paths.length) console.log(`No linked docs for session ${sessionId.slice(0, 8)}.`);
|
|
507
|
+
else for (const p of paths) console.log(p);
|
|
353
508
|
return 0;
|
|
354
509
|
} catch (e) { reportCliError(e); return 1; }
|
|
355
510
|
}
|
|
@@ -375,10 +530,11 @@ function parseLimit(args, { fallback, allowAll = false }) {
|
|
|
375
530
|
return { ok: true, limit: n };
|
|
376
531
|
}
|
|
377
532
|
|
|
378
|
-
async function fetchSessionsList(limit, pinnedIds = []) {
|
|
533
|
+
async function fetchSessionsList(limit, pinnedIds = [], project = null) {
|
|
379
534
|
const q = limit === null ? 'all' : String(limit);
|
|
380
535
|
const pinnedQ = pinnedIds.length ? `&pinned=${pinnedIds.join(',')}` : '';
|
|
381
|
-
const
|
|
536
|
+
const projectQ = project ? `&project=${encodeURIComponent(project)}` : '';
|
|
537
|
+
const res = await cliFetch(`/api/sessions?limit=${q}${pinnedQ}${projectQ}`);
|
|
382
538
|
if (!res.ok) throw new Error(`Failed to fetch sessions (${res.status})`);
|
|
383
539
|
return res.json();
|
|
384
540
|
}
|
|
@@ -424,19 +580,18 @@ async function runSessionListCli(args) {
|
|
|
424
580
|
const daysArg = getArgValue(args, 'days');
|
|
425
581
|
const days = daysArg !== null ? parseFloat(daysArg) : null;
|
|
426
582
|
if (daysArg !== null && (Number.isNaN(days) || days <= 0)) {
|
|
427
|
-
|
|
428
|
-
return 1;
|
|
583
|
+
return usageError(COMMANDS.session.verbs.list, `Invalid --days value: ${daysArg}`);
|
|
429
584
|
}
|
|
430
585
|
const parsed = parseLimit(args, { fallback: 10, allowAll: true });
|
|
431
|
-
if (!parsed.ok)
|
|
586
|
+
if (!parsed.ok) return usageError(COMMANDS.session.verbs.list, parsed.error);
|
|
432
587
|
const limit = parsed.limit;
|
|
433
588
|
const asJson = args.includes('--json');
|
|
434
589
|
const pinsMap = noPins ? {} : await fetchPinsMap();
|
|
435
590
|
const pinnedIds = Object.keys(pinsMap);
|
|
436
|
-
const hasClientFilter = activeOnly || days !== null
|
|
591
|
+
const hasClientFilter = activeOnly || days !== null;
|
|
437
592
|
let list;
|
|
438
593
|
try {
|
|
439
|
-
list = await fetchSessionsList(hasClientFilter ? null : limit, pinnedIds);
|
|
594
|
+
list = await fetchSessionsList(hasClientFilter ? null : limit, pinnedIds, projectFilter);
|
|
440
595
|
} catch (e) {
|
|
441
596
|
reportCliError(e);
|
|
442
597
|
return 1;
|
|
@@ -447,10 +602,6 @@ async function runSessionListCli(args) {
|
|
|
447
602
|
const cutoff = Date.now() - days * 86_400_000;
|
|
448
603
|
list = list.filter(s => pinOf(s.id) || (s.modifiedAt && new Date(s.modifiedAt).getTime() >= cutoff));
|
|
449
604
|
}
|
|
450
|
-
if (projectFilter) {
|
|
451
|
-
const needle = projectFilter.toLowerCase();
|
|
452
|
-
list = list.filter(s => (s.project || '').toLowerCase().includes(needle));
|
|
453
|
-
}
|
|
454
605
|
const pinRank = id => pinOf(id) === 'sticky' ? 0 : pinOf(id) === 'pinned' ? 1 : 2;
|
|
455
606
|
list.sort((a, b) => {
|
|
456
607
|
const r = pinRank(a.id) - pinRank(b.id);
|
|
@@ -510,7 +661,7 @@ function formatAge(ms) {
|
|
|
510
661
|
async function runSessionOpenCli(args) {
|
|
511
662
|
const idArg = args.find(a => !a.startsWith('--'));
|
|
512
663
|
if (!idArg) {
|
|
513
|
-
printLeafHelp(
|
|
664
|
+
printLeafHelp(COMMANDS.session.verbs.open);
|
|
514
665
|
return 1;
|
|
515
666
|
}
|
|
516
667
|
const resolved = await resolveSessionByIdOrPrefix(idArg);
|
|
@@ -525,7 +676,7 @@ async function runSessionOpenCli(args) {
|
|
|
525
676
|
async function runSessionPinCli(args) {
|
|
526
677
|
const idArg = args.find(a => !a.startsWith('--'));
|
|
527
678
|
if (!idArg) {
|
|
528
|
-
printLeafHelp(
|
|
679
|
+
printLeafHelp(COMMANDS.session.verbs.pin);
|
|
529
680
|
return 1;
|
|
530
681
|
}
|
|
531
682
|
const state = args.includes('--unpin') ? 'none' : args.includes('--sticky') ? 'sticky' : 'pinned';
|
|
@@ -555,7 +706,7 @@ async function runSessionPinsCli(args) {
|
|
|
555
706
|
sessions = await fetchSessionsList(items.length, items.map(p => p.id));
|
|
556
707
|
} catch (e) { reportCliError(e); return 1; }
|
|
557
708
|
const byId = new Map(sessions.map(s => [s.id, s]));
|
|
558
|
-
|
|
709
|
+
const rows = items
|
|
559
710
|
.map(p => {
|
|
560
711
|
const s = byId.get(p.id) || {};
|
|
561
712
|
return {
|
|
@@ -589,7 +740,7 @@ async function runSessionPinsCli(args) {
|
|
|
589
740
|
async function runSessionViewCli(args) {
|
|
590
741
|
const idArg = args.find(a => !a.startsWith('--'));
|
|
591
742
|
if (!idArg) {
|
|
592
|
-
printLeafHelp(
|
|
743
|
+
printLeafHelp(COMMANDS.session.verbs.view);
|
|
593
744
|
return 1;
|
|
594
745
|
}
|
|
595
746
|
const asJson = args.includes('--json');
|
|
@@ -644,11 +795,11 @@ async function runSessionViewCli(args) {
|
|
|
644
795
|
async function runSessionPeekCli(args) {
|
|
645
796
|
const idArg = args.find(a => !a.startsWith('--'));
|
|
646
797
|
if (!idArg) {
|
|
647
|
-
printLeafHelp(
|
|
798
|
+
printLeafHelp(COMMANDS.session.verbs.peek);
|
|
648
799
|
return 1;
|
|
649
800
|
}
|
|
650
801
|
const parsed = parseLimit(args, { fallback: 10 });
|
|
651
|
-
if (!parsed.ok)
|
|
802
|
+
if (!parsed.ok) return usageError(COMMANDS.session.verbs.peek, parsed.error);
|
|
652
803
|
const limit = parsed.limit;
|
|
653
804
|
const asJson = args.includes('--json');
|
|
654
805
|
const resolved = await resolveSessionByIdOrPrefix(idArg);
|
|
@@ -680,6 +831,168 @@ async function runSessionPeekCli(args) {
|
|
|
680
831
|
} catch (e) { reportCliError(e); return 1; }
|
|
681
832
|
}
|
|
682
833
|
|
|
834
|
+
function printTable(header, rows) {
|
|
835
|
+
const last = header.length - 1;
|
|
836
|
+
const width = header.map((h, i) => Math.max(h.length, ...rows.map(r => String(r[i]).length)));
|
|
837
|
+
const line = cells => cells.map((c, i) => (i === last ? String(c) : String(c).padEnd(width[i]))).join(' ');
|
|
838
|
+
console.log(line(header));
|
|
839
|
+
for (const r of rows) console.log(line(r));
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const ageOf = (iso) => (iso ? formatAge(Date.now() - new Date(iso).getTime()) : '-');
|
|
843
|
+
|
|
844
|
+
async function cliGetJson(urlPath, label) {
|
|
845
|
+
const res = await cliFetch(urlPath);
|
|
846
|
+
if (!res.ok) throw new Error(`${label} failed (${res.status}): ${await res.text()}`);
|
|
847
|
+
return res.json();
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function runSessionSearchCli(args) {
|
|
851
|
+
const entry = COMMANDS.session.verbs.search;
|
|
852
|
+
const text = positionals(args, ['--limit']).join(' ').trim();
|
|
853
|
+
if (!text) {
|
|
854
|
+
printLeafHelp(entry);
|
|
855
|
+
return 1;
|
|
856
|
+
}
|
|
857
|
+
if (text.length < 3) return usageError(entry, 'Search text needs at least 3 characters.');
|
|
858
|
+
const parsed = parseLimit(args, { fallback: 20 });
|
|
859
|
+
if (!parsed.ok) return usageError(entry, parsed.error);
|
|
860
|
+
try {
|
|
861
|
+
const ids = (await cliGetJson(`/api/sessions/search?q=${encodeURIComponent(text)}`, 'Search')).slice(0, parsed.limit);
|
|
862
|
+
const list = ids.length ? await cliGetJson(`/api/sessions?limit=1&include=${ids.join(',')}`, 'Search') : [];
|
|
863
|
+
const byId = new Map(list.map(s => [s.id, s]));
|
|
864
|
+
const rows = ids.map(id => byId.get(id) || { id });
|
|
865
|
+
if (args.includes('--json')) {
|
|
866
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
867
|
+
return 0;
|
|
868
|
+
}
|
|
869
|
+
if (!rows.length) {
|
|
870
|
+
console.log(`No sessions match "${text}".`);
|
|
871
|
+
return 0;
|
|
872
|
+
}
|
|
873
|
+
printTable(['ID', 'STATUS', 'AGE', 'PROJECT', 'TITLE'], rows.map(s => [
|
|
874
|
+
s.id.slice(0, 8),
|
|
875
|
+
byId.has(s.id) ? sessionStatus(s) : '-',
|
|
876
|
+
ageOf(s.modifiedAt),
|
|
877
|
+
path.basename(s.project || ''),
|
|
878
|
+
s.customTitle || s.name || s.slug || '',
|
|
879
|
+
]));
|
|
880
|
+
return 0;
|
|
881
|
+
} catch (e) { reportCliError(e); return 1; }
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
async function runSessionPlanCli(args) {
|
|
885
|
+
const [idArg] = positionals(args, []);
|
|
886
|
+
if (!idArg) {
|
|
887
|
+
printLeafHelp(COMMANDS.session.verbs.plan);
|
|
888
|
+
return 1;
|
|
889
|
+
}
|
|
890
|
+
const resolved = await resolveSessionByIdOrPrefix(idArg);
|
|
891
|
+
if (!resolved) return 1;
|
|
892
|
+
try {
|
|
893
|
+
const plan = await cliGetJson(`/api/sessions/${resolved.id}/plan`, 'Plan');
|
|
894
|
+
if (args.includes('--json')) console.log(JSON.stringify(plan, null, 2));
|
|
895
|
+
else if (!plan.content) console.log(`No plan saved for session ${resolved.id.slice(0, 8)}.`);
|
|
896
|
+
else process.stdout.write(plan.content.endsWith('\n') ? plan.content : `${plan.content}\n`);
|
|
897
|
+
return 0;
|
|
898
|
+
} catch (e) { reportCliError(e); return 1; }
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
async function runSessionAgentsCli(args) {
|
|
902
|
+
const [idArg] = positionals(args, []);
|
|
903
|
+
if (!idArg) {
|
|
904
|
+
printLeafHelp(COMMANDS.session.verbs.agents);
|
|
905
|
+
return 1;
|
|
906
|
+
}
|
|
907
|
+
const resolved = await resolveSessionByIdOrPrefix(idArg);
|
|
908
|
+
if (!resolved) return 1;
|
|
909
|
+
try {
|
|
910
|
+
const out = await cliGetJson(`/api/sessions/${resolved.id}/agents`, 'Agents');
|
|
911
|
+
if (args.includes('--json')) {
|
|
912
|
+
console.log(JSON.stringify(out, null, 2));
|
|
913
|
+
return 0;
|
|
914
|
+
}
|
|
915
|
+
const agents = out.agents || [];
|
|
916
|
+
if (!agents.length) console.log(`No agents for session ${resolved.id.slice(0, 8)}.`);
|
|
917
|
+
else {
|
|
918
|
+
printTable(['AGENT', 'STATUS', 'AGE', 'TYPE', 'DESCRIPTION'], agents.map(a => [
|
|
919
|
+
String(a.agentId || '').slice(0, 8),
|
|
920
|
+
a.status || '-',
|
|
921
|
+
ageOf(a.updatedAt || a.startedAt),
|
|
922
|
+
a.type || a.agentType || '',
|
|
923
|
+
(a.description || a.name || '').replace(/\s+/g, ' ').trim(),
|
|
924
|
+
]));
|
|
925
|
+
}
|
|
926
|
+
if (out.waitingForUser) console.log('Waiting for the user.');
|
|
927
|
+
return 0;
|
|
928
|
+
} catch (e) { reportCliError(e); return 1; }
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
async function runTaskListCli(args) {
|
|
932
|
+
const entry = COMMANDS.task.verbs.list;
|
|
933
|
+
const [sessionArg] = positionals(args, ['--project', '--status']);
|
|
934
|
+
const project = getArgValue(args, 'project');
|
|
935
|
+
const all = args.includes('--all');
|
|
936
|
+
const status = getArgValue(args, 'status');
|
|
937
|
+
const sources = [sessionArg, project, all || null].filter(Boolean).length;
|
|
938
|
+
if (sources === 0) {
|
|
939
|
+
printLeafHelp(entry);
|
|
940
|
+
return 1;
|
|
941
|
+
}
|
|
942
|
+
if (sources > 1) return usageError(entry, 'Give one of <session>, --project or --all.');
|
|
943
|
+
if (args.includes('--status') && !status) return usageError(entry, '--status needs a value, e.g. in_progress');
|
|
944
|
+
let tasks;
|
|
945
|
+
let resolved = null;
|
|
946
|
+
try {
|
|
947
|
+
if (sessionArg) {
|
|
948
|
+
resolved = await resolveSessionByIdOrPrefix(sessionArg);
|
|
949
|
+
if (!resolved) return 1;
|
|
950
|
+
tasks = await cliGetJson(`/api/sessions/${resolved.id}`, 'Task list');
|
|
951
|
+
} else if (project) {
|
|
952
|
+
const encoded = Buffer.from(canonicalDir(project), 'utf8').toString('base64');
|
|
953
|
+
tasks = await cliGetJson(`/api/projects/${encodeURIComponent(encoded)}/tasks`, 'Task list');
|
|
954
|
+
} else {
|
|
955
|
+
tasks = await cliGetJson('/api/tasks/all', 'Task list');
|
|
956
|
+
}
|
|
957
|
+
} catch (e) { reportCliError(e); return 1; }
|
|
958
|
+
if (status) tasks = tasks.filter(t => t.status === status);
|
|
959
|
+
if (args.includes('--json')) {
|
|
960
|
+
console.log(JSON.stringify(tasks, null, 2));
|
|
961
|
+
return 0;
|
|
962
|
+
}
|
|
963
|
+
if (!tasks.length) {
|
|
964
|
+
console.log('No tasks match.');
|
|
965
|
+
return 0;
|
|
966
|
+
}
|
|
967
|
+
const showSource = !resolved;
|
|
968
|
+
const header = ['ID', 'STATUS', ...(showSource ? ['SESSION'] : []), 'SUBJECT'];
|
|
969
|
+
printTable(header, tasks.map(t => [
|
|
970
|
+
t.id,
|
|
971
|
+
t.status || '-',
|
|
972
|
+
...(showSource ? [String(t.sessionId || t._taskDir || '').slice(0, 8)] : []),
|
|
973
|
+
`${t.subject || ''}${t.blockedBy?.length ? ` (blocked by ${t.blockedBy.join(', ')})` : ''}`,
|
|
974
|
+
]));
|
|
975
|
+
return 0;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
async function runProjectListCli(args) {
|
|
979
|
+
let projects;
|
|
980
|
+
try {
|
|
981
|
+
projects = await cliGetJson('/api/projects', 'Project list');
|
|
982
|
+
} catch (e) { reportCliError(e); return 1; }
|
|
983
|
+
projects.sort((a, b) => new Date(b.modifiedAt || 0) - new Date(a.modifiedAt || 0));
|
|
984
|
+
if (args.includes('--json')) {
|
|
985
|
+
console.log(JSON.stringify(projects, null, 2));
|
|
986
|
+
return 0;
|
|
987
|
+
}
|
|
988
|
+
if (!projects.length) {
|
|
989
|
+
console.log('No projects.');
|
|
990
|
+
return 0;
|
|
991
|
+
}
|
|
992
|
+
printTable(['AGE', 'PATH'], projects.map(p => [ageOf(p.modifiedAt), `${p.path}${p.temp ? ' (temp)' : ''}`]));
|
|
993
|
+
return 0;
|
|
994
|
+
}
|
|
995
|
+
|
|
683
996
|
function positionals(args, valueFlags) {
|
|
684
997
|
return args.filter((a, i) => !a.startsWith('--') && !valueFlags.includes(args[i - 1]));
|
|
685
998
|
}
|
|
@@ -708,23 +1021,27 @@ function printDispatch(r) {
|
|
|
708
1021
|
}
|
|
709
1022
|
|
|
710
1023
|
async function runDispatchStartCli(args) {
|
|
711
|
-
const
|
|
1024
|
+
const port = cliTargetPort();
|
|
1025
|
+
if (port === null) {
|
|
1026
|
+
console.error(unreachable());
|
|
1027
|
+
return 1;
|
|
1028
|
+
}
|
|
1029
|
+
const token = port && readCckJson(`terminal-tokens/${port}.json`)?.token;
|
|
712
1030
|
if (!token) {
|
|
713
|
-
console.error(`No terminal token for ${displayPath(getClaudeDir())}. The cck server must be running with the terminal enabled.`);
|
|
1031
|
+
console.error(`No terminal token for ${displayPath(getClaudeDir())} at ${cliBaseUrl()}. The cck server must be running with the terminal enabled.`);
|
|
714
1032
|
return 1;
|
|
715
1033
|
}
|
|
716
1034
|
let spec;
|
|
717
1035
|
try { spec = textArg(args, 'spec'); } catch (e) { console.error(e.message); return 1; }
|
|
718
1036
|
if (!spec) {
|
|
719
|
-
printLeafHelp(
|
|
1037
|
+
printLeafHelp(COMMANDS.dispatch.verbs.start);
|
|
720
1038
|
return 1;
|
|
721
1039
|
}
|
|
722
1040
|
const hasGroup = args.some(a => a === '--group' || a.startsWith('--group='));
|
|
723
1041
|
const group = hasGroup ? getArgValue(args, 'group') || '' : null;
|
|
724
1042
|
if (hasGroup && !isGroupName(group)) {
|
|
725
1043
|
const hint = suggestGroupName(group);
|
|
726
|
-
|
|
727
|
-
return 1;
|
|
1044
|
+
return usageError(COMMANDS.dispatch.verbs.start, `Group names are kebab-case${hint ? `: try --group ${hint}` : ', e.g. auth-refactor'}`);
|
|
728
1045
|
}
|
|
729
1046
|
const worktree = args.includes('--worktree') ? getArgValue(args, 'worktree') || true : false;
|
|
730
1047
|
const body = {
|
|
@@ -753,7 +1070,7 @@ async function runDispatchDoneCli(args) {
|
|
|
753
1070
|
try { summary = textArg(args, 'summary'); } catch (e) { console.error(e.message); return 1; }
|
|
754
1071
|
const body = { cap: getArgValue(args, 'cap'), outcome: getArgValue(args, 'outcome'), summary };
|
|
755
1072
|
if (!id || !body.cap || !body.outcome) {
|
|
756
|
-
printLeafHelp(
|
|
1073
|
+
printLeafHelp(COMMANDS.dispatch.verbs.done);
|
|
757
1074
|
return 1;
|
|
758
1075
|
}
|
|
759
1076
|
try {
|
|
@@ -773,10 +1090,7 @@ function dispatchQuery(ids, all = false) {
|
|
|
773
1090
|
async function runDispatchWaitCli(args) {
|
|
774
1091
|
const timeoutRaw = getArgValue(args, 'timeout');
|
|
775
1092
|
const timeoutSec = parseDuration(timeoutRaw, 600);
|
|
776
|
-
if (timeoutSec === null) {
|
|
777
|
-
console.error(`Invalid --timeout value: ${timeoutRaw}`);
|
|
778
|
-
return 1;
|
|
779
|
-
}
|
|
1093
|
+
if (timeoutSec === null) return usageError(COMMANDS.dispatch.verbs.wait, `Invalid --timeout value: ${timeoutRaw}`);
|
|
780
1094
|
const q = dispatchQuery(positionals(args, ['--timeout']));
|
|
781
1095
|
const deadline = Date.now() + timeoutSec * 1000;
|
|
782
1096
|
try {
|
|
@@ -813,8 +1127,7 @@ async function runSkillsGetCli(args) {
|
|
|
813
1127
|
const file = name && /^[a-z][a-z-]*$/.test(name) ? path.join(__dirname, 'skill-guides', `${name}.md`) : null;
|
|
814
1128
|
if (!file || !fs.existsSync(file)) {
|
|
815
1129
|
const known = fs.readdirSync(path.join(__dirname, 'skill-guides')).map(f => f.replace(/\.md$/, ''));
|
|
816
|
-
|
|
817
|
-
return 1;
|
|
1130
|
+
return usageError(COMMANDS.skills.verbs.get, `Unknown skill guide: ${name || '(none)'}. Known: ${known.join(', ')}`);
|
|
818
1131
|
}
|
|
819
1132
|
process.stdout.write(fs.readFileSync(file, 'utf8'));
|
|
820
1133
|
return 0;
|