crbro-memory 1.16.0 → 2.0.1
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/README.md +57 -30
- package/dist/engine/brain.js +1 -1
- package/dist/engine/brain.js.map +1 -1
- package/dist/engine/cortex.d.ts +189 -1
- package/dist/engine/cortex.d.ts.map +1 -1
- package/dist/engine/cortex.js +639 -17
- package/dist/engine/cortex.js.map +1 -1
- package/dist/engine/hippocampus.d.ts +27 -1
- package/dist/engine/hippocampus.d.ts.map +1 -1
- package/dist/engine/hippocampus.js +51 -2
- package/dist/engine/hippocampus.js.map +1 -1
- package/dist/engine/maintenance.d.ts +49 -2
- package/dist/engine/maintenance.d.ts.map +1 -1
- package/dist/engine/maintenance.js +301 -28
- package/dist/engine/maintenance.js.map +1 -1
- package/dist/engine/prefrontal.d.ts +25 -7
- package/dist/engine/prefrontal.d.ts.map +1 -1
- package/dist/engine/prefrontal.js +55 -27
- package/dist/engine/prefrontal.js.map +1 -1
- package/dist/engine/synapses.d.ts +38 -0
- package/dist/engine/synapses.d.ts.map +1 -1
- package/dist/engine/synapses.js +116 -11
- package/dist/engine/synapses.js.map +1 -1
- package/dist/search/index.d.ts +14 -2
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +40 -6
- package/dist/search/index.js.map +1 -1
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +760 -830
- package/dist/server.js.map +1 -1
- package/dist/sync/materialize.d.ts +1 -1
- package/dist/sync/materialize.d.ts.map +1 -1
- package/dist/sync/materialize.js +40 -3
- package/dist/sync/materialize.js.map +1 -1
- package/dist/sync/ops.d.ts +9 -4
- package/dist/sync/ops.d.ts.map +1 -1
- package/dist/sync/ops.js.map +1 -1
- package/dist/sync/space.d.ts +27 -0
- package/dist/sync/space.d.ts.map +1 -1
- package/dist/sync/space.js +113 -1
- package/dist/sync/space.js.map +1 -1
- package/dist/types/index.d.ts +29 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// ─── CRBRO MCP Server ────────────────────────────────────────────
|
|
3
|
-
// Main server with
|
|
3
|
+
// Main server with the 15 tools of the 2.0 surface registered.
|
|
4
4
|
//
|
|
5
5
|
// Every tool goes through registerTool with a title, MCP annotations
|
|
6
6
|
// (readOnlyHint / destructiveHint / idempotentHint / openWorldHint) and, for
|
|
@@ -9,8 +9,15 @@
|
|
|
9
9
|
// it over its siblings, its side effects and what it returns — and nothing
|
|
10
10
|
// else: the discipline of using the memory well is said once, at boot, in
|
|
11
11
|
// memory_discipline, because these definitions are paid on every request in
|
|
12
|
-
// clients that load all tools
|
|
12
|
+
// clients that load all tools.
|
|
13
|
+
//
|
|
14
|
+
// 2.0 folded the nine read/duplicate tools of 1.x (status, neuron, neurons,
|
|
15
|
+
// hot_topics, connections, sessions, global_map, session_log, sync) into
|
|
16
|
+
// crbro_inspect, crbro_space action=sync and crbro_consolidate. RETIRED_TOOLS
|
|
17
|
+
// below is the only place the old names survive: boot serves it so a client
|
|
18
|
+
// with an old habit finds the replacement without a round trip.
|
|
13
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.RETIRED_TOOLS = void 0;
|
|
14
21
|
exports.createServer = createServer;
|
|
15
22
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
16
23
|
const zod_1 = require("zod");
|
|
@@ -27,6 +34,30 @@ const semantic_js_1 = require("./search/semantic.js");
|
|
|
27
34
|
const maintenance_js_1 = require("./engine/maintenance.js");
|
|
28
35
|
const space_js_1 = require("./sync/space.js");
|
|
29
36
|
const keychain_js_1 = require("./engine/keychain.js");
|
|
37
|
+
const fs_js_1 = require("./utils/fs.js");
|
|
38
|
+
const hash_js_1 = require("./utils/hash.js");
|
|
39
|
+
/**
|
|
40
|
+
* Old tool name → how to do the same thing on the 2.0 surface. Served by
|
|
41
|
+
* crbro_boot as `retired_tools` for the whole 2.x line, and asserted by the
|
|
42
|
+
* definitions test. Keys are the ONLY place a retired name may appear in a
|
|
43
|
+
* string the model receives.
|
|
44
|
+
*/
|
|
45
|
+
exports.RETIRED_TOOLS = {
|
|
46
|
+
crbro_status: 'crbro_inspect view=status',
|
|
47
|
+
crbro_neuron: 'crbro_inspect view=neuron neuron=<id or name>',
|
|
48
|
+
crbro_neurons: 'crbro_inspect view=neurons [domain|type|min_heat|limit|offset]',
|
|
49
|
+
crbro_hot_topics: 'crbro_inspect view=neurons (rows) and crbro_inspect view=status (hot_topics_recalculated)',
|
|
50
|
+
crbro_connections: 'crbro_inspect view=neuron neuron=<id> [min_strength]',
|
|
51
|
+
crbro_sessions: 'crbro_inspect view=sessions [limit]',
|
|
52
|
+
crbro_global_map: 'crbro_inspect view=global_map',
|
|
53
|
+
crbro_session_log: 'crbro_consolidate summary=... [topics_touched=[...] for neuron ids you only read] (logs the session) plus crbro_context set_topics=[...] if you need to replace the active topics',
|
|
54
|
+
crbro_sync: 'crbro_space action=sync [name]',
|
|
55
|
+
};
|
|
56
|
+
/** The one sentence about the lifecycle, shared by three descriptions and boot. */
|
|
57
|
+
const THREE_STAGES = 'A new truth that REPLACES an old one → crbro_learn with supersedes (one call does both). ' +
|
|
58
|
+
'Something stopped being true, or was never true, and nothing replaces it → crbro_revise ' +
|
|
59
|
+
'(kept in the file, gone from recall, reversible with status active). Something must not exist ' +
|
|
60
|
+
'on disk at all — a credential, personal data, a whole neuron → crbro_forget (quarantine copy first).';
|
|
30
61
|
/**
|
|
31
62
|
* The version of CRBRO that is actually running. The manifest carries its own
|
|
32
63
|
* version, but that one stamps the brain FORMAT and has not moved since 1.0.0
|
|
@@ -42,6 +73,17 @@ function runningVersion() {
|
|
|
42
73
|
return 'unknown';
|
|
43
74
|
}
|
|
44
75
|
}
|
|
76
|
+
function textResult(text, isError = false) {
|
|
77
|
+
return { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) };
|
|
78
|
+
}
|
|
79
|
+
function jsonResult(payload) {
|
|
80
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
81
|
+
}
|
|
82
|
+
function errorResult(where, err) {
|
|
83
|
+
return textResult(`CRBRO ${where} error: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
84
|
+
}
|
|
85
|
+
const NEURON_TYPES = ['project', 'tech', 'lang', 'person', 'domain', 'process', 'protocol'];
|
|
86
|
+
const INSPECT_VIEWS = ['status', 'neuron', 'neurons', 'sessions', 'global_map'];
|
|
45
87
|
function createServer() {
|
|
46
88
|
const server = new mcp_js_1.McpServer({
|
|
47
89
|
name: 'crbro-memory',
|
|
@@ -61,6 +103,10 @@ function createServer() {
|
|
|
61
103
|
// the miner path, which used to write straight to disk. Result on the
|
|
62
104
|
// reference brain: only 106 of 1,183 neurons were searchable.
|
|
63
105
|
cortex.setIndexer(neuron => searchEngine.indexNeuron(neuron));
|
|
106
|
+
// The mirror of the indexer: when a neuron leaves the cortex (forget entire,
|
|
107
|
+
// merge_into) its chunks leave the index too. The index is derived data, so
|
|
108
|
+
// a failure here is swallowed by the cortex like a failed reindex.
|
|
109
|
+
cortex.setRemover(id => searchEngine.removeNeuron(id));
|
|
64
110
|
// When a neuron belongs to a shared space, every write also appends a note
|
|
65
111
|
// to this machine's own log. Nobody ever writes to anyone else's file, so
|
|
66
112
|
// two people working at once have nothing to collide over.
|
|
@@ -68,12 +114,14 @@ function createServer() {
|
|
|
68
114
|
// v1.4.0: CRBRO is fully free — no license, no network calls. The former
|
|
69
115
|
// license engine (Firestore-backed freemium) lives in git history before
|
|
70
116
|
// that version if it is ever needed again.
|
|
117
|
+
/** id first, then name — the resolution every neuron-taking tool shares. */
|
|
118
|
+
const resolveNeuron = async (ref) => (await cortex.peek(ref)) || (await cortex.findByName(ref));
|
|
71
119
|
// ═══════════════════════════════════════════════════════════════
|
|
72
120
|
// TOOL 1: crbro_boot — Boot sequence
|
|
73
121
|
// ═══════════════════════════════════════════════════════════════
|
|
74
122
|
server.registerTool('crbro_boot', {
|
|
75
123
|
title: 'Boot the brain',
|
|
76
|
-
description: '
|
|
124
|
+
description: 'Read the brain at session start — call it FIRST in every conversation, before any other work; writes only the boot stamp (and the brain itself on first use). Loads 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_sessions, counts, active protocols as a protocol_enforcement block you must follow, memory_discipline (the rules for using this memory well) and retired_tools (old tool names → their replacement). Readies the search index and syncs shared team spaces (offline is normal). Skipping it loses all context; close the session with crbro_consolidate.',
|
|
77
125
|
inputSchema: {},
|
|
78
126
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
79
127
|
}, async () => {
|
|
@@ -86,6 +134,12 @@ function createServer() {
|
|
|
86
134
|
// its own and anything pending goes out on the next sync.
|
|
87
135
|
const equipos = await (0, space_js_1.syncAll)(brain, cortex, 5_000);
|
|
88
136
|
const response = { ...result };
|
|
137
|
+
// The last three real session logs, and last_session taken from them:
|
|
138
|
+
// the manifest field brain.boot reads was never written by anything,
|
|
139
|
+
// so it came back null in every session.
|
|
140
|
+
response.recent_sessions = await hippocampus.listSessions(3);
|
|
141
|
+
response.last_session = response.recent_sessions[0]?.session_id ?? result.last_session ?? null;
|
|
142
|
+
response.retired_tools = exports.RETIRED_TOOLS;
|
|
89
143
|
// Inject protocol enforcement.
|
|
90
144
|
// The instruction text goes in ONE place. It used to be emitted twice --
|
|
91
145
|
// inside active_protocols[].instructions and again here -- which on a
|
|
@@ -119,8 +173,8 @@ function createServer() {
|
|
|
119
173
|
}
|
|
120
174
|
// How to use this memory well — said once here, at boot, instead of
|
|
121
175
|
// repeated inside every tool description. Measured with a real
|
|
122
|
-
// tools/list: the
|
|
123
|
-
// in clients that load all tools; this paragraph costs ~
|
|
176
|
+
// tools/list in 1.13: the definitions cost thousands of tokens on every
|
|
177
|
+
// request in clients that load all tools; this paragraph costs ~250, once.
|
|
124
178
|
// Semantic recall is installed by init since 1.16; say so once when
|
|
125
179
|
// it is missing, so the agent can offer the one command that fixes it.
|
|
126
180
|
const sem = (0, semantic_js_1.semanticStatus)();
|
|
@@ -132,6 +186,8 @@ function createServer() {
|
|
|
132
186
|
response.memory_discipline =
|
|
133
187
|
'Before crbro_learn, crbro_recall: what you are about to save may already exist — then pass ' +
|
|
134
188
|
'supersedes instead of adding a sibling (two versions of one fact compete on recall as equals). ' +
|
|
189
|
+
'crbro_recall searches by content; to read one neuron by id or name, list neurons, sessions or ' +
|
|
190
|
+
'the global map, use crbro_inspect. ' +
|
|
135
191
|
'Structure — paths, what serves what, traps — goes in crbro_map, not in facts; anything derivable ' +
|
|
136
192
|
'from the repo or git history is not worth storing. Write facts dense and self-contained: they are ' +
|
|
137
193
|
'recalled without this conversation, and add keywords: the words a future question may use that ' +
|
|
@@ -140,70 +196,165 @@ function createServer() {
|
|
|
140
196
|
'deliberate deferral with its ceiling and revisit trigger. Credentials never go in the brain: ' +
|
|
141
197
|
'crbro_secret, then record only the NAME. Recall results carry confidence — "weak" means the match ' +
|
|
142
198
|
'covers little of the question, verify before relying on it — and when two facts disagree, prefer ' +
|
|
143
|
-
'the more recent.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
type: 'text',
|
|
147
|
-
text: JSON.stringify(response, null, 2),
|
|
148
|
-
}],
|
|
149
|
-
};
|
|
199
|
+
'the more recent. ' + THREE_STAGES + ' ' +
|
|
200
|
+
'Call crbro_consolidate before the conversation ends; it logs the session too.';
|
|
201
|
+
return jsonResult(response);
|
|
150
202
|
}
|
|
151
203
|
catch (err) {
|
|
152
|
-
return
|
|
153
|
-
content: [{
|
|
154
|
-
type: 'text',
|
|
155
|
-
text: `CRBRO boot error: ${err instanceof Error ? err.message : String(err)}`,
|
|
156
|
-
}],
|
|
157
|
-
isError: true,
|
|
158
|
-
};
|
|
204
|
+
return errorResult('boot', err);
|
|
159
205
|
}
|
|
160
206
|
});
|
|
161
207
|
// ═══════════════════════════════════════════════════════════════
|
|
162
|
-
// TOOL 2:
|
|
208
|
+
// TOOL 2: crbro_inspect — Read-only views of the brain
|
|
163
209
|
// ═══════════════════════════════════════════════════════════════
|
|
164
|
-
server.registerTool('
|
|
165
|
-
title: '
|
|
166
|
-
description: 'Read-only
|
|
167
|
-
inputSchema: {
|
|
210
|
+
server.registerTool('crbro_inspect', {
|
|
211
|
+
title: 'Inspect the brain',
|
|
212
|
+
description: 'Read-only views of the brain by id or name; to search by content use crbro_recall. Nothing is written by any view: every read leaves the brain untouched. view=status: version, brain path, totals, last boot/consolidation, semantic state, hot_topics_recalculated. view=neuron: one neuron in full — facts newest first, paged with limit/offset (superseded hidden unless include_superseded), decisions, patterns, preferences, errors, debts, entry_status, system map, and its connections resolved with name, type and strength (min_strength filters). view=neurons: rows hottest first (id, name, domain, type, heat, last_accessed, facts_count), filtered by domain, type, min_heat, paged with limit/offset. view=sessions: day logs newest first, the only place session summaries are read. view=global_map: one cluster per domain plus cross-domain bridges, computed live. Params of other views are ignored.',
|
|
213
|
+
inputSchema: {
|
|
214
|
+
view: zod_1.z.enum(INSPECT_VIEWS).describe('Which read to perform. Only the params listed for that view are honoured; the rest are ignored, never an error.'),
|
|
215
|
+
neuron: zod_1.z.string().optional().describe('view=neuron only, required there: neuron id (e.g. "project_octochat") or name (e.g. "OctoChat").'),
|
|
216
|
+
domain: zod_1.z.string().optional().describe('view=neurons: exact domain match, e.g. "proyectos-web".'),
|
|
217
|
+
type: zod_1.z.enum(NEURON_TYPES).optional().describe('view=neurons: only this neuron type.'),
|
|
218
|
+
min_heat: zod_1.z.number().min(0).max(1).optional().describe('view=neurons: minimum heat, 0.0-1.0. Heat blends access frequency, recency and connectivity.'),
|
|
219
|
+
min_strength: zod_1.z.number().min(0).max(1).optional().describe('view=neuron: drop connections weaker than this (0.0-1.0). Omit or 0 = all.'),
|
|
220
|
+
limit: zod_1.z.number().int().positive().optional().describe('Page size. view=neuron: facts per page (default 40, max 200); view=neurons: rows (default 50, max 500); view=sessions: day logs (default 10, max 100). Other views ignore it.'),
|
|
221
|
+
offset: zod_1.z.number().int().min(0).optional().describe('Items to skip. view=neuron: facts (newest first); view=neurons: rows after the heat sort. Default 0.'),
|
|
222
|
+
include_superseded: zod_1.z.boolean().optional().describe('view=neuron: also return superseded and retracted facts (default false). Retired decisions, patterns, errors and debts are always returned, with entry_status saying which are retired.'),
|
|
223
|
+
},
|
|
168
224
|
outputSchema: {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
225
|
+
view: zod_1.z.enum(INSPECT_VIEWS),
|
|
226
|
+
status: zod_1.z.object({
|
|
227
|
+
crbro_version: zod_1.z.string(),
|
|
228
|
+
brain_format: zod_1.z.string().optional(),
|
|
229
|
+
total_neurons: zod_1.z.number(),
|
|
230
|
+
total_synapses: zod_1.z.number(),
|
|
231
|
+
total_sessions: zod_1.z.number(),
|
|
232
|
+
brain_path: zod_1.z.string().optional(),
|
|
233
|
+
last_boot: zod_1.z.string().nullable().optional(),
|
|
234
|
+
last_consolidation: zod_1.z.string().nullable().optional(),
|
|
235
|
+
semantic: zod_1.z.object({ installed: zod_1.z.boolean(), enabled: zod_1.z.boolean(), mode: zod_1.z.string(), model_downloaded: zod_1.z.boolean(), home: zod_1.z.string(), model: zod_1.z.string() }).optional(),
|
|
236
|
+
hot_topics_recalculated: zod_1.z.string().nullable(),
|
|
237
|
+
}).optional(),
|
|
238
|
+
neuron: zod_1.z.object({}).loose().optional(),
|
|
239
|
+
neurons: zod_1.z.object({
|
|
240
|
+
total: zod_1.z.number(),
|
|
241
|
+
offset: zod_1.z.number(),
|
|
242
|
+
neurons: zod_1.z.array(zod_1.z.object({
|
|
243
|
+
id: zod_1.z.string(), name: zod_1.z.string(), domain: zod_1.z.string(), type: zod_1.z.string(),
|
|
244
|
+
heat: zod_1.z.number(), last_accessed: zod_1.z.string(), facts_count: zod_1.z.number(),
|
|
245
|
+
}).loose()),
|
|
246
|
+
}).optional(),
|
|
247
|
+
sessions: zod_1.z.object({
|
|
248
|
+
total: zod_1.z.number(),
|
|
249
|
+
sessions: zod_1.z.array(zod_1.z.object({
|
|
250
|
+
session_id: zod_1.z.string().optional(), date: zod_1.z.string().optional(), summary: zod_1.z.string().optional(),
|
|
251
|
+
topics_touched: zod_1.z.array(zod_1.z.string()).optional(),
|
|
252
|
+
key_facts_added: zod_1.z.number().optional(), decisions_made: zod_1.z.number().optional(),
|
|
253
|
+
}).loose()),
|
|
254
|
+
}).optional(),
|
|
255
|
+
global_map: zod_1.z.object({
|
|
256
|
+
total_clusters: zod_1.z.number(),
|
|
257
|
+
total_bridges: zod_1.z.number(),
|
|
258
|
+
computed_at: zod_1.z.string(),
|
|
259
|
+
clusters: zod_1.z.array(zod_1.z.object({}).loose()),
|
|
260
|
+
bridges: zod_1.z.array(zod_1.z.object({}).loose()),
|
|
261
|
+
}).optional(),
|
|
178
262
|
},
|
|
179
263
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
180
|
-
}, async () => {
|
|
264
|
+
}, async (args) => {
|
|
181
265
|
try {
|
|
182
|
-
const
|
|
183
|
-
const payload = {
|
|
184
|
-
crbro_version: runningVersion(),
|
|
185
|
-
brain_format: manifest.version,
|
|
186
|
-
total_neurons: manifest.total_neurons,
|
|
187
|
-
total_synapses: manifest.total_synapses,
|
|
188
|
-
total_sessions: manifest.total_sessions,
|
|
189
|
-
brain_path: manifest.brain_path,
|
|
190
|
-
last_boot: manifest.last_boot,
|
|
191
|
-
last_consolidation: manifest.last_consolidation,
|
|
192
|
-
semantic: (0, semantic_js_1.semanticStatus)(),
|
|
193
|
-
};
|
|
194
|
-
return {
|
|
266
|
+
const done = (payload) => ({
|
|
195
267
|
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
196
|
-
structuredContent: payload,
|
|
197
|
-
};
|
|
268
|
+
structuredContent: { view: args.view, [args.view]: payload },
|
|
269
|
+
});
|
|
270
|
+
if (args.view === 'status') {
|
|
271
|
+
const manifest = await brain.getManifest();
|
|
272
|
+
const hot = await (0, fs_js_1.readJSON)(brain.paths.hotTopics());
|
|
273
|
+
return done({
|
|
274
|
+
crbro_version: runningVersion(),
|
|
275
|
+
brain_format: manifest.version,
|
|
276
|
+
total_neurons: manifest.total_neurons,
|
|
277
|
+
total_synapses: manifest.total_synapses,
|
|
278
|
+
total_sessions: manifest.total_sessions,
|
|
279
|
+
brain_path: manifest.brain_path,
|
|
280
|
+
last_boot: manifest.last_boot,
|
|
281
|
+
last_consolidation: manifest.last_consolidation,
|
|
282
|
+
semantic: (0, semantic_js_1.semanticStatus)(),
|
|
283
|
+
hot_topics_recalculated: hot?.last_recalculated ?? null,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if (args.view === 'neuron') {
|
|
287
|
+
if (!args.neuron) {
|
|
288
|
+
return textResult('view=neuron needs `neuron`: a neuron id or name.', true);
|
|
289
|
+
}
|
|
290
|
+
// peek, not get: a tool that declares readOnlyHint must not write.
|
|
291
|
+
// get() stamps last_accessed and access_count, which made this view
|
|
292
|
+
// contradict its own annotation. Heat keeps its frequency signal from
|
|
293
|
+
// the write paths (learn, map), and crbro_recall — the read an agent
|
|
294
|
+
// actually makes — never bumped either.
|
|
295
|
+
let neuron = await cortex.peek(args.neuron);
|
|
296
|
+
if (!neuron) {
|
|
297
|
+
const found = await cortex.findByName(args.neuron);
|
|
298
|
+
if (found)
|
|
299
|
+
neuron = await cortex.peek(found.id);
|
|
300
|
+
}
|
|
301
|
+
if (!neuron) {
|
|
302
|
+
return textResult(`Neuron not found: "${args.neuron}". Find the id with crbro_recall or crbro_inspect view=neurons.`, true);
|
|
303
|
+
}
|
|
304
|
+
// A whole neuron can be enormous - the biggest on the reference brain
|
|
305
|
+
// serialises to 528,836 characters, more than most models can hold - so
|
|
306
|
+
// facts are paged instead of dumped.
|
|
307
|
+
const limit = Math.min(Math.max(args.limit ?? 40, 1), 200);
|
|
308
|
+
const offset = Math.max(args.offset ?? 0, 0);
|
|
309
|
+
const visible = (neuron.facts || []).filter(f => args.include_superseded ? true : (f.status !== 'superseded' && f.status !== 'retracted'));
|
|
310
|
+
const ordered = [...visible].sort((a, b) => String(b.added || '').localeCompare(String(a.added || '')));
|
|
311
|
+
const page = ordered.slice(offset, offset + limit);
|
|
312
|
+
const connections = await synapses.getConnections(neuron.id, args.min_strength);
|
|
313
|
+
return done({
|
|
314
|
+
...neuron,
|
|
315
|
+
connection_ids: neuron.connections || [],
|
|
316
|
+
connections,
|
|
317
|
+
total_connections: connections.length,
|
|
318
|
+
facts: page,
|
|
319
|
+
facts_pagination: {
|
|
320
|
+
total: ordered.length,
|
|
321
|
+
returned: page.length,
|
|
322
|
+
offset,
|
|
323
|
+
has_more: offset + page.length < ordered.length,
|
|
324
|
+
order: 'newest first',
|
|
325
|
+
hidden_superseded: (neuron.facts || []).length - visible.length,
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (args.view === 'neurons') {
|
|
330
|
+
const limit = Math.min(Math.max(args.limit ?? 50, 1), 500);
|
|
331
|
+
const offset = Math.max(args.offset ?? 0, 0);
|
|
332
|
+
const rows = await cortex.list({
|
|
333
|
+
domain: args.domain,
|
|
334
|
+
type: args.type,
|
|
335
|
+
min_heat: args.min_heat,
|
|
336
|
+
limit,
|
|
337
|
+
offset,
|
|
338
|
+
});
|
|
339
|
+
return done({ total: rows.length, offset, neurons: rows });
|
|
340
|
+
}
|
|
341
|
+
if (args.view === 'sessions') {
|
|
342
|
+
const limit = Math.min(Math.max(args.limit ?? 10, 1), 100);
|
|
343
|
+
const sessions = await hippocampus.listSessions(limit);
|
|
344
|
+
return done({ total: sessions.length, sessions });
|
|
345
|
+
}
|
|
346
|
+
// view === 'global_map': computed live, never cached, nothing written.
|
|
347
|
+
const globalMap = await prefrontal.getGlobalMap();
|
|
348
|
+
return done({
|
|
349
|
+
total_clusters: globalMap.clusters.length,
|
|
350
|
+
total_bridges: globalMap.bridges.length,
|
|
351
|
+
computed_at: globalMap.last_rebuilt,
|
|
352
|
+
clusters: globalMap.clusters,
|
|
353
|
+
bridges: globalMap.bridges,
|
|
354
|
+
});
|
|
198
355
|
}
|
|
199
356
|
catch (err) {
|
|
200
|
-
return
|
|
201
|
-
content: [{
|
|
202
|
-
type: 'text',
|
|
203
|
-
text: `CRBRO status error: ${err instanceof Error ? err.message : String(err)}`,
|
|
204
|
-
}],
|
|
205
|
-
isError: true,
|
|
206
|
-
};
|
|
357
|
+
return errorResult('inspect', err);
|
|
207
358
|
}
|
|
208
359
|
});
|
|
209
360
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -211,17 +362,18 @@ function createServer() {
|
|
|
211
362
|
// ═══════════════════════════════════════════════════════════════
|
|
212
363
|
server.registerTool('crbro_learn', {
|
|
213
364
|
title: 'Learn something',
|
|
214
|
-
description: '
|
|
365
|
+
description: 'Write: store a fact, decision, pattern, preference, error or debt on a topic; the neuron is created if missing (or pass neuron_id). Stage 1 of the lifecycle: a new truth that REPLACES an old one → crbro_learn with supersedes (one call does both); to retire with no replacement use crbro_revise; to delete from disk use crbro_forget. crbro_recall first — it may already exist. The same fact text again is not duplicated: keywords merge (or keywords_replace) and a changed confidence applies (updated_in_place); text matching a retired fact or entry is refused with skipped_retired. Decisions always append; preferences never leave this machine. Credentials are replaced with a marker and listed in redacted — crbro_secret them, record only the name. Returns neuron_id, action, superseded count, near_duplicates (stored anyway; retire the old telling), supersedes_unmatched (still live) and totals.',
|
|
215
366
|
inputSchema: {
|
|
216
367
|
topic: zod_1.z.string().describe('Topic name, e.g. "OctoChat", "Firebase", "SEO Strategy".'),
|
|
217
368
|
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."'),
|
|
218
369
|
content: zod_1.z.string().describe('The knowledge itself. Dense and self-contained: it is recalled without this conversation as context.'),
|
|
219
|
-
confidence: zod_1.z.number().min(0).max(1).optional().describe('0.0-1.0, default 1.0. Facts only.'),
|
|
220
|
-
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".'),
|
|
370
|
+
confidence: zod_1.z.number().min(0).max(1).optional().describe('0.0-1.0, default 1.0. Facts only. On an exact-duplicate active fact the stored confidence is updated to this value (updated_in_place:true).'),
|
|
371
|
+
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" (crbro_revise domain replaces it unconditionally).'),
|
|
221
372
|
rationale: zod_1.z.string().optional().describe('Why the decision was taken. Stored and indexed with it; ignored for other types.'),
|
|
222
373
|
neuron_id: zod_1.z.string().optional().describe('Exact neuron id from crbro_recall, e.g. "project_octochat". Skips name matching entirely.'),
|
|
223
374
|
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.'),
|
|
224
375
|
keywords: zod_1.z.array(zod_1.z.string()).optional().describe('Facts only. 2-5 words a future question may use that the text does not contain: synonyms, the other language, the generic name of the product named. Indexed with the fact, never shown. The same text again with new keywords merges them.'),
|
|
376
|
+
keywords_replace: zod_1.z.boolean().optional().describe('When the exact fact text already exists, replace its stored keywords with `keywords` instead of merging (default false). Teammates in a shared space only ever receive the union.'),
|
|
225
377
|
},
|
|
226
378
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
227
379
|
}, async (args) => {
|
|
@@ -233,187 +385,78 @@ function createServer() {
|
|
|
233
385
|
neuronId: args.neuron_id,
|
|
234
386
|
supersedes: args.supersedes,
|
|
235
387
|
keys: args.keywords,
|
|
388
|
+
keysReplace: args.keywords_replace,
|
|
236
389
|
});
|
|
237
390
|
// Indexing happens inside cortex.learn, through the indexer hook.
|
|
391
|
+
if (result.action === 'skipped_retired' && result.skipped_retired) {
|
|
392
|
+
const r = result.skipped_retired;
|
|
393
|
+
return jsonResult({
|
|
394
|
+
neuron_id: result.neuron?.id ?? null,
|
|
395
|
+
action: 'skipped_retired',
|
|
396
|
+
skipped_retired: r,
|
|
397
|
+
message: `Not stored: this text matches a ${r.status} entry (${r.id}${r.revised ? `, revised ${r.revised}` : ''}). ` +
|
|
398
|
+
(r.note ? `Note: ${r.note}. ` : '') +
|
|
399
|
+
'If it is true again, reactivate it with crbro_revise status active; if you meant a different fact, reword it.',
|
|
400
|
+
});
|
|
401
|
+
}
|
|
238
402
|
// `neuron` is only null when the caller asked not to create one,
|
|
239
403
|
// which the MCP path never does. Guard anyway so the types stay honest.
|
|
240
404
|
if (!result.neuron) {
|
|
241
|
-
return {
|
|
242
|
-
content: [{
|
|
243
|
-
type: 'text',
|
|
244
|
-
text: `No neuron matched "${args.topic}" and none was created.`,
|
|
245
|
-
}],
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
return {
|
|
249
|
-
content: [{
|
|
250
|
-
type: 'text',
|
|
251
|
-
text: JSON.stringify({
|
|
252
|
-
neuron_id: result.neuron.id,
|
|
253
|
-
action: result.action,
|
|
254
|
-
superseded_facts: result.superseded,
|
|
255
|
-
near_duplicates: result.near_duplicates.length > 0
|
|
256
|
-
? result.near_duplicates
|
|
257
|
-
: undefined,
|
|
258
|
-
near_duplicates_warning: result.near_duplicates.length > 0
|
|
259
|
-
? 'Stored, but this closely resembles the fact(s) listed above. If this is ' +
|
|
260
|
-
'a newer telling of the same thing, retire the old one: pass its id in ' +
|
|
261
|
-
'`supersedes` next time, or crbro_revise it now. Two versions of one fact ' +
|
|
262
|
-
'keep competing on recall as equals.'
|
|
263
|
-
: undefined,
|
|
264
|
-
supersedes_unmatched: result.supersedes_unmatched.length > 0
|
|
265
|
-
? result.supersedes_unmatched
|
|
266
|
-
: undefined,
|
|
267
|
-
supersedes_warning: result.supersedes_unmatched.length > 0
|
|
268
|
-
? 'These supersedes targets matched NO active fact — the old version is ' +
|
|
269
|
-
'still live and will keep appearing on recall. Find its id with ' +
|
|
270
|
-
'crbro_recall and retire it with crbro_revise.'
|
|
271
|
-
: undefined,
|
|
272
|
-
redacted: result.redacted.length > 0 ? result.redacted : undefined,
|
|
273
|
-
redaction_note: result.redacted.length > 0
|
|
274
|
-
? `Stored, but ${result.redacted.length} credential(s) were replaced with a marker: ` +
|
|
275
|
-
`${result.redacted.join(', ')}. The sentence around them was kept. ` +
|
|
276
|
-
'Do not try to store the value again. Offer the user crbro_secret instead: ' +
|
|
277
|
-
'it puts the credential in the OS keychain, and then you record only its name here.'
|
|
278
|
-
: undefined,
|
|
279
|
-
total_facts: result.neuron.facts.length,
|
|
280
|
-
total_decisions: result.neuron.decisions.length,
|
|
281
|
-
total_patterns: result.neuron.patterns.length,
|
|
282
|
-
message: result.action === 'created'
|
|
283
|
-
? `New neuron "${result.neuron.name}" created in ${result.neuron.domain}`
|
|
284
|
-
: `Updated neuron "${result.neuron.name}" — ${args.type} added` +
|
|
285
|
-
(result.superseded > 0 ? `, ${result.superseded} older fact(s) superseded` : ''),
|
|
286
|
-
}, null, 2),
|
|
287
|
-
}],
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
catch (err) {
|
|
291
|
-
return {
|
|
292
|
-
content: [{
|
|
293
|
-
type: 'text',
|
|
294
|
-
text: `CRBRO learn error: ${err instanceof Error ? err.message : String(err)}`,
|
|
295
|
-
}],
|
|
296
|
-
isError: true,
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
// ═══════════════════════════════════════════════════════════════
|
|
301
|
-
// TOOL 4: crbro_neuron — Read a specific neuron
|
|
302
|
-
// ═══════════════════════════════════════════════════════════════
|
|
303
|
-
server.registerTool('crbro_neuron', {
|
|
304
|
-
title: 'Read a neuron',
|
|
305
|
-
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.',
|
|
306
|
-
inputSchema: {
|
|
307
|
-
id: zod_1.z.string().describe('Neuron id (e.g. "project_octochat") or name (e.g. "OctoChat").'),
|
|
308
|
-
limit: zod_1.z.number().optional().describe('Facts to return: default 40, max 200.'),
|
|
309
|
-
offset: zod_1.z.number().optional().describe('Facts to skip. Facts come newest first.'),
|
|
310
|
-
include_superseded: zod_1.z.boolean().optional().describe('Also return superseded and retracted facts (default false).'),
|
|
311
|
-
},
|
|
312
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
313
|
-
}, async (args) => {
|
|
314
|
-
try { // Try by ID first, then by name
|
|
315
|
-
let neuron = await cortex.get(args.id);
|
|
316
|
-
if (!neuron) {
|
|
317
|
-
neuron = await cortex.findByName(args.id);
|
|
318
|
-
if (neuron) {
|
|
319
|
-
// Touch the found neuron
|
|
320
|
-
neuron = await cortex.get(neuron.id);
|
|
321
|
-
}
|
|
405
|
+
return textResult(`No neuron matched "${args.topic}" and none was created.`);
|
|
322
406
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
});
|
|
367
|
-
// ═══════════════════════════════════════════════════════════════
|
|
368
|
-
// TOOL 5: crbro_neurons — List neurons
|
|
369
|
-
// ═══════════════════════════════════════════════════════════════
|
|
370
|
-
server.registerTool('crbro_neurons', {
|
|
371
|
-
title: 'List neurons',
|
|
372
|
-
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.',
|
|
373
|
-
inputSchema: {
|
|
374
|
-
domain: zod_1.z.string().optional().describe('Only this domain, e.g. "proyectos-web".'),
|
|
375
|
-
type: zod_1.z.enum(['project', 'tech', 'lang', 'person', 'domain', 'process', 'protocol']).optional().describe('Only this neuron type.'),
|
|
376
|
-
min_heat: zod_1.z.number().optional().describe('Minimum heat, 0.0-1.0. Heat blends access frequency, recency and connectivity.'),
|
|
377
|
-
limit: zod_1.z.number().optional().describe('Max rows (default 50).'),
|
|
378
|
-
},
|
|
379
|
-
outputSchema: {
|
|
380
|
-
total: zod_1.z.number(),
|
|
381
|
-
neurons: zod_1.z.array(zod_1.z.object({
|
|
382
|
-
id: zod_1.z.string(), name: zod_1.z.string(), domain: zod_1.z.string(), type: zod_1.z.string(),
|
|
383
|
-
heat: zod_1.z.number(), last_accessed: zod_1.z.string(), facts_count: zod_1.z.number(),
|
|
384
|
-
}).loose()),
|
|
385
|
-
},
|
|
386
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
387
|
-
}, async (args) => {
|
|
388
|
-
try {
|
|
389
|
-
const neurons = await cortex.list({
|
|
390
|
-
domain: args.domain,
|
|
391
|
-
type: args.type,
|
|
392
|
-
min_heat: args.min_heat,
|
|
393
|
-
limit: args.limit,
|
|
407
|
+
return jsonResult({
|
|
408
|
+
neuron_id: result.neuron.id,
|
|
409
|
+
action: result.action,
|
|
410
|
+
duplicate: result.duplicate || undefined,
|
|
411
|
+
updated_in_place: result.updated_in_place || undefined,
|
|
412
|
+
superseded_facts: result.superseded,
|
|
413
|
+
near_duplicates: result.near_duplicates.length > 0
|
|
414
|
+
? result.near_duplicates
|
|
415
|
+
: undefined,
|
|
416
|
+
near_duplicates_warning: result.near_duplicates.length > 0
|
|
417
|
+
? 'Stored, but this closely resembles the fact(s) listed above. If this is ' +
|
|
418
|
+
'a newer telling of the same thing, retire the old one: pass its id in ' +
|
|
419
|
+
'`supersedes` next time, or crbro_revise it now. Two versions of one fact ' +
|
|
420
|
+
'keep competing on recall as equals.'
|
|
421
|
+
: undefined,
|
|
422
|
+
supersedes_unmatched: result.supersedes_unmatched.length > 0
|
|
423
|
+
? result.supersedes_unmatched
|
|
424
|
+
: undefined,
|
|
425
|
+
supersedes_warning: result.supersedes_unmatched.length > 0
|
|
426
|
+
? 'These supersedes targets matched NO active fact — the old version is ' +
|
|
427
|
+
'still live and will keep appearing on recall. Find its id with ' +
|
|
428
|
+
'crbro_recall and retire it with crbro_revise.'
|
|
429
|
+
: undefined,
|
|
430
|
+
redacted: result.redacted.length > 0 ? result.redacted : undefined,
|
|
431
|
+
redaction_note: result.redacted.length > 0
|
|
432
|
+
? `Stored, but ${result.redacted.length} credential(s) were replaced with a marker: ` +
|
|
433
|
+
`${result.redacted.join(', ')}. The sentence around them was kept. ` +
|
|
434
|
+
'Do not try to store the value again. Offer the user crbro_secret instead: ' +
|
|
435
|
+
'it puts the credential in the OS keychain, and then you record only its name here.'
|
|
436
|
+
: undefined,
|
|
437
|
+
total_facts: result.neuron.facts.length,
|
|
438
|
+
total_decisions: result.neuron.decisions.length,
|
|
439
|
+
total_patterns: result.neuron.patterns.length,
|
|
440
|
+
message: result.action === 'created'
|
|
441
|
+
? `New neuron "${result.neuron.name}" created in ${result.neuron.domain}`
|
|
442
|
+
: result.action === 'skipped'
|
|
443
|
+
? (result.updated_in_place
|
|
444
|
+
? `"${result.neuron.name}" already held this ${args.type}; its confidence/keywords were updated in place.`
|
|
445
|
+
: `"${result.neuron.name}" already held this ${args.type} verbatim; nothing added.`)
|
|
446
|
+
: `Updated neuron "${result.neuron.name}" — ${args.type} added` +
|
|
447
|
+
(result.superseded > 0 ? `, ${result.superseded} older fact(s) superseded` : ''),
|
|
394
448
|
});
|
|
395
|
-
const payload = { total: neurons.length, neurons };
|
|
396
|
-
return {
|
|
397
|
-
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
398
|
-
structuredContent: payload,
|
|
399
|
-
};
|
|
400
449
|
}
|
|
401
450
|
catch (err) {
|
|
402
|
-
return
|
|
403
|
-
content: [{
|
|
404
|
-
type: 'text',
|
|
405
|
-
text: `CRBRO neurons error: ${err instanceof Error ? err.message : String(err)}`,
|
|
406
|
-
}],
|
|
407
|
-
isError: true,
|
|
408
|
-
};
|
|
451
|
+
return errorResult('learn', err);
|
|
409
452
|
}
|
|
410
453
|
});
|
|
411
454
|
// ═══════════════════════════════════════════════════════════════
|
|
412
|
-
// TOOL
|
|
455
|
+
// TOOL 4: crbro_recall — Search the brain
|
|
413
456
|
// ═══════════════════════════════════════════════════════════════
|
|
414
457
|
server.registerTool('crbro_recall', {
|
|
415
458
|
title: 'Recall',
|
|
416
|
-
description: '
|
|
459
|
+
description: 'Read-only search of everything saved in earlier sessions — the full text of facts, decisions, patterns, preferences, errors, debts and maps, not just topic names; to read one neuron by id or name use crbro_inspect view=neuron. One result per neuron: its best matching chunk with matched_kind and matched_added, a confidence label (weak = little of the question covered; verify first), plus also_matched. Retired facts and entries never surface. Call it before asking the user what they may already have told you, and before crbro_learn. If nothing matches, retry with 2-4 phrasings in queries or fewer, distinctive words. has_map:true: read the system map with crbro_map before touching that system.',
|
|
417
460
|
inputSchema: {
|
|
418
461
|
query: zod_1.z.string().describe('What to look for, e.g. "Firebase authentication setup". Fewer, distinctive terms beat full sentences.'),
|
|
419
462
|
queries: zod_1.z.array(zod_1.z.string()).optional().describe('Alternative phrasings of the same question, searched together with query and fused by rank. Use synonyms, the other language and the concrete product name; 2-4 is plenty.'),
|
|
@@ -447,7 +490,7 @@ function createServer() {
|
|
|
447
490
|
results,
|
|
448
491
|
hint: results.length === 0
|
|
449
492
|
? 'Nothing matched. Try fewer, more distinctive words - names, ids, filenames - rather than a full sentence.'
|
|
450
|
-
: '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.',
|
|
493
|
+
: '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. To read the whole neuron: crbro_inspect view=neuron.',
|
|
451
494
|
};
|
|
452
495
|
return {
|
|
453
496
|
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
@@ -455,506 +498,471 @@ function createServer() {
|
|
|
455
498
|
};
|
|
456
499
|
}
|
|
457
500
|
catch (err) {
|
|
458
|
-
return
|
|
459
|
-
content: [{
|
|
460
|
-
type: 'text',
|
|
461
|
-
text: `CRBRO recall error: ${err instanceof Error ? err.message : String(err)}`,
|
|
462
|
-
}],
|
|
463
|
-
isError: true,
|
|
464
|
-
};
|
|
501
|
+
return errorResult('recall', err);
|
|
465
502
|
}
|
|
466
503
|
});
|
|
467
504
|
// ═══════════════════════════════════════════════════════════════
|
|
468
|
-
// TOOL
|
|
505
|
+
// TOOL 5: crbro_revise — Retire, reactivate, edit metadata
|
|
469
506
|
// ═══════════════════════════════════════════════════════════════
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
507
|
+
const reviseSchema = zod_1.z.object({
|
|
508
|
+
neuron: zod_1.z.string().describe('Neuron id or name holding what to revise, e.g. "project_octochat".'),
|
|
509
|
+
facts: zod_1.z.array(zod_1.z.string()).optional().describe('Facts to move to `status`: their ids (from crbro_recall) or exact text (trimmed, case-insensitive). For superseded/retracted only active facts match; for active only retired ones do.'),
|
|
510
|
+
entries: zod_1.z.array(zod_1.z.string()).optional().describe('Exact texts of decisions, patterns, errors or debts to move to `status`. Retired entries stay in the file (entry_status) but leave recall like a superseded fact.'),
|
|
511
|
+
status: zod_1.z.enum(['superseded', 'retracted', 'active']).optional().describe('superseded = a newer truth exists (default); retracted = it was never true; active = reactivate a retired fact or entry. Reactivation is local: on a shared neuron the next sync re-applies the retirement (the response carries shared_warning).'),
|
|
512
|
+
note: zod_1.z.string().optional().describe('Why. Stored as revision_note on facts and entry_status.note on entries. The next reader will wonder.'),
|
|
513
|
+
summary: zod_1.z.string().optional().describe('Replace the neuron summary. Credentials are redacted and listed in redacted.'),
|
|
514
|
+
domain: zod_1.z.string().optional().describe('Replace the neuron domain unconditionally, e.g. "proyectos-web".'),
|
|
515
|
+
tags: zod_1.z.array(zod_1.z.string()).optional().describe('Replace the WHOLE tag list (trimmed, deduplicated). On protocol neurons re-send the priority: and source: tags or they are gone.'),
|
|
516
|
+
name: zod_1.z.string().optional().describe('Rename the neuron. Its id, file, synapses and shared state stay the same.'),
|
|
517
|
+
}).superRefine((v, ctx) => {
|
|
518
|
+
const any = [v.facts, v.entries, v.summary, v.domain, v.tags, v.name].some(x => x !== undefined);
|
|
519
|
+
if (!any) {
|
|
520
|
+
ctx.addIssue({
|
|
521
|
+
code: 'custom',
|
|
522
|
+
message: 'Nothing to revise: pass facts, entries, or a metadata field (summary, domain, tags, name).',
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
server.registerTool('crbro_revise', {
|
|
527
|
+
title: 'Revise a neuron',
|
|
528
|
+
description: 'Write: change what a neuron says without deleting anything. Stage 2 of the lifecycle: something stopped being true, or was never true, and nothing replaces it → crbro_revise (kept in the file, gone from recall, reversible with status active). If a replacement exists, crbro_learn with supersedes does both; for what must not exist on disk use crbro_forget. facts retires facts by id or exact text; entries retires decisions, patterns, errors and debts by exact text; status active reactivates either (local only on a shared neuron: the next sync re-applies the retirement, shared_warning says so). summary, domain, tags and name edit metadata in the same call (tags replaces the whole list; the id never changes). Anything in unmatched is STILL LIVE — fix and re-run.',
|
|
529
|
+
inputSchema: reviseSchema,
|
|
530
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
480
531
|
}, async (args) => {
|
|
481
532
|
try {
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
533
|
+
const target = await resolveNeuron(args.neuron);
|
|
534
|
+
if (!target) {
|
|
535
|
+
return textResult(`Neuron not found: "${args.neuron}". Use crbro_recall to find the right neuron id first.`);
|
|
536
|
+
}
|
|
537
|
+
const status = args.status || 'superseded';
|
|
538
|
+
let revisedFacts = 0;
|
|
539
|
+
let revisedEntries = 0;
|
|
540
|
+
const unmatched = [];
|
|
541
|
+
let changed = [];
|
|
542
|
+
let redacted = [];
|
|
543
|
+
if (args.facts && args.facts.length > 0) {
|
|
544
|
+
const r = await cortex.revise(target.id, args.facts, { status, note: args.note });
|
|
545
|
+
revisedFacts = r.revised;
|
|
546
|
+
unmatched.push(...r.unmatched);
|
|
547
|
+
}
|
|
548
|
+
if (args.entries && args.entries.length > 0) {
|
|
549
|
+
const r = await cortex.retireEntries(target.id, args.entries, { status, note: args.note });
|
|
550
|
+
revisedEntries = r.revised;
|
|
551
|
+
unmatched.push(...r.unmatched);
|
|
552
|
+
}
|
|
553
|
+
const meta = { summary: args.summary, domain: args.domain, tags: args.tags, name: args.name };
|
|
554
|
+
if (Object.values(meta).some(v => v !== undefined)) {
|
|
555
|
+
const r = await cortex.setMeta(target.id, meta);
|
|
556
|
+
changed = r.changed;
|
|
557
|
+
redacted = r.redacted;
|
|
558
|
+
}
|
|
559
|
+
const shared = (await (0, space_js_1.sharedMap)(brain))[target.id];
|
|
560
|
+
const sharedWarning = status === 'active' && shared
|
|
561
|
+
? `"${target.id}" is shared in space "${shared}": reactivation is local only — the retirement comes back from the shared log on the next sync.`
|
|
562
|
+
: undefined;
|
|
563
|
+
const parts = [];
|
|
564
|
+
if (revisedFacts > 0)
|
|
565
|
+
parts.push(`${revisedFacts} fact(s) ${status === 'active' ? 'reactivated' : 'retired'}`);
|
|
566
|
+
if (revisedEntries > 0)
|
|
567
|
+
parts.push(`${revisedEntries} entr(y/ies) ${status === 'active' ? 'reactivated' : 'retired'}`);
|
|
568
|
+
if (changed.length > 0)
|
|
569
|
+
parts.push(`${changed.join(', ')} updated`);
|
|
570
|
+
const touchedTargets = (args.facts?.length ?? 0) + (args.entries?.length ?? 0);
|
|
571
|
+
return jsonResult({
|
|
572
|
+
neuron_id: target.id,
|
|
573
|
+
revised_facts: revisedFacts,
|
|
574
|
+
revised_entries: revisedEntries,
|
|
575
|
+
unmatched: unmatched.length > 0 ? unmatched : undefined,
|
|
576
|
+
changed,
|
|
577
|
+
status,
|
|
578
|
+
shared_warning: sharedWarning,
|
|
579
|
+
redacted: redacted.length > 0 ? redacted : undefined,
|
|
580
|
+
message: parts.length > 0
|
|
581
|
+
? `${parts.join('; ')} in "${target.name}".` +
|
|
582
|
+
(status !== 'active' && (revisedFacts + revisedEntries) > 0 ? ' They no longer appear in recall.' : '') +
|
|
583
|
+
(unmatched.length > 0 ? ` WARNING: ${unmatched.length} target(s) matched nothing and are unchanged.` : '')
|
|
584
|
+
: touchedTargets > 0
|
|
585
|
+
? 'Nothing matched. Pass the fact id from crbro_recall, or the exact text; for status active only retired items match.'
|
|
586
|
+
: 'Nothing changed: the metadata already had those values.',
|
|
587
|
+
});
|
|
496
588
|
}
|
|
497
589
|
catch (err) {
|
|
498
|
-
return
|
|
499
|
-
content: [{
|
|
500
|
-
type: 'text',
|
|
501
|
-
text: `CRBRO connect error: ${err instanceof Error ? err.message : String(err)}`,
|
|
502
|
-
}],
|
|
503
|
-
isError: true,
|
|
504
|
-
};
|
|
590
|
+
return errorResult('revise', err);
|
|
505
591
|
}
|
|
506
592
|
});
|
|
507
593
|
// ═══════════════════════════════════════════════════════════════
|
|
508
|
-
// TOOL
|
|
594
|
+
// TOOL 6: crbro_forget — Remove from disk, five modes
|
|
509
595
|
// ═══════════════════════════════════════════════════════════════
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
532
|
-
structuredContent: payload,
|
|
533
|
-
};
|
|
596
|
+
const forgetSchema = zod_1.z.object({
|
|
597
|
+
neuron: zod_1.z.string().optional().describe('Neuron id or name the mode acts on. Required for every mode except session. restore needs the exact neuron id.'),
|
|
598
|
+
facts: zod_1.z.array(zod_1.z.string()).optional().describe('Mode facts: 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. Deleted for good after a quarantine copy; decision/pattern removals travel to shared spaces like errors and debts.'),
|
|
599
|
+
entire: zod_1.z.boolean().optional().describe('Mode entire: delete the whole neuron and its synapses. Without confirm_token it is a dry run — { neuron_id, dry_run:true, counts, shared_in, confirm_token }. Refused (no token) while the neuron is shared: crbro_share unshare first.'),
|
|
600
|
+
confirm_token: zod_1.z.string().optional().describe('Only with entire:true — the token from the dry run. Derived from the neuron\'s counts, so it goes stale (and is refused) when the neuron changed in between.'),
|
|
601
|
+
restore: zod_1.z.boolean().optional().describe('Mode restore: bring back the newest quarantine copy of `neuron` (exact id). If the neuron exists again, the copy is merged into it (merged_into_existing:true, moved counts). The quarantine file stays, so restore is repeatable.'),
|
|
602
|
+
merge_into: zod_1.z.string().optional().describe('Mode merge_into: target neuron id or name. Everything of `neuron` is unioned into it, synapses rewired, then `neuron` is deleted (quarantined first). Refused while `neuron` is shared.'),
|
|
603
|
+
session: zod_1.z.string().optional().describe('Mode session: session id ("session_2026-09-03" or "2026-09-03") whose log is deleted after a quarantine copy. `neuron` is not needed.'),
|
|
604
|
+
}).superRefine((v, ctx) => {
|
|
605
|
+
const modes = [
|
|
606
|
+
v.facts !== undefined ? 'facts' : null,
|
|
607
|
+
v.entire ? 'entire' : null,
|
|
608
|
+
v.restore ? 'restore' : null,
|
|
609
|
+
v.merge_into !== undefined ? 'merge_into' : null,
|
|
610
|
+
v.session !== undefined ? 'session' : null,
|
|
611
|
+
].filter((m) => m !== null);
|
|
612
|
+
if (modes.length !== 1) {
|
|
613
|
+
ctx.addIssue({
|
|
614
|
+
code: 'custom',
|
|
615
|
+
message: `Pass exactly ONE mode: facts (entries of a neuron), entire (whole neuron, two steps), restore (from quarantine), merge_into (another neuron) or session (a day log). Got ${modes.length === 0 ? 'none' : modes.join(' + ')}. confirm_token is only valid with entire.`,
|
|
616
|
+
});
|
|
534
617
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}],
|
|
541
|
-
isError: true,
|
|
542
|
-
};
|
|
618
|
+
if (v.confirm_token !== undefined && !v.entire) {
|
|
619
|
+
ctx.addIssue({ code: 'custom', path: ['confirm_token'], message: 'confirm_token is only valid with entire:true.' });
|
|
620
|
+
}
|
|
621
|
+
if (v.session === undefined && !v.neuron) {
|
|
622
|
+
ctx.addIssue({ code: 'custom', path: ['neuron'], message: 'neuron (id or name) is required for every mode except session.' });
|
|
543
623
|
}
|
|
544
624
|
});
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
inputSchema: {
|
|
552
|
-
summary: zod_1.z.string().describe('What happened in this session. Appended if today already has an entry.'),
|
|
553
|
-
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.'),
|
|
554
|
-
key_facts_added: zod_1.z.number().optional().describe('New facts stored. Summed into the day total on same-day calls.'),
|
|
555
|
-
decisions_made: zod_1.z.number().optional().describe('Decisions recorded. Summed into the day total on same-day calls.'),
|
|
556
|
-
},
|
|
557
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
625
|
+
const forgetToken = (n) => (0, hash_js_1.contentHash)(`forget:${n.id}:${(n.facts || []).length}:${(n.decisions || []).length}:${(n.patterns || []).length}:${(n.connections || []).length}`, 8);
|
|
626
|
+
server.registerTool('crbro_forget', {
|
|
627
|
+
title: 'Forget for good',
|
|
628
|
+
description: 'Write, destructive: remove from disk after a quarantine copy (backup returned). Stage 3 of the lifecycle: something must not exist on disk at all — a credential, personal data, a whole neuron → crbro_forget; for knowledge that merely stopped being true use crbro_revise, which keeps the history. One mode per call. facts: delete entries of a neuron (facts, decisions, patterns, preferences, errors, debts, the map) by id or exact text. entire: delete the whole neuron and its synapses — call it without confirm_token first: the dry run reports what would happen and returns confirm_token. Show the user, get agreement, call again with the token; a stale token is refused. restore: bring back the newest quarantine copy. merge_into: union a neuron into another, rewire synapses, delete the source. session: delete one day\'s log. entire and merge_into refuse a shared neuron — crbro_share unshare first. A removed credential must still be rotated.',
|
|
629
|
+
inputSchema: forgetSchema,
|
|
630
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
558
631
|
}, async (args) => {
|
|
559
632
|
try {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
633
|
+
// ── session ──
|
|
634
|
+
if (args.session !== undefined) {
|
|
635
|
+
const r = await hippocampus.forgetSession(args.session);
|
|
636
|
+
return jsonResult({
|
|
637
|
+
...r,
|
|
638
|
+
message: r.removed
|
|
639
|
+
? `Session log ${r.session_id} deleted. A copy is in ${r.backup}.`
|
|
640
|
+
: `No session log called ${r.session_id}. List them with crbro_inspect view=sessions.`,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
const ref = args.neuron;
|
|
644
|
+
// ── facts ──
|
|
645
|
+
if (args.facts !== undefined) {
|
|
646
|
+
const r = await cortex.forget(ref, args.facts);
|
|
647
|
+
if (!r.neuron_id) {
|
|
648
|
+
return textResult(`Neuron not found: "${ref}". Use crbro_recall or crbro_audit to find the right one.`);
|
|
649
|
+
}
|
|
650
|
+
return jsonResult({
|
|
651
|
+
neuron_id: r.neuron_id,
|
|
652
|
+
removed: r.removed,
|
|
653
|
+
backup: r.backup,
|
|
654
|
+
message: r.removed > 0
|
|
655
|
+
? `${r.removed} entr(y/ies) removed from "${r.neuron_id}". A copy of the neuron as it was is in ${r.backup}. ` +
|
|
656
|
+
'If any of them was a credential, rotate it: it existed on disk and in the index.'
|
|
657
|
+
: 'Nothing matched. Pass the fact id from crbro_recall, or its exact text.',
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
// ── restore ──
|
|
661
|
+
if (args.restore) {
|
|
662
|
+
const r = await cortex.restoreNeuron(ref);
|
|
663
|
+
if (!r.neuron) {
|
|
664
|
+
return textResult(`No quarantine copy found for neuron id "${ref}". restore needs the exact id the neuron had; names are not resolved here.`);
|
|
665
|
+
}
|
|
666
|
+
return jsonResult({
|
|
667
|
+
neuron_id: r.neuron.id,
|
|
668
|
+
restored_from: r.restored_from,
|
|
669
|
+
merged_into_existing: r.merged_into_existing,
|
|
670
|
+
moved: r.moved,
|
|
671
|
+
message: r.merged_into_existing
|
|
672
|
+
? `"${r.neuron.name}" existed again, so the quarantine copy was merged into it (see moved). Synapses were not restored: reconnect with crbro_connect if needed.`
|
|
673
|
+
: `"${r.neuron.name}" restored from ${r.restored_from}. Synapses were not restored: reconnect with crbro_connect if needed.`,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
// ── merge_into ──
|
|
677
|
+
if (args.merge_into !== undefined) {
|
|
678
|
+
const from = await resolveNeuron(ref);
|
|
679
|
+
if (!from)
|
|
680
|
+
return textResult(`Neuron not found: "${ref}".`);
|
|
681
|
+
const into = await resolveNeuron(args.merge_into);
|
|
682
|
+
if (!into)
|
|
683
|
+
return textResult(`Target neuron not found: "${args.merge_into}".`);
|
|
684
|
+
if (from.id === into.id) {
|
|
685
|
+
return textResult(`"${ref}" and "${args.merge_into}" are the same neuron (${from.id}); nothing to merge.`);
|
|
686
|
+
}
|
|
687
|
+
const shared = (await (0, space_js_1.sharedMap)(brain))[from.id];
|
|
688
|
+
if (shared) {
|
|
689
|
+
return textResult(`Refused: "${from.id}" is shared in space "${shared}" and would be re-created by the next sync. ` +
|
|
690
|
+
'Take it out first with crbro_share unshare:true, then merge.', true);
|
|
691
|
+
}
|
|
692
|
+
const r = await cortex.mergeNeurons(from.id, into.id);
|
|
693
|
+
if (!r.from || !r.into || !r.moved) {
|
|
694
|
+
return textResult(`Merge did not run: ${!r.from ? `source "${ref}"` : `target "${args.merge_into}"`} could not be resolved.`, true);
|
|
695
|
+
}
|
|
696
|
+
const rewired = await synapses.rewire(from.id, into.id);
|
|
697
|
+
return jsonResult({
|
|
698
|
+
from: r.from,
|
|
699
|
+
into: r.into,
|
|
700
|
+
backup: r.backup,
|
|
701
|
+
moved: r.moved,
|
|
702
|
+
synapses: rewired,
|
|
703
|
+
message: `"${from.name}" merged into "${into.name}" and deleted; a copy is in ${r.backup}. ` +
|
|
704
|
+
`Synapses: ${rewired.moved} moved, ${rewired.merged} merged, ${rewired.dropped} dropped. ` +
|
|
705
|
+
`Undo with crbro_forget restore:true neuron="${from.id}".`,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
// ── entire (two steps) ──
|
|
709
|
+
const target = await resolveNeuron(ref);
|
|
710
|
+
if (!target)
|
|
711
|
+
return textResult(`Neuron not found: "${ref}".`);
|
|
712
|
+
const shared = (await (0, space_js_1.sharedMap)(brain))[target.id];
|
|
713
|
+
const counts = {
|
|
714
|
+
facts: (target.facts || []).length,
|
|
715
|
+
decisions: (target.decisions || []).length,
|
|
716
|
+
patterns: (target.patterns || []).length,
|
|
717
|
+
preferences: (target.preferences || []).length,
|
|
718
|
+
errors: (target.errors || []).length,
|
|
719
|
+
debts: (target.debts || []).length,
|
|
720
|
+
connections: (target.connections || []).length,
|
|
579
721
|
};
|
|
722
|
+
const token = forgetToken(target);
|
|
723
|
+
if (!args.confirm_token) {
|
|
724
|
+
return jsonResult({
|
|
725
|
+
neuron_id: target.id,
|
|
726
|
+
name: target.name,
|
|
727
|
+
dry_run: true,
|
|
728
|
+
counts,
|
|
729
|
+
shared_in: shared ?? null,
|
|
730
|
+
confirm_token: shared ? undefined : token,
|
|
731
|
+
message: shared
|
|
732
|
+
? `Refused: "${target.id}" is shared in space "${shared}" and would be re-created by the next sync. ` +
|
|
733
|
+
'Take it out first with crbro_share unshare:true, then call again.'
|
|
734
|
+
: `Dry run: deleting "${target.name}" removes ${counts.facts} fact(s), ${counts.decisions} decision(s), ` +
|
|
735
|
+
`${counts.patterns} pattern(s), ${counts.preferences} preference(s), ${counts.errors} error(s), ${counts.debts} debt(s) ` +
|
|
736
|
+
`and ${counts.connections} synapse(s). A quarantine copy is kept (restore:true brings it back). ` +
|
|
737
|
+
'Show this to the user; with their agreement call again with confirm_token.',
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
if (shared) {
|
|
741
|
+
return textResult(`Refused: "${target.id}" is shared in space "${shared}". Take it out first with crbro_share unshare:true.`, true);
|
|
742
|
+
}
|
|
743
|
+
if (args.confirm_token !== token) {
|
|
744
|
+
return textResult(`Stale confirm_token: "${target.id}" changed since the dry run. Call again without the token to get a fresh one.`, true);
|
|
745
|
+
}
|
|
746
|
+
const r = await cortex.forgetNeuron(target.id);
|
|
747
|
+
if (!r.neuron_id)
|
|
748
|
+
return textResult(`Neuron not found: "${ref}".`);
|
|
749
|
+
const synapsesRemoved = await synapses.removeAllFor(target.id);
|
|
750
|
+
return jsonResult({
|
|
751
|
+
neuron_id: r.neuron_id,
|
|
752
|
+
removed: 'neuron',
|
|
753
|
+
backup: r.backup,
|
|
754
|
+
counts: r.counts,
|
|
755
|
+
synapses_removed: synapsesRemoved,
|
|
756
|
+
message: `"${target.name}" deleted with ${synapsesRemoved} synapse(s). A copy is in ${r.backup}; ` +
|
|
757
|
+
`crbro_forget restore:true neuron="${target.id}" brings it back. If it held a credential, rotate it.`,
|
|
758
|
+
});
|
|
580
759
|
}
|
|
581
760
|
catch (err) {
|
|
582
|
-
return
|
|
583
|
-
content: [{
|
|
584
|
-
type: 'text',
|
|
585
|
-
text: `CRBRO session_log error: ${err instanceof Error ? err.message : String(err)}`,
|
|
586
|
-
}],
|
|
587
|
-
isError: true,
|
|
588
|
-
};
|
|
761
|
+
return errorResult('forget', err);
|
|
589
762
|
}
|
|
590
763
|
});
|
|
591
764
|
// ═══════════════════════════════════════════════════════════════
|
|
592
|
-
// TOOL
|
|
765
|
+
// TOOL 7: crbro_connect — Create, strengthen or delete a synapse
|
|
593
766
|
// ═══════════════════════════════════════════════════════════════
|
|
594
|
-
server.registerTool('
|
|
595
|
-
title: '
|
|
596
|
-
description: '
|
|
767
|
+
server.registerTool('crbro_connect', {
|
|
768
|
+
title: 'Connect two neurons',
|
|
769
|
+
description: 'Write: create, strengthen or delete the undirected synapse between two neurons; both ids are validated. action=connect (default) creates at strength 0.5 and adds +0.1 per repeat call (cap 1.0), or sets the absolute strength you pass; action=disconnect deletes the synapse and unlinks both neurons — the destructive side. Idle synapses decay and crbro_maintenance prunes the weak; crbro_consolidate links neurons written in the same session by itself, so use this for relationships beyond co-occurrence. To read connections use crbro_inspect view=neuron. Returns synapse_id, action (created|strengthened|disconnected|absent) and strength.',
|
|
597
770
|
inputSchema: {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
topics_touched: zod_1.z.array(zod_1.z.string()).optional(),
|
|
605
|
-
key_facts_added: zod_1.z.number().optional(), decisions_made: zod_1.z.number().optional(),
|
|
606
|
-
}).loose()),
|
|
771
|
+
action: zod_1.z.enum(['connect', 'disconnect']).optional().describe('connect = create or strengthen (default); disconnect = delete the synapse and unlink both neurons. An absent synapse returns action:absent, removed:false, not an error.'),
|
|
772
|
+
from: zod_1.z.string().describe('Exact neuron id, e.g. "project_octochat". Validated: an unknown id is an error.'),
|
|
773
|
+
to: zod_1.z.string().describe('Exact neuron id. Order does not matter — (a,b) and (b,a) are the same synapse.'),
|
|
774
|
+
type: zod_1.z.enum(['dependency', 'causal', 'temporal', 'conceptual', 'hierarchy', 'alternative']).optional().describe('Relationship kind, used only when the synapse is created (default conceptual). Ignored on strengthen and on disconnect.'),
|
|
775
|
+
strength: zod_1.z.number().min(0).max(1).optional().describe('Absolute strength 0.0-1.0 to set, on create or on an existing synapse, instead of the 0.5 / +0.1 rule.'),
|
|
776
|
+
context: zod_1.z.string().optional().describe('One line on the relationship. On strengthen it replaces the stored text; omit to keep it.'),
|
|
607
777
|
},
|
|
608
|
-
annotations: { readOnlyHint:
|
|
778
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
609
779
|
}, async (args) => {
|
|
610
780
|
try {
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
781
|
+
for (const id of [args.from, args.to]) {
|
|
782
|
+
if (!(await cortex.peek(id))) {
|
|
783
|
+
return textResult(`Unknown neuron: ${id}. Pass exact ids — find them with crbro_recall or crbro_inspect view=neurons.`, true);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (args.action === 'disconnect') {
|
|
787
|
+
const r = await synapses.disconnect(args.from, args.to);
|
|
788
|
+
return jsonResult({
|
|
789
|
+
synapse_id: r.synapse_id,
|
|
790
|
+
action: r.removed ? 'disconnected' : 'absent',
|
|
791
|
+
removed: r.removed,
|
|
792
|
+
message: r.removed
|
|
793
|
+
? `Synapse removed: ${args.from} ↔ ${args.to}. Both neurons no longer list each other.`
|
|
794
|
+
: `No synapse between ${args.from} and ${args.to}; nothing to remove.`,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
const type = args.type ?? 'conceptual';
|
|
798
|
+
const result = await synapses.connect(args.from, args.to, type, args.context, { strength: args.strength });
|
|
799
|
+
return jsonResult({
|
|
800
|
+
synapse_id: result.synapse.id,
|
|
801
|
+
action: result.action,
|
|
802
|
+
strength: result.synapse.strength,
|
|
803
|
+
message: result.action === 'created'
|
|
804
|
+
? `New synapse: ${args.from} ↔ ${args.to} (${type}) at strength ${result.synapse.strength}`
|
|
805
|
+
: `Synapse strengthened: ${args.from} ↔ ${args.to} → strength ${result.synapse.strength}`,
|
|
806
|
+
});
|
|
617
807
|
}
|
|
618
808
|
catch (err) {
|
|
619
|
-
return
|
|
620
|
-
content: [{
|
|
621
|
-
type: 'text',
|
|
622
|
-
text: `CRBRO sessions error: ${err instanceof Error ? err.message : String(err)}`,
|
|
623
|
-
}],
|
|
624
|
-
isError: true,
|
|
625
|
-
};
|
|
809
|
+
return errorResult('connect', err);
|
|
626
810
|
}
|
|
627
811
|
});
|
|
628
812
|
// ═══════════════════════════════════════════════════════════════
|
|
629
|
-
// TOOL
|
|
813
|
+
// TOOL 8: crbro_context — Active context
|
|
630
814
|
// ═══════════════════════════════════════════════════════════════
|
|
631
815
|
server.registerTool('crbro_context', {
|
|
632
816
|
title: 'Working context',
|
|
633
|
-
description: 'Read or
|
|
817
|
+
description: 'Read or write the working context: active topics, open items, recently closed, last session. Called with no arguments it only reads (written:false, nothing touched); any argument writes and returns the full state plus resolved and discarded. Close items as soon as they are done — resolve_pending records them in recently_closed, discard_pending drops one without recording it, clear empties everything — because an item left open is repeated back to the user in later sessions long after it was finished. Sessions are logged by crbro_consolidate, not here.',
|
|
634
818
|
inputSchema: {
|
|
635
|
-
set_topics: zod_1.z.array(zod_1.z.string()).optional().describe('Replace the whole active-topics list with these neuron ids (no merge).
|
|
819
|
+
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_consolidate also rewrites it from the session.'),
|
|
636
820
|
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.'),
|
|
637
821
|
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.'),
|
|
822
|
+
discard_pending: zod_1.z.string().optional().describe('Drop an open item by id or 8+ characters of its text WITHOUT recording it as done (it never appears in recently_closed). Same matcher as resolve_pending; matches come back in discarded.'),
|
|
823
|
+
clear: zod_1.z.boolean().optional().describe('Empty active_topics, pending_tasks and recently_closed. Runs before the other updates in the same call.'),
|
|
638
824
|
},
|
|
639
|
-
|
|
825
|
+
// clear empties the whole working context and discard_pending drops an
|
|
826
|
+
// item without recording it: destructive, whatever the common path does.
|
|
827
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
640
828
|
}, async (args) => {
|
|
641
829
|
try {
|
|
642
830
|
const ctx = await prefrontal.updateContext({
|
|
643
831
|
set_topics: args.set_topics,
|
|
644
832
|
add_pending: args.add_pending,
|
|
645
833
|
resolve_pending: args.resolve_pending,
|
|
834
|
+
discard_pending: args.discard_pending,
|
|
835
|
+
clear: args.clear,
|
|
646
836
|
});
|
|
647
|
-
return
|
|
648
|
-
content: [{
|
|
649
|
-
type: 'text',
|
|
650
|
-
text: JSON.stringify(ctx, null, 2),
|
|
651
|
-
}],
|
|
652
|
-
};
|
|
837
|
+
return jsonResult(ctx);
|
|
653
838
|
}
|
|
654
839
|
catch (err) {
|
|
655
|
-
return
|
|
656
|
-
content: [{
|
|
657
|
-
type: 'text',
|
|
658
|
-
text: `CRBRO context error: ${err instanceof Error ? err.message : String(err)}`,
|
|
659
|
-
}],
|
|
660
|
-
isError: true,
|
|
661
|
-
};
|
|
840
|
+
return errorResult('context', err);
|
|
662
841
|
}
|
|
663
842
|
});
|
|
664
843
|
// ═══════════════════════════════════════════════════════════════
|
|
665
|
-
// TOOL
|
|
844
|
+
// TOOL 9: crbro_map — The living map of a system
|
|
666
845
|
// ═══════════════════════════════════════════════════════════════
|
|
667
|
-
server.registerTool('
|
|
668
|
-
title: '
|
|
669
|
-
description: '
|
|
670
|
-
inputSchema: {
|
|
671
|
-
limit: zod_1.z.number().optional().describe('Topics to return (default 15; the cache never holds more than 20).'),
|
|
672
|
-
},
|
|
673
|
-
outputSchema: {
|
|
674
|
-
topics: zod_1.z.array(zod_1.z.object({
|
|
675
|
-
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(),
|
|
676
|
-
}).loose()),
|
|
677
|
-
last_recalculated: zod_1.z.string().optional(),
|
|
678
|
-
},
|
|
679
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
680
|
-
}, async (args) => {
|
|
681
|
-
try {
|
|
682
|
-
const hotTopics = await prefrontal.getHotTopics(args.limit);
|
|
683
|
-
const payload = { ...hotTopics };
|
|
684
|
-
return {
|
|
685
|
-
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
686
|
-
structuredContent: payload,
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
catch (err) {
|
|
690
|
-
return {
|
|
691
|
-
content: [{
|
|
692
|
-
type: 'text',
|
|
693
|
-
text: `CRBRO hot_topics error: ${err instanceof Error ? err.message : String(err)}`,
|
|
694
|
-
}],
|
|
695
|
-
isError: true,
|
|
696
|
-
};
|
|
697
|
-
}
|
|
698
|
-
});
|
|
699
|
-
// ═══════════════════════════════════════════════════════════════
|
|
700
|
-
// TOOL 13: crbro_global_map — Global neural map
|
|
701
|
-
// ═══════════════════════════════════════════════════════════════
|
|
702
|
-
server.registerTool('crbro_global_map', {
|
|
703
|
-
title: 'Global map',
|
|
704
|
-
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.',
|
|
705
|
-
inputSchema: {
|
|
706
|
-
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.'),
|
|
707
|
-
},
|
|
708
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
709
|
-
}, async (args) => {
|
|
710
|
-
try {
|
|
711
|
-
const globalMap = await prefrontal.getGlobalMap(args.rebuild);
|
|
712
|
-
return {
|
|
713
|
-
content: [{
|
|
714
|
-
type: 'text',
|
|
715
|
-
text: JSON.stringify({
|
|
716
|
-
total_clusters: globalMap.clusters.length,
|
|
717
|
-
total_bridges: globalMap.bridges.length,
|
|
718
|
-
last_rebuilt: globalMap.last_rebuilt,
|
|
719
|
-
clusters: globalMap.clusters,
|
|
720
|
-
bridges: globalMap.bridges,
|
|
721
|
-
}, null, 2),
|
|
722
|
-
}],
|
|
723
|
-
};
|
|
724
|
-
}
|
|
725
|
-
catch (err) {
|
|
726
|
-
return {
|
|
727
|
-
content: [{
|
|
728
|
-
type: 'text',
|
|
729
|
-
text: `CRBRO global_map error: ${err instanceof Error ? err.message : String(err)}`,
|
|
730
|
-
}],
|
|
731
|
-
isError: true,
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
});
|
|
735
|
-
// ═══════════════════════════════════════════════════════════════
|
|
736
|
-
// TOOL 14: crbro_maintenance — Run maintenance
|
|
737
|
-
// ═══════════════════════════════════════════════════════════════
|
|
738
|
-
server.registerTool('crbro_maintenance', {
|
|
739
|
-
title: 'Brain maintenance',
|
|
740
|
-
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.',
|
|
846
|
+
server.registerTool('crbro_map', {
|
|
847
|
+
title: 'System map',
|
|
848
|
+
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. crbro_inspect view=neuron already returns the map; use crbro_map to read it alone, or to rewrite it. Omit content to read (map:null if none); content replaces the previous version entirely (append-only maps rot); an empty string clears it. Read it before working on a system touched in past sessions; after changing the system rewrite the whole map. Reading never creates a neuron, writing does. Credentials are redacted on write. Atomic facts belong in crbro_learn — the map is the prose around them.',
|
|
741
849
|
inputSchema: {
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
850
|
+
neuron: zod_1.z.string().describe('Neuron id or name, e.g. "project_octochat" or "OctoChat".'),
|
|
851
|
+
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.'),
|
|
852
|
+
domain: zod_1.z.string().optional().describe('Domain if the neuron has to be created, e.g. "proyectos-web". Ignored when it exists.'),
|
|
745
853
|
},
|
|
746
854
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
747
855
|
}, async (args) => {
|
|
748
856
|
try {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
857
|
+
if (args.content === undefined) {
|
|
858
|
+
const neuron = await resolveNeuron(args.neuron);
|
|
859
|
+
if (!neuron) {
|
|
860
|
+
return textResult(`Neuron not found: "${args.neuron}". Use crbro_recall to find the right neuron id first.`);
|
|
861
|
+
}
|
|
862
|
+
if (!neuron.map || !neuron.map.text) {
|
|
863
|
+
return jsonResult({
|
|
864
|
+
neuron_id: neuron.id,
|
|
865
|
+
map: null,
|
|
866
|
+
message: `"${neuron.name}" has no system map yet. After working on this system, write one with crbro_map + content: where it lives, what serves what, the traps.`,
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
return jsonResult({
|
|
870
|
+
neuron_id: neuron.id,
|
|
871
|
+
updated: neuron.map.updated,
|
|
872
|
+
by: neuron.map.by,
|
|
873
|
+
map: neuron.map.text,
|
|
874
|
+
hint: 'If anything here proved wrong or the system changed, rewrite the map before closing the task.',
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
const result = await cortex.setMap(args.neuron, args.content, {
|
|
878
|
+
domain: args.domain,
|
|
879
|
+
});
|
|
880
|
+
if (!result.neuron) {
|
|
881
|
+
return textResult(`Could not store the map for "${args.neuron}".`);
|
|
882
|
+
}
|
|
883
|
+
return jsonResult({
|
|
884
|
+
neuron_id: result.neuron.id,
|
|
885
|
+
action: result.action,
|
|
886
|
+
updated: result.neuron.map?.updated,
|
|
887
|
+
length: args.content.length,
|
|
888
|
+
redacted: result.redacted.length > 0 ? result.redacted : undefined,
|
|
889
|
+
message: args.content.trim() === ''
|
|
890
|
+
? `System map of "${result.neuron.name}" cleared.`
|
|
891
|
+
: `System map of "${result.neuron.name}" replaced. The previous version is gone - this one is now the reference.`,
|
|
892
|
+
});
|
|
759
893
|
}
|
|
760
894
|
catch (err) {
|
|
761
|
-
return
|
|
762
|
-
content: [{
|
|
763
|
-
type: 'text',
|
|
764
|
-
text: `CRBRO maintenance error: ${err instanceof Error ? err.message : String(err)}`,
|
|
765
|
-
}],
|
|
766
|
-
isError: true,
|
|
767
|
-
};
|
|
895
|
+
return errorResult('map', err);
|
|
768
896
|
}
|
|
769
897
|
});
|
|
770
898
|
// ═══════════════════════════════════════════════════════════════
|
|
771
|
-
// TOOL
|
|
899
|
+
// TOOL 10: crbro_consolidate — End-of-session consolidation
|
|
772
900
|
// ═══════════════════════════════════════════════════════════════
|
|
773
901
|
server.registerTool('crbro_consolidate', {
|
|
774
902
|
title: 'Consolidate the session',
|
|
775
|
-
description: '
|
|
903
|
+
description: 'Write: close the session — the only way to log a session. Call it before the conversation ends. Persists pending knowledge and index writes, logs the session from summary (credentials stripped, kinds in redacted), sets the context\'s last_session, 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). Returns session_id, facts_saved, decisions_saved, topics_touched and per-space sync state; topics_touched logs neurons you only read. Not consolidating loses the session\'s knowledge. Mid-session open items go to crbro_context; housekeeping is crbro_maintenance.',
|
|
776
904
|
inputSchema: {
|
|
777
|
-
summary: zod_1.z.string().describe('What was accomplished: concrete work, decisions, outcomes. Stored
|
|
905
|
+
summary: zod_1.z.string().describe('What was accomplished: concrete work, decisions, outcomes. Stored (after credential redaction) as the session log later sessions read.'),
|
|
906
|
+
topics_touched: zod_1.z.array(zod_1.z.string()).optional().describe('Neuron ids this session used WITHOUT writing (recalled, inspected, discussed). Added to the log\'s topics_touched next to the ids written this session; write counters stay real. Unknown ids are dropped and listed in topics_unknown.'),
|
|
778
907
|
},
|
|
779
908
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
780
909
|
}, async (args) => {
|
|
781
910
|
try {
|
|
782
|
-
const result = await maintenance.consolidate(args.summary);
|
|
911
|
+
const result = await maintenance.consolidate(args.summary, { topicsTouched: args.topics_touched });
|
|
783
912
|
// Flush any index writes still sitting in the debounce window, so a
|
|
784
913
|
// session that ends right after a learn does not lose it.
|
|
785
914
|
await searchEngine.flush();
|
|
786
915
|
// Send the session's notes to the team before the lights go out.
|
|
787
916
|
const compartidos = await (0, space_js_1.syncAll)(brain, cortex, 10_000);
|
|
788
|
-
return {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
: undefined,
|
|
796
|
-
message: 'Session consolidated. Brain state persisted.',
|
|
797
|
-
}, null, 2),
|
|
798
|
-
}],
|
|
799
|
-
};
|
|
917
|
+
return jsonResult({
|
|
918
|
+
...result,
|
|
919
|
+
shared_spaces: compartidos.length > 0
|
|
920
|
+
? compartidos.map(c => ({ space: c.space, state: c.state, pushed: c.pushed }))
|
|
921
|
+
: undefined,
|
|
922
|
+
message: 'Session consolidated. Brain state persisted.',
|
|
923
|
+
});
|
|
800
924
|
}
|
|
801
925
|
catch (err) {
|
|
802
|
-
return
|
|
803
|
-
content: [{
|
|
804
|
-
type: 'text',
|
|
805
|
-
text: `CRBRO consolidate error: ${err instanceof Error ? err.message : String(err)}`,
|
|
806
|
-
}],
|
|
807
|
-
isError: true,
|
|
808
|
-
};
|
|
926
|
+
return errorResult('consolidate', err);
|
|
809
927
|
}
|
|
810
928
|
});
|
|
811
929
|
// ═══════════════════════════════════════════════════════════════
|
|
812
|
-
// TOOL:
|
|
930
|
+
// TOOL 11: crbro_maintenance — Run maintenance
|
|
813
931
|
// ═══════════════════════════════════════════════════════════════
|
|
814
|
-
server.registerTool('
|
|
815
|
-
title: '
|
|
816
|
-
description: '
|
|
932
|
+
server.registerTool('crbro_maintenance', {
|
|
933
|
+
title: 'Brain maintenance',
|
|
934
|
+
description: 'Write: brain housekeeping — recalculate heat, prune weak synapses, check integrity, rebuild the search index. Returns a report (counts, integrity_issues, repairable, notes) and flags debts without a revisit trigger. dry_run:true writes nothing at all (the global map is computed live, never cached). Extras are OFF unless asked: archive cold neurons (on a mature brain most look cold, and archived ones stop being searchable), unarchive them back, purge_boilerplate left by early miners, repair what the integrity check found. For session close use crbro_consolidate; to only read the brain use crbro_inspect.',
|
|
817
935
|
inputSchema: {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
936
|
+
dry_run: zod_1.z.boolean().optional().describe('true = report only: no heat recalc, archiving, unarchiving, purge, repair, lock sweep, pruning or index rebuild, and no file written. Counts, debts and integrity checks still run.'),
|
|
937
|
+
archive: zod_1.z.boolean().optional().describe('Also move cold neurons (heat < 0.05, untouched 90+ days) out of the cortex into archives/. Off by default; run dry_run first and read archivable_neurons. Undo with unarchive.'),
|
|
938
|
+
unarchive: zod_1.z.union([zod_1.z.array(zod_1.z.string()), zod_1.z.literal('all')]).optional().describe('Move these neuron ids (or "all") from archives/ back into the cortex and reindex them. Off in dry_run; the report says archives_count and unarchived_neurons; unknown ids are listed in notes.'),
|
|
939
|
+
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.'),
|
|
940
|
+
repair: zod_1.z.boolean().optional().describe('Fix what the integrity check found: dangling connection ids, synapse files pointing at missing neurons, entry_dates/entry_status keys with no live entry, manifest counters. Off in dry_run; the report lists repairs[] one line each.'),
|
|
821
941
|
},
|
|
822
942
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
823
943
|
}, async (args) => {
|
|
824
944
|
try {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
type: 'text',
|
|
831
|
-
text: `Neuron not found: "${args.neuron}". Use crbro_recall to find the right neuron_id first.`,
|
|
832
|
-
}],
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
if (!neuron.map || !neuron.map.text) {
|
|
836
|
-
return {
|
|
837
|
-
content: [{
|
|
838
|
-
type: 'text',
|
|
839
|
-
text: JSON.stringify({
|
|
840
|
-
neuron_id: neuron.id,
|
|
841
|
-
map: null,
|
|
842
|
-
message: `"${neuron.name}" has no system map yet. After working on this system, write one with crbro_map + content: where it lives, what serves what, the traps.`,
|
|
843
|
-
}, null, 2),
|
|
844
|
-
}],
|
|
845
|
-
};
|
|
846
|
-
}
|
|
847
|
-
return {
|
|
848
|
-
content: [{
|
|
849
|
-
type: 'text',
|
|
850
|
-
text: JSON.stringify({
|
|
851
|
-
neuron_id: neuron.id,
|
|
852
|
-
updated: neuron.map.updated,
|
|
853
|
-
by: neuron.map.by,
|
|
854
|
-
map: neuron.map.text,
|
|
855
|
-
hint: 'If anything here proved wrong or the system changed, rewrite the map before closing the task.',
|
|
856
|
-
}, null, 2),
|
|
857
|
-
}],
|
|
858
|
-
};
|
|
859
|
-
}
|
|
860
|
-
const result = await cortex.setMap(args.neuron, args.content, {
|
|
861
|
-
domain: args.domain,
|
|
945
|
+
const report = await maintenance.run(args.dry_run, {
|
|
946
|
+
archive: args.archive,
|
|
947
|
+
purgeBoilerplate: args.purge_boilerplate,
|
|
948
|
+
repair: args.repair,
|
|
949
|
+
unarchive: args.unarchive,
|
|
862
950
|
});
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
type: 'text',
|
|
867
|
-
text: `Could not store the map for "${args.neuron}".`,
|
|
868
|
-
}],
|
|
869
|
-
};
|
|
870
|
-
}
|
|
871
|
-
return {
|
|
872
|
-
content: [{
|
|
873
|
-
type: 'text',
|
|
874
|
-
text: JSON.stringify({
|
|
875
|
-
neuron_id: result.neuron.id,
|
|
876
|
-
action: result.action,
|
|
877
|
-
updated: result.neuron.map?.updated,
|
|
878
|
-
length: args.content.length,
|
|
879
|
-
redacted: result.redacted.length > 0 ? result.redacted : undefined,
|
|
880
|
-
message: args.content.trim() === ''
|
|
881
|
-
? `System map of "${result.neuron.name}" cleared.`
|
|
882
|
-
: `System map of "${result.neuron.name}" replaced. The previous version is gone - this one is now the reference.`,
|
|
883
|
-
}, null, 2),
|
|
884
|
-
}],
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
catch (err) {
|
|
888
|
-
return {
|
|
889
|
-
content: [{
|
|
890
|
-
type: 'text',
|
|
891
|
-
text: `CRBRO map error: ${err instanceof Error ? err.message : String(err)}`,
|
|
892
|
-
}],
|
|
893
|
-
isError: true,
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
});
|
|
897
|
-
// ═══════════════════════════════════════════════════════════════
|
|
898
|
-
// TOOL 16: crbro_revise — Retire knowledge that stopped being true
|
|
899
|
-
// ═══════════════════════════════════════════════════════════════
|
|
900
|
-
server.registerTool('crbro_revise', {
|
|
901
|
-
title: 'Revise facts',
|
|
902
|
-
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.',
|
|
903
|
-
inputSchema: {
|
|
904
|
-
neuron: zod_1.z.string().describe('Neuron id or name holding the facts, e.g. "project_octochat".'),
|
|
905
|
-
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.'),
|
|
906
|
-
status: zod_1.z.enum(['superseded', 'retracted']).optional().describe('superseded = there is a newer truth (default); retracted = it was never true.'),
|
|
907
|
-
note: zod_1.z.string().optional().describe('Why it stopped being true. The next reader will wonder.'),
|
|
908
|
-
},
|
|
909
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
910
|
-
}, async (args) => {
|
|
911
|
-
try {
|
|
912
|
-
const result = await cortex.revise(args.neuron, args.facts, {
|
|
913
|
-
status: args.status,
|
|
914
|
-
note: args.note,
|
|
951
|
+
return jsonResult({
|
|
952
|
+
mode: args.dry_run ? 'DRY RUN' : 'EXECUTED',
|
|
953
|
+
...report,
|
|
915
954
|
});
|
|
916
|
-
if (!result.neuron) {
|
|
917
|
-
return {
|
|
918
|
-
content: [{
|
|
919
|
-
type: 'text',
|
|
920
|
-
text: `Neuron not found: "${args.neuron}". Use crbro_recall to find the right neuron_id first.`,
|
|
921
|
-
}],
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
return {
|
|
925
|
-
content: [{
|
|
926
|
-
type: 'text',
|
|
927
|
-
text: JSON.stringify({
|
|
928
|
-
neuron_id: result.neuron.id,
|
|
929
|
-
revised: result.revised,
|
|
930
|
-
status: args.status || 'superseded',
|
|
931
|
-
unmatched: result.unmatched.length > 0 ? result.unmatched : undefined,
|
|
932
|
-
message: result.revised > 0
|
|
933
|
-
? `${result.revised} fact(s) retired in "${result.neuron.name}". They no longer appear in recall.` +
|
|
934
|
-
(result.unmatched.length > 0
|
|
935
|
-
? ` WARNING: ${result.unmatched.length} target(s) matched nothing and are still live.`
|
|
936
|
-
: '')
|
|
937
|
-
: 'Nothing matched. Pass the fact id from crbro_recall, or its exact text.',
|
|
938
|
-
}, null, 2),
|
|
939
|
-
}],
|
|
940
|
-
};
|
|
941
955
|
}
|
|
942
956
|
catch (err) {
|
|
943
|
-
return
|
|
944
|
-
content: [{
|
|
945
|
-
type: 'text',
|
|
946
|
-
text: `CRBRO revise error: ${err instanceof Error ? err.message : String(err)}`,
|
|
947
|
-
}],
|
|
948
|
-
isError: true,
|
|
949
|
-
};
|
|
957
|
+
return errorResult('maintenance', err);
|
|
950
958
|
}
|
|
951
959
|
});
|
|
952
960
|
// ═══════════════════════════════════════════════════════════════
|
|
953
|
-
// TOOL
|
|
961
|
+
// TOOL 12: crbro_audit — What should not be in the brain
|
|
954
962
|
// ═══════════════════════════════════════════════════════════════
|
|
955
963
|
server.registerTool('crbro_audit', {
|
|
956
964
|
title: 'Audit for credentials',
|
|
957
|
-
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.',
|
|
965
|
+
description: 'Read-only scan of every field of every neuron (facts, decisions, patterns, preferences, errors, debts, system map) and of every session log 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 (facts for entries, session for a day log), then rotate the credential. Run it after upgrading and whenever a secret may have been pasted into a conversation. crbro_inspect shows content; this only judges it.',
|
|
958
966
|
inputSchema: {},
|
|
959
967
|
outputSchema: {
|
|
960
968
|
neurons_affected: zod_1.z.number(),
|
|
@@ -964,6 +972,10 @@ function createServer() {
|
|
|
964
972
|
facts: zod_1.z.number(), decisions: zod_1.z.number(), patterns: zod_1.z.number(), preferences: zod_1.z.number(),
|
|
965
973
|
errors: zod_1.z.number(), debts: zod_1.z.number(), map: zod_1.z.number(),
|
|
966
974
|
}).loose()),
|
|
975
|
+
sessions_affected: zod_1.z.number().optional(),
|
|
976
|
+
session_findings: zod_1.z.array(zod_1.z.object({
|
|
977
|
+
session_id: zod_1.z.string(), date: zod_1.z.string(), kinds: zod_1.z.array(zod_1.z.string()),
|
|
978
|
+
}).loose()).optional(),
|
|
967
979
|
message: zod_1.z.string(),
|
|
968
980
|
note: zod_1.z.string().optional(),
|
|
969
981
|
},
|
|
@@ -971,16 +983,25 @@ function createServer() {
|
|
|
971
983
|
}, async () => {
|
|
972
984
|
try {
|
|
973
985
|
const hallazgos = await cortex.auditSecrets();
|
|
986
|
+
const sesiones = await hippocampus.auditSecrets();
|
|
974
987
|
const total = hallazgos.reduce((n, h) => n + h.facts + h.decisions + h.patterns + h.preferences + h.errors + h.debts + h.map, 0);
|
|
988
|
+
const clean = hallazgos.length === 0 && sesiones.length === 0;
|
|
975
989
|
const payload = {
|
|
976
990
|
neurons_affected: hallazgos.length,
|
|
977
991
|
facts_affected: total,
|
|
978
992
|
findings: hallazgos,
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
993
|
+
sessions_affected: sesiones.length,
|
|
994
|
+
session_findings: sesiones,
|
|
995
|
+
message: clean
|
|
996
|
+
? 'No credentials found in the brain or in the session logs.'
|
|
997
|
+
: (hallazgos.length > 0
|
|
998
|
+
? `${total} entr(y/ies) across ${hallazgos.length} neuron(s) contain something that looks like a credential. ` +
|
|
999
|
+
'They are also inside the search index, so recall can return them. Remove them with crbro_forget facts. '
|
|
1000
|
+
: '') +
|
|
1001
|
+
(sesiones.length > 0
|
|
1002
|
+
? `${sesiones.length} session log(s) contain something that looks like a credential; remove each with crbro_forget session. `
|
|
1003
|
+
: '') +
|
|
1004
|
+
'Then rotate the credentials — assume they are compromised.',
|
|
984
1005
|
note: 'Values are never shown here, by design.',
|
|
985
1006
|
};
|
|
986
1007
|
return {
|
|
@@ -989,21 +1010,15 @@ function createServer() {
|
|
|
989
1010
|
};
|
|
990
1011
|
}
|
|
991
1012
|
catch (err) {
|
|
992
|
-
return
|
|
993
|
-
content: [{
|
|
994
|
-
type: 'text',
|
|
995
|
-
text: `CRBRO audit error: ${err instanceof Error ? err.message : String(err)}`,
|
|
996
|
-
}],
|
|
997
|
-
isError: true,
|
|
998
|
-
};
|
|
1013
|
+
return errorResult('audit', err);
|
|
999
1014
|
}
|
|
1000
1015
|
});
|
|
1001
1016
|
// ═══════════════════════════════════════════════════════════════
|
|
1002
|
-
// TOOL
|
|
1017
|
+
// TOOL 13: crbro_secret — Credentials, brokered to the OS keychain
|
|
1003
1018
|
// ═══════════════════════════════════════════════════════════════
|
|
1004
1019
|
server.registerTool('crbro_secret', {
|
|
1005
1020
|
title: 'Keychain secret',
|
|
1006
|
-
description: '
|
|
1021
|
+
description: 'Read or write credentials in the operating system\'s keychain (macOS Keychain, Linux Secret Service, Windows DPAPI): CRBRO keeps no copy, invents no crypto, and no sync or team space can reach the store. When 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 — never print it back unless the user asked. list returns names only; remove deletes one; status says which store this machine has (none is a normal answer; env vars still work). Names are SCREAMING_SNAKE_CASE; set updates in place and rejects empty values.',
|
|
1007
1022
|
inputSchema: {
|
|
1008
1023
|
action: zod_1.z.enum(['get', 'set', 'list', 'remove', 'status'])
|
|
1009
1024
|
.describe('get = read one, set = store or update one, list = names only, remove = delete one, status = which keychain this machine offers, or why none.'),
|
|
@@ -1068,84 +1083,26 @@ function createServer() {
|
|
|
1068
1083
|
note: 'Use it for the task at hand. Do not repeat it back to the user and do not write it into any file.',
|
|
1069
1084
|
};
|
|
1070
1085
|
}
|
|
1071
|
-
return
|
|
1072
|
-
content: [{
|
|
1073
|
-
type: 'text',
|
|
1074
|
-
text: JSON.stringify(payload, null, 2),
|
|
1075
|
-
}],
|
|
1076
|
-
};
|
|
1086
|
+
return jsonResult(payload);
|
|
1077
1087
|
}
|
|
1078
1088
|
catch (err) {
|
|
1079
|
-
return
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
text: err instanceof keychain_js_1.KeychainUnavailable
|
|
1083
|
-
? `No credential store available: ${err.message}`
|
|
1084
|
-
: `CRBRO secret error: ${err instanceof Error ? err.message : String(err)}`,
|
|
1085
|
-
}],
|
|
1086
|
-
isError: true,
|
|
1087
|
-
};
|
|
1089
|
+
return textResult(err instanceof keychain_js_1.KeychainUnavailable
|
|
1090
|
+
? `No credential store available: ${err.message}`
|
|
1091
|
+
: `CRBRO secret error: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
1088
1092
|
}
|
|
1089
1093
|
});
|
|
1090
1094
|
// ═══════════════════════════════════════════════════════════════
|
|
1091
|
-
// TOOL
|
|
1092
|
-
// ═══════════════════════════════════════════════════════════════
|
|
1093
|
-
server.registerTool('crbro_forget', {
|
|
1094
|
-
title: 'Forget for good',
|
|
1095
|
-
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.',
|
|
1096
|
-
inputSchema: {
|
|
1097
|
-
neuron: zod_1.z.string().describe('Neuron id or name holding the entries.'),
|
|
1098
|
-
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.'),
|
|
1099
|
-
},
|
|
1100
|
-
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
1101
|
-
}, async (args) => {
|
|
1102
|
-
try {
|
|
1103
|
-
const r = await cortex.forget(args.neuron, args.facts);
|
|
1104
|
-
if (!r.neuron_id) {
|
|
1105
|
-
return {
|
|
1106
|
-
content: [{
|
|
1107
|
-
type: 'text',
|
|
1108
|
-
text: `Neuron not found: "${args.neuron}". Use crbro_recall or crbro_audit to find the right one.`,
|
|
1109
|
-
}],
|
|
1110
|
-
};
|
|
1111
|
-
}
|
|
1112
|
-
return {
|
|
1113
|
-
content: [{
|
|
1114
|
-
type: 'text',
|
|
1115
|
-
text: JSON.stringify({
|
|
1116
|
-
neuron_id: r.neuron_id,
|
|
1117
|
-
removed: r.removed,
|
|
1118
|
-
backup: r.backup,
|
|
1119
|
-
message: r.removed > 0
|
|
1120
|
-
? `${r.removed} fact(s) removed from "${r.neuron_id}". A copy of the neuron as it was is in ${r.backup}. ` +
|
|
1121
|
-
'If any of them was a credential, rotate it: it existed on disk and in the index.'
|
|
1122
|
-
: 'Nothing matched. Pass the fact id from crbro_recall, or its exact text.',
|
|
1123
|
-
}, null, 2),
|
|
1124
|
-
}],
|
|
1125
|
-
};
|
|
1126
|
-
}
|
|
1127
|
-
catch (err) {
|
|
1128
|
-
return {
|
|
1129
|
-
content: [{
|
|
1130
|
-
type: 'text',
|
|
1131
|
-
text: `CRBRO forget error: ${err instanceof Error ? err.message : String(err)}`,
|
|
1132
|
-
}],
|
|
1133
|
-
isError: true,
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
});
|
|
1137
|
-
// ═══════════════════════════════════════════════════════════════
|
|
1138
|
-
// TOOL 19: crbro_space — Join a team's shared memory
|
|
1095
|
+
// TOOL 14: crbro_space — A team's shared memory
|
|
1139
1096
|
// ═══════════════════════════════════════════════════════════════
|
|
1140
1097
|
server.registerTool('crbro_space', {
|
|
1141
1098
|
title: 'Team space',
|
|
1142
|
-
description: '
|
|
1099
|
+
description: 'Read or write team spaces — shared memory with teammates: a private git repository holding notes about the projects you choose to share; nothing else from your brain goes near it. create starts one (name, remote, author); join clones one a teammate created; status reads your identity and spaces; sync exchanges notes now — the manual form of what crbro_boot and crbro_consolidate do alone, useful right after crbro_share (offline is a normal answer, not a failure); leave pushes pending notes, deletes the local copy and stops following its neurons (neurons untouched). Joining shares nothing: put each project in with crbro_share. create and join reply ok:false with the reason on failure.',
|
|
1143
1100
|
inputSchema: {
|
|
1144
|
-
action: zod_1.z.enum(['create', 'join', 'status']).describe('create = start a new space and push it
|
|
1145
|
-
name: zod_1.z.string().optional().describe('Short name, e.g. "equipo" — the same on everyone\'s machine. Required for create and
|
|
1101
|
+
action: zod_1.z.enum(['create', 'join', 'status', 'sync', 'leave']).describe('create = start a new space and push it; join = clone one a teammate created; status = your identity and spaces; sync = exchange notes now; leave = sync, then forget the space locally.'),
|
|
1102
|
+
name: zod_1.z.string().optional().describe('Short name, e.g. "equipo" — the same on everyone\'s machine. Required for create, join and leave; optional for sync (omit = every space); ignored for status.'),
|
|
1146
1103
|
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.'),
|
|
1147
1104
|
author: zod_1.z.string().optional().describe('How your notes are signed, e.g. "ana". Lowercase, no spaces. Required for create and join.'),
|
|
1148
|
-
branch: zod_1.z.string().optional().describe('Branch to use (default "main").'),
|
|
1105
|
+
branch: zod_1.z.string().optional().describe('Branch to use (default "main"). create and join only.'),
|
|
1149
1106
|
},
|
|
1150
1107
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
1151
1108
|
}, async (args) => {
|
|
@@ -1159,151 +1116,124 @@ function createServer() {
|
|
|
1159
1116
|
if (cfg)
|
|
1160
1117
|
detalle.push({ name: cfg.name, created_by: cfg.created_by, branch: cfg.branch });
|
|
1161
1118
|
}
|
|
1162
|
-
return {
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1119
|
+
return jsonResult({
|
|
1120
|
+
you_are: id.author,
|
|
1121
|
+
device: id.device,
|
|
1122
|
+
spaces: detalle,
|
|
1123
|
+
message: detalle.length === 0
|
|
1124
|
+
? 'You are not in any shared space. Use action "create" to start one, or "join" if a teammate already did.'
|
|
1125
|
+
: `In ${detalle.length} space(s). They sync automatically at boot and on consolidate; action "sync" does it now.`,
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
if (args.action === 'sync') {
|
|
1129
|
+
const informes = args.name
|
|
1130
|
+
? [await (0, space_js_1.syncSpaceNow)(brain, cortex, args.name, 30_000)]
|
|
1131
|
+
: await (0, space_js_1.syncAll)(brain, cortex, 30_000);
|
|
1132
|
+
if (informes.length === 0) {
|
|
1133
|
+
return textResult('You are not in any shared space yet. Use action "create" or "join" first.');
|
|
1134
|
+
}
|
|
1135
|
+
return jsonResult({
|
|
1136
|
+
spaces: informes.map(i => ({
|
|
1137
|
+
space: i.space,
|
|
1138
|
+
state: i.state,
|
|
1139
|
+
neurons_updated: i.neurons_touched,
|
|
1140
|
+
new_facts: i.merged.reduce((n, m) => n + m.facts_added, 0),
|
|
1141
|
+
retracted: i.merged.reduce((n, m) => n + m.facts_retracted, 0),
|
|
1142
|
+
teammates_seen: [...new Set(i.merged.flatMap(m => m.authors))],
|
|
1143
|
+
divergence: i.merged.flatMap(m => m.divergence),
|
|
1144
|
+
pushed: i.pushed,
|
|
1145
|
+
message: i.message,
|
|
1146
|
+
})),
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
if (args.action === 'leave') {
|
|
1150
|
+
if (!args.name) {
|
|
1151
|
+
return textResult('name is required for leave: which space to leave.');
|
|
1152
|
+
}
|
|
1153
|
+
const r = await (0, space_js_1.leaveSpace)(brain, cortex, args.name);
|
|
1154
|
+
return jsonResult({
|
|
1155
|
+
...r,
|
|
1156
|
+
next: r.ok
|
|
1157
|
+
? 'Your neurons are intact and no longer followed; the remote repository was not touched. Re-join later with action "join" and the same URL.'
|
|
1158
|
+
: undefined,
|
|
1159
|
+
});
|
|
1175
1160
|
}
|
|
1176
1161
|
if (!args.name || !args.remote) {
|
|
1177
|
-
return
|
|
1178
|
-
content: [{ type: 'text', text: 'Both name and remote are required for create and join.' }],
|
|
1179
|
-
};
|
|
1162
|
+
return textResult('Both name and remote are required for create and join.');
|
|
1180
1163
|
}
|
|
1181
1164
|
if (!args.author) {
|
|
1182
|
-
return
|
|
1183
|
-
content: [{ type: 'text', text: 'Pass author so your notes carry your name, e.g. author: "ana".' }],
|
|
1184
|
-
};
|
|
1165
|
+
return textResult('Pass author so your notes carry your name, e.g. author: "ana".');
|
|
1185
1166
|
}
|
|
1186
1167
|
const r = args.action === 'create'
|
|
1187
1168
|
? await (0, space_js_1.createSpace)(brain, args.name, args.remote, args.author, args.branch || 'main')
|
|
1188
1169
|
: await (0, space_js_1.joinSpace)(brain, args.name, args.remote, args.author, args.branch || 'main');
|
|
1189
|
-
return {
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
? 'Run crbro_sync to pull in what the others already know.'
|
|
1200
|
-
: undefined,
|
|
1201
|
-
}, null, 2),
|
|
1202
|
-
}],
|
|
1203
|
-
};
|
|
1170
|
+
return jsonResult({
|
|
1171
|
+
ok: r.ok,
|
|
1172
|
+
message: r.message,
|
|
1173
|
+
detail: r.detail,
|
|
1174
|
+
next: r.ok && args.action === 'create'
|
|
1175
|
+
? 'Now share a project into it with crbro_share, and invite your teammates to the repository.'
|
|
1176
|
+
: r.ok
|
|
1177
|
+
? 'Run crbro_space action=sync to pull in what the others already know.'
|
|
1178
|
+
: undefined,
|
|
1179
|
+
});
|
|
1204
1180
|
}
|
|
1205
1181
|
catch (err) {
|
|
1206
|
-
return
|
|
1207
|
-
content: [{ type: 'text', text: `CRBRO space error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
1208
|
-
isError: true,
|
|
1209
|
-
};
|
|
1182
|
+
return errorResult('space', err);
|
|
1210
1183
|
}
|
|
1211
1184
|
});
|
|
1212
1185
|
// ═══════════════════════════════════════════════════════════════
|
|
1213
|
-
// TOOL
|
|
1186
|
+
// TOOL 15: crbro_share — Put one project into a space, or take it out
|
|
1214
1187
|
// ═══════════════════════════════════════════════════════════════
|
|
1188
|
+
const shareSchema = zod_1.z.object({
|
|
1189
|
+
neuron: zod_1.z.string().describe('Neuron id or name to share or unshare.'),
|
|
1190
|
+
space: zod_1.z.string().optional().describe('Space name, as created or joined with crbro_space. Required unless unshare:true.'),
|
|
1191
|
+
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. Ignored with unshare.'),
|
|
1192
|
+
unshare: zod_1.z.boolean().optional().describe('Stop following `neuron` in its space: no more notes go out, the next sync ignores it, and the neuron can then be forgotten. Already-sent notes stay in the remote and in teammates\' brains. space and confirm are ignored in this mode.'),
|
|
1193
|
+
}).superRefine((v, ctx) => {
|
|
1194
|
+
if (!v.unshare && !v.space) {
|
|
1195
|
+
ctx.addIssue({ code: 'custom', path: ['space'], message: 'space is required unless unshare:true.' });
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1215
1198
|
server.registerTool('crbro_share', {
|
|
1216
1199
|
title: 'Share a project',
|
|
1217
|
-
description: '
|
|
1218
|
-
inputSchema:
|
|
1219
|
-
neuron: zod_1.z.string().describe('Neuron id or name to share.'),
|
|
1220
|
-
space: zod_1.z.string().describe('Space name, as created or joined with crbro_space.'),
|
|
1221
|
-
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.'),
|
|
1222
|
-
},
|
|
1200
|
+
description: 'Write: put one neuron into a team space, or take it out with unshare. Call it without confirm first: the dry run reports what would be sent — ops_to_emit, skipped_preferences (preferences never leave this machine) — and refuses outright if it finds a credential (crbro_forget it and rotate it). Show the user, get agreement, call again with the confirm token; a stale token is refused. Entries go out on the next crbro_space action=sync or consolidate, and from then on everything learned about that project flows to the team. unshare stops future notes; what was already sent stays in the remote and in teammates\' brains. Spaces are managed with crbro_space.',
|
|
1201
|
+
inputSchema: shareSchema,
|
|
1223
1202
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
1224
1203
|
}, async (args) => {
|
|
1225
1204
|
try {
|
|
1205
|
+
if (args.unshare) {
|
|
1206
|
+
const r = await (0, space_js_1.unshareNeuron)(brain, cortex, args.neuron);
|
|
1207
|
+
return jsonResult({
|
|
1208
|
+
...r,
|
|
1209
|
+
next: r.ok
|
|
1210
|
+
? 'No more notes go out for this neuron. What was already sent stays in the remote and in teammates\' brains. It can now be forgotten or merged with crbro_forget.'
|
|
1211
|
+
: undefined,
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
const space = args.space;
|
|
1226
1215
|
if (!args.confirm) {
|
|
1227
|
-
const prep = await (0, space_js_1.prepareShare)(brain, cortex, args.neuron,
|
|
1216
|
+
const prep = await (0, space_js_1.prepareShare)(brain, cortex, args.neuron, space);
|
|
1228
1217
|
if ('error' in prep) {
|
|
1229
|
-
return
|
|
1218
|
+
return textResult(prep.error);
|
|
1230
1219
|
}
|
|
1231
|
-
return {
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
: `Ready to share ${prep.ops_to_emit} entries. ${prep.skipped_preferences} preference(s) will NOT be sent — ` +
|
|
1240
|
-
'preferences never leave this machine. Show the user what is about to be shared, then call again with the confirm token.',
|
|
1241
|
-
}, null, 2),
|
|
1242
|
-
}],
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
const r = await (0, space_js_1.commitShare)(brain, cortex, args.neuron, args.space, args.confirm);
|
|
1246
|
-
return {
|
|
1247
|
-
content: [{
|
|
1248
|
-
type: 'text',
|
|
1249
|
-
text: JSON.stringify({
|
|
1250
|
-
...r,
|
|
1251
|
-
next: r.ok ? 'Run crbro_sync to send it now, or let it go out on the next consolidate.' : undefined,
|
|
1252
|
-
}, null, 2),
|
|
1253
|
-
}],
|
|
1254
|
-
};
|
|
1255
|
-
}
|
|
1256
|
-
catch (err) {
|
|
1257
|
-
return {
|
|
1258
|
-
content: [{ type: 'text', text: `CRBRO share error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
1259
|
-
isError: true,
|
|
1260
|
-
};
|
|
1261
|
-
}
|
|
1262
|
-
});
|
|
1263
|
-
// ═══════════════════════════════════════════════════════════════
|
|
1264
|
-
// TOOL 21: crbro_sync — Exchange notes with the team now
|
|
1265
|
-
// ═══════════════════════════════════════════════════════════════
|
|
1266
|
-
server.registerTool('crbro_sync', {
|
|
1267
|
-
title: 'Sync with the team',
|
|
1268
|
-
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.',
|
|
1269
|
-
inputSchema: {
|
|
1270
|
-
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.'),
|
|
1271
|
-
},
|
|
1272
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
1273
|
-
}, async (args) => {
|
|
1274
|
-
try {
|
|
1275
|
-
const informes = args.space
|
|
1276
|
-
? [await (0, space_js_1.syncSpaceNow)(brain, cortex, args.space, 30_000)]
|
|
1277
|
-
: await (0, space_js_1.syncAll)(brain, cortex, 30_000);
|
|
1278
|
-
if (informes.length === 0) {
|
|
1279
|
-
return {
|
|
1280
|
-
content: [{ type: 'text', text: 'You are not in any shared space yet. Use crbro_space to create or join one.' }],
|
|
1281
|
-
};
|
|
1220
|
+
return jsonResult({
|
|
1221
|
+
...prep,
|
|
1222
|
+
message: prep.blocked.length > 0
|
|
1223
|
+
? `Refused: ${prep.blocked.length} credential(s) found in this neuron. Nothing was sent. ` +
|
|
1224
|
+
'Remove them with crbro_forget and rotate them, then try again.'
|
|
1225
|
+
: `Ready to share ${prep.ops_to_emit} entries. ${prep.skipped_preferences} preference(s) will NOT be sent — ` +
|
|
1226
|
+
'preferences never leave this machine. Show the user what is about to be shared, then call again with the confirm token.',
|
|
1227
|
+
});
|
|
1282
1228
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
space: i.space,
|
|
1289
|
-
state: i.state,
|
|
1290
|
-
neurons_updated: i.neurons_touched,
|
|
1291
|
-
new_facts: i.merged.reduce((n, m) => n + m.facts_added, 0),
|
|
1292
|
-
retracted: i.merged.reduce((n, m) => n + m.facts_retracted, 0),
|
|
1293
|
-
teammates_seen: [...new Set(i.merged.flatMap(m => m.authors))],
|
|
1294
|
-
divergence: i.merged.flatMap(m => m.divergence),
|
|
1295
|
-
pushed: i.pushed,
|
|
1296
|
-
message: i.message,
|
|
1297
|
-
})),
|
|
1298
|
-
}, null, 2),
|
|
1299
|
-
}],
|
|
1300
|
-
};
|
|
1229
|
+
const r = await (0, space_js_1.commitShare)(brain, cortex, args.neuron, space, args.confirm);
|
|
1230
|
+
return jsonResult({
|
|
1231
|
+
...r,
|
|
1232
|
+
next: r.ok ? 'Run crbro_space action=sync to send it now, or let it go out on the next consolidate. unshare:true stops future notes.' : undefined,
|
|
1233
|
+
});
|
|
1301
1234
|
}
|
|
1302
1235
|
catch (err) {
|
|
1303
|
-
return
|
|
1304
|
-
content: [{ type: 'text', text: `CRBRO sync error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
1305
|
-
isError: true,
|
|
1306
|
-
};
|
|
1236
|
+
return errorResult('share', err);
|
|
1307
1237
|
}
|
|
1308
1238
|
});
|
|
1309
1239
|
return server;
|