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/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
+ });