@theronap/agnoclast-mcp 0.9.223 → 0.9.225

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,30 @@
1
+ // update_series replies — pure, so every rendering has a test. The tool and its engine
2
+ // (web/lib/engine/series_update.ts) exist because a series' container type could only be changed with
3
+ // SQL until 2026-10-08 (Agnoclast page "Agent SQL gap").
4
+
5
+ const OWES = {
6
+ meeting: 'owes a transcript or a note; closes 15 minutes after it ends',
7
+ work_block: 'owes a session in the window; closes when it ends',
8
+ deadline: 'owes the artifact; closes when it is due',
9
+ appointment: 'owes nothing (a class, a visit); admits a transcript or a note',
10
+ }
11
+
12
+ const line = (s) => `• ${s.title} [${s.seriesKey}] ${s.containerType ?? 'no container (a marker)'}${s.brain ? ` · ${s.brain}` : ''}`
13
+
14
+ /** PATCH /api/series/<ref> 200 → text. */
15
+ export function formatSeriesUpdated(r) {
16
+ if (r.from === r.to) return `${r.series.title} is already ${r.to} — nothing changed.`
17
+ return [
18
+ `${r.series.title} is now ${r.to} (was ${r.from}): ${OWES[r.to] ?? ''}.`,
19
+ `${r.containersRetyped} instance container(s) retyped${r.openContainersRetimed ? `, ${r.openContainersRetimed} still open with their close time recomputed` : ''}; instances made later open as ${r.to}.`,
20
+ 'Containers that already closed keep the time they closed at.',
21
+ ].join('\n')
22
+ }
23
+
24
+ /** A refusal body (404 / 409 / 400) → text that lets the agent pick again. */
25
+ export function formatSeriesRefusal(j) {
26
+ const L = [j?.message ?? j?.error ?? 'Not changed.']
27
+ if (j?.candidates?.length) L.push(j.error === 'ambiguous' ? 'Which one? Pass its key:' : 'Your series:', ...j.candidates.map(line))
28
+ else if (j?.error === 'not_found') L.push('You own no series. (A series is a recurring class or meeting made in Agnoclast.)')
29
+ return L.join('\n')
30
+ }
package/lib/server.mjs CHANGED
@@ -28,6 +28,7 @@ import { renderConversationOffer, renderMaterializeBatch } from './intake_batch_
28
28
  import { obligationsForUnits, renderFilingObligations, filingObligationsBrief, OBLIGATIONS_UNANSWERED_TEXT } from './filing_obligations.mjs'
29
29
  import { renderRecord, READ_RECORD_DEFAULT_CHARS, READ_RECORD_PARTS } from './record_render.mjs'
30
30
  import { formatCreated, formatDryRun, formatSources, homeFromArgs, pickSource, renderHomeProblem } from './custom_sources.mjs'
31
+ import { formatSeriesRefusal, formatSeriesUpdated } from './series_render.mjs'
31
32
  import { formatSends } from './sends_render.mjs'
32
33
 
33
34
  // PAGE tiers (ADR-0071). Six for a PAGE; a RECORD keeps three (`privacy` on log_session and
@@ -4986,6 +4987,44 @@ function renderNudge(payload) {
4986
4987
  },
4987
4988
  )
4988
4989
 
4990
+ // update_series — the user-parity sweep, 2026-10-08 (Theron: "things I think are totally working …
4991
+ // that regular users can't do because all of my agents are querying the SQL directly"). A series'
4992
+ // container type could only be changed with owner-connection SQL (2026-09-03, ENT 425). The sweep's
4993
+ // other gap, editing a custom source's mapping, shipped in parallel as set_custom_source_mapping (#1438).
4994
+ // Agnoclast page "Agent SQL gap".
4995
+ server.registerTool(
4996
+ 'update_series',
4997
+ {
4998
+ title: 'Change a recurring series\' container type',
4999
+ description: 'Change what a recurring class or meeting series (one made in Agnoclast) COLLECTS and OWES — its container type — for the whole series: the series itself and the container of every instance already made. meeting = owes a transcript or a note · work_block = owes a work session in the window · deadline = owes the artifact · appointment = owes nothing (a class, a visit) and still admits a transcript or a note. Use it when a series was typed wrong, e.g. a 3-hour lecture typed work_block, which flags every class as "nothing happened". Containers that already closed keep their close time. Name the series by its exact title, its key, or its id; an unknown or ambiguous name answers with your series to choose from.',
5000
+ inputSchema: {
5001
+ series: z.string().describe('the series — its exact title ("ENT 425"), its key (with or without "series:"), or its id'),
5002
+ container_type: z.enum(['meeting', 'work_block', 'deadline', 'appointment']).describe('what it should collect and owe'),
5003
+ },
5004
+ },
5005
+ async ({ series, container_type }) => {
5006
+ const ref = String(series).trim().replace(/^series:/i, '')
5007
+ let res
5008
+ try {
5009
+ res = await fetchCortex(`${BASE}/api/series/${encodeURIComponent(ref)}`, {
5010
+ method: 'PATCH',
5011
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
5012
+ body: JSON.stringify({ containerType: container_type }),
5013
+ })
5014
+ } catch (e) {
5015
+ return toolError(`Not changed: ${e.message}`)
5016
+ }
5017
+ if (!res.ok) {
5018
+ const body = await res.text()
5019
+ let j = null
5020
+ try { j = JSON.parse(body) } catch { /* not JSON */ }
5021
+ if (j && [400, 404, 409].includes(res.status)) return toolError(formatSeriesRefusal(j))
5022
+ return toolError(`Not changed: ${classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message}`)
5023
+ }
5024
+ return { content: [{ type: 'text', text: formatSeriesUpdated(await res.json()) }] }
5025
+ },
5026
+ )
5027
+
4989
5028
  server.registerTool(
4990
5029
  'my_day',
4991
5030
  {
@@ -5231,13 +5270,14 @@ function renderNudge(payload) {
5231
5270
  'set_routing_identifier',
5232
5271
  {
5233
5272
  title: 'Claim a routing identifier on a page',
5234
- 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.',
5273
+ 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. ' +
5274
+ '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.',
5235
5275
  inputSchema: {
5236
5276
  kind: z.enum(['project', 'person', 'org', 'user']).describe('the page kind'),
5237
5277
  name: z.string().optional().describe('page title (or pass ref)'),
5238
5278
  ref: z.string().optional().describe('node ref from read_page — prefer over name when available'),
5239
5279
  brain: z.string().optional().describe('brain label when the title is ambiguous across brains'),
5240
- identifier: z.string().describe('canonical identifier, e.g. repo:theronap/cortex or file:theronap/cortex:web/lib/engine/github_intake.ts'),
5280
+ 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)'),
5241
5281
  },
5242
5282
  },
5243
5283
  async ({ kind, name, ref, brain, identifier }) => {
@@ -5253,7 +5293,12 @@ function renderNudge(payload) {
5253
5293
  return toolError(`Could not set routing identifier: ${e.message}`)
5254
5294
  }
5255
5295
  const out = await res.json().catch(() => null)
5256
- if (!res.ok) return toolError(`Could not set routing identifier: ${out?.error ?? res.status}`)
5296
+ if (!res.ok) {
5297
+ // A per-identifier refusal arrives in `errors[]` with a top-level 400 — name it, not just the status.
5298
+ const why = out?.error ?? out?.errors?.map((e) => e.error).join(', ') ?? res.status
5299
+ const hint = /phone_claims_person_pages_only/.test(String(why)) ? ' A phone: claim goes on the PERSON page for that contact (kind: person).' : ''
5300
+ return toolError(`Could not set routing identifier: ${why}.${hint}`)
5301
+ }
5257
5302
  const set = out?.set?.join(', ') ?? identifier
5258
5303
  // ⚠ SAY WHAT ALREADY HAPPENED, NOT ONLY WHAT WILL. The claim heals history in the same
5259
5304
  // statement, so by the time this renders, existing records are ALREADY attached. The previous
@@ -5267,7 +5312,11 @@ function renderNudge(payload) {
5267
5312
  : healed > 0
5268
5313
  ? ` ${healed} existing record(s) ALREADY attached to this page as a result.`
5269
5314
  : ' 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).'
5270
- 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).` }] }
5315
+ const isContact = /^(phone|email):/.test(String(out?.set?.[0] ?? identifier).toLowerCase())
5316
+ const privacyLine = isContact
5317
+ ? ' 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.'
5318
+ : ''
5319
+ 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}` }] }
5271
5320
  },
5272
5321
  )
5273
5322
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.223",
3
+ "version": "0.9.225",
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": {