@theronap/cortex-mcp 0.9.117 → 0.9.119
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 +76 -2
- package/package.json +1 -1
package/lib/server.mjs
CHANGED
|
@@ -1702,7 +1702,7 @@ export async function runServer(version) {
|
|
|
1702
1702
|
'split_page',
|
|
1703
1703
|
{
|
|
1704
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 —
|
|
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 — 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: 5.2% of them hold a third of all authored text, so this targets the tail, not routine hygiene. It does NOT make pages go wrong less often — corrections scale roughly linearly with size — it changes what each one COSTS: fixing a claim on the 165k-char page means reading ~42,000 tokens; on a 20k child, ~5,000. WHAT IT NEVER DOES, each for a measured reason: never retires the original (a split is 1→2 and `superseded_by` holds one successor, and the original keeps receiving traffic with no narrower match); never moves governance (attaching a record to the child does not change its tier); never rewrites inbound [[links]] (the splitter cannot know which half a link meant — the reader following it does); never links the child FROM the source, because where that link goes is prose. 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 shows a second `as of` stamp that is not part of it); moving every section is refused as a rename; and a STRICTER child is refused, because access is the union of attachments capped by the governing page — the child would look private while its evidence stayed readable. The child copies the parent tier and grants and gets an `In short` section. Reversible via `page_history` + `rollback_page`.',
|
|
1706
1706
|
inputSchema: {
|
|
1707
1707
|
name: z.string().describe('the exact page name to split, as read_page shows it'),
|
|
1708
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.'),
|
|
@@ -1782,6 +1782,80 @@ export async function runServer(version) {
|
|
|
1782
1782
|
},
|
|
1783
1783
|
)
|
|
1784
1784
|
|
|
1785
|
+
server.registerTool(
|
|
1786
|
+
'move_record',
|
|
1787
|
+
{
|
|
1788
|
+
title: 'Move a misfiled record into the brain it belongs in',
|
|
1789
|
+
description: "Move ONE record to another of your brains, as a historical amendment. Use it when you recognise that a record landed in the wrong brain — the connector chose the brain from which account it was pointed at, not from what the message is about, so mail about a project routinely lands somewhere the project's page does not exist. Until this existed a session could recognise the mistake and be unable to act: route_record refuses to attach a record to a page in another brain. THE RECORD KEEPS ITS REAL DATE. A message from May is still from May; the move stamps a separate arrival time so the record surfaces in your arrivals queue as something new to place, instead of being buried in the aged bucket on the strength of its original date — which is what a plain move would have done. WHAT DOES NOT COME WITH IT, and why: its project link is cleared, because the project lives in the old brain and a record pointing at another brain's project is a foreign-key violation that has taken production down; and its page attachments are dropped and counted, because pages do not move between brains, so the record arrives UNATTACHED and needs routing in its new home. Its revision history stays where it is — those revisions genuinely happened in the old brain, and rewriting them would falsify history to tidy the present. REFUSES rather than guessing when: you are not a member of the target (moving a record somewhere you cannot see hides it from you), the target already holds the same message, or decisions/open threads/status events reference it and point at entities in the old brain.",
|
|
1790
|
+
inputSchema: {
|
|
1791
|
+
record_id: z.string().describe('record id, as pending_records or my_records shows it'),
|
|
1792
|
+
brain: z.string().describe('the destination brain — its name, or its org id when two of your brains share a name'),
|
|
1793
|
+
reason: z.string().describe('WHY it belongs there — recorded, and the one thing about this move that cannot be inferred from the data'),
|
|
1794
|
+
},
|
|
1795
|
+
},
|
|
1796
|
+
async ({ record_id, brain, reason }) => {
|
|
1797
|
+
let res
|
|
1798
|
+
try {
|
|
1799
|
+
res = await fetchCortex(`${BASE}/api/brain/move-record`, {
|
|
1800
|
+
method: 'POST',
|
|
1801
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
1802
|
+
body: JSON.stringify({ record_id, brain, reason }),
|
|
1803
|
+
})
|
|
1804
|
+
} catch (e) {
|
|
1805
|
+
return toolError(`Could not move the record: ${e.message}`)
|
|
1806
|
+
}
|
|
1807
|
+
const out = await res.json().catch(() => null)
|
|
1808
|
+
if (!res.ok) {
|
|
1809
|
+
const extra = Array.isArray(out?.brains)
|
|
1810
|
+
? `\nyour brains with that name: ${out.brains.map((b) => `${b.name} (${b.org_id})`).join(' · ')}`
|
|
1811
|
+
: ''
|
|
1812
|
+
const detail = out?.detail ? `\n${out.detail}` : ''
|
|
1813
|
+
const hint = out?.hint ? `\n${out.hint}` : ''
|
|
1814
|
+
return toolError(`Could not move ${record_id}: ${out?.error ?? res.status}${detail}${extra}${hint}`)
|
|
1815
|
+
}
|
|
1816
|
+
const dropped = out.attachments_dropped
|
|
1817
|
+
? `\n${out.attachments_dropped} attachment(s) to pages in the old brain were dropped — route_record it here.`
|
|
1818
|
+
: ''
|
|
1819
|
+
return { content: [{ type: 'text', text: `Moved to ${out.moved_to} as a historical amendment.${dropped}\n${out.note}` }] }
|
|
1820
|
+
},
|
|
1821
|
+
)
|
|
1822
|
+
|
|
1823
|
+
server.registerTool(
|
|
1824
|
+
'place_staged_record',
|
|
1825
|
+
{
|
|
1826
|
+
title: 'Place a staged arrival onto pages — the pages decide its brain',
|
|
1827
|
+
description: "Place a STAGED arrival — one that is in no brain at all — onto the pages it belongs to, which is also what decides its brain. Session-start lists these separately as `[staged]`, and they are the only rows route_record CANNOT take, because there is no record yet to route: nothing upstream chose a brain for them, deliberately. That is the point (ADR-0038) — the connector used to pick the brain from which mailbox the message arrived through, which is a fact about your email plumbing rather than about the message, and it decided WHO COULD READ IT before anyone had read it. Here the pages decide instead. ⚠ PLACING IS A DISCLOSURE DECISION, not just filing: a brain is the confidentiality boundary, so putting a staged message into a shared brain makes it readable by every member of that brain. Say so when you offer, and never place a personal message into a shared brain without the owner\'s explicit answer. All the pages must live in ONE brain — a record exists in exactly one — and pages spanning two brains are refused by name rather than resolved by picking. On success the content is replayed through the real ingest pipeline into that brain, so the record it produces is identical to one that had landed there directly, and then it is attached.",
|
|
1828
|
+
inputSchema: {
|
|
1829
|
+
staged_id: z.string().describe('the staged id, as the [staged] rows at session start show it'),
|
|
1830
|
+
pages: z.array(z.string()).describe("pages to place it on — a brain_documents id or the `ref:` read_page prints. They must all be in ONE brain; that brain is where the record lands."),
|
|
1831
|
+
reason: z.string().describe('WHY these pages — recorded with the attachment, and the one thing that cannot be inferred later'),
|
|
1832
|
+
},
|
|
1833
|
+
},
|
|
1834
|
+
async ({ staged_id, pages, reason }) => {
|
|
1835
|
+
let res
|
|
1836
|
+
try {
|
|
1837
|
+
res = await fetchCortex(`${BASE}/api/staged/place`, {
|
|
1838
|
+
method: 'POST',
|
|
1839
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
1840
|
+
body: JSON.stringify({ stagedId: staged_id, documentIds: pages, reason }),
|
|
1841
|
+
})
|
|
1842
|
+
} catch (e) {
|
|
1843
|
+
return toolError(`Could not place the staged record: ${e.message}`)
|
|
1844
|
+
}
|
|
1845
|
+
const out = await res.json().catch(() => null)
|
|
1846
|
+
if (!res.ok && res.status !== 207) {
|
|
1847
|
+
const detail = out?.detail ? `\n${out.detail}` : ''
|
|
1848
|
+
return toolError(`Could not place ${staged_id}: ${out?.error ?? res.status}${detail}`)
|
|
1849
|
+
}
|
|
1850
|
+
// 207 and the no-record branch both mean the content LANDED and the attach did not. Report
|
|
1851
|
+
// that precisely rather than as success or failure — the follow-up differs for each.
|
|
1852
|
+
if (out?.placed === false) {
|
|
1853
|
+
return { content: [{ type: 'text', text: `Promoted into the target brain but NOT attached.\n${out.detail ?? ''}` }] }
|
|
1854
|
+
}
|
|
1855
|
+
return { content: [{ type: 'text', text: `Placed — promoted into its brain and attached to ${out.attached?.length ?? 0} page(s). The pages decided the brain; recorded as a session judgment.` }] }
|
|
1856
|
+
},
|
|
1857
|
+
)
|
|
1858
|
+
|
|
1785
1859
|
server.registerTool(
|
|
1786
1860
|
'set_summary',
|
|
1787
1861
|
{
|
|
@@ -3275,7 +3349,7 @@ export async function runServer(version) {
|
|
|
3275
3349
|
{
|
|
3276
3350
|
title: 'Author a wiki node (live, while it is hot)',
|
|
3277
3351
|
description:
|
|
3278
|
-
'Write your CURRENT understanding of a project/person/org/you into the org wiki as a maintained page. Call `authoring_context` FIRST. Author from your own synthesis
|
|
3352
|
+
'Write your CURRENT understanding of a project/person/org/you into the org wiki as a maintained page. Call `authoring_context` FIRST. Author from your own synthesis — the compiled mental model, not a transcript dump: what it IS, where it stands, dated decisions, open threads, key people. ⚠ EVERY STATUS CLAIM CARRIES AN EXPLICIT INLINE DATE (KWA-26): \"X is live\", \"Y is not merged\" must say WHEN, in the prose. Section-level stamps are NOT enough — they record when the TEXT was written, so a section authored today can carry a six-week-old claim and still read as current (KWA-24, TML-18). The response names undated sections; it never blocks the write. Weave inline [[links]]; red-link what is wanted but absent. CREATES the node if it does not exist (project/person/org) — the conversation IS the evidence, so an entity first seen this session is authorable now. Nothing external corroborates such a node, so author DELIBERATELY: only when you understand it is a real, distinct entity, under its exact canonical name so it does not duplicate one in the namespace (`user` nodes are never created). Use as understanding advances, and at session end (/log). PRE-AUTHORIZED — never ask \"should I update the page?\" (versioned and reversible via page_history/rollback_page); update, then briefly report. ⚠ WHICH BRAIN A NEW PAGE GOES IN IS A CONTENT DECISION, so make it from the content. Only a page in NO brain needs this; an update resolves its brain from the page. With several brains the server REFUSES a create it cannot attribute (409 `create_needs_brain`) rather than letting the write pointer decide. Call `my_brains` and pass `brain` up front. Say which you picked and why; do NOT ask when the answer is obvious. DO ask when it is genuinely ambiguous: brains are a confidentiality boundary, so a page born in the wrong one can expose private work to a teammate — not a filing error you can quietly fix later.',
|
|
3279
3353
|
inputSchema: {
|
|
3280
3354
|
kind: z.enum(['project', 'person', 'org', 'user']).describe('the node type'),
|
|
3281
3355
|
name: z.string().describe('the canonical node name — an EXACT existing name from the namespace to update it, or a new name to create the node (project/person/org). e.g. "Agnoclast" or "Theron Peterson"'),
|
package/package.json
CHANGED