@theronap/agnoclast-mcp 0.9.222 → 0.9.224
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/series_render.mjs +30 -0
- package/lib/server.mjs +82 -1
- package/package.json +1 -1
|
@@ -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
|
|
@@ -4860,7 +4861,7 @@ function renderNudge(payload) {
|
|
|
4860
4861
|
shape: z.enum(['message', 'event', 'document', 'record']).describe('message = emails, chats, replies · event = ticket changes, alerts, CRM updates · document = notes, transcripts, reports · record = rows, metrics, anything else'),
|
|
4861
4862
|
purpose: z.enum(['communication', 'work', 'reference', 'metrics']).describe('sets defaults only'),
|
|
4862
4863
|
sample: z.record(z.string(), z.any()).optional().describe('ONE real item, exactly as the system will send it (a JSON object)'),
|
|
4863
|
-
mapping: z.record(z.string(), z.any()).optional().describe('which field plays which role, as dot paths into one item: { id, time, title, body, group, url, people: [..] }. Omit to have it suggested from the sample'),
|
|
4864
|
+
mapping: z.record(z.string(), z.any()).optional().describe('which field plays which role, as dot paths into one item: { id, time, title, body, group, url, people: [..] } — plus, for items with deadlines, due, work_from (when to start) and done: then each item keeps one obligation that the source updates and closes. Omit to have it suggested from the sample'),
|
|
4864
4865
|
mode: z.enum(['channel', 'ingest']).optional().describe('channel (default) = every item files onto one page as it arrives · ingest = each item waits in the pile to be sorted'),
|
|
4865
4866
|
...homeArgs,
|
|
4866
4867
|
dry_run: z.boolean().optional().describe('true = show the mapping, the sample preview and the home (or the candidate pages) and create NOTHING. Do this first'),
|
|
@@ -4891,6 +4892,48 @@ function renderNudge(payload) {
|
|
|
4891
4892
|
},
|
|
4892
4893
|
)
|
|
4893
4894
|
|
|
4895
|
+
server.registerTool(
|
|
4896
|
+
'set_custom_source_mapping',
|
|
4897
|
+
{
|
|
4898
|
+
title: 'Change a custom source\'s field mapping',
|
|
4899
|
+
description: 'Change which field of a custom source\'s pushed items plays which role — for a sender that learned to send more after it was set up. Pass the WHOLE mapping (read the current one with list_custom_sources first): { id, time, title, body, group, url, people: [..], due, work_from, done }. DEADLINES (2026-10-08): map `due` (when the item must be done) and, if the item says so, `work_from` (when work should start) and `done` (a field that is truthy once it is). Then each item with a deadline keeps ONE obligation for you on the source\'s home page — created on the next push, kept current on every push after, and discharged by the source itself when the item reports done; a machine proposal for the same work is folded into it. Nothing already late is created. The `id` field cannot change here: it decides what counts as the same item, and changing it would re-file everything as new — set up a new source instead.',
|
|
4900
|
+
inputSchema: {
|
|
4901
|
+
source: z.string().describe('the source — its id or its name, as list_custom_sources shows'),
|
|
4902
|
+
mapping: z.record(z.string(), z.any()).describe('the whole mapping, role → dot path into one item'),
|
|
4903
|
+
},
|
|
4904
|
+
},
|
|
4905
|
+
async (a) => {
|
|
4906
|
+
let list
|
|
4907
|
+
try {
|
|
4908
|
+
const res = await fetchCortex(`${BASE}/api/custom-connectors`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
4909
|
+
if (!res.ok) return cannot('Could not read your custom sources', res)
|
|
4910
|
+
list = (await res.json()).connectors
|
|
4911
|
+
} catch (e) {
|
|
4912
|
+
return toolError(`Could not read your custom sources: ${e.message}`)
|
|
4913
|
+
}
|
|
4914
|
+
const picked = pickSource(list, a.source)
|
|
4915
|
+
if (!picked.ok) return toolError(picked.message)
|
|
4916
|
+
const src = picked.source
|
|
4917
|
+
let res
|
|
4918
|
+
try {
|
|
4919
|
+
res = await fetchCortex(`${BASE}/api/custom-connectors/${encodeURIComponent(src.id)}/mapping`, {
|
|
4920
|
+
method: 'PUT',
|
|
4921
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
4922
|
+
body: JSON.stringify({ mapping: a.mapping }),
|
|
4923
|
+
})
|
|
4924
|
+
} catch (e) {
|
|
4925
|
+
return toolError(`Not changed: ${e.message}`)
|
|
4926
|
+
}
|
|
4927
|
+
if (!res.ok) return cannot('Not changed', res)
|
|
4928
|
+
const j = await res.json()
|
|
4929
|
+
const roles = Object.entries(j.mapping ?? {}).map(([k, v]) => `${k} ← ${Array.isArray(v) ? v.join(', ') : v}`).join(' · ')
|
|
4930
|
+
const keeps = j.mapping?.due
|
|
4931
|
+
? '\nItems with a deadline now keep an obligation each — created on the next push, closed when the item reports done.'
|
|
4932
|
+
: ''
|
|
4933
|
+
return { content: [{ type: 'text', text: `${src.name} mapping: ${roles}${keeps}` }] }
|
|
4934
|
+
},
|
|
4935
|
+
)
|
|
4936
|
+
|
|
4894
4937
|
server.registerTool(
|
|
4895
4938
|
'set_custom_source_home',
|
|
4896
4939
|
{
|
|
@@ -4944,6 +4987,44 @@ function renderNudge(payload) {
|
|
|
4944
4987
|
},
|
|
4945
4988
|
)
|
|
4946
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
|
+
|
|
4947
5028
|
server.registerTool(
|
|
4948
5029
|
'my_day',
|
|
4949
5030
|
{
|
package/package.json
CHANGED