@theronap/cortex-mcp 0.9.108 → 0.9.109
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 +65 -0
- package/package.json +1 -1
package/lib/server.mjs
CHANGED
|
@@ -1540,6 +1540,71 @@ export async function runServer(version) {
|
|
|
1540
1540
|
},
|
|
1541
1541
|
)
|
|
1542
1542
|
|
|
1543
|
+
server.registerTool(
|
|
1544
|
+
'merge_sections',
|
|
1545
|
+
{
|
|
1546
|
+
title: 'Condense two sections into one, or drop a spent section',
|
|
1547
|
+
description: 'CONDENSE a page: fold one section into another, or remove one outright. This is the only route that can make a page SMALLER — `author` refuses any write that omits a section (409 would_drop_sections), `edit_page` reaches inside one section, and `rename_section` touches only a heading, so without this every correction is additive and pages grow without bound. Use it when two sections overlap and one shorter section would say the same thing, or when a section is spent — closed work, a retraction whose lesson now lives elsewhere, a superseded status block. Pass `into` + `new_body` to condense (the surviving section gets `new_body`, `from` is removed); omit BOTH to drop `from` outright. GUARDS, all of which refuse rather than guess: `base_version` is the same page-level CAS `edit_page` uses; a merge never crosses tiers; the last remaining section cannot be dropped; and if the removal would orphan a `[[link]]` — leaving it nowhere on the page — it 409s with the list, because a [[link]] is a graph EDGE and losing one silently is the exact 2026-07-30 failure the no-drop rule was written for. CURRENCY: the surviving section inherits the OLDER of the two as-of stamps, never now() — relocating a claim does not re-verify it. Fully reversible: the prior revision keeps the removed section in full, so `rollback_page` restores it.',
|
|
1548
|
+
inputSchema: {
|
|
1549
|
+
name: z.string().describe('the exact page name, as read_page shows it'),
|
|
1550
|
+
from: z.string().describe('the heading of the section to REMOVE (match is case- and whitespace-insensitive)'),
|
|
1551
|
+
into: z.string().optional().describe('the heading of the section that SURVIVES and absorbs the content. Omit to drop `from` outright without folding it anywhere.'),
|
|
1552
|
+
new_body: z.string().optional().describe('the condensed body the surviving section should read AFTER the merge — you write it, this route does not summarize. Required with `into`; must be absent without it.'),
|
|
1553
|
+
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.'),
|
|
1554
|
+
tier: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('only when the page exists at MORE THAN ONE tier — which one to condense in. A merge never moves content between tiers.'),
|
|
1555
|
+
from_ordinal: z.number().optional().describe('only when `from`\'s heading appears more than once on the page — which occurrence (the error lists the ordinals).'),
|
|
1556
|
+
into_ordinal: z.number().optional().describe('only when `into`\'s heading appears more than once on the page — which occurrence (the error lists the ordinals).'),
|
|
1557
|
+
reason: z.string().optional().describe('WHY you are condensing, in one short phrase — recorded in page_history. Say what became redundant, not what you did.'),
|
|
1558
|
+
drop_links_ack: z.boolean().optional().describe('ONLY after a 409 drops_links refusal, and only if the answer is genuinely yes: those [[links]] are meant to stop being edges of this page. Otherwise carry them into `new_body` instead.'),
|
|
1559
|
+
identity_claim_ack: z.boolean().optional().describe('ONLY after a 409 identity_claim refusal, and only if the answer is genuinely yes: this text is meant to publish an email address or phone number as page content at this tier.'),
|
|
1560
|
+
},
|
|
1561
|
+
},
|
|
1562
|
+
async ({ name, from, into, new_body, base_version, tier, from_ordinal, into_ordinal, reason, drop_links_ack, identity_claim_ack }) => {
|
|
1563
|
+
let res
|
|
1564
|
+
try {
|
|
1565
|
+
res = await fetchCortex(`${BASE}/api/brain/merge-sections`, {
|
|
1566
|
+
method: 'POST',
|
|
1567
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
1568
|
+
body: JSON.stringify({
|
|
1569
|
+
name, from, base_version,
|
|
1570
|
+
...(into ? { into } : {}), ...(new_body !== undefined ? { new_body } : {}),
|
|
1571
|
+
...(tier ? { tier } : {}),
|
|
1572
|
+
...(from_ordinal !== undefined ? { from_ordinal } : {}),
|
|
1573
|
+
...(into_ordinal !== undefined ? { into_ordinal } : {}),
|
|
1574
|
+
...(reason ? { reason } : {}),
|
|
1575
|
+
...(drop_links_ack ? { drop_links_ack: true } : {}),
|
|
1576
|
+
...(identity_claim_ack ? { identity_claim_ack: true } : {}),
|
|
1577
|
+
}),
|
|
1578
|
+
})
|
|
1579
|
+
} catch (e) {
|
|
1580
|
+
return toolError(`Could not condense the section: ${e.message}`)
|
|
1581
|
+
}
|
|
1582
|
+
const out = await res.json().catch(() => null)
|
|
1583
|
+
if (!res.ok) {
|
|
1584
|
+
// Surface every disambiguator the server named, so a 409 is directly actionable rather than
|
|
1585
|
+
// something to retry blindly. `dropped` is the one that matters most: it names the graph edges
|
|
1586
|
+
// that would be lost, which is the decision the caller actually has to make.
|
|
1587
|
+
const extra = [
|
|
1588
|
+
Array.isArray(out?.dropped) ? `would orphan these [[links]]: ${out.dropped.join(', ')}` : '',
|
|
1589
|
+
Array.isArray(out?.available) ? `existing headings: ${out.available.join(' · ')}` : '',
|
|
1590
|
+
Array.isArray(out?.identityClaims) ? `identity identifiers this would publish: ${out.identityClaims.join(', ')}` : '',
|
|
1591
|
+
Array.isArray(out?.tiers) ? `tiers: ${out.tiers.join(', ')}` : '',
|
|
1592
|
+
Array.isArray(out?.ordinals) ? `ordinals: ${out.ordinals.join(', ')}` : '',
|
|
1593
|
+
out?.currentVersion ? `current version: ${out.currentVersion}` : '',
|
|
1594
|
+
].filter(Boolean).join('\n')
|
|
1595
|
+
const hint = out?.hint ? `\n${out.hint}` : ''
|
|
1596
|
+
return toolError(`Could not condense "${from}" on "${name}": ${out?.error ?? res.status}${extra ? `\n${extra}` : ''}${hint}`)
|
|
1597
|
+
}
|
|
1598
|
+
const what = out.kept
|
|
1599
|
+
? `Condensed "${out.removed}" into "${out.kept}"`
|
|
1600
|
+
: `Removed section "${out.removed}"`
|
|
1601
|
+
const undated = out.undated ? '\n⚠ The surviving section carries no explicit calendar date — add one so a reader can see when it became true.' : ''
|
|
1602
|
+
const red = Array.isArray(out.redLinks) && out.redLinks.length
|
|
1603
|
+
? `\nRed links now on this page: ${out.redLinks.join(', ')}` : ''
|
|
1604
|
+
return { content: [{ type: 'text', text: `${what} on "${name}" (${out.brain} · ${out.tier} tier). Reclaimed ${out.chars_reclaimed} chars. New version: ${out.version}${undated}${red}\nReversible: \`page_history "${name}"\` then \`rollback_page\` restores the removed section in full.` }] }
|
|
1605
|
+
},
|
|
1606
|
+
)
|
|
1607
|
+
|
|
1543
1608
|
server.registerTool(
|
|
1544
1609
|
'set_summary',
|
|
1545
1610
|
{
|
package/package.json
CHANGED