@theronap/agnoclast-mcp 0.9.204 → 0.9.206
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/member_remove_render.mjs +29 -0
- package/lib/server.mjs +35 -2
- package/package.json +1 -1
|
@@ -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
|
{
|
|
@@ -4081,7 +4114,7 @@ function renderNudge(payload) {
|
|
|
4081
4114
|
'add_to_brain',
|
|
4082
4115
|
{
|
|
4083
4116
|
title: 'Add someone to one of your brains',
|
|
4084
|
-
description: "Add a person to one of your brains, or work out what it would take. Resolves which brain, whether you may add to it, and the manager id they are placed under. WITHOUT execute:true it only reports the plan and hands back a console link — call it that way first and show the user what you are about to do. WITH execute:true it performs the add, and then `brain` is REQUIRED: an access grant must never be aimed by a shared write pointer.
|
|
4117
|
+
description: "Add a person to one of your brains, or work out what it would take. Resolves which brain, whether you may add to it, and the manager id they are placed under. WITHOUT execute:true it only reports the plan and hands back a console link — call it that way first and show the user what you are about to do. WITH execute:true it performs the add, and then `brain` is REQUIRED: an access grant must never be aimed by a shared write pointer. To undo an add later, use remove_member — it shows the person what removal would do and asks first. IF THE BRAIN PAYS FOR ITS MEMBERS' SEATS, adding someone raises its owner's monthly charge: the first execute is refused with the new total, you ASK THE USER whether to accept that charge, and only on a yes re-run with confirm_seat_total set to that number. Use when asked to invite/add someone to a brain.",
|
|
4085
4118
|
inputSchema: {
|
|
4086
4119
|
email: z.string().describe("the person's email address — the login their Agnoclast account is (or will be) on"),
|
|
4087
4120
|
name: z.string().optional().describe('their full name; falls back to the email local-part'),
|
|
@@ -4225,7 +4258,7 @@ function renderNudge(payload) {
|
|
|
4225
4258
|
// What the seat sync did, in the server's words: the new monthly total, or that the seat
|
|
4226
4259
|
// could NOT be charged and the brain is not paying for them yet.
|
|
4227
4260
|
const seatLine = typeof r.seatNote === 'string' && r.seatNote ? `\n${r.seatNote}` : ''
|
|
4228
|
-
return { content: [{ type: 'text', text: `Added ${payload.name} <${payload.email}> to ${target.name} as ${payload.role}, placed under you.\n${how}${page}${seatLine}\n\
|
|
4261
|
+
return { content: [{ type: 'text', text: `Added ${payload.name} <${payload.email}> to ${target.name} as ${payload.role}, placed under you.\n${how}${page}${seatLine}\n\nTo undo this, remove_member takes them out of this brain — it shows you what that would do and asks first.` }] }
|
|
4229
4262
|
}
|
|
4230
4263
|
|
|
4231
4264
|
// ADR-0023: does this brain already have a page about them? The inviter decides — list, never pick.
|
package/package.json
CHANGED