@theronap/agnoclast-mcp 0.9.204 → 0.9.205

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.
@@ -0,0 +1,29 @@
1
+ // PURE: what remove_member prints (2026-10-02). Out of server.mjs so a test can reach it.
2
+ //
3
+ // ⚠ THE FIRST ANSWER IS A QUESTION FOR THE PERSON, NOT AN ERROR. Removing someone from a brain takes
4
+ // access away from a real person, so the server changes nothing until it is called again with the token
5
+ // for exactly the picture shown: who, from which brain, and where every one of their pages goes.
6
+ //
7
+ // Names, emails and brain names are other people's text: one line each, bounded.
8
+ const one = (s, n = 160) => String(s ?? '').replace(/\s+/g, ' ').trim().slice(0, n)
9
+
10
+ export function renderRemovalQuestion(out) {
11
+ const p = out?.preview ?? {}
12
+ const lines = [
13
+ `NOT REMOVED — this needs the person's yes.${out?.stale ? ' (The picture changed since the last confirmation; this is the current one.)' : ''}`,
14
+ `Remove ${one(p.name, 80)} (${one(p.email, 120)}) from the brain "${one(p.brain, 80)}"?`,
15
+ 'They lose access to this brain only. Their login, their other brains and everything they wrote stay.',
16
+ ]
17
+ for (const s of Array.isArray(p.summary) ? p.summary : []) lines.push(one(s, 400))
18
+ lines.push(`Tell the person exactly this and ask. Only if they say yes: remove_member again with confirm: "${one(out?.confirm_token, 40)}".`)
19
+ return lines.join('\n')
20
+ }
21
+
22
+ export function renderRemoved(out, person) {
23
+ const name = one(out?.displayName || person, 80)
24
+ const lines = [out?.alreadyInactive
25
+ ? `${name} was already removed from this brain — nothing more to do.`
26
+ : `Removed ${name} from "${one(out?.orgName, 80)}". They can no longer open anything in it.`]
27
+ for (const w of Array.isArray(out?.warnings) ? out.warnings : []) lines.push(`- ${one(w, 400)}`)
28
+ return lines.join('\n')
29
+ }
package/lib/server.mjs CHANGED
@@ -20,6 +20,7 @@ import { repoFullNameFrom, workingFilesFrom } from './capture.mjs'
20
20
  import { findTranscript, createWorkedFilesTracker } from './working_files.mjs'
21
21
  import { spawnObligationWorker } from './obligations_worker.mjs'
22
22
  import { renderObligations, renderPageObligations, tierReads, who } from './obligations_render.mjs'
23
+ import { renderRemovalQuestion, renderRemoved } from './member_remove_render.mjs'
23
24
  import { renderSettings } from './settings_render.mjs'
24
25
  import { formatCreated, formatDryRun, formatSources, homeFromArgs, pickSource, renderHomeProblem } from './custom_sources.mjs'
25
26
 
@@ -3033,6 +3034,38 @@ function renderNudge(payload) {
3033
3034
  },
3034
3035
  )
3035
3036
 
3037
+ // 2026-10-02. Removal was built in August with this tool planned and never wired, so an assistant could
3038
+ // add people to a brain but not remove them. Two calls, always: the first changes nothing.
3039
+ server.registerTool(
3040
+ 'remove_member',
3041
+ {
3042
+ title: 'Remove someone from a brain',
3043
+ description: "Remove a member from one of the person's brains — ONLY when the person asked you to. It takes access away from a real person, so the FIRST call changes nothing: it answers with who would be removed, from which brain, and where every one of their pages would go, plus a confirm token. Tell the person exactly that and ask. Only if they say yes, call again with `confirm` set to that token. Never pass a confirm token the person has not just approved. Removal is soft and per brain: the person keeps their login, their other brains and everything they wrote; their pages go to a steward (usually their manager), and their own profile becomes a person page. Only an owner or admin of the brain can remove anyone, never themselves, and never the last owner. Use an email address when a name could match two people.",
3044
+ inputSchema: {
3045
+ person: z.string().describe("who to remove: their email address, or their name as it appears in the brain"),
3046
+ brain: z.string().optional().describe('which brain — required if the person belongs to more than one'),
3047
+ confirm: z.string().optional().describe('ONLY after the person has seen the removal and said yes: the confirm token from the first call'),
3048
+ },
3049
+ },
3050
+ async ({ person, brain, confirm }) => {
3051
+ let res
3052
+ try {
3053
+ res = await fetchCortex(`${BASE}/api/members/remove`, {
3054
+ method: 'POST',
3055
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
3056
+ body: JSON.stringify({ person, brain, confirm }),
3057
+ })
3058
+ } catch (e) { return toolError(`Could not reach Agnoclast: ${e.message}`) }
3059
+ const out = await res.json().catch(() => null)
3060
+ if (res.status === 409 && out?.error === 'confirm_removal') {
3061
+ // Not an error: a person has to decide. Said as the question it is.
3062
+ return { content: [{ type: 'text', text: renderRemovalQuestion(out) }] }
3063
+ }
3064
+ if (!res.ok) return toolError(`Not removed: ${out?.detail ?? out?.error ?? res.status}`)
3065
+ return { content: [{ type: 'text', text: renderRemoved(out, person) }] }
3066
+ },
3067
+ )
3068
+
3036
3069
  server.registerTool(
3037
3070
  'set_obligation_tier',
3038
3071
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.204",
3
+ "version": "0.9.205",
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": {