neuro-cli 4.1.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 (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,1199 @@
1
+ // ============================================================
2
+ // NeuroCLI - Smart Monitor (GAP-34)
3
+ // LLM-based real-time action evaluation for auto mode
4
+ // Bridges the gap between manual approval and yolo mode
5
+ // ============================================================
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'fs';
7
+ import { join } from 'path';
8
+ import { homedir } from 'os';
9
+ import { execSync } from 'child_process';
10
+ // ---------------------------------------------------------------------------
11
+ // Default configuration
12
+ // ---------------------------------------------------------------------------
13
+ const DEFAULT_CONFIG = {
14
+ enabled: true,
15
+ evaluatorModel: 'gemma-4-31b',
16
+ riskThresholds: {
17
+ autoApprove: 30,
18
+ askUser: 70,
19
+ },
20
+ learning: {
21
+ enabled: true,
22
+ storagePath: join(homedir(), '.neuro', 'monitor-patterns.json'),
23
+ minSamples: 5,
24
+ },
25
+ escalationRules: [],
26
+ contextAwareness: {
27
+ checkGitStatus: true,
28
+ checkTestCoverage: true,
29
+ checkProductionFiles: true,
30
+ protectedPaths: [
31
+ 'src/production/**',
32
+ 'src/core/**',
33
+ 'src/main/**',
34
+ 'package.json',
35
+ 'package-lock.json',
36
+ 'yarn.lock',
37
+ '.env',
38
+ '.env.*',
39
+ 'Dockerfile*',
40
+ 'docker-compose.*',
41
+ 'tsconfig.json',
42
+ 'next.config.*',
43
+ 'webpack.config.*',
44
+ ],
45
+ },
46
+ };
47
+ const PRODUCTION_PATH_SEGMENTS = [
48
+ 'src/production',
49
+ 'src/core',
50
+ 'src/main',
51
+ 'src/app',
52
+ 'src/server',
53
+ 'src/api',
54
+ 'dist/',
55
+ 'build/',
56
+ ];
57
+ const TEST_PATH_SEGMENTS = [
58
+ '__tests__',
59
+ 'test/',
60
+ 'tests/',
61
+ 'spec/',
62
+ '.test.',
63
+ '.spec.',
64
+ ];
65
+ const CONFIG_FILES = [
66
+ 'package.json',
67
+ 'tsconfig.json',
68
+ '.eslintrc',
69
+ '.prettierrc',
70
+ 'vite.config',
71
+ 'webpack.config',
72
+ 'next.config',
73
+ 'tailwind.config',
74
+ 'docker-compose',
75
+ 'Dockerfile',
76
+ '.env',
77
+ ];
78
+ const DANGEROUS_COMMANDS = [
79
+ 'sudo',
80
+ 'rm -rf',
81
+ 'rm -r',
82
+ 'chmod',
83
+ 'chown',
84
+ 'mkfs',
85
+ 'dd if=',
86
+ 'kill -9',
87
+ 'pkill',
88
+ 'killall',
89
+ 'shutdown',
90
+ 'reboot',
91
+ 'npm publish',
92
+ 'git push --force',
93
+ 'git reset --hard',
94
+ 'drop table',
95
+ 'drop database',
96
+ 'truncate table',
97
+ 'pip uninstall',
98
+ 'npm uninstall',
99
+ ];
100
+ const MODERATE_COMMANDS = [
101
+ 'npm install',
102
+ 'npm update',
103
+ 'git push',
104
+ 'git merge',
105
+ 'git rebase',
106
+ 'docker',
107
+ 'kubectl',
108
+ 'helm',
109
+ ];
110
+ const SAFE_COMMANDS = [
111
+ 'ls',
112
+ 'cat',
113
+ 'head',
114
+ 'tail',
115
+ 'pwd',
116
+ 'echo',
117
+ 'which',
118
+ 'node -v',
119
+ 'npm -v',
120
+ 'git status',
121
+ 'git log',
122
+ 'git diff',
123
+ 'git branch',
124
+ 'wc',
125
+ ];
126
+ const SAFE_TOOLS = [
127
+ 'read_file',
128
+ 'search_files',
129
+ 'list_directory',
130
+ 'web_search',
131
+ 'web_fetch',
132
+ 'recall_memory',
133
+ 'project_context',
134
+ 'todowrite',
135
+ 'get_file_info',
136
+ 'glob',
137
+ ];
138
+ const MODERATE_TOOLS = [
139
+ 'write_file',
140
+ 'edit_file',
141
+ 'apply_diff',
142
+ 'execute_command',
143
+ 'create_file',
144
+ ];
145
+ const DANGEROUS_TOOLS = [
146
+ 'delete_file',
147
+ 'shell_exec',
148
+ 'sudo_exec',
149
+ 'system_command',
150
+ ];
151
+ const IRREVERSIBLE_PATTERNS = [
152
+ 'rm ',
153
+ 'delete',
154
+ 'remove',
155
+ 'drop ',
156
+ 'truncate',
157
+ 'destroy',
158
+ 'wipe',
159
+ 'format',
160
+ 'mkfs',
161
+ ];
162
+ function evaluateFilePathRisk(toolName, args, context) {
163
+ const filePath = extractFilePath(args);
164
+ if (!filePath)
165
+ return null;
166
+ const normalized = filePath.replace(/\\/g, '/').toLowerCase();
167
+ if (PRODUCTION_PATH_SEGMENTS.some((seg) => normalized.includes(seg.toLowerCase()))) {
168
+ return {
169
+ name: 'file-path-risk',
170
+ contribution: 65,
171
+ description: `Path "${filePath}" appears to be a production file`,
172
+ };
173
+ }
174
+ if (CONFIG_FILES.some((cfg) => normalized.includes(cfg.toLowerCase()))) {
175
+ return {
176
+ name: 'file-path-risk',
177
+ contribution: 55,
178
+ description: `Path "${filePath}" is a configuration file`,
179
+ };
180
+ }
181
+ if (TEST_PATH_SEGMENTS.some((seg) => normalized.includes(seg.toLowerCase()))) {
182
+ return {
183
+ name: 'file-path-risk',
184
+ contribution: 10,
185
+ description: `Path "${filePath}" appears to be a test file (lower risk)`,
186
+ };
187
+ }
188
+ return {
189
+ name: 'file-path-risk',
190
+ contribution: 25,
191
+ description: `Path "${filePath}" is a standard source file`,
192
+ };
193
+ }
194
+ function evaluateCommandRisk(toolName, args, _context) {
195
+ const command = extractCommand(args);
196
+ if (!command)
197
+ return null;
198
+ const lowerCmd = command.toLowerCase();
199
+ for (const dangerous of DANGEROUS_COMMANDS) {
200
+ if (lowerCmd.includes(dangerous.toLowerCase())) {
201
+ return {
202
+ name: 'command-risk',
203
+ contribution: 85,
204
+ description: `Command contains dangerous pattern "${dangerous}": ${truncate(command, 80)}`,
205
+ };
206
+ }
207
+ }
208
+ for (const moderate of MODERATE_COMMANDS) {
209
+ if (lowerCmd.includes(moderate.toLowerCase())) {
210
+ return {
211
+ name: 'command-risk',
212
+ contribution: 40,
213
+ description: `Command contains moderate-risk pattern "${moderate}": ${truncate(command, 80)}`,
214
+ };
215
+ }
216
+ }
217
+ for (const safe of SAFE_COMMANDS) {
218
+ if (lowerCmd.startsWith(safe.toLowerCase())) {
219
+ return {
220
+ name: 'command-risk',
221
+ contribution: 5,
222
+ description: `Command appears safe: ${truncate(command, 80)}`,
223
+ };
224
+ }
225
+ }
226
+ return {
227
+ name: 'command-risk',
228
+ contribution: 35,
229
+ description: `Command risk unknown: ${truncate(command, 80)}`,
230
+ };
231
+ }
232
+ function evaluateToolRisk(toolName, _args, _context) {
233
+ if (SAFE_TOOLS.includes(toolName)) {
234
+ return {
235
+ name: 'tool-risk',
236
+ contribution: 5,
237
+ description: `Tool "${toolName}" is a safe read-only / informational tool`,
238
+ };
239
+ }
240
+ if (DANGEROUS_TOOLS.includes(toolName)) {
241
+ return {
242
+ name: 'tool-risk',
243
+ contribution: 75,
244
+ description: `Tool "${toolName}" is classified as dangerous`,
245
+ };
246
+ }
247
+ if (MODERATE_TOOLS.includes(toolName)) {
248
+ return {
249
+ name: 'tool-risk',
250
+ contribution: 35,
251
+ description: `Tool "${toolName}" can modify files or execute commands`,
252
+ };
253
+ }
254
+ return {
255
+ name: 'tool-risk',
256
+ contribution: 25,
257
+ description: `Tool "${toolName}" is not in any known risk category`,
258
+ };
259
+ }
260
+ function evaluateScopeRisk(_toolName, args, context) {
261
+ const files = extractMultipleFiles(args);
262
+ if (files.length > 5) {
263
+ return {
264
+ name: 'scope-risk',
265
+ contribution: 60,
266
+ description: `Action affects ${files.length} files — wide scope`,
267
+ };
268
+ }
269
+ if (files.length > 1) {
270
+ return {
271
+ name: 'scope-risk',
272
+ contribution: 30,
273
+ description: `Action affects ${files.length} files`,
274
+ };
275
+ }
276
+ if (files.length === 1) {
277
+ return {
278
+ name: 'scope-risk',
279
+ contribution: 10,
280
+ description: 'Action affects a single file',
281
+ };
282
+ }
283
+ return {
284
+ name: 'scope-risk',
285
+ contribution: 5,
286
+ description: 'Action has no direct file scope',
287
+ };
288
+ }
289
+ function evaluateNetworkRisk(toolName, args, _context) {
290
+ const urlFields = ['url', 'endpoint', 'api_url', 'webhook_url', 'server_url'];
291
+ for (const field of urlFields) {
292
+ const val = args[field];
293
+ if (typeof val === 'string' && (val.startsWith('http://') || val.startsWith('https://'))) {
294
+ const isInternal = val.includes('localhost') || val.includes('127.0.0.1') || val.includes('0.0.0.0');
295
+ return {
296
+ name: 'network-risk',
297
+ contribution: isInternal ? 15 : 50,
298
+ description: isInternal
299
+ ? `Internal network call to ${truncate(val, 60)}`
300
+ : `External network call to ${truncate(val, 60)}`,
301
+ };
302
+ }
303
+ }
304
+ if (['web_search', 'web_fetch', 'http_request', 'api_call'].includes(toolName)) {
305
+ return {
306
+ name: 'network-risk',
307
+ contribution: 25,
308
+ description: `Tool "${toolName}" makes network requests`,
309
+ };
310
+ }
311
+ return {
312
+ name: 'network-risk',
313
+ contribution: 0,
314
+ description: 'No network activity detected',
315
+ };
316
+ }
317
+ function evaluateReversibilityRisk(toolName, args, _context) {
318
+ if (DANGEROUS_TOOLS.includes(toolName) || toolName.includes('delete') || toolName.includes('remove')) {
319
+ return {
320
+ name: 'reversibility-risk',
321
+ contribution: 80,
322
+ description: `Action via "${toolName}" is likely irreversible`,
323
+ };
324
+ }
325
+ const command = extractCommand(args);
326
+ if (command) {
327
+ const lowerCmd = command.toLowerCase();
328
+ for (const pattern of IRREVERSIBLE_PATTERNS) {
329
+ if (lowerCmd.includes(pattern.toLowerCase())) {
330
+ return {
331
+ name: 'reversibility-risk',
332
+ contribution: 80,
333
+ description: `Command contains irreversible pattern "${pattern}"`,
334
+ };
335
+ }
336
+ }
337
+ }
338
+ if (MODERATE_TOOLS.includes(toolName)) {
339
+ return {
340
+ name: 'reversibility-risk',
341
+ contribution: 35,
342
+ description: `Action via "${toolName}" can likely be undone with effort`,
343
+ };
344
+ }
345
+ if (SAFE_TOOLS.includes(toolName)) {
346
+ return {
347
+ name: 'reversibility-risk',
348
+ contribution: 2,
349
+ description: 'Read-only action — trivially reversible (no-op)',
350
+ };
351
+ }
352
+ return {
353
+ name: 'reversibility-risk',
354
+ contribution: 20,
355
+ description: 'Reversibility uncertain',
356
+ };
357
+ }
358
+ function evaluateImpactRadiusRisk(toolName, args, context) {
359
+ const filePath = extractFilePath(args);
360
+ if (!filePath) {
361
+ return {
362
+ name: 'impact-radius-risk',
363
+ contribution: 5,
364
+ description: 'No specific file target — low impact radius',
365
+ };
366
+ }
367
+ const normalized = filePath.replace(/\\/g, '/').toLowerCase();
368
+ const coreModules = ['index.ts', 'index.js', 'main.ts', 'main.js', 'app.ts', 'app.js', 'server.ts', 'server.js'];
369
+ const fileName = normalized.split('/').pop() ?? '';
370
+ if (coreModules.some((m) => fileName === m)) {
371
+ return {
372
+ name: 'impact-radius-risk',
373
+ contribution: 70,
374
+ description: `File "${fileName}" is a core entry point — high impact radius`,
375
+ };
376
+ }
377
+ if (normalized.includes('util') || normalized.includes('helper') || normalized.includes('shared')) {
378
+ return {
379
+ name: 'impact-radius-risk',
380
+ contribution: 55,
381
+ description: `Path includes shared/utility — potentially wide impact`,
382
+ };
383
+ }
384
+ if (normalized.includes('test') || normalized.includes('spec') || normalized.includes('mock')) {
385
+ return {
386
+ name: 'impact-radius-risk',
387
+ contribution: 10,
388
+ description: 'Test / spec file — isolated impact',
389
+ };
390
+ }
391
+ return {
392
+ name: 'impact-radius-risk',
393
+ contribution: 25,
394
+ description: 'Standard source file — moderate impact radius',
395
+ };
396
+ }
397
+ function evaluateCostRisk(_toolName, _args, context) {
398
+ if (context.spendingLimit <= 0) {
399
+ return {
400
+ name: 'cost-risk',
401
+ contribution: 0,
402
+ description: 'No spending limit configured',
403
+ };
404
+ }
405
+ const ratio = context.currentCost / context.spendingLimit;
406
+ if (ratio >= 0.95) {
407
+ return {
408
+ name: 'cost-risk',
409
+ contribution: 90,
410
+ description: `Spending at ${(ratio * 100).toFixed(1)}% of limit ($${context.currentCost.toFixed(4)} / $${context.spendingLimit})`,
411
+ };
412
+ }
413
+ if (ratio >= 0.8) {
414
+ return {
415
+ name: 'cost-risk',
416
+ contribution: 55,
417
+ description: `Spending at ${(ratio * 100).toFixed(1)}% of limit`,
418
+ };
419
+ }
420
+ if (ratio >= 0.5) {
421
+ return {
422
+ name: 'cost-risk',
423
+ contribution: 25,
424
+ description: `Spending at ${(ratio * 100).toFixed(1)}% of limit`,
425
+ };
426
+ }
427
+ return {
428
+ name: 'cost-risk',
429
+ contribution: 5,
430
+ description: `Spending at ${(ratio * 100).toFixed(1)}% of limit — plenty of budget`,
431
+ };
432
+ }
433
+ const BUILTIN_RISK_EVALUATORS = [
434
+ evaluateFilePathRisk,
435
+ evaluateCommandRisk,
436
+ evaluateToolRisk,
437
+ evaluateScopeRisk,
438
+ evaluateNetworkRisk,
439
+ evaluateReversibilityRisk,
440
+ evaluateImpactRadiusRisk,
441
+ evaluateCostRisk,
442
+ ];
443
+ // ---------------------------------------------------------------------------
444
+ // Helpers
445
+ // ---------------------------------------------------------------------------
446
+ function extractFilePath(args) {
447
+ for (const key of ['path', 'filePath', 'file_path', 'filename', 'dest', 'destination']) {
448
+ if (typeof args[key] === 'string')
449
+ return args[key];
450
+ }
451
+ return null;
452
+ }
453
+ function extractCommand(args) {
454
+ for (const key of ['command', 'cmd', 'script', 'shell_command', 'exec']) {
455
+ if (typeof args[key] === 'string')
456
+ return args[key];
457
+ }
458
+ return null;
459
+ }
460
+ function extractMultipleFiles(args) {
461
+ const files = [];
462
+ for (const key of ['paths', 'files', 'file_list']) {
463
+ const val = args[key];
464
+ if (Array.isArray(val)) {
465
+ for (const item of val) {
466
+ if (typeof item === 'string')
467
+ files.push(item);
468
+ }
469
+ }
470
+ }
471
+ const single = extractFilePath(args);
472
+ if (single)
473
+ files.push(single);
474
+ return files;
475
+ }
476
+ function truncate(str, maxLen) {
477
+ return str.length > maxLen ? str.slice(0, maxLen) + '...' : str;
478
+ }
479
+ function normalizePattern(toolName, args) {
480
+ const filePath = extractFilePath(args);
481
+ if (filePath) {
482
+ const ext = filePath.includes('.') ? '.' + filePath.split('.').pop() : '';
483
+ return `${toolName}:*${ext}`;
484
+ }
485
+ const command = extractCommand(args);
486
+ if (command) {
487
+ const base = command.trim().split(/\s+/)[0];
488
+ return `${toolName}:${base}`;
489
+ }
490
+ return `${toolName}:*`;
491
+ }
492
+ function matchesEscalationCondition(rule, toolName, args, context) {
493
+ const condition = rule.condition.toLowerCase();
494
+ if (condition.includes('file matches')) {
495
+ const pattern = condition.replace(/file matches\s+/i, '').trim();
496
+ const filePath = extractFilePath(args);
497
+ if (filePath) {
498
+ const regexStr = pattern
499
+ .replace(/\./g, '\\.')
500
+ .replace(/\*\*/g, '.*')
501
+ .replace(/\*/g, '[^/]*');
502
+ try {
503
+ const re = new RegExp(regexStr, 'i');
504
+ if (re.test(filePath))
505
+ return true;
506
+ }
507
+ catch {
508
+ if (filePath.toLowerCase().includes(pattern.replace(/\*/g, '').toLowerCase()))
509
+ return true;
510
+ }
511
+ }
512
+ }
513
+ if (condition.includes('tool is')) {
514
+ const targetTool = condition.replace(/tool is\s+/i, '').trim();
515
+ if (toolName.toLowerCase() === targetTool.toLowerCase())
516
+ return true;
517
+ }
518
+ if (condition.includes('command contains')) {
519
+ const fragment = condition.replace(/command contains\s+/i, '').trim();
520
+ const command = extractCommand(args);
521
+ if (command && command.toLowerCase().includes(fragment.toLowerCase()))
522
+ return true;
523
+ }
524
+ if (condition.includes('uncommitted changes') && context.hasUncommittedChanges) {
525
+ return true;
526
+ }
527
+ if (condition.includes('production')) {
528
+ const filePath = extractFilePath(args);
529
+ if (filePath && PRODUCTION_PATH_SEGMENTS.some((seg) => filePath.toLowerCase().includes(seg.toLowerCase()))) {
530
+ return true;
531
+ }
532
+ }
533
+ if (condition.includes('cost exceeds')) {
534
+ const threshold = parseFloat(condition.replace(/cost exceeds\s+/i, '').replace(/[^0-9.]/g, ''));
535
+ if (!isNaN(threshold) && context.currentCost > threshold)
536
+ return true;
537
+ }
538
+ return false;
539
+ }
540
+ // ---------------------------------------------------------------------------
541
+ // SmartMonitor class
542
+ // ---------------------------------------------------------------------------
543
+ const NEURO_DIR = join(homedir(), '.neuro');
544
+ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
545
+ const MAX_CACHE_ENTRIES = 200;
546
+ export class SmartMonitor {
547
+ config;
548
+ apiClient;
549
+ patterns = new Map();
550
+ evaluationCache = new Map();
551
+ stats;
552
+ constructor(config, apiClient) {
553
+ this.config = { ...DEFAULT_CONFIG, ...config };
554
+ this.config.riskThresholds = { ...DEFAULT_CONFIG.riskThresholds, ...config.riskThresholds };
555
+ this.config.learning = { ...DEFAULT_CONFIG.learning, ...config.learning };
556
+ this.config.contextAwareness = { ...DEFAULT_CONFIG.contextAwareness, ...config.contextAwareness };
557
+ if (config.escalationRules) {
558
+ this.config.escalationRules = [...config.escalationRules];
559
+ }
560
+ this.apiClient = apiClient;
561
+ this.stats = this.freshStats();
562
+ this.loadPatterns();
563
+ }
564
+ // -----------------------------------------------------------------------
565
+ // Core evaluation
566
+ // -----------------------------------------------------------------------
567
+ async evaluate(toolName, args, context) {
568
+ if (!this.config.enabled) {
569
+ return {
570
+ action: 'approve',
571
+ confidence: 1.0,
572
+ riskScore: 0,
573
+ reasoning: 'Smart monitor is disabled',
574
+ learned: false,
575
+ };
576
+ }
577
+ const startTime = Date.now();
578
+ // 1. Check escalation rules first — these are hard overrides
579
+ for (const rule of this.config.escalationRules) {
580
+ if (matchesEscalationCondition(rule, toolName, args, context)) {
581
+ this.stats.totalEvaluations++;
582
+ this.stats.escalations++;
583
+ const elapsed = Date.now() - startTime;
584
+ this.stats.totalEvaluationTimeMs += elapsed;
585
+ return {
586
+ action: rule.action === 'deny' ? 'deny' : 'ask-user',
587
+ confidence: 0.95,
588
+ riskScore: 95,
589
+ reasoning: rule.reason,
590
+ userQuestion: rule.action === 'ask-user'
591
+ ? `Escalation rule triggered: ${rule.reason}. Proceed anyway?`
592
+ : undefined,
593
+ learned: false,
594
+ };
595
+ }
596
+ }
597
+ // 2. Check learned patterns
598
+ if (this.config.learning.enabled) {
599
+ const learnedDecision = this.checkLearnedPatterns(toolName, args);
600
+ if (learnedDecision) {
601
+ this.stats.totalEvaluations++;
602
+ this.stats.learnedDecisions++;
603
+ if (learnedDecision.action === 'approve')
604
+ this.stats.approvals++;
605
+ else if (learnedDecision.action === 'deny')
606
+ this.stats.denials++;
607
+ const elapsed = Date.now() - startTime;
608
+ this.stats.totalEvaluationTimeMs += elapsed;
609
+ this.stats.totalRiskScore += learnedDecision.riskScore;
610
+ return learnedDecision;
611
+ }
612
+ }
613
+ // 3. Perform risk assessment
614
+ const risk = await this.assessRisk(toolName, args, context);
615
+ // 4. Apply threshold logic
616
+ let decision;
617
+ if (risk.score <= this.config.riskThresholds.autoApprove) {
618
+ decision = {
619
+ action: 'approve',
620
+ confidence: this.scoreToConfidence(risk.score, 'approve'),
621
+ riskScore: risk.score,
622
+ reasoning: risk.reasoning,
623
+ learned: false,
624
+ };
625
+ }
626
+ else if (risk.score >= this.config.riskThresholds.askUser) {
627
+ decision = {
628
+ action: 'ask-user',
629
+ confidence: this.scoreToConfidence(risk.score, 'ask-user'),
630
+ riskScore: risk.score,
631
+ reasoning: risk.reasoning,
632
+ userQuestion: `Risk score ${risk.score}/100. ${risk.reasoning}. Allow this action?`,
633
+ learned: false,
634
+ };
635
+ }
636
+ else {
637
+ // Middle zone — use LLM evaluation
638
+ const llmDecision = await this.evaluateWithLLM(toolName, args, context, risk);
639
+ decision = {
640
+ ...llmDecision,
641
+ riskScore: risk.score,
642
+ learned: false,
643
+ };
644
+ }
645
+ // 5. Safety-level adjustments
646
+ decision = this.adjustForSafetyLevel(decision, context.safetyLevel);
647
+ // 6. Budget enforcement
648
+ if (context.spendingLimit > 0 &&
649
+ context.currentCost >= context.spendingLimit * 0.95 &&
650
+ decision.action === 'approve') {
651
+ decision = {
652
+ action: 'ask-user',
653
+ confidence: 0.85,
654
+ riskScore: Math.max(decision.riskScore, 75),
655
+ reasoning: `Approaching spending limit ($${context.currentCost.toFixed(4)} / $${context.spendingLimit}). ${decision.reasoning}`,
656
+ userQuestion: `You've used 95%+ of your spending limit. Proceed with this action?`,
657
+ learned: false,
658
+ };
659
+ }
660
+ // Record stats
661
+ this.stats.totalEvaluations++;
662
+ this.stats.totalRiskScore += decision.riskScore;
663
+ const elapsed = Date.now() - startTime;
664
+ this.stats.totalEvaluationTimeMs += elapsed;
665
+ if (decision.action === 'approve')
666
+ this.stats.approvals++;
667
+ else if (decision.action === 'deny')
668
+ this.stats.denials++;
669
+ else if (decision.action === 'modify')
670
+ this.stats.modifications++;
671
+ else if (decision.action === 'ask-user')
672
+ this.stats.escalations++;
673
+ // Cache the decision
674
+ this.cacheDecision(toolName, args, decision);
675
+ return decision;
676
+ }
677
+ // -----------------------------------------------------------------------
678
+ // Risk assessment
679
+ // -----------------------------------------------------------------------
680
+ async assessRisk(toolName, args, context) {
681
+ const factors = [];
682
+ // Run all built-in risk evaluators
683
+ for (const evaluator of BUILTIN_RISK_EVALUATORS) {
684
+ const factor = evaluator(toolName, args, context);
685
+ if (factor)
686
+ factors.push(factor);
687
+ }
688
+ // Check protected paths from context awareness
689
+ if (this.config.contextAwareness.checkProductionFiles) {
690
+ const filePath = extractFilePath(args);
691
+ if (filePath) {
692
+ for (const protectedPath of this.config.contextAwareness.protectedPaths) {
693
+ const regexStr = protectedPath
694
+ .replace(/\./g, '\\.')
695
+ .replace(/\*\*/g, '.*')
696
+ .replace(/\*/g, '[^/]*');
697
+ try {
698
+ const re = new RegExp(regexStr, 'i');
699
+ if (re.test(filePath)) {
700
+ factors.push({
701
+ name: 'protected-path-risk',
702
+ contribution: 70,
703
+ description: `Path "${filePath}" matches protected pattern "${protectedPath}"`,
704
+ });
705
+ break;
706
+ }
707
+ }
708
+ catch {
709
+ if (filePath.toLowerCase().includes(protectedPath.replace(/\*/g, '').toLowerCase())) {
710
+ factors.push({
711
+ name: 'protected-path-risk',
712
+ contribution: 70,
713
+ description: `Path "${filePath}" matches protected pattern "${protectedPath}"`,
714
+ });
715
+ break;
716
+ }
717
+ }
718
+ }
719
+ }
720
+ }
721
+ // Check git status context
722
+ if (this.config.contextAwareness.checkGitStatus && context.hasUncommittedChanges) {
723
+ factors.push({
724
+ name: 'git-status-risk',
725
+ contribution: 20,
726
+ description: 'There are uncommitted changes in the repository',
727
+ });
728
+ }
729
+ // Compute weighted aggregate score
730
+ // Each factor contributes a weight; we use a weighted average with a
731
+ // minimum of 0 and ceiling of 100.
732
+ const totalContribution = factors.reduce((sum, f) => sum + f.contribution, 0);
733
+ const factorCount = factors.length || 1;
734
+ // Weighted average: contributions above 50 count double (amplification)
735
+ let score = 0;
736
+ for (const factor of factors) {
737
+ const weight = factor.contribution >= 50 ? 1.5 : 1.0;
738
+ score += factor.contribution * weight;
739
+ }
740
+ score = score / (factorCount + factors.filter((f) => f.contribution >= 50).length * 0.5);
741
+ score = Math.round(Math.max(0, Math.min(100, score)));
742
+ // Determine recommendation
743
+ let recommendation;
744
+ if (score <= this.config.riskThresholds.autoApprove) {
745
+ recommendation = 'approve';
746
+ }
747
+ else if (score >= this.config.riskThresholds.askUser) {
748
+ recommendation = 'ask-user';
749
+ }
750
+ else {
751
+ // Factor in specific signals
752
+ const hasIrreversible = factors.some((f) => f.name === 'reversibility-risk' && f.contribution >= 60);
753
+ const hasDangerousCommand = factors.some((f) => f.name === 'command-risk' && f.contribution >= 70);
754
+ const hasProtectedPath = factors.some((f) => f.name === 'protected-path-risk');
755
+ if (hasIrreversible || hasDangerousCommand || hasProtectedPath) {
756
+ recommendation = 'ask-user';
757
+ }
758
+ else {
759
+ recommendation = 'modify';
760
+ }
761
+ }
762
+ const reasoning = factors
763
+ .map((f) => `[${f.contribution}] ${f.name}: ${f.description}`)
764
+ .join('; ');
765
+ return { score, factors, recommendation, reasoning };
766
+ }
767
+ // -----------------------------------------------------------------------
768
+ // LLM evaluation
769
+ // -----------------------------------------------------------------------
770
+ async evaluateWithLLM(toolName, args, context, risk) {
771
+ // Check cache first
772
+ const cacheKey = this.computeCacheKey(toolName, args);
773
+ const cached = this.evaluationCache.get(cacheKey);
774
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
775
+ this.stats.cacheHits++;
776
+ return cached.decision;
777
+ }
778
+ this.stats.cacheMisses++;
779
+ const safeArgsPreview = this.sanitizeArgsForPrompt(args);
780
+ const prompt = `You are a safety monitor for an AI coding assistant. Evaluate this proposed action.
781
+
782
+ Tool: ${toolName}
783
+ Args: ${safeArgsPreview}
784
+ Risk Score: ${risk.score}/100
785
+ Risk Factors: ${risk.factors.map((f) => `${f.name}(${f.contribution})`).join(', ')}
786
+ Safety Level: ${context.safetyLevel}
787
+ Uncommitted Changes: ${context.hasUncommittedChanges}
788
+ Current Cost: $${context.currentCost.toFixed(4)}${context.spendingLimit > 0 ? ` / $${context.spendingLimit}` : ''}
789
+
790
+ Respond with exactly one line in this format:
791
+ DECISION|confidence|reasoning
792
+
793
+ Where DECISION is one of: approve, deny, modify, ask-user
794
+ Confidence is 0.0 to 1.0
795
+ If modify, add a second line with the modified args as JSON.
796
+ If ask-user, add a second line with the question to ask.`;
797
+ try {
798
+ const result = await this.callEvaluatorModel(prompt);
799
+ return this.parseLLMResponse(result.text, risk.score);
800
+ }
801
+ catch {
802
+ // LLM call failed — fall back to risk-based decision
803
+ return {
804
+ action: risk.score >= this.config.riskThresholds.askUser ? 'ask-user' : 'approve',
805
+ confidence: 0.5,
806
+ riskScore: risk.score,
807
+ reasoning: `LLM evaluation failed; falling back to risk threshold. ${risk.reasoning}`,
808
+ learned: false,
809
+ };
810
+ }
811
+ }
812
+ async callEvaluatorModel(prompt) {
813
+ if (!this.apiClient || typeof this.apiClient.createChatCompletion !== 'function') {
814
+ throw new Error('API client not available for smart monitor evaluation');
815
+ }
816
+ const result = await this.apiClient.createChatCompletion({
817
+ model: this.config.evaluatorModel,
818
+ messages: [
819
+ {
820
+ role: 'system',
821
+ content: 'You are a concise safety evaluator. Respond only in the specified format. No explanations beyond the reasoning field.',
822
+ },
823
+ { role: 'user', content: prompt },
824
+ ],
825
+ max_tokens: 200,
826
+ temperature: 0.1,
827
+ });
828
+ const choice = result.choices?.[0];
829
+ if (!choice?.message?.content) {
830
+ throw new Error('Empty response from evaluator model');
831
+ }
832
+ return {
833
+ text: choice.message.content.trim(),
834
+ inputTokens: result.usage?.prompt_tokens ?? 0,
835
+ outputTokens: result.usage?.completion_tokens ?? 0,
836
+ cost: this.estimateCost(result.usage?.prompt_tokens ?? 0, result.usage?.completion_tokens ?? 0),
837
+ };
838
+ }
839
+ parseLLMResponse(text, riskScore) {
840
+ const lines = text.split('\n').filter((l) => l.trim().length > 0);
841
+ const firstLine = lines[0] ?? '';
842
+ const parts = firstLine.split('|');
843
+ const actionStr = (parts[0] ?? 'approve').trim().toLowerCase();
844
+ const confidence = parseFloat(parts[1] ?? '0.5');
845
+ const reasoning = parts.slice(2).join('|').trim() || 'LLM evaluation';
846
+ const validActions = ['approve', 'deny', 'modify', 'ask-user'];
847
+ const action = validActions.includes(actionStr)
848
+ ? actionStr
849
+ : 'ask-user';
850
+ const decision = {
851
+ action,
852
+ confidence: isNaN(confidence) ? 0.5 : Math.max(0, Math.min(1, confidence)),
853
+ riskScore,
854
+ reasoning,
855
+ learned: false,
856
+ };
857
+ if (action === 'modify' && lines.length > 1) {
858
+ try {
859
+ decision.modifiedArgs = JSON.parse(lines[1]);
860
+ }
861
+ catch {
862
+ // Could not parse modified args — ignore
863
+ }
864
+ }
865
+ if (action === 'ask-user' && lines.length > 1) {
866
+ decision.userQuestion = lines[1].trim();
867
+ }
868
+ return decision;
869
+ }
870
+ estimateCost(inputTokens, outputTokens) {
871
+ // Rough pricing for cheap models
872
+ const inputPricePer1M = 0.15; // ~gemma pricing
873
+ const outputPricePer1M = 0.15;
874
+ return (inputTokens * inputPricePer1M + outputTokens * outputPricePer1M) / 1_000_000;
875
+ }
876
+ // -----------------------------------------------------------------------
877
+ // Learning
878
+ // -----------------------------------------------------------------------
879
+ recordDecision(toolName, args, decision, userOverride) {
880
+ if (!this.config.learning.enabled)
881
+ return;
882
+ const patternKey = normalizePattern(toolName, args);
883
+ const existing = this.patterns.get(patternKey);
884
+ if (existing) {
885
+ existing.lastSeen = Date.now();
886
+ if (userOverride === true) {
887
+ // User explicitly overrode a deny → count as approval
888
+ existing.approvals++;
889
+ }
890
+ else if (userOverride === false) {
891
+ // User explicitly overrode an approval → count as denial
892
+ existing.denials++;
893
+ }
894
+ else if (decision.action === 'approve') {
895
+ existing.approvals++;
896
+ }
897
+ else if (decision.action === 'deny') {
898
+ existing.denials++;
899
+ }
900
+ // Check if pattern now qualifies for auto-approve
901
+ existing.active =
902
+ existing.approvals >= this.config.learning.minSamples &&
903
+ existing.denials === 0;
904
+ }
905
+ else {
906
+ this.patterns.set(patternKey, {
907
+ pattern: patternKey,
908
+ approvals: decision.action === 'approve' ? 1 : 0,
909
+ denials: decision.action === 'deny' ? 1 : 0,
910
+ lastSeen: Date.now(),
911
+ active: false,
912
+ });
913
+ }
914
+ this.persistPatterns();
915
+ }
916
+ getLearnedPatterns() {
917
+ return Array.from(this.patterns.values());
918
+ }
919
+ resetLearning() {
920
+ this.patterns.clear();
921
+ try {
922
+ if (existsSync(this.config.learning.storagePath)) {
923
+ const { unlinkSync } = require('fs');
924
+ unlinkSync(this.config.learning.storagePath);
925
+ }
926
+ }
927
+ catch {
928
+ // Ignore
929
+ }
930
+ }
931
+ checkLearnedPatterns(toolName, args) {
932
+ const patternKey = normalizePattern(toolName, args);
933
+ const pattern = this.patterns.get(patternKey);
934
+ if (!pattern || !pattern.active)
935
+ return null;
936
+ if (pattern.approvals < this.config.learning.minSamples)
937
+ return null;
938
+ if (pattern.denials > 0)
939
+ return null;
940
+ return {
941
+ action: 'approve',
942
+ confidence: Math.min(0.95, 0.6 + pattern.approvals * 0.03),
943
+ riskScore: 10,
944
+ reasoning: `Auto-approved based on ${pattern.approvals} past approvals for pattern "${patternKey}"`,
945
+ learned: true,
946
+ };
947
+ }
948
+ loadPatterns() {
949
+ try {
950
+ if (existsSync(this.config.learning.storagePath)) {
951
+ const data = JSON.parse(readFileSync(this.config.learning.storagePath, 'utf-8'));
952
+ if (Array.isArray(data)) {
953
+ for (const p of data) {
954
+ if (p.pattern) {
955
+ this.patterns.set(p.pattern, p);
956
+ }
957
+ }
958
+ }
959
+ }
960
+ }
961
+ catch {
962
+ // Corrupt or missing file — start fresh
963
+ }
964
+ }
965
+ persistPatterns() {
966
+ try {
967
+ const dir = join(this.config.learning.storagePath, '..');
968
+ if (!existsSync(dir))
969
+ mkdirSync(dir, { recursive: true });
970
+ const data = Array.from(this.patterns.values());
971
+ writeFileSync(this.config.learning.storagePath, JSON.stringify(data, null, 2), 'utf-8');
972
+ }
973
+ catch {
974
+ // Cannot persist — continue in-memory
975
+ }
976
+ }
977
+ // -----------------------------------------------------------------------
978
+ // Configuration
979
+ // -----------------------------------------------------------------------
980
+ updateThresholds(thresholds) {
981
+ if (thresholds.autoApprove !== undefined) {
982
+ this.config.riskThresholds.autoApprove = thresholds.autoApprove;
983
+ }
984
+ if (thresholds.askUser !== undefined) {
985
+ this.config.riskThresholds.askUser = thresholds.askUser;
986
+ }
987
+ // Ensure autoApprove < askUser
988
+ if (this.config.riskThresholds.autoApprove >= this.config.riskThresholds.askUser) {
989
+ this.config.riskThresholds.askUser = this.config.riskThresholds.autoApprove + 1;
990
+ }
991
+ }
992
+ addEscalationRule(rule) {
993
+ // Avoid duplicate conditions
994
+ const exists = this.config.escalationRules.some((r) => r.condition === rule.condition);
995
+ if (!exists) {
996
+ this.config.escalationRules.push(rule);
997
+ }
998
+ }
999
+ removeEscalationRule(condition) {
1000
+ this.config.escalationRules = this.config.escalationRules.filter((r) => r.condition !== condition);
1001
+ }
1002
+ getConfig() {
1003
+ return Object.freeze({
1004
+ ...this.config,
1005
+ riskThresholds: { ...this.config.riskThresholds },
1006
+ learning: { ...this.config.learning },
1007
+ contextAwareness: { ...this.config.contextAwareness },
1008
+ escalationRules: [...this.config.escalationRules],
1009
+ });
1010
+ }
1011
+ setEnabled(enabled) {
1012
+ this.config.enabled = enabled;
1013
+ }
1014
+ isEnabled() {
1015
+ return this.config.enabled;
1016
+ }
1017
+ setEvaluatorModel(model) {
1018
+ this.config.evaluatorModel = model;
1019
+ }
1020
+ addProtectedPath(pathPattern) {
1021
+ if (!this.config.contextAwareness.protectedPaths.includes(pathPattern)) {
1022
+ this.config.contextAwareness.protectedPaths.push(pathPattern);
1023
+ }
1024
+ }
1025
+ removeProtectedPath(pathPattern) {
1026
+ this.config.contextAwareness.protectedPaths =
1027
+ this.config.contextAwareness.protectedPaths.filter((p) => p !== pathPattern);
1028
+ }
1029
+ // -----------------------------------------------------------------------
1030
+ // Statistics
1031
+ // -----------------------------------------------------------------------
1032
+ getStats() {
1033
+ const total = this.stats.totalEvaluations || 1;
1034
+ const cacheTotal = this.stats.cacheHits + this.stats.cacheMisses || 1;
1035
+ return {
1036
+ totalEvaluations: this.stats.totalEvaluations,
1037
+ approvals: this.stats.approvals,
1038
+ denials: this.stats.denials,
1039
+ modifications: this.stats.modifications,
1040
+ escalations: this.stats.escalations,
1041
+ approvalRate: this.stats.approvals / total,
1042
+ avgRiskScore: this.stats.totalRiskScore / total,
1043
+ avgEvaluationTimeMs: this.stats.totalEvaluationTimeMs / total,
1044
+ learnedDecisions: this.stats.learnedDecisions,
1045
+ cacheHitRate: this.stats.cacheHits / cacheTotal,
1046
+ };
1047
+ }
1048
+ resetStats() {
1049
+ this.stats = this.freshStats();
1050
+ }
1051
+ // -----------------------------------------------------------------------
1052
+ // Context helpers — gather live project context
1053
+ // -----------------------------------------------------------------------
1054
+ gatherContext(workingDirectory, currentCost, spendingLimit) {
1055
+ let hasUncommittedChanges = false;
1056
+ if (this.config.contextAwareness.checkGitStatus) {
1057
+ try {
1058
+ const status = execSync('git status --porcelain', {
1059
+ cwd: workingDirectory,
1060
+ encoding: 'utf-8',
1061
+ timeout: 5000,
1062
+ });
1063
+ hasUncommittedChanges = status.trim().length > 0;
1064
+ }
1065
+ catch {
1066
+ // Not a git repo or git not available
1067
+ }
1068
+ }
1069
+ let neuroMdContent;
1070
+ const neuroMdPath = join(workingDirectory, 'NEURO.md');
1071
+ try {
1072
+ if (existsSync(neuroMdPath)) {
1073
+ neuroMdContent = readFileSync(neuroMdPath, 'utf-8');
1074
+ }
1075
+ }
1076
+ catch {
1077
+ // Cannot read
1078
+ }
1079
+ return {
1080
+ workingDirectory,
1081
+ modifiedFiles: [],
1082
+ currentCost,
1083
+ spendingLimit,
1084
+ safetyLevel: 'moderate',
1085
+ recentHistory: [],
1086
+ hasUncommittedChanges,
1087
+ neuroMdContent,
1088
+ };
1089
+ }
1090
+ // -----------------------------------------------------------------------
1091
+ // Internal utilities
1092
+ // -----------------------------------------------------------------------
1093
+ adjustForSafetyLevel(decision, level) {
1094
+ if (level === 'conservative') {
1095
+ // Conservative: escalate moderate decisions
1096
+ if (decision.action === 'approve' && decision.riskScore > 20) {
1097
+ return {
1098
+ ...decision,
1099
+ action: 'ask-user',
1100
+ userQuestion: `Conservative mode: risk score ${decision.riskScore}. ${decision.reasoning}. Allow?`,
1101
+ };
1102
+ }
1103
+ if (decision.action === 'modify') {
1104
+ return {
1105
+ ...decision,
1106
+ action: 'ask-user',
1107
+ userQuestion: `Conservative mode requires approval for modifications. ${decision.reasoning}. Allow?`,
1108
+ };
1109
+ }
1110
+ }
1111
+ if (level === 'aggressive') {
1112
+ // Aggressive: auto-approve moderate decisions
1113
+ if (decision.action === 'ask-user' && decision.riskScore < 60) {
1114
+ return {
1115
+ ...decision,
1116
+ action: 'approve',
1117
+ reasoning: `Aggressive mode auto-approved (risk ${decision.riskScore} < 60). ${decision.reasoning}`,
1118
+ };
1119
+ }
1120
+ }
1121
+ return decision;
1122
+ }
1123
+ scoreToConfidence(score, action) {
1124
+ if (action === 'approve') {
1125
+ // Lower score → higher confidence in approving
1126
+ return Math.max(0.5, 1.0 - score / 100);
1127
+ }
1128
+ // Higher score → higher confidence in escalating
1129
+ return Math.max(0.5, score / 100);
1130
+ }
1131
+ computeCacheKey(toolName, args) {
1132
+ const filePath = extractFilePath(args);
1133
+ const command = extractCommand(args);
1134
+ return `${toolName}:${filePath ?? ''}:${command ?? ''}:${this.sanitizeArgsForPrompt(args).slice(0, 100)}`;
1135
+ }
1136
+ cacheDecision(toolName, args, decision) {
1137
+ const key = this.computeCacheKey(toolName, args);
1138
+ this.evaluationCache.set(key, { key, decision, timestamp: Date.now() });
1139
+ // Evict old entries if cache is too large
1140
+ if (this.evaluationCache.size > MAX_CACHE_ENTRIES) {
1141
+ const entries = Array.from(this.evaluationCache.entries());
1142
+ entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
1143
+ const toRemove = entries.slice(0, entries.length - MAX_CACHE_ENTRIES);
1144
+ for (const [k] of toRemove) {
1145
+ this.evaluationCache.delete(k);
1146
+ }
1147
+ }
1148
+ }
1149
+ sanitizeArgsForPrompt(args) {
1150
+ try {
1151
+ const sanitized = {};
1152
+ for (const [key, value] of Object.entries(args)) {
1153
+ if (typeof value === 'string') {
1154
+ sanitized[key] = value.length > 200 ? value.slice(0, 200) + '...' : value;
1155
+ }
1156
+ else if (typeof value === 'number' || typeof value === 'boolean') {
1157
+ sanitized[key] = value;
1158
+ }
1159
+ else if (Array.isArray(value)) {
1160
+ sanitized[key] = `[Array:${value.length}]`;
1161
+ }
1162
+ else if (value && typeof value === 'object') {
1163
+ sanitized[key] = '{Object}';
1164
+ }
1165
+ else {
1166
+ sanitized[key] = String(value);
1167
+ }
1168
+ }
1169
+ return JSON.stringify(sanitized);
1170
+ }
1171
+ catch {
1172
+ return '{unable to serialize args}';
1173
+ }
1174
+ }
1175
+ freshStats() {
1176
+ return {
1177
+ totalEvaluations: 0,
1178
+ approvals: 0,
1179
+ denials: 0,
1180
+ modifications: 0,
1181
+ escalations: 0,
1182
+ learnedDecisions: 0,
1183
+ cacheHits: 0,
1184
+ cacheMisses: 0,
1185
+ totalRiskScore: 0,
1186
+ totalEvaluationTimeMs: 0,
1187
+ };
1188
+ }
1189
+ }
1190
+ // ---------------------------------------------------------------------------
1191
+ // Factory helper
1192
+ // ---------------------------------------------------------------------------
1193
+ export function createSmartMonitor(config = {}, apiClient = null) {
1194
+ return new SmartMonitor(config, apiClient);
1195
+ }
1196
+ export function defaultSmartMonitorConfig() {
1197
+ return { ...DEFAULT_CONFIG };
1198
+ }
1199
+ //# sourceMappingURL=smart-monitor.js.map