clearctx 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/LICENSE +21 -0
- package/README.md +1006 -0
- package/STRATEGY.md +485 -0
- package/bin/cli.js +1756 -0
- package/bin/continuity-hook.js +118 -0
- package/bin/mcp.js +27 -0
- package/bin/setup.js +929 -0
- package/package.json +56 -0
- package/src/artifact-store.js +710 -0
- package/src/atomic-io.js +99 -0
- package/src/briefing-generator.js +451 -0
- package/src/continuity-hooks.js +253 -0
- package/src/contract-store.js +525 -0
- package/src/decision-journal.js +229 -0
- package/src/delegate.js +348 -0
- package/src/dependency-resolver.js +453 -0
- package/src/diff-engine.js +473 -0
- package/src/file-lock.js +161 -0
- package/src/index.js +61 -0
- package/src/lineage-graph.js +402 -0
- package/src/manager.js +510 -0
- package/src/mcp-server.js +3501 -0
- package/src/pattern-registry.js +221 -0
- package/src/pipeline-engine.js +618 -0
- package/src/prompts.js +1217 -0
- package/src/safety-net.js +170 -0
- package/src/session-snapshot.js +508 -0
- package/src/snapshot-engine.js +490 -0
- package/src/stale-detector.js +169 -0
- package/src/store.js +131 -0
- package/src/stream-session.js +463 -0
- package/src/team-hub.js +615 -0
package/bin/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 };
|