@theronap/cortex-mcp 0.9.26 → 0.9.28

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.
Files changed (2) hide show
  1. package/lib/server.mjs +65 -8
  2. package/package.json +1 -1
package/lib/server.mjs CHANGED
@@ -249,12 +249,69 @@ export async function runServer(version) {
249
249
  description:
250
250
  'READ the full authored wiki page for one node (project/person/org/you) by its canonical name — every section, every tier you can see. This is how you READ a node; `grep` only LOCATES pages (snippets + their [[links]]), it does not read them. Navigate like a researcher: read the page you need, then FOLLOW its inline [[links]] by calling read_page on each linked name — keep following while the linked pages stay relevant, stop when they do not. You decide how deep to go. Returns only what you are permitted to see.',
251
251
  inputSchema: {
252
- name: z.string().describe('the canonical node name exactly as written (e.g. "Cortex", "Ben", or a [[link]] target)'),
252
+ name: z.string().describe('the canonical node name exactly as written (e.g. "Cortex", "Ben", or a [[link]] target) — identifier links ([[repo:owner/name]]) resolve to their authored HOME + a visible-event count'),
253
253
  kind: z.enum(['project', 'person', 'org', 'user']).optional().describe('node kind (default project; pass person/org for people/teams)'),
254
+ expand: z.boolean().optional().describe('identifier names only: also list recent visible timeline events for this identifier (default: home + count)'),
255
+ history: z.boolean().optional().describe('node names only: return the node\'s TIMELINE (events joined via its [[repo:…]] stamps, reverse-chron, viewer-visible) instead of the page body. The page is the present; this is the history.'),
254
256
  },
255
257
  },
256
- async ({ name, kind }) => {
258
+ async ({ name, kind, expand, history }) => {
257
259
  const k = kind ?? 'project'
260
+ // PER-NODE TIMELINE (slice 4): history = the projection over the node's identifier stamps.
261
+ if (history && !/^[a-z][a-z0-9_-]*:.+/i.test(name)) {
262
+ try {
263
+ const qs = new URLSearchParams({ kind: k, key: name })
264
+ const r = await fetchCortex(`${BASE}/api/node/timeline?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
265
+ if (r.status === 404) return { content: [{ type: 'text', text: `No ${k} named "${name}" — cannot project a timeline.` }] }
266
+ if (!r.ok) {
267
+ const d = classify(r.status, r.headers.get('content-type'), await r.text(), r.headers.get('x-vercel-id'))
268
+ return { content: [{ type: 'text', text: `Could not read the timeline for "${name}": ${d.message}` }] }
269
+ }
270
+ const t = await r.json()
271
+ if (!t.identifiers?.length) {
272
+ return { content: [{ type: 'text', text: `"${name}" carries no identifier stamps yet — no history joins. Stamp its page with [[repo:owner/name]] (what it identifies) and events will accrue here.` }] }
273
+ }
274
+ const lines = [`# ${name} — node timeline (history; the page is the present)`]
275
+ lines.push(`Joins: ${t.identifiers.map((i) => `[[${i.id}]] · ${i.visibleCount} visible`).join(' | ')}${t.incomplete ? ' (partial — one join failed to read)' : ''}`)
276
+ for (const e of t.events) lines.push(`- ${String(e.occurredAt).slice(0, 10)} · ${e.source} · ${e.summary}`)
277
+ if (!t.events.length) lines.push('(no events visible to you yet on these joins)')
278
+ if (t.siblings?.length) lines.push(`Sibling homes (share a stamp — bridges, not history): ${t.siblings.map((s) => `"${s.title}"`).join(', ')}`)
279
+ return { content: [{ type: 'text', text: lines.join('\n') }] }
280
+ } catch (e) {
281
+ return { content: [{ type: 'text', text: `Could not read the timeline for "${name}": ${e.message}` }] }
282
+ }
283
+ }
284
+ // IDENTIFIER RESOLUTION (slice 3): an identifier-shaped name is a JOIN KEY, not a page — resolve
285
+ // it to its authored home + a viewer-honest event count instead of 404ing. Loose shape-detect here;
286
+ // the server enforces the strict canonical law (identifiers.ts) and 400s malformed forms with an
287
+ // actionable message we surface verbatim.
288
+ if (/^[a-z][a-z0-9_-]*:.+/i.test(name)) {
289
+ try {
290
+ const qs = new URLSearchParams({ id: name, ...(expand ? { expand: '1' } : {}) })
291
+ const r = await fetchCortex(`${BASE}/api/identifier/resolve?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
292
+ if (r.ok) {
293
+ const out = await r.json()
294
+ const lines = [`# ${out.identifier} (identifier — a join key, not a page)`]
295
+ if (out.status === 'unhomed') {
296
+ lines.push(`No authored home carries this stamp yet — data but no home. If you know what this identifies, author its page and stamp [[${out.identifier}]] on it (that page becomes the home).`)
297
+ } else if (out.status === 'healthy') {
298
+ const h = out.homes[0]
299
+ lines.push(`Home: "${h.title}" (${h.kind}, ${h.tier}) — \`read_page "${h.title}"\` for the full page.`)
300
+ } else {
301
+ lines.push(`⚠ ${out.homes.length} pages carry this stamp — possible duplicate homes, consider reconciling:`)
302
+ for (const h of out.homes) lines.push(` - "${h.title}" (${h.kind}, ${h.tier})`)
303
+ }
304
+ lines.push(`· ${out.events.visibleCount} tagged timeline event${out.events.visibleCount === 1 ? '' : 's'} visible to you${out.events.recent.length ? ':' : expand ? '.' : ' — pass expand: true to list recent ones.'}`)
305
+ for (const e of out.events.recent) lines.push(` - ${String(e.occurredAt).slice(0, 10)} · ${e.source} · ${e.summary}`)
306
+ return { content: [{ type: 'text', text: lines.join('\n') }] }
307
+ }
308
+ if (r.status === 400) {
309
+ const body = await r.json().catch(() => null)
310
+ return { content: [{ type: 'text', text: `"${name}" looks like an identifier but is not canonical: ${body?.error ?? 'expected repo:owner/name'}. Fix the stamp, or \`grep "${name}"\` to find where it appears.` }] }
311
+ }
312
+ // resolver endpoint unavailable (older server) → fall through to the normal page path below
313
+ } catch { /* network hiccup → fall through to the page path */ }
314
+ }
258
315
  let res
259
316
  try {
260
317
  res = await fetchCortex(`${BASE}/api/brain/page?kind=${k}&key=${encodeURIComponent(name)}`,
@@ -263,11 +320,6 @@ export async function runServer(version) {
263
320
  return { content: [{ type: 'text', text: `Could not read "${name}": ${e.message}` }] }
264
321
  }
265
322
  if (res.status === 404) {
266
- // Identifier links ([[repo:owner/name]]) are JOIN KEYS, not pages — a 404 here is expected.
267
- // The home/expand resolver arrives in a later slice; until then grep is the join.
268
- if (/^[a-z][a-z0-9_-]*:.+/i.test(name)) {
269
- return { content: [{ type: 'text', text: `"${name}" is an identifier link (a join key), not a page — there is nothing to read at it yet. \`grep "${name}"\` to find every page carrying this stamp; the timeline resolver for identifiers lands in a later slice.` }] }
270
- }
271
323
  return { content: [{ type: 'text', text: `No ${k} page named "${name}". If it's a person or team, pass kind (person/org). Otherwise \`grep "${name}"\` to locate it — it may be a red-link (a wanted page that isn't authored yet).` }] }
272
324
  }
273
325
  if (!res.ok) {
@@ -284,7 +336,12 @@ export async function runServer(version) {
284
336
  const head = `[${t.tier}${day(t.updated_at) ? ` · authored ${day(t.updated_at)}` : ''}${t.validity && t.validity !== 'current' ? ` · ${t.validity}` : ''}${t.version ? `\nversion: ${t.version}` : ''}]`
285
337
  return [head, t.summary, secs].filter(Boolean).join('\n')
286
338
  })
287
- const footer = `— Follow any [[links]] above with read_page to go deeper.\n— If you hold fresher FIRST-HAND truth than this page — something you established THIS session (ran the command, made the change, hold the artifact) that a section here gets concretely wrong — re-author just those sections now with \`author\` (any node you are permitted to edit; concise; a material contradiction, not merely extra detail). Pass this page's \`version\` as base_version when you re-author — if the save comes back stale, the page changed under you: re-read, then re-author. Reading a stale page you can fix IS the trigger to fix it.`
339
+ let footer = `— Follow any [[links]] above with read_page to go deeper.\n— If you hold fresher FIRST-HAND truth than this page — something you established THIS session (ran the command, made the change, hold the artifact) that a section here gets concretely wrong — re-author just those sections now with \`author\` (any node you are permitted to edit; concise; a material contradiction, not merely extra detail). Pass this page's \`version\` as base_version when you re-author — if the save comes back stale, the page changed under you: re-read, then re-author. Reading a stale page you can fix IS the trigger to fix it.`
340
+ // slice 4: when the page carries identifier stamps, the history projection is one flag away.
341
+ // (client-side shape check — the acknowledged registry copy of identifiers.ts)
342
+ const allBody = page.tiers.flatMap((t) => (t.sections ?? []).map((s) => s.body)).join('\n')
343
+ const stamps = [...new Set((allBody.match(/\[\[repo:[a-z0-9][a-z0-9-]*\/[a-z0-9_.-]+\]\]/gi) ?? []).map((s) => s.toLowerCase()))]
344
+ if (stamps.length) footer += `\n— This page carries ${stamps.join(', ')} — \`read_page "${name}"\` with history: true for its event timeline (page = present, timeline = history).`
288
345
  return { content: [{ type: 'text', text: `# ${page.title ?? name} (full authored page)\n\n${blocks.join('\n\n---\n\n')}\n\n${footer}` }] }
289
346
  },
290
347
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.26",
3
+ "version": "0.9.28",
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": {