@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.
- package/lib/calendar_events.mjs +173 -0
- package/lib/capture.mjs +6 -2
- package/lib/code_graph_cli.mjs +2 -2
- package/lib/edge_extract.mjs +1 -1
- package/lib/extract_typed.mjs +1 -1
- package/lib/graphify_sync.mjs +2 -2
- package/lib/login.mjs +1 -1
- package/lib/obligation_review.mjs +1 -0
- package/lib/obligations_worker.mjs +1 -0
- package/lib/server.mjs +120 -0
- package/lib/uninstall.mjs +5 -5
- package/lib/working_files.mjs +1 -1
- 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/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
|
package/lib/code_graph_cli.mjs
CHANGED
|
@@ -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()
|
package/lib/edge_extract.mjs
CHANGED
|
@@ -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)
|
package/lib/extract_typed.mjs
CHANGED
|
@@ -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)
|
package/lib/graphify_sync.mjs
CHANGED
|
@@ -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()
|
|
@@ -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 */ } }
|
package/lib/working_files.mjs
CHANGED
|
@@ -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