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/LICENSE +21 -0
- package/README.md +545 -0
- package/STRATEGY.md +179 -0
- package/bin/cli.js +693 -0
- package/bin/mcp.js +27 -0
- package/bin/setup.js +469 -0
- package/package.json +42 -0
- package/src/delegate.js +343 -0
- package/src/index.js +35 -0
- package/src/manager.js +510 -0
- package/src/mcp-server.js +808 -0
- package/src/safety-net.js +170 -0
- package/src/store.js +130 -0
- package/src/stream-session.js +463 -0
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
|
+
});
|