@theronap/agnoclast-mcp 0.9.185 → 0.9.186
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/redact.mjs +2 -0
- package/lib/server.mjs +43 -6
- package/package.json +1 -1
package/lib/redact.mjs
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// [pattern, replacement]. Order matters: most specific Anthropic forms first so a
|
|
11
11
|
// login token reads as [REDACTED:anthropic-oauth], not the generic key bucket.
|
|
12
12
|
const PATTERNS = [
|
|
13
|
+
// Agnoclast's own write-only ingest keys (custom sources): agn_in_ + 43 base64url characters.
|
|
14
|
+
[/agn_in_[A-Za-z0-9_-]{40,}/g, '[REDACTED:agnoclast-ingest-key]'],
|
|
13
15
|
[/sk-ant-oat\d{2}-[A-Za-z0-9_-]{20,}/g, '[REDACTED:anthropic-oauth]'],
|
|
14
16
|
[/sk-ant-api\d{2}-[A-Za-z0-9_-]{20,}/g, '[REDACTED:anthropic-key]'],
|
|
15
17
|
[/sk-ant-[A-Za-z0-9_-]{20,}/g, '[REDACTED:anthropic]'],
|
package/lib/server.mjs
CHANGED
|
@@ -24,6 +24,26 @@ import { renderObligations } from './obligations_render.mjs'
|
|
|
24
24
|
// tracked wanted page, whether a bare node exists for it, and whether it's a deliberately demoted page,
|
|
25
25
|
// and turn that into an actionable prompt. Wording lives in ./red_link_triage.mjs (pure + tested).
|
|
26
26
|
// Returns '' on any error so a miss never gets worse.
|
|
27
|
+
/**
|
|
28
|
+
* A page name held by pages in more than one brain: list them, each with the ref that picks it. The page
|
|
29
|
+
* read already returns every match (matches[] with brain + ref), so this reuses it instead of leaving the
|
|
30
|
+
* caller at a bare 409. Never throws — a failure falls back to the generic advice.
|
|
31
|
+
*/
|
|
32
|
+
async function ambiguousPageHint(base, token, kind, name) {
|
|
33
|
+
const generic = `"${name}" names a page in more than one of your brains. Call read_page "${name}" to see each one with its ref, then pass that ref.`
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetchCortex(`${base}/api/brain/page?kind=${kind}&key=${encodeURIComponent(name)}`, { headers: { Authorization: `Bearer ${token}` } })
|
|
36
|
+
if (!res.ok) return generic
|
|
37
|
+
const page = await res.json()
|
|
38
|
+
const matches = Array.isArray(page?.matches) ? page.matches.filter((m) => m.ref) : []
|
|
39
|
+
if (!matches.length) return generic
|
|
40
|
+
return [`"${name}" names a page in ${matches.length} of your brains — pass the ref of the one you mean:`,
|
|
41
|
+
...matches.map((m) => ` - "${m.title ?? name}" in ${m.brain ?? 'a brain'} — ref: ${m.ref}`)].join('\n')
|
|
42
|
+
} catch {
|
|
43
|
+
return generic
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
27
47
|
async function redLinkTriage(BASE, TOKEN, name) {
|
|
28
48
|
try {
|
|
29
49
|
const r = await fetchCortex(`${BASE}/api/brain/red-link?name=${encodeURIComponent(name)}`, {
|
|
@@ -1810,16 +1830,20 @@ function renderNudge(payload) {
|
|
|
1810
1830
|
expand: z.boolean().optional().describe('identifier names only: also list recent visible timeline events for this identifier (default: home + count)'),
|
|
1811
1831
|
history: z.boolean().optional().describe('node names only: return the node\'s TIMELINE (events joined via its [[repo:…]] stamps, reverse-chron, viewer-visible) instead of the page body. The page is the present; this is the history.'),
|
|
1812
1832
|
version: z.string().optional().describe('read a HISTORICAL version of this page instead of the current one: a rev_no (e.g. "3") or a content_hash from page_history. Use page_history first to see the versions, then rollback_page to restore one.'),
|
|
1833
|
+
ref: z.string().optional().describe('the page\'s ref (read_page prints it as `ref:`). Addresses exactly one page, so use it when a name exists in more than one brain — history and version reads refuse an ambiguous name and list the refs to choose from.'),
|
|
1813
1834
|
},
|
|
1814
1835
|
},
|
|
1815
|
-
async ({ name, kind, expand, history, version }) => {
|
|
1836
|
+
async ({ name, kind, expand, history, version, ref }) => {
|
|
1816
1837
|
const k = kind ?? 'project'
|
|
1838
|
+
// A ref addresses exactly one page; a name can match one page in each of several brains.
|
|
1839
|
+
const address = (extra = {}) => new URLSearchParams(ref ? { ref, ...extra } : { kind: k, key: name, ...extra })
|
|
1817
1840
|
// PAGE HISTORY (Part A): a specific version reads one historical body, resolved within this page's
|
|
1818
1841
|
// own history. Not for identifier-shaped names (those are join keys, handled below).
|
|
1819
1842
|
if (version && !/^[a-z][a-z0-9_-]*:.+/i.test(name)) {
|
|
1820
1843
|
try {
|
|
1821
|
-
const qs =
|
|
1844
|
+
const qs = address({ version })
|
|
1822
1845
|
const vr = await fetchCortex(`${BASE}/api/brain/page?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
1846
|
+
if (vr.status === 409) return { content: [{ type: 'text', text: await ambiguousPageHint(BASE, TOKEN, k, name) }] }
|
|
1823
1847
|
if (vr.status === 404) return { content: [{ type: 'text', text: `No version "${version}" for "${name}" (or you can't see it). Use page_history "${name}" to list its versions.` }] }
|
|
1824
1848
|
if (!vr.ok) {
|
|
1825
1849
|
const d = classify(vr.status, vr.headers.get('content-type'), await vr.text(), vr.headers.get('x-vercel-id'))
|
|
@@ -1834,9 +1858,12 @@ function renderNudge(payload) {
|
|
|
1834
1858
|
// PER-NODE TIMELINE (slice 4): history = the projection over the node's identifier stamps.
|
|
1835
1859
|
if (history && !/^[a-z][a-z0-9_-]*:.+/i.test(name)) {
|
|
1836
1860
|
try {
|
|
1837
|
-
const qs =
|
|
1861
|
+
const qs = address()
|
|
1838
1862
|
const r = await fetchCortex(`${BASE}/api/node/timeline?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
1839
1863
|
if (r.status === 404) return { content: [{ type: 'text', text: `No ${k} named "${name}" — cannot project a timeline.` }] }
|
|
1864
|
+
// Two brains hold a page by this name. Say which, with the ref for each, rather than a bare 409 —
|
|
1865
|
+
// "Agnoclast API 409: ambiguous_page_name" was a dead end (Reader Engine, 2026-09-29).
|
|
1866
|
+
if (r.status === 409) return { content: [{ type: 'text', text: await ambiguousPageHint(BASE, TOKEN, k, name) }] }
|
|
1840
1867
|
if (!r.ok) {
|
|
1841
1868
|
const d = classify(r.status, r.headers.get('content-type'), await r.text(), r.headers.get('x-vercel-id'))
|
|
1842
1869
|
return toolError(`Could not read the timeline for "${name}": ${d.message}`)
|
|
@@ -1868,6 +1895,8 @@ function renderNudge(payload) {
|
|
|
1868
1895
|
}
|
|
1869
1896
|
for (const e of t.events) lines.push(`- ${String(e.occurredAt).slice(0, 10)} · ${e.source} · ${e.summary}`)
|
|
1870
1897
|
if (!t.events.length) lines.push('(no events visible to you yet on these joins)')
|
|
1898
|
+
// The timeline is a list of headlines. The records themselves — every word — are one call away.
|
|
1899
|
+
else lines.push(`Full text of the records on this page: list_records page:"${t.ref ?? ref ?? name}" (order:"oldest" reads them in sequence; offset pages past 50).`)
|
|
1871
1900
|
if (t.siblings?.length) lines.push(`Sibling homes (share a stamp — bridges, not history): ${t.siblings.map((s) => `"${s.title}"`).join(', ')}`)
|
|
1872
1901
|
return { content: [{ type: 'text', text: lines.join('\n') }] }
|
|
1873
1902
|
} catch (e) {
|
|
@@ -1915,7 +1944,7 @@ function renderNudge(payload) {
|
|
|
1915
1944
|
}
|
|
1916
1945
|
let res
|
|
1917
1946
|
try {
|
|
1918
|
-
res = await fetchCortex(`${BASE}/api/brain/page
|
|
1947
|
+
res = await fetchCortex(`${BASE}/api/brain/page?${address()}`,
|
|
1919
1948
|
{ headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
1920
1949
|
} catch (e) {
|
|
1921
1950
|
return toolError(`Could not read "${name}": ${e.message}`)
|
|
@@ -4084,17 +4113,25 @@ function renderNudge(payload) {
|
|
|
4084
4113
|
'list_records',
|
|
4085
4114
|
{
|
|
4086
4115
|
title: 'List activity records (filtered)',
|
|
4087
|
-
description: 'List recent activity records you can see, filtered by type, project, and recency. RLS-scoped. Use to enumerate "what happened on project X this week"
|
|
4116
|
+
description: 'List recent activity records you can see, filtered by type, project, page, source and recency. RLS-scoped. Use to enumerate "what happened on project X this week", "recent meetings", or EVERYTHING FILED ON A PAGE — pass `page` (its name or ref) to read a channel\'s items or a project page\'s records, with order:"oldest" to read a series in sequence and `offset` to page past 50. Returns each record\'s FULL summary + id — never a truncated preview, so what you get back is the whole record you are permitted to read (use an id with request_file to ask its owner for the original file).',
|
|
4088
4117
|
inputSchema: {
|
|
4089
4118
|
type: z.string().optional().describe("record type, e.g. 'meeting', 'comm', 'activity', 'ai_session', 'doc', 'note'"),
|
|
4090
4119
|
project: z.string().optional().describe('project key to filter to (e.g. "cortex")'),
|
|
4091
4120
|
since_days: z.number().optional().describe('only records from the last N days'),
|
|
4092
4121
|
limit: z.number().optional().describe('max rows (1-50, default 20)'),
|
|
4093
4122
|
session: z.string().optional().describe('a session id — returns every record that session produced, INCLUDING the separate halves of a log that was split across brains. Segmented logs share a sessionId and never link to each other, so this is the only way to reassemble one. RLS-scoped: you get the halves you are cleared for and cannot tell whether others exist'),
|
|
4123
|
+
page: z.string().optional().describe('a page — its name or its ref (from read_page) — to list the records filed on it. A name held by pages in several brains lists them all, each row tagged with its brain; pass the ref to pick one.'),
|
|
4124
|
+
source: z.string().optional().describe("a record source, e.g. 'custom' (custom sources), 'github', 'email', 'imessage', 'meeting'"),
|
|
4125
|
+
order: z.enum(['newest', 'oldest']).optional().describe('newest first (default), or oldest first to read a series in sequence'),
|
|
4126
|
+
offset: z.number().optional().describe('skip this many records — page through more than 50 by calling again with offset 50, 100…'),
|
|
4094
4127
|
},
|
|
4095
4128
|
},
|
|
4096
|
-
async ({ type, project, since_days, limit, session }) => {
|
|
4129
|
+
async ({ type, project, since_days, limit, session, page, source, order, offset }) => {
|
|
4097
4130
|
const qs = new URLSearchParams()
|
|
4131
|
+
if (page) qs.set('page', page)
|
|
4132
|
+
if (source) qs.set('source', source)
|
|
4133
|
+
if (order) qs.set('order', order)
|
|
4134
|
+
if (offset != null) qs.set('offset', String(offset))
|
|
4098
4135
|
if (type) qs.set('type', type)
|
|
4099
4136
|
if (project) qs.set('project', project)
|
|
4100
4137
|
if (since_days != null) qs.set('since_days', String(since_days))
|
package/package.json
CHANGED