@theronap/cortex-mcp 0.9.94 → 0.9.96
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 +17 -0
- package/lib/capture.mjs +117 -11
- package/lib/context_log.mjs +5 -12
- package/lib/diagnose.mjs +72 -9
- package/lib/doctor.mjs +11 -13
- package/lib/editors/antigravity.mjs +13 -6
- package/lib/editors/claude.mjs +20 -12
- package/lib/graphify_sync.mjs +2 -2
- package/lib/grep_cli.mjs +2 -2
- package/lib/hydrate.mjs +2 -2
- package/lib/ingest_folder.mjs +2 -2
- package/lib/managed.mjs +49 -0
- package/lib/migrate_key.mjs +139 -0
- package/lib/publish_targets.mjs +51 -0
- package/lib/redact.mjs +1 -1
- package/lib/rename_notice.mjs +31 -0
- package/lib/resolve.mjs +2 -2
- package/lib/server.mjs +31 -6
- package/lib/session_key.mjs +24 -0
- package/lib/setup.mjs +24 -11
- package/lib/skills.mjs +4 -4
- package/lib/uninstall.mjs +79 -44
- package/lib/with_token.mjs +66 -0
- package/package.json +1 -1
package/lib/uninstall.mjs
CHANGED
|
@@ -2,10 +2,12 @@ import { readFileSync, writeFileSync, existsSync, copyFileSync, rmSync } from 'f
|
|
|
2
2
|
import { homedir } from 'os'
|
|
3
3
|
import { join } from 'path'
|
|
4
4
|
import { execFileSync } from 'child_process'
|
|
5
|
+
import { isManagedCommand } from './managed.mjs'
|
|
5
6
|
|
|
6
7
|
// Full uninstall — the reverse of setup.mjs. Removes EVERY touch-point Agnoclast writes onto a machine:
|
|
7
|
-
// 1. ~/.claude.json → mcpServers.cortex
|
|
8
|
-
//
|
|
8
|
+
// 1. ~/.claude.json → mcpServers.cortex / .agnoclast
|
|
9
|
+
// 1b. ~/.cursor/mcp.json → same shape (editors/cursor.mjs writes it; was never removed)
|
|
10
|
+
// 2. ~/.claude/settings.json → Stop/SessionStart/UserPromptSubmit/PreCompact hooks + allow rules
|
|
9
11
|
// 3. ~/.codex/config.toml → [mcp_servers.cortex] + [mcp_servers.cortex.env]
|
|
10
12
|
// 4. ~/.codex/hooks.json → cortex capture Stop hook
|
|
11
13
|
// 5. managed skills → ~/.claude/skills/cortex* + ~/.codex/skills/cortex*
|
|
@@ -15,17 +17,21 @@ import { execFileSync } from 'child_process'
|
|
|
15
17
|
// --purge also removes: ~/.cortex state dir, the npx cache, and every .cortex-bak backup.
|
|
16
18
|
//
|
|
17
19
|
// 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
|
|
19
|
-
// leaves every other MCP server / hook
|
|
20
|
+
// up to <file>.cortex-uninstall-bak before edit; JSON surgery filters only entries we wrote (see
|
|
21
|
+
// managed.mjs — marker first, legacy name match second) and leaves every other MCP server / hook
|
|
22
|
+
// untouched. Idempotent — re-running is a no-op.
|
|
20
23
|
|
|
21
24
|
const HOME = homedir()
|
|
22
25
|
const CLAUDE_JSON = join(HOME, '.claude.json')
|
|
23
26
|
const SETTINGS = join(HOME, '.claude', 'settings.json')
|
|
24
27
|
const CODEX_TOML = join(HOME, '.codex', 'config.toml')
|
|
25
28
|
const CODEX_HOOKS = join(HOME, '.codex', 'hooks.json')
|
|
29
|
+
const CURSOR_JSON = join(HOME, '.cursor', 'mcp.json')
|
|
26
30
|
const CORTEX_DIR = join(HOME, '.cortex')
|
|
27
31
|
const LAUNCH_AGENTS = join(HOME, 'Library', 'LaunchAgents')
|
|
28
|
-
|
|
32
|
+
// Hook events the installer writes (editors/claude.mjs). UserPromptSubmit was MISSING here until
|
|
33
|
+
// 2026-08-19, so every uninstall left the `hydrate` hook behind while reporting success.
|
|
34
|
+
export const MANAGED_HOOK_EVENTS = ['Stop', 'SessionStart', 'UserPromptSubmit', 'PreCompact']
|
|
29
35
|
|
|
30
36
|
export function runUninstall(argv = []) {
|
|
31
37
|
const dry = argv.includes('--dry-run') || argv.includes('-n')
|
|
@@ -36,41 +42,24 @@ export function runUninstall(argv = []) {
|
|
|
36
42
|
|
|
37
43
|
process.stdout.write(dry ? '\nAgnoclast uninstall — DRY RUN (nothing will change):\n\n' : '\nAgnoclast uninstall — removing all wiring…\n\n')
|
|
38
44
|
|
|
39
|
-
// 1. MCP server out of ~/.claude.json
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
// 1. MCP server out of ~/.claude.json — and 1b, out of ~/.cursor/mcp.json, which uses the same
|
|
46
|
+
// shape (editors/cursor.mjs:15) and which uninstall never touched until 2026-08-19, so every
|
|
47
|
+
// `install --editor all` left Cursor permanently wired.
|
|
48
|
+
for (const path of [CLAUDE_JSON, CURSOR_JSON]) {
|
|
49
|
+
editJson(path, (cfg) => {
|
|
50
|
+
const removed = stripManagedMcpServers(cfg)
|
|
51
|
+
if (removed.length) act(` - mcpServers.${removed.join(' / ')} ← ${path}`)
|
|
52
|
+
return removed.length > 0
|
|
53
|
+
}, write)
|
|
54
|
+
}
|
|
44
55
|
|
|
45
|
-
// 2. Hooks out of ~/.claude/settings.json
|
|
56
|
+
// 2. Hooks + allow rules out of ~/.claude/settings.json
|
|
46
57
|
editJson(SETTINGS, (s) => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
// cortex permission allowlist (setup wires CORTEX_ALLOWED_TOOLS so authoring never stalls on a
|
|
62
|
-
// prompt) — strip every mcp__cortex__* allow rule; the user's deny list is never touched.
|
|
63
|
-
if (Array.isArray(s.permissions?.allow)) {
|
|
64
|
-
const before = s.permissions.allow.length
|
|
65
|
-
s.permissions.allow = s.permissions.allow.filter((r) => !/^mcp__cortex__/.test(String(r)))
|
|
66
|
-
if (s.permissions.allow.length !== before) {
|
|
67
|
-
changed = true
|
|
68
|
-
act(` - mcp__cortex__* permission allow rules ← ${SETTINGS}`)
|
|
69
|
-
if (s.permissions.allow.length === 0) delete s.permissions.allow
|
|
70
|
-
if (Object.keys(s.permissions).length === 0) delete s.permissions
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return changed
|
|
58
|
+
const hooks = stripManagedHooks(s)
|
|
59
|
+
const allows = stripManagedAllows(s)
|
|
60
|
+
if (hooks) act(` - ${MANAGED_HOOK_EVENTS.join('/')} hooks ← ${SETTINGS}`)
|
|
61
|
+
if (allows) act(` - mcp__cortex__* / mcp__agnoclast__* permission allow rules ← ${SETTINGS}`)
|
|
62
|
+
return hooks || allows
|
|
74
63
|
}, write)
|
|
75
64
|
|
|
76
65
|
// 3. Codex MCP tables (strip [mcp_servers.cortex] + [mcp_servers.cortex.env])
|
|
@@ -87,7 +76,7 @@ export function runUninstall(argv = []) {
|
|
|
87
76
|
for (const grp of h.hooks.Stop) {
|
|
88
77
|
if (!Array.isArray(grp.hooks)) continue
|
|
89
78
|
const before = grp.hooks.length
|
|
90
|
-
grp.hooks = grp.hooks.filter((c) => !
|
|
79
|
+
grp.hooks = grp.hooks.filter((c) => !isManagedCommand(c?.command))
|
|
91
80
|
if (grp.hooks.length !== before) changed = true
|
|
92
81
|
}
|
|
93
82
|
h.hooks.Stop = h.hooks.Stop.filter((g) => !Array.isArray(g.hooks) || g.hooks.length > 0)
|
|
@@ -98,7 +87,8 @@ export function runUninstall(argv = []) {
|
|
|
98
87
|
|
|
99
88
|
// 5. Managed skills
|
|
100
89
|
for (const skillsRoot of [join(HOME, '.claude', 'skills'), join(HOME, '.codex', 'skills')]) {
|
|
101
|
-
for (const name of ['cortex', 'cortex-author-docs', 'cortex-context', 'cortex-log'
|
|
90
|
+
for (const name of ['cortex', 'cortex-author-docs', 'cortex-context', 'cortex-log', 'cortex-walkthrough',
|
|
91
|
+
'agnoclast', 'agnoclast-author-docs', 'agnoclast-context', 'agnoclast-log', 'agnoclast-walkthrough']) {
|
|
102
92
|
const p = join(skillsRoot, name)
|
|
103
93
|
if (existsSync(p)) { act(` - skill ${p}`); if (!dry) rmSync(p, { recursive: true, force: true }) }
|
|
104
94
|
}
|
|
@@ -107,7 +97,7 @@ export function runUninstall(argv = []) {
|
|
|
107
97
|
// 6. launchd agents (com.cortex.*)
|
|
108
98
|
if (existsSync(LAUNCH_AGENTS)) {
|
|
109
99
|
for (const f of safeReaddir(LAUNCH_AGENTS)) {
|
|
110
|
-
if (!/^com\.cortex\..*\.plist$/.test(f)) continue
|
|
100
|
+
if (!/^com\.(cortex|agnoclast)\..*\.plist$/.test(f)) continue
|
|
111
101
|
const p = join(LAUNCH_AGENTS, f)
|
|
112
102
|
act(` - launchd agent ${p} (unload + remove)`)
|
|
113
103
|
if (!dry) {
|
|
@@ -121,7 +111,7 @@ export function runUninstall(argv = []) {
|
|
|
121
111
|
// 7. crontab — drop cortex-tagged lines
|
|
122
112
|
const cron = tryExecOut('crontab', ['-l'])
|
|
123
113
|
if (cron != null) {
|
|
124
|
-
const kept = cron.split('\n').filter((l) => !
|
|
114
|
+
const kept = cron.split('\n').filter((l) => !isManagedCommand(l) && !/\.cortex\//.test(l))
|
|
125
115
|
if (kept.join('\n') !== cron) {
|
|
126
116
|
act(' - crontab cortex entries')
|
|
127
117
|
if (!dry) tryExecIn('crontab', ['-'], kept.join('\n').replace(/\n+$/, '') + '\n')
|
|
@@ -171,10 +161,55 @@ function editJson(path, mutate, write) {
|
|
|
171
161
|
if (mutate(obj)) write(path, JSON.stringify(obj, null, 2) + '\n')
|
|
172
162
|
}
|
|
173
163
|
|
|
164
|
+
// PURE. Removes our MCP server entry under EITHER spelling. uninstall.mjs:45 used to delete only
|
|
165
|
+
// `cortex`, so after the Phase C key flip (ADR-0033 §5) the server entry — the single most important
|
|
166
|
+
// thing uninstall removes — would survive a "successful" uninstall. Returns the keys it removed so
|
|
167
|
+
// the caller can report which spelling this machine actually carried.
|
|
168
|
+
export function stripManagedMcpServers(cfg) {
|
|
169
|
+
const removed = []
|
|
170
|
+
for (const key of ['cortex', 'agnoclast']) {
|
|
171
|
+
if (cfg?.mcpServers && cfg.mcpServers[key]) { delete cfg.mcpServers[key]; removed.push(key) }
|
|
172
|
+
}
|
|
173
|
+
return removed
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// PURE (mutates the passed object, returns whether anything changed) so uninstall's riskiest logic
|
|
177
|
+
// is testable without touching a real filesystem. Empty hook groups and empty events are pruned so
|
|
178
|
+
// a fully-uninstalled settings.json is indistinguishable from one we never touched.
|
|
179
|
+
export function stripManagedHooks(s) {
|
|
180
|
+
let changed = false
|
|
181
|
+
for (const evt of MANAGED_HOOK_EVENTS) {
|
|
182
|
+
if (!Array.isArray(s?.hooks?.[evt])) continue
|
|
183
|
+
for (const grp of s.hooks[evt]) {
|
|
184
|
+
if (!Array.isArray(grp.hooks)) continue
|
|
185
|
+
const before = grp.hooks.length
|
|
186
|
+
grp.hooks = grp.hooks.filter((h) => !isManagedCommand(h?.command))
|
|
187
|
+
if (grp.hooks.length !== before) changed = true
|
|
188
|
+
}
|
|
189
|
+
s.hooks[evt] = s.hooks[evt].filter((g) => !Array.isArray(g.hooks) || g.hooks.length > 0)
|
|
190
|
+
if (s.hooks[evt].length === 0) delete s.hooks[evt]
|
|
191
|
+
}
|
|
192
|
+
return changed
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Strip every allow rule for OUR tool namespaces. Both spellings: a machine mid-migration can carry
|
|
196
|
+
// mcp__cortex__* and mcp__agnoclast__* at once, because C1 adds the new rules without removing the
|
|
197
|
+
// old ones (ADR-0033 §5). The user's `deny` list is never touched — a user deny always wins.
|
|
198
|
+
export function stripManagedAllows(s) {
|
|
199
|
+
if (!Array.isArray(s?.permissions?.allow)) return false
|
|
200
|
+
const before = s.permissions.allow.length
|
|
201
|
+
s.permissions.allow = s.permissions.allow.filter((r) => !/^mcp__(cortex|agnoclast)__/.test(String(r)))
|
|
202
|
+
if (s.permissions.allow.length === before) return false
|
|
203
|
+
if (s.permissions.allow.length === 0) delete s.permissions.allow
|
|
204
|
+
if (Object.keys(s.permissions).length === 0) delete s.permissions
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
|
|
174
208
|
// Strip [mcp_servers.cortex] and [mcp_servers.cortex.env] tables from a Codex config.toml.
|
|
175
209
|
// Same table-skip logic as setup.mergeCodexToml, in reverse.
|
|
176
210
|
export function stripCodexCortexTables(text) {
|
|
177
|
-
const targets = new Set(['[mcp_servers.cortex]', '[mcp_servers.cortex.env]'
|
|
211
|
+
const targets = new Set(['[mcp_servers.cortex]', '[mcp_servers.cortex.env]',
|
|
212
|
+
'[mcp_servers.agnoclast]', '[mcp_servers.agnoclast.env]'])
|
|
178
213
|
const kept = []
|
|
179
214
|
let skipping = false
|
|
180
215
|
for (const line of (text || '').split('\n')) {
|
|
@@ -193,7 +228,7 @@ function npxCacheDirs() {
|
|
|
193
228
|
const out = []
|
|
194
229
|
for (const d of safeReaddir(base)) {
|
|
195
230
|
const dir = join(base, d)
|
|
196
|
-
try { if (execFileSync('grep', ['-
|
|
231
|
+
try { if (execFileSync('grep', ['-rlE', '@theronap/(cortex|agnoclast)-mcp', join(dir, 'package.json')], { encoding: 'utf8' }).trim()) out.push(dir) } catch { /* not one of ours */ }
|
|
197
232
|
}
|
|
198
233
|
return out
|
|
199
234
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process'
|
|
2
|
+
import { resolveTokenSource, TOKEN_ENV_VARS } from './diagnose.mjs'
|
|
3
|
+
|
|
4
|
+
// `with-token -- <command…>` — run a command with the resolved Agnoclast token in its environment.
|
|
5
|
+
//
|
|
6
|
+
// Why this exists (ADR-0033 D9). Generated helper scripts used to inline their own config parse:
|
|
7
|
+
//
|
|
8
|
+
// TOKEN="$(node -e "…JSON.parse(…'/.claude.json')?.mcpServers?.cortex?.env?.CORTEX_TOKEN…")"
|
|
9
|
+
// CORTEX_TOKEN="$TOKEN" bun run …
|
|
10
|
+
//
|
|
11
|
+
// Those scripts are written to disk once and FROZEN. Upgrading the package never rewrites a file
|
|
12
|
+
// that already exists, so any change to config shape strands them — and the Phase C key flip would
|
|
13
|
+
// have done exactly that, silently, because the script's own `catch { exit 0 }` swallows it.
|
|
14
|
+
// Delegating means the resolution rule lives in one versioned place and a script written today keeps
|
|
15
|
+
// working through arbitrary future config changes.
|
|
16
|
+
//
|
|
17
|
+
// It also removes the secret from the shell. Previously the token landed in a shell variable and was
|
|
18
|
+
// re-exported by the caller; here it goes straight from the resolver into the child's environment,
|
|
19
|
+
// so it is never a shell value, never in a log line, and never in an argv the process table shows.
|
|
20
|
+
//
|
|
21
|
+
// Both variable names are injected so the child works whichever one it reads — the whole point of a
|
|
22
|
+
// transition period is that callers need not be updated in lockstep.
|
|
23
|
+
|
|
24
|
+
/** Split `with-token -- cmd args` (or `with-token cmd args`) into the command to run. */
|
|
25
|
+
export function parseWithTokenArgs(rest = []) {
|
|
26
|
+
const args = rest.filter((a) => a !== undefined && a !== null)
|
|
27
|
+
const sep = args.indexOf('--')
|
|
28
|
+
const cmd = sep === -1 ? args : args.slice(sep + 1)
|
|
29
|
+
if (!cmd.length) {
|
|
30
|
+
return { error: 'Usage: cortex-mcp with-token -- <command> [args…]\n Runs <command> with the wired Agnoclast token in its environment.\n' }
|
|
31
|
+
}
|
|
32
|
+
return { command: cmd[0], args: cmd.slice(1) }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Build the child environment. Exported so a test can assert it without spawning anything. */
|
|
36
|
+
export function tokenEnv(token, baseEnv = process.env) {
|
|
37
|
+
const out = { ...baseEnv }
|
|
38
|
+
for (const name of TOKEN_ENV_VARS) out[name] = token
|
|
39
|
+
return out
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Exit codes, distinct so a caller's log can tell them apart:
|
|
43
|
+
// 3 — no token wired (the "skip quietly" case a cron wrapper wants)
|
|
44
|
+
// 2 — bad usage
|
|
45
|
+
// otherwise the child's own code
|
|
46
|
+
export const EXIT_NO_TOKEN = 3
|
|
47
|
+
export const EXIT_USAGE = 2
|
|
48
|
+
|
|
49
|
+
export function runWithToken(rest = [], deps = {}) {
|
|
50
|
+
const { resolve = resolveTokenSource, spawn = spawnSync, stderr = (m) => process.stderr.write(m) } = deps
|
|
51
|
+
const parsed = parseWithTokenArgs(rest)
|
|
52
|
+
if (parsed.error) { stderr(parsed.error); return EXIT_USAGE }
|
|
53
|
+
|
|
54
|
+
const { token } = resolve()
|
|
55
|
+
if (!token) {
|
|
56
|
+
stderr('cortex-mcp with-token: no Agnoclast token in the environment or wired config — skipping.\n')
|
|
57
|
+
return EXIT_NO_TOKEN
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const res = spawn(parsed.command, parsed.args, { stdio: 'inherit', env: tokenEnv(token) })
|
|
61
|
+
if (res?.error) {
|
|
62
|
+
stderr(`cortex-mcp with-token: could not run ${parsed.command}: ${res.error.message}\n`)
|
|
63
|
+
return 1
|
|
64
|
+
}
|
|
65
|
+
return res?.status ?? 1
|
|
66
|
+
}
|