@theronap/cortex-mcp 0.9.116 → 0.9.118

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 (2) hide show
  1. package/lib/server.mjs +73 -2
  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 — 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.',
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.'),
@@ -1749,6 +1749,77 @@ export async function runServer(version) {
1749
1749
  },
1750
1750
  )
1751
1751
 
1752
+ server.registerTool(
1753
+ 'snooze_routing_claim',
1754
+ {
1755
+ title: 'Stop surfacing one unclaimed source',
1756
+ description: "Silence one identifier in the `Sources nothing has claimed` block at session start. Use it when a recurring source genuinely belongs nowhere — a scratch repo, a bot channel, someone else's project that happens to cc you. NOT for something you simply have not got to yet: that is what leaving it alone does. A snooze silences the NOTICE and routes nothing; the events keep landing exactly where they land today. The block already has a volume floor that keeps one-off identifiers out, so anything you see there is recurring by construction — which is precisely why no threshold can tell 'not yet decided' from 'decided: nowhere', and why this exists. Bounded to at most a year and defaulting to 90 days, because a repo that was noise in August may be the centre of the work by November. Identity namespaces (`email:`, `thread:`) are refused: they are never surfaced as claimable in the first place, so there is nothing to snooze.",
1757
+ inputSchema: {
1758
+ identifier: z.string().describe('the identifier exactly as the context block prints it, e.g. `repo:owner/name`'),
1759
+ brain: z.string().optional().describe('which brain to stop surfacing it in. Required when you belong to more than one — the call refuses rather than picking.'),
1760
+ days: z.number().optional().describe('how long to stay quiet (1-365, default 90).'),
1761
+ reason: z.string().optional().describe('why it belongs nowhere, in one phrase — the next person to see it resurface will want this.'),
1762
+ },
1763
+ },
1764
+ async ({ identifier, brain, days, reason }) => {
1765
+ let res
1766
+ try {
1767
+ res = await fetchCortex(`${BASE}/api/brain/snooze-routing-claim`, {
1768
+ method: 'POST',
1769
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
1770
+ body: JSON.stringify({ identifier, ...(brain ? { brain } : {}), ...(days ? { days } : {}), ...(reason ? { reason } : {}) }),
1771
+ })
1772
+ } catch (e) {
1773
+ return toolError(`Could not snooze ${identifier}: ${e.message}`)
1774
+ }
1775
+ const out = await res.json().catch(() => null)
1776
+ if (!res.ok) {
1777
+ const extra = Array.isArray(out?.brains) ? `\nyour brains: ${out.brains.join(' · ')}` : ''
1778
+ const hint = out?.hint ? `\n${out.hint}` : ''
1779
+ return toolError(`Could not snooze ${identifier}: ${out?.error ?? res.status}${extra}${hint}`)
1780
+ }
1781
+ return { content: [{ type: 'text', text: `${out.note}\nIf it should route somewhere after all, \`set_routing_identifier\` on the owning page overrides this — a claim always beats a snooze.` }] }
1782
+ },
1783
+ )
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
+
1752
1823
  server.registerTool(
1753
1824
  'set_summary',
1754
1825
  {
@@ -3242,7 +3313,7 @@ export async function runServer(version) {
3242
3313
  {
3243
3314
  title: 'Author a wiki node (live, while it is hot)',
3244
3315
  description:
3245
- '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 of the session — 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): a sentence asserting what IS or IS NOT true right now — "X is live", "Y is not merged", "Z is blocked" — must say WHEN, in the prose, the way PRD items do. Section-level stamps are NOT enough: they record when the TEXT was written, so a section authored today can carry a six-week-old status claim and still read as current — exactly what made KWA-24 and TML-18 wrong. The response names any section that landed undated so you can fix it in-turn; it never blocks the write. Weave inline [[links]] to other nodes (canonical names from the namespace; red-links for wanted-but-absent nodes). The server re-authorizes the tier and resolves links. CREATES the node if it does not exist yet (project/person/org) — the conversation IS the evidence, so a brand-new entity that surfaced only in this session is authorable on the spot; you do NOT need prior records. Because such a node has nothing external to corroborate it, author it DELIBERATELY: only when you genuinely understand it is a real, distinct entity, and use its exact canonical name so it does not duplicate one already in the namespace (`user` nodes are never created). Use this continuously whenever your understanding of a node meaningfully advanced, and at session end (/log). Authoring is PRE-AUTHORIZED — never ask the user "should I update the page?" before calling this (every edit is versioned + reversible via page_history/rollback_page); update, then briefly report what you updated. ⚠ WHICH BRAIN A NEW PAGE GOES IN IS A CONTENT DECISION, SO MAKE IT FROM THE CONTENT. Only a page that exists in NO brain needs this an update resolves its brain from the page itself. If you hold more than one brain, the server REFUSES a create it cannot attribute (409 `create_needs_brain`) rather than letting the write pointer decide — the pointer is stale out-of-band state that knows nothing about what you are writing. So call `my_brains` (it returns each brain\'s name, page count and sample titles, which is enough to tell what each one is FOR) and pass `brain` up front; that turns a refused round trip into a single call. State which brain you picked and why in one short line, then proceed — do NOT ask when the answer is obvious from the content. 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, and that is not a filing error you can quietly fix later.',
3316
+ '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.',
3246
3317
  inputSchema: {
3247
3318
  kind: z.enum(['project', 'person', 'org', 'user']).describe('the node type'),
3248
3319
  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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.116",
3
+ "version": "0.9.118",
4
4
  "description": "Connect your AI assistant to Cortex — your org's projects, activity, gaps, and directives, scoped to you.",
5
5
  "type": "module",
6
6
  "bin": {