@theronap/agnoclast-mcp 0.9.238 → 0.9.240
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/arrivals_notice.mjs +17 -0
- package/lib/contacts_render.mjs +114 -0
- package/lib/server.mjs +85 -7
- package/package.json +1 -1
package/lib/arrivals_notice.mjs
CHANGED
|
@@ -59,6 +59,8 @@ import { fetchCortex, resolveBase, resolveTokenSource } from './diagnose.mjs'
|
|
|
59
59
|
// Enough to hold a session's own recent output in full; small enough that the notice is READ rather
|
|
60
60
|
// than spilled to a file, which is what happened at 49 KB.
|
|
61
61
|
const MAX_YOURS = 25
|
|
62
|
+
/** Assigned groups listed per prompt; the server hands out at most 5 new groups per prompt anyway. */
|
|
63
|
+
const MAX_ASSIGNED = 8
|
|
62
64
|
|
|
63
65
|
// A title is somebody else's text — anyone can send a calendar invite — and this prints into a
|
|
64
66
|
// session's context. One line, bounded, and quoted so it reads as the name of a thing.
|
|
@@ -176,6 +178,21 @@ export function formatNotice(signals) {
|
|
|
176
178
|
const total = Number(signals?.settling?.total ?? offers.length)
|
|
177
179
|
const delta = Number(signals?.arrival?.count ?? 0)
|
|
178
180
|
|
|
181
|
+
// ⭐ ADR-0083 §2.3 — arrivals ASSIGNED to this session. Not an offer: this session is their one owner, and
|
|
182
|
+
// it is told at a prompt boundary so it handles them before the user's request. A conversation stays with
|
|
183
|
+
// this session for its lifetime, so its later texts come here too.
|
|
184
|
+
const assigned = Array.isArray(signals?.assigned) ? signals.assigned : []
|
|
185
|
+
if (assigned.length) {
|
|
186
|
+
const n = assigned.reduce((t, g) => t + (Number(g?.count) || 1), 0)
|
|
187
|
+
lines.push(`📥 ${n} arrival${n === 1 ? ' is' : 's are'} ASSIGNED TO THIS SESSION — you are ${n === 1 ? 'its' : 'their'} only owner. Handle ${n === 1 ? 'it' : 'them'} before the user's request, then answer the user:`)
|
|
188
|
+
for (const g of assigned.slice(0, MAX_ASSIGNED)) {
|
|
189
|
+
const many = Number(g?.count) > 1 ? ` ×${g.count}` : ''
|
|
190
|
+
const ids = (g?.ids ?? []).slice(0, 3).join(', ') + ((g?.ids ?? []).length > 3 ? ', …' : '')
|
|
191
|
+
lines.push(` ${g?.source ?? '?'}${many} — ${g?.title ?? '(unreadable)'} · ${g?.hoursAgo ?? '?'}h · ids: ${ids}`)
|
|
192
|
+
}
|
|
193
|
+
if (assigned.length > MAX_ASSIGNED) lines.push(` … and ${assigned.length - MAX_ASSIGNED} more groups`)
|
|
194
|
+
lines.push(' `intake_claim {intakeItemIds}` → `intake_materialize` (or `intake_discard` for junk) · `not_mine` hands one to the next session · only what only the user can decide → `intake_defer`.')
|
|
195
|
+
}
|
|
179
196
|
if (delta) {
|
|
180
197
|
lines.push(`⚡ ${delta} new arrival${delta === 1 ? '' : 's'} in the pile since this session was last told.`)
|
|
181
198
|
}
|
|
@@ -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
|
+
}
|
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
|
|
@@ -4165,6 +4166,76 @@ function renderNudge(payload) {
|
|
|
4165
4166
|
},
|
|
4166
4167
|
)
|
|
4167
4168
|
|
|
4169
|
+
// ⭐ AGNOCLAST'S OWN CONTACT BOOK (ADR-0084; Theron, 2026-10-17). The person's book is every person page
|
|
4170
|
+
// they can read, merged on a shared phone/email; `contact` is the briefing an AI reads before reaching out.
|
|
4171
|
+
server.registerTool(
|
|
4172
|
+
'my_contacts',
|
|
4173
|
+
{
|
|
4174
|
+
title: 'Your contacts — person pages you can read, merged',
|
|
4175
|
+
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.',
|
|
4176
|
+
inputSchema: {
|
|
4177
|
+
query: z.string().optional().describe('a name, an email address, or digits of a phone number'),
|
|
4178
|
+
brain: z.string().optional().describe('the brain you are acting in (name or id) — decides where outreach files for a contact in several brains'),
|
|
4179
|
+
limit: z.number().optional().describe('max contacts (default 50, max 200)'),
|
|
4180
|
+
offset: z.number().optional().describe('skip this many (paging)'),
|
|
4181
|
+
reach: z.boolean().optional().describe('also check who can be CALLED (uses the person\'s call-lookup budget; default false)'),
|
|
4182
|
+
},
|
|
4183
|
+
},
|
|
4184
|
+
async ({ query, brain, limit, offset, reach }) => {
|
|
4185
|
+
const q = typeof query === 'string' ? query.trim() : ''
|
|
4186
|
+
let res
|
|
4187
|
+
try {
|
|
4188
|
+
res = q
|
|
4189
|
+
? await fetchCortex(`${BASE}/api/contacts/search`, {
|
|
4190
|
+
method: 'POST',
|
|
4191
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
4192
|
+
body: JSON.stringify({ q, brain, limit, offset, reach: reach === true }),
|
|
4193
|
+
})
|
|
4194
|
+
: await fetchCortex(`${BASE}/api/contacts?${new URLSearchParams({
|
|
4195
|
+
...(brain ? { brain } : {}), ...(limit ? { limit: String(limit) } : {}), ...(offset ? { offset: String(offset) } : {}),
|
|
4196
|
+
...(reach ? { reach: '1' } : {}),
|
|
4197
|
+
})}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
4198
|
+
} catch (e) {
|
|
4199
|
+
return toolError(`Could not read your contacts: ${e.message}`)
|
|
4200
|
+
}
|
|
4201
|
+
const out = await res.json().catch(() => null)
|
|
4202
|
+
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.')
|
|
4203
|
+
if (!res.ok) return toolError(`Could not read your contacts: ${out?.error ?? res.status}`)
|
|
4204
|
+
return { content: [{ type: 'text', text: renderContactList(out, { query: q }) }] }
|
|
4205
|
+
},
|
|
4206
|
+
)
|
|
4207
|
+
|
|
4208
|
+
server.registerTool(
|
|
4209
|
+
'contact',
|
|
4210
|
+
{
|
|
4211
|
+
title: 'One contact — identifiers AND their pages, before you reach out',
|
|
4212
|
+
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`.',
|
|
4213
|
+
inputSchema: {
|
|
4214
|
+
page: z.string().optional().describe('a page id from my_contacts, or a person page ref from read_page'),
|
|
4215
|
+
name: z.string().optional().describe('their name, when you have no page id'),
|
|
4216
|
+
brain: z.string().optional().describe('the brain you are acting in (name or id), if any'),
|
|
4217
|
+
},
|
|
4218
|
+
},
|
|
4219
|
+
async ({ page, name, brain }) => {
|
|
4220
|
+
if (!page && !name) return toolError('Pass page or name')
|
|
4221
|
+
let res
|
|
4222
|
+
try {
|
|
4223
|
+
res = await fetchCortex(`${BASE}/api/contacts/briefing`, {
|
|
4224
|
+
method: 'POST',
|
|
4225
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
4226
|
+
body: JSON.stringify({ page, name, brain }),
|
|
4227
|
+
})
|
|
4228
|
+
} catch (e) {
|
|
4229
|
+
return toolError(`Could not read that contact: ${e.message}`)
|
|
4230
|
+
}
|
|
4231
|
+
const out = await res.json().catch(() => null)
|
|
4232
|
+
if (res.status === 409 && out?.error === 'ambiguous') return { content: [{ type: 'text', text: renderContactCandidates(out.candidates, name ?? page) }] }
|
|
4233
|
+
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.` }] }
|
|
4234
|
+
if (!res.ok) return toolError(`Could not read that contact: ${out?.error ?? res.status}`)
|
|
4235
|
+
return { content: [{ type: 'text', text: renderContactBriefing(out) }] }
|
|
4236
|
+
},
|
|
4237
|
+
)
|
|
4238
|
+
|
|
4168
4239
|
server.registerTool(
|
|
4169
4240
|
'set_summary',
|
|
4170
4241
|
{
|
|
@@ -5516,23 +5587,27 @@ function renderNudge(payload) {
|
|
|
5516
5587
|
{
|
|
5517
5588
|
title: 'Claim a routing identifier on a page',
|
|
5518
5589
|
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
|
-
'
|
|
5590
|
+
'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
5591
|
inputSchema: {
|
|
5521
5592
|
kind: z.enum(['project', 'person', 'org', 'user']).describe('the page kind'),
|
|
5522
5593
|
name: z.string().optional().describe('page title (or pass ref)'),
|
|
5523
5594
|
ref: z.string().optional().describe('node ref from read_page — prefer over name when available'),
|
|
5524
5595
|
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
|
|
5596
|
+
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'),
|
|
5597
|
+
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
5598
|
},
|
|
5527
5599
|
},
|
|
5528
|
-
async ({ kind, name, ref, brain, identifier }) => {
|
|
5600
|
+
async ({ kind, name, ref, brain, identifier, private: isPrivate }) => {
|
|
5529
5601
|
if (!name && !ref) return toolError('Pass name or ref')
|
|
5602
|
+
// ADR-0084: a person page's phone/email is shared with its readers unless the person says private.
|
|
5603
|
+
const contactClaim = kind === 'person' && /^(phone|email):/i.test(String(identifier).trim())
|
|
5604
|
+
const sharing = contactClaim ? { shared: isPrivate !== true, sharing_explicit: typeof isPrivate === 'boolean' } : {}
|
|
5530
5605
|
let res
|
|
5531
5606
|
try {
|
|
5532
5607
|
res = await fetchCortex(`${BASE}/api/brain/routing-identifiers`, {
|
|
5533
5608
|
method: 'POST',
|
|
5534
5609
|
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
5535
|
-
body: JSON.stringify({ kind, name, ref, brain, identifier }),
|
|
5610
|
+
body: JSON.stringify({ kind, name, ref, brain, identifier, ...sharing }),
|
|
5536
5611
|
})
|
|
5537
5612
|
} catch (e) {
|
|
5538
5613
|
return toolError(`Could not set routing identifier: ${e.message}`)
|
|
@@ -5558,9 +5633,12 @@ function renderNudge(payload) {
|
|
|
5558
5633
|
? ` ${healed} existing record(s) ALREADY attached to this page as a result.`
|
|
5559
5634
|
: ' 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
5635
|
const isContact = /^(phone|email):/.test(String(out?.set?.[0] ?? identifier).toLowerCase())
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5636
|
+
// Say what the SERVER did: one older than ADR-0084 ignores `shared` and keeps every claim private.
|
|
5637
|
+
const privacyLine = !isContact
|
|
5638
|
+
? ''
|
|
5639
|
+
: out?.shared === true
|
|
5640
|
+
? ` 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.`
|
|
5641
|
+
: ' 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
5642
|
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
5643
|
},
|
|
5566
5644
|
)
|
package/package.json
CHANGED