@theronap/cortex-mcp 0.9.80 → 0.9.81

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/skills.mjs CHANGED
@@ -3,7 +3,7 @@ import { homedir } from 'os'
3
3
  import { join, dirname } from 'path'
4
4
  import { fileURLToPath } from 'url'
5
5
 
6
- // Managed Agnoclast skills.
6
+ // Managed Cortex skills.
7
7
  //
8
8
  // Two sources, one install pipeline:
9
9
  // 1. BUNDLED — shipped inside this package (cortex-log, cortex-context, cortex-author-docs).
@@ -155,42 +155,21 @@ export function installSkills(opts = {}) {
155
155
 
156
156
  if (quiet && (summary.installed.length || summary.repaired.length)) {
157
157
  // SessionStart surfaces one line in Claude Code so a silent self-heal isn't invisible.
158
- process.stdout.write(`Agnoclast: synced managed skill(s) into ${summary.targets.join(' + ')}.\n`)
158
+ process.stdout.write(`Cortex: synced managed skill(s) into ${summary.targets.join(' + ')}.\n`)
159
159
  }
160
160
  return summary
161
161
  }
162
162
 
163
- // Decide what to install from a served org-skill list: drop invalid names, bundled collisions
164
- // (bundled wins — core skills are inalterable) and cross-brain collisions. Pure, so it's
165
- // unit-testable without a network.
163
+ // Decide what to install from a served org-skill list: drop invalid names and bundled collisions
164
+ // (bundled wins — core skills are inalterable). Pure, so it's unit-testable without a network.
166
165
  export function planOrgInstall(served, bundledNames) {
167
166
  const bundled = new Set(bundledNames)
168
167
  const install = []
169
168
  const skipped = []
170
-
171
- // /api/skills now fans out across EVERY brain the caller belongs to (ADR-0022 acrossMyBrains), so
172
- // for the first time two brains can serve the SAME skill name. Installing both would write one
173
- // file twice and silently leave whichever landed last — an executable body from a brain the user
174
- // never chose. There is no principled winner, so a cross-brain collision installs NEITHER and says
175
- // so, matching how a bundled collision already resolves: when in doubt, do not install.
176
- const byName = new Map()
177
- for (const s of served ?? []) {
178
- const n = (s?.name ?? '').trim()
179
- if (!byName.has(n)) byName.set(n, [])
180
- byName.get(n).push(s)
181
- }
182
-
183
169
  for (const s of served ?? []) {
184
170
  const name = (s?.name ?? '').trim()
185
171
  if (!NAME_RE.test(name) || typeof s?.body_md !== 'string' || !s.body_md.trim()) { skipped.push({ name: name || '(unnamed)', why: 'invalid' }); continue }
186
172
  if (bundled.has(name)) { skipped.push({ name, why: 'collides with a bundled core skill' }); continue }
187
- const dupes = byName.get(name) ?? []
188
- if (dupes.length > 1) {
189
- // Name the brains so the owner knows which to rename. `brain` is the tag acrossMyBrains adds.
190
- const brains = [...new Set(dupes.map((d) => d?.brain).filter(Boolean))]
191
- skipped.push({ name, why: `published by ${dupes.length} brains (${brains.join(', ') || 'unknown'}) — rename one; installing neither` })
192
- continue
193
- }
194
173
  install.push({ name, source: s.body_md })
195
174
  }
196
175
  return { install, skipped }
@@ -222,13 +201,6 @@ export async function syncOrgSkills(opts = {}) {
222
201
  if (res.ok) {
223
202
  served = (await res.json())?.skills ?? []
224
203
  summary.source = 'server'
225
- } else {
226
- // ⚠ DO NOT MAKE THIS SILENT AGAIN. `if (res.ok)` alone is how a 100% failure hid for weeks:
227
- // /api/skills answered 409 to every multi-brain caller, this fell through to the cache below,
228
- // and the only log line ('unreachable and no cache') does NOT print when a cache exists. So
229
- // org skills silently stopped updating and nothing anywhere said so. Fail-soft is right —
230
- // SessionStart must not break — but fail-soft is not fail-quiet.
231
- log(` · org skills: server said ${res.status} — using last-good cache. Run \`doctor\` if this persists.`)
232
204
  }
233
205
  } catch { /* fall through to cache */ }
234
206
  if (!served && Array.isArray(cache?.skills)) { served = cache.skills; summary.source = 'cache' }
@@ -258,72 +230,29 @@ export async function syncOrgSkills(opts = {}) {
258
230
  const bits = []
259
231
  if (changed.length) bits.push(`synced ${changed.join(', ')}`)
260
232
  if (summary.removed.length) bits.push(`removed ${summary.removed.join(', ')}`)
261
- process.stdout.write(`Agnoclast: org skills — ${bits.join('; ')}${summary.source === 'cache' ? ' (offline cache)' : ''}.\n`)
233
+ process.stdout.write(`Cortex: org skills — ${bits.join('; ')}${summary.source === 'cache' ? ' (offline cache)' : ''}.\n`)
262
234
  } else if (!quiet) {
263
235
  log(` ✓ org skills up to date (${currentNames.length} published${skipped.length ? `, ${skipped.length} skipped` : ''})`)
264
236
  }
265
237
  return summary
266
238
  }
267
239
 
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) {
240
+ // `skills push <file>` / `skills push --disable <name>` / `skills push --enable <name>`
241
+ // publish or toggle an org skill (owner/manager/admin; the server enforces the role).
242
+ async function runSkillsPush(argv) {
303
243
  const { resolveBase, readWiredToken, fetchCortex } = await import('./diagnose.mjs')
304
244
  const token = process.env.CORTEX_TOKEN || readWiredToken()
305
- if (!token) { process.stderr.write('No Agnoclast token wired — run setup first.\n'); return 1 }
245
+ if (!token) { process.stderr.write('No Cortex token wired — run setup first.\n'); return 1 }
306
246
  const base = resolveBase(process.env.CORTEX_URL)
307
247
 
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
-
319
248
  let payload
320
- const toggleIdx = rest.findIndex((a) => a === '--disable' || a === '--enable')
249
+ const toggleIdx = argv.findIndex((a) => a === '--disable' || a === '--enable')
321
250
  if (toggleIdx !== -1) {
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' }
251
+ const name = argv[toggleIdx + 1]
252
+ if (!name) { process.stderr.write(`Usage: skills push ${argv[toggleIdx]} <name>\n`); return 1 }
253
+ payload = { name, enabled: argv[toggleIdx] === '--enable' }
325
254
  } else {
326
- const file = rest.find((a) => !a.startsWith('-'))
255
+ const file = argv.find((a) => !a.startsWith('-'))
327
256
  if (!file || !existsSync(file)) { process.stderr.write('Usage: skills push <SKILL.md> (file not found)\n'); return 1 }
328
257
  const body_md = readFileSync(file, 'utf8')
329
258
  const name = frontmatterName(body_md, '').toLowerCase()
@@ -335,14 +264,13 @@ export async function runSkillsPush(argv) {
335
264
  }
336
265
 
337
266
  try {
338
- const qs = brain ? `?brain=${encodeURIComponent(brain)}` : ''
339
- const res = await fetchCortex(`${base}/api/skills${qs}`, {
267
+ const res = await fetchCortex(`${base}/api/skills`, {
340
268
  method: 'POST',
341
269
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
342
270
  body: JSON.stringify(payload),
343
271
  })
344
272
  const body = await res.json().catch(() => ({}))
345
- if (!res.ok) { process.stderr.write(`✗ ${renderPushError(body, res.status)}\n`); return 1 }
273
+ if (!res.ok) { process.stderr.write(`✗ ${body.error ?? `HTTP ${res.status}`}\n`); return 1 }
346
274
  process.stdout.write(
347
275
  payload.body_md
348
276
  ? `✓ published "${payload.name}" to your org — every seat installs it on next session start.\n`
@@ -361,7 +289,7 @@ export async function runSkills(argv = []) {
361
289
  if (argv[0] === 'push') return runSkillsPush(argv.slice(1))
362
290
 
363
291
  const quiet = argv.includes('--quiet')
364
- if (!quiet) process.stdout.write('\nAgnoclast skills — installing managed skills…\n')
292
+ if (!quiet) process.stdout.write('\nCortex skills — installing managed skills…\n')
365
293
  const r = installSkills({ quiet })
366
294
  if (!quiet) {
367
295
  if (!r.targets.length) process.stdout.write(' ! No agent CLI found (~/.claude or ~/.codex). Nothing to install.\n')
package/lib/uninstall.mjs CHANGED
@@ -3,7 +3,7 @@ import { homedir } from 'os'
3
3
  import { join } from 'path'
4
4
  import { execFileSync } from 'child_process'
5
5
 
6
- // Full uninstall — the reverse of setup.mjs. Removes EVERY touch-point Agnoclast writes onto a machine:
6
+ // Full uninstall — the reverse of setup.mjs. Removes EVERY touch-point Cortex writes onto a machine:
7
7
  // 1. ~/.claude.json → mcpServers.cortex
8
8
  // 2. ~/.claude/settings.json → Stop/SessionStart/PreCompact cortex hooks
9
9
  // 3. ~/.codex/config.toml → [mcp_servers.cortex] + [mcp_servers.cortex.env]
@@ -34,7 +34,7 @@ export function runUninstall(argv = []) {
34
34
  const act = (msg) => plan.push(msg)
35
35
  const write = (path, data) => { if (!dry) { backup(path); writeFileSync(path, data) } }
36
36
 
37
- process.stdout.write(dry ? '\nAgnoclast uninstall — DRY RUN (nothing will change):\n\n' : '\nAgnoclast uninstall — removing all wiring…\n\n')
37
+ process.stdout.write(dry ? '\nCortex uninstall — DRY RUN (nothing will change):\n\n' : '\nCortex uninstall — removing all wiring…\n\n')
38
38
 
39
39
  // 1. MCP server out of ~/.claude.json
40
40
  editJson(CLAUDE_JSON, (cfg) => {
@@ -144,7 +144,7 @@ export function runUninstall(argv = []) {
144
144
  }
145
145
  }
146
146
 
147
- if (plan.length === 0) { process.stdout.write(' Nothing to remove — this machine has no Agnoclast wiring.\n\n'); return }
147
+ if (plan.length === 0) { process.stdout.write(' Nothing to remove — this machine has no Cortex wiring.\n\n'); return }
148
148
  process.stdout.write(plan.join('\n') + '\n\n')
149
149
  if (dry) {
150
150
  process.stdout.write('DRY RUN — nothing changed. Re-run without --dry-run to apply.\n')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.80",
3
+ "version": "0.9.81",
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": {
@@ -1,22 +1,22 @@
1
1
  ---
2
2
  name: cortex-author-docs
3
- description: Push new/changed documentation (specs, plans, design docs) from disk into Agnoclast as authored wiki pages. Run after writing a spec/plan/design doc, when the user asks to sync docs to Agnoclast, or as part of session close-out.
3
+ description: Push new/changed documentation (specs, plans, design docs) from disk into Cortex as authored wiki pages. Run after writing a spec/plan/design doc, when the user asks to sync docs to Cortex, or as part of session close-out.
4
4
  ---
5
5
 
6
- > **Agnoclast-managed skill.** This file is installed and kept up to date by Agnoclast. Local edits are
6
+ > **Cortex-managed skill.** This file is installed and kept up to date by Cortex. Local edits are
7
7
  > restored on the next session (a backup of your version is saved alongside). Don't rely on changes here.
8
8
 
9
9
  ## Why this exists
10
10
 
11
11
  Specs, plans, and design docs get written to disk (a repo's `docs/`, skill-generated design docs)
12
- and never reach Agnoclast — so the org brain misses its richest artifacts. A spec IS a page: this
12
+ and never reach Cortex — so the org brain misses its richest artifacts. A spec IS a page: this
13
13
  skill turns pending docs into authored wiki pages. You (the live session) are the pipe — you read
14
14
  the doc and author a synthesis. Never dump raw markdown into a page.
15
15
 
16
16
  ## When to use
17
17
 
18
18
  - Right after you write or substantially update a spec/plan/design/runbook doc on disk.
19
- - When the user asks to push/sync docs to Agnoclast.
19
+ - When the user asks to push/sync docs to Cortex.
20
20
  - During session close-out (`/cortex-log` runs this as a sweep step).
21
21
 
22
22
  ## Steps
@@ -67,7 +67,7 @@ the doc and author a synthesis. Never dump raw markdown into a page.
67
67
  ## Safety rules
68
68
 
69
69
  - Do NOT register or sweep the local brain repo (`~/Documents/brain`) while the Robin parity soak
70
- is running — the experiment forbids re-syncing Robin into Agnoclast mid-window.
70
+ is running — the experiment forbids re-syncing Robin into Cortex mid-window.
71
71
  - Respect tiers: if a doc is clearly personal/sensitive, author it `confidential` or ask; default
72
72
  for work docs is the author path's normal default.
73
73
  - This skill writes wiki pages via the `author` tool only. It never sends external messages and
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: cortex-context
3
- description: Automatically hydrate Agnoclast context at the start of a substantive session. Use when Agnoclast MCP is available and the user has made a real request, so the first answer is grounded in query-centered org context instead of the static baseline alone.
3
+ description: Automatically hydrate Cortex context at the start of a substantive session. Use when Cortex MCP is available and the user has made a real request, so the first answer is grounded in query-centered org context instead of the static baseline alone.
4
4
  ---
5
5
 
6
- > **Agnoclast-managed skill.** This file is installed and kept up to date by Agnoclast. Local edits are
6
+ > **Cortex-managed skill.** This file is installed and kept up to date by Cortex. Local edits are
7
7
  > restored on the next session (a backup of your version is saved alongside). Don't rely on changes here.
8
8
 
9
9
  ## When to use
@@ -14,12 +14,12 @@ chit-chat and requests where org context is obviously irrelevant.
14
14
  ## Steps
15
15
 
16
16
  1. Call `session_context` with the user's opening request, preserving the actual topic in their words.
17
- 2. Use that returned block as the primary Agnoclast grounding for the first response.
17
+ 2. Use that returned block as the primary Cortex grounding for the first response.
18
18
  3. If `session_context` is unavailable or errors, fall back to `my_context`.
19
19
  4. If the conversation materially changes topics later, call `session_context` again for the new topic.
20
20
 
21
21
  ## Safety rules
22
22
 
23
- - Do not fabricate Agnoclast context if the tool fails.
23
+ - Do not fabricate Cortex context if the tool fails.
24
24
  - Prefer the query-centered `session_context` over static `my_context` whenever the user's topic is clear.
25
25
  - Do not call `session_context` for every tiny follow-up; refresh only when the topic meaningfully shifts.
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: cortex-log
3
- description: Close out a work session into Agnoclast — summarize what happened, confirm it reached the org, and surface anything teammates should know. Run at or near the end of any working session.
3
+ description: Close out a work session into Cortex — summarize what happened, confirm it reached the org, and surface anything teammates should know. Run at or near the end of any working session.
4
4
  ---
5
5
 
6
- > **Agnoclast-managed skill.** This file is installed and kept up to date by Agnoclast. Local edits are
6
+ > **Cortex-managed skill.** This file is installed and kept up to date by Cortex. Local edits are
7
7
  > restored on the next session (a backup of your version is saved alongside). Don't rely on changes here.
8
8
 
9
9
  ## Model: session-primary, daily-derived
10
10
 
11
- The **session is the primary atomic unit** — one session = one durable Agnoclast record (via
11
+ The **session is the primary atomic unit** — one session = one durable Cortex record (via
12
12
  `log_session`, keyed by `sessionId`), which is also the per-record privacy unit (`set_record_privacy`
13
13
  is per record). Any "what happened today / this week" view is a **derived rollup** over those session
14
14
  records, never a separately-authored primary. This mirrors records(atomic) → digests(derived); the
@@ -16,7 +16,7 @@ personal `/log` skill follows the same shape against the local brain.
16
16
 
17
17
  ## When to use
18
18
 
19
- At the end of a Claude Code session, or after finishing a meaningful phase of work. Agnoclast keeps a
19
+ At the end of a Claude Code session, or after finishing a meaningful phase of work. Cortex keeps a
20
20
  background auto-capture as a fallback, but this skill is the *authoritative* close-out: it composes a
21
21
  clean, structured summary and persists THAT as the session's durable record (superseding the
22
22
  auto-capture's raw-transcript re-derivation).
@@ -33,7 +33,7 @@ No arguments. Read the conversation context.
33
33
  These are the things a teammate or manager would want to know without reading the whole transcript.
34
34
  3. **Persist it as the durable record** — call the `log_session` MCP tool with your curated `summary`
35
35
  (plus `project`, and the Claude Code `sessionId` if you know it). This writes YOUR summary as the
36
- session's authoritative Agnoclast record (`capture_source='skill'`). The background auto-capture is a
36
+ session's authoritative Cortex record (`capture_source='skill'`). The background auto-capture is a
37
37
  fallback and will not overwrite it; passing the same `sessionId` the auto-capture uses dedupes them
38
38
  onto one record. This — not the raw-transcript re-derivation — is the canonical record going forward.
39
39
  4. **Confirm + flag privacy** — `log_session` returns a confirmation; if it errors, tell the user to
@@ -72,7 +72,7 @@ No arguments. Read the conversation context.
72
72
  > verify a write; the result you already have in hand is the strongest.
73
73
  >
74
74
  > **What replaces it — at the moment of each write, not at the end:** a rejected `author` comes back
75
- > as an ORDINARY tool result with **no error flag** — `Could not author "<page>": Agnoclast API 409:
75
+ > as an ORDINARY tool result with **no error flag** — `Could not author "<page>": Cortex API 409:
76
76
  > <reason>` — and `No change to "<page>"` is a **200 OK that wrote nothing**. Neither is an error at
77
77
  > the protocol level, so nothing will interrupt you. **Read the result text of every write; never
78
78
  > skim it.** That inline read is where this step's value actually was.
@@ -3,7 +3,7 @@ name: cortex-walkthrough
3
3
  description: Run the guided Agnoclast walkthrough for someone new. Use when the person asks for the walkthrough, a tutorial, or getting started — "give me the walkthrough", "walk me through this", "how do I use this", "show me around", "what can this do", "remind me how this works" — or when a brand-new user needs orienting for the first time.
4
4
  ---
5
5
 
6
- > **Agnoclast-managed skill.** This file is installed and kept up to date by Agnoclast. Local edits are
6
+ > **Cortex-managed skill.** This file is installed and kept up to date by Cortex. Local edits are
7
7
  > restored on the next session (a backup of your version is saved alongside). Don't rely on changes here.
8
8
 
9
9
  # The Agnoclast walkthrough
@@ -1,25 +0,0 @@
1
- // Render the route's claim envelope rather than inferring success from the rows it returned. A pull
2
- // without this receipt is the exact failure mode Gate 4 is trying to rule out: work can be leased (or
3
- // not leased) invisibly, and a reader has no way to distinguish the two.
4
- export function renderTimelineClaimReceipt(claim) {
5
- if (!claim || typeof claim !== 'object' || Array.isArray(claim)) return ''
6
-
7
- if (claim.active !== true) {
8
- if (claim.reason === 'session_key_required') {
9
- return 'Viewing only — no session key was supplied, so no work was leased or attributed.'
10
- }
11
- if (claim.reason === 'disabled') return 'Viewing only — claiming was disabled, so no work was leased or attributed.'
12
- return ''
13
- }
14
-
15
- const leased = Number(claim.leased)
16
- const count = Number.isFinite(leased) && leased >= 0 ? Math.trunc(leased) : null
17
- const expiry = typeof claim.leasedUntil === 'string' && claim.leasedUntil ? claim.leasedUntil : null
18
-
19
- if (count === null) return 'Claim receipt: this session requested work; the server did not return a valid lease count.'
20
- if (count === 0) return 'Claim receipt: no work was leased to this session.'
21
-
22
- const noun = count === 1 ? 'record' : 'records'
23
- return `Claim receipt: ${count} ${noun} leased to this session${expiry ? ` until ${expiry}` : ''}. ` +
24
- 'Attribute only email threads you genuinely recognize; leave unknown work unjudged.'
25
- }
package/lib/use_brain.mjs DELETED
@@ -1,82 +0,0 @@
1
- import { fetchCortex, classify, resolveBase } from './diagnose.mjs'
2
- import { resolveToken } from './doctor.mjs'
3
-
4
- // `use-brain` — set (or show) which brain this machine's unattended session captures land in.
5
- //
6
- // WHY A SUBCOMMAND EXISTS AT ALL. The server side of this shipped 2026-08-09 with NO client surface:
7
- // no CLI, no MCP tool, no console setting. The only way to set a capture default was a raw
8
- // authenticated HTTP call, which meant the only people who could fix a broken capture were the ones
9
- // who could hand-write a curl with a bearer token. Three real users needed it; one of them is not
10
- // technical. A fix only its author can operate is not a fix.
11
- //
12
- // Pairs with the SessionStart notice: the notice tells you captures are being held and names this
13
- // command, so the loop from "something is wrong" to "it is fixed" is one paste with no docs.
14
-
15
- function out(m) { process.stdout.write(m + '\n') }
16
-
17
- export async function runUseBrain(args) {
18
- const base = resolveBase(process.env.CORTEX_URL)
19
- const { token } = resolveToken()
20
- if (!token) {
21
- out('Agnoclast: no token found. Run: npx -y @theronap/cortex-mcp setup <token>')
22
- return 1
23
- }
24
-
25
- // Everything after the subcommand is the brain, joined — so an unquoted multi-word name still
26
- // works. `use-brain Real estate` is what a person actually types; refusing it over a missing pair
27
- // of quotes would be the same species of unhelpfulness this command exists to remove.
28
- const wanted = (args ?? []).filter((a) => !a.startsWith('--')).join(' ').trim()
29
- const url = `${base}/api/brain/capture-default`
30
-
31
- if (!wanted) {
32
- // No argument: report the current state rather than erroring. "What is it set to?" is a fair
33
- // question and the answer is one GET away.
34
- try {
35
- const res = await fetchCortex(url, { headers: { Authorization: `Bearer ${token}` } })
36
- if (!res.ok) {
37
- const body = await res.text()
38
- out(`Agnoclast: could not read your capture settings — ${classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message}`)
39
- return 1
40
- }
41
- const { defaults } = await res.json()
42
- if (!defaults?.length) {
43
- out('Agnoclast: no capture brain set. Your unattended session captures land in a brain only if')
44
- out(' you belong to exactly one; otherwise they are HELD outside every brain until you set this.')
45
- out(' Set one: npx -y @theronap/cortex-mcp use-brain "<brain name or org id>"')
46
- return 0
47
- }
48
- for (const d of defaults) out(`Agnoclast: ${d.sourceType} captures land in "${d.orgName}" (${d.orgId})`)
49
- return 0
50
- } catch (e) {
51
- out(`Agnoclast: could not reach the server (${e?.message ?? String(e)})`)
52
- return 1
53
- }
54
- }
55
-
56
- try {
57
- const res = await fetchCortex(url, {
58
- method: 'POST',
59
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
60
- body: JSON.stringify({ sourceType: 'claude-code', brain: wanted }),
61
- })
62
- const body = await res.text()
63
- if (!res.ok) {
64
- let msg = classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message
65
- // The server's own error text is better than a generic one here: a 404 names the brain that was
66
- // not found, and a 409 lists the org ids of an ambiguous name — which is the whole remedy.
67
- try { const j = JSON.parse(body); if (j.error) msg = j.error } catch { /* keep the classified message */ }
68
- out(`Agnoclast: ${msg}`)
69
- return 1
70
- }
71
- const j = JSON.parse(body)
72
- out(`Agnoclast: ✓ your Claude Code sessions now land in "${j.brain}".`)
73
- // Say plainly what this does NOT do. The setter's own server-side note makes the same point,
74
- // because "I fixed it" reading as "and the backlog is handled" is how held records stay held.
75
- out(' Sessions captured BEFORE now are still held — they keep their original dates until sorted.')
76
- out(' Ask your assistant to file them (they may not all belong in the same brain).')
77
- return 0
78
- } catch (e) {
79
- out(`Agnoclast: could not reach the server (${e?.message ?? String(e)})`)
80
- return 1
81
- }
82
- }