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
@@ -6,6 +6,7 @@ import { PluginManager } from "../modules/plugins/manager";
6
6
  import { logSecurityBlock } from "../modules/security/audit-log";
7
7
  import { getSessionSecurityConfig } from "../modules/security/session-isolation";
8
8
  import { ArtifactStore } from "../modules/artifacts/store";
9
+ import { ScopeDenialCollector, formatScopeRequest } from "./scope-request";
9
10
  /** Fixed system prompt (KV-cache friendly when stableSystemPrompt is on). */
10
11
  export const SUBAGENT_SYSTEM_PROMPT_TEMPLATE = `You are a sub-agent working on a specific task. You have your own context window and tool set.
11
12
 
@@ -24,6 +25,7 @@ export const subagentTool = {
24
25
  icon: "🤖",
25
26
  description: "Spawn an isolated sub-agent to work on a task independently. The sub-agent has its own context and executes autonomously. Use for parallel work or complex sub-tasks.",
26
27
  tags: ["code"],
28
+ timeoutMs: 180000,
27
29
  parameters: {
28
30
  type: "object",
29
31
  properties: {
@@ -84,9 +86,14 @@ export const subagentTool = {
84
86
  const securityConfig = ctx.sessionContext
85
87
  ? getSessionSecurityConfig(ctx.config, ctx.sessionContext)
86
88
  : ctx.config.security;
87
- // Security check: limit recursion depth
89
+ // Security check: limit recursion depth. Read the depth from the SHARED
90
+ // executor ctx (which nested sub-agents actually see) — `ctx.recursionDepth`
91
+ // was captured once at bootstrap and never incremented, so the guard never
92
+ // fired and a sub-agent could spawn sub-agents unbounded.
93
+ const executor = ctx.toolExecutor;
94
+ const parentDepth = executor?.getRecursionDepth?.() ?? (ctx.recursionDepth ?? 0);
88
95
  const maxDepth = securityConfig?.maxRecursionDepth ?? 3;
89
- const currentDepth = ctx.recursionDepth ?? 0;
96
+ const currentDepth = parentDepth;
90
97
  if (currentDepth >= maxDepth) {
91
98
  logSecurityBlock(ctx.sessionId, "bash_command", `Maximum recursion depth (${maxDepth}) exceeded`, task);
92
99
  return {
@@ -124,19 +131,38 @@ export const subagentTool = {
124
131
  try {
125
132
  const subContextManager = new ContextManager(ctx.config.contextWindow, ctx.config.contextBudget);
126
133
  const subPluginManager = new PluginManager();
134
+ // Deterministic scope-denial tracking: when the child's file tools are
135
+ // blocked by the scope guard, the denial is surfaced as a machine-readable
136
+ // <scope-request> block in the result (see MoE executor / Router).
137
+ const scopeCollector = new ScopeDenialCollector();
138
+ subPluginManager.register(scopeCollector.plugin, "builtin");
127
139
  const hallucinationDetector = new HallucinationDetector(ctx.baseDir, ctx.llmProvider);
128
140
  // Save/restore the shared executor scope so the sub-agent doesn't
129
141
  // permanently mutate the parent's scope.
130
142
  const parentScope = ctx.scope;
131
143
  ctx.toolExecutor.setScope(scope || { allowed_files: [], read_only_files: [] });
144
+ // Register cleanup so the executor restores scope on timeout/abort
145
+ // even when the agent's `finally` block never runs (abandoned promise).
146
+ ctx.cleanup = () => ctx.toolExecutor?.setScope(parentScope);
147
+ // Increment recursion depth on the shared executor ctx so NESTED
148
+ // sub-agents see the deeper level (and the guard above can fire).
149
+ ctx.recursionDepth = currentDepth + 1;
150
+ executor?.setRecursionDepth?.(currentDepth + 1);
132
151
  const systemPrompt = {
133
152
  content: buildSubagentSystemPrompt(context, ctx.config.subagent?.stableSystemPrompt !== false),
134
153
  priority: "critical",
135
154
  essential: true,
136
155
  estimatedTokens: 150,
137
156
  };
157
+ // MoE is a TOP-LEVEL orchestration mode: a sub-agent spawned by the
158
+ // subagent tool must always run the plain single-agent loop. Sharing the
159
+ // parent's config here made every MoE sub-agent re-enter runWithMoE —
160
+ // planning its own subtasks and spawning nested sub-agents recursively
161
+ // (bounded only by maxRecursionDepth), which hung execution for many
162
+ // minutes with zero progress events.
163
+ const subConfig = { ...ctx.config, moe: { ...ctx.config.moe, enabled: false } };
138
164
  const subDeps = {
139
- config: ctx.config,
165
+ config: subConfig,
140
166
  llmProvider: ctx.llmProvider,
141
167
  toolExecutor: ctx.toolExecutor,
142
168
  pluginManager: subPluginManager,
@@ -147,17 +173,24 @@ export const subagentTool = {
147
173
  scope,
148
174
  toolTags,
149
175
  promptBlocks: [systemPrompt],
150
- recursionDepth: currentDepth + 1, // Increment recursion depth for sub-agent
151
176
  };
152
- const subAgent = new Agent(subDeps);
177
+ const subAgent = (ctx.agentFactory ?? ((d) => new Agent(d)))(subDeps);
153
178
  const fullTask = context ? `${task}\n\nContext: ${context}` : task;
154
179
  try {
155
180
  const result = await subAgent.run(fullTask);
181
+ const pickUsage = () => result.promptTokens !== undefined || result.totalTokens !== undefined
182
+ ? {
183
+ promptTokens: result.promptTokens ?? 0,
184
+ completionTokens: result.completionTokens ?? 0,
185
+ totalTokens: result.totalTokens ?? 0,
186
+ }
187
+ : undefined;
156
188
  if (!result.success) {
157
- return {
158
- success: false,
159
- output: `Sub-agent failed: ${result.error}\nPartial output: ${result.text}`,
160
- };
189
+ let output = `Sub-agent failed: ${result.error}\nPartial output: ${result.text}`;
190
+ const scopeReq = scopeCollector.toScopeRequest();
191
+ if (scopeReq)
192
+ output += `\n${formatScopeRequest(scopeReq)}`;
193
+ return { success: false, output };
161
194
  }
162
195
  const resultMode = args.result_mode || ctx.config.subagent?.resultMode || "text";
163
196
  if (resultMode === "file") {
@@ -176,17 +209,25 @@ export const subagentTool = {
176
209
  iterations: String(result.iterationCount),
177
210
  summary,
178
211
  }),
212
+ // Structured path so callers (MoE executor) don't parse the
213
+ // localized `output` string.
214
+ artifactPath: abs,
215
+ usage: pickUsage(),
179
216
  };
180
217
  }
181
218
  return {
182
219
  success: true,
183
220
  output: `Sub-agent completed:\n${result.text}\n\nIterations: ${result.iterationCount}`,
221
+ usage: pickUsage(),
184
222
  };
185
223
  }
186
224
  finally {
187
225
  // Restore the parent's exact scope: undefined means "no restrictions"
188
226
  // and must not become an empty scope (which denies everything).
189
227
  ctx.toolExecutor.setScope(parentScope);
228
+ ctx.cleanup = undefined;
229
+ ctx.recursionDepth = currentDepth;
230
+ executor?.setRecursionDepth?.(currentDepth);
190
231
  }
191
232
  }
192
233
  catch (e) {
@@ -0,0 +1,177 @@
1
+ import { writeFileSync, existsSync, unlinkSync } from "fs";
2
+ import { extname, dirname } from "path";
3
+ import { spawn } from "child_process";
4
+ const SYNTAX_CACHE = new Map();
5
+ function contentHash(content) {
6
+ let h = 5381;
7
+ for (let i = 0; i < content.length; i++) {
8
+ h = ((h << 5) + h + content.charCodeAt(i)) | 0;
9
+ }
10
+ return String(h);
11
+ }
12
+ /**
13
+ * Run a command with timeout. Returns stdout/stderr on success, throws on failure.
14
+ */
15
+ function runCommand(command, cwd, timeoutMs) {
16
+ return new Promise((resolve, reject) => {
17
+ const child = spawn(command, {
18
+ cwd,
19
+ shell: true,
20
+ windowsHide: true,
21
+ stdio: ["ignore", "pipe", "pipe"],
22
+ });
23
+ const stdoutChunks = [];
24
+ const stderrChunks = [];
25
+ child.stdout?.on("data", (d) => stdoutChunks.push(d));
26
+ child.stderr?.on("data", (d) => stderrChunks.push(d));
27
+ const timer = setTimeout(() => {
28
+ child.kill();
29
+ reject(new Error(`Syntax check timed out after ${timeoutMs}ms`));
30
+ }, timeoutMs);
31
+ child.on("error", (err) => {
32
+ clearTimeout(timer);
33
+ reject(err);
34
+ });
35
+ child.on("close", (code) => {
36
+ clearTimeout(timer);
37
+ const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
38
+ const stderr = Buffer.concat(stderrChunks).toString("utf-8");
39
+ if (code === 0) {
40
+ resolve({ stdout, stderr });
41
+ }
42
+ else {
43
+ const err = new Error(`Syntax check failed with exit code ${code}`);
44
+ err.stdout = stdout;
45
+ err.stderr = stderr;
46
+ err.status = code;
47
+ reject(err);
48
+ }
49
+ });
50
+ });
51
+ }
52
+ /**
53
+ * Supported file extensions for syntax validation.
54
+ */
55
+ const TS_EXTENSIONS = new Set([".ts", ".tsx", ".cts", ".mts"]);
56
+ const JS_EXTENSIONS = new Set([".js", ".jsx", ".cjs", ".mjs"]);
57
+ const JSON_EXTENSIONS = new Set([".json"]);
58
+ /**
59
+ * Pre-validate file syntax BEFORE writing to disk.
60
+ * Returns { valid: true } if syntax is OK or language is not supported.
61
+ * Returns { valid: false, error: "..." } if syntax is invalid.
62
+ *
63
+ * For TypeScript/JavaScript: writes to a temp file and runs the compiler.
64
+ * For JSON: parses the content directly.
65
+ */
66
+ export async function preValidateSyntax(filePath, content, baseDir) {
67
+ const ext = extname(filePath).toLowerCase();
68
+ // JSON — parse directly
69
+ if (JSON_EXTENSIONS.has(ext)) {
70
+ try {
71
+ JSON.parse(content);
72
+ return { valid: true };
73
+ }
74
+ catch (e) {
75
+ return { valid: false, error: e.message };
76
+ }
77
+ }
78
+ // TypeScript — syntax check via bun build
79
+ if (TS_EXTENSIONS.has(ext)) {
80
+ const hash = contentHash(content);
81
+ const cached = SYNTAX_CACHE.get(filePath);
82
+ if (cached && cached.hash === hash) {
83
+ return cached.error ? { valid: false, error: cached.error } : { valid: true };
84
+ }
85
+ const tmpFile = `${filePath}.tmp${ext}`;
86
+ try {
87
+ writeFileSync(tmpFile, content, "utf-8");
88
+ await runCommand(`bun build --no-bundle --target=bun "${tmpFile}"`, dirname(filePath), 5000);
89
+ SYNTAX_CACHE.set(filePath, { hash, error: null });
90
+ return { valid: true };
91
+ }
92
+ catch (err) {
93
+ const stderr = err.stderr || err.stdout || "";
94
+ const firstError = stderr.split("\n").find((line) => line.trim()) || "TypeScript syntax error";
95
+ const error = firstError.trim();
96
+ SYNTAX_CACHE.set(filePath, { hash, error });
97
+ return { valid: false, error };
98
+ }
99
+ finally {
100
+ try {
101
+ if (existsSync(tmpFile))
102
+ unlinkSync(tmpFile);
103
+ }
104
+ catch {
105
+ /* ignore cleanup errors */
106
+ }
107
+ }
108
+ }
109
+ // JavaScript — syntax check via node --check
110
+ if (JS_EXTENSIONS.has(ext)) {
111
+ const hash = contentHash(content);
112
+ const cached = SYNTAX_CACHE.get(filePath);
113
+ if (cached && cached.hash === hash) {
114
+ return cached.error ? { valid: false, error: cached.error } : { valid: true };
115
+ }
116
+ const tmpFile = `${filePath}.tmp${ext}`;
117
+ try {
118
+ writeFileSync(tmpFile, content, "utf-8");
119
+ await runCommand(`node --check "${tmpFile}"`, dirname(filePath), 5000);
120
+ SYNTAX_CACHE.set(filePath, { hash, error: null });
121
+ return { valid: true };
122
+ }
123
+ catch (err) {
124
+ const stderr = err.stderr || "";
125
+ const firstError = stderr.split("\n").find((line) => line.trim()) || "JavaScript syntax error";
126
+ const error = firstError.trim();
127
+ SYNTAX_CACHE.set(filePath, { hash, error });
128
+ return { valid: false, error };
129
+ }
130
+ finally {
131
+ try {
132
+ if (existsSync(tmpFile))
133
+ unlinkSync(tmpFile);
134
+ }
135
+ catch {
136
+ /* ignore cleanup errors */
137
+ }
138
+ }
139
+ }
140
+ // Unsupported extension — skip validation
141
+ return { valid: true };
142
+ }
143
+ /**
144
+ * Detect import/export conflicts in TypeScript/JavaScript code.
145
+ * Returns array of conflicting names.
146
+ */
147
+ export function detectImportConflicts(content) {
148
+ const conflicts = [];
149
+ // Extract all named imports: import { foo, bar as baz } from '...'
150
+ const importRegex = /import\s+(?:type\s+)?\{([^}]+)\}\s+from/g;
151
+ const exportRegex = /export\s+(?:function|class|const|let|var|type|interface|async\s+function)\s+(\w+)/g;
152
+ const imports = new Set();
153
+ const exports = new Set();
154
+ let match;
155
+ while ((match = importRegex.exec(content))) {
156
+ const names = match[1]
157
+ .split(",")
158
+ .map((s) => s.trim())
159
+ .map((s) => {
160
+ // Handle "foo as bar" — take the local name (foo)
161
+ const asMatch = s.match(/^(\w+)\s+as\s+\w+$/);
162
+ return asMatch ? asMatch[1] : s;
163
+ })
164
+ .filter((s) => /^\w+$/.test(s));
165
+ names.forEach((n) => imports.add(n));
166
+ }
167
+ while ((match = exportRegex.exec(content))) {
168
+ exports.add(match[1]);
169
+ }
170
+ // Check for conflicts
171
+ for (const name of imports) {
172
+ if (exports.has(name)) {
173
+ conflicts.push(name);
174
+ }
175
+ }
176
+ return conflicts;
177
+ }
@@ -1,5 +1,6 @@
1
1
  import * as readline from "readline";
2
2
  import { t } from "../i18n/index";
3
+ import { getDefaultChannel } from "../output/index";
3
4
  /** Index returned by askChoice when the user picks the "custom answer" entry. */
4
5
  export const CUSTOM_INDEX = -1;
5
6
  /**
@@ -77,14 +78,15 @@ export async function askChoice(question, options, opts = {}) {
77
78
  const customEntry = options.length; // index of the custom entry, if enabled
78
79
  const rl = createRl();
79
80
  try {
80
- console.log(formatMenu(question, options, opts));
81
+ const channel = getDefaultChannel();
82
+ channel.writeLine(formatMenu(question, options, opts));
81
83
  for (;;) {
82
84
  const answer = await promptLine(rl, choicePrompt(entryCount, multiple));
83
85
  const parsed = parseSelection(answer, entryCount, multiple);
84
86
  if (parsed) {
85
87
  return parsed.map((i) => (opts.allowCustom && i === customEntry ? CUSTOM_INDEX : i));
86
88
  }
87
- console.log(t("tool.user_input.invalid"));
89
+ channel.writeLine(t("tool.user_input.invalid"));
88
90
  }
89
91
  }
90
92
  finally {
@@ -95,23 +97,28 @@ export async function askChoice(question, options, opts = {}) {
95
97
  * High-level helper used by the question tool.
96
98
  * - No options: free-text input, returns [text] or [] when empty.
97
99
  * - With options: returns selected labels; custom entry resolves to typed text.
100
+ *
101
+ * When `io` is provided (REPL), all prompting goes through it so the tool can
102
+ * borrow the terminal from the input editor. Without it (headless/non-REPL)
103
+ * the module-level readline helpers above remain the fallback.
98
104
  */
99
- export async function askUser(question, opts = {}) {
105
+ export async function askUser(question, opts = {}, io) {
100
106
  const header = [opts.progress, opts.header].filter(Boolean).join(" — ");
101
107
  const text = header ? `${header}\n${question}` : question;
102
108
  if (!opts.options || opts.options.length === 0) {
103
- const answer = await askText(text);
109
+ const answer = io ? await io.askText(text) : await askText(text);
104
110
  return answer ? [answer] : [];
105
111
  }
106
112
  const allowCustom = opts.custom ?? true;
107
- const indexes = await askChoice(text, opts.options, {
108
- multiple: opts.multiple,
109
- allowCustom,
110
- });
113
+ const indexes = io
114
+ ? await io.askChoice(text, opts.options, { multiple: opts.multiple, allowCustom })
115
+ : await askChoice(text, opts.options, { multiple: opts.multiple, allowCustom });
111
116
  const labels = [];
112
117
  for (const idx of indexes) {
113
118
  if (idx === CUSTOM_INDEX) {
114
- const custom = await askText(t("tool.user_input.custom_prompt"));
119
+ const custom = io
120
+ ? await io.askText(t("tool.user_input.custom_prompt"))
121
+ : await askText(t("tool.user_input.custom_prompt"));
115
122
  if (custom)
116
123
  labels.push(custom);
117
124
  }
@@ -7,6 +7,7 @@ import { logFileWrite, logSecurityBlock } from "../modules/security/audit-log";
7
7
  import { getSessionSecurityConfig } from "../modules/security/session-isolation";
8
8
  import { generateDiff, generateNewFileDiff } from "../ui/diff";
9
9
  import { safeResolvePath } from "./path-utils";
10
+ import { preValidateSyntax, detectImportConflicts } from "./syntax-validator";
10
11
  export const writeFileTool = {
11
12
  name: "write_file",
12
13
  icon: "📝",
@@ -66,6 +67,16 @@ export const writeFileTool = {
66
67
  if (!existsSync(dir)) {
67
68
  mkdirSync(dir, { recursive: true });
68
69
  }
70
+ // Pre-validate syntax BEFORE writing — prevents "file written" + syntax error loop
71
+ const validation = await preValidateSyntax(resolved, content, ctx.baseDir);
72
+ if (!validation.valid) {
73
+ return {
74
+ success: false,
75
+ output: t("file.syntax_error", { path, error: validation.error || "Unknown syntax error" }),
76
+ };
77
+ }
78
+ // Detect import/export conflicts — warning only (code may be valid)
79
+ const conflicts = detectImportConflicts(content);
69
80
  const fileExists = existsSync(resolved);
70
81
  let oldContent = "";
71
82
  if (fileExists) {
@@ -77,6 +88,11 @@ export const writeFileTool = {
77
88
  ctx.fileOperationsCount = currentCount + 1;
78
89
  // Log successful file write
79
90
  logFileWrite(ctx.sessionId, path, true, `File ${fileExists ? "updated" : "created"}`);
80
- return { success: true, output: t("file.written", { path }), diff };
91
+ // Add conflict warning to output if any
92
+ let output = t("file.written", { path });
93
+ if (conflicts.length > 0) {
94
+ output += `\n[WARNING] Import/export conflicts: ${conflicts.join(", ")}`;
95
+ }
96
+ return { success: true, output, diff };
81
97
  },
82
98
  };
package/dist/ui/diff.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import { pc } from "./colors";
2
2
  const CONTEXT_LINES = 3;
3
3
  const MAX_DIFF_LINES = 100;
4
+ // LCS is O(m*n) in time and memory — cap the DP input so a huge edit_file
5
+ // can't allocate hundreds of MB. Beyond the cap, diff degrades to a
6
+ // "changed N lines" summary instead of a full LCS.
7
+ const MAX_LCS_INPUT_LINES = 2000;
4
8
  function computeLCS(oldLines, newLines) {
5
9
  const m = oldLines.length;
6
10
  const n = newLines.length;
@@ -127,6 +131,12 @@ export function generateDiff(oldContent, newContent) {
127
131
  return "";
128
132
  const oldLines = oldContent.split("\n");
129
133
  const newLines = newContent.split("\n");
134
+ if (oldLines.length > MAX_LCS_INPUT_LINES ||
135
+ newLines.length > MAX_LCS_INPUT_LINES) {
136
+ // Too large for the O(m*n) DP — show a bounded summary instead of
137
+ // allocating an m*n matrix.
138
+ return pc.dim(` ... (file too large for line diff: ${oldLines.length} -> ${newLines.length} lines)`);
139
+ }
130
140
  const diff = buildDiff(oldLines, newLines);
131
141
  if (diff.length === 0)
132
142
  return "";