@theronap/agnoclast-mcp 0.9.239 → 0.9.241

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.
@@ -0,0 +1,114 @@
1
+ // `my_contacts` and `contact` — AGNOCLAST'S OWN CONTACT BOOK (ADR-0084; Theron, 2026-10-17).
2
+ //
3
+ // Rendered from GET /api/contacts (or POST /api/contacts/search) and POST /api/contacts/briefing. The
4
+ // book is the caller's alone: every person page they can read, in every brain they are in, merged into
5
+ // one contact when pages share a phone or email the caller can see. A same-name match is NEVER a merge.
6
+ //
7
+ // The contact LIST (app and website) shows names and identifiers only. `contact` is the AI's view: the
8
+ // identifiers AND every page of theirs, told apart by brain, plus the ONE page an outreach is filed on.
9
+
10
+ /** Pure. `(801) 555-0123` for a US number; anything else as stored. */
11
+ export function displayPhone(e164) {
12
+ const m = /^\+1(\d{3})(\d{3})(\d{4})$/.exec(String(e164 ?? ''))
13
+ return m ? `(${m[1]}) ${m[2]}-${m[3]}` : String(e164 ?? '')
14
+ }
15
+
16
+ /** Pure. One identifier, saying who can see it. */
17
+ export function identifierLine(i) {
18
+ const v = i.kind === 'phone' ? displayPhone(i.value) : i.value
19
+ const who = i.shared ? '' : i.mine ? ' (private — only you)' : ''
20
+ return `${v}${who}`
21
+ }
22
+
23
+ /** Pure. Where reaching out to this contact files the record. */
24
+ export function fileToLine(fileTo) {
25
+ if (!fileTo) return ''
26
+ if (fileTo.ok) {
27
+ const why = fileTo.why === 'context' ? 'the brain you are acting in' : 'the only brain they are in'
28
+ return `files to ${fileTo.page.brain} → "${fileTo.page.title}" (${why})`
29
+ }
30
+ if (fileTo.reason === 'ambiguous') return `files to: ASK which brain — ${fileTo.options.map((o) => o.brain).join(' or ')}`
31
+ return 'no page to file on'
32
+ }
33
+
34
+ function reachLine(reach, ringing) {
35
+ if (!reach) return ''
36
+ const bits = []
37
+ if (ringing && reach.call === true) bits.push('Call')
38
+ else if (ringing && reach.call === null) bits.push('Call? (not checked)')
39
+ if (reach.text) bits.push('Text an invite')
40
+ if (reach.email) bits.push('Email')
41
+ return bits.length ? bits.join(' · ') : 'no way to reach them yet'
42
+ }
43
+
44
+ /** Pure. The contact list. */
45
+ export function renderContactList(out, { query } = {}) {
46
+ const contacts = Array.isArray(out?.contacts) ? out.contacts : []
47
+ const brains = Array.isArray(out?.brains) ? out.brains.map((b) => b.brain) : []
48
+ const lines = []
49
+ if (!contacts.length) {
50
+ lines.push(query
51
+ ? `No contact of yours matches "${query}". Your book is every person page you can read${brains.length ? ` in ${brains.join(', ')}` : ''}.`
52
+ : 'Your contact book is empty: no person page you can read has been written yet.')
53
+ } else {
54
+ const total = typeof out.total === 'number' ? out.total : contacts.length
55
+ lines.push(`${total} contact(s)${query ? ` matching "${query}"` : ''}${total > contacts.length ? `, showing ${contacts.length}` : ''} — person pages you can read${brains.length ? ` in ${brains.join(', ')}` : ''}, merged on a shared phone/email:`)
56
+ for (const c of contacts) {
57
+ const ids = (c.identifiers ?? []).map(identifierLine)
58
+ const where = [...new Set((c.pages ?? []).map((p) => p.brain))]
59
+ const parts = [`· ${c.name}`]
60
+ if (ids.length) parts.push(ids.join(' · '))
61
+ parts.push(`in ${where.join(', ')}${(c.pages ?? []).length > 1 ? ` (${c.pages.length} pages)` : ''}`)
62
+ const reach = reachLine(c.reach, out.ringing === true)
63
+ if (reach) parts.push(reach)
64
+ parts.push(fileToLine(c.fileTo))
65
+ lines.push(parts.filter(Boolean).join(' — '))
66
+ lines.push(` page: ${c.id}`)
67
+ }
68
+ }
69
+ const sug = Array.isArray(out?.suggestions) ? out.suggestions : []
70
+ if (sug.length) {
71
+ const nameOf = new Map(contacts.map((c) => [c.id, c]))
72
+ lines.push('', 'SAME NAME, NOT MERGED — no phone or email in common. Ask the person before treating them as one:')
73
+ for (const s of sug) {
74
+ const [a, b] = s.contactIds.map((id) => nameOf.get(id))
75
+ const label = (c, id) => (c ? `${c.name} (${[...new Set(c.pages.map((p) => p.brain))].join(', ')})` : `page ${id}`)
76
+ lines.push(` ${label(a, s.contactIds[0])} / ${label(b, s.contactIds[1])}`)
77
+ }
78
+ }
79
+ for (const e of out?.errors ?? []) lines.push(`⚠ ${e.brain}: could not be read (${e.error}) — its contacts are missing above.`)
80
+ lines.push('', '— Read one with `contact` (page: …) before reaching out: it returns their pages too. A phone number is for showing and "Text an invite" only; a call reaches an account by its email.')
81
+ return lines.join('\n')
82
+ }
83
+
84
+ /** Pure. The AI's briefing on one contact. */
85
+ export function renderContactBriefing(out) {
86
+ const c = out?.contact
87
+ if (!c) return 'No such contact.'
88
+ const lines = [`${c.name}`]
89
+ const phones = (c.identifiers ?? []).filter((i) => i.kind === 'phone')
90
+ const emails = (c.identifiers ?? []).filter((i) => i.kind === 'email')
91
+ lines.push(phones.length ? `Phone: ${phones.map(identifierLine).join(' · ')}` : 'Phone: none you can see')
92
+ lines.push(emails.length ? `Email: ${emails.map(identifierLine).join(' · ')}` : 'Email: none you can see')
93
+ const pages = Array.isArray(out.pages) ? out.pages : []
94
+ const brains = [...new Set(pages.map((p) => p.brain))]
95
+ lines.push(`Reaching out: ${fileToLine(out.fileTo)}`)
96
+ if (out.fileTo && !out.fileTo.ok && out.fileTo.reason === 'ambiguous') {
97
+ lines.push(' → ask the person which brain this is for (or pass `brain`). File the record on that ONE page, not on all of them.')
98
+ }
99
+ lines.push('', pages.length > 1
100
+ ? `${pages.length} pages in ${brains.length} brain(s) — the same person (they share a phone or email), kept as separate pages. Say which brain a fact came from:`
101
+ : 'Their page:')
102
+ for (const p of pages) {
103
+ lines.push('', `## ${p.brain} — "${p.title}" [ref: ${p.ref} · tier: ${p.tier}]`)
104
+ for (const s of p.sections ?? []) lines.push(`### ${s.heading}`, s.body ?? '')
105
+ }
106
+ return lines.join('\n')
107
+ }
108
+
109
+ /** Pure. A name that fits several contacts. */
110
+ export function renderContactCandidates(candidates, name) {
111
+ const list = Array.isArray(candidates) ? candidates : []
112
+ return [`"${name}" fits ${list.length} contacts — pass \`page\` for the one you mean:`,
113
+ ...list.map((c) => ` · ${c.name} (${(c.brains ?? []).join(', ')}) — page: ${c.id}`)].join('\n')
114
+ }
@@ -30,7 +30,7 @@ export function mergeClaudeMcp(existing, spec, token) {
30
30
  export const ALLOWED_TOOL_NAMES = [
31
31
  // read surface
32
32
  'grep', 'read_page', 'my_context', 'project_status', 'session_context', 'search_org',
33
- 'list_records', 'read_record', 'page_history', 'page_diff', 'timeline_pull', 'my_brains', 'my_sessions', 'writing_style',
33
+ 'list_records', 'read_record', 'page_history', 'page_diff', 'timeline_pull', 'my_brains', 'my_sessions', 'token_activity', 'writing_style',
34
34
  'code_graph_query', 'my_retier_notices', 'list_page_grants', 'list_project_members', 'file_requests',
35
35
  // the questions agents used to answer with SQL (2026-10-07) — read-only, the caller's own view
36
36
  'record_status', 'my_connectors', 'brain_stats', 'my_sends', 'find_arrivals',
package/lib/server.mjs CHANGED
@@ -32,6 +32,7 @@ import { formatSeriesRefusal, formatSeriesUpdated } from './series_render.mjs'
32
32
  import { formatSends } from './sends_render.mjs'
33
33
  import { applyLock, fetchLockState, lockedAnswerFor, lockStateWithin } from './locked_tools.mjs'
34
34
  import { filterUnshownSettling } from './settling_shown.mjs'
35
+ import { renderContactBriefing, renderContactCandidates, renderContactList } from './contacts_render.mjs'
35
36
 
36
37
  // PAGE tiers (ADR-0071). Six for a PAGE; a RECORD keeps three (`privacy` on log_session and
37
38
  // set_record_privacy below) — an owner-relative tier is applied to a page's records when they are read
@@ -2363,7 +2364,9 @@ function renderNudge(payload) {
2363
2364
  const revs = out.revisions ?? []
2364
2365
  if (!revs.length) return { content: [{ type: 'text', text: `"${name}" (${k}) has no recorded version history yet.` }] }
2365
2366
  const lines = revs.map((r) => {
2366
- const who = r.actor_name ? ` · ${r.actor_name}` : ''
2367
+ // "Theron via Frack" — the label only ever reaches the token's owner (bdb2339b phase 2).
2368
+ const via = r.via_token_label ? ` via ${r.via_token_label}` : ''
2369
+ const who = r.actor_name ? ` · ${r.actor_name}${via}` : (via ? ` ·${via}` : '')
2367
2370
  // change_kind first and bracketed so a column of [correct] is scannable — the whole point is
2368
2371
  // that a page with repeated corrections looks different at a glance from one that only grew.
2369
2372
  const what = r.change_kind ? ` · [${r.change_kind}]` : ''
@@ -2418,7 +2421,7 @@ function renderNudge(payload) {
2418
2421
  ? `# ${name} — rev ${d.to.revNo} (first version)`
2419
2422
  : `# ${name} — rev ${d.from.revNo} → rev ${d.to.revNo}`
2420
2423
  const meta = []
2421
- meta.push(`${String(d.to.createdAt).slice(0, 10)} · ${d.to.op}${d.to.changeKind ? ` · [${d.to.changeKind}]` : ''}${d.to.actorName ? ` · ${d.to.actorName}` : ''}`)
2424
+ meta.push(`${String(d.to.createdAt).slice(0, 10)} · ${d.to.op}${d.to.changeKind ? ` · [${d.to.changeKind}]` : ''}${d.to.actorName ? ` · ${d.to.actorName}${d.to.viaTokenLabel ? ` via ${d.to.viaTokenLabel}` : ''}` : ''}`)
2422
2425
  if (d.to.reason) meta.push(`reason: ${d.to.reason}`)
2423
2426
  if (d.to.sessionKey) meta.push(`session: ${String(d.to.sessionKey).slice(0, 24)}`)
2424
2427
  const body = []
@@ -4165,6 +4168,76 @@ function renderNudge(payload) {
4165
4168
  },
4166
4169
  )
4167
4170
 
4171
+ // ⭐ AGNOCLAST'S OWN CONTACT BOOK (ADR-0084; Theron, 2026-10-17). The person's book is every person page
4172
+ // they can read, merged on a shared phone/email; `contact` is the briefing an AI reads before reaching out.
4173
+ server.registerTool(
4174
+ 'my_contacts',
4175
+ {
4176
+ title: 'Your contacts — person pages you can read, merged',
4177
+ description: 'The person\'s OWN Agnoclast contact book: every person page they can read, in every brain they are in. Pages in several brains that share a phone or email are ONE contact (same person, pages kept separate); a same-NAME match is listed as a question, never merged. Shows each contact\'s phone numbers and emails (marked when private to the person), the brains they are in, Call / Text an invite / Email, and where reaching out would be filed. Page text is not included — call `contact` for that before you reach out. Pass `query` to search by name, email or digits of a number. A phone number never rings anyone: a call reaches an Agnoclast account by its email.',
4178
+ inputSchema: {
4179
+ query: z.string().optional().describe('a name, an email address, or digits of a phone number'),
4180
+ brain: z.string().optional().describe('the brain you are acting in (name or id) — decides where outreach files for a contact in several brains'),
4181
+ limit: z.number().optional().describe('max contacts (default 50, max 200)'),
4182
+ offset: z.number().optional().describe('skip this many (paging)'),
4183
+ reach: z.boolean().optional().describe('also check who can be CALLED (uses the person\'s call-lookup budget; default false)'),
4184
+ },
4185
+ },
4186
+ async ({ query, brain, limit, offset, reach }) => {
4187
+ const q = typeof query === 'string' ? query.trim() : ''
4188
+ let res
4189
+ try {
4190
+ res = q
4191
+ ? await fetchCortex(`${BASE}/api/contacts/search`, {
4192
+ method: 'POST',
4193
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
4194
+ body: JSON.stringify({ q, brain, limit, offset, reach: reach === true }),
4195
+ })
4196
+ : await fetchCortex(`${BASE}/api/contacts?${new URLSearchParams({
4197
+ ...(brain ? { brain } : {}), ...(limit ? { limit: String(limit) } : {}), ...(offset ? { offset: String(offset) } : {}),
4198
+ ...(reach ? { reach: '1' } : {}),
4199
+ })}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
4200
+ } catch (e) {
4201
+ return toolError(`Could not read your contacts: ${e.message}`)
4202
+ }
4203
+ const out = await res.json().catch(() => null)
4204
+ if (res.status === 404) return toolError('This Agnoclast server has no contact book yet (it predates ADR-0084). Use grep / read_page for person pages.')
4205
+ if (!res.ok) return toolError(`Could not read your contacts: ${out?.error ?? res.status}`)
4206
+ return { content: [{ type: 'text', text: renderContactList(out, { query: q }) }] }
4207
+ },
4208
+ )
4209
+
4210
+ server.registerTool(
4211
+ 'contact',
4212
+ {
4213
+ title: 'One contact — identifiers AND their pages, before you reach out',
4214
+ description: 'Brief yourself on one contact before a call, invite or email: their phone numbers and emails the person may see, EVERY person page of theirs the person can read (one per brain, labelled — the same person, kept separate, so say which brain a fact came from), and the ONE page the outreach should be filed on. If they are in several brains and none is obvious it says ASK: ask the person which brain this is for, then file the record on that one page only. Address them by `page` (from my_contacts, or a person page ref) or by `name`.',
4215
+ inputSchema: {
4216
+ page: z.string().optional().describe('a page id from my_contacts, or a person page ref from read_page'),
4217
+ name: z.string().optional().describe('their name, when you have no page id'),
4218
+ brain: z.string().optional().describe('the brain you are acting in (name or id), if any'),
4219
+ },
4220
+ },
4221
+ async ({ page, name, brain }) => {
4222
+ if (!page && !name) return toolError('Pass page or name')
4223
+ let res
4224
+ try {
4225
+ res = await fetchCortex(`${BASE}/api/contacts/briefing`, {
4226
+ method: 'POST',
4227
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
4228
+ body: JSON.stringify({ page, name, brain }),
4229
+ })
4230
+ } catch (e) {
4231
+ return toolError(`Could not read that contact: ${e.message}`)
4232
+ }
4233
+ const out = await res.json().catch(() => null)
4234
+ if (res.status === 409 && out?.error === 'ambiguous') return { content: [{ type: 'text', text: renderContactCandidates(out.candidates, name ?? page) }] }
4235
+ if (res.status === 404 && out?.error === 'not_found') return { content: [{ type: 'text', text: `No contact of yours matches "${name ?? page}". my_contacts lists them; a person you have never written a page for is not a contact yet.` }] }
4236
+ if (!res.ok) return toolError(`Could not read that contact: ${out?.error ?? res.status}`)
4237
+ return { content: [{ type: 'text', text: renderContactBriefing(out) }] }
4238
+ },
4239
+ )
4240
+
4168
4241
  server.registerTool(
4169
4242
  'set_summary',
4170
4243
  {
@@ -5299,6 +5372,46 @@ function renderNudge(payload) {
5299
5372
  },
5300
5373
  )
5301
5374
 
5375
+ server.registerTool(
5376
+ 'token_activity',
5377
+ {
5378
+ title: 'What this agent token has done',
5379
+ description: "Audit YOUR OWN recent activity through the token you are connected with: tool calls, wiki page edits, record placements and intake discards, newest first. Use it to report back what you did (\"what did I discard today?\"), or to check a run before handing off. Self-only: it reads the CALLING token's activity and never another agent's — the person audits all their tokens from Connect → Agent tokens. Discards are content-free by design, so they name the source, not the item. Errors if you are not connected with an agent token (console sessions have none).",
5380
+ inputSchema: {
5381
+ days: z.number().int().optional().describe('how far back (default 7, max 30)'),
5382
+ kinds: z.array(z.enum(['call', 'edit', 'placement', 'discard'])).optional().describe('only these kinds (default all)'),
5383
+ limit: z.number().int().optional().describe('max items (default 100, max 500)'),
5384
+ },
5385
+ },
5386
+ async ({ days, kinds, limit }) => {
5387
+ let res
5388
+ try {
5389
+ const qs = new URLSearchParams({
5390
+ ...(days ? { days: String(days) } : {}), ...(limit ? { limit: String(limit) } : {}),
5391
+ ...(kinds?.length ? { kinds: kinds.join(',') } : {}),
5392
+ })
5393
+ res = await fetchCortex(`${BASE}/api/me/token-activity?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
5394
+ } catch (e) {
5395
+ return toolError(`Could not read this token's activity: ${e.message}`)
5396
+ }
5397
+ if (res.status === 400) return { content: [{ type: 'text', text: 'You are not connected with an agent token, so there is no per-token activity to show. The person can see every token\'s activity under Connect → Agent tokens.' }] }
5398
+ if (!res.ok) {
5399
+ const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
5400
+ return toolError(`Could not read this token's activity: ${d.message}`)
5401
+ }
5402
+ const out = await res.json()
5403
+ const name = out.token?.label ?? 'this token'
5404
+ const items = out.items ?? []
5405
+ if (!items.length) return { content: [{ type: 'text', text: `${name}: no activity in the last ${days ?? 7} day(s).` }] }
5406
+ const counts = {}
5407
+ for (const i of items) counts[i.kind] = (counts[i.kind] ?? 0) + 1
5408
+ const lines = items.map((i) =>
5409
+ `- ${String(i.at).slice(0, 16).replace('T', ' ')} · ${i.kind} · ${i.what ?? '—'}${i.detail ? ` · ${i.detail}` : ''}${i.reason ? ` — ${i.reason}` : ''}`)
5410
+ const summary = Object.entries(counts).map(([k, n]) => `${n} ${k}`).join(' · ')
5411
+ return { content: [{ type: 'text', text: `# ${name} — activity, last ${days ?? 7} day(s), newest first (UTC)\n${summary}\n\n${lines.join('\n')}` }] }
5412
+ },
5413
+ )
5414
+
5302
5415
  server.registerTool(
5303
5416
  'my_sessions',
5304
5417
  {
@@ -5516,23 +5629,27 @@ function renderNudge(payload) {
5516
5629
  {
5517
5630
  title: 'Claim a routing identifier on a page',
5518
5631
  description: 'Declare that a page is a Gate 4 attach HOME for an identifier (repo:owner/name or file:owner/name:path). Body [[repo:…]] stamps are navigation only and do NOT drive attach — use this instead. Prefer the narrowest id: mother pages own repo:; feature pages own file: paths. Do not copy every body mention into a routing claim. ' +
5519
- 'PRIVATE CONTACT CLAIMS (ADR-0079): on a PERSON page you may claim `phone:<number>` (normalized to E.164, US default — `phone:(801) 636-7505` stores `phone:+18016367505`) or `email:<address>` (lowercased). These are visible ONLY to you — never in the page body, grep, or to other brain members — and they make your captured texts / mail from that contact propose that person page. This is the right place for a contact\'s number; never put it in page text.',
5632
+ 'CONTACT IDENTIFIERS (ADR-0079, ADR-0084): on a PERSON page you may claim `phone:<number>` (E.164, US default — `phone:(801) 636-7505` stores `phone:+18016367505`) or `email:<address>`. A NEW one is SHARED with everyone who can read that page, like a shared CRM (Theron, 2026-10-17), unless you pass `private: true` — then only you see it. Pass `private` explicitly to change one you added before; without it an existing claim keeps its visibility. Either way it is never in the page body or grep, and it files only YOUR captured texts/mail onto the page. This is the right place for a contact\'s number; never put it in page text.',
5520
5633
  inputSchema: {
5521
5634
  kind: z.enum(['project', 'person', 'org', 'user']).describe('the page kind'),
5522
5635
  name: z.string().optional().describe('page title (or pass ref)'),
5523
5636
  ref: z.string().optional().describe('node ref from read_page — prefer over name when available'),
5524
5637
  brain: z.string().optional().describe('brain label when the title is ambiguous across brains'),
5525
- identifier: z.string().describe('canonical identifier, e.g. repo:theronap/cortex, file:theronap/cortex:web/lib/engine/github_intake.ts, or on a person page phone:+18015550123 / email:someone@example.com (private to you)'),
5638
+ identifier: z.string().describe('canonical identifier, e.g. repo:theronap/cortex, file:theronap/cortex:web/lib/engine/github_intake.ts, or on a person page phone:+18015550123 / email:someone@example.com'),
5639
+ private: z.boolean().optional().describe('person-page phone:/email: only. true = only you see it; false = everyone who can read the page. Omit for a new one to be shared and an existing one to stay as it is.'),
5526
5640
  },
5527
5641
  },
5528
- async ({ kind, name, ref, brain, identifier }) => {
5642
+ async ({ kind, name, ref, brain, identifier, private: isPrivate }) => {
5529
5643
  if (!name && !ref) return toolError('Pass name or ref')
5644
+ // ADR-0084: a person page's phone/email is shared with its readers unless the person says private.
5645
+ const contactClaim = kind === 'person' && /^(phone|email):/i.test(String(identifier).trim())
5646
+ const sharing = contactClaim ? { shared: isPrivate !== true, sharing_explicit: typeof isPrivate === 'boolean' } : {}
5530
5647
  let res
5531
5648
  try {
5532
5649
  res = await fetchCortex(`${BASE}/api/brain/routing-identifiers`, {
5533
5650
  method: 'POST',
5534
5651
  headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
5535
- body: JSON.stringify({ kind, name, ref, brain, identifier }),
5652
+ body: JSON.stringify({ kind, name, ref, brain, identifier, ...sharing }),
5536
5653
  })
5537
5654
  } catch (e) {
5538
5655
  return toolError(`Could not set routing identifier: ${e.message}`)
@@ -5558,9 +5675,12 @@ function renderNudge(payload) {
5558
5675
  ? ` ${healed} existing record(s) ALREADY attached to this page as a result.`
5559
5676
  : ' 0 existing records attached — either nothing carries this identifier yet, or records do but belong to another owner (an identity claim only attaches your own).'
5560
5677
  const isContact = /^(phone|email):/.test(String(out?.set?.[0] ?? identifier).toLowerCase())
5561
- const privacyLine = isContact
5562
- ? ' This contact claim is PRIVATE to you: no other brain member can see it, and it is never rendered in the page. Your captured texts/mail from this contact will now propose this page.'
5563
- : ''
5678
+ // Say what the SERVER did: one older than ADR-0084 ignores `shared` and keeps every claim private.
5679
+ const privacyLine = !isContact
5680
+ ? ''
5681
+ : out?.shared === true
5682
+ ? ` Everyone who can read this page can see and use this ${kind === 'person' ? 'contact detail' : 'identifier'} (shared; pass private: true to make a new one yours alone). It is never rendered in the page text, and it files only YOUR captured texts/mail here.`
5683
+ : ' This contact claim is PRIVATE to you: no other brain member can see it, and it is never rendered in the page. Your captured texts/mail from this contact will now propose this page.'
5564
5684
  return { content: [{ type: 'text', text: `Routing identifier set on document ${out?.documentId ?? '?'}: ${set}.${healedLine} Future matching events will attach here too (body mentions alone will not).${privacyLine}` }] }
5565
5685
  },
5566
5686
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.239",
3
+ "version": "0.9.241",
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": {