@theronap/cortex-mcp 0.9.77 → 0.9.79
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 +17 -17
- 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 +9 -72
- package/lib/docs_scan.mjs +4 -4
- package/lib/doctor.mjs +10 -34
- package/lib/editors/antigravity.mjs +1 -1
- package/lib/editors/claude.mjs +4 -6
- 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 +1 -1
- package/lib/redact.mjs +1 -1
- package/lib/resolve.mjs +12 -62
- package/lib/server.mjs +51 -92
- package/lib/setup.mjs +8 -8
- 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/hydrate.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { formatReceipt, renderUncounted, subjectOf } from './presence.mjs'
|
|
|
7
7
|
import { writePresence } from './statusline.mjs'
|
|
8
8
|
|
|
9
9
|
// UserPromptSubmit hook (① discovery): on the FIRST substantive turn of a session, hydrate the model
|
|
10
|
-
// with query-centered
|
|
10
|
+
// with query-centered Cortex context BEFORE it answers — then never again this session. It closes the
|
|
11
11
|
// gap that read-triggered authoring can't: an agent that never READS the wiki (works from code + memory)
|
|
12
12
|
// never triggers a currency update, and re-derives already-authored truth. cortex-context is a SKILL the
|
|
13
13
|
// model may forget to invoke; this makes the first hydration non-discretionary (the same move that made
|
package/lib/imessage_send.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// send_imessage — outbound iMessage via Messages.app. This is a personal automation, NOT org
|
|
2
|
-
// intelligence: it writes nothing to
|
|
2
|
+
// intelligence: it writes nothing to Cortex. Three layers of safety (eng-review D3/D6/D10):
|
|
3
3
|
// D6 argv-safe: recipient + body are passed as osascript `on run argv` arguments, NEVER
|
|
4
4
|
// interpolated into the script source → no AppleScript injection, no quote/newline breakage.
|
|
5
5
|
// D3 draft-by-default: nothing sends unless the caller explicitly passes send:true.
|
package/lib/ingest_folder.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { join, relative, basename, extname } from 'node:path'
|
|
|
3
3
|
import { fetchCortex, classify, resolveBase } from './diagnose.mjs'
|
|
4
4
|
|
|
5
5
|
// `cortex-mcp ingest-folder <path>` — walk a local markdown folder and upsert each file as an
|
|
6
|
-
// AUTHORED record in
|
|
6
|
+
// AUTHORED record in Cortex (source='brain'), so a user's personal digest is built from THEIR
|
|
7
7
|
// notes (Robin parity). Only an EXCERPT of each file leaves the machine (frontmatter + first
|
|
8
8
|
// ~800 chars), never the full body. The server clamps privacy for source='brain' and treats
|
|
9
9
|
// sessionId (the file's relative path) as the stable dedupe key, so re-running updates in place.
|
|
@@ -100,7 +100,7 @@ export async function runIngestFolder(argv) {
|
|
|
100
100
|
if (!path) {
|
|
101
101
|
process.stderr.write(
|
|
102
102
|
'usage: cortex-mcp ingest-folder <path>\n' +
|
|
103
|
-
' Ingest a local markdown folder as your authored
|
|
103
|
+
' Ingest a local markdown folder as your authored Cortex records.\n',
|
|
104
104
|
)
|
|
105
105
|
return
|
|
106
106
|
}
|
package/lib/install.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `cortex install` — the cross-editor hub installer (P0-b). One command that wires the
|
|
1
|
+
// `cortex install` — the cross-editor hub installer (P0-b). One command that wires the Cortex MCP
|
|
2
2
|
// server + client shim into EVERY detected editor via the adapter registry, then writes a capability
|
|
3
3
|
// manifest so later pillars (session sync, skill sync, doc sync) know which channels each editor
|
|
4
4
|
// supports. `setup <token>` stays as the single-editor path the console prints; `install` is the
|
|
@@ -78,7 +78,7 @@ export async function runInstall(argv, version) {
|
|
|
78
78
|
process.stderr.write(
|
|
79
79
|
'Usage: npx @theronap/cortex-mcp install [<CORTEX_TOKEN>] [--editor auto|all|<id,...>]\n\n' +
|
|
80
80
|
'No token was given and none is already wired.\n' +
|
|
81
|
-
'Get your token from the
|
|
81
|
+
'Get your token from the Cortex console → Connect your AI.\n',
|
|
82
82
|
)
|
|
83
83
|
process.exit(1)
|
|
84
84
|
}
|
|
@@ -92,7 +92,7 @@ export async function runInstall(argv, version) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
log('')
|
|
95
|
-
log('
|
|
95
|
+
log('Cortex install — wiring your AI editors…')
|
|
96
96
|
if (editor === 'auto' && targets.length === 0) {
|
|
97
97
|
log(' (no supported editors detected — pass --editor all to force, or install an editor first)')
|
|
98
98
|
}
|
|
@@ -135,7 +135,7 @@ export async function runInstall(argv, version) {
|
|
|
135
135
|
// Verify the token against the live API — writing config proves "files written", not "it works".
|
|
136
136
|
const base = resolveBase(process.env.CORTEX_URL)
|
|
137
137
|
log('')
|
|
138
|
-
log('Verifying your token against
|
|
138
|
+
log('Verifying your token against Cortex…')
|
|
139
139
|
try {
|
|
140
140
|
const health = await checkToken(token, base)
|
|
141
141
|
if (health.ok) {
|
|
@@ -147,13 +147,13 @@ export async function runInstall(argv, version) {
|
|
|
147
147
|
log(' The files are in place; fix the above, then re-check with `doctor`.')
|
|
148
148
|
}
|
|
149
149
|
} catch (e) {
|
|
150
|
-
log(` ⚠ Could not reach
|
|
150
|
+
log(` ⚠ Could not reach Cortex to verify (${e.message}). Config is written; re-check with 'doctor'.`)
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
const wired = entries.filter((e) => targetIds.has(e.adapter.id) && e.wired).map((e) => e.adapter.displayName)
|
|
154
154
|
log('')
|
|
155
155
|
log(`⟳ Wired ${wired.length} editor(s): ${wired.join(', ') || '(none)'}`)
|
|
156
|
-
log(' Fully quit and reopen each editor to load the
|
|
156
|
+
log(' Fully quit and reopen each editor to load the Cortex server.')
|
|
157
157
|
log(` Console: ${base}`)
|
|
158
158
|
log('')
|
|
159
159
|
}
|
package/lib/precompact.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// Output goes to stdout, which the Claude Code harness surfaces as additional context for the next turn.
|
|
9
9
|
export function runPrecompactReminder() {
|
|
10
10
|
process.stdout.write(
|
|
11
|
-
'
|
|
11
|
+
'Cortex: context is about to compact. If your understanding of any node (the project(s) you worked ' +
|
|
12
12
|
'on, people you coordinated with, or yourself) advanced this session, AUTHOR it into the wiki NOW ' +
|
|
13
13
|
'before it is lost: call `authoring_context` then `author` for each. This is a synthesis of your ' +
|
|
14
14
|
'compiled understanding with inline [[links]], not a transcript dump. Skip nodes you did not advance.\n',
|
package/lib/redact.mjs
CHANGED
|
@@ -21,7 +21,7 @@ const PATTERNS = [
|
|
|
21
21
|
[/xox[baprs]-[A-Za-z0-9-]{10,}/g, '[REDACTED:slack]'],
|
|
22
22
|
[/eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, '[REDACTED:jwt]'],
|
|
23
23
|
[/(Bearer\s+)[A-Za-z0-9._-]{20,}/g, '$1[REDACTED]'],
|
|
24
|
-
//
|
|
24
|
+
// Cortex's own login token wherever it appears in KEY=value / KEY: value form (hooks.json,
|
|
25
25
|
// config.toml, shell commands — the 2026-07-02 finding: a grep of hooks.json put the live token
|
|
26
26
|
// in a transcript and nothing below caught it). Specific pattern first for the accurate label.
|
|
27
27
|
[/(CORTEX_TOKEN["']?\s*[=:]\s*["']?)[0-9a-fA-F][0-9a-fA-F-]{30,}/g, '$1[REDACTED:cortex-token]'],
|
package/lib/resolve.mjs
CHANGED
|
@@ -8,97 +8,47 @@ import { edgeSafeEnv } from './edge_extract.mjs'
|
|
|
8
8
|
// and pushes decisions back (POST /api/resolve-apply): confident-same → entity_merges, else → rejected
|
|
9
9
|
// so the pair never re-flags. Conservative by construction. Always exits cleanly.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
// answering a scoped read out of an ARBITRARY brain is the defect that whole class exists to
|
|
13
|
-
// prevent). Until 2026-08-09 this command named none, so a multi-brain caller got 409 on step 1 and
|
|
14
|
-
// the dedup sweep did nothing at all, silently, forever.
|
|
15
|
-
//
|
|
16
|
-
// Naming ONE brain would have been the smaller fix and the wrong one: `resolve` is a MAINTENANCE
|
|
17
|
-
// SWEEP over the user's entities, so doing one brain and reporting success is ADR-0022's other
|
|
18
|
-
// failure — "silently truncating a result set and presenting it as complete". With no --brain we
|
|
19
|
-
// enumerate the caller's brains and sweep EACH, naming it explicitly. Not a guess: every request
|
|
20
|
-
// still names exactly one brain, and all of them actually get done.
|
|
21
|
-
//
|
|
22
|
-
// A sole-brain caller sees today's behaviour: one pass, no flag, no prompt.
|
|
23
|
-
export async function brainsToSweep(base, token, wanted, deps = {}) {
|
|
24
|
-
const fetchFn = deps.fetchCortex ?? fetchCortex
|
|
25
|
-
if (wanted) return [{ orgId: wanted, name: wanted }] // explicit: pass through (name or org id)
|
|
26
|
-
const res = await fetchFn(`${base}/api/brains`, { headers: { Authorization: `Bearer ${token}` } })
|
|
27
|
-
if (!res.ok) {
|
|
28
|
-
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
29
|
-
throw new Error(`could not list your brains — ${d.message}`)
|
|
30
|
-
}
|
|
31
|
-
const j = await res.json().catch(() => ({}))
|
|
32
|
-
const brains = Array.isArray(j.brains) ? j.brains : []
|
|
33
|
-
// Carry the ORG ID, never the name: brain names are NOT unique (this account holds two called
|
|
34
|
-
// "Personal"), and a duplicate name comes back as unknown_brain.
|
|
35
|
-
return brains.filter((b) => b?.orgId).map((b) => ({ orgId: b.orgId, name: b.name ?? b.orgId }))
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function runResolve(argv = []) {
|
|
11
|
+
export async function runResolve() {
|
|
39
12
|
// recursion guard (we spawn `claude --print`; if its Stop hook fires capture, that no-ops on this flag)
|
|
40
13
|
if (process.env.CORTEX_SUMMARIZING) { process.stderr.write('cortex: summarizer subprocess, skipping\n'); return }
|
|
41
14
|
const token = process.env.CORTEX_TOKEN
|
|
42
15
|
if (!token) { process.stderr.write('cortex: CORTEX_TOKEN not set, skipping\n'); return }
|
|
43
16
|
const base = resolveBase(process.env.CORTEX_URL)
|
|
44
17
|
|
|
45
|
-
const bIdx = argv.indexOf('--brain')
|
|
46
|
-
const wanted = bIdx === -1 ? null : argv[bIdx + 1]
|
|
47
|
-
if (bIdx !== -1 && (!wanted || wanted.startsWith('-'))) {
|
|
48
|
-
process.stderr.write('Usage: resolve [--brain <name-or-org-id>]\n'); return
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
let targets
|
|
52
|
-
try {
|
|
53
|
-
targets = await brainsToSweep(base, token, wanted)
|
|
54
|
-
} catch (e) { process.stderr.write(`cortex: resolve — ${e.message}\n`); return }
|
|
55
|
-
if (!targets.length) { process.stderr.write('cortex: no brains to sweep\n'); return }
|
|
56
|
-
|
|
57
|
-
for (const t of targets) {
|
|
58
|
-
// Label each line with the brain when sweeping several: an unlabelled "merged 3" cannot be acted
|
|
59
|
-
// on, because you cannot tell WHERE three entities just merged.
|
|
60
|
-
await resolveOneBrain(base, token, t, targets.length > 1)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function resolveOneBrain(base, token, brain, labelled) {
|
|
65
|
-
const tag = labelled ? `[${brain.name}] ` : ''
|
|
66
|
-
const qs = `?brain=${encodeURIComponent(brain.orgId)}`
|
|
67
|
-
|
|
68
18
|
// 1. pull the flagged candidate pairs
|
|
69
19
|
let candidates = []
|
|
70
20
|
try {
|
|
71
|
-
const res = await fetchCortex(`${base}/api/resolve-candidates
|
|
21
|
+
const res = await fetchCortex(`${base}/api/resolve-candidates`, { headers: { Authorization: `Bearer ${token}` } })
|
|
72
22
|
if (!res.ok) {
|
|
73
23
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
74
|
-
process.stderr.write(`cortex:
|
|
24
|
+
process.stderr.write(`cortex: resolve-candidates failed — ${d.message}\n`); return
|
|
75
25
|
}
|
|
76
26
|
const j = await res.json().catch(() => ({}))
|
|
77
27
|
candidates = Array.isArray(j.candidates) ? j.candidates : []
|
|
78
|
-
} catch (e) { process.stderr.write(`cortex:
|
|
28
|
+
} catch (e) { process.stderr.write(`cortex: resolve fetch failed — ${e.message}\n`); return }
|
|
79
29
|
|
|
80
|
-
if (!candidates.length) { process.stderr.write(
|
|
81
|
-
process.stderr.write(`cortex:
|
|
30
|
+
if (!candidates.length) { process.stderr.write('cortex: no duplicate candidates to judge\n'); return }
|
|
31
|
+
process.stderr.write(`cortex: judging ${candidates.length} candidate pair(s) locally…\n`)
|
|
82
32
|
|
|
83
33
|
// 2. judge locally on the subscription
|
|
84
34
|
const decisions = judgeCandidates(candidates)
|
|
85
|
-
if (decisions === null) { process.stderr.write(
|
|
35
|
+
if (decisions === null) { process.stderr.write('cortex: judge unavailable (is `claude` on PATH?) — skipping\n'); return }
|
|
86
36
|
|
|
87
|
-
// 3. apply
|
|
37
|
+
// 3. apply judged decisions
|
|
88
38
|
try {
|
|
89
|
-
const res = await fetchCortex(`${base}/api/resolve-apply
|
|
39
|
+
const res = await fetchCortex(`${base}/api/resolve-apply`, {
|
|
90
40
|
method: 'POST',
|
|
91
41
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
92
42
|
body: JSON.stringify({ decisions }),
|
|
93
43
|
})
|
|
94
44
|
if (res.ok) {
|
|
95
45
|
const j = await res.json().catch(() => ({}))
|
|
96
|
-
process.stderr.write(`cortex:
|
|
46
|
+
process.stderr.write(`cortex: dedup — merged ${j.merged ?? 0}, rejected ${j.rejected ?? 0}, skipped ${j.skipped ?? 0}\n`)
|
|
97
47
|
} else {
|
|
98
48
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
99
|
-
process.stderr.write(`cortex:
|
|
49
|
+
process.stderr.write(`cortex: resolve-apply failed — ${d.message}\n`)
|
|
100
50
|
}
|
|
101
|
-
} catch (e) { process.stderr.write(`cortex:
|
|
51
|
+
} catch (e) { process.stderr.write(`cortex: resolve-apply failed — ${e.message}\n`) }
|
|
102
52
|
}
|
|
103
53
|
|
|
104
54
|
// ONE `claude --print` call judges every pair. Returns DedupDecision[] for the apply endpoint, or null
|
package/lib/server.mjs
CHANGED
|
@@ -40,7 +40,7 @@ async function redLinkTriage(BASE, TOKEN, name) {
|
|
|
40
40
|
// file-request tools, and a module-level `fail` would be silently shadowed by it from that point on.
|
|
41
41
|
const toolError = (text) => ({ content: [{ type: 'text', text }], isError: true })
|
|
42
42
|
|
|
43
|
-
// The
|
|
43
|
+
// The Cortex MCP server (stdio). Serves the signed-in employee's scoped org
|
|
44
44
|
// context to their AI assistant. CORTEX_TOKEN identifies the user + org.
|
|
45
45
|
|
|
46
46
|
export async function runServer(version) {
|
|
@@ -48,7 +48,7 @@ export async function runServer(version) {
|
|
|
48
48
|
const BASE = resolveBase(process.env.CORTEX_URL)
|
|
49
49
|
|
|
50
50
|
if (!TOKEN) {
|
|
51
|
-
process.stderr.write('cortex-mcp: CORTEX_TOKEN is required. Get yours from the
|
|
51
|
+
process.stderr.write('cortex-mcp: CORTEX_TOKEN is required. Get yours from the Cortex console → Connect your AI.\n')
|
|
52
52
|
process.exit(1)
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -90,7 +90,7 @@ export async function runServer(version) {
|
|
|
90
90
|
const now = Date.now()
|
|
91
91
|
if (cache && now - cache.ts < 5 * 60 * 1000) return cache.text
|
|
92
92
|
// fetchCortex retries transient infra/5xx; classify turns a failure into an honest message
|
|
93
|
-
// (token vs infra-block vs network) instead of a bare "
|
|
93
|
+
// (token vs infra-block vs network) instead of a bare "Cortex API 403: unknown".
|
|
94
94
|
const res = await fetchCortex(`${BASE}/api/mcp-context`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
95
95
|
if (!res.ok) {
|
|
96
96
|
const body = await res.text()
|
|
@@ -117,7 +117,7 @@ export async function runServer(version) {
|
|
|
117
117
|
server.registerTool(
|
|
118
118
|
'my_context',
|
|
119
119
|
{
|
|
120
|
-
title: 'My
|
|
120
|
+
title: 'My Cortex context',
|
|
121
121
|
description: 'Your current work context from the org. Pass a question to get query-centered session context seeded from the most relevant node and its neighborhood; omit it for the baseline snapshot.',
|
|
122
122
|
inputSchema: { question: z.string().optional().describe('optional opening user question to center the context around') },
|
|
123
123
|
},
|
|
@@ -144,8 +144,8 @@ export async function runServer(version) {
|
|
|
144
144
|
server.registerTool(
|
|
145
145
|
'log_session',
|
|
146
146
|
{
|
|
147
|
-
title: 'Log this session to
|
|
148
|
-
description: 'Persist a CURATED summary of this work session as its durable
|
|
147
|
+
title: 'Log this session to Cortex',
|
|
148
|
+
description: 'Persist a CURATED summary of this work session as its durable Cortex record (authoritative — supersedes the auto-capture hook). Call at session close after composing the summary. Pass sessionId (the Claude Code session id) if you have it so this dedupes with the auto-capture of the same session.',
|
|
149
149
|
inputSchema: {
|
|
150
150
|
summary: z.string().describe('the curated session summary (what was done, decided, left open) — becomes the durable record'),
|
|
151
151
|
project: z.string().optional().describe('project key/name this session worked in'),
|
|
@@ -172,7 +172,7 @@ export async function runServer(version) {
|
|
|
172
172
|
throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
|
|
173
173
|
}
|
|
174
174
|
const j = await res.json().catch(() => ({}))
|
|
175
|
-
return { content: [{ type: 'text', text: `Logged to
|
|
175
|
+
return { content: [{ type: 'text', text: `Logged to Cortex (authoritative): "${j.title ?? title ?? 'session'}" — ${j.inserted ? 'new record' : 'updated existing'}.` }] }
|
|
176
176
|
},
|
|
177
177
|
)
|
|
178
178
|
|
|
@@ -286,6 +286,31 @@ export async function runServer(version) {
|
|
|
286
286
|
},
|
|
287
287
|
)
|
|
288
288
|
|
|
289
|
+
server.registerTool(
|
|
290
|
+
'maintenance_candidates',
|
|
291
|
+
{
|
|
292
|
+
title: 'Review recent project-linked maintenance evidence',
|
|
293
|
+
description: 'Start here when preparing a handoff, status, or next step for a NAMED project. Returns a bounded, RLS-scoped set of recent raw records already linked to that project, before you trust the authored page. These are candidate evidence, not a command to edit: read them, call project_status, then decide whether a material contradiction warrants a minimal accountable correction. Never mutate merely to clear a candidate.',
|
|
294
|
+
inputSchema: {
|
|
295
|
+
project: z.string().describe('the project key from the task, e.g. "checkout-v2"'),
|
|
296
|
+
since_days: z.number().optional().describe('how far back to inspect (1-90 days, default 30)'),
|
|
297
|
+
limit: z.number().optional().describe('max candidate records (1-50, default 20)'),
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
async ({ project, since_days, limit }) => {
|
|
301
|
+
const qs = new URLSearchParams({ project })
|
|
302
|
+
if (since_days != null) qs.set('since_days', String(since_days))
|
|
303
|
+
if (limit != null) qs.set('limit', String(limit))
|
|
304
|
+
const res = await fetchCortex(`${BASE}/api/maintenance/candidates?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
305
|
+
if (!res.ok) {
|
|
306
|
+
const body = await res.text()
|
|
307
|
+
throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
|
|
308
|
+
}
|
|
309
|
+
const { text } = await res.json()
|
|
310
|
+
return { content: [{ type: 'text', text }] }
|
|
311
|
+
},
|
|
312
|
+
)
|
|
313
|
+
|
|
289
314
|
server.registerTool(
|
|
290
315
|
'search_org',
|
|
291
316
|
{
|
|
@@ -354,7 +379,7 @@ export async function runServer(version) {
|
|
|
354
379
|
{
|
|
355
380
|
title: 'Query the local code structure graph (graphify)',
|
|
356
381
|
description:
|
|
357
|
-
'Query a structural code graph for the repo at the CURRENT working directory, built locally by graphify (tree-sitter AST — deterministic, no LLM, no server round-trip; this is LOCAL MACHINE data, not org-shared
|
|
382
|
+
'Query a structural code graph for the repo at the CURRENT working directory, built locally by graphify (tree-sitter AST — deterministic, no LLM, no server round-trip; this is LOCAL MACHINE data, not org-shared Cortex content, and reflects a snapshot of one commit, not live files). Use for MULTI-HOP questions a single grep cannot answer: what calls/imports/depends on X, how A structurally reaches B, or a repo-wide overview (hub/community files). Do NOT use for single-hop lookups (does file X import Y) — grep is faster and always current. Structure only — it knows what imports/calls what, never WHY; read the actual files or authored Cortex pages for intent.',
|
|
358
383
|
inputSchema: {
|
|
359
384
|
action: z.enum(['query', 'path', 'explain']).describe('"query" = open-ended natural-language question (graph traversal); "path" = shortest structural path between two named nodes; "explain" = describe one node and list its direct connections'),
|
|
360
385
|
question: z.string().optional().describe('required for action:"query" — the natural-language question'),
|
|
@@ -419,7 +444,7 @@ export async function runServer(version) {
|
|
|
419
444
|
description:
|
|
420
445
|
'READ the full authored wiki page for one node (project/person/org/you) by its canonical name — every section, every tier you can see. This is how you READ a node; `grep` only LOCATES pages (snippets + their [[links]]), it does not read them. Navigate like a researcher: read the page you need, then FOLLOW its inline [[links]] by calling read_page on each linked name — keep following while the linked pages stay relevant, stop when they do not. You decide how deep to go. Returns only what you are permitted to see.',
|
|
421
446
|
inputSchema: {
|
|
422
|
-
name: z.string().describe('the canonical node name exactly as written (e.g. "
|
|
447
|
+
name: z.string().describe('the canonical node name exactly as written (e.g. "Cortex", "Ben", or a [[link]] target) — identifier links ([[repo:owner/name]]) resolve to their authored HOME + a visible-event count'),
|
|
423
448
|
kind: z.enum(['project', 'person', 'org', 'user']).optional().describe('node kind (default project; pass person/org for people/teams)'),
|
|
424
449
|
expand: z.boolean().optional().describe('identifier names only: also list recent visible timeline events for this identifier (default: home + count)'),
|
|
425
450
|
history: z.boolean().optional().describe('node names only: return the node\'s TIMELINE (events joined via its [[repo:…]] stamps, reverse-chron, viewer-visible) instead of the page body. The page is the present; this is the history.'),
|
|
@@ -593,7 +618,7 @@ export async function runServer(version) {
|
|
|
593
618
|
title: 'See a wiki page\'s edit history',
|
|
594
619
|
description: 'Show the VERSION history of an authored wiki page — every prior version, who changed it and when, newest first. This is how you see "what changed on this page and by whom", and it includes privacy changes (re-tiers). Then use `read_page` with a version to view an old body, or `rollback_page` to restore one. (Distinct from read_page\'s `history: true`, which is the raw event timeline via [[repo:…]] stamps.) RLS-scoped: you see history only for pages you may read.',
|
|
595
620
|
inputSchema: {
|
|
596
|
-
name: z.string().describe('the canonical node name exactly as written (e.g. "
|
|
621
|
+
name: z.string().describe('the canonical node name exactly as written (e.g. "Cortex", "Ben")'),
|
|
597
622
|
kind: z.enum(['project', 'person', 'org', 'user']).optional().describe('node kind (default project)'),
|
|
598
623
|
limit: z.number().int().optional().describe('how many recent versions to show (default 20, max 200)'),
|
|
599
624
|
},
|
|
@@ -719,14 +744,7 @@ export async function runServer(version) {
|
|
|
719
744
|
}
|
|
720
745
|
const out = await res.json().catch(() => null)
|
|
721
746
|
if (!res.ok) return toolError(`Could not roll back "${name}": ${out?.error ?? res.status}`)
|
|
722
|
-
|
|
723
|
-
// erased. Say it on its own line instead of at the tail of `note` — this is the 2026-08-04 W3
|
|
724
|
-
// shape, where empty summaries copied over populated ones destroyed 31 of them under a report
|
|
725
|
-
// that read as success. A rescue the operator does not see is still a silent write.
|
|
726
|
-
const keptLine = out.summaryKept === 'current'
|
|
727
|
-
? `\n\n⚠ That revision had NO summary, so the page's current summary was KEPT rather than erased — check it still describes the restored body, and use set_summary if not.`
|
|
728
|
-
: ''
|
|
729
|
-
return { content: [{ type: 'text', text: `Done — "${name}" ${out.note}. \`read_page "${name}"\` to confirm the current content.${keptLine}` }] }
|
|
747
|
+
return { content: [{ type: 'text', text: `Done — "${name}" ${out.note}. \`read_page "${name}"\` to confirm the current content.` }] }
|
|
730
748
|
},
|
|
731
749
|
)
|
|
732
750
|
|
|
@@ -892,15 +910,13 @@ export async function runServer(version) {
|
|
|
892
910
|
'writing_style',
|
|
893
911
|
{
|
|
894
912
|
title: 'How the user writes (for drafting in their voice)',
|
|
895
|
-
description: 'Returns the user\'s saved writing-style profile so you can DRAFT in their voice (email, message, doc). Call this right before composing anything on their behalf. Self-only — it is always the calling user\'s own profile. If none is saved, it tells you to derive one and save it with set_writing_style.
|
|
896
|
-
inputSchema: {
|
|
897
|
-
brain: z.string().optional().describe('which brain\'s style profile, by name or org id. Unnecessary when you only have one brain; pass the org id when a name matches more than one of yours'),
|
|
898
|
-
},
|
|
913
|
+
description: 'Returns the user\'s saved writing-style profile so you can DRAFT in their voice (email, message, doc). Call this right before composing anything on their behalf. Self-only — it is always the calling user\'s own profile. If none is saved, it tells you to derive one and save it with set_writing_style.',
|
|
914
|
+
inputSchema: {},
|
|
899
915
|
},
|
|
900
|
-
async (
|
|
916
|
+
async () => {
|
|
901
917
|
let res
|
|
902
918
|
try {
|
|
903
|
-
res = await fetchCortex(`${BASE}/api/style
|
|
919
|
+
res = await fetchCortex(`${BASE}/api/style`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
904
920
|
} catch (e) {
|
|
905
921
|
return toolError(`Could not load writing style: ${e.message}`)
|
|
906
922
|
}
|
|
@@ -918,15 +934,12 @@ export async function runServer(version) {
|
|
|
918
934
|
{
|
|
919
935
|
title: 'Save the user\'s writing-style profile',
|
|
920
936
|
description: 'Save (or update) a description of HOW the user writes — tone, sentence rhythm, structure, formatting habits, signature quirks — derived from prose you have seen them write this session. Store the STYLE, never their private content. Self-only: it always updates the calling user\'s own profile. Pass an empty string to clear it.',
|
|
921
|
-
inputSchema: {
|
|
922
|
-
style_md: z.string().describe('a concise markdown description of the user\'s writing voice (tone/structure/quirks), ~1-2 paragraphs'),
|
|
923
|
-
brain: z.string().optional().describe('which brain to save the profile in, by name or org id. A profile is stored per (user, brain), so this is a real choice when you hold several; pass the org id when a name matches more than one of yours'),
|
|
924
|
-
},
|
|
937
|
+
inputSchema: { style_md: z.string().describe('a concise markdown description of the user\'s writing voice (tone/structure/quirks), ~1-2 paragraphs') },
|
|
925
938
|
},
|
|
926
|
-
async ({ style_md
|
|
939
|
+
async ({ style_md }) => {
|
|
927
940
|
let res
|
|
928
941
|
try {
|
|
929
|
-
res = await fetchCortex(`${BASE}/api/style
|
|
942
|
+
res = await fetchCortex(`${BASE}/api/style`, {
|
|
930
943
|
method: 'PUT',
|
|
931
944
|
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
932
945
|
body: JSON.stringify({ style_md }),
|
|
@@ -1164,7 +1177,7 @@ export async function runServer(version) {
|
|
|
1164
1177
|
'list_brain_pages',
|
|
1165
1178
|
{
|
|
1166
1179
|
title: 'List every authored page in one brain',
|
|
1167
|
-
description: 'Enumerate ALL authored pages in ONE brain, by its org id (from my_brains). Unlike my_brains — which ships only a count plus a few sample titles — this returns the FULL page list: one row per node with its kind, validity, tier(s), last-updated date and content hash. Use it to audit a brain, or to VERIFY a brain-to-brain migration — list BOTH brains, diff the page sets, and compare freshness before retiring any original. You can only list a brain you are a member of
|
|
1180
|
+
description: 'Enumerate ALL authored pages in ONE brain, by its org id (from my_brains). Unlike my_brains — which ships only a count plus a few sample titles — this returns the FULL page list: one row per node with its kind, validity, tier(s), last-updated date and content hash. Use it to audit a brain, or to VERIFY a brain-to-brain migration — list BOTH brains, diff the page sets, and compare freshness before retiring any original. You can only list a brain you are a member of; reads never widen past your own brains.',
|
|
1168
1181
|
inputSchema: {
|
|
1169
1182
|
org_id: z.string().describe('the org id of the brain to enumerate (from my_brains)'),
|
|
1170
1183
|
},
|
|
@@ -1200,7 +1213,7 @@ export async function runServer(version) {
|
|
|
1200
1213
|
{
|
|
1201
1214
|
title: 'Create a new brain under your existing account',
|
|
1202
1215
|
description: 'Create a brand-new brain (org/workspace) — a fully independent knowledge graph — under your EXISTING account. No new login, no new email/password: this adds a second membership to the account you are already using. Reads never cross brains; new pages default to your active brain, so use pass `brain` when an operation needs one named',
|
|
1203
|
-
inputSchema: { name: z.string().describe('display name for the new brain, e.g. "
|
|
1216
|
+
inputSchema: { name: z.string().describe('display name for the new brain, e.g. "Cortex Codebase"') },
|
|
1204
1217
|
},
|
|
1205
1218
|
async ({ name }) => {
|
|
1206
1219
|
let res
|
|
@@ -1291,7 +1304,7 @@ export async function runServer(version) {
|
|
|
1291
1304
|
'my_sessions',
|
|
1292
1305
|
{
|
|
1293
1306
|
title: 'My active AI sessions',
|
|
1294
|
-
description: "See what all of YOUR OWN active
|
|
1307
|
+
description: "See what all of YOUR OWN active Cortex/AI sessions are doing right now (working directory + how recently each was active), so you can coordinate across windows/devices. Self-only — only your own sessions, never anyone else's.",
|
|
1295
1308
|
inputSchema: {},
|
|
1296
1309
|
},
|
|
1297
1310
|
async () => {
|
|
@@ -1481,60 +1494,6 @@ export async function runServer(version) {
|
|
|
1481
1494
|
},
|
|
1482
1495
|
)
|
|
1483
1496
|
|
|
1484
|
-
server.registerTool(
|
|
1485
|
-
'replace_variant',
|
|
1486
|
-
{
|
|
1487
|
-
title: 'Move one tier variant\'s body into another, collapsing the node to one page',
|
|
1488
|
-
description: 'DESTRUCTIVE, and the only sanctioned way to fix a FORKED page. When one node exists at two tiers with different bodies, this moves the SOURCE variant\'s body into the TARGET variant\'s slot and DELETES the source, leaving the node with a single page. The source\'s body WINS — this is not `absorb`, where the target survives; in a fork repair the target is usually the damaged page, so mirroring absorb would keep the damage and delete the good copy. Sections are copied as ROWS, never re-derived from text: re-deriving a body from context is exactly what destroyed 258 sections on 2026-07-18 while sincerely reporting "copied verbatim". BEFORE CALLING: read_page the TARGET and pass its version as target_version — it proves you know which body is about to be overwritten, and a stale or guessed value is rejected rather than silently accepted. If the target tier has NO page, do not use this: the slot is free, so set_page_privacy moves the page there cheaply. Both prior bodies are retained in page history and the operation is reversible via page_history/rollback_page. Owner or editor on BOTH variants; an OWNERLESS target may be overwritten only by an org admin.',
|
|
1489
|
-
inputSchema: {
|
|
1490
|
-
kind: z.enum(['project', 'person', 'org', 'user']).describe('the page kind'),
|
|
1491
|
-
name: z.string().optional().describe('the exact page name (or pass `ref` instead — one of the two is required)'),
|
|
1492
|
-
ref: z.string().optional().describe('the page\'s stable id, printed as `ref:` by read_page. PREFER THIS over name when you have it: a ref is unique across brains, so it addresses exactly one page and never needs a brain to disambiguate it.'),
|
|
1493
|
-
source_tier: z.enum(['accessible', 'scoped', 'confidential']).describe('the variant whose BODY WINS and survives. This variant\'s row is then deleted.'),
|
|
1494
|
-
target_tier: z.enum(['accessible', 'scoped', 'confidential']).describe('the OCCUPIED slot the body lands in. This variant\'s current body is DESTROYED (snapshotted to page history first). The surviving page sits at this tier.'),
|
|
1495
|
-
target_version: z.string().describe('REQUIRED — the TARGET page\'s version, from read_page. Proves you know what is being overwritten. Do not retry a rejection blindly; re-read the target and confirm you are replacing what you think you are.'),
|
|
1496
|
-
brain: z.string().optional().describe('only when the same page name exists in more than one of your brains'),
|
|
1497
|
-
},
|
|
1498
|
-
},
|
|
1499
|
-
async ({ kind, name, ref, source_tier, target_tier, target_version, brain }) => {
|
|
1500
|
-
let res
|
|
1501
|
-
try {
|
|
1502
|
-
res = await fetchCortex(`${BASE}/api/brain/replace-variant`, {
|
|
1503
|
-
method: 'POST',
|
|
1504
|
-
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
1505
|
-
body: JSON.stringify({
|
|
1506
|
-
kind, ...(name ? { name } : {}), ...(ref ? { ref } : {}),
|
|
1507
|
-
source_tier, target_tier, target_version, ...(brain ? { brain } : {}),
|
|
1508
|
-
}),
|
|
1509
|
-
})
|
|
1510
|
-
} catch (e) {
|
|
1511
|
-
return toolError(`Could not replace variant: ${e.message}`)
|
|
1512
|
-
}
|
|
1513
|
-
const out = await res.json().catch(() => null)
|
|
1514
|
-
if (!res.ok) {
|
|
1515
|
-
// The engine's rejections carry the remedy in their text (free slot → use set_page_privacy;
|
|
1516
|
-
// hash mismatch → re-read, do not retry blindly). Surface it verbatim rather than paraphrasing.
|
|
1517
|
-
if (out?.error) return toolError(`Could not replace variant: ${out.error}`)
|
|
1518
|
-
const d = classify(res.status, res.headers.get('content-type'), '', res.headers.get('x-vercel-id'))
|
|
1519
|
-
return toolError(`Could not replace variant: ${d.message}`)
|
|
1520
|
-
}
|
|
1521
|
-
if (!out) return { content: [{ type: 'text', text: 'Replace reported success, but the server returned no body — re-read the page before assuming it landed.' }] }
|
|
1522
|
-
// The summary rides along with the body, EXCEPT when the source has none — then the target's is
|
|
1523
|
-
// kept rather than erased. Say so on its own line rather than at the tail of `note`: this is the
|
|
1524
|
-
// 2026-08-04 W3 shape, where an empty summary copied over a populated one destroyed 31 of them
|
|
1525
|
-
// under a report that read as success. A rescue the operator does not see is still a silent write.
|
|
1526
|
-
const rescued = out.summaryRescued
|
|
1527
|
-
? `\n\n⚠ The ${out.moved.from} variant had NO summary. The ${out.moved.to} page's own summary was KEPT rather than overwritten with an empty one — check it still describes the body that just landed, and set_summary if not.`
|
|
1528
|
-
: ''
|
|
1529
|
-
return {
|
|
1530
|
-
content: [{
|
|
1531
|
-
type: 'text',
|
|
1532
|
-
text: `Done — "${out.moved.title}": the ${out.moved.from} body now occupies the ${out.moved.to} page (${out.sections} section${out.sections === 1 ? '' : 's'}), and the ${out.moved.from} variant was removed. The node now has ONE variant. Both prior bodies are retained in page history.${rescued}`,
|
|
1533
|
-
}],
|
|
1534
|
-
}
|
|
1535
|
-
},
|
|
1536
|
-
)
|
|
1537
|
-
|
|
1538
1497
|
server.registerTool(
|
|
1539
1498
|
'grant_page_access',
|
|
1540
1499
|
{
|
|
@@ -1729,7 +1688,7 @@ export async function runServer(version) {
|
|
|
1729
1688
|
'decide_file_request',
|
|
1730
1689
|
{
|
|
1731
1690
|
title: 'Approve or deny a file request',
|
|
1732
|
-
description: 'As the OWNER of a record, approve or deny someone\'s request for its full original. On approve, your
|
|
1691
|
+
description: 'As the OWNER of a record, approve or deny someone\'s request for its full original. On approve, your Cortex desktop app fetches + shares the file. Get request_id from file_requests.',
|
|
1733
1692
|
inputSchema: { request_id: z.string(), decision: z.enum(['approve', 'deny']) },
|
|
1734
1693
|
},
|
|
1735
1694
|
async ({ request_id, decision }) => {
|
|
@@ -1771,7 +1730,7 @@ export async function runServer(version) {
|
|
|
1771
1730
|
},
|
|
1772
1731
|
)
|
|
1773
1732
|
|
|
1774
|
-
// send_imessage — local outbound texting (NOT org intelligence; writes nothing to
|
|
1733
|
+
// send_imessage — local outbound texting (NOT org intelligence; writes nothing to Cortex). Runs on
|
|
1775
1734
|
// this machine via Messages.app. Draft-by-default + recipient allowlist + OOB confirm (D3/D6/D10).
|
|
1776
1735
|
server.registerTool(
|
|
1777
1736
|
'send_imessage',
|
|
@@ -1795,7 +1754,7 @@ export async function runServer(version) {
|
|
|
1795
1754
|
// Two tools the working session uses to AUTHOR its understanding into the org wiki while it's hot:
|
|
1796
1755
|
// authoring_context → the companion call (§3): fetch the visible NAMESPACE + the node-type connection
|
|
1797
1756
|
// rules BEFORE writing, so the page links canonically (the L2 lever).
|
|
1798
|
-
// author → the write (§9 step 3/4): hand
|
|
1757
|
+
// author → the write (§9 step 3/4): hand Cortex a finished page (summary + sections WITH
|
|
1799
1758
|
// inline [[links]]); the server runs the resolution pass + tier-safe 2B write.
|
|
1800
1759
|
|
|
1801
1760
|
server.registerTool(
|
|
@@ -1803,7 +1762,7 @@ export async function runServer(version) {
|
|
|
1803
1762
|
{
|
|
1804
1763
|
title: 'Authoring context (call before author)',
|
|
1805
1764
|
description:
|
|
1806
|
-
'Fetch the scaffolding to author a
|
|
1765
|
+
'Fetch the scaffolding to author a Cortex wiki node: the canonical NAMESPACE (current node names — link to these with the EXACT name inside [[ ]]), any deliberately RETIRED page names and their successors, and the node-type CONNECTION RULES. ALWAYS call this BEFORE `author` so the page links to current knowledge rather than minting synonyms or reviving a retired page. Reference a node in the namespace as [[Name]]; if you reference something real that is NOT in the namespace, still write [[Name]] — that is a red-link marking a node worth creating. If the page IS about a specific code repo or chat channel, also stamp it once — [[repo:owner/name]] or [[channel:name]] (lowercase, no #) — identifier join keys, not page links.',
|
|
1807
1766
|
inputSchema: {
|
|
1808
1767
|
kind: z.enum(['project', 'person', 'org', 'user']).optional().describe('the node type you are about to author (default project)'),
|
|
1809
1768
|
brain: z.string().optional().describe('which brain\'s namespace to describe — pass the SAME brain you will pass to `author`, so the namespace you plan against is the one your write lands in. Unnecessary when you only have one brain.'),
|
|
@@ -1846,7 +1805,7 @@ export async function runServer(version) {
|
|
|
1846
1805
|
'Write your CURRENT understanding of a project/person/org/you into the org wiki as a maintained page. Call `authoring_context` FIRST. Author from your own synthesis of the session — the compiled mental model, not a transcript dump: what it IS, where it stands, dated decisions, open threads, key people. Weave inline [[links]] to other nodes (canonical names from the namespace; red-links for wanted-but-absent nodes). The server re-authorizes the tier and resolves links. CREATES the node if it does not exist yet (project/person/org) — the conversation IS the evidence, so a brand-new entity that surfaced only in this session is authorable on the spot; you do NOT need prior records. Because such a node has nothing external to corroborate it, author it DELIBERATELY: only when you genuinely understand it is a real, distinct entity, and use its exact canonical name so it does not duplicate one already in the namespace (`user` nodes are never created). Use this continuously whenever your understanding of a node meaningfully advanced, and at session end (/log). Authoring is PRE-AUTHORIZED — never ask the user "should I update the page?" before calling this (every edit is versioned + reversible via page_history/rollback_page); update, then briefly report what you updated. ⚠ WHICH BRAIN A NEW PAGE GOES IN IS A CONTENT DECISION, SO MAKE IT FROM THE CONTENT. Only a page that exists in NO brain needs this — an update resolves its brain from the page itself. If you hold more than one brain, the server REFUSES a create it cannot attribute (409 `create_needs_brain`) rather than letting the write pointer decide — the pointer is stale out-of-band state that knows nothing about what you are writing. So call `my_brains` (it returns each brain\'s name, page count and sample titles, which is enough to tell what each one is FOR) and pass `brain` up front; that turns a refused round trip into a single call. State which brain you picked and why in one short line, then proceed — do NOT ask when the answer is obvious from the content. DO ask when it is genuinely ambiguous: brains are a confidentiality boundary, so a page born in the wrong one can expose private work to a teammate, and that is not a filing error you can quietly fix later.',
|
|
1847
1806
|
inputSchema: {
|
|
1848
1807
|
kind: z.enum(['project', 'person', 'org', 'user']).describe('the node type'),
|
|
1849
|
-
name: z.string().describe('the canonical node name — an EXACT existing name from the namespace to update it, or a new name to create the node (project/person/org). e.g. "
|
|
1808
|
+
name: z.string().describe('the canonical node name — an EXACT existing name from the namespace to update it, or a new name to create the node (project/person/org). e.g. "Cortex" or "Theron Peterson"'),
|
|
1850
1809
|
summary: z.string().describe('one-sentence summary of what this is and its current state (may contain [[links]])'),
|
|
1851
1810
|
sections: z.array(z.object({
|
|
1852
1811
|
heading: z.string().describe('e.g. Overview, Current state, Decisions, Open threads, People'),
|
package/lib/setup.mjs
CHANGED
|
@@ -44,7 +44,7 @@ export async function runSetup(argv, version) {
|
|
|
44
44
|
if (!token || token.startsWith('-')) {
|
|
45
45
|
process.stderr.write(
|
|
46
46
|
'Usage: npx @theronap/cortex-mcp setup <CORTEX_TOKEN>\n\n' +
|
|
47
|
-
'Get your token from the
|
|
47
|
+
'Get your token from the Cortex console → Connect your AI.\n'
|
|
48
48
|
)
|
|
49
49
|
process.exit(1)
|
|
50
50
|
}
|
|
@@ -63,7 +63,7 @@ export async function runSetup(argv, version) {
|
|
|
63
63
|
|
|
64
64
|
const log = (m) => process.stdout.write(m + '\n')
|
|
65
65
|
log('')
|
|
66
|
-
log('
|
|
66
|
+
log('Cortex setup — wiring your AI assistant…')
|
|
67
67
|
|
|
68
68
|
// ── 1. MCP server in ~/.claude.json ──────────────────────────────────────
|
|
69
69
|
try {
|
|
@@ -83,7 +83,7 @@ export async function runSetup(argv, version) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// ── 1b. MCP server in Codex (~/.codex/config.toml), only if Codex is installed ──
|
|
86
|
-
// Codex gets the same
|
|
86
|
+
// Codex gets the same Cortex context tools as Claude Code. Non-fatal: a Codex hiccup must
|
|
87
87
|
// never block the primary Claude wiring.
|
|
88
88
|
const codexDir = join(home, '.codex')
|
|
89
89
|
if (existsSync(codexDir)) {
|
|
@@ -157,7 +157,7 @@ export async function runSetup(argv, version) {
|
|
|
157
157
|
// ── 4. Self-verify — writing config proves "files written", NOT "connection works".
|
|
158
158
|
// Actually call the API so a bad/expired token is caught HERE, not 40 minutes into debugging.
|
|
159
159
|
log('')
|
|
160
|
-
log('Verifying your token against
|
|
160
|
+
log('Verifying your token against Cortex…')
|
|
161
161
|
const health = await checkToken(token, base)
|
|
162
162
|
if (health.ok) {
|
|
163
163
|
const n = health.projectCount
|
|
@@ -169,8 +169,8 @@ export async function runSetup(argv, version) {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
log('')
|
|
172
|
-
log('⟳ IMPORTANT: fully quit and reopen Claude Code to load the
|
|
173
|
-
log(' Then your AI sees your
|
|
172
|
+
log('⟳ IMPORTANT: fully quit and reopen Claude Code to load the Cortex server.')
|
|
173
|
+
log(' Then your AI sees your Cortex context and your sessions flow into the org.')
|
|
174
174
|
log(' Re-check anytime: npx -y @theronap/cortex-mcp doctor')
|
|
175
175
|
log(` Console: ${base}`)
|
|
176
176
|
log('')
|
|
@@ -191,11 +191,11 @@ export async function runRepair(version) {
|
|
|
191
191
|
const token = readWiredToken()
|
|
192
192
|
if (!token) {
|
|
193
193
|
process.stderr.write(
|
|
194
|
-
'No existing
|
|
194
|
+
'No existing Cortex token found in ~/.claude.json or ~/.codex/config.toml.\n' +
|
|
195
195
|
'Run setup once with your token: npx -y @theronap/cortex-mcp setup <YOUR_TOKEN>\n',
|
|
196
196
|
)
|
|
197
197
|
process.exit(1)
|
|
198
198
|
}
|
|
199
|
-
process.stdout.write('
|
|
199
|
+
process.stdout.write('Cortex repair — re-running setup with your existing token at this version…\n')
|
|
200
200
|
await runSetup([token], version)
|
|
201
201
|
}
|