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