@theronap/cortex-mcp 0.9.5 → 0.9.7

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.
@@ -17,7 +17,13 @@
17
17
  * Get your token from the Cortex console → Connect your AI.
18
18
  */
19
19
 
20
- const VERSION = '0.9.5'
20
+ import { readFileSync } from 'node:fs'
21
+ import { fileURLToPath } from 'node:url'
22
+ import { dirname, join } from 'node:path'
23
+ // VERSION = package.json, the single source of truth. NEVER hardcode it: a stale constant here is
24
+ // exactly what shipped 0.9.6 as "0.9.5", so `setup` pinned the wrong version into every config and
25
+ // no amount of cache-clearing/@version could fix an install. Derive it so it can never drift again.
26
+ const VERSION = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8')).version
21
27
  const cmd = process.argv[2]
22
28
  const rest = process.argv.slice(3)
23
29
 
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
@@ -70,9 +70,12 @@ export async function runSetup(argv, version) {
70
70
  )
71
71
  process.exit(1)
72
72
  }
73
- // Pin the wired commands to the installed version so the config can't later run a stale
74
- // cached build (npx may reuse a cached older version for an unpinned spec).
75
- const spec = version ? `${PKG}@${version}` : PKG
73
+ // Wire the moving `stable` dist-tag NOT a frozen version. A bare spec lets npx reuse a stale
74
+ // cached build; a frozen `@x.y.z` freezes the machine on that version forever (the 0.9.5→0.9.6
75
+ // freeze that stranded a pilot install). A tag is re-resolved by npx against the registry, so
76
+ // machines pick up promoted releases on next launch without re-running setup. Promote a
77
+ // validated build with: npm dist-tag add @theronap/cortex-mcp@<version> stable
78
+ const spec = `${PKG}@stable`
76
79
  const base = resolveBase(process.env.CORTEX_URL)
77
80
  const home = homedir()
78
81
  const claudeJson = join(home, '.claude.json')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "description": "Connect your AI assistant to Cortex — your org's projects, activity, gaps, and directives, scoped to you.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 already
12
- captures your sessions automatically in the background — this skill is the *deliberate* close-out: it
13
- produces a clean, structured summary and confirms the org received it.
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. **Confirm capture** — check the Cortex MCP is connected (`my_context` returns your context). Your
26
- session flows to the org automatically at session end via the capture hook; if `my_context` errors,
27
- tell the user their session may not be captured and to run `npx -y @theronap/cortex-mcp doctor`.
28
- 4. **Flag privacy** if any record from this session should be confidential, note it so the user can
29
- mark it (`set_record_privacy`). Default is org-visible under access rules.
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
- A short structured summary:
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
- **Capture:** ✅ flowing to Cortex (or ⚠ not connected — run doctor)
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 only summarizes and reports. It never sends external messages, never deletes anything,
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 by
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.