@theronap/cortex-mcp 0.9.130 → 0.9.132
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/server.mjs +117 -0
- package/package.json +1 -1
package/lib/server.mjs
CHANGED
|
@@ -2167,6 +2167,111 @@ function renderNudge(payload) {
|
|
|
2167
2167
|
},
|
|
2168
2168
|
)
|
|
2169
2169
|
|
|
2170
|
+
server.registerTool(
|
|
2171
|
+
'propose_containers',
|
|
2172
|
+
{
|
|
2173
|
+
title: 'Which event could this record belong to?',
|
|
2174
|
+
description: "Given a record, list the events (class sessions, meetings) it could have COME FROM — candidates only, nothing is written. Candidates are generated by time overlap and filtered by whether the container's type admits that kind of record; a CLOSED container still qualifies, because closing seals a candidate set but does not stop admitting (a transcript synced days later still belongs to the meeting it came from). ⚠ OVERLAP IS NOT AN ANSWER: a coding session that overlapped a meeting may be evidence you SKIPPED it. Ranked by how TIGHT the container is, ascending — a point in a 50-minute class is specific evidence, the same point in an all-day event is nearly none. Choose, then call contain_record.",
|
|
2175
|
+
inputSchema: {
|
|
2176
|
+
record_id: z.string().describe('the record to place'),
|
|
2177
|
+
limit: z.number().optional().describe('max candidates (default 10)'),
|
|
2178
|
+
},
|
|
2179
|
+
},
|
|
2180
|
+
async ({ record_id, limit }) => {
|
|
2181
|
+
const qs = new URLSearchParams(limit ? { limit: String(limit) } : {})
|
|
2182
|
+
let res
|
|
2183
|
+
try {
|
|
2184
|
+
res = await fetchCortex(`${BASE}/api/records/${encodeURIComponent(record_id)}/containers?${qs}`,
|
|
2185
|
+
{ headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
2186
|
+
} catch (e) {
|
|
2187
|
+
return toolError(`Could not reach Agnoclast to find containers for ${record_id}: ${e.message}`)
|
|
2188
|
+
}
|
|
2189
|
+
const body = await res.json().catch(() => ({}))
|
|
2190
|
+
if (!res.ok || !body?.ok) return toolError(`Could not propose containers: ${body?.error ?? res.status}`)
|
|
2191
|
+
if (!body.proposals?.length) {
|
|
2192
|
+
return { content: [{ type: 'text', text: `No container candidates for ${record_id}. Either nothing overlaps its time, or no overlapping event's type admits this kind of record.` }] }
|
|
2193
|
+
}
|
|
2194
|
+
const lines = [`Container candidates for ${record_id} (tightest first):`]
|
|
2195
|
+
for (const p of body.proposals) {
|
|
2196
|
+
lines.push(`- ${p.title ?? '(untitled)'} · ${p.containerType} · ${p.containerMinutes}min · starts ${p.startsAt}`
|
|
2197
|
+
+ `\n id: ${p.containerRecordId}${p.late ? '\n LATE — this arrived after the container closed; still a valid home' : ''}`)
|
|
2198
|
+
}
|
|
2199
|
+
lines.push('', 'Nothing has been written. Use contain_record with the id you choose.')
|
|
2200
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] }
|
|
2201
|
+
},
|
|
2202
|
+
)
|
|
2203
|
+
|
|
2204
|
+
server.registerTool(
|
|
2205
|
+
'contain_record',
|
|
2206
|
+
{
|
|
2207
|
+
title: 'Place a record inside an event',
|
|
2208
|
+
description: "Put a record INSIDE an event — the transcript that came out of a meeting, the session that filled a work block. This is a DECISION, not an inference, and it is recorded as one (designated_by: human). ⚠ It only accepts a container that propose_containers actually offered for this record: naming an arbitrary id would skip the checks that keep containment inside one brain and inside the container type's admission set. `kind` defaults to 'produced' (it came OUT of that event); use 'context' for something that was merely going on at the time. Placing a record in an event is different from attaching it to a PAGE: pages are what a record is ABOUT, containers are where it CAME FROM.",
|
|
2209
|
+
inputSchema: {
|
|
2210
|
+
record_id: z.string().describe('the record to place'),
|
|
2211
|
+
container_record_id: z.string().describe('the event to place it in — an id from propose_containers or find_sessions'),
|
|
2212
|
+
kind: z.enum(['produced', 'context']).optional().describe("'produced' (default) = it came out of that event; 'context' = it was merely happening at the same time"),
|
|
2213
|
+
},
|
|
2214
|
+
},
|
|
2215
|
+
async ({ record_id, container_record_id, kind }) => {
|
|
2216
|
+
let res
|
|
2217
|
+
try {
|
|
2218
|
+
res = await fetchCortex(`${BASE}/api/records/${encodeURIComponent(record_id)}/containers`, {
|
|
2219
|
+
method: 'POST',
|
|
2220
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
2221
|
+
body: JSON.stringify({ containerRecordId: container_record_id, kind }),
|
|
2222
|
+
})
|
|
2223
|
+
} catch (e) {
|
|
2224
|
+
return toolError(`Could not reach Agnoclast to place ${record_id}: ${e.message}`)
|
|
2225
|
+
}
|
|
2226
|
+
const body = await res.json().catch(() => ({}))
|
|
2227
|
+
if (!res.ok || !body?.ok) {
|
|
2228
|
+
return toolError(`Could not place ${record_id}: ${body?.error ?? res.status}${body?.detail ? ` — ${body.detail}` : ''}`)
|
|
2229
|
+
}
|
|
2230
|
+
// `contained: false` means it was ALREADY in that container — a no-op, not a failure. Saying
|
|
2231
|
+
// "placed" either way would hide that a second call did nothing.
|
|
2232
|
+
return { content: [{ type: 'text', text: body.contained
|
|
2233
|
+
? `Placed ${record_id} inside ${container_record_id} as '${kind ?? 'produced'}'.`
|
|
2234
|
+
: `${record_id} was already inside ${container_record_id} — nothing changed.` }] }
|
|
2235
|
+
},
|
|
2236
|
+
)
|
|
2237
|
+
|
|
2238
|
+
server.registerTool(
|
|
2239
|
+
'find_sessions',
|
|
2240
|
+
{
|
|
2241
|
+
title: 'Find the calendar sessions on a given day',
|
|
2242
|
+
description: "Find the event records on a local calendar day — how you get the id of a SPECIFIC session (\"the Wednesday MSB 380 class\") so you can place something inside it with capture_record's container_record_id. Pass `tz` as the person's own timezone: a day means the day THEY lived through, and resolving it in UTC puts an evening class on the wrong date. Each result says whether it has a CONTAINER — a session with `container_type: null` cannot hold anything, and naming it as a target fails later for a reason you could not otherwise have seen.",
|
|
2243
|
+
inputSchema: {
|
|
2244
|
+
date: z.string().describe('the local calendar day, YYYY-MM-DD'),
|
|
2245
|
+
tz: z.string().optional().describe("the person's IANA timezone, e.g. America/Denver. Defaults to UTC, which is usually NOT what you want."),
|
|
2246
|
+
q: z.string().optional().describe('case-insensitive title filter, e.g. "MSB 380"'),
|
|
2247
|
+
},
|
|
2248
|
+
},
|
|
2249
|
+
async ({ date, tz, q }) => {
|
|
2250
|
+
const qs = new URLSearchParams({ date, ...(tz ? { tz } : {}), ...(q ? { q } : {}) })
|
|
2251
|
+
let res
|
|
2252
|
+
try {
|
|
2253
|
+
res = await fetchCortex(`${BASE}/api/sessions?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
2254
|
+
} catch (e) {
|
|
2255
|
+
return toolError(`Could not reach Agnoclast to find sessions on ${date}: ${e.message}`)
|
|
2256
|
+
}
|
|
2257
|
+
const body = await res.json().catch(() => ({}))
|
|
2258
|
+
if (!res.ok || !body?.ok) return toolError(`Could not find sessions on ${date}: ${body?.error ?? res.status}`)
|
|
2259
|
+
if (!body.sessions?.length) {
|
|
2260
|
+
return { content: [{ type: 'text', text: `No sessions on ${date}${q ? ` matching "${q}"` : ''}${tz ? ` (${tz})` : ' (UTC — pass tz if that is wrong)'}.` }] }
|
|
2261
|
+
}
|
|
2262
|
+
const lines = [`Sessions on ${date}${tz ? ` (${tz})` : ' (UTC)'}:`]
|
|
2263
|
+
for (const s of body.sessions) {
|
|
2264
|
+
const when = new Date(s.startsAt).toISOString().slice(11, 16)
|
|
2265
|
+
const box = s.containerType
|
|
2266
|
+
? `container: ${s.containerType}${s.sealed ? ', sealed' : ''}${s.alreadyContains ? `, holds ${s.alreadyContains}` : ''}`
|
|
2267
|
+
: '⚠ NO CONTAINER — cannot hold anything'
|
|
2268
|
+
lines.push(`- ${s.title ?? '(untitled)'} · ${when}Z${s.location ? ` · ${s.location}` : ''}\n id: ${s.recordId}\n ${box}${s.seriesKey ? `\n series: ${s.seriesKey}` : ''}`)
|
|
2269
|
+
}
|
|
2270
|
+
lines.push('', 'Use an id as capture_record\'s container_record_id to place something inside that session.')
|
|
2271
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] }
|
|
2272
|
+
},
|
|
2273
|
+
)
|
|
2274
|
+
|
|
2170
2275
|
server.registerTool(
|
|
2171
2276
|
'capture_record',
|
|
2172
2277
|
{
|
|
@@ -2202,6 +2307,18 @@ function renderNudge(payload) {
|
|
|
2202
2307
|
if (!res.ok || !body?.ok) {
|
|
2203
2308
|
return toolError(`Could not capture "${title}": ${body?.error ?? res.status}${body?.detail ? ` — ${body.detail}` : ''}`)
|
|
2204
2309
|
}
|
|
2310
|
+
// ⚠ A SEALED UNIT IS NOT A RECORD YET, AND SAYING "captured" WOULD BE A LIE OF OMISSION.
|
|
2311
|
+
// On a private-intake account the content is stored encrypted and becomes a record only when
|
|
2312
|
+
// materialised — so it cannot be found, routed or contained until then. Reporting it as a
|
|
2313
|
+
// plain success is how someone believes a transcript is filed when it is sitting sealed.
|
|
2314
|
+
if (body.sealed) {
|
|
2315
|
+
const l = [`Stored "${title}" as a SEALED private-intake unit (${body.intakeItemId}).`]
|
|
2316
|
+
l.push('It is NOT a record yet — it becomes one when you materialise it, and only then can it be routed or placed in a container.')
|
|
2317
|
+
if (body.duplicate) l.push('This matched an existing unit rather than creating a new one.')
|
|
2318
|
+
if (body.routed_by?.length) l.push(`Carries: ${body.routed_by.join(', ')} — it will route on those once materialised.`)
|
|
2319
|
+
else l.push('⚠ NO IDENTIFIERS — once materialised it will reach no page.')
|
|
2320
|
+
return { content: [{ type: 'text', text: l.join('\n') }] }
|
|
2321
|
+
}
|
|
2205
2322
|
const lines = [`Captured "${title}" as record ${body.id}.`]
|
|
2206
2323
|
// ⚠ AN UNROUTED RECORD IS REPORTED AS SUCH. It was stored, but nothing will find it — reporting
|
|
2207
2324
|
// that as a plain success is how a record becomes invisible while looking filed.
|
package/package.json
CHANGED