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.
Files changed (27) hide show
  1. package/README.md +18 -17
  2. package/package.json +1 -1
  3. package/packages/@monomind/cli/.claude/commands/mastermind/createorg.md +8 -9
  4. package/packages/@monomind/cli/.claude/helpers/handlers/capture-handler.cjs +32 -15
  5. package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +238 -23
  6. package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +74 -1
  7. package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +14 -12
  8. package/packages/@monomind/cli/.claude/helpers/router.cjs +12 -2
  9. package/packages/@monomind/cli/.claude/skills/mastermind-skills/createorg.md +91 -701
  10. package/packages/@monomind/cli/.claude/skills/mastermind-skills/org-settings.md +46 -47
  11. package/packages/@monomind/cli/.claude/skills/mastermind-skills/runorg.md +12 -1
  12. package/packages/@monomind/cli/README.md +18 -17
  13. package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +18 -0
  14. package/packages/@monomind/cli/dist/src/orgrt/daemon.js +76 -1
  15. package/packages/@monomind/cli/dist/src/orgrt/forwarder.js +7 -3
  16. package/packages/@monomind/cli/dist/src/orgrt/server.js +24 -0
  17. package/packages/@monomind/cli/dist/src/orgrt/session.d.ts +1 -0
  18. package/packages/@monomind/cli/dist/src/orgrt/session.js +8 -0
  19. package/packages/@monomind/cli/dist/src/orgrt/types.d.ts +1 -1
  20. package/packages/@monomind/cli/dist/src/pricing/model-pricing.d.ts +1 -1
  21. package/packages/@monomind/cli/dist/src/ui/collector.mjs +20 -5
  22. package/packages/@monomind/cli/dist/src/ui/dashboard.html +270 -499
  23. package/packages/@monomind/cli/dist/src/ui/orgs-files.js +1 -1
  24. package/packages/@monomind/cli/dist/src/ui/server.mjs +132 -158
  25. package/packages/@monomind/cli/package.json +3 -3
  26. package/packages/@monomind/cli/scripts/publish.sh +6 -0
  27. 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, 0 external API calls
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. No telemetry, no phone-home, no external API calls from Monomind itself. Your code stays on your machine. |
52
- | **Dependencies** | Standard npm packages (tree-sitter, sql.js, zod). No native binaries. No post-install scripts that download code. |
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, serves a live WebSocket dashboard, and can discover and message other Monomind orgs running on the same machine.
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[("Live Dashboard\n:4243")]
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 <-->|serves| DASH
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
- | **Live dashboard** | WebSocket-served at `:4243` by default (`--port` to change, `--serve=false` to disable) |
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 "..."] [--port N] [--cross-process] # start a daemon
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 [--port N] # host-only mode, runs scheduled orgs
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 60+ slash commands available:
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
- ## 📋 60+ Slash Commands
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 (60+ commands)](https://monoes.github.io/monomind/#slash)**
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",
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, hierarchy, and communication topology. Suggest or confirm roles, then persist the org for use with runorg. -->
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 org runs autonomously across sessions: a boss agent coordinates specialists who pick up tasks from a shared board, execute them, and loop until stopped.
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 `/mastermind:runorg` (derived from goal if omitted)
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>` — make this a self-scheduling loop org (e.g. `"every 30 minutes"`, `"every hour"`, `"daily"`)
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 `/mastermind:runorg --org <name>`.
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. `"every 30 minutes"`, `"every hour"`, `"daily"`; triggers loop org generation)
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 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
+ };