@theronap/agnoclast-mcp 0.9.96

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.
Files changed (44) hide show
  1. package/README.md +47 -0
  2. package/bin/cortex-mcp.mjs +223 -0
  3. package/lib/capture.mjs +470 -0
  4. package/lib/code_graph_cli.mjs +59 -0
  5. package/lib/context_log.mjs +92 -0
  6. package/lib/diagnose.mjs +360 -0
  7. package/lib/docs_scan.mjs +171 -0
  8. package/lib/doctor.mjs +117 -0
  9. package/lib/edge_extract.mjs +156 -0
  10. package/lib/editors/_fsutil.mjs +31 -0
  11. package/lib/editors/antigravity.mjs +130 -0
  12. package/lib/editors/claude.mjs +202 -0
  13. package/lib/editors/codex.mjs +111 -0
  14. package/lib/editors/cursor.mjs +77 -0
  15. package/lib/editors/index.mjs +42 -0
  16. package/lib/extract_typed.mjs +68 -0
  17. package/lib/graphify_sync.mjs +134 -0
  18. package/lib/grep_cli.mjs +82 -0
  19. package/lib/hydrate.mjs +181 -0
  20. package/lib/imessage_send.mjs +88 -0
  21. package/lib/ingest_folder.mjs +170 -0
  22. package/lib/install.mjs +163 -0
  23. package/lib/login.mjs +148 -0
  24. package/lib/managed.mjs +49 -0
  25. package/lib/migrate_key.mjs +139 -0
  26. package/lib/presence.mjs +226 -0
  27. package/lib/publish_targets.mjs +51 -0
  28. package/lib/red_link_triage.mjs +37 -0
  29. package/lib/redact.mjs +40 -0
  30. package/lib/rename_notice.mjs +31 -0
  31. package/lib/resolve.mjs +153 -0
  32. package/lib/server.mjs +2986 -0
  33. package/lib/session_key.mjs +37 -0
  34. package/lib/setup.mjs +215 -0
  35. package/lib/skills.mjs +374 -0
  36. package/lib/statusline.mjs +67 -0
  37. package/lib/uninstall.mjs +237 -0
  38. package/lib/use_brain.mjs +82 -0
  39. package/lib/with_token.mjs +66 -0
  40. package/package.json +36 -0
  41. package/skills/author-docs/SKILL.md +74 -0
  42. package/skills/context/SKILL.md +25 -0
  43. package/skills/log/SKILL.md +114 -0
  44. package/skills/walkthrough/SKILL.md +189 -0
@@ -0,0 +1,153 @@
1
+ import { spawnSync } from 'child_process'
2
+ import { fetchCortex, resolveBase, resolveEnvToken, 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
+ // Which brains to sweep. BOTH endpoints require the brain to be NAMED (ADR-0022 requireBrain —
12
+ // answering a scoped read out of an ARBITRARY brain is the defect that whole class exists to
13
+ // prevent). Until 2026-08-09 this command named none, so a multi-brain caller got 409 on step 1 and
14
+ // the dedup sweep did nothing at all, silently, forever.
15
+ //
16
+ // Naming ONE brain would have been the smaller fix and the wrong one: `resolve` is a MAINTENANCE
17
+ // SWEEP over the user's entities, so doing one brain and reporting success is ADR-0022's other
18
+ // failure — "silently truncating a result set and presenting it as complete". With no --brain we
19
+ // enumerate the caller's brains and sweep EACH, naming it explicitly. Not a guess: every request
20
+ // still names exactly one brain, and all of them actually get done.
21
+ //
22
+ // A sole-brain caller sees today's behaviour: one pass, no flag, no prompt.
23
+ export async function brainsToSweep(base, token, wanted, deps = {}) {
24
+ const fetchFn = deps.fetchCortex ?? fetchCortex
25
+ if (wanted) return [{ orgId: wanted, name: wanted }] // explicit: pass through (name or org id)
26
+ const res = await fetchFn(`${base}/api/brains`, { headers: { Authorization: `Bearer ${token}` } })
27
+ if (!res.ok) {
28
+ const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
29
+ throw new Error(`could not list your brains — ${d.message}`)
30
+ }
31
+ const j = await res.json().catch(() => ({}))
32
+ const brains = Array.isArray(j.brains) ? j.brains : []
33
+ // Carry the ORG ID, never the name: brain names are NOT unique (this account holds two called
34
+ // "Personal"), and a duplicate name comes back as unknown_brain.
35
+ return brains.filter((b) => b?.orgId).map((b) => ({ orgId: b.orgId, name: b.name ?? b.orgId }))
36
+ }
37
+
38
+ export async function runResolve(argv = []) {
39
+ // recursion guard (we spawn `claude --print`; if its Stop hook fires capture, that no-ops on this flag)
40
+ if (process.env.CORTEX_SUMMARIZING) { process.stderr.write('cortex: summarizer subprocess, skipping\n'); return }
41
+ const token = resolveEnvToken().token
42
+ if (!token) { process.stderr.write('cortex: CORTEX_TOKEN not set, skipping\n'); return }
43
+ const base = resolveBase(process.env.CORTEX_URL)
44
+
45
+ const bIdx = argv.indexOf('--brain')
46
+ const wanted = bIdx === -1 ? null : argv[bIdx + 1]
47
+ if (bIdx !== -1 && (!wanted || wanted.startsWith('-'))) {
48
+ process.stderr.write('Usage: resolve [--brain <name-or-org-id>]\n'); return
49
+ }
50
+
51
+ let targets
52
+ try {
53
+ targets = await brainsToSweep(base, token, wanted)
54
+ } catch (e) { process.stderr.write(`cortex: resolve — ${e.message}\n`); return }
55
+ if (!targets.length) { process.stderr.write('cortex: no brains to sweep\n'); return }
56
+
57
+ for (const t of targets) {
58
+ // Label each line with the brain when sweeping several: an unlabelled "merged 3" cannot be acted
59
+ // on, because you cannot tell WHERE three entities just merged.
60
+ await resolveOneBrain(base, token, t, targets.length > 1)
61
+ }
62
+ }
63
+
64
+ async function resolveOneBrain(base, token, brain, labelled) {
65
+ const tag = labelled ? `[${brain.name}] ` : ''
66
+ const qs = `?brain=${encodeURIComponent(brain.orgId)}`
67
+
68
+ // 1. pull the flagged candidate pairs
69
+ let candidates = []
70
+ try {
71
+ const res = await fetchCortex(`${base}/api/resolve-candidates${qs}`, { headers: { Authorization: `Bearer ${token}` } })
72
+ if (!res.ok) {
73
+ const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
74
+ process.stderr.write(`cortex: ${tag}resolve-candidates failed — ${d.message}\n`); return
75
+ }
76
+ const j = await res.json().catch(() => ({}))
77
+ candidates = Array.isArray(j.candidates) ? j.candidates : []
78
+ } catch (e) { process.stderr.write(`cortex: ${tag}resolve fetch failed — ${e.message}\n`); return }
79
+
80
+ if (!candidates.length) { process.stderr.write(`cortex: ${tag}no duplicate candidates to judge\n`); return }
81
+ process.stderr.write(`cortex: ${tag}judging ${candidates.length} candidate pair(s) locally…\n`)
82
+
83
+ // 2. judge locally on the subscription
84
+ const decisions = judgeCandidates(candidates)
85
+ if (decisions === null) { process.stderr.write(`cortex: ${tag}judge unavailable (is \`claude\` on PATH?) — skipping\n`); return }
86
+
87
+ // 3. apply — SAME brain the candidates came from, or the merges land in the wrong graph
88
+ try {
89
+ const res = await fetchCortex(`${base}/api/resolve-apply${qs}`, {
90
+ method: 'POST',
91
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
92
+ body: JSON.stringify({ decisions }),
93
+ })
94
+ if (res.ok) {
95
+ const j = await res.json().catch(() => ({}))
96
+ process.stderr.write(`cortex: ${tag}dedup — merged ${j.merged ?? 0}, rejected ${j.rejected ?? 0}, skipped ${j.skipped ?? 0}\n`)
97
+ } else {
98
+ const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
99
+ process.stderr.write(`cortex: ${tag}resolve-apply failed — ${d.message}\n`)
100
+ }
101
+ } catch (e) { process.stderr.write(`cortex: ${tag}resolve-apply failed — ${e.message}\n`) }
102
+ }
103
+
104
+ // ONE `claude --print` call judges every pair. Returns DedupDecision[] for the apply endpoint, or null
105
+ // if the CLI is unavailable. Every judged pair (merge AND non-merge) is returned so non-merges get
106
+ // recorded as rejected and never re-flag.
107
+ function judgeCandidates(candidates) {
108
+ const list = candidates.map((c, i) =>
109
+ `[${i}] kind=${c.kind}\n` +
110
+ ` A: "${c.aName}" (e.g. record: ${c.aSample ?? 'n/a'}; ${c.aMentions} mentions)\n` +
111
+ ` B: "${c.bName}" (e.g. record: ${c.bSample ?? 'n/a'}; ${c.bMentions} mentions)\n` +
112
+ ` shared records: ${c.sharedRecords}; name similarity: ${Number(c.sim).toFixed(2)}`,
113
+ ).join('\n')
114
+ const prompt =
115
+ 'You are deduplicating an organization knowledge graph. For each candidate pair below, decide ' +
116
+ 'whether A and B are the SAME real-world entity (a spelling/abbreviation/variant of ONE thing) or ' +
117
+ 'DIFFERENT things that merely have similar names. Be CONSERVATIVE: only merge when confident; when ' +
118
+ 'in doubt, do NOT merge. Same kind only.\n' +
119
+ 'Return ONLY a minified JSON array — one object per pair: ' +
120
+ '{"i":<index>,"merge":<true|false>,"confidence":<0..1>,"reason":"<short>"}.\n\n' + list
121
+ try {
122
+ const r = spawnSync(
123
+ 'claude',
124
+ ['--print', '--model', process.env.CORTEX_SUMMARY_MODEL ?? 'claude-haiku-4-5', prompt],
125
+ { env: edgeSafeEnv(process.env, { CORTEX_SUMMARIZING: '1' }), encoding: 'utf8', timeout: 120_000, maxBuffer: 4 * 1024 * 1024 },
126
+ )
127
+ if (r.status !== 0 || !r.stdout) return null
128
+ return parseDecisions(r.stdout, candidates)
129
+ } catch {
130
+ return null
131
+ }
132
+ }
133
+
134
+ function parseDecisions(out, candidates) {
135
+ const start = out.indexOf('[')
136
+ const end = out.lastIndexOf(']')
137
+ if (start < 0 || end <= start) return []
138
+ let arr
139
+ try { arr = JSON.parse(out.slice(start, end + 1)) } catch { return [] }
140
+ if (!Array.isArray(arr)) return []
141
+ const decisions = []
142
+ for (const d of arr) {
143
+ const c = candidates[Number(d?.i)]
144
+ if (!c) continue
145
+ decisions.push({
146
+ aId: c.aId, bId: c.bId, kind: c.kind,
147
+ merge: !!d.merge,
148
+ confidence: Number(d.confidence) || 0,
149
+ reason: typeof d.reason === 'string' ? d.reason.slice(0, 200) : undefined,
150
+ })
151
+ }
152
+ return decisions
153
+ }