@theronap/cortex-mcp 0.9.101 → 0.9.103
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 +86 -8
- package/package.json +1 -1
package/lib/server.mjs
CHANGED
|
@@ -1436,9 +1436,10 @@ export async function runServer(version) {
|
|
|
1436
1436
|
tier: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('only when the page exists at MORE THAN ONE tier — which one to rename in. A rename never moves content between tiers.'),
|
|
1437
1437
|
ordinal: z.number().optional().describe('only when the same heading appears more than once on the page — which occurrence to rename (the error lists the ordinals).'),
|
|
1438
1438
|
reason: z.string().optional().describe('why you are renaming it — recorded in page_history like any other edit'),
|
|
1439
|
+
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 — e.g. the subject is publishing their OWN contact detail on their own page. Leave unset otherwise; the alternatives the refusal names (a confidential section, or set_routing_identifier for a private claim) are the right answer in every other case.'),
|
|
1439
1440
|
},
|
|
1440
1441
|
},
|
|
1441
|
-
async ({ name, from, to, base_version, tier, ordinal, reason }) => {
|
|
1442
|
+
async ({ name, from, to, base_version, tier, ordinal, reason, identity_claim_ack }) => {
|
|
1442
1443
|
let res
|
|
1443
1444
|
try {
|
|
1444
1445
|
res = await fetchCortex(`${BASE}/api/brain/rename-section`, {
|
|
@@ -1447,7 +1448,7 @@ export async function runServer(version) {
|
|
|
1447
1448
|
body: JSON.stringify({
|
|
1448
1449
|
name, from, to, base_version,
|
|
1449
1450
|
...(tier ? { tier } : {}), ...(ordinal !== undefined ? { ordinal } : {}),
|
|
1450
|
-
...(reason ? { reason } : {}),
|
|
1451
|
+
...(reason ? { reason } : {}), ...(identity_claim_ack ? { identity_claim_ack: true } : {}),
|
|
1451
1452
|
}),
|
|
1452
1453
|
})
|
|
1453
1454
|
} catch (e) {
|
|
@@ -1459,6 +1460,7 @@ export async function runServer(version) {
|
|
|
1459
1460
|
// rather than something to retry blindly.
|
|
1460
1461
|
const extra = [
|
|
1461
1462
|
out?.detail ? `existing: ${out.detail}` : '',
|
|
1463
|
+
Array.isArray(out?.identityClaims) ? `identity identifiers this would publish: ${out.identityClaims.join(', ')}` : '',
|
|
1462
1464
|
Array.isArray(out?.tiers) ? `tiers: ${out.tiers.join(', ')}` : '',
|
|
1463
1465
|
Array.isArray(out?.ordinals) ? `ordinals: ${out.ordinals.join(', ')}` : '',
|
|
1464
1466
|
out?.currentVersion ? `current version: ${out.currentVersion}` : '',
|
|
@@ -1481,9 +1483,10 @@ export async function runServer(version) {
|
|
|
1481
1483
|
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.'),
|
|
1482
1484
|
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.'),
|
|
1483
1485
|
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.'),
|
|
1486
|
+
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 — e.g. the subject is publishing their OWN contact detail on their own page. Leave unset otherwise; the alternatives the refusal names (a confidential section, or set_routing_identifier for a private claim) are the right answer in every other case.'),
|
|
1484
1487
|
},
|
|
1485
1488
|
},
|
|
1486
|
-
async ({ name, summary, base_version, reason, tier }) => {
|
|
1489
|
+
async ({ name, summary, base_version, reason, tier, identity_claim_ack }) => {
|
|
1487
1490
|
let res
|
|
1488
1491
|
try {
|
|
1489
1492
|
res = await fetchCortex(`${BASE}/api/brain/set-summary`, {
|
|
@@ -1492,6 +1495,7 @@ export async function runServer(version) {
|
|
|
1492
1495
|
body: JSON.stringify({
|
|
1493
1496
|
name, summary, base_version,
|
|
1494
1497
|
...(tier ? { tier } : {}), ...(reason ? { reason } : {}),
|
|
1498
|
+
...(identity_claim_ack ? { identity_claim_ack: true } : {}),
|
|
1495
1499
|
}),
|
|
1496
1500
|
})
|
|
1497
1501
|
} catch (e) {
|
|
@@ -1503,6 +1507,7 @@ export async function runServer(version) {
|
|
|
1503
1507
|
// rather than something to retry blindly.
|
|
1504
1508
|
const extra = [
|
|
1505
1509
|
out?.detail ? `detail: ${out.detail}` : '',
|
|
1510
|
+
Array.isArray(out?.identityClaims) ? `identity identifiers this would publish: ${out.identityClaims.join(', ')}` : '',
|
|
1506
1511
|
Array.isArray(out?.tiers) ? `tiers: ${out.tiers.join(', ')}` : '',
|
|
1507
1512
|
out?.currentVersion ? `current version: ${out.currentVersion}` : '',
|
|
1508
1513
|
].filter(Boolean).join(' · ')
|
|
@@ -1528,9 +1533,10 @@ export async function runServer(version) {
|
|
|
1528
1533
|
reason: z.string().describe('WHY you are making this change, in one short phrase — recorded in page_history exactly like an author edit.'),
|
|
1529
1534
|
tier: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('only when the page exists at MORE THAN ONE tier — which one to edit. An edit never moves content between tiers.'),
|
|
1530
1535
|
ordinal: z.number().optional().describe('only when the same heading appears more than once on the page — which occurrence (the error lists the ordinals).'),
|
|
1536
|
+
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 — e.g. the subject is publishing their OWN contact detail on their own page. Leave unset otherwise; the alternatives the refusal names (a confidential section, or set_routing_identifier for a private claim) are the right answer in every other case.'),
|
|
1531
1537
|
},
|
|
1532
1538
|
},
|
|
1533
|
-
async ({ name, heading, old_string, new_string, base_version, reason, tier, ordinal }) => {
|
|
1539
|
+
async ({ name, heading, old_string, new_string, base_version, reason, tier, ordinal, identity_claim_ack }) => {
|
|
1534
1540
|
let res
|
|
1535
1541
|
try {
|
|
1536
1542
|
res = await fetchCortex(`${BASE}/api/brain/edit-page`, {
|
|
@@ -1539,6 +1545,7 @@ export async function runServer(version) {
|
|
|
1539
1545
|
body: JSON.stringify({
|
|
1540
1546
|
name, heading, old_string, new_string, base_version, reason,
|
|
1541
1547
|
...(tier ? { tier } : {}), ...(ordinal !== undefined ? { ordinal } : {}),
|
|
1548
|
+
...(identity_claim_ack ? { identity_claim_ack: true } : {}),
|
|
1542
1549
|
}),
|
|
1543
1550
|
})
|
|
1544
1551
|
} catch (e) {
|
|
@@ -1554,6 +1561,7 @@ export async function runServer(version) {
|
|
|
1554
1561
|
Array.isArray(out?.ordinals) ? `ordinals: ${out.ordinals.join(', ')}` : '',
|
|
1555
1562
|
out?.count ? `matches: ${out.count}` : '',
|
|
1556
1563
|
out?.whitespaceNear === true ? 'YOUR TEXT IS PRESENT but the whitespace differs — re-copy it from the stored body' : '',
|
|
1564
|
+
Array.isArray(out?.identityClaims) ? `identity identifiers this edit would publish: ${out.identityClaims.join(', ')}` : '',
|
|
1557
1565
|
out?.currentVersion ? `current version: ${out.currentVersion}` : '',
|
|
1558
1566
|
].filter(Boolean).join('\n')
|
|
1559
1567
|
const cur = out?.currentSectionBody
|
|
@@ -2120,7 +2128,48 @@ export async function runServer(version) {
|
|
|
2120
2128
|
const out = await res.json().catch(() => null)
|
|
2121
2129
|
if (!res.ok) return toolError(`Could not alias "${name}": ${out?.error ?? res.status}`)
|
|
2122
2130
|
if (!out) return { content: [{ type: 'text', text: `Aliased "${name}", but the server returned no body — re-read the page to confirm.` }] }
|
|
2123
|
-
|
|
2131
|
+
// Render what the server actually OBSERVED, never a fixed sentence. The old copy asserted both
|
|
2132
|
+
// "now resolves to X" and "out of the wanted-page backlog" unconditionally — it said the second
|
|
2133
|
+
// about a probe name nothing referenced, and said the first for a month about three aliases a
|
|
2134
|
+
// page-less node was shadowing (#687). The route now checks both and reports them.
|
|
2135
|
+
const lines = [`Aliased [[${out.alias}]] -> "${out.target}" (${out.target_kind}).`]
|
|
2136
|
+
if (out.resolves === true) lines.push('Verified: the name resolves to that page now.')
|
|
2137
|
+
else if (out.resolves === false) lines.push('\u26a0 WRITTEN BUT NOT RESOLVING — the alias row is saved, yet the name still does not lead to that page, so something is shadowing it. Re-running this will not help; report it rather than retrying.')
|
|
2138
|
+
else lines.push('Could not verify resolution on this call — re-read the page to confirm.')
|
|
2139
|
+
lines.push(out.backlogCleared
|
|
2140
|
+
? "It's out of the wanted-page backlog."
|
|
2141
|
+
: 'It was not in the wanted-page backlog, so nothing was cleared there.')
|
|
2142
|
+
return { content: [{ type: 'text', text: lines.join(' ') }] }
|
|
2143
|
+
},
|
|
2144
|
+
)
|
|
2145
|
+
|
|
2146
|
+
server.registerTool(
|
|
2147
|
+
'unalias_page',
|
|
2148
|
+
{
|
|
2149
|
+
title: 'Withdraw an alias',
|
|
2150
|
+
description: 'Remove a name -> page redirect created by `alias_page`, when the alias was wrong or is no longer wanted. The name stops resolving to that page and goes BACK into the org\'s wanted-page backlog, so it can be authored or re-aliased. Aliasing used to be one-way — a mistaken redirect silently sent every future reader of that name somewhere else, with no way back. This does NOT touch the target page itself, only the redirect.',
|
|
2151
|
+
inputSchema: {
|
|
2152
|
+
name: z.string().describe('the aliased [[Name]] to stop redirecting'),
|
|
2153
|
+
},
|
|
2154
|
+
},
|
|
2155
|
+
async ({ name }) => {
|
|
2156
|
+
let res
|
|
2157
|
+
try {
|
|
2158
|
+
res = await fetchCortex(`${BASE}/api/brain/alias?name=${encodeURIComponent(name)}`, {
|
|
2159
|
+
method: 'DELETE',
|
|
2160
|
+
headers: { Authorization: `Bearer ${TOKEN}` },
|
|
2161
|
+
})
|
|
2162
|
+
} catch (e) {
|
|
2163
|
+
return toolError(`Could not withdraw the alias: ${e.message}`)
|
|
2164
|
+
}
|
|
2165
|
+
const out = await res.json().catch(() => null)
|
|
2166
|
+
if (!res.ok) return toolError(`Could not withdraw "${name}": ${out?.message ?? out?.error ?? res.status}`)
|
|
2167
|
+
if (!out) return { content: [{ type: 'text', text: `Withdrew the alias for "${name}", but the server returned no body — re-read the page to confirm.` }] }
|
|
2168
|
+
const lines = [`Withdrew the alias [[${out.alias}]] in ${out.brain}.`]
|
|
2169
|
+
lines.push(out.backlogReopened
|
|
2170
|
+
? 'The name is back in the wanted-page backlog.'
|
|
2171
|
+
: 'It was not marked authored in the backlog, so nothing there changed.')
|
|
2172
|
+
return { content: [{ type: 'text', text: lines.join(' ') }] }
|
|
2124
2173
|
},
|
|
2125
2174
|
)
|
|
2126
2175
|
|
|
@@ -2929,10 +2978,11 @@ export async function runServer(version) {
|
|
|
2929
2978
|
base_version: z.string().optional().describe('the `version` hash shown when you read this page (read_page) — REQUIRED when updating an existing page, so a concurrent edit is caught instead of clobbered. Omit only for a brand-new node. If the save returns "stale" or "read first", read_page again and retry with the fresh version.'),
|
|
2930
2979
|
reason: z.string().describe('WHY you are making this edit, in one short phrase — recorded permanently in page_history so a later reader can tell a routine addition from a correction. Say what CHANGED and what prompted it ("Ben pilot abandoned per Theron 07-17", "corrected: 0069 already widened the CHECK"), not what you did ("updated page"). This is the field that makes staleness auditable.'),
|
|
2931
2980
|
change_kind: z.enum(['add', 'correct', 'supersede', 'expand', 'retire']).optional().describe('what KIND of edit: "add" (new information), "correct" (the page said something FALSE — the currency-critical one), "supersede" (was true, now outdated by events), "expand" (elaborates, no claim changed), "retire" (putting the page or a section to rest). Be honest with "correct" — a page whose history shows repeated corrections is a page whose claims need checking, and that signal is the point.'),
|
|
2981
|
+
identity_claim_ack: z.boolean().optional().describe('ONLY after a 409 identity_claim_unacked refusal, and only if the answer is genuinely yes: this page is meant to publish an email address or phone number as page content at this tier — e.g. the subject is publishing their OWN contact detail on their own page. Leave unset otherwise; the alternatives the refusal names (a confidential section, or set_routing_identifier for a private claim) are the right answer in every other case.'),
|
|
2932
2982
|
brain: z.string().optional().describe('which brain a genuinely NEW page is created in — a brain name or its org id. Choose by RELEVANCE to what you are writing (`my_brains` shows what each brain holds), not by the active pointer. Has top precedence, so it also disambiguates a page name you hold in several brains. Unnecessary when the brain is resolvable from the write itself (base_version, or an existing page of this name) and unnecessary when you only have one brain.'),
|
|
2933
2983
|
},
|
|
2934
2984
|
},
|
|
2935
|
-
async ({ kind, name, summary, sections, tier, base_version, reason, change_kind, brain }) => {
|
|
2985
|
+
async ({ kind, name, summary, sections, tier, base_version, reason, change_kind, brain, identity_claim_ack }) => {
|
|
2936
2986
|
// No client-side tier default — the server computes the per-kind safe default (page-privacy
|
|
2937
2987
|
// T4/D10) so version-pinned installs can't bake a stale policy.
|
|
2938
2988
|
const pages = [{ ...(tier ? { tier } : {}), summary, base_version, sections: Array.isArray(sections) ? sections : [] }]
|
|
@@ -2944,7 +2994,7 @@ export async function runServer(version) {
|
|
|
2944
2994
|
// `brain` is forwarded only when the caller named one. The server's resolveAuthorBrain gives
|
|
2945
2995
|
// an explicit brain top precedence and 409s on an unknown one rather than falling back to
|
|
2946
2996
|
// the pointer, so sending an empty value would turn "I did not choose" into "I chose wrong".
|
|
2947
|
-
body: JSON.stringify({ kind, name, pages, reason, change_kind, ...(brain ? { brain } : {}) }),
|
|
2997
|
+
body: JSON.stringify({ kind, name, pages, reason, change_kind, ...(brain ? { brain } : {}), ...(identity_claim_ack ? { identity_claim_ack: true } : {}) }),
|
|
2948
2998
|
})
|
|
2949
2999
|
} catch (e) {
|
|
2950
3000
|
return toolError(`Could not author "${name}": ${e.message}`)
|
|
@@ -2971,6 +3021,23 @@ export async function runServer(version) {
|
|
|
2971
3021
|
`\n\nRe-run author with brain:"<name>" — choose by what each brain HOLDS, not by its name.`,
|
|
2972
3022
|
)
|
|
2973
3023
|
}
|
|
3024
|
+
// IDENTITY-CLAIM REFUSAL — carries the identifiers and the three ways out. classify() would
|
|
3025
|
+
// flatten this to "409", which is the one failure mode that must NOT be generic: an agent that
|
|
3026
|
+
// cannot see WHICH address it tried to publish, or that a private alternative exists, will
|
|
3027
|
+
// simply re-send with the ack. The refusal has to teach, or it just trains the bypass.
|
|
3028
|
+
if (err?.error === 'identity_claim_unacked') {
|
|
3029
|
+
const claims = Array.isArray(err.claims) ? err.claims.join(', ') : ''
|
|
3030
|
+
return toolError(
|
|
3031
|
+
`Refused — NOTHING was written to "${name}".` +
|
|
3032
|
+
`\n\nThis write puts ${claims ? `${claims} ` : 'an identity identifier '}into the page BODY at the ${err.tier} tier.` +
|
|
3033
|
+
` A body claim is page TEXT governed by that tier — it is NOT the private, claimant-read` +
|
|
3034
|
+
` routing claim — so everyone who can read this page can read the address or phone number.` +
|
|
3035
|
+
`\n\nPick one:` +
|
|
3036
|
+
`\n • the subject is publishing their OWN contact detail → re-send with identity_claim_ack: true` +
|
|
3037
|
+
`\n • it belongs on the page but not to everyone → put it in a confidential section` +
|
|
3038
|
+
`\n • you just want your own records to join here → set_routing_identifier (private to you)`,
|
|
3039
|
+
)
|
|
3040
|
+
}
|
|
2974
3041
|
const d = classify(res.status, res.headers.get('content-type'), raw, res.headers.get('x-vercel-id'))
|
|
2975
3042
|
return toolError(`Could not author "${name}": ${d.message}`)
|
|
2976
3043
|
}
|
|
@@ -3011,8 +3078,19 @@ export async function runServer(version) {
|
|
|
3011
3078
|
` STATUS ("X is live", "Y is not merged"), add the date inline with edit_page while you still` +
|
|
3012
3079
|
` hold the context. The write already landed; this is advisory.`
|
|
3013
3080
|
: ''
|
|
3081
|
+
// PARTIAL-WRITE REFUSAL. A multi-tier call where one tier landed and another was gated returns
|
|
3082
|
+
// 200 (per the route's mixed-outcome rule), so the 409 branch never runs and the refusal would
|
|
3083
|
+
// ride silently in `skipped` — which this success path does not print. That is the exact
|
|
3084
|
+
// computed-but-never-printed failure the KWA-26 note below is about, and a privacy refusal is
|
|
3085
|
+
// the worst thing to lose it on: the agent would read "Authored" and believe the claim landed.
|
|
3086
|
+
const identityRefused = Array.isArray(out?.identityClaimTiers) && out.identityClaimTiers.length
|
|
3087
|
+
? `\n⚠ REFUSED at ${out.identityClaimTiers.map((t) => `${t.tier} (${t.claims.join(', ')})`).join('; ')}` +
|
|
3088
|
+
` — that tier was NOT written. A body claim is page text at that tier, readable by everyone who` +
|
|
3089
|
+
` can read the page. Re-send with identity_claim_ack: true only if the subject is publishing` +
|
|
3090
|
+
` their own contact detail; otherwise use a confidential section or set_routing_identifier.`
|
|
3091
|
+
: ''
|
|
3014
3092
|
const verb = out?.created ? 'Created + authored' : 'Authored'
|
|
3015
|
-
const note = out?.built ? `${verb} "${name}" (${out.built} tier${out.built === 1 ? '' : 's'}). Links: ${blue} resolved, ${retired} retired, ${red} red.${corrections}${retiredList}${redList}${stamps}${undated}`
|
|
3093
|
+
const note = out?.built ? `${verb} "${name}" (${out.built} tier${out.built === 1 ? '' : 's'}). Links: ${blue} resolved, ${retired} retired, ${red} red.${corrections}${retiredList}${redList}${stamps}${undated}${identityRefused}`
|
|
3016
3094
|
: `No change to "${name}"${out?.skipped?.length ? ` (${out.skipped.join(', ')})` : ''}.${corrections}`
|
|
3017
3095
|
return { content: [{ type: 'text', text: note }] }
|
|
3018
3096
|
},
|
package/package.json
CHANGED