@theronap/cortex-mcp 0.9.123 → 0.9.125

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/diagnose.mjs CHANGED
@@ -155,6 +155,7 @@ export function classify(status, contentType, bodyText, requestId) {
155
155
  let appHint = null
156
156
  let appMessage = null
157
157
  let appBrains = null
158
+ let appCandidates = null
158
159
  if (isJson) {
159
160
  try {
160
161
  const parsed = JSON.parse(bodyText)
@@ -166,6 +167,9 @@ export function classify(status, contentType, bodyText, requestId) {
166
167
  // and could not answer it. Measured on the CLI twin of this bug: `✗ brain_required`, full stop.
167
168
  appMessage = typeof parsed?.message === 'string' && parsed.message.trim() ? parsed.message.trim() : null
168
169
  appBrains = Array.isArray(parsed?.brains) ? parsed.brains : null
170
+ // ambiguous_brain carries the COLLIDING subset separately. Prefer it: listing all of
171
+ // someone's brains when only two share the name buries the answer in the noise.
172
+ appCandidates = Array.isArray(parsed?.candidates) ? parsed.candidates : null
169
173
  // `hint` carries the RECOVERY instruction for the errors an agent is meant to act on, not
170
174
  // just report: 409 would_drop ("re-author including the dropped sections…") and 413 too_large
171
175
  // ("split the section…"). It used to be dropped here — only `error` survived — so the agent
@@ -193,21 +197,32 @@ export function classify(status, contentType, bodyText, requestId) {
193
197
  }
194
198
  }
195
199
  // A brain-choice refusal is ANSWERABLE, so it must arrive as the question it is rather than as a
196
- // code. Deliberately narrow: only these two errors reshape the message, so every other classify()
200
+ // code. Deliberately narrow: only these three errors reshape the message, so every other classify()
197
201
  // output keeps its existing wording (and its tests).
198
- if (isJson && (appError === 'brain_required' || appError === 'unknown_brain') && (appMessage || appBrains)) {
199
- const list = (appBrains ?? []).map((b) => {
202
+ //
203
+ // `ambiguous_brain` was added 2026-08-28 WITH the server change that first emits it. Shipping that
204
+ // server change alone would have been a net REGRESSION here: the fallthrough below renders
205
+ // `appError`, not `appMessage`, so a duplicate brain name would have reached the agent as the bare
206
+ // string "Agnoclast API 409: ambiguous_brain." — strictly less actionable than the unknown_brain it
207
+ // replaced, which this branch already caught. Same defect the CHANGELOG records for `brain_required`.
208
+ const isBrainChoice = appError === 'brain_required' || appError === 'unknown_brain' || appError === 'ambiguous_brain'
209
+ if (isJson && isBrainChoice && (appMessage || appBrains || appCandidates)) {
210
+ // `?.length`, not `??`: an empty candidates array is not nullish, so `??` would have kept it
211
+ // and rendered a refusal that says "the candidates below" above nothing at all.
212
+ const list = (appCandidates?.length ? appCandidates : (appBrains ?? [])).map((b) => {
200
213
  const pages = typeof b?.pageCount === 'number' ? ` — ${b.pageCount} page${b.pageCount === 1 ? '' : 's'}` : ''
201
214
  const titles = Array.isArray(b?.sampleTitles) && b.sampleTitles.length
202
215
  ? `: ${b.sampleTitles.slice(0, 2).join('; ')}` : ''
203
216
  // Name AND id: brain names are NOT unique (one account holds two called "Personal"), so a name
204
217
  // alone can come back as unknown_brain. The id always resolves.
205
- return ` - ${b?.name ?? '(unnamed)'}${pages}${titles}${b?.orgId ? ` [${b.orgId}]` : ''}`
218
+ return ` - ${b?.name ?? b?.brain ?? '(unnamed)'}${pages}${titles}${b?.orgId ? ` [${b.orgId}]` : ''}`
206
219
  })
207
220
  return {
208
221
  kind: 'app', retriable: false,
209
222
  message: `Agnoclast API ${status}: ${appMessage ?? appError}${list.length ? `\n${list.join('\n')}` : ''}` +
210
- `\nRe-run this tool with \`brain\` set to one of the names or ids above.${rid}`,
223
+ `\n${appError === 'ambiguous_brain'
224
+ ? 'Re-run this tool with `brain` set to one of the IDS above. A name is what was ambiguous, so passing a name again cannot resolve it.'
225
+ : 'Re-run this tool with `brain` set to one of the names or ids above.'}${rid}`,
211
226
  }
212
227
  }
213
228
 
package/lib/resolve.mjs CHANGED
@@ -31,7 +31,7 @@ export async function brainsToSweep(base, token, wanted, deps = {}) {
31
31
  const j = await res.json().catch(() => ({}))
32
32
  const brains = Array.isArray(j.brains) ? j.brains : []
33
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.
34
+ // "Personal"), and a duplicate name comes back as ambiguous_brain (409).
35
35
  return brains.filter((b) => b?.orgId).map((b) => ({ orgId: b.orgId, name: b.name ?? b.orgId }))
36
36
  }
37
37
 
package/lib/server.mjs CHANGED
@@ -324,9 +324,14 @@ function renderNudge(payload) {
324
324
  }
325
325
 
326
326
  // Records are unique on the ORG-SCOPED (org_id, dedupe_key) and every segment carries this
327
- // session's dedupe key. Cross-brain segments therefore never collide — that constraint is what
328
- // makes the whole design work — but two aimed at the SAME brain would silently merge and lose
329
- // one. Refuse instead of letting that happen quietly.
327
+ // session's dedupe key, so cross-brain segments never collide AS RECORDS.
328
+ //
329
+ // ⚠ That is only half the path, and reading it as the whole path is what broke segmenting for
330
+ // twelve days. Upstream of records sits a private-intake unit keyed per ACCOUNT, which the
331
+ // org-scoping does not help at all — see `segmentKey` below. Records were never the problem.
332
+ //
333
+ // Two segments aimed at the SAME brain would still silently merge and lose one, at both
334
+ // layers. Refuse instead of letting that happen quietly.
330
335
  const seenBrain = new Set()
331
336
  for (const s of list) {
332
337
  const k = String(s.brain ?? '').trim().toLowerCase()
@@ -371,6 +376,13 @@ function renderNudge(payload) {
371
376
  ...(seg.project ? { project: seg.project } : {}),
372
377
  ...(seg.title ? { title: seg.title } : {}),
373
378
  ...(sessionId ? { sessionId } : {}),
379
+ // ONE session split across brains: every segment carries the SAME `sessionId` on
380
+ // purpose — it is the only thing pairing them. But a private-intake unit is keyed per
381
+ // ACCOUNT, not per brain, so without a discriminator all segments collapse onto ONE
382
+ // unit: the first materializes and the rest fail `materialize_failed`. Sent ONLY when
383
+ // actually segmenting, so a single-brain log keeps its exact previous key and no
384
+ // pending unit is orphaned.
385
+ ...(list.length > 1 && seg.brain ? { segmentKey: String(seg.brain) } : {}),
374
386
  // ADR-0022 deleted the write pointer, so a session-class source routes ONLY by an
375
387
  // explicit brain or a sole membership — anything else STAGES. This tool never sent one,
376
388
  // so every close-out from a multi-brain member landed in staged_records instead of the
@@ -1871,13 +1883,70 @@ function renderNudge(payload) {
1871
1883
  },
1872
1884
  )
1873
1885
 
1886
+ server.registerTool(
1887
+ 'staged_records',
1888
+ {
1889
+ title: 'List staged arrivals — the ones in no brain yet, with their ids',
1890
+ description: "List arrivals that are STAGED: held in no brain at all, because nothing could decide which brain they belong to, or because they carry identifiers the graph has never seen. This is the ONLY way to obtain a `staged_id`. `pending_records` structurally cannot see these (it reads records, and a staged row is not one); `intake_changes` returns ids from a different queue that `place_staged_record` rejects; and the session-start block prints titles without ids. Until this existed, placing a staged arrival required a human to read a uuid off a screen. Returns each arrival's id, title, age and the identifiers BLOCKING it, plus `blocking_summary` — the unclaimed identifiers ranked by how many arrivals each one holds, so you can see which single `set_routing_identifier` releases the most. ⚠ Claiming an identifier UNBLOCKS but does not place: which pages an arrival lands on decides its brain, and therefore who can read it, so that stays a decision someone makes.",
1891
+ inputSchema: {
1892
+ limit: z.number().optional().describe('max arrivals to return (default 50, max 200). `total` always reports the WHOLE queue regardless.'),
1893
+ source: z.string().optional().describe("narrow to one connector kind, e.g. 'email' or 'github'"),
1894
+ },
1895
+ },
1896
+ async ({ limit, source }) => {
1897
+ const qs = new URLSearchParams()
1898
+ if (limit) qs.set('limit', String(limit))
1899
+ if (source) qs.set('source', source)
1900
+ let res
1901
+ try {
1902
+ res = await fetchCortex(`${BASE}/api/staged${qs.toString() ? `?${qs}` : ''}`, {
1903
+ headers: { Authorization: `Bearer ${TOKEN}` },
1904
+ })
1905
+ } catch (e) {
1906
+ return toolError(`Could not list staged arrivals: ${e.message}`)
1907
+ }
1908
+ const out = await res.json().catch(() => null)
1909
+ if (!res.ok) return toolError(`Could not list staged arrivals: ${out?.error ?? res.status}`)
1910
+ if (!out) return toolError('The server returned no body.')
1911
+
1912
+ if (!out.total) {
1913
+ return { content: [{ type: 'text', text: 'Nothing staged — every arrival has reached a brain.' }] }
1914
+ }
1915
+ const lines = [`${out.total} staged arrival(s) — in NO brain yet:`, '']
1916
+ for (const a of out.arrivals ?? []) {
1917
+ // The id goes FIRST on its own line. It is the argument place_staged_record needs and the
1918
+ // entire reason this tool exists; burying it after prose is how the session-start block
1919
+ // managed to list these rows for weeks without making one of them actionable.
1920
+ lines.push(` ${a.stagedId}`)
1921
+ lines.push(` ${a.title ?? '(no subject)'} · ${a.sourceType} · ${a.occurredAt?.slice(0, 10) ?? ''}`)
1922
+ if (a.blockingIdentifiers?.length) {
1923
+ lines.push(` blocked by: ${a.blockingIdentifiers.join(', ')}`)
1924
+ } else {
1925
+ lines.push(' held because no brain could be determined (no unknown identifiers)')
1926
+ }
1927
+ }
1928
+ if ((out.arrivals ?? []).length < out.total) {
1929
+ lines.push('', ` … ${out.total - out.arrivals.length} more not shown — raise \`limit\` to see them.`)
1930
+ }
1931
+ if (out.blockingSummary?.length) {
1932
+ lines.push('', 'Unclaimed identifiers holding the most arrivals — claim one and that many unblock:')
1933
+ for (const b of out.blockingSummary.slice(0, 10)) {
1934
+ lines.push(` ${b.arrivals}x ${b.identifier}`)
1935
+ }
1936
+ lines.push('', 'Claim with set_routing_identifier on the page whose subject HAS that identifier.')
1937
+ }
1938
+ lines.push('', 'Place one with place_staged_record (staged_id + pages). The pages decide the brain, which decides who can read it.')
1939
+ return { content: [{ type: 'text', text: lines.join('\n') }] }
1940
+ },
1941
+ )
1942
+
1874
1943
  server.registerTool(
1875
1944
  'place_staged_record',
1876
1945
  {
1877
1946
  title: 'Place a staged arrival onto pages — the pages decide its brain',
1878
1947
  description: "Place a STAGED arrival — one that is in no brain at all — onto the pages it belongs to, which is also what decides its brain. Session-start lists these separately as `[staged]`, and they are the only rows route_record CANNOT take, because there is no record yet to route: nothing upstream chose a brain for them, deliberately. That is the point (ADR-0038) — the connector used to pick the brain from which mailbox the message arrived through, which is a fact about your email plumbing rather than about the message, and it decided WHO COULD READ IT before anyone had read it. Here the pages decide instead. ⚠ PLACING IS A DISCLOSURE DECISION, not just filing: a brain is the confidentiality boundary, so putting a staged message into a shared brain makes it readable by every member of that brain. Say so when you offer, and never place a personal message into a shared brain without the owner\'s explicit answer. All the pages must live in ONE brain — a record exists in exactly one — and pages spanning two brains are refused by name rather than resolved by picking. On success the content is replayed through the real ingest pipeline into that brain, so the record it produces is identical to one that had landed there directly, and then it is attached.",
1879
1948
  inputSchema: {
1880
- staged_id: z.string().describe('the staged id, as the [staged] rows at session start show it'),
1949
+ staged_id: z.string().describe('the staged id — get it from `staged_records`, which is the only surface that prints one (session-start lists these arrivals but not their ids)'),
1881
1950
  pages: z.array(z.string()).describe("pages to place it on — a brain_documents id or the `ref:` read_page prints. They must all be in ONE brain; that brain is where the record lands."),
1882
1951
  reason: z.string().describe('WHY these pages — recorded with the attachment, and the one thing that cannot be inferred later'),
1883
1952
  },
@@ -2605,6 +2674,57 @@ function renderNudge(payload) {
2605
2674
  },
2606
2675
  )
2607
2676
 
2677
+ server.registerTool(
2678
+ 'rename_page',
2679
+ {
2680
+ title: 'Give a page a correct name',
2681
+ description: "Change the TITLE of an existing page, keeping the page itself — every record, attachment, routing claim, grant and revision on it survives untouched, because they all point at the node id and nothing anywhere points at a name. Use it on the stub names automatic minting produces from an email localpart or a display-name header: `Bna2005` -> `Brandon Andersen`, `Theriv` -> `The Riviera`, `Kentgee` -> `Kent Gee`. NOT COSMETIC — a stub carrying a bad name will not dedupe against the real person when they are authored properly, so renaming it is how you PREVENT the duplicate that would otherwise have to be merged later. The old name is kept as an alias automatically, so existing [[links]] and readers who know the old name still resolve. Renaming ONTO a name another page already holds is refused, naming the holder: combining two pages is a merge decision, not a rename. `user` pages cannot be renamed (that is an account identity).",
2682
+ inputSchema: {
2683
+ kind: z.enum(['project', 'person', 'org']).describe('the page kind'),
2684
+ name: z.string().optional().describe('current page title (or pass ref)'),
2685
+ ref: z.string().optional().describe('node ref from read_page — prefer over name when available, and REQUIRED when the current name is ambiguous across brains'),
2686
+ brain: z.string().optional().describe('brain label when the title is ambiguous across brains'),
2687
+ new_name: z.string().describe('the correct title, written the way it should read on the page'),
2688
+ },
2689
+ },
2690
+ async ({ kind, name, ref, brain, new_name }) => {
2691
+ if (!name && !ref) return toolError('Pass the page by `name` or `ref`.')
2692
+ let res
2693
+ try {
2694
+ res = await fetchCortex(`${BASE}/api/brain/rename-page`, {
2695
+ method: 'POST',
2696
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
2697
+ body: JSON.stringify({ kind, newName: new_name, ...(name ? { name } : {}), ...(ref ? { ref } : {}), ...(brain ? { brain } : {}) }),
2698
+ })
2699
+ } catch (e) {
2700
+ return toolError(`Could not rename: ${e.message}`)
2701
+ }
2702
+ const out = await res.json().catch(() => null)
2703
+ if (!res.ok) {
2704
+ // An ambiguous name is the case most likely to hit here, because a stub name that needs
2705
+ // fixing is exactly the kind of name that got minted more than once. Hand back the refs.
2706
+ if (out?.error === 'ambiguous_page' && Array.isArray(out.candidates)) {
2707
+ const rows = out.candidates.map((c) => ` • ${c.brain} — ref ${c.ref}`).join('\n')
2708
+ return toolError(`"${name}" names a page in more than one brain. Re-run with the ref of the one you mean:\n${rows}`)
2709
+ }
2710
+ if (out?.error === 'name_taken') {
2711
+ return toolError(`Not renamed — ${out.detail ?? `"${new_name}" is already held by another page`}. Renaming onto it would silently combine two pages; request a merge instead.`)
2712
+ }
2713
+ if (out?.error === 'unchanged') return toolError(`Not renamed — ${out.detail ?? 'that is the same name'}.`)
2714
+ if (out?.error === 'kind_not_renamable') return toolError(`Not renamed — ${out.detail ?? 'that page kind cannot be renamed'}.`)
2715
+ return toolError(`Could not rename: ${out?.detail ?? out?.error ?? res.status}`)
2716
+ }
2717
+ if (!out) return { content: [{ type: 'text', text: `Renamed to "${new_name}", but the server returned no body — re-read the page to confirm.` }] }
2718
+ // Report what the server OBSERVED. `recordsCarried` is the number that answers the question a
2719
+ // caller actually has about a rename ("did the page keep its stuff?"), so it is never omitted —
2720
+ // including when it is 0, which is the truth for a freshly minted stub.
2721
+ const lines = [`Renamed "${out.from}" -> "${out.to}" in ${out.brain} (${out.kind}, ref ${out.ref}).`]
2722
+ lines.push(`${out.recordsCarried} record(s) came along; ${out.documentsRetitled} tier-doc(s) retitled.`)
2723
+ lines.push(`"${out.aliasWritten}" still resolves to this page, so existing [[links]] are not broken.`)
2724
+ return { content: [{ type: 'text', text: lines.join(' ') }] }
2725
+ },
2726
+ )
2727
+
2608
2728
  server.registerTool(
2609
2729
  'unalias_page',
2610
2730
  {
@@ -2795,20 +2915,62 @@ function renderNudge(payload) {
2795
2915
  reason: z.string().describe('why these pages — recorded with the attachment'),
2796
2916
  park: z.boolean().optional().describe('no good home exists; leave it alone rather than guessing'),
2797
2917
  tier: z.number().int().optional().describe('2 when this came from the cheap sweep rather than your own context'),
2918
+ identifierDispositions: z.array(z.any()).optional().describe(
2919
+ 'ADR-0044. REQUIRED when this record carries identifiers no page claims — the refusal names exactly which. '
2920
+ + 'One entry per unresolved identifier, and this is a DIFFERENT question from documentIds. '
2921
+ + '⚠ AN IDENTIFIER BELONGS ON THE PAGE WHOSE SUBJECT *HAS* IT — a person page for their address, a project '
2922
+ + "page for that project's repo. Association is not attribution: a person who WORKS ON a repo does not get "
2923
+ + 'the repo identifier, and a project does not get a contributor\'s email. So the right page is FREQUENTLY '
2924
+ + 'NOT one of the pages you are routing to. Routing asks what this RECORD is about (several pages); this '
2925
+ + 'asks what each IDENTIFIER is an attribute of (one page). '
2926
+ + 'Forms: {identifier, page} to claim on an existing page (id or ref) · '
2927
+ + '{identifier, createPage:{name, kind, brain}} when the thing has no page yet — the usual case for a new '
2928
+ + 'correspondent, and brain is required because pages cannot move between brains · '
2929
+ + '{none:true, identifiers:[...], reason} when they are attributes of nothing worth a page. '
2930
+ + '`none` takes MANY at once by design — a mailing list can carry 190 recipients and disposing of them one '
2931
+ + 'by one would make the record impossible to finish. It SUPPRESSES NOTHING: the same address arriving '
2932
+ + 'tomorrow is asked about again, and the record itself is still filed either way.',
2933
+ ),
2798
2934
  },
2799
2935
  },
2800
- async ({ recordId, documentIds, reason, park, tier }) => {
2936
+ async ({ recordId, documentIds, reason, park, tier, identifierDispositions }) => {
2801
2937
  let res
2802
2938
  try {
2803
2939
  res = await fetchCortex(`${BASE}/api/brain/triage`, {
2804
2940
  method: 'POST',
2805
2941
  headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
2806
- body: JSON.stringify({ action: park ? 'park' : 'route', recordId, documentIds, reason, tier }),
2942
+ body: JSON.stringify({ action: park ? 'park' : 'route', recordId, documentIds, reason, tier, identifierDispositions }),
2807
2943
  })
2808
2944
  } catch (e) {
2809
2945
  return toolError(`Could not route record: ${e.message}`)
2810
2946
  }
2811
2947
  const out = await res.json().catch(() => null)
2948
+ // ADR-0044 refusal. Render it as INSTRUCTIONS, not as an error string: the agent is one call
2949
+ // away from succeeding and needs to know which identifiers and what the rule is. A bare
2950
+ // "identifiers_undisposed" would send it hunting through docs for a contract it was never told.
2951
+ if (!res.ok && out?.error === 'identifiers_undisposed') {
2952
+ const lines = [
2953
+ 'Cannot finish — this record carries identifiers that no page claims:',
2954
+ ...(out.undisposed ?? []).map((i) => ` ${i}`),
2955
+ '',
2956
+ 'Each needs a disposition. An identifier belongs on the page whose SUBJECT HAS it — a person',
2957
+ 'page for their address, a project page for that project\'s repo. Association is not',
2958
+ 'attribution, so this is often NOT a page you are routing the record to.',
2959
+ '',
2960
+ ' {identifier, page} claim on an existing page',
2961
+ ' {identifier, createPage:{name, kind, brain}} the thing has no page yet',
2962
+ ' {none:true, identifiers:[...], reason} attributes of nothing worth a page',
2963
+ '',
2964
+ '`none` takes many at once and suppresses nothing — tomorrow\'s copy is asked again.',
2965
+ ]
2966
+ if (out.claimedSoFar?.length) {
2967
+ lines.push('', `Already claimed this call: ${out.claimedSoFar.map((c) => `${c.identifier} -> ${c.page}`).join(', ')}`)
2968
+ }
2969
+ if (out.errors?.length) {
2970
+ lines.push('', `Refused: ${out.errors.map((e) => `${e.identifier} (${e.error})`).join(', ')}`)
2971
+ }
2972
+ return toolError(lines.join('\n'))
2973
+ }
2812
2974
  if (!res.ok) return toolError(`Could not route record: ${out?.error ?? res.status}${out?.detail ? ` — ${out.detail}` : ''}`)
2813
2975
  if (park) return { content: [{ type: 'text', text: 'Parked. It stays visible and unrouted rather than badly attached.' }] }
2814
2976
  return { content: [{ type: 'text', text: `Routed — attached to ${out?.attached?.length ?? 0} page(s). Recorded as a session judgment, not a rule match.` }] }
package/lib/skills.mjs CHANGED
@@ -320,16 +320,25 @@ export async function syncOrgSkills(opts = {}) {
320
320
  // have, or what to type next. Pure + exported so the shape is unit-testable without a network.
321
321
  export function renderPushError(body, status) {
322
322
  const lines = [body?.message ?? body?.error ?? `HTTP ${status}`]
323
- const brains = Array.isArray(body?.brains) ? body.brains : []
323
+ const ambiguous = body?.error === 'ambiguous_brain'
324
+ // Prefer the COLLIDING subset. On an ambiguity the full list buries the answer: two rows reading
325
+ // "Personal" among five brains is not a choice anyone can make.
326
+ const candidates = Array.isArray(body?.candidates) ? body.candidates : []
327
+ const brains = candidates.length ? candidates : (Array.isArray(body?.brains) ? body.brains : [])
324
328
  if (brains.length) {
325
- lines.push('', ' Your brains:')
329
+ lines.push('', ambiguous ? ' Brains matching that name:' : ' Your brains:')
326
330
  for (const b of brains) {
327
331
  const pages = typeof b?.pageCount === 'number' ? ` — ${b.pageCount} page${b.pageCount === 1 ? '' : 's'}` : ''
328
332
  const titles = Array.isArray(b?.sampleTitles) && b.sampleTitles.length
329
333
  ? `: ${b.sampleTitles.slice(0, 2).join('; ')}` : ''
330
- lines.push(` ${b?.name ?? b?.orgId ?? '(unnamed)'}${pages}${titles}`)
334
+ // ALWAYS print the id. Without it an ambiguity refusal is a dead end: every row reads the same,
335
+ // and the only address that separates them is the one this line was omitting.
336
+ const id = b?.orgId ? ` [${b.orgId}]` : ''
337
+ lines.push(` ${b?.name ?? b?.brain ?? '(unnamed)'}${pages}${titles}${id}`)
331
338
  }
332
- lines.push('', ' Re-run with --brain "<name>".')
339
+ lines.push('', ambiguous
340
+ ? ' Re-run with --brain "<id>" — a name is what was ambiguous, so a name cannot resolve it.'
341
+ : ' Re-run with --brain "<name>".')
333
342
  }
334
343
  return lines.join('\n')
335
344
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.123",
3
+ "version": "0.9.125",
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": {