@theronap/cortex-mcp 0.9.114 → 0.9.116
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/context_log.mjs +6 -1
- package/lib/server.mjs +63 -3
- package/package.json +1 -1
package/lib/context_log.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlink
|
|
|
2
2
|
import { homedir } from 'os'
|
|
3
3
|
import { join } from 'path'
|
|
4
4
|
import { fetchCortex, classify, resolveBase, resolveTokenSource } from './diagnose.mjs'
|
|
5
|
+
import { repoFullNameFrom } from './capture.mjs'
|
|
5
6
|
|
|
6
7
|
// The private third copy of this lived here until 2026-08-19. doctor.mjs:14 warned that a third
|
|
7
8
|
// copy is how a machine ends up connected to one command and 'no token found' to another; it also
|
|
@@ -47,9 +48,13 @@ export async function runSnapshotContext() {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
const base = resolveBase(process.env.CORTEX_URL)
|
|
51
|
+
// The SessionStart snapshot is the surface that actually reaches a session's first turn, so the
|
|
52
|
+
// repo-scoped Gate 4 block is worth more here than anywhere else. Fail-closed: a non-GitHub cwd
|
|
53
|
+
// yields null and the hint is omitted (see repoFullNameFrom).
|
|
54
|
+
const repo = repoFullNameFrom(process.cwd())
|
|
50
55
|
let res
|
|
51
56
|
try {
|
|
52
|
-
res = await fetchCortex(`${base}/api/mcp-context`, { headers: { Authorization: `Bearer ${token}` } })
|
|
57
|
+
res = await fetchCortex(`${base}/api/mcp-context${repo ? `?repo=${encodeURIComponent(repo)}` : ''}`, { headers: { Authorization: `Bearer ${token}` } })
|
|
53
58
|
} catch (e) {
|
|
54
59
|
out(`Agnoclast: context snapshot failed — ${e?.message ?? String(e)}`)
|
|
55
60
|
return 0
|
package/lib/server.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { runSendImessage } from './imessage_send.mjs'
|
|
|
12
12
|
import { formatGrepHits } from './grep_cli.mjs'
|
|
13
13
|
import { renderTriage } from './red_link_triage.mjs'
|
|
14
14
|
import { runCodeGraphQuery } from './code_graph_cli.mjs'
|
|
15
|
+
import { repoFullNameFrom } from './capture.mjs'
|
|
15
16
|
|
|
16
17
|
// Reactive red-link triage (Mechanism 2). On a read_page miss, ask the server whether the name is a
|
|
17
18
|
// tracked wanted page, whether a bare node exists for it, and whether it's a deliberately demoted page,
|
|
@@ -126,19 +127,27 @@ export async function runServer(version) {
|
|
|
126
127
|
if (typeof _hb.unref === 'function') _hb.unref() // don't keep the process alive just for the heartbeat
|
|
127
128
|
|
|
128
129
|
// Cache context for 5 minutes so repeated tool calls don't re-fetch.
|
|
130
|
+
// KEYED BY REPO since 2026-08-27: the context now carries a repo-scoped Gate 4 block, so the same
|
|
131
|
+
// token can legitimately get different context from different cwds. One stdio server serves one cwd
|
|
132
|
+
// today and this is belt-and-braces — but an unkeyed cache would serve one repo's block to another
|
|
133
|
+
// silently, and "your repo has 12 records waiting" pointing at the wrong repo is worse than no line.
|
|
129
134
|
let cache = null
|
|
130
135
|
async function fetchContext() {
|
|
131
136
|
const now = Date.now()
|
|
132
|
-
|
|
137
|
+
// Fail-closed by construction (see repoFullNameFrom): a non-GitHub or non-repo cwd yields null and
|
|
138
|
+
// the request simply omits the hint, degrading to the generic block.
|
|
139
|
+
const repo = repoFullNameFrom(process.cwd())
|
|
140
|
+
if (cache && cache.repo === repo && now - cache.ts < 5 * 60 * 1000) return cache.text
|
|
133
141
|
// fetchCortex retries transient infra/5xx; classify turns a failure into an honest message
|
|
134
142
|
// (token vs infra-block vs network) instead of a bare "Agnoclast API 403: unknown".
|
|
135
|
-
const
|
|
143
|
+
const url = `${BASE}/api/mcp-context${repo ? `?repo=${encodeURIComponent(repo)}` : ''}`
|
|
144
|
+
const res = await fetchCortex(url, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
136
145
|
if (!res.ok) {
|
|
137
146
|
const body = await res.text()
|
|
138
147
|
throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
|
|
139
148
|
}
|
|
140
149
|
const { context, brainRefs } = await res.json()
|
|
141
|
-
cache = { text: context, ts: now }
|
|
150
|
+
cache = { text: context, ts: now, repo }
|
|
142
151
|
// T11: stash the digest node refs surfaced this fetch so capture.mjs can forward them as
|
|
143
152
|
// hydrated_from at the session's ingest (feedback-loop guard). Keyed by cwd so the matching
|
|
144
153
|
// session picks them up. Best-effort + inert when brainRefs is empty (digest flag off).
|
|
@@ -1689,6 +1698,57 @@ export async function runServer(version) {
|
|
|
1689
1698
|
},
|
|
1690
1699
|
)
|
|
1691
1700
|
|
|
1701
|
+
server.registerTool(
|
|
1702
|
+
'split_page',
|
|
1703
|
+
{
|
|
1704
|
+
title: 'Move sections onto a new child page',
|
|
1705
|
+
description: 'SPLIT a page: move whole sections onto a NEW page, leaving the original in place. Use it when a page has grown past what can be read in one turn — that is a correctness problem, not just a cost one, because an agent that cannot read the whole authority answers from part of it (a head page and its governing page disagreed about a gate status for a week that way). Measured across 480 pages: the median page is ~3,500 chars, but 5.2% of pages hold a third of all authored text, so this is a targeted tool for the tail, not routine hygiene. It does NOT make pages go wrong less often — corrections scale roughly linearly with size — it changes what each correction COSTS to make: fixing one claim on a 165k-char page means reading ~42,000 tokens; on a 20k child, ~5,000. WHAT IT NEVER DOES, each for a measured reason: it never retires the original (a split is 1→2 and `superseded_by` holds one successor, so naming one would be false; the original also keeps receiving traffic that has no narrower match); it never moves governance (attaching a record to the child does not change its tier — reassigning governance is a privacy act and stays separate); it never rewrites inbound [[links]] (the splitter cannot know which half a link meant, the reader following it does — so the child says where it came from and lets them decide); and it never adds a link on the SOURCE, because where that link goes is prose — the response tells you to add one. GUARDS: the child is created BEFORE the source is trimmed, so a mid-way failure duplicates sections rather than losing them; `headings` must match the STORED heading exactly, INCLUDING any `· as of <date>` suffix (the rendered page can show a second `as of` stamp that is not part of it); moving every section is refused as a rename; and a child STRICTER than its parent is refused outright, because access is the union of attachments capped by the governing page — records attached to a stricter child keep their audience through the original, so it would look private while its evidence stayed readable. The child inherits the parent tier and its access grants, and gets an `In short` section rather than a summary. Fully reversible: `page_history` + `rollback_page` restore the source, and the child can be retired.',
|
|
1706
|
+
inputSchema: {
|
|
1707
|
+
name: z.string().describe('the exact page name to split, as read_page shows it'),
|
|
1708
|
+
headings: z.array(z.string()).min(1).describe('the headings of the sections to MOVE, matched EXACTLY against the stored heading — include any `· as of <date>` suffix. Everything not listed stays on the original.'),
|
|
1709
|
+
new_title: z.string().describe('the title of the new child page. It must not already exist in this brain.'),
|
|
1710
|
+
base_version: z.string().describe('the `version` read_page prints for the SOURCE page (64-hex). REQUIRED — it is the concurrency check AND how the right brain is resolved.'),
|
|
1711
|
+
reason: z.string().describe('WHY you are splitting, in one short phrase — recorded in page_history. Say what outgrew the page.'),
|
|
1712
|
+
owner: z.string().optional().describe('user id to own the child. Defaults to you, and the response says so when it does — worth setting deliberately, because ownership transfer has no reachable path once an owner is deactivated.'),
|
|
1713
|
+
tier: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('omit to COPY the source page tier, which is almost always right. A LOOSER tier is a deliberate widening; a STRICTER one is refused, since a split cannot tighten access.'),
|
|
1714
|
+
},
|
|
1715
|
+
},
|
|
1716
|
+
async ({ name, headings, new_title, base_version, reason, owner, tier }) => {
|
|
1717
|
+
let res
|
|
1718
|
+
try {
|
|
1719
|
+
res = await fetchCortex(`${BASE}/api/brain/split-page`, {
|
|
1720
|
+
method: 'POST',
|
|
1721
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
1722
|
+
body: JSON.stringify({
|
|
1723
|
+
name, headings, new_title, base_version, reason,
|
|
1724
|
+
...(owner ? { owner } : {}), ...(tier ? { tier } : {}),
|
|
1725
|
+
}),
|
|
1726
|
+
})
|
|
1727
|
+
} catch (e) {
|
|
1728
|
+
return toolError(`Could not split the page: ${e.message}`)
|
|
1729
|
+
}
|
|
1730
|
+
const out = await res.json().catch(() => null)
|
|
1731
|
+
if (!res.ok) {
|
|
1732
|
+
// `child_created` + `remaining` is the one failure worth reading carefully: it means the split
|
|
1733
|
+
// is HALF DONE and nothing was lost. Surface it first so the caller finishes rather than retries
|
|
1734
|
+
// from the top, which would 409 on the now-taken title and look like a different problem.
|
|
1735
|
+
const extra = [
|
|
1736
|
+
out?.child_created ? `the child "${out.child_created}" EXISTS and holds every moved section — nothing was lost` : '',
|
|
1737
|
+
Array.isArray(out?.remaining) ? `still on BOTH pages, re-run for these: ${out.remaining.join(' · ')}` : '',
|
|
1738
|
+
Array.isArray(out?.detail) ? `detail: ${out.detail.join(' · ')}` : '',
|
|
1739
|
+
out?.message ?? '',
|
|
1740
|
+
].filter(Boolean).join('\n')
|
|
1741
|
+
const hint = out?.hint ? `\n${out.hint}` : ''
|
|
1742
|
+
return toolError(`Could not split "${name}": ${out?.error ?? res.status}${extra ? `\n${extra}` : ''}${hint}`)
|
|
1743
|
+
}
|
|
1744
|
+
const ownerNote = out.owner_defaulted
|
|
1745
|
+
? '\n⚠ The child is owned by you because no `owner` was given. Set one deliberately if it should belong to someone else — transfer is unreachable once an owner is deactivated.'
|
|
1746
|
+
: ''
|
|
1747
|
+
const grants = out.grants_copied ? ` ${out.grants_copied} access grant(s) copied.` : ''
|
|
1748
|
+
return { content: [{ type: 'text', text: `Split "${name}" (${out.brain} · ${out.tier} tier) → new page "${out.child}". Moved: ${out.moved.join(' · ')}.${grants} Child version: ${out.version}\n${out.note}\nNEXT: ${out.next}${ownerNote}\nReversible: \`page_history "${name}"\` then \`rollback_page\` restores the source; the child can be retired with set_page_validity.` }] }
|
|
1749
|
+
},
|
|
1750
|
+
)
|
|
1751
|
+
|
|
1692
1752
|
server.registerTool(
|
|
1693
1753
|
'set_summary',
|
|
1694
1754
|
{
|
package/package.json
CHANGED