@theronap/cortex-mcp 0.9.12 → 0.9.14

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 CHANGED
@@ -4,6 +4,20 @@ import { resolve } from 'path'
4
4
  import { createHash } from 'crypto'
5
5
  import { fetchCortex, classify, resolveBase } from './diagnose.mjs'
6
6
  import { extractSession } from './edge_extract.mjs'
7
+ import { extractTyped } from './extract_typed.mjs'
8
+
9
+ // Fetch the node-type registry (global + this org's custom types) — the catalog the typed extractor
10
+ // produces against. Best-effort: null on any failure (typed extraction is then skipped, never blocks capture).
11
+ async function fetchRegistry(base, token) {
12
+ try {
13
+ const res = await fetchCortex(`${base}/api/node-types`, { headers: { Authorization: `Bearer ${token}` } })
14
+ if (!res.ok) return null
15
+ const j = await res.json().catch(() => ({}))
16
+ return Array.isArray(j.types) ? j.types : null
17
+ } catch {
18
+ return null
19
+ }
20
+ }
7
21
 
8
22
  // Project name from the hook cwd — cross-platform. Windows hooks send backslash paths,
9
23
  // and splitting on '/' alone turned the WHOLE path into one garbage project slug
@@ -112,6 +126,19 @@ export async function runCapture() {
112
126
  ? { ...common, summary: extracted.summary, people: extracted.people, entities: extracted.namedEntities }
113
127
  : { ...common, transcript }
114
128
 
129
+ // Parallel typed extraction (OPT-IN via CORTEX_TYPED): registry-driven typed notes, sent ALONGSIDE the
130
+ // people/entities above. The server's typedNotes receiver persists them additively. Off by default so it
131
+ // never adds a 2nd `claude` call / latency until verified; flip to default once typed ≥ the blob path.
132
+ if (process.env.CORTEX_TYPED && transcript) {
133
+ try {
134
+ const registry = await fetchRegistry(base, token)
135
+ if (registry?.length) {
136
+ const typed = extractTyped(transcript, registry)
137
+ if (typed?.notes?.length) ingestBody.typedNotes = typed.notes
138
+ }
139
+ } catch { /* best-effort — never block capture */ }
140
+ }
141
+
115
142
  let res
116
143
  try {
117
144
  res = await fetchCortex(`${base}/api/ingest`, {
@@ -6,7 +6,7 @@ import { spawnSync } from 'child_process'
6
6
  // both session people AND the entity catalogue down; extracting here keeps the engine on `claude -p`
7
7
  // (the standing preference) and removes that dependency. Node-native (no bun) to match capture.mjs.
8
8
  //
9
- // edgeSafeEnv() strips EVERY ANTHROPIC_* var + CLAUDE_CODE_OAUTH_TOKEN before spawning, so the call
9
+ // edgeSafeEnv() strips EVERY ANTHROPIC_* var (but KEEPS CLAUDE_CODE_OAUTH_TOKEN the headless
10
10
  // runs on the file-based subscription login and a stray ANTHROPIC_BASE_URL (e.g. a keytunnel proxy)
11
11
  // can't reroute "free, local" inference through a billed proxy or ship raw text off-machine. The
12
12
  // spawn also sets CORTEX_SUMMARIZING=1 so the headless session's own Stop hook no-ops (runCapture
@@ -21,7 +21,12 @@ export function edgeSafeEnv(base = process.env, extra = {}) {
21
21
  const env = {}
22
22
  for (const [k, v] of Object.entries(base)) {
23
23
  if (v === undefined) continue
24
- if (k.startsWith('ANTHROPIC_') || k === 'CLAUDE_CODE_OAUTH_TOKEN') continue
24
+ // Strip billed/proxy routing only (ANTHROPIC_API_KEY metered, ANTHROPIC_BASE_URL keytunnel,
25
+ // ANTHROPIC_AUTH_TOKEN/CUSTOM_HEADERS proxy) so edge inference runs on the SUBSCRIPTION, never a
26
+ // metered/proxy path. KEEP CLAUDE_CODE_OAUTH_TOKEN — it IS the headless subscription credential
27
+ // (from `claude setup-token`); stripping it leaves `claude --print` with no usable auth → 401.
28
+ // (The earlier "subscription is file-based" assumption was wrong on macOS — headless needs this token.)
29
+ if (k.startsWith('ANTHROPIC_')) continue
25
30
  env[k] = v
26
31
  }
27
32
  return { ...env, ...extra }
@@ -0,0 +1,68 @@
1
+ import { spawnSync } from 'child_process'
2
+ import { edgeSafeEnv } from './edge_extract.mjs'
3
+
4
+ // Typed extraction (synthesis slice 2) — registry-driven. Given a transcript + the node_types registry,
5
+ // the LLM emits TYPED NOTES (one per real thing that happened / was mentioned) conforming to the type
6
+ // definitions, instead of the old loose {summary, people, namedEntities} blob. Runs locally on the
7
+ // subscription (edgeSafeEnv strips ANTHROPIC_*). PARALLEL to edge_extract.mjs — does NOT replace it yet;
8
+ // built to be tested in isolation, then wired into capture once proven (slice 2b).
9
+ //
10
+ // Per the ontology (cortex-node-ontology-spec): programmatic fields are filled by the caller from source
11
+ // metadata BEFORE this runs; here the LLM fills only what the text supports, self-flags llm_filled fields,
12
+ // and uses "unknown" for required fields it can't determine (→ red-link downstream). It may also COIN a
13
+ // new type (open registry) when nothing fits.
14
+
15
+ function typeCard(t) {
16
+ const fields = (arr) => (Array.isArray(arr) ? arr : []).map((f) => `${f.field}${f.note ? ` (${f.note})` : ''}`).join(', ')
17
+ return `• ${t.name} [${t.layer}]${t.applies_to?.length ? ` applies_to:${t.applies_to.join('/')}` : ''}\n` +
18
+ ` required: ${fields(t.required_fields) || '—'}\n` +
19
+ ` optional: ${fields(t.optional_fields) || '—'}\n` +
20
+ ` how: ${t.extraction_instructions}`
21
+ }
22
+
23
+ export function extractTyped(transcript, registry, opts = {}) {
24
+ const text = (transcript ?? '').trim()
25
+ if (!text || !Array.isArray(registry) || !registry.length) return null
26
+ const catalog = registry.map(typeCard).join('\n')
27
+ const prompt =
28
+ 'You are building an organization knowledge graph from a work session. Using ONLY the node/note types ' +
29
+ 'in the CATALOG below, emit the typed notes this session supports — one per real thing that happened ' +
30
+ '(actions, decisions, communications) or entity referenced (people, projects, etc.).\n\n' +
31
+ 'RULES:\n' +
32
+ '- Fill a field ONLY if the text supports it. If a REQUIRED field is unknowable, set it to "unknown".\n' +
33
+ '- List the fields YOU inferred (vs. ones obviously given) in "llm_filled".\n' +
34
+ '- Reference other things by name (e.g. actors:["Jane Doe"]) — do not invent ids.\n' +
35
+ '- NEVER invent numbers (amounts, values, metrics) — "unknown" if not explicitly stated.\n' +
36
+ '- Be conservative: only emit a note for something genuinely present. Empty array is fine.\n' +
37
+ '- If something important fits NO type, you may coin one: add it to "new_types" with ' +
38
+ '{name, layer, required_fields:[{field}], why}.\n\n' +
39
+ 'Return ONLY minified JSON: {"notes":[{"type","fields":{...},"llm_filled":[...]}],"new_types":[...]}.\n\n' +
40
+ '--- CATALOG ---\n' + catalog + '\n\n--- SESSION ---\n' + text.slice(0, 12000) + '\n--- END ---'
41
+ try {
42
+ const r = spawnSync(
43
+ 'claude',
44
+ ['--print', '--model', opts.model ?? process.env.CORTEX_SUMMARY_MODEL ?? 'claude-haiku-4-5', prompt],
45
+ { env: edgeSafeEnv(process.env, { CORTEX_SUMMARIZING: '1' }), encoding: 'utf8', timeout: 120_000, maxBuffer: 8 * 1024 * 1024 },
46
+ )
47
+ if (r.status !== 0 || !r.stdout) return null
48
+ return parseTyped(r.stdout)
49
+ } catch {
50
+ return null
51
+ }
52
+ }
53
+
54
+ export function parseTyped(out) {
55
+ if (!out) return null
56
+ const start = out.indexOf('{')
57
+ const end = out.lastIndexOf('}')
58
+ if (start < 0 || end <= start) return null
59
+ try {
60
+ const o = JSON.parse(out.slice(start, end + 1))
61
+ return {
62
+ notes: Array.isArray(o.notes) ? o.notes : [],
63
+ new_types: Array.isArray(o.new_types) ? o.new_types : [],
64
+ }
65
+ } catch {
66
+ return null
67
+ }
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.12",
3
+ "version": "0.9.14",
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": {