@theronap/cortex-mcp 0.9.76 → 0.9.77
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/bin/cortex-mcp.mjs +2 -2
- package/lib/diagnose.mjs +28 -0
- package/lib/graphify_sync.mjs +48 -3
- package/lib/resolve.mjs +62 -12
- package/lib/server.mjs +13 -8
- package/package.json +1 -1
package/bin/cortex-mcp.mjs
CHANGED
|
@@ -52,7 +52,7 @@ if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
|
52
52
|
` skills install/repair the managed Agnoclast skills — bundled + org-published (also wired by setup)\n` +
|
|
53
53
|
` skills push <file> publish a SKILL.md to your org (owner/manager/admin)\n` +
|
|
54
54
|
` docs-scan detect new/changed local docs pending Agnoclast authoring (used by /cortex-author-docs)\n` +
|
|
55
|
-
` graphify-sync [path] rebuild the local code graph
|
|
55
|
+
` graphify-sync [path] [--brain <name-or-id>] rebuild the local code graph + log a timeline event\n` +
|
|
56
56
|
` snapshot-context save the exact startup context Agnoclast served to a local snapshot\n` +
|
|
57
57
|
` hydrate UserPromptSubmit hook — inject query-centered context on the first substantive turn\n` +
|
|
58
58
|
` statusline ambient presence line for the Claude Code statusline (local read only)\n` +
|
|
@@ -122,7 +122,7 @@ if (cmd === 'login') {
|
|
|
122
122
|
} else if (cmd === 'resolve') {
|
|
123
123
|
// Entity identity dedup: judge the server-flagged fuzzy duplicate pairs locally via `claude -p`.
|
|
124
124
|
const { runResolve } = await import('../lib/resolve.mjs')
|
|
125
|
-
await runResolve()
|
|
125
|
+
await runResolve(rest)
|
|
126
126
|
const { closeFetch } = await import('../lib/diagnose.mjs')
|
|
127
127
|
await closeFetch()
|
|
128
128
|
} else if (cmd === 'materialize') {
|
package/lib/diagnose.mjs
CHANGED
|
@@ -90,10 +90,19 @@ export function classify(status, contentType, bodyText, requestId) {
|
|
|
90
90
|
const isJson = (contentType ?? '').includes('application/json')
|
|
91
91
|
let appError = null
|
|
92
92
|
let appHint = null
|
|
93
|
+
let appMessage = null
|
|
94
|
+
let appBrains = null
|
|
93
95
|
if (isJson) {
|
|
94
96
|
try {
|
|
95
97
|
const parsed = JSON.parse(bodyText)
|
|
96
98
|
appError = parsed?.error ?? null
|
|
99
|
+
// Same lesson as `hint`, one layer up. brain_choice_response.ts writes a full explanation to
|
|
100
|
+
// `message` and every brain's NAME / PAGE COUNT / SAMPLE TITLES to `brains` — its comment says
|
|
101
|
+
// "THE BODY IS THE ANSWER TO ITS OWN QUESTION", precisely so a model can pick a brain from what
|
|
102
|
+
// each one HOLDS. Only `error` survived here, so the agent got the bare code `brain_required`
|
|
103
|
+
// and could not answer it. Measured on the CLI twin of this bug: `✗ brain_required`, full stop.
|
|
104
|
+
appMessage = typeof parsed?.message === 'string' && parsed.message.trim() ? parsed.message.trim() : null
|
|
105
|
+
appBrains = Array.isArray(parsed?.brains) ? parsed.brains : null
|
|
97
106
|
// `hint` carries the RECOVERY instruction for the errors an agent is meant to act on, not
|
|
98
107
|
// just report: 409 would_drop ("re-author including the dropped sections…") and 413 too_large
|
|
99
108
|
// ("split the section…"). It used to be dropped here — only `error` survived — so the agent
|
|
@@ -120,6 +129,25 @@ export function classify(status, contentType, bodyText, requestId) {
|
|
|
120
129
|
`Vercel firewall / deployment protection on the API route. Re-running setup will not help.${rid}`,
|
|
121
130
|
}
|
|
122
131
|
}
|
|
132
|
+
// A brain-choice refusal is ANSWERABLE, so it must arrive as the question it is rather than as a
|
|
133
|
+
// code. Deliberately narrow: only these two errors reshape the message, so every other classify()
|
|
134
|
+
// output keeps its existing wording (and its tests).
|
|
135
|
+
if (isJson && (appError === 'brain_required' || appError === 'unknown_brain') && (appMessage || appBrains)) {
|
|
136
|
+
const list = (appBrains ?? []).map((b) => {
|
|
137
|
+
const pages = typeof b?.pageCount === 'number' ? ` — ${b.pageCount} page${b.pageCount === 1 ? '' : 's'}` : ''
|
|
138
|
+
const titles = Array.isArray(b?.sampleTitles) && b.sampleTitles.length
|
|
139
|
+
? `: ${b.sampleTitles.slice(0, 2).join('; ')}` : ''
|
|
140
|
+
// Name AND id: brain names are NOT unique (one account holds two called "Personal"), so a name
|
|
141
|
+
// alone can come back as unknown_brain. The id always resolves.
|
|
142
|
+
return ` - ${b?.name ?? '(unnamed)'}${pages}${titles}${b?.orgId ? ` [${b.orgId}]` : ''}`
|
|
143
|
+
})
|
|
144
|
+
return {
|
|
145
|
+
kind: 'app', retriable: false,
|
|
146
|
+
message: `Agnoclast API ${status}: ${appMessage ?? appError}${list.length ? `\n${list.join('\n')}` : ''}` +
|
|
147
|
+
`\nRe-run this tool with \`brain\` set to one of the names or ids above.${rid}`,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
123
151
|
return {
|
|
124
152
|
kind: 'app', retriable: status >= 500,
|
|
125
153
|
message: `Agnoclast API ${status}: ${appError ?? 'unknown error'}.${appHint ? ` ${appHint}` : ''}${rid}`,
|
package/lib/graphify_sync.mjs
CHANGED
|
@@ -26,8 +26,42 @@ function repoFullNameFromRemote(cwd) {
|
|
|
26
26
|
return m ? `${m[1]}/${m[2]}` : null
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// `--brain <name-or-org-id>` — which brain an UNROUTED repo's graph event lands in.
|
|
30
|
+
//
|
|
31
|
+
// Deliberately a flag and nothing cleverer. /api/timeline/graphify routes deterministically by repo
|
|
32
|
+
// (resolveSourceRoute) and only asks when the repo has no route; its comment is blunt about why it
|
|
33
|
+
// must not be guessed: this writes a `records` row unique on (org_id, dedupe_key), so "a repo whose
|
|
34
|
+
// graph updates land in two brains becomes two record sets that never reconcile, and re-pointing
|
|
35
|
+
// later converges on nothing (ADR-0020). It is the one write class where a wrong answer is genuinely
|
|
36
|
+
// unrecoverable." So: no sweep (unlike `resolve`, which is a maintenance pass over everything) and
|
|
37
|
+
// no auto-pick.
|
|
38
|
+
//
|
|
39
|
+
// It does NOT create a source route as a side effect, though that would stop the question recurring:
|
|
40
|
+
// routes are append-only precisely because "re-pointing a live source splits its history
|
|
41
|
+
// irreparably", and a near-irreversible write should not fall out of a CLI flag. This command runs
|
|
42
|
+
// from a per-repo cron/launchd job, so the flag lives in that job's definition — answered once,
|
|
43
|
+
// where it is visible. (Route creation currently has NO client on any surface; that is a separate
|
|
44
|
+
// gap, not this command's to paper over.)
|
|
45
|
+
// Split argv into { cwd, brain }. Pure + exported so the ordering trap below is unit-testable
|
|
46
|
+
// without a git repo, a graphify binary or a network.
|
|
47
|
+
//
|
|
48
|
+
// THE TRAP: argv[0] doubles as the optional repo path, and `--brain`'s VALUE has no leading '-'.
|
|
49
|
+
// Parsed naively, `graphify-sync --brain Personal` reads "Personal" as the path and syncs whatever
|
|
50
|
+
// happens to be there. So the flag and its value are stripped BEFORE the positional check.
|
|
51
|
+
export function parseGraphifyArgs(argv = [], fallbackCwd = process.cwd()) {
|
|
52
|
+
const bIdx = argv.indexOf('--brain')
|
|
53
|
+
const brain = bIdx === -1 ? null : argv[bIdx + 1]
|
|
54
|
+
if (bIdx !== -1 && (!brain || brain.startsWith('-'))) {
|
|
55
|
+
return { error: 'Usage: graphify-sync [path] [--brain <name-or-org-id>]' }
|
|
56
|
+
}
|
|
57
|
+
const rest = bIdx === -1 ? argv : argv.filter((_, i) => i !== bIdx && i !== bIdx + 1)
|
|
58
|
+
return { cwd: rest[0] && !rest[0].startsWith('-') ? rest[0] : fallbackCwd, brain }
|
|
59
|
+
}
|
|
60
|
+
|
|
29
61
|
export async function runGraphifySync(argv = []) {
|
|
30
|
-
const
|
|
62
|
+
const parsed = parseGraphifyArgs(argv)
|
|
63
|
+
if (parsed.error) { process.stderr.write(parsed.error + '\n'); return 1 }
|
|
64
|
+
const { cwd, brain } = parsed
|
|
31
65
|
const TOKEN = process.env.CORTEX_TOKEN
|
|
32
66
|
const BASE = resolveBase(process.env.CORTEX_URL)
|
|
33
67
|
if (!TOKEN) {
|
|
@@ -73,11 +107,22 @@ export async function runGraphifySync(argv = []) {
|
|
|
73
107
|
const res = await fetchCortex(`${BASE}/api/timeline/graphify`, {
|
|
74
108
|
method: 'POST',
|
|
75
109
|
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
76
|
-
body: JSON.stringify({ repo, commitSha, nodeCount, edgeCount, communityCount }),
|
|
110
|
+
body: JSON.stringify({ repo, commitSha, nodeCount, edgeCount, communityCount, ...(brain ? { brain } : {}) }),
|
|
77
111
|
})
|
|
78
112
|
if (!res.ok) {
|
|
79
113
|
const body = await res.text()
|
|
80
|
-
|
|
114
|
+
const d = classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id'))
|
|
115
|
+
process.stderr.write(d.message + '\n')
|
|
116
|
+
// classify names the brains but speaks in MCP terms ("re-run this tool with `brain`"). Say the
|
|
117
|
+
// actual flag, and where to put it — this runs unattended from cron, so the person reading this
|
|
118
|
+
// is looking at a log after the fact, not a prompt.
|
|
119
|
+
if (res.status === 409 && !brain) {
|
|
120
|
+
process.stderr.write(
|
|
121
|
+
`\n${repo} has no routing decision yet, so it cannot be filed without one.\n` +
|
|
122
|
+
`Re-run with: cortex-mcp graphify-sync ${cwd === process.cwd() ? '' : `${cwd} `}--brain "<name-or-org-id>"\n` +
|
|
123
|
+
`and add that flag to this repo's cron/launchd job so it stops asking.\n`,
|
|
124
|
+
)
|
|
125
|
+
}
|
|
81
126
|
return 1
|
|
82
127
|
}
|
|
83
128
|
const payload = await res.json()
|
package/lib/resolve.mjs
CHANGED
|
@@ -8,47 +8,97 @@ import { edgeSafeEnv } from './edge_extract.mjs'
|
|
|
8
8
|
// and pushes decisions back (POST /api/resolve-apply): confident-same → entity_merges, else → rejected
|
|
9
9
|
// so the pair never re-flags. Conservative by construction. Always exits cleanly.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// Which brains to sweep. BOTH endpoints require the brain to be NAMED (ADR-0022 requireBrain —
|
|
12
|
+
// answering a scoped read out of an ARBITRARY brain is the defect that whole class exists to
|
|
13
|
+
// prevent). Until 2026-08-09 this command named none, so a multi-brain caller got 409 on step 1 and
|
|
14
|
+
// the dedup sweep did nothing at all, silently, forever.
|
|
15
|
+
//
|
|
16
|
+
// Naming ONE brain would have been the smaller fix and the wrong one: `resolve` is a MAINTENANCE
|
|
17
|
+
// SWEEP over the user's entities, so doing one brain and reporting success is ADR-0022's other
|
|
18
|
+
// failure — "silently truncating a result set and presenting it as complete". With no --brain we
|
|
19
|
+
// enumerate the caller's brains and sweep EACH, naming it explicitly. Not a guess: every request
|
|
20
|
+
// still names exactly one brain, and all of them actually get done.
|
|
21
|
+
//
|
|
22
|
+
// A sole-brain caller sees today's behaviour: one pass, no flag, no prompt.
|
|
23
|
+
export async function brainsToSweep(base, token, wanted, deps = {}) {
|
|
24
|
+
const fetchFn = deps.fetchCortex ?? fetchCortex
|
|
25
|
+
if (wanted) return [{ orgId: wanted, name: wanted }] // explicit: pass through (name or org id)
|
|
26
|
+
const res = await fetchFn(`${base}/api/brains`, { headers: { Authorization: `Bearer ${token}` } })
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
29
|
+
throw new Error(`could not list your brains — ${d.message}`)
|
|
30
|
+
}
|
|
31
|
+
const j = await res.json().catch(() => ({}))
|
|
32
|
+
const brains = Array.isArray(j.brains) ? j.brains : []
|
|
33
|
+
// Carry the ORG ID, never the name: brain names are NOT unique (this account holds two called
|
|
34
|
+
// "Personal"), and a duplicate name comes back as unknown_brain.
|
|
35
|
+
return brains.filter((b) => b?.orgId).map((b) => ({ orgId: b.orgId, name: b.name ?? b.orgId }))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function runResolve(argv = []) {
|
|
12
39
|
// recursion guard (we spawn `claude --print`; if its Stop hook fires capture, that no-ops on this flag)
|
|
13
40
|
if (process.env.CORTEX_SUMMARIZING) { process.stderr.write('cortex: summarizer subprocess, skipping\n'); return }
|
|
14
41
|
const token = process.env.CORTEX_TOKEN
|
|
15
42
|
if (!token) { process.stderr.write('cortex: CORTEX_TOKEN not set, skipping\n'); return }
|
|
16
43
|
const base = resolveBase(process.env.CORTEX_URL)
|
|
17
44
|
|
|
45
|
+
const bIdx = argv.indexOf('--brain')
|
|
46
|
+
const wanted = bIdx === -1 ? null : argv[bIdx + 1]
|
|
47
|
+
if (bIdx !== -1 && (!wanted || wanted.startsWith('-'))) {
|
|
48
|
+
process.stderr.write('Usage: resolve [--brain <name-or-org-id>]\n'); return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let targets
|
|
52
|
+
try {
|
|
53
|
+
targets = await brainsToSweep(base, token, wanted)
|
|
54
|
+
} catch (e) { process.stderr.write(`cortex: resolve — ${e.message}\n`); return }
|
|
55
|
+
if (!targets.length) { process.stderr.write('cortex: no brains to sweep\n'); return }
|
|
56
|
+
|
|
57
|
+
for (const t of targets) {
|
|
58
|
+
// Label each line with the brain when sweeping several: an unlabelled "merged 3" cannot be acted
|
|
59
|
+
// on, because you cannot tell WHERE three entities just merged.
|
|
60
|
+
await resolveOneBrain(base, token, t, targets.length > 1)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function resolveOneBrain(base, token, brain, labelled) {
|
|
65
|
+
const tag = labelled ? `[${brain.name}] ` : ''
|
|
66
|
+
const qs = `?brain=${encodeURIComponent(brain.orgId)}`
|
|
67
|
+
|
|
18
68
|
// 1. pull the flagged candidate pairs
|
|
19
69
|
let candidates = []
|
|
20
70
|
try {
|
|
21
|
-
const res = await fetchCortex(`${base}/api/resolve-candidates`, { headers: { Authorization: `Bearer ${token}` } })
|
|
71
|
+
const res = await fetchCortex(`${base}/api/resolve-candidates${qs}`, { headers: { Authorization: `Bearer ${token}` } })
|
|
22
72
|
if (!res.ok) {
|
|
23
73
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
24
|
-
process.stderr.write(`cortex: resolve-candidates failed — ${d.message}\n`); return
|
|
74
|
+
process.stderr.write(`cortex: ${tag}resolve-candidates failed — ${d.message}\n`); return
|
|
25
75
|
}
|
|
26
76
|
const j = await res.json().catch(() => ({}))
|
|
27
77
|
candidates = Array.isArray(j.candidates) ? j.candidates : []
|
|
28
|
-
} catch (e) { process.stderr.write(`cortex: resolve fetch failed — ${e.message}\n`); return }
|
|
78
|
+
} catch (e) { process.stderr.write(`cortex: ${tag}resolve fetch failed — ${e.message}\n`); return }
|
|
29
79
|
|
|
30
|
-
if (!candidates.length) { process.stderr.write(
|
|
31
|
-
process.stderr.write(`cortex: judging ${candidates.length} candidate pair(s) locally…\n`)
|
|
80
|
+
if (!candidates.length) { process.stderr.write(`cortex: ${tag}no duplicate candidates to judge\n`); return }
|
|
81
|
+
process.stderr.write(`cortex: ${tag}judging ${candidates.length} candidate pair(s) locally…\n`)
|
|
32
82
|
|
|
33
83
|
// 2. judge locally on the subscription
|
|
34
84
|
const decisions = judgeCandidates(candidates)
|
|
35
|
-
if (decisions === null) { process.stderr.write(
|
|
85
|
+
if (decisions === null) { process.stderr.write(`cortex: ${tag}judge unavailable (is \`claude\` on PATH?) — skipping\n`); return }
|
|
36
86
|
|
|
37
|
-
// 3. apply
|
|
87
|
+
// 3. apply — SAME brain the candidates came from, or the merges land in the wrong graph
|
|
38
88
|
try {
|
|
39
|
-
const res = await fetchCortex(`${base}/api/resolve-apply`, {
|
|
89
|
+
const res = await fetchCortex(`${base}/api/resolve-apply${qs}`, {
|
|
40
90
|
method: 'POST',
|
|
41
91
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
42
92
|
body: JSON.stringify({ decisions }),
|
|
43
93
|
})
|
|
44
94
|
if (res.ok) {
|
|
45
95
|
const j = await res.json().catch(() => ({}))
|
|
46
|
-
process.stderr.write(`cortex: dedup — merged ${j.merged ?? 0}, rejected ${j.rejected ?? 0}, skipped ${j.skipped ?? 0}\n`)
|
|
96
|
+
process.stderr.write(`cortex: ${tag}dedup — merged ${j.merged ?? 0}, rejected ${j.rejected ?? 0}, skipped ${j.skipped ?? 0}\n`)
|
|
47
97
|
} else {
|
|
48
98
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
49
|
-
process.stderr.write(`cortex: resolve-apply failed — ${d.message}\n`)
|
|
99
|
+
process.stderr.write(`cortex: ${tag}resolve-apply failed — ${d.message}\n`)
|
|
50
100
|
}
|
|
51
|
-
} catch (e) { process.stderr.write(`cortex: resolve-apply failed — ${e.message}\n`) }
|
|
101
|
+
} catch (e) { process.stderr.write(`cortex: ${tag}resolve-apply failed — ${e.message}\n`) }
|
|
52
102
|
}
|
|
53
103
|
|
|
54
104
|
// ONE `claude --print` call judges every pair. Returns DedupDecision[] for the apply endpoint, or null
|
package/lib/server.mjs
CHANGED
|
@@ -892,13 +892,15 @@ export async function runServer(version) {
|
|
|
892
892
|
'writing_style',
|
|
893
893
|
{
|
|
894
894
|
title: 'How the user writes (for drafting in their voice)',
|
|
895
|
-
description: 'Returns the user\'s saved writing-style profile so you can DRAFT in their voice (email, message, doc). Call this right before composing anything on their behalf. Self-only — it is always the calling user\'s own profile. If none is saved, it tells you to derive one and save it with set_writing_style.',
|
|
896
|
-
inputSchema: {
|
|
895
|
+
description: 'Returns the user\'s saved writing-style profile so you can DRAFT in their voice (email, message, doc). Call this right before composing anything on their behalf. Self-only — it is always the calling user\'s own profile. If none is saved, it tells you to derive one and save it with set_writing_style. A profile is stored per (user, BRAIN) — it is injected into authoring IN a brain — so if you hold several, name the one you are drafting in.',
|
|
896
|
+
inputSchema: {
|
|
897
|
+
brain: z.string().optional().describe('which brain\'s style profile, by name or org id. Unnecessary when you only have one brain; pass the org id when a name matches more than one of yours'),
|
|
898
|
+
},
|
|
897
899
|
},
|
|
898
|
-
async () => {
|
|
900
|
+
async ({ brain } = {}) => {
|
|
899
901
|
let res
|
|
900
902
|
try {
|
|
901
|
-
res = await fetchCortex(`${BASE}/api/style`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
903
|
+
res = await fetchCortex(`${BASE}/api/style${brain ? `?brain=${encodeURIComponent(brain)}` : ''}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
902
904
|
} catch (e) {
|
|
903
905
|
return toolError(`Could not load writing style: ${e.message}`)
|
|
904
906
|
}
|
|
@@ -916,12 +918,15 @@ export async function runServer(version) {
|
|
|
916
918
|
{
|
|
917
919
|
title: 'Save the user\'s writing-style profile',
|
|
918
920
|
description: 'Save (or update) a description of HOW the user writes — tone, sentence rhythm, structure, formatting habits, signature quirks — derived from prose you have seen them write this session. Store the STYLE, never their private content. Self-only: it always updates the calling user\'s own profile. Pass an empty string to clear it.',
|
|
919
|
-
inputSchema: {
|
|
921
|
+
inputSchema: {
|
|
922
|
+
style_md: z.string().describe('a concise markdown description of the user\'s writing voice (tone/structure/quirks), ~1-2 paragraphs'),
|
|
923
|
+
brain: z.string().optional().describe('which brain to save the profile in, by name or org id. A profile is stored per (user, brain), so this is a real choice when you hold several; pass the org id when a name matches more than one of yours'),
|
|
924
|
+
},
|
|
920
925
|
},
|
|
921
|
-
async ({ style_md }) => {
|
|
926
|
+
async ({ style_md, brain }) => {
|
|
922
927
|
let res
|
|
923
928
|
try {
|
|
924
|
-
res = await fetchCortex(`${BASE}/api/style`, {
|
|
929
|
+
res = await fetchCortex(`${BASE}/api/style${brain ? `?brain=${encodeURIComponent(brain)}` : ''}`, {
|
|
925
930
|
method: 'PUT',
|
|
926
931
|
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
927
932
|
body: JSON.stringify({ style_md }),
|
|
@@ -1159,7 +1164,7 @@ export async function runServer(version) {
|
|
|
1159
1164
|
'list_brain_pages',
|
|
1160
1165
|
{
|
|
1161
1166
|
title: 'List every authored page in one brain',
|
|
1162
|
-
description: 'Enumerate ALL authored pages in ONE brain, by its org id (from my_brains). Unlike my_brains — which ships only a count plus a few sample titles — this returns the FULL page list: one row per node with its kind, validity, tier(s), last-updated date and content hash. Use it to audit a brain, or to VERIFY a brain-to-brain migration — list BOTH brains, diff the page sets, and compare freshness before retiring any original. You can only list a brain you are a member of
|
|
1167
|
+
description: 'Enumerate ALL authored pages in ONE brain, by its org id (from my_brains). Unlike my_brains — which ships only a count plus a few sample titles — this returns the FULL page list: one row per node with its kind, validity, tier(s), last-updated date and content hash. Use it to audit a brain, or to VERIFY a brain-to-brain migration — list BOTH brains, diff the page sets, and compare freshness before retiring any original. You can only list a brain you are a member of, and within it you see only the pages you are cleared to read — a confidential page owned by someone else is not listed, not even by title.',
|
|
1163
1168
|
inputSchema: {
|
|
1164
1169
|
org_id: z.string().describe('the org id of the brain to enumerate (from my_brains)'),
|
|
1165
1170
|
},
|