@theronap/agnoclast-mcp 0.9.243 → 0.9.244
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 +173 -0
- package/lib/server.mjs +120 -0
- package/package.json +1 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// The Agnoclast calendar on the stdio server: upcoming, create_event, update_event.
|
|
2
|
+
//
|
|
3
|
+
// ⚠ WHY THIS FILE EXISTS. Those three tools shipped on the claude.ai connector only (web/lib/mcp/tools.ts,
|
|
4
|
+
// 2026-10-06), so a person set up through the desktop app or Claude Code — the stdio server — could track
|
|
5
|
+
// obligations but had no way to read or add calendar events. Found at David Morgan's setup, 2026-10-19.
|
|
6
|
+
//
|
|
7
|
+
// ⚠ THIS IS A PORT, PINNED BY A PARITY TEST. resolveEventTimes / describeEventTime are
|
|
8
|
+
// web/lib/engine/event_time.ts and the three renderers are web/lib/engine/mcp_remote_render.ts. This
|
|
9
|
+
// package ships to npm on its own and cannot import the web app's TypeScript, so the logic is copied —
|
|
10
|
+
// and calendar_events.parity.test.mjs runs both copies on the same inputs, so an edit to one without the
|
|
11
|
+
// other fails CI instead of silently putting a Denver 3pm class at 9am on one surface.
|
|
12
|
+
|
|
13
|
+
export const CONSOLE = 'https://agnoclast.com'
|
|
14
|
+
export const NATIVE_EVENT_SOURCE = 'agnoclast-calendar'
|
|
15
|
+
|
|
16
|
+
const oneLine = (s, max = 140) => String(s ?? '').replace(/\s+/g, ' ').trim().slice(0, max)
|
|
17
|
+
|
|
18
|
+
// ── event_time.ts ───────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const OFFSET_RE = /(Z|[+-]\d{2}:?\d{2})$/i
|
|
21
|
+
const NAIVE_RE = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?$/
|
|
22
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/
|
|
23
|
+
|
|
24
|
+
function offsetMinutes(utcMs, tz) {
|
|
25
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
26
|
+
timeZone: tz, hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
27
|
+
}).formatToParts(new Date(utcMs))
|
|
28
|
+
const n = (t) => Number(parts.find((p) => p.type === t)?.value)
|
|
29
|
+
const asUtc = Date.UTC(n('year'), n('month') - 1, n('day'), n('hour'), n('minute'), n('second'))
|
|
30
|
+
return Math.round((asUtc - utcMs) / 60000)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** An ISO-ish time to a UTC instant. Without an offset it is the wall-clock time in `tz`. */
|
|
34
|
+
export function toInstant(raw, tz) {
|
|
35
|
+
const s = raw.trim()
|
|
36
|
+
if (OFFSET_RE.test(s)) {
|
|
37
|
+
const d = new Date(s)
|
|
38
|
+
return Number.isNaN(d.getTime()) ? null : d
|
|
39
|
+
}
|
|
40
|
+
const m = NAIVE_RE.exec(s)
|
|
41
|
+
if (!m) return null
|
|
42
|
+
const wallAsUtc = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +(m[6] ?? 0))
|
|
43
|
+
let utc = wallAsUtc - offsetMinutes(wallAsUtc, tz) * 60000
|
|
44
|
+
utc = wallAsUtc - offsetMinutes(utc, tz) * 60000
|
|
45
|
+
return new Date(utc)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Validate and resolve an event's start/end, in the person's timezone. */
|
|
49
|
+
export function resolveEventTimes(input, tz) {
|
|
50
|
+
const start = typeof input.start === 'string' ? input.start.trim() : ''
|
|
51
|
+
const end = typeof input.end === 'string' && input.end.trim() ? input.end.trim() : null
|
|
52
|
+
if (!start) return { ok: false, error: 'start is required (ISO 8601, e.g. 2026-10-08T15:00, or a date for an all-day event).' }
|
|
53
|
+
|
|
54
|
+
if (input.allDay === true || DATE_RE.test(start)) {
|
|
55
|
+
const date = start.slice(0, 10)
|
|
56
|
+
if (!DATE_RE.test(date) || Number.isNaN(Date.parse(`${date}T00:00:00Z`))) return { ok: false, error: `Not a date: ${start}` }
|
|
57
|
+
const endDate = end ? end.slice(0, 10) : null
|
|
58
|
+
if (endDate && (!DATE_RE.test(endDate) || endDate < date)) return { ok: false, error: 'An all-day event cannot end before it starts.' }
|
|
59
|
+
return {
|
|
60
|
+
ok: true, allDay: true, date,
|
|
61
|
+
startsAt: `${date}T00:00:00.000Z`,
|
|
62
|
+
endsAt: endDate && endDate !== date ? `${endDate}T23:59:59.000Z` : null,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const s = toInstant(start, tz)
|
|
67
|
+
if (!s) return { ok: false, error: `Could not read the start time: ${start}` }
|
|
68
|
+
let e = null
|
|
69
|
+
if (end) {
|
|
70
|
+
e = toInstant(end, tz)
|
|
71
|
+
if (!e) return { ok: false, error: `Could not read the end time: ${end}` }
|
|
72
|
+
if (e.getTime() <= s.getTime()) return { ok: false, error: 'The event has to end after it starts.' }
|
|
73
|
+
}
|
|
74
|
+
return { ok: true, allDay: false, startsAt: s.toISOString(), endsAt: e ? e.toISOString() : null }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** "Thu, Oct 8, 3:00–4:00 PM" in the person's timezone, or "Tue, Oct 20 (all day)". */
|
|
78
|
+
export function describeEventTime(t, tz) {
|
|
79
|
+
if (t.allDay) {
|
|
80
|
+
const d = new Intl.DateTimeFormat('en-US', { timeZone: 'UTC', weekday: 'short', month: 'short', day: 'numeric' }).format(new Date(`${t.date}T12:00:00Z`))
|
|
81
|
+
return `${d} (all day)`
|
|
82
|
+
}
|
|
83
|
+
const day = new Intl.DateTimeFormat('en-US', { timeZone: tz, weekday: 'short', month: 'short', day: 'numeric' }).format(new Date(t.startsAt))
|
|
84
|
+
const time = (iso) => new Intl.DateTimeFormat('en-US', { timeZone: tz, hour: 'numeric', minute: '2-digit' }).format(new Date(iso))
|
|
85
|
+
return `${day}, ${time(t.startsAt)}${t.endsAt ? `–${time(t.endsAt)}` : ''}`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── mcp_remote_render.ts ────────────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
function dayLabel(d, tz) {
|
|
91
|
+
return new Intl.DateTimeFormat('en-US', { timeZone: tz, weekday: 'short', month: 'short', day: 'numeric' }).format(d)
|
|
92
|
+
}
|
|
93
|
+
function timeLabel(d, tz) {
|
|
94
|
+
return new Intl.DateTimeFormat('en-US', { timeZone: tz, hour: 'numeric', minute: '2-digit' }).format(d)
|
|
95
|
+
}
|
|
96
|
+
function allDayLabel(ymd) {
|
|
97
|
+
return dayLabel(new Date(`${ymd}T12:00:00Z`), 'UTC')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The calendar, in the person's own timezone. An ALL-DAY event renders from `allDayDate`, never `startsAt`. */
|
|
101
|
+
export function renderUpcoming(items, tz, incomplete = false) {
|
|
102
|
+
if (!items.length) {
|
|
103
|
+
return 'Nothing on the calendar in that window.' + (incomplete ? ' (One brain could not be read, so this may be missing events.)' : '') +
|
|
104
|
+
' If the person expected events, their calendar may not be connected — that is set up at ' + CONSOLE + '/connectors.'
|
|
105
|
+
}
|
|
106
|
+
const lines = [`Coming up (times in ${tz}):`]
|
|
107
|
+
let lastDay = ''
|
|
108
|
+
for (const it of items) {
|
|
109
|
+
const start = new Date(it.startsAt)
|
|
110
|
+
const day = it.allDayDate ? allDayLabel(it.allDayDate) : dayLabel(start, tz)
|
|
111
|
+
if (day !== lastDay) { lines.push('', day); lastDay = day }
|
|
112
|
+
const when = it.allDayDate
|
|
113
|
+
? 'all day'
|
|
114
|
+
: `${timeLabel(start, tz)}${it.endsAt ? `–${timeLabel(new Date(it.endsAt), tz)}` : ''}`
|
|
115
|
+
const more = it.furtherOccurrences > 0 ? ` (+${it.furtherOccurrences} more this series)` : ''
|
|
116
|
+
const where = it.location ? ` · ${oneLine(it.location, 80)}` : ''
|
|
117
|
+
const id = it.source === NATIVE_EVENT_SOURCE && it.recordId ? ` [event id ${it.recordId}]` : ''
|
|
118
|
+
lines.push(` ${when} ${oneLine(it.title ?? '(untitled)', 140)}${where}${more}${id}`)
|
|
119
|
+
}
|
|
120
|
+
if (incomplete) lines.push('', '(One brain could not be read, so this may be missing events.)')
|
|
121
|
+
return lines.join('\n')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** create_event's answer. A SEALED event (private-intake accounts) is not on the calendar yet — say so. */
|
|
125
|
+
export function renderEventCreated(title, when, tz, body) {
|
|
126
|
+
if (body?.sealed) {
|
|
127
|
+
return `Saved "${oneLine(title, 200)}" (${when}), but it is NOT on the calendar yet: this account holds new items until they are filed from its intake pile, which an Agnoclast session in Claude Code or the desktop app does. It shows on the calendar once filed.` +
|
|
128
|
+
(body.intakeItemId ? `\nEvent id: ${body.intakeItemId} — if anything about it is wrong (time, end, place, name), fix it with update_event; do not save a second copy.` : '')
|
|
129
|
+
}
|
|
130
|
+
return `Added to the Agnoclast calendar: "${oneLine(title, 200)}", ${when} (${tz}). It shows on ${CONSOLE}/calendar and in upcoming. Nothing was added to Google or Outlook, and no one was invited.` +
|
|
131
|
+
(body?.id ? `\nEvent id: ${body.id} — if anything about it changes, fix it with update_event; do not save a second copy.` : '')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function renderEventUpdated(b) {
|
|
135
|
+
const changed = (b?.changed ?? []).filter((c) => ['title', 'start', 'end', 'location'].includes(c))
|
|
136
|
+
const where = b?.location ? `, at ${oneLine(b.location, 120)}` : ''
|
|
137
|
+
const head = changed.length
|
|
138
|
+
? `Updated "${oneLine(b?.title, 200)}": now ${b?.when ?? ''}${where} (${b?.tz ?? 'UTC'}). Changed: ${changed.join(', ')}.`
|
|
139
|
+
: `Nothing to change: "${oneLine(b?.title, 200)}" is already ${b?.when ?? ''}${where}.`
|
|
140
|
+
return b?.kind === 'sealed'
|
|
141
|
+
? `${head} It is still held in this account's intake pile and shows on the calendar once filed.`
|
|
142
|
+
: `${head} Nothing in Google or Outlook was touched.`
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── request bodies ──────────────────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
/** POST /api/records/capture body for a new Agnoclast-calendar event — the same shape create_event sends on claude.ai. */
|
|
148
|
+
export function captureEventBody({ title, location, notes, brain }, t, when) {
|
|
149
|
+
return {
|
|
150
|
+
title,
|
|
151
|
+
content: [title, when, location, notes].filter(Boolean).join('\n'),
|
|
152
|
+
summary: notes ? `${title} — ${notes}` : title,
|
|
153
|
+
source: NATIVE_EVENT_SOURCE,
|
|
154
|
+
recordType: 'event',
|
|
155
|
+
occurredAt: t.startsAt,
|
|
156
|
+
...(t.endsAt ? { endsAt: t.endsAt } : {}),
|
|
157
|
+
location,
|
|
158
|
+
eventStart: t.allDay ? t.date : t.startsAt,
|
|
159
|
+
...(t.endsAt ? { eventEnd: t.allDay ? t.endsAt.slice(0, 10) : t.endsAt } : {}),
|
|
160
|
+
brain,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** POST /api/events/update body, or null when nothing would change. "" for end/location removes it. */
|
|
165
|
+
export function updateEventBody(args) {
|
|
166
|
+
const body = { eventId: String(args.event_id ?? '').trim() }
|
|
167
|
+
if (typeof args.title === 'string' && args.title.trim()) body.title = args.title.trim()
|
|
168
|
+
if (typeof args.start === 'string' && args.start.trim()) body.start = args.start.trim()
|
|
169
|
+
if (args.end !== undefined) body.end = typeof args.end === 'string' && args.end.trim() ? args.end.trim() : null
|
|
170
|
+
if (typeof args.all_day === 'boolean') body.allDay = args.all_day
|
|
171
|
+
if (args.location !== undefined) body.location = typeof args.location === 'string' && args.location.trim() ? args.location.trim() : null
|
|
172
|
+
return Object.keys(body).length === 1 ? null : body
|
|
173
|
+
}
|
package/lib/server.mjs
CHANGED
|
@@ -35,6 +35,7 @@ import { applyRelease, fetchReleaseState, releaseStateWithin } from './release_g
|
|
|
35
35
|
import { filterUnshownSettling } from './settling_shown.mjs'
|
|
36
36
|
import { renderContactBriefing, renderContactCandidates, renderContactList } from './contacts_render.mjs'
|
|
37
37
|
import { renderConversation, renderMessageInbox } from './messages_render.mjs'
|
|
38
|
+
import { captureEventBody, describeEventTime, renderEventCreated, renderEventUpdated, renderUpcoming, resolveEventTimes, updateEventBody } from './calendar_events.mjs'
|
|
38
39
|
|
|
39
40
|
// PAGE tiers (ADR-0071). Six for a PAGE; a RECORD keeps three (`privacy` on log_session and
|
|
40
41
|
// set_record_privacy below) — an owner-relative tier is applied to a page's records when they are read
|
|
@@ -5510,6 +5511,125 @@ function renderNudge(payload) {
|
|
|
5510
5511
|
},
|
|
5511
5512
|
)
|
|
5512
5513
|
|
|
5514
|
+
// ── The Agnoclast calendar: upcoming, create_event, update_event (lib/calendar_events.mjs) ──────────
|
|
5515
|
+
// These shipped on the claude.ai connector only, so a desktop / Claude Code seat could track
|
|
5516
|
+
// obligations but never read or add an event (David Morgan's setup, 2026-10-19). Same routes, same
|
|
5517
|
+
// bodies, same words as web/lib/mcp/tools.ts; the parity test pins the copied logic.
|
|
5518
|
+
|
|
5519
|
+
// The person's STORED timezone (Settings), as the connector uses — else this machine's.
|
|
5520
|
+
const personTz = async () => {
|
|
5521
|
+
try {
|
|
5522
|
+
const res = await fetchCortex(`${BASE}/api/settings`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
5523
|
+
const out = res.ok ? await res.json().catch(() => null) : null
|
|
5524
|
+
if (typeof out?.timezone === 'string' && out.timezone) return out.timezone
|
|
5525
|
+
} catch { /* fall through to the local zone */ }
|
|
5526
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
|
|
5527
|
+
}
|
|
5528
|
+
|
|
5529
|
+
server.registerTool(
|
|
5530
|
+
'upcoming',
|
|
5531
|
+
{
|
|
5532
|
+
title: "What's coming up",
|
|
5533
|
+
description: "The person's calendar. Use it FIRST whenever they ask what is on their calendar or schedule — today, tonight, tomorrow, this week, their classes or meetings — even when another calendar tool (such as Google Calendar) is connected. It is the one combined view: events from every calendar they connected to Agnoclast, their classes, and events made in Agnoclast, which a single calendar's own tool does not have. Next 14 days by default, in their own timezone; a recurring class is listed once with how many more times it meets. Agnoclast-calendar events carry an event id for update_event.",
|
|
5534
|
+
inputSchema: {
|
|
5535
|
+
days: z.number().int().min(1).max(60).optional().describe('how many days ahead (default 14)'),
|
|
5536
|
+
limit: z.number().int().min(1).max(50).optional().describe('max events (default 15)'),
|
|
5537
|
+
},
|
|
5538
|
+
},
|
|
5539
|
+
async ({ days, limit }) => {
|
|
5540
|
+
const qs = new URLSearchParams({ days: String(days ?? 14), limit: String(limit ?? 15) })
|
|
5541
|
+
let res
|
|
5542
|
+
try {
|
|
5543
|
+
res = await fetchCortex(`${BASE}/api/upcoming?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
5544
|
+
} catch (e) { return toolError(`Could not load the calendar: ${e.message}`) }
|
|
5545
|
+
if (!res.ok) {
|
|
5546
|
+
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
5547
|
+
return toolError(`Could not load the calendar: ${d.message}`)
|
|
5548
|
+
}
|
|
5549
|
+
const out = await res.json().catch(() => null)
|
|
5550
|
+
const tz = await personTz()
|
|
5551
|
+
return { content: [{ type: 'text', text: renderUpcoming(out?.items ?? [], tz, out?.incomplete === true) }] }
|
|
5552
|
+
},
|
|
5553
|
+
)
|
|
5554
|
+
|
|
5555
|
+
server.registerTool(
|
|
5556
|
+
'create_event',
|
|
5557
|
+
{
|
|
5558
|
+
title: 'Add to my Agnoclast calendar',
|
|
5559
|
+
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. 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.",
|
|
5560
|
+
inputSchema: {
|
|
5561
|
+
title: z.string().describe('what the event is, e.g. "Study group — STRAT 421"'),
|
|
5562
|
+
start: z.string().describe('ISO 8601, e.g. "2026-10-08T15:00"; a date (YYYY-MM-DD) for an all-day event'),
|
|
5563
|
+
end: z.string().optional().describe('ISO 8601 end; omit if unknown — never guess one'),
|
|
5564
|
+
all_day: z.boolean().optional().describe('true for an all-day event or deadline'),
|
|
5565
|
+
location: z.string().optional().describe('where, if known'),
|
|
5566
|
+
notes: z.string().optional().describe('anything else the person said about it'),
|
|
5567
|
+
brain: z.string().optional().describe('which brain, by name or org id. Required only when the person belongs to more than one — ask them; placing something in a brain decides who can see it.'),
|
|
5568
|
+
},
|
|
5569
|
+
},
|
|
5570
|
+
async ({ title, start, end, all_day, location, notes, brain }) => {
|
|
5571
|
+
const name = String(title ?? '').trim()
|
|
5572
|
+
if (!name) return toolError('title is required.')
|
|
5573
|
+
const tz = await personTz()
|
|
5574
|
+
const t = resolveEventTimes({ start, end, allDay: all_day }, tz)
|
|
5575
|
+
if (!t.ok) return toolError(t.error)
|
|
5576
|
+
const when = describeEventTime(t, tz)
|
|
5577
|
+
const loc = location?.trim() || undefined
|
|
5578
|
+
const note = notes?.trim() || undefined
|
|
5579
|
+
let res
|
|
5580
|
+
try {
|
|
5581
|
+
res = await fetchCortex(`${BASE}/api/records/capture`, {
|
|
5582
|
+
method: 'POST',
|
|
5583
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
5584
|
+
body: JSON.stringify(captureEventBody({ title: name, location: loc, notes: note, brain: brain?.trim() || undefined }, t, when)),
|
|
5585
|
+
})
|
|
5586
|
+
} catch (e) { return toolError(`Could not reach Agnoclast to add "${name}": ${e.message}`) }
|
|
5587
|
+
const body = await res.json().catch(() => ({}))
|
|
5588
|
+
if (res.status === 400 && body?.error === 'brain_required') {
|
|
5589
|
+
return toolError(`You belong to more than one brain, so say which one "${name}" goes in (my_brains lists them) and call again with \`brain\`. The brain decides who can see it — ask the person if they did not say.`)
|
|
5590
|
+
}
|
|
5591
|
+
if (body?.sealed) return { content: [{ type: 'text', text: renderEventCreated(name, when, tz, body) }] }
|
|
5592
|
+
if (!res.ok || body?.ok !== true) {
|
|
5593
|
+
return toolError(`Could not add "${name}": ${body?.error ?? res.status}${body?.detail ? ` — ${body.detail}` : ''}`)
|
|
5594
|
+
}
|
|
5595
|
+
return { content: [{ type: 'text', text: renderEventCreated(name, when, tz, body) }] }
|
|
5596
|
+
},
|
|
5597
|
+
)
|
|
5598
|
+
|
|
5599
|
+
server.registerTool(
|
|
5600
|
+
'update_event',
|
|
5601
|
+
{
|
|
5602
|
+
title: 'Change an event on my Agnoclast calendar',
|
|
5603
|
+
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.",
|
|
5604
|
+
inputSchema: {
|
|
5605
|
+
event_id: z.string().describe('the event id create_event or upcoming printed'),
|
|
5606
|
+
title: z.string().optional().describe('a new name'),
|
|
5607
|
+
start: z.string().optional().describe('new start, ISO 8601 (e.g. "2026-10-09T12:00"); a date for an all-day event'),
|
|
5608
|
+
end: z.string().optional().describe('new end, ISO 8601; "" removes the end'),
|
|
5609
|
+
all_day: z.boolean().optional().describe('true to make it all-day, false to make it timed (then give start)'),
|
|
5610
|
+
location: z.string().optional().describe('new place; "" removes it'),
|
|
5611
|
+
},
|
|
5612
|
+
},
|
|
5613
|
+
async (args) => {
|
|
5614
|
+
if (!String(args.event_id ?? '').trim()) return toolError('event_id is required — create_event and upcoming print it.')
|
|
5615
|
+
const body = updateEventBody(args)
|
|
5616
|
+
if (!body) return toolError('Say what to change: title, start, end, all_day or location.')
|
|
5617
|
+
let res
|
|
5618
|
+
try {
|
|
5619
|
+
res = await fetchCortex(`${BASE}/api/events/update`, {
|
|
5620
|
+
method: 'POST',
|
|
5621
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
5622
|
+
body: JSON.stringify(body),
|
|
5623
|
+
})
|
|
5624
|
+
} catch (e) { return toolError(`Could not reach Agnoclast to change the event: ${e.message}`) }
|
|
5625
|
+
const b = await res.json().catch(() => null)
|
|
5626
|
+
if (res.status === 404) return toolError(b?.detail ? `Not changed: ${b.detail}` : 'No event of theirs on the Agnoclast calendar has that id.')
|
|
5627
|
+
if (res.status === 409 || res.status === 400) return toolError(`Not changed: ${b?.detail ?? b?.error ?? 'the request was refused'}`)
|
|
5628
|
+
if (!res.ok) return toolError(`Could not change the event: ${b?.error ?? res.status}${b?.detail ? ` — ${b.detail}` : ''}`)
|
|
5629
|
+
return { content: [{ type: 'text', text: renderEventUpdated(b) }] }
|
|
5630
|
+
},
|
|
5631
|
+
)
|
|
5632
|
+
|
|
5513
5633
|
server.registerTool(
|
|
5514
5634
|
'token_activity',
|
|
5515
5635
|
{
|
package/package.json
CHANGED