micro-models-agent 0.63.3 → 1.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/CHANGELOG.md +148 -1
- package/dist/cli/cache-line.js +30 -0
- package/dist/cli/command-suggest.js +38 -0
- package/dist/cli/commands.js +285 -60
- package/dist/cli/completer.js +16 -16
- package/dist/cli/json-payload.js +32 -0
- package/dist/cli/main.js +165 -77
- package/dist/cli/plugin-commands.js +5 -4
- package/dist/cli/relaunch.js +37 -0
- package/dist/cli/repl-commands.js +441 -307
- package/dist/cli/repl.js +360 -83
- package/dist/cli/run-result.js +12 -6
- package/dist/cli/security-commands.js +64 -60
- package/dist/cli/setup-order.js +57 -0
- package/dist/cli/setup-prompt.js +49 -0
- package/dist/cli/setup.js +52 -48
- package/dist/config/budget.js +48 -0
- package/dist/config/config.js +132 -70
- package/dist/config/defaults.js +37 -11
- package/dist/config/domains.js +9 -50
- package/dist/config/utils.js +56 -0
- package/dist/core/agent/audit-gate.js +49 -0
- package/dist/core/agent/compaction.js +89 -0
- package/dist/core/agent/constants.js +61 -0
- package/dist/core/agent/context-renderer.js +40 -0
- package/dist/core/agent/hallucination-gate.js +87 -0
- package/dist/core/agent/loop-state.js +53 -0
- package/dist/core/agent/prefix-monitor.js +101 -0
- package/dist/core/agent/reasoning-resolver.js +56 -0
- package/dist/core/agent/token-tracker.js +96 -0
- package/dist/core/agent/tool-batch.js +237 -0
- package/dist/core/agent/tool-output.js +62 -0
- package/dist/core/agent-moe.js +214 -69
- package/dist/core/agent.js +506 -546
- package/dist/core/bootstrap.js +297 -98
- package/dist/core/crash-handler.js +2 -1
- package/dist/core/prompt-builder.js +3 -0
- package/dist/core/prompt-overflow.js +307 -0
- package/dist/core/session-logger.js +34 -2
- package/dist/i18n/en.json +7 -4
- package/dist/i18n/ru.json +7 -4
- package/dist/index.js +5 -1
- package/dist/llm/cache-usage.js +76 -0
- package/dist/llm/image-utils.js +20 -16
- package/dist/llm/llm-errors.js +41 -0
- package/dist/llm/model-loader.js +30 -0
- package/dist/llm/openai-compat.js +287 -101
- package/dist/llm/orchestrator.js +140 -68
- package/dist/llm/provider-budget.js +68 -0
- package/dist/llm/provider.js +0 -1
- package/dist/llm/stream-state.js +26 -0
- package/dist/llm/token-counter.js +28 -0
- package/dist/logger/app-logger.js +12 -15
- package/dist/main.js +1606 -800
- package/dist/migration/detect.js +3 -1
- package/dist/modules/browser/actions.js +0 -3
- package/dist/modules/browser/bridge-client.js +2 -0
- package/dist/modules/browser/driver.js +46 -4
- package/dist/modules/certification/cli.js +85 -42
- package/dist/modules/certification/loader.js +15 -1
- package/dist/modules/certification/manifest.js +126 -15
- package/dist/modules/certification/runner.js +4 -26
- package/dist/modules/certification/scenarios.js +184 -5
- package/dist/modules/certification/syntax-scenarios.js +51 -0
- package/dist/modules/context/chunk-query.js +25 -5
- package/dist/modules/context/fact-extractor.js +6 -2
- package/dist/modules/context/manager.js +23 -7
- package/dist/modules/execution/audit-runners.js +7 -1
- package/dist/modules/execution/auditor.js +3 -3
- package/dist/modules/execution/execution-plugin.js +22 -15
- package/dist/modules/execution/input-from.js +46 -0
- package/dist/modules/execution/module.js +107 -18
- package/dist/modules/execution/moe-executor.js +166 -54
- package/dist/modules/execution/plan-actions.js +524 -0
- package/dist/modules/execution/plan-steps.js +23 -0
- package/dist/modules/execution/plan-store.js +15 -3
- package/dist/modules/execution/plan-tool.js +6 -488
- package/dist/modules/execution/plan-validator.js +24 -0
- package/dist/modules/execution/stuck-detector.js +3 -18
- package/dist/modules/execution/tracker.js +14 -5
- package/dist/modules/execution/transient-error.js +30 -0
- package/dist/modules/execution/verifier.js +94 -7
- package/dist/modules/execution/windows-commands.js +11 -0
- package/dist/modules/hallucination/confidence.js +36 -23
- package/dist/modules/hallucination/consistency.js +3 -0
- package/dist/modules/hallucination/detector.js +8 -3
- package/dist/modules/hallucination/factual.js +26 -7
- package/dist/modules/hallucination/llm-judge.js +12 -2
- package/dist/modules/indexer/map-command.js +35 -0
- package/dist/modules/indexer/map-select.js +87 -0
- package/dist/modules/indexer/module.js +34 -22
- package/dist/modules/indexer/symbols.js +189 -0
- package/dist/modules/indexer/walker.js +96 -42
- package/dist/modules/lsp/check-tool.js +2 -1
- package/dist/modules/lsp/client.js +49 -32
- package/dist/modules/lsp/config.js +55 -2
- package/dist/modules/lsp/module.js +38 -5
- package/dist/modules/lsp/probe.js +4 -3
- package/dist/modules/lsp/project-root.js +41 -1
- package/dist/modules/lsp/startup-check.js +12 -4
- package/dist/modules/mcp/client.js +153 -104
- package/dist/modules/mcp/module.js +165 -41
- package/dist/modules/memory/module.js +4 -3
- package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
- package/dist/modules/plugins/manager.js +47 -84
- package/dist/modules/pricing/index.js +17 -7
- package/dist/modules/pricing/prices.js +30 -12
- package/dist/modules/processes/index.js +1 -0
- package/dist/modules/processes/kill-tree.js +56 -0
- package/dist/modules/processes/registry.js +2 -54
- package/dist/modules/providers/cache.js +23 -0
- package/dist/modules/providers/factory.js +28 -0
- package/dist/modules/providers/fallback.js +7 -5
- package/dist/modules/providers/health.js +2 -1
- package/dist/modules/providers/index.js +1 -0
- package/dist/modules/providers/manager.js +17 -2
- package/dist/modules/providers/presets.js +79 -6
- package/dist/modules/reasoning/policy.js +40 -0
- package/dist/modules/reasoning/probe.js +111 -0
- package/dist/modules/security/audit-notifier.js +42 -27
- package/dist/modules/security/command-validator.js +25 -20
- package/dist/modules/security/encryption.js +6 -12
- package/dist/modules/security/network-validator.js +76 -5
- package/dist/modules/security/path-validator.js +77 -34
- package/dist/modules/security/rate-limiter.js +11 -0
- package/dist/modules/security/security-policies.js +1 -1
- package/dist/modules/security/session-encryption.js +13 -2
- package/dist/modules/security/session-isolation.js +2 -9
- package/dist/modules/session/manager.js +11 -0
- package/dist/modules/session/module.js +11 -3
- package/dist/modules/session/store.js +41 -5
- package/dist/modules/skills/loader.js +7 -1
- package/dist/modules/skills/module.js +2 -1
- package/dist/modules/updater/changelog-reader.js +94 -0
- package/dist/modules/updater/dev-detect.js +17 -0
- package/dist/modules/updater/index.js +1 -0
- package/dist/modules/updater/module.js +14 -3
- package/dist/output/bus.js +32 -0
- package/dist/output/channel.js +233 -0
- package/dist/output/format.js +14 -0
- package/dist/output/index.js +7 -0
- package/dist/output/json-sink.js +22 -0
- package/dist/output/machine.js +8 -0
- package/dist/output/session-sink.js +27 -0
- package/dist/output/types.js +1 -0
- package/dist/tools/approve.js +6 -2
- package/dist/tools/attach-image.js +11 -11
- package/dist/tools/auto-fixer.js +198 -0
- package/dist/tools/bash.js +142 -89
- package/dist/tools/chunk-query.js +10 -6
- package/dist/tools/download-file.js +1 -1
- package/dist/tools/edit-file.js +20 -2
- package/dist/tools/executor.js +54 -9
- package/dist/tools/glob-tool.js +7 -0
- package/dist/tools/grep-tool.js +15 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/list-dir.js +3 -1
- package/dist/tools/load-skill.js +2 -1
- package/dist/tools/mcp-call.js +1 -1
- package/dist/tools/move-file.js +5 -4
- package/dist/tools/path-utils.js +7 -0
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/prompt-io.js +28 -0
- package/dist/tools/question.js +12 -12
- package/dist/tools/scope-request.js +91 -0
- package/dist/tools/session-info.js +44 -0
- package/dist/tools/set-thinking.js +71 -0
- package/dist/tools/subagent.js +50 -9
- package/dist/tools/syntax-validator.js +177 -0
- package/dist/tools/user-input.js +16 -9
- package/dist/tools/write-file.js +17 -1
- package/dist/ui/diff.js +10 -0
- package/dist/ui/line-editor.js +179 -26
- package/dist/ui/line-math.js +20 -3
- package/dist/ui/md-formatter.js +100 -10
- package/dist/ui/output.js +5 -4
- package/dist/ui/plan-view.js +2 -7
- package/dist/ui/renderer.js +89 -85
- package/dist/ui/spinner.js +14 -4
- package/dist/utils/error.js +4 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/retry.js +17 -0
- package/dist/utils/sleep.js +23 -0
- package/dist/utils/truncate.js +9 -0
- package/package.json +1 -1
|
@@ -59,17 +59,17 @@ export const attachImageTool = {
|
|
|
59
59
|
output: t("image.not_found", { path: source }),
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
-
// Security: check path scope
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
}
|
|
62
|
+
// Security: check global path policy AND subagent scope (same
|
|
63
|
+
// combination as read_file).
|
|
64
|
+
const { isPathInScope } = await import("../modules/security/path-validator");
|
|
65
|
+
const { DEFAULT_SECURITY_CONFIG } = await import("../config/security");
|
|
66
|
+
const validation = isPathInScope(ctx.baseDir, absPath, ctx.scope, ctx.config?.security?.paths || DEFAULT_SECURITY_CONFIG.paths);
|
|
67
|
+
if (!validation.allowed) {
|
|
68
|
+
logSecurityBlock(ctx.sessionId, "file_read", validation.reason ?? "attach_image out of scope", absPath);
|
|
69
|
+
return {
|
|
70
|
+
success: false,
|
|
71
|
+
output: t("file.path_not_allowed", { path: source }),
|
|
72
|
+
};
|
|
73
73
|
}
|
|
74
74
|
const result = await loadFileAsDataUrl(absPath);
|
|
75
75
|
dataUrl = result.dataUrl;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { extname } from "path";
|
|
2
|
+
/**
|
|
3
|
+
* LSP error codes that can be auto-fixed.
|
|
4
|
+
*/
|
|
5
|
+
const AUTO_FIXABLE_CODES = new Set([
|
|
6
|
+
2307, // Cannot find module (import missing)
|
|
7
|
+
2440, // Import declaration conflicts with local declaration
|
|
8
|
+
6133, // Variable is declared but its value is never read
|
|
9
|
+
2769, // No overload matches this call (simple type mismatches)
|
|
10
|
+
]);
|
|
11
|
+
/**
|
|
12
|
+
* Extract LSP errors from tsc output.
|
|
13
|
+
* Format: file(line,col): error TSxxxx: message
|
|
14
|
+
*/
|
|
15
|
+
export function parseTscOutput(output) {
|
|
16
|
+
const errors = [];
|
|
17
|
+
const regex = /(.+?)\((\d+),(\d+)\): error TS(\d+): (.+)/g;
|
|
18
|
+
let match;
|
|
19
|
+
while ((match = regex.exec(output))) {
|
|
20
|
+
errors.push({
|
|
21
|
+
file: match[1],
|
|
22
|
+
line: parseInt(match[2], 10),
|
|
23
|
+
column: parseInt(match[3], 10),
|
|
24
|
+
length: 0, // tsc doesn't provide length
|
|
25
|
+
code: parseInt(match[4], 10),
|
|
26
|
+
message: match[5],
|
|
27
|
+
severity: "error",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return errors;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Try to auto-fix LSP errors in TypeScript/JavaScript code.
|
|
34
|
+
* Only fixes common, safe patterns:
|
|
35
|
+
* - TS2440: Import conflict → rename import with `as`
|
|
36
|
+
* - TS6133: Unused import → remove from import list
|
|
37
|
+
*/
|
|
38
|
+
export function autoFixErrors(filePath, content, errors) {
|
|
39
|
+
const ext = extname(filePath).toLowerCase();
|
|
40
|
+
const isTsFile = [".ts", ".tsx", ".mts", ".cts"].includes(ext);
|
|
41
|
+
const isJsFile = [".js", ".jsx", ".mjs", ".cjs"].includes(ext);
|
|
42
|
+
if (!isTsFile && !isJsFile) {
|
|
43
|
+
return { fixed: false, newContent: content, applied: [], remaining: errors };
|
|
44
|
+
}
|
|
45
|
+
let newContent = content;
|
|
46
|
+
const applied = [];
|
|
47
|
+
const remaining = [];
|
|
48
|
+
for (const error of errors) {
|
|
49
|
+
if (!AUTO_FIXABLE_CODES.has(error.code)) {
|
|
50
|
+
remaining.push(error);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const result = fixError(newContent, error);
|
|
55
|
+
if (result.fixed) {
|
|
56
|
+
newContent = result.newContent;
|
|
57
|
+
applied.push(`TS${error.code}: ${error.message.slice(0, 50)}`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
remaining.push(error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
remaining.push(error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
fixed: applied.length > 0,
|
|
69
|
+
newContent,
|
|
70
|
+
applied,
|
|
71
|
+
remaining,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Try to fix a single LSP error.
|
|
76
|
+
*/
|
|
77
|
+
function fixError(content, error) {
|
|
78
|
+
const lines = content.split("\n");
|
|
79
|
+
const lineIndex = error.line - 1;
|
|
80
|
+
if (lineIndex < 0 || lineIndex >= lines.length) {
|
|
81
|
+
return { fixed: false, newContent: content };
|
|
82
|
+
}
|
|
83
|
+
const line = lines[lineIndex];
|
|
84
|
+
switch (error.code) {
|
|
85
|
+
case 2440: // Import declaration conflicts with local declaration
|
|
86
|
+
return fixImportConflict(lines, lineIndex, error);
|
|
87
|
+
case 6133: // Variable is declared but its value is never read
|
|
88
|
+
return fixUnusedImport(lines, lineIndex, error);
|
|
89
|
+
default:
|
|
90
|
+
return { fixed: false, newContent: content };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Fix TS2440: Import declaration conflicts with local declaration.
|
|
95
|
+
* Strategy: Add `as LocalName` to the import.
|
|
96
|
+
*
|
|
97
|
+
* Example:
|
|
98
|
+
* import { foo } from './module';
|
|
99
|
+
* export function foo() { ... }
|
|
100
|
+
* Becomes:
|
|
101
|
+
* import { foo as fooImport } from './module';
|
|
102
|
+
* export function foo() { ... }
|
|
103
|
+
*/
|
|
104
|
+
function fixImportConflict(lines, lineIndex, error) {
|
|
105
|
+
const line = lines[lineIndex];
|
|
106
|
+
// Find the conflicting name in the error message
|
|
107
|
+
const nameMatch = error.message.match(/Import declaration conflicts with local declaration of '(\w+)'/);
|
|
108
|
+
if (!nameMatch) {
|
|
109
|
+
return { fixed: false, newContent: lines.join("\n") };
|
|
110
|
+
}
|
|
111
|
+
const conflictingName = nameMatch[1];
|
|
112
|
+
// Find the import line (might be above the current line)
|
|
113
|
+
let importLineIndex = lineIndex;
|
|
114
|
+
while (importLineIndex >= 0 && !lines[importLineIndex].includes("import")) {
|
|
115
|
+
importLineIndex--;
|
|
116
|
+
}
|
|
117
|
+
if (importLineIndex < 0) {
|
|
118
|
+
return { fixed: false, newContent: lines.join("\n") };
|
|
119
|
+
}
|
|
120
|
+
const importLine = lines[importLineIndex];
|
|
121
|
+
// Check if this is a named import: import { ... } from '...'
|
|
122
|
+
const namedImportMatch = importLine.match(/import\s+(?:type\s+)?\{([^}]+)\}\s+from/);
|
|
123
|
+
if (!namedImportMatch) {
|
|
124
|
+
return { fixed: false, newContent: lines.join("\n") };
|
|
125
|
+
}
|
|
126
|
+
const imports = namedImportMatch[1];
|
|
127
|
+
const importParts = imports.split(",").map((s) => s.trim());
|
|
128
|
+
// Check if the conflicting name is in the imports
|
|
129
|
+
const nameIndex = importParts.findIndex((part) => {
|
|
130
|
+
const baseName = part.split(/\s+as\s+/)[0].trim();
|
|
131
|
+
return baseName === conflictingName;
|
|
132
|
+
});
|
|
133
|
+
if (nameIndex === -1) {
|
|
134
|
+
return { fixed: false, newContent: lines.join("\n") };
|
|
135
|
+
}
|
|
136
|
+
// Check if already has an alias
|
|
137
|
+
const currentImport = importParts[nameIndex];
|
|
138
|
+
if (currentImport.includes(" as ")) {
|
|
139
|
+
return { fixed: false, newContent: lines.join("\n") };
|
|
140
|
+
}
|
|
141
|
+
// Add alias
|
|
142
|
+
importParts[nameIndex] = `${conflictingName} as ${conflictingName}Import`;
|
|
143
|
+
lines[importLineIndex] = importLine.replace(namedImportMatch[1], importParts.join(", "));
|
|
144
|
+
return { fixed: true, newContent: lines.join("\n") };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Fix TS6133: Variable is declared but its value is never read.
|
|
148
|
+
* Strategy: Remove the unused import from the import list.
|
|
149
|
+
*
|
|
150
|
+
* Example:
|
|
151
|
+
* import { foo, bar } from './module';
|
|
152
|
+
* console.log(bar);
|
|
153
|
+
* Becomes:
|
|
154
|
+
* import { bar } from './module';
|
|
155
|
+
* console.log(bar);
|
|
156
|
+
*/
|
|
157
|
+
function fixUnusedImport(lines, lineIndex, error) {
|
|
158
|
+
const line = lines[lineIndex];
|
|
159
|
+
// Find the unused name in the error message
|
|
160
|
+
const nameMatch = error.message.match(/'(\w+)' is declared but its value is never read/);
|
|
161
|
+
if (!nameMatch) {
|
|
162
|
+
return { fixed: false, newContent: lines.join("\n") };
|
|
163
|
+
}
|
|
164
|
+
const unusedName = nameMatch[1];
|
|
165
|
+
// Find the import line (might be above the current line)
|
|
166
|
+
let importLineIndex = lineIndex;
|
|
167
|
+
while (importLineIndex >= 0 && !lines[importLineIndex].includes("import")) {
|
|
168
|
+
importLineIndex--;
|
|
169
|
+
}
|
|
170
|
+
if (importLineIndex < 0) {
|
|
171
|
+
return { fixed: false, newContent: lines.join("\n") };
|
|
172
|
+
}
|
|
173
|
+
const importLine = lines[importLineIndex];
|
|
174
|
+
// Check if this is a named import: import { ... } from '...'
|
|
175
|
+
const namedImportMatch = importLine.match(/import\s+(?:type\s+)?\{([^}]+)\}\s+from/);
|
|
176
|
+
if (!namedImportMatch) {
|
|
177
|
+
return { fixed: false, newContent: lines.join("\n") };
|
|
178
|
+
}
|
|
179
|
+
const imports = namedImportMatch[1];
|
|
180
|
+
const importParts = imports.split(",").map((s) => s.trim());
|
|
181
|
+
// Find and remove the unused import
|
|
182
|
+
const nameIndex = importParts.findIndex((part) => {
|
|
183
|
+
const baseName = part.split(/\s+as\s+/)[0].trim();
|
|
184
|
+
return baseName === unusedName;
|
|
185
|
+
});
|
|
186
|
+
if (nameIndex === -1) {
|
|
187
|
+
return { fixed: false, newContent: lines.join("\n") };
|
|
188
|
+
}
|
|
189
|
+
importParts.splice(nameIndex, 1);
|
|
190
|
+
// If no imports left, remove the entire import line
|
|
191
|
+
if (importParts.length === 0) {
|
|
192
|
+
lines.splice(importLineIndex, 1);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
lines[importLineIndex] = importLine.replace(namedImportMatch[1], importParts.join(", "));
|
|
196
|
+
}
|
|
197
|
+
return { fixed: true, newContent: lines.join("\n") };
|
|
198
|
+
}
|
package/dist/tools/bash.js
CHANGED
|
@@ -80,13 +80,13 @@ function adaptCommandForWindows(command) {
|
|
|
80
80
|
if (/\|\|\s*true\b/.test(command)) {
|
|
81
81
|
command = command.replace(/\s*\|\|\s*true\b/g, "; exit 0");
|
|
82
82
|
}
|
|
83
|
-
// Windows mkdir does not support -p flag, but creates intermediate dirs by default
|
|
83
|
+
// Windows mkdir does not support -p/--parents flag, but creates intermediate dirs by default
|
|
84
84
|
const trimmed = command.trim();
|
|
85
|
-
if (
|
|
86
|
-
return trimmed.replace(/^mkdir
|
|
85
|
+
if (/^mkdir\s+(-p|--parents)\b/.test(trimmed)) {
|
|
86
|
+
return trimmed.replace(/^mkdir\s+(-p|--parents)\s*/, "mkdir ");
|
|
87
87
|
}
|
|
88
|
-
if (
|
|
89
|
-
return
|
|
88
|
+
if (/^mkdir\s+(-p|--parents)\s*$/.test(trimmed)) {
|
|
89
|
+
return "mkdir";
|
|
90
90
|
}
|
|
91
91
|
// Translate simple Unix commands to their cmd.exe equivalents. Only the
|
|
92
92
|
// leading word is rewritten; flags are passed through (ls -la → dir -la,
|
|
@@ -112,12 +112,24 @@ const UNIX_TO_WIN_HINTS = {
|
|
|
112
112
|
mv: "Use the move_file tool instead.",
|
|
113
113
|
rm: "Use the delete_file tool instead.",
|
|
114
114
|
grep: "Use the grep tool instead.",
|
|
115
|
+
sed: "Use the edit_file tool instead.",
|
|
116
|
+
awk: "Not available in cmd.exe. Use a Node.js one-liner or the grep tool instead.",
|
|
115
117
|
chmod: "Use the chmod tool instead.",
|
|
116
118
|
touch: "Use the write_file tool instead.",
|
|
117
119
|
find: "Use the glob tool instead.",
|
|
118
120
|
head: "Use the read_file tool with offset/limit instead.",
|
|
119
121
|
tail: "Use the read_file tool instead.",
|
|
120
122
|
wc: "Use the read_file tool instead.",
|
|
123
|
+
uniq: "Not available in cmd.exe. Use a Node.js one-liner instead.",
|
|
124
|
+
xargs: "Not available in cmd.exe. Use a Node.js one-liner instead.",
|
|
125
|
+
cut: "Not available in cmd.exe. Use a Node.js one-liner instead.",
|
|
126
|
+
tr: "Not available in cmd.exe. Use a Node.js one-liner instead.",
|
|
127
|
+
basename: "Not available in cmd.exe. Use a Node.js one-liner instead.",
|
|
128
|
+
dirname: "Not available in cmd.exe. Use a Node.js one-liner instead.",
|
|
129
|
+
export: 'cmd.exe has no export - use "set VAR=value" (no spaces around =).',
|
|
130
|
+
source: 'cmd.exe has no source - use "call script.bat".',
|
|
131
|
+
sleep: 'Use "timeout /t N" instead.',
|
|
132
|
+
env: "cmd.exe has no env - use set to list/set variables.",
|
|
121
133
|
diff: "Use the diff tool instead.",
|
|
122
134
|
which: 'Use "where" instead.',
|
|
123
135
|
echo: "echo works on Windows, but avoid pipes (|).",
|
|
@@ -298,14 +310,126 @@ export function emptyCliRunHint(command, output, code) {
|
|
|
298
310
|
* the same failing `npx`/`bunx`/`npm run` command).
|
|
299
311
|
*/
|
|
300
312
|
const NPM_EXEC_RE = /could not determine executable to run/i;
|
|
313
|
+
/**
|
|
314
|
+
* A command that waited for interactive input with no TTY aborts with
|
|
315
|
+
* "Operation cancelled" (Bun/Node prompt) or "stdin is not a TTY". Common on
|
|
316
|
+
* bare `bun create`/`npm create` scaffolds. Append a concrete non-interactive
|
|
317
|
+
* remedy instead of letting the model retry the identical interactive command.
|
|
318
|
+
*/
|
|
319
|
+
const INTERACTIVE_PROMPT_RE = /operation (?:cancelled|canceled|aborted)|stdin is not a tty|not a tty|cannot prompt without a tty/i;
|
|
301
320
|
/** process-management tools hidden behind enable_tools (tag "shell"). */
|
|
302
321
|
const HIDDEN_PROCESS_TOOLS = ["process_list", "process_log", "process_kill"];
|
|
322
|
+
/**
|
|
323
|
+
* Разрешение bash-security-конфига: session-изоляция поверх глобального,
|
|
324
|
+
* мастер-выключатель `enabled: false` гасит все под-модули.
|
|
325
|
+
*/
|
|
326
|
+
function resolveBashSecurityConfig(ctx) {
|
|
327
|
+
const appConfig = ctx.config || {};
|
|
328
|
+
const fullSecurityConfig = ctx.sessionContext
|
|
329
|
+
? getSessionSecurityConfig(appConfig, ctx.sessionContext)
|
|
330
|
+
: appConfig.security || DEFAULT_SECURITY_CONFIG;
|
|
331
|
+
return fullSecurityConfig.enabled === false
|
|
332
|
+
? { ...(fullSecurityConfig.bash || DEFAULT_SECURITY_CONFIG.bash), enabled: false }
|
|
333
|
+
: fullSecurityConfig.bash || DEFAULT_SECURITY_CONFIG.bash;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Декорация вывода завершённой команды: подсказки (пустой прогон скрипта,
|
|
337
|
+
* упавшие тесты, npm-без-bin, Unix-команды в cmd.exe), audit-лог и обрезка.
|
|
338
|
+
* Вынесена из god-handler'а bash — инкапсулирует всю пост-обработку вывода.
|
|
339
|
+
*/
|
|
340
|
+
function decorateCommandOutput(opts) {
|
|
341
|
+
let output = opts.output;
|
|
342
|
+
const { ctx, command, originalCommand, code, testRun, workdir, securityConfig } = opts;
|
|
343
|
+
// R5: script file ran with exit 0 but empty output — likely a missing
|
|
344
|
+
// entry point (main is never called with argv).
|
|
345
|
+
const cliHint = emptyCliRunHint(command, output, code);
|
|
346
|
+
if (cliHint) {
|
|
347
|
+
output = `(exit code 0, no output)\n\nHint: ${cliHint}`;
|
|
348
|
+
}
|
|
349
|
+
// Auto-verify test runs: a failing suite must not be reported as a clean
|
|
350
|
+
// success. Inject a prominent marker the model cannot miss (the typical
|
|
351
|
+
// failure: tests fail but the CLI exit code is 0).
|
|
352
|
+
if (testRun && testRun.failed > 0) {
|
|
353
|
+
output =
|
|
354
|
+
t("exec.test_runner_fail", {
|
|
355
|
+
framework: testRun.framework,
|
|
356
|
+
failed: String(testRun.failed),
|
|
357
|
+
passed: String(testRun.passed),
|
|
358
|
+
}) +
|
|
359
|
+
`\n\n${output}`;
|
|
360
|
+
}
|
|
361
|
+
else if (testRun && testRun.failed === 0 && testRun.passed > 0) {
|
|
362
|
+
output = `${t("exec.test_runner_pass", { framework: testRun.framework, passed: String(testRun.passed) })}\n\n${output}`;
|
|
363
|
+
}
|
|
364
|
+
if (!output && code !== 0) {
|
|
365
|
+
output = `(exit code ${code})`;
|
|
366
|
+
}
|
|
367
|
+
// npm/npx/bunx: "could not determine executable to run" — the package has
|
|
368
|
+
// no `bin` entry (or the script does not exist). Cross-platform, so it runs
|
|
369
|
+
// before the win32 hint block.
|
|
370
|
+
output = npmExecHint(output);
|
|
371
|
+
// Interactive prompt with no TTY (Bun/Node "Operation cancelled").
|
|
372
|
+
output = interactivePromptHint(output);
|
|
373
|
+
// On Windows, hint about Unix commands that don't work, and hard-stop a
|
|
374
|
+
// command that keeps failing the same way. The hint keys on the ORIGINAL
|
|
375
|
+
// command word (before adaptCommandForWindows translated cat→type): the
|
|
376
|
+
// translation is only for execution, but the model wrote `cat`, and that is
|
|
377
|
+
// what UNIX_TO_WIN_HINTS knows about. Keying on the translated word left
|
|
378
|
+
// `cat` invisible to the hint/block (observed: model ran cat 4+ times).
|
|
379
|
+
if (platform() === "win32") {
|
|
380
|
+
const originalFirstWord = originalCommand.trim().split(/\s+/)[0]?.split(/[\\/]/).pop();
|
|
381
|
+
if (originalFirstWord && originalFirstWord in UNIX_TO_WIN_HINTS) {
|
|
382
|
+
if (code === 0) {
|
|
383
|
+
FAILING_FIRST_WORDS.delete(originalFirstWord);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
const failures = (FAILING_FIRST_WORDS.get(originalFirstWord) || 0) + 1;
|
|
387
|
+
FAILING_FIRST_WORDS.set(originalFirstWord, failures);
|
|
388
|
+
if (failures >= HARD_BLOCK_THRESHOLD) {
|
|
389
|
+
output = `STOP using "${originalFirstWord}" — it does not work in this cmd.exe shell and has failed ${failures} times in a row. ${UNIX_TO_WIN_HINTS[originalFirstWord]}`;
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
output = `${output}\n\nHint: "${originalFirstWord}" may not work on Windows. ${UNIX_TO_WIN_HINTS[originalFirstWord]}`;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// Update audit log with result
|
|
398
|
+
if (securityConfig?.logCommands) {
|
|
399
|
+
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), code === 0, `Working directory: ${workdir}, Output length: ${output.length}`);
|
|
400
|
+
}
|
|
401
|
+
const lines = output.split("\n");
|
|
402
|
+
if (testRun && testRun.failed > 0) {
|
|
403
|
+
// Keep the failure details: test runners print passing markers first and
|
|
404
|
+
// the errors + summary at the very end. A short head preview hides exactly
|
|
405
|
+
// what the model needs to fix the failures.
|
|
406
|
+
const TEST_TAIL_LINES = 400;
|
|
407
|
+
const kept = lines.slice(-TEST_TAIL_LINES);
|
|
408
|
+
const skipped = lines.length - kept.length;
|
|
409
|
+
output =
|
|
410
|
+
(skipped > 0 ? `[... ${skipped} earlier lines omitted — failing tests below]\n` : "") +
|
|
411
|
+
kept.join("\n");
|
|
412
|
+
}
|
|
413
|
+
else if (lines.length > MAX_PREVIEW_LINES) {
|
|
414
|
+
output =
|
|
415
|
+
lines.slice(0, MAX_PREVIEW_LINES).join("\n") +
|
|
416
|
+
`\n... (${lines.length - MAX_PREVIEW_LINES} more lines)`;
|
|
417
|
+
}
|
|
418
|
+
return output;
|
|
419
|
+
}
|
|
303
420
|
export function npmExecHint(output) {
|
|
304
421
|
if (NPM_EXEC_RE.test(output)) {
|
|
305
422
|
return `${output}\n\nHint: ${t("exec.npm_exec_hint")}`;
|
|
306
423
|
}
|
|
307
424
|
return output;
|
|
308
425
|
}
|
|
426
|
+
/** Append a non-interactive remedy when a command aborted waiting on stdin. */
|
|
427
|
+
export function interactivePromptHint(output) {
|
|
428
|
+
if (INTERACTIVE_PROMPT_RE.test(output)) {
|
|
429
|
+
return `${output}\n\nHint: ${t("exec.interactive_hint")}`;
|
|
430
|
+
}
|
|
431
|
+
return output;
|
|
432
|
+
}
|
|
309
433
|
export const bashTool = {
|
|
310
434
|
name: "bash",
|
|
311
435
|
icon: "💻",
|
|
@@ -370,15 +494,8 @@ export const bashTool = {
|
|
|
370
494
|
}
|
|
371
495
|
}
|
|
372
496
|
const workdir = args.workdir ? String(args.workdir) : ctx.baseDir;
|
|
373
|
-
//
|
|
374
|
-
const
|
|
375
|
-
const fullSecurityConfig = ctx.sessionContext
|
|
376
|
-
? getSessionSecurityConfig(appConfig, ctx.sessionContext)
|
|
377
|
-
: appConfig.security || DEFAULT_SECURITY_CONFIG;
|
|
378
|
-
// Master switch off → all sub-modules off, even when defaults are enabled.
|
|
379
|
-
const securityConfig = fullSecurityConfig.enabled === false
|
|
380
|
-
? { ...(fullSecurityConfig.bash || DEFAULT_SECURITY_CONFIG.bash), enabled: false }
|
|
381
|
-
: fullSecurityConfig.bash || DEFAULT_SECURITY_CONFIG.bash;
|
|
497
|
+
// Session-specific security config (мастер-выключатель учтён внутри).
|
|
498
|
+
const securityConfig = resolveBashSecurityConfig(ctx);
|
|
382
499
|
const validation = isCommandAllowed(command, securityConfig);
|
|
383
500
|
if (!validation.allowed) {
|
|
384
501
|
// Log security block
|
|
@@ -412,82 +529,18 @@ export const bashTool = {
|
|
|
412
529
|
const code = entry.exitCode;
|
|
413
530
|
let output = entry.log.join("\n");
|
|
414
531
|
processRegistry.remove(entry.id);
|
|
415
|
-
//
|
|
416
|
-
// missing entry point (main is never called with argv).
|
|
417
|
-
const cliHint = emptyCliRunHint(command, output, code);
|
|
418
|
-
if (cliHint) {
|
|
419
|
-
output = `(exit code 0, no output)\n\nHint: ${cliHint}`;
|
|
420
|
-
}
|
|
421
|
-
// Auto-verify test runs: a failing suite must not be reported as a
|
|
422
|
-
// clean success. Inject a prominent marker the model cannot miss
|
|
423
|
-
// (the tpical failure: tests fail but the CLI exit code is 0).
|
|
532
|
+
// Подсказки, аудит и обрезка вывода — единая точка (decorateCommandOutput).
|
|
424
533
|
const testRun = detectTestResults(output);
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
output = `${t("exec.test_runner_pass", { framework: testRun.framework, passed: String(testRun.passed) })}\n\n${output}`;
|
|
436
|
-
}
|
|
437
|
-
if (!output && code !== 0) {
|
|
438
|
-
output = `(exit code ${code})`;
|
|
439
|
-
}
|
|
440
|
-
// npm/npx/bunx: "could not determine executable to run" — the
|
|
441
|
-
// package has no `bin` entry (or the script does not exist).
|
|
442
|
-
// This is cross-platform, so it runs before the win32 hint block.
|
|
443
|
-
output = npmExecHint(output);
|
|
444
|
-
// On Windows, hint about Unix commands that don't work, and
|
|
445
|
-
// hard-stop a command that keeps failing the same way. The
|
|
446
|
-
// hint keys on the ORIGINAL command word (before
|
|
447
|
-
// adaptCommandForWindows translated cat→type): the translation
|
|
448
|
-
// is only for execution, but the model wrote `cat`, and that is
|
|
449
|
-
// what UNIX_TO_WIN_HINTS knows about. Keying on the translated
|
|
450
|
-
// word left `cat` invisible to the hint/block (observed: model
|
|
451
|
-
// ran cat 4+ times in one session with no guidance).
|
|
452
|
-
if (platform() === "win32") {
|
|
453
|
-
const originalFirstWord = originalCommand.trim().split(/\s+/)[0]?.split(/[\\/]/).pop();
|
|
454
|
-
if (originalFirstWord && originalFirstWord in UNIX_TO_WIN_HINTS) {
|
|
455
|
-
if (code === 0) {
|
|
456
|
-
FAILING_FIRST_WORDS.delete(originalFirstWord);
|
|
457
|
-
}
|
|
458
|
-
else {
|
|
459
|
-
const failures = (FAILING_FIRST_WORDS.get(originalFirstWord) || 0) + 1;
|
|
460
|
-
FAILING_FIRST_WORDS.set(originalFirstWord, failures);
|
|
461
|
-
if (failures >= HARD_BLOCK_THRESHOLD) {
|
|
462
|
-
output = `STOP using "${originalFirstWord}" — it does not work in this cmd.exe shell and has failed ${failures} times in a row. ${UNIX_TO_WIN_HINTS[originalFirstWord]}`;
|
|
463
|
-
}
|
|
464
|
-
else {
|
|
465
|
-
output = `${output}\n\nHint: "${originalFirstWord}" may not work on Windows. ${UNIX_TO_WIN_HINTS[originalFirstWord]}`;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
// Update audit log with result
|
|
471
|
-
if (securityConfig?.logCommands) {
|
|
472
|
-
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), code === 0, `Working directory: ${workdir}, Output length: ${output.length}`);
|
|
473
|
-
}
|
|
474
|
-
const lines = output.split("\n");
|
|
475
|
-
if (testRun && testRun.failed > 0) {
|
|
476
|
-
// Keep the failure details: test runners print passing markers
|
|
477
|
-
// first and the errors + summary at the very end. A short head
|
|
478
|
-
// preview hides exactly what the model needs to fix the failures.
|
|
479
|
-
const TEST_TAIL_LINES = 400;
|
|
480
|
-
const kept = lines.slice(-TEST_TAIL_LINES);
|
|
481
|
-
const skipped = lines.length - kept.length;
|
|
482
|
-
output =
|
|
483
|
-
(skipped > 0 ? `[... ${skipped} earlier lines omitted — failing tests below]\n` : "") +
|
|
484
|
-
kept.join("\n");
|
|
485
|
-
}
|
|
486
|
-
else if (lines.length > MAX_PREVIEW_LINES) {
|
|
487
|
-
output =
|
|
488
|
-
lines.slice(0, MAX_PREVIEW_LINES).join("\n") +
|
|
489
|
-
`\n... (${lines.length - MAX_PREVIEW_LINES} more lines)`;
|
|
490
|
-
}
|
|
534
|
+
output = decorateCommandOutput({
|
|
535
|
+
ctx,
|
|
536
|
+
command,
|
|
537
|
+
originalCommand,
|
|
538
|
+
code,
|
|
539
|
+
output,
|
|
540
|
+
testRun,
|
|
541
|
+
workdir,
|
|
542
|
+
securityConfig,
|
|
543
|
+
});
|
|
491
544
|
return { success: code === 0, output };
|
|
492
545
|
}
|
|
493
546
|
// Still running — promote to a background process.
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { t } from "../i18n/index";
|
|
4
|
-
import { isPathInScope } from "
|
|
4
|
+
import { isPathInScope } from "../modules/security/path-validator";
|
|
5
|
+
import { DEFAULT_SECURITY_CONFIG } from "../config/security";
|
|
5
6
|
import { runChunkQuery } from "../modules/context/chunk-query";
|
|
6
7
|
export const chunkQueryTool = {
|
|
7
8
|
name: "chunk_query",
|
|
8
9
|
icon: "🧩",
|
|
9
10
|
description: "Answer a query over a large text (file or inline) by splitting it into chunks and querying the model per chunk in parallel, then optionally synthesizing a final answer. Use when the input is too large for the context window.",
|
|
10
|
-
tags: ["research"
|
|
11
|
+
tags: ["research"],
|
|
11
12
|
/** Multiple LLM calls (parallel chunk queries + synthesis) need a longer budget. */
|
|
12
13
|
timeoutMs: 300000,
|
|
13
14
|
parameters: {
|
|
@@ -43,7 +44,7 @@ export const chunkQueryTool = {
|
|
|
43
44
|
},
|
|
44
45
|
reasoning_effort: {
|
|
45
46
|
type: "string",
|
|
46
|
-
enum: ["none", "low", "medium", "high"],
|
|
47
|
+
enum: ["none", "low", "medium", "high", "max"],
|
|
47
48
|
description: 'Reasoning effort for chunk/synthesis calls (default "none"). "none" prevents long thinking chains from eating the token budget on reasoning models.',
|
|
48
49
|
},
|
|
49
50
|
synthesize: {
|
|
@@ -63,12 +64,15 @@ export const chunkQueryTool = {
|
|
|
63
64
|
return { success: false, output: t("tool.chunk_query_no_input") };
|
|
64
65
|
let content = text || "";
|
|
65
66
|
if (inputPath) {
|
|
66
|
-
|
|
67
|
+
// Same policy as read_file: global security paths (denied patterns
|
|
68
|
+
// like .env/keys) + subagent scope.
|
|
69
|
+
const resolvedInput = resolve(ctx.baseDir, inputPath);
|
|
70
|
+
const check = isPathInScope(ctx.baseDir, resolvedInput, ctx.scope, ctx.config?.security?.paths || DEFAULT_SECURITY_CONFIG.paths);
|
|
67
71
|
if (!check.allowed) {
|
|
68
72
|
return { success: false, output: `[SCOPE] ${check.reason || "Path not allowed"}` };
|
|
69
73
|
}
|
|
70
74
|
try {
|
|
71
|
-
content = readFileSync(
|
|
75
|
+
content = readFileSync(resolvedInput, "utf8");
|
|
72
76
|
}
|
|
73
77
|
catch (e) {
|
|
74
78
|
return { success: false, output: `Cannot read ${inputPath}: ${e.message}` };
|
|
@@ -86,7 +90,7 @@ export const chunkQueryTool = {
|
|
|
86
90
|
synthesize: typeof args.synthesize === "boolean" ? args.synthesize : undefined,
|
|
87
91
|
maxChunkTokens: typeof args.max_chunk_tokens === "number" ? args.max_chunk_tokens : undefined,
|
|
88
92
|
maxSynthesisTokens: typeof args.max_synthesis_tokens === "number" ? args.max_synthesis_tokens : undefined,
|
|
89
|
-
reasoningEffort: ["none", "low", "medium", "high"].includes(args.reasoning_effort)
|
|
93
|
+
reasoningEffort: ["none", "low", "medium", "high", "max"].includes(args.reasoning_effort)
|
|
90
94
|
? args.reasoning_effort
|
|
91
95
|
: undefined,
|
|
92
96
|
logger: ctx.logger,
|
|
@@ -14,7 +14,7 @@ export const downloadFileTool = {
|
|
|
14
14
|
name: "download_file",
|
|
15
15
|
icon: "⬇️",
|
|
16
16
|
description: "Download a file (image, archive, binary, font, etc.) from a URL and save it to disk. Returns only the path, size and content type — read the saved file with read_file/attach_image afterwards. Use this for binary files; web_fetch/web_browse return text only and cannot save bytes.",
|
|
17
|
-
tags: ["
|
|
17
|
+
tags: ["research"],
|
|
18
18
|
parameters: {
|
|
19
19
|
type: "object",
|
|
20
20
|
properties: {
|
package/dist/tools/edit-file.js
CHANGED
|
@@ -6,6 +6,7 @@ import { logFileWrite, logSecurityBlock } from "../modules/security/audit-log";
|
|
|
6
6
|
import { getSessionSecurityConfig } from "../modules/security/session-isolation";
|
|
7
7
|
import { generateDiff } from "../ui/diff";
|
|
8
8
|
import { safeResolvePath } from "./path-utils";
|
|
9
|
+
import { preValidateSyntax, detectImportConflicts } from "./syntax-validator";
|
|
9
10
|
export const editFileTool = {
|
|
10
11
|
name: "edit_file",
|
|
11
12
|
icon: "✏️",
|
|
@@ -57,7 +58,9 @@ export const editFileTool = {
|
|
|
57
58
|
output: t("file.string_not_found", { str: oldStr, path }),
|
|
58
59
|
};
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
+
// Replacement via function — otherwise newStr is treated as a template
|
|
62
|
+
// and `$&`, `$1` etc. would silently corrupt the file content.
|
|
63
|
+
const updated = content.replace(oldStr, () => newStr);
|
|
61
64
|
// Check new content for dangerous patterns — only when security is enabled
|
|
62
65
|
if (securityConfig?.enabled && securityConfig?.contentScan?.enabled) {
|
|
63
66
|
const scanResult = scanContent(updated, path, securityConfig.contentScan);
|
|
@@ -69,12 +72,27 @@ export const editFileTool = {
|
|
|
69
72
|
};
|
|
70
73
|
}
|
|
71
74
|
}
|
|
75
|
+
// Pre-validate syntax BEFORE writing — prevents "file written" + syntax error loop
|
|
76
|
+
const validation = await preValidateSyntax(resolved, updated, ctx.baseDir);
|
|
77
|
+
if (!validation.valid) {
|
|
78
|
+
return {
|
|
79
|
+
success: false,
|
|
80
|
+
output: t("file.syntax_error", { path, error: validation.error || "Unknown syntax error" }),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Detect import/export conflicts — warning only (code may be valid)
|
|
84
|
+
const conflicts = detectImportConflicts(updated);
|
|
72
85
|
writeFileSync(resolved, updated, "utf-8");
|
|
73
86
|
const diff = generateDiff(content, updated);
|
|
74
87
|
// Increment file operations counter
|
|
75
88
|
ctx.fileOperationsCount = currentCount + 1;
|
|
76
89
|
// Log successful file edit
|
|
77
90
|
logFileWrite(ctx.sessionId, path, true, "File edited");
|
|
78
|
-
|
|
91
|
+
// Add conflict warning to output if any
|
|
92
|
+
let output = t("file.replaced_in", { path });
|
|
93
|
+
if (conflicts.length > 0) {
|
|
94
|
+
output += `\n[WARNING] Import/export conflicts: ${conflicts.join(", ")}`;
|
|
95
|
+
}
|
|
96
|
+
return { success: true, output, diff };
|
|
79
97
|
},
|
|
80
98
|
};
|