@theronap/cortex-mcp 0.9.35 → 0.9.37

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 +140 -1
  2. package/package.json +1 -1
package/lib/server.mjs CHANGED
@@ -253,10 +253,28 @@ export async function runServer(version) {
253
253
  kind: z.enum(['project', 'person', 'org', 'user']).optional().describe('node kind (default project; pass person/org for people/teams)'),
254
254
  expand: z.boolean().optional().describe('identifier names only: also list recent visible timeline events for this identifier (default: home + count)'),
255
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.'),
256
+ version: z.string().optional().describe('read a HISTORICAL version of this page instead of the current one: a rev_no (e.g. "3") or a content_hash from page_history. Use page_history first to see the versions, then rollback_page to restore one.'),
256
257
  },
257
258
  },
258
- async ({ name, kind, expand, history }) => {
259
+ async ({ name, kind, expand, history, version }) => {
259
260
  const k = kind ?? 'project'
261
+ // PAGE HISTORY (Part A): a specific version reads one historical body, resolved within this page's
262
+ // own history. Not for identifier-shaped names (those are join keys, handled below).
263
+ if (version && !/^[a-z][a-z0-9_-]*:.+/i.test(name)) {
264
+ try {
265
+ const qs = new URLSearchParams({ kind: k, key: name, version })
266
+ const vr = await fetchCortex(`${BASE}/api/brain/page?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
267
+ if (vr.status === 404) return { content: [{ type: 'text', text: `No version "${version}" for "${name}" (or you can't see it). Use page_history "${name}" to list its versions.` }] }
268
+ if (!vr.ok) {
269
+ const d = classify(vr.status, vr.headers.get('content-type'), await vr.text(), vr.headers.get('x-vercel-id'))
270
+ return { content: [{ type: 'text', text: `Could not read version "${version}" of "${name}": ${d.message}` }] }
271
+ }
272
+ const v = await vr.json()
273
+ return { content: [{ type: 'text', text: `# ${name} — historical version (rev ${v.revNo} · ${v.op} · ${String(v.createdAt).slice(0, 10)} · ${v.tier})\nversion: ${v.version}\n\n${v.body}\n\n— This is a HISTORICAL snapshot, not the current page. \`read_page "${name}"\` (no version) shows what's live; \`rollback_page\` restores this one as a new version.` }] }
274
+ } catch (e) {
275
+ return { content: [{ type: 'text', text: `Could not read version "${version}" of "${name}": ${e.message}` }] }
276
+ }
277
+ }
260
278
  // PER-NODE TIMELINE (slice 4): history = the projection over the node's identifier stamps.
261
279
  if (history && !/^[a-z][a-z0-9_-]*:.+/i.test(name)) {
262
280
  try {
@@ -353,6 +371,74 @@ export async function runServer(version) {
353
371
  },
354
372
  )
355
373
 
374
+ server.registerTool(
375
+ 'page_history',
376
+ {
377
+ title: 'See a wiki page\'s edit history',
378
+ description: 'Show the VERSION history of an authored wiki page — every prior version, who changed it and when, newest first. This is how you see "what changed on this page and by whom", and it includes privacy changes (re-tiers). Then use `read_page` with a version to view an old body, or `rollback_page` to restore one. (Distinct from read_page\'s `history: true`, which is the raw event timeline via [[repo:…]] stamps.) RLS-scoped: you see history only for pages you may read.',
379
+ inputSchema: {
380
+ name: z.string().describe('the canonical node name exactly as written (e.g. "Cortex", "Ben")'),
381
+ kind: z.enum(['project', 'person', 'org', 'user']).optional().describe('node kind (default project)'),
382
+ limit: z.number().int().optional().describe('how many recent versions to show (default 20, max 200)'),
383
+ },
384
+ },
385
+ async ({ name, kind, limit }) => {
386
+ const k = kind ?? 'project'
387
+ let res
388
+ try {
389
+ const qs = new URLSearchParams({ kind: k, key: name, ...(limit ? { limit: String(limit) } : {}) })
390
+ res = await fetchCortex(`${BASE}/api/brain/page-history?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
391
+ } catch (e) {
392
+ return { content: [{ type: 'text', text: `Could not read history for "${name}": ${e.message}` }] }
393
+ }
394
+ if (res.status === 404) return { content: [{ type: 'text', text: `No ${k} named "${name}". Pass kind (person/org) if it isn't a project.` }] }
395
+ if (!res.ok) {
396
+ const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
397
+ return { content: [{ type: 'text', text: `Could not read history for "${name}": ${d.message}` }] }
398
+ }
399
+ const out = await res.json()
400
+ const revs = out.revisions ?? []
401
+ if (!revs.length) return { content: [{ type: 'text', text: `"${name}" (${k}) has no recorded version history yet.` }] }
402
+ const lines = revs.map((r) => {
403
+ const who = r.actor_name ? ` · ${r.actor_name}` : ''
404
+ const why = r.reason ? ` — ${r.reason}` : ''
405
+ return `- rev ${r.rev_no} · ${String(r.created_at).slice(0, 10)} · ${r.op} · ${r.tier}${who}${why}\n version: ${r.content_hash}`
406
+ })
407
+ return { content: [{ type: 'text', text: `# ${name} — page history (newest first)\n${lines.join('\n')}\n\n— \`read_page "${name}"\` with version:<rev_no|version> to view an old body; \`rollback_page\` to restore one.` }] }
408
+ },
409
+ )
410
+
411
+ server.registerTool(
412
+ 'rollback_page',
413
+ {
414
+ title: 'Roll a wiki page back to a prior version',
415
+ description: 'Restore a wiki page to an earlier version from its page_history — a forward, non-destructive write (the old versions are kept; a new "rollback" version is recorded). Use this to undo a mistaken or bad edit. You may only roll back a page you are allowed to edit. Find the target version with page_history first.',
416
+ inputSchema: {
417
+ name: z.string().describe('the exact page name'),
418
+ kind: z.enum(['project', 'person', 'org', 'user']).optional().describe('node kind (default project)'),
419
+ to_version: z.string().describe('which version to restore: a rev_no (e.g. "3") or a content_hash, from page_history'),
420
+ tier: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('which tier variant to roll back, if the node has more than one'),
421
+ reason: z.string().optional().describe('optional note recorded on the new rollback version (why you rolled back)'),
422
+ },
423
+ },
424
+ async ({ name, kind, to_version, tier, reason }) => {
425
+ const k = kind ?? 'project'
426
+ let res
427
+ try {
428
+ res = await fetchCortex(`${BASE}/api/brain/rollback`, {
429
+ method: 'POST',
430
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
431
+ body: JSON.stringify({ kind: k, name, to_version, ...(tier ? { tier } : {}), ...(reason ? { reason } : {}) }),
432
+ })
433
+ } catch (e) {
434
+ return { content: [{ type: 'text', text: `Could not roll back "${name}": ${e.message}` }] }
435
+ }
436
+ const out = await res.json().catch(() => null)
437
+ if (!res.ok) return { content: [{ type: 'text', text: `Could not roll back "${name}": ${out?.error ?? res.status}` }] }
438
+ return { content: [{ type: 'text', text: `Done — "${name}" ${out.note}. \`read_page "${name}"\` to confirm the current content.` }] }
439
+ },
440
+ )
441
+
356
442
  server.registerTool(
357
443
  'writing_style',
358
444
  {
@@ -403,6 +489,59 @@ export async function runServer(version) {
403
489
  },
404
490
  )
405
491
 
492
+ server.registerTool(
493
+ 'my_brains',
494
+ {
495
+ title: 'List your brains + which one writes land in',
496
+ description: 'List the brains (orgs/workspaces) you belong to and show which one is your ACTIVE WRITE brain — where author/log_session/capture currently land. Reads span all your brains; writes go to the active one. Use this to see your options before set_active_brain, or when a session seems to belong to a different brain than the one you are writing to.',
497
+ inputSchema: {},
498
+ },
499
+ async () => {
500
+ let res
501
+ try {
502
+ res = await fetchCortex(`${BASE}/api/brains`, { headers: { Authorization: `Bearer ${TOKEN}` } })
503
+ } catch (e) {
504
+ return { content: [{ type: 'text', text: `Could not list brains: ${e.message}` }] }
505
+ }
506
+ if (!res.ok) {
507
+ const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
508
+ return { content: [{ type: 'text', text: `Could not list brains: ${d.message}` }] }
509
+ }
510
+ const { brains, activeIsExplicit } = await res.json()
511
+ if (!brains?.length) return { content: [{ type: 'text', text: 'You have no brains.' }] }
512
+ const lines = brains.map((b) => `${b.isActive ? '▶' : ' '} ${b.name} (${b.role}${b.status !== 'active' ? `, ${b.status}` : ''}) [${b.orgId}]`)
513
+ const note = activeIsExplicit ? '' : '\n(active brain is the default — you have one brain; set_active_brain once you hold more.)'
514
+ return { content: [{ type: 'text', text: `Your brains (▶ = writes land here):\n${lines.join('\n')}${note}` }] }
515
+ },
516
+ )
517
+
518
+ server.registerTool(
519
+ 'set_active_brain',
520
+ {
521
+ title: 'Choose the brain your writes go to',
522
+ description: 'Point your writes (author / log_session / capture) at one of your brains, by its org id (from my_brains). Sticky — it stays until you change it. Pass org_id = null to clear it and fall back to the default. You can only select a brain you are a member of.',
523
+ inputSchema: { org_id: z.string().nullable().describe('the org id of the brain to write to (from my_brains), or null to clear the pointer') },
524
+ },
525
+ async ({ org_id }) => {
526
+ let res
527
+ try {
528
+ res = await fetchCortex(`${BASE}/api/brains`, {
529
+ method: 'PUT',
530
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
531
+ body: JSON.stringify({ orgId: org_id ?? null }),
532
+ })
533
+ } catch (e) {
534
+ return { content: [{ type: 'text', text: `Could not set active brain: ${e.message}` }] }
535
+ }
536
+ if (!res.ok) {
537
+ const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
538
+ return { content: [{ type: 'text', text: `Could not set active brain: ${d.message}` }] }
539
+ }
540
+ const r = await res.json()
541
+ return { content: [{ type: 'text', text: r.activeOrgId ? `Writes now land in brain ${r.activeOrgId}.` : 'Cleared your active-brain pointer (writes fall back to your default brain).' }] }
542
+ },
543
+ )
544
+
406
545
  server.registerTool(
407
546
  'list_records',
408
547
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.35",
3
+ "version": "0.9.37",
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": {