@theronap/cortex-mcp 0.9.92 → 0.9.94
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 +120 -0
- package/package.json +1 -1
package/lib/server.mjs
CHANGED
|
@@ -461,6 +461,36 @@ export async function runServer(version) {
|
|
|
461
461
|
},
|
|
462
462
|
)
|
|
463
463
|
|
|
464
|
+
server.registerTool(
|
|
465
|
+
'gate2_status',
|
|
466
|
+
{
|
|
467
|
+
title: 'Gate 2 edit-accountability monitor',
|
|
468
|
+
description: 'Read the aggregate-only Gate 2 status for this brain — whether every edit records WHICH SESSION made it. It counts only the REPAIRED write paths (absorb, retier, replace) since the 2026-08-17 fix, because the author path was never broken and would certify a repair it never exercised. Operator and migration writes are excluded: they legitimately have no session, and imputing one would violate the 0093 don\'t-impute rule. It never exposes page titles, refs, reasons or session keys. "regressed" means a repaired path lost its session attribution again and the gate must NOT be closed; "machine_evidence_ready" means enough ordinary-work evidence has accumulated to request a human closure decision.',
|
|
469
|
+
inputSchema: {
|
|
470
|
+
days: z.number().optional().describe('rolling window in days (1-90, default 14)'),
|
|
471
|
+
brain: z.string().optional().describe('brain name or org id when you belong to more than one brain; omit for a sole brain'),
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
async ({ days, brain }) => {
|
|
475
|
+
const qs = new URLSearchParams()
|
|
476
|
+
if (days != null) qs.set('days', String(days))
|
|
477
|
+
if (brain) qs.set('brain', brain)
|
|
478
|
+
const suffix = qs.size ? `?${qs}` : ''
|
|
479
|
+
const res = await fetchCortex(`${BASE}/api/gates/2/status${suffix}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
480
|
+
if (!res.ok) {
|
|
481
|
+
const body = await res.text()
|
|
482
|
+
throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
|
|
483
|
+
}
|
|
484
|
+
const status = await res.json()
|
|
485
|
+
const text = status.status === 'regressed'
|
|
486
|
+
? `\u26a0 Gate 2 has REGRESSED: ${status.unattributedMcpRevisions} MCP-authored revisions since ${status.since} carry NO session (${status.unattributedRepairedRevisions} of them on the repaired absorb/retier/replace paths). Attribution is being dropped again — do not close this gate; find the writer.`
|
|
487
|
+
: status.machineEvidenceReady
|
|
488
|
+
? `Gate 2 machine evidence is ready: ${status.repairedPathSessions} distinct sessions exercised the repaired write paths across ${status.repairedPathRevisions} revisions since ${status.since}, and NONE lost its session. A human should confirm these were ordinary work before closing the gate.`
|
|
489
|
+
: `Gate 2 is still collecting evidence: ${status.repairedPathSessions}/${status.requiredRepairedPathSessions} distinct sessions have exercised the repaired write paths (absorb/retier/replace) since ${status.since}, across ${status.repairedPathRevisions} revisions, 0 unattributed. No gate decision has been made.`
|
|
490
|
+
return { content: [{ type: 'text', text }] }
|
|
491
|
+
},
|
|
492
|
+
)
|
|
493
|
+
|
|
464
494
|
server.registerTool(
|
|
465
495
|
'session_context',
|
|
466
496
|
{
|
|
@@ -886,6 +916,40 @@ export async function runServer(version) {
|
|
|
886
916
|
},
|
|
887
917
|
)
|
|
888
918
|
|
|
919
|
+
server.registerTool(
|
|
920
|
+
'search_spam_email',
|
|
921
|
+
{
|
|
922
|
+
title: 'Search Gmail Spam intentionally',
|
|
923
|
+
description: 'Search a connected Gmail Spam folder only when the user explicitly asks you to find a message that may have been marked as Spam. This is read-only: results are returned for this request only and are NOT added to Agnoclast records, timeline, or future context. Give a specific Gmail search such as a sender, subject words, or date. Normal Gmail sync never reads Spam.',
|
|
924
|
+
inputSchema: {
|
|
925
|
+
query: z.string().min(2).describe('specific Gmail search within Spam, e.g. `from:billing@example.com`, `subject:(appointment reminder)`, or `after:2026/08/01`'),
|
|
926
|
+
account: z.string().email().optional().describe('which connected Gmail account to search when more than one is available'),
|
|
927
|
+
limit: z.number().int().min(1).max(10).optional().describe('maximum matches to return (default 5; max 10)'),
|
|
928
|
+
},
|
|
929
|
+
},
|
|
930
|
+
async ({ query, account, limit }) => {
|
|
931
|
+
const res = await fetchCortex(`${BASE}/api/gmail/search-spam`, {
|
|
932
|
+
method: 'POST',
|
|
933
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
934
|
+
body: JSON.stringify({ query, ...(account ? { account } : {}), ...(limit ? { limit } : {}) }),
|
|
935
|
+
})
|
|
936
|
+
const body = await res.json().catch(() => null)
|
|
937
|
+
if (!res.ok) {
|
|
938
|
+
if (res.status === 409 && Array.isArray(body?.accounts)) {
|
|
939
|
+
return toolError(`Choose a connected Gmail account and call again with account: ${body.accounts.join(', ')}`)
|
|
940
|
+
}
|
|
941
|
+
return toolError(body?.error ?? `Spam search failed (${res.status}).`)
|
|
942
|
+
}
|
|
943
|
+
const rows = Array.isArray(body?.messages) ? body.messages : []
|
|
944
|
+
if (!rows.length) return { content: [{ type: 'text', text: `No Spam matches in ${body?.account ?? 'the connected Gmail account'}. Nothing was saved to Agnoclast.` }] }
|
|
945
|
+
const lines = [`Spam matches in ${body.account} (read-only; not saved to Agnoclast):`]
|
|
946
|
+
for (const m of rows) {
|
|
947
|
+
lines.push(`- ${m.date || 'unknown date'} · ${m.from || 'unknown sender'} · ${m.subject}\n ${m.snippet || '(no preview)'}\n ${m.sourceUri}`)
|
|
948
|
+
}
|
|
949
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] }
|
|
950
|
+
},
|
|
951
|
+
)
|
|
952
|
+
|
|
889
953
|
server.registerTool(
|
|
890
954
|
'grep',
|
|
891
955
|
{
|
|
@@ -2224,6 +2288,62 @@ export async function runServer(version) {
|
|
|
2224
2288
|
},
|
|
2225
2289
|
)
|
|
2226
2290
|
|
|
2291
|
+
server.registerTool(
|
|
2292
|
+
'set_governing_page',
|
|
2293
|
+
{
|
|
2294
|
+
title: 'Choose which attached page sets a record\'s tier',
|
|
2295
|
+
description:
|
|
2296
|
+
'Move a record\'s GOVERNING page — the one attached page whose tier the record takes. A record can sit on several pages, but exactly one of them decides how visible it is; the others confer access without authority (ADR-0027). Use this when a record is on the right pages but the WRONG one is deciding its tier — most often a record governed by your own user node when it plainly belongs to a project. The page must already be attached: run route_record first if it is not, because attaching is a relevance judgement and this is not. It applies immediately in BOTH directions, tightening or widening, because you asking for it IS the human confirmation a widening requires — so read the tier you are moving to before you move. Every move is recorded as a session judgment and is the signal the placement heuristics are calibrated against, which is why the reason matters.',
|
|
2297
|
+
inputSchema: {
|
|
2298
|
+
recordId: z.string().describe('record id (from pending_records or my_records)'),
|
|
2299
|
+
documentId: z.string().describe('document id of the ATTACHED page that should govern the tier'),
|
|
2300
|
+
reason: z.string().describe('why this page should set the tier — recorded, and read as calibration signal'),
|
|
2301
|
+
},
|
|
2302
|
+
},
|
|
2303
|
+
async ({ recordId, documentId, reason }) => {
|
|
2304
|
+
let res
|
|
2305
|
+
try {
|
|
2306
|
+
res = await fetchCortex(`${BASE}/api/brain/triage`, {
|
|
2307
|
+
method: 'POST',
|
|
2308
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
2309
|
+
body: JSON.stringify({ action: 'regovern', recordId, documentId, reason }),
|
|
2310
|
+
})
|
|
2311
|
+
} catch (e) {
|
|
2312
|
+
return toolError(`Could not set the governing page: ${e.message}`)
|
|
2313
|
+
}
|
|
2314
|
+
const out = await res.json().catch(() => null)
|
|
2315
|
+
if (!res.ok) {
|
|
2316
|
+
// A guard rail, not a fault — say what to do instead of naming the code.
|
|
2317
|
+
if (out?.error === 'not_attached') {
|
|
2318
|
+
return toolError(
|
|
2319
|
+
'That page is not attached to this record, so it cannot govern it. Attach it first with route_record.',
|
|
2320
|
+
)
|
|
2321
|
+
}
|
|
2322
|
+
return toolError(
|
|
2323
|
+
`Could not set the governing page: ${out?.error ?? res.status}${out?.detail ? ` — ${out.detail}` : ''}`,
|
|
2324
|
+
)
|
|
2325
|
+
}
|
|
2326
|
+
if (out?.reaffirmed) {
|
|
2327
|
+
return {
|
|
2328
|
+
content: [{
|
|
2329
|
+
type: 'text',
|
|
2330
|
+
text: `That page already governed this record; recorded your confirmation. Tier: ${out?.toPrivacy ?? 'unchanged'}.`,
|
|
2331
|
+
}],
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
const moved =
|
|
2335
|
+
out?.fromPrivacy && out?.toPrivacy && out.fromPrivacy !== out.toPrivacy
|
|
2336
|
+
? `Tier ${out.fromPrivacy} -> ${out.toPrivacy} (${out?.direction}).`
|
|
2337
|
+
: `Tier unchanged (${out?.toPrivacy ?? 'unknown'}).`
|
|
2338
|
+
return {
|
|
2339
|
+
content: [{
|
|
2340
|
+
type: 'text',
|
|
2341
|
+
text: `Governing page moved. ${moved} Recorded as a session judgment${out?.correctedAuto ? ' and counted as a correction to the placement heuristics' : ''}.`,
|
|
2342
|
+
}],
|
|
2343
|
+
}
|
|
2344
|
+
},
|
|
2345
|
+
)
|
|
2346
|
+
|
|
2227
2347
|
server.registerTool(
|
|
2228
2348
|
'snooze_red_link',
|
|
2229
2349
|
{
|