clearctx 3.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/CHANGELOG.md +71 -0
- package/LICENSE +21 -0
- package/README.md +1006 -0
- package/STRATEGY.md +485 -0
- package/bin/cli.js +1756 -0
- package/bin/continuity-hook.js +118 -0
- package/bin/mcp.js +27 -0
- package/bin/setup.js +929 -0
- package/package.json +56 -0
- package/src/artifact-store.js +710 -0
- package/src/atomic-io.js +99 -0
- package/src/briefing-generator.js +451 -0
- package/src/continuity-hooks.js +253 -0
- package/src/contract-store.js +525 -0
- package/src/decision-journal.js +229 -0
- package/src/delegate.js +348 -0
- package/src/dependency-resolver.js +453 -0
- package/src/diff-engine.js +473 -0
- package/src/file-lock.js +161 -0
- package/src/index.js +61 -0
- package/src/lineage-graph.js +402 -0
- package/src/manager.js +510 -0
- package/src/mcp-server.js +3501 -0
- package/src/pattern-registry.js +221 -0
- package/src/pipeline-engine.js +618 -0
- package/src/prompts.js +1217 -0
- package/src/safety-net.js +170 -0
- package/src/session-snapshot.js +508 -0
- package/src/snapshot-engine.js +490 -0
- package/src/stale-detector.js +169 -0
- package/src/store.js +131 -0
- package/src/stream-session.js +463 -0
- package/src/team-hub.js +615 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,1756 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ╔══════════════════════════════════════════════════════════════╗
|
|
5
|
+
* ║ clearctx (cms) — CLI for multi-session control ║
|
|
6
|
+
* ╚══════════════════════════════════════════════════════════════╝
|
|
7
|
+
*
|
|
8
|
+
* Usage: clearctx <command> [options]
|
|
9
|
+
* or: npx clearctx <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
|
+
* team-roster Show team members
|
|
29
|
+
* team-send Send message to teammate
|
|
30
|
+
* team-broadcast Send to all teammates
|
|
31
|
+
* team-inbox Check inbox
|
|
32
|
+
* artifact-publish Publish artifact
|
|
33
|
+
* artifact-get Read artifact
|
|
34
|
+
* artifact-list List artifacts
|
|
35
|
+
* contract-create Create contract
|
|
36
|
+
* contract-list List contracts
|
|
37
|
+
* contract-start Start contract
|
|
38
|
+
* pipeline-create Create pipeline
|
|
39
|
+
* pipeline-list List pipelines
|
|
40
|
+
* snapshot-create Create snapshot
|
|
41
|
+
* snapshot-list List snapshots
|
|
42
|
+
* snapshot-rollback Rollback to snapshot
|
|
43
|
+
* setup Register MCP server with Claude Code
|
|
44
|
+
* help Show this help
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
const { SessionManager, Delegate } = require('../src/index');
|
|
48
|
+
const TeamHub = require('../src/team-hub');
|
|
49
|
+
const ArtifactStore = require('../src/artifact-store');
|
|
50
|
+
const ContractStore = require('../src/contract-store');
|
|
51
|
+
const PipelineEngine = require('../src/pipeline-engine');
|
|
52
|
+
const SnapshotEngine = require('../src/snapshot-engine');
|
|
53
|
+
const SessionSnapshot = require('../src/session-snapshot');
|
|
54
|
+
const BriefingGenerator = require('../src/briefing-generator');
|
|
55
|
+
const StaleDetector = require('../src/stale-detector');
|
|
56
|
+
const DecisionJournal = require('../src/decision-journal');
|
|
57
|
+
const PatternRegistry = require('../src/pattern-registry');
|
|
58
|
+
const fs = require('fs');
|
|
59
|
+
const path = require('path');
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Argument Parser
|
|
63
|
+
// ============================================================================
|
|
64
|
+
function parseArgs(argv) {
|
|
65
|
+
const args = argv.slice(2);
|
|
66
|
+
if (args.length === 0) return { command: 'help', flags: {} };
|
|
67
|
+
|
|
68
|
+
const command = args[0];
|
|
69
|
+
const flags = {};
|
|
70
|
+
let i = 1;
|
|
71
|
+
|
|
72
|
+
while (i < args.length) {
|
|
73
|
+
const arg = args[i];
|
|
74
|
+
if (arg.startsWith('--')) {
|
|
75
|
+
const key = arg.slice(2);
|
|
76
|
+
if (key.includes('=')) {
|
|
77
|
+
const [k, ...v] = key.split('=');
|
|
78
|
+
flags[k] = v.join('=');
|
|
79
|
+
} else if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
80
|
+
flags[key] = args[i + 1];
|
|
81
|
+
i++;
|
|
82
|
+
} else {
|
|
83
|
+
flags[key] = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { command, flags };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Utility Functions
|
|
94
|
+
// ============================================================================
|
|
95
|
+
function truncate(str, maxLen) {
|
|
96
|
+
if (!str) return '';
|
|
97
|
+
const single = str.replace(/\n/g, ' ').trim();
|
|
98
|
+
return single.length <= maxLen ? single : single.slice(0, maxLen - 3) + '...';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function timeAgo(iso) {
|
|
102
|
+
if (!iso) return 'unknown';
|
|
103
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
104
|
+
if (diff < 60000) return 'just now';
|
|
105
|
+
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
|
106
|
+
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
|
107
|
+
return `${Math.floor(diff / 86400000)}d ago`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function statusIcon(status) {
|
|
111
|
+
return {
|
|
112
|
+
created: '○', starting: '◐', ready: '●', busy: '⟳',
|
|
113
|
+
paused: '⏸', stopped: '□', killed: '⊘', failed: '✗', error: '✗',
|
|
114
|
+
completed: '✓',
|
|
115
|
+
}[status] || '?';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function printResponse(name, response) {
|
|
119
|
+
console.log(`\n=== ${name} ===`);
|
|
120
|
+
if (response.isError) console.log('Status: ERROR');
|
|
121
|
+
if (response.cost) console.log(`Cost: $${response.cost.toFixed(4)}`);
|
|
122
|
+
if (response.turns) console.log(`Turns: ${response.turns}`);
|
|
123
|
+
if (response.duration) console.log(`Duration: ${(response.duration / 1000).toFixed(1)}s`);
|
|
124
|
+
console.log('---');
|
|
125
|
+
console.log(response.text || '(no output)');
|
|
126
|
+
console.log('');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// Command Handlers
|
|
131
|
+
// ============================================================================
|
|
132
|
+
async function cmdSpawn(mgr, flags) {
|
|
133
|
+
if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
|
|
134
|
+
if (!flags.prompt) { console.error('Error: --prompt required'); process.exit(1); }
|
|
135
|
+
|
|
136
|
+
console.log(`Spawning "${flags.name}" (model: ${flags.model || 'sonnet'})...`);
|
|
137
|
+
|
|
138
|
+
const { session, response } = await mgr.spawn(flags.name, {
|
|
139
|
+
prompt: flags.prompt,
|
|
140
|
+
model: flags.model,
|
|
141
|
+
workDir: flags['work-dir'],
|
|
142
|
+
permissionMode: flags['permission-mode'],
|
|
143
|
+
allowedTools: flags['allowed-tools'] ? flags['allowed-tools'].split(',') : undefined,
|
|
144
|
+
systemPrompt: flags['system-prompt'],
|
|
145
|
+
maxBudget: flags['max-budget'] ? parseFloat(flags['max-budget']) : undefined,
|
|
146
|
+
agent: flags.agent,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (response) {
|
|
150
|
+
printResponse(flags.name, response);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.log(`Session "${flags.name}" is ready (ID: ${session.id})`);
|
|
154
|
+
|
|
155
|
+
// If --stop flag, stop session after first response (one-shot mode)
|
|
156
|
+
if (flags.stop) {
|
|
157
|
+
mgr.stop(flags.name);
|
|
158
|
+
console.log(`Session stopped. Resume later: clearctx resume --name ${flags.name}`);
|
|
159
|
+
} else {
|
|
160
|
+
console.log(`Send follow-up: clearctx send --name ${flags.name} --message "..."`);
|
|
161
|
+
console.log(`Session is alive and waiting. Stop with: clearctx stop --name ${flags.name}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function cmdSend(mgr, flags) {
|
|
166
|
+
if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
|
|
167
|
+
if (!flags.message) { console.error('Error: --message required'); process.exit(1); }
|
|
168
|
+
|
|
169
|
+
// Check if session is alive — if not, auto-resume
|
|
170
|
+
const sessions = mgr.sessions;
|
|
171
|
+
if (!sessions.has(flags.name)) {
|
|
172
|
+
console.log(`Session "${flags.name}" not alive. Auto-resuming...`);
|
|
173
|
+
const response = await mgr.resume(flags.name, flags.message);
|
|
174
|
+
if (response) {
|
|
175
|
+
printResponse(flags.name, response);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Stop after if requested
|
|
179
|
+
if (flags.stop) {
|
|
180
|
+
mgr.stop(flags.name);
|
|
181
|
+
console.log(`Session stopped.`);
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
console.log(`Sending to "${flags.name}"...`);
|
|
187
|
+
const response = await mgr.send(flags.name, flags.message);
|
|
188
|
+
printResponse(flags.name, response);
|
|
189
|
+
|
|
190
|
+
if (flags.stop) {
|
|
191
|
+
mgr.stop(flags.name);
|
|
192
|
+
console.log(`Session stopped.`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function cmdResume(mgr, flags) {
|
|
197
|
+
if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
|
|
198
|
+
|
|
199
|
+
console.log(`Resuming "${flags.name}"...`);
|
|
200
|
+
const response = await mgr.resume(flags.name, flags.message);
|
|
201
|
+
|
|
202
|
+
if (response) {
|
|
203
|
+
printResponse(flags.name, response);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(`Session "${flags.name}" is alive.`);
|
|
207
|
+
console.log(`Send: clearctx send --name ${flags.name} --message "..."`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function cmdPause(mgr, flags) {
|
|
211
|
+
if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
|
|
212
|
+
mgr.pause(flags.name);
|
|
213
|
+
console.log(`Session "${flags.name}" paused.`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function cmdFork(mgr, flags) {
|
|
217
|
+
if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
|
|
218
|
+
if (!flags['new-name']) { console.error('Error: --new-name required'); process.exit(1); }
|
|
219
|
+
|
|
220
|
+
console.log(`Forking "${flags.name}" -> "${flags['new-name']}"...`);
|
|
221
|
+
const { session, response } = await mgr.fork(flags.name, flags['new-name'], {
|
|
222
|
+
message: flags.message || 'Continue from the forked conversation.',
|
|
223
|
+
model: flags.model,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (response) {
|
|
227
|
+
printResponse(flags['new-name'], response);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log(`Forked session "${flags['new-name']}" is ready.`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function cmdStop(mgr, flags) {
|
|
234
|
+
if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
|
|
235
|
+
mgr.stop(flags.name);
|
|
236
|
+
console.log(`Session "${flags.name}" stopped. Resume: clearctx resume --name ${flags.name}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function cmdKill(mgr, flags) {
|
|
240
|
+
if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
|
|
241
|
+
mgr.kill(flags.name);
|
|
242
|
+
console.log(`Session "${flags.name}" killed.`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function cmdStatus(mgr, flags) {
|
|
246
|
+
if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
|
|
247
|
+
|
|
248
|
+
const info = mgr.status(flags.name);
|
|
249
|
+
console.log(`\n=== Session: ${info.name} ===`);
|
|
250
|
+
console.log(`Status: ${statusIcon(info.status)} ${info.status}`);
|
|
251
|
+
console.log(`Model: ${info.model}`);
|
|
252
|
+
console.log(`Session ID: ${info.claudeSessionId || info.id || 'unknown'}`);
|
|
253
|
+
console.log(`Work Dir: ${info.workDir}`);
|
|
254
|
+
console.log(`Cost: $${(info.totalCostUsd || 0).toFixed(4)}`);
|
|
255
|
+
console.log(`Turns: ${info.totalTurns || 0}`);
|
|
256
|
+
console.log(`Interactions: ${info.interactionCount || (info.interactions || []).length}`);
|
|
257
|
+
if (info.pid) console.log(`PID: ${info.pid}`);
|
|
258
|
+
console.log('');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function cmdOutput(mgr, flags) {
|
|
262
|
+
if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
|
|
263
|
+
|
|
264
|
+
const last = mgr.lastOutput(flags.name);
|
|
265
|
+
if (!last) {
|
|
266
|
+
console.log(`No output for "${flags.name}" yet.`);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (flags.full) {
|
|
271
|
+
console.log(last.response || '(no output)');
|
|
272
|
+
} else {
|
|
273
|
+
console.log(`\n[${flags.name}] (${last.timestamp || 'unknown'})`);
|
|
274
|
+
console.log(`Prompt: ${truncate(last.prompt, 100)}`);
|
|
275
|
+
console.log('---');
|
|
276
|
+
console.log(last.response || '(no output)');
|
|
277
|
+
console.log('');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function cmdList(mgr, flags) {
|
|
282
|
+
const sessions = mgr.list(flags.status);
|
|
283
|
+
|
|
284
|
+
if (sessions.length === 0) {
|
|
285
|
+
console.log(flags.status ? `No sessions with status "${flags.status}".` : 'No sessions.');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
sessions.sort((a, b) => (b.lastSaved || b.created || '') > (a.lastSaved || a.created || '') ? 1 : -1);
|
|
290
|
+
|
|
291
|
+
console.log(`\n${'Name'.padEnd(25)} ${'Status'.padEnd(12)} ${'Model'.padEnd(8)} ${'Turns'.padEnd(6)} ${'Cost'.padEnd(10)} ${'Interactions'}`);
|
|
292
|
+
console.log('-'.repeat(80));
|
|
293
|
+
|
|
294
|
+
for (const s of sessions) {
|
|
295
|
+
const icon = statusIcon(s.status);
|
|
296
|
+
const interactions = s.interactionCount || (s.interactions || []).length;
|
|
297
|
+
console.log(
|
|
298
|
+
`${truncate(s.name, 24).padEnd(25)} ` +
|
|
299
|
+
`${(icon + ' ' + s.status).padEnd(12)} ` +
|
|
300
|
+
`${(s.model || '?').padEnd(8)} ` +
|
|
301
|
+
`${String(s.totalTurns || 0).padEnd(6)} ` +
|
|
302
|
+
`${'$' + (s.totalCostUsd || 0).toFixed(4).padEnd(9)} ` +
|
|
303
|
+
`${interactions}`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
console.log(`\nTotal: ${sessions.length}`);
|
|
308
|
+
console.log('');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function cmdHistory(mgr, flags) {
|
|
312
|
+
if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
|
|
313
|
+
|
|
314
|
+
const hist = mgr.history(flags.name);
|
|
315
|
+
if (hist.length === 0) {
|
|
316
|
+
console.log(`No history for "${flags.name}".`);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
console.log(`\n=== History: ${flags.name} (${hist.length} interactions) ===\n`);
|
|
321
|
+
|
|
322
|
+
hist.forEach((h, i) => {
|
|
323
|
+
console.log(`--- #${i} ${h.type || 'send'} — ${h.timestamp || 'unknown'} ---`);
|
|
324
|
+
console.log(`PROMPT: ${flags.full ? h.prompt : truncate(h.prompt, 200)}`);
|
|
325
|
+
console.log(`RESPONSE: ${flags.full ? h.response : truncate(h.response, 300)}`);
|
|
326
|
+
if (h.cost) console.log(`COST: $${h.cost.toFixed(4)}`);
|
|
327
|
+
if (h.duration) console.log(`DURATION: ${(h.duration / 1000).toFixed(1)}s`);
|
|
328
|
+
console.log('');
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function cmdDelete(mgr, flags) {
|
|
333
|
+
if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
|
|
334
|
+
mgr.delete(flags.name);
|
|
335
|
+
console.log(`Session "${flags.name}" deleted permanently.`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function cmdBatch(mgr, flags) {
|
|
339
|
+
if (!flags.file) { console.error('Error: --file required (JSON file path)'); process.exit(1); }
|
|
340
|
+
|
|
341
|
+
const filePath = path.resolve(flags.file);
|
|
342
|
+
if (!fs.existsSync(filePath)) {
|
|
343
|
+
console.error(`File not found: ${filePath}`);
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let specs;
|
|
348
|
+
try { specs = JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch (e) {
|
|
349
|
+
console.error(`Invalid JSON: ${e.message}`);
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (!Array.isArray(specs)) {
|
|
354
|
+
console.error('JSON must be an array of session specs.');
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
console.log(`Spawning ${specs.length} sessions in parallel...`);
|
|
359
|
+
const results = await mgr.batch(specs);
|
|
360
|
+
|
|
361
|
+
for (const r of results) {
|
|
362
|
+
if (r.error) {
|
|
363
|
+
console.log(` ✗ ${r.session?.name || 'unknown'}: ${r.error}`);
|
|
364
|
+
} else {
|
|
365
|
+
const resp = r.response;
|
|
366
|
+
console.log(` ✓ ${r.session.name}: ${resp ? truncate(resp.text, 80) : 'started (no prompt)'}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
console.log(`\nDone. List all: clearctx list`);
|
|
371
|
+
|
|
372
|
+
// Stop all after batch unless --keep-alive
|
|
373
|
+
if (!flags['keep-alive']) {
|
|
374
|
+
mgr.stopAll();
|
|
375
|
+
console.log('All sessions stopped. Resume individually: clearctx resume --name <name>');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ============================================================================
|
|
380
|
+
// Delegate Commands (Control Loop + Safety Net)
|
|
381
|
+
// ============================================================================
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* DELEGATE — Smart task delegation with safety limits and auto-permission handling.
|
|
385
|
+
*
|
|
386
|
+
* Required: --name, --task
|
|
387
|
+
* Optional: --model, --preset, --max-cost, --max-turns, --context, --work-dir, --json
|
|
388
|
+
*/
|
|
389
|
+
async function cmdDelegate(mgr, flags) {
|
|
390
|
+
if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
|
|
391
|
+
if (!flags.task) { console.error('Error: --task required'); process.exit(1); }
|
|
392
|
+
|
|
393
|
+
const delegate = new Delegate(mgr);
|
|
394
|
+
|
|
395
|
+
const preset = flags.preset || 'edit';
|
|
396
|
+
const maxCost = flags['max-cost'] ? parseFloat(flags['max-cost']) : 2.00;
|
|
397
|
+
const maxTurns = flags['max-turns'] ? parseInt(flags['max-turns']) : 50;
|
|
398
|
+
|
|
399
|
+
if (!flags.json) {
|
|
400
|
+
console.log(`Delegating to "${flags.name}" (model: ${flags.model || 'sonnet'}, preset: ${preset}, max: $${maxCost.toFixed(2)})...`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// --no-safety flag disables the safety net entirely
|
|
404
|
+
const useSafety = !flags['no-safety'];
|
|
405
|
+
|
|
406
|
+
const result = await delegate.run(flags.name, {
|
|
407
|
+
task: flags.task,
|
|
408
|
+
model: flags.model,
|
|
409
|
+
preset: preset,
|
|
410
|
+
workDir: flags['work-dir'],
|
|
411
|
+
maxCost: maxCost,
|
|
412
|
+
maxTurns: maxTurns,
|
|
413
|
+
context: flags.context,
|
|
414
|
+
systemPrompt: flags['system-prompt'],
|
|
415
|
+
agent: flags.agent,
|
|
416
|
+
safety: useSafety,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Stop session after delegation (can be resumed)
|
|
420
|
+
delegate.finish(flags.name);
|
|
421
|
+
|
|
422
|
+
if (flags.json) {
|
|
423
|
+
// Output as JSON for programmatic consumption by parent Claude
|
|
424
|
+
console.log(JSON.stringify(result, null, 2));
|
|
425
|
+
} else {
|
|
426
|
+
printDelegateResult(result);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* CONTINUE — Send follow-up to a delegated session with safety limits.
|
|
432
|
+
*
|
|
433
|
+
* Required: --name, --message
|
|
434
|
+
* Optional: --json
|
|
435
|
+
*/
|
|
436
|
+
async function cmdContinue(mgr, flags) {
|
|
437
|
+
if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
|
|
438
|
+
if (!flags.message) { console.error('Error: --message required'); process.exit(1); }
|
|
439
|
+
|
|
440
|
+
const delegate = new Delegate(mgr);
|
|
441
|
+
|
|
442
|
+
if (!flags.json) {
|
|
443
|
+
console.log(`Continuing "${flags.name}"...`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const result = await delegate.continue(flags.name, flags.message);
|
|
447
|
+
|
|
448
|
+
// Stop after
|
|
449
|
+
delegate.finish(flags.name);
|
|
450
|
+
|
|
451
|
+
if (flags.json) {
|
|
452
|
+
console.log(JSON.stringify(result, null, 2));
|
|
453
|
+
} else {
|
|
454
|
+
printDelegateResult(result);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Print a DelegateResult in human-readable format.
|
|
460
|
+
*/
|
|
461
|
+
function printDelegateResult(result) {
|
|
462
|
+
const statusEmoji = {
|
|
463
|
+
completed: '✓',
|
|
464
|
+
needs_input: '?',
|
|
465
|
+
failed: '✗',
|
|
466
|
+
cost_exceeded: '$',
|
|
467
|
+
turns_exceeded: '#',
|
|
468
|
+
protected_path: '!',
|
|
469
|
+
}[result.status] || '?';
|
|
470
|
+
|
|
471
|
+
console.log(`\n${statusEmoji} Status: ${result.status.toUpperCase()}`);
|
|
472
|
+
if (result.error) console.log(` Error: ${result.error}`);
|
|
473
|
+
if (result.cost) console.log(` Cost: $${result.cost.toFixed(4)}`);
|
|
474
|
+
if (result.turns) console.log(` Turns: ${result.turns}`);
|
|
475
|
+
if (result.duration) console.log(` Duration: ${(result.duration / 1000).toFixed(1)}s`);
|
|
476
|
+
if (result.toolsUsed.length > 0) console.log(` Tools: ${[...new Set(result.toolsUsed)].join(', ')}`);
|
|
477
|
+
console.log(` Can continue: ${result.canContinue ? 'yes' : 'no'}`);
|
|
478
|
+
|
|
479
|
+
if (result.safety && result.safety.totalViolations > 0) {
|
|
480
|
+
console.log(` Safety violations: ${result.safety.totalViolations}`);
|
|
481
|
+
result.safety.violations.forEach(v => console.log(` - ${v.message}`));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
console.log(`\n--- Response ---`);
|
|
485
|
+
console.log(result.response || '(no output)');
|
|
486
|
+
console.log('');
|
|
487
|
+
|
|
488
|
+
if (result.canContinue) {
|
|
489
|
+
console.log(`Continue: clearctx continue --name ${result.name} --message "..."`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ============================================================================
|
|
494
|
+
// Setup Command
|
|
495
|
+
// ============================================================================
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* SETUP — Register the MCP server with Claude Code settings.
|
|
499
|
+
* Delegates to bin/setup.js which handles the interactive wizard.
|
|
500
|
+
*/
|
|
501
|
+
async function cmdSetup(flags) {
|
|
502
|
+
const { run } = require('./setup');
|
|
503
|
+
await run(flags);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function cmdCleanup(mgr, flags) {
|
|
507
|
+
const days = flags.days ? parseInt(flags.days) : 7;
|
|
508
|
+
const removed = mgr.cleanup(days);
|
|
509
|
+
if (removed.length === 0) {
|
|
510
|
+
console.log(`No sessions older than ${days} days to clean.`);
|
|
511
|
+
} else {
|
|
512
|
+
console.log(`Removed ${removed.length} sessions: ${removed.join(', ')}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ============================================================================
|
|
517
|
+
// Team/Artifact/Contract/Pipeline/Snapshot Commands
|
|
518
|
+
// ============================================================================
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* TEAM-ROSTER — Show team members
|
|
522
|
+
*/
|
|
523
|
+
function cmdTeamRoster(flags) {
|
|
524
|
+
const team = flags.team || 'default';
|
|
525
|
+
const hub = new TeamHub(team);
|
|
526
|
+
const roster = hub.getRoster();
|
|
527
|
+
|
|
528
|
+
console.log(`\n=== Team Roster: ${team} ===\n`);
|
|
529
|
+
console.log(`${'Name'.padEnd(25)} ${'Role'.padEnd(20)} ${'Status'.padEnd(12)} ${'Task'.padEnd(30)} ${'Last Seen'}`);
|
|
530
|
+
console.log('-'.repeat(120));
|
|
531
|
+
|
|
532
|
+
for (const member of roster) {
|
|
533
|
+
console.log(
|
|
534
|
+
`${truncate(member.name, 24).padEnd(25)} ` +
|
|
535
|
+
`${truncate(member.role || '-', 19).padEnd(20)} ` +
|
|
536
|
+
`${(member.status || 'offline').padEnd(12)} ` +
|
|
537
|
+
`${truncate(member.task || '-', 29).padEnd(30)} ` +
|
|
538
|
+
`${timeAgo(member.lastSeen)}`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
console.log(`\nTotal: ${roster.length}\n`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* TEAM-SEND — Send message to teammate
|
|
547
|
+
*/
|
|
548
|
+
function cmdTeamSend(flags) {
|
|
549
|
+
if (!flags.from) { console.error('Error: --from required'); process.exit(1); }
|
|
550
|
+
if (!flags.to) { console.error('Error: --to required'); process.exit(1); }
|
|
551
|
+
if (!flags.message) { console.error('Error: --message required'); process.exit(1); }
|
|
552
|
+
|
|
553
|
+
const team = flags.team || 'default';
|
|
554
|
+
const hub = new TeamHub(team);
|
|
555
|
+
|
|
556
|
+
hub.sendDirect(flags.from, flags.to, flags.message, flags.priority || 'normal');
|
|
557
|
+
console.log(`Message sent from "${flags.from}" to "${flags.to}".`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* TEAM-BROADCAST — Send to all teammates
|
|
562
|
+
*/
|
|
563
|
+
function cmdTeamBroadcast(flags) {
|
|
564
|
+
if (!flags.from) { console.error('Error: --from required'); process.exit(1); }
|
|
565
|
+
if (!flags.message) { console.error('Error: --message required'); process.exit(1); }
|
|
566
|
+
|
|
567
|
+
const team = flags.team || 'default';
|
|
568
|
+
const hub = new TeamHub(team);
|
|
569
|
+
|
|
570
|
+
hub.sendBroadcast(flags.from, flags.message, flags.priority || 'normal');
|
|
571
|
+
console.log(`Broadcast sent from "${flags.from}" to all team members.`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* TEAM-INBOX — Check inbox
|
|
576
|
+
*/
|
|
577
|
+
function cmdTeamInbox(flags) {
|
|
578
|
+
if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
|
|
579
|
+
|
|
580
|
+
const team = flags.team || 'default';
|
|
581
|
+
const hub = new TeamHub(team);
|
|
582
|
+
const markRead = flags['mark-read'] !== undefined ? flags['mark-read'] : true;
|
|
583
|
+
const limit = flags.limit ? parseInt(flags.limit) : 20;
|
|
584
|
+
|
|
585
|
+
const messages = hub.getInbox(flags.name, markRead, limit);
|
|
586
|
+
|
|
587
|
+
console.log(`\n=== Inbox: ${flags.name} (${messages.length} messages) ===\n`);
|
|
588
|
+
|
|
589
|
+
if (messages.length === 0) {
|
|
590
|
+
console.log('No messages.\n');
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
for (const msg of messages) {
|
|
595
|
+
console.log(`[${msg.priority || 'normal'}] From: ${msg.from} | ${timeAgo(msg.timestamp)}`);
|
|
596
|
+
console.log(` ${msg.content}`);
|
|
597
|
+
if (msg.messageId) console.log(` ID: ${msg.messageId}`);
|
|
598
|
+
console.log('');
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* ARTIFACT-PUBLISH — Publish artifact
|
|
604
|
+
*/
|
|
605
|
+
function cmdArtifactPublish(flags) {
|
|
606
|
+
if (!flags.id) { console.error('Error: --id required'); process.exit(1); }
|
|
607
|
+
if (!flags.type) { console.error('Error: --type required'); process.exit(1); }
|
|
608
|
+
if (!flags.name) { console.error('Error: --name required'); process.exit(1); }
|
|
609
|
+
if (!flags.data) { console.error('Error: --data required (JSON string)'); process.exit(1); }
|
|
610
|
+
|
|
611
|
+
const team = flags.team || 'default';
|
|
612
|
+
const store = new ArtifactStore(team);
|
|
613
|
+
|
|
614
|
+
let data;
|
|
615
|
+
try {
|
|
616
|
+
data = JSON.parse(flags.data);
|
|
617
|
+
} catch (e) {
|
|
618
|
+
console.error(`Invalid JSON in --data: ${e.message}`);
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const artifact = store.publish(flags.id, {
|
|
623
|
+
type: flags.type,
|
|
624
|
+
name: flags.name,
|
|
625
|
+
data: data,
|
|
626
|
+
publisher: flags.publisher,
|
|
627
|
+
summary: flags.summary,
|
|
628
|
+
tags: flags.tags ? flags.tags.split(',') : [],
|
|
629
|
+
derivedFrom: flags['derived-from'] ? flags['derived-from'].split(',') : [],
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
console.log(`Artifact published: ${flags.id} (version ${artifact.version})`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* ARTIFACT-GET — Read artifact
|
|
637
|
+
*/
|
|
638
|
+
function cmdArtifactGet(flags) {
|
|
639
|
+
if (!flags.id) { console.error('Error: --id required'); process.exit(1); }
|
|
640
|
+
|
|
641
|
+
const team = flags.team || 'default';
|
|
642
|
+
const store = new ArtifactStore(team);
|
|
643
|
+
|
|
644
|
+
const artifact = store.get(flags.id, flags.version ? parseInt(flags.version) : undefined);
|
|
645
|
+
|
|
646
|
+
if (!artifact) {
|
|
647
|
+
console.log(`Artifact "${flags.id}" not found.`);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
console.log(`\n=== Artifact: ${artifact.artifactId} (v${artifact.version}) ===\n`);
|
|
652
|
+
console.log(`Type: ${artifact.type}`);
|
|
653
|
+
console.log(`Name: ${artifact.name}`);
|
|
654
|
+
console.log(`Publisher: ${artifact.publisher || 'unknown'}`);
|
|
655
|
+
console.log(`Published: ${artifact.timestamp}`);
|
|
656
|
+
if (artifact.summary) console.log(`Summary: ${artifact.summary}`);
|
|
657
|
+
if (artifact.tags && artifact.tags.length > 0) console.log(`Tags: ${artifact.tags.join(', ')}`);
|
|
658
|
+
console.log('\nData:');
|
|
659
|
+
console.log(JSON.stringify(artifact.data, null, 2));
|
|
660
|
+
console.log('');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* ARTIFACT-LIST — List artifacts
|
|
665
|
+
*/
|
|
666
|
+
function cmdArtifactList(flags) {
|
|
667
|
+
const team = flags.team || 'default';
|
|
668
|
+
const store = new ArtifactStore(team);
|
|
669
|
+
|
|
670
|
+
const artifacts = store.list(flags.type, flags.publisher, flags.tag);
|
|
671
|
+
|
|
672
|
+
console.log(`\n=== Artifacts (${artifacts.length}) ===\n`);
|
|
673
|
+
|
|
674
|
+
if (artifacts.length === 0) {
|
|
675
|
+
console.log('No artifacts.\n');
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
console.log(`${'ID'.padEnd(30)} ${'Type'.padEnd(20)} ${'Version'.padEnd(8)} ${'Publisher'.padEnd(15)} ${'Published'}`);
|
|
680
|
+
console.log('-'.repeat(100));
|
|
681
|
+
|
|
682
|
+
for (const a of artifacts) {
|
|
683
|
+
console.log(
|
|
684
|
+
`${truncate(a.artifactId, 29).padEnd(30)} ` +
|
|
685
|
+
`${truncate(a.type, 19).padEnd(20)} ` +
|
|
686
|
+
`${String(a.version).padEnd(8)} ` +
|
|
687
|
+
`${truncate(a.publisher || '-', 14).padEnd(15)} ` +
|
|
688
|
+
`${timeAgo(a.timestamp)}`
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
console.log('');
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* CONTRACT-CREATE — Create contract
|
|
697
|
+
*/
|
|
698
|
+
function cmdContractCreate(flags) {
|
|
699
|
+
if (!flags.id) { console.error('Error: --id required'); process.exit(1); }
|
|
700
|
+
if (!flags.title) { console.error('Error: --title required'); process.exit(1); }
|
|
701
|
+
if (!flags.assignee) { console.error('Error: --assignee required'); process.exit(1); }
|
|
702
|
+
if (!flags.assigner) { console.error('Error: --assigner required'); process.exit(1); }
|
|
703
|
+
|
|
704
|
+
const team = flags.team || 'default';
|
|
705
|
+
const store = new ContractStore(team);
|
|
706
|
+
|
|
707
|
+
const contract = store.create(flags.id, {
|
|
708
|
+
title: flags.title,
|
|
709
|
+
assignee: flags.assignee,
|
|
710
|
+
assigner: flags.assigner,
|
|
711
|
+
description: flags.description,
|
|
712
|
+
inputs: flags.inputs ? JSON.parse(flags.inputs) : {},
|
|
713
|
+
expectedOutputs: flags['expected-outputs'] ? JSON.parse(flags['expected-outputs']) : [],
|
|
714
|
+
acceptanceCriteria: flags['acceptance-criteria'] ? JSON.parse(flags['acceptance-criteria']) : [],
|
|
715
|
+
dependencies: flags.dependencies ? flags.dependencies.split(',') : [],
|
|
716
|
+
priority: flags.priority || 'normal',
|
|
717
|
+
timeoutMs: flags['timeout-ms'] ? parseInt(flags['timeout-ms']) : null,
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
console.log(`Contract created: ${flags.id} (status: ${contract.status})`);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* CONTRACT-LIST — List contracts
|
|
725
|
+
*/
|
|
726
|
+
function cmdContractList(flags) {
|
|
727
|
+
const team = flags.team || 'default';
|
|
728
|
+
const store = new ContractStore(team);
|
|
729
|
+
|
|
730
|
+
const contracts = store.list(flags.status, flags.assignee, flags.assigner);
|
|
731
|
+
|
|
732
|
+
console.log(`\n=== Contracts (${contracts.length}) ===\n`);
|
|
733
|
+
|
|
734
|
+
if (contracts.length === 0) {
|
|
735
|
+
console.log('No contracts.\n');
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
console.log(`${'ID'.padEnd(25)} ${'Title'.padEnd(30)} ${'Status'.padEnd(15)} ${'Assignee'.padEnd(15)} ${'Created'}`);
|
|
740
|
+
console.log('-'.repeat(110));
|
|
741
|
+
|
|
742
|
+
for (const c of contracts) {
|
|
743
|
+
console.log(
|
|
744
|
+
`${truncate(c.contractId, 24).padEnd(25)} ` +
|
|
745
|
+
`${truncate(c.title, 29).padEnd(30)} ` +
|
|
746
|
+
`${(c.status || 'pending').padEnd(15)} ` +
|
|
747
|
+
`${truncate(c.assignee, 14).padEnd(15)} ` +
|
|
748
|
+
`${timeAgo(c.created)}`
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
console.log('');
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* CONTRACT-START — Start contract
|
|
757
|
+
*/
|
|
758
|
+
function cmdContractStart(flags) {
|
|
759
|
+
if (!flags.id) { console.error('Error: --id required'); process.exit(1); }
|
|
760
|
+
|
|
761
|
+
const team = flags.team || 'default';
|
|
762
|
+
const store = new ContractStore(team);
|
|
763
|
+
|
|
764
|
+
const contract = store.start(flags.id);
|
|
765
|
+
console.log(`Contract "${flags.id}" started (status: ${contract.status})`);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* PIPELINE-CREATE — Create pipeline
|
|
770
|
+
*/
|
|
771
|
+
function cmdPipelineCreate(flags) {
|
|
772
|
+
if (!flags.id) { console.error('Error: --id required'); process.exit(1); }
|
|
773
|
+
if (!flags.rules) { console.error('Error: --rules required (JSON string)'); process.exit(1); }
|
|
774
|
+
|
|
775
|
+
const team = flags.team || 'default';
|
|
776
|
+
const engine = new PipelineEngine(team);
|
|
777
|
+
|
|
778
|
+
let rules;
|
|
779
|
+
try {
|
|
780
|
+
rules = JSON.parse(flags.rules);
|
|
781
|
+
} catch (e) {
|
|
782
|
+
console.error(`Invalid JSON in --rules: ${e.message}`);
|
|
783
|
+
process.exit(1);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const pipeline = engine.create(flags.id, rules, flags.owner);
|
|
787
|
+
console.log(`Pipeline created: ${flags.id} (enabled: ${pipeline.enabled})`);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* PIPELINE-LIST — List pipelines
|
|
792
|
+
*/
|
|
793
|
+
function cmdPipelineList(flags) {
|
|
794
|
+
const team = flags.team || 'default';
|
|
795
|
+
const engine = new PipelineEngine(team);
|
|
796
|
+
|
|
797
|
+
const pipelines = engine.list(flags.owner);
|
|
798
|
+
|
|
799
|
+
console.log(`\n=== Pipelines (${pipelines.length}) ===\n`);
|
|
800
|
+
|
|
801
|
+
if (pipelines.length === 0) {
|
|
802
|
+
console.log('No pipelines.\n');
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
console.log(`${'ID'.padEnd(25)} ${'Enabled'.padEnd(10)} ${'Rules'.padEnd(8)} ${'Executed'.padEnd(10)} ${'Owner'}`);
|
|
807
|
+
console.log('-'.repeat(80));
|
|
808
|
+
|
|
809
|
+
for (const p of pipelines) {
|
|
810
|
+
console.log(
|
|
811
|
+
`${truncate(p.pipelineId, 24).padEnd(25)} ` +
|
|
812
|
+
`${(p.enabled ? 'yes' : 'no').padEnd(10)} ` +
|
|
813
|
+
`${String(p.rules.length).padEnd(8)} ` +
|
|
814
|
+
`${String(p.executionCount || 0).padEnd(10)} ` +
|
|
815
|
+
`${p.owner || '-'}`
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
console.log('');
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* SNAPSHOT-CREATE — Create snapshot
|
|
824
|
+
*/
|
|
825
|
+
function cmdSnapshotCreate(flags) {
|
|
826
|
+
if (!flags.id) { console.error('Error: --id required'); process.exit(1); }
|
|
827
|
+
|
|
828
|
+
const team = flags.team || 'default';
|
|
829
|
+
const engine = new SnapshotEngine(team);
|
|
830
|
+
|
|
831
|
+
const snapshot = engine.createSnapshot(flags.id, flags.label, flags.description);
|
|
832
|
+
console.log(`Snapshot created: ${flags.id} at ${snapshot.timestamp}`);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* SNAPSHOT-LIST — List snapshots
|
|
837
|
+
*/
|
|
838
|
+
function cmdSnapshotList(flags) {
|
|
839
|
+
const team = flags.team || 'default';
|
|
840
|
+
const engine = new SnapshotEngine(team);
|
|
841
|
+
|
|
842
|
+
const snapshots = engine.listSnapshots();
|
|
843
|
+
|
|
844
|
+
console.log(`\n=== Snapshots (${snapshots.length}) ===\n`);
|
|
845
|
+
|
|
846
|
+
if (snapshots.length === 0) {
|
|
847
|
+
console.log('No snapshots.\n');
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
console.log(`${'ID'.padEnd(25)} ${'Label'.padEnd(30)} ${'Created'}`);
|
|
852
|
+
console.log('-'.repeat(70));
|
|
853
|
+
|
|
854
|
+
for (const s of snapshots) {
|
|
855
|
+
console.log(
|
|
856
|
+
`${truncate(s.snapshotId, 24).padEnd(25)} ` +
|
|
857
|
+
`${truncate(s.label || '-', 29).padEnd(30)} ` +
|
|
858
|
+
`${timeAgo(s.timestamp)}`
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
console.log('');
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* SNAPSHOT-ROLLBACK — Rollback to snapshot
|
|
867
|
+
*/
|
|
868
|
+
function cmdSnapshotRollback(flags) {
|
|
869
|
+
if (!flags.id) { console.error('Error: --id required'); process.exit(1); }
|
|
870
|
+
|
|
871
|
+
const team = flags.team || 'default';
|
|
872
|
+
const engine = new SnapshotEngine(team);
|
|
873
|
+
|
|
874
|
+
engine.rollback(flags.id, flags['preserve-artifacts'] !== false);
|
|
875
|
+
console.log(`Rolled back to snapshot: ${flags.id}`);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// ============================================================================
|
|
879
|
+
// Session Continuity Commands (Layer 0)
|
|
880
|
+
// ============================================================================
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* SNAPSHOT — Capture a session snapshot
|
|
884
|
+
*/
|
|
885
|
+
function cmdSnapshot(flags) {
|
|
886
|
+
if (!flags['project-path']) { console.error('Error: --project-path required'); process.exit(1); }
|
|
887
|
+
|
|
888
|
+
const snap = new SessionSnapshot(flags['project-path']);
|
|
889
|
+
const sessionName = flags.session || 'cli-session';
|
|
890
|
+
const taskSummary = flags.task || 'Work in progress';
|
|
891
|
+
|
|
892
|
+
const result = snap.capture(sessionName, {
|
|
893
|
+
taskSummary: taskSummary,
|
|
894
|
+
activeFiles: flags.files ? flags.files.split(',') : [],
|
|
895
|
+
openQuestions: flags.questions ? flags.questions.split(',') : []
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
console.log(`Snapshot captured: ${result.snapshotId}`);
|
|
899
|
+
console.log(` Session: ${sessionName}`);
|
|
900
|
+
console.log(` Captured at: ${result.capturedAt}`);
|
|
901
|
+
console.log(` Files tracked: ${result.workingContext?.activeFiles?.length || 0}`);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* BRIEFING — Generate session briefing
|
|
906
|
+
*/
|
|
907
|
+
function cmdBriefing(flags) {
|
|
908
|
+
if (!flags['project-path']) { console.error('Error: --project-path required'); process.exit(1); }
|
|
909
|
+
|
|
910
|
+
const gen = new BriefingGenerator(flags['project-path']);
|
|
911
|
+
const maxTokens = flags['max-tokens'] ? parseInt(flags['max-tokens']) : 4000;
|
|
912
|
+
const includeDecisions = flags['include-decisions'] !== false;
|
|
913
|
+
const includePatterns = flags['include-patterns'] !== false;
|
|
914
|
+
|
|
915
|
+
const result = gen.generate({
|
|
916
|
+
maxTokens: maxTokens,
|
|
917
|
+
includeDecisions: includeDecisions,
|
|
918
|
+
includePatterns: includePatterns
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
console.log('\n' + result.markdown);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* STALE-CHECK — Check if context is stale
|
|
926
|
+
*/
|
|
927
|
+
function cmdStaleCheck(flags) {
|
|
928
|
+
if (!flags['project-path']) { console.error('Error: --project-path required'); process.exit(1); }
|
|
929
|
+
|
|
930
|
+
const detector = new StaleDetector(flags['project-path']);
|
|
931
|
+
const result = detector.check();
|
|
932
|
+
|
|
933
|
+
console.log(`\n=== Stale Context Check ===\n`);
|
|
934
|
+
console.log(`Status: ${result.isStale ? 'STALE' : 'FRESH'}`);
|
|
935
|
+
|
|
936
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
937
|
+
console.log(`\nWarnings:`);
|
|
938
|
+
result.warnings.forEach(w => console.log(` - ${w}`));
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (result.changedFiles && result.changedFiles.length > 0) {
|
|
942
|
+
console.log(`\nChanged files since last snapshot: ${result.changedFiles.length}`);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
console.log('');
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* DECISIONS — List decisions
|
|
950
|
+
*/
|
|
951
|
+
function cmdDecisions(flags) {
|
|
952
|
+
if (!flags['project-path']) { console.error('Error: --project-path required'); process.exit(1); }
|
|
953
|
+
|
|
954
|
+
const journal = new DecisionJournal(flags['project-path']);
|
|
955
|
+
const limit = flags.limit ? parseInt(flags.limit) : 50;
|
|
956
|
+
const tag = flags.tag;
|
|
957
|
+
|
|
958
|
+
const decisions = journal.list({ limit: limit, tag: tag });
|
|
959
|
+
|
|
960
|
+
console.log(`\n=== Decisions (${decisions.length}) ===\n`);
|
|
961
|
+
|
|
962
|
+
if (decisions.length === 0) {
|
|
963
|
+
console.log('No decisions recorded.\n');
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
for (const d of decisions) {
|
|
968
|
+
console.log(`[${d.createdAt || 'unknown'}]`);
|
|
969
|
+
console.log(` Decision: ${d.decision}`);
|
|
970
|
+
if (d.reason) console.log(` Reason: ${d.reason}`);
|
|
971
|
+
if (d.tags && d.tags.length > 0) console.log(` Tags: ${d.tags.join(', ')}`);
|
|
972
|
+
console.log('');
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* PATTERNS — List patterns
|
|
978
|
+
*/
|
|
979
|
+
function cmdPatterns(flags) {
|
|
980
|
+
if (!flags['project-path']) { console.error('Error: --project-path required'); process.exit(1); }
|
|
981
|
+
|
|
982
|
+
const registry = new PatternRegistry(flags['project-path']);
|
|
983
|
+
const tag = flags.tag;
|
|
984
|
+
|
|
985
|
+
const patterns = registry.list({ tag: tag });
|
|
986
|
+
|
|
987
|
+
console.log(`\n=== Patterns (${patterns.length}) ===\n`);
|
|
988
|
+
|
|
989
|
+
if (patterns.length === 0) {
|
|
990
|
+
console.log('No patterns recorded.\n');
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
for (const p of patterns) {
|
|
995
|
+
console.log(`[${p.id}] ${p.rule}`);
|
|
996
|
+
if (p.context) console.log(` Context: ${p.context}`);
|
|
997
|
+
if (p.example) console.log(` Example: ${p.example}`);
|
|
998
|
+
if (p.tags && p.tags.length > 0) console.log(` Tags: ${p.tags.join(', ')}`);
|
|
999
|
+
console.log('');
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// ============================================================================
|
|
1004
|
+
// Session Continuity Automation Commands
|
|
1005
|
+
// ============================================================================
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* CONTINUITY-SETUP — Install Claude Code hooks for automatic session continuity.
|
|
1009
|
+
*
|
|
1010
|
+
* Writes 4 hook entries to ~/.claude/settings.json so that:
|
|
1011
|
+
* - SessionStart automatically injects a diff-aware briefing
|
|
1012
|
+
* - Stop quietly checkpoints in the background
|
|
1013
|
+
* - PreCompact saves context before compaction
|
|
1014
|
+
* - SessionEnd captures a final snapshot on exit
|
|
1015
|
+
*/
|
|
1016
|
+
function cmdContinuitySetup() {
|
|
1017
|
+
const os = require('os');
|
|
1018
|
+
const crypto = require('crypto');
|
|
1019
|
+
|
|
1020
|
+
// Resolve the absolute path to our hook entry point script
|
|
1021
|
+
const hookScript = path.resolve(__dirname, 'continuity-hook.js');
|
|
1022
|
+
|
|
1023
|
+
// Path to Claude Code's settings file
|
|
1024
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
1025
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
1026
|
+
|
|
1027
|
+
// Create ~/.claude/ directory if it doesn't exist
|
|
1028
|
+
if (!fs.existsSync(claudeDir)) {
|
|
1029
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Read existing settings (or start fresh)
|
|
1033
|
+
let settings = {};
|
|
1034
|
+
if (fs.existsSync(settingsPath)) {
|
|
1035
|
+
try {
|
|
1036
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
1037
|
+
} catch (e) {
|
|
1038
|
+
// If settings file is corrupted, start fresh
|
|
1039
|
+
settings = {};
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Make sure hooks object exists
|
|
1044
|
+
if (!settings.hooks) {
|
|
1045
|
+
settings.hooks = {};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// The hook command uses the absolute path to our entry point
|
|
1049
|
+
// On Windows, we need forward slashes in the JSON string for safety
|
|
1050
|
+
const hookScriptEscaped = hookScript.replace(/\\/g, '\\\\');
|
|
1051
|
+
const hookCmd = `node "${hookScriptEscaped}"`;
|
|
1052
|
+
|
|
1053
|
+
// Define the 4 hook configurations we need to install
|
|
1054
|
+
const hookConfigs = {
|
|
1055
|
+
SessionStart: {
|
|
1056
|
+
matcher: '',
|
|
1057
|
+
hooks: [{
|
|
1058
|
+
type: 'command',
|
|
1059
|
+
command: `${hookCmd} SessionStart`,
|
|
1060
|
+
timeout: 5
|
|
1061
|
+
}]
|
|
1062
|
+
},
|
|
1063
|
+
Stop: {
|
|
1064
|
+
matcher: '',
|
|
1065
|
+
hooks: [{
|
|
1066
|
+
type: 'command',
|
|
1067
|
+
command: `${hookCmd} Stop`,
|
|
1068
|
+
async: true,
|
|
1069
|
+
timeout: 10
|
|
1070
|
+
}]
|
|
1071
|
+
},
|
|
1072
|
+
PreCompact: {
|
|
1073
|
+
matcher: '',
|
|
1074
|
+
hooks: [{
|
|
1075
|
+
type: 'command',
|
|
1076
|
+
command: `${hookCmd} PreCompact`,
|
|
1077
|
+
timeout: 10
|
|
1078
|
+
}]
|
|
1079
|
+
},
|
|
1080
|
+
SessionEnd: {
|
|
1081
|
+
matcher: '',
|
|
1082
|
+
hooks: [{
|
|
1083
|
+
type: 'command',
|
|
1084
|
+
command: `${hookCmd} SessionEnd`,
|
|
1085
|
+
async: true,
|
|
1086
|
+
timeout: 15
|
|
1087
|
+
}]
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
// Check if we're updating existing hooks (contains continuity-hook.js)
|
|
1092
|
+
let isUpdate = false;
|
|
1093
|
+
|
|
1094
|
+
// Merge hooks into settings — don't overwrite existing hooks from other tools
|
|
1095
|
+
for (const [eventName, newHookEntry] of Object.entries(hookConfigs)) {
|
|
1096
|
+
if (!settings.hooks[eventName]) {
|
|
1097
|
+
// No existing hooks for this event — create the array
|
|
1098
|
+
settings.hooks[eventName] = [newHookEntry];
|
|
1099
|
+
} else {
|
|
1100
|
+
// Existing hooks — check if our hook is already installed
|
|
1101
|
+
const existingArray = settings.hooks[eventName];
|
|
1102
|
+
let found = false;
|
|
1103
|
+
|
|
1104
|
+
for (let i = 0; i < existingArray.length; i++) {
|
|
1105
|
+
const entry = existingArray[i];
|
|
1106
|
+
// Check if any hook command in this entry contains our script
|
|
1107
|
+
if (entry.hooks && Array.isArray(entry.hooks)) {
|
|
1108
|
+
for (let j = 0; j < entry.hooks.length; j++) {
|
|
1109
|
+
if (entry.hooks[j].command && entry.hooks[j].command.includes('continuity-hook')) {
|
|
1110
|
+
// Update the existing hook with new path
|
|
1111
|
+
existingArray[i] = newHookEntry;
|
|
1112
|
+
found = true;
|
|
1113
|
+
isUpdate = true;
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
if (found) break;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
if (!found) {
|
|
1122
|
+
// Our hook isn't installed yet — add it to the array
|
|
1123
|
+
existingArray.push(newHookEntry);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Write the settings back using atomic write (write to temp, then rename)
|
|
1129
|
+
const tmpPath = settingsPath + '.tmp.' + Date.now();
|
|
1130
|
+
fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
1131
|
+
fs.renameSync(tmpPath, settingsPath);
|
|
1132
|
+
|
|
1133
|
+
// Print success output
|
|
1134
|
+
if (isUpdate) {
|
|
1135
|
+
console.log(`\n Session Continuity hooks updated in ~/.claude/settings.json`);
|
|
1136
|
+
console.log(` (Hook script path updated to: ${hookScript})\n`);
|
|
1137
|
+
} else {
|
|
1138
|
+
console.log(`\n Session Continuity hooks installed to ~/.claude/settings.json\n`);
|
|
1139
|
+
console.log(` Hooks configured:`);
|
|
1140
|
+
console.log(` SessionStart -> Auto-inject session briefing (timeout: 5s)`);
|
|
1141
|
+
console.log(` Stop -> Background checkpoint (async, timeout: 10s)`);
|
|
1142
|
+
console.log(` PreCompact -> Critical save before compaction (timeout: 10s)`);
|
|
1143
|
+
console.log(` SessionEnd -> Final snapshot on exit (async, timeout: 15s)`);
|
|
1144
|
+
console.log(`\n Hook script: ${hookScript}`);
|
|
1145
|
+
console.log(`\n Continuity is now active for ALL projects.`);
|
|
1146
|
+
console.log(` Start a Claude Code session, do some work, exit, and start again.`);
|
|
1147
|
+
console.log(` The next session will automatically receive a briefing about what changed.\n`);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* CONTINUITY-UNSETUP — Remove continuity hooks from ~/.claude/settings.json.
|
|
1153
|
+
*/
|
|
1154
|
+
function cmdContinuityUnsetup() {
|
|
1155
|
+
const os = require('os');
|
|
1156
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
1157
|
+
|
|
1158
|
+
// If settings file doesn't exist, nothing to remove
|
|
1159
|
+
if (!fs.existsSync(settingsPath)) {
|
|
1160
|
+
console.log('No settings file found at ~/.claude/settings.json — nothing to remove.');
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Read existing settings
|
|
1165
|
+
let settings;
|
|
1166
|
+
try {
|
|
1167
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
1168
|
+
} catch (e) {
|
|
1169
|
+
console.error('Error reading settings file:', e.message);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
if (!settings.hooks) {
|
|
1174
|
+
console.log('No hooks found in settings — nothing to remove.');
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
let removedCount = 0;
|
|
1179
|
+
|
|
1180
|
+
// Remove any hook entries that contain 'continuity-hook' in their command
|
|
1181
|
+
for (const eventName of Object.keys(settings.hooks)) {
|
|
1182
|
+
const entries = settings.hooks[eventName];
|
|
1183
|
+
if (!Array.isArray(entries)) continue;
|
|
1184
|
+
|
|
1185
|
+
// Filter out entries that have continuity-hook commands
|
|
1186
|
+
settings.hooks[eventName] = entries.filter(entry => {
|
|
1187
|
+
if (entry.hooks && Array.isArray(entry.hooks)) {
|
|
1188
|
+
const hasContinuity = entry.hooks.some(h =>
|
|
1189
|
+
h.command && h.command.includes('continuity-hook')
|
|
1190
|
+
);
|
|
1191
|
+
if (hasContinuity) {
|
|
1192
|
+
removedCount++;
|
|
1193
|
+
return false; // Remove this entry
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return true; // Keep this entry
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
// Clean up empty arrays
|
|
1200
|
+
if (settings.hooks[eventName].length === 0) {
|
|
1201
|
+
delete settings.hooks[eventName];
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Clean up empty hooks object
|
|
1206
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
1207
|
+
delete settings.hooks;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Write back
|
|
1211
|
+
const tmpPath = settingsPath + '.tmp.' + Date.now();
|
|
1212
|
+
fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
1213
|
+
fs.renameSync(tmpPath, settingsPath);
|
|
1214
|
+
|
|
1215
|
+
if (removedCount > 0) {
|
|
1216
|
+
console.log(`\n Removed ${removedCount} continuity hook(s) from ~/.claude/settings.json`);
|
|
1217
|
+
console.log(` Session Continuity automation is now disabled.\n`);
|
|
1218
|
+
} else {
|
|
1219
|
+
console.log('No continuity hooks found in settings — nothing to remove.');
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* CONTINUITY-RESET — Delete ALL continuity data for a project.
|
|
1225
|
+
*/
|
|
1226
|
+
function cmdContinuityReset(flags) {
|
|
1227
|
+
if (!flags['project-path']) {
|
|
1228
|
+
console.error('Error: --project-path required');
|
|
1229
|
+
process.exit(1);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const os = require('os');
|
|
1233
|
+
const crypto = require('crypto');
|
|
1234
|
+
|
|
1235
|
+
const projectPath = flags['project-path'];
|
|
1236
|
+
|
|
1237
|
+
// Compute the same project hash used by all continuity modules
|
|
1238
|
+
const projectHash = crypto
|
|
1239
|
+
.createHash('sha256')
|
|
1240
|
+
.update(projectPath)
|
|
1241
|
+
.digest('hex')
|
|
1242
|
+
.slice(0, 16);
|
|
1243
|
+
|
|
1244
|
+
// Primary storage location: ~/.clearctx/continuity/{hash}/
|
|
1245
|
+
const continuityDir = path.join(
|
|
1246
|
+
os.homedir(),
|
|
1247
|
+
'.clearctx',
|
|
1248
|
+
'continuity',
|
|
1249
|
+
projectHash
|
|
1250
|
+
);
|
|
1251
|
+
|
|
1252
|
+
let deleted = false;
|
|
1253
|
+
|
|
1254
|
+
// Delete the primary continuity directory recursively
|
|
1255
|
+
if (fs.existsSync(continuityDir)) {
|
|
1256
|
+
fs.rmSync(continuityDir, { recursive: true, force: true });
|
|
1257
|
+
deleted = true;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// Also check the old BriefingGenerator path (in case data was stored there before the fix)
|
|
1261
|
+
const oldBriefingsDir = path.join(
|
|
1262
|
+
os.homedir(),
|
|
1263
|
+
'.claude',
|
|
1264
|
+
'multi-session',
|
|
1265
|
+
projectHash,
|
|
1266
|
+
'briefings'
|
|
1267
|
+
);
|
|
1268
|
+
if (fs.existsSync(oldBriefingsDir)) {
|
|
1269
|
+
fs.rmSync(oldBriefingsDir, { recursive: true, force: true });
|
|
1270
|
+
deleted = true;
|
|
1271
|
+
|
|
1272
|
+
// Clean up the parent directory if it's now empty
|
|
1273
|
+
const oldParent = path.join(os.homedir(), '.claude', 'multi-session', projectHash);
|
|
1274
|
+
try {
|
|
1275
|
+
const remaining = fs.readdirSync(oldParent);
|
|
1276
|
+
if (remaining.length === 0) {
|
|
1277
|
+
fs.rmdirSync(oldParent);
|
|
1278
|
+
}
|
|
1279
|
+
} catch (e) {
|
|
1280
|
+
// Ignore — parent might not exist
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
if (deleted) {
|
|
1285
|
+
console.log(`\n Continuity data cleared for ${projectPath}`);
|
|
1286
|
+
console.log(` Project hash: ${projectHash}\n`);
|
|
1287
|
+
} else {
|
|
1288
|
+
console.log(`\n No continuity data found for ${projectPath}`);
|
|
1289
|
+
console.log(` Project hash: ${projectHash}\n`);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/**
|
|
1294
|
+
* CONTINUITY-PRUNE — Keep only the N most recent snapshots for a project.
|
|
1295
|
+
*/
|
|
1296
|
+
function cmdContinuityPrune(flags) {
|
|
1297
|
+
if (!flags['project-path']) {
|
|
1298
|
+
console.error('Error: --project-path required');
|
|
1299
|
+
process.exit(1);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const projectPath = flags['project-path'];
|
|
1303
|
+
const keep = flags.keep ? parseInt(flags.keep) : 10;
|
|
1304
|
+
|
|
1305
|
+
// Instantiate SessionSnapshot to get the snapshots directory
|
|
1306
|
+
const snap = new SessionSnapshot(projectPath);
|
|
1307
|
+
const snapshotsDir = path.join(snap.getProjectDir(), 'snapshots');
|
|
1308
|
+
|
|
1309
|
+
if (!fs.existsSync(snapshotsDir)) {
|
|
1310
|
+
console.log('No snapshots directory found — nothing to prune.');
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Read all snapshot files
|
|
1315
|
+
const files = fs.readdirSync(snapshotsDir)
|
|
1316
|
+
.filter(f => f.startsWith('snap_') && f.endsWith('.json'));
|
|
1317
|
+
|
|
1318
|
+
if (files.length <= keep) {
|
|
1319
|
+
console.log(`\n Only ${files.length} snapshot(s) found — nothing to prune (keeping ${keep}).\n`);
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// Sort ascending by filename (contains timestamp) — oldest first
|
|
1324
|
+
files.sort();
|
|
1325
|
+
|
|
1326
|
+
// Delete the oldest files, keep the most recent
|
|
1327
|
+
const toDelete = files.slice(0, files.length - keep);
|
|
1328
|
+
let deletedCount = 0;
|
|
1329
|
+
|
|
1330
|
+
for (const file of toDelete) {
|
|
1331
|
+
try {
|
|
1332
|
+
fs.unlinkSync(path.join(snapshotsDir, file));
|
|
1333
|
+
deletedCount++;
|
|
1334
|
+
} catch (e) {
|
|
1335
|
+
console.error(` Warning: could not delete ${file}: ${e.message}`);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
console.log(`\n Pruned ${deletedCount} snapshot(s), kept ${keep} most recent.\n`);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* CONTINUITY-STATUS — Show continuity status at a glance.
|
|
1344
|
+
*/
|
|
1345
|
+
function cmdContinuityStatus(flags) {
|
|
1346
|
+
if (!flags['project-path']) {
|
|
1347
|
+
console.error('Error: --project-path required');
|
|
1348
|
+
process.exit(1);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const os = require('os');
|
|
1352
|
+
|
|
1353
|
+
const projectPath = flags['project-path'];
|
|
1354
|
+
|
|
1355
|
+
// Get snapshot info
|
|
1356
|
+
const snap = new SessionSnapshot(projectPath);
|
|
1357
|
+
const latest = snap.getLatest();
|
|
1358
|
+
const allSnapshots = snap.list(100);
|
|
1359
|
+
const projectHash = snap.getProjectHash();
|
|
1360
|
+
|
|
1361
|
+
// Get decision count
|
|
1362
|
+
const journal = new DecisionJournal(projectPath);
|
|
1363
|
+
const decisions = journal.list({});
|
|
1364
|
+
|
|
1365
|
+
// Get pattern count
|
|
1366
|
+
const registry = new PatternRegistry(projectPath);
|
|
1367
|
+
const patterns = registry.list({});
|
|
1368
|
+
|
|
1369
|
+
// Check staleness
|
|
1370
|
+
let staleStatus = 'N/A';
|
|
1371
|
+
let changedCount = 0;
|
|
1372
|
+
if (latest) {
|
|
1373
|
+
try {
|
|
1374
|
+
const detector = new StaleDetector(projectPath);
|
|
1375
|
+
const staleResult = detector.check();
|
|
1376
|
+
if (staleResult.isStale) {
|
|
1377
|
+
changedCount = staleResult.totalFileChanges || 0;
|
|
1378
|
+
staleStatus = `STALE -- ${staleResult.warnings.length} warning(s), ${changedCount} file change(s)`;
|
|
1379
|
+
} else {
|
|
1380
|
+
staleStatus = 'FRESH';
|
|
1381
|
+
}
|
|
1382
|
+
} catch (e) {
|
|
1383
|
+
staleStatus = 'Error checking: ' + e.message;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// Check if hooks are installed
|
|
1388
|
+
let hooksInstalled = false;
|
|
1389
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
1390
|
+
if (fs.existsSync(settingsPath)) {
|
|
1391
|
+
try {
|
|
1392
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
1393
|
+
if (settings.hooks) {
|
|
1394
|
+
// Check if any hook command contains continuity-hook
|
|
1395
|
+
const hookStr = JSON.stringify(settings.hooks);
|
|
1396
|
+
hooksInstalled = hookStr.includes('continuity-hook');
|
|
1397
|
+
}
|
|
1398
|
+
} catch (e) {
|
|
1399
|
+
// Ignore
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// Format last snapshot time
|
|
1404
|
+
let lastSnapshotStr = 'None';
|
|
1405
|
+
let lastTrigger = 'N/A';
|
|
1406
|
+
let oldestStr = 'N/A';
|
|
1407
|
+
if (latest) {
|
|
1408
|
+
const capturedAt = new Date(latest.capturedAt);
|
|
1409
|
+
const ago = timeAgo(latest.capturedAt);
|
|
1410
|
+
lastSnapshotStr = `${capturedAt.toISOString()} (${ago})`;
|
|
1411
|
+
lastTrigger = latest.capturedBy || 'unknown';
|
|
1412
|
+
}
|
|
1413
|
+
if (allSnapshots.length > 0) {
|
|
1414
|
+
const oldest = allSnapshots[allSnapshots.length - 1];
|
|
1415
|
+
oldestStr = timeAgo(oldest.capturedAt);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// Print the status
|
|
1419
|
+
console.log(`\n=== Session Continuity Status ===\n`);
|
|
1420
|
+
console.log(`Project: ${projectPath}`);
|
|
1421
|
+
console.log(`Project hash: ${projectHash}`);
|
|
1422
|
+
console.log(`Last snapshot: ${lastSnapshotStr}`);
|
|
1423
|
+
console.log(`Trigger: ${lastTrigger}`);
|
|
1424
|
+
console.log(`Snapshots stored: ${allSnapshots.length} (oldest: ${oldestStr})`);
|
|
1425
|
+
console.log(`Decisions logged: ${decisions.length}`);
|
|
1426
|
+
console.log(`Patterns saved: ${patterns.length}`);
|
|
1427
|
+
console.log(`Context status: ${staleStatus}`);
|
|
1428
|
+
console.log(`Hooks installed: ${hooksInstalled ? 'Yes (in ~/.claude/settings.json)' : 'No'}`);
|
|
1429
|
+
console.log('');
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function cmdHelp() {
|
|
1433
|
+
console.log(`
|
|
1434
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
1435
|
+
║ clearctx (cms) — Multi-Session Orchestrator ║
|
|
1436
|
+
║ Streaming-powered session management for Claude Code CLI ║
|
|
1437
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
1438
|
+
|
|
1439
|
+
COMMANDS:
|
|
1440
|
+
|
|
1441
|
+
spawn Start a new streaming session (keeps process alive)
|
|
1442
|
+
--name <name> --prompt <text>
|
|
1443
|
+
[--model sonnet|opus|haiku] [--stop]
|
|
1444
|
+
[--work-dir <path>] [--permission-mode <mode>]
|
|
1445
|
+
[--allowed-tools <t1,t2>] [--system-prompt <text>]
|
|
1446
|
+
[--max-budget <usd>] [--agent <name>]
|
|
1447
|
+
|
|
1448
|
+
send Send a follow-up message (auto-resumes if stopped)
|
|
1449
|
+
--name <name> --message <text> [--stop]
|
|
1450
|
+
|
|
1451
|
+
resume Restore a stopped/paused session
|
|
1452
|
+
--name <name> [--message <text>]
|
|
1453
|
+
|
|
1454
|
+
pause Pause session (process stays alive, no messages accepted)
|
|
1455
|
+
--name <name>
|
|
1456
|
+
|
|
1457
|
+
fork Branch off a session into a new one
|
|
1458
|
+
--name <source> --new-name <target>
|
|
1459
|
+
[--message <text>] [--model <model>]
|
|
1460
|
+
|
|
1461
|
+
stop Gracefully stop (saves state, can resume later)
|
|
1462
|
+
--name <name>
|
|
1463
|
+
|
|
1464
|
+
kill Force kill a session
|
|
1465
|
+
--name <name>
|
|
1466
|
+
|
|
1467
|
+
status Show detailed session info
|
|
1468
|
+
--name <name>
|
|
1469
|
+
|
|
1470
|
+
output Get last response text
|
|
1471
|
+
--name <name> [--full]
|
|
1472
|
+
|
|
1473
|
+
list List all sessions
|
|
1474
|
+
[--status ready|paused|stopped|killed]
|
|
1475
|
+
|
|
1476
|
+
history Show full interaction history
|
|
1477
|
+
--name <name> [--full]
|
|
1478
|
+
|
|
1479
|
+
delete Permanently remove a session
|
|
1480
|
+
--name <name>
|
|
1481
|
+
|
|
1482
|
+
batch Spawn multiple sessions from JSON file
|
|
1483
|
+
--file <path> [--keep-alive]
|
|
1484
|
+
|
|
1485
|
+
cleanup Remove old sessions
|
|
1486
|
+
[--days <n>]
|
|
1487
|
+
|
|
1488
|
+
delegate Smart task delegation with safety limits & auto-permission
|
|
1489
|
+
--name <name> --task <text>
|
|
1490
|
+
[--model sonnet|opus|haiku] [--preset read-only|review|edit|full|plan]
|
|
1491
|
+
[--max-cost <usd>] [--max-turns <n>] [--no-safety]
|
|
1492
|
+
[--context <text>] [--work-dir <path>]
|
|
1493
|
+
[--system-prompt <text>] [--agent <name>] [--json]
|
|
1494
|
+
|
|
1495
|
+
continue Send follow-up to a delegated session
|
|
1496
|
+
--name <name> --message <text> [--json]
|
|
1497
|
+
|
|
1498
|
+
setup Register MCP server with Claude Code
|
|
1499
|
+
[--global] [--local] [--uninstall] [--migrate]
|
|
1500
|
+
[--yes] [--no-guide]
|
|
1501
|
+
|
|
1502
|
+
TEAM COMMANDS:
|
|
1503
|
+
|
|
1504
|
+
team-roster Show team members
|
|
1505
|
+
[--team <name>]
|
|
1506
|
+
|
|
1507
|
+
team-send Send message to teammate
|
|
1508
|
+
--from <name> --to <name> --message <text>
|
|
1509
|
+
[--priority low|normal|high|urgent] [--team <name>]
|
|
1510
|
+
|
|
1511
|
+
team-broadcast Send to all teammates
|
|
1512
|
+
--from <name> --message <text>
|
|
1513
|
+
[--priority low|normal|high|urgent] [--team <name>]
|
|
1514
|
+
|
|
1515
|
+
team-inbox Check inbox
|
|
1516
|
+
--name <name> [--mark-read] [--limit <n>] [--team <name>]
|
|
1517
|
+
|
|
1518
|
+
ARTIFACT COMMANDS:
|
|
1519
|
+
|
|
1520
|
+
artifact-publish Publish artifact
|
|
1521
|
+
--id <id> --type <type> --name <name> --data <json>
|
|
1522
|
+
[--publisher <name>] [--summary <text>] [--tags <t1,t2>]
|
|
1523
|
+
[--derived-from <id1,id2>] [--team <name>]
|
|
1524
|
+
|
|
1525
|
+
artifact-get Read artifact
|
|
1526
|
+
--id <id> [--version <n>] [--team <name>]
|
|
1527
|
+
|
|
1528
|
+
artifact-list List artifacts
|
|
1529
|
+
[--type <type>] [--publisher <name>] [--tag <tag>] [--team <name>]
|
|
1530
|
+
|
|
1531
|
+
CONTRACT COMMANDS:
|
|
1532
|
+
|
|
1533
|
+
contract-create Create contract
|
|
1534
|
+
--id <id> --title <text> --assignee <name> --assigner <name>
|
|
1535
|
+
[--description <text>] [--priority low|normal|high|urgent]
|
|
1536
|
+
[--team <name>]
|
|
1537
|
+
|
|
1538
|
+
contract-list List contracts
|
|
1539
|
+
[--status pending|ready|in_progress|completed|failed]
|
|
1540
|
+
[--assignee <name>] [--assigner <name>] [--team <name>]
|
|
1541
|
+
|
|
1542
|
+
contract-start Start contract
|
|
1543
|
+
--id <id> [--team <name>]
|
|
1544
|
+
|
|
1545
|
+
PIPELINE COMMANDS:
|
|
1546
|
+
|
|
1547
|
+
pipeline-create Create pipeline
|
|
1548
|
+
--id <id> --rules <json> [--owner <name>] [--team <name>]
|
|
1549
|
+
|
|
1550
|
+
pipeline-list List pipelines
|
|
1551
|
+
[--owner <name>] [--team <name>]
|
|
1552
|
+
|
|
1553
|
+
SNAPSHOT COMMANDS:
|
|
1554
|
+
|
|
1555
|
+
snapshot-create Create snapshot
|
|
1556
|
+
--id <id> [--label <text>] [--description <text>] [--team <name>]
|
|
1557
|
+
|
|
1558
|
+
snapshot-list List snapshots
|
|
1559
|
+
[--team <name>]
|
|
1560
|
+
|
|
1561
|
+
snapshot-rollback Rollback to snapshot
|
|
1562
|
+
--id <id> [--preserve-artifacts] [--team <name>]
|
|
1563
|
+
|
|
1564
|
+
SESSION CONTINUITY COMMANDS (Layer 0):
|
|
1565
|
+
|
|
1566
|
+
snapshot Capture session snapshot
|
|
1567
|
+
--project-path <path> [--session <name>] [--task <summary>]
|
|
1568
|
+
[--files <file1,file2>] [--questions <q1,q2>]
|
|
1569
|
+
|
|
1570
|
+
briefing Generate session briefing
|
|
1571
|
+
--project-path <path> [--max-tokens <n>]
|
|
1572
|
+
[--include-decisions] [--include-patterns]
|
|
1573
|
+
|
|
1574
|
+
stale-check Check if context is stale
|
|
1575
|
+
--project-path <path>
|
|
1576
|
+
|
|
1577
|
+
decisions List decisions
|
|
1578
|
+
--project-path <path> [--limit <n>] [--tag <tag>]
|
|
1579
|
+
|
|
1580
|
+
patterns List patterns
|
|
1581
|
+
--project-path <path> [--tag <tag>]
|
|
1582
|
+
|
|
1583
|
+
CONTINUITY AUTOMATION COMMANDS:
|
|
1584
|
+
|
|
1585
|
+
continuity-setup Install hooks for automatic session continuity
|
|
1586
|
+
(writes to ~/.claude/settings.json)
|
|
1587
|
+
|
|
1588
|
+
continuity-unsetup Remove continuity hooks from settings
|
|
1589
|
+
|
|
1590
|
+
continuity-reset Delete ALL continuity data for a project
|
|
1591
|
+
--project-path <path>
|
|
1592
|
+
|
|
1593
|
+
continuity-prune Keep only N most recent snapshots
|
|
1594
|
+
--project-path <path> [--keep <n>]
|
|
1595
|
+
|
|
1596
|
+
continuity-status Show continuity status at a glance
|
|
1597
|
+
--project-path <path>
|
|
1598
|
+
|
|
1599
|
+
HOW IT WORKS:
|
|
1600
|
+
|
|
1601
|
+
Unlike the resume approach (new process per message), this system
|
|
1602
|
+
uses Claude's stream-json protocol to keep ONE process alive per
|
|
1603
|
+
session. Messages are piped in/out via stdin/stdout.
|
|
1604
|
+
|
|
1605
|
+
Benefits:
|
|
1606
|
+
• No process restart overhead (~2-3s saved per message)
|
|
1607
|
+
• No context reload (lower token cost)
|
|
1608
|
+
• Instant follow-up responses
|
|
1609
|
+
• True multi-turn conversations
|
|
1610
|
+
|
|
1611
|
+
Sessions are persisted to ~/.clearctx/ and can be
|
|
1612
|
+
resumed even after the manager process exits (using --resume).
|
|
1613
|
+
|
|
1614
|
+
EXAMPLES:
|
|
1615
|
+
|
|
1616
|
+
# Start a session, send prompt, keep alive for follow-ups
|
|
1617
|
+
clearctx spawn --name fix-auth --prompt "Fix the auth bug in auth.service.ts"
|
|
1618
|
+
clearctx send --name fix-auth --message "Also add input validation"
|
|
1619
|
+
clearctx send --name fix-auth --message "Now write tests"
|
|
1620
|
+
clearctx stop --name fix-auth
|
|
1621
|
+
|
|
1622
|
+
# One-shot: spawn, get result, stop automatically
|
|
1623
|
+
clearctx spawn --name quick-task --prompt "Count TypeScript files" --stop
|
|
1624
|
+
|
|
1625
|
+
# Resume a stopped session days later
|
|
1626
|
+
clearctx resume --name fix-auth --message "Actually, also handle edge cases"
|
|
1627
|
+
|
|
1628
|
+
# Fork to try a different approach
|
|
1629
|
+
clearctx fork --name fix-auth --new-name fix-auth-v2 --message "Try JWT instead"
|
|
1630
|
+
|
|
1631
|
+
# Parallel batch
|
|
1632
|
+
clearctx batch --file tasks.json
|
|
1633
|
+
|
|
1634
|
+
# Send to stopped session (auto-resumes)
|
|
1635
|
+
clearctx send --name fix-auth --message "Add error handling"
|
|
1636
|
+
|
|
1637
|
+
DELEGATE (Control Loop + Safety Net):
|
|
1638
|
+
|
|
1639
|
+
# Delegate a task with $1 cost limit, auto-permission handling
|
|
1640
|
+
clearctx delegate --name fix-bug --task "Fix auth bug in auth.service.ts" --max-cost 1.00
|
|
1641
|
+
|
|
1642
|
+
# Delegate with read-only preset (no file edits allowed)
|
|
1643
|
+
clearctx delegate --name review --task "Review code quality" --preset read-only
|
|
1644
|
+
|
|
1645
|
+
# Get structured JSON output (for programmatic use by parent Claude)
|
|
1646
|
+
clearctx delegate --name task-1 --task "Count files" --model haiku --json
|
|
1647
|
+
|
|
1648
|
+
# Send follow-up to a delegated session
|
|
1649
|
+
clearctx continue --name fix-bug --message "Also add input validation"
|
|
1650
|
+
|
|
1651
|
+
# Full control: delegate with context and custom limits
|
|
1652
|
+
clearctx delegate --name big-task --task "Refactor auth" --model opus \\
|
|
1653
|
+
--preset full --max-cost 5.00 --max-turns 100 \\
|
|
1654
|
+
--context "Use JWT, not sessions"
|
|
1655
|
+
|
|
1656
|
+
PRESETS (for delegate):
|
|
1657
|
+
|
|
1658
|
+
read-only Can search/read files, no edits
|
|
1659
|
+
review Same as read-only (code review mode)
|
|
1660
|
+
edit Can read and edit files (default)
|
|
1661
|
+
full Full access (use with caution)
|
|
1662
|
+
plan Explore only, no execution
|
|
1663
|
+
|
|
1664
|
+
INSTALL:
|
|
1665
|
+
|
|
1666
|
+
npm install -g clearctx
|
|
1667
|
+
clearctx setup # Register MCP server with Claude Code
|
|
1668
|
+
# Then use: clearctx <command>
|
|
1669
|
+
# Or: npx clearctx <command>
|
|
1670
|
+
`);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// ============================================================================
|
|
1674
|
+
// Main
|
|
1675
|
+
// ============================================================================
|
|
1676
|
+
async function main() {
|
|
1677
|
+
const { command, flags } = parseArgs(process.argv);
|
|
1678
|
+
|
|
1679
|
+
// Create manager (shared across commands)
|
|
1680
|
+
const mgr = new SessionManager();
|
|
1681
|
+
|
|
1682
|
+
const commands = {
|
|
1683
|
+
spawn: () => cmdSpawn(mgr, flags),
|
|
1684
|
+
send: () => cmdSend(mgr, flags),
|
|
1685
|
+
input: () => cmdSend(mgr, flags), // Alias
|
|
1686
|
+
resume: () => cmdResume(mgr, flags),
|
|
1687
|
+
pause: () => cmdPause(mgr, flags),
|
|
1688
|
+
fork: () => cmdFork(mgr, flags),
|
|
1689
|
+
stop: () => cmdStop(mgr, flags),
|
|
1690
|
+
kill: () => cmdKill(mgr, flags),
|
|
1691
|
+
status: () => cmdStatus(mgr, flags),
|
|
1692
|
+
output: () => cmdOutput(mgr, flags),
|
|
1693
|
+
list: () => cmdList(mgr, flags),
|
|
1694
|
+
history: () => cmdHistory(mgr, flags),
|
|
1695
|
+
delete: () => cmdDelete(mgr, flags),
|
|
1696
|
+
batch: () => cmdBatch(mgr, flags),
|
|
1697
|
+
cleanup: () => cmdCleanup(mgr, flags),
|
|
1698
|
+
delegate: () => cmdDelegate(mgr, flags),
|
|
1699
|
+
continue: () => cmdContinue(mgr, flags),
|
|
1700
|
+
'team-roster': () => cmdTeamRoster(flags),
|
|
1701
|
+
'team-send': () => cmdTeamSend(flags),
|
|
1702
|
+
'team-broadcast': () => cmdTeamBroadcast(flags),
|
|
1703
|
+
'team-inbox': () => cmdTeamInbox(flags),
|
|
1704
|
+
'artifact-publish': () => cmdArtifactPublish(flags),
|
|
1705
|
+
'artifact-get': () => cmdArtifactGet(flags),
|
|
1706
|
+
'artifact-list': () => cmdArtifactList(flags),
|
|
1707
|
+
'contract-create': () => cmdContractCreate(flags),
|
|
1708
|
+
'contract-list': () => cmdContractList(flags),
|
|
1709
|
+
'contract-start': () => cmdContractStart(flags),
|
|
1710
|
+
'pipeline-create': () => cmdPipelineCreate(flags),
|
|
1711
|
+
'pipeline-list': () => cmdPipelineList(flags),
|
|
1712
|
+
'snapshot-create': () => cmdSnapshotCreate(flags),
|
|
1713
|
+
'snapshot-list': () => cmdSnapshotList(flags),
|
|
1714
|
+
'snapshot-rollback': () => cmdSnapshotRollback(flags),
|
|
1715
|
+
snapshot: () => cmdSnapshot(flags),
|
|
1716
|
+
briefing: () => cmdBriefing(flags),
|
|
1717
|
+
'stale-check': () => cmdStaleCheck(flags),
|
|
1718
|
+
decisions: () => cmdDecisions(flags),
|
|
1719
|
+
patterns: () => cmdPatterns(flags),
|
|
1720
|
+
'continuity-setup': () => cmdContinuitySetup(),
|
|
1721
|
+
'continuity-unsetup': () => cmdContinuityUnsetup(),
|
|
1722
|
+
'continuity-reset': () => cmdContinuityReset(flags),
|
|
1723
|
+
'continuity-prune': () => cmdContinuityPrune(flags),
|
|
1724
|
+
'continuity-status': () => cmdContinuityStatus(flags),
|
|
1725
|
+
setup: () => cmdSetup(flags),
|
|
1726
|
+
help: () => cmdHelp(),
|
|
1727
|
+
};
|
|
1728
|
+
|
|
1729
|
+
if (!commands[command]) {
|
|
1730
|
+
console.error(`Unknown command: "${command}". Run "clearctx help" for usage.`);
|
|
1731
|
+
process.exit(1);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
try {
|
|
1735
|
+
await commands[command]();
|
|
1736
|
+
} catch (err) {
|
|
1737
|
+
console.error(`Error: ${err.message}`);
|
|
1738
|
+
process.exit(1);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// If any sessions are still alive, keep the process running
|
|
1742
|
+
// so streaming sessions don't get killed
|
|
1743
|
+
if (mgr.sessions.size > 0 && !['list', 'status', 'output', 'history', 'help', 'cleanup'].includes(command)) {
|
|
1744
|
+
// Keep process alive until stopped
|
|
1745
|
+
if (['spawn', 'send', 'resume', 'fork'].includes(command) && !flags.stop) {
|
|
1746
|
+
// Don't hold the process — sessions survive in background
|
|
1747
|
+
// We stop them gracefully and they can be resumed
|
|
1748
|
+
mgr.stopAll();
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
main().catch(err => {
|
|
1754
|
+
console.error(`Fatal: ${err.message}`);
|
|
1755
|
+
process.exit(1);
|
|
1756
|
+
});
|