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.
- package/LICENSE +21 -0
- package/README.md +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- 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
|