micro-models-agent 0.63.0 → 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.
Files changed (186) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/dist/cli/cache-line.js +30 -0
  3. package/dist/cli/command-suggest.js +38 -0
  4. package/dist/cli/commands.js +285 -60
  5. package/dist/cli/completer.js +16 -16
  6. package/dist/cli/json-payload.js +32 -0
  7. package/dist/cli/main.js +165 -77
  8. package/dist/cli/plugin-commands.js +5 -4
  9. package/dist/cli/relaunch.js +37 -0
  10. package/dist/cli/repl-commands.js +441 -307
  11. package/dist/cli/repl.js +360 -83
  12. package/dist/cli/run-result.js +12 -6
  13. package/dist/cli/security-commands.js +64 -60
  14. package/dist/cli/setup-order.js +57 -0
  15. package/dist/cli/setup-prompt.js +49 -0
  16. package/dist/cli/setup.js +52 -48
  17. package/dist/config/budget.js +48 -0
  18. package/dist/config/config.js +132 -70
  19. package/dist/config/defaults.js +37 -11
  20. package/dist/config/domains.js +9 -50
  21. package/dist/config/utils.js +56 -0
  22. package/dist/core/agent/audit-gate.js +49 -0
  23. package/dist/core/agent/compaction.js +89 -0
  24. package/dist/core/agent/constants.js +61 -0
  25. package/dist/core/agent/context-renderer.js +40 -0
  26. package/dist/core/agent/hallucination-gate.js +87 -0
  27. package/dist/core/agent/loop-state.js +53 -0
  28. package/dist/core/agent/prefix-monitor.js +101 -0
  29. package/dist/core/agent/reasoning-resolver.js +56 -0
  30. package/dist/core/agent/token-tracker.js +96 -0
  31. package/dist/core/agent/tool-batch.js +237 -0
  32. package/dist/core/agent/tool-output.js +62 -0
  33. package/dist/core/agent-moe.js +214 -69
  34. package/dist/core/agent.js +506 -546
  35. package/dist/core/bootstrap.js +297 -98
  36. package/dist/core/crash-handler.js +2 -1
  37. package/dist/core/prompt-builder.js +3 -0
  38. package/dist/core/prompt-overflow.js +307 -0
  39. package/dist/core/session-logger.js +34 -2
  40. package/dist/i18n/en.json +8 -4
  41. package/dist/i18n/ru.json +8 -4
  42. package/dist/index.js +5 -1
  43. package/dist/llm/cache-usage.js +76 -0
  44. package/dist/llm/image-utils.js +20 -16
  45. package/dist/llm/llm-errors.js +41 -0
  46. package/dist/llm/model-loader.js +30 -0
  47. package/dist/llm/openai-compat.js +287 -101
  48. package/dist/llm/orchestrator.js +140 -68
  49. package/dist/llm/provider-budget.js +68 -0
  50. package/dist/llm/provider.js +0 -1
  51. package/dist/llm/stream-state.js +26 -0
  52. package/dist/llm/token-counter.js +28 -0
  53. package/dist/logger/app-logger.js +12 -15
  54. package/dist/main.js +1755 -841
  55. package/dist/migration/detect.js +3 -1
  56. package/dist/modules/browser/actions.js +0 -3
  57. package/dist/modules/browser/bridge-client.js +2 -0
  58. package/dist/modules/browser/bridge-server.mjs +37 -4
  59. package/dist/modules/browser/driver.js +46 -4
  60. package/dist/modules/certification/cli.js +85 -42
  61. package/dist/modules/certification/loader.js +15 -1
  62. package/dist/modules/certification/manifest.js +126 -15
  63. package/dist/modules/certification/runner.js +4 -26
  64. package/dist/modules/certification/scenarios.js +184 -5
  65. package/dist/modules/certification/syntax-scenarios.js +51 -0
  66. package/dist/modules/context/chunk-query.js +25 -5
  67. package/dist/modules/context/fact-extractor.js +6 -2
  68. package/dist/modules/context/manager.js +23 -7
  69. package/dist/modules/execution/audit-runners.js +7 -1
  70. package/dist/modules/execution/auditor.js +3 -3
  71. package/dist/modules/execution/execution-plugin.js +22 -15
  72. package/dist/modules/execution/input-from.js +46 -0
  73. package/dist/modules/execution/module.js +107 -18
  74. package/dist/modules/execution/moe-executor.js +166 -54
  75. package/dist/modules/execution/plan-actions.js +524 -0
  76. package/dist/modules/execution/plan-steps.js +23 -0
  77. package/dist/modules/execution/plan-store.js +15 -3
  78. package/dist/modules/execution/plan-tool.js +6 -488
  79. package/dist/modules/execution/plan-validator.js +24 -0
  80. package/dist/modules/execution/stuck-detector.js +3 -18
  81. package/dist/modules/execution/tracker.js +14 -5
  82. package/dist/modules/execution/transient-error.js +30 -0
  83. package/dist/modules/execution/verifier.js +94 -7
  84. package/dist/modules/execution/windows-commands.js +11 -0
  85. package/dist/modules/hallucination/confidence.js +36 -23
  86. package/dist/modules/hallucination/consistency.js +3 -0
  87. package/dist/modules/hallucination/detector.js +8 -3
  88. package/dist/modules/hallucination/factual.js +26 -7
  89. package/dist/modules/hallucination/llm-judge.js +12 -2
  90. package/dist/modules/indexer/map-command.js +35 -0
  91. package/dist/modules/indexer/map-select.js +87 -0
  92. package/dist/modules/indexer/module.js +34 -22
  93. package/dist/modules/indexer/symbols.js +189 -0
  94. package/dist/modules/indexer/walker.js +96 -42
  95. package/dist/modules/lsp/check-tool.js +2 -1
  96. package/dist/modules/lsp/client.js +49 -32
  97. package/dist/modules/lsp/config.js +55 -2
  98. package/dist/modules/lsp/module.js +38 -5
  99. package/dist/modules/lsp/probe.js +4 -3
  100. package/dist/modules/lsp/project-root.js +41 -1
  101. package/dist/modules/lsp/startup-check.js +12 -4
  102. package/dist/modules/mcp/client.js +153 -104
  103. package/dist/modules/mcp/module.js +165 -41
  104. package/dist/modules/memory/module.js +4 -3
  105. package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
  106. package/dist/modules/plugins/manager.js +47 -84
  107. package/dist/modules/pricing/index.js +17 -7
  108. package/dist/modules/pricing/prices.js +30 -12
  109. package/dist/modules/processes/index.js +1 -0
  110. package/dist/modules/processes/kill-tree.js +56 -0
  111. package/dist/modules/processes/registry.js +2 -54
  112. package/dist/modules/providers/cache.js +23 -0
  113. package/dist/modules/providers/factory.js +28 -0
  114. package/dist/modules/providers/fallback.js +7 -5
  115. package/dist/modules/providers/health.js +2 -1
  116. package/dist/modules/providers/index.js +1 -0
  117. package/dist/modules/providers/manager.js +17 -2
  118. package/dist/modules/providers/presets.js +79 -6
  119. package/dist/modules/reasoning/policy.js +40 -0
  120. package/dist/modules/reasoning/probe.js +111 -0
  121. package/dist/modules/security/audit-notifier.js +42 -27
  122. package/dist/modules/security/command-validator.js +25 -20
  123. package/dist/modules/security/encryption.js +6 -12
  124. package/dist/modules/security/network-validator.js +76 -5
  125. package/dist/modules/security/path-validator.js +77 -34
  126. package/dist/modules/security/rate-limiter.js +11 -0
  127. package/dist/modules/security/security-policies.js +1 -1
  128. package/dist/modules/security/session-encryption.js +13 -2
  129. package/dist/modules/security/session-isolation.js +2 -9
  130. package/dist/modules/session/manager.js +11 -0
  131. package/dist/modules/session/module.js +11 -3
  132. package/dist/modules/session/store.js +41 -5
  133. package/dist/modules/skills/loader.js +7 -1
  134. package/dist/modules/skills/module.js +2 -1
  135. package/dist/modules/updater/changelog-reader.js +94 -0
  136. package/dist/modules/updater/dev-detect.js +17 -0
  137. package/dist/modules/updater/index.js +1 -0
  138. package/dist/modules/updater/module.js +14 -3
  139. package/dist/output/bus.js +32 -0
  140. package/dist/output/channel.js +233 -0
  141. package/dist/output/format.js +14 -0
  142. package/dist/output/index.js +7 -0
  143. package/dist/output/json-sink.js +22 -0
  144. package/dist/output/machine.js +8 -0
  145. package/dist/output/session-sink.js +27 -0
  146. package/dist/output/types.js +1 -0
  147. package/dist/tools/approve.js +6 -2
  148. package/dist/tools/attach-image.js +11 -11
  149. package/dist/tools/auto-fixer.js +198 -0
  150. package/dist/tools/bash.js +142 -89
  151. package/dist/tools/chunk-query.js +10 -6
  152. package/dist/tools/download-file.js +1 -1
  153. package/dist/tools/edit-file.js +20 -2
  154. package/dist/tools/executor.js +54 -9
  155. package/dist/tools/glob-tool.js +7 -0
  156. package/dist/tools/grep-tool.js +15 -1
  157. package/dist/tools/index.js +3 -1
  158. package/dist/tools/list-dir.js +3 -1
  159. package/dist/tools/load-skill.js +2 -1
  160. package/dist/tools/mcp-call.js +1 -1
  161. package/dist/tools/move-file.js +5 -4
  162. package/dist/tools/path-utils.js +7 -0
  163. package/dist/tools/pipeline-run.js +1 -1
  164. package/dist/tools/prompt-io.js +28 -0
  165. package/dist/tools/question.js +12 -12
  166. package/dist/tools/scope-request.js +91 -0
  167. package/dist/tools/session-info.js +44 -0
  168. package/dist/tools/set-thinking.js +71 -0
  169. package/dist/tools/subagent.js +50 -9
  170. package/dist/tools/syntax-validator.js +177 -0
  171. package/dist/tools/user-input.js +16 -9
  172. package/dist/tools/write-file.js +17 -1
  173. package/dist/ui/diff.js +10 -0
  174. package/dist/ui/line-editor.js +179 -26
  175. package/dist/ui/line-math.js +20 -3
  176. package/dist/ui/md-formatter.js +100 -10
  177. package/dist/ui/output.js +5 -4
  178. package/dist/ui/plan-view.js +2 -7
  179. package/dist/ui/renderer.js +89 -85
  180. package/dist/ui/spinner.js +14 -4
  181. package/dist/utils/error.js +4 -0
  182. package/dist/utils/index.js +4 -0
  183. package/dist/utils/retry.js +17 -0
  184. package/dist/utils/sleep.js +23 -0
  185. package/dist/utils/truncate.js +9 -0
  186. 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
- if (ctx.scope) {
64
- const { isPathInScope } = await import("../modules/security/path-validator");
65
- const validation = isPathInScope(ctx.baseDir, absPath, ctx.scope);
66
- if (!validation.allowed) {
67
- logSecurityBlock(ctx.sessionId, "file_read", validation.reason ?? "attach_image out of scope", absPath);
68
- return {
69
- success: false,
70
- output: t("file.path_not_allowed", { path: source }),
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
+ }
@@ -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 (trimmed.startsWith("mkdir -p ")) {
86
- return trimmed.replace(/^mkdir -p /, "mkdir ");
85
+ if (/^mkdir\s+(-p|--parents)\b/.test(trimmed)) {
86
+ return trimmed.replace(/^mkdir\s+(-p|--parents)\s*/, "mkdir ");
87
87
  }
88
- if (trimmed === "mkdir -p" || trimmed.startsWith("mkdir -p ")) {
89
- return trimmed.replace(/mkdir -p/g, "mkdir");
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
- // Get session-specific security config with defaults
374
- const appConfig = ctx.config || {};
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
- // R5: script file ran with exit 0 but empty output — likely a
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
- if (testRun && testRun.failed > 0) {
426
- output =
427
- t("exec.test_runner_fail", {
428
- framework: testRun.framework,
429
- failed: String(testRun.failed),
430
- passed: String(testRun.passed),
431
- }) +
432
- `\n\n${output}`;
433
- }
434
- else if (testRun && testRun.failed === 0 && testRun.passed > 0) {
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 "./scope-check";
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", "code"],
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
- const check = isPathInScope(ctx.baseDir, inputPath, ctx.scope);
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(resolve(ctx.baseDir, inputPath), "utf8");
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: ["file", "research"],
17
+ tags: ["research"],
18
18
  parameters: {
19
19
  type: "object",
20
20
  properties: {
@@ -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
- const updated = content.replace(oldStr, newStr);
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
- return { success: true, output: t("file.replaced_in", { path }), diff };
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
  };