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/hooks/crbro-guard.mjs
CHANGED
|
@@ -1,173 +1,281 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// CRBRO — Claude Code PreToolUse hook: memory at the moment of action
|
|
3
|
-
//
|
|
4
|
-
// Recall only answers when somebody asks, and nobody asks "have I broken
|
|
5
|
-
// this before?" one second before `firebase deploy`. This hook does: it
|
|
6
|
-
// looks the command up in the trigger index the server derives from the
|
|
7
|
-
// error, debt and pattern ledgers (<brain>/.search/triggers.json) and, when
|
|
8
|
-
// a stored lesson mentions that command, adds it to the model's context for
|
|
9
|
-
// this one tool call. It never blocks, never asks, never decides.
|
|
10
|
-
//
|
|
11
|
-
// Cost:
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*/
|
|
57
|
-
function
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
for (const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
function
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
let
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CRBRO — Claude Code PreToolUse hook: memory at the moment of action
|
|
3
|
+
//
|
|
4
|
+
// Recall only answers when somebody asks, and nobody asks "have I broken
|
|
5
|
+
// this before?" one second before `firebase deploy`. This hook does: it
|
|
6
|
+
// looks the command up in the trigger index the server derives from the
|
|
7
|
+
// error, debt and pattern ledgers (<brain>/.search/triggers.json) and, when
|
|
8
|
+
// a stored lesson mentions that command, adds it to the model's context for
|
|
9
|
+
// this one tool call. It never blocks, never asks, never decides.
|
|
10
|
+
//
|
|
11
|
+
// Cost: node's own start (~45 ms) and, for most commands, one small file: the
|
|
12
|
+
// list of keys the index has, cached in the temp folder by the index's mtime,
|
|
13
|
+
// so a command no lesson mentions never parses the whole index.
|
|
14
|
+
//
|
|
15
|
+
// Noise control: a lesson is shown once per session, at most three per
|
|
16
|
+
// command and six per session, errors before debts before patterns, newest
|
|
17
|
+
// first. Lessons of this project come first; a lesson tied to another project
|
|
18
|
+
// speaks only when the command walks into that project's folder; lessons that
|
|
19
|
+
// came from a team space or the miner stay out (CRBRO_GUARD_ORIGINS=all shows
|
|
20
|
+
// them, labelled with where they came from).
|
|
21
|
+
//
|
|
22
|
+
// OPT-IN (crbro install-hooks --guard). An injection nobody measured must not
|
|
23
|
+
// be a default; this one is small and removable, and says where it came from.
|
|
24
|
+
//
|
|
25
|
+
// Failure contract: any error — no index, bad JSON, stdin that never ends —
|
|
26
|
+
// degrades to silence. Always exit 0.
|
|
27
|
+
//
|
|
28
|
+
// The key extraction below mirrors keysOfCommand() in src/engine/triggers.ts.
|
|
29
|
+
// tests/triggers.test.ts runs this file against that function: they cannot
|
|
30
|
+
// drift without a red test.
|
|
31
|
+
|
|
32
|
+
import { readFileSync, writeFileSync, mkdirSync, statSync, readdirSync, rmSync, existsSync } from 'node:fs';
|
|
33
|
+
import { join, isAbsolute, resolve, dirname, basename } from 'node:path';
|
|
34
|
+
import { homedir, tmpdir } from 'node:os';
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
const MAX_LESSONS = 3;
|
|
38
|
+
/** Lessons per session: past this the guard stays quiet for the rest of it. */
|
|
39
|
+
const SESSION_MAX = 6;
|
|
40
|
+
/** Index formats this hook reads (src/engine/triggers.ts TRIGGER_INDEX_VERSION and the one before). */
|
|
41
|
+
const INDEX_VERSIONS = new Set([2, 3]);
|
|
42
|
+
const WEEK_MS = 7 * 24 * 3600 * 1000;
|
|
43
|
+
const WRAPPERS = new Set(['npx', 'bunx', 'pnpx', 'sudo', 'time', 'call', 'exec', 'start']);
|
|
44
|
+
const INTERPRETERS = new Set(['node', 'deno', 'bun', 'python', 'python3', 'py', 'bash', 'sh', 'zsh', 'powershell', 'pwsh', 'tsx', 'ts-node', 'php', 'ruby', 'perl']);
|
|
45
|
+
/** Two words that still name nothing: `npm run` is not an action, `npm run build` is. */
|
|
46
|
+
const DEEP = new Set(('npm run|yarn run|pnpm run|bun run|docker compose|wp plugin|wp post|wp option|wp theme|wp user|wp cache|wp db|'
|
|
47
|
+
+ 'gh pr|gh issue|gh repo|gh release|gh run|gcloud run|gcloud auth|gcloud functions|gcloud app|git remote').split('|'));
|
|
48
|
+
/** A flag names an action only where it changes what the program does to your files. `curl -s` and `grep -n` are not lessons. */
|
|
49
|
+
const FLAG_PAIRS = new Set(['rm -rf', 'rm -r', 'rm -fr', 'git -c', 'cp -r', 'node --check', 'node -e', 'python -c', 'python -m', 'python3 -m', 'npx -y']);
|
|
50
|
+
const SCRIPT = /\.(ps1|sh|bash|py|mjs|cjs|js|ts|bat|cmd|rb|php)$/;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Same resolution as resolveBrainDir() in src/engine/brain.ts and brainDir() in
|
|
54
|
+
* the other hooks: CRBRO_PATH (CRBRO_BRAIN_PATH as an alias), with ~, relative
|
|
55
|
+
* paths and unexpanded placeholders handled. Tested against the source.
|
|
56
|
+
*/
|
|
57
|
+
export function brainDir(env = process.env) {
|
|
58
|
+
const home = env.HOME || env.USERPROFILE || homedir();
|
|
59
|
+
const fallback = join(home, '.crbro');
|
|
60
|
+
const raw = (env.CRBRO_PATH || env.CRBRO_BRAIN_PATH || '').trim();
|
|
61
|
+
if (!raw) return fallback;
|
|
62
|
+
if (/\$\{|%[A-Za-z_][A-Za-z0-9_]*%/.test(raw)) return fallback;
|
|
63
|
+
if (raw === '~' || raw.startsWith('~/') || raw.startsWith('~\\')) return join(home, raw.slice(1));
|
|
64
|
+
if (!isAbsolute(raw)) return join(home, raw);
|
|
65
|
+
return raw;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function word(raw) {
|
|
69
|
+
const w = raw.replace(/^["'`(]+|["'`),;:.]+$/g, '').toLowerCase();
|
|
70
|
+
if (!w || w.startsWith('-')) return w;
|
|
71
|
+
return w.split(/[\\/]/).pop() || w;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A heredoc body is data, not commands: a Python script fed through <<'EOF'
|
|
76
|
+
* named every file it mentioned and woke lessons that had nothing to do with
|
|
77
|
+
* the command. Procedural on purpose — no backreferences to get wrong.
|
|
78
|
+
*/
|
|
79
|
+
function stripHeredocs(command) {
|
|
80
|
+
const out = [];
|
|
81
|
+
let end = null;
|
|
82
|
+
for (const line of command.split('\n')) {
|
|
83
|
+
if (end !== null) { if (line.trim() === end) end = null; continue; }
|
|
84
|
+
out.push(line);
|
|
85
|
+
const m = /<<-?\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?/.exec(line);
|
|
86
|
+
if (m) end = m[1];
|
|
87
|
+
}
|
|
88
|
+
return out.join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Mirrors pathKeys() in src/engine/triggers.ts: the project folders a text walks into. */
|
|
92
|
+
function pathKeys(text) {
|
|
93
|
+
const keys = new Set();
|
|
94
|
+
for (const m of text.matchAll(/[A-Za-z0-9._-]{2,}[\\/][A-Za-z0-9._\\/-]{2,}/g)) {
|
|
95
|
+
for (const seg of m[0].split(/[\\/]+/)) {
|
|
96
|
+
const s = seg.toLowerCase();
|
|
97
|
+
if (s.length >= 6 && s.includes('-') && !/^-|-$/.test(s) && !/\.[a-z0-9]{1,5}$/.test(s)) keys.add(`path:${s}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return [...keys];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function keysOfCommand(command) {
|
|
104
|
+
const keys = new Set();
|
|
105
|
+
const body = stripHeredocs(command);
|
|
106
|
+
for (const k of pathKeys(body)) keys.add(k);
|
|
107
|
+
for (const segment of body.split(/&&|\|\||[;|\n]/)) {
|
|
108
|
+
let t = segment.trim().split(/\s+/).map(word).filter(Boolean);
|
|
109
|
+
while (t.length && (/^[a-z_][a-z0-9_]*=/.test(t[0]) || t[0] === '&' || WRAPPERS.has(t[0]))) {
|
|
110
|
+
if (WRAPPERS.has(t[0]) && t[1] && !t[1].startsWith('-')) keys.add(`${t[0]} ${t[1]}`);
|
|
111
|
+
t = t.slice(1);
|
|
112
|
+
}
|
|
113
|
+
if (t.length === 0) continue;
|
|
114
|
+
if (SCRIPT.test(t[0])) keys.add(t[0]);
|
|
115
|
+
if (t[1]) {
|
|
116
|
+
const pair = `${t[0]} ${t[1]}`;
|
|
117
|
+
if (!t[1].startsWith('-') || FLAG_PAIRS.has(pair)) keys.add(DEEP.has(pair) && t[2] && !t[2].startsWith('-') ? `${pair} ${t[2]}` : pair);
|
|
118
|
+
if (INTERPRETERS.has(t[0])) {
|
|
119
|
+
const file = t.slice(1).find(x => !x.startsWith('-'));
|
|
120
|
+
if (file && SCRIPT.test(file)) keys.add(file);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return [...keys];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Mirrors projectOf() in src/engine/triggers.ts: the project the working folder (or its repository root) belongs to. */
|
|
128
|
+
export function projectOf(index, cwd) {
|
|
129
|
+
const map = index && index.projects;
|
|
130
|
+
if (!map || !cwd) return null;
|
|
131
|
+
const candidates = [];
|
|
132
|
+
let dir = resolve(cwd);
|
|
133
|
+
for (let i = 0; i < 12; i++) {
|
|
134
|
+
candidates.push(dir);
|
|
135
|
+
if (existsSync(join(dir, '.git'))) break;
|
|
136
|
+
const up = dirname(dir);
|
|
137
|
+
if (up === dir) break;
|
|
138
|
+
dir = up;
|
|
139
|
+
}
|
|
140
|
+
const firstAndRoot = candidates.length > 1 ? [candidates[0], candidates[candidates.length - 1]] : candidates;
|
|
141
|
+
for (const c of firstAndRoot) {
|
|
142
|
+
const full = c.toLowerCase().replace(/\\/g, '/').replace(/\/+$/, '');
|
|
143
|
+
if (map[full]) return map[full];
|
|
144
|
+
}
|
|
145
|
+
for (const c of firstAndRoot) {
|
|
146
|
+
const name = basename(c).toLowerCase();
|
|
147
|
+
if (map[name]) return map[name];
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Mirrors lessonsFor() in src/engine/triggers.ts (tested): this project's, or no project's, or walked into; never another origin unless asked. */
|
|
153
|
+
export function lessonsFor(index, command, opts = {}) {
|
|
154
|
+
const hits = new Map();
|
|
155
|
+
const walked = new Set();
|
|
156
|
+
for (const k of keysOfCommand(command)) {
|
|
157
|
+
if (k.startsWith('path:') && index.projects && index.projects[k.slice(5)]) walked.add(index.projects[k.slice(5)]);
|
|
158
|
+
for (const i of index.keys[k] || []) hits.set(i, hits.get(i) || k.startsWith('path:'));
|
|
159
|
+
}
|
|
160
|
+
const here = projectOf(index, opts.cwd);
|
|
161
|
+
const rank = { error: 0, debt: 1, pattern: 2 };
|
|
162
|
+
const mine = l => (here && Array.isArray(l.p) && l.p.includes(here) ? 0 : 1);
|
|
163
|
+
return [...hits].map(([i, viaPath]) => ({ l: index.entries[i], viaPath }))
|
|
164
|
+
.filter(({ l, viaPath }) => !!l
|
|
165
|
+
&& (!l.o || !!opts.allOrigins)
|
|
166
|
+
&& (!Array.isArray(l.p) || !l.p.length || (here !== null && l.p.includes(here)) || viaPath || l.p.some(x => walked.has(x))))
|
|
167
|
+
.map(({ l }) => l)
|
|
168
|
+
.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));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Which lessons this session already saw. A temp file per session; losing it only repeats a lesson. */
|
|
172
|
+
function seenStore(sessionId) {
|
|
173
|
+
const safe = String(sessionId || 'nosession').replace(/[^A-Za-z0-9_-]/g, '').slice(0, 80) || 'nosession';
|
|
174
|
+
const dir = join(tmpdir(), 'crbro-guard');
|
|
175
|
+
const file = join(dir, `${safe}.json`);
|
|
176
|
+
let seen = [];
|
|
177
|
+
try { seen = JSON.parse(readFileSync(file, 'utf8')); } catch { /* first call of the session */ }
|
|
178
|
+
return {
|
|
179
|
+
count: () => seen.length,
|
|
180
|
+
has: id => seen.includes(id),
|
|
181
|
+
add: ids => {
|
|
182
|
+
try {
|
|
183
|
+
mkdirSync(dir, { recursive: true });
|
|
184
|
+
writeFileSync(file, JSON.stringify([...seen, ...ids]));
|
|
185
|
+
// Sessions older than a week leave nothing behind.
|
|
186
|
+
for (const name of readdirSync(dir)) {
|
|
187
|
+
const p = join(dir, name);
|
|
188
|
+
try { if (Date.now() - statSync(p).mtimeMs > WEEK_MS) rmSync(p, { force: true }); } catch { /* next time */ }
|
|
189
|
+
}
|
|
190
|
+
} catch { /* best effort */ }
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The keys the index has, cached in the temp folder per index file and keyed
|
|
197
|
+
* by its mtime and size: a command that hits none of them is answered without
|
|
198
|
+
* parsing the whole index. Null when there is no fresh cache.
|
|
199
|
+
*/
|
|
200
|
+
function keysCache(file, st) {
|
|
201
|
+
// FNV-1a of the path: one name per index file, without loading node:crypto on every command.
|
|
202
|
+
let h = 0x811c9dc5;
|
|
203
|
+
for (let i = 0; i < file.length; i++) { h ^= file.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; }
|
|
204
|
+
const name = h.toString(16);
|
|
205
|
+
const path = join(tmpdir(), 'crbro-guard', `keys-${name}.json`);
|
|
206
|
+
let keys = null;
|
|
207
|
+
try {
|
|
208
|
+
const c = JSON.parse(readFileSync(path, 'utf8'));
|
|
209
|
+
if (c.mtimeMs === st.mtimeMs && c.size === st.size && Array.isArray(c.keys)) keys = new Set(c.keys);
|
|
210
|
+
} catch { /* none yet */ }
|
|
211
|
+
return {
|
|
212
|
+
keys,
|
|
213
|
+
save: list => {
|
|
214
|
+
try {
|
|
215
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
216
|
+
writeFileSync(path, JSON.stringify({ mtimeMs: st.mtimeMs, size: st.size, keys: list }));
|
|
217
|
+
} catch { /* best effort */ }
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function respond(input) {
|
|
223
|
+
const command = input?.tool_input?.command;
|
|
224
|
+
if (typeof command !== 'string' || !command.trim()) return null;
|
|
225
|
+
|
|
226
|
+
const wanted = keysOfCommand(command);
|
|
227
|
+
if (wanted.length === 0) return null; // nothing in it any lesson could be about
|
|
228
|
+
const file = join(brainDir(), '.search', 'triggers.json');
|
|
229
|
+
let st;
|
|
230
|
+
try { st = statSync(file); } catch { return null; } // no index yet: the next crbro_consolidate writes it
|
|
231
|
+
const cache = keysCache(file, st);
|
|
232
|
+
if (cache.keys && !wanted.some(k => cache.keys.has(k))) return null;
|
|
233
|
+
|
|
234
|
+
let index;
|
|
235
|
+
try {
|
|
236
|
+
index = JSON.parse(readFileSync(file, 'utf8'));
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
// Mirrors TRIGGER_INDEX_VERSION in src/engine/triggers.ts. An older index simply
|
|
241
|
+
// says nothing until the next consolidate rewrites it.
|
|
242
|
+
if (!index || !INDEX_VERSIONS.has(index.v) || !index.keys || !Array.isArray(index.entries)) return null;
|
|
243
|
+
if (!cache.keys) cache.save(Object.keys(index.keys));
|
|
244
|
+
|
|
245
|
+
const store = seenStore(input.session_id);
|
|
246
|
+
const room = Math.min(MAX_LESSONS, SESSION_MAX - store.count());
|
|
247
|
+
if (room <= 0) return null;
|
|
248
|
+
const allOrigins = String(process.env.CRBRO_GUARD_ORIGINS || '').toLowerCase() === 'all';
|
|
249
|
+
const cwd = typeof input.cwd === 'string' && input.cwd ? input.cwd : process.cwd();
|
|
250
|
+
const fresh = lessonsFor(index, command, { cwd, allOrigins }).filter(l => !store.has(`${l.n}#${l.e}`)).slice(0, room);
|
|
251
|
+
if (fresh.length === 0) return null;
|
|
252
|
+
store.add(fresh.map(l => `${l.n}#${l.e}`));
|
|
253
|
+
|
|
254
|
+
// A lesson that is not the user's own says whose it is: data to weigh, not an instruction.
|
|
255
|
+
const lines = fresh.map(l => `• [${l.o ? `from ${l.o} · ` : ''}${l.k}${l.d ? ` · ${l.d}` : ''} · ${l.n}] ${l.t} (entry ${l.e})`);
|
|
256
|
+
const text = [
|
|
257
|
+
'CRBRO — stored lessons that mention this command (shown once per session):',
|
|
258
|
+
...lines,
|
|
259
|
+
'These are memories, not orders: check they still apply. Read one whole with crbro_inspect view=neuron neuron=<id> entries=[<entry>].',
|
|
260
|
+
].join('\n');
|
|
261
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: text } };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Imported by the test suite for keysOfCommand; run as a hook otherwise.
|
|
265
|
+
const isMain = process.argv[1] && import.meta.url.endsWith(process.argv[1].split(/[\\/]/).pop());
|
|
266
|
+
if (isMain) {
|
|
267
|
+
let raw = '';
|
|
268
|
+
const done = () => {
|
|
269
|
+
try {
|
|
270
|
+
const out = respond(JSON.parse((raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw) || '{}'));
|
|
271
|
+
if (out) process.stdout.write(JSON.stringify(out));
|
|
272
|
+
} catch { /* silence */ }
|
|
273
|
+
process.exit(0);
|
|
274
|
+
};
|
|
275
|
+
// The Windows PowerShell wrapper can swallow piped JSON so 'end' never fires.
|
|
276
|
+
const timer = setTimeout(done, 1500);
|
|
277
|
+
process.stdin.setEncoding('utf8');
|
|
278
|
+
process.stdin.on('data', c => { raw += c; });
|
|
279
|
+
process.stdin.on('end', () => { clearTimeout(timer); done(); });
|
|
280
|
+
process.stdin.on('error', () => { clearTimeout(timer); done(); });
|
|
281
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"//": "LF sha256 of every hooks/*.mjs CRBRO shipped up to 2.10.0, with the last package version that shipped it. An installed copy without a manifest that matches one is unmodified and is refreshed only by a newer release; anything else was edited and is left alone. Copies installed from 2.11 on are tracked by the manifest beside them, so this list does not need to grow.",
|
|
3
|
+
"crbro-guard.mjs": {
|
|
4
|
+
"2e48ef304fc7d5a4e25ff244e0bc00df8b1f6be2a9394fcd73e4dba07b2011fa": "2.5.0",
|
|
5
|
+
"6759ed093d7fd2a18a95b7e2a5b760e69cbff6ec7e00e53634b3c80b581f0a3f": "2.5.0",
|
|
6
|
+
"f95ae9748132f8f3fcd1e9ad504e1ef9c903aea86cbfee776c5f84c4a1b0b498": "2.5.0",
|
|
7
|
+
"e795f02ddcd0298769ff14c65c6c7bef71b6f574b9d22c745cbba85bf0d37fb7": "2.5.1",
|
|
8
|
+
"5bc7f192cc19a3bab4ad23924ebde1a71e34016f9373ea12124274a26ff516fc": "2.10.0"
|
|
9
|
+
},
|
|
10
|
+
"crbro-lifecycle.mjs": {
|
|
11
|
+
"fabd4da9dd74a0076316fc4005e81e994f10068d60d6b2445edb485fd8f97dcc": "2.7.0",
|
|
12
|
+
"6f435794f93f7afd6a3799098b6ed20734a4710223a403d73d32b761b6a42707": "2.9.0",
|
|
13
|
+
"2466c9c02cece56a0c6a352d3d869209a01a5e16b525c7aa5cdd5989efb2268e": "2.10.0"
|
|
14
|
+
},
|
|
15
|
+
"crbro-subagent.mjs": {
|
|
16
|
+
"c154e8a5772bb0773fb5ba12f87a03ee3c03e466a5e26dcccacc75a7acba5cf6": "1.11.1",
|
|
17
|
+
"13f61c686fbd8faba8c1647d19e47cec9b2cd466f7fa20fc5fbfc044dcc06b3f": "2.6.0",
|
|
18
|
+
"dbdd768f138892f5617502e9f3b03086d7a8ec9b4258f20f17c1430f2eff1108": "2.10.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -108,7 +108,10 @@ export const SECRET_PATTERNS = [
|
|
|
108
108
|
kind: 'auth token (hex)',
|
|
109
109
|
re: /\bauth[ _-]?token\b[^\S\n]{0,3}(?:[:=]|es|is)?[^\S\n]{0,3}["'`]?([0-9a-fA-F]{30,34})\b/gi,
|
|
110
110
|
},
|
|
111
|
-
{
|
|
111
|
+
{
|
|
112
|
+
kind: 'private key block',
|
|
113
|
+
re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----(?:[\s\S]{0,16384}?-----END [A-Z0-9 ]*PRIVATE KEY-----|(?:\s{1,4}(?:[A-Za-z0-9+/]{16,}={0,2}|[A-Za-z0-9+/]{2,}={1,2})(?!\S)){0,256})/g,
|
|
114
|
+
},
|
|
112
115
|
{ kind: 'JSON Web Token', re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
|
|
113
116
|
{
|
|
114
117
|
kind: 'WordPress application password',
|
|
@@ -116,7 +119,7 @@ export const SECRET_PATTERNS = [
|
|
|
116
119
|
},
|
|
117
120
|
{
|
|
118
121
|
kind: 'password',
|
|
119
|
-
re: /\b(?:
|
|
122
|
+
re: /\b(?:[Pp]assword|PASSWORD|[Pp]asswd|PASSWD|[Cc]ontrase[nñ]a|CONTRASE[NÑ]A)\s*(?:es|is|ES|IS)?\s*[:=]\s*["'`]?(?![A-Z][a-z]+(?![^\s"'`,;]))(?![A-Za-z_]+=)(?!~?\/)([^\s"'`,;]{8,})/g,
|
|
120
123
|
},
|
|
121
124
|
{
|
|
122
125
|
kind: 'password',
|
|
@@ -126,6 +129,14 @@ export const SECRET_PATTERNS = [
|
|
|
126
129
|
kind: 'password (prose)',
|
|
127
130
|
re: /\b(?:password|passwd|contrase[nñ]a|clave)\b(?:\s+(?:de|del|of|for)\s+[^\s]{1,30})?\s+(?:es|is)\s+["'`]?((?=[^\s"'`,;]*[A-Za-z])(?=[^\s"'`,;]*[0-9])[^\s"'`,;]{8,})/gi,
|
|
128
131
|
},
|
|
132
|
+
{
|
|
133
|
+
kind: 'password',
|
|
134
|
+
re: /\b(?:PASS|PASSWORD|PASSWD|[Pp]assword|[Pp]asswd|[Cc]ontrase[nñ]a|CONTRASE[NÑ]A)\b[^\S\n]+["'`]?((?:(?=[^\s"'`,;()]*[a-z])(?=[^\s"'`,;()]*[A-Z])(?=[^\s"'`,;()]*[0-9])|(?=[^\s"'`,;()]*[a-z])(?=[^\s"'`,;()]*[A-Z])(?=[^\s"'`,;()]*[!@#$%^&*+=?~])|(?=[^\s"'`,;()]*[a-z])(?=[^\s"'`,;()]*[0-9])(?=[^\s"'`,;()]*[!@#$%^&*+=?~])|(?=[^\s"'`,;()]*[A-Z])(?=[^\s"'`,;()]*[0-9])(?=[^\s"'`,;()]*[!@#$%^&*+=?~]))[^\s"'`,;()]{8,})/g,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
kind: 'password',
|
|
138
|
+
re: /\b(?:PASS|[Pp]ass)\b(?:[^\S\n]*[:=][^\S\n]*["'`]?((?=[^\s"'`,;()]*[A-Za-z])(?=[^\s"'`,;()]*[0-9!@#$%^&*+=?~])[^\s"'`,;()]{8,})|[^\S\n]+["'`]?(?![$-])((?=[^\s"'`,;()]*[A-Za-z])(?=[^\s"'`,;()]*[0-9])(?=[^\s"'`,;()]*[!@#$%^&*+=?~])[^\s"'`,;()]{8,}))/g,
|
|
139
|
+
},
|
|
129
140
|
{
|
|
130
141
|
kind: 'split credential',
|
|
131
142
|
re: /\b(?:token|key|clave|password|contrase[nñ]a|secret|secreto)\b[^.?!\n]{0,40}?\b(?:empieza|comienza|starts?)\s+(?:por|con|with)\s+(\S{2,15}[^.?!\n]{0,30}?\b(?:sigue con|contin[uú]a con|y luego|followed by|then|continues with)\s+["'`]?(?=[A-Za-z0-9_\-+/=]*[A-Za-z])(?=[A-Za-z0-9_\-+/=]*[0-9])[A-Za-z0-9_\-+/=]{16,})/gi,
|
|
@@ -150,8 +161,9 @@ export function redact(text) {
|
|
|
150
161
|
re.lastIndex = 0;
|
|
151
162
|
let m;
|
|
152
163
|
while ((m = re.exec(text)) !== null) {
|
|
153
|
-
const
|
|
154
|
-
const
|
|
164
|
+
const group = m.slice(1).find(g => g !== undefined);
|
|
165
|
+
const value = group !== undefined ? group : m[0];
|
|
166
|
+
const at = group !== undefined ? m.index + m[0].lastIndexOf(group) : m.index;
|
|
155
167
|
hits.push({ kind, index: at, length: value.length });
|
|
156
168
|
if (m[0].length === 0) re.lastIndex++;
|
|
157
169
|
}
|