monomind 2.1.4 → 2.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monomind",
3
- "version": "2.1.4",
3
+ "version": "2.1.6",
4
4
  "description": "Open-source CLI extension for Claude Code. Adds an MCP server with a codebase knowledge graph, persistent memory, multi-agent coordination, and reusable slash commands. MIT licensed, runs locally, no data leaves your machine.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -41,10 +41,14 @@ function subagentKey(hookInput) {
41
41
  return crypto.createHash('md5').update(String(raw)).digest('hex').slice(0, 16);
42
42
  }
43
43
 
44
- // Determine subagent success from its result summary — errors/failures/
45
- // exceptions indicate failure. Shared by the Monograph `success` column and
46
- // the intelligence.feedback() call below, so both use one definition.
47
- function deriveSubagentSuccess(summary) {
44
+ // Determine subagent success. `lastToolError` (was the transcript's final
45
+ // tool_result an is_error block a real signal parsed straight off the
46
+ // transcript, not text sniffing) is authoritative when present; the summary
47
+ // keyword check is a fallback for failures narrated without a tool error
48
+ // (e.g. "I couldn't find X" with no failing tool call). Shared by the
49
+ // Monograph `success` column and the intelligence.feedback() call below.
50
+ function deriveSubagentSuccess(summary, lastToolError) {
51
+ if (lastToolError) return false;
48
52
  if (!summary) return true;
49
53
  var sumLower = summary.toLowerCase();
50
54
  if (sumLower.includes('error') || sumLower.includes('failed') || sumLower.includes('exception') || sumLower.includes('fatal')) {
@@ -100,6 +104,7 @@ function parseJSONLForData(filePath) {
100
104
  let tin = 0, tout = 0;
101
105
  let allMsgs = [];
102
106
  let toolCalls = [];
107
+ let lastToolError = false; // tracks the most recent tool_result seen, in file order
103
108
  try {
104
109
  const lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
105
110
  for (const line of lines) {
@@ -119,6 +124,12 @@ function parseJSONLForData(filePath) {
119
124
  } else if (typeof content === 'string' && content) {
120
125
  allMsgs.push(content);
121
126
  }
127
+ } else if (d?.message?.role === 'user') {
128
+ const content = d?.message?.content;
129
+ if (Array.isArray(content)) {
130
+ const results = content.filter(b => b.type === 'tool_result');
131
+ if (results.length > 0) lastToolError = results.some(b => b.is_error === true);
132
+ }
122
133
  }
123
134
  } catch { /* skip malformed lines */ }
124
135
  }
@@ -129,7 +140,7 @@ function parseJSONLForData(filePath) {
129
140
  const summary = allMsgs.length > 1
130
141
  ? (firstMsg.slice(0, 300) + (allMsgs.length > 2 ? `\n…[${allMsgs.length - 2} more msgs]…\n` : '\n') + lastMsg.slice(0, 700))
131
142
  : lastMsg.slice(0, 1000);
132
- return { tokens_in: tin, tokens_out: tout, summary, last_msg: lastMsg, toolCalls: [...new Set(toolCalls)] };
143
+ return { tokens_in: tin, tokens_out: tout, summary, last_msg: lastMsg, toolCalls: [...new Set(toolCalls)], lastToolError };
133
144
  }
134
145
 
135
146
  function getActiveRun() {
@@ -335,6 +346,7 @@ async function handleSubagentStop(hookInput) {
335
346
  let totalTin = 0, totalTout = 0;
336
347
  let summary = '';
337
348
  let toolCalls = [];
349
+ let lastToolError = false;
338
350
  const capturedFiles = [];
339
351
 
340
352
  for (const f of currentFiles) {
@@ -344,6 +356,7 @@ async function handleSubagentStop(hookInput) {
344
356
  totalTout += parsed.tokens_out;
345
357
  if (parsed.summary) summary = parsed.summary;
346
358
  toolCalls.push(...parsed.toolCalls);
359
+ lastToolError = parsed.lastToolError; // last new file wins, mirroring `summary`'s last-wins semantics
347
360
  capturedFiles.push(f.name);
348
361
  }
349
362
  }
@@ -365,7 +378,7 @@ async function handleSubagentStop(hookInput) {
365
378
  // (TeammateIdle/TaskCompleted) is dead code (those aren't valid Claude
366
379
  // Code hook events and are stripped from settings.json on init).
367
380
  try {
368
- require('../intelligence.cjs').feedback(deriveSubagentSuccess(summary));
381
+ require('../intelligence.cjs').feedback(deriveSubagentSuccess(summary, lastToolError));
369
382
  } catch (e) { /* non-fatal — feedback recording must never block subagent-stop */ }
370
383
 
371
384
  if (!org && !session) {
@@ -509,7 +522,7 @@ async function handleSubagentStop(hookInput) {
509
522
  tokens_in: totalTin || 0,
510
523
  tokens_out: totalTout || 0,
511
524
  cost_usd: costUsd || 0,
512
- success: deriveSubagentSuccess(summary) ? 1 : 0,
525
+ success: deriveSubagentSuccess(summary, lastToolError) ? 1 : 0,
513
526
  duration_ms: snap.ts ? (Date.now() - snap.ts) : 0,
514
527
  timestamp: Date.now(),
515
528
  });
@@ -643,12 +656,16 @@ async function handlePostTool(hookInput) {
643
656
 
644
657
  // ─── Dispatch ─────────────────────────────────────────────────────────────────
645
658
 
646
- const eventType = process.argv[2]; // 'subagent-start' | 'subagent-stop' | 'pretool' | 'posttool'
659
+ if (require.main === module) {
660
+ const eventType = process.argv[2]; // 'subagent-start' | 'subagent-stop' | 'pretool' | 'posttool'
661
+
662
+ readStdin().then(hookInput => {
663
+ if (eventType === 'subagent-start') return handleSubagentStart(hookInput);
664
+ if (eventType === 'subagent-stop') return handleSubagentStop(hookInput);
665
+ if (eventType === 'pretool') return handlePreTool(hookInput);
666
+ if (eventType === 'posttool') return handlePostTool(hookInput);
667
+ console.log('[CAPTURE] unknown event type: ' + eventType);
668
+ }).catch(() => process.exit(0));
669
+ }
647
670
 
648
- readStdin().then(hookInput => {
649
- if (eventType === 'subagent-start') return handleSubagentStart(hookInput);
650
- if (eventType === 'subagent-stop') return handleSubagentStop(hookInput);
651
- if (eventType === 'pretool') return handlePreTool(hookInput);
652
- if (eventType === 'posttool') return handlePostTool(hookInput);
653
- console.log('[CAPTURE] unknown event type: ' + eventType);
654
- }).catch(() => process.exit(0));
671
+ module.exports = { deriveSubagentSuccess, parseJSONLForData };
@@ -1,9 +1,13 @@
1
1
  /**
2
2
  * Enforcement Gates Handler
3
3
  *
4
- * Inline gate logic for PreToolUse hooks — no ESM/package dependency.
5
- * This regex table is the canonical (and only) gate definition; the former
6
- * @monomind/guidance package it mirrored has been removed.
4
+ * Runs on every PreToolUse — must stay fast (no package import).
5
+ *
6
+ * The regex tables below are the canonical (and only) gate definition; the
7
+ * @monomind/guidance package that used to compile them was removed. An optional
8
+ * project override can still be supplied at `.monomind/guidance/active-gates.json`
9
+ * ({destructivePatterns, secretPatterns} as {source, flags} pairs) — if present
10
+ * and valid it replaces the built-in tables.
7
11
  *
8
12
  * Gates enforced at runtime:
9
13
  * pre-bash → destructive-ops (hard block; no confirm-and-proceed path exists)
@@ -12,29 +16,150 @@
12
16
 
13
17
  'use strict';
14
18
 
15
- // ─── Patterns (superset of DEFAULT_GATE_CONFIG in gates.ts) ──────────────────
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ // ─── monofence-ai integration (additional layer on top of regex gates) ───────
23
+ //
24
+ // monofence-ai (packages/monofence-ai) has real prompt-injection / evasion
25
+ // detection but is never wired into the live Claude Code PreToolUse path —
26
+ // it only registers hooks on @monomind/hooks' internal HookRegistry, which
27
+ // this CJS dispatch path does not use. This loads it lazily (so a missing
28
+ // or unbuilt package never breaks the existing regex gates), bounds it with
29
+ // a hard timeout, and fails open on any error.
30
+
31
+ const MONOFENCE_TIMEOUT_MS = 1500;
32
+ const MONOFENCE_ABORT_THRESHOLD = 0.8;
33
+
34
+ let _monofenceModulePromise = null;
35
+
36
+ /**
37
+ * Resolve and import monofence-ai. Bare-specifier `import('monofence-ai')`
38
+ * only works when this file's ancestor node_modules chain contains the
39
+ * package (pnpm hoists it only into packages that declare it as a direct
40
+ * dependency, e.g. @monomind/cli) — so we also try resolving it explicitly
41
+ * from likely workspace locations before falling back to the bare import.
42
+ */
43
+ /**
44
+ * Walk up from `startDir` looking for `<dir>/node_modules/monofence-ai/package.json`.
45
+ * Package "exports" maps intentionally omit "./package.json" as a subpath, so
46
+ * `require.resolve('monofence-ai/package.json', ...)` throws even when the
47
+ * package is present — a plain filesystem walk sidesteps that restriction.
48
+ */
49
+ function _findMonofencePkgJson(startDir) {
50
+ var dir = startDir;
51
+ for (var depth = 0; depth < 20; depth++) {
52
+ var candidate = path.join(dir, 'node_modules', 'monofence-ai', 'package.json');
53
+ if (fs.existsSync(candidate)) return candidate;
54
+ var parent = path.dirname(dir);
55
+ if (parent === dir) break;
56
+ dir = parent;
57
+ }
58
+ return null;
59
+ }
60
+
61
+ function _loadMonofence() {
62
+ if (!_monofenceModulePromise) {
63
+ _monofenceModulePromise = (async () => {
64
+ var candidateDirs = [
65
+ __dirname,
66
+ process.env.CLAUDE_PROJECT_DIR || process.cwd(),
67
+ path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), 'packages', '@monomind', 'cli'),
68
+ path.join(__dirname, '..', '..', '..', 'packages', '@monomind', 'cli'),
69
+ ];
70
+ var resolvedPkgJson = null;
71
+ for (var i = 0; i < candidateDirs.length; i++) {
72
+ resolvedPkgJson = _findMonofencePkgJson(candidateDirs[i]);
73
+ if (resolvedPkgJson) break;
74
+ }
75
+ var specifier = 'monofence-ai';
76
+ if (resolvedPkgJson) {
77
+ try {
78
+ var pkg = JSON.parse(fs.readFileSync(resolvedPkgJson, 'utf-8'));
79
+ var mainFile = (pkg.exports && pkg.exports['.'] && pkg.exports['.'].import) || pkg.main || 'dist/index.js';
80
+ specifier = 'file://' + path.join(path.dirname(resolvedPkgJson), mainFile);
81
+ } catch (e) { /* fall back to bare specifier below */ }
82
+ }
83
+ try {
84
+ return await import(specifier);
85
+ } catch (e) {
86
+ return null; // not installed / not built — fail open
87
+ }
88
+ })().catch(() => null);
89
+ }
90
+ return _monofenceModulePromise;
91
+ }
92
+
93
+ function _withTimeout(promise, ms) {
94
+ return new Promise((resolve) => {
95
+ var settled = false;
96
+ var timer = setTimeout(() => {
97
+ if (!settled) { settled = true; resolve(null); }
98
+ }, ms);
99
+ if (timer.unref) timer.unref();
100
+ Promise.resolve(promise).then(
101
+ (v) => { if (!settled) { settled = true; clearTimeout(timer); resolve(v); } },
102
+ () => { if (!settled) { settled = true; clearTimeout(timer); resolve(null); } }
103
+ );
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Scan `input` with monofence-ai's threat detector, bounded by a timeout.
109
+ * Returns null (never throws) when monofence is unavailable, times out, or errors.
110
+ */
111
+ async function monofenceScan(input) {
112
+ if (!input || typeof input !== 'string') return null;
113
+ var mod = await _withTimeout(_loadMonofence(), MONOFENCE_TIMEOUT_MS);
114
+ if (!mod || typeof mod.getMonoDefence !== 'function') return null;
115
+ try {
116
+ var defence = mod.getMonoDefence();
117
+ var result = await _withTimeout(defence.detect(input), MONOFENCE_TIMEOUT_MS);
118
+ return result || null;
119
+ } catch (e) {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Given a monofence ThreatDetectionResult, return the highest-confidence
126
+ * threat if it meets the abort threshold, else null.
127
+ */
128
+ function monofenceWorstThreat(result) {
129
+ if (!result || result.safe || !Array.isArray(result.threats) || result.threats.length === 0) {
130
+ return null;
131
+ }
132
+ var worst = result.threats.reduce(
133
+ (max, t) => (t.confidence > max.confidence ? t : max),
134
+ result.threats[0]
135
+ );
136
+ return worst.confidence >= MONOFENCE_ABORT_THRESHOLD ? worst : null;
137
+ }
16
138
 
17
- const DESTRUCTIVE_PATTERNS = [
139
+ // ─── Fallback patterns (used only if the compiled config file is missing/unreadable) ──
140
+ const FALLBACK_DESTRUCTIVE_PATTERNS = [
18
141
  /\brm\s+(?:-[a-z]*f[a-z]*r|-[a-z]*r[a-z]*f|--recursive.*--force|--force.*--recursive|-rf?)\b/i,
19
- /\bdrop\s+(database|table|schema|index|view|function|procedure)\b/i,
142
+ /\bdrop\s+(database|table|schema|index)\b/i,
20
143
  /\btruncate\s+table\b/i,
21
- /\bgit\s+push\s+(?:.*\s)?(?:--force(?:-with-lease)?|-[a-z]*f)\b/i,
144
+ /\bgit\s+push\s+.*--force\b/i,
22
145
  /\bgit\s+reset\s+--hard\b/i,
23
146
  /\bgit\s+clean\s+.*-f/i,
24
147
  /\bformat\s+[a-z]:/i,
25
148
  /\bdel\s+\/[sf]\b/i,
26
- /\b(?:kubectl|helm)\s+delete\b.*\b(?:--all|namespace|all)\b/i,
149
+ /\b(?:kubectl|helm)\s+delete\s+(?:--all|namespace)\b/i,
150
+ /\bDROP\s+(?:DATABASE|TABLE|SCHEMA)\b/i,
27
151
  /\bDELETE\s+FROM\s+\w+/i,
28
152
  /\bALTER\s+TABLE\s+\w+\s+DROP\b/i,
29
153
  ];
30
154
 
31
- const SECRET_PATTERNS = [
155
+ const FALLBACK_SECRET_PATTERNS = [
32
156
  /(?:api[_-]?key|apikey)\s*[:=]\s*['"][^'"]{8,}['"]/gi,
33
157
  /(?:secret|password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]/gi,
34
158
  /(?:token|bearer)\s*[:=]\s*['"][^'"]{10,}['"]/gi,
35
- // Unquoted variants — env-style KEY equals value with no surrounding
36
- // quotes (the single most common real leak pattern; the quoted patterns
37
- // above never match a bare assignment in a shell export or .env file).
159
+ // Unquoted variants — env-style `KEY=value` / `key: value` with no quotes,
160
+ // e.g. `ANTHROPIC_API_KEY=sk-ant-...` in a .env file or shell export. The
161
+ // quoted patterns above never match these, which was the single most
162
+ // common real leak pattern (P1-25).
38
163
  /(?:api[_-]?key|apikey|token|secret|password|passwd|pwd)\s*[:=]\s*[^\s'"]{8,}/gi,
39
164
  /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
40
165
  /sk-ant-[a-zA-Z0-9_-]{20,}/g,
@@ -44,6 +169,54 @@ const SECRET_PATTERNS = [
44
169
  /AKIA[0-9A-Z]{16}/g,
45
170
  ];
46
171
 
172
+ // ─── Compiled config loader ─────────────────────────────────────────────────
173
+
174
+ var MAX_CONFIG_SIZE = 256 * 1024; // 256 KiB — compiled gate config is small
175
+
176
+ function toRegExp(serialized) {
177
+ try {
178
+ return new RegExp(serialized.source, serialized.flags);
179
+ } catch (e) {
180
+ return null;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Load an optional project-level gate override from
186
+ * .monomind/guidance/active-gates.json. Returns the built-in tables when the
187
+ * file is absent (the normal case), oversized, or malformed.
188
+ * Not cached across invocations: each PreToolUse hook is its own subprocess.
189
+ */
190
+ function loadCompiledConfig(cwd) {
191
+ try {
192
+ var configPath = path.join(cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd(), '.monomind', 'guidance', 'active-gates.json');
193
+ var stat = fs.statSync(configPath);
194
+ if (stat.size > MAX_CONFIG_SIZE) throw new Error('active-gates.json too large');
195
+ var raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
196
+
197
+ var destructivePatterns = Array.isArray(raw.destructivePatterns)
198
+ ? raw.destructivePatterns.map(toRegExp).filter(Boolean)
199
+ : [];
200
+ var secretPatterns = Array.isArray(raw.secretPatterns)
201
+ ? raw.secretPatterns.map(toRegExp).filter(Boolean)
202
+ : [];
203
+
204
+ if (destructivePatterns.length === 0 && secretPatterns.length === 0) {
205
+ throw new Error('active-gates.json had no usable patterns');
206
+ }
207
+
208
+ return {
209
+ destructivePatterns: raw.destructiveOps === false ? [] : destructivePatterns,
210
+ secretPatterns: raw.secrets === false ? [] : secretPatterns,
211
+ };
212
+ } catch (e) {
213
+ return {
214
+ destructivePatterns: FALLBACK_DESTRUCTIVE_PATTERNS,
215
+ secretPatterns: FALLBACK_SECRET_PATTERNS,
216
+ };
217
+ }
218
+ }
219
+
47
220
  // ─── Helpers ──────────────────────────────────────────────────────────────────
48
221
 
49
222
  function redact(match) {
@@ -52,8 +225,9 @@ function redact(match) {
52
225
  : '*'.repeat(match.length);
53
226
  }
54
227
 
55
- function checkDestructive(command) {
56
- for (const pattern of DESTRUCTIVE_PATTERNS) {
228
+ function checkDestructive(command, patterns) {
229
+ var list = patterns || FALLBACK_DESTRUCTIVE_PATTERNS;
230
+ for (const pattern of list) {
57
231
  pattern.lastIndex = 0;
58
232
  const match = pattern.exec(command);
59
233
  if (match) {
@@ -67,9 +241,10 @@ function checkDestructive(command) {
67
241
  return { triggered: false };
68
242
  }
69
243
 
70
- function checkSecrets(content) {
244
+ function checkSecrets(content, patterns) {
245
+ var list = patterns || FALLBACK_SECRET_PATTERNS;
71
246
  const found = [];
72
- for (const pattern of SECRET_PATTERNS) {
247
+ for (const pattern of list) {
73
248
  pattern.lastIndex = 0;
74
249
  const matches = content.match(pattern);
75
250
  if (matches) {
@@ -88,14 +263,16 @@ function checkSecrets(content) {
88
263
  // ─── Hook handlers ────────────────────────────────────────────────────────────
89
264
 
90
265
  /**
91
- * pre-bash: check for destructive shell commands.
266
+ * pre-bash: check for destructive shell commands, then (additionally)
267
+ * run monofence-ai's threat detector on the raw command string.
92
268
  * Outputs Claude Code block decision to stdout when triggered.
93
269
  */
94
- function handlePreBash(hCtx) {
270
+ async function handlePreBash(hCtx) {
95
271
  var cmd = (hCtx.toolInput && (hCtx.toolInput.command || hCtx.toolInput.cmd)) || '';
96
272
  if (!cmd) return;
97
273
 
98
- var result = checkDestructive(cmd);
274
+ var config = loadCompiledConfig(hCtx.CWD);
275
+ var result = checkDestructive(cmd, config.destructivePatterns);
99
276
  if (result.triggered) {
100
277
  // Set exit code 2 to block, and write the reason to STDERR — per Claude
101
278
  // Code's PreToolUse hook protocol, stdout JSON is only parsed when exit
@@ -106,14 +283,29 @@ function handlePreBash(hCtx) {
106
283
  reason: '[gates] ' + result.reason,
107
284
  }) + '\n');
108
285
  process.exitCode = 2;
286
+ return;
287
+ }
288
+
289
+ // Additional layer: monofence-ai threat detection (prompt injection, evasion, etc.)
290
+ // Fails open — never blocks a command just because monofence is unavailable/slow.
291
+ var mf = await monofenceScan(cmd);
292
+ var worst = monofenceWorstThreat(mf);
293
+ if (worst) {
294
+ process.stderr.write(JSON.stringify({
295
+ decision: 'block',
296
+ reason: '[monofence] Threat detected in command — ' + worst.type +
297
+ ' (confidence ' + Math.round(worst.confidence * 100) + '%): ' + worst.description,
298
+ }) + '\n');
299
+ process.exitCode = 2;
109
300
  }
110
301
  }
111
302
 
112
303
  /**
113
- * pre-write: check for secrets in Write / Edit / MultiEdit content before it lands on disk.
304
+ * pre-write: check for secrets in Write / Edit / MultiEdit content before it lands on disk,
305
+ * then (additionally) run monofence-ai's threat detector on the same content.
114
306
  * Outputs Claude Code block decision to stdout when triggered.
115
307
  */
116
- function handlePreWrite(hCtx) {
308
+ async function handlePreWrite(hCtx) {
117
309
  var toolInput = hCtx.toolInput || {};
118
310
  // Write: toolInput.content — Edit: toolInput.new_string
119
311
  // MultiEdit: toolInput.edits is an array of { old_string, new_string }
@@ -126,7 +318,8 @@ function handlePreWrite(hCtx) {
126
318
  var MAX_SCAN = 524288;
127
319
  if (content.length > MAX_SCAN) content = content.slice(0, MAX_SCAN);
128
320
 
129
- var result = checkSecrets(content);
321
+ var config = loadCompiledConfig(hCtx.CWD);
322
+ var result = checkSecrets(content, config.secretPatterns);
130
323
  if (result.triggered) {
131
324
  // Set exit code 2 to block, and write the reason to STDERR — see the
132
325
  // matching comment in handlePreBash for why stdout is the wrong stream.
@@ -135,7 +328,29 @@ function handlePreWrite(hCtx) {
135
328
  reason: '[gates] ' + result.reason,
136
329
  }) + '\n');
137
330
  process.exitCode = 2;
331
+ return;
332
+ }
333
+
334
+ // Additional layer: monofence-ai threat detection on the content being written.
335
+ // Fails open — never blocks a write just because monofence is unavailable/slow.
336
+ var mf = await monofenceScan(content);
337
+ var worst = monofenceWorstThreat(mf);
338
+ if (worst) {
339
+ process.stderr.write(JSON.stringify({
340
+ decision: 'block',
341
+ reason: '[monofence] Threat detected in written content — ' + worst.type +
342
+ ' (confidence ' + Math.round(worst.confidence * 100) + '%): ' + worst.description,
343
+ }) + '\n');
344
+ process.exitCode = 2;
138
345
  }
139
346
  }
140
347
 
141
- module.exports = { handlePreBash, handlePreWrite, checkDestructive, checkSecrets };
348
+ module.exports = {
349
+ handlePreBash,
350
+ handlePreWrite,
351
+ checkDestructive,
352
+ checkSecrets,
353
+ loadCompiledConfig,
354
+ monofenceScan,
355
+ monofenceWorstThreat,
356
+ };
@@ -18,6 +18,38 @@
18
18
  const path = require('path');
19
19
  const fs = require('fs');
20
20
 
21
+ // ── Intelligence read-path bridge ───────────────────────────────────────────
22
+ // route-handler.cjs runs as a fresh node subprocess per invocation, but a
23
+ // single process may call handle() more than once (e.g. tests, or a daemon
24
+ // wrapper) — cache the dynamic import of the CLI's compiled intelligence
25
+ // bridge (hooks-embedding.js -> suggestAgentsFromIntelligence) so repeat
26
+ // calls within the same process don't re-resolve/re-import the module.
27
+ // suggestAgentsFromIntelligence() reads the SONA/ReasoningBank embedding
28
+ // store populated by recordMemoryDecision() on every post-task — this is
29
+ // the module that makes stored embedding patterns affect live routing.
30
+ var _intelligenceModPromise = null;
31
+ function _loadIntelligenceModule(CWD) {
32
+ if (!_intelligenceModPromise) {
33
+ _intelligenceModPromise = (async function() {
34
+ var candidates = [
35
+ path.resolve(CWD, 'packages', '@monomind', 'cli', 'dist', 'src', 'mcp-tools', 'hooks-embedding.js'),
36
+ path.resolve(CWD, 'packages', '@monomind', 'cli', 'node_modules', '@monomind', 'cli', 'dist', 'src', 'mcp-tools', 'hooks-embedding.js'),
37
+ ];
38
+ for (var i = 0; i < candidates.length; i++) {
39
+ if (fs.existsSync(candidates[i])) {
40
+ try {
41
+ return await import(candidates[i]);
42
+ } catch (e) {
43
+ return null;
44
+ }
45
+ }
46
+ }
47
+ return null;
48
+ })();
49
+ }
50
+ return _intelligenceModPromise;
51
+ }
52
+
21
53
  module.exports = {
22
54
  handle: async function(hCtx) {
23
55
  var prompt = hCtx.prompt;
@@ -104,7 +136,7 @@ module.exports = {
104
136
  if (erRule.pattern && erRule.pattern.test(prompt)) {
105
137
  result.agent = erRule.routeName || erRule.agentSlug;
106
138
  result.agentSlug = erRule.agentSlug;
107
- result.confidence = 0.90;
139
+ result.confidence = erRule.score != null ? Math.min(0.98, Math.max(0.70, erRule.score)) : 0.85;
108
140
  result.reason = 'Enriched: ' + (erRule.description || erRule.routeName);
109
141
  result.enrichedFrom = 'routing-keyword-pre-filter';
110
142
  break;
@@ -115,6 +147,47 @@ module.exports = {
115
147
  } catch (e) { /* non-fatal — routing package may not be available */ }
116
148
  }
117
149
 
150
+ // ── Intelligence embedding suggestion (SONA/ReasoningBank read path) ──
151
+ // Wires the previously-write-only intelligence system into live routing:
152
+ // suggestAgentsFromIntelligence() runs an embedding similarity search
153
+ // over patterns stored by recordMemoryDecision() on every post-task.
154
+ // This ENHANCES the keyword route — it only overrides when the
155
+ // embedding match is meaningfully more confident, and never blocks or
156
+ // delays routing beyond a 2s budget (fails silently otherwise).
157
+ try {
158
+ var intelResult = await Promise.race([
159
+ (async function() {
160
+ var mod = await _loadIntelligenceModule(CWD);
161
+ if (!mod || !mod.suggestAgentsFromIntelligence) return null;
162
+ return await mod.suggestAgentsFromIntelligence(prompt);
163
+ })(),
164
+ new Promise(function(resolve) { setTimeout(function() { resolve(null); }, 2000); })
165
+ ]);
166
+ if (intelResult && intelResult.agents && intelResult.agents.length > 0) {
167
+ var topIntelAgent = intelResult.agents[0];
168
+ var intelConf = intelResult.confidence != null ? intelResult.confidence : 0;
169
+ result.intelligenceSuggestion = { agents: intelResult.agents, confidence: intelConf };
170
+
171
+ var curAgent = result.agentSlug || result.agent;
172
+ var curConf = result.confidence != null ? result.confidence : 0;
173
+ if (topIntelAgent !== curAgent) {
174
+ // Only override when the embedding match clearly beats the keyword
175
+ // route (0.1 margin) — otherwise just surface it as a signal.
176
+ if (intelConf > curConf + 0.1) {
177
+ console.log('[INTELLIGENCE] Embedding suggestion overrides keyword routing: ' + topIntelAgent + ' (confidence ' + intelConf.toFixed(2) + ') vs keyword ' + curAgent + ' (' + curConf.toFixed(2) + ')');
178
+ result.keywordAgent = curAgent;
179
+ result.agent = topIntelAgent;
180
+ result.agentSlug = topIntelAgent;
181
+ result.confidence = intelConf;
182
+ result.reason = 'Intelligence embedding match (SONA/ReasoningBank)' + (result.reason ? '; keyword route was: ' + result.reason : '');
183
+ result.enrichedFrom = 'intelligence-embedding';
184
+ } else {
185
+ console.log('[INTELLIGENCE] Embedding suggestion: ' + topIntelAgent + ' (confidence ' + intelConf.toFixed(2) + ') — kept keyword route ' + curAgent);
186
+ }
187
+ }
188
+ }
189
+ } catch (e) { /* non-fatal — intelligence system unavailable or timed out */ }
190
+
118
191
  // ── Agent success pattern lookup ──────────────────────────
119
192
  try {
120
193
  var patternDb = hCtx._openMonographDb();
@@ -45,9 +45,13 @@ module.exports = {
45
45
  }
46
46
  } catch (e) { /* @monomind/hooks not available or not built — skip */ }
47
47
 
48
- // Stale helper detectionwarn when project helpers drift from the bundled npm copy.
49
- // Skip when running inside the monomind dev repo itself: local helpers ARE the
50
- // source of truth there, so any diff vs. the npm global install is expected.
48
+ // Stale helper self-healsilently refresh project helpers that drift from
49
+ // the bundled npm copy, so a `npm i -g monomind@latest` (or npx picking up a
50
+ // new version) takes effect on the very next session instead of requiring a
51
+ // manual `doctor --fix` / `init upgrade`. Skip when running inside the
52
+ // monomind dev repo itself: local helpers ARE the source of truth there, so
53
+ // any diff vs. the npm global install is expected (and would self-clobber
54
+ // in-progress edits).
51
55
  try {
52
56
  var _isDevRepo = fs.existsSync(path.join(CWD, 'packages', '@monomind', 'cli', 'package.json'));
53
57
  if (!_isDevRepo) {
@@ -76,24 +80,60 @@ module.exports = {
76
80
  return null;
77
81
  }
78
82
 
83
+ function _hashFile(p) {
84
+ try { return crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex'); }
85
+ catch (_) { return null; }
86
+ }
87
+
88
+ // Copy `bundledF` -> `localF` iff their contents differ (or local is missing).
89
+ // Atomic copy-via-rename so a partial write can never leave a broken hook.
90
+ function _healIfStale(localF, bundledF) {
91
+ if (!fs.existsSync(bundledF)) return null;
92
+ var hashB = _hashFile(bundledF);
93
+ var hashL = fs.existsSync(localF) ? _hashFile(localF) : null;
94
+ if (hashL === hashB) return null;
95
+ try {
96
+ var tmp = localF + '.' + process.pid + '.tmp';
97
+ fs.mkdirSync(path.dirname(localF), { recursive: true });
98
+ fs.copyFileSync(bundledF, tmp);
99
+ try { fs.chmodSync(tmp, 0o755); } catch (_) {}
100
+ fs.renameSync(tmp, localF);
101
+ return path.relative(path.join(CWD, '.claude', 'helpers'), localF);
102
+ } catch (_) { return null; }
103
+ }
104
+
79
105
  var bundledDir = _findBundledHelpers();
80
106
  if (bundledDir) {
81
- var helpersToCheck = ['hook-handler.cjs', 'statusline.cjs'];
82
- var stale = [];
107
+ var healed = [];
108
+ // Top-level critical files — mirrors executor.ts's `criticalHelpers` list.
109
+ var helpersToCheck = ['hook-handler.cjs', 'statusline.cjs', 'router.cjs', 'graphify-freshen.cjs', 'intelligence.cjs', 'auto-memory-hook.mjs'];
83
110
  for (var hi = 0; hi < helpersToCheck.length; hi++) {
84
111
  var hName = helpersToCheck[hi];
85
- var localF = path.join(CWD, '.claude', 'helpers', hName);
86
- var bundledF = path.join(bundledDir, hName);
87
- if (!fs.existsSync(localF) || !fs.existsSync(bundledF)) continue;
88
- try {
89
- var hashL = crypto.createHash('sha256').update(fs.readFileSync(localF)).digest('hex');
90
- var hashB = crypto.createHash('sha256').update(fs.readFileSync(bundledF)).digest('hex');
91
- if (hashL !== hashB) stale.push(hName);
92
- } catch (_) {}
112
+ var healedName = _healIfStale(
113
+ path.join(CWD, '.claude', 'helpers', hName),
114
+ path.join(bundledDir, hName)
115
+ );
116
+ if (healedName) healed.push(healedName);
117
+ }
118
+ // handlers/ and utils/ subdirectories — where most real hook-behavior
119
+ // fixes actually live (capture-handler.cjs, gates-handler.cjs, etc.).
120
+ var subdirs = ['handlers', 'utils'];
121
+ for (var si = 0; si < subdirs.length; si++) {
122
+ var bundledSub = path.join(bundledDir, subdirs[si]);
123
+ if (!fs.existsSync(bundledSub)) continue;
124
+ var files;
125
+ try { files = fs.readdirSync(bundledSub).filter(function(f) { return !f.startsWith('._') && !fs.statSync(path.join(bundledSub, f)).isDirectory(); }); }
126
+ catch (_) { files = []; }
127
+ for (var fi = 0; fi < files.length; fi++) {
128
+ var healedName2 = _healIfStale(
129
+ path.join(CWD, '.claude', 'helpers', subdirs[si], files[fi]),
130
+ path.join(bundledSub, files[fi])
131
+ );
132
+ if (healedName2) healed.push(healedName2);
133
+ }
93
134
  }
94
- if (stale.length > 0) {
95
- console.log('[STALE_HELPERS] Project helpers differ from bundled version: ' + stale.join(', '));
96
- console.log(' Run `npx monomind@latest init upgrade` to refresh and pick up the latest features.');
135
+ if (healed.length > 0) {
136
+ console.log('[STALE_HELPERS] Refreshed ' + healed.length + ' helper(s) from bundled version: ' + healed.join(', '));
97
137
  }
98
138
  }
99
139
  }
@@ -393,15 +393,16 @@ const handlers = {
393
393
  },
394
394
 
395
395
  'pre-bash': async () => {
396
- // SECURITY GATE FIRST — destructive-ops enforcement must never be
397
- // starved by the slower enrichment work below (monograph hint lookups).
398
- // Previously this ran LAST, after up to 10 monograph SQLite strategies,
399
- // AND was fired without awaiting its promise meaning the block
400
- // decision (process.exitCode) could still be unset when the process
401
- // exited under the global 5s safety timer (see main()'s
402
- // `process.exit(0)`), i.e. the gate could fail OPEN under time
403
- // pressure. Awaiting it first guarantees the block/allow decision is
404
- // computed and set before any enrichment work even starts.
396
+ // SECURITY GATE FIRST — destructive-ops + secrets enforcement must never
397
+ // be starved by the slower enrichment work below (monograph hint lookups,
398
+ // monofence-ai scan). Previously this ran LAST, after up to 10 monograph
399
+ // SQLite strategies and a 1.5s monofence budget; under load/slow disk the
400
+ // global 5s safety timer (see main()'s `process.exit(0)`) could fire
401
+ // before the gate ever printed its block decision — i.e. the gate could
402
+ // fail OPEN under time pressure. Computing it first guarantees the
403
+ // block/allow decision (process.exitCode) is set before any enrichment
404
+ // work even starts; enrichment output is still attached afterward if
405
+ // there's time left in the process's lifetime.
405
406
  var gates = require('./handlers/gates-handler.cjs');
406
407
  await gates.handlePreBash(hCtx);
407
408
  if (process.exitCode === 2) return; // blocked — skip enrichment entirely
@@ -661,10 +662,11 @@ const handlers = {
661
662
  }
662
663
  },
663
664
 
664
- 'pre-write': () => {
665
- // Enforcement gate: secrets detection before Write/Edit/MultiEdit lands on disk
665
+ 'pre-write': async () => {
666
+ // Enforcement gate: secrets detection + monofence-ai threat scan before
667
+ // Write/Edit/MultiEdit content lands on disk
666
668
  var gates = require('./handlers/gates-handler.cjs');
667
- gates.handlePreWrite(hCtx);
669
+ await gates.handlePreWrite(hCtx);
668
670
  },
669
671
 
670
672
  'pre-search': () => {
@@ -310,12 +310,22 @@ function routeTask(prompt) {
310
310
  if (slug === 'coder' && hasStrongSkill) continue;
311
311
  var pattern = _ROUTING_PATTERNS[slug];
312
312
  if (pattern && pattern.test(safePrompt)) {
313
- var confidence = applyFeedbackWeight(slug, TASK_CONFIDENCES[slug] || 0.75);
313
+ // Count how many distinct keywords from the pattern actually matched
314
+ // so confidence reflects real match quality, not just a static constant.
315
+ var caps = AGENT_CAPABILITIES[slug] || [];
316
+ var promptLower = safePrompt.toLowerCase();
317
+ var matchedKw = 0;
318
+ for (var ki = 0; ki < caps.length; ki++) {
319
+ if (promptLower.indexOf(caps[ki].toLowerCase()) !== -1) matchedKw++;
320
+ }
321
+ var baseConf = TASK_CONFIDENCES[slug] || 0.75;
322
+ var matchBonus = Math.min(0.10, matchedKw * 0.02);
323
+ var confidence = Math.min(0.98, applyFeedbackWeight(slug, baseConf + matchBonus));
314
324
  return {
315
325
  agent: TASK_AGENTS[slug],
316
326
  agentSlug: slug,
317
327
  confidence: confidence,
318
- reason: ('Keyword match: ' + slug).slice(0, 80),
328
+ reason: ('Keyword match: ' + slug + ' (' + matchedKw + ' kw)').slice(0, 80),
319
329
  semanticRouting: false,
320
330
  specificAgents: [{ slug: slug, name: TASK_AGENTS[slug], confidence: confidence }],
321
331
  skillMatches: skills,
@@ -574,7 +574,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
574
574
  if (sourceHelpersForUpgrade) {
575
575
  const destHelpersDir = path.join(targetDir, '.claude', 'helpers');
576
576
  // Copy top-level critical files atomically
577
- const criticalHelpers = ['auto-memory-hook.mjs', 'hook-handler.cjs', 'intelligence.cjs', 'statusline.cjs', 'graphify-freshen.cjs'];
577
+ const criticalHelpers = ['auto-memory-hook.mjs', 'hook-handler.cjs', 'intelligence.cjs', 'statusline.cjs', 'graphify-freshen.cjs', 'router.cjs'];
578
578
  // Generated fallback for any critical helper missing from the source dir itself
579
579
  // (e.g. the published npm template lacking auto-memory-hook.mjs).
580
580
  const criticalGenerators = {
@@ -630,6 +630,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
630
630
  'hook-handler.cjs': generateHookHandler(),
631
631
  'intelligence.cjs': generateIntelligenceStub(),
632
632
  'auto-memory-hook.mjs': generateAutoMemoryHook(),
633
+ 'router.cjs': generateAgentRouter(),
633
634
  };
634
635
  for (const [helperName, content] of Object.entries(generatedCritical)) {
635
636
  const targetPath = path.join(targetDir, '.claude', 'helpers', helperName);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/monomindcli",
3
- "version": "2.1.4",
3
+ "version": "2.1.6",
4
4
  "type": "module",
5
5
  "description": "CLI engine for Monomind — an open-source MCP server that extends Claude Code with a codebase knowledge graph (tree-sitter + SQLite), persistent memory, multi-agent task coordination, and session hooks. MIT licensed, fully local.",
6
6
  "main": "dist/src/index.js",