@theronap/cortex-mcp 0.9.38 → 0.9.40
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/cortex-mcp.mjs +7 -0
- package/lib/capture.mjs +46 -5
- package/lib/diagnose.mjs +12 -2
- package/lib/edge_extract.mjs +60 -1
- package/lib/uninstall.mjs +190 -0
- package/package.json +1 -1
package/bin/cortex-mcp.mjs
CHANGED
|
@@ -43,6 +43,7 @@ if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
|
43
43
|
`Subcommands:\n` +
|
|
44
44
|
` setup <token> wire MCP server + capture hook into ~/.claude config\n` +
|
|
45
45
|
` repair re-run setup at the latest version using your existing token (no token needed)\n` +
|
|
46
|
+
` uninstall remove ALL Cortex wiring (MCP, hooks, skills, launchd, cron). --dry-run to preview, --purge to also wipe ~/.cortex + npx cache\n` +
|
|
46
47
|
` doctor live health check — confirm your token works (no restart needed)\n` +
|
|
47
48
|
` status one-line connected/not-connected check (used by the SessionStart hook)\n` +
|
|
48
49
|
` skills install/repair the managed Cortex skills — bundled + org-published (also wired by setup)\n` +
|
|
@@ -69,6 +70,12 @@ if (cmd === 'setup') {
|
|
|
69
70
|
await runSetup(rest, VERSION)
|
|
70
71
|
const { closeFetch } = await import('../lib/diagnose.mjs')
|
|
71
72
|
await closeFetch()
|
|
73
|
+
} else if (cmd === 'uninstall' || cmd === 'remove') {
|
|
74
|
+
// Full reverse of setup: strip every Cortex touch-point (MCP entries, hooks, skills, launchd, cron).
|
|
75
|
+
// --dry-run prints the plan and changes nothing; --purge also removes ~/.cortex, the npx cache, and
|
|
76
|
+
// backups. No network — safe to run even when the token is dead or the server is unreachable.
|
|
77
|
+
const { runUninstall } = await import('../lib/uninstall.mjs')
|
|
78
|
+
runUninstall(rest)
|
|
72
79
|
} else if (cmd === 'repair' || cmd === 'update') {
|
|
73
80
|
// Re-run setup at THIS version using the already-wired token (no token arg needed). Fixes a
|
|
74
81
|
// machine set up with an older version: re-pins MCP + hooks, reinstalls skills to the flat path.
|
package/lib/capture.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from 'fs'
|
|
2
|
+
import { spawn } from 'child_process'
|
|
2
3
|
import { homedir } from 'os'
|
|
3
|
-
import { resolve } from 'path'
|
|
4
|
+
import { resolve, dirname, join } from 'path'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
4
6
|
import { createHash } from 'crypto'
|
|
5
7
|
import { fetchCortex, classify, resolveBase, readWiredToken } from './diagnose.mjs'
|
|
6
8
|
import { extractSession } from './edge_extract.mjs'
|
|
@@ -80,12 +82,51 @@ function transcriptTail(path) {
|
|
|
80
82
|
: tail
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
// Stop-hook entry point. The Stop hook fires after EVERY assistant turn, and Claude Code BLOCKS the
|
|
86
|
+
// input box until this returns — so the heavy work (a synchronous `claude -p` edge summary, up to
|
|
87
|
+
// tens of seconds, and worse when it hangs under concurrent sessions) must NEVER run in-band here.
|
|
88
|
+
// Instead we read the (small) stdin payload, hand it to a DETACHED background worker, and return in
|
|
89
|
+
// ~milliseconds. The worker does the extraction + ingest out of band; the user's next prompt is never
|
|
90
|
+
// held. Set CORTEX_CAPTURE_SYNC=1 to force the old in-band behavior (tests / debugging).
|
|
83
91
|
export async function runCapture() {
|
|
84
|
-
// Recursion guard: edge extraction
|
|
85
|
-
//
|
|
86
|
-
//
|
|
92
|
+
// Recursion guard: edge extraction shells out to `claude --print` with CORTEX_SUMMARIZING=1. That
|
|
93
|
+
// headless session fires its own Stop hook → this same capture command. Without this guard it would
|
|
94
|
+
// recurse (and re-ingest the summarizer's prompt as a phantom session). Bail immediately.
|
|
87
95
|
if (process.env.CORTEX_SUMMARIZING) { process.stderr.write('cortex: summarizer subprocess, skipping\n'); return }
|
|
88
96
|
|
|
97
|
+
const stdinRaw = readStdin()
|
|
98
|
+
|
|
99
|
+
// Detach the slow work unless we ARE the detached worker (or a caller forced sync). A spawn failure
|
|
100
|
+
// falls through to the in-band path so a session is never dropped just because fork() failed.
|
|
101
|
+
if (!process.env.CORTEX_CAPTURE_DETACHED && !process.env.CORTEX_CAPTURE_SYNC) {
|
|
102
|
+
if (spawnDetachedWorker(stdinRaw)) return
|
|
103
|
+
}
|
|
104
|
+
await captureWork(stdinRaw)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Re-invoke this same CLI as `capture` in a fully detached child (own session, stdio ignored) and
|
|
108
|
+
// feed it the hook payload on its stdin. Returns true if the child was launched (parent may return
|
|
109
|
+
// immediately), false if spawning failed (caller then does the work in-band). The child sees
|
|
110
|
+
// CORTEX_CAPTURE_DETACHED=1 so it runs captureWork() directly instead of forking again.
|
|
111
|
+
function spawnDetachedWorker(stdinRaw) {
|
|
112
|
+
try {
|
|
113
|
+
const bin = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'cortex-mcp.mjs')
|
|
114
|
+
const child = spawn(process.execPath, [bin, 'capture'], {
|
|
115
|
+
env: { ...process.env, CORTEX_CAPTURE_DETACHED: '1' },
|
|
116
|
+
detached: true,
|
|
117
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
118
|
+
})
|
|
119
|
+
child.on('error', () => {}) // never let an async spawn error crash the hook
|
|
120
|
+
child.stdin.on('error', () => {})
|
|
121
|
+
child.stdin.end(stdinRaw)
|
|
122
|
+
child.unref()
|
|
123
|
+
return true
|
|
124
|
+
} catch {
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function captureWork(stdinRaw) {
|
|
89
130
|
// Env first (explicit override / legacy inlined hooks), else the token wired into this machine's
|
|
90
131
|
// MCP config — so hook commands carry no secret (token-hygiene, 2026-07-02).
|
|
91
132
|
const token = process.env.CORTEX_TOKEN || readWiredToken()
|
|
@@ -93,7 +134,7 @@ export async function runCapture() {
|
|
|
93
134
|
const base = resolveBase(process.env.CORTEX_URL)
|
|
94
135
|
|
|
95
136
|
let hook = {}
|
|
96
|
-
try { hook = JSON.parse(
|
|
137
|
+
try { hook = JSON.parse(stdinRaw) } catch { /* no/invalid stdin */ }
|
|
97
138
|
const repo = projectFrom(hook.cwd)
|
|
98
139
|
// Redact credential-shaped strings BEFORE anything leaves the machine — this `transcript`
|
|
99
140
|
// feeds local edge/typed extraction AND the fallback POST to the org. A secret in a
|
package/lib/diagnose.mjs
CHANGED
|
@@ -68,10 +68,20 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
|
68
68
|
// 'app' → a real Cortex API error with a JSON message (retriable only if 5xx)
|
|
69
69
|
export function classify(status, contentType, bodyText, requestId) {
|
|
70
70
|
const isJson = (contentType ?? '').includes('application/json')
|
|
71
|
-
let
|
|
72
|
-
if (isJson) { try {
|
|
71
|
+
let body = null
|
|
72
|
+
if (isJson) { try { body = JSON.parse(bodyText) } catch { /* not json after all */ } }
|
|
73
|
+
const appError = body?.error ?? null
|
|
73
74
|
const rid = requestId ? ` [request id: ${requestId}]` : ''
|
|
74
75
|
|
|
76
|
+
// Multi-brain: 409 no_active_brain means the caller belongs to >1 brain and hasn't chosen one. It is
|
|
77
|
+
// NOT an auth failure — surface the action (set_active_brain) directly, not a "Cortex API 409:" wrapper.
|
|
78
|
+
if (status === 409 && body?.code === 'no_active_brain') {
|
|
79
|
+
return {
|
|
80
|
+
kind: 'app', retriable: false,
|
|
81
|
+
message: `${appError ?? 'You belong to more than one brain — call set_active_brain to choose where your writes land.'} (see my_brains for your options)${rid}`,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
75
85
|
if (status === 401 || (isJson && appError === 'invalid token')) {
|
|
76
86
|
return {
|
|
77
87
|
kind: 'auth', retriable: false,
|
package/lib/edge_extract.mjs
CHANGED
|
@@ -1,4 +1,58 @@
|
|
|
1
1
|
import { spawnSync } from 'child_process'
|
|
2
|
+
import { openSync, closeSync, writeSync, unlinkSync, readFileSync, statSync } from 'fs'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import { homedir } from 'os'
|
|
5
|
+
|
|
6
|
+
// Single-flight lock so at most ONE edge summarizer (`claude -p`) runs on this machine at a time.
|
|
7
|
+
// Without it, N concurrent Claude sessions all fire their Stop hook at once → N simultaneous headless
|
|
8
|
+
// `claude --print` calls that contend on the same subscription-OAuth refresh and can deadlock (the
|
|
9
|
+
// "prompts hang forever" incident, 2026-07-08). If the lock is already held by a live, recent holder,
|
|
10
|
+
// the caller skips extraction and ships the transcript tail instead (the server summarizes async) —
|
|
11
|
+
// no session is dropped, just summarized server-side that turn. A stale lock (older than the summary
|
|
12
|
+
// timeout + grace, or whose PID is dead) is reclaimed.
|
|
13
|
+
const LOCK_PATH = join(homedir(), '.cortex', 'summarize.lock')
|
|
14
|
+
|
|
15
|
+
function summaryTimeoutMs() {
|
|
16
|
+
const n = Number(process.env.CORTEX_SUMMARY_TIMEOUT_MS)
|
|
17
|
+
return Number.isFinite(n) && n > 0 ? n : 45_000
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Try to acquire the lock. Returns true if acquired (caller must call releaseSummaryLock in a finally),
|
|
21
|
+
// false if another live holder has it. O_CREAT|O_EXCL is the atomic "create only if absent" primitive.
|
|
22
|
+
function acquireSummaryLock(_retried = false) {
|
|
23
|
+
try {
|
|
24
|
+
const fd = openSync(LOCK_PATH, 'wx') // wx = O_CREAT|O_EXCL|O_WRONLY — fails if it exists
|
|
25
|
+
try { writeSync(fd, String(process.pid)) } catch { /* PID write is best-effort */ }
|
|
26
|
+
closeSync(fd)
|
|
27
|
+
return true
|
|
28
|
+
} catch (e) {
|
|
29
|
+
if (e && e.code === 'EEXIST') {
|
|
30
|
+
// Someone holds it — reclaim only if it's stale (older than timeout + 15s grace, or PID dead).
|
|
31
|
+
if (_retried) return false // one reclaim attempt only; a live race means "ship the tail"
|
|
32
|
+
try {
|
|
33
|
+
const age = Date.now() - statSync(LOCK_PATH).mtimeMs
|
|
34
|
+
const holderDead = !pidAlive(readFileSync(LOCK_PATH, 'utf8').trim())
|
|
35
|
+
if (age > summaryTimeoutMs() + 15_000 || holderDead) {
|
|
36
|
+
try { unlinkSync(LOCK_PATH) } catch { /* raced with another reclaimer */ }
|
|
37
|
+
return acquireSummaryLock(true)
|
|
38
|
+
}
|
|
39
|
+
} catch { /* unreadable lock — treat as held; ship the tail this turn */ }
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
// Any other error (e.g. ~/.cortex missing): don't block extraction, just run without the lock.
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function pidAlive(pid) {
|
|
48
|
+
const n = Number(pid)
|
|
49
|
+
if (!Number.isInteger(n) || n <= 0) return false
|
|
50
|
+
try { process.kill(n, 0); return true } catch (e) { return e && e.code === 'EPERM' }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function releaseSummaryLock() {
|
|
54
|
+
try { unlinkSync(LOCK_PATH) } catch { /* already gone */ }
|
|
55
|
+
}
|
|
2
56
|
|
|
3
57
|
// Edge session extractor (slice 2c) — ONE `claude --print` call on the user's SUBSCRIPTION that
|
|
4
58
|
// produces summary + people + non-person entities LOCALLY, so the cloud receives only the derived
|
|
@@ -62,16 +116,21 @@ export function extractSession(transcript) {
|
|
|
62
116
|
PEOPLE_FRAGMENT + ',\n' +
|
|
63
117
|
ENTITY_FRAGMENT +
|
|
64
118
|
'\n\n--- SESSION ---\n' + text.slice(0, 12000) + '\n--- END ---'
|
|
119
|
+
// Single-flight: if another session already has a summarizer running, skip (caller ships the tail;
|
|
120
|
+
// server summarizes). Prevents the concurrent-`claude -p` stampede that deadlocks the OAuth refresh.
|
|
121
|
+
if (!acquireSummaryLock()) return null
|
|
65
122
|
try {
|
|
66
123
|
const r = spawnSync(
|
|
67
124
|
'claude',
|
|
68
125
|
['--print', '--model', process.env.CORTEX_SUMMARY_MODEL ?? 'claude-haiku-4-5', prompt],
|
|
69
|
-
{ env: edgeSafeEnv(process.env, { CORTEX_SUMMARIZING: '1' }), encoding: 'utf8', timeout:
|
|
126
|
+
{ env: edgeSafeEnv(process.env, { CORTEX_SUMMARIZING: '1' }), encoding: 'utf8', timeout: summaryTimeoutMs(), maxBuffer: 4 * 1024 * 1024 },
|
|
70
127
|
)
|
|
71
128
|
if (r.status !== 0 || !r.stdout) return null
|
|
72
129
|
return parseEdgeJson(r.stdout.trim())
|
|
73
130
|
} catch {
|
|
74
131
|
return null
|
|
132
|
+
} finally {
|
|
133
|
+
releaseSummaryLock()
|
|
75
134
|
}
|
|
76
135
|
}
|
|
77
136
|
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, copyFileSync, rmSync } from 'fs'
|
|
2
|
+
import { homedir } from 'os'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import { execFileSync } from 'child_process'
|
|
5
|
+
|
|
6
|
+
// Full uninstall — the reverse of setup.mjs. Removes EVERY touch-point Cortex writes onto a machine:
|
|
7
|
+
// 1. ~/.claude.json → mcpServers.cortex
|
|
8
|
+
// 2. ~/.claude/settings.json → Stop/SessionStart/PreCompact cortex hooks
|
|
9
|
+
// 3. ~/.codex/config.toml → [mcp_servers.cortex] + [mcp_servers.cortex.env]
|
|
10
|
+
// 4. ~/.codex/hooks.json → cortex capture Stop hook
|
|
11
|
+
// 5. managed skills → ~/.claude/skills/cortex* + ~/.codex/skills/cortex*
|
|
12
|
+
// 6. launchd agent → ~/Library/LaunchAgents/com.cortex.*.plist (unload + remove)
|
|
13
|
+
// 7. crontab → lines tagged "# cortex ..."
|
|
14
|
+
// 8. off-switch env → launchctl unsetenv CORTEX_SUMMARIZE_DISABLED (macOS)
|
|
15
|
+
// --purge also removes: ~/.cortex state dir, the npx cache, and every .cortex-bak backup.
|
|
16
|
+
//
|
|
17
|
+
// Safe by construction: --dry-run prints the plan and changes nothing; every JSON/TOML file is backed
|
|
18
|
+
// up to <file>.cortex-uninstall-bak before edit; JSON surgery filters only cortex-tagged entries and
|
|
19
|
+
// leaves every other MCP server / hook untouched. Idempotent — re-running is a no-op.
|
|
20
|
+
|
|
21
|
+
const HOME = homedir()
|
|
22
|
+
const CLAUDE_JSON = join(HOME, '.claude.json')
|
|
23
|
+
const SETTINGS = join(HOME, '.claude', 'settings.json')
|
|
24
|
+
const CODEX_TOML = join(HOME, '.codex', 'config.toml')
|
|
25
|
+
const CODEX_HOOKS = join(HOME, '.codex', 'hooks.json')
|
|
26
|
+
const CORTEX_DIR = join(HOME, '.cortex')
|
|
27
|
+
const LAUNCH_AGENTS = join(HOME, 'Library', 'LaunchAgents')
|
|
28
|
+
const CORTEX_RE = /cortex-mcp|capture-session-cloud|@theronap\/cortex/
|
|
29
|
+
|
|
30
|
+
export function runUninstall(argv = []) {
|
|
31
|
+
const dry = argv.includes('--dry-run') || argv.includes('-n')
|
|
32
|
+
const purge = argv.includes('--purge')
|
|
33
|
+
const plan = [] // human-readable action log
|
|
34
|
+
const act = (msg) => plan.push(msg)
|
|
35
|
+
const write = (path, data) => { if (!dry) { backup(path); writeFileSync(path, data) } }
|
|
36
|
+
|
|
37
|
+
process.stdout.write(dry ? '\nCortex uninstall — DRY RUN (nothing will change):\n\n' : '\nCortex uninstall — removing all wiring…\n\n')
|
|
38
|
+
|
|
39
|
+
// 1. MCP server out of ~/.claude.json
|
|
40
|
+
editJson(CLAUDE_JSON, (cfg) => {
|
|
41
|
+
if (cfg.mcpServers && cfg.mcpServers.cortex) { delete cfg.mcpServers.cortex; act(` - mcpServers.cortex ← ${CLAUDE_JSON}`); return true }
|
|
42
|
+
return false
|
|
43
|
+
}, write)
|
|
44
|
+
|
|
45
|
+
// 2. Hooks out of ~/.claude/settings.json (Stop, SessionStart, PreCompact)
|
|
46
|
+
editJson(SETTINGS, (s) => {
|
|
47
|
+
let changed = false
|
|
48
|
+
for (const evt of ['Stop', 'SessionStart', 'PreCompact']) {
|
|
49
|
+
if (!Array.isArray(s.hooks?.[evt])) continue
|
|
50
|
+
for (const grp of s.hooks[evt]) {
|
|
51
|
+
if (!Array.isArray(grp.hooks)) continue
|
|
52
|
+
const before = grp.hooks.length
|
|
53
|
+
grp.hooks = grp.hooks.filter((h) => !CORTEX_RE.test(h?.command ?? ''))
|
|
54
|
+
if (grp.hooks.length !== before) changed = true
|
|
55
|
+
}
|
|
56
|
+
// drop groups we emptied
|
|
57
|
+
s.hooks[evt] = s.hooks[evt].filter((g) => !Array.isArray(g.hooks) || g.hooks.length > 0)
|
|
58
|
+
if (s.hooks[evt].length === 0) delete s.hooks[evt]
|
|
59
|
+
}
|
|
60
|
+
if (changed) act(` - Stop/SessionStart/PreCompact cortex hooks ← ${SETTINGS}`)
|
|
61
|
+
return changed
|
|
62
|
+
}, write)
|
|
63
|
+
|
|
64
|
+
// 3. Codex MCP tables (strip [mcp_servers.cortex] + [mcp_servers.cortex.env])
|
|
65
|
+
if (existsSync(CODEX_TOML)) {
|
|
66
|
+
const text = readFileSync(CODEX_TOML, 'utf8')
|
|
67
|
+
const stripped = stripCodexCortexTables(text)
|
|
68
|
+
if (stripped !== text) { write(CODEX_TOML, stripped); act(` - [mcp_servers.cortex] tables ← ${CODEX_TOML}`) }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 4. Codex capture hook
|
|
72
|
+
editJson(CODEX_HOOKS, (h) => {
|
|
73
|
+
let changed = false
|
|
74
|
+
if (Array.isArray(h.hooks?.Stop)) {
|
|
75
|
+
for (const grp of h.hooks.Stop) {
|
|
76
|
+
if (!Array.isArray(grp.hooks)) continue
|
|
77
|
+
const before = grp.hooks.length
|
|
78
|
+
grp.hooks = grp.hooks.filter((c) => !CORTEX_RE.test(c?.command ?? ''))
|
|
79
|
+
if (grp.hooks.length !== before) changed = true
|
|
80
|
+
}
|
|
81
|
+
h.hooks.Stop = h.hooks.Stop.filter((g) => !Array.isArray(g.hooks) || g.hooks.length > 0)
|
|
82
|
+
}
|
|
83
|
+
if (changed) act(` - cortex capture hook ← ${CODEX_HOOKS}`)
|
|
84
|
+
return changed
|
|
85
|
+
}, write)
|
|
86
|
+
|
|
87
|
+
// 5. Managed skills
|
|
88
|
+
for (const skillsRoot of [join(HOME, '.claude', 'skills'), join(HOME, '.codex', 'skills')]) {
|
|
89
|
+
for (const name of ['cortex', 'cortex-author-docs', 'cortex-context', 'cortex-log']) {
|
|
90
|
+
const p = join(skillsRoot, name)
|
|
91
|
+
if (existsSync(p)) { act(` - skill ${p}`); if (!dry) rmSync(p, { recursive: true, force: true }) }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 6. launchd agents (com.cortex.*)
|
|
96
|
+
if (existsSync(LAUNCH_AGENTS)) {
|
|
97
|
+
for (const f of safeReaddir(LAUNCH_AGENTS)) {
|
|
98
|
+
if (!/^com\.cortex\..*\.plist$/.test(f)) continue
|
|
99
|
+
const p = join(LAUNCH_AGENTS, f)
|
|
100
|
+
act(` - launchd agent ${p} (unload + remove)`)
|
|
101
|
+
if (!dry) {
|
|
102
|
+
tryExec('launchctl', ['bootout', `gui/${process.getuid?.() ?? ''}/${f.replace(/\.plist$/, '')}`])
|
|
103
|
+
tryExec('launchctl', ['unload', p])
|
|
104
|
+
rmSync(p, { force: true })
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 7. crontab — drop cortex-tagged lines
|
|
110
|
+
const cron = tryExecOut('crontab', ['-l'])
|
|
111
|
+
if (cron != null) {
|
|
112
|
+
const kept = cron.split('\n').filter((l) => !CORTEX_RE.test(l) && !/\.cortex\//.test(l))
|
|
113
|
+
if (kept.join('\n') !== cron) {
|
|
114
|
+
act(' - crontab cortex entries')
|
|
115
|
+
if (!dry) tryExecIn('crontab', ['-'], kept.join('\n').replace(/\n+$/, '') + '\n')
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 8. off-switch env var (macOS launchd session env)
|
|
120
|
+
act(' - launchctl unsetenv CORTEX_SUMMARIZE_DISABLED')
|
|
121
|
+
if (!dry) tryExec('launchctl', ['unsetenv', 'CORTEX_SUMMARIZE_DISABLED'])
|
|
122
|
+
|
|
123
|
+
// --purge: state dir + npx cache + backups
|
|
124
|
+
if (purge) {
|
|
125
|
+
if (existsSync(CORTEX_DIR)) { act(` - PURGE state dir ${CORTEX_DIR}`); if (!dry) rmSync(CORTEX_DIR, { recursive: true, force: true }) }
|
|
126
|
+
for (const cache of npxCacheDirs()) { act(` - PURGE npx cache ${cache}`); if (!dry) rmSync(cache, { recursive: true, force: true }) }
|
|
127
|
+
for (const bak of [CLAUDE_JSON, SETTINGS, CODEX_TOML, CODEX_HOOKS]) {
|
|
128
|
+
for (const suffix of ['.cortex-bak', '.cortex-uninstall-bak']) {
|
|
129
|
+
const b = bak + suffix
|
|
130
|
+
if (existsSync(b)) { act(` - PURGE backup ${b}`); if (!dry) rmSync(b, { force: true }) }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (plan.length === 0) { process.stdout.write(' Nothing to remove — this machine has no Cortex wiring.\n\n'); return }
|
|
136
|
+
process.stdout.write(plan.join('\n') + '\n\n')
|
|
137
|
+
if (dry) {
|
|
138
|
+
process.stdout.write('DRY RUN — nothing changed. Re-run without --dry-run to apply.\n')
|
|
139
|
+
process.stdout.write(' Full wipe (state dir + npx cache + backups too): add --purge\n\n')
|
|
140
|
+
} else {
|
|
141
|
+
process.stdout.write('Done. Fully quit and reopen Claude Code (and Codex) to drop the server.\n')
|
|
142
|
+
if (!purge) process.stdout.write(' State dir ~/.cortex and .cortex-bak backups were KEPT. Remove them too with --purge.\n')
|
|
143
|
+
process.stdout.write('\n')
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
148
|
+
function backup(path) {
|
|
149
|
+
if (!existsSync(path)) return
|
|
150
|
+
copyFileSync(path, `${path}.cortex-uninstall-bak`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Read JSON, run mutator (returns true if it changed anything), write back via `write` if so.
|
|
154
|
+
// Malformed JSON is left untouched (never risk corrupting a file we can't parse).
|
|
155
|
+
function editJson(path, mutate, write) {
|
|
156
|
+
if (!existsSync(path)) return
|
|
157
|
+
let obj
|
|
158
|
+
try { obj = JSON.parse(readFileSync(path, 'utf8') || '{}') } catch { process.stderr.write(` ! ${path} is not valid JSON — skipped (left as-is)\n`); return }
|
|
159
|
+
if (mutate(obj)) write(path, JSON.stringify(obj, null, 2) + '\n')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Strip [mcp_servers.cortex] and [mcp_servers.cortex.env] tables from a Codex config.toml.
|
|
163
|
+
// Same table-skip logic as setup.mergeCodexToml, in reverse.
|
|
164
|
+
export function stripCodexCortexTables(text) {
|
|
165
|
+
const targets = new Set(['[mcp_servers.cortex]', '[mcp_servers.cortex.env]'])
|
|
166
|
+
const kept = []
|
|
167
|
+
let skipping = false
|
|
168
|
+
for (const line of (text || '').split('\n')) {
|
|
169
|
+
const t = line.trim()
|
|
170
|
+
if (t.startsWith('[') && t.endsWith(']')) skipping = targets.has(t)
|
|
171
|
+
if (!skipping) kept.push(line)
|
|
172
|
+
}
|
|
173
|
+
return kept.join('\n').replace(/\n{3,}/g, '\n\n')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function safeReaddir(dir) {
|
|
177
|
+
try { return execFileSync('ls', ['-1', dir], { encoding: 'utf8' }).split('\n').filter(Boolean) } catch { return [] }
|
|
178
|
+
}
|
|
179
|
+
function npxCacheDirs() {
|
|
180
|
+
const base = join(HOME, '.npm', '_npx')
|
|
181
|
+
const out = []
|
|
182
|
+
for (const d of safeReaddir(base)) {
|
|
183
|
+
const dir = join(base, d)
|
|
184
|
+
try { if (execFileSync('grep', ['-rl', '@theronap/cortex-mcp', join(dir, 'package.json')], { encoding: 'utf8' }).trim()) out.push(dir) } catch { /* not a cortex cache */ }
|
|
185
|
+
}
|
|
186
|
+
return out
|
|
187
|
+
}
|
|
188
|
+
function tryExec(cmd, args) { try { execFileSync(cmd, args, { stdio: 'ignore' }) } catch { /* best-effort */ } }
|
|
189
|
+
function tryExecOut(cmd, args) { try { return execFileSync(cmd, args, { encoding: 'utf8' }) } catch { return null } }
|
|
190
|
+
function tryExecIn(cmd, args, input) { try { execFileSync(cmd, args, { input }) } catch { /* best-effort */ } }
|