crbro-memory 2.9.1 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +256 -456
- package/SECURITY.md +135 -8
- package/bin/crbro.mjs +555 -299
- package/dist/build-id.json +10 -0
- package/dist/cli/common.js +211 -0
- package/dist/cli/export.js +522 -0
- package/dist/cli/import.js +683 -0
- package/dist/cli/markdown.js +313 -0
- package/dist/cli/mcpclient.js +107 -0
- package/dist/cli/serve.js +453 -0
- package/dist/cli/view.js +358 -0
- package/dist/cli/viewer-page.js +378 -0
- package/dist/daemon/daemon.js +119 -11
- package/dist/daemon/endpoint.js +222 -21
- package/dist/daemon/lines.js +102 -13
- package/dist/daemon/proxy.js +480 -125
- package/dist/daemon/surface.js +116 -0
- package/dist/daemon/trace.js +73 -0
- package/dist/engine/atrest.js +132 -0
- package/dist/engine/backup.js +143 -19
- package/dist/engine/brain.js +22 -6
- package/dist/engine/codexconfig.js +476 -0
- package/dist/engine/cortex.js +762 -311
- package/dist/engine/doctor.js +676 -0
- package/dist/engine/evidence.js +171 -0
- package/dist/engine/heat.js +81 -37
- package/dist/engine/hippocampus.js +50 -36
- package/dist/engine/hookrefresh.js +296 -0
- package/dist/engine/hookverify.js +15 -3
- package/dist/engine/installer.js +1022 -0
- package/dist/engine/installio.js +279 -0
- package/dist/engine/keychain.js +735 -144
- package/dist/engine/maintenance.js +355 -87
- package/dist/engine/modinstall.js +98 -45
- package/dist/engine/prefrontal.js +126 -69
- package/dist/engine/projects.js +80 -0
- package/dist/engine/quarantine.js +204 -0
- package/dist/engine/secrets.js +41 -5
- package/dist/engine/shelf.js +34 -11
- package/dist/engine/synapses.js +183 -80
- package/dist/engine/triggers.js +141 -11
- package/dist/index.js +37 -3
- package/dist/mcp-surface.json +1 -0
- package/dist/miner/extractor.js +64 -17
- package/dist/miner/scheduler.js +144 -68
- package/dist/search/index.js +810 -176
- package/dist/search/quality.js +31 -0
- package/dist/search/semantic.js +208 -18
- package/dist/search/tokenize.js +44 -0
- package/dist/server.js +649 -343
- package/dist/sync/git.js +203 -72
- package/dist/sync/materialize.js +33 -8
- package/dist/sync/space.js +286 -101
- package/dist/sync/validate.js +204 -0
- package/dist/utils/budget.js +15 -2
- package/dist/utils/fs.js +40 -6
- package/dist/utils/ids.js +128 -14
- package/dist/utils/lock.js +219 -52
- package/hooks/crbro-codex.mjs +284 -0
- package/hooks/crbro-guard.mjs +281 -173
- package/hooks/crbro-hooks.shipped.json +20 -0
- package/hooks/crbro-lifecycle.mjs +16 -4
- package/hooks/crbro-subagent.mjs +180 -180
- package/package.json +12 -8
- package/dist/daemon/daemon.d.ts +0 -25
- package/dist/daemon/daemon.d.ts.map +0 -1
- package/dist/daemon/daemon.js.map +0 -1
- package/dist/daemon/endpoint.d.ts +0 -66
- package/dist/daemon/endpoint.d.ts.map +0 -1
- package/dist/daemon/endpoint.js.map +0 -1
- package/dist/daemon/lines.d.ts +0 -55
- package/dist/daemon/lines.d.ts.map +0 -1
- package/dist/daemon/lines.js.map +0 -1
- package/dist/daemon/proxy.d.ts +0 -55
- package/dist/daemon/proxy.d.ts.map +0 -1
- package/dist/daemon/proxy.js.map +0 -1
- package/dist/engine/backup.d.ts +0 -68
- package/dist/engine/backup.d.ts.map +0 -1
- package/dist/engine/backup.js.map +0 -1
- package/dist/engine/brain.d.ts +0 -79
- package/dist/engine/brain.d.ts.map +0 -1
- package/dist/engine/brain.js.map +0 -1
- package/dist/engine/cortex.d.ts +0 -670
- package/dist/engine/cortex.d.ts.map +0 -1
- package/dist/engine/cortex.js.map +0 -1
- package/dist/engine/dates.d.ts +0 -28
- package/dist/engine/dates.d.ts.map +0 -1
- package/dist/engine/dates.js.map +0 -1
- package/dist/engine/heat.d.ts +0 -28
- package/dist/engine/heat.d.ts.map +0 -1
- package/dist/engine/heat.js.map +0 -1
- package/dist/engine/hippocampus.d.ts +0 -60
- package/dist/engine/hippocampus.d.ts.map +0 -1
- package/dist/engine/hippocampus.js.map +0 -1
- package/dist/engine/hookverify.d.ts +0 -42
- package/dist/engine/hookverify.d.ts.map +0 -1
- package/dist/engine/hookverify.js.map +0 -1
- package/dist/engine/keychain.d.ts +0 -32
- package/dist/engine/keychain.d.ts.map +0 -1
- package/dist/engine/keychain.js.map +0 -1
- package/dist/engine/maintenance.d.ts +0 -218
- package/dist/engine/maintenance.d.ts.map +0 -1
- package/dist/engine/maintenance.js.map +0 -1
- package/dist/engine/modinstall.d.ts +0 -215
- package/dist/engine/modinstall.d.ts.map +0 -1
- package/dist/engine/modinstall.js.map +0 -1
- package/dist/engine/postmortem.d.ts +0 -81
- package/dist/engine/postmortem.d.ts.map +0 -1
- package/dist/engine/postmortem.js.map +0 -1
- package/dist/engine/prefrontal.d.ts +0 -61
- package/dist/engine/prefrontal.d.ts.map +0 -1
- package/dist/engine/prefrontal.js.map +0 -1
- package/dist/engine/secrets.d.ts +0 -30
- package/dist/engine/secrets.d.ts.map +0 -1
- package/dist/engine/secrets.js.map +0 -1
- package/dist/engine/shelf.d.ts +0 -156
- package/dist/engine/shelf.d.ts.map +0 -1
- package/dist/engine/shelf.js.map +0 -1
- package/dist/engine/source.d.ts +0 -6
- package/dist/engine/source.d.ts.map +0 -1
- package/dist/engine/source.js.map +0 -1
- package/dist/engine/synapses.d.ts +0 -82
- package/dist/engine/synapses.d.ts.map +0 -1
- package/dist/engine/synapses.js.map +0 -1
- package/dist/engine/triggers.d.ts +0 -53
- package/dist/engine/triggers.d.ts.map +0 -1
- package/dist/engine/triggers.js.map +0 -1
- package/dist/engine/usage.d.ts +0 -61
- package/dist/engine/usage.d.ts.map +0 -1
- package/dist/engine/usage.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/miner/extractor.d.ts +0 -12
- package/dist/miner/extractor.d.ts.map +0 -1
- package/dist/miner/extractor.js.map +0 -1
- package/dist/miner/index.d.ts +0 -61
- package/dist/miner/index.d.ts.map +0 -1
- package/dist/miner/index.js.map +0 -1
- package/dist/miner/scheduler.d.ts +0 -30
- package/dist/miner/scheduler.d.ts.map +0 -1
- package/dist/miner/scheduler.js.map +0 -1
- package/dist/miner/types.d.ts +0 -11
- package/dist/miner/types.d.ts.map +0 -1
- package/dist/miner/types.js.map +0 -1
- package/dist/search/index.d.ts +0 -323
- package/dist/search/index.d.ts.map +0 -1
- package/dist/search/index.js.map +0 -1
- package/dist/search/semantic.d.ts +0 -80
- package/dist/search/semantic.d.ts.map +0 -1
- package/dist/search/semantic.js.map +0 -1
- package/dist/search/synonyms.d.ts +0 -2
- package/dist/search/synonyms.d.ts.map +0 -1
- package/dist/search/synonyms.js.map +0 -1
- package/dist/search/tokenize.d.ts +0 -39
- package/dist/search/tokenize.d.ts.map +0 -1
- package/dist/search/tokenize.js.map +0 -1
- package/dist/server.d.ts +0 -41
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/sync/git.d.ts +0 -36
- package/dist/sync/git.d.ts.map +0 -1
- package/dist/sync/git.js.map +0 -1
- package/dist/sync/materialize.d.ts +0 -35
- package/dist/sync/materialize.d.ts.map +0 -1
- package/dist/sync/materialize.js.map +0 -1
- package/dist/sync/ops.d.ts +0 -142
- package/dist/sync/ops.d.ts.map +0 -1
- package/dist/sync/ops.js.map +0 -1
- package/dist/sync/space.d.ts +0 -116
- package/dist/sync/space.d.ts.map +0 -1
- package/dist/sync/space.js.map +0 -1
- package/dist/types/index.d.ts +0 -346
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/budget.d.ts +0 -44
- package/dist/utils/budget.d.ts.map +0 -1
- package/dist/utils/budget.js.map +0 -1
- package/dist/utils/fs.d.ts +0 -59
- package/dist/utils/fs.d.ts.map +0 -1
- package/dist/utils/fs.js.map +0 -1
- package/dist/utils/hash.d.ts +0 -20
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js.map +0 -1
- package/dist/utils/ids.d.ts +0 -47
- package/dist/utils/ids.d.ts.map +0 -1
- package/dist/utils/ids.js.map +0 -1
- package/dist/utils/lock.d.ts +0 -11
- package/dist/utils/lock.d.ts.map +0 -1
- package/dist/utils/lock.js.map +0 -1
- package/dist/utils/transcripts.d.ts +0 -63
- package/dist/utils/transcripts.d.ts.map +0 -1
- package/dist/utils/transcripts.js.map +0 -1
- package/dist/version.d.ts +0 -26
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
package/dist/engine/synapses.js
CHANGED
|
@@ -3,13 +3,40 @@
|
|
|
3
3
|
// Connection management between neurons with strength decay
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.Synapses = void 0;
|
|
6
|
+
exports.weeklyDecayFactor = weeklyDecayFactor;
|
|
7
|
+
exports.decayedStrength = decayedStrength;
|
|
6
8
|
const fs_js_1 = require("../utils/fs.js");
|
|
9
|
+
const lock_js_1 = require("../utils/lock.js");
|
|
7
10
|
const ids_js_1 = require("../utils/ids.js");
|
|
11
|
+
const quarantine_js_1 = require("./quarantine.js");
|
|
8
12
|
/** Strength is stored in [0, 1] with three decimals, like decay writes it. */
|
|
9
13
|
function clampStrength(value) {
|
|
10
14
|
const v = Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0;
|
|
11
15
|
return Math.round(v * 1000) / 1000;
|
|
12
16
|
}
|
|
17
|
+
const DAY_MS = 86_400_000;
|
|
18
|
+
/**
|
|
19
|
+
* Weekly decay factor by how long the link has been idle. Applied for the
|
|
20
|
+
* time actually elapsed since the last decay (2.11): until then the same
|
|
21
|
+
* factor hit on EVERY maintenance run, so a brain maintained daily pruned an
|
|
22
|
+
* idle link in a week and one maintained monthly in eight months.
|
|
23
|
+
*/
|
|
24
|
+
function weeklyDecayFactor(idleDays) {
|
|
25
|
+
if (idleDays > 90)
|
|
26
|
+
return 0.8;
|
|
27
|
+
if (idleDays > 30)
|
|
28
|
+
return 0.9;
|
|
29
|
+
if (idleDays > 7)
|
|
30
|
+
return 0.95;
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
/** Strength after `elapsedDays` of decay at the rate an `idleDays` idle link gets. */
|
|
34
|
+
function decayedStrength(strength, idleDays, elapsedDays) {
|
|
35
|
+
const f = weeklyDecayFactor(idleDays);
|
|
36
|
+
if (f === 1 || elapsedDays <= 0)
|
|
37
|
+
return strength;
|
|
38
|
+
return Math.round(strength * Math.pow(f, elapsedDays / 7) * 1000) / 1000;
|
|
39
|
+
}
|
|
13
40
|
class Synapses {
|
|
14
41
|
brain;
|
|
15
42
|
constructor(brain) {
|
|
@@ -17,43 +44,51 @@ class Synapses {
|
|
|
17
44
|
}
|
|
18
45
|
/**
|
|
19
46
|
* Create or strengthen a synapse between two neurons.
|
|
47
|
+
*
|
|
48
|
+
* Every write goes through the same locked read-modify-write as the rest of
|
|
49
|
+
* the brain (2.11). Before, the synapse file and BOTH neurons were read and
|
|
50
|
+
* rewritten without a lock: a learn landing on a 1 MB neuron while
|
|
51
|
+
* consolidate linked it lost its fact, silently.
|
|
20
52
|
*/
|
|
21
53
|
async connect(fromId, toId, type, context, options) {
|
|
22
54
|
const id = (0, ids_js_1.synapseId)(fromId, toId);
|
|
23
|
-
let
|
|
24
|
-
let action;
|
|
55
|
+
let action = 'strengthened';
|
|
25
56
|
const absoluta = typeof options?.strength === 'number' ? clampStrength(options.strength) : undefined;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
57
|
+
const synapse = await (0, fs_js_1.updateJSON)(this.brain.paths.synapse(id), existing => {
|
|
58
|
+
const cuando = (0, fs_js_1.now)();
|
|
59
|
+
if (existing) {
|
|
60
|
+
// Strengthen existing synapse
|
|
61
|
+
existing.strength = absoluta !== undefined ? absoluta : Math.min(1.0, existing.strength + 0.1);
|
|
62
|
+
existing.co_access_count += 1;
|
|
63
|
+
existing.last_co_access = cuando;
|
|
64
|
+
// A fresh co-access restarts the decay clock.
|
|
65
|
+
existing.decayed_at = cuando;
|
|
66
|
+
if (context && !(options?.keepExistingContext && existing.context)) {
|
|
67
|
+
existing.context = context;
|
|
68
|
+
}
|
|
69
|
+
action = 'strengthened';
|
|
70
|
+
return existing;
|
|
33
71
|
}
|
|
34
|
-
action = '
|
|
35
|
-
|
|
36
|
-
else {
|
|
37
|
-
// Create new synapse
|
|
38
|
-
synapse = {
|
|
72
|
+
action = 'created';
|
|
73
|
+
return {
|
|
39
74
|
id,
|
|
40
75
|
nodes: [fromId, toId],
|
|
41
76
|
strength: absoluta !== undefined ? absoluta : (options?.initialStrength ?? 0.5),
|
|
42
77
|
type,
|
|
43
78
|
context: context || '',
|
|
44
79
|
co_access_count: 1,
|
|
45
|
-
last_co_access:
|
|
80
|
+
last_co_access: cuando,
|
|
81
|
+
decayed_at: cuando,
|
|
46
82
|
};
|
|
47
|
-
|
|
48
|
-
|
|
83
|
+
});
|
|
84
|
+
if (action === 'created') {
|
|
49
85
|
const manifest = await this.brain.getManifest();
|
|
50
86
|
await this.brain.updateManifest({ total_synapses: manifest.total_synapses + 1 });
|
|
51
87
|
}
|
|
52
|
-
await (0, fs_js_1.writeJSON)(this.brain.paths.synapse(id), synapse);
|
|
53
88
|
// Update neuron connection lists
|
|
54
89
|
await this.addConnectionToNeuron(fromId, toId);
|
|
55
90
|
await this.addConnectionToNeuron(toId, fromId);
|
|
56
|
-
return { synapse, action };
|
|
91
|
+
return { synapse: synapse, action };
|
|
57
92
|
}
|
|
58
93
|
/**
|
|
59
94
|
* Remove one synapse and unlink both neurons. Absent → removed:false, no
|
|
@@ -61,10 +96,16 @@ class Synapses {
|
|
|
61
96
|
*/
|
|
62
97
|
async disconnect(fromId, toId) {
|
|
63
98
|
const id = (0, ids_js_1.synapseId)(fromId, toId);
|
|
64
|
-
const
|
|
99
|
+
const file = this.brain.paths.synapse(id);
|
|
100
|
+
const synapse = await (0, lock_js_1.withLock)(file, async () => {
|
|
101
|
+
const s = await (0, fs_js_1.readJSON)(file).catch(() => null);
|
|
102
|
+
if (!s)
|
|
103
|
+
return null;
|
|
104
|
+
await (0, fs_js_1.deleteJSON)(file);
|
|
105
|
+
return s;
|
|
106
|
+
});
|
|
65
107
|
if (!synapse)
|
|
66
108
|
return { synapse_id: id, removed: false };
|
|
67
|
-
await (0, fs_js_1.deleteJSON)(this.brain.paths.synapse(id));
|
|
68
109
|
// Remove from neuron connection lists. Use the nodes the file recorded,
|
|
69
110
|
// not the ids the caller passed: the id is order-free and prefix-free, so
|
|
70
111
|
// the caller's spelling may differ from what the neurons hold.
|
|
@@ -81,12 +122,15 @@ class Synapses {
|
|
|
81
122
|
*/
|
|
82
123
|
async setStrength(fromId, toId, strength) {
|
|
83
124
|
const id = (0, ids_js_1.synapseId)(fromId, toId);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
125
|
+
let found = false;
|
|
126
|
+
const s = await (0, fs_js_1.updateJSON)(this.brain.paths.synapse(id), current => {
|
|
127
|
+
if (!current)
|
|
128
|
+
return null;
|
|
129
|
+
found = true;
|
|
130
|
+
current.strength = clampStrength(strength);
|
|
131
|
+
return current;
|
|
132
|
+
});
|
|
133
|
+
return found ? s : null;
|
|
90
134
|
}
|
|
91
135
|
/**
|
|
92
136
|
* Remove every synapse touching a neuron. Returns how many went. Used when
|
|
@@ -95,7 +139,7 @@ class Synapses {
|
|
|
95
139
|
async removeAllFor(neuronId) {
|
|
96
140
|
let removed = 0;
|
|
97
141
|
for (const id of await (0, fs_js_1.listJSONFiles)(this.brain.paths.synapses)) {
|
|
98
|
-
const synapse = await
|
|
142
|
+
const synapse = await this.readSynapse(id);
|
|
99
143
|
if (!synapse || !synapse.nodes.includes(neuronId))
|
|
100
144
|
continue;
|
|
101
145
|
const r = await this.disconnect(synapse.nodes[0], synapse.nodes[1]);
|
|
@@ -117,7 +161,7 @@ class Synapses {
|
|
|
117
161
|
if (fromId === intoId)
|
|
118
162
|
return out;
|
|
119
163
|
for (const id of await (0, fs_js_1.listJSONFiles)(this.brain.paths.synapses)) {
|
|
120
|
-
const synapse = await
|
|
164
|
+
const synapse = await this.readSynapse(id);
|
|
121
165
|
if (!synapse || !synapse.nodes.includes(fromId))
|
|
122
166
|
continue;
|
|
123
167
|
const other = synapse.nodes[0] === fromId ? synapse.nodes[1] : synapse.nodes[0];
|
|
@@ -130,24 +174,25 @@ class Synapses {
|
|
|
130
174
|
const nuevoId = (0, ids_js_1.synapseId)(intoId, other);
|
|
131
175
|
// Ids drop the type prefix, so two neurons differing only by prefix
|
|
132
176
|
// yield the same synapse id: then the file stays, only the nodes move.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
existente.
|
|
177
|
+
let fusion = false;
|
|
178
|
+
await (0, fs_js_1.updateJSON)(this.brain.paths.synapse(nuevoId), existente => {
|
|
179
|
+
if (existente && nuevoId !== id) {
|
|
180
|
+
fusion = true;
|
|
181
|
+
existente.strength = clampStrength(Math.max(existente.strength, synapse.strength));
|
|
182
|
+
existente.co_access_count = (existente.co_access_count || 0) + (synapse.co_access_count || 0);
|
|
183
|
+
if ((synapse.last_co_access || '') > (existente.last_co_access || '')) {
|
|
184
|
+
existente.last_co_access = synapse.last_co_access;
|
|
185
|
+
}
|
|
186
|
+
return existente;
|
|
139
187
|
}
|
|
140
|
-
|
|
188
|
+
return { ...synapse, id: nuevoId, nodes: [intoId, other] };
|
|
189
|
+
});
|
|
190
|
+
if (nuevoId !== id)
|
|
141
191
|
await (0, fs_js_1.deleteJSON)(this.brain.paths.synapse(id));
|
|
192
|
+
if (fusion)
|
|
142
193
|
out.merged++;
|
|
143
|
-
|
|
144
|
-
else {
|
|
145
|
-
const movido = { ...synapse, id: nuevoId, nodes: [intoId, other] };
|
|
146
|
-
await (0, fs_js_1.writeJSON)(this.brain.paths.synapse(nuevoId), movido);
|
|
147
|
-
if (nuevoId !== id)
|
|
148
|
-
await (0, fs_js_1.deleteJSON)(this.brain.paths.synapse(id));
|
|
194
|
+
else
|
|
149
195
|
out.moved++;
|
|
150
|
-
}
|
|
151
196
|
await this.removeConnectionFromNeuron(other, fromId);
|
|
152
197
|
await this.addConnectionToNeuron(other, intoId);
|
|
153
198
|
await this.addConnectionToNeuron(intoId, other);
|
|
@@ -166,7 +211,7 @@ class Synapses {
|
|
|
166
211
|
const ids = await (0, fs_js_1.listJSONFiles)(this.brain.paths.synapses);
|
|
167
212
|
const connections = [];
|
|
168
213
|
for (const id of ids) {
|
|
169
|
-
const synapse = await
|
|
214
|
+
const synapse = await this.readSynapse(id);
|
|
170
215
|
if (!synapse)
|
|
171
216
|
continue;
|
|
172
217
|
if (!synapse.nodes.includes(neuronId))
|
|
@@ -174,8 +219,12 @@ class Synapses {
|
|
|
174
219
|
if (minStrength && synapse.strength < minStrength)
|
|
175
220
|
continue;
|
|
176
221
|
const targetId = synapse.nodes[0] === neuronId ? synapse.nodes[1] : synapse.nodes[0];
|
|
177
|
-
// Get target neuron name
|
|
178
|
-
|
|
222
|
+
// Get target neuron name. An unreadable neighbour shows by its id.
|
|
223
|
+
let targetNeuron = null;
|
|
224
|
+
try {
|
|
225
|
+
targetNeuron = await (0, fs_js_1.readJSON)(this.brain.paths.neuron(targetId));
|
|
226
|
+
}
|
|
227
|
+
catch { /* corrupt */ }
|
|
179
228
|
const targetName = targetNeuron?.name || targetId;
|
|
180
229
|
connections.push({
|
|
181
230
|
target_id: targetId,
|
|
@@ -197,36 +246,63 @@ class Synapses {
|
|
|
197
246
|
return (0, fs_js_1.readJSON)(this.brain.paths.synapse(id));
|
|
198
247
|
}
|
|
199
248
|
/**
|
|
200
|
-
* Decay all synapses
|
|
201
|
-
*
|
|
249
|
+
* Decay all synapses by the time elapsed since each was last decayed, and
|
|
250
|
+
* prune the ones that fall below the threshold (a quarantine copy first).
|
|
251
|
+
* Returns how many were pruned — or, with dryRun, how many would be, with
|
|
252
|
+
* nothing written.
|
|
253
|
+
*
|
|
254
|
+
* A synapse written before 2.11 has no decayed_at: the first pass only
|
|
255
|
+
* stamps it, so upgrading never applies a backlog of decay at once.
|
|
202
256
|
*/
|
|
203
|
-
async decay(pruneThreshold = 0.05) {
|
|
257
|
+
async decay(pruneThreshold = 0.05, options) {
|
|
204
258
|
const ids = await (0, fs_js_1.listJSONFiles)(this.brain.paths.synapses);
|
|
259
|
+
const ahora = options?.now ?? new Date();
|
|
260
|
+
const ahoraIso = ahora.toISOString();
|
|
205
261
|
let pruned = 0;
|
|
206
262
|
for (const id of ids) {
|
|
207
|
-
const
|
|
208
|
-
|
|
263
|
+
const file = this.brain.paths.synapse(id);
|
|
264
|
+
const leida = await this.readSynapse(id);
|
|
265
|
+
if (!leida)
|
|
266
|
+
continue;
|
|
267
|
+
// An archived neuron's links wait for it, untouched (2.11).
|
|
268
|
+
if (options?.skipNodes && leida.nodes.some(n => options.skipNodes.has(n)))
|
|
209
269
|
continue;
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const r = await this.disconnect(synapse.nodes[0], synapse.nodes[1]);
|
|
270
|
+
const idle = (ahora.getTime() - new Date(leida.last_co_access).getTime()) / DAY_MS;
|
|
271
|
+
const desde = leida.decayed_at ? Date.parse(leida.decayed_at) : NaN;
|
|
272
|
+
const elapsed = Number.isFinite(desde) ? (ahora.getTime() - desde) / DAY_MS : 0;
|
|
273
|
+
const nueva = decayedStrength(leida.strength, idle, elapsed);
|
|
274
|
+
if (nueva < pruneThreshold) {
|
|
275
|
+
if (options?.dryRun) {
|
|
276
|
+
pruned++;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
// Prune weak synapse: same path as an explicit disconnect, after a
|
|
280
|
+
// copy — a link that took months to build is not deleted outright.
|
|
281
|
+
await (0, quarantine_js_1.writeQuarantine)(this.brain.paths.quarantine, leida.id || id, { ...leida, strength: nueva, pruned_at: ahoraIso }, 'whole');
|
|
282
|
+
const r = await this.disconnect(leida.nodes[0], leida.nodes[1]);
|
|
224
283
|
if (r.removed)
|
|
225
284
|
pruned++;
|
|
285
|
+
continue;
|
|
226
286
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
287
|
+
if (options?.dryRun)
|
|
288
|
+
continue;
|
|
289
|
+
// Write only when something changes: the strength, or the first stamp.
|
|
290
|
+
// Unchanged, decayed_at stays where it was and the elapsed time keeps
|
|
291
|
+
// accumulating, so slow decay is not rounded away run after run.
|
|
292
|
+
if (nueva === leida.strength && leida.decayed_at)
|
|
293
|
+
continue;
|
|
294
|
+
await (0, fs_js_1.updateJSON)(file, current => {
|
|
295
|
+
if (!current)
|
|
296
|
+
return null;
|
|
297
|
+
// Re-derived from the fresh read under the lock: a strengthen that
|
|
298
|
+
// landed meanwhile restarted the clock and is not undone.
|
|
299
|
+
const idleNow = (ahora.getTime() - new Date(current.last_co_access).getTime()) / DAY_MS;
|
|
300
|
+
const d = current.decayed_at ? Date.parse(current.decayed_at) : NaN;
|
|
301
|
+
const el = Number.isFinite(d) ? (ahora.getTime() - d) / DAY_MS : 0;
|
|
302
|
+
current.strength = Math.max(pruneThreshold, decayedStrength(current.strength, idleNow, el));
|
|
303
|
+
current.decayed_at = ahoraIso;
|
|
304
|
+
return current;
|
|
305
|
+
});
|
|
230
306
|
}
|
|
231
307
|
return pruned;
|
|
232
308
|
}
|
|
@@ -238,21 +314,48 @@ class Synapses {
|
|
|
238
314
|
return ids.length;
|
|
239
315
|
}
|
|
240
316
|
// ─── Private helpers ────────────────────────────────────────────
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
317
|
+
/** A synapse file, or null when absent or unreadable (the integrity check names those). */
|
|
318
|
+
async readSynapse(id) {
|
|
319
|
+
try {
|
|
320
|
+
const s = await (0, fs_js_1.readJSON)(this.brain.paths.synapse(id));
|
|
321
|
+
return s && Array.isArray(s.nodes) && s.nodes.length === 2 ? s : null;
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return null;
|
|
248
325
|
}
|
|
249
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Locked, and never creating: a neuron deleted meanwhile is not brought
|
|
329
|
+
* back by a connection list. A corrupt neuron file throws — linking to it
|
|
330
|
+
* would mean rewriting a file nobody can read.
|
|
331
|
+
*/
|
|
332
|
+
async addConnectionToNeuron(neuronId, connectedId) {
|
|
333
|
+
await (0, fs_js_1.updateJSON)(this.brain.paths.neuron(neuronId), current => {
|
|
334
|
+
if (!current)
|
|
335
|
+
return null;
|
|
336
|
+
if (!Array.isArray(current.connections))
|
|
337
|
+
current.connections = [];
|
|
338
|
+
if (current.connections.includes(connectedId))
|
|
339
|
+
return null;
|
|
340
|
+
current.connections.push(connectedId);
|
|
341
|
+
return current;
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
/** Locked; a missing or unreadable neuron is left alone, never a reason to stop a disconnect. */
|
|
250
345
|
async removeConnectionFromNeuron(neuronId, connectedId) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
346
|
+
try {
|
|
347
|
+
await (0, fs_js_1.updateJSON)(this.brain.paths.neuron(neuronId), current => {
|
|
348
|
+
if (!current)
|
|
349
|
+
return null;
|
|
350
|
+
const antes = (current.connections || []).length;
|
|
351
|
+
current.connections = (current.connections || []).filter(c => c !== connectedId);
|
|
352
|
+
return current.connections.length === antes ? null : current;
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
if (!(err instanceof fs_js_1.CorruptJSONError))
|
|
357
|
+
throw err;
|
|
358
|
+
}
|
|
256
359
|
}
|
|
257
360
|
}
|
|
258
361
|
exports.Synapses = Synapses;
|
package/dist/engine/triggers.js
CHANGED
|
@@ -21,19 +21,36 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
21
21
|
exports.MAX_LESSONS = exports.TRIGGER_FILE = exports.TRIGGER_INDEX_VERSION = void 0;
|
|
22
22
|
exports.keysOfCommand = keysOfCommand;
|
|
23
23
|
exports.keysOfEntry = keysOfEntry;
|
|
24
|
+
exports.projectMaps = projectMaps;
|
|
24
25
|
exports.buildTriggerIndex = buildTriggerIndex;
|
|
26
|
+
exports.projectOf = projectOf;
|
|
25
27
|
exports.lessonsFor = lessonsFor;
|
|
26
28
|
exports.triggerIndexPath = triggerIndexPath;
|
|
27
29
|
exports.loadAllNeurons = loadAllNeurons;
|
|
28
30
|
exports.writeTriggerIndex = writeTriggerIndex;
|
|
29
31
|
const node_path_1 = __importDefault(require("node:path"));
|
|
32
|
+
const node_fs_1 = require("node:fs");
|
|
30
33
|
const fs_js_1 = require("../utils/fs.js");
|
|
31
34
|
const ops_js_1 = require("../sync/ops.js");
|
|
32
|
-
|
|
35
|
+
// 2: path: keys, so exploring a project wakes its lessons. 3 (2.11): each entry
|
|
36
|
+
// says which projects it belongs to (p) and, when it is not the user's own,
|
|
37
|
+
// where it came from (o); `projects` maps a folder or alias to its project.
|
|
38
|
+
exports.TRIGGER_INDEX_VERSION = 3;
|
|
33
39
|
exports.TRIGGER_FILE = 'triggers.json';
|
|
34
40
|
const PREVIEW = 320;
|
|
35
41
|
/** Lessons handed over per command. More than this is a lecture, and lectures get skimmed. */
|
|
36
42
|
exports.MAX_LESSONS = 3;
|
|
43
|
+
/**
|
|
44
|
+
* Subcommands that only look: `git status`, `gh repo view`… A lesson that
|
|
45
|
+
* mentions one in passing ("after git status showed…") is not about running it,
|
|
46
|
+
* so prose never makes a key of these — only a command in backticks does. They
|
|
47
|
+
* were most of the guard's noise: `git status` in one repo woke three lessons
|
|
48
|
+
* about another project because each said "git status" somewhere.
|
|
49
|
+
*/
|
|
50
|
+
const READ_ONLY = new Set(('git status|git log|git branch|git diff|git show|git remote|git config|git rev-parse|git ls-files|git blame|'
|
|
51
|
+
+ 'gh repo view|gh pr view|gh pr list|gh pr status|gh pr checks|gh issue view|gh issue list|gh run list|gh run view|gh release view|gh release list|gh api|'
|
|
52
|
+
+ 'npm ls|npm view|npm list|npm outdated|docker ps|docker images|docker logs|kubectl get|kubectl describe|kubectl logs|'
|
|
53
|
+
+ 'gcloud config|aws sts|node --version|npm --version|python --version').split('|'));
|
|
37
54
|
/** Programs whose next word names the action. Anything else needs backticks or a script name to count. */
|
|
38
55
|
const HEADS = new Set(('git npm npx pnpm yarn bun bunx node deno python python3 py pip pipx uv firebase gcloud gsutil gh docker '
|
|
39
56
|
+ 'kubectl helm terraform wp composer php supabase vercel netlify wrangler aws az ssh scp rsync curl wget robocopy '
|
|
@@ -163,16 +180,71 @@ function keysOfEntry(text) {
|
|
|
163
180
|
if (!next || !SUBCOMMAND.test(next) || PROSE.has(next))
|
|
164
181
|
continue;
|
|
165
182
|
const third = word(words[i + 2] || '');
|
|
166
|
-
|
|
183
|
+
const key = DEEP.has(`${w} ${next}`) && third && SUBCOMMAND.test(third) && !third.startsWith('-') && !PROSE.has(third) ? `${w} ${next} ${third}` : `${w} ${next}`;
|
|
184
|
+
if (READ_ONLY.has(key))
|
|
185
|
+
continue; // from backticks only
|
|
186
|
+
keys.add(key);
|
|
167
187
|
}
|
|
168
188
|
// A script is only a trigger as the thing that runs; a program pair needs a known program.
|
|
169
189
|
return [...keys].filter(k => k.startsWith('path:') || (k.includes(' ') ? HEADS.has(k.split(' ')[0]) || WRAPPERS.has(k.split(' ')[0]) : SCRIPT.test(k)));
|
|
170
190
|
}
|
|
171
|
-
|
|
191
|
+
/** "C:\\Work\\Shop-Site" and "shop_site" alike: lower case, every run of other characters one dash. */
|
|
192
|
+
function slug(s) {
|
|
193
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Alias → project id, and neuron id → its projects. A neuron belongs to a
|
|
197
|
+
* project when the registry binds it (crbro_boot project_neurons), or when
|
|
198
|
+
* its id or name spells the project's folder name as whole words
|
|
199
|
+
* (`project_shop_site` and the alias `shop-site`). Aliases shorter than four
|
|
200
|
+
* characters name too much to count by name.
|
|
201
|
+
*/
|
|
202
|
+
function projectMaps(registry, neurons) {
|
|
203
|
+
const aliases = {};
|
|
204
|
+
const byNeuron = new Map();
|
|
205
|
+
const named = [];
|
|
206
|
+
for (const [key, p] of Object.entries(registry?.projects ?? {})) {
|
|
207
|
+
const id = typeof p?.id === 'string' ? p.id : key;
|
|
208
|
+
for (const a of [id, ...(Array.isArray(p?.aliases) ? p.aliases : [])]) {
|
|
209
|
+
if (typeof a !== 'string' || !a)
|
|
210
|
+
continue;
|
|
211
|
+
const lower = a.toLowerCase().replace(/\\/g, '/').replace(/\/+$/, '');
|
|
212
|
+
aliases[lower] = id;
|
|
213
|
+
const base = lower.split('/').pop() || lower;
|
|
214
|
+
aliases[base] ??= id;
|
|
215
|
+
const token = slug(base);
|
|
216
|
+
if (token.length >= 4 && !token.startsWith('project-'))
|
|
217
|
+
named.push({ id, token });
|
|
218
|
+
}
|
|
219
|
+
for (const n of Array.isArray(p?.neurons) ? p.neurons : []) {
|
|
220
|
+
if (!byNeuron.has(n))
|
|
221
|
+
byNeuron.set(n, new Set());
|
|
222
|
+
byNeuron.get(n).add(id);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (const n of neurons) {
|
|
226
|
+
if (!n?.id)
|
|
227
|
+
continue;
|
|
228
|
+
const words = `-${slug(n.id)}-${slug(n.name || '')}-`;
|
|
229
|
+
for (const { id, token } of named) {
|
|
230
|
+
if (words.includes(`-${token}-`)) {
|
|
231
|
+
if (!byNeuron.has(n.id))
|
|
232
|
+
byNeuron.set(n.id, new Set());
|
|
233
|
+
byNeuron.get(n.id).add(id);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { aliases, byNeuron };
|
|
238
|
+
}
|
|
239
|
+
function buildTriggerIndex(neurons, builtAt = (0, fs_js_1.now)(), registry) {
|
|
172
240
|
const index = { v: exports.TRIGGER_INDEX_VERSION, built: builtAt, keys: {}, entries: [] };
|
|
241
|
+
const { aliases, byNeuron } = projectMaps(registry, neurons);
|
|
242
|
+
if (Object.keys(aliases).length)
|
|
243
|
+
index.projects = aliases;
|
|
173
244
|
for (const n of neurons) {
|
|
174
245
|
if (!n || n.type === 'protocol')
|
|
175
246
|
continue;
|
|
247
|
+
const projects = [...(byNeuron.get(n.id) ?? [])].sort();
|
|
176
248
|
const lists = [['error', n.errors], ['debt', n.debts], ['pattern', n.patterns]];
|
|
177
249
|
for (const [k, list] of lists) {
|
|
178
250
|
for (const text of list || []) {
|
|
@@ -182,10 +254,13 @@ function buildTriggerIndex(neurons, builtAt = (0, fs_js_1.now)()) {
|
|
|
182
254
|
if (keys.length === 0)
|
|
183
255
|
continue;
|
|
184
256
|
const at = index.entries.length;
|
|
257
|
+
const origin = n.entry_source?.[(0, ops_js_1.entryId)(text)];
|
|
185
258
|
index.entries.push({
|
|
186
259
|
n: n.id, name: n.name, e: (0, ops_js_1.entryId)(text), k,
|
|
187
260
|
d: (n.entry_dates?.[(0, ops_js_1.entryId)(text)] || '').slice(0, 10),
|
|
188
261
|
t: text.length > PREVIEW ? `${text.slice(0, PREVIEW).trimEnd()}…` : text,
|
|
262
|
+
...(projects.length ? { p: projects } : {}),
|
|
263
|
+
...(origin ? { o: origin } : {}),
|
|
189
264
|
});
|
|
190
265
|
for (const key of keys)
|
|
191
266
|
(index.keys[key] ||= []).push(at);
|
|
@@ -194,15 +269,65 @@ function buildTriggerIndex(neurons, builtAt = (0, fs_js_1.now)()) {
|
|
|
194
269
|
}
|
|
195
270
|
return index;
|
|
196
271
|
}
|
|
197
|
-
/**
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
272
|
+
/**
|
|
273
|
+
* The project a working folder belongs to, by the index's alias map: the
|
|
274
|
+
* folder itself, the repository root it is in, or their names. Mirrored in
|
|
275
|
+
* hooks/crbro-guard.mjs (tested).
|
|
276
|
+
*/
|
|
277
|
+
function projectOf(index, cwd) {
|
|
278
|
+
const map = index.projects;
|
|
279
|
+
if (!map || !cwd)
|
|
280
|
+
return null;
|
|
281
|
+
const candidates = [];
|
|
282
|
+
let dir = node_path_1.default.resolve(cwd);
|
|
283
|
+
for (let i = 0; i < 12; i++) {
|
|
284
|
+
candidates.push(dir);
|
|
285
|
+
if ((0, node_fs_1.existsSync)(node_path_1.default.join(dir, '.git')))
|
|
286
|
+
break;
|
|
287
|
+
const up = node_path_1.default.dirname(dir);
|
|
288
|
+
if (up === dir)
|
|
289
|
+
break;
|
|
290
|
+
dir = up;
|
|
291
|
+
}
|
|
292
|
+
// The folder and the repository root, by full path first, then by name.
|
|
293
|
+
const firstAndRoot = candidates.length > 1 ? [candidates[0], candidates[candidates.length - 1]] : candidates;
|
|
294
|
+
for (const c of firstAndRoot) {
|
|
295
|
+
const full = c.toLowerCase().replace(/\\/g, '/').replace(/\/+$/, '');
|
|
296
|
+
if (map[full])
|
|
297
|
+
return map[full];
|
|
298
|
+
}
|
|
299
|
+
for (const c of firstAndRoot) {
|
|
300
|
+
const name = node_path_1.default.basename(c).toLowerCase();
|
|
301
|
+
if (map[name])
|
|
302
|
+
return map[name];
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* What the hook would say for this command: the lessons whose keys it hits,
|
|
308
|
+
* from this project or from no project — another project's lesson speaks only
|
|
309
|
+
* when the command walks into that project's folder (a path: key) — never a
|
|
310
|
+
* teammate's or the miner's unless asked; this project's first, then errors,
|
|
311
|
+
* debts, patterns, newest first, capped.
|
|
312
|
+
*/
|
|
313
|
+
function lessonsFor(index, command, max = exports.MAX_LESSONS, opts = {}) {
|
|
314
|
+
const hits = new Map(); // entry → reached through a path: key
|
|
315
|
+
const walked = new Set(); // projects whose folder the command walks into
|
|
316
|
+
for (const k of keysOfCommand(command)) {
|
|
317
|
+
if (k.startsWith('path:') && index.projects?.[k.slice(5)])
|
|
318
|
+
walked.add(index.projects[k.slice(5)]);
|
|
201
319
|
for (const i of index.keys[k] || [])
|
|
202
|
-
hits.
|
|
320
|
+
hits.set(i, hits.get(i) || k.startsWith('path:'));
|
|
321
|
+
}
|
|
322
|
+
const here = projectOf(index, opts.cwd);
|
|
203
323
|
const rank = { error: 0, debt: 1, pattern: 2 };
|
|
204
|
-
|
|
205
|
-
|
|
324
|
+
const mine = (l) => (here && l.p?.includes(here) ? 0 : 1);
|
|
325
|
+
return [...hits].map(([i, viaPath]) => ({ l: index.entries[i], viaPath }))
|
|
326
|
+
.filter(({ l, viaPath }) => !!l
|
|
327
|
+
&& (!l.o || !!opts.allOrigins)
|
|
328
|
+
&& (!l.p?.length || (here !== null && l.p.includes(here)) || viaPath || l.p.some(x => walked.has(x))))
|
|
329
|
+
.map(({ l }) => l)
|
|
330
|
+
.sort((a, b) => mine(a) - mine(b) || rank[a.k] - rank[b.k] || (a.d < b.d ? 1 : a.d > b.d ? -1 : a.e < b.e ? -1 : 1))
|
|
206
331
|
.slice(0, max);
|
|
207
332
|
}
|
|
208
333
|
function triggerIndexPath(brain) {
|
|
@@ -229,7 +354,12 @@ async function loadAllNeurons(brain) {
|
|
|
229
354
|
async function writeTriggerIndex(brain, preloaded) {
|
|
230
355
|
try {
|
|
231
356
|
const neurons = preloaded ?? await loadAllNeurons(brain);
|
|
232
|
-
|
|
357
|
+
let registry = null;
|
|
358
|
+
try {
|
|
359
|
+
registry = await (0, fs_js_1.readJSON)(node_path_1.default.join(brain.paths.root, 'projects.json'));
|
|
360
|
+
}
|
|
361
|
+
catch { /* no projects yet */ }
|
|
362
|
+
const index = buildTriggerIndex(neurons, (0, fs_js_1.now)(), registry);
|
|
233
363
|
await (0, fs_js_1.writeJSON)(triggerIndexPath(brain), index, { pretty: false });
|
|
234
364
|
return { entries: index.entries.length, keys: Object.keys(index.keys).length };
|
|
235
365
|
}
|
package/dist/index.js
CHANGED
|
@@ -16,26 +16,60 @@ async function main() {
|
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
18
|
const { resolveBrainDir } = await import('./engine/brain.js');
|
|
19
|
-
const { daemonEnabled } = await import('./daemon/endpoint.js');
|
|
19
|
+
const { daemonEnabled, buildId } = await import('./daemon/endpoint.js');
|
|
20
|
+
const { trace, traceStart, TRACE_ON } = await import('./daemon/trace.js');
|
|
20
21
|
const brainRoot = resolveBrainDir();
|
|
21
22
|
if (daemonEnabled(brainRoot)) {
|
|
23
|
+
traceStart(brainRoot, 'proxy');
|
|
22
24
|
const { runProxy, spawnDetachedDaemon } = await import('./daemon/proxy.js');
|
|
25
|
+
const { loadSurface } = await import('./daemon/surface.js');
|
|
26
|
+
const build = buildId();
|
|
27
|
+
// The answers to initialize and tools/list, from the snapshot the build
|
|
28
|
+
// wrote; without one (a bare tsc build) the proxy asks a real server here.
|
|
29
|
+
const surface = loadSurface(build) ?? undefined;
|
|
23
30
|
const proxy = runProxy({
|
|
24
31
|
input: process.stdin,
|
|
25
32
|
output: process.stdout,
|
|
26
33
|
brainRoot,
|
|
34
|
+
build,
|
|
35
|
+
surface,
|
|
27
36
|
spawnDaemon: () => spawnDetachedDaemon(brainRoot),
|
|
28
37
|
isEnabled: () => daemonEnabled(brainRoot),
|
|
29
38
|
log: line => process.stderr.write(`[crbro] ${line}\n`), // stdout is the MCP wire
|
|
39
|
+
trace,
|
|
30
40
|
});
|
|
31
41
|
await proxy.closed;
|
|
32
42
|
process.exit(0);
|
|
33
43
|
}
|
|
44
|
+
traceStart(brainRoot, 'classic');
|
|
34
45
|
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
|
35
|
-
const { createServer } = await import('./server.js');
|
|
36
|
-
const
|
|
46
|
+
const { createServer, createEngines } = await import('./server.js');
|
|
47
|
+
const engines = createEngines();
|
|
48
|
+
const server = createServer(engines);
|
|
37
49
|
const transport = new StdioServerTransport();
|
|
38
50
|
await server.connect(transport);
|
|
51
|
+
// Classic mode only: write a dirty index before leaving (2.11).
|
|
52
|
+
const { flushOnExit } = await import('./search/index.js');
|
|
53
|
+
flushOnExit(engines.searchEngine);
|
|
54
|
+
trace('engine ready', 'server connected (the index loads at crbro_boot)');
|
|
55
|
+
if (TRACE_ON) {
|
|
56
|
+
// Only with the trace on: watch the wire for the two moments that matter.
|
|
57
|
+
const toolCalls = new Set();
|
|
58
|
+
const onmessage = transport.onmessage;
|
|
59
|
+
transport.onmessage = (m) => {
|
|
60
|
+
if (m?.method === 'tools/call' && m.id !== undefined)
|
|
61
|
+
toolCalls.add(m.id);
|
|
62
|
+
onmessage?.(m);
|
|
63
|
+
};
|
|
64
|
+
const send = transport.send.bind(transport);
|
|
65
|
+
transport.send = (m) => {
|
|
66
|
+
if (m?.result?.serverInfo)
|
|
67
|
+
trace('initialize answered', 'by the server', true);
|
|
68
|
+
else if (m?.id !== undefined && toolCalls.delete(m.id))
|
|
69
|
+
trace('first tool result', '', true);
|
|
70
|
+
return send(m);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
39
73
|
}
|
|
40
74
|
main().catch((err) => {
|
|
41
75
|
console.error('CRBRO Fatal Error:', err);
|