@theronap/cortex-mcp 0.9.27 → 0.9.29

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.
@@ -46,7 +46,6 @@ if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
46
46
  ` doctor live health check — confirm your token works (no restart needed)\n` +
47
47
  ` status one-line connected/not-connected check (used by the SessionStart hook)\n` +
48
48
  ` skills install/repair the managed Cortex skills (also wired by setup)\n` +
49
- ` materialize [--drain] build brain pages on your Claude subscription (claude -p); --drain clears the backlog\n` +
50
49
  ` snapshot-context save the exact startup context Cortex served to a local snapshot\n` +
51
50
  ` capture Stop-hook capturer (invoked by Claude Code)\n` +
52
51
  ` ingest-folder <path> ingest a local markdown folder as your authored records\n` +
@@ -98,12 +97,9 @@ if (cmd === 'setup') {
98
97
  const { closeFetch } = await import('../lib/diagnose.mjs')
99
98
  await closeFetch()
100
99
  } else if (cmd === 'materialize') {
101
- // Brain-page summarizer on the SUBSCRIPTION (claude -p): claim authorized digest jobs, summarize
102
- // locally, submit the text for the server to write. `--drain` clears the whole backlog.
103
- const { runMaterializeCli } = await import('../lib/materialize.mjs')
104
- process.exitCode = await runMaterializeCli(rest)
105
- const { closeFetch } = await import('../lib/diagnose.mjs')
106
- await closeFetch()
100
+ // EXCISED 2026-07-02 (legacy-materializer incident): the digest pipeline deleted live-authored
101
+ // pages. Pages come from live authoring now; this stub keeps old hook invocations harmless.
102
+ process.stderr.write('cortex-mcp: `materialize` was removed pages come from live authoring (the `author` tool + /log). Nothing to do.\n')
107
103
  } else if (cmd === 'ingest-folder') {
108
104
  const { runIngestFolder } = await import('../lib/ingest_folder.mjs')
109
105
  await runIngestFolder(rest)
package/lib/capture.mjs CHANGED
@@ -5,7 +5,6 @@ import { createHash } from 'crypto'
5
5
  import { fetchCortex, classify, resolveBase } from './diagnose.mjs'
6
6
  import { extractSession } from './edge_extract.mjs'
7
7
  import { extractTyped } from './extract_typed.mjs'
8
- import { runMaterialize } from './materialize.mjs'
9
8
  import { redactSecrets } from './redact.mjs'
10
9
 
11
10
  // Fetch the node-type registry (global + this org's custom types) — the catalog the typed extractor
@@ -166,11 +165,7 @@ export async function runCapture() {
166
165
  process.stderr.write(`cortex: ingest failed — ${d.message}\n`)
167
166
  }
168
167
 
169
- // Keystone fix (b): ingest just marked this session's nodes dirty server-side. Summarize the freshly
170
- // dirtied pages on the SUBSCRIPTION (claude -p) so the brain stays current without the dead server
171
- // API. OPT-IN (CORTEX_MATERIALIZE) + bounded so it never adds unbounded latency to session stop;
172
- // `cortex-mcp materialize --drain` is the bulk/backlog path. Best-effort: never blocks capture.
173
- if (process.env.CORTEX_MATERIALIZE) {
174
- try { await runMaterialize({ max: 3 }) } catch { /* best-effort */ }
175
- }
168
+ // The post-capture edge-materialize batch (CORTEX_MATERIALIZE runMaterialize) was EXCISED
169
+ // 2026-07-02 with the legacy materializer: its server pipeline deleted live-authored pages whose
170
+ // record-hashes drifted. Pages come from live authoring (the `author` tool + /log sweep) now.
176
171
  }
package/lib/server.mjs CHANGED
@@ -252,10 +252,35 @@ export async function runServer(version) {
252
252
  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'),
253
253
  kind: z.enum(['project', 'person', 'org', 'user']).optional().describe('node kind (default project; pass person/org for people/teams)'),
254
254
  expand: z.boolean().optional().describe('identifier names only: also list recent visible timeline events for this identifier (default: home + count)'),
255
+ 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.'),
255
256
  },
256
257
  },
257
- async ({ name, kind, expand }) => {
258
+ async ({ name, kind, expand, history }) => {
258
259
  const k = kind ?? 'project'
260
+ // PER-NODE TIMELINE (slice 4): history = the projection over the node's identifier stamps.
261
+ if (history && !/^[a-z][a-z0-9_-]*:.+/i.test(name)) {
262
+ try {
263
+ const qs = new URLSearchParams({ kind: k, key: name })
264
+ const r = await fetchCortex(`${BASE}/api/node/timeline?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
265
+ if (r.status === 404) return { content: [{ type: 'text', text: `No ${k} named "${name}" — cannot project a timeline.` }] }
266
+ if (!r.ok) {
267
+ const d = classify(r.status, r.headers.get('content-type'), await r.text(), r.headers.get('x-vercel-id'))
268
+ return { content: [{ type: 'text', text: `Could not read the timeline for "${name}": ${d.message}` }] }
269
+ }
270
+ const t = await r.json()
271
+ if (!t.identifiers?.length) {
272
+ return { content: [{ type: 'text', text: `"${name}" carries no identifier stamps yet — no history joins. Stamp its page with [[repo:owner/name]] (what it identifies) and events will accrue here.` }] }
273
+ }
274
+ const lines = [`# ${name} — node timeline (history; the page is the present)`]
275
+ lines.push(`Joins: ${t.identifiers.map((i) => `[[${i.id}]] · ${i.visibleCount} visible`).join(' | ')}${t.incomplete ? ' (partial — one join failed to read)' : ''}`)
276
+ for (const e of t.events) lines.push(`- ${String(e.occurredAt).slice(0, 10)} · ${e.source} · ${e.summary}`)
277
+ if (!t.events.length) lines.push('(no events visible to you yet on these joins)')
278
+ if (t.siblings?.length) lines.push(`Sibling homes (share a stamp — bridges, not history): ${t.siblings.map((s) => `"${s.title}"`).join(', ')}`)
279
+ return { content: [{ type: 'text', text: lines.join('\n') }] }
280
+ } catch (e) {
281
+ return { content: [{ type: 'text', text: `Could not read the timeline for "${name}": ${e.message}` }] }
282
+ }
283
+ }
259
284
  // IDENTIFIER RESOLUTION (slice 3): an identifier-shaped name is a JOIN KEY, not a page — resolve
260
285
  // it to its authored home + a viewer-honest event count instead of 404ing. Loose shape-detect here;
261
286
  // the server enforces the strict canonical law (identifiers.ts) and 400s malformed forms with an
@@ -311,7 +336,12 @@ export async function runServer(version) {
311
336
  const head = `[${t.tier}${day(t.updated_at) ? ` · authored ${day(t.updated_at)}` : ''}${t.validity && t.validity !== 'current' ? ` · ${t.validity}` : ''}${t.version ? `\nversion: ${t.version}` : ''}]`
312
337
  return [head, t.summary, secs].filter(Boolean).join('\n')
313
338
  })
314
- const 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.`
339
+ 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.`
340
+ // slice 4: when the page carries identifier stamps, the history projection is one flag away.
341
+ // (client-side shape check — the acknowledged registry copy of identifiers.ts)
342
+ const allBody = page.tiers.flatMap((t) => (t.sections ?? []).map((s) => s.body)).join('\n')
343
+ const stamps = [...new Set((allBody.match(/\[\[repo:[a-z0-9][a-z0-9-]*\/[a-z0-9_.-]+\]\]/gi) ?? []).map((s) => s.toLowerCase()))]
344
+ if (stamps.length) footer += `\n— This page carries ${stamps.join(', ')} — \`read_page "${name}"\` with history: true for its event timeline (page = present, timeline = history).`
315
345
  return { content: [{ type: 'text', text: `# ${page.title ?? name} (full authored page)\n\n${blocks.join('\n\n---\n\n')}\n\n${footer}` }] }
316
346
  },
317
347
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.27",
3
+ "version": "0.9.29",
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": {
@@ -1,117 +0,0 @@
1
- import { spawnSync } from 'child_process'
2
- import { fetchCortex, resolveBase } from './diagnose.mjs'
3
- import { edgeSafeEnv } from './edge_extract.mjs'
4
-
5
- // Edge materialize worker (keystone fix "b"): the brain-page summarizer moved OFF the dead server-side
6
- // Anthropic API onto the user's Claude SUBSCRIPTION (`claude -p`), mirroring edge extraction + edge
7
- // relationship inference. The SERVER still owns gather + tier-authorization + the write
8
- // (/api/brain/materialize/{claim,submit}); this worker ONLY runs the LLM step. It claims a batch of
9
- // jobs the user is authorized to summarize, runs `claude --print` per (node, tier), and POSTs the RAW
10
- // model text back for the server to parse + validate + write. The edge is never trusted: it cannot
11
- // pick a node, choose a tier, or shape a section — it only turns a server-handed corpus into text.
12
- //
13
- // Best-effort throughout: a tier the edge can't summarize is simply not submitted and stays queued for
14
- // a later run. Guarded by CORTEX_SUMMARIZING so it never recurses through the Stop-hook capturer.
15
-
16
- const SUMMARY_MODEL = process.env.CORTEX_SUMMARY_MODEL ?? 'claude-haiku-4-5'
17
-
18
- // ONE `claude --print` call → the raw model text for a single tier's digest prompt. Subscription auth
19
- // via edgeSafeEnv (keeps CLAUDE_CODE_OAUTH_TOKEN, strips billed/proxy ANTHROPIC_*). null on any failure.
20
- // The server returns a separate system + user prompt; claude --print takes one prompt, so we prepend
21
- // the system instructions (same as edge_extract puts its instructions in the prompt body).
22
- function summarizeTier(tier) {
23
- const prompt = `${tier.system}\n\n${tier.user}`
24
- try {
25
- const r = spawnSync(
26
- 'claude',
27
- ['--print', '--model', SUMMARY_MODEL, prompt],
28
- {
29
- env: edgeSafeEnv(process.env, { CORTEX_SUMMARIZING: '1' }),
30
- encoding: 'utf8',
31
- timeout: 120_000,
32
- maxBuffer: 8 * 1024 * 1024,
33
- },
34
- )
35
- if (r.status !== 0 || !r.stdout) return null
36
- return r.stdout.trim() || null
37
- } catch {
38
- return null
39
- }
40
- }
41
-
42
- async function claimBatch(base, token, max) {
43
- const res = await fetchCortex(`${base}/api/brain/materialize/claim`, {
44
- method: 'POST',
45
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
46
- body: JSON.stringify({ max }),
47
- })
48
- if (!res.ok) throw new Error(`claim HTTP ${res.status}`)
49
- const j = await res.json().catch(() => ({}))
50
- return Array.isArray(j.jobs) ? j.jobs : []
51
- }
52
-
53
- async function submitNode(base, token, kind, ref, results) {
54
- const res = await fetchCortex(`${base}/api/brain/materialize/submit`, {
55
- method: 'POST',
56
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
57
- body: JSON.stringify({ kind, ref, results }),
58
- })
59
- if (!res.ok) throw new Error(`submit HTTP ${res.status}`)
60
- return res.json().catch(() => ({}))
61
- }
62
-
63
- // Claim → summarize → submit. opts.drain loops until the queue is empty (clears a backlog); otherwise
64
- // it does ONE bounded batch (the freshness path called after a capture). Returns a tally.
65
- export async function runMaterialize(opts = {}) {
66
- const log = opts.log ?? ((m) => process.stderr.write(`${m}\n`))
67
- const token = process.env.CORTEX_TOKEN
68
- if (!token) { log('cortex: CORTEX_TOKEN not set, skipping materialize'); return { built: 0, batches: 0 } }
69
- // Never run inside the summarizer subprocess (it sets CORTEX_SUMMARIZING) — would recurse.
70
- if (process.env.CORTEX_SUMMARIZING) return { built: 0, batches: 0 }
71
-
72
- const base = resolveBase(process.env.CORTEX_URL)
73
- const max = Math.min(Math.max(Number(opts.max) || 6, 1), 12)
74
- const maxBatches = opts.drain ? (Number(opts.maxBatches) || 50) : 1
75
-
76
- let built = 0
77
- let batches = 0
78
- for (let i = 0; i < maxBatches; i++) {
79
- let jobs
80
- try {
81
- jobs = await claimBatch(base, token, max)
82
- } catch (e) {
83
- log(`cortex: materialize claim failed — ${e.message}`)
84
- break
85
- }
86
- if (!jobs.length) break
87
- batches++
88
- for (const job of jobs) {
89
- const results = []
90
- for (const tier of job.tiers ?? []) {
91
- const text = summarizeTier(tier)
92
- if (text) results.push({ tier: tier.tier, text })
93
- }
94
- if (!results.length) { log(`cortex: ${job.kind} "${job.name}" — summarizer produced nothing, leaving queued`); continue }
95
- try {
96
- const out = await submitNode(base, token, job.kind, job.ref, results)
97
- const n = out.built ?? 0
98
- built += n
99
- log(`cortex: materialized ${job.kind} "${job.name}" — ${n} tier${n === 1 ? '' : 's'}${out.settled ? '' : ' (partial)'}`)
100
- } catch (e) {
101
- log(`cortex: materialize submit failed for ${job.kind} "${job.name}" — ${e.message}`)
102
- }
103
- }
104
- }
105
- if (opts.drain) log(`cortex: materialize drain complete — ${built} tier-page(s) built across ${batches} batch(es)`)
106
- return { built, batches }
107
- }
108
-
109
- // CLI entry: `cortex-mcp materialize [--drain] [--max N]`. --drain clears the whole backlog now;
110
- // without it, one bounded batch. Always exits 0 (best-effort maintenance, never a hard failure).
111
- export async function runMaterializeCli(args = []) {
112
- const drain = args.includes('--drain')
113
- const mi = args.indexOf('--max')
114
- const max = mi >= 0 ? Number(args[mi + 1]) : undefined
115
- await runMaterialize({ drain, max })
116
- return 0
117
- }