@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 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 = new URLSearchParams({ kind: k, key: name, version })
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 = new URLSearchParams({ kind: k, key: name })
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?kind=${k}&key=${encodeURIComponent(name)}`,
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" or "recent meetings". 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).',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.185",
3
+ "version": "0.9.186",
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": {