@theronap/cortex-mcp 0.9.11 → 0.9.12

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.
@@ -90,6 +90,12 @@ if (cmd === 'setup') {
90
90
  await runCapture()
91
91
  const { closeFetch } = await import('../lib/diagnose.mjs')
92
92
  await closeFetch()
93
+ } else if (cmd === 'resolve') {
94
+ // Entity identity dedup: judge the server-flagged fuzzy duplicate pairs locally via `claude -p`.
95
+ const { runResolve } = await import('../lib/resolve.mjs')
96
+ await runResolve()
97
+ const { closeFetch } = await import('../lib/diagnose.mjs')
98
+ await closeFetch()
93
99
  } else if (cmd === 'ingest-folder') {
94
100
  const { runIngestFolder } = await import('../lib/ingest_folder.mjs')
95
101
  await runIngestFolder(rest)
@@ -0,0 +1,103 @@
1
+ import { spawnSync } from 'child_process'
2
+ import { fetchCortex, resolveBase, classify } from './diagnose.mjs'
3
+ import { edgeSafeEnv } from './edge_extract.mjs'
4
+
5
+ // `cortex-mcp resolve` — the JUDGE half of entity identity dedup (Grey harvest). The server FLAGS the
6
+ // fuzzy band of near-duplicate non-person entities (GET /api/resolve-candidates); this command judges
7
+ // each pair LOCALLY via `claude -p` (the engine preference — the server never calls the metered API)
8
+ // and pushes decisions back (POST /api/resolve-apply): confident-same → entity_merges, else → rejected
9
+ // so the pair never re-flags. Conservative by construction. Always exits cleanly.
10
+
11
+ export async function runResolve() {
12
+ // recursion guard (we spawn `claude --print`; if its Stop hook fires capture, that no-ops on this flag)
13
+ if (process.env.CORTEX_SUMMARIZING) { process.stderr.write('cortex: summarizer subprocess, skipping\n'); return }
14
+ const token = process.env.CORTEX_TOKEN
15
+ if (!token) { process.stderr.write('cortex: CORTEX_TOKEN not set, skipping\n'); return }
16
+ const base = resolveBase(process.env.CORTEX_URL)
17
+
18
+ // 1. pull the flagged candidate pairs
19
+ let candidates = []
20
+ try {
21
+ const res = await fetchCortex(`${base}/api/resolve-candidates`, { headers: { Authorization: `Bearer ${token}` } })
22
+ if (!res.ok) {
23
+ const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
24
+ process.stderr.write(`cortex: resolve-candidates failed — ${d.message}\n`); return
25
+ }
26
+ const j = await res.json().catch(() => ({}))
27
+ candidates = Array.isArray(j.candidates) ? j.candidates : []
28
+ } catch (e) { process.stderr.write(`cortex: resolve fetch failed — ${e.message}\n`); return }
29
+
30
+ if (!candidates.length) { process.stderr.write('cortex: no duplicate candidates to judge\n'); return }
31
+ process.stderr.write(`cortex: judging ${candidates.length} candidate pair(s) locally…\n`)
32
+
33
+ // 2. judge locally on the subscription
34
+ const decisions = judgeCandidates(candidates)
35
+ if (decisions === null) { process.stderr.write('cortex: judge unavailable (is `claude` on PATH?) — skipping\n'); return }
36
+
37
+ // 3. apply judged decisions
38
+ try {
39
+ const res = await fetchCortex(`${base}/api/resolve-apply`, {
40
+ method: 'POST',
41
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ decisions }),
43
+ })
44
+ if (res.ok) {
45
+ const j = await res.json().catch(() => ({}))
46
+ process.stderr.write(`cortex: dedup — merged ${j.merged ?? 0}, rejected ${j.rejected ?? 0}, skipped ${j.skipped ?? 0}\n`)
47
+ } else {
48
+ const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
49
+ process.stderr.write(`cortex: resolve-apply failed — ${d.message}\n`)
50
+ }
51
+ } catch (e) { process.stderr.write(`cortex: resolve-apply failed — ${e.message}\n`) }
52
+ }
53
+
54
+ // ONE `claude --print` call judges every pair. Returns DedupDecision[] for the apply endpoint, or null
55
+ // if the CLI is unavailable. Every judged pair (merge AND non-merge) is returned so non-merges get
56
+ // recorded as rejected and never re-flag.
57
+ function judgeCandidates(candidates) {
58
+ const list = candidates.map((c, i) =>
59
+ `[${i}] kind=${c.kind}\n` +
60
+ ` A: "${c.aName}" (e.g. record: ${c.aSample ?? 'n/a'}; ${c.aMentions} mentions)\n` +
61
+ ` B: "${c.bName}" (e.g. record: ${c.bSample ?? 'n/a'}; ${c.bMentions} mentions)\n` +
62
+ ` shared records: ${c.sharedRecords}; name similarity: ${Number(c.sim).toFixed(2)}`,
63
+ ).join('\n')
64
+ const prompt =
65
+ 'You are deduplicating an organization knowledge graph. For each candidate pair below, decide ' +
66
+ 'whether A and B are the SAME real-world entity (a spelling/abbreviation/variant of ONE thing) or ' +
67
+ 'DIFFERENT things that merely have similar names. Be CONSERVATIVE: only merge when confident; when ' +
68
+ 'in doubt, do NOT merge. Same kind only.\n' +
69
+ 'Return ONLY a minified JSON array — one object per pair: ' +
70
+ '{"i":<index>,"merge":<true|false>,"confidence":<0..1>,"reason":"<short>"}.\n\n' + list
71
+ try {
72
+ const r = spawnSync(
73
+ 'claude',
74
+ ['--print', '--model', process.env.CORTEX_SUMMARY_MODEL ?? 'claude-haiku-4-5', prompt],
75
+ { env: edgeSafeEnv(process.env, { CORTEX_SUMMARIZING: '1' }), encoding: 'utf8', timeout: 120_000, maxBuffer: 4 * 1024 * 1024 },
76
+ )
77
+ if (r.status !== 0 || !r.stdout) return null
78
+ return parseDecisions(r.stdout, candidates)
79
+ } catch {
80
+ return null
81
+ }
82
+ }
83
+
84
+ function parseDecisions(out, candidates) {
85
+ const start = out.indexOf('[')
86
+ const end = out.lastIndexOf(']')
87
+ if (start < 0 || end <= start) return []
88
+ let arr
89
+ try { arr = JSON.parse(out.slice(start, end + 1)) } catch { return [] }
90
+ if (!Array.isArray(arr)) return []
91
+ const decisions = []
92
+ for (const d of arr) {
93
+ const c = candidates[Number(d?.i)]
94
+ if (!c) continue
95
+ decisions.push({
96
+ aId: c.aId, bId: c.bId, kind: c.kind,
97
+ merge: !!d.merge,
98
+ confidence: Number(d.confidence) || 0,
99
+ reason: typeof d.reason === 'string' ? d.reason.slice(0, 200) : undefined,
100
+ })
101
+ }
102
+ return decisions
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.11",
3
+ "version": "0.9.12",
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": {