@theronap/cortex-mcp 0.9.80 → 0.9.82
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/README.md +3 -3
- package/bin/cortex-mcp.mjs +22 -38
- package/lib/capture.mjs +1 -1
- package/lib/code_graph_cli.mjs +1 -1
- package/lib/context_log.mjs +5 -5
- package/lib/diagnose.mjs +11 -81
- package/lib/docs_scan.mjs +4 -4
- package/lib/doctor.mjs +11 -48
- package/lib/editors/antigravity.mjs +1 -1
- package/lib/editors/claude.mjs +14 -29
- package/lib/editors/codex.mjs +1 -1
- package/lib/editors/cursor.mjs +1 -1
- package/lib/editors/index.mjs +1 -1
- package/lib/graphify_sync.mjs +4 -49
- package/lib/grep_cli.mjs +1 -1
- package/lib/hydrate.mjs +1 -1
- package/lib/imessage_send.mjs +1 -1
- package/lib/ingest_folder.mjs +2 -2
- package/lib/install.mjs +6 -6
- package/lib/precompact.mjs +16 -0
- package/lib/redact.mjs +1 -1
- package/lib/resolve.mjs +12 -62
- package/lib/server.mjs +41 -171
- package/lib/setup.mjs +10 -11
- package/lib/skills.mjs +17 -89
- package/lib/uninstall.mjs +3 -3
- package/package.json +1 -1
- package/skills/author-docs/SKILL.md +5 -5
- package/skills/context/SKILL.md +4 -4
- package/skills/log/SKILL.md +6 -6
- package/skills/walkthrough/SKILL.md +1 -1
- package/lib/timeline_claim_receipt.mjs +0 -25
- package/lib/use_brain.mjs +0 -82
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# cortex-mcp
|
|
2
2
|
|
|
3
|
-
Connect your AI assistant to **
|
|
3
|
+
Connect your AI assistant to **Cortex** — your org's projects, recent activity, gaps, and directives, scoped to exactly what you're permitted to see.
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
7
|
-
1. Get your personal token from the
|
|
7
|
+
1. Get your personal token from the Cortex console → **Connect your AI**.
|
|
8
8
|
2. Add this to your Claude Code config (`~/.claude.json`, under `mcpServers`):
|
|
9
9
|
|
|
10
10
|
```json
|
|
@@ -21,7 +21,7 @@ Connect your AI assistant to **Agnoclast** — your org's projects, recent activ
|
|
|
21
21
|
|
|
22
22
|
3. Restart Claude Code. Your AI now sees your org context automatically, the managed startup skill
|
|
23
23
|
will prefer query-centered `session_context` on substantive session opens, and each session start
|
|
24
|
-
will log the exact baseline
|
|
24
|
+
will log the exact baseline Cortex context to `~/.cortex/context-snapshots/`.
|
|
25
25
|
|
|
26
26
|
No clone, no path, no build step — `npx` fetches and runs it.
|
|
27
27
|
|
package/bin/cortex-mcp.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* cortex-mcp — connect your AI assistant to
|
|
3
|
+
* cortex-mcp — connect your AI assistant to Cortex.
|
|
4
4
|
*
|
|
5
5
|
* Subcommands:
|
|
6
6
|
* (none) run the MCP server (stdio) — used by your Claude config
|
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
* doctor live health check — is the token actually working? (no restart needed)
|
|
9
9
|
* capture the Stop-hook capturer (invoked by Claude Code, not by hand)
|
|
10
10
|
* ingest-folder <path> ingest a local markdown folder as your authored records
|
|
11
|
-
* snapshot-context save the exact startup context served by
|
|
11
|
+
* snapshot-context save the exact startup context served by Cortex to a local log file
|
|
12
12
|
* --version | -v
|
|
13
13
|
* --help | -h
|
|
14
14
|
*
|
|
15
15
|
* Zero-install onboarding:
|
|
16
16
|
* npx -y @theronap/cortex-mcp setup <your-token>
|
|
17
17
|
*
|
|
18
|
-
* Get your token from the
|
|
18
|
+
* Get your token from the Cortex console → Connect your AI.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { readFileSync } from 'node:fs'
|
|
@@ -35,32 +35,31 @@ if (cmd === '--version' || cmd === '-v') {
|
|
|
35
35
|
|
|
36
36
|
if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
37
37
|
process.stdout.write(
|
|
38
|
-
`cortex-mcp ${VERSION} — connect your AI assistant to
|
|
38
|
+
`cortex-mcp ${VERSION} — connect your AI assistant to Cortex\n\n` +
|
|
39
39
|
`Onboard (one command, no token to copy):\n` +
|
|
40
40
|
` npx -y @theronap/cortex-mcp login\n\n` +
|
|
41
41
|
`This opens your browser, you click Approve, and it wires everything up. Restart\n` +
|
|
42
|
-
`Claude Code after, and your AI sees your
|
|
42
|
+
`Claude Code after, and your AI sees your Cortex context while your sessions flow\n` +
|
|
43
43
|
`into the org automatically.\n\n` +
|
|
44
44
|
`Subcommands:\n` +
|
|
45
45
|
` login [--label <name>] browser-approved sign-in — gets a token for you, then runs setup\n` +
|
|
46
46
|
` setup <token> wire MCP server + capture hook into ~/.claude config (single editor)\n` +
|
|
47
|
-
` install [<token>] [--editor auto|all|<id,...>] wire
|
|
47
|
+
` install [<token>] [--editor auto|all|<id,...>] wire Cortex into EVERY detected editor + write the capability manifest\n` +
|
|
48
48
|
` repair re-run setup at the latest version using your existing token (no token needed)\n` +
|
|
49
|
-
` uninstall remove ALL
|
|
49
|
+
` uninstall remove ALL Cortex wiring (MCP, hooks, skills, launchd, cron). --dry-run to preview, --purge to also wipe ~/.cortex + npx cache\n` +
|
|
50
50
|
` doctor live health check — confirm your token works (no restart needed)\n` +
|
|
51
51
|
` status one-line connected/not-connected check (used by the SessionStart hook)\n` +
|
|
52
|
-
`
|
|
53
|
-
` skills install/repair the managed Agnoclast skills — bundled + org-published (also wired by setup)\n` +
|
|
52
|
+
` skills install/repair the managed Cortex skills — bundled + org-published (also wired by setup)\n` +
|
|
54
53
|
` skills push <file> publish a SKILL.md to your org (owner/manager/admin)\n` +
|
|
55
|
-
` docs-scan detect new/changed local docs pending
|
|
56
|
-
` graphify-sync [path]
|
|
57
|
-
` snapshot-context save the exact startup context
|
|
54
|
+
` docs-scan detect new/changed local docs pending Cortex authoring (used by /cortex-author-docs)\n` +
|
|
55
|
+
` graphify-sync [path] rebuild the local code graph (graphify) + log an evidence-tier timeline event\n` +
|
|
56
|
+
` snapshot-context save the exact startup context Cortex served to a local snapshot\n` +
|
|
58
57
|
` hydrate UserPromptSubmit hook — inject query-centered context on the first substantive turn\n` +
|
|
59
58
|
` statusline ambient presence line for the Claude Code statusline (local read only)\n` +
|
|
60
59
|
` capture Stop-hook capturer (invoked by Claude Code)\n` +
|
|
61
60
|
` ingest-folder <path> ingest a local markdown folder as your authored records\n` +
|
|
62
61
|
` (no args) run the MCP server (used by your Claude config)\n\n` +
|
|
63
|
-
`Get your token from the
|
|
62
|
+
`Get your token from the Cortex console → Connect your AI.\n`,
|
|
64
63
|
)
|
|
65
64
|
process.exit(0)
|
|
66
65
|
}
|
|
@@ -85,14 +84,14 @@ if (cmd === 'login') {
|
|
|
85
84
|
const { closeFetch } = await import('../lib/diagnose.mjs')
|
|
86
85
|
await closeFetch()
|
|
87
86
|
} else if (cmd === 'install') {
|
|
88
|
-
// The cross-editor hub installer: wire
|
|
87
|
+
// The cross-editor hub installer: wire Cortex into every detected editor via the adapter
|
|
89
88
|
// registry, then write the capability manifest (~/.cortex/editors.json). Superset of `setup`.
|
|
90
89
|
const { runInstall } = await import('../lib/install.mjs')
|
|
91
90
|
await runInstall(rest, VERSION)
|
|
92
91
|
const { closeFetch } = await import('../lib/diagnose.mjs')
|
|
93
92
|
await closeFetch()
|
|
94
93
|
} else if (cmd === 'uninstall' || cmd === 'remove') {
|
|
95
|
-
// Full reverse of setup: strip every
|
|
94
|
+
// Full reverse of setup: strip every Cortex touch-point (MCP entries, hooks, skills, launchd, cron).
|
|
96
95
|
// --dry-run prints the plan and changes nothing; --purge also removes ~/.cortex, the npx cache, and
|
|
97
96
|
// backups. No network — safe to run even when the token is dead or the server is unreachable.
|
|
98
97
|
const { runUninstall } = await import('../lib/uninstall.mjs')
|
|
@@ -123,7 +122,7 @@ if (cmd === 'login') {
|
|
|
123
122
|
} else if (cmd === 'resolve') {
|
|
124
123
|
// Entity identity dedup: judge the server-flagged fuzzy duplicate pairs locally via `claude -p`.
|
|
125
124
|
const { runResolve } = await import('../lib/resolve.mjs')
|
|
126
|
-
await runResolve(
|
|
125
|
+
await runResolve()
|
|
127
126
|
const { closeFetch } = await import('../lib/diagnose.mjs')
|
|
128
127
|
await closeFetch()
|
|
129
128
|
} else if (cmd === 'materialize') {
|
|
@@ -154,7 +153,7 @@ if (cmd === 'login') {
|
|
|
154
153
|
const { closeFetch } = await import('../lib/diagnose.mjs')
|
|
155
154
|
await closeFetch()
|
|
156
155
|
} else if (cmd === 'hydrate') {
|
|
157
|
-
// UserPromptSubmit hook (① discovery): hydrate the model with query-centered
|
|
156
|
+
// UserPromptSubmit hook (① discovery): hydrate the model with query-centered Cortex context on the
|
|
158
157
|
// FIRST substantive turn, before it answers — then never again this session (topic-shift refresh stays
|
|
159
158
|
// the cortex-context skill's job). Synchronous by necessity, but once-per-session + 8s + fail-open.
|
|
160
159
|
const { runHydrate } = await import('../lib/hydrate.mjs')
|
|
@@ -167,7 +166,7 @@ if (cmd === 'login') {
|
|
|
167
166
|
const { runStatusline } = await import('../lib/statusline.mjs')
|
|
168
167
|
process.exitCode = runStatusline()
|
|
169
168
|
} else if (cmd === 'skills') {
|
|
170
|
-
// Install / repair the managed
|
|
169
|
+
// Install / repair the managed Cortex skills — bundled + org-published (`skills push` publishes).
|
|
171
170
|
// Org sync is network-fail-soft so the SessionStart hook stays safe offline.
|
|
172
171
|
const { runSkills } = await import('../lib/skills.mjs')
|
|
173
172
|
process.exitCode = await runSkills(rest)
|
|
@@ -178,27 +177,12 @@ if (cmd === 'login') {
|
|
|
178
177
|
// The cortex-author-docs skill authors the pending docs into wiki pages. No network.
|
|
179
178
|
const { runDocsScan } = await import('../lib/docs_scan.mjs')
|
|
180
179
|
process.exitCode = await runDocsScan(rest)
|
|
181
|
-
} else if (cmd === 'use-brain') {
|
|
182
|
-
// Set (or show) which brain this machine's unattended session captures land in. Registered here,
|
|
183
|
-
// BEFORE the default branch — an unrecognised subcommand falls through to "start the MCP server",
|
|
184
|
-
// which is how `connect-calendar` once silently became the file-watching daemon on a pilot user's
|
|
185
|
-
// machine and reported that daemon's errors instead of its own.
|
|
186
|
-
const { runUseBrain } = await import('../lib/use_brain.mjs')
|
|
187
|
-
process.exitCode = await runUseBrain(rest)
|
|
188
|
-
const { closeFetch } = await import('../lib/diagnose.mjs')
|
|
189
|
-
await closeFetch()
|
|
190
180
|
} else if (cmd === 'precompact') {
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
//
|
|
197
|
-
// Why this branch survives the removal: seats installed before this release still have
|
|
198
|
-
// `PreCompact: npx -y @theronap/cortex-mcp@latest precompact` in ~/.claude/settings.json, and @latest
|
|
199
|
-
// resolves to THIS build. Deleting the branch would turn a harmless no-op into an unknown-command
|
|
200
|
-
// error on every compaction for anyone who has not re-run install. mergeClaudeSettings unwires them
|
|
201
|
-
// on their next install/repair; drop this branch a release after that has had time to propagate.
|
|
181
|
+
// PreCompact hook (③ live wiki authoring, best-effort): print an "author now" reminder so the session
|
|
182
|
+
// sweeps its understanding into the wiki BEFORE compaction drops it. A hook cannot force a model turn
|
|
183
|
+
// (D2) — this is the documented fallback; /log is the hard backstop. No network; prints + exits.
|
|
184
|
+
const { runPrecompactReminder } = await import('../lib/precompact.mjs')
|
|
185
|
+
runPrecompactReminder()
|
|
202
186
|
} else {
|
|
203
187
|
// Default: run the MCP server (stays alive; never exits).
|
|
204
188
|
const { runServer } = await import('../lib/server.mjs')
|
package/lib/capture.mjs
CHANGED
|
@@ -35,7 +35,7 @@ export function projectFrom(cwd) {
|
|
|
35
35
|
return base ?? 'general'
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// Claude Code Stop hook → POSTs a session digest to
|
|
38
|
+
// Claude Code Stop hook → POSTs a session digest to Cortex cloud, which
|
|
39
39
|
// summarizes server-side and upserts ONE record per session. Node-native
|
|
40
40
|
// SHR-01/T6 — parse a git remote URL into GitHub 'owner/name', or null.
|
|
41
41
|
//
|
package/lib/code_graph_cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { join } from 'path'
|
|
|
4
4
|
|
|
5
5
|
// Thin local wrapper around the `graphify` CLI's read-only query subcommands. Deliberately NOT a
|
|
6
6
|
// fetchCortex client like grep/read_page: this is LOCAL-MACHINE data (a tree-sitter AST graph of
|
|
7
|
-
// whatever repo the session's cwd happens to be in), not org-shared
|
|
7
|
+
// whatever repo the session's cwd happens to be in), not org-shared Cortex content, and it never
|
|
8
8
|
// becomes the wiki graph — see cortex-wiki-primary-spec (structural/extracted data is evidence,
|
|
9
9
|
// never auto-promoted into authored pages). No LLM, no network call; graphify already built the
|
|
10
10
|
// graph on disk, this just queries it.
|
package/lib/context_log.mjs
CHANGED
|
@@ -49,7 +49,7 @@ export async function runSnapshotContext() {
|
|
|
49
49
|
const out = (m) => process.stdout.write(m + '\n')
|
|
50
50
|
const token = resolveToken()
|
|
51
51
|
if (!token) {
|
|
52
|
-
out('
|
|
52
|
+
out('Cortex: context snapshot skipped — no token found.')
|
|
53
53
|
return 0
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -58,13 +58,13 @@ export async function runSnapshotContext() {
|
|
|
58
58
|
try {
|
|
59
59
|
res = await fetchCortex(`${base}/api/mcp-context`, { headers: { Authorization: `Bearer ${token}` } })
|
|
60
60
|
} catch (e) {
|
|
61
|
-
out(`
|
|
61
|
+
out(`Cortex: context snapshot failed — ${e?.message ?? String(e)}`)
|
|
62
62
|
return 0
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
if (!res.ok) {
|
|
66
66
|
const body = await res.text()
|
|
67
|
-
out(`
|
|
67
|
+
out(`Cortex: context snapshot failed — ${classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message}`)
|
|
68
68
|
return 0
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -76,7 +76,7 @@ export async function runSnapshotContext() {
|
|
|
76
76
|
const capturedAt = new Date().toISOString()
|
|
77
77
|
const file = join(dir, `${stamp()}.md`)
|
|
78
78
|
const header = [
|
|
79
|
-
'#
|
|
79
|
+
'# Cortex startup context snapshot',
|
|
80
80
|
`- Captured: ${capturedAt}`,
|
|
81
81
|
`- Source: ${base}/api/mcp-context`,
|
|
82
82
|
'',
|
|
@@ -94,6 +94,6 @@ export async function runSnapshotContext() {
|
|
|
94
94
|
|
|
95
95
|
pruneSnapshots(dir)
|
|
96
96
|
|
|
97
|
-
out(`
|
|
97
|
+
out(`Cortex: logged startup context → ${file}`)
|
|
98
98
|
return 0
|
|
99
99
|
}
|
package/lib/diagnose.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Shared health / diagnosis helpers for the cortex MCP client.
|
|
2
2
|
//
|
|
3
3
|
// The whole reason this file exists: a transient infrastructure block (Vercel firewall / bot
|
|
4
|
-
// protection / deployment protection) once surfaced as a bare "
|
|
4
|
+
// protection / deployment protection) once surfaced as a bare "Cortex API 403: unknown", which
|
|
5
5
|
// read like an auth failure and sent everyone chasing token regeneration for hours. The fix is
|
|
6
6
|
// to tell the truth about WHAT failed.
|
|
7
7
|
//
|
|
8
|
-
// KEY SIGNAL: the
|
|
9
|
-
// 4xx/5xx means infrastructure handled the request, not
|
|
8
|
+
// KEY SIGNAL: the Cortex app ALWAYS returns JSON ({ error: ... }). So a NON-JSON body on a
|
|
9
|
+
// 4xx/5xx means infrastructure handled the request, not Cortex auth — re-running setup or
|
|
10
10
|
// regenerating the token will not help; it is usually transient and worth a retry.
|
|
11
11
|
|
|
12
12
|
import { readFileSync } from 'fs'
|
|
@@ -85,24 +85,15 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
|
85
85
|
// A non-OK response → an actionable diagnosis: { kind, retriable, message }.
|
|
86
86
|
// kind: 'auth' → token bad/revoked/wrong-deployment (NOT retriable)
|
|
87
87
|
// 'infra' → blocked by infrastructure, non-JSON body (retriable, usually transient)
|
|
88
|
-
// 'app' → a real
|
|
88
|
+
// 'app' → a real Cortex API error with a JSON message (retriable only if 5xx)
|
|
89
89
|
export function classify(status, contentType, bodyText, requestId) {
|
|
90
90
|
const isJson = (contentType ?? '').includes('application/json')
|
|
91
91
|
let appError = null
|
|
92
92
|
let appHint = null
|
|
93
|
-
let appMessage = null
|
|
94
|
-
let appBrains = null
|
|
95
93
|
if (isJson) {
|
|
96
94
|
try {
|
|
97
95
|
const parsed = JSON.parse(bodyText)
|
|
98
96
|
appError = parsed?.error ?? null
|
|
99
|
-
// Same lesson as `hint`, one layer up. brain_choice_response.ts writes a full explanation to
|
|
100
|
-
// `message` and every brain's NAME / PAGE COUNT / SAMPLE TITLES to `brains` — its comment says
|
|
101
|
-
// "THE BODY IS THE ANSWER TO ITS OWN QUESTION", precisely so a model can pick a brain from what
|
|
102
|
-
// each one HOLDS. Only `error` survived here, so the agent got the bare code `brain_required`
|
|
103
|
-
// and could not answer it. Measured on the CLI twin of this bug: `✗ brain_required`, full stop.
|
|
104
|
-
appMessage = typeof parsed?.message === 'string' && parsed.message.trim() ? parsed.message.trim() : null
|
|
105
|
-
appBrains = Array.isArray(parsed?.brains) ? parsed.brains : null
|
|
106
97
|
// `hint` carries the RECOVERY instruction for the errors an agent is meant to act on, not
|
|
107
98
|
// just report: 409 would_drop ("re-author including the dropped sections…") and 413 too_large
|
|
108
99
|
// ("split the section…"). It used to be dropped here — only `error` survived — so the agent
|
|
@@ -118,39 +109,20 @@ export function classify(status, contentType, bodyText, requestId) {
|
|
|
118
109
|
kind: 'auth', retriable: false,
|
|
119
110
|
message: `Token rejected (HTTP ${status}: ${appError ?? 'unauthorized'}). The token is invalid, ` +
|
|
120
111
|
`revoked, or for a different deployment — not an infrastructure problem. Get a fresh token from ` +
|
|
121
|
-
`the
|
|
112
|
+
`the Cortex console → Connect your AI, then re-run setup.${rid}`,
|
|
122
113
|
}
|
|
123
114
|
}
|
|
124
115
|
if (!isJson) {
|
|
125
116
|
return {
|
|
126
117
|
kind: 'infra', retriable: true,
|
|
127
|
-
message: `Blocked by infrastructure (HTTP ${status}, non-JSON response) — NOT by
|
|
118
|
+
message: `Blocked by infrastructure (HTTP ${status}, non-JSON response) — NOT by Cortex auth. This is ` +
|
|
128
119
|
`usually a transient firewall / bot-protection hiccup; retrying often clears it. If it persists, check ` +
|
|
129
120
|
`Vercel firewall / deployment protection on the API route. Re-running setup will not help.${rid}`,
|
|
130
121
|
}
|
|
131
122
|
}
|
|
132
|
-
// A brain-choice refusal is ANSWERABLE, so it must arrive as the question it is rather than as a
|
|
133
|
-
// code. Deliberately narrow: only these two errors reshape the message, so every other classify()
|
|
134
|
-
// output keeps its existing wording (and its tests).
|
|
135
|
-
if (isJson && (appError === 'brain_required' || appError === 'unknown_brain') && (appMessage || appBrains)) {
|
|
136
|
-
const list = (appBrains ?? []).map((b) => {
|
|
137
|
-
const pages = typeof b?.pageCount === 'number' ? ` — ${b.pageCount} page${b.pageCount === 1 ? '' : 's'}` : ''
|
|
138
|
-
const titles = Array.isArray(b?.sampleTitles) && b.sampleTitles.length
|
|
139
|
-
? `: ${b.sampleTitles.slice(0, 2).join('; ')}` : ''
|
|
140
|
-
// Name AND id: brain names are NOT unique (one account holds two called "Personal"), so a name
|
|
141
|
-
// alone can come back as unknown_brain. The id always resolves.
|
|
142
|
-
return ` - ${b?.name ?? '(unnamed)'}${pages}${titles}${b?.orgId ? ` [${b.orgId}]` : ''}`
|
|
143
|
-
})
|
|
144
|
-
return {
|
|
145
|
-
kind: 'app', retriable: false,
|
|
146
|
-
message: `Agnoclast API ${status}: ${appMessage ?? appError}${list.length ? `\n${list.join('\n')}` : ''}` +
|
|
147
|
-
`\nRe-run this tool with \`brain\` set to one of the names or ids above.${rid}`,
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
123
|
return {
|
|
152
124
|
kind: 'app', retriable: status >= 500,
|
|
153
|
-
message: `
|
|
125
|
+
message: `Cortex API ${status}: ${appError ?? 'unknown error'}.${appHint ? ` ${appHint}` : ''}${rid}`,
|
|
154
126
|
}
|
|
155
127
|
}
|
|
156
128
|
|
|
@@ -204,7 +176,7 @@ export async function fetchCortex(url, opts = {}, { retries = 2, baseDelayMs = 4
|
|
|
204
176
|
}
|
|
205
177
|
}
|
|
206
178
|
throw new Error(
|
|
207
|
-
`Could not reach
|
|
179
|
+
`Could not reach Cortex at ${url} — ${lastErr?.message ?? 'network error'}. ` +
|
|
208
180
|
`Check your connection (and CORTEX_URL if you set it).`,
|
|
209
181
|
)
|
|
210
182
|
}
|
|
@@ -218,7 +190,7 @@ export async function checkToken(token, base) {
|
|
|
218
190
|
}
|
|
219
191
|
if (!isUuid(token)) {
|
|
220
192
|
return { ok: false, status: 0, diagnosis: { kind: 'config', retriable: false,
|
|
221
|
-
message: `Token "${String(token).slice(0, 8)}…" is not a valid
|
|
193
|
+
message: `Token "${String(token).slice(0, 8)}…" is not a valid Cortex token (expected a UUID). ` +
|
|
222
194
|
`Re-run setup with the token from the console.` } }
|
|
223
195
|
}
|
|
224
196
|
const url = `${(base ?? 'https://cortex-console.vercel.app').replace(/\/$/, '')}/api/mcp-context`
|
|
@@ -235,54 +207,12 @@ export async function checkToken(token, base) {
|
|
|
235
207
|
return { ok: false, status: res.status, requestId, diagnosis: classify(res.status, contentType, body, requestId) }
|
|
236
208
|
}
|
|
237
209
|
let projectCount
|
|
238
|
-
let captureNotice = null
|
|
239
210
|
try {
|
|
240
|
-
const
|
|
241
|
-
const ctx = parsed.context ?? ''
|
|
211
|
+
const ctx = JSON.parse(body).context ?? ''
|
|
242
212
|
const m = ctx.match(/## Projects \((\d+)\)/)
|
|
243
213
|
if (m) projectCount = Number(m[1])
|
|
244
|
-
// The server's "your work is not landing" advisory. Optional by design: an older server does not
|
|
245
|
-
// send it and this must stay a health check, so a missing field is simply no notice.
|
|
246
|
-
if (parsed.captureNotice && typeof parsed.captureNotice.message === 'string') {
|
|
247
|
-
captureNotice = parsed.captureNotice
|
|
248
|
-
}
|
|
249
214
|
} catch { /* context shape changed — non-fatal for a health check */ }
|
|
250
|
-
return { ok: true, status: 200, projectCount, requestId
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Probe the org-skills surface. SEPARATE FROM checkToken ON PURPOSE.
|
|
254
|
-
//
|
|
255
|
-
// WHY THIS EXISTS. `doctor` checked exactly one endpoint — /api/mcp-context — and reported PASS on
|
|
256
|
-
// the strength of it. Meanwhile /api/skills answered 409 to every multi-brain caller for three days
|
|
257
|
-
// (measured 2026-08-08: 14 requests, 14 × 409, zero successes; the local cache sat frozen from Aug 5
|
|
258
|
-
// to Aug 8). Org-published skills silently never installed, the client swallowed the status, and
|
|
259
|
-
// doctor — the tool setup.txt tells people to run FIRST when something is wrong — said everything
|
|
260
|
-
// was fine, every single time. A check that probes one surface is a health check for that surface,
|
|
261
|
-
// not for the system, and must not be reported as the latter.
|
|
262
|
-
//
|
|
263
|
-
// Returns checkToken's shape so the caller renders both the same way. `skillCount` is
|
|
264
|
-
// informational: an org with zero published skills is perfectly healthy, so it is NOT a failure.
|
|
265
|
-
export async function checkSkills(token, base) {
|
|
266
|
-
if (!token || !isUuid(token)) {
|
|
267
|
-
return { ok: false, status: 0, diagnosis: { kind: 'config', retriable: false,
|
|
268
|
-
message: 'no usable token — the context check above already covers this' } }
|
|
269
|
-
}
|
|
270
|
-
const url = `${(base ?? CANONICAL_BASE).replace(/\/$/, '')}/api/skills`
|
|
271
|
-
let res
|
|
272
|
-
try {
|
|
273
|
-
res = await fetchCortex(url, { headers: { Authorization: `Bearer ${token}` } })
|
|
274
|
-
} catch (e) {
|
|
275
|
-
return { ok: false, status: 0, diagnosis: { kind: 'network', retriable: true, message: e.message } }
|
|
276
|
-
}
|
|
277
|
-
const requestId = res.headers.get('x-vercel-id') ?? null
|
|
278
|
-
const contentType = res.headers.get('content-type')
|
|
279
|
-
const body = await res.text()
|
|
280
|
-
if (!res.ok) {
|
|
281
|
-
return { ok: false, status: res.status, requestId, diagnosis: classify(res.status, contentType, body, requestId) }
|
|
282
|
-
}
|
|
283
|
-
let skillCount
|
|
284
|
-
try { skillCount = (JSON.parse(body)?.skills ?? []).length } catch { /* shape drift — non-fatal */ }
|
|
285
|
-
return { ok: true, status: 200, skillCount, requestId }
|
|
215
|
+
return { ok: true, status: 200, projectCount, requestId }
|
|
286
216
|
}
|
|
287
217
|
|
|
288
218
|
// Release the keep-alive sockets the global fetch (undici) holds, so a short-lived CLI command
|
package/lib/docs_scan.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { join, resolve, dirname } from 'path'
|
|
|
6
6
|
// Documentation ingestion — detection half (D1 of docs/documentation-ingestion-spec.md).
|
|
7
7
|
//
|
|
8
8
|
// Specs/plans/design docs get written to disk (repo docs/, ~/.gstack/projects/…) and never reach
|
|
9
|
-
//
|
|
9
|
+
// Cortex as pages. This subcommand DETECTS new/changed markdown under registered roots by content
|
|
10
10
|
// hash; the AUTHORING is done by the live session (the cortex-author-docs skill reads each pending
|
|
11
11
|
// doc and calls the `author` MCP tool) — the agent is the pipe, never a raw-markdown dump.
|
|
12
12
|
//
|
|
@@ -90,7 +90,7 @@ export function markFiles(state, paths, now = new Date().toISOString()) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
// Heuristic soak guardrail (spec D5): the Robin parity experiment forbids re-syncing the local
|
|
93
|
-
// brain into
|
|
93
|
+
// brain into Cortex during the window, so warn when a root looks like the brain repo.
|
|
94
94
|
const looksLikeBrain = (dir) => /\/Documents\/brain(\/|$)/.test(dir)
|
|
95
95
|
|
|
96
96
|
export async function runDocsScan(argv = []) {
|
|
@@ -111,7 +111,7 @@ export async function runDocsScan(argv = []) {
|
|
|
111
111
|
const abs = resolve(dir)
|
|
112
112
|
if (!existsSync(abs)) { process.stderr.write(`Not a directory: ${abs}\n`); return 1 }
|
|
113
113
|
if (looksLikeBrain(abs)) {
|
|
114
|
-
out(`⚠ ${abs} looks like the local brain repo — the Robin parity soak forbids re-syncing it into
|
|
114
|
+
out(`⚠ ${abs} looks like the local brain repo — the Robin parity soak forbids re-syncing it into Cortex.`)
|
|
115
115
|
out(' Registering anyway is on you; the cortex-author-docs skill will also warn.')
|
|
116
116
|
}
|
|
117
117
|
if (!state.roots.includes(abs)) state.roots.push(abs)
|
|
@@ -163,7 +163,7 @@ export async function runDocsScan(argv = []) {
|
|
|
163
163
|
}
|
|
164
164
|
if (!pending.length) out(`✓ up to date — ${scanned} doc(s) scanned, nothing pending`)
|
|
165
165
|
else {
|
|
166
|
-
out(`${pending.length} doc(s) pending
|
|
166
|
+
out(`${pending.length} doc(s) pending Cortex authoring (of ${scanned} scanned):`)
|
|
167
167
|
for (const p of pending) out(` ${p.status === 'new' ? '+ ' : '~ '}${p.path}`)
|
|
168
168
|
out('Author them via the cortex-author-docs skill, then: docs-scan --mark <file>...')
|
|
169
169
|
}
|
package/lib/doctor.mjs
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs'
|
|
2
2
|
import { homedir } from 'os'
|
|
3
3
|
import { join } from 'path'
|
|
4
|
-
import { checkToken,
|
|
4
|
+
import { checkToken, resolveBase } from './diagnose.mjs'
|
|
5
5
|
|
|
6
6
|
// `npx @theronap/cortex-mcp doctor` — a live, one-command health check.
|
|
7
7
|
//
|
|
8
8
|
// This is the "is it ACTUALLY working?" tool that was missing: it reads your token, calls the
|
|
9
|
-
// real
|
|
9
|
+
// real Cortex API, and prints a clear PASS/FAIL with the actual cause. No Claude Code restart
|
|
10
10
|
// needed — so onboarding can confirm the connection independently of "did the server load."
|
|
11
11
|
|
|
12
12
|
// Token resolution: env first, then the Claude config the setup command wrote (so `doctor`
|
|
13
13
|
// works the moment after `setup`, before any restart). Returns { token, source }.
|
|
14
|
-
|
|
15
|
-
// subtly different copy of this in context_log.mjs (returns a bare token, not {token, source}); a
|
|
16
|
-
// third copy is how a machine ends up "connected" to one command and "no token found" to another.
|
|
17
|
-
export function resolveToken() {
|
|
14
|
+
function resolveToken() {
|
|
18
15
|
if (process.env.CORTEX_TOKEN) return { token: process.env.CORTEX_TOKEN, source: 'CORTEX_TOKEN env' }
|
|
19
16
|
const claudeJson = join(homedir(), '.claude.json')
|
|
20
17
|
if (existsSync(claudeJson)) {
|
|
@@ -27,7 +24,7 @@ export function resolveToken() {
|
|
|
27
24
|
return { token: null, source: null }
|
|
28
25
|
}
|
|
29
26
|
|
|
30
|
-
// `status` — the one-line SessionStart variant of doctor: a visible "is
|
|
27
|
+
// `status` — the one-line SessionStart variant of doctor: a visible "is Cortex capturing?"
|
|
31
28
|
// signal inside Claude Code itself (three-machine dry-run finding 2026-06-09: with no
|
|
32
29
|
// indicator, a user can't tell whether their sessions are flowing to the org).
|
|
33
30
|
// Always returns 0 — a status line must never break a session start.
|
|
@@ -36,29 +33,19 @@ export async function runStatus() {
|
|
|
36
33
|
const out = (m) => process.stdout.write(m + '\n')
|
|
37
34
|
const { token } = resolveToken()
|
|
38
35
|
if (!token) {
|
|
39
|
-
out('
|
|
36
|
+
out('Cortex: NOT connected — no token found. Run: npx -y @theronap/cortex-mcp setup <token>')
|
|
40
37
|
return 0
|
|
41
38
|
}
|
|
42
39
|
try {
|
|
43
40
|
const r = await checkToken(token, base)
|
|
44
41
|
if (r.ok) {
|
|
45
|
-
// ⚠ THE NOTICE REPLACES THE HAPPY LINE RATHER THAN FOLLOWING IT.
|
|
46
|
-
// "connected — sessions on this machine are captured to your org" was printed truthfully to
|
|
47
|
-
// three people whose sessions were, at that moment, landing in NO brain: connected is a fact
|
|
48
|
-
// about the TOKEN, and every reader takes it as a fact about their WORK. Printing both would
|
|
49
|
-
// leave the reassurance that caused four days of silent loss sitting directly above the
|
|
50
|
-
// warning that contradicts it.
|
|
51
|
-
if (r.captureNotice?.message) {
|
|
52
|
-
out(`Agnoclast: ⚠ ${r.captureNotice.message}`)
|
|
53
|
-
return 0
|
|
54
|
-
}
|
|
55
42
|
const n = typeof r.projectCount === 'number' ? ` · ${r.projectCount} project${r.projectCount === 1 ? '' : 's'} visible` : ''
|
|
56
|
-
out(`
|
|
43
|
+
out(`Cortex: connected — sessions on this machine are captured to your org${n}.`)
|
|
57
44
|
} else {
|
|
58
|
-
out(`
|
|
45
|
+
out(`Cortex: NOT connected — ${r.diagnosis?.message ?? 'check failed'}. Run: npx -y @theronap/cortex-mcp doctor`)
|
|
59
46
|
}
|
|
60
47
|
} catch (e) {
|
|
61
|
-
out(`
|
|
48
|
+
out(`Cortex: status check failed (${e?.message ?? String(e)}) — run doctor.`)
|
|
62
49
|
}
|
|
63
50
|
return 0
|
|
64
51
|
}
|
|
@@ -68,7 +55,7 @@ export async function runDoctor() {
|
|
|
68
55
|
const out = (m) => process.stdout.write(m + '\n')
|
|
69
56
|
|
|
70
57
|
out('')
|
|
71
|
-
out('
|
|
58
|
+
out('Cortex doctor — checking your connection…')
|
|
72
59
|
const { token, source } = resolveToken()
|
|
73
60
|
out(` token source: ${source ?? 'NONE FOUND'}`)
|
|
74
61
|
out(` endpoint: ${base}`)
|
|
@@ -77,35 +64,11 @@ export async function runDoctor() {
|
|
|
77
64
|
const r = await checkToken(token, base)
|
|
78
65
|
|
|
79
66
|
if (r.ok) {
|
|
80
|
-
out(' ✓
|
|
67
|
+
out(' ✓ PASS — your token authenticates and Cortex returned your context.')
|
|
81
68
|
if (typeof r.projectCount === 'number') out(` You can currently see ${r.projectCount} project(s).`)
|
|
82
69
|
if (r.requestId) out(` (request id: ${r.requestId})`)
|
|
83
|
-
|
|
84
|
-
// A SECOND surface, because one endpoint answering is not "the system is healthy". /api/skills
|
|
85
|
-
// 409'd every multi-brain caller for three days while this command printed PASS on the strength
|
|
86
|
-
// of /api/mcp-context alone. See checkSkills' header for the full record.
|
|
87
|
-
const s = await checkSkills(token, base)
|
|
88
|
-
if (s.ok) {
|
|
89
|
-
const n = typeof s.skillCount === 'number'
|
|
90
|
-
? ` (${s.skillCount} org skill${s.skillCount === 1 ? '' : 's'} published)` : ''
|
|
91
|
-
out(` ✓ skills — the org-skill surface answers${n}.`)
|
|
92
|
-
} else {
|
|
93
|
-
// NOT a total failure — context works, so capture and retrieval are fine. But it must never
|
|
94
|
-
// again render as PASS, because org skills silently stop updating when this breaks.
|
|
95
|
-
out(` ✗ skills — ${s.diagnosis?.kind ?? 'error'}${s.status ? ` (HTTP ${s.status})` : ''}`)
|
|
96
|
-
out(` ${s.diagnosis?.message ?? 'unknown error'}`)
|
|
97
|
-
out('')
|
|
98
|
-
out(' ⚠ PARTIAL — your connection is fine and capture/retrieval work, but org-published')
|
|
99
|
-
out(' skills cannot be fetched, so they will silently stop updating (the client falls')
|
|
100
|
-
out(' back to its last-good cache). Report this rather than ignoring it.')
|
|
101
|
-
out('')
|
|
102
|
-
return 1
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
out('')
|
|
106
|
-
out(' ✓ PASS — every surface checked is answering.')
|
|
107
70
|
out('')
|
|
108
|
-
out(' If your AI still does not see
|
|
71
|
+
out(' If your AI still does not see Cortex, the server just is not loaded yet —')
|
|
109
72
|
out(' fully quit and reopen Claude Code (the MCP server starts on launch).')
|
|
110
73
|
out('')
|
|
111
74
|
return 0
|
|
@@ -55,7 +55,7 @@ export function renderAntigravityPlist({ home = homedir() } = {}) {
|
|
|
55
55
|
* NOTE the AGENT_DIR line is the portability blocker documented above. */
|
|
56
56
|
export function renderAntigravitySyncSh() {
|
|
57
57
|
return `#!/bin/bash
|
|
58
|
-
#
|
|
58
|
+
# Cortex ⇄ Antigravity one-shot sync, triggered by launchd WatchPaths on the Antigravity
|
|
59
59
|
# trajectory store. There is no resident daemon — launchd wakes this on change and it exits.
|
|
60
60
|
# Token is resolved from the wired MCP config at runtime (never stored in plist/script).
|
|
61
61
|
# ⚠ AGENT_DIR points at a local dev checkout — see antigravity.mjs BLOCKER note (not coworker-portable).
|
package/lib/editors/claude.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync } from 'node:fs'
|
|
|
6
6
|
import { join } from 'node:path'
|
|
7
7
|
import { readJson, backupFile, ensureDir, writeJson } from './_fsutil.mjs'
|
|
8
8
|
|
|
9
|
-
/** Merge the
|
|
9
|
+
/** Merge the Cortex MCP server into a ~/.claude.json object. Pure + idempotent: sets only the
|
|
10
10
|
* `cortex` entry (type:'stdio'), preserves every other server. `spec` = the package@dist-tag string. */
|
|
11
11
|
export function mergeClaudeMcp(existing, spec, token) {
|
|
12
12
|
const cfg = existing && typeof existing === 'object' ? { ...existing } : {}
|
|
@@ -15,16 +15,14 @@ export function mergeClaudeMcp(existing, spec, token) {
|
|
|
15
15
|
return cfg
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/** The
|
|
18
|
+
/** The Cortex tools every seat may run WITHOUT a permission prompt: the read surface, the live
|
|
19
19
|
* authoring core, and trivially-reversible maintenance. The contract this enforces: authoring is
|
|
20
20
|
* EXPECTED agent behavior — a page update must never stall on a yes/no dialog the user won't read
|
|
21
21
|
* (the ask-permission failure mode is how pages go stale). Safe because every page edit is
|
|
22
22
|
* CAS-protected + snapshotted (page_revisions → page_history/rollback_page).
|
|
23
23
|
* Deliberately EXCLUDED — these keep prompting: send_imessage (external side effect),
|
|
24
24
|
* set_page_privacy / set_record_privacy / grant_page_access (visibility widening — the
|
|
25
|
-
* unowned-project accessible-default sharp edge, 2026-07-02),
|
|
26
|
-
* and deletes a variant row — the one write here that is not merely CAS-protected but genuinely
|
|
27
|
-
* lossy at the row level, so the prompt IS the guard D1 argued for), rollback_page, decide_page_merge /
|
|
25
|
+
* unowned-project accessible-default sharp edge, 2026-07-02), rollback_page, decide_page_merge /
|
|
28
26
|
* decide_file_request / request_file / get_file, create_brain / set_active_brain, alias_page,
|
|
29
27
|
* set_writing_style. */
|
|
30
28
|
export const CORTEX_ALLOWED_TOOLS = [
|
|
@@ -38,11 +36,10 @@ export const CORTEX_ALLOWED_TOOLS = [
|
|
|
38
36
|
'set_page_validity', 'snooze_red_link', 'attribute_thread',
|
|
39
37
|
].map((t) => `mcp__cortex__${t}`)
|
|
40
38
|
|
|
41
|
-
/** Merge
|
|
39
|
+
/** Merge Cortex's Claude Code hooks into a ~/.claude/settings.json object. Pure + idempotent: drops
|
|
42
40
|
* any prior cortex entry (old token/path/version) from each hook array before appending the current
|
|
43
41
|
* one — capture (Stop), status + skills-repair + snapshot-context (SessionStart), hydrate
|
|
44
|
-
* (UserPromptSubmit)
|
|
45
|
-
* Commands carry NO inline token (each subcommand
|
|
42
|
+
* (UserPromptSubmit), precompact (PreCompact). Commands carry NO inline token (each subcommand
|
|
46
43
|
* self-resolves it). Mirrors setup.mjs
|
|
47
44
|
* step 2 exactly; foreign hooks are never touched. Also merges the CORTEX_ALLOWED_TOOLS permission
|
|
48
45
|
* allowlist — additive-only: a user's own allow entries (even extra mcp__cortex__* ones) are never
|
|
@@ -106,28 +103,16 @@ export function mergeClaudeSettings(existing, spec) {
|
|
|
106
103
|
hgrp.hooks = hgrp.hooks ?? []
|
|
107
104
|
hgrp.hooks.push({ type: 'command', command: hydrateCmd })
|
|
108
105
|
|
|
109
|
-
// PreCompact —
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
// local transcripts: the reminder text appears 5 times, every one of them a tool_result from someone
|
|
115
|
-
// READING the file, assistant prose about it, or a compaction summary that absorbed such prose. Zero
|
|
116
|
-
// injections, against >=13 transcripts that demonstrably compacted. The control is what makes that
|
|
117
|
-
// conclusive rather than merely absent: SessionStart's hook output, on a channel that IS injected,
|
|
118
|
-
// appears in 912 transcripts of the same corpus.
|
|
119
|
-
//
|
|
120
|
-
// So: filter, never append. Every install/repair strips the stale entry from seats that already have
|
|
121
|
-
// it, which is why this runs unconditionally instead of shipping as a separate migration.
|
|
122
|
-
if (Array.isArray(s.hooks.PreCompact)) {
|
|
123
|
-
for (const pg of s.hooks.PreCompact) {
|
|
124
|
-
if (Array.isArray(pg.hooks)) pg.hooks = pg.hooks.filter((h) => !/cortex-mcp(@[^ ]*)? precompact/.test(h.command ?? ''))
|
|
125
|
-
}
|
|
126
|
-
// Drop groups we just emptied, then the key itself if no foreign hook remains — a bare
|
|
127
|
-
// `PreCompact: []` reads as "cortex wires this event" to the next person to open settings.json.
|
|
128
|
-
s.hooks.PreCompact = s.hooks.PreCompact.filter((pg) => (pg.hooks ?? []).length > 0)
|
|
129
|
-
if (s.hooks.PreCompact.length === 0) delete s.hooks.PreCompact
|
|
106
|
+
// PreCompact — "author now" reminder.
|
|
107
|
+
s.hooks.PreCompact = Array.isArray(s.hooks.PreCompact) ? s.hooks.PreCompact : []
|
|
108
|
+
const precompactCmd = `npx -y ${spec} precompact`
|
|
109
|
+
for (const pg of s.hooks.PreCompact) {
|
|
110
|
+
if (Array.isArray(pg.hooks)) pg.hooks = pg.hooks.filter((h) => !/cortex-mcp(@[^ ]*)? precompact/.test(h.command ?? ''))
|
|
130
111
|
}
|
|
112
|
+
let pgrp = s.hooks.PreCompact.find((g) => (g.matcher ?? '') === '')
|
|
113
|
+
if (!pgrp) { pgrp = { matcher: '', hooks: [] }; s.hooks.PreCompact.push(pgrp) }
|
|
114
|
+
pgrp.hooks = pgrp.hooks ?? []
|
|
115
|
+
pgrp.hooks.push({ type: 'command', command: precompactCmd })
|
|
131
116
|
|
|
132
117
|
// Permissions — pre-authorize the read + authoring core so a page update never stalls on a
|
|
133
118
|
// permission prompt. Append-missing only (no filter-and-rebuild like the hooks above): removals
|