monomind 2.1.4 → 2.1.5
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 +1 -1
- package/packages/@monomind/cli/.claude/helpers/handlers/capture-handler.cjs +32 -15
- package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +238 -23
- package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +74 -1
- package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +14 -12
- package/packages/@monomind/cli/.claude/helpers/router.cjs +12 -2
- package/packages/@monomind/cli/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "monomind",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.5",
|
|
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
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
142
|
+
/\bdrop\s+(database|table|schema|index)\b/i,
|
|
20
143
|
/\btruncate\s+table\b/i,
|
|
21
|
-
/\bgit\s+push\s
|
|
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\
|
|
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
|
|
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
|
|
36
|
-
//
|
|
37
|
-
// above never match
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 = {
|
|
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.
|
|
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();
|
|
@@ -393,15 +393,16 @@ const handlers = {
|
|
|
393
393
|
},
|
|
394
394
|
|
|
395
395
|
'pre-bash': async () => {
|
|
396
|
-
// SECURITY GATE FIRST — destructive-ops enforcement must never
|
|
397
|
-
// starved by the slower enrichment work below (monograph hint lookups
|
|
398
|
-
// Previously this ran LAST, after up to 10 monograph
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
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
|
|
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
|
-
|
|
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,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoes/monomindcli",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.5",
|
|
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",
|