@theronap/agnoclast-mcp 0.9.251 → 0.9.253

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.
@@ -216,7 +216,7 @@ export const CREATE_EVENT_DESCRIPTION = 'Put an event on the person\'s calendar
216
216
 
217
217
  export const UPDATE_EVENT_DESCRIPTION = 'Fix, move or cancel ONE event the person made on their Agnoclast calendar — a new time, an end or length, a new name or place, or `cancel: true` to take it off the calendar (one meeting of a repeating event too, e.g. no class on a holiday) — when they ask. Pass `event_id` (create_event and upcoming print it) and ONLY what changes. Moving the start keeps the event\'s length unless you also give `end`; a time with no offset is read in their own timezone. Pass "" as `end` or `location` to remove it. `cancel: false` puts a cancelled event back. To change or remove EVERY meeting of a repeating event, use update_series. Only events made on the Agnoclast calendar can be changed — never Google or Outlook ones.'
218
218
 
219
- export const UPDATE_SERIES_DESCRIPTION = 'Change or delete a whole repeating event (a class or standing meeting made in Agnoclast) when the person asks. Name it with `series`: its exact title, its key, or its id; an unknown or ambiguous name answers with the choices. Pass ONLY what changes: `days` (MO TU WE TH FR SA SU), `start_time` / `end_time` (HH:MM, their local time), `from` / `until` (YYYY-MM-DD), `location` ("" removes it), `title`. Changes apply from today on: meetings that already happened keep their time, and a meeting cancelled on its own stays cancelled. `delete: true` removes the series and every upcoming meeting (past ones stay where they were filed). `container_type` changes what each meeting owes: meeting = a transcript or a note · work_block = a work session in the window · deadline = the artifact · appointment = nothing (a class, a visit). To change ONE meeting, use update_event instead.'
219
+ export const UPDATE_SERIES_DESCRIPTION = 'Change or delete a whole repeating event (a class or standing meeting made in Agnoclast) when the person asks. Name it with `series`: its exact title, its key, or its id; an unknown or ambiguous name answers with the choices. Pass ONLY what changes: `days` (MO TU WE TH FR SA SU), `start_time` / `end_time` (HH:MM, their local time), `from` / `until` (YYYY-MM-DD), `location` ("" removes it), `title`. Changes apply from today on: meetings that already happened keep their time, and a meeting cancelled on its own stays cancelled. EXCEPTIONS by date (YYYY-MM-DD): `cancel_dates` skips those meetings (a holiday), `restore_dates` puts them back, `add_dates` adds one extra meeting on that day at the series\' own time (e.g. a Tuesday that runs a Monday schedule); a meeting moved by hand with update_event keeps its new time through any later change. `delete: true` removes the series and every upcoming meeting (past ones stay where they were filed). `container_type` changes what each meeting owes: meeting = a transcript or a note · work_block = a work session in the window · deadline = the artifact · appointment = nothing (a class, a visit). To change ONE meeting, use update_event instead.'
220
220
 
221
221
  /** PATCH /api/series/<ref> body for update_series, or null when nothing would change (event_time.ts seriesPatchBody). */
222
222
  export function seriesPatchBody(args) {
@@ -231,6 +231,9 @@ export function seriesPatchBody(args) {
231
231
  if (str(args.from)) body.windowFrom = str(args.from)
232
232
  if (str(args.until)) body.windowUntil = str(args.until)
233
233
  if (args.location !== undefined) body.location = str(args.location) || null
234
+ if (Array.isArray(args.cancel_dates) && args.cancel_dates.length) body.cancelDates = args.cancel_dates
235
+ if (Array.isArray(args.restore_dates) && args.restore_dates.length) body.restoreDates = args.restore_dates
236
+ if (Array.isArray(args.add_dates) && args.add_dates.length) body.addDates = args.add_dates
234
237
  return Object.keys(body).length === 0 ? null : body
235
238
  }
236
239
 
@@ -251,7 +254,7 @@ export function renderSeriesCreated(title, when, tz, body) {
251
254
  }
252
255
  if (body?.page?.title) lines.push(`Filed to the page "${oneLine(body.page.title, 120)}", so anything from those meetings lands there.`)
253
256
  lines.push(`It shows on ${CONSOLE}/calendar and in upcoming. Nothing was added to Google or Outlook, and no one was invited.`)
254
- lines.push(`To move ONE meeting, use update_event with that meeting's event id from upcoming. Changing the whole series' days or times is not possible yet${body?.identifier ? ` (series ${body.identifier})` : ''}.`)
257
+ lines.push(`To change it later, use update_series${body?.identifier ? ` (series ${body.identifier})` : ''}: its days or times, cancel_dates for a holiday, add_dates for an extra meeting, or delete. update_event changes one meeting by its event id.`)
255
258
  return lines.join('\n')
256
259
  }
257
260
 
@@ -285,7 +288,13 @@ export function renderSeriesRevised(r) {
285
288
  `${r.upcoming ?? 0} upcoming meeting(s) on the calendar${r.firstStartsAt ? `; the next is ${dayLabel(new Date(r.firstStartsAt), tz)}, ${timeLabel(new Date(r.firstStartsAt), tz)}${r.firstEndsAt ? `–${timeLabel(new Date(r.firstEndsAt), tz)}` : ''}` : ''}.`,
286
289
  ]
287
290
  if (r.cancelled) lines.push(`${r.cancelled} meeting(s) no longer on the schedule were cancelled.`)
288
- lines.push('Meetings that already happened keep their time. A meeting cancelled on its own with update_event stays cancelled.')
291
+ const d = r.dates ?? {}
292
+ const day = (ymd) => allDayLabel(ymd)
293
+ if (d.cancelled?.length) lines.push(`Cancelled: ${d.cancelled.map(day).join('; ')}.`)
294
+ if (d.restored?.length) lines.push(`Back on the calendar: ${d.restored.map(day).join('; ')}.`)
295
+ if (d.added?.length) lines.push(`Extra meeting added: ${d.added.map(day).join('; ')}.`)
296
+ if (d.skipped?.length) lines.push(`Not changed: ${d.skipped.map((x) => `${day(x.date)} (${x.why})`).join('; ')}.`)
297
+ lines.push('Meetings that already happened keep their time. Your exceptions (a cancelled, moved or added meeting) stay through any later change.')
289
298
  return lines.join('\n')
290
299
  }
291
300
 
@@ -122,6 +122,8 @@ export function renderObligations(obs, now = Date.now(), extras = null) {
122
122
  const pages = Array.isArray(o.pages) ? o.pages.filter((p) => p?.origin !== 'fallback') : []
123
123
  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` : ''}`)
124
124
  if (o.needsPage === true) lines.push(' ⚠ on no page anyone chose — set_obligation_pages')
125
+ // ADR-0093: who is waiting on it. Absent on an older server: nothing is said.
126
+ if (o.owedTo?.title) lines.push(` → owed to “${String(o.owedTo.title).replace(/\s+/g, ' ').trim().slice(0, 60)}” (ref ${o.owedTo.ref}) — read their page for context`)
125
127
  // Only when it is wider than the default: who else can see this when they look at its page.
126
128
  if (tierReads(o.tier)) lines.push(` visible to: ${tierReads(o.tier)} — they can see it; nobody but you is told about it`)
127
129
  // Whose idea it was, and whether it is on its way to someone else. Both change what to DO with it.
@@ -5,7 +5,7 @@
5
5
  // that web/lib/engine/release.test.ts pins to the server's registry.
6
6
 
7
7
  /** Tools any release flag gates — hidden when the server cannot be asked. Mirror of allGatedTools(). */
8
- export const GATED_FALLBACK = ['my_messages', 'read_conversation', 'send_message']
8
+ export const GATED_FALLBACK = ['my_messages', 'read_conversation', 'send_message', 'set_obligation_owed_to']
9
9
 
10
10
  export const RELEASE_FETCH_TIMEOUT_MS = 2500
11
11
 
package/lib/server.mjs CHANGED
@@ -3095,9 +3095,10 @@ function renderNudge(payload) {
3095
3095
  closed: z.boolean().optional().describe('true = also show discharged/cancelled ones'),
3096
3096
  limit: z.number().optional().describe('default 50'),
3097
3097
  page: z.string().optional().describe("a page's `ref:` as read_page prints it. Lists EVERYONE's open obligations on that page that the person may see — theirs first, then other people's under each owner's name. Use it when a page read says other people have obligations there and the person asks to see them; seeing one does not make it theirs."),
3098
+ owed_to: z.string().optional().describe("a person's page `ref:` — only what the person owes THEM (\"what do I owe Tanner?\"), across every page"),
3098
3099
  },
3099
3100
  },
3100
- async ({ closed, limit, page }) => {
3101
+ async ({ closed, limit, page, owed_to }) => {
3101
3102
  let res
3102
3103
  if (page) {
3103
3104
  try {
@@ -3111,6 +3112,7 @@ function renderNudge(payload) {
3111
3112
  const qs = new URLSearchParams()
3112
3113
  if (closed) qs.set('closed', 'true')
3113
3114
  if (limit) qs.set('limit', String(limit))
3115
+ if (owed_to) qs.set('owedTo', owed_to.trim())
3114
3116
  res = await fetchCortex(`${BASE}/api/obligations?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
3115
3117
  } catch (e) { return toolError(`Could not reach obligations: ${e.message}`) }
3116
3118
  const out = await res.json().catch(() => null)
@@ -3124,7 +3126,7 @@ function renderNudge(payload) {
3124
3126
  'track_obligation',
3125
3127
  {
3126
3128
  title: 'Record something you owe',
3127
- 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.",
3129
+ 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: an invented date will interrupt someone. Omit due_at when there is no real deadline (a standing obligation); a guessed date is indistinguishable from a real one once stored, and leads straight to nagging about something that was never late. ⚠ OWED TO SOMEONE? Pass `owed_to`: the ref of their page (\"I owe Tanner a text\" → Tanner's page). It is attached, so a later agent reads it instead of asking who, and records from their email or phone count as evidence this may be done. `owed_to` is who WAITS; `for` is who OWES. `anchor_identifier` (e.g. `repo:owner/name`) covers evidence that is not a person.",
3128
3130
  inputSchema: {
3129
3131
  subject: z.string().describe('what is owed, in your own words'),
3130
3132
  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.'),
@@ -3132,18 +3134,19 @@ function renderNudge(payload) {
3132
3134
  note: z.string().optional().describe('with `for`: a short note to the person it is given to'),
3133
3135
  due_at: z.string().optional().describe('ISO 8601. OMIT when there is no real deadline — never guess one.'),
3134
3136
  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.'),
3135
- anchor_identifier: z.string().optional().describe('e.g. "email:ben@example.com" — records from them become evidence this may be done'),
3137
+ owed_to: z.string().optional().describe("the `ref:` of the page of the person (or org) this is owed TO — who is waiting on it. Read their page first. Omit when it is owed to nobody in particular."),
3138
+ anchor_identifier: z.string().optional().describe('e.g. "repo:owner/name" — records carrying it become evidence this may be done. For a person, prefer owed_to.'),
3136
3139
  source_record_id: z.string().optional().describe('the record this came from, if any'),
3137
- brain: z.string().optional().describe('required only if you belong to more than one brain'),
3140
+ brain: z.string().optional().describe('the brain\'s name or id — required only if you belong to more than one'),
3138
3141
  },
3139
3142
  },
3140
- async ({ subject, pages, due_at, work_from, anchor_identifier, source_record_id, brain, for: giveTo, note: giveNote }) => {
3143
+ async ({ subject, pages, due_at, work_from, anchor_identifier, source_record_id, brain, for: giveTo, note: giveNote, owed_to }) => {
3141
3144
  let res
3142
3145
  try {
3143
3146
  res = await fetchCortex(`${BASE}/api/obligations`, {
3144
3147
  method: 'POST',
3145
3148
  headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
3146
- body: JSON.stringify({ subject, pages, dueAt: due_at, workFrom: work_from, anchorIdentifier: anchor_identifier, sourceRecordId: source_record_id, brain, for: giveTo, note: giveNote }),
3149
+ body: JSON.stringify({ subject, pages, dueAt: due_at, workFrom: work_from, anchorIdentifier: anchor_identifier, sourceRecordId: source_record_id, brain, for: giveTo, note: giveNote, owedTo: owed_to }),
3147
3150
  })
3148
3151
  } catch (e) { return toolError(`Could not reach obligations: ${e.message}`) }
3149
3152
  const out = await res.json().catch(() => null)
@@ -3154,9 +3157,9 @@ function renderNudge(payload) {
3154
3157
  ? (work_from ? `From ${work_from.slice(0, 10)} it is named daily as "work on now" (and once on the desktop). ` : '')
3155
3158
  + '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.' : '.')
3156
3159
  : 'It has no date, so nothing will raise it \u2014 ask with my_obligations.'
3157
- const note = anchor_identifier
3158
- ? `\nRecords carrying ${anchor_identifier} will be offered as evidence this is done. ${nudge}`
3159
- : `\n${nudge}`
3160
+ const note = (owed_to ? '\nOwed to that person: their page is attached, and records from them will be offered as evidence this is done.' : '')
3161
+ + (anchor_identifier ? `\nRecords carrying ${anchor_identifier} will be offered as evidence this is done.` : '')
3162
+ + `\n${nudge}`
3160
3163
  const on = Number(out.pages) > 0 ? ` — on ${out.pages} page${out.pages === 1 ? '' : 's'}` : ''
3161
3164
  // Given to someone else: say plainly which of the two things happened. "Assigned" and "asked"
3162
3165
  // leave the obligation with different people, and the agent must not report one as the other.
@@ -3446,6 +3449,32 @@ function renderNudge(payload) {
3446
3449
  },
3447
3450
  )
3448
3451
 
3452
+ server.registerTool(
3453
+ 'set_obligation_owed_to',
3454
+ {
3455
+ title: 'Say who an obligation is owed to',
3456
+ description: "Say who an existing obligation is owed TO — the person waiting on it (ADR-0093) — or clear it. Pass the `ref:` of their page (person or org), read first. Their page is attached to the obligation, my_obligations shows \"→ owed to …\", and records from any email, thread or phone claimed on that page become evidence it may be done. Pass an empty string to clear it; clearing does not take the obligation off their page (use set_obligation_pages for that). Owner only.",
3457
+ inputSchema: {
3458
+ obligation_id: z.string().describe('from my_obligations'),
3459
+ owed_to: z.string().describe("the person's page `ref:` as read_page prints it, or \"\" to clear"),
3460
+ },
3461
+ },
3462
+ async ({ obligation_id, owed_to }) => {
3463
+ let res
3464
+ try {
3465
+ res = await fetchCortex(`${BASE}/api/obligations/owed-to`, {
3466
+ method: 'POST',
3467
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
3468
+ body: JSON.stringify({ obligationId: obligation_id, owedTo: owed_to }),
3469
+ })
3470
+ } catch (e) { return toolError(`Could not reach obligations: ${e.message}`) }
3471
+ const out = await res.json().catch(() => null)
3472
+ if (!res.ok) return toolError(`Could not set it: ${out?.detail ?? out?.error ?? res.status}`)
3473
+ const t = out?.owedTo?.title
3474
+ return { content: [{ type: 'text', text: t ? `Owed to “${String(t).replace(/\s+/g, ' ').trim().slice(0, 60)}” — their page is attached.` : 'No longer marked as owed to anyone. Its pages are unchanged.' }] }
3475
+ },
3476
+ )
3477
+
3449
3478
  server.registerTool(
3450
3479
  'propose_obligation',
3451
3480
  {
@@ -5523,13 +5552,16 @@ function renderNudge(payload) {
5523
5552
  until: z.string().optional().describe('new last date, YYYY-MM-DD'),
5524
5553
  location: z.string().optional().describe('new place; "" removes it'),
5525
5554
  container_type: z.enum(['meeting', 'work_block', 'deadline', 'appointment']).optional().describe('what each meeting owes'),
5555
+ cancel_dates: z.array(z.string()).optional().describe('skip the meeting on these dates (YYYY-MM-DD) — e.g. a holiday'),
5556
+ restore_dates: z.array(z.string()).optional().describe('put back a cancelled meeting on these dates'),
5557
+ add_dates: z.array(z.string()).optional().describe('one extra meeting on each of these dates, at the series\' own time'),
5526
5558
  },
5527
5559
  },
5528
5560
  async (args) => {
5529
5561
  const ref = String(args.series ?? '').trim().replace(/^series:/i, '')
5530
5562
  if (!ref) return toolError('series is required — its title, key or id.')
5531
5563
  const body = seriesPatchBody(args)
5532
- if (!body) return toolError('Say what to change: days, start_time, end_time, from, until, location, title, container_type — or delete: true.')
5564
+ if (!body) return toolError('Say what to change: days, start_time, end_time, from, until, location, title, container_type, cancel_dates, restore_dates, add_dates — or delete: true.')
5533
5565
  let res
5534
5566
  try {
5535
5567
  res = await fetchCortex(`${BASE}/api/series/${encodeURIComponent(ref)}`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.251",
3
+ "version": "0.9.253",
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": {