@theronap/cortex-mcp 0.9.75 → 0.9.76

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 CHANGED
@@ -719,7 +719,14 @@ export async function runServer(version) {
719
719
  }
720
720
  const out = await res.json().catch(() => null)
721
721
  if (!res.ok) return toolError(`Could not roll back "${name}": ${out?.error ?? res.status}`)
722
- return { content: [{ type: 'text', text: `Done "${name}" ${out.note}. \`read_page "${name}"\` to confirm the current content.` }] }
722
+ // #477: when the target revision had no summary, the page's CURRENT summary was kept rather than
723
+ // erased. Say it on its own line instead of at the tail of `note` — this is the 2026-08-04 W3
724
+ // shape, where empty summaries copied over populated ones destroyed 31 of them under a report
725
+ // that read as success. A rescue the operator does not see is still a silent write.
726
+ const keptLine = out.summaryKept === 'current'
727
+ ? `\n\n⚠ That revision had NO summary, so the page's current summary was KEPT rather than erased — check it still describes the restored body, and use set_summary if not.`
728
+ : ''
729
+ return { content: [{ type: 'text', text: `Done — "${name}" ${out.note}. \`read_page "${name}"\` to confirm the current content.${keptLine}` }] }
723
730
  },
724
731
  )
725
732
 
package/lib/skills.mjs CHANGED
@@ -265,22 +265,65 @@ export async function syncOrgSkills(opts = {}) {
265
265
  return summary
266
266
  }
267
267
 
268
- // `skills push <file>` / `skills push --disable <name>` / `skills push --enable <name>`
269
- // publish or toggle an org skill (owner/manager/admin; the server enforces the role).
270
- async function runSkillsPush(argv) {
268
+ // Render a failed push. THE SERVER ALREADY ANSWERED THE QUESTION brain_choice_response.ts builds a
269
+ // body carrying a full `message` plus every brain's NAME, PAGE COUNT and SAMPLE TITLES, explicitly so
270
+ // the caller can choose "by what each one HOLDS, not by which name sounds related". This function
271
+ // existed as `body.error ?? HTTP ${status}`, which printed the bare code `brain_required` and threw
272
+ // all of that away — the user saw a two-word error with no notion of what a brain is, which ones they
273
+ // have, or what to type next. Pure + exported so the shape is unit-testable without a network.
274
+ export function renderPushError(body, status) {
275
+ const lines = [body?.message ?? body?.error ?? `HTTP ${status}`]
276
+ const brains = Array.isArray(body?.brains) ? body.brains : []
277
+ if (brains.length) {
278
+ lines.push('', ' Your brains:')
279
+ for (const b of brains) {
280
+ const pages = typeof b?.pageCount === 'number' ? ` — ${b.pageCount} page${b.pageCount === 1 ? '' : 's'}` : ''
281
+ const titles = Array.isArray(b?.sampleTitles) && b.sampleTitles.length
282
+ ? `: ${b.sampleTitles.slice(0, 2).join('; ')}` : ''
283
+ lines.push(` ${b?.name ?? b?.orgId ?? '(unnamed)'}${pages}${titles}`)
284
+ }
285
+ lines.push('', ' Re-run with --brain "<name>".')
286
+ }
287
+ return lines.join('\n')
288
+ }
289
+
290
+ // `skills push <file> [--brain <name>]` / `skills push --disable <name> [--brain <name>]` /
291
+ // `skills push --enable <name> [--brain <name>]` — publish or toggle an org skill
292
+ // (owner/manager/admin; the server enforces the role).
293
+ //
294
+ // ⚠ --brain IS NOT OPTIONAL FOR A MULTI-BRAIN CALLER, and until 2026-08-08 there was no way to pass
295
+ // it. Publishing MODIFIES one brain, so the server correctly uses ADR-0022's `requireBrain` half and
296
+ // refuses to guess — but this command sent no `brain`, so every push and every --disable from a
297
+ // multi-brain account died on `brain_required` with no way forward. Confirmed against production:
298
+ // `skills push --disable cortex-author-docs` → `✗ brain_required`, full stop.
299
+ //
300
+ // This is the WRITE twin of the GET bug fixed in #467. The read was miscategorised and now fans out;
301
+ // the write was categorised correctly and simply had no input for the answer it demanded.
302
+ export async function runSkillsPush(argv) {
271
303
  const { resolveBase, readWiredToken, fetchCortex } = await import('./diagnose.mjs')
272
304
  const token = process.env.CORTEX_TOKEN || readWiredToken()
273
305
  if (!token) { process.stderr.write('No Agnoclast token wired — run setup first.\n'); return 1 }
274
306
  const base = resolveBase(process.env.CORTEX_URL)
275
307
 
308
+ const brainIdx = argv.findIndex((a) => a === '--brain')
309
+ const brain = brainIdx === -1 ? null : argv[brainIdx + 1]
310
+ if (brainIdx !== -1 && (!brain || brain.startsWith('-'))) {
311
+ process.stderr.write('Usage: skills push … --brain <name> (missing brain name)\n')
312
+ return 1
313
+ }
314
+ // Strip --brain AND its value before any positional parsing below. The value does not start with
315
+ // '-', so the `argv.find(a => !a.startsWith('-'))` file lookup would otherwise take it as the
316
+ // SKILL.md path and push the wrong thing.
317
+ const rest = brainIdx === -1 ? argv : argv.filter((_, i) => i !== brainIdx && i !== brainIdx + 1)
318
+
276
319
  let payload
277
- const toggleIdx = argv.findIndex((a) => a === '--disable' || a === '--enable')
320
+ const toggleIdx = rest.findIndex((a) => a === '--disable' || a === '--enable')
278
321
  if (toggleIdx !== -1) {
279
- const name = argv[toggleIdx + 1]
280
- if (!name) { process.stderr.write(`Usage: skills push ${argv[toggleIdx]} <name>\n`); return 1 }
281
- payload = { name, enabled: argv[toggleIdx] === '--enable' }
322
+ const name = rest[toggleIdx + 1]
323
+ if (!name) { process.stderr.write(`Usage: skills push ${rest[toggleIdx]} <name>\n`); return 1 }
324
+ payload = { name, enabled: rest[toggleIdx] === '--enable' }
282
325
  } else {
283
- const file = argv.find((a) => !a.startsWith('-'))
326
+ const file = rest.find((a) => !a.startsWith('-'))
284
327
  if (!file || !existsSync(file)) { process.stderr.write('Usage: skills push <SKILL.md> (file not found)\n'); return 1 }
285
328
  const body_md = readFileSync(file, 'utf8')
286
329
  const name = frontmatterName(body_md, '').toLowerCase()
@@ -292,13 +335,14 @@ async function runSkillsPush(argv) {
292
335
  }
293
336
 
294
337
  try {
295
- const res = await fetchCortex(`${base}/api/skills`, {
338
+ const qs = brain ? `?brain=${encodeURIComponent(brain)}` : ''
339
+ const res = await fetchCortex(`${base}/api/skills${qs}`, {
296
340
  method: 'POST',
297
341
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
298
342
  body: JSON.stringify(payload),
299
343
  })
300
344
  const body = await res.json().catch(() => ({}))
301
- if (!res.ok) { process.stderr.write(`✗ ${body.error ?? `HTTP ${res.status}`}\n`); return 1 }
345
+ if (!res.ok) { process.stderr.write(`✗ ${renderPushError(body, res.status)}\n`); return 1 }
302
346
  process.stdout.write(
303
347
  payload.body_md
304
348
  ? `✓ published "${payload.name}" to your org — every seat installs it on next session start.\n`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.75",
3
+ "version": "0.9.76",
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": {