@theronap/cortex-mcp 0.9.79 → 0.9.80
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 +38 -22
- 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 +81 -11
- package/lib/docs_scan.mjs +4 -4
- package/lib/doctor.mjs +48 -11
- package/lib/editors/antigravity.mjs +1 -1
- package/lib/editors/claude.mjs +29 -14
- 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 +49 -4
- 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/redact.mjs +1 -1
- package/lib/resolve.mjs +62 -12
- package/lib/server.mjs +171 -41
- package/lib/setup.mjs +11 -10
- package/lib/skills.mjs +89 -17
- package/lib/timeline_claim_receipt.mjs +25 -0
- package/lib/uninstall.mjs +3 -3
- package/lib/use_brain.mjs +82 -0
- 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/precompact.mjs +0 -16
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 **Agnoclast** — 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 Agnoclast 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 **Cortex** — your org's projects, recent activity
|
|
|
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 Agnoclast 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 Agnoclast.
|
|
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 Agnoclast 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 Agnoclast console → Connect your AI.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { readFileSync } from 'node:fs'
|
|
@@ -35,31 +35,32 @@ 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 Agnoclast\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 Agnoclast 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 Agnoclast 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 Agnoclast 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
|
-
`
|
|
52
|
+
` use-brain [<brain>] where your session captures are saved — no arg shows the current setting\n` +
|
|
53
|
+
` skills install/repair the managed Agnoclast skills — bundled + org-published (also wired by setup)\n` +
|
|
53
54
|
` skills push <file> publish a SKILL.md to your org (owner/manager/admin)\n` +
|
|
54
|
-
` docs-scan detect new/changed local docs pending
|
|
55
|
-
` graphify-sync [path] rebuild the local code graph
|
|
56
|
-
` snapshot-context save the exact startup context
|
|
55
|
+
` docs-scan detect new/changed local docs pending Agnoclast authoring (used by /cortex-author-docs)\n` +
|
|
56
|
+
` graphify-sync [path] [--brain <name-or-id>] rebuild the local code graph + log a timeline event\n` +
|
|
57
|
+
` snapshot-context save the exact startup context Agnoclast served to a local snapshot\n` +
|
|
57
58
|
` hydrate UserPromptSubmit hook — inject query-centered context on the first substantive turn\n` +
|
|
58
59
|
` statusline ambient presence line for the Claude Code statusline (local read only)\n` +
|
|
59
60
|
` capture Stop-hook capturer (invoked by Claude Code)\n` +
|
|
60
61
|
` ingest-folder <path> ingest a local markdown folder as your authored records\n` +
|
|
61
62
|
` (no args) run the MCP server (used by your Claude config)\n\n` +
|
|
62
|
-
`Get your token from the
|
|
63
|
+
`Get your token from the Agnoclast console → Connect your AI.\n`,
|
|
63
64
|
)
|
|
64
65
|
process.exit(0)
|
|
65
66
|
}
|
|
@@ -84,14 +85,14 @@ if (cmd === 'login') {
|
|
|
84
85
|
const { closeFetch } = await import('../lib/diagnose.mjs')
|
|
85
86
|
await closeFetch()
|
|
86
87
|
} else if (cmd === 'install') {
|
|
87
|
-
// The cross-editor hub installer: wire
|
|
88
|
+
// The cross-editor hub installer: wire Agnoclast into every detected editor via the adapter
|
|
88
89
|
// registry, then write the capability manifest (~/.cortex/editors.json). Superset of `setup`.
|
|
89
90
|
const { runInstall } = await import('../lib/install.mjs')
|
|
90
91
|
await runInstall(rest, VERSION)
|
|
91
92
|
const { closeFetch } = await import('../lib/diagnose.mjs')
|
|
92
93
|
await closeFetch()
|
|
93
94
|
} else if (cmd === 'uninstall' || cmd === 'remove') {
|
|
94
|
-
// Full reverse of setup: strip every
|
|
95
|
+
// Full reverse of setup: strip every Agnoclast touch-point (MCP entries, hooks, skills, launchd, cron).
|
|
95
96
|
// --dry-run prints the plan and changes nothing; --purge also removes ~/.cortex, the npx cache, and
|
|
96
97
|
// backups. No network — safe to run even when the token is dead or the server is unreachable.
|
|
97
98
|
const { runUninstall } = await import('../lib/uninstall.mjs')
|
|
@@ -122,7 +123,7 @@ if (cmd === 'login') {
|
|
|
122
123
|
} else if (cmd === 'resolve') {
|
|
123
124
|
// Entity identity dedup: judge the server-flagged fuzzy duplicate pairs locally via `claude -p`.
|
|
124
125
|
const { runResolve } = await import('../lib/resolve.mjs')
|
|
125
|
-
await runResolve()
|
|
126
|
+
await runResolve(rest)
|
|
126
127
|
const { closeFetch } = await import('../lib/diagnose.mjs')
|
|
127
128
|
await closeFetch()
|
|
128
129
|
} else if (cmd === 'materialize') {
|
|
@@ -153,7 +154,7 @@ if (cmd === 'login') {
|
|
|
153
154
|
const { closeFetch } = await import('../lib/diagnose.mjs')
|
|
154
155
|
await closeFetch()
|
|
155
156
|
} else if (cmd === 'hydrate') {
|
|
156
|
-
// UserPromptSubmit hook (① discovery): hydrate the model with query-centered
|
|
157
|
+
// UserPromptSubmit hook (① discovery): hydrate the model with query-centered Agnoclast context on the
|
|
157
158
|
// FIRST substantive turn, before it answers — then never again this session (topic-shift refresh stays
|
|
158
159
|
// the cortex-context skill's job). Synchronous by necessity, but once-per-session + 8s + fail-open.
|
|
159
160
|
const { runHydrate } = await import('../lib/hydrate.mjs')
|
|
@@ -166,7 +167,7 @@ if (cmd === 'login') {
|
|
|
166
167
|
const { runStatusline } = await import('../lib/statusline.mjs')
|
|
167
168
|
process.exitCode = runStatusline()
|
|
168
169
|
} else if (cmd === 'skills') {
|
|
169
|
-
// Install / repair the managed
|
|
170
|
+
// Install / repair the managed Agnoclast skills — bundled + org-published (`skills push` publishes).
|
|
170
171
|
// Org sync is network-fail-soft so the SessionStart hook stays safe offline.
|
|
171
172
|
const { runSkills } = await import('../lib/skills.mjs')
|
|
172
173
|
process.exitCode = await runSkills(rest)
|
|
@@ -177,12 +178,27 @@ if (cmd === 'login') {
|
|
|
177
178
|
// The cortex-author-docs skill authors the pending docs into wiki pages. No network.
|
|
178
179
|
const { runDocsScan } = await import('../lib/docs_scan.mjs')
|
|
179
180
|
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()
|
|
180
190
|
} else if (cmd === 'precompact') {
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
191
|
+
// RETIRED — deliberately kept as a silent no-op, do not delete yet.
|
|
192
|
+
//
|
|
193
|
+
// The reminder it used to print never reached a model: PreCompact takes a blocking `decision` and has
|
|
194
|
+
// no additionalContext channel, so its stdout went nowhere (verified 2026-08-10 against 7,232 local
|
|
195
|
+
// transcripts — see the PreCompact block in lib/editors/claude.mjs for the measurement and control).
|
|
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.
|
|
186
202
|
} else {
|
|
187
203
|
// Default: run the MCP server (stays alive; never exits).
|
|
188
204
|
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 Agnoclast 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 Agnoclast 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('Agnoclast: 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(`Agnoclast: 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(`Agnoclast: 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
|
+
'# Agnoclast 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(`Agnoclast: 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 "Agnoclast 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 Agnoclast app ALWAYS returns JSON ({ error: ... }). So a NON-JSON body on a
|
|
9
|
+
// 4xx/5xx means infrastructure handled the request, not Agnoclast 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,15 +85,24 @@ 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 Agnoclast 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
|
|
93
95
|
if (isJson) {
|
|
94
96
|
try {
|
|
95
97
|
const parsed = JSON.parse(bodyText)
|
|
96
98
|
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
|
|
97
106
|
// `hint` carries the RECOVERY instruction for the errors an agent is meant to act on, not
|
|
98
107
|
// just report: 409 would_drop ("re-author including the dropped sections…") and 413 too_large
|
|
99
108
|
// ("split the section…"). It used to be dropped here — only `error` survived — so the agent
|
|
@@ -109,20 +118,39 @@ export function classify(status, contentType, bodyText, requestId) {
|
|
|
109
118
|
kind: 'auth', retriable: false,
|
|
110
119
|
message: `Token rejected (HTTP ${status}: ${appError ?? 'unauthorized'}). The token is invalid, ` +
|
|
111
120
|
`revoked, or for a different deployment — not an infrastructure problem. Get a fresh token from ` +
|
|
112
|
-
`the
|
|
121
|
+
`the Agnoclast console → Connect your AI, then re-run setup.${rid}`,
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
124
|
if (!isJson) {
|
|
116
125
|
return {
|
|
117
126
|
kind: 'infra', retriable: true,
|
|
118
|
-
message: `Blocked by infrastructure (HTTP ${status}, non-JSON response) — NOT by
|
|
127
|
+
message: `Blocked by infrastructure (HTTP ${status}, non-JSON response) — NOT by Agnoclast auth. This is ` +
|
|
119
128
|
`usually a transient firewall / bot-protection hiccup; retrying often clears it. If it persists, check ` +
|
|
120
129
|
`Vercel firewall / deployment protection on the API route. Re-running setup will not help.${rid}`,
|
|
121
130
|
}
|
|
122
131
|
}
|
|
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
|
+
|
|
123
151
|
return {
|
|
124
152
|
kind: 'app', retriable: status >= 500,
|
|
125
|
-
message: `
|
|
153
|
+
message: `Agnoclast API ${status}: ${appError ?? 'unknown error'}.${appHint ? ` ${appHint}` : ''}${rid}`,
|
|
126
154
|
}
|
|
127
155
|
}
|
|
128
156
|
|
|
@@ -176,7 +204,7 @@ export async function fetchCortex(url, opts = {}, { retries = 2, baseDelayMs = 4
|
|
|
176
204
|
}
|
|
177
205
|
}
|
|
178
206
|
throw new Error(
|
|
179
|
-
`Could not reach
|
|
207
|
+
`Could not reach Agnoclast at ${url} — ${lastErr?.message ?? 'network error'}. ` +
|
|
180
208
|
`Check your connection (and CORTEX_URL if you set it).`,
|
|
181
209
|
)
|
|
182
210
|
}
|
|
@@ -190,7 +218,7 @@ export async function checkToken(token, base) {
|
|
|
190
218
|
}
|
|
191
219
|
if (!isUuid(token)) {
|
|
192
220
|
return { ok: false, status: 0, diagnosis: { kind: 'config', retriable: false,
|
|
193
|
-
message: `Token "${String(token).slice(0, 8)}…" is not a valid
|
|
221
|
+
message: `Token "${String(token).slice(0, 8)}…" is not a valid Agnoclast token (expected a UUID). ` +
|
|
194
222
|
`Re-run setup with the token from the console.` } }
|
|
195
223
|
}
|
|
196
224
|
const url = `${(base ?? 'https://cortex-console.vercel.app').replace(/\/$/, '')}/api/mcp-context`
|
|
@@ -207,12 +235,54 @@ export async function checkToken(token, base) {
|
|
|
207
235
|
return { ok: false, status: res.status, requestId, diagnosis: classify(res.status, contentType, body, requestId) }
|
|
208
236
|
}
|
|
209
237
|
let projectCount
|
|
238
|
+
let captureNotice = null
|
|
210
239
|
try {
|
|
211
|
-
const
|
|
240
|
+
const parsed = JSON.parse(body)
|
|
241
|
+
const ctx = parsed.context ?? ''
|
|
212
242
|
const m = ctx.match(/## Projects \((\d+)\)/)
|
|
213
243
|
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
|
+
}
|
|
214
249
|
} catch { /* context shape changed — non-fatal for a health check */ }
|
|
215
|
-
return { ok: true, status: 200, projectCount, requestId }
|
|
250
|
+
return { ok: true, status: 200, projectCount, requestId, captureNotice }
|
|
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 }
|
|
216
286
|
}
|
|
217
287
|
|
|
218
288
|
// 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
|
+
// Agnoclast 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 Agnoclast 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 Agnoclast.`)
|
|
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 Agnoclast 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,17 +1,20 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs'
|
|
2
2
|
import { homedir } from 'os'
|
|
3
3
|
import { join } from 'path'
|
|
4
|
-
import { checkToken, resolveBase } from './diagnose.mjs'
|
|
4
|
+
import { checkToken, checkSkills, 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 Agnoclast 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
|
-
|
|
14
|
+
// Exported so `use-brain` resolves the token EXACTLY as doctor/status do. There is already a second,
|
|
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() {
|
|
15
18
|
if (process.env.CORTEX_TOKEN) return { token: process.env.CORTEX_TOKEN, source: 'CORTEX_TOKEN env' }
|
|
16
19
|
const claudeJson = join(homedir(), '.claude.json')
|
|
17
20
|
if (existsSync(claudeJson)) {
|
|
@@ -24,7 +27,7 @@ function resolveToken() {
|
|
|
24
27
|
return { token: null, source: null }
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
// `status` — the one-line SessionStart variant of doctor: a visible "is
|
|
30
|
+
// `status` — the one-line SessionStart variant of doctor: a visible "is Agnoclast capturing?"
|
|
28
31
|
// signal inside Claude Code itself (three-machine dry-run finding 2026-06-09: with no
|
|
29
32
|
// indicator, a user can't tell whether their sessions are flowing to the org).
|
|
30
33
|
// Always returns 0 — a status line must never break a session start.
|
|
@@ -33,19 +36,29 @@ export async function runStatus() {
|
|
|
33
36
|
const out = (m) => process.stdout.write(m + '\n')
|
|
34
37
|
const { token } = resolveToken()
|
|
35
38
|
if (!token) {
|
|
36
|
-
out('
|
|
39
|
+
out('Agnoclast: NOT connected — no token found. Run: npx -y @theronap/cortex-mcp setup <token>')
|
|
37
40
|
return 0
|
|
38
41
|
}
|
|
39
42
|
try {
|
|
40
43
|
const r = await checkToken(token, base)
|
|
41
44
|
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
|
+
}
|
|
42
55
|
const n = typeof r.projectCount === 'number' ? ` · ${r.projectCount} project${r.projectCount === 1 ? '' : 's'} visible` : ''
|
|
43
|
-
out(`
|
|
56
|
+
out(`Agnoclast: connected — sessions on this machine are captured to your org${n}.`)
|
|
44
57
|
} else {
|
|
45
|
-
out(`
|
|
58
|
+
out(`Agnoclast: NOT connected — ${r.diagnosis?.message ?? 'check failed'}. Run: npx -y @theronap/cortex-mcp doctor`)
|
|
46
59
|
}
|
|
47
60
|
} catch (e) {
|
|
48
|
-
out(`
|
|
61
|
+
out(`Agnoclast: status check failed (${e?.message ?? String(e)}) — run doctor.`)
|
|
49
62
|
}
|
|
50
63
|
return 0
|
|
51
64
|
}
|
|
@@ -55,7 +68,7 @@ export async function runDoctor() {
|
|
|
55
68
|
const out = (m) => process.stdout.write(m + '\n')
|
|
56
69
|
|
|
57
70
|
out('')
|
|
58
|
-
out('
|
|
71
|
+
out('Agnoclast doctor — checking your connection…')
|
|
59
72
|
const { token, source } = resolveToken()
|
|
60
73
|
out(` token source: ${source ?? 'NONE FOUND'}`)
|
|
61
74
|
out(` endpoint: ${base}`)
|
|
@@ -64,11 +77,35 @@ export async function runDoctor() {
|
|
|
64
77
|
const r = await checkToken(token, base)
|
|
65
78
|
|
|
66
79
|
if (r.ok) {
|
|
67
|
-
out(' ✓
|
|
80
|
+
out(' ✓ context — your token authenticates and Agnoclast returned your context.')
|
|
68
81
|
if (typeof r.projectCount === 'number') out(` You can currently see ${r.projectCount} project(s).`)
|
|
69
82
|
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.')
|
|
70
107
|
out('')
|
|
71
|
-
out(' If your AI still does not see
|
|
108
|
+
out(' If your AI still does not see Agnoclast, the server just is not loaded yet —')
|
|
72
109
|
out(' fully quit and reopen Claude Code (the MCP server starts on launch).')
|
|
73
110
|
out('')
|
|
74
111
|
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
|
+
# Agnoclast ⇄ 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 Agnoclast 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,14 +15,16 @@ export function mergeClaudeMcp(existing, spec, token) {
|
|
|
15
15
|
return cfg
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/** The
|
|
18
|
+
/** The Agnoclast 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),
|
|
25
|
+
* unowned-project accessible-default sharp edge, 2026-07-02), replace_variant (destroys a page body
|
|
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 /
|
|
26
28
|
* decide_file_request / request_file / get_file, create_brain / set_active_brain, alias_page,
|
|
27
29
|
* set_writing_style. */
|
|
28
30
|
export const CORTEX_ALLOWED_TOOLS = [
|
|
@@ -36,10 +38,11 @@ export const CORTEX_ALLOWED_TOOLS = [
|
|
|
36
38
|
'set_page_validity', 'snooze_red_link', 'attribute_thread',
|
|
37
39
|
].map((t) => `mcp__cortex__${t}`)
|
|
38
40
|
|
|
39
|
-
/** Merge
|
|
41
|
+
/** Merge Agnoclast's Claude Code hooks into a ~/.claude/settings.json object. Pure + idempotent: drops
|
|
40
42
|
* any prior cortex entry (old token/path/version) from each hook array before appending the current
|
|
41
43
|
* one — capture (Stop), status + skills-repair + snapshot-context (SessionStart), hydrate
|
|
42
|
-
* (UserPromptSubmit)
|
|
44
|
+
* (UserPromptSubmit). Also UNWIRES the retired PreCompact reminder from seats that still carry it.
|
|
45
|
+
* Commands carry NO inline token (each subcommand
|
|
43
46
|
* self-resolves it). Mirrors setup.mjs
|
|
44
47
|
* step 2 exactly; foreign hooks are never touched. Also merges the CORTEX_ALLOWED_TOOLS permission
|
|
45
48
|
* allowlist — additive-only: a user's own allow entries (even extra mcp__cortex__* ones) are never
|
|
@@ -103,16 +106,28 @@ export function mergeClaudeSettings(existing, spec) {
|
|
|
103
106
|
hgrp.hooks = hgrp.hooks ?? []
|
|
104
107
|
hgrp.hooks.push({ type: 'command', command: hydrateCmd })
|
|
105
108
|
|
|
106
|
-
// PreCompact —
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
// PreCompact — REMOVED, and this block is the migration that unwires existing seats.
|
|
110
|
+
//
|
|
111
|
+
// The "author now" reminder wrote to stdout on the assumption the harness surfaced it as context for
|
|
112
|
+
// the next turn. It does not: PreCompact accepts a blocking `decision` and nothing else — it has no
|
|
113
|
+
// additionalContext channel — so the reminder never reached a model. Measured 2026-08-10 across 7,232
|
|
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
|
|
111
130
|
}
|
|
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 })
|
|
116
131
|
|
|
117
132
|
// Permissions — pre-authorize the read + authoring core so a page update never stalls on a
|
|
118
133
|
// permission prompt. Append-missing only (no filter-and-rebuild like the hooks above): removals
|