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/setup.js ADDED
@@ -0,0 +1,929 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ╔══════════════════════════════════════════════════════════════╗
5
+ * ║ clearctx — Interactive Setup CLI ║
6
+ * ╚══════════════════════════════════════════════════════════════╝
7
+ *
8
+ * Beautiful interactive installer using @clack/prompts.
9
+ * Lets users choose between local and global installation.
10
+ *
11
+ * Usage:
12
+ * ctx-setup Interactive setup wizard
13
+ * ctx-setup --global Global install (all projects)
14
+ * ctx-setup --local Local install (this project only)
15
+ * ctx-setup --global --yes Non-interactive global install + guide (CI mode)
16
+ * ctx-setup --local --yes Non-interactive local install + guide (CI mode)
17
+ * ctx-setup --global --no-guide Global install without CLAUDE.md guide
18
+ * ctx-setup --uninstall Interactive uninstall
19
+ * ctx-setup --uninstall --global Non-interactive global uninstall
20
+ * ctx-setup --migrate Clean up old CLAUDE.md injection
21
+ * ctx-setup --postinstall-hint (Internal) Print hint after npm install
22
+ */
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+ const os = require('os');
27
+
28
+ // ============================================================================
29
+ // Constants
30
+ // ============================================================================
31
+
32
+ // The name of the MCP server entry in config files
33
+ const MCP_SERVER_NAME = 'multi-session';
34
+
35
+ // Markers used to identify our section in CLAUDE.md
36
+ // (so we can detect duplicates and remove on uninstall)
37
+ const CLAUDE_MD_START_MARKER = '<!-- clearctx:start -->';
38
+ const CLAUDE_MD_END_MARKER = '<!-- clearctx:end -->';
39
+
40
+ // ============================================================================
41
+ // Strategy Guide Content — appended to CLAUDE.md when user opts in
42
+ // ============================================================================
43
+
44
+ // This teaches the main Claude session how to orchestrate team workers.
45
+ // Wrapped in markers so we can find and remove it later.
46
+ //
47
+ // IMPORTANT: This content MUST stay in sync with docs/ORCHESTRATOR-CLAUDE.md.
48
+ // When updating rules or adding features, update BOTH files.
49
+ // Failure to sync caused all test runs 1-6 to use stale orchestrator rules.
50
+ const STRATEGY_CONTENT = `
51
+ ${CLAUDE_MD_START_MARKER}
52
+
53
+ ## Multi-Session MCP Available (clearctx)
54
+
55
+ You have access to a Multi-Session MCP server (\`mcp__multi-session__*\` tools) that lets you spawn and coordinate multiple Claude Code sessions working in parallel.
56
+
57
+ IMPORTANT: When the user asks you to build something complex (more than 2 related tasks), use the multi-session system to parallelize the work instead of doing everything yourself.
58
+
59
+ ## Step 0: Verify Your Tools
60
+
61
+ Before starting ANY orchestration work, call \`server_version()\` to verify you're running the latest MCP tools. If the response shows a version mismatch, tell the user to restart Claude Code before proceeding — stale tools cause phantom failures.
62
+
63
+ ## How to Orchestrate
64
+
65
+ ### Rule 0: Define shared conventions BEFORE spawning workers
66
+ Before spawning workers, fill in the CONVENTION CHECKLIST. Either publish as an artifact (\`shared-conventions\`) or embed in every worker's prompt.
67
+
68
+ === CONVENTION CHECKLIST (define every item before spawning) ===
69
+ - [ ] Response format: e.g., \`{ data: <result> }\`
70
+ - [ ] Error format: e.g., \`{ error: <message> }\`
71
+ - [ ] Status codes: create=201, read=200, update=200, delete=200, notFound=404, badRequest=400, conflict=409
72
+ - [ ] Naming: e.g., snake_case for DB columns, camelCase for JS variables
73
+ - [ ] File paths: relative only, never absolute
74
+ - [ ] Enum/status values: list EXACT strings (e.g., "pending", "in_progress", "completed" — NOT "Pending" or "InProgress")
75
+ - [ ] Boolean handling: true/false vs 1/0 — pick one, specify it
76
+ - [ ] Date format: ISO 8601 strings, Unix timestamps, or other — specify which
77
+ - [ ] Audit/log action names: exact strings (e.g., "created" vs "create" vs "CREATE")
78
+ - [ ] Shared column names: list exact DB column names for tables multiple workers reference
79
+
80
+ Missing even ONE item causes convention mismatches that the orchestrator then has to fix manually — which violates Rule 6.
81
+
82
+ NEVER assume workers will independently agree on conventions — they won't.
83
+
84
+ ### Rule 1: You are the ORCHESTRATOR — not the implementer
85
+ - Plan the work, spawn workers, monitor progress
86
+ - Do NOT implement code yourself when you can delegate
87
+ - Do NOT create project foundation files (package.json, db.js, app.js, server.js) yourself — spawn a setup worker for Phase 0
88
+ - Do NOT read full outputs from workers — check artifacts and contract status instead
89
+
90
+ **Phase 0: Foundation Setup** — If the project needs shared infrastructure (database, app skeleton, package.json), spawn a \`setup\` worker FIRST. Wait for its \`project-foundation\` artifact before spawning other workers. Do NOT create these files yourself.
91
+
92
+ ### Rule 2: Use team_spawn for multi-session work
93
+ IMPORTANT: Spawn ALL independent workers in a SINGLE message with multiple tool calls. This makes them run in parallel.
94
+
95
+ ### Rule 3: Workers talk to each other — not through you
96
+ - Workers have team_ask, team_send_message, team_broadcast tools
97
+ - They can publish and read artifacts directly
98
+ - You should NOT relay messages between them
99
+ - If workers need each other's output, tell them to use team_ask
100
+ - Note: team_ask is a **fallback** for unexpected ambiguity. In well-orchestrated projects where you provide all context upfront, team_ask may never be called — this is the ideal case.
101
+
102
+ ### Rule 4: Post-Phase Verification (MANDATORY)
103
+ After ALL workers in a phase complete, BEFORE spawning the next phase, STOP and fill in this checklist:
104
+
105
+ === PHASE GATE CHECKPOINT (use phase_gate tool before EVERY team_spawn after Phase 0) ===
106
+
107
+ Instead of manually running 4 separate tool calls, use the \`phase_gate\` tool which does ALL checks in one call:
108
+
109
+ \`\`\`
110
+ mcp__multi-session__phase_gate({
111
+ phase_completing: "Phase 0: Foundation",
112
+ phase_starting: "Phase 1: Routes",
113
+ expected_artifacts: ["project-foundation", "shared-conventions"],
114
+ expected_idle: ["setup"],
115
+ expected_readers: { "shared-conventions": ["api-dev", "db-dev"] }
116
+ })
117
+ \`\`\`
118
+
119
+ The tool automatically:
120
+ 1. Checks all expected artifacts exist
121
+ 2. Validates artifact content and tracks the read as "orchestrator"
122
+ 3. Verifies all previous-phase workers are idle
123
+ 4. Confirms expected consumers actually read the artifacts
124
+ 5. Convention completeness advisory — warns if \`shared-conventions\` artifact is missing or incomplete (checks all 10 checklist keys: responseFormat, errorFormat, statusCodes, namingConventions, filePaths, enumValues, booleanHandling, dateFormat, auditActions, sharedColumnNames)
125
+
126
+ Check 5 is advisory (WARNING, not a gate failure) — the gate still passes/fails based on checks 1-4. But convention warnings are highly visible and should be addressed.
127
+
128
+ Returns a structured pass/fail report with recommendation.
129
+ PROCEED ONLY IF the report says ALL CHECKS PASSED.
130
+
131
+ **ENFORCED:** \`team_spawn\` will return an error if \`phase_gate\` was not called between spawn batches. The first batch (Phase 0) is free; every subsequent batch requires a passing \`phase_gate\` call first.
132
+
133
+ Count your phases upfront. If you have N phases, fill in this checkpoint exactly N-1 times (between every adjacent pair of phases). Skipping verification for later phases is the #1 cause of test failures.
134
+
135
+ Only intervene in workers when a session is BLOCKED or FAILED.
136
+ Do NOT verify worker output by reading files directly — check artifacts instead.
137
+
138
+ ### Rule 5: Always tell workers to publish artifacts
139
+ Every worker prompt should include instructions to:
140
+ 1. Check inbox before starting (\`team_check_inbox\`) — **ENFORCED**: artifact and contract tools are blocked until workers call this
141
+ 2. Use \`team_ask\` to communicate with teammates (NOT you)
142
+ 3. Publish output as artifacts (\`artifact_publish\`) — auto-registers file ownership from \`files\`/\`filesCreated\`/\`filesModified\` in data
143
+ 4. Broadcast completion (\`team_broadcast\`)
144
+ 5. ~~Update status to idle when done~~ — **AUTO-MANAGED**: status is automatically set to "active" on spawn and "idle" on stop/kill. Workers only need \`team_update_status\` for custom statuses like "blocked".
145
+ 6. Follow shared conventions defined in Rule 0 (include them in the prompt or reference the conventions artifact)
146
+
147
+ ### Rule 6: Don't fix worker code yourself (pragmatic exception for trivial fixes)
148
+
149
+ === FIX PROTOCOL (when you must fix worker code directly) ===
150
+ STOP. Before editing any file a worker created, call \`check_file_owner({ file: "path/to/file" })\` to verify ownership, then answer these questions:
151
+
152
+ 1. Is this fix ≤ 3 lines?
153
+ NO → \`send_message\` to worker or spawn fix-worker. Do NOT fix yourself.
154
+ YES → continue to step 2.
155
+
156
+ 2. Is the worker done (idle status in \`team_roster\`)?
157
+ NO → \`send_message\` to worker. Do NOT fix yourself.
158
+ YES → continue to step 3.
159
+
160
+ 3. Make the fix.
161
+
162
+ 4. Broadcast: \`team_broadcast({ from: "orchestrator", content: "Fixed [file]:[lines] — [description of change]" })\`
163
+
164
+ 5. Re-publish: If the fix changes data in a published artifact, call \`artifact_publish\` to update it.
165
+
166
+ NEVER skip steps 4-5. Unannounced fixes cause downstream workers to use stale assumptions.
167
+
168
+ If the failure is due to convention mismatch (wrong response format, etc.), that's YOUR fault — update the conventions and notify the affected workers.
169
+
170
+ ### Rule 7: Verify artifacts between phases (Phase Gates)
171
+ Use the PHASE GATE CHECKPOINT from Rule 4 between every pair of phases. This is the same checklist — Rule 7 reinforces that it applies to EVERY phase transition, not just the first one.
172
+
173
+ After all workers finish, verify they consumed shared artifacts:
174
+ \`\`\`
175
+ mcp__multi-session__artifact_readers({ artifactId: "shared-conventions" })
176
+ \`\`\`
177
+ This shows which workers actually read the conventions. If a worker is missing, they may have ignored the shared contract.
178
+
179
+ NEVER trust a worker's self-reported completion — verify the artifact exists yourself.
180
+
181
+ ## Auto-Behaviors (v2.7.0)
182
+
183
+ These happen automatically — no action needed from you or the workers:
184
+ - **Roster summary injection**: \`team_spawn\`, \`phase_gate\`, and \`send_message\` (to team workers) responses include a compact roster line: \`[Team: worker-a=active, worker-b=idle]\`
185
+ - **Auto-status management**: Workers are set to "active" on spawn and "idle" when their session stops/is killed
186
+ - **Inbox enforcement**: Workers are blocked from using artifact/contract tools until they call \`team_check_inbox\`
187
+ - **File ownership tracking**: \`artifact_publish\` auto-registers file ownership from \`files\`/\`filesCreated\`/\`filesModified\` arrays in artifact data
188
+ - **Convention completeness check**: \`phase_gate\` warns if \`shared-conventions\` artifact is missing or has incomplete fields
189
+
190
+ ## Quick Reference (68 tools)
191
+
192
+ | You want to... | Use this tool |
193
+ |----------------|---------------|
194
+ | Verify tools before starting | \`server_version\` |
195
+ | Build a multi-person project | \`team_spawn\` (multiple in parallel) |
196
+ | Run a single isolated task | \`delegate_task\` |
197
+ | Check who's working on what | \`team_roster\` (also auto-injected in spawn/gate/message responses) |
198
+ | See published outputs | \`artifact_list\` |
199
+ | See task completion status | \`contract_list\` |
200
+ | Verify phase completion | \`phase_gate\` |
201
+ | Send a correction to a worker | \`send_message\` to that session |
202
+ | Check who read an artifact | \`artifact_readers\` |
203
+ | Check file ownership (Rule 6) | \`check_file_owner\` |
204
+ | Register file ownership | \`register_files\` |
205
+ | Clean up between runs | \`team_reset\` |
206
+
207
+ ### When NOT to Delegate
208
+ - Simple tasks (< 5 min, < 3 files) — do it yourself
209
+ - Just reading/exploring — use Read, Grep, Glob directly
210
+ - Tightly coupled changes — must happen atomically
211
+
212
+ ### Resetting Between Runs
213
+ Call \`team_reset({ confirm: true })\` to clean up all team state between orchestration runs. This clears artifacts, contracts, roster, and messages.
214
+
215
+ ${CLAUDE_MD_END_MARKER}
216
+ `;
217
+
218
+ // ============================================================================
219
+ // Helpers (kept from original — zero dependencies)
220
+ // ============================================================================
221
+
222
+ /**
223
+ * Figure out whether this package was installed globally or locally,
224
+ * and return the right MCP server command config.
225
+ *
226
+ * Global install: the `ctx-mcp` binary is on PATH, so we can just call it.
227
+ * Local install: we need an absolute path to bin/mcp.js.
228
+ */
229
+ function detectMcpCommand() {
230
+ // __dirname is the "bin/" folder where this script lives
231
+ // One level up is the package root
232
+ const packageRoot = path.resolve(__dirname, '..');
233
+ const mcpBin = path.join(packageRoot, 'bin', 'mcp.js');
234
+
235
+ // Check if ctx-mcp is likely on PATH (global install)
236
+ // We do this by checking if the package is inside a global node_modules
237
+ const isGlobal = packageRoot.includes(path.join('node_modules', 'clearctx'))
238
+ && (
239
+ packageRoot.includes(path.join('lib', 'node_modules')) // npm global on Linux/Mac
240
+ || packageRoot.includes(path.join('AppData', 'Roaming', 'npm')) // npm global on Windows
241
+ || packageRoot.includes('nvm') // nvm managed
242
+ || packageRoot.includes('.volta') // volta managed
243
+ );
244
+
245
+ if (isGlobal) {
246
+ // Global install — ctx-mcp should be on PATH
247
+ return { command: 'ctx-mcp' };
248
+ } else {
249
+ // Local or dev install — use absolute path to bin/mcp.js
250
+ return { command: 'node', args: [mcpBin] };
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Read a JSON file safely. Returns empty object if file doesn't exist
256
+ * or has invalid JSON.
257
+ */
258
+ function readJsonSafe(filePath) {
259
+ try {
260
+ if (!fs.existsSync(filePath)) return {};
261
+ const raw = fs.readFileSync(filePath, 'utf-8');
262
+ return JSON.parse(raw);
263
+ } catch {
264
+ return {};
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Write JSON to a file with nice formatting.
270
+ * Creates parent directories if they don't exist.
271
+ */
272
+ function writeJson(filePath, data) {
273
+ const dir = path.dirname(filePath);
274
+ if (!fs.existsSync(dir)) {
275
+ fs.mkdirSync(dir, { recursive: true });
276
+ }
277
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
278
+ }
279
+
280
+ // ============================================================================
281
+ // Core Functions
282
+ // ============================================================================
283
+
284
+ /**
285
+ * Register MCP server globally in ~/.claude.json.
286
+ * This is a simple upsert — adds our entry to the top-level "mcpServers" key.
287
+ * Safe and idempotent: won't overwrite if already present.
288
+ *
289
+ * @param {string} configPath — Path to ~/.claude.json
290
+ * @returns {boolean} true if we added/updated, false if already existed
291
+ */
292
+ function registerGlobal(configPath) {
293
+ const config = readJsonSafe(configPath);
294
+
295
+ // Make sure the mcpServers object exists
296
+ if (!config.mcpServers) {
297
+ config.mcpServers = {};
298
+ }
299
+
300
+ // Skip if already registered (don't overwrite existing config)
301
+ if (config.mcpServers[MCP_SERVER_NAME]) {
302
+ return false; // already existed
303
+ }
304
+
305
+ // Add our MCP server entry
306
+ config.mcpServers[MCP_SERVER_NAME] = detectMcpCommand();
307
+ writeJson(configPath, config);
308
+ return true; // newly added
309
+ }
310
+
311
+ /**
312
+ * Register MCP server locally by creating .mcp.json in the project root.
313
+ * This is the standard Claude Code local MCP config format.
314
+ * Uses absolute path to the bin/mcp.js file.
315
+ *
316
+ * @param {string} projectDir — The project root directory (usually cwd)
317
+ * @returns {boolean} true if we created/updated, false if already existed
318
+ */
319
+ function registerLocal(projectDir) {
320
+ const mcpJsonPath = path.join(projectDir, '.mcp.json');
321
+ const config = readJsonSafe(mcpJsonPath);
322
+
323
+ // Make sure the mcpServers object exists
324
+ if (!config.mcpServers) {
325
+ config.mcpServers = {};
326
+ }
327
+
328
+ // Skip if already registered
329
+ if (config.mcpServers[MCP_SERVER_NAME]) {
330
+ return false; // already existed
331
+ }
332
+
333
+ // Add our MCP server entry with absolute path
334
+ config.mcpServers[MCP_SERVER_NAME] = detectMcpCommand();
335
+ writeJson(mcpJsonPath, config);
336
+ return true; // newly added
337
+ }
338
+
339
+ /**
340
+ * Remove MCP server from global config (~/.claude.json).
341
+ * Cleans both the top-level mcpServers key and legacy per-project format.
342
+ *
343
+ * @param {string} configPath — Path to ~/.claude.json
344
+ * @returns {boolean} true if something was removed
345
+ */
346
+ function removeGlobal(configPath) {
347
+ if (!fs.existsSync(configPath)) return false;
348
+
349
+ const config = readJsonSafe(configPath);
350
+ let removed = false;
351
+
352
+ // Remove from top-level mcpServers
353
+ if (config.mcpServers && config.mcpServers[MCP_SERVER_NAME]) {
354
+ delete config.mcpServers[MCP_SERVER_NAME];
355
+ removed = true;
356
+ }
357
+
358
+ // Also clean up legacy format: projects."<path>".mcpServers
359
+ if (config.projects) {
360
+ for (const projPath of Object.keys(config.projects)) {
361
+ const proj = config.projects[projPath];
362
+ if (proj && proj.mcpServers && proj.mcpServers[MCP_SERVER_NAME]) {
363
+ delete proj.mcpServers[MCP_SERVER_NAME];
364
+ removed = true;
365
+ }
366
+ }
367
+ }
368
+
369
+ if (removed) {
370
+ writeJson(configPath, config);
371
+ }
372
+
373
+ return removed;
374
+ }
375
+
376
+ /**
377
+ * Remove MCP server from local .mcp.json.
378
+ *
379
+ * @param {string} projectDir — The project root directory
380
+ * @returns {boolean} true if something was removed
381
+ */
382
+ function removeLocal(projectDir) {
383
+ const mcpJsonPath = path.join(projectDir, '.mcp.json');
384
+ if (!fs.existsSync(mcpJsonPath)) return false;
385
+
386
+ const config = readJsonSafe(mcpJsonPath);
387
+ let removed = false;
388
+
389
+ if (config.mcpServers && config.mcpServers[MCP_SERVER_NAME]) {
390
+ delete config.mcpServers[MCP_SERVER_NAME];
391
+ removed = true;
392
+ }
393
+
394
+ if (removed) {
395
+ // If mcpServers is now empty, delete the file entirely
396
+ if (Object.keys(config.mcpServers).length === 0) {
397
+ fs.unlinkSync(mcpJsonPath);
398
+ } else {
399
+ writeJson(mcpJsonPath, config);
400
+ }
401
+ }
402
+
403
+ return removed;
404
+ }
405
+
406
+ /**
407
+ * Append the orchestrator guide to a CLAUDE.md file.
408
+ * Safe: checks for existing markers to avoid duplicates.
409
+ * Never deletes or replaces existing content.
410
+ *
411
+ * @param {'global'|'local'} scope — Where to write the guide
412
+ * @returns {{ path: string, created: boolean, skipped: boolean }}
413
+ */
414
+ function addGuide(scope) {
415
+ // Choose the right CLAUDE.md path based on scope
416
+ const claudeMdPath = scope === 'global'
417
+ ? path.join(os.homedir(), '.claude', 'CLAUDE.md')
418
+ : path.join(process.cwd(), 'CLAUDE.md');
419
+
420
+ const result = { path: claudeMdPath, created: false, skipped: false };
421
+
422
+ // Read existing content (if any)
423
+ let existing = '';
424
+ if (fs.existsSync(claudeMdPath)) {
425
+ existing = fs.readFileSync(claudeMdPath, 'utf-8');
426
+ }
427
+
428
+ // Check if our section already exists
429
+ if (existing.includes(CLAUDE_MD_START_MARKER)) {
430
+ // Extract current content between markers and compare with latest
431
+ const startIdx = existing.indexOf(CLAUDE_MD_START_MARKER);
432
+ const endIdx = existing.indexOf(CLAUDE_MD_END_MARKER);
433
+ if (startIdx !== -1 && endIdx !== -1) {
434
+ const currentContent = existing.slice(startIdx, endIdx + CLAUDE_MD_END_MARKER.length).trim();
435
+ const newContent = STRATEGY_CONTENT.trim();
436
+ if (currentContent === newContent) {
437
+ result.skipped = true;
438
+ return result;
439
+ }
440
+ // Content is stale — replace our section, preserve user's other content
441
+ const before = existing.slice(0, startIdx).trimEnd();
442
+ const after = existing.slice(endIdx + CLAUDE_MD_END_MARKER.length).trimStart();
443
+ const updated = (before ? before + '\n\n' : '') + newContent + (after ? '\n\n' + after : '\n');
444
+ fs.writeFileSync(claudeMdPath, updated, 'utf-8');
445
+ result.updated = true;
446
+ return result;
447
+ }
448
+ // Malformed markers — skip to be safe
449
+ result.skipped = true;
450
+ return result;
451
+ }
452
+
453
+ // Create directory if needed
454
+ const dir = path.dirname(claudeMdPath);
455
+ if (!fs.existsSync(dir)) {
456
+ fs.mkdirSync(dir, { recursive: true });
457
+ }
458
+
459
+ // Append or create the file
460
+ if (existing) {
461
+ // Append to existing file — preserves all existing content
462
+ fs.appendFileSync(claudeMdPath, '\n' + STRATEGY_CONTENT, 'utf-8');
463
+ } else {
464
+ // Create new file
465
+ fs.writeFileSync(claudeMdPath, STRATEGY_CONTENT.trim() + '\n', 'utf-8');
466
+ result.created = true;
467
+ }
468
+
469
+ return result;
470
+ }
471
+
472
+ /**
473
+ * Remove the orchestrator guide section from a CLAUDE.md file.
474
+ * Finds content between our markers and removes it cleanly.
475
+ * If the file becomes empty after removal, deletes it.
476
+ *
477
+ * @param {string} claudeMdPath — Path to the CLAUDE.md file
478
+ * @returns {boolean} true if content was removed
479
+ */
480
+ function removeGuideFromFile(claudeMdPath) {
481
+ if (!fs.existsSync(claudeMdPath)) return false;
482
+
483
+ let content = fs.readFileSync(claudeMdPath, 'utf-8');
484
+ if (!content.includes(CLAUDE_MD_START_MARKER)) return false;
485
+
486
+ // Find the markers and remove everything between them (inclusive)
487
+ const startIdx = content.indexOf(CLAUDE_MD_START_MARKER);
488
+ const endIdx = content.indexOf(CLAUDE_MD_END_MARKER);
489
+
490
+ if (startIdx === -1 || endIdx === -1) return false;
491
+
492
+ // Extract content before and after our section
493
+ const before = content.slice(0, startIdx).trimEnd();
494
+ const after = content.slice(endIdx + CLAUDE_MD_END_MARKER.length).trimStart();
495
+
496
+ // Rebuild the content
497
+ content = before + (after ? '\n\n' + after : '') + '\n';
498
+
499
+ // If the file is now effectively empty, delete it
500
+ if (content.trim().length === 0) {
501
+ fs.unlinkSync(claudeMdPath);
502
+ } else {
503
+ fs.writeFileSync(claudeMdPath, content, 'utf-8');
504
+ }
505
+
506
+ return true;
507
+ }
508
+
509
+ /**
510
+ * Run migration — clean up old CLAUDE.md injection from both global and local files.
511
+ *
512
+ * @returns {{ global: boolean, local: boolean }} which files were cleaned
513
+ */
514
+ function runMigration() {
515
+ const globalPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
516
+ const localPath = path.join(process.cwd(), 'CLAUDE.md');
517
+
518
+ return {
519
+ global: removeGuideFromFile(globalPath),
520
+ local: removeGuideFromFile(localPath),
521
+ };
522
+ }
523
+
524
+ // ============================================================================
525
+ // Postinstall Hint — silent, safe, runs after `npm install`
526
+ // ============================================================================
527
+
528
+ /**
529
+ * Called by npm's postinstall script.
530
+ * Does two things:
531
+ * 1. Silently registers MCP server globally (safe/additive, never overwrites)
532
+ * 2. Prints a short stderr hint telling users to run ctx-setup
533
+ *
534
+ * Does NOT touch CLAUDE.md — that requires interactive user consent.
535
+ */
536
+ function runPostinstallHint() {
537
+ // npm suppresses stdout, so we use stderr for messages
538
+ const write = (msg) => process.stderr.write(msg + '\n');
539
+
540
+ try {
541
+ const configPath = path.join(os.homedir(), '.claude.json');
542
+
543
+ // Auto-register MCP server globally (safe — only adds if not present)
544
+ const added = registerGlobal(configPath);
545
+
546
+ if (added) {
547
+ write('');
548
+ write(' clearctx: MCP server registered globally.');
549
+ write(' Run "ctx-setup" to configure orchestrator guide.');
550
+ write('');
551
+ } else {
552
+ // Already registered — check if CLAUDE.md guide needs updating
553
+ write('');
554
+ const guideResult = addGuide('global');
555
+ if (guideResult.updated) {
556
+ write(' clearctx: Orchestrator guide updated in ~/.claude/CLAUDE.md');
557
+ write(' Restart Claude Code to use the latest rules.');
558
+ } else {
559
+ write(' clearctx: Already configured and up to date.');
560
+ }
561
+ write('');
562
+ }
563
+ } catch {
564
+ // If auto-config fails (permissions, etc.), print manual hint
565
+ write('');
566
+ write(' clearctx installed! Run "ctx-setup" to configure.');
567
+ write('');
568
+ }
569
+ }
570
+
571
+ // ============================================================================
572
+ // Interactive Setup — beautiful CLI using @clack/prompts
573
+ // ============================================================================
574
+
575
+ /**
576
+ * Run the interactive setup wizard.
577
+ * Uses @clack/prompts for a beautiful terminal UI.
578
+ *
579
+ * @param {object} flags — CLI flags from argv
580
+ */
581
+ async function runInteractiveSetup(flags) {
582
+ // Load @clack/prompts and picocolors
583
+ // (require here so postinstall-hint doesn't need them)
584
+ const clack = require('@clack/prompts');
585
+ const pc = require('picocolors');
586
+
587
+ // Read package version for the header
588
+ let version = '2.x';
589
+ try {
590
+ const pkg = require('../package.json');
591
+ version = pkg.version || version;
592
+ } catch {
593
+ // fallback to default
594
+ }
595
+
596
+ // Determine if we're in non-interactive (CI) mode
597
+ const nonInteractive = flags.yes === true;
598
+
599
+ // Determine scope from flags (if provided)
600
+ let scope = flags.global ? 'global' : flags.local ? 'local' : null;
601
+
602
+ // Determine guide preference from flags
603
+ let wantGuide = flags['no-guide'] ? false : (flags.yes ? true : null);
604
+
605
+ // ── Start the interactive UI ──
606
+ clack.intro(pc.cyan(`Claude Multi-Session System v${version}`));
607
+
608
+ // ── Step 1: Choose scope (global vs local) ──
609
+ if (!scope) {
610
+ if (nonInteractive) {
611
+ // Default to global in CI mode
612
+ scope = 'global';
613
+ } else {
614
+ const scopeChoice = await clack.select({
615
+ message: 'How would you like to install?',
616
+ options: [
617
+ {
618
+ value: 'global',
619
+ label: 'Global',
620
+ hint: 'Available in all projects',
621
+ },
622
+ {
623
+ value: 'local',
624
+ label: 'Local',
625
+ hint: 'This project only',
626
+ },
627
+ ],
628
+ });
629
+
630
+ // User cancelled (Ctrl+C)
631
+ if (clack.isCancel(scopeChoice)) {
632
+ clack.cancel('Setup cancelled.');
633
+ process.exit(0);
634
+ }
635
+
636
+ scope = scopeChoice;
637
+ }
638
+ }
639
+
640
+ // ── Step 2: Register MCP server ──
641
+ const spin = clack.spinner();
642
+ spin.start(scope === 'global' ? 'Registering MCP server globally...' : 'Registering MCP server locally...');
643
+
644
+ let registered;
645
+ const configPath = path.join(os.homedir(), '.claude.json');
646
+ const localMcpJsonPath = path.join(process.cwd(), '.mcp.json');
647
+
648
+ if (scope === 'global') {
649
+ registered = registerGlobal(configPath);
650
+ } else {
651
+ registered = registerLocal(process.cwd());
652
+ }
653
+
654
+ if (registered) {
655
+ if (scope === 'global') {
656
+ spin.stop(pc.green('MCP server registered globally in ~/.claude.json'));
657
+ } else {
658
+ spin.stop(pc.green('MCP server registered locally in .mcp.json'));
659
+ }
660
+ } else {
661
+ spin.stop(pc.yellow('MCP server already registered — skipped.'));
662
+ }
663
+
664
+ // ── Step 3: Check for conflicting registrations ──
665
+ // After global registration, check if local .mcp.json also has it
666
+ if (scope === 'global' && !nonInteractive) {
667
+ const localConfig = readJsonSafe(localMcpJsonPath);
668
+ if (localConfig.mcpServers && localConfig.mcpServers[MCP_SERVER_NAME]) {
669
+ const removeLocalChoice = await clack.confirm({
670
+ message: 'Local .mcp.json also has multi-session registered. Remove the local entry?',
671
+ initialValue: true,
672
+ });
673
+
674
+ if (!clack.isCancel(removeLocalChoice) && removeLocalChoice) {
675
+ removeLocal(process.cwd());
676
+ clack.log.success('Removed local .mcp.json entry.');
677
+ }
678
+ }
679
+ }
680
+
681
+ // After local registration, check if global config also has it
682
+ if (scope === 'local' && !nonInteractive) {
683
+ const globalConfig = readJsonSafe(configPath);
684
+ if (globalConfig.mcpServers && globalConfig.mcpServers[MCP_SERVER_NAME]) {
685
+ const removeGlobalChoice = await clack.confirm({
686
+ message: 'Global ~/.claude.json also has multi-session registered. Remove the global entry?',
687
+ initialValue: false,
688
+ });
689
+
690
+ if (!clack.isCancel(removeGlobalChoice) && removeGlobalChoice) {
691
+ removeGlobal(configPath);
692
+ clack.log.success('Removed global entry from ~/.claude.json');
693
+ }
694
+ }
695
+ }
696
+
697
+ // ── Step 4: Orchestrator guide in CLAUDE.md ──
698
+ // Figure out the target path and check if a file already exists there
699
+ const guideTargetPath = scope === 'global'
700
+ ? path.join(os.homedir(), '.claude', 'CLAUDE.md')
701
+ : path.join(process.cwd(), 'CLAUDE.md');
702
+ const guideDisplayPath = scope === 'global' ? '~/.claude/CLAUDE.md' : './CLAUDE.md';
703
+ const guideFileExists = fs.existsSync(guideTargetPath);
704
+ const guideAlreadyInjected = guideFileExists
705
+ && fs.readFileSync(guideTargetPath, 'utf-8').includes(CLAUDE_MD_START_MARKER);
706
+
707
+ if (wantGuide === null) {
708
+ // Tell the user what will happen before they confirm
709
+ if (guideAlreadyInjected) {
710
+ // Check if content is stale
711
+ const currentFile = fs.readFileSync(guideTargetPath, 'utf-8');
712
+ const startIdx = currentFile.indexOf(CLAUDE_MD_START_MARKER);
713
+ const endIdx = currentFile.indexOf(CLAUDE_MD_END_MARKER);
714
+ const currentContent = currentFile.slice(startIdx, endIdx + CLAUDE_MD_END_MARKER.length).trim();
715
+ const isStale = currentContent !== STRATEGY_CONTENT.trim();
716
+ if (isStale) {
717
+ clack.log.warn(`Orchestrator guide in ${guideDisplayPath} is outdated.`);
718
+ wantGuide = true; // Auto-update — addGuide handles the replacement
719
+ } else {
720
+ clack.log.info(`Orchestrator guide in ${guideDisplayPath} is up to date.`);
721
+ wantGuide = false;
722
+ }
723
+ } else if (guideFileExists) {
724
+ // File exists — reassure user their content is safe
725
+ clack.log.info(`Found existing ${guideDisplayPath} — your content will be preserved.`);
726
+ const guideChoice = await clack.confirm({
727
+ message: `Append orchestrator guide to ${guideDisplayPath}? (Recommended — your existing content stays intact)`,
728
+ initialValue: true,
729
+ });
730
+
731
+ if (clack.isCancel(guideChoice)) {
732
+ clack.cancel('Setup cancelled.');
733
+ process.exit(0);
734
+ }
735
+
736
+ wantGuide = guideChoice;
737
+ } else {
738
+ // No file yet — straightforward ask
739
+ const guideChoice = await clack.confirm({
740
+ message: `Create ${guideDisplayPath} with orchestrator guide? (Recommended)`,
741
+ initialValue: true,
742
+ });
743
+
744
+ if (clack.isCancel(guideChoice)) {
745
+ clack.cancel('Setup cancelled.');
746
+ process.exit(0);
747
+ }
748
+
749
+ wantGuide = guideChoice;
750
+ }
751
+ }
752
+
753
+ if (wantGuide) {
754
+ const guideResult = addGuide(scope);
755
+
756
+ if (guideResult.updated) {
757
+ clack.log.success(`Updated orchestrator guide in ${guideDisplayPath} (new rules synced)`);
758
+ } else if (guideResult.skipped) {
759
+ clack.log.info('Orchestrator guide already up to date — skipped.');
760
+ } else if (guideResult.created) {
761
+ clack.log.success(`Created ${guideDisplayPath} with orchestrator guide`);
762
+ } else {
763
+ clack.log.success(`Appended orchestrator guide to ${guideDisplayPath} (existing content preserved)`);
764
+ }
765
+ }
766
+
767
+ // ── Done! ──
768
+ clack.outro(pc.green('Setup complete!') + pc.dim(' Restart Claude Code to load multi-session tools.'));
769
+ }
770
+
771
+ // ============================================================================
772
+ // Interactive Uninstall
773
+ // ============================================================================
774
+
775
+ /**
776
+ * Run the interactive uninstall wizard.
777
+ *
778
+ * @param {object} flags — CLI flags
779
+ */
780
+ async function runUninstall(flags) {
781
+ const clack = require('@clack/prompts');
782
+ const pc = require('picocolors');
783
+
784
+ clack.intro(pc.red('Uninstall Claude Multi-Session'));
785
+
786
+ // Determine scope
787
+ let scope = flags.global ? 'global' : flags.local ? 'local' : flags.both ? 'both' : null;
788
+
789
+ if (!scope) {
790
+ const scopeChoice = await clack.select({
791
+ message: 'What would you like to remove?',
792
+ options: [
793
+ { value: 'global', label: 'Global', hint: 'Remove from ~/.claude.json' },
794
+ { value: 'local', label: 'Local', hint: 'Remove from .mcp.json' },
795
+ { value: 'both', label: 'Both', hint: 'Remove all registrations' },
796
+ ],
797
+ });
798
+
799
+ if (clack.isCancel(scopeChoice)) {
800
+ clack.cancel('Uninstall cancelled.');
801
+ process.exit(0);
802
+ }
803
+
804
+ scope = scopeChoice;
805
+ }
806
+
807
+ const spin = clack.spinner();
808
+ const configPath = path.join(os.homedir(), '.claude.json');
809
+
810
+ // Remove MCP registrations
811
+ spin.start('Removing MCP server registrations...');
812
+
813
+ let removedGlobal = false;
814
+ let removedLocal = false;
815
+
816
+ if (scope === 'global' || scope === 'both') {
817
+ removedGlobal = removeGlobal(configPath);
818
+ }
819
+
820
+ if (scope === 'local' || scope === 'both') {
821
+ removedLocal = removeLocal(process.cwd());
822
+ }
823
+
824
+ if (removedGlobal || removedLocal) {
825
+ const parts = [];
826
+ if (removedGlobal) parts.push('global (~/.claude.json)');
827
+ if (removedLocal) parts.push('local (.mcp.json)');
828
+ spin.stop(pc.green(`Removed MCP entries: ${parts.join(', ')}`));
829
+ } else {
830
+ spin.stop(pc.yellow('No MCP entries found to remove.'));
831
+ }
832
+
833
+ // Clean up CLAUDE.md
834
+ spin.start('Cleaning up CLAUDE.md...');
835
+
836
+ const migration = runMigration();
837
+
838
+ if (migration.global || migration.local) {
839
+ const parts = [];
840
+ if (migration.global) parts.push('~/.claude/CLAUDE.md');
841
+ if (migration.local) parts.push('./CLAUDE.md');
842
+ spin.stop(pc.green(`Removed orchestrator guide from: ${parts.join(', ')}`));
843
+ } else {
844
+ spin.stop(pc.dim('No orchestrator guide found in CLAUDE.md — nothing to clean.'));
845
+ }
846
+
847
+ clack.outro(pc.green('Uninstall complete.') + pc.dim(' Restart Claude Code to apply.'));
848
+ }
849
+
850
+ // ============================================================================
851
+ // Main Entry Point — dispatches to the right handler
852
+ // ============================================================================
853
+
854
+ /**
855
+ * Main entry point.
856
+ * Dispatches based on CLI flags to the right handler.
857
+ *
858
+ * @param {object} flags — CLI flags
859
+ */
860
+ async function run(flags = {}) {
861
+ // --postinstall-hint: silent, safe, runs after npm install
862
+ if (flags['postinstall-hint']) {
863
+ runPostinstallHint();
864
+ return;
865
+ }
866
+
867
+ // Legacy --postinstall flag (backwards compatibility)
868
+ if (flags.postinstall) {
869
+ runPostinstallHint();
870
+ return;
871
+ }
872
+
873
+ // --migrate: clean up old CLAUDE.md injection
874
+ if (flags.migrate) {
875
+ const clack = require('@clack/prompts');
876
+ const pc = require('picocolors');
877
+
878
+ clack.intro(pc.cyan('Migrate: Clean up old CLAUDE.md injection'));
879
+
880
+ const spin = clack.spinner();
881
+ spin.start('Scanning for old injection markers...');
882
+
883
+ const result = runMigration();
884
+
885
+ if (result.global || result.local) {
886
+ const parts = [];
887
+ if (result.global) parts.push('~/.claude/CLAUDE.md');
888
+ if (result.local) parts.push('./CLAUDE.md');
889
+ spin.stop(pc.green(`Cleaned up: ${parts.join(', ')}`));
890
+ } else {
891
+ spin.stop(pc.dim('No old injection markers found — nothing to clean.'));
892
+ }
893
+
894
+ clack.outro(pc.green('Migration complete.'));
895
+ return;
896
+ }
897
+
898
+ // --uninstall: interactive uninstall wizard
899
+ if (flags.uninstall) {
900
+ await runUninstall(flags);
901
+ return;
902
+ }
903
+
904
+ // Default: interactive setup wizard
905
+ await runInteractiveSetup(flags);
906
+ }
907
+
908
+ // ============================================================================
909
+ // Direct invocation support (node bin/setup.js or ctx-setup)
910
+ // ============================================================================
911
+
912
+ // If this script is run directly (not required by cli.js),
913
+ // parse flags and run the wizard.
914
+ if (require.main === module) {
915
+ const args = process.argv.slice(2);
916
+ const flags = {};
917
+ for (const arg of args) {
918
+ if (arg.startsWith('--')) {
919
+ const key = arg.slice(2);
920
+ flags[key] = true;
921
+ }
922
+ }
923
+ run(flags).catch((err) => {
924
+ console.error(`\n Setup failed: ${err.message}\n`);
925
+ process.exit(1);
926
+ });
927
+ }
928
+
929
+ module.exports = { run };