@theronap/cortex-mcp 0.9.70 → 0.9.71

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.
@@ -22,7 +22,9 @@ export function mergeClaudeMcp(existing, spec, token) {
22
22
  * CAS-protected + snapshotted (page_revisions → page_history/rollback_page).
23
23
  * Deliberately EXCLUDED — these keep prompting: send_imessage (external side effect),
24
24
  * set_page_privacy / set_record_privacy / grant_page_access (visibility widening — the
25
- * unowned-project accessible-default sharp edge, 2026-07-02), rollback_page, decide_page_merge /
25
+ * unowned-project accessible-default sharp edge, 2026-07-02), replace_variant (destroys a page body
26
+ * and deletes a variant row — the one write here that is not merely CAS-protected but genuinely
27
+ * lossy at the row level, so the prompt IS the guard D1 argued for), rollback_page, decide_page_merge /
26
28
  * decide_file_request / request_file / get_file, create_brain / set_active_brain, alias_page,
27
29
  * set_writing_style. */
28
30
  export const CORTEX_ALLOWED_TOOLS = [
package/lib/server.mjs CHANGED
@@ -1469,6 +1469,53 @@ export async function runServer(version) {
1469
1469
  },
1470
1470
  )
1471
1471
 
1472
+ server.registerTool(
1473
+ 'replace_variant',
1474
+ {
1475
+ title: 'Move one tier variant\'s body into another, collapsing the node to one page',
1476
+ description: 'DESTRUCTIVE, and the only sanctioned way to fix a FORKED page. When one node exists at two tiers with different bodies, this moves the SOURCE variant\'s body into the TARGET variant\'s slot and DELETES the source, leaving the node with a single page. The source\'s body WINS — this is not `absorb`, where the target survives; in a fork repair the target is usually the damaged page, so mirroring absorb would keep the damage and delete the good copy. Sections are copied as ROWS, never re-derived from text: re-deriving a body from context is exactly what destroyed 258 sections on 2026-07-18 while sincerely reporting "copied verbatim". BEFORE CALLING: read_page the TARGET and pass its version as target_version — it proves you know which body is about to be overwritten, and a stale or guessed value is rejected rather than silently accepted. If the target tier has NO page, do not use this: the slot is free, so set_page_privacy moves the page there cheaply. Both prior bodies are retained in page history and the operation is reversible via page_history/rollback_page. Owner or editor on BOTH variants; an OWNERLESS target may be overwritten only by an org admin.',
1477
+ inputSchema: {
1478
+ kind: z.enum(['project', 'person', 'org', 'user']).describe('the page kind'),
1479
+ name: z.string().optional().describe('the exact page name (or pass `ref` instead — one of the two is required)'),
1480
+ ref: z.string().optional().describe('the page\'s stable id, printed as `ref:` by read_page. PREFER THIS over name when you have it: a ref is unique across brains, so it addresses exactly one page and never needs a brain to disambiguate it.'),
1481
+ source_tier: z.enum(['accessible', 'scoped', 'confidential']).describe('the variant whose BODY WINS and survives. This variant\'s row is then deleted.'),
1482
+ target_tier: z.enum(['accessible', 'scoped', 'confidential']).describe('the OCCUPIED slot the body lands in. This variant\'s current body is DESTROYED (snapshotted to page history first). The surviving page sits at this tier.'),
1483
+ target_version: z.string().describe('REQUIRED — the TARGET page\'s version, from read_page. Proves you know what is being overwritten. Do not retry a rejection blindly; re-read the target and confirm you are replacing what you think you are.'),
1484
+ brain: z.string().optional().describe('only when the same page name exists in more than one of your brains'),
1485
+ },
1486
+ },
1487
+ async ({ kind, name, ref, source_tier, target_tier, target_version, brain }) => {
1488
+ let res
1489
+ try {
1490
+ res = await fetchCortex(`${BASE}/api/brain/replace-variant`, {
1491
+ method: 'POST',
1492
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
1493
+ body: JSON.stringify({
1494
+ kind, ...(name ? { name } : {}), ...(ref ? { ref } : {}),
1495
+ source_tier, target_tier, target_version, ...(brain ? { brain } : {}),
1496
+ }),
1497
+ })
1498
+ } catch (e) {
1499
+ return toolError(`Could not replace variant: ${e.message}`)
1500
+ }
1501
+ const out = await res.json().catch(() => null)
1502
+ if (!res.ok) {
1503
+ // The engine's rejections carry the remedy in their text (free slot → use set_page_privacy;
1504
+ // hash mismatch → re-read, do not retry blindly). Surface it verbatim rather than paraphrasing.
1505
+ if (out?.error) return toolError(`Could not replace variant: ${out.error}`)
1506
+ const d = classify(res.status, res.headers.get('content-type'), '', res.headers.get('x-vercel-id'))
1507
+ return toolError(`Could not replace variant: ${d.message}`)
1508
+ }
1509
+ if (!out) return { content: [{ type: 'text', text: 'Replace reported success, but the server returned no body — re-read the page before assuming it landed.' }] }
1510
+ return {
1511
+ content: [{
1512
+ type: 'text',
1513
+ text: `Done — "${out.moved.title}": the ${out.moved.from} body now occupies the ${out.moved.to} page (${out.sections} section${out.sections === 1 ? '' : 's'}), and the ${out.moved.from} variant was removed. The node now has ONE variant. ${out.note}`,
1514
+ }],
1515
+ }
1516
+ },
1517
+ )
1518
+
1472
1519
  server.registerTool(
1473
1520
  'grant_page_access',
1474
1521
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.70",
3
+ "version": "0.9.71",
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": {