@ironbee-ai/cli 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +48 -17
  3. package/dist/clients/claude/commands/ironbee-verify.md +19 -106
  4. package/dist/clients/claude/hooks/clear-verdict.d.ts.map +1 -1
  5. package/dist/clients/claude/hooks/clear-verdict.js +25 -4
  6. package/dist/clients/claude/hooks/clear-verdict.js.map +1 -1
  7. package/dist/clients/claude/hooks/require-verdict.d.ts +3 -3
  8. package/dist/clients/claude/hooks/require-verdict.d.ts.map +1 -1
  9. package/dist/clients/claude/hooks/require-verdict.js +26 -8
  10. package/dist/clients/claude/hooks/require-verdict.js.map +1 -1
  11. package/dist/clients/claude/hooks/require-verification.d.ts +6 -5
  12. package/dist/clients/claude/hooks/require-verification.d.ts.map +1 -1
  13. package/dist/clients/claude/hooks/require-verification.js +20 -17
  14. package/dist/clients/claude/hooks/require-verification.js.map +1 -1
  15. package/dist/clients/claude/hooks/track-action-monitor.d.ts.map +1 -1
  16. package/dist/clients/claude/hooks/track-action-monitor.js +4 -1
  17. package/dist/clients/claude/hooks/track-action-monitor.js.map +1 -1
  18. package/dist/clients/claude/hooks/track-action.d.ts +11 -8
  19. package/dist/clients/claude/hooks/track-action.d.ts.map +1 -1
  20. package/dist/clients/claude/hooks/track-action.js +14 -9
  21. package/dist/clients/claude/hooks/track-action.js.map +1 -1
  22. package/dist/clients/claude/index.d.ts.map +1 -1
  23. package/dist/clients/claude/index.js +79 -18
  24. package/dist/clients/claude/index.js.map +1 -1
  25. package/dist/clients/claude/platforms/command-verify.backend.md +74 -0
  26. package/dist/clients/claude/platforms/command-verify.browser.md +108 -0
  27. package/dist/clients/claude/platforms/command-verify.node.md +67 -0
  28. package/dist/clients/claude/platforms/rule.backend.md +23 -0
  29. package/dist/clients/claude/platforms/rule.browser.md +17 -0
  30. package/dist/clients/claude/{fragments → platforms}/rule.node.md +3 -3
  31. package/dist/clients/claude/platforms/skill.backend.md +65 -0
  32. package/dist/clients/claude/platforms/skill.browser.md +31 -0
  33. package/dist/clients/claude/{fragments → platforms}/skill.node.md +2 -2
  34. package/dist/clients/claude/rules/ironbee-verification.md +14 -13
  35. package/dist/clients/claude/skills/ironbee-verification.md +19 -49
  36. package/dist/clients/cursor/commands/ironbee-verify/SKILL.md +21 -108
  37. package/dist/clients/cursor/hooks/clear-verdict.d.ts.map +1 -1
  38. package/dist/clients/cursor/hooks/clear-verdict.js +31 -5
  39. package/dist/clients/cursor/hooks/clear-verdict.js.map +1 -1
  40. package/dist/clients/cursor/hooks/require-verdict.d.ts +1 -1
  41. package/dist/clients/cursor/hooks/require-verdict.d.ts.map +1 -1
  42. package/dist/clients/cursor/hooks/require-verdict.js +27 -6
  43. package/dist/clients/cursor/hooks/require-verdict.js.map +1 -1
  44. package/dist/clients/cursor/hooks/require-verification.d.ts.map +1 -1
  45. package/dist/clients/cursor/hooks/require-verification.js +9 -5
  46. package/dist/clients/cursor/hooks/require-verification.js.map +1 -1
  47. package/dist/clients/cursor/hooks/track-action-monitor.d.ts.map +1 -1
  48. package/dist/clients/cursor/hooks/track-action-monitor.js +4 -1
  49. package/dist/clients/cursor/hooks/track-action-monitor.js.map +1 -1
  50. package/dist/clients/cursor/hooks/track-action.d.ts +14 -12
  51. package/dist/clients/cursor/hooks/track-action.d.ts.map +1 -1
  52. package/dist/clients/cursor/hooks/track-action.js +25 -16
  53. package/dist/clients/cursor/hooks/track-action.js.map +1 -1
  54. package/dist/clients/cursor/index.d.ts.map +1 -1
  55. package/dist/clients/cursor/index.js +45 -11
  56. package/dist/clients/cursor/index.js.map +1 -1
  57. package/dist/clients/cursor/platforms/command-verify.backend.md +74 -0
  58. package/dist/clients/cursor/platforms/command-verify.browser.md +108 -0
  59. package/dist/clients/cursor/platforms/command-verify.node.md +67 -0
  60. package/dist/clients/cursor/platforms/rule.backend.md +23 -0
  61. package/dist/clients/cursor/platforms/rule.browser.md +17 -0
  62. package/dist/clients/cursor/{fragments → platforms}/rule.node.md +3 -3
  63. package/dist/clients/cursor/platforms/skill.backend.md +65 -0
  64. package/dist/clients/cursor/platforms/skill.browser.md +31 -0
  65. package/dist/clients/cursor/{fragments → platforms}/skill.node.md +2 -2
  66. package/dist/clients/cursor/rules/ironbee-verification.mdc +14 -13
  67. package/dist/clients/cursor/skills/ironbee-verification.md +19 -49
  68. package/dist/commands/backend.d.ts +17 -0
  69. package/dist/commands/backend.d.ts.map +1 -0
  70. package/dist/commands/backend.js +58 -0
  71. package/dist/commands/backend.js.map +1 -0
  72. package/dist/commands/browser.d.ts +19 -0
  73. package/dist/commands/browser.d.ts.map +1 -0
  74. package/dist/commands/browser.js +60 -0
  75. package/dist/commands/browser.js.map +1 -0
  76. package/dist/commands/config.d.ts +45 -10
  77. package/dist/commands/config.d.ts.map +1 -1
  78. package/dist/commands/config.js +80 -28
  79. package/dist/commands/config.js.map +1 -1
  80. package/dist/commands/cycle-toggle.d.ts +89 -0
  81. package/dist/commands/cycle-toggle.d.ts.map +1 -0
  82. package/dist/commands/cycle-toggle.js +264 -0
  83. package/dist/commands/cycle-toggle.js.map +1 -0
  84. package/dist/commands/disable-verification.d.ts.map +1 -1
  85. package/dist/commands/disable-verification.js +5 -2
  86. package/dist/commands/disable-verification.js.map +1 -1
  87. package/dist/commands/enable-verification.d.ts.map +1 -1
  88. package/dist/commands/enable-verification.js +5 -2
  89. package/dist/commands/enable-verification.js.map +1 -1
  90. package/dist/commands/node.d.ts +16 -0
  91. package/dist/commands/node.d.ts.map +1 -0
  92. package/dist/commands/node.js +57 -0
  93. package/dist/commands/node.js.map +1 -0
  94. package/dist/commands/verification-toggle.d.ts +18 -1
  95. package/dist/commands/verification-toggle.d.ts.map +1 -1
  96. package/dist/commands/verification-toggle.js +96 -21
  97. package/dist/commands/verification-toggle.js.map +1 -1
  98. package/dist/hooks/core/actions.d.ts +20 -2
  99. package/dist/hooks/core/actions.d.ts.map +1 -1
  100. package/dist/hooks/core/actions.js.map +1 -1
  101. package/dist/hooks/core/file-diff.d.ts +17 -0
  102. package/dist/hooks/core/file-diff.d.ts.map +1 -1
  103. package/dist/hooks/core/file-diff.js +72 -0
  104. package/dist/hooks/core/file-diff.js.map +1 -1
  105. package/dist/hooks/core/tool-use-stash.d.ts +7 -1
  106. package/dist/hooks/core/tool-use-stash.d.ts.map +1 -1
  107. package/dist/hooks/core/tool-use-stash.js.map +1 -1
  108. package/dist/hooks/core/verify-gate.d.ts.map +1 -1
  109. package/dist/hooks/core/verify-gate.js +44 -14
  110. package/dist/hooks/core/verify-gate.js.map +1 -1
  111. package/dist/index.js +9 -6
  112. package/dist/index.js.map +1 -1
  113. package/dist/lib/config.d.ts +218 -36
  114. package/dist/lib/config.d.ts.map +1 -1
  115. package/dist/lib/config.js +359 -95
  116. package/dist/lib/config.js.map +1 -1
  117. package/dist/lib/gitignore.d.ts +26 -11
  118. package/dist/lib/gitignore.d.ts.map +1 -1
  119. package/dist/lib/gitignore.js +71 -24
  120. package/dist/lib/gitignore.js.map +1 -1
  121. package/dist/lib/platform-section.d.ts +126 -0
  122. package/dist/lib/platform-section.d.ts.map +1 -0
  123. package/dist/lib/platform-section.js +279 -0
  124. package/dist/lib/platform-section.js.map +1 -0
  125. package/package.json +1 -1
  126. package/dist/clients/claude/fragments/command-verify.node.md +0 -33
  127. package/dist/clients/cursor/fragments/command-verify.node.md +0 -33
  128. package/dist/commands/backend-toggle.d.ts +0 -45
  129. package/dist/commands/backend-toggle.d.ts.map +0 -1
  130. package/dist/commands/backend-toggle.js +0 -192
  131. package/dist/commands/backend-toggle.js.map +0 -1
  132. package/dist/commands/disable-backend.d.ts +0 -14
  133. package/dist/commands/disable-backend.d.ts.map +0 -1
  134. package/dist/commands/disable-backend.js +0 -34
  135. package/dist/commands/disable-backend.js.map +0 -1
  136. package/dist/commands/enable-backend.d.ts +0 -15
  137. package/dist/commands/enable-backend.d.ts.map +0 -1
  138. package/dist/commands/enable-backend.js +0 -35
  139. package/dist/commands/enable-backend.js.map +0 -1
  140. package/dist/lib/runtime-section.d.ts +0 -118
  141. package/dist/lib/runtime-section.d.ts.map +0 -1
  142. package/dist/lib/runtime-section.js +0 -256
  143. package/dist/lib/runtime-section.js.map +0 -1
@@ -2,31 +2,38 @@
2
2
  /**
3
3
  * IronBee — Configuration Loader
4
4
  *
5
- * Loads config from two sources and merges them:
6
- * 1. Global: ~/.ironbee/config.json
7
- * 2. Project: <projectDir>/.ironbee/config.json
5
+ * Loads config from up to three sources and merges them in order:
6
+ * 1. Global: ~/.ironbee/config.json
7
+ * 2. Project: <projectDir>/.ironbee/config.json (committed)
8
+ * 3. Project-local: <projectDir>/.ironbee/config.local.json (gitignored)
8
9
  *
9
- * Project config overrides global config (deep merge for nested sections,
10
- * shallow for primitives see deepMerge).
11
- *
12
- * The pre-split layout (top-level `verifyPatterns` / `additionalVerifyPatterns`)
13
- * is no longer supported. Configs carrying those fields throw at load time —
14
- * patterns must move under `browser.*` (or the appropriate runtime under
15
- * `backend.<runtime>.*`).
10
+ * Each later layer overrides earlier ones (deep merge for nested cycle
11
+ * blocks: `browser`, `node`, `backend`; shallow for primitives and other
12
+ * top-level keys). The local layer is intended for personal / per-machine
13
+ * overrides (debug toggles, machine-specific paths, alternate collector
14
+ * endpoints) and must not be committed `ironbee install` adds
15
+ * `.ironbee/config.local.json` to `.gitignore` automatically.
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.DEFAULT_BACKEND_NODE_EVIDENCE_PATHS = exports.DEFAULT_BACKEND_NODE_ALWAYS_REQUIRED = exports.DEFAULT_BROWSER_ALWAYS_REQUIRED = exports.DEFAULT_BROWSER_VERIFY_PATTERNS = exports.RUNTIME_TO_SERVER = exports.BACKEND_SERVERS = void 0;
18
+ exports.CONFIG_TARGETS_BY_PRECEDENCE = exports.DEFAULT_BACKEND_EVIDENCE_PATHS = exports.DEFAULT_BACKEND_ALWAYS_REQUIRED = exports.DEFAULT_NODE_EVIDENCE_PATHS = exports.DEFAULT_NODE_ALWAYS_REQUIRED = exports.DEFAULT_BROWSER_ALWAYS_REQUIRED = exports.CYCLE_DEFAULT_VERIFY_PATTERNS = exports.DEFAULT_BACKEND_VERIFY_PATTERNS = exports.DEFAULT_NODE_VERIFY_PATTERNS = exports.DEFAULT_BROWSER_VERIFY_PATTERNS = exports.CYCLE_TO_SERVER = exports.CYCLES_ENABLED_BY_DEFAULT = exports.ALL_CYCLES = exports.OPTIONAL_CYCLES = void 0;
19
+ exports.getConfigLayerPaths = getConfigLayerPaths;
20
+ exports.getTargetConfigPath = getTargetConfigPath;
21
+ exports.resolveConfigTargetFromFlags = resolveConfigTargetFromFlags;
19
22
  exports.loadConfig = loadConfig;
20
23
  exports.getActiveCycles = getActiveCycles;
21
24
  exports.requiresVerification = requiresVerification;
25
+ exports.isCycleEnabled = isCycleEnabled;
22
26
  exports.getRequiredToolsConfig = getRequiredToolsConfig;
23
27
  exports.getMcpServerEntry = getMcpServerEntry;
24
28
  exports.getNodeDevToolsMcpEntry = getNodeDevToolsMcpEntry;
29
+ exports.getBackendDevToolsMcpEntry = getBackendDevToolsMcpEntry;
25
30
  exports.getMaxRetries = getMaxRetries;
26
31
  exports.isCollectorConfigured = isCollectorConfigured;
27
32
  exports.isJobQueueEnabled = isJobQueueEnabled;
28
33
  exports.isRecordingEnabled = isRecordingEnabled;
29
34
  exports.getVerificationEnabled = getVerificationEnabled;
35
+ exports.getCaptureFileChangeset = getCaptureFileChangeset;
36
+ exports.getMaxChangesetBytes = getMaxChangesetBytes;
30
37
  exports.isAnalyticsEnabled = isAnalyticsEnabled;
31
38
  exports.isAnalyticsEmitOnStopEnabled = isAnalyticsEmitOnStopEnabled;
32
39
  exports.getAnalyticsEmitOnStopMinIntervalSeconds = getAnalyticsEmitOnStopMinIntervalSeconds;
@@ -37,13 +44,44 @@ const fs_1 = require("fs");
37
44
  const path_1 = require("path");
38
45
  const os_1 = require("os");
39
46
  const logger_1 = require("./logger");
40
- /** Backend MCP servers in the runtime registry. Code change to extend. */
41
- exports.BACKEND_SERVERS = ["node-devtools"];
42
- /** Map runtime key (`node`) backend server name (`node-devtools`). */
43
- exports.RUNTIME_TO_SERVER = {
47
+ /**
48
+ * Optional cycles — those that default to **disabled** (empty `verifyPatterns`)
49
+ * until the operator explicitly opts in via `ironbee <cycle> enable`. The
50
+ * always-on browser cycle is the only default-enabled cycle and is excluded
51
+ * from this list. Order is the canonical sort order used by verify-gate when
52
+ * building per-cycle messages.
53
+ */
54
+ exports.OPTIONAL_CYCLES = ["node", "backend"];
55
+ /**
56
+ * Every cycle the runtime knows about — browser plus all optional cycles.
57
+ * Used by the platform-section sync (skill / rule / command-verify md files
58
+ * carry one marker block per cycle) and by toggle commands when validating
59
+ * the cycle name. Order matches verify-gate's canonical block ordering
60
+ * (browser first, then OPTIONAL_CYCLES).
61
+ */
62
+ exports.ALL_CYCLES = ["browser", ...exports.OPTIONAL_CYCLES];
63
+ /**
64
+ * Cycles whose default state is "enabled" (the cycle activates without the
65
+ * operator having to write `verifyPatterns` in config). Currently only the
66
+ * browser cycle ships with built-in default patterns; node and backend cycles
67
+ * default to inert (`[]`) and require `ironbee <cycle> enable`.
68
+ *
69
+ * Used by `getCyclePatterns` runtime fallback (block-absent + default-on
70
+ * → code defaults activate) and by toggle commands. `<cycle> disable` records
71
+ * its intent via the top-level `disabled` list — that takes precedence over
72
+ * the default-on fallback, so a disabled browser stays off even though
73
+ * `CYCLES_ENABLED_BY_DEFAULT` contains it.
74
+ */
75
+ exports.CYCLES_ENABLED_BY_DEFAULT = new Set(["browser"]);
76
+ /** Map cycle key → MCP server name. Browser is always `browser-devtools`. */
77
+ exports.CYCLE_TO_SERVER = {
78
+ browser: "browser-devtools",
44
79
  node: "node-devtools",
80
+ backend: "backend-devtools",
45
81
  };
46
- /** Browser default verify patterns — preserves the pre-split list as-is. */
82
+ /** Browser default verify patterns — kicks in whenever the browser block is
83
+ * present without an explicit `verifyPatterns`, OR (since browser is
84
+ * default-on) whenever no browser block is present at any layer. */
47
85
  exports.DEFAULT_BROWSER_VERIFY_PATTERNS = [
48
86
  "*.ts", "*.tsx",
49
87
  "*.js", "*.jsx", "*.mjs", "*.cjs",
@@ -71,6 +109,65 @@ exports.DEFAULT_BROWSER_VERIFY_PATTERNS = [
71
109
  "*.hbs", "*.ejs", "*.pug", "*.jade",
72
110
  "*.astro",
73
111
  ];
112
+ /**
113
+ * Node-cycle default verify patterns — kicks in when the `node` block is
114
+ * present without an explicit `verifyPatterns`. The node cycle is opt-in so
115
+ * the block must be present (a fully-absent block keeps the cycle disabled).
116
+ *
117
+ * Heuristic-tuned for typical Node.js backends — server entry points, API
118
+ * route handlers (Next.js / Remix / SvelteKit conventions), and the routes/
119
+ * folder.
120
+ */
121
+ exports.DEFAULT_NODE_VERIFY_PATTERNS = [
122
+ "server/**/*.{ts,js,mjs,cjs}",
123
+ "src/server/**/*.{ts,js,mjs,cjs}",
124
+ "backend/**/*.{ts,js,mjs,cjs}",
125
+ "api/**/*.{ts,js,mjs,cjs}",
126
+ "src/api/**/*.{ts,js,mjs,cjs}",
127
+ "pages/api/**/*.{ts,js,mjs,cjs}",
128
+ "app/api/**/*.{ts,js,mjs,cjs}",
129
+ "routes/**/*.{ts,js,mjs,cjs}",
130
+ "**/server.{ts,js,mjs,cjs}",
131
+ ];
132
+ /**
133
+ * Backend-cycle default verify patterns — kicks in when the `backend` block
134
+ * is present without an explicit `verifyPatterns`. Multi-language coverage —
135
+ * the cycle drives wire protocols, not language-specific tooling, so the
136
+ * heuristic spans every common server-side language.
137
+ */
138
+ exports.DEFAULT_BACKEND_VERIFY_PATTERNS = [
139
+ "server/**/*.{ts,js,mjs,cjs,py,go,java,rb,cs,rs,kt,scala,ex,exs,php,clj}",
140
+ "src/server/**/*.{ts,js,mjs,cjs,py,go,java,rb,cs,rs,kt,scala,ex,exs,php,clj}",
141
+ "backend/**/*.{ts,js,mjs,cjs,py,go,java,rb,cs,rs,kt,scala,ex,exs,php,clj}",
142
+ "api/**/*.{ts,js,mjs,cjs,py,go,java,rb,cs,rs,kt,scala,ex,exs,php,clj}",
143
+ "src/api/**/*.{ts,js,mjs,cjs,py,go,java,rb,cs,rs,kt,scala,ex,exs,php,clj}",
144
+ "pages/api/**/*.{ts,js,mjs,cjs}",
145
+ "app/api/**/*.{ts,js,mjs,cjs}",
146
+ "routes/**/*.{ts,js,mjs,cjs,py,go,java,rb,cs,rs,kt,scala,ex,exs,php,clj}",
147
+ "controllers/**/*.{ts,js,mjs,cjs,py,go,java,rb,cs,rs,kt,scala,ex,exs,php,clj}",
148
+ "handlers/**/*.{ts,js,mjs,cjs,py,go,java,rb,cs,rs,kt,scala,ex,exs,php,clj}",
149
+ "services/**/*.{ts,js,mjs,cjs,py,go,java,rb,cs,rs,kt,scala,ex,exs,php,clj}",
150
+ "**/server.{ts,js,mjs,cjs,py,go,java,rb,cs,rs,kt,scala,ex,exs,php,clj}",
151
+ "**/main.{go,py,java,rb,kt,scala}",
152
+ ];
153
+ /**
154
+ * Per-cycle default `verifyPatterns` lookup. Single source of truth used by:
155
+ *
156
+ * - `getCyclePatterns` runtime fallback when the cycle block is present
157
+ * without an explicit `verifyPatterns`.
158
+ * - For browser only: also kicks in when the cycle block is fully absent
159
+ * (browser is the default-on cycle — `CYCLES_ENABLED_BY_DEFAULT`).
160
+ *
161
+ * `<cycle> enable` does NOT materialize these into config.json; the cycle
162
+ * block is written empty (`{}`) and these defaults flow in at runtime.
163
+ * Keeps `config.json` minimal and lets defaults track the CLI version
164
+ * automatically.
165
+ */
166
+ exports.CYCLE_DEFAULT_VERIFY_PATTERNS = {
167
+ browser: exports.DEFAULT_BROWSER_VERIFY_PATTERNS,
168
+ node: exports.DEFAULT_NODE_VERIFY_PATTERNS,
169
+ backend: exports.DEFAULT_BACKEND_VERIFY_PATTERNS,
170
+ };
74
171
  /** Browser-cycle required tools — strict all-of, no alternative paths. */
75
172
  exports.DEFAULT_BROWSER_ALWAYS_REQUIRED = [
76
173
  "bdt_navigation_go-to",
@@ -79,10 +176,10 @@ exports.DEFAULT_BROWSER_ALWAYS_REQUIRED = [
79
176
  "bdt_o11y_get-console-messages",
80
177
  ];
81
178
  /** Node-cycle required tools — connect always; then probe path or log path. */
82
- exports.DEFAULT_BACKEND_NODE_ALWAYS_REQUIRED = [
179
+ exports.DEFAULT_NODE_ALWAYS_REQUIRED = [
83
180
  "ndt_debug_connect",
84
181
  ];
85
- exports.DEFAULT_BACKEND_NODE_EVIDENCE_PATHS = [
182
+ exports.DEFAULT_NODE_EVIDENCE_PATHS = [
86
183
  {
87
184
  name: "probe",
88
185
  allOf: [
@@ -101,6 +198,29 @@ exports.DEFAULT_BACKEND_NODE_EVIDENCE_PATHS = [
101
198
  allOf: ["ndt_debug_get-logs"],
102
199
  },
103
200
  ];
201
+ /**
202
+ * Backend-cycle required tools — no single tool is mandatory (different
203
+ * tasks need different protocols), so `alwaysRequired` is empty and the
204
+ * gate is satisfied by any one protocol-call evidence path. The replay
205
+ * tool counts because it issues a real protocol call against the target.
206
+ */
207
+ exports.DEFAULT_BACKEND_ALWAYS_REQUIRED = [];
208
+ exports.DEFAULT_BACKEND_EVIDENCE_PATHS = [
209
+ {
210
+ name: "protocol-call",
211
+ allOf: [
212
+ {
213
+ anyOf: [
214
+ "bedt_request_http",
215
+ "bedt_request_grpc",
216
+ "bedt_request_graphql",
217
+ "bedt_request_websocket-open",
218
+ "bedt_request_replay",
219
+ ],
220
+ },
221
+ ],
222
+ },
223
+ ];
104
224
  const DEFAULT_MAX_RETRIES = 3;
105
225
  function loadJsonFile(filePath) {
106
226
  if (!(0, fs_1.existsSync)(filePath)) {
@@ -114,21 +234,6 @@ function loadJsonFile(filePath) {
114
234
  return {};
115
235
  }
116
236
  }
117
- /** Detect pre-split flat config. Loud failure per design D6. */
118
- function assertNoLegacyShape(config, sourcePath) {
119
- const legacy = [];
120
- if (Object.prototype.hasOwnProperty.call(config, "verifyPatterns")) {
121
- legacy.push("'verifyPatterns'");
122
- }
123
- if (Object.prototype.hasOwnProperty.call(config, "additionalVerifyPatterns")) {
124
- legacy.push("'additionalVerifyPatterns'");
125
- }
126
- if (legacy.length === 0) {
127
- return;
128
- }
129
- throw new Error(`Legacy IronBee config detected in ${sourcePath}: ${legacy.join(", ")} at the top level is no longer supported. ` +
130
- `Move them under 'browser.*' (or the appropriate runtime under 'backend.<runtime>.*').`);
131
- }
132
237
  function assertVerificationShape(config, sourcePath) {
133
238
  if (!Object.prototype.hasOwnProperty.call(config, "verification")) {
134
239
  return;
@@ -144,51 +249,96 @@ function assertVerificationShape(config, sourcePath) {
144
249
  `Got ${typeof block.enable}.`);
145
250
  }
146
251
  }
147
- /** Deep merge for the nested `browser` / `backend.<runtime>` blocks. */
252
+ /** Deep merge for a single cycle block (browser / node / backend). */
148
253
  function mergeCycleConfig(base, override) {
149
254
  if (base === undefined && override === undefined) {
150
255
  return undefined;
151
256
  }
152
257
  return { ...(base ?? {}), ...(override ?? {}) };
153
258
  }
154
- function mergeBackend(base, override) {
155
- if (base === undefined && override === undefined) {
156
- return undefined;
157
- }
158
- const merged = {};
159
- const keys = new Set([
160
- ...Object.keys(base ?? {}),
161
- ...Object.keys(override ?? {}),
162
- ]);
163
- for (const key of keys) {
164
- merged[key] = mergeCycleConfig(base?.[key], override?.[key]);
165
- }
259
+ /**
260
+ * Merge two `IronBeeConfig` layers — `override` wins. Top-level keys are
261
+ * shallow-merged; the cycle blocks (`browser`, `node`, `backend`) deep-merge
262
+ * so a project override of `browser.verifyPatterns` doesn't wipe a global
263
+ * `browser.evidencePaths` (and vice-versa for the local layer).
264
+ */
265
+ function mergeConfigLayers(base, override) {
266
+ const merged = { ...base, ...override };
267
+ merged.browser = mergeCycleConfig(base.browser, override.browser);
268
+ merged.node = mergeCycleConfig(base.node, override.node);
269
+ merged.backend = mergeCycleConfig(base.backend, override.backend);
166
270
  return merged;
167
271
  }
168
- function loadConfig(projectDir) {
169
- const globalPath = (0, path_1.join)((0, os_1.homedir)(), ".ironbee", "config.json");
170
- const globalConfig = loadJsonFile(globalPath);
171
- if ((0, fs_1.existsSync)(globalPath)) {
172
- assertNoLegacyShape(globalConfig, globalPath);
173
- assertVerificationShape(globalConfig, globalPath);
174
- }
175
- let projectConfig = {};
176
- let projectPath = "";
177
- if (projectDir) {
178
- projectPath = (0, path_1.join)(projectDir, ".ironbee", "config.json");
179
- projectConfig = loadJsonFile(projectPath);
180
- if ((0, fs_1.existsSync)(projectPath)) {
181
- assertNoLegacyShape(projectConfig, projectPath);
182
- assertVerificationShape(projectConfig, projectPath);
272
+ /** Returns the on-disk paths of all three config layers without reading them. */
273
+ function getConfigLayerPaths(projectDir) {
274
+ return {
275
+ global: (0, path_1.join)((0, os_1.homedir)(), ".ironbee", "config.json"),
276
+ project: projectDir ? (0, path_1.join)(projectDir, ".ironbee", "config.json") : undefined,
277
+ local: projectDir ? (0, path_1.join)(projectDir, ".ironbee", "config.local.json") : undefined,
278
+ };
279
+ }
280
+ /** Layers in merge-precedence order, low → high. Used by override detection. */
281
+ exports.CONFIG_TARGETS_BY_PRECEDENCE = ["global", "project", "local"];
282
+ /**
283
+ * Returns the on-disk path for a single config layer. Throws when the
284
+ * caller asks for a project-scoped layer without a `projectDir` (e.g.
285
+ * `--local` outside a project context). Used by all write-side commands
286
+ * to keep the targeted-path resolution in one place.
287
+ */
288
+ function getTargetConfigPath(target, projectDir) {
289
+ const paths = getConfigLayerPaths(projectDir);
290
+ if (target === "global") {
291
+ return paths.global;
292
+ }
293
+ if (target === "project") {
294
+ if (paths.project === undefined) {
295
+ throw new Error("Project layer requested but no projectDir was provided.");
183
296
  }
297
+ return paths.project;
184
298
  }
185
- const merged = {
186
- ...globalConfig,
187
- ...projectConfig,
188
- };
189
- merged.browser = mergeCycleConfig(globalConfig.browser, projectConfig.browser);
190
- merged.backend = mergeBackend(globalConfig.backend, projectConfig.backend);
191
- return merged;
299
+ // local
300
+ if (paths.local === undefined) {
301
+ throw new Error("Local layer requested but no projectDir was provided.");
302
+ }
303
+ return paths.local;
304
+ }
305
+ /**
306
+ * Resolves a `ConfigTarget` from CLI flag booleans, enforcing mutual
307
+ * exclusion. Used by every write-side command that exposes
308
+ * `-g / --global` and `--local` flags (`config set`, `config unset`,
309
+ * `enable-verification`, `disable-verification`, `backend enable`,
310
+ * `backend disable`) so the flag-resolution rules stay identical.
311
+ *
312
+ * Returns `"project"` when neither flag is set (the default for
313
+ * project-scoped commands). Throws on the impossible `--global --local`
314
+ * combination — commander doesn't enforce mutual exclusion across
315
+ * arbitrary option pairs, so we do.
316
+ */
317
+ function resolveConfigTargetFromFlags(opts) {
318
+ if (opts.global === true && opts.local === true) {
319
+ throw new Error("Pass at most one of --global / --local.");
320
+ }
321
+ if (opts.global === true) {
322
+ return "global";
323
+ }
324
+ if (opts.local === true) {
325
+ return "local";
326
+ }
327
+ return "project";
328
+ }
329
+ function loadAndValidateLayer(path) {
330
+ const raw = loadJsonFile(path);
331
+ if ((0, fs_1.existsSync)(path)) {
332
+ assertVerificationShape(raw, path);
333
+ }
334
+ return raw;
335
+ }
336
+ function loadConfig(projectDir) {
337
+ const paths = getConfigLayerPaths(projectDir);
338
+ const globalConfig = loadAndValidateLayer(paths.global);
339
+ const projectConfig = paths.project ? loadAndValidateLayer(paths.project) : {};
340
+ const localConfig = paths.local ? loadAndValidateLayer(paths.local) : {};
341
+ return mergeConfigLayers(mergeConfigLayers(globalConfig, projectConfig), localConfig);
192
342
  }
193
343
  // Glob → RegExp. Supports *, **, ** + slash (zero-or-more segments), ?, and
194
344
  // brace expansion {a,b,c} → (a|b|c). The trailing-slash form of ** matches
@@ -226,26 +376,76 @@ function matchesAny(filePath, patterns) {
226
376
  }
227
377
  return false;
228
378
  }
229
- /** Returns the effective pattern set for the browser cycle. */
230
- function getBrowserPatterns(config) {
231
- const block = config.browser;
232
- const base = block?.verifyPatterns ?? exports.DEFAULT_BROWSER_VERIFY_PATTERNS;
233
- const additional = block?.additionalVerifyPatterns ?? [];
234
- return [...base, ...additional];
379
+ /** Returns the per-cycle config block. Always-on browser, opt-in node / backend. */
380
+ function getCycleBlock(config, cycle) {
381
+ if (cycle === "browser") {
382
+ return config.browser;
383
+ }
384
+ if (cycle === "node") {
385
+ return config.node;
386
+ }
387
+ if (cycle === "backend") {
388
+ return config.backend;
389
+ }
390
+ return undefined;
235
391
  }
236
- /** Returns the effective pattern set for `backend.<runtime>`. Empty by default. */
237
- function getBackendRuntimePatterns(config, runtime) {
238
- const block = config.backend?.[runtime];
239
- const base = block?.verifyPatterns ?? [];
240
- const additional = block?.additionalVerifyPatterns ?? [];
392
+ /**
393
+ * Returns the effective pattern set for a cycle. Resolution order:
394
+ *
395
+ * 1. **`block.enable === false`** → cycle-specific disable signal written
396
+ * by `<cycle> disable`. Mirrors `recording.enable` / `jobQueue.enable`
397
+ * / `verification.enable`.
398
+ * 2. **Block absent** → for default-on cycles (browser), the code defaults
399
+ * kick in. For default-off cycles (node, backend), no patterns.
400
+ * 3. **`verifyPatterns: []`** → hard kill (legacy disable marker — still
401
+ * respected for older configs; new disables write `enable: false`).
402
+ * 4. **Block present, `verifyPatterns` undefined** → enabled with code
403
+ * defaults (`CYCLE_DEFAULT_VERIFY_PATTERNS[cycle]`). Post-`<cycle> enable`
404
+ * shape — minimal config, defaults track CLI version.
405
+ * 5. **`verifyPatterns: [...non-empty]`** → enabled with custom patterns.
406
+ *
407
+ * `additionalVerifyPatterns` is appended to whatever base resolves at step
408
+ * 4/5 — EXCEPT when the cycle is disabled via step 1 or 3.
409
+ *
410
+ * **Note:** does NOT check the master `verification.enable` switch. That
411
+ * happens at the verification-level (install-time gating + the higher-level
412
+ * {@link isCycleEnabled} helper). This function returns the cycle's pattern
413
+ * shape independent of master state — useful for code paths that want
414
+ * pattern info even when enforcement is off.
415
+ */
416
+ function getCyclePatterns(config, cycle) {
417
+ const block = getCycleBlock(config, cycle);
418
+ // Explicit per-cycle disable signal.
419
+ if (block !== undefined && block.enable === false) {
420
+ return [];
421
+ }
422
+ const cycleDefaults = exports.CYCLE_DEFAULT_VERIFY_PATTERNS[cycle] ?? [];
423
+ if (block === undefined) {
424
+ // Block absent: only default-on cycles activate via code defaults.
425
+ if (exports.CYCLES_ENABLED_BY_DEFAULT.has(cycle)) {
426
+ return [...cycleDefaults];
427
+ }
428
+ return [];
429
+ }
430
+ // Legacy hard kill: `verifyPatterns: []` still fully disables. Newer
431
+ // configs use `enable: false` (above) but pre-existing configs may
432
+ // carry this older marker — keep honoring it.
433
+ if (Array.isArray(block.verifyPatterns) && block.verifyPatterns.length === 0) {
434
+ return [];
435
+ }
436
+ // Block present, enabled: `verifyPatterns` wins when set; otherwise
437
+ // fall back to code defaults (post-`<cycle> enable` shape).
438
+ const base = block.verifyPatterns ?? cycleDefaults;
439
+ const additional = block.additionalVerifyPatterns ?? [];
241
440
  return [...base, ...additional];
242
441
  }
243
442
  /**
244
443
  * Returns the names of cycles whose pattern set matches `filePath`.
245
444
  * Used by verify-gate to determine which cycles are active for a Stop hook.
246
445
  *
247
- * Order: browser first, then each registered backend runtime. `ignoredVerifyPatterns`
248
- * applies globally — a file matched there activates no cycles.
446
+ * Order: browser first, then each optional cycle in {@link OPTIONAL_CYCLES}
447
+ * order. `ignoredVerifyPatterns` applies globally — a file matched there
448
+ * activates no cycles.
249
449
  */
250
450
  function getActiveCycles(filePath, config) {
251
451
  const ignored = config.ignoredVerifyPatterns ?? [];
@@ -253,17 +453,13 @@ function getActiveCycles(filePath, config) {
253
453
  return [];
254
454
  }
255
455
  const active = [];
256
- if (matchesAny(filePath, getBrowserPatterns(config))) {
456
+ if (matchesAny(filePath, getCyclePatterns(config, "browser"))) {
257
457
  active.push("browser");
258
458
  }
259
- for (const server of exports.BACKEND_SERVERS) {
260
- const runtime = Object.keys(exports.RUNTIME_TO_SERVER).find((k) => exports.RUNTIME_TO_SERVER[k] === server);
261
- if (runtime === undefined) {
262
- continue;
263
- }
264
- const patterns = getBackendRuntimePatterns(config, runtime);
459
+ for (const cycle of exports.OPTIONAL_CYCLES) {
460
+ const patterns = getCyclePatterns(config, cycle);
265
461
  if (patterns.length > 0 && matchesAny(filePath, patterns)) {
266
- active.push(runtime);
462
+ active.push(cycle);
267
463
  }
268
464
  }
269
465
  return active;
@@ -276,6 +472,34 @@ function getActiveCycles(filePath, config) {
276
472
  function requiresVerification(filePath, config) {
277
473
  return getActiveCycles(filePath, config).length > 0;
278
474
  }
475
+ /**
476
+ * Returns true if `cycle` is currently enabled for verification in `config`
477
+ * (effective merged shape).
478
+ *
479
+ * Two conditions must BOTH hold:
480
+ * 1. The master switch `verification.enable` is on (or absent — defaults
481
+ * to true). When `verification.enable: false`, every cycle is off
482
+ * regardless of its own block — per-cycle settings never override the
483
+ * master switch.
484
+ * 2. The cycle's own resolution per {@link getCyclePatterns} yields
485
+ * non-empty patterns:
486
+ * - `block.enable === false` → disabled (canonical signal).
487
+ * - Block absent + default-on (browser) → enabled (code defaults).
488
+ * - Block absent + default-off (node, backend) → disabled.
489
+ * - `verifyPatterns: []` (legacy) → disabled.
490
+ * - Block present with `verifyPatterns` undefined → enabled (defaults).
491
+ * - Block present with non-empty `verifyPatterns` → enabled (custom).
492
+ *
493
+ * Used by `platform-section.syncPlatformSectionsToConfig` to decide whether to
494
+ * splice the cycle's fragment or the placeholder into installed md files,
495
+ * and by client `install` paths to gate MCP server entries + permissions.
496
+ */
497
+ function isCycleEnabled(config, cycle) {
498
+ if (!getVerificationEnabled(config)) {
499
+ return false;
500
+ }
501
+ return getCyclePatterns(config, cycle).length > 0;
502
+ }
279
503
  /**
280
504
  * Per-cycle resolved required-tools spec. Falls back to defaults at this layer
281
505
  * so consumers always get a complete `{ alwaysRequired, evidencePaths }` shape.
@@ -291,13 +515,16 @@ function getRequiredToolsConfig(config, cycle) {
291
515
  evidencePaths = config.browser?.evidencePaths ?? [];
292
516
  }
293
517
  else if (cycle === "node") {
294
- alwaysRequired = config.backend?.node?.alwaysRequired ?? exports.DEFAULT_BACKEND_NODE_ALWAYS_REQUIRED;
295
- evidencePaths = config.backend?.node?.evidencePaths ?? exports.DEFAULT_BACKEND_NODE_EVIDENCE_PATHS;
518
+ alwaysRequired = config.node?.alwaysRequired ?? exports.DEFAULT_NODE_ALWAYS_REQUIRED;
519
+ evidencePaths = config.node?.evidencePaths ?? exports.DEFAULT_NODE_EVIDENCE_PATHS;
520
+ }
521
+ else if (cycle === "backend") {
522
+ alwaysRequired = config.backend?.alwaysRequired ?? exports.DEFAULT_BACKEND_ALWAYS_REQUIRED;
523
+ evidencePaths = config.backend?.evidencePaths ?? exports.DEFAULT_BACKEND_EVIDENCE_PATHS;
296
524
  }
297
525
  else {
298
- const block = config.backend?.[cycle];
299
- alwaysRequired = block?.alwaysRequired ?? [];
300
- evidencePaths = block?.evidencePaths ?? [];
526
+ alwaysRequired = [];
527
+ evidencePaths = [];
301
528
  }
302
529
  if (alwaysRequired.length === 0 && evidencePaths.length === 0) {
303
530
  throw new Error(`Invalid required-tools config for cycle '${cycle}': both 'alwaysRequired' and 'evidencePaths' are empty. ` +
@@ -316,10 +543,16 @@ const NODE_IRONBEE_ENV = {
316
543
  TOOL_NAME_PREFIX: "ndt_",
317
544
  TOOL_INPUT_METADATA_ENABLE: "true",
318
545
  };
546
+ const BACKEND_IRONBEE_ENV = {
547
+ PLATFORM: "backend",
548
+ TOOL_NAME_PREFIX: "bedt_",
549
+ TOOL_INPUT_METADATA_ENABLE: "true",
550
+ };
319
551
  const BROWSER_DEFAULT_MCP_ENV = {
320
552
  BROWSER_DEVTOOLS_INSTALL_CHROMIUM: "true",
321
553
  };
322
554
  const NODE_DEFAULT_MCP_ENV = {};
555
+ const BACKEND_DEFAULT_MCP_ENV = {};
323
556
  /**
324
557
  * Auto-derive OTEL exporter env for the devtools MCP server when the
325
558
  * IronBee Collector is configured + enabled. Returns `{}` when the
@@ -334,7 +567,8 @@ const NODE_DEFAULT_MCP_ENV = {};
334
567
  * by setting `OTEL_ENABLE: "false"` in their override.
335
568
  *
336
569
  * `runtime` toggles browser-only vars (USER_INTERACTION_EVENTS,
337
- * BROWSER_HEADLESS_ENABLE) — node-devtools doesn't honor those.
570
+ * BROWSER_HEADLESS_ENABLE) — node-devtools and backend-devtools don't
571
+ * honor those.
338
572
  */
339
573
  function buildOtelEnv(config, runtime) {
340
574
  if (!isCollectorConfigured(config)) {
@@ -423,6 +657,11 @@ function getNodeDevToolsMcpEntry(projectDir) {
423
657
  const config = loadConfig(projectDir);
424
658
  return buildMcpEntry(config, "nodeDevTools", NODE_IRONBEE_ENV, NODE_DEFAULT_MCP_ENV, "node");
425
659
  }
660
+ /** Returns the MCP server entry for `backend-devtools` (PLATFORM=backend, bedt_). */
661
+ function getBackendDevToolsMcpEntry(projectDir) {
662
+ const config = loadConfig(projectDir);
663
+ return buildMcpEntry(config, "backendDevTools", BACKEND_IRONBEE_ENV, BACKEND_DEFAULT_MCP_ENV, "backend");
664
+ }
426
665
  function getMaxRetries(config) {
427
666
  return (typeof config.maxRetries === "number" && config.maxRetries > 0)
428
667
  ? config.maxRetries
@@ -533,6 +772,31 @@ function getVerificationEnabled(config) {
533
772
  }
534
773
  return section.enable !== false;
535
774
  }
775
+ const DEFAULT_MAX_CHANGESET_BYTES = 65536;
776
+ /**
777
+ * Returns true when `file_change` events should carry a unified-diff
778
+ * `changeset` string. Off by default — the default wire shape stays
779
+ * metadata-only (operation + line counts).
780
+ */
781
+ function getCaptureFileChangeset(config) {
782
+ const section = config.fileChange;
783
+ if (!section || typeof section !== "object" || Array.isArray(section)) {
784
+ return false;
785
+ }
786
+ return section.captureChangeset === true;
787
+ }
788
+ /** Per-event hard cap in bytes for the `changeset` string. Truncated past this. */
789
+ function getMaxChangesetBytes(config) {
790
+ const section = config.fileChange;
791
+ if (!section || typeof section !== "object" || Array.isArray(section)) {
792
+ return DEFAULT_MAX_CHANGESET_BYTES;
793
+ }
794
+ const raw = section.maxChangesetBytes;
795
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) {
796
+ return DEFAULT_MAX_CHANGESET_BYTES;
797
+ }
798
+ return Math.floor(raw);
799
+ }
536
800
  /**
537
801
  * Returns true when analytics collection is enabled for this project.
538
802
  * Same resolution as `isJobQueueEnabled` / `isRecordingEnabled`: