@theronap/agnoclast-mcp 0.9.249 → 0.9.251
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/calendar_events.mjs +55 -0
- package/lib/server.mjs +42 -14
- package/package.json +1 -1
package/lib/calendar_events.mjs
CHANGED
|
@@ -214,6 +214,26 @@ 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. `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
|
+
return Object.keys(body).length === 0 ? null : body
|
|
235
|
+
}
|
|
236
|
+
|
|
217
237
|
/** create_event's answer for a repeating event. */
|
|
218
238
|
export function renderSeriesCreated(title, when, tz, body) {
|
|
219
239
|
const n = body?.generated ?? 0
|
|
@@ -235,6 +255,40 @@ export function renderSeriesCreated(title, when, tz, body) {
|
|
|
235
255
|
return lines.join('\n')
|
|
236
256
|
}
|
|
237
257
|
|
|
258
|
+
/** update_event with `cancel` (mcp_remote_render.ts renderEventCancelled). */
|
|
259
|
+
export function renderEventCancelled(b) {
|
|
260
|
+
const t = oneLine(b?.title ?? 'the event', 200)
|
|
261
|
+
return b?.cancelled === false
|
|
262
|
+
? `Restored "${t}": it is back on the Agnoclast calendar and in upcoming.`
|
|
263
|
+
: `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.`
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const DAY = { MO: 'Mon', TU: 'Tue', WE: 'Wed', TH: 'Thu', FR: 'Fri', SA: 'Sat', SU: 'Sun' }
|
|
267
|
+
|
|
268
|
+
/** update_series edit / delete (mcp_remote_render.ts renderSeriesRevised). */
|
|
269
|
+
export function renderSeriesRevised(r) {
|
|
270
|
+
const title = oneLine(r?.series?.title ?? 'the series', 200)
|
|
271
|
+
if (r?.deleted) {
|
|
272
|
+
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.`
|
|
273
|
+
}
|
|
274
|
+
if (!r?.changed?.length) return `Nothing to change: "${title}" already has that schedule.`
|
|
275
|
+
const s = r.schedule ?? {}
|
|
276
|
+
const names = (s.days ?? []).map((d) => DAY[d] ?? d)
|
|
277
|
+
const days = names.length > 1 ? `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}` : (names[0] ?? '')
|
|
278
|
+
const [h, m] = String(s.startsTime ?? '00:00').split(':').map(Number)
|
|
279
|
+
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)))
|
|
280
|
+
const start = h * 60 + m
|
|
281
|
+
const window = s.windowUntil ? `${allDayLabel(String(s.windowFrom))} to ${allDayLabel(s.windowUntil)}` : `from ${allDayLabel(String(s.windowFrom))}, no end date`
|
|
282
|
+
const tz = s.timezone ?? 'UTC'
|
|
283
|
+
const lines = [
|
|
284
|
+
`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}).`,
|
|
285
|
+
`${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
|
+
]
|
|
287
|
+
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.')
|
|
289
|
+
return lines.join('\n')
|
|
290
|
+
}
|
|
291
|
+
|
|
238
292
|
export function renderEventUpdated(b) {
|
|
239
293
|
const changed = (b?.changed ?? []).filter((c) => ['title', 'start', 'end', 'location'].includes(c))
|
|
240
294
|
const where = b?.location ? `, at ${oneLine(b.location, 120)}` : ''
|
|
@@ -273,5 +327,6 @@ export function updateEventBody(args) {
|
|
|
273
327
|
if (args.end !== undefined) body.end = typeof args.end === 'string' && args.end.trim() ? args.end.trim() : null
|
|
274
328
|
if (typeof args.all_day === 'boolean') body.allDay = args.all_day
|
|
275
329
|
if (args.location !== undefined) body.location = typeof args.location === 'string' && args.location.trim() ? args.location.trim() : null
|
|
330
|
+
if (typeof args.cancel === 'boolean') body.cancel = args.cancel
|
|
276
331
|
return Object.keys(body).length === 1 ? null : body
|
|
277
332
|
}
|
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
|
|
@@ -1951,7 +1952,7 @@ function renderNudge(payload) {
|
|
|
1951
1952
|
{
|
|
1952
1953
|
title: 'Ask to send an email from the user\'s mailbox (Gmail or Outlook)',
|
|
1953
1954
|
description:
|
|
1954
|
-
'Send an email from the USER\'S OWN connected mailbox, Gmail or Outlook. By default this does NOT send immediately: it creates a request that WAITS for the user to approve it in Agnoclast, and it goes out at once only if their own send rule allows it. The message is frozen when the request is made, so write the final wording. Always show the user what you are about to send and get their agreement first. Returns either an approval link to relay to them, or confirmation that it already went. ⚠ NEVER call this twice for the same email — it is not idempotent, and a retry can send twice.',
|
|
1955
|
+
'Send an email from the USER\'S OWN connected mailbox, Gmail or Outlook. By default this does NOT send immediately: it creates a request that WAITS for the user to approve it in Agnoclast, and it goes out at once only if their own send rule allows it. The message is frozen when the request is made, so write the final wording. Always show the user what you are about to send and get their agreement first. Returns either an approval link to relay to them, or confirmation that it already went. ⚠ NEVER call this twice for the same email — it is not idempotent, and a retry can send twice. IF YOU ARE AN AI AGENT WITH YOUR OWN IDENTITY (a member agent): you send from YOUR OWN address, labelled as an AI agent of your owner, and one of your overseers approves unless their rule for you allows it; a line saying an AI wrote it is added for you. Pass from: \'overseer\' only if an overseer has opened their mailbox to you.',
|
|
1955
1956
|
inputSchema: {
|
|
1956
1957
|
to: z.array(z.string()).min(1).describe('recipient email addresses'),
|
|
1957
1958
|
subject: z.string().min(1).describe('the subject line'),
|
|
@@ -1961,9 +1962,11 @@ function renderNudge(payload) {
|
|
|
1961
1962
|
replyToRecordId: z.string().optional().describe('reply in the thread of this Agnoclast email record'),
|
|
1962
1963
|
replyToMessageId: z.string().optional().describe('reply in the thread of this message id (Gmail or Outlook)'),
|
|
1963
1964
|
account: z.string().email().optional().describe('which connected mailbox (Gmail or Outlook) to send from, when the user has more than one'),
|
|
1965
|
+
from: z.enum(['own', 'overseer']).optional().describe("AI member agents only: 'own' (default) = your own address; 'overseer' = the mailbox an overseer opened to you"),
|
|
1966
|
+
brain: z.string().optional().describe('AI member agents only: which brain this email is for, when you are in more than one'),
|
|
1964
1967
|
},
|
|
1965
1968
|
},
|
|
1966
|
-
async ({ to, subject, text, cc, bcc, replyToRecordId, replyToMessageId, account }) => {
|
|
1969
|
+
async ({ to, subject, text, cc, bcc, replyToRecordId, replyToMessageId, account, from, brain }) => {
|
|
1967
1970
|
let res
|
|
1968
1971
|
try {
|
|
1969
1972
|
res = await fetchCortex(`${BASE}/api/gmail/send-requests`, {
|
|
@@ -1975,6 +1978,8 @@ function renderNudge(payload) {
|
|
|
1975
1978
|
...(replyToRecordId ? { replyToRecordId } : {}),
|
|
1976
1979
|
...(replyToMessageId ? { replyToMessageId } : {}),
|
|
1977
1980
|
...(account ? { account } : {}),
|
|
1981
|
+
...(from ? { from } : {}),
|
|
1982
|
+
...(brain ? { brain } : {}),
|
|
1978
1983
|
}),
|
|
1979
1984
|
}, { idempotent: false, retries: 0 })
|
|
1980
1985
|
} catch (e) {
|
|
@@ -1991,6 +1996,12 @@ function renderNudge(payload) {
|
|
|
1991
1996
|
const reviewUrl = body?.reviewUrl
|
|
1992
1997
|
const acct = body?.request?.account ?? 'your mailbox'
|
|
1993
1998
|
const n = body?.request?.recipientCount ?? to.length
|
|
1999
|
+
// ADR-0092: a member agent's request waits for one of its OVERSEERS, not for "you".
|
|
2000
|
+
if (reviewUrl && body?.waitingFor) {
|
|
2001
|
+
return { content: [{ type: 'text', text:
|
|
2002
|
+
`Email request created from ${acct} to ${n} recipient${n === 1 ? '' : 's'}. It is NOT sent yet: it waits for ` +
|
|
2003
|
+
`${body.waitingFor} to approve it in Agnoclast (${reviewUrl}). Do not send it again.` }] }
|
|
2004
|
+
}
|
|
1994
2005
|
if (reviewUrl) {
|
|
1995
2006
|
// Relayed VERBATIM — this exact sentence is the contract the create route records.
|
|
1996
2007
|
return { content: [{ type: 'text', text:
|
|
@@ -5499,33 +5510,48 @@ function renderNudge(payload) {
|
|
|
5499
5510
|
server.registerTool(
|
|
5500
5511
|
'update_series',
|
|
5501
5512
|
{
|
|
5502
|
-
title: 'Change
|
|
5503
|
-
description:
|
|
5513
|
+
title: 'Change or delete a repeating event',
|
|
5514
|
+
description: UPDATE_SERIES_DESCRIPTION,
|
|
5504
5515
|
inputSchema: {
|
|
5505
5516
|
series: z.string().describe('the series — its exact title ("ENT 425"), its key (with or without "series:"), or its id'),
|
|
5506
|
-
|
|
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'),
|
|
5507
5526
|
},
|
|
5508
5527
|
},
|
|
5509
|
-
async (
|
|
5510
|
-
const ref = String(series).trim().replace(/^series:/i, '')
|
|
5528
|
+
async (args) => {
|
|
5529
|
+
const ref = String(args.series ?? '').trim().replace(/^series:/i, '')
|
|
5530
|
+
if (!ref) return toolError('series is required — its title, key or id.')
|
|
5531
|
+
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.')
|
|
5511
5533
|
let res
|
|
5512
5534
|
try {
|
|
5513
5535
|
res = await fetchCortex(`${BASE}/api/series/${encodeURIComponent(ref)}`, {
|
|
5514
5536
|
method: 'PATCH',
|
|
5515
5537
|
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
5516
|
-
body: JSON.stringify(
|
|
5538
|
+
body: JSON.stringify(body),
|
|
5517
5539
|
})
|
|
5518
5540
|
} catch (e) {
|
|
5519
5541
|
return toolError(`Not changed: ${e.message}`)
|
|
5520
5542
|
}
|
|
5521
5543
|
if (!res.ok) {
|
|
5522
|
-
const
|
|
5544
|
+
const text = await res.text()
|
|
5523
5545
|
let j = null
|
|
5524
|
-
try { j = JSON.parse(
|
|
5546
|
+
try { j = JSON.parse(text) } catch { /* not JSON */ }
|
|
5525
5547
|
if (j && [400, 404, 409].includes(res.status)) return toolError(formatSeriesRefusal(j))
|
|
5526
|
-
return toolError(`Not changed: ${classify(res.status, res.headers.get('content-type'),
|
|
5548
|
+
return toolError(`Not changed: ${classify(res.status, res.headers.get('content-type'), text, res.headers.get('x-vercel-id')).message}`)
|
|
5527
5549
|
}
|
|
5528
|
-
|
|
5550
|
+
const out = await res.json()
|
|
5551
|
+
const parts = []
|
|
5552
|
+
if (out.from !== undefined) parts.push(formatSeriesUpdated(out))
|
|
5553
|
+
if (out.revision) parts.push(renderSeriesRevised(out.revision))
|
|
5554
|
+
return { content: [{ type: 'text', text: parts.join('\n\n') || 'Done.' }] }
|
|
5529
5555
|
},
|
|
5530
5556
|
)
|
|
5531
5557
|
|
|
@@ -5683,7 +5709,7 @@ function renderNudge(payload) {
|
|
|
5683
5709
|
'update_event',
|
|
5684
5710
|
{
|
|
5685
5711
|
title: 'Change an event on my Agnoclast calendar',
|
|
5686
|
-
description:
|
|
5712
|
+
description: UPDATE_EVENT_DESCRIPTION,
|
|
5687
5713
|
inputSchema: {
|
|
5688
5714
|
event_id: z.string().describe('the event id create_event or upcoming printed'),
|
|
5689
5715
|
title: z.string().optional().describe('a new name'),
|
|
@@ -5691,12 +5717,13 @@ function renderNudge(payload) {
|
|
|
5691
5717
|
end: z.string().optional().describe('new end, ISO 8601; "" removes the end'),
|
|
5692
5718
|
all_day: z.boolean().optional().describe('true to make it all-day, false to make it timed (then give start)'),
|
|
5693
5719
|
location: z.string().optional().describe('new place; "" removes it'),
|
|
5720
|
+
cancel: z.boolean().optional().describe('true takes it off the calendar (one meeting of a repeating event too); false puts a cancelled one back'),
|
|
5694
5721
|
},
|
|
5695
5722
|
},
|
|
5696
5723
|
async (args) => {
|
|
5697
5724
|
if (!String(args.event_id ?? '').trim()) return toolError('event_id is required — create_event and upcoming print it.')
|
|
5698
5725
|
const body = updateEventBody(args)
|
|
5699
|
-
if (!body) return toolError('Say what to change: title, start, end, all_day or
|
|
5726
|
+
if (!body) return toolError('Say what to change: title, start, end, all_day, location, or cancel.')
|
|
5700
5727
|
let res
|
|
5701
5728
|
try {
|
|
5702
5729
|
res = await fetchCortex(`${BASE}/api/events/update`, {
|
|
@@ -5709,6 +5736,7 @@ function renderNudge(payload) {
|
|
|
5709
5736
|
if (res.status === 404) return toolError(b?.detail ? `Not changed: ${b.detail}` : 'No event of theirs on the Agnoclast calendar has that id.')
|
|
5710
5737
|
if (res.status === 409 || res.status === 400) return toolError(`Not changed: ${b?.detail ?? b?.error ?? 'the request was refused'}`)
|
|
5711
5738
|
if (!res.ok) return toolError(`Could not change the event: ${b?.error ?? res.status}${b?.detail ? ` — ${b.detail}` : ''}`)
|
|
5739
|
+
if (typeof b?.cancelled === 'boolean') return { content: [{ type: 'text', text: renderEventCancelled(b) }] }
|
|
5712
5740
|
return { content: [{ type: 'text', text: renderEventUpdated(b) }] }
|
|
5713
5741
|
},
|
|
5714
5742
|
)
|
package/package.json
CHANGED