@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 +8 -1
- package/lib/skills.mjs +54 -10
- package/package.json +1 -1
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
|
-
|
|
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
|
-
//
|
|
269
|
-
//
|
|
270
|
-
|
|
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 =
|
|
320
|
+
const toggleIdx = rest.findIndex((a) => a === '--disable' || a === '--enable')
|
|
278
321
|
if (toggleIdx !== -1) {
|
|
279
|
-
const name =
|
|
280
|
-
if (!name) { process.stderr.write(`Usage: skills push ${
|
|
281
|
-
payload = { name, enabled:
|
|
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 =
|
|
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
|
|
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
|
|
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`
|