@theronap/cortex-mcp 0.9.5 → 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/lib/capture.mjs +13 -0
- package/lib/server.mjs +52 -1
- package/package.json +1 -1
- package/skills/log/SKILL.md +26 -14
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/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.
|