@theronap/agnoclast-mcp 0.9.221 → 0.9.223
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 +16 -1
- package/lib/obligations_render.mjs +7 -1
- package/lib/server.mjs +81 -5
- package/package.json +1 -1
package/lib/arrivals_notice.mjs
CHANGED
|
@@ -101,6 +101,20 @@ export function formatPressing(pressing) {
|
|
|
101
101
|
const more = count > names.length ? ` … and ${count - names.length} more` : ''
|
|
102
102
|
lines.push(`⏰ Due today (${count}): ${names.join(' · ')}${more}`)
|
|
103
103
|
}
|
|
104
|
+
// 2026-10-08 — WHAT TO BE WORKING ON: obligations whose work window is open, soonest deadline first.
|
|
105
|
+
// Theron: "that is the thing that they should be working on in the time frame in advance of that
|
|
106
|
+
// thing." Once a day. A window that opened in the last day is marked so the start is not missed.
|
|
107
|
+
else if (p?.kind === 'work_now') {
|
|
108
|
+
const count = Number(p.count) || 0
|
|
109
|
+
if (!count) continue
|
|
110
|
+
const names = (Array.isArray(p.names) ? p.names : []).map((n) => {
|
|
111
|
+
const due = String(n?.due ?? '').replace(/\s+/g, ' ').trim().slice(0, 40)
|
|
112
|
+
const done = n?.maybeDone ? ' — may ALREADY BE DONE; ask' : ''
|
|
113
|
+
return `${named(n?.text)} (${n?.opened ? 'starts today — ' : ''}${due ? `due ${due}` : 'due soon'})${done}`
|
|
114
|
+
})
|
|
115
|
+
const more = count > names.length ? ` … and ${count - names.length} more` : ''
|
|
116
|
+
lines.push(`🧭 Work on now (${count}): ${names.join(' · ')}${more}`)
|
|
117
|
+
}
|
|
104
118
|
// ADR-0073 §2.7 — a hand-off, said once to the one person it concerns. Names and notes are other
|
|
105
119
|
// people's text: quoted and bounded like every title above.
|
|
106
120
|
else if (p?.kind === 'asked') {
|
|
@@ -140,7 +154,8 @@ export function pressingAcks(pressing) {
|
|
|
140
154
|
for (const p of Array.isArray(pressing) ? pressing : []) {
|
|
141
155
|
if (typeof p?.subject !== 'string' || typeof p?.bucket !== 'string') continue
|
|
142
156
|
const handoff = p.kind === 'asked' || p.kind === 'given' || p.kind === 'declined' || p.kind === 'lapsed'
|
|
143
|
-
|
|
157
|
+
const rollup = (p.kind === 'due_today' || p.kind === 'work_now') && Number(p.count) > 0
|
|
158
|
+
if (p.kind === 'event' || p.kind === 'due_soon' || handoff || rollup) {
|
|
144
159
|
out.push({ subject: p.subject, bucket: p.bucket })
|
|
145
160
|
}
|
|
146
161
|
}
|
|
@@ -152,5 +152,11 @@ function describeDue(o, now) {
|
|
|
152
152
|
const what = o.dueAllDay ? `${day} (all day)` : `${day} ${o.dueAt.slice(11, 16)}`
|
|
153
153
|
if (after <= now) return `OVERDUE ${what}`
|
|
154
154
|
if (o.dueAllDay && at <= now) return `DUE TODAY ${what}`
|
|
155
|
-
return `due ${what}`
|
|
155
|
+
return `due ${what}${windowNote(o.workFrom, now)}`
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 2026-10-08 — when work should start. Absent on a server older than work windows: nothing is said.
|
|
159
|
+
function windowNote(workFrom, now) {
|
|
160
|
+
if (!workFrom) return ''
|
|
161
|
+
return new Date(workFrom).getTime() <= now ? ` · IN ITS WORK WINDOW since ${String(workFrom).slice(0, 10)}` : ` · work from ${String(workFrom).slice(0, 10)}`
|
|
156
162
|
}
|
package/lib/server.mjs
CHANGED
|
@@ -2985,25 +2985,26 @@ function renderNudge(payload) {
|
|
|
2985
2985
|
'track_obligation',
|
|
2986
2986
|
{
|
|
2987
2987
|
title: 'Record something you owe',
|
|
2988
|
-
description: "Record something you owe: a subject, the page or pages it is about, optionally when it comes due, optionally whose reply would discharge it. ⚠ EVERY OBLIGATION LIVES ON A PAGE, CHOSEN NOW. `pages` is required: READ the page this is about first (read_page prints its `ref:`), then pass that ref — several when it genuinely belongs on several. Never guess a page to get past the check, and never default to the person's own page: if you cannot tell where it belongs, ask them. A page says where the obligation is FOUND, not who can see it: it is visible to its owner only unless the person later widens it (`set_obligation_tier`). ⚠ ONLY A DATE MAKES IT COME BACK. A DATED obligation is raised to the person's sessions by time, unasked, BEFORE it lapses: counted on the day it is due, and named when a stated clock time is within the hour. Once late it is never raised again, and an UNDATED one is never raised by anything — it surfaces only when someone asks with my_obligations. ⚠ NEVER INVENT A DUE DATE — and that matters more now, because an invented date will interrupt someone. Omit due_at when there genuinely is no deadline (a standing obligation); a guessed date is indistinguishable from a real one the moment it is stored, and it is the direct route to nagging about something that was never actually late. `anchor_identifier` is the useful part: give it the counterparty's address as `email:someone@example.com` and any record arriving from them is automatically offered as evidence that this may be done — computed when the record lands, not by a scanner.",
|
|
2988
|
+
description: "Record something you owe: a subject, the page or pages it is about, optionally when it comes due, optionally whose reply would discharge it. ⚠ EVERY OBLIGATION LIVES ON A PAGE, CHOSEN NOW. `pages` is required: READ the page this is about first (read_page prints its `ref:`), then pass that ref — several when it genuinely belongs on several. Never guess a page to get past the check, and never default to the person's own page: if you cannot tell where it belongs, ask them. A page says where the obligation is FOUND, not who can see it: it is visible to its owner only unless the person later widens it (`set_obligation_tier`). ⚠ ONLY A DATE MAKES IT COME BACK. A DATED obligation is raised to the person's sessions by time, unasked, BEFORE it lapses: counted on the day it is due, and named when a stated clock time is within the hour. ⚠ A DEADLINE IS NOT WHEN TO START: pass `work_from` when the work needs lead time (a 10-day project opens 10 days ahead; an exam, days of study). From then it is named daily as \"work on now\", and once on the desktop when it opens. Once late it is never raised again, and an UNDATED one is never raised by anything — it surfaces only when someone asks with my_obligations. ⚠ NEVER INVENT A DUE DATE — and that matters more now, because an invented date will interrupt someone. Omit due_at when there genuinely is no deadline (a standing obligation); a guessed date is indistinguishable from a real one the moment it is stored, and it is the direct route to nagging about something that was never actually late. `anchor_identifier` is the useful part: give it the counterparty's address as `email:someone@example.com` and any record arriving from them is automatically offered as evidence that this may be done — computed when the record lands, not by a scanner.",
|
|
2989
2989
|
inputSchema: {
|
|
2990
2990
|
subject: z.string().describe('what is owed, in your own words'),
|
|
2991
2991
|
pages: z.array(z.string()).min(1).describe('REQUIRED. The `ref:` of each page this obligation is about, exactly as read_page prints it. Read the page first; do not guess.'),
|
|
2992
2992
|
for: z.string().optional().describe('ONLY when it is someone ELSE who should owe this: their email address, or their name as it appears in this brain. Omit for something the person owes themselves. You do not choose whether it is assigned or requested — the reporting line does.'),
|
|
2993
2993
|
note: z.string().optional().describe('with `for`: a short note to the person it is given to'),
|
|
2994
2994
|
due_at: z.string().optional().describe('ISO 8601. OMIT when there is no real deadline — never guess one.'),
|
|
2995
|
+
work_from: z.string().optional().describe('ISO 8601: when work should START — the window opens (a bare date = the start of that day). Needs due_at, and must come before it. Estimate it from what the work involves; omit for things needing no lead time.'),
|
|
2995
2996
|
anchor_identifier: z.string().optional().describe('e.g. "email:ben@example.com" — records from them become evidence this may be done'),
|
|
2996
2997
|
source_record_id: z.string().optional().describe('the record this came from, if any'),
|
|
2997
2998
|
brain: z.string().optional().describe('required only if you belong to more than one brain'),
|
|
2998
2999
|
},
|
|
2999
3000
|
},
|
|
3000
|
-
async ({ subject, pages, due_at, anchor_identifier, source_record_id, brain, for: giveTo, note: giveNote }) => {
|
|
3001
|
+
async ({ subject, pages, due_at, work_from, anchor_identifier, source_record_id, brain, for: giveTo, note: giveNote }) => {
|
|
3001
3002
|
let res
|
|
3002
3003
|
try {
|
|
3003
3004
|
res = await fetchCortex(`${BASE}/api/obligations`, {
|
|
3004
3005
|
method: 'POST',
|
|
3005
3006
|
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
3006
|
-
body: JSON.stringify({ subject, pages, dueAt: due_at, anchorIdentifier: anchor_identifier, sourceRecordId: source_record_id, brain, for: giveTo, note: giveNote }),
|
|
3007
|
+
body: JSON.stringify({ subject, pages, dueAt: due_at, workFrom: work_from, anchorIdentifier: anchor_identifier, sourceRecordId: source_record_id, brain, for: giveTo, note: giveNote }),
|
|
3007
3008
|
})
|
|
3008
3009
|
} catch (e) { return toolError(`Could not reach obligations: ${e.message}`) }
|
|
3009
3010
|
const out = await res.json().catch(() => null)
|
|
@@ -3011,7 +3012,8 @@ function renderNudge(payload) {
|
|
|
3011
3012
|
// What will actually happen, stated per case: a promise of a nudge that never comes is worse
|
|
3012
3013
|
// than no promise, and so is "nothing will nudge you" over something that now will.
|
|
3013
3014
|
const nudge = due_at
|
|
3014
|
-
?
|
|
3015
|
+
? (work_from ? `From ${work_from.slice(0, 10)} it is named daily as "work on now" (and once on the desktop). ` : '')
|
|
3016
|
+
+ 'It will be raised to your sessions on the day it is due' + (/T\d{2}:\d{2}/.test(due_at) ? ', and again when it is within the hour.' : '.')
|
|
3015
3017
|
: 'It has no date, so nothing will raise it \u2014 ask with my_obligations.'
|
|
3016
3018
|
const note = anchor_identifier
|
|
3017
3019
|
? `\nRecords carrying ${anchor_identifier} will be offered as evidence this is done. ${nudge}`
|
|
@@ -3029,6 +3031,38 @@ function renderNudge(payload) {
|
|
|
3029
3031
|
},
|
|
3030
3032
|
)
|
|
3031
3033
|
|
|
3034
|
+
server.registerTool(
|
|
3035
|
+
'set_obligation_window',
|
|
3036
|
+
{
|
|
3037
|
+
title: 'Set when work on an obligation should start',
|
|
3038
|
+
description: "Open, move or remove an obligation's WORK WINDOW — when work on it should start, not when it is due (2026-10-08). From that moment until it is late, it is named once a day in the person's sessions as \"work on now\", soonest deadline first, and announced once on the desktop when the window opens. Use it when you learn what the work involves after it was recorded: \"it needs ten consecutive days of observation\" opens the window ten days before the deadline, whatever was guessed at capture; \"study for the final\" might open five days ahead. A bare date opens it at the start of that day in the owner's zone. It needs a due date and must come before it. Pass work_from null to remove the window. Only the owner can set it, and only while the obligation is open. A proposal (unconfirmed) is never raised by its window.",
|
|
3039
|
+
inputSchema: {
|
|
3040
|
+
obligation_id: z.string().describe('from my_obligations'),
|
|
3041
|
+
work_from: z.string().nullable().describe('ISO 8601 date or date-time when work should start; null removes the window'),
|
|
3042
|
+
},
|
|
3043
|
+
},
|
|
3044
|
+
async ({ obligation_id, work_from }) => {
|
|
3045
|
+
let res
|
|
3046
|
+
try {
|
|
3047
|
+
res = await fetchCortex(`${BASE}/api/obligations/window`, {
|
|
3048
|
+
method: 'POST',
|
|
3049
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
3050
|
+
body: JSON.stringify({ obligationId: obligation_id, workFrom: work_from ?? null }),
|
|
3051
|
+
})
|
|
3052
|
+
} catch (e) { return toolError(`Could not reach obligations: ${e.message}`) }
|
|
3053
|
+
const out = await res.json().catch(() => null)
|
|
3054
|
+
if (!res.ok) return toolError(`Could not set the window: ${out?.detail ?? out?.error ?? res.status}`)
|
|
3055
|
+
return {
|
|
3056
|
+
content: [{
|
|
3057
|
+
type: 'text',
|
|
3058
|
+
text: out.workFrom
|
|
3059
|
+
? `Window set: work on it from ${out.workFrom}. From then until it is due it is named daily as "work on now".`
|
|
3060
|
+
: 'Window removed: it is raised only on the day it is due.',
|
|
3061
|
+
}],
|
|
3062
|
+
}
|
|
3063
|
+
},
|
|
3064
|
+
)
|
|
3065
|
+
|
|
3032
3066
|
server.registerTool(
|
|
3033
3067
|
'give_obligation',
|
|
3034
3068
|
{
|
|
@@ -4826,7 +4860,7 @@ function renderNudge(payload) {
|
|
|
4826
4860
|
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'),
|
|
4827
4861
|
purpose: z.enum(['communication', 'work', 'reference', 'metrics']).describe('sets defaults only'),
|
|
4828
4862
|
sample: z.record(z.string(), z.any()).optional().describe('ONE real item, exactly as the system will send it (a JSON object)'),
|
|
4829
|
-
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'),
|
|
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: [..] } — 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'),
|
|
4830
4864
|
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'),
|
|
4831
4865
|
...homeArgs,
|
|
4832
4866
|
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'),
|
|
@@ -4857,6 +4891,48 @@ function renderNudge(payload) {
|
|
|
4857
4891
|
},
|
|
4858
4892
|
)
|
|
4859
4893
|
|
|
4894
|
+
server.registerTool(
|
|
4895
|
+
'set_custom_source_mapping',
|
|
4896
|
+
{
|
|
4897
|
+
title: 'Change a custom source\'s field mapping',
|
|
4898
|
+
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.',
|
|
4899
|
+
inputSchema: {
|
|
4900
|
+
source: z.string().describe('the source — its id or its name, as list_custom_sources shows'),
|
|
4901
|
+
mapping: z.record(z.string(), z.any()).describe('the whole mapping, role → dot path into one item'),
|
|
4902
|
+
},
|
|
4903
|
+
},
|
|
4904
|
+
async (a) => {
|
|
4905
|
+
let list
|
|
4906
|
+
try {
|
|
4907
|
+
const res = await fetchCortex(`${BASE}/api/custom-connectors`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
4908
|
+
if (!res.ok) return cannot('Could not read your custom sources', res)
|
|
4909
|
+
list = (await res.json()).connectors
|
|
4910
|
+
} catch (e) {
|
|
4911
|
+
return toolError(`Could not read your custom sources: ${e.message}`)
|
|
4912
|
+
}
|
|
4913
|
+
const picked = pickSource(list, a.source)
|
|
4914
|
+
if (!picked.ok) return toolError(picked.message)
|
|
4915
|
+
const src = picked.source
|
|
4916
|
+
let res
|
|
4917
|
+
try {
|
|
4918
|
+
res = await fetchCortex(`${BASE}/api/custom-connectors/${encodeURIComponent(src.id)}/mapping`, {
|
|
4919
|
+
method: 'PUT',
|
|
4920
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
4921
|
+
body: JSON.stringify({ mapping: a.mapping }),
|
|
4922
|
+
})
|
|
4923
|
+
} catch (e) {
|
|
4924
|
+
return toolError(`Not changed: ${e.message}`)
|
|
4925
|
+
}
|
|
4926
|
+
if (!res.ok) return cannot('Not changed', res)
|
|
4927
|
+
const j = await res.json()
|
|
4928
|
+
const roles = Object.entries(j.mapping ?? {}).map(([k, v]) => `${k} ← ${Array.isArray(v) ? v.join(', ') : v}`).join(' · ')
|
|
4929
|
+
const keeps = j.mapping?.due
|
|
4930
|
+
? '\nItems with a deadline now keep an obligation each — created on the next push, closed when the item reports done.'
|
|
4931
|
+
: ''
|
|
4932
|
+
return { content: [{ type: 'text', text: `${src.name} mapping: ${roles}${keeps}` }] }
|
|
4933
|
+
},
|
|
4934
|
+
)
|
|
4935
|
+
|
|
4860
4936
|
server.registerTool(
|
|
4861
4937
|
'set_custom_source_home',
|
|
4862
4938
|
{
|
package/package.json
CHANGED