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
package/dist/tools/subagent.js
CHANGED
|
@@ -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 =
|
|
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:
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
+
}
|
package/dist/tools/user-input.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
}
|
package/dist/tools/write-file.js
CHANGED
|
@@ -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
|
-
|
|
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 "";
|