@theronap/cortex-mcp 0.9.4 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cortex-mcp.mjs +9 -1
- package/lib/capture.mjs +13 -0
- package/lib/server.mjs +52 -1
- package/lib/setup.mjs +32 -0
- package/package.json +1 -1
- package/skills/log/SKILL.md +26 -14
package/bin/cortex-mcp.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* Get your token from the Cortex console → Connect your AI.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
const VERSION = '0.9.
|
|
20
|
+
const VERSION = '0.9.5'
|
|
21
21
|
const cmd = process.argv[2]
|
|
22
22
|
const rest = process.argv.slice(3)
|
|
23
23
|
|
|
@@ -35,6 +35,7 @@ if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
|
35
35
|
`sessions flow into the org automatically. Restart Claude Code after.\n\n` +
|
|
36
36
|
`Subcommands:\n` +
|
|
37
37
|
` setup <token> wire MCP server + capture hook into ~/.claude config\n` +
|
|
38
|
+
` repair re-run setup at the latest version using your existing token (no token needed)\n` +
|
|
38
39
|
` doctor live health check — confirm your token works (no restart needed)\n` +
|
|
39
40
|
` status one-line connected/not-connected check (used by the SessionStart hook)\n` +
|
|
40
41
|
` skills install/repair the managed Cortex skills (also wired by setup)\n` +
|
|
@@ -58,6 +59,13 @@ if (cmd === 'setup') {
|
|
|
58
59
|
await runSetup(rest, VERSION)
|
|
59
60
|
const { closeFetch } = await import('../lib/diagnose.mjs')
|
|
60
61
|
await closeFetch()
|
|
62
|
+
} else if (cmd === 'repair' || cmd === 'update') {
|
|
63
|
+
// Re-run setup at THIS version using the already-wired token (no token arg needed). Fixes a
|
|
64
|
+
// machine set up with an older version: re-pins MCP + hooks, reinstalls skills to the flat path.
|
|
65
|
+
const { runRepair } = await import('../lib/setup.mjs')
|
|
66
|
+
await runRepair(VERSION)
|
|
67
|
+
const { closeFetch } = await import('../lib/diagnose.mjs')
|
|
68
|
+
await closeFetch()
|
|
61
69
|
} else if (cmd === 'doctor') {
|
|
62
70
|
const { runDoctor } = await import('../lib/doctor.mjs')
|
|
63
71
|
process.exitCode = await runDoctor()
|
package/lib/capture.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'fs'
|
|
2
2
|
import { homedir } from 'os'
|
|
3
3
|
import { resolve } from 'path'
|
|
4
|
+
import { createHash } from 'crypto'
|
|
4
5
|
import { fetchCortex, classify, resolveBase } from './diagnose.mjs'
|
|
5
6
|
|
|
6
7
|
// Project name from the hook cwd — cross-platform. Windows hooks send backslash paths,
|
|
@@ -75,6 +76,16 @@ export async function runCapture() {
|
|
|
75
76
|
|
|
76
77
|
if (!transcript && !hook.session_id) { process.stderr.write('cortex: empty session, skipping\n'); return }
|
|
77
78
|
|
|
79
|
+
// T11: digest node refs surfaced to this session (stashed by the MCP server's my_context, keyed by
|
|
80
|
+
// cwd). Forwarded as hydratedFrom so the materializer excludes this session from the digests it
|
|
81
|
+
// consumed (feedback-loop guard). Best-effort + inert when absent (digest flag off / no my_context).
|
|
82
|
+
let hydratedFrom = []
|
|
83
|
+
try {
|
|
84
|
+
const key = createHash('sha1').update(hook.cwd || process.cwd()).digest('hex').slice(0, 16)
|
|
85
|
+
const parsed = JSON.parse(readFileSync(resolve(homedir(), '.cortex', 'brain-refs', `${key}.json`), 'utf8'))
|
|
86
|
+
if (Array.isArray(parsed.refs) && Date.now() - (parsed.ts ?? 0) < 12 * 3600 * 1000) hydratedFrom = parsed.refs
|
|
87
|
+
} catch { /* none — guard stays a no-op */ }
|
|
88
|
+
|
|
78
89
|
let res
|
|
79
90
|
try {
|
|
80
91
|
res = await fetchCortex(`${base}/api/ingest`, {
|
|
@@ -87,6 +98,8 @@ export async function runCapture() {
|
|
|
87
98
|
transcript,
|
|
88
99
|
title: `Worked in ${repo}`,
|
|
89
100
|
payload: { session_id: hook.session_id, cwd: hook.cwd },
|
|
101
|
+
captureSource: 'hook', // T8: fallback writer — never clobbers a cortex-log ('skill') record
|
|
102
|
+
...(hydratedFrom.length ? { hydratedFrom } : {}),
|
|
90
103
|
}),
|
|
91
104
|
})
|
|
92
105
|
} catch (e) {
|
package/lib/server.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { z } from 'zod'
|
|
|
4
4
|
import { writeFileSync, mkdirSync } from 'fs'
|
|
5
5
|
import { homedir } from 'os'
|
|
6
6
|
import { join } from 'path'
|
|
7
|
+
import { createHash } from 'crypto'
|
|
7
8
|
import { fetchCortex, classify, resolveBase } from './diagnose.mjs'
|
|
8
9
|
|
|
9
10
|
// The Cortex MCP server (stdio). Serves the signed-in employee's scoped org
|
|
@@ -30,8 +31,19 @@ export async function runServer(version) {
|
|
|
30
31
|
const body = await res.text()
|
|
31
32
|
throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
|
|
32
33
|
}
|
|
33
|
-
const { context } = await res.json()
|
|
34
|
+
const { context, brainRefs } = await res.json()
|
|
34
35
|
cache = { text: context, ts: now }
|
|
36
|
+
// T11: stash the digest node refs surfaced this fetch so capture.mjs can forward them as
|
|
37
|
+
// hydrated_from at the session's ingest (feedback-loop guard). Keyed by cwd so the matching
|
|
38
|
+
// session picks them up. Best-effort + inert when brainRefs is empty (digest flag off).
|
|
39
|
+
try {
|
|
40
|
+
if (Array.isArray(brainRefs) && brainRefs.length) {
|
|
41
|
+
const dir = join(homedir(), '.cortex', 'brain-refs')
|
|
42
|
+
mkdirSync(dir, { recursive: true })
|
|
43
|
+
const key = createHash('sha1').update(process.cwd()).digest('hex').slice(0, 16)
|
|
44
|
+
writeFileSync(join(dir, `${key}.json`), JSON.stringify({ refs: brainRefs, ts: now }))
|
|
45
|
+
}
|
|
46
|
+
} catch { /* best-effort — never break context serving */ }
|
|
35
47
|
return context
|
|
36
48
|
}
|
|
37
49
|
|
|
@@ -60,6 +72,45 @@ export async function runServer(version) {
|
|
|
60
72
|
},
|
|
61
73
|
)
|
|
62
74
|
|
|
75
|
+
// T8: cortex-log's authoritative writer. The skill composes a curated summary, then calls this to
|
|
76
|
+
// persist it AS the durable record (capture_source='skill'). The ingest conflict guard ensures the
|
|
77
|
+
// auto-capture hook never clobbers it. Pass the SAME sessionId the hook uses so the two dedupe onto
|
|
78
|
+
// one record; without it the curated log still lands as its own authoritative record.
|
|
79
|
+
server.registerTool(
|
|
80
|
+
'log_session',
|
|
81
|
+
{
|
|
82
|
+
title: 'Log this session to Cortex',
|
|
83
|
+
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.',
|
|
84
|
+
inputSchema: {
|
|
85
|
+
summary: z.string().describe('the curated session summary (what was done, decided, left open) — becomes the durable record'),
|
|
86
|
+
project: z.string().optional().describe('project key/name this session worked in'),
|
|
87
|
+
title: z.string().optional().describe('short title for the session'),
|
|
88
|
+
sessionId: z.string().optional().describe('the Claude Code session id (dedupes with the auto-capture hook of the same session)'),
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
async ({ summary, project, title, sessionId }) => {
|
|
92
|
+
const res = await fetchCortex(`${BASE}/api/ingest`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
source: 'claude-code',
|
|
97
|
+
captureSource: 'skill',
|
|
98
|
+
summary,
|
|
99
|
+
...(project ? { project } : {}),
|
|
100
|
+
...(title ? { title } : {}),
|
|
101
|
+
...(sessionId ? { sessionId } : {}),
|
|
102
|
+
payload: { via: 'log_session' },
|
|
103
|
+
}),
|
|
104
|
+
})
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
const body = await res.text()
|
|
107
|
+
throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
|
|
108
|
+
}
|
|
109
|
+
const j = await res.json().catch(() => ({}))
|
|
110
|
+
return { content: [{ type: 'text', text: `Logged to Cortex (authoritative): "${j.title ?? title ?? 'session'}" — ${j.inserted ? 'new record' : 'updated existing'}.` }] }
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
|
|
63
114
|
server.registerTool(
|
|
64
115
|
'session_context',
|
|
65
116
|
{
|
package/lib/setup.mjs
CHANGED
|
@@ -215,3 +215,35 @@ export async function runSetup(argv, version) {
|
|
|
215
215
|
log(` Console: ${base}`)
|
|
216
216
|
log('')
|
|
217
217
|
}
|
|
218
|
+
|
|
219
|
+
// Find the token already wired on this machine, so `repair` can re-run setup without re-pasting it.
|
|
220
|
+
// Checks Claude's config first, then Codex's config.toml.
|
|
221
|
+
export function readWiredToken() {
|
|
222
|
+
try {
|
|
223
|
+
const cfg = readJson(join(homedir(), '.claude.json'))
|
|
224
|
+
const t = cfg?.mcpServers?.cortex?.env?.CORTEX_TOKEN
|
|
225
|
+
if (t) return t
|
|
226
|
+
} catch { /* fall through */ }
|
|
227
|
+
try {
|
|
228
|
+
const toml = readFileSync(join(homedir(), '.codex', 'config.toml'), 'utf8')
|
|
229
|
+
const m = toml.match(/\[mcp_servers\.cortex\.env\][\s\S]*?CORTEX_TOKEN\s*=\s*"([^"]+)"/)
|
|
230
|
+
if (m) return m[1]
|
|
231
|
+
} catch { /* fall through */ }
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// `repair`: re-run the FULL setup at THIS version using the already-wired token. The one-command fix
|
|
236
|
+
// for a machine set up with an older version (e.g. when skills were installed to the old nested path,
|
|
237
|
+
// or the hooks are pinned to a stale version). No token argument needed.
|
|
238
|
+
export async function runRepair(version) {
|
|
239
|
+
const token = readWiredToken()
|
|
240
|
+
if (!token) {
|
|
241
|
+
process.stderr.write(
|
|
242
|
+
'No existing Cortex token found in ~/.claude.json or ~/.codex/config.toml.\n' +
|
|
243
|
+
'Run setup once with your token: npx -y @theronap/cortex-mcp setup <YOUR_TOKEN>\n',
|
|
244
|
+
)
|
|
245
|
+
process.exit(1)
|
|
246
|
+
}
|
|
247
|
+
process.stdout.write('Cortex repair — re-running setup with your existing token at this version…\n')
|
|
248
|
+
await runSetup([token], version)
|
|
249
|
+
}
|
package/package.json
CHANGED
package/skills/log/SKILL.md
CHANGED
|
@@ -6,11 +6,20 @@ description: Close out a work session into Cortex — summarize what happened, c
|
|
|
6
6
|
> **Cortex-managed skill.** This file is installed and kept up to date by Cortex. Local edits are
|
|
7
7
|
> restored on the next session (a backup of your version is saved alongside). Don't rely on changes here.
|
|
8
8
|
|
|
9
|
+
## Model: session-primary, daily-derived
|
|
10
|
+
|
|
11
|
+
The **session is the primary atomic unit** — one session = one durable Cortex record (via
|
|
12
|
+
`log_session`, keyed by `sessionId`), which is also the per-record privacy unit (`set_record_privacy`
|
|
13
|
+
is per record). Any "what happened today / this week" view is a **derived rollup** over those session
|
|
14
|
+
records, never a separately-authored primary. This mirrors records(atomic) → digests(derived); the
|
|
15
|
+
personal `/log` skill follows the same shape against the local brain.
|
|
16
|
+
|
|
9
17
|
## When to use
|
|
10
18
|
|
|
11
|
-
At the end of a Claude Code session, or after finishing a meaningful phase of work. Cortex
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
At the end of a Claude Code session, or after finishing a meaningful phase of work. Cortex keeps a
|
|
20
|
+
background auto-capture as a fallback, but this skill is the *authoritative* close-out: it composes a
|
|
21
|
+
clean, structured summary and persists THAT as the session's durable record (superseding the
|
|
22
|
+
auto-capture's raw-transcript re-derivation).
|
|
14
23
|
|
|
15
24
|
## Inputs
|
|
16
25
|
|
|
@@ -22,15 +31,18 @@ No arguments. Read the conversation context.
|
|
|
22
31
|
the projects, files, and people involved.
|
|
23
32
|
2. **Surface org-relevant signal** — blockers, decisions, handoffs, and anyone you coordinated with.
|
|
24
33
|
These are the things a teammate or manager would want to know without reading the whole transcript.
|
|
25
|
-
3. **
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
3. **Persist it as the durable record** — call the `log_session` MCP tool with your curated `summary`
|
|
35
|
+
(plus `project`, and the Claude Code `sessionId` if you know it). This writes YOUR summary as the
|
|
36
|
+
session's authoritative Cortex record (`capture_source='skill'`). The background auto-capture is a
|
|
37
|
+
fallback and will not overwrite it; passing the same `sessionId` the auto-capture uses dedupes them
|
|
38
|
+
onto one record. This — not the raw-transcript re-derivation — is the canonical record going forward.
|
|
39
|
+
4. **Confirm + flag privacy** — `log_session` returns a confirmation; if it errors, tell the user to
|
|
40
|
+
run `npx -y @theronap/cortex-mcp doctor`. If any record from this session should be confidential,
|
|
41
|
+
note it so the user can mark it (`set_record_privacy`). Default is org-visible under access rules.
|
|
30
42
|
|
|
31
43
|
## Output
|
|
32
44
|
|
|
33
|
-
|
|
45
|
+
After calling `log_session`, show a short structured summary:
|
|
34
46
|
|
|
35
47
|
```markdown
|
|
36
48
|
## Session summary
|
|
@@ -39,12 +51,12 @@ A short structured summary:
|
|
|
39
51
|
**Decisions:** decision 1; decision 2
|
|
40
52
|
**Open / blocked:** anything unresolved or waiting on someone
|
|
41
53
|
**Coordinated with:** people involved
|
|
42
|
-
**
|
|
54
|
+
**Logged:** ✅ persisted as the session's record (authoritative) (or ⚠ log_session errored — run doctor)
|
|
43
55
|
```
|
|
44
56
|
|
|
45
57
|
## Safety rules
|
|
46
58
|
|
|
47
|
-
- This skill
|
|
48
|
-
and never changes access on a record without the user explicitly asking.
|
|
49
|
-
- Raw session text stays on this machine — only the summary-grade record reaches the org, scoped
|
|
50
|
-
access rules.
|
|
59
|
+
- This skill summarizes and persists the session's record (via `log_session`). It never sends external
|
|
60
|
+
messages, never deletes anything, and never changes access on a record without the user explicitly asking.
|
|
61
|
+
- Raw session text stays on this machine — only the curated summary-grade record reaches the org, scoped
|
|
62
|
+
by access rules.
|