@theronap/cortex-mcp 0.9.67 → 0.9.69
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/server.mjs +51 -4
- package/package.json +1 -1
package/lib/server.mjs
CHANGED
|
@@ -556,7 +556,7 @@ export async function runServer(version) {
|
|
|
556
556
|
// retype silently deleted a sentence, a [[link]] (a graph edge), a command list and the
|
|
557
557
|
// word "today" from sections it was never meant to touch. This footer renders on EVERY
|
|
558
558
|
// page read in the system, so it was the single widest surface pointing the wrong way.
|
|
559
|
-
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 — fix just that passage with \`edit_page\`: quote the wrong text as old_string and pass this page's \`version\` as base_version (any node you are permitted to edit; concise; a material contradiction, not merely extra detail). If the save comes back stale the page changed under you — the conflict hands back what changed, so re-anchor from that instead of re-reading. Reading a stale page you can fix IS the trigger to fix it. Editing is pre-authorized — do NOT ask the user before updating (edits are versioned + reversible via page_history/rollback_page); update, then briefly report it. Use \`author\` only to CREATE a page or rewrite one wholesale: it re-emits every section, so untouched sections get retyped on the way through and drift.\n— Citing code? Use a SYMBOL and file (\`formConnections\` in \`web/app/api/ingest/route.ts\`), never a line number — line numbers drift with every commit above them. And cite only what you opened THIS session; re-emitting a reference you read on another page is how a stale claim gains a second source and starts looking corroborated.`
|
|
559
|
+
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 — fix just that passage with \`edit_page\`: quote the wrong text as old_string and pass this page's \`version\` as base_version (any node you are permitted to edit; concise; a material contradiction, not merely extra detail). If the save comes back stale the page changed under you — the conflict hands back what changed, so re-anchor from that instead of re-reading. Reading a stale page you can fix IS the trigger to fix it. Editing is pre-authorized — do NOT ask the user before updating (edits are versioned + reversible via page_history/rollback_page); update, then briefly report it. If what went false is the SUMMARY at the top rather than a section, use \`set_summary\` — \`edit_page\` cannot reach it, and worse, it carries the old summary FORWARD into every revision it writes, so fixing a section silently re-certifies a summary that already contradicts it. Use \`author\` only to CREATE a page or rewrite one wholesale: it re-emits every section, so untouched sections get retyped on the way through and drift.\n— Citing code? Use a SYMBOL and file (\`formConnections\` in \`web/app/api/ingest/route.ts\`), never a line number — line numbers drift with every commit above them. And cite only what you opened THIS session; re-emitting a reference you read on another page is how a stale claim gains a second source and starts looking corroborated.`
|
|
560
560
|
// ADDRESSING (2026-08-04). The server accepts `ref` on every page/node route and its ambiguity
|
|
561
561
|
// 409s hand refs back — but read_page never PRINTED one, so the only way to obtain a ref was to
|
|
562
562
|
// trigger the error first. That made ID addressing reachable in principle and unusable in
|
|
@@ -770,6 +770,49 @@ export async function runServer(version) {
|
|
|
770
770
|
},
|
|
771
771
|
)
|
|
772
772
|
|
|
773
|
+
server.registerTool(
|
|
774
|
+
'set_summary',
|
|
775
|
+
{
|
|
776
|
+
title: 'Rewrite a wiki page summary without touching its sections',
|
|
777
|
+
description: 'Replace a page\'s SUMMARY — the one-sentence line at the top — in place, leaving every section untouched. Use this the moment you notice a summary that no longer matches the page: `edit_page` structurally CANNOT reach it (it edits section bodies; the summary is not one), and `author` reaches it only by retyping every section on the way through, which is how a 2026-07-30 write silently deleted a sentence and a [[link]] from a section nobody meant to touch. Correcting a body with `edit_page` actively carries the OLD summary forward into the new revision, so a stale summary does not decay quietly — each unrelated fix re-certifies it. Fixing it matters more than it looks: page retrieval FTS-matches on title + summary ALONE — the tsvector is built over title and summary, and section bodies are not in that index — so a stale summary decides whether the page is found by that path at all, and it is the first line every reader sees before a word of the body. Requires base_version (the `version` read_page prints) — that is also how it finds the right brain, so it can never write to the wrong one. Recorded in page_history as a correction, and reversible with rollback_page.',
|
|
778
|
+
inputSchema: {
|
|
779
|
+
name: z.string().describe('the exact page name, as read_page shows it'),
|
|
780
|
+
summary: z.string().describe('the new summary: one sentence saying what this is and where it stands. May contain [[links]]. Max 2,000 chars — detail belongs in a section.'),
|
|
781
|
+
base_version: z.string().describe('the `version` read_page prints for this page (64-hex). REQUIRED — it is the concurrency check AND how the right brain is resolved. A per-SECTION hash is not valid here.'),
|
|
782
|
+
reason: z.string().optional().describe('WHY the summary was wrong, in one short phrase — recorded in page_history. Say what changed ("blocker resolved 08-04; was still claiming BLOCKED"), not what you did.'),
|
|
783
|
+
tier: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('only when the page exists at MORE THAN ONE tier — which one to rewrite. This never moves content between tiers.'),
|
|
784
|
+
},
|
|
785
|
+
},
|
|
786
|
+
async ({ name, summary, base_version, reason, tier }) => {
|
|
787
|
+
let res
|
|
788
|
+
try {
|
|
789
|
+
res = await fetchCortex(`${BASE}/api/brain/set-summary`, {
|
|
790
|
+
method: 'POST',
|
|
791
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
792
|
+
body: JSON.stringify({
|
|
793
|
+
name, summary, base_version,
|
|
794
|
+
...(tier ? { tier } : {}), ...(reason ? { reason } : {}),
|
|
795
|
+
}),
|
|
796
|
+
})
|
|
797
|
+
} catch (e) {
|
|
798
|
+
return toolError(`Could not set the summary: ${e.message}`)
|
|
799
|
+
}
|
|
800
|
+
const out = await res.json().catch(() => null)
|
|
801
|
+
if (!res.ok) {
|
|
802
|
+
// Surface the server's hint AND the disambiguators it named, so a 409 is directly actionable
|
|
803
|
+
// rather than something to retry blindly.
|
|
804
|
+
const extra = [
|
|
805
|
+
out?.detail ? `detail: ${out.detail}` : '',
|
|
806
|
+
Array.isArray(out?.tiers) ? `tiers: ${out.tiers.join(', ')}` : '',
|
|
807
|
+
out?.currentVersion ? `current version: ${out.currentVersion}` : '',
|
|
808
|
+
].filter(Boolean).join(' · ')
|
|
809
|
+
const hint = out?.hint ? `\n${out.hint}` : ''
|
|
810
|
+
return toolError(`Could not set the summary on "${name}": ${out?.error ?? res.status}${extra ? `\n${extra}` : ''}${hint}`)
|
|
811
|
+
}
|
|
812
|
+
return { content: [{ type: 'text', text: `Summary rewritten on "${name}" (${out.brain} · ${out.tier} tier). Every section kept its body, position and as-of date.\nwas: ${out.previousSummary}\nnow: ${out.summary}\nNew version: ${out.version}` }] }
|
|
813
|
+
},
|
|
814
|
+
)
|
|
815
|
+
|
|
773
816
|
server.registerTool(
|
|
774
817
|
'edit_page',
|
|
775
818
|
{
|
|
@@ -1359,7 +1402,10 @@ export async function runServer(version) {
|
|
|
1359
1402
|
const out = await res.json().catch(() => null)
|
|
1360
1403
|
if (!res.ok) return toolError(`Could not snooze "${name}": ${out?.error ?? res.status}`)
|
|
1361
1404
|
if (!out) return { content: [{ type: 'text', text: `Snoozed "${name}", but the server returned no body.` }] }
|
|
1362
|
-
|
|
1405
|
+
// Name the brains when there is more than one: a snooze that quieted the same wanted name in two
|
|
1406
|
+
// brains you steward is a multi-row write, and reporting it as a single one hides that.
|
|
1407
|
+
const where = out.brains?.length > 1 ? ` in ${out.brains.map((b) => b.brain).join(' and ')}` : ''
|
|
1408
|
+
return { content: [{ type: 'text', text: `Snoozed "${out.name}"${where} for ${out.days} day${out.days === 1 ? '' : 's'} — it won't surface until then.` }] }
|
|
1363
1409
|
},
|
|
1364
1410
|
)
|
|
1365
1411
|
|
|
@@ -1680,13 +1726,14 @@ export async function runServer(version) {
|
|
|
1680
1726
|
'Fetch the scaffolding to author a Cortex wiki node: the canonical NAMESPACE (current node names — link to these with the EXACT name inside [[ ]]), any deliberately RETIRED page names and their successors, and the node-type CONNECTION RULES. ALWAYS call this BEFORE `author` so the page links to current knowledge rather than minting synonyms or reviving a retired page. Reference a node in the namespace as [[Name]]; if you reference something real that is NOT in the namespace, still write [[Name]] — that is a red-link marking a node worth creating. If the page IS about a specific code repo or chat channel, also stamp it once — [[repo:owner/name]] or [[channel:name]] (lowercase, no #) — identifier join keys, not page links.',
|
|
1681
1727
|
inputSchema: {
|
|
1682
1728
|
kind: z.enum(['project', 'person', 'org', 'user']).optional().describe('the node type you are about to author (default project)'),
|
|
1729
|
+
brain: z.string().optional().describe('which brain\'s namespace to describe — pass the SAME brain you will pass to `author`, so the namespace you plan against is the one your write lands in. Unnecessary when you only have one brain.'),
|
|
1683
1730
|
},
|
|
1684
1731
|
},
|
|
1685
|
-
async ({ kind }) => {
|
|
1732
|
+
async ({ kind, brain }) => {
|
|
1686
1733
|
const k = kind ?? 'project'
|
|
1687
1734
|
let res
|
|
1688
1735
|
try {
|
|
1689
|
-
res = await fetchCortex(`${BASE}/api/brain/authoring-context?kind=${k}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
1736
|
+
res = await fetchCortex(`${BASE}/api/brain/authoring-context?kind=${k}${brain ? `&brain=${encodeURIComponent(brain)}` : ''}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
1690
1737
|
} catch (e) {
|
|
1691
1738
|
return toolError(`Could not fetch authoring context: ${e.message}`)
|
|
1692
1739
|
}
|