claude-multi-session 1.0.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/bin/cli.js ADDED
@@ -0,0 +1,693 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ╔══════════════════════════════════════════════════════════════╗
5
+ * ║ claude-multi-session (cms) — CLI for multi-session control ║
6
+ * ╚══════════════════════════════════════════════════════════════╝
7
+ *
8
+ * Usage: cms <command> [options]
9
+ * or: npx claude-multi-session <command> [options]
10
+ *
11
+ * Commands:
12
+ * spawn Start a new streaming session
13
+ * send Send a message to an existing session
14
+ * resume Restore and resume a stopped/paused session
15
+ * pause Pause a session (keeps process alive)
16
+ * fork Branch off a session into a new one
17
+ * stop Gracefully stop a session
18
+ * kill Force kill a session
19
+ * status Show detailed session info
20
+ * output Get last response text
21
+ * list List all sessions
22
+ * history Show full interaction history
23
+ * delete Permanently remove a session
24
+ * batch Spawn multiple sessions from JSON file
25
+ * cleanup Remove old sessions
26
+ * delegate Smart task delegation with safety + control loop
27
+ * continue Send follow-up to a delegated session
28
+ * setup Register MCP server with Claude Code
29
+ * help Show this help
30
+ */
31
+
32
+ const { SessionManager, Delegate } = require('../src/index');
33
+ const fs = require('fs');
34
+ const path = require('path');
35
+
36
+ // ============================================================================
37
+ // Argument Parser
38
+ // ============================================================================
39
+ function parseArgs(argv) {
40
+ const args = argv.slice(2);
41
+ if (args.length === 0) return { command: 'help', flags: {} };
42
+
43
+ const command = args[0];
44
+ const flags = {};
45
+ let i = 1;
46
+
47
+ while (i < args.length) {
48
+ const arg = args[i];
49
+ if (arg.startsWith('--')) {
50
+ const key = arg.slice(2);
51
+ if (key.includes('=')) {
52
+ const [k, ...v] = key.split('=');
53
+ flags[k] = v.join('=');
54
+ } else if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
55
+ flags[key] = args[i + 1];
56
+ i++;
57
+ } else {
58
+ flags[key] = true;
59
+ }
60
+ }
61
+ i++;
62
+ }
63
+
64
+ return { command, flags };
65
+ }
66
+
67
+ // ============================================================================
68
+ // Utility Functions
69
+ // ============================================================================
70
+ function truncate(str, maxLen) {
71
+ if (!str) return '';
72
+ const single = str.replace(/\n/g, ' ').trim();
73
+ return single.length <= maxLen ? single : single.slice(0, maxLen - 3) + '...';
74
+ }
75
+
76
+ function timeAgo(iso) {
77
+ if (!iso) return 'unknown';
78
+ const diff = Date.now() - new Date(iso).getTime();
79
+ if (diff < 60000) return 'just now';
80
+ if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
81
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
82
+ return `${Math.floor(diff / 86400000)}d ago`;
83
+ }
84
+
85
+ function statusIcon(status) {
86
+ return {
87
+ created: '○', starting: '◐', ready: '●', busy: '⟳',
88
+ paused: '⏸', stopped: '□', killed: '⊘', failed: '✗', error: '✗',
89
+ completed: '✓',
90
+ }[status] || '?';
91
+ }
92
+
93
+ function printResponse(name, response) {
94
+ console.log(`\n=== ${name} ===`);
95
+ if (response.isError) console.log('Status: ERROR');
96
+ if (response.cost) console.log(`Cost: $${response.cost.toFixed(4)}`);
97
+ if (response.turns) console.log(`Turns: ${response.turns}`);
98
+ if (response.duration) console.log(`Duration: ${(response.duration / 1000).toFixed(1)}s`);
99
+ console.log('---');
100
+ console.log(response.text || '(no output)');
101
+ console.log('');
102
+ }
103
+
104
+ // ============================================================================
105
+ // Command Handlers
106
+ // ============================================================================
107
+ async function cmdSpawn(mgr, flags) {
108
+ if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
109
+ if (!flags.prompt) { console.error('Error: --prompt required'); process.exit(1); }
110
+
111
+ console.log(`Spawning "${flags.name}" (model: ${flags.model || 'sonnet'})...`);
112
+
113
+ const { session, response } = await mgr.spawn(flags.name, {
114
+ prompt: flags.prompt,
115
+ model: flags.model,
116
+ workDir: flags['work-dir'],
117
+ permissionMode: flags['permission-mode'],
118
+ allowedTools: flags['allowed-tools'] ? flags['allowed-tools'].split(',') : undefined,
119
+ systemPrompt: flags['system-prompt'],
120
+ maxBudget: flags['max-budget'] ? parseFloat(flags['max-budget']) : undefined,
121
+ agent: flags.agent,
122
+ });
123
+
124
+ if (response) {
125
+ printResponse(flags.name, response);
126
+ }
127
+
128
+ console.log(`Session "${flags.name}" is ready (ID: ${session.id})`);
129
+
130
+ // If --stop flag, stop session after first response (one-shot mode)
131
+ if (flags.stop) {
132
+ mgr.stop(flags.name);
133
+ console.log(`Session stopped. Resume later: cms resume --name ${flags.name}`);
134
+ } else {
135
+ console.log(`Send follow-up: cms send --name ${flags.name} --message "..."`);
136
+ console.log(`Session is alive and waiting. Stop with: cms stop --name ${flags.name}`);
137
+ }
138
+ }
139
+
140
+ async function cmdSend(mgr, flags) {
141
+ if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
142
+ if (!flags.message) { console.error('Error: --message required'); process.exit(1); }
143
+
144
+ // Check if session is alive — if not, auto-resume
145
+ const sessions = mgr.sessions;
146
+ if (!sessions.has(flags.name)) {
147
+ console.log(`Session "${flags.name}" not alive. Auto-resuming...`);
148
+ const response = await mgr.resume(flags.name, flags.message);
149
+ if (response) {
150
+ printResponse(flags.name, response);
151
+ }
152
+
153
+ // Stop after if requested
154
+ if (flags.stop) {
155
+ mgr.stop(flags.name);
156
+ console.log(`Session stopped.`);
157
+ }
158
+ return;
159
+ }
160
+
161
+ console.log(`Sending to "${flags.name}"...`);
162
+ const response = await mgr.send(flags.name, flags.message);
163
+ printResponse(flags.name, response);
164
+
165
+ if (flags.stop) {
166
+ mgr.stop(flags.name);
167
+ console.log(`Session stopped.`);
168
+ }
169
+ }
170
+
171
+ async function cmdResume(mgr, flags) {
172
+ if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
173
+
174
+ console.log(`Resuming "${flags.name}"...`);
175
+ const response = await mgr.resume(flags.name, flags.message);
176
+
177
+ if (response) {
178
+ printResponse(flags.name, response);
179
+ }
180
+
181
+ console.log(`Session "${flags.name}" is alive.`);
182
+ console.log(`Send: cms send --name ${flags.name} --message "..."`);
183
+ }
184
+
185
+ function cmdPause(mgr, flags) {
186
+ if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
187
+ mgr.pause(flags.name);
188
+ console.log(`Session "${flags.name}" paused.`);
189
+ }
190
+
191
+ async function cmdFork(mgr, flags) {
192
+ if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
193
+ if (!flags['new-name']) { console.error('Error: --new-name required'); process.exit(1); }
194
+
195
+ console.log(`Forking "${flags.name}" -> "${flags['new-name']}"...`);
196
+ const { session, response } = await mgr.fork(flags.name, flags['new-name'], {
197
+ message: flags.message || 'Continue from the forked conversation.',
198
+ model: flags.model,
199
+ });
200
+
201
+ if (response) {
202
+ printResponse(flags['new-name'], response);
203
+ }
204
+
205
+ console.log(`Forked session "${flags['new-name']}" is ready.`);
206
+ }
207
+
208
+ function cmdStop(mgr, flags) {
209
+ if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
210
+ mgr.stop(flags.name);
211
+ console.log(`Session "${flags.name}" stopped. Resume: cms resume --name ${flags.name}`);
212
+ }
213
+
214
+ function cmdKill(mgr, flags) {
215
+ if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
216
+ mgr.kill(flags.name);
217
+ console.log(`Session "${flags.name}" killed.`);
218
+ }
219
+
220
+ function cmdStatus(mgr, flags) {
221
+ if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
222
+
223
+ const info = mgr.status(flags.name);
224
+ console.log(`\n=== Session: ${info.name} ===`);
225
+ console.log(`Status: ${statusIcon(info.status)} ${info.status}`);
226
+ console.log(`Model: ${info.model}`);
227
+ console.log(`Session ID: ${info.claudeSessionId || info.id || 'unknown'}`);
228
+ console.log(`Work Dir: ${info.workDir}`);
229
+ console.log(`Cost: $${(info.totalCostUsd || 0).toFixed(4)}`);
230
+ console.log(`Turns: ${info.totalTurns || 0}`);
231
+ console.log(`Interactions: ${info.interactionCount || (info.interactions || []).length}`);
232
+ if (info.pid) console.log(`PID: ${info.pid}`);
233
+ console.log('');
234
+ }
235
+
236
+ function cmdOutput(mgr, flags) {
237
+ if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
238
+
239
+ const last = mgr.lastOutput(flags.name);
240
+ if (!last) {
241
+ console.log(`No output for "${flags.name}" yet.`);
242
+ return;
243
+ }
244
+
245
+ if (flags.full) {
246
+ console.log(last.response || '(no output)');
247
+ } else {
248
+ console.log(`\n[${flags.name}] (${last.timestamp || 'unknown'})`);
249
+ console.log(`Prompt: ${truncate(last.prompt, 100)}`);
250
+ console.log('---');
251
+ console.log(last.response || '(no output)');
252
+ console.log('');
253
+ }
254
+ }
255
+
256
+ function cmdList(mgr, flags) {
257
+ const sessions = mgr.list(flags.status);
258
+
259
+ if (sessions.length === 0) {
260
+ console.log(flags.status ? `No sessions with status "${flags.status}".` : 'No sessions.');
261
+ return;
262
+ }
263
+
264
+ sessions.sort((a, b) => (b.lastSaved || b.created || '') > (a.lastSaved || a.created || '') ? 1 : -1);
265
+
266
+ console.log(`\n${'Name'.padEnd(25)} ${'Status'.padEnd(12)} ${'Model'.padEnd(8)} ${'Turns'.padEnd(6)} ${'Cost'.padEnd(10)} ${'Interactions'}`);
267
+ console.log('-'.repeat(80));
268
+
269
+ for (const s of sessions) {
270
+ const icon = statusIcon(s.status);
271
+ const interactions = s.interactionCount || (s.interactions || []).length;
272
+ console.log(
273
+ `${truncate(s.name, 24).padEnd(25)} ` +
274
+ `${(icon + ' ' + s.status).padEnd(12)} ` +
275
+ `${(s.model || '?').padEnd(8)} ` +
276
+ `${String(s.totalTurns || 0).padEnd(6)} ` +
277
+ `${'$' + (s.totalCostUsd || 0).toFixed(4).padEnd(9)} ` +
278
+ `${interactions}`
279
+ );
280
+ }
281
+
282
+ console.log(`\nTotal: ${sessions.length}`);
283
+ console.log('');
284
+ }
285
+
286
+ function cmdHistory(mgr, flags) {
287
+ if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
288
+
289
+ const hist = mgr.history(flags.name);
290
+ if (hist.length === 0) {
291
+ console.log(`No history for "${flags.name}".`);
292
+ return;
293
+ }
294
+
295
+ console.log(`\n=== History: ${flags.name} (${hist.length} interactions) ===\n`);
296
+
297
+ hist.forEach((h, i) => {
298
+ console.log(`--- #${i} ${h.type || 'send'} — ${h.timestamp || 'unknown'} ---`);
299
+ console.log(`PROMPT: ${flags.full ? h.prompt : truncate(h.prompt, 200)}`);
300
+ console.log(`RESPONSE: ${flags.full ? h.response : truncate(h.response, 300)}`);
301
+ if (h.cost) console.log(`COST: $${h.cost.toFixed(4)}`);
302
+ if (h.duration) console.log(`DURATION: ${(h.duration / 1000).toFixed(1)}s`);
303
+ console.log('');
304
+ });
305
+ }
306
+
307
+ function cmdDelete(mgr, flags) {
308
+ if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
309
+ mgr.delete(flags.name);
310
+ console.log(`Session "${flags.name}" deleted permanently.`);
311
+ }
312
+
313
+ async function cmdBatch(mgr, flags) {
314
+ if (!flags.file) { console.error('Error: --file required (JSON file path)'); process.exit(1); }
315
+
316
+ const filePath = path.resolve(flags.file);
317
+ if (!fs.existsSync(filePath)) {
318
+ console.error(`File not found: ${filePath}`);
319
+ process.exit(1);
320
+ }
321
+
322
+ let specs;
323
+ try { specs = JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch (e) {
324
+ console.error(`Invalid JSON: ${e.message}`);
325
+ process.exit(1);
326
+ }
327
+
328
+ if (!Array.isArray(specs)) {
329
+ console.error('JSON must be an array of session specs.');
330
+ process.exit(1);
331
+ }
332
+
333
+ console.log(`Spawning ${specs.length} sessions in parallel...`);
334
+ const results = await mgr.batch(specs);
335
+
336
+ for (const r of results) {
337
+ if (r.error) {
338
+ console.log(` ✗ ${r.session?.name || 'unknown'}: ${r.error}`);
339
+ } else {
340
+ const resp = r.response;
341
+ console.log(` ✓ ${r.session.name}: ${resp ? truncate(resp.text, 80) : 'started (no prompt)'}`);
342
+ }
343
+ }
344
+
345
+ console.log(`\nDone. List all: cms list`);
346
+
347
+ // Stop all after batch unless --keep-alive
348
+ if (!flags['keep-alive']) {
349
+ mgr.stopAll();
350
+ console.log('All sessions stopped. Resume individually: cms resume --name <name>');
351
+ }
352
+ }
353
+
354
+ // ============================================================================
355
+ // Delegate Commands (Control Loop + Safety Net)
356
+ // ============================================================================
357
+
358
+ /**
359
+ * DELEGATE — Smart task delegation with safety limits and auto-permission handling.
360
+ *
361
+ * Required: --name, --task
362
+ * Optional: --model, --preset, --max-cost, --max-turns, --context, --work-dir, --json
363
+ */
364
+ async function cmdDelegate(mgr, flags) {
365
+ if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
366
+ if (!flags.task) { console.error('Error: --task required'); process.exit(1); }
367
+
368
+ const delegate = new Delegate(mgr);
369
+
370
+ const preset = flags.preset || 'edit';
371
+ const maxCost = flags['max-cost'] ? parseFloat(flags['max-cost']) : 2.00;
372
+ const maxTurns = flags['max-turns'] ? parseInt(flags['max-turns']) : 50;
373
+
374
+ if (!flags.json) {
375
+ console.log(`Delegating to "${flags.name}" (model: ${flags.model || 'sonnet'}, preset: ${preset}, max: $${maxCost.toFixed(2)})...`);
376
+ }
377
+
378
+ // --no-safety flag disables the safety net entirely
379
+ const useSafety = !flags['no-safety'];
380
+
381
+ const result = await delegate.run(flags.name, {
382
+ task: flags.task,
383
+ model: flags.model,
384
+ preset: preset,
385
+ workDir: flags['work-dir'],
386
+ maxCost: maxCost,
387
+ maxTurns: maxTurns,
388
+ context: flags.context,
389
+ systemPrompt: flags['system-prompt'],
390
+ agent: flags.agent,
391
+ safety: useSafety,
392
+ });
393
+
394
+ // Stop session after delegation (can be resumed)
395
+ delegate.finish(flags.name);
396
+
397
+ if (flags.json) {
398
+ // Output as JSON for programmatic consumption by parent Claude
399
+ console.log(JSON.stringify(result, null, 2));
400
+ } else {
401
+ printDelegateResult(result);
402
+ }
403
+ }
404
+
405
+ /**
406
+ * CONTINUE — Send follow-up to a delegated session with safety limits.
407
+ *
408
+ * Required: --name, --message
409
+ * Optional: --json
410
+ */
411
+ async function cmdContinue(mgr, flags) {
412
+ if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
413
+ if (!flags.message) { console.error('Error: --message required'); process.exit(1); }
414
+
415
+ const delegate = new Delegate(mgr);
416
+
417
+ if (!flags.json) {
418
+ console.log(`Continuing "${flags.name}"...`);
419
+ }
420
+
421
+ const result = await delegate.continue(flags.name, flags.message);
422
+
423
+ // Stop after
424
+ delegate.finish(flags.name);
425
+
426
+ if (flags.json) {
427
+ console.log(JSON.stringify(result, null, 2));
428
+ } else {
429
+ printDelegateResult(result);
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Print a DelegateResult in human-readable format.
435
+ */
436
+ function printDelegateResult(result) {
437
+ const statusEmoji = {
438
+ completed: '✓',
439
+ needs_input: '?',
440
+ failed: '✗',
441
+ cost_exceeded: '$',
442
+ turns_exceeded: '#',
443
+ protected_path: '!',
444
+ }[result.status] || '?';
445
+
446
+ console.log(`\n${statusEmoji} Status: ${result.status.toUpperCase()}`);
447
+ if (result.error) console.log(` Error: ${result.error}`);
448
+ if (result.cost) console.log(` Cost: $${result.cost.toFixed(4)}`);
449
+ if (result.turns) console.log(` Turns: ${result.turns}`);
450
+ if (result.duration) console.log(` Duration: ${(result.duration / 1000).toFixed(1)}s`);
451
+ if (result.toolsUsed.length > 0) console.log(` Tools: ${[...new Set(result.toolsUsed)].join(', ')}`);
452
+ console.log(` Can continue: ${result.canContinue ? 'yes' : 'no'}`);
453
+
454
+ if (result.safety && result.safety.totalViolations > 0) {
455
+ console.log(` Safety violations: ${result.safety.totalViolations}`);
456
+ result.safety.violations.forEach(v => console.log(` - ${v.message}`));
457
+ }
458
+
459
+ console.log(`\n--- Response ---`);
460
+ console.log(result.response || '(no output)');
461
+ console.log('');
462
+
463
+ if (result.canContinue) {
464
+ console.log(`Continue: cms continue --name ${result.name} --message "..."`);
465
+ }
466
+ }
467
+
468
+ // ============================================================================
469
+ // Setup Command
470
+ // ============================================================================
471
+
472
+ /**
473
+ * SETUP — Register the MCP server with Claude Code settings.
474
+ * Delegates to bin/setup.js which handles the interactive wizard.
475
+ */
476
+ async function cmdSetup(flags) {
477
+ const { run } = require('./setup');
478
+ await run(flags);
479
+ }
480
+
481
+ function cmdCleanup(mgr, flags) {
482
+ const days = flags.days ? parseInt(flags.days) : 7;
483
+ const removed = mgr.cleanup(days);
484
+ if (removed.length === 0) {
485
+ console.log(`No sessions older than ${days} days to clean.`);
486
+ } else {
487
+ console.log(`Removed ${removed.length} sessions: ${removed.join(', ')}`);
488
+ }
489
+ }
490
+
491
+ function cmdHelp() {
492
+ console.log(`
493
+ ╔══════════════════════════════════════════════════════════════╗
494
+ ║ claude-multi-session (cms) — Multi-Session Orchestrator ║
495
+ ║ Streaming-powered session management for Claude Code CLI ║
496
+ ╚══════════════════════════════════════════════════════════════╝
497
+
498
+ COMMANDS:
499
+
500
+ spawn Start a new streaming session (keeps process alive)
501
+ --name <name> --prompt <text>
502
+ [--model sonnet|opus|haiku] [--stop]
503
+ [--work-dir <path>] [--permission-mode <mode>]
504
+ [--allowed-tools <t1,t2>] [--system-prompt <text>]
505
+ [--max-budget <usd>] [--agent <name>]
506
+
507
+ send Send a follow-up message (auto-resumes if stopped)
508
+ --name <name> --message <text> [--stop]
509
+
510
+ resume Restore a stopped/paused session
511
+ --name <name> [--message <text>]
512
+
513
+ pause Pause session (process stays alive, no messages accepted)
514
+ --name <name>
515
+
516
+ fork Branch off a session into a new one
517
+ --name <source> --new-name <target>
518
+ [--message <text>] [--model <model>]
519
+
520
+ stop Gracefully stop (saves state, can resume later)
521
+ --name <name>
522
+
523
+ kill Force kill a session
524
+ --name <name>
525
+
526
+ status Show detailed session info
527
+ --name <name>
528
+
529
+ output Get last response text
530
+ --name <name> [--full]
531
+
532
+ list List all sessions
533
+ [--status ready|paused|stopped|killed]
534
+
535
+ history Show full interaction history
536
+ --name <name> [--full]
537
+
538
+ delete Permanently remove a session
539
+ --name <name>
540
+
541
+ batch Spawn multiple sessions from JSON file
542
+ --file <path> [--keep-alive]
543
+
544
+ cleanup Remove old sessions
545
+ [--days <n>]
546
+
547
+ delegate Smart task delegation with safety limits & auto-permission
548
+ --name <name> --task <text>
549
+ [--model sonnet|opus|haiku] [--preset read-only|review|edit|full|plan]
550
+ [--max-cost <usd>] [--max-turns <n>] [--no-safety]
551
+ [--context <text>] [--work-dir <path>]
552
+ [--system-prompt <text>] [--agent <name>] [--json]
553
+
554
+ continue Send follow-up to a delegated session
555
+ --name <name> --message <text> [--json]
556
+
557
+ setup Register MCP server with Claude Code
558
+ [--global] [--local] [--uninstall]
559
+ [--guide] [--no-guide]
560
+
561
+ HOW IT WORKS:
562
+
563
+ Unlike the resume approach (new process per message), this system
564
+ uses Claude's stream-json protocol to keep ONE process alive per
565
+ session. Messages are piped in/out via stdin/stdout.
566
+
567
+ Benefits:
568
+ • No process restart overhead (~2-3s saved per message)
569
+ • No context reload (lower token cost)
570
+ • Instant follow-up responses
571
+ • True multi-turn conversations
572
+
573
+ Sessions are persisted to ~/.claude-multi-session/ and can be
574
+ resumed even after the manager process exits (using --resume).
575
+
576
+ EXAMPLES:
577
+
578
+ # Start a session, send prompt, keep alive for follow-ups
579
+ cms spawn --name fix-auth --prompt "Fix the auth bug in auth.service.ts"
580
+ cms send --name fix-auth --message "Also add input validation"
581
+ cms send --name fix-auth --message "Now write tests"
582
+ cms stop --name fix-auth
583
+
584
+ # One-shot: spawn, get result, stop automatically
585
+ cms spawn --name quick-task --prompt "Count TypeScript files" --stop
586
+
587
+ # Resume a stopped session days later
588
+ cms resume --name fix-auth --message "Actually, also handle edge cases"
589
+
590
+ # Fork to try a different approach
591
+ cms fork --name fix-auth --new-name fix-auth-v2 --message "Try JWT instead"
592
+
593
+ # Parallel batch
594
+ cms batch --file tasks.json
595
+
596
+ # Send to stopped session (auto-resumes)
597
+ cms send --name fix-auth --message "Add error handling"
598
+
599
+ DELEGATE (Control Loop + Safety Net):
600
+
601
+ # Delegate a task with $1 cost limit, auto-permission handling
602
+ cms delegate --name fix-bug --task "Fix auth bug in auth.service.ts" --max-cost 1.00
603
+
604
+ # Delegate with read-only preset (no file edits allowed)
605
+ cms delegate --name review --task "Review code quality" --preset read-only
606
+
607
+ # Get structured JSON output (for programmatic use by parent Claude)
608
+ cms delegate --name task-1 --task "Count files" --model haiku --json
609
+
610
+ # Send follow-up to a delegated session
611
+ cms continue --name fix-bug --message "Also add input validation"
612
+
613
+ # Full control: delegate with context and custom limits
614
+ cms delegate --name big-task --task "Refactor auth" --model opus \\
615
+ --preset full --max-cost 5.00 --max-turns 100 \\
616
+ --context "Use JWT, not sessions"
617
+
618
+ PRESETS (for delegate):
619
+
620
+ read-only Can search/read files, no edits
621
+ review Same as read-only (code review mode)
622
+ edit Can read and edit files (default)
623
+ full Full access (use with caution)
624
+ plan Explore only, no execution
625
+
626
+ INSTALL:
627
+
628
+ npm install -g claude-multi-session
629
+ cms setup # Register MCP server with Claude Code
630
+ # Then use: cms <command>
631
+ # Or: npx claude-multi-session <command>
632
+ `);
633
+ }
634
+
635
+ // ============================================================================
636
+ // Main
637
+ // ============================================================================
638
+ async function main() {
639
+ const { command, flags } = parseArgs(process.argv);
640
+
641
+ // Create manager (shared across commands)
642
+ const mgr = new SessionManager();
643
+
644
+ const commands = {
645
+ spawn: () => cmdSpawn(mgr, flags),
646
+ send: () => cmdSend(mgr, flags),
647
+ input: () => cmdSend(mgr, flags), // Alias
648
+ resume: () => cmdResume(mgr, flags),
649
+ pause: () => cmdPause(mgr, flags),
650
+ fork: () => cmdFork(mgr, flags),
651
+ stop: () => cmdStop(mgr, flags),
652
+ kill: () => cmdKill(mgr, flags),
653
+ status: () => cmdStatus(mgr, flags),
654
+ output: () => cmdOutput(mgr, flags),
655
+ list: () => cmdList(mgr, flags),
656
+ history: () => cmdHistory(mgr, flags),
657
+ delete: () => cmdDelete(mgr, flags),
658
+ batch: () => cmdBatch(mgr, flags),
659
+ cleanup: () => cmdCleanup(mgr, flags),
660
+ delegate: () => cmdDelegate(mgr, flags),
661
+ continue: () => cmdContinue(mgr, flags),
662
+ setup: () => cmdSetup(flags),
663
+ help: () => cmdHelp(),
664
+ };
665
+
666
+ if (!commands[command]) {
667
+ console.error(`Unknown command: "${command}". Run "cms help" for usage.`);
668
+ process.exit(1);
669
+ }
670
+
671
+ try {
672
+ await commands[command]();
673
+ } catch (err) {
674
+ console.error(`Error: ${err.message}`);
675
+ process.exit(1);
676
+ }
677
+
678
+ // If any sessions are still alive, keep the process running
679
+ // so streaming sessions don't get killed
680
+ if (mgr.sessions.size > 0 && !['list', 'status', 'output', 'history', 'help', 'cleanup'].includes(command)) {
681
+ // Keep process alive until stopped
682
+ if (['spawn', 'send', 'resume', 'fork'].includes(command) && !flags.stop) {
683
+ // Don't hold the process — sessions survive in background
684
+ // We stop them gracefully and they can be resumed
685
+ mgr.stopAll();
686
+ }
687
+ }
688
+ }
689
+
690
+ main().catch(err => {
691
+ console.error(`Fatal: ${err.message}`);
692
+ process.exit(1);
693
+ });