@theronap/agnoclast-mcp 0.9.243 → 0.9.245

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.
@@ -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/capture.mjs CHANGED
@@ -72,7 +72,7 @@ export function repoFullNameFrom(cwd) {
72
72
  if (!cwd) return null
73
73
  try {
74
74
  const url = execFileSync('git', ['-C', String(cwd), 'config', '--get', 'remote.origin.url'], {
75
- encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'],
75
+ encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
76
76
  })
77
77
  return githubFullName(url)
78
78
  } catch {
@@ -108,7 +108,7 @@ export function workingFilesFrom(cwd, max = 40) {
108
108
  const run = (args) => {
109
109
  try {
110
110
  return execFileSync('git', ['-C', String(cwd), ...args], {
111
- encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'],
111
+ encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
112
112
  }).split('\n').map((l) => l.trim()).filter(Boolean)
113
113
  } catch {
114
114
  return []
@@ -316,12 +316,16 @@ function captureLogFd() {
316
316
  // feed it the hook payload on its stdin. Returns true if the child was launched (parent may return
317
317
  // immediately), false if spawning failed (caller then does the work in-band). The child sees
318
318
  // CORTEX_CAPTURE_DETACHED=1 so it runs captureWork() directly instead of forking again.
319
+ // ⚠ WINDOWS: `detached` there means DETACHED_PROCESS — this worker has NO console, so every console
320
+ // program it starts (`claude --print`, `git`) would get a brand-new, visible Windows Terminal window
321
+ // titled with its path. Every spawn in this package passes windowsHide (windows_hide.test.mjs).
319
322
  function spawnDetachedWorker(stdinRaw) {
320
323
  try {
321
324
  const bin = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'cortex-mcp.mjs')
322
325
  const child = spawn(process.execPath, [bin, 'capture'], {
323
326
  env: { ...process.env, CORTEX_CAPTURE_DETACHED: '1' },
324
327
  detached: true,
328
+ windowsHide: true,
325
329
  stdio: ['pipe', captureLogFd(), captureLogFd()],
326
330
  })
327
331
  child.on('error', () => {}) // never let an async spawn error crash the hook
@@ -14,7 +14,7 @@ function graphPath(cwd) {
14
14
  }
15
15
 
16
16
  function hasGraphifyBinary() {
17
- const r = spawnSync('graphify', ['--version'], { encoding: 'utf8', timeout: 10_000 })
17
+ const r = spawnSync('graphify', ['--version'], { encoding: 'utf8', timeout: 10_000, windowsHide: true })
18
18
  return !r.error && r.status === 0
19
19
  }
20
20
 
@@ -50,7 +50,7 @@ export function runCodeGraphQuery({ action, question, from, to, node }, cwd = pr
50
50
  const built = buildArgs({ action, question, from, to, node })
51
51
  if (built.error) return { ok: false, text: built.error }
52
52
 
53
- const r = spawnSync('graphify', built.args, { cwd, encoding: 'utf8', timeout: 60_000, maxBuffer: 4 * 1024 * 1024 })
53
+ const r = spawnSync('graphify', built.args, { cwd, encoding: 'utf8', timeout: 60_000, maxBuffer: 4 * 1024 * 1024, windowsHide: true })
54
54
  if (r.error) return { ok: false, text: `graphify failed to run: ${r.error.message}` }
55
55
  const out = (r.stdout || '').trim()
56
56
  const err = (r.stderr || '').trim()
@@ -496,7 +496,7 @@ export function extractSession(transcript, opts = {}) {
496
496
  const r = spawnSync(
497
497
  'claude',
498
498
  ['--print', '--model', process.env.CORTEX_SUMMARY_MODEL ?? 'claude-haiku-4-5'],
499
- { input: prompt, env: edgeSafeEnv(process.env, { CORTEX_SUMMARIZING: '1' }), encoding: 'utf8', timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024 },
499
+ { input: prompt, env: edgeSafeEnv(process.env, { CORTEX_SUMMARIZING: '1' }), encoding: 'utf8', timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024, windowsHide: true },
500
500
  )
501
501
  const why = classifyEdgeFailure(r, timeoutMs)
502
502
  if (why) return edgeSkip(why.reason, why.detail)
@@ -42,7 +42,7 @@ export function extractTyped(transcript, registry, opts = {}) {
42
42
  const r = spawnSync(
43
43
  'claude',
44
44
  ['--print', '--model', opts.model ?? process.env.CORTEX_SUMMARY_MODEL ?? 'claude-haiku-4-5', prompt],
45
- { env: edgeSafeEnv(process.env, { CORTEX_SUMMARIZING: '1' }), encoding: 'utf8', timeout: 120_000, maxBuffer: 8 * 1024 * 1024 },
45
+ { env: edgeSafeEnv(process.env, { CORTEX_SUMMARIZING: '1' }), encoding: 'utf8', timeout: 120_000, maxBuffer: 8 * 1024 * 1024, windowsHide: true },
46
46
  )
47
47
  if (r.status !== 0 || !r.stdout) return null
48
48
  return parseTyped(r.stdout)
@@ -15,7 +15,7 @@ import { fetchCortex, classify, resolveBase, resolveEnvToken } from './diagnose.
15
15
  // job just reads whatever the tree looks like at that moment, no hook into git operations at all.
16
16
 
17
17
  function run(cmd, args, cwd) {
18
- const r = spawnSync(cmd, args, { cwd, encoding: 'utf8', timeout: 120_000 })
18
+ const r = spawnSync(cmd, args, { cwd, encoding: 'utf8', timeout: 120_000, windowsHide: true })
19
19
  return r.status === 0 ? (r.stdout || '').trim() : null
20
20
  }
21
21
 
@@ -78,7 +78,7 @@ export async function runGraphifySync(argv = []) {
78
78
  return 1
79
79
  }
80
80
 
81
- const update = spawnSync('graphify', ['update', '.'], { cwd, encoding: 'utf8', timeout: 300_000, maxBuffer: 8 * 1024 * 1024 })
81
+ const update = spawnSync('graphify', ['update', '.'], { cwd, encoding: 'utf8', timeout: 300_000, maxBuffer: 8 * 1024 * 1024, windowsHide: true })
82
82
  if (update.status !== 0) {
83
83
  process.stderr.write(`cortex-mcp graphify-sync: \`graphify update\` failed:\n${(update.stderr || update.stdout || '').trim()}\n`)
84
84
  return 1
package/lib/login.mjs CHANGED
@@ -27,7 +27,7 @@ function openBrowser(url) {
27
27
  process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open'
28
28
  const args = process.platform === 'win32' ? ['/c', 'start', '""', url] : [url]
29
29
  try {
30
- const child = spawn(cmd, args, { stdio: 'ignore', detached: true })
30
+ const child = spawn(cmd, args, { stdio: 'ignore', detached: true, windowsHide: true })
31
31
  // If the browser cannot be launched we still printed the URL, so this is genuinely non-fatal.
32
32
  child.on('error', () => {})
33
33
  child.unref()
@@ -144,6 +144,7 @@ function runClaude(prompt, model) {
144
144
  encoding: 'utf8',
145
145
  timeout: REVIEW_TIMEOUT_MS,
146
146
  maxBuffer: 1024 * 1024,
147
+ windowsHide: true,
147
148
  })
148
149
  }
149
150
 
@@ -244,6 +244,7 @@ export function spawnObligationWorker(job) {
244
244
  const child = spawn(process.execPath, [bin, 'extract-obligations'], {
245
245
  env: { ...process.env },
246
246
  detached: true,
247
+ windowsHide: true,
247
248
  stdio: ['pipe', fd, fd],
248
249
  })
249
250
  child.on('error', () => {})
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/lib/uninstall.mjs CHANGED
@@ -221,17 +221,17 @@ export function stripCodexCortexTables(text) {
221
221
  }
222
222
 
223
223
  function safeReaddir(dir) {
224
- try { return execFileSync('ls', ['-1', dir], { encoding: 'utf8' }).split('\n').filter(Boolean) } catch { return [] }
224
+ try { return execFileSync('ls', ['-1', dir], { encoding: 'utf8', windowsHide: true }).split('\n').filter(Boolean) } catch { return [] }
225
225
  }
226
226
  function npxCacheDirs() {
227
227
  const base = join(HOME, '.npm', '_npx')
228
228
  const out = []
229
229
  for (const d of safeReaddir(base)) {
230
230
  const dir = join(base, d)
231
- try { if (execFileSync('grep', ['-rlE', '@theronap/(cortex|agnoclast)-mcp', join(dir, 'package.json')], { encoding: 'utf8' }).trim()) out.push(dir) } catch { /* not one of ours */ }
231
+ try { if (execFileSync('grep', ['-rlE', '@theronap/(cortex|agnoclast)-mcp', join(dir, 'package.json')], { encoding: 'utf8', windowsHide: true }).trim()) out.push(dir) } catch { /* not one of ours */ }
232
232
  }
233
233
  return out
234
234
  }
235
- function tryExec(cmd, args) { try { execFileSync(cmd, args, { stdio: 'ignore' }) } catch { /* best-effort */ } }
236
- function tryExecOut(cmd, args) { try { return execFileSync(cmd, args, { encoding: 'utf8' }) } catch { return null } }
237
- function tryExecIn(cmd, args, input) { try { execFileSync(cmd, args, { input }) } catch { /* best-effort */ } }
235
+ function tryExec(cmd, args) { try { execFileSync(cmd, args, { stdio: 'ignore', windowsHide: true }) } catch { /* best-effort */ } }
236
+ function tryExecOut(cmd, args) { try { return execFileSync(cmd, args, { encoding: 'utf8', windowsHide: true }) } catch { return null } }
237
+ function tryExecIn(cmd, args, input) { try { execFileSync(cmd, args, { input, windowsHide: true }) } catch { /* best-effort */ } }
@@ -383,7 +383,7 @@ export function findTranscript(sessionId, projectsDir = join(homedir(), '.claude
383
383
  function gitRoot(dir) {
384
384
  try {
385
385
  return execFileSync('git', ['-C', dir, 'rev-parse', '--show-toplevel'], {
386
- encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'],
386
+ encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
387
387
  }).trim() || null
388
388
  } catch {
389
389
  return null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.243",
3
+ "version": "0.9.245",
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": {