@theronap/agnoclast-mcp 0.9.197 → 0.9.198

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.
@@ -19,16 +19,26 @@
19
19
  export function renderObligations(obs, now = Date.now()) {
20
20
  if (!obs?.length) return 'Nothing open.'
21
21
  const proposed = obs.filter((o) => o.state === 'proposed').length
22
+ // ADR-0073: every obligation lives on a page. `needsPage` is sent only by a server that knows the
23
+ // rule, so against an older one this count is zero and the output is exactly what it was.
24
+ const homeless = obs.filter((o) => o.needsPage === true).length
22
25
  const lines = [
23
26
  proposed
24
27
  ? `${obs.length} open — ${proposed} of them PROPOSED: extracted from a record, not entered by you. A wrong one: resolve_obligation with state "cancelled".`
25
28
  : `${obs.length} open:`,
29
+ ...(homeless
30
+ ? [`${homeless} of them ${homeless === 1 ? 'is' : 'are'} on NO PAGE anyone chose — marked ⚠ below. Read the page each belongs on, then \`set_obligation_pages\`. Do not guess: ask when it is not clear.`]
31
+ : []),
26
32
  '',
27
33
  ]
28
34
  for (const o of obs) {
29
35
  const when = describeDue(o, now)
30
36
  lines.push(`${o.id}`)
31
37
  lines.push(` ${o.subject} — ${when}${o.state === 'snoozed' ? ' (snooze expired)' : ''}${o.state === 'proposed' ? ' [PROPOSED]' : ''}`)
38
+ // Where it lives. Page titles are other people's text as much as subjects are: quoted, one line.
39
+ const pages = Array.isArray(o.pages) ? o.pages.filter((p) => p?.origin !== 'fallback') : []
40
+ if (pages.length) lines.push(` on: ${pages.slice(0, 4).map((p) => `“${String(p.title ?? '').replace(/\s+/g, ' ').trim().slice(0, 60)}”`).join(' · ')}${pages.length > 4 ? ` … and ${pages.length - 4} more` : ''}`)
41
+ if (o.needsPage === true) lines.push(' ⚠ on no page anyone chose — set_obligation_pages')
32
42
  // ⚠ EVIDENCE IS RENDERED AS A QUESTION, NEVER AS A VERDICT. The phrasing is the feature:
33
43
  // "is this done?" costs the reader a second, "you still owe this" about finished work costs
34
44
  // the channel its credibility.
package/lib/server.mjs CHANGED
@@ -2918,22 +2918,23 @@ function renderNudge(payload) {
2918
2918
  'track_obligation',
2919
2919
  {
2920
2920
  title: 'Record something you owe',
2921
- description: "Record something you owe: a subject, optionally when it comes due, optionally whose reply would discharge it. ⚠ 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.",
2921
+ 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; it stays visible to its owner only. ⚠ 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.",
2922
2922
  inputSchema: {
2923
2923
  subject: z.string().describe('what is owed, in your own words'),
2924
+ 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.'),
2924
2925
  due_at: z.string().optional().describe('ISO 8601. OMIT when there is no real deadline — never guess one.'),
2925
2926
  anchor_identifier: z.string().optional().describe('e.g. "email:ben@example.com" — records from them become evidence this may be done'),
2926
2927
  source_record_id: z.string().optional().describe('the record this came from, if any'),
2927
2928
  brain: z.string().optional().describe('required only if you belong to more than one brain'),
2928
2929
  },
2929
2930
  },
2930
- async ({ subject, due_at, anchor_identifier, source_record_id, brain }) => {
2931
+ async ({ subject, pages, due_at, anchor_identifier, source_record_id, brain }) => {
2931
2932
  let res
2932
2933
  try {
2933
2934
  res = await fetchCortex(`${BASE}/api/obligations`, {
2934
2935
  method: 'POST',
2935
2936
  headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
2936
- body: JSON.stringify({ subject, dueAt: due_at, anchorIdentifier: anchor_identifier, sourceRecordId: source_record_id, brain }),
2937
+ body: JSON.stringify({ subject, pages, dueAt: due_at, anchorIdentifier: anchor_identifier, sourceRecordId: source_record_id, brain }),
2937
2938
  })
2938
2939
  } catch (e) { return toolError(`Could not reach obligations: ${e.message}`) }
2939
2940
  const out = await res.json().catch(() => null)
@@ -2946,7 +2947,34 @@ function renderNudge(payload) {
2946
2947
  const note = anchor_identifier
2947
2948
  ? `\nRecords carrying ${anchor_identifier} will be offered as evidence this is done. ${nudge}`
2948
2949
  : `\n${nudge}`
2949
- return { content: [{ type: 'text', text: `Recorded: ${subject}${due_at ? ` (due ${due_at.slice(0, 10)})` : ' (no deadline)'}\n${out.id}${note}` }] }
2950
+ const on = Number(out.pages) > 0 ? ` — on ${out.pages} page${out.pages === 1 ? '' : 's'}` : ''
2951
+ return { content: [{ type: 'text', text: `Recorded: ${subject}${due_at ? ` (due ${due_at.slice(0, 10)})` : ' (no deadline)'}${on}\n${out.id}${note}` }] }
2952
+ },
2953
+ )
2954
+
2955
+ server.registerTool(
2956
+ 'set_obligation_pages',
2957
+ {
2958
+ title: 'Say which pages an obligation lives on',
2959
+ description: "Say which page or pages an existing obligation lives on (ADR-0073). The list you pass REPLACES what it had, so pass the complete set. Use it for an obligation my_obligations marks ⚠ — one created before pages were required, or one a machine put on its owner's own page for want of anywhere better — and to correct a wrong placement. READ the pages first and choose; when it is not clear where something belongs, ask the person rather than guessing. An obligation can never be left on zero pages, only its owner can place it, and it stays visible to its owner only.",
2960
+ inputSchema: {
2961
+ obligation_id: z.string().describe('from my_obligations'),
2962
+ pages: z.array(z.string()).min(1).describe('the COMPLETE list of page refs it should live on, as read_page prints them'),
2963
+ },
2964
+ },
2965
+ async ({ obligation_id, pages }) => {
2966
+ let res
2967
+ try {
2968
+ res = await fetchCortex(`${BASE}/api/obligations/pages`, {
2969
+ method: 'POST',
2970
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
2971
+ body: JSON.stringify({ obligationId: obligation_id, pages }),
2972
+ })
2973
+ } catch (e) { return toolError(`Could not reach obligations: ${e.message}`) }
2974
+ const out = await res.json().catch(() => null)
2975
+ if (!res.ok) return toolError(`Could not place it: ${out?.detail ?? out?.error ?? res.status}`)
2976
+ const titles = (out?.pages ?? []).map((p) => `“${String(p.title ?? '').replace(/\s+/g, ' ').trim().slice(0, 60)}”`)
2977
+ return { content: [{ type: 'text', text: `It now lives on: ${titles.join(' · ') || '(no page returned)'}` }] }
2950
2978
  },
2951
2979
  )
2952
2980
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.197",
3
+ "version": "0.9.198",
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": {