monomind 2.1.3 → 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/README.md +18 -17
- package/package.json +1 -1
- package/packages/@monomind/cli/.claude/commands/mastermind/createorg.md +8 -9
- 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/.claude/skills/mastermind-skills/createorg.md +91 -701
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/org-settings.md +46 -47
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/runorg.md +12 -1
- package/packages/@monomind/cli/README.md +18 -17
- package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +18 -0
- package/packages/@monomind/cli/dist/src/orgrt/daemon.js +76 -1
- package/packages/@monomind/cli/dist/src/orgrt/forwarder.js +7 -3
- package/packages/@monomind/cli/dist/src/orgrt/server.js +24 -0
- package/packages/@monomind/cli/dist/src/orgrt/session.d.ts +1 -0
- package/packages/@monomind/cli/dist/src/orgrt/session.js +8 -0
- package/packages/@monomind/cli/dist/src/orgrt/types.d.ts +1 -1
- package/packages/@monomind/cli/dist/src/pricing/model-pricing.d.ts +1 -1
- package/packages/@monomind/cli/dist/src/ui/collector.mjs +20 -5
- package/packages/@monomind/cli/dist/src/ui/dashboard.html +270 -499
- package/packages/@monomind/cli/dist/src/ui/orgs-files.js +1 -1
- package/packages/@monomind/cli/dist/src/ui/server.mjs +132 -158
- package/packages/@monomind/cli/package.json +3 -3
- package/packages/@monomind/cli/scripts/publish.sh +6 -0
- package/scripts/generate-agent-avatars.mjs +3 -3
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="assets/banner.png" alt="Monomind" width="600" />
|
|
2
|
+
<img src="https://raw.githubusercontent.com/monoes/monomind/main/assets/banner.png" alt="Monomind" width="600" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">Monomind</h1>
|
|
@@ -38,9 +38,9 @@ Monomind is an **open-source CLI and MCP server** that plugs into Claude Code vi
|
|
|
38
38
|
- **Reusable slash commands** — 30+ development workflows (build, review, debug, TDD, architecture) available as `/mastermind:*` commands inside Claude Code.
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
npm install -g monomind # MIT licensed,
|
|
41
|
+
npm install -g monomind # MIT licensed, runs entirely on your machine
|
|
42
42
|
cd your-project && monomind init
|
|
43
|
-
claude mcp add monomind npx monomind mcp start
|
|
43
|
+
claude mcp add monomind -- npx -y monomind@latest mcp start
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
### Trust & Security
|
|
@@ -48,8 +48,8 @@ claude mcp add monomind npx monomind mcp start
|
|
|
48
48
|
| Concern | Answer |
|
|
49
49
|
|---|---|
|
|
50
50
|
| **License** | [MIT](LICENSE) — use it however you want |
|
|
51
|
-
| **Data privacy** | Everything runs locally.
|
|
52
|
-
| **Dependencies** | Standard npm packages (tree-sitter, sql.js, zod).
|
|
51
|
+
| **Data privacy** | Everything runs locally — your code and memory store never leave your machine. The one exception: crash reporting is on by default (`monomind crash-reporting disable` to opt out) — a hard crash in monomind/mono-agent/monotask/mono-clip files a GitHub issue on that tool's own repo via the GitHub API. That's the only network call Monomind itself makes; it never phones a monoes-controlled server. |
|
|
52
|
+
| **Dependencies** | Standard npm packages (tree-sitter, better-sqlite3, sql.js, zod). tree-sitter and better-sqlite3 are native (prebuilt) Node addons, not pure JS/WASM — sql.js is WASM. No post-install scripts that download code. |
|
|
53
53
|
| **Permissions** | Registers as an MCP server — Claude Code controls what tools are available and prompts you before executing anything sensitive. |
|
|
54
54
|
| **Source** | Fully open. Read every line at [github.com/monoes/monomind](https://github.com/monoes/monomind). |
|
|
55
55
|
| **Maintenance** | Active development, regular releases on npm. |
|
|
@@ -62,7 +62,7 @@ claude mcp add monomind npx monomind mcp start
|
|
|
62
62
|
|
|
63
63
|
### The idea
|
|
64
64
|
|
|
65
|
-
Every business function needs a team. Define the org once as a JSON file — goal, roles, who reports to whom, per-role tool/file/budget policy — then run it as a real background daemon backed by the Claude Agent SDK. It persists across sessions,
|
|
65
|
+
Every business function needs a team. Define the org once as a JSON file — goal, roles, who reports to whom, per-role tool/file/budget policy — then run it as a real background daemon backed by the Claude Agent SDK. It persists across sessions, streams live into the dashboard Claude Code auto-starts for the project, and can discover and message other Monomind orgs running on the same machine.
|
|
66
66
|
|
|
67
67
|
```mermaid
|
|
68
68
|
flowchart TD
|
|
@@ -73,14 +73,14 @@ flowchart TD
|
|
|
73
73
|
W["Writer"]
|
|
74
74
|
S["SEO Specialist"]
|
|
75
75
|
R["Reviewer"]
|
|
76
|
-
DASH[("
|
|
76
|
+
DASH[("Dashboard\n:4242\nauto-started by\nClaude Code hook")]
|
|
77
77
|
XORG[("Other orgs\ncross-process")]
|
|
78
78
|
|
|
79
79
|
U --> DEF --> RUN --> BOSS
|
|
80
80
|
BOSS -->|spawns| W
|
|
81
81
|
BOSS -->|spawns| S
|
|
82
82
|
BOSS -->|spawns| R
|
|
83
|
-
RUN
|
|
83
|
+
RUN -->|forwards events| DASH
|
|
84
84
|
RUN <-.->|--cross-process| XORG
|
|
85
85
|
|
|
86
86
|
style BOSS fill:#00D2AA22,stroke:#00D2AA
|
|
@@ -93,13 +93,14 @@ flowchart TD
|
|
|
93
93
|
```bash
|
|
94
94
|
# .monomind/orgs/<name>.json defines the org: goal, roles, policy.
|
|
95
95
|
# See .monomind/orgs/sample-team.json in a fresh `monomind init` for a working example.
|
|
96
|
+
# Open the project in Claude Code first — a SessionStart hook auto-launches
|
|
97
|
+
# the dashboard at http://localhost:4242 if it isn't already running.
|
|
96
98
|
|
|
97
99
|
monomind org run content-team --task "Build and publish 3 blog posts per week"
|
|
98
100
|
|
|
99
|
-
# ✓ Live dashboard: http://localhost:4243
|
|
100
101
|
# ✓ Boss agent (Claude Agent SDK session) spawns, reads the org goal,
|
|
101
102
|
# assigns work to role agents, coordinates until the task completes
|
|
102
|
-
# or you stop it.
|
|
103
|
+
# or you stop it. Every event streams into the dashboard above.
|
|
103
104
|
|
|
104
105
|
monomind org status content-team # runtime state
|
|
105
106
|
monomind org stop content-team # request a graceful stop
|
|
@@ -112,18 +113,18 @@ monomind org list # every org + status
|
|
|
112
113
|
|---|---|
|
|
113
114
|
| **OrgDaemon** | Hosts one or more orgs in a single process; real Claude Agent SDK sessions per role, not simulated |
|
|
114
115
|
| **PolicyEngine** | Per-role gates on tool access, file read/write scope, web access, token budget — enforced, with a full audit trail |
|
|
115
|
-
| **
|
|
116
|
+
| **Dashboard** | `org run` forwards every event to the control server on `:4242` (found via `.monomind/control.json`) — that server is auto-launched by a Claude Code SessionStart hook, not by any CLI command; there's no separate per-org dashboard process |
|
|
116
117
|
| **Cross-process comms** | `--cross-process` (default on) lets orgs on different `monomind` processes/projects discover and message each other |
|
|
117
118
|
| **Scheduling** | `monomind org serve` hosts orgs whose definition has a `schedule` field, running them on interval |
|
|
118
119
|
|
|
119
120
|
### Org management commands
|
|
120
121
|
|
|
121
122
|
```bash
|
|
122
|
-
monomind org run <name> [--task "..."] [--
|
|
123
|
+
monomind org run <name> [--task "..."] [--cross-process] # start a daemon
|
|
123
124
|
monomind org stop <name> # request a running org to stop
|
|
124
125
|
monomind org status [name] # runtime state for one or all orgs
|
|
125
126
|
monomind org list # list every org + status
|
|
126
|
-
monomind org serve [--
|
|
127
|
+
monomind org serve [--cross-process] # host-only mode, runs scheduled orgs
|
|
127
128
|
monomind org delete <name> # remove an org
|
|
128
129
|
```
|
|
129
130
|
|
|
@@ -179,13 +180,13 @@ cd your-project
|
|
|
179
180
|
monomind init
|
|
180
181
|
|
|
181
182
|
# 3. Wire into Claude Code as an MCP server
|
|
182
|
-
claude mcp add monomind npx monomind mcp start
|
|
183
|
+
claude mcp add monomind -- npx -y monomind@latest mcp start
|
|
183
184
|
|
|
184
185
|
# 4. Health check
|
|
185
186
|
monomind doctor --fix
|
|
186
187
|
```
|
|
187
188
|
|
|
188
|
-
Open Claude Code. You now have
|
|
189
|
+
Open Claude Code. You now have 49 `/mastermind:*` workflows available:
|
|
189
190
|
|
|
190
191
|
```bash
|
|
191
192
|
/mastermind:autodev --tillend # start autonomous code loop
|
|
@@ -275,7 +276,7 @@ const result = await fence.detect(userInput);
|
|
|
275
276
|
|
|
276
277
|
---
|
|
277
278
|
|
|
278
|
-
## 📋
|
|
279
|
+
## 📋 49 Mastermind Commands
|
|
279
280
|
|
|
280
281
|
Everything runs from inside Claude Code via slash commands. Here's the highlight reel:
|
|
281
282
|
|
|
@@ -308,7 +309,7 @@ Everything runs from inside Claude Code via slash commands. Here's the highlight
|
|
|
308
309
|
| `/mastermind:finance` | Budgets, invoicing, modeling |
|
|
309
310
|
| `/mastermind:ops` | Operations and workflow automation |
|
|
310
311
|
|
|
311
|
-
**[→ Full reference (
|
|
312
|
+
**[→ Full reference (49 commands)](https://monoes.github.io/monomind/#slash)**
|
|
312
313
|
|
|
313
314
|
---
|
|
314
315
|
|
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",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- Define and save an autonomous agent organization — roles
|
|
1
|
+
<!-- Define and save an autonomous agent organization (Org Runtime v2) — roles and hierarchy. Suggest or confirm roles, then persist the org config for `monomind org run`/`serve`. -->
|
|
2
2
|
|
|
3
3
|
**If $ARGUMENTS is empty:** Output the following and wait.
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**MASTERMIND: CREATE ORG**
|
|
8
8
|
|
|
9
|
-
An org is a named, persistent agent team — not a one-shot Mastermind run. Once created, the
|
|
9
|
+
An org is a named, persistent agent team, run by the Org Runtime v2 daemon — not a one-shot Mastermind run and not a Task-tool-spawned boss. Once created, every role in the config starts as its own live agent session the moment you run the org; roles message each other directly (no shared task board) and the org keeps running until stopped.
|
|
10
10
|
|
|
11
11
|
Use orgs when the work is ongoing, not single-shot. A content team that ships 10 posts a month. A research squad that runs competitive scans weekly. A dev team with a permanent backlog.
|
|
12
12
|
|
|
@@ -25,16 +25,15 @@ Use orgs when the work is ongoing, not single-shot. A content team that ships 10
|
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
**Options:**
|
|
28
|
-
`--name <slug>` — org identifier used with
|
|
28
|
+
`--name <slug>` — org identifier used with `monomind org run <name>` (derived from goal if omitted)
|
|
29
29
|
`--roles <list>` — explicit role list (e.g. "boss, writer, reviewer, marketer")
|
|
30
|
-
`--schedule <interval>` —
|
|
30
|
+
`--schedule <interval>` — daemon schedule for `monomind org serve` to pick up, e.g. `"30m"`, `"2h"`, `"1440m"` for daily (omit for a manual, one-shot org)
|
|
31
31
|
`--auto` — skip confirmation, create immediately
|
|
32
32
|
`--confirm` — always ask before saving (default)
|
|
33
|
-
`--delete <name>` — delete a saved org and all associated data files
|
|
34
|
-
`--list` — list all saved orgs with their status
|
|
33
|
+
`--delete <name>` — delete a saved org and all associated data files (`monomind org delete <name> --yes`)
|
|
34
|
+
`--list` — list all saved orgs with their status (`monomind org list`)
|
|
35
35
|
|
|
36
|
-
Once created, start the org with
|
|
37
|
-
For scheduled orgs: `runorg` activates the loop; `stoporg` stops it; `orgs` lists all.
|
|
36
|
+
Once created, start the org with `monomind org run <name>` (foreground, one-shot) or let `monomind org serve` pick it up if `--schedule` was set. Check status with `monomind org status <name>`; stop with `monomind org stop <name>`.
|
|
38
37
|
|
|
39
38
|
---
|
|
40
39
|
|
|
@@ -47,7 +46,7 @@ Parse `$ARGUMENTS` for:
|
|
|
47
46
|
- `--confirm` flag → mode = confirm
|
|
48
47
|
- `--name <name>` → org_name = <name> (must match `^[a-z0-9][a-z0-9-]{0,63}$`; if omitted, derived from goal)
|
|
49
48
|
- `--roles <desc>` → roles_desc = <desc> (explicit role list, e.g. "boss, writer, reviewer, marketer")
|
|
50
|
-
- `--schedule <interval>` → schedule = <interval> (e.g. `"
|
|
49
|
+
- `--schedule <interval>` → schedule = <interval>, daemon format only: `"<N>s"`, `"<N>m"`, or `"<N>h"` (e.g. `"30m"`, `"2h"`) — this is passed straight into `parseSchedule()` in `orgrt/scheduler.ts`, whose regex `^(\d+)\s*(s|m|h)$` rejects anything else (e.g. `"every 30 minutes"`, `"daily"`) and silently leaves the org unscheduled
|
|
51
50
|
- `--delete <name>` → delete_mode = true, delete_name = <name>
|
|
52
51
|
- `--list` flag → list_mode = true
|
|
53
52
|
- Remaining text = prompt (goal description)
|
|
@@ -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
|
+
};
|