@theronap/agnoclast-mcp 0.9.250 → 0.9.252

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.
@@ -214,6 +214,29 @@ export function renderEventCreated(title, when, tz, body) {
214
214
 
215
215
  export const CREATE_EVENT_DESCRIPTION = 'Put an event on the person\'s calendar — a class, meeting, deadline, appointment or reminder — whenever they ask to add, schedule or put something on their calendar. Agnoclast is their calendar: use this rather than another calendar tool (such as Google Calendar) unless they name that other calendar. Give `start` as ISO 8601 (a time with no offset is read in their own timezone), an `end`, or `all_day: true` with a date. If they gave no end or length for a timed event, ASK how long before saving rather than leaving it open. For something that REPEATS weekly (a class, a standing meeting), call this ONCE with `repeat` {days, until} and `page`, e.g. start "2026-09-03T15:30", end "2026-09-03T16:45", repeat {days:["TU","TH"], until:"2026-12-10"}, page "STRAT 421" — never one call per meeting. It appears on their Agnoclast calendar and in `upcoming`; it is not copied to Google or Outlook and invites no one. To change it later, use update_event — never save a second copy.'
216
216
 
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
+
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
+
221
+ /** PATCH /api/series/<ref> body for update_series, or null when nothing would change (event_time.ts seriesPatchBody). */
222
+ export function seriesPatchBody(args) {
223
+ const body = {}
224
+ const str = (v) => (typeof v === 'string' ? v.trim() : undefined)
225
+ if (str(args.container_type)) body.containerType = str(args.container_type)
226
+ if (args.delete === true) body.delete = true
227
+ if (str(args.title)) body.title = str(args.title)
228
+ if (Array.isArray(args.days)) body.days = args.days
229
+ if (str(args.start_time)) body.startsTime = str(args.start_time)
230
+ if (str(args.end_time)) body.endsTime = str(args.end_time)
231
+ if (str(args.from)) body.windowFrom = str(args.from)
232
+ if (str(args.until)) body.windowUntil = str(args.until)
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
237
+ return Object.keys(body).length === 0 ? null : body
238
+ }
239
+
217
240
  /** create_event's answer for a repeating event. */
218
241
  export function renderSeriesCreated(title, when, tz, body) {
219
242
  const n = body?.generated ?? 0
@@ -231,7 +254,47 @@ export function renderSeriesCreated(title, when, tz, body) {
231
254
  }
232
255
  if (body?.page?.title) lines.push(`Filed to the page "${oneLine(body.page.title, 120)}", so anything from those meetings lands there.`)
233
256
  lines.push(`It shows on ${CONSOLE}/calendar and in upcoming. Nothing was added to Google or Outlook, and no one was invited.`)
234
- 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.`)
258
+ return lines.join('\n')
259
+ }
260
+
261
+ /** update_event with `cancel` (mcp_remote_render.ts renderEventCancelled). */
262
+ export function renderEventCancelled(b) {
263
+ const t = oneLine(b?.title ?? 'the event', 200)
264
+ return b?.cancelled === false
265
+ ? `Restored "${t}": it is back on the Agnoclast calendar and in upcoming.`
266
+ : `Cancelled "${t}": it is off the Agnoclast calendar and upcoming. To put it back, call update_event with cancel: false. Nothing in Google or Outlook was touched.`
267
+ }
268
+
269
+ const DAY = { MO: 'Mon', TU: 'Tue', WE: 'Wed', TH: 'Thu', FR: 'Fri', SA: 'Sat', SU: 'Sun' }
270
+
271
+ /** update_series edit / delete (mcp_remote_render.ts renderSeriesRevised). */
272
+ export function renderSeriesRevised(r) {
273
+ const title = oneLine(r?.series?.title ?? 'the series', 200)
274
+ if (r?.deleted) {
275
+ return `Deleted the series "${title}": ${r.cancelled ?? 0} upcoming meeting(s) cancelled and off the calendar. Meetings that already happened stay where they were filed. Nothing in Google or Outlook was touched.`
276
+ }
277
+ if (!r?.changed?.length) return `Nothing to change: "${title}" already has that schedule.`
278
+ const s = r.schedule ?? {}
279
+ const names = (s.days ?? []).map((d) => DAY[d] ?? d)
280
+ const days = names.length > 1 ? `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}` : (names[0] ?? '')
281
+ const [h, m] = String(s.startsTime ?? '00:00').split(':').map(Number)
282
+ const clock = (mins) => new Intl.DateTimeFormat('en-US', { timeZone: 'UTC', hour: 'numeric', minute: '2-digit' }).format(new Date(Date.UTC(2000, 0, 1, 0, mins % 1440)))
283
+ const start = h * 60 + m
284
+ const window = s.windowUntil ? `${allDayLabel(String(s.windowFrom))} to ${allDayLabel(s.windowUntil)}` : `from ${allDayLabel(String(s.windowFrom))}, no end date`
285
+ const tz = s.timezone ?? 'UTC'
286
+ const lines = [
287
+ `Updated "${title}" (changed: ${r.changed.join(', ')}). Now every ${days}, ${clock(start)}–${clock(start + (s.durationMinutes ?? 0))}, ${window}${s.location ? `, at ${oneLine(s.location, 120)}` : ''} (${tz}).`,
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)}` : ''}` : ''}.`,
289
+ ]
290
+ if (r.cancelled) lines.push(`${r.cancelled} meeting(s) no longer on the schedule were 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.')
235
298
  return lines.join('\n')
236
299
  }
237
300
 
@@ -273,5 +336,6 @@ export function updateEventBody(args) {
273
336
  if (args.end !== undefined) body.end = typeof args.end === 'string' && args.end.trim() ? args.end.trim() : null
274
337
  if (typeof args.all_day === 'boolean') body.allDay = args.all_day
275
338
  if (args.location !== undefined) body.location = typeof args.location === 'string' && args.location.trim() ? args.location.trim() : null
339
+ if (typeof args.cancel === 'boolean') body.cancel = args.cancel
276
340
  return Object.keys(body).length === 1 ? null : body
277
341
  }
package/lib/server.mjs CHANGED
@@ -38,6 +38,7 @@ import { renderConversation, renderMessageInbox } from './messages_render.mjs'
38
38
  import {
39
39
  CREATE_EVENT_DESCRIPTION, captureEventBody, describeEventTime, describeSeriesPlacement, renderEventCreated, renderEventUpdated,
40
40
  renderSeriesCreated, renderUpcoming, resolveEventTimes, resolveSeriesPlacement, seriesRequestBody, updateEventBody,
41
+ UPDATE_EVENT_DESCRIPTION, UPDATE_SERIES_DESCRIPTION, renderEventCancelled, renderSeriesRevised, seriesPatchBody,
41
42
  } from './calendar_events.mjs'
42
43
 
43
44
  // PAGE tiers (ADR-0071). Six for a PAGE; a RECORD keeps three (`privacy` on log_session and
@@ -5509,33 +5510,51 @@ function renderNudge(payload) {
5509
5510
  server.registerTool(
5510
5511
  'update_series',
5511
5512
  {
5512
- title: 'Change a recurring series\' container type',
5513
- 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.',
5513
+ title: 'Change or delete a repeating event',
5514
+ description: UPDATE_SERIES_DESCRIPTION,
5514
5515
  inputSchema: {
5515
5516
  series: z.string().describe('the series — its exact title ("ENT 425"), its key (with or without "series:"), or its id'),
5516
- container_type: z.enum(['meeting', 'work_block', 'deadline', 'appointment']).describe('what it should collect and owe'),
5517
+ delete: z.boolean().optional().describe('true removes the series and every upcoming meeting; past ones stay filed'),
5518
+ title: z.string().optional().describe('a new name'),
5519
+ days: z.array(z.string()).optional().describe('the days it meets now: MO TU WE TH FR SA SU'),
5520
+ start_time: z.string().optional().describe('new start, HH:MM in their local time (e.g. "15:30")'),
5521
+ end_time: z.string().optional().describe('new end, HH:MM in their local time'),
5522
+ from: z.string().optional().describe('new first date, YYYY-MM-DD'),
5523
+ until: z.string().optional().describe('new last date, YYYY-MM-DD'),
5524
+ location: z.string().optional().describe('new place; "" removes it'),
5525
+ container_type: z.enum(['meeting', 'work_block', 'deadline', 'appointment']).optional().describe('what each meeting owes'),
5526
+ cancel_dates: z.array(z.string()).optional().describe('skip the meeting on these dates (YYYY-MM-DD) — e.g. a holiday'),
5527
+ restore_dates: z.array(z.string()).optional().describe('put back a cancelled meeting on these dates'),
5528
+ add_dates: z.array(z.string()).optional().describe('one extra meeting on each of these dates, at the series\' own time'),
5517
5529
  },
5518
5530
  },
5519
- async ({ series, container_type }) => {
5520
- const ref = String(series).trim().replace(/^series:/i, '')
5531
+ async (args) => {
5532
+ const ref = String(args.series ?? '').trim().replace(/^series:/i, '')
5533
+ if (!ref) return toolError('series is required — its title, key or id.')
5534
+ const body = seriesPatchBody(args)
5535
+ 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.')
5521
5536
  let res
5522
5537
  try {
5523
5538
  res = await fetchCortex(`${BASE}/api/series/${encodeURIComponent(ref)}`, {
5524
5539
  method: 'PATCH',
5525
5540
  headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
5526
- body: JSON.stringify({ containerType: container_type }),
5541
+ body: JSON.stringify(body),
5527
5542
  })
5528
5543
  } catch (e) {
5529
5544
  return toolError(`Not changed: ${e.message}`)
5530
5545
  }
5531
5546
  if (!res.ok) {
5532
- const body = await res.text()
5547
+ const text = await res.text()
5533
5548
  let j = null
5534
- try { j = JSON.parse(body) } catch { /* not JSON */ }
5549
+ try { j = JSON.parse(text) } catch { /* not JSON */ }
5535
5550
  if (j && [400, 404, 409].includes(res.status)) return toolError(formatSeriesRefusal(j))
5536
- return toolError(`Not changed: ${classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message}`)
5551
+ return toolError(`Not changed: ${classify(res.status, res.headers.get('content-type'), text, res.headers.get('x-vercel-id')).message}`)
5537
5552
  }
5538
- return { content: [{ type: 'text', text: formatSeriesUpdated(await res.json()) }] }
5553
+ const out = await res.json()
5554
+ const parts = []
5555
+ if (out.from !== undefined) parts.push(formatSeriesUpdated(out))
5556
+ if (out.revision) parts.push(renderSeriesRevised(out.revision))
5557
+ return { content: [{ type: 'text', text: parts.join('\n\n') || 'Done.' }] }
5539
5558
  },
5540
5559
  )
5541
5560
 
@@ -5693,7 +5712,7 @@ function renderNudge(payload) {
5693
5712
  'update_event',
5694
5713
  {
5695
5714
  title: 'Change an event on my Agnoclast calendar',
5696
- description: "Fix or move an event the person made on their Agnoclast calendar — a new time, an end or length, a new name or place — 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. Only events made on the Agnoclast calendar can be changed — never Google or Outlook ones — and this cannot cancel an event.",
5715
+ description: UPDATE_EVENT_DESCRIPTION,
5697
5716
  inputSchema: {
5698
5717
  event_id: z.string().describe('the event id create_event or upcoming printed'),
5699
5718
  title: z.string().optional().describe('a new name'),
@@ -5701,12 +5720,13 @@ function renderNudge(payload) {
5701
5720
  end: z.string().optional().describe('new end, ISO 8601; "" removes the end'),
5702
5721
  all_day: z.boolean().optional().describe('true to make it all-day, false to make it timed (then give start)'),
5703
5722
  location: z.string().optional().describe('new place; "" removes it'),
5723
+ cancel: z.boolean().optional().describe('true takes it off the calendar (one meeting of a repeating event too); false puts a cancelled one back'),
5704
5724
  },
5705
5725
  },
5706
5726
  async (args) => {
5707
5727
  if (!String(args.event_id ?? '').trim()) return toolError('event_id is required — create_event and upcoming print it.')
5708
5728
  const body = updateEventBody(args)
5709
- if (!body) return toolError('Say what to change: title, start, end, all_day or location.')
5729
+ if (!body) return toolError('Say what to change: title, start, end, all_day, location, or cancel.')
5710
5730
  let res
5711
5731
  try {
5712
5732
  res = await fetchCortex(`${BASE}/api/events/update`, {
@@ -5719,6 +5739,7 @@ function renderNudge(payload) {
5719
5739
  if (res.status === 404) return toolError(b?.detail ? `Not changed: ${b.detail}` : 'No event of theirs on the Agnoclast calendar has that id.')
5720
5740
  if (res.status === 409 || res.status === 400) return toolError(`Not changed: ${b?.detail ?? b?.error ?? 'the request was refused'}`)
5721
5741
  if (!res.ok) return toolError(`Could not change the event: ${b?.error ?? res.status}${b?.detail ? ` — ${b.detail}` : ''}`)
5742
+ if (typeof b?.cancelled === 'boolean') return { content: [{ type: 'text', text: renderEventCancelled(b) }] }
5722
5743
  return { content: [{ type: 'text', text: renderEventUpdated(b) }] }
5723
5744
  },
5724
5745
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.250",
3
+ "version": "0.9.252",
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": {