@theronap/cortex-mcp 0.9.51 → 0.9.53
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/lib/diagnose.mjs +20 -0
- package/lib/install.mjs +4 -2
- package/lib/server.mjs +46 -2
- package/lib/session_key.mjs +13 -0
- package/lib/setup.mjs +9 -7
- package/package.json +1 -1
- package/skills/log/SKILL.md +23 -0
package/lib/diagnose.mjs
CHANGED
|
@@ -36,6 +36,26 @@ export function readWiredToken() {
|
|
|
36
36
|
return null
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Pure: pull the cortex-mcp dist-tag / version out of a wired command line. Exported for tests.
|
|
40
|
+
// Returns 'latest' | 'stable' | a version string (e.g. '0.9.4') | null (no cortex-mcp spec present).
|
|
41
|
+
export function parseDistTag(line) {
|
|
42
|
+
const m = String(line || '').match(/@theronap\/cortex-mcp@(latest|stable|[0-9][^\s"']*)/)
|
|
43
|
+
return m ? m[1] : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Read the dist-tag / version this machine is currently wired to (from the ~/.claude.json cortex MCP
|
|
47
|
+
// command), so a re-run of setup/repair can PRESERVE an intentional channel instead of forcing @stable
|
|
48
|
+
// every time — that silently knocks a dogfooder on @latest back to the pilot channel (bit Theron
|
|
49
|
+
// 2026-07-24). Returns 'latest' | 'stable' | a version string | null (nothing wired yet).
|
|
50
|
+
export function wiredDistTag() {
|
|
51
|
+
try {
|
|
52
|
+
const cfg = JSON.parse(readFileSync(join(homedir(), '.claude.json'), 'utf8'))
|
|
53
|
+
const c = cfg?.mcpServers?.cortex
|
|
54
|
+
return parseDistTag([c?.command, ...(c?.args ?? [])].filter(Boolean).join(' '))
|
|
55
|
+
} catch { /* fall through */ }
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
39
59
|
// The production alias — exempt from Vercel Deployment Protection.
|
|
40
60
|
export const CANONICAL_BASE = 'https://cortex-console.vercel.app'
|
|
41
61
|
|
package/lib/install.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { homedir } from 'node:os'
|
|
|
7
7
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs'
|
|
8
8
|
import { join, dirname } from 'node:path'
|
|
9
9
|
import { ADAPTERS, resolveEditors } from './editors/index.mjs'
|
|
10
|
-
import { checkToken, resolveBase, readWiredToken } from './diagnose.mjs'
|
|
10
|
+
import { checkToken, resolveBase, readWiredToken, wiredDistTag } from './diagnose.mjs'
|
|
11
11
|
|
|
12
12
|
const PKG = '@theronap/cortex-mcp'
|
|
13
13
|
export const MANIFEST_PATH = join(homedir(), '.cortex', 'editors.json')
|
|
@@ -68,7 +68,9 @@ export function writeManifest(manifest, { path = MANIFEST_PATH } = {}) {
|
|
|
68
68
|
export async function runInstall(argv, version) {
|
|
69
69
|
const { token: argToken, editor } = parseInstallArgs(argv)
|
|
70
70
|
const token = argToken || readWiredToken()
|
|
71
|
-
|
|
71
|
+
// Preserve an intentional @latest (dogfood) pin across re-runs; fresh installs + existing @stable
|
|
72
|
+
// get @stable. See wiredDistTag — forcing @stable here silently demoted a dogfooder (2026-07-24).
|
|
73
|
+
const spec = wiredDistTag() === 'latest' ? `${PKG}@latest` : `${PKG}@stable`
|
|
72
74
|
const home = homedir()
|
|
73
75
|
const log = (m) => process.stdout.write(m + '\n')
|
|
74
76
|
|
package/lib/server.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { homedir } from 'os'
|
|
|
6
6
|
import { join } from 'path'
|
|
7
7
|
import { createHash, randomUUID } from 'crypto'
|
|
8
8
|
import { fetchCortex, classify, resolveBase, setSessionKey } from './diagnose.mjs'
|
|
9
|
+
import { resolveSessionKey } from './session_key.mjs'
|
|
9
10
|
import { runSendImessage } from './imessage_send.mjs'
|
|
10
11
|
import { formatGrepHits } from './grep_cli.mjs'
|
|
11
12
|
import { renderTriage } from './red_link_triage.mjs'
|
|
@@ -42,7 +43,18 @@ export async function runServer(version) {
|
|
|
42
43
|
// Self active-sessions (ADR-0017): one MCP process = one AI session. A stable per-process key + the
|
|
43
44
|
// working dir identify this session; we heartbeat /api/session-ping while alive so the user can see
|
|
44
45
|
// all THEIR sessions via my_sessions. Self-only on the server; best-effort (never breaks serving).
|
|
45
|
-
|
|
46
|
+
//
|
|
47
|
+
// PGL-21 fix: "one MCP process = one AI session" is an assumption, not a guarantee — the HOST
|
|
48
|
+
// (Claude Code) can restart this stdio process mid-conversation (reconnects, tool-loading events),
|
|
49
|
+
// and a fresh randomUUID() on every restart silently orphaned the PRIOR process's session-scoped
|
|
50
|
+
// write-pointer row (session_write_pointers): an explicit set_active_brain(scope:'session') would
|
|
51
|
+
// stop taking effect one restart later, falling back to the account pointer with no visible cause
|
|
52
|
+
// — reproduced live 2026-07-27. Claude Code sets CLAUDE_CODE_SESSION_ID for the lifetime of one
|
|
53
|
+
// logical conversation across any number of subprocess restarts, so prefer it as the session key;
|
|
54
|
+
// fall back to a fresh randomUUID() for any other MCP host that doesn't set it (unchanged behavior
|
|
55
|
+
// there — no host that never had continuity loses anything). Bonus: my_sessions / session_presence
|
|
56
|
+
// (also keyed on SESSION_KEY, ADR-0017) stop fragmenting one conversation into several "sessions" too.
|
|
57
|
+
const SESSION_KEY = resolveSessionKey(process.env, randomUUID)
|
|
46
58
|
// ADR-0020 Stage 2: stamp this key on every outbound request (fetchCortex injects it) so writes
|
|
47
59
|
// resolve THIS session's brain pointer. Must be set BEFORE the first fetchCortex call below —
|
|
48
60
|
// otherwise the opening requests of a session would silently resolve by the account pointer.
|
|
@@ -498,7 +510,7 @@ export async function runServer(version) {
|
|
|
498
510
|
const head = `[${t.tier}${day(t.updated_at) ? ` · authored ${day(t.updated_at)}` : ''}${t.validity && t.validity !== 'current' ? ` · ${t.validity}` : ''}${versionLine}]`
|
|
499
511
|
return [head, t.summary, secs].filter(Boolean).join('\n')
|
|
500
512
|
})
|
|
501
|
-
let footer = `— Follow any [[links]] above with read_page to go deeper.\n— If you hold fresher FIRST-HAND truth than this page — something you established THIS session (ran the command, made the change, hold the artifact) that a section here gets concretely wrong — re-author just those sections now with \`author\` (any node you are permitted to edit; concise; a material contradiction, not merely extra detail). Pass this page's \`version\` as base_version when you re-author — if the save comes back stale, the page changed under you: re-read, then re-author. Reading a stale page you can fix IS the trigger to fix it. Re-authoring is pre-authorized — do NOT ask the user before updating (edits are versioned + reversible via page_history/rollback_page); update, then briefly report it.`
|
|
513
|
+
let footer = `— Follow any [[links]] above with read_page to go deeper.\n— If you hold fresher FIRST-HAND truth than this page — something you established THIS session (ran the command, made the change, hold the artifact) that a section here gets concretely wrong — re-author just those sections now with \`author\` (any node you are permitted to edit; concise; a material contradiction, not merely extra detail). Pass this page's \`version\` as base_version when you re-author — if the save comes back stale, the page changed under you: re-read, then re-author. Reading a stale page you can fix IS the trigger to fix it. Re-authoring is pre-authorized — do NOT ask the user before updating (edits are versioned + reversible via page_history/rollback_page); update, then briefly report it.\n— Citing code? Use a SYMBOL and file (\`formConnections\` in \`web/app/api/ingest/route.ts\`), never a line number — line numbers drift with every commit above them. And cite only what you opened THIS session; re-emitting a reference you read on another page is how a stale claim gains a second source and starts looking corroborated.`
|
|
502
514
|
// slice 4: when the page carries identifier stamps, the history projection is one flag away.
|
|
503
515
|
const allBody = m.tiers.flatMap((t) => (t.sections ?? []).map((s) => s.body)).join('\n')
|
|
504
516
|
const stamps = [...new Set((allBody.match(/\[\[repo:[a-z0-9][a-z0-9-]*\/[a-z0-9_.-]+\]\]/gi) ?? []).map((s) => s.toLowerCase()))]
|
|
@@ -808,6 +820,38 @@ export async function runServer(version) {
|
|
|
808
820
|
},
|
|
809
821
|
)
|
|
810
822
|
|
|
823
|
+
server.registerTool(
|
|
824
|
+
'my_day',
|
|
825
|
+
{
|
|
826
|
+
title: 'My daily log (chronological)',
|
|
827
|
+
description: "Your OWN activity for a day, composed chronologically from that day's records of every kind — sessions, meetings, comms, docs, notes. The \"what did I do today\" rollup: a derived view over records (nothing new is stored), self-scoped to you, each item tagged with its kind. Defaults to today in your local timezone. Pass `date` (YYYY-MM-DD) for another day, or `days` for a trailing window (e.g. days:7 for the week). Confidential records are segregated into their own block.",
|
|
828
|
+
inputSchema: {
|
|
829
|
+
date: z.string().optional().describe('the day to roll up, YYYY-MM-DD (default: today in your local timezone)'),
|
|
830
|
+
days: z.number().optional().describe('trailing window ending on `date` — e.g. 7 for the past week (default 1, max 31)'),
|
|
831
|
+
},
|
|
832
|
+
},
|
|
833
|
+
async ({ date, days }) => {
|
|
834
|
+
// Resolve the caller's local timezone + today client-side (the MCP server runs on the user's
|
|
835
|
+
// machine) so the day boundary matches their wall clock, not the server's UTC.
|
|
836
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
|
|
837
|
+
const today = new Date().toLocaleDateString('en-CA', { timeZone: tz }) // en-CA → YYYY-MM-DD
|
|
838
|
+
const qs = new URLSearchParams({ date: date || today, tz })
|
|
839
|
+
if (days != null) qs.set('days', String(days))
|
|
840
|
+
let res
|
|
841
|
+
try {
|
|
842
|
+
res = await fetchCortex(`${BASE}/api/records/daily?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
843
|
+
} catch (e) {
|
|
844
|
+
return { content: [{ type: 'text', text: `Could not build daily log: ${e.message}` }] }
|
|
845
|
+
}
|
|
846
|
+
if (!res.ok) {
|
|
847
|
+
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
848
|
+
return { content: [{ type: 'text', text: `Could not build daily log: ${d.message}` }] }
|
|
849
|
+
}
|
|
850
|
+
const { text } = await res.json()
|
|
851
|
+
return { content: [{ type: 'text', text }] }
|
|
852
|
+
},
|
|
853
|
+
)
|
|
854
|
+
|
|
811
855
|
server.registerTool(
|
|
812
856
|
'my_sessions',
|
|
813
857
|
{
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// PGL-21: derive the AI session's stable identity key. Pure — see session_key.test.mjs.
|
|
2
|
+
//
|
|
3
|
+
// Claude Code sets CLAUDE_CODE_SESSION_ID for the lifetime of one logical conversation, surviving
|
|
4
|
+
// any number of MCP subprocess restarts (reconnects, tool-loading events) within it. Prefer it so a
|
|
5
|
+
// session-scoped set_active_brain (ADR-0020 Stage 2) keeps pointing at the right brain across a
|
|
6
|
+
// restart instead of silently falling back to the account pointer on a brand-new randomUUID() —
|
|
7
|
+
// reproduced live 2026-07-27: an explicit set_active_brain(scope:'session') stopped taking effect
|
|
8
|
+
// one restart later, with no visible cause, because the new process minted an unrelated session key
|
|
9
|
+
// with no session_write_pointers row of its own. Any other MCP host that doesn't set the var gets
|
|
10
|
+
// today's unchanged per-process-random behavior.
|
|
11
|
+
export function resolveSessionKey(env, randomUUID) {
|
|
12
|
+
return env.CLAUDE_CODE_SESSION_ID || randomUUID()
|
|
13
|
+
}
|
package/lib/setup.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from 'fs'
|
|
2
2
|
import { homedir } from 'os'
|
|
3
3
|
import { join, dirname } from 'path'
|
|
4
|
-
import { checkToken, resolveBase, readWiredToken } from './diagnose.mjs'
|
|
4
|
+
import { checkToken, resolveBase, readWiredToken, wiredDistTag } from './diagnose.mjs'
|
|
5
5
|
import { installSkills } from './skills.mjs'
|
|
6
6
|
// Pure config-merge functions live in the editor adapters; setup imports (and re-exports) THE SAME
|
|
7
7
|
// functions the `cortex install` path uses, so both write byte-identical config.
|
|
@@ -48,12 +48,14 @@ export async function runSetup(argv, version) {
|
|
|
48
48
|
)
|
|
49
49
|
process.exit(1)
|
|
50
50
|
}
|
|
51
|
-
// Wire
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
51
|
+
// Wire a moving dist-tag — NOT a frozen version. A bare spec lets npx reuse a stale cached build; a
|
|
52
|
+
// frozen `@x.y.z` freezes the machine on that version forever (the 0.9.5→0.9.6 freeze that stranded a
|
|
53
|
+
// pilot install). A tag is re-resolved by npx against the registry, so machines pick up promoted
|
|
54
|
+
// releases on next launch without re-running setup. Promote a validated build with:
|
|
55
|
+
// npm dist-tag add @theronap/cortex-mcp@<version> stable
|
|
56
|
+
// PRESERVE an intentional @latest (dogfood) pin across re-runs (repair/setup) — otherwise this
|
|
57
|
+
// silently knocks a dogfooder back to @stable. Fresh installs and existing @stable get @stable.
|
|
58
|
+
const spec = wiredDistTag() === 'latest' ? `${PKG}@latest` : `${PKG}@stable`
|
|
57
59
|
const base = resolveBase(process.env.CORTEX_URL)
|
|
58
60
|
const home = homedir()
|
|
59
61
|
const claudeJson = join(home, '.claude.json')
|
package/package.json
CHANGED
package/skills/log/SKILL.md
CHANGED
|
@@ -52,6 +52,28 @@ No arguments. Read the conversation context.
|
|
|
52
52
|
docs are pending, follow the `cortex-author-docs` skill (author each into its page, then
|
|
53
53
|
`docs-scan --mark`). Specs/plans written to disk this session must not die on disk — a spec IS
|
|
54
54
|
a page. If no roots are registered or nothing is pending, skip silently.
|
|
55
|
+
7. **Reconcile & verify (prove the sweep — don't trust it).** Step 5 relies on your in-the-moment
|
|
56
|
+
judgment of "what advanced"; this step closes the loop so nothing is silently missed and no stale
|
|
57
|
+
write slips through. Before printing the Output:
|
|
58
|
+
a. **Enumerate what you touched** — from the transcript, list the concrete entities this session
|
|
59
|
+
advanced: the project(s), notable files/artifacts, and the people you coordinated with. Derive
|
|
60
|
+
this checklist from what actually *happened*, not from what you remember authoring — the whole
|
|
61
|
+
point is to catch the node you forgot.
|
|
62
|
+
b. **Assert one outcome per entity** — every item gets exactly `authored [[Page]]` **or**
|
|
63
|
+
`skipped — <reason>` (e.g. "no material change", "not a node", "already current"). Nothing may be
|
|
64
|
+
left unaccounted for. If an entity that genuinely advanced has neither, `author` it now (Step 5).
|
|
65
|
+
c. **Verify the writes landed and are right** — for each page you claim you authored, `read_page` it
|
|
66
|
+
(or check `page_history`) and confirm both: (i) **the change applied** — an `author` that returned
|
|
67
|
+
"no change" when you *intended* an update means it did NOT land (stale `base_version`, wrong
|
|
68
|
+
brain/namespace, or nothing actually differed) — re-check rather than assume; and (ii) **the page
|
|
69
|
+
reflects THIS session's first-hand findings**, not a prior you copied forward. This is the
|
|
70
|
+
read-after-write half of the read-before-write rule — the guard against laundering stale priors
|
|
71
|
+
into the wiki. Fix a page you can edit; `set_page_validity` on one you can't. (Cross-brain note:
|
|
72
|
+
`read_page` can resolve a page in another brain that `author` won't write — if a "verify" read
|
|
73
|
+
looks right but your write reported no-op, run `my_brains` / check `authoring_context` before
|
|
74
|
+
trusting the read.)
|
|
75
|
+
Carry the tally into the Output. If any intended write did not land, say so — never report a clean
|
|
76
|
+
sweep you didn't confirm.
|
|
55
77
|
|
|
56
78
|
## Output
|
|
57
79
|
|
|
@@ -66,6 +88,7 @@ After calling `log_session`, show a short structured summary:
|
|
|
66
88
|
**Coordinated with:** people involved
|
|
67
89
|
**Logged:** ✅ persisted as the session's record (authoritative) (or ⚠ log_session errored — run doctor)
|
|
68
90
|
**Wiki authored:** [[Node A]], [[Node B]] — pages updated (or "— nothing advanced this session")
|
|
91
|
+
**Reconciled:** N touched → M authored, K skipped (reason each); writes verified ✅ (or ⚠ <what didn't land>)
|
|
69
92
|
```
|
|
70
93
|
|
|
71
94
|
## Safety rules
|