crbro-memory 1.12.0 → 1.13.0

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/dist/server.js CHANGED
@@ -1,6 +1,15 @@
1
1
  "use strict";
2
2
  // ─── CRBRO MCP Server ────────────────────────────────────────────
3
- // Main server with all 22 tools registered
3
+ // Main server with all 23 tools registered.
4
+ //
5
+ // Every tool goes through registerTool with a title, MCP annotations
6
+ // (readOnlyHint / destructiveHint / idempotentHint / openWorldHint) and, for
7
+ // the read tools with a stable shape, an outputSchema honoured with
8
+ // structuredContent. The descriptions state what the tool does, when to use
9
+ // it over its siblings, its side effects and what it returns — and nothing
10
+ // else: the discipline of using the memory well is said once, at boot, in
11
+ // memory_discipline, because these definitions are paid on every request in
12
+ // clients that load all tools (measured: ~5.4k tokens for the 23).
4
13
  Object.defineProperty(exports, "__esModule", { value: true });
5
14
  exports.createServer = createServer;
6
15
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
@@ -35,7 +44,7 @@ function runningVersion() {
35
44
  function createServer() {
36
45
  const server = new mcp_js_1.McpServer({
37
46
  name: 'crbro-memory',
38
- version: '1.8.2',
47
+ version: runningVersion(),
39
48
  });
40
49
  // ─── Initialize engines ──────────────────────────────────────
41
50
  const brain = new brain_js_1.Brain();
@@ -61,7 +70,12 @@ function createServer() {
61
70
  // ═══════════════════════════════════════════════════════════════
62
71
  // TOOL 1: crbro_boot — Boot sequence
63
72
  // ═══════════════════════════════════════════════════════════════
64
- server.tool('crbro_boot', '⚠️ MANDATORY FIRST ACTION: Boot the CRBRO brain. You MUST call this as your very first action in every new conversation, before doing ANY other work. This loads persistent memory from all previous sessions — projects, decisions, patterns, and preferences the user has built over time. Skipping this means losing all accumulated context and forcing the user to repeat themselves. On first use, initializes the brain structure. On subsequent uses, loads hot topics, active context, and recent session history.', {}, async () => {
73
+ server.registerTool('crbro_boot', {
74
+ title: 'Boot the brain',
75
+ description: 'Boot the CRBRO brain — call it FIRST in every conversation, before any other work. Loads persistent memory from earlier sessions: hot topics, active context with open_items and recently_closed (never report recently_closed as pending; verify open_items before repeating them), recent session history, counts, any active protocols as a protocol_enforcement block you must follow, and memory_discipline — the rules for using this memory well. Initializes the brain on first use, readies the search index and syncs shared team spaces (offline is a normal outcome, not an error). Skipping it means losing all accumulated context.',
76
+ inputSchema: {},
77
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
78
+ }, async () => {
65
79
  try {
66
80
  const result = await brain.boot();
67
81
  // Initialize search engine
@@ -102,6 +116,20 @@ function createServer() {
102
116
  summary: e.message,
103
117
  }));
104
118
  }
119
+ // How to use this memory well — said once here, at boot, instead of
120
+ // repeated inside every tool description. Measured with a real
121
+ // tools/list: the 23 definitions cost ~6.3k tokens on every request
122
+ // in clients that load all tools; this paragraph costs ~200, once.
123
+ response.memory_discipline =
124
+ 'Before crbro_learn, crbro_recall: what you are about to save may already exist — then pass ' +
125
+ 'supersedes instead of adding a sibling (two versions of one fact compete on recall as equals). ' +
126
+ 'Structure — paths, what serves what, traps — goes in crbro_map, not in facts; anything derivable ' +
127
+ 'from the repo or git history is not worth storing. Write facts dense and self-contained: they are ' +
128
+ 'recalled without this conversation. type:error keeps a mistake with its fix; type:debt keeps a ' +
129
+ 'deliberate deferral with its ceiling and revisit trigger. Credentials never go in the brain: ' +
130
+ 'crbro_secret, then record only the NAME. Recall results carry confidence — "weak" means the match ' +
131
+ 'covers little of the question, verify before relying on it — and when two facts disagree, prefer ' +
132
+ 'the more recent. Call crbro_consolidate before the conversation ends; it logs the session too.';
105
133
  return {
106
134
  content: [{
107
135
  type: 'text',
@@ -122,23 +150,37 @@ function createServer() {
122
150
  // ═══════════════════════════════════════════════════════════════
123
151
  // TOOL 2: crbro_status — Brain status
124
152
  // ═══════════════════════════════════════════════════════════════
125
- server.tool('crbro_status', 'Get the current status of the CRBRO brain — total neurons, synapses, sessions, and brain path.', {}, async () => {
153
+ server.registerTool('crbro_status', {
154
+ title: 'Brain status',
155
+ description: 'Read-only snapshot: CRBRO version, brain format, neuron/synapse/session totals, brain path, last boot and last consolidation. Loads no memory — that is crbro_boot.',
156
+ inputSchema: {},
157
+ outputSchema: {
158
+ crbro_version: zod_1.z.string(),
159
+ brain_format: zod_1.z.string().optional(),
160
+ total_neurons: zod_1.z.number(),
161
+ total_synapses: zod_1.z.number(),
162
+ total_sessions: zod_1.z.number(),
163
+ brain_path: zod_1.z.string().optional(),
164
+ last_boot: zod_1.z.string().nullable().optional(),
165
+ last_consolidation: zod_1.z.string().nullable().optional(),
166
+ },
167
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
168
+ }, async () => {
126
169
  try {
127
170
  const manifest = await brain.getManifest();
171
+ const payload = {
172
+ crbro_version: runningVersion(),
173
+ brain_format: manifest.version,
174
+ total_neurons: manifest.total_neurons,
175
+ total_synapses: manifest.total_synapses,
176
+ total_sessions: manifest.total_sessions,
177
+ brain_path: manifest.brain_path,
178
+ last_boot: manifest.last_boot,
179
+ last_consolidation: manifest.last_consolidation,
180
+ };
128
181
  return {
129
- content: [{
130
- type: 'text',
131
- text: JSON.stringify({
132
- crbro_version: runningVersion(),
133
- brain_format: manifest.version,
134
- total_neurons: manifest.total_neurons,
135
- total_synapses: manifest.total_synapses,
136
- total_sessions: manifest.total_sessions,
137
- brain_path: manifest.brain_path,
138
- last_boot: manifest.last_boot,
139
- last_consolidation: manifest.last_consolidation,
140
- }, null, 2),
141
- }],
182
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
183
+ structuredContent: payload,
142
184
  };
143
185
  }
144
186
  catch (err) {
@@ -154,15 +196,20 @@ function createServer() {
154
196
  // ═══════════════════════════════════════════════════════════════
155
197
  // TOOL 3: crbro_learn — Add knowledge to the brain
156
198
  // ═══════════════════════════════════════════════════════════════
157
- server.tool('crbro_learn', 'Teach the brain a fact, decision, pattern, preference or error about a topic. If the neuron (topic) does not exist, it will be created automatically. Use this to store knowledge that should persist across sessions. BEFORE saving, walk the ladder: does this already exist (crbro_recall first)? does it update something (pass `supersedes`, do not add a sibling)? is it structure rather than an event (crbro_map, not a fact)? is it derivable from the repo or git history (then do not store it)? and would it survive losing half its words (then cut them - every word should carry weight)? Type `error` is for a mistake you made and how it was corrected - store both halves in one entry, and check for them with crbro_recall before repeating a task where you have slipped before. Type `debt` is the twin for deliberate deferrals - what was skipped on purpose, its ceiling, and when to revisit; before re-proposing or re-discussing something, recall may surface that it was already deferred with a reason. If the new fact closely resembles an active one, the response warns with `near_duplicates` - it is stored anyway, but retire the old telling or two versions keep coming back on recall as equals.', {
158
- topic: zod_1.z.string().describe('The topic name (e.g., "OctoChat", "Firebase", "SEO Strategy")'),
159
- type: zod_1.z.enum(['fact', 'decision', 'pattern', 'preference', 'error', 'debt']).describe('Type of knowledge to store. `error` = a mistake plus its correction, kept as a ledger you can check before repeating the task. `debt` = a deliberate deferral: what was NOT done on purpose, its ceiling, and the condition to revisit — write all three in one entry, e.g. "DEFERRED: protecting the PDFs. CEILING: anyone can download the lead magnets without signing up. REVISIT WHEN: the signup flow works." When someone re-proposes a dead idea, recall serves the decision with its date and trigger.'),
160
- content: zod_1.z.string().describe('The knowledge content to remember'),
161
- confidence: zod_1.z.number().min(0).max(1).optional().describe('Confidence level 0.0-1.0 (default 1.0)'),
162
- domain: zod_1.z.string().optional().describe('Domain category (e.g., "proyectos-web", "infraestructura")'),
163
- rationale: zod_1.z.string().optional().describe('Rationale for decisions'),
164
- neuron_id: zod_1.z.string().optional().describe('Exact neuron ID to write to (e.g. "project_octochat"). Pass the neuron_id you got back from crbro_recall: it skips name matching entirely and guarantees the knowledge lands where you mean.'),
165
- supersedes: zod_1.z.array(zod_1.z.string()).optional().describe('Facts this one replaces: their ids, or their exact text. They stop showing up in recall but stay in the neuron file.'),
199
+ server.registerTool('crbro_learn', {
200
+ title: 'Learn something',
201
+ description: 'Store a fact, decision, pattern, preference, error or debt on a topic. The neuron is created if it does not exist; pass neuron_id (from crbro_recall) to target an exact one and skip name matching. Recall first: to replace an outdated fact pass its id in supersedes rather than adding a sibling. A fact stored verbatim before is skipped silently; decisions always append; preferences never leave this machine. Credential-like values are replaced with a marker before touching disk and listed in redacted — store the value with crbro_secret and record only its name. Returns neuron_id, action (created|updated), superseded count, near_duplicates (stored anyway; retire the old telling), supersedes_unmatched (those targets are still live — retire them with crbro_revise) and running totals.',
202
+ inputSchema: {
203
+ topic: zod_1.z.string().describe('Topic name, e.g. "OctoChat", "Firebase", "SEO Strategy".'),
204
+ type: zod_1.z.enum(['fact', 'decision', 'pattern', 'preference', 'error', 'debt']).describe('error = a mistake plus its correction, in one entry. debt = a deliberate deferral: what was NOT done on purpose, its ceiling, and the revisit condition, e.g. "DEFERRED: protecting the PDFs. CEILING: anyone can download them without signing up. REVISIT WHEN: the signup flow works."'),
205
+ content: zod_1.z.string().describe('The knowledge itself. Dense and self-contained: it is recalled without this conversation as context.'),
206
+ confidence: zod_1.z.number().min(0).max(1).optional().describe('0.0-1.0, default 1.0. Facts only.'),
207
+ domain: zod_1.z.string().optional().describe('Domain, e.g. "proyectos-web". Applied when the neuron is created; on an existing neuron it only replaces the default "general".'),
208
+ rationale: zod_1.z.string().optional().describe('Why the decision was taken. Stored and indexed with it; ignored for other types.'),
209
+ neuron_id: zod_1.z.string().optional().describe('Exact neuron id from crbro_recall, e.g. "project_octochat". Skips name matching entirely.'),
210
+ supersedes: zod_1.z.array(zod_1.z.string()).optional().describe('Facts this one replaces: their ids or exact text. They leave recall but stay in the file. Unmatched targets are reported and stay live.'),
211
+ },
212
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
166
213
  }, async (args) => {
167
214
  try {
168
215
  const result = await cortex.learn(args.topic, args.type, args.content, {
@@ -238,11 +285,16 @@ function createServer() {
238
285
  // ═══════════════════════════════════════════════════════════════
239
286
  // TOOL 4: crbro_neuron — Read a specific neuron
240
287
  // ═══════════════════════════════════════════════════════════════
241
- server.tool('crbro_neuron', 'Read a specific neuron by ID or name. Returns its facts (newest first), decisions, patterns, connections and heat. Big neurons are paginated - page through them with offset instead of trying to pull everything at once.', {
242
- id: zod_1.z.string().describe('Neuron ID (e.g., "project_octochat") or name (e.g., "OctoChat")'),
243
- limit: zod_1.z.number().optional().describe('How many facts to return (default 40, max 200)'),
244
- offset: zod_1.z.number().optional().describe('Skip this many facts. Facts come newest first.'),
245
- include_superseded: zod_1.z.boolean().optional().describe('Include facts marked superseded or retracted (default false)'),
288
+ server.registerTool('crbro_neuron', {
289
+ title: 'Read a neuron',
290
+ description: 'Read one neuron by id or name: facts newest first (superseded and retracted hidden unless include_superseded), decisions, patterns, preferences, errors, debts, entry dates, connections, heat and system map. Reading bumps its access stats. Big neurons are paged — use offset rather than pulling everything at once. To find the right neuron first, use crbro_recall.',
291
+ inputSchema: {
292
+ id: zod_1.z.string().describe('Neuron id (e.g. "project_octochat") or name (e.g. "OctoChat").'),
293
+ limit: zod_1.z.number().optional().describe('Facts to return: default 40, max 200.'),
294
+ offset: zod_1.z.number().optional().describe('Facts to skip. Facts come newest first.'),
295
+ include_superseded: zod_1.z.boolean().optional().describe('Also return superseded and retracted facts (default false).'),
296
+ },
297
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
246
298
  }, async (args) => {
247
299
  try { // Try by ID first, then by name
248
300
  let neuron = await cortex.get(args.id);
@@ -300,11 +352,23 @@ function createServer() {
300
352
  // ═══════════════════════════════════════════════════════════════
301
353
  // TOOL 5: crbro_neurons — List neurons
302
354
  // ═══════════════════════════════════════════════════════════════
303
- server.tool('crbro_neurons', 'List all neurons in the brain with optional filters. Returns ID, name, domain, heat, and facts count.', {
304
- domain: zod_1.z.string().optional().describe('Filter by domain (e.g., "proyectos-web")'),
305
- type: zod_1.z.enum(['project', 'tech', 'lang', 'person', 'domain', 'process', 'protocol']).optional().describe('Filter by neuron type'),
306
- min_heat: zod_1.z.number().optional().describe('Minimum heat score (0.0-1.0)'),
307
- limit: zod_1.z.number().optional().describe('Max results (default 50)'),
355
+ server.registerTool('crbro_neurons', {
356
+ title: 'List neurons',
357
+ description: 'List neurons, hottest first, filtered by domain, type or min_heat. Read-only. Each row: id, name, domain, type, heat, last_accessed, facts_count. To search content rather than list topics, use crbro_recall.',
358
+ inputSchema: {
359
+ domain: zod_1.z.string().optional().describe('Only this domain, e.g. "proyectos-web".'),
360
+ type: zod_1.z.enum(['project', 'tech', 'lang', 'person', 'domain', 'process', 'protocol']).optional().describe('Only this neuron type.'),
361
+ min_heat: zod_1.z.number().optional().describe('Minimum heat, 0.0-1.0. Heat blends access frequency, recency and connectivity.'),
362
+ limit: zod_1.z.number().optional().describe('Max rows (default 50).'),
363
+ },
364
+ outputSchema: {
365
+ total: zod_1.z.number(),
366
+ neurons: zod_1.z.array(zod_1.z.object({
367
+ id: zod_1.z.string(), name: zod_1.z.string(), domain: zod_1.z.string(), type: zod_1.z.string(),
368
+ heat: zod_1.z.number(), last_accessed: zod_1.z.string(), facts_count: zod_1.z.number(),
369
+ }).loose()),
370
+ },
371
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
308
372
  }, async (args) => {
309
373
  try {
310
374
  const neurons = await cortex.list({
@@ -313,14 +377,10 @@ function createServer() {
313
377
  min_heat: args.min_heat,
314
378
  limit: args.limit,
315
379
  });
380
+ const payload = { total: neurons.length, neurons };
316
381
  return {
317
- content: [{
318
- type: 'text',
319
- text: JSON.stringify({
320
- total: neurons.length,
321
- neurons,
322
- }, null, 2),
323
- }],
382
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
383
+ structuredContent: payload,
324
384
  };
325
385
  }
326
386
  catch (err) {
@@ -336,28 +396,46 @@ function createServer() {
336
396
  // ═══════════════════════════════════════════════════════════════
337
397
  // TOOL 6: crbro_recall — Search the brain
338
398
  // ═══════════════════════════════════════════════════════════════
339
- server.tool('crbro_recall', 'Search the brain for knowledge saved in earlier sessions. Searches the full text of every fact, decision, pattern, error and system map, not just topic names, and each result carries the exact chunk that matched plus the date it was recorded. Call this before asking the user something they may already have told you, and before assuming a past decision. A result with has_map: true belongs to a neuron that keeps a system map - read it with crbro_map before touching that system. If a result looks right, pass its neuron_id back to crbro_learn so new knowledge lands in the same place.', {
340
- query: zod_1.z.string().describe('What to search for (e.g., "Firebase authentication setup")'),
341
- domain: zod_1.z.string().optional().describe('Filter by domain'),
342
- limit: zod_1.z.number().optional().describe('Max results (default 10)'),
399
+ server.registerTool('crbro_recall', {
400
+ title: 'Recall',
401
+ description: 'Search everything saved in earlier sessions — the full text of facts, decisions, patterns, preferences, errors, debts and system maps, not just topic names. Read-only. One result per neuron: its best matching chunk (matching_content, matched_kind, matched_added), a confidence label (weak = the match covers little of the question; verify before relying on it) and, for the top results, also_matched — the neuron\'s next best lines. Superseded and retracted facts never surface. Call it before asking the user something they may already have told you, and before crbro_learn. If nothing matches, retry with fewer, more distinctive words (names, ids, filenames). has_map:true means the neuron keeps a system map — read it with crbro_map before touching that system.',
402
+ inputSchema: {
403
+ query: zod_1.z.string().describe('What to look for, e.g. "Firebase authentication setup". Fewer, distinctive terms beat full sentences.'),
404
+ domain: zod_1.z.string().optional().describe('Only neurons in this domain (exact match, e.g. "proyectos-web").'),
405
+ limit: zod_1.z.number().optional().describe('Max neurons returned (default 10).'),
406
+ },
407
+ outputSchema: {
408
+ query: zod_1.z.string(),
409
+ total_results: zod_1.z.number(),
410
+ results: zod_1.z.array(zod_1.z.object({
411
+ neuron_id: zod_1.z.string(), name: zod_1.z.string(), domain: zod_1.z.string(),
412
+ relevance_score: zod_1.z.number(), matching_content: zod_1.z.string(),
413
+ matched_kind: zod_1.z.string().optional(), matched_added: zod_1.z.string().optional(),
414
+ heat: zod_1.z.number(), has_map: zod_1.z.boolean().optional(),
415
+ matched_terms: zod_1.z.number().optional(), query_terms: zod_1.z.number().optional(),
416
+ confidence: zod_1.z.enum(['strong', 'weak']).optional(),
417
+ also_matched: zod_1.z.array(zod_1.z.object({ text: zod_1.z.string(), kind: zod_1.z.string(), added: zod_1.z.string() })).optional(),
418
+ }).loose()),
419
+ hint: zod_1.z.string(),
420
+ },
421
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
343
422
  }, async (args) => {
344
423
  try {
345
424
  const results = await searchEngine.search(args.query, {
346
425
  domain: args.domain,
347
426
  limit: args.limit,
348
427
  });
428
+ const payload = {
429
+ query: args.query,
430
+ total_results: results.length,
431
+ results,
432
+ hint: results.length === 0
433
+ ? 'Nothing matched. Try fewer, more distinctive words - names, ids, filenames - rather than a full sentence.'
434
+ : 'matching_content is the chunk that matched; matched_added is when it was recorded; confidence "weak" means little of the question was covered - verify before relying on it. Prefer recent facts when two disagree. has_map: true means the neuron holds a system map - read it with crbro_map before working on that system.',
435
+ };
349
436
  return {
350
- content: [{
351
- type: 'text',
352
- text: JSON.stringify({
353
- query: args.query,
354
- total_results: results.length,
355
- results,
356
- hint: results.length === 0
357
- ? 'Nothing matched. Try fewer, more distinctive words - names, ids, filenames - rather than a full sentence.'
358
- : 'matching_content is the chunk that matched; matched_added is when it was recorded. Prefer recent facts when two disagree. Results with has_map: true belong to neurons holding a system map - read it with crbro_map before working on that system.',
359
- }, null, 2),
360
- }],
437
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
438
+ structuredContent: payload,
361
439
  };
362
440
  }
363
441
  catch (err) {
@@ -373,11 +451,16 @@ function createServer() {
373
451
  // ═══════════════════════════════════════════════════════════════
374
452
  // TOOL 7: crbro_connect — Create/strengthen a synapse
375
453
  // ═══════════════════════════════════════════════════════════════
376
- server.tool('crbro_connect', 'Create or strengthen a connection (synapse) between two neurons. Synapses track relationships and strengthen with repeated co-access.', {
377
- from: zod_1.z.string().describe('Source neuron ID'),
378
- to: zod_1.z.string().describe('Target neuron ID'),
379
- type: zod_1.z.enum(['dependency', 'causal', 'temporal', 'conceptual', 'hierarchy', 'alternative']).describe('Connection type'),
380
- context: zod_1.z.string().optional().describe('Description of the relationship'),
454
+ server.registerTool('crbro_connect', {
455
+ title: 'Connect two neurons',
456
+ description: 'Create or strengthen the undirected synapse between two neurons: created at strength 0.5, +0.1 per repeat call (cap 1.0). Idle synapses decay and crbro_maintenance prunes the weak. Returns synapse_id, action (created|strengthened) and strength. Neurons written in the same session are linked automatically by crbro_consolidate; use this for relationships that are not just co-occurrence.',
457
+ inputSchema: {
458
+ from: zod_1.z.string().describe('Source neuron id, e.g. "project_octochat". Not validated: use an exact id from crbro_recall or crbro_neurons, or the synapse points at nothing.'),
459
+ to: zod_1.z.string().describe('Target neuron id. Order does not matter — (a,b) and (b,a) are the same synapse.'),
460
+ type: zod_1.z.enum(['dependency', 'causal', 'temporal', 'conceptual', 'hierarchy', 'alternative']).describe('Relationship kind. Used only on creation; a strengthening call keeps the existing type.'),
461
+ context: zod_1.z.string().optional().describe('One line on the relationship. On strengthen it replaces the stored text; omit to keep it.'),
462
+ },
463
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
381
464
  }, async (args) => {
382
465
  try {
383
466
  const result = await synapses.connect(args.from, args.to, args.type, args.context);
@@ -408,21 +491,29 @@ function createServer() {
408
491
  // ═══════════════════════════════════════════════════════════════
409
492
  // TOOL 8: crbro_connections — Get neuron connections
410
493
  // ═══════════════════════════════════════════════════════════════
411
- server.tool('crbro_connections', 'Get all connections (synapses) for a specific neuron. Shows related topics with connection strength and type.', {
412
- neuron_id: zod_1.z.string().describe('Neuron ID to get connections for'),
413
- min_strength: zod_1.z.number().optional().describe('Minimum synapse strength (0.0-1.0)'),
494
+ server.registerTool('crbro_connections', {
495
+ title: 'Neuron connections',
496
+ description: 'List every synapse touching one neuron, strongest first — target_id, target_name, type, strength and context per entry. Read-only; an unknown or unconnected id returns an empty list, not an error.',
497
+ inputSchema: {
498
+ neuron_id: zod_1.z.string().describe('Exact neuron id, e.g. "project_octochat". Names are not resolved here — get the id from crbro_recall or crbro_neurons.'),
499
+ min_strength: zod_1.z.number().optional().describe('Drop connections weaker than this (0.0-1.0). Omit for all; 0 is no filter.'),
500
+ },
501
+ outputSchema: {
502
+ neuron_id: zod_1.z.string(),
503
+ total_connections: zod_1.z.number(),
504
+ connections: zod_1.z.array(zod_1.z.object({
505
+ target_id: zod_1.z.string(), target_name: zod_1.z.string(), type: zod_1.z.string(),
506
+ strength: zod_1.z.number(), context: zod_1.z.string(),
507
+ }).loose()),
508
+ },
509
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
414
510
  }, async (args) => {
415
511
  try {
416
512
  const connections = await synapses.getConnections(args.neuron_id, args.min_strength);
513
+ const payload = { neuron_id: args.neuron_id, total_connections: connections.length, connections };
417
514
  return {
418
- content: [{
419
- type: 'text',
420
- text: JSON.stringify({
421
- neuron_id: args.neuron_id,
422
- total_connections: connections.length,
423
- connections,
424
- }, null, 2),
425
- }],
515
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
516
+ structuredContent: payload,
426
517
  };
427
518
  }
428
519
  catch (err) {
@@ -438,11 +529,16 @@ function createServer() {
438
529
  // ═══════════════════════════════════════════════════════════════
439
530
  // TOOL 9: crbro_session_log — Log a session
440
531
  // ═══════════════════════════════════════════════════════════════
441
- server.tool('crbro_session_log', 'Log a session summary to the hippocampus. Call at the end of a work session to record what was done.', {
442
- summary: zod_1.z.string().describe('Summary of what happened in this session'),
443
- topics_touched: zod_1.z.array(zod_1.z.string()).describe('List of neuron IDs that were relevant'),
444
- key_facts_added: zod_1.z.number().optional().describe('Number of new facts stored'),
445
- decisions_made: zod_1.z.number().optional().describe('Number of decisions recorded'),
532
+ server.registerTool('crbro_session_log', {
533
+ title: 'Log a session',
534
+ description: 'Log a session summary to the hippocampus — one entry per calendar day; a same-day call appends to it. Also replaces the active-topics list with topics_touched. Normally unnecessary: crbro_consolidate logs the session itself.',
535
+ inputSchema: {
536
+ summary: zod_1.z.string().describe('What happened in this session. Appended if today already has an entry.'),
537
+ topics_touched: zod_1.z.array(zod_1.z.string()).describe('Relevant neuron ids. Merged (deduplicated) into the day entry; becomes the active-topics list.'),
538
+ key_facts_added: zod_1.z.number().optional().describe('New facts stored. Summed into the day total on same-day calls.'),
539
+ decisions_made: zod_1.z.number().optional().describe('Decisions recorded. Summed into the day total on same-day calls.'),
540
+ },
541
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
446
542
  }, async (args) => {
447
543
  try {
448
544
  const session = await hippocampus.logSession({
@@ -479,19 +575,28 @@ function createServer() {
479
575
  // ═══════════════════════════════════════════════════════════════
480
576
  // TOOL 10: crbro_sessions — List recent sessions
481
577
  // ═══════════════════════════════════════════════════════════════
482
- server.tool('crbro_sessions', 'List recent session logs from the hippocampus.', {
483
- limit: zod_1.z.number().optional().describe('Number of sessions to return (default 10)'),
578
+ server.registerTool('crbro_sessions', {
579
+ title: 'Recent sessions',
580
+ description: 'List recent session logs, newest first, one per day: date, merged summary, topics_touched neuron ids, fact/decision counters. Read-only. Read them before asking the user what was already done; crbro_boot already returns the last one.',
581
+ inputSchema: {
582
+ limit: zod_1.z.number().optional().describe('Day logs to return, newest first (default 10).'),
583
+ },
584
+ outputSchema: {
585
+ total: zod_1.z.number(),
586
+ sessions: zod_1.z.array(zod_1.z.object({
587
+ session_id: zod_1.z.string().optional(), date: zod_1.z.string().optional(), summary: zod_1.z.string().optional(),
588
+ topics_touched: zod_1.z.array(zod_1.z.string()).optional(),
589
+ key_facts_added: zod_1.z.number().optional(), decisions_made: zod_1.z.number().optional(),
590
+ }).loose()),
591
+ },
592
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
484
593
  }, async (args) => {
485
594
  try {
486
595
  const sessions = await hippocampus.listSessions(args.limit);
596
+ const payload = { total: sessions.length, sessions };
487
597
  return {
488
- content: [{
489
- type: 'text',
490
- text: JSON.stringify({
491
- total: sessions.length,
492
- sessions,
493
- }, null, 2),
494
- }],
598
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
599
+ structuredContent: payload,
495
600
  };
496
601
  }
497
602
  catch (err) {
@@ -507,10 +612,15 @@ function createServer() {
507
612
  // ═══════════════════════════════════════════════════════════════
508
613
  // TOOL 11: crbro_context — Active context
509
614
  // ═══════════════════════════════════════════════════════════════
510
- server.tool('crbro_context', 'Read or update the active working context: current topics, open items and last session. Close items as soon as they are done - an item left open here gets repeated back to the user in later sessions long after it was finished.', {
511
- set_topics: zod_1.z.array(zod_1.z.string()).optional().describe('Set active topics (neuron IDs)'),
512
- add_pending: zod_1.z.string().optional().describe('Add an open item. Write it so it can be checked later, not as a vague reminder.'),
513
- resolve_pending: zod_1.z.string().optional().describe('Close an open item: its id (e.g. "p_ab12cd"), or enough of its text to identify it. It moves to recently_closed.'),
615
+ server.registerTool('crbro_context', {
616
+ title: 'Working context',
617
+ description: 'Read or update the working context: active topics, open items, last session. Call with no arguments to read; every call returns the full state plus resolved, the items it closed. Close items as soon as they are done — an item left open is repeated back to the user in later sessions long after it was finished.',
618
+ inputSchema: {
619
+ set_topics: zod_1.z.array(zod_1.z.string()).optional().describe('Replace the whole active-topics list with these neuron ids (no merge). crbro_session_log also overwrites it.'),
620
+ add_pending: zod_1.z.string().optional().describe('Add an open item, written so it can be checked later. Identical text is deduplicated, so re-adding is a safe no-op.'),
621
+ resolve_pending: zod_1.z.string().optional().describe('Close an open item by id (e.g. "p_ab12cd") or by 8+ characters of its text (case-insensitive substring; several items can close at once). Matches move to recently_closed, newest first, capped at 15. An empty resolved in the reply means nothing matched.'),
622
+ },
623
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
514
624
  }, async (args) => {
515
625
  try {
516
626
  const ctx = await prefrontal.updateContext({
@@ -538,16 +648,26 @@ function createServer() {
538
648
  // ═══════════════════════════════════════════════════════════════
539
649
  // TOOL 12: crbro_hot_topics — Hot topics
540
650
  // ═══════════════════════════════════════════════════════════════
541
- server.tool('crbro_hot_topics', 'Get the hottest topics — neurons with the highest heat scores (based on frequency, recency, and connectivity).', {
542
- limit: zod_1.z.number().optional().describe('Number of topics to return (default 15)'),
651
+ server.registerTool('crbro_hot_topics', {
652
+ title: 'Hot topics',
653
+ description: 'The hottest neurons by heat (access frequency, recency, connectivity). Read-only, served from a cache rebuilt at consolidate and maintenance — last_recalculated says when. crbro_boot already returns this list.',
654
+ inputSchema: {
655
+ limit: zod_1.z.number().optional().describe('Topics to return (default 15; the cache never holds more than 20).'),
656
+ },
657
+ outputSchema: {
658
+ topics: zod_1.z.array(zod_1.z.object({
659
+ id: zod_1.z.string(), name: zod_1.z.string(), heat: zod_1.z.number(), last_access: zod_1.z.string(), domain: zod_1.z.string(),
660
+ }).loose()),
661
+ last_recalculated: zod_1.z.string().optional(),
662
+ },
663
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
543
664
  }, async (args) => {
544
665
  try {
545
666
  const hotTopics = await prefrontal.getHotTopics(args.limit);
667
+ const payload = { ...hotTopics };
546
668
  return {
547
- content: [{
548
- type: 'text',
549
- text: JSON.stringify(hotTopics, null, 2),
550
- }],
669
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
670
+ structuredContent: payload,
551
671
  };
552
672
  }
553
673
  catch (err) {
@@ -563,8 +683,13 @@ function createServer() {
563
683
  // ═══════════════════════════════════════════════════════════════
564
684
  // TOOL 13: crbro_global_map — Global neural map
565
685
  // ═══════════════════════════════════════════════════════════════
566
- server.tool('crbro_global_map', 'View the global neural map — clusters of related topics and bridges between domains.', {
567
- rebuild: zod_1.z.boolean().optional().describe('Force rebuild the map (default: use cached)'),
686
+ server.registerTool('crbro_global_map', {
687
+ title: 'Global map',
688
+ description: 'The global map: one cluster per domain (node ids, top neurons, heat) and bridges where synapses cross domains, served from a cache stamped last_rebuilt. Read-only unless rebuild:true, which rescans every neuron and rewrites that cache (derived data, no knowledge is touched). For one system\'s internals use crbro_map instead.',
689
+ inputSchema: {
690
+ rebuild: zod_1.z.boolean().optional().describe('true = rescan every neuron and rewrite the cached map (slower on big brains). Default: serve the cache, building it only if missing — it can lag recent learning; crbro_maintenance also rebuilds it.'),
691
+ },
692
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
568
693
  }, async (args) => {
569
694
  try {
570
695
  const globalMap = await prefrontal.getGlobalMap(args.rebuild);
@@ -594,10 +719,15 @@ function createServer() {
594
719
  // ═══════════════════════════════════════════════════════════════
595
720
  // TOOL 14: crbro_maintenance — Run maintenance
596
721
  // ═══════════════════════════════════════════════════════════════
597
- server.tool('crbro_maintenance', 'Run brain maintenance: recalculate heat, prune weak synapses, check integrity and rebuild the search index. Archiving cold neurons is OFF unless you ask for it - on a mature brain most neurons look cold, and archived ones stop being searchable.', {
598
- dry_run: zod_1.z.boolean().optional().describe('If true, only report what would happen without acting'),
599
- archive: zod_1.z.boolean().optional().describe('Also move cold neurons (heat < 0.05, untouched 90+ days) out of the cortex. Off by default. Run with dry_run first and read archivable_neurons before turning this on.'),
600
- purge_boilerplate: zod_1.z.boolean().optional().describe('Also delete contentless facts left by early versions of the miner ("Referenced in: file.md"). Off by default; every run reports how many there are.'),
722
+ server.registerTool('crbro_maintenance', {
723
+ title: 'Brain maintenance',
724
+ description: 'Run brain maintenance: recalculate heat, prune weak synapses, check integrity and rebuild the search index. Returns a report (counts, integrity_issues, notes) and flags debts that never named a revisit trigger. dry_run:true reports without writing anything. Archiving cold neurons and purging miner boilerplate are OFF unless asked — on a mature brain most neurons look cold, and archived ones stop being searchable. For session close use crbro_consolidate, not this.',
725
+ inputSchema: {
726
+ dry_run: zod_1.z.boolean().optional().describe('true = report only: no heat recalc, archiving, purge, lock sweep, pruning or index rebuild. Counts, debts and integrity checks still run.'),
727
+ archive: zod_1.z.boolean().optional().describe('Also move cold neurons (heat < 0.05, untouched 90+ days) out of the cortex. Off by default; run dry_run first and read archivable_neurons. Restore by moving the file from archives/ back into cortex/.'),
728
+ purge_boilerplate: zod_1.z.boolean().optional().describe('Also delete contentless facts left by early miner versions ("Referenced in: file.md"). Off by default; every run reports how many there are. Neurons left empty are kept.'),
729
+ },
730
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
601
731
  }, async (args) => {
602
732
  try {
603
733
  const report = await maintenance.run(args.dry_run, { archive: args.archive, purgeBoilerplate: args.purge_boilerplate });
@@ -624,8 +754,13 @@ function createServer() {
624
754
  // ═══════════════════════════════════════════════════════════════
625
755
  // TOOL 15: crbro_consolidate — End-of-session consolidation
626
756
  // ═══════════════════════════════════════════════════════════════
627
- server.tool('crbro_consolidate', '⚠️ CALL BEFORE SESSION ENDS: Consolidate the brain at end of session. You MUST call this before the conversation ends if ANY significant work was done (code changes, decisions made, new information learned). This persists all new knowledge, logs the session summary, recalculates topic heat scores, and updates the manifest. Failing to consolidate means this entire session\'s knowledge is permanently lost. Always provide a meaningful summary of what was accomplished.', {
628
- summary: zod_1.z.string().describe('Summary of the session being consolidated'),
757
+ server.registerTool('crbro_consolidate', {
758
+ title: 'Consolidate the session',
759
+ description: 'Call before the conversation ends whenever significant work was done. Persists pending knowledge and index writes, logs the session from summary (no separate crbro_session_log needed), recalculates heat, links the neurons written this session with weak temporal synapses (synapses_updated), updates the manifest and syncs shared team spaces (offline is normal; notes go out next time). Returns the session\'s real write counts — facts_saved, decisions_saved, topics_touched — and per-space sync state. Not consolidating loses the session\'s knowledge.',
760
+ inputSchema: {
761
+ summary: zod_1.z.string().describe('What was accomplished: concrete work, decisions, outcomes. Stored verbatim as the session log later sessions read.'),
762
+ },
763
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
629
764
  }, async (args) => {
630
765
  try {
631
766
  const result = await maintenance.consolidate(args.summary);
@@ -660,10 +795,15 @@ function createServer() {
660
795
  // ═══════════════════════════════════════════════════════════════
661
796
  // TOOL: crbro_map — The living map of a system
662
797
  // ═══════════════════════════════════════════════════════════════
663
- server.tool('crbro_map', 'Read or replace the system map of a neuron. A map is ONE living document answering: where does this system live, what serves what, which pieces talk to each other, and what are the traps that cost hours. Read it BEFORE working on a system you have touched in past sessions - it is the difference between continuing and re-discovering. After building or changing a system, rewrite the whole map so it stays true: pass `content` and it replaces the previous version entirely (append-only maps rot). Without `content` it returns the current map.', {
664
- neuron: zod_1.z.string().describe('Neuron ID or name (e.g. "project_octochat" or "OctoChat")'),
665
- content: zod_1.z.string().optional().describe('The new map, replacing the old one whole. Omit to read. Write it as the reference you will need next time: paths, ids, what-serves-what, gotchas.'),
666
- domain: zod_1.z.string().optional().describe('Domain for the neuron if it has to be created (e.g. "proyectos-web")'),
798
+ server.registerTool('crbro_map', {
799
+ title: 'System map',
800
+ description: 'Read or replace a neuron\'s system map: ONE living document — where the system lives, what serves what, which pieces talk to each other, the traps that cost hours. Read it before working on a system touched in past sessions; after changing the system, rewrite the whole map — content replaces the previous version entirely (append-only maps rot). Omit content to read (map:null if none yet); an empty string clears it. Reading never creates a neuron, writing does. Credentials are redacted on write and listed in redacted. Atomic facts belong in crbro_learn — the map is the prose reference around them.',
801
+ inputSchema: {
802
+ neuron: zod_1.z.string().describe('Neuron id or name, e.g. "project_octochat" or "OctoChat".'),
803
+ content: zod_1.z.string().optional().describe('The new map, replacing the old one whole; omit to read. Write the reference you will need next time: paths, ids, what-serves-what, gotchas. An empty string clears the map.'),
804
+ domain: zod_1.z.string().optional().describe('Domain if the neuron has to be created, e.g. "proyectos-web". Ignored when it exists.'),
805
+ },
806
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
667
807
  }, async (args) => {
668
808
  try {
669
809
  if (args.content === undefined) {
@@ -741,11 +881,16 @@ function createServer() {
741
881
  // ═══════════════════════════════════════════════════════════════
742
882
  // TOOL 16: crbro_revise — Retire knowledge that stopped being true
743
883
  // ═══════════════════════════════════════════════════════════════
744
- server.tool('crbro_revise', 'Mark stored facts as no longer current. Use this the moment you discover something you saved is out of date or was wrong: a memory that only ever appends keeps serving the old version alongside the new one, with equal confidence. Superseded facts disappear from crbro_recall but stay in the neuron file, so nothing is lost and the correction is auditable.', {
745
- neuron: zod_1.z.string().describe('Neuron ID or name holding the facts (e.g. "project_octochat")'),
746
- facts: zod_1.z.array(zod_1.z.string()).describe('Which facts to retire: their ids, or their exact text.'),
747
- status: zod_1.z.enum(['superseded', 'retracted']).optional().describe('"superseded" = there is a newer truth (default). "retracted" = it was never true.'),
748
- note: zod_1.z.string().optional().describe('Why it stopped being true. Worth writing: the next reader will wonder.'),
884
+ server.registerTool('crbro_revise', {
885
+ title: 'Revise facts',
886
+ description: 'Mark stored facts as no longer current — the moment you find something saved is out of date or was wrong, since a memory that only appends keeps serving the old version with equal confidence. Superseded facts leave crbro_recall but stay in the neuron file, so the correction is auditable. Matches by fact id or exact text; anything reported in unmatched is STILL LIVE — fix and re-run. If a replacement fact exists, crbro_learn with supersedes does both in one call. To delete outright, use crbro_forget.',
887
+ inputSchema: {
888
+ neuron: zod_1.z.string().describe('Neuron id or name holding the facts, e.g. "project_octochat".'),
889
+ facts: zod_1.z.array(zod_1.z.string()).describe('Facts to retire: their ids (from crbro_recall) or exact text (trimmed, case-insensitive). Already-retired facts never match.'),
890
+ status: zod_1.z.enum(['superseded', 'retracted']).optional().describe('superseded = there is a newer truth (default); retracted = it was never true.'),
891
+ note: zod_1.z.string().optional().describe('Why it stopped being true. The next reader will wonder.'),
892
+ },
893
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
749
894
  }, async (args) => {
750
895
  try {
751
896
  const result = await cortex.revise(args.neuron, args.facts, {
@@ -791,25 +936,40 @@ function createServer() {
791
936
  // ═══════════════════════════════════════════════════════════════
792
937
  // TOOL 17: crbro_audit — What should not be in the brain
793
938
  // ═══════════════════════════════════════════════════════════════
794
- server.tool('crbro_audit', 'Check the brain for credentials that were stored before they could be filtered out — API keys, tokens, passwords. Reports which neurons hold them and what kind, never the values themselves. Run it once after upgrading, and any time you suspect a secret was pasted into a conversation. Use crbro_forget to remove what it finds.', {}, async () => {
939
+ server.registerTool('crbro_audit', {
940
+ title: 'Audit for credentials',
941
+ description: 'Read-only scan of every field of every neuron (facts, decisions, patterns, preferences, errors, debts, system map) for credentials stored before the filter caught them — API keys, tokens, passwords. Reports where they sit and what kind, never the values. Findings are in the search index too, so recall can return them: remove with crbro_forget, then rotate the credential. Run it after upgrading and whenever a secret may have been pasted into a conversation.',
942
+ inputSchema: {},
943
+ outputSchema: {
944
+ neurons_affected: zod_1.z.number(),
945
+ facts_affected: zod_1.z.number(),
946
+ findings: zod_1.z.array(zod_1.z.object({
947
+ neuron_id: zod_1.z.string(), name: zod_1.z.string(), kinds: zod_1.z.array(zod_1.z.string()),
948
+ facts: zod_1.z.number(), decisions: zod_1.z.number(), patterns: zod_1.z.number(), preferences: zod_1.z.number(),
949
+ errors: zod_1.z.number(), debts: zod_1.z.number(), map: zod_1.z.number(),
950
+ }).loose()),
951
+ message: zod_1.z.string(),
952
+ note: zod_1.z.string().optional(),
953
+ },
954
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
955
+ }, async () => {
795
956
  try {
796
957
  const hallazgos = await cortex.auditSecrets();
797
- const total = hallazgos.reduce((n, h) => n + h.facts + h.decisions + h.patterns + h.preferences, 0);
958
+ const total = hallazgos.reduce((n, h) => n + h.facts + h.decisions + h.patterns + h.preferences + h.errors + h.debts + h.map, 0);
959
+ const payload = {
960
+ neurons_affected: hallazgos.length,
961
+ facts_affected: total,
962
+ findings: hallazgos,
963
+ message: hallazgos.length === 0
964
+ ? 'No credentials found in the brain.'
965
+ : `${total} entr(y/ies) across ${hallazgos.length} neuron(s) contain something that looks like a credential. ` +
966
+ 'They are also inside the search index, so recall can return them. ' +
967
+ 'Remove them with crbro_forget, then rotate the credentials — assume they are compromised.',
968
+ note: 'Values are never shown here, by design.',
969
+ };
798
970
  return {
799
- content: [{
800
- type: 'text',
801
- text: JSON.stringify({
802
- neurons_affected: hallazgos.length,
803
- facts_affected: total,
804
- findings: hallazgos,
805
- message: hallazgos.length === 0
806
- ? 'No credentials found in the brain.'
807
- : `${total} fact(s) across ${hallazgos.length} neuron(s) contain something that looks like a credential. ` +
808
- 'They are also inside the search index, so recall can return them. ' +
809
- 'Remove them with crbro_forget, then rotate the credentials — assume they are compromised.',
810
- note: 'Values are never shown here, by design.',
811
- }, null, 2),
812
- }],
971
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
972
+ structuredContent: payload,
813
973
  };
814
974
  }
815
975
  catch (err) {
@@ -825,12 +985,17 @@ function createServer() {
825
985
  // ═══════════════════════════════════════════════════════════════
826
986
  // TOOL 22: crbro_secret — Credentials, brokered to the OS keychain
827
987
  // ═══════════════════════════════════════════════════════════════
828
- server.tool('crbro_secret', 'Store and read credentials in the operating system own keychain — macOS Keychain, the Linux Secret Service, or DPAPI on Windows. CRBRO keeps no copy and invents no crypto: it brokers access to the store the machine already has, outside the brain, where no sync and no team space can reach it. Use it the moment the user hands you a credential: store it here, then record only the NAME with crbro_learn, never the value. Use get when a task needs one, and do not print the value back to the user unless they asked for that specific secret. Names are SCREAMING_SNAKE_CASE, e.g. WORDPRESS_APP_PASSWORD.', {
829
- action: zod_1.z.enum(['get', 'set', 'list', 'remove', 'status'])
830
- .describe('get = read one, set = store or update one, list = names only, remove = delete one, status = which keychain this machine offers'),
831
- name: zod_1.z.string().optional().describe('Secret name in SCREAMING_SNAKE_CASE'),
832
- value: zod_1.z.string().optional().describe('The credential itself. Only for set.'),
833
- description: zod_1.z.string().optional().describe('What it is for, e.g. "WordPress example.com - REST API"'),
988
+ server.registerTool('crbro_secret', {
989
+ title: 'Keychain secret',
990
+ description: 'Credentials, brokered to the operating system\'s own keychain (macOS Keychain, Linux Secret Service, Windows DPAPI): CRBRO keeps no copy and invents no crypto, and no sync or team space can reach the store. The moment the user hands you a credential: set it here, then record only the NAME with crbro_learn. get returns the value for the task at hand — an environment variable of the same name wins, a missing secret returns found:false, not an error — and it must not be printed back unless the user asked for that secret. list returns names only; remove deletes one; status says which store this machine has. Names are SCREAMING_SNAKE_CASE; set updates an existing name in place and rejects empty values. A machine with no store is a normal status answer, not a failure — environment variables still work.',
991
+ inputSchema: {
992
+ action: zod_1.z.enum(['get', 'set', 'list', 'remove', 'status'])
993
+ .describe('get = read one, set = store or update one, list = names only, remove = delete one, status = which keychain this machine offers, or why none.'),
994
+ name: zod_1.z.string().optional().describe('SCREAMING_SNAKE_CASE, e.g. WORDPRESS_APP_PASSWORD. Required for get, set and remove.'),
995
+ value: zod_1.z.string().optional().describe('The credential itself, non-empty. Only for set.'),
996
+ description: zod_1.z.string().optional().describe('What it is for, e.g. "WordPress example.com - REST API". Only for set.'),
997
+ },
998
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
834
999
  }, async (args) => {
835
1000
  try {
836
1001
  let payload;
@@ -909,9 +1074,14 @@ function createServer() {
909
1074
  // ═══════════════════════════════════════════════════════════════
910
1075
  // TOOL 18: crbro_forget — Remove knowledge for good
911
1076
  // ═══════════════════════════════════════════════════════════════
912
- server.tool('crbro_forget', 'Permanently remove entries from a neuron. This is for things that must not exist at all — a credential, personal data, something stored by mistake. It removes facts, decisions, patterns, preferences and errors matched by id or exact text, and the system map when given its exact full text (or clear the map with crbro_map and empty content). For knowledge that merely stopped being true, use crbro_revise instead, which keeps the history. The whole neuron is copied to .quarantine/ before anything is removed, so a mistake can be undone by hand. On shared neurons the removal travels: facts retract, errors are purged, a cleared map stays cleared. Always tell the user what you are about to remove and get their agreement first.', {
913
- neuron: zod_1.z.string().describe('Neuron ID or name holding the entries'),
914
- facts: zod_1.z.array(zod_1.z.string()).describe('What to remove: fact ids, or the exact text of a fact, decision, pattern or preference'),
1077
+ server.registerTool('crbro_forget', {
1078
+ title: 'Forget for good',
1079
+ description: 'Permanently remove entries from a neuron — for what must not exist at all: a credential, personal data, something stored by mistake. Matches facts, decisions, patterns, preferences, errors and debts by id or exact text (trimmed, case-insensitive), and the system map by its exact full text. Destructive, with a net: the whole neuron is copied to .quarantine/ first (backup path returned) and the search index is updated; nothing matched returns removed:0, not an error. On shared neurons the removal travels. For knowledge that merely stopped being true use crbro_revise, which keeps the history. Tell the user what will be removed and get their agreement first; a removed credential must still be rotated — it existed on disk and in the index.',
1080
+ inputSchema: {
1081
+ neuron: zod_1.z.string().describe('Neuron id or name holding the entries.'),
1082
+ facts: zod_1.z.array(zod_1.z.string()).describe('Fact ids, or the exact text of a fact, decision, pattern, preference, error or debt. The exact full text of the map removes the map.'),
1083
+ },
1084
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
915
1085
  }, async (args) => {
916
1086
  try {
917
1087
  const r = await cortex.forget(args.neuron, args.facts);
@@ -951,12 +1121,17 @@ function createServer() {
951
1121
  // ═══════════════════════════════════════════════════════════════
952
1122
  // TOOL 19: crbro_space — Join a team's shared memory
953
1123
  // ═══════════════════════════════════════════════════════════════
954
- server.tool('crbro_space', 'Set up shared memory with teammates. A space is a private git repository holding notes about the projects you choose to share — nothing else from your brain goes near it. One person runs create with the repository URL; everyone else runs join with the same URL. After that it syncs by itself at the start and end of every session.', {
955
- action: zod_1.z.enum(['create', 'join', 'status']).describe('create = start a new space, join = enter one a teammate created, status = what you are in'),
956
- name: zod_1.z.string().optional().describe('Short name for the space, e.g. "equipo". Same on everyone\'s machine.'),
957
- remote: zod_1.z.string().optional().describe('Git URL of an EMPTY private repository, e.g. git@github.com:acme/team-memory.git'),
958
- author: zod_1.z.string().optional().describe('How your notes are signed, e.g. "ana". Lowercase, no spaces.'),
959
- branch: zod_1.z.string().optional().describe('Branch to use (default "main")'),
1124
+ server.registerTool('crbro_space', {
1125
+ title: 'Team space',
1126
+ description: 'Shared memory with teammates. A space is a private git repository holding notes about the projects you choose to share — nothing else from your brain goes near it. One person runs create with the repository URL; everyone else runs join with the same URL; afterwards it syncs by itself at boot and consolidate. Joining shares nothing by itself: put each project in with crbro_share. status lists your identity and spaces. create and join need name, remote and author, and reply ok:false with the reason when git is missing or the push/clone fails.',
1127
+ inputSchema: {
1128
+ action: zod_1.z.enum(['create', 'join', 'status']).describe('create = start a new space and push it, join = clone one a teammate created, status = your identity and the spaces you are in.'),
1129
+ name: zod_1.z.string().optional().describe('Short name, e.g. "equipo" — the same on everyone\'s machine. Required for create and join.'),
1130
+ remote: zod_1.z.string().optional().describe('Git URL of a private repository — EMPTY for create, the same URL the creator used for join. E.g. git@github.com:acme/team-memory.git.'),
1131
+ author: zod_1.z.string().optional().describe('How your notes are signed, e.g. "ana". Lowercase, no spaces. Required for create and join.'),
1132
+ branch: zod_1.z.string().optional().describe('Branch to use (default "main").'),
1133
+ },
1134
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
960
1135
  }, async (args) => {
961
1136
  try {
962
1137
  if (args.action === 'status') {
@@ -1021,10 +1196,15 @@ function createServer() {
1021
1196
  // ═══════════════════════════════════════════════════════════════
1022
1197
  // TOOL 20: crbro_share — Put one project into a space
1023
1198
  // ═══════════════════════════════════════════════════════════════
1024
- server.tool('crbro_share', 'Share one neuron with a team space. Always run it without a token first: it reports exactly what would be sent and refuses outright if it finds a credential — it will not redact and send anyway. Show the user that report and get their agreement before confirming. Once shared, everything you learn about that project flows to the team automatically; everything else in your brain stays private.', {
1025
- neuron: zod_1.z.string().describe('Neuron ID or name to share'),
1026
- space: zod_1.z.string().describe('Name of the space'),
1027
- confirm: zod_1.z.string().optional().describe('The confirm_token from the dry run. Omit it the first time.'),
1199
+ server.registerTool('crbro_share', {
1200
+ title: 'Share a project',
1201
+ description: 'Share one neuron with a team space. Always call it without confirm first: the dry run reports exactly what would be sent — ops_to_emit, skipped_preferences (preferences never leave this machine) — and refuses outright if it finds a credential (it will not redact and send anyway: crbro_forget it and rotate it). Show the user that report and get their agreement, then call again with the confirm_token; a stale token is refused. Entries go out on the next crbro_sync or consolidate, and from then on everything learned about that project flows to the team. Sharing cannot be undone.',
1202
+ inputSchema: {
1203
+ neuron: zod_1.z.string().describe('Neuron id or name to share.'),
1204
+ space: zod_1.z.string().describe('Space name, as created or joined with crbro_space.'),
1205
+ confirm: zod_1.z.string().optional().describe('The confirm_token from the dry run — returned only when no credential was found. Omit the first time.'),
1206
+ },
1207
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
1028
1208
  }, async (args) => {
1029
1209
  try {
1030
1210
  if (!args.confirm) {
@@ -1067,8 +1247,13 @@ function createServer() {
1067
1247
  // ═══════════════════════════════════════════════════════════════
1068
1248
  // TOOL 21: crbro_sync — Exchange notes with the team now
1069
1249
  // ═══════════════════════════════════════════════════════════════
1070
- server.tool('crbro_sync', 'Exchange notes with your team right now, instead of waiting for the next boot or consolidate. Pulls what everyone else recorded and sends yours. Being offline is a normal answer, not a failure: your memory works either way and pending notes go out next time.', {
1071
- space: zod_1.z.string().optional().describe('Which space. Omit to sync all of them.'),
1250
+ server.registerTool('crbro_sync', {
1251
+ title: 'Sync with the team',
1252
+ description: 'Exchange notes with the team right now instead of waiting for the next boot or consolidate, which sync on their own — use it mid-session, e.g. right after crbro_share. Pulls what everyone else recorded and pushes yours, reporting per space: state, neurons updated, new facts, teammates seen, pushed. Being offline is a normal answer, not a failure: local memory works either way and pending notes go out next time.',
1253
+ inputSchema: {
1254
+ space: zod_1.z.string().optional().describe('One space; state comes back "not_joined" if you are not in it. Omit to sync all of them.'),
1255
+ },
1256
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
1072
1257
  }, async (args) => {
1073
1258
  try {
1074
1259
  const informes = args.space