micro-models-agent 0.28.17 → 0.29.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/dist/cli/commands.js +3 -116
- package/dist/cli/main.js +8 -35
- package/dist/cli/repl.js +611 -110
- package/dist/cli/setup.js +12 -32
- package/dist/config/config.js +30 -46
- package/dist/config/defaults.js +1 -10
- package/dist/config/security.js +8 -15
- package/dist/core/agent-moe.js +12 -24
- package/dist/core/agent.js +47 -281
- package/dist/core/bootstrap.js +36 -52
- package/dist/core/session-logger.js +2 -35
- package/dist/i18n/en.json +15 -79
- package/dist/i18n/index.js +9 -12
- package/dist/i18n/ru.json +15 -79
- package/dist/index.js +13 -13
- package/dist/llm/openai-compat.js +10 -39
- package/dist/logger/app-logger.js +16 -83
- package/dist/main.js +624 -243
- package/dist/modules/browser/session.js +60 -108
- package/dist/modules/context/history.js +15 -0
- package/dist/modules/context/manager.js +10 -119
- package/dist/modules/execution/auditor.js +39 -33
- package/dist/modules/execution/index.js +6 -8
- package/dist/modules/execution/module.js +32 -474
- package/dist/modules/execution/moe-executor.js +40 -97
- package/dist/modules/execution/planner.js +13 -63
- package/dist/modules/execution/stuck-detector.js +39 -252
- package/dist/modules/execution/tracker.js +7 -21
- package/dist/modules/execution/verifier.js +17 -46
- package/dist/modules/hallucination/confidence.js +2 -7
- package/dist/modules/hallucination/consistency.js +42 -8
- package/dist/modules/hallucination/detector.js +21 -26
- package/dist/modules/hallucination/factual.js +150 -170
- package/dist/modules/hallucination/index.js +4 -5
- package/dist/modules/index.js +5 -5
- package/dist/modules/mcp/client.js +2 -8
- package/dist/modules/memory/store.js +0 -4
- package/dist/modules/plugins/builtin/lint-on-write.js +38 -143
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +2 -1
- package/dist/modules/processes/registry.js +35 -125
- package/dist/modules/processes/runner.js +110 -9
- package/dist/modules/security/audit-log.js +10 -30
- package/dist/modules/security/command-validator.js +16 -42
- package/dist/modules/security/content-scanner.js +8 -9
- package/dist/modules/security/network-validator.js +2 -2
- package/dist/modules/security/path-validator.js +10 -64
- package/dist/modules/security/security-policies.js +67 -221
- package/dist/modules/security/session-encryption.js +25 -42
- package/dist/modules/session/manager.js +10 -15
- package/dist/modules/session/store.js +8 -62
- package/dist/modules/skills/index.js +3 -2
- package/dist/modules/skills/matcher.js +27 -0
- package/dist/modules/skills/module.js +23 -10
- package/dist/tools/bash.js +90 -287
- package/dist/tools/create-dir.js +1 -0
- package/dist/tools/delete-file.js +1 -0
- package/dist/tools/edit-file.js +8 -10
- package/dist/tools/executor.js +7 -57
- package/dist/tools/grep-tool.js +29 -51
- package/dist/tools/index.js +40 -55
- package/dist/tools/load-skill.js +18 -14
- package/dist/tools/move-file.js +2 -3
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/read-file.js +5 -15
- package/dist/tools/search-history.js +22 -42
- package/dist/tools/subagent.js +12 -21
- package/dist/tools/web-browse.js +25 -54
- package/dist/tools/web-fetch.js +34 -60
- package/dist/tools/web-search.js +20 -39
- package/dist/tools/write-file.js +10 -13
- package/dist/ui/diff.js +16 -9
- package/dist/ui/renderer.js +6 -69
- package/package.json +1 -1
- package/dist/cli/repl-commands.js +0 -633
- package/dist/core/workspace.js +0 -76
- package/dist/logger/file-log.js +0 -151
- package/dist/modules/certification/cli.js +0 -176
- package/dist/modules/certification/fact-checker.js +0 -84
- package/dist/modules/certification/loader.js +0 -111
- package/dist/modules/certification/manifest.js +0 -50
- package/dist/modules/certification/runner.js +0 -162
- package/dist/modules/certification/scenarios.js +0 -124
- package/dist/modules/certification/types.js +0 -1
- package/dist/modules/execution/plan-coverage.js +0 -68
- package/dist/modules/execution/plan-persister.js +0 -46
- package/dist/modules/execution/plan-store.js +0 -159
- package/dist/modules/hallucination/js-identifiers.js +0 -72
- package/dist/modules/hallucination/llm-judge.js +0 -103
- package/dist/modules/lsp/client.js +0 -235
- package/dist/modules/lsp/config.js +0 -81
- package/dist/modules/lsp/index.js +0 -3
- package/dist/modules/lsp/module.js +0 -68
- package/dist/modules/lsp/types.js +0 -1
|
@@ -1,62 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, readFileSync } from
|
|
3
|
-
import { resolve, extname, join } from
|
|
4
|
-
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { resolve, extname, join } from 'path';
|
|
4
|
+
let projectTypeCheckPromise = null;
|
|
5
|
+
let projectTypeCheckTimestamp = 0;
|
|
5
6
|
const TYPE_CHECK_DEBOUNCE_MS = 2000;
|
|
6
|
-
// Per-file syntax checks are cached by (path, content hash): re-writing a file
|
|
7
|
-
// with identical content skips the (expensive) check. Content changes → cache
|
|
8
|
-
// miss → fresh check.
|
|
9
|
-
const syntaxCache = new Map();
|
|
10
|
-
function contentHash(content) {
|
|
11
|
-
let h = 5381;
|
|
12
|
-
for (let i = 0; i < content.length; i++) {
|
|
13
|
-
h = ((h << 5) + h + content.charCodeAt(i)) | 0;
|
|
14
|
-
}
|
|
15
|
-
return String(h);
|
|
16
|
-
}
|
|
17
|
-
let _winDecoder;
|
|
18
|
-
function getWinDecoder() {
|
|
19
|
-
if (_winDecoder === undefined) {
|
|
20
|
-
if (platform() !== "win32") {
|
|
21
|
-
_winDecoder = null;
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
try {
|
|
25
|
-
const cpOut = execSync("chcp.com", {
|
|
26
|
-
encoding: "buffer",
|
|
27
|
-
timeout: 2000,
|
|
28
|
-
windowsHide: true,
|
|
29
|
-
}).toString("latin1");
|
|
30
|
-
const m = cpOut.match(/(\d+)/);
|
|
31
|
-
_winDecoder = m ? new TextDecoder("ibm" + m[1]) : null;
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
_winDecoder = null;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return _winDecoder ?? new TextDecoder("utf-8");
|
|
39
|
-
}
|
|
40
7
|
export class LintOnWritePlugin {
|
|
41
|
-
name =
|
|
8
|
+
name = 'lint-on-write';
|
|
42
9
|
priority = 10;
|
|
43
|
-
_checkPromise = null;
|
|
44
|
-
_checkTimestamp = 0;
|
|
45
10
|
async onAfterTool(ctx, call, result) {
|
|
46
|
-
if (call.name !==
|
|
11
|
+
if (call.name !== 'write_file' && call.name !== 'edit_file') {
|
|
47
12
|
return;
|
|
48
13
|
}
|
|
49
14
|
if (!result.success) {
|
|
50
15
|
return;
|
|
51
16
|
}
|
|
52
|
-
const path = String(call.arguments.path ||
|
|
17
|
+
const path = String(call.arguments.path || '');
|
|
53
18
|
if (!path)
|
|
54
19
|
return;
|
|
55
20
|
const fullPath = resolve(ctx.baseDir, path);
|
|
56
21
|
if (!existsSync(fullPath))
|
|
57
22
|
return;
|
|
58
23
|
const ext = extname(fullPath);
|
|
59
|
-
const syntaxError =
|
|
24
|
+
const syntaxError = this.checkSyntax(fullPath, ext, ctx.baseDir);
|
|
60
25
|
if (syntaxError) {
|
|
61
26
|
result.output += `\n\n[Syntax check failed]: ${syntaxError}`;
|
|
62
27
|
return;
|
|
@@ -64,50 +29,32 @@ export class LintOnWritePlugin {
|
|
|
64
29
|
await this.runProjectLint(ctx, result);
|
|
65
30
|
await this.runProjectTypeCheck(ctx, result);
|
|
66
31
|
}
|
|
67
|
-
|
|
68
|
-
if (ext ===
|
|
69
|
-
// Syntax-only check via bun's parser (~200ms) instead of `npx tsc`
|
|
70
|
-
// (~3-5s per write). Full type errors are still surfaced by the
|
|
71
|
-
// debounced project typecheck below when a tsconfig exists.
|
|
72
|
-
let content = "";
|
|
73
|
-
try {
|
|
74
|
-
content = readFileSync(filePath, "utf-8");
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
const hash = contentHash(content);
|
|
80
|
-
const cached = syntaxCache.get(filePath);
|
|
81
|
-
if (cached && cached.hash === hash) {
|
|
82
|
-
return cached.error;
|
|
83
|
-
}
|
|
32
|
+
checkSyntax(filePath, ext, baseDir) {
|
|
33
|
+
if (ext === '.ts' || ext === '.tsx') {
|
|
84
34
|
try {
|
|
85
|
-
|
|
86
|
-
syntaxCache.set(filePath, { hash, error: null });
|
|
35
|
+
execSync(`npx tsc --noEmit --skipLibCheck "${filePath}"`, { cwd: baseDir, stdio: 'pipe', timeout: 10000 });
|
|
87
36
|
return null;
|
|
88
37
|
}
|
|
89
38
|
catch (err) {
|
|
90
|
-
if (err.status === 127 ||
|
|
91
|
-
err.message.includes("not found") ||
|
|
92
|
-
err.message.includes("ENOENT")) {
|
|
39
|
+
if (err.status === 127 || err.message.includes('not found') || err.message.includes('ENOENT')) {
|
|
93
40
|
return null;
|
|
94
41
|
}
|
|
95
|
-
const stderr = err.stderr?.toString() || err.stdout?.toString() ||
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
42
|
+
const stderr = err.stderr?.toString() || err.stdout?.toString() || '';
|
|
43
|
+
if (stderr.includes('error TS')) {
|
|
44
|
+
const firstError = stderr.split('\n').find((line) => line.includes('error TS')) || 'TypeScript syntax error';
|
|
45
|
+
return firstError.trim();
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
100
48
|
}
|
|
101
49
|
}
|
|
102
|
-
if (ext ===
|
|
50
|
+
if (ext === '.js' || ext === '.jsx') {
|
|
103
51
|
try {
|
|
104
|
-
|
|
52
|
+
execSync(`node --check "${filePath}"`, { cwd: baseDir, stdio: 'pipe', timeout: 5000 });
|
|
105
53
|
return null;
|
|
106
54
|
}
|
|
107
55
|
catch (err) {
|
|
108
|
-
const stderr = err.stderr?.toString() ||
|
|
109
|
-
const firstError = stderr.split(
|
|
110
|
-
"JavaScript syntax error";
|
|
56
|
+
const stderr = err.stderr?.toString() || '';
|
|
57
|
+
const firstError = stderr.split('\n').find((line) => line.trim()) || 'JavaScript syntax error';
|
|
111
58
|
return firstError.trim();
|
|
112
59
|
}
|
|
113
60
|
}
|
|
@@ -115,18 +62,18 @@ export class LintOnWritePlugin {
|
|
|
115
62
|
}
|
|
116
63
|
async runProjectLint(ctx, result) {
|
|
117
64
|
try {
|
|
118
|
-
const packageJsonPath = join(ctx.baseDir,
|
|
65
|
+
const packageJsonPath = join(ctx.baseDir, 'package.json');
|
|
119
66
|
if (!existsSync(packageJsonPath)) {
|
|
120
67
|
return;
|
|
121
68
|
}
|
|
122
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath,
|
|
69
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
123
70
|
const lintScript = packageJson.scripts?.lint;
|
|
124
71
|
if (!lintScript) {
|
|
125
72
|
return;
|
|
126
73
|
}
|
|
127
74
|
ctx.logger.debug(`Running lint: ${lintScript}`);
|
|
128
|
-
|
|
129
|
-
ctx.logger.debug(
|
|
75
|
+
execSync(lintScript, { cwd: ctx.baseDir, stdio: 'pipe' });
|
|
76
|
+
ctx.logger.debug('Lint passed');
|
|
130
77
|
}
|
|
131
78
|
catch (err) {
|
|
132
79
|
ctx.logger.warn(`Lint failed: ${err.message}`);
|
|
@@ -134,93 +81,41 @@ export class LintOnWritePlugin {
|
|
|
134
81
|
}
|
|
135
82
|
}
|
|
136
83
|
async runProjectTypeCheck(ctx, result) {
|
|
137
|
-
const tsconfigPath = join(ctx.baseDir,
|
|
84
|
+
const tsconfigPath = join(ctx.baseDir, 'tsconfig.json');
|
|
138
85
|
if (!existsSync(tsconfigPath)) {
|
|
139
86
|
return;
|
|
140
87
|
}
|
|
141
88
|
const now = Date.now();
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
const error = await this._checkPromise;
|
|
89
|
+
if (projectTypeCheckPromise && now - projectTypeCheckTimestamp < TYPE_CHECK_DEBOUNCE_MS) {
|
|
90
|
+
const error = await projectTypeCheckPromise;
|
|
145
91
|
if (error) {
|
|
146
92
|
result.output += `\n\n[Project typecheck failed]: ${error}`;
|
|
147
93
|
}
|
|
148
94
|
return;
|
|
149
95
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const error = await
|
|
96
|
+
projectTypeCheckTimestamp = now;
|
|
97
|
+
projectTypeCheckPromise = this.runTscCheck(ctx.baseDir);
|
|
98
|
+
const error = await projectTypeCheckPromise;
|
|
153
99
|
if (error) {
|
|
154
100
|
result.output += `\n\n[Project typecheck failed]: ${error}`;
|
|
155
101
|
}
|
|
156
102
|
}
|
|
157
103
|
async runTscCheck(baseDir) {
|
|
158
104
|
try {
|
|
159
|
-
|
|
105
|
+
execSync(`npx tsc --noEmit --skipLibCheck`, { cwd: baseDir, stdio: 'pipe', timeout: 30000 });
|
|
160
106
|
return null;
|
|
161
107
|
}
|
|
162
108
|
catch (err) {
|
|
163
|
-
if (err.status === 127 ||
|
|
164
|
-
err.message.includes("not found") ||
|
|
165
|
-
err.message.includes("ENOENT")) {
|
|
109
|
+
if (err.status === 127 || err.message.includes('not found') || err.message.includes('ENOENT')) {
|
|
166
110
|
return null;
|
|
167
111
|
}
|
|
168
|
-
const stderr = err.stderr?.toString() || err.stdout?.toString() ||
|
|
169
|
-
if (stderr.includes(
|
|
170
|
-
const firstError = stderr
|
|
171
|
-
.split("\n")
|
|
172
|
-
.find((line) => line.includes("error TS")) ||
|
|
173
|
-
"TypeScript type error";
|
|
112
|
+
const stderr = err.stderr?.toString() || err.stdout?.toString() || '';
|
|
113
|
+
if (stderr.includes('error TS')) {
|
|
114
|
+
const firstError = stderr.split('\n').find((line) => line.includes('error TS')) || 'TypeScript type error';
|
|
174
115
|
return firstError.trim();
|
|
175
116
|
}
|
|
176
117
|
return null;
|
|
177
118
|
}
|
|
178
119
|
}
|
|
179
120
|
}
|
|
180
|
-
/**
|
|
181
|
-
* Run a command asynchronously (non-blocking event loop) and resolve when it
|
|
182
|
-
* exits. Rejects with captured stderr/stdout on non-zero exit or timeout.
|
|
183
|
-
*/
|
|
184
|
-
function runAsync(command, cwd, timeoutMs) {
|
|
185
|
-
return new Promise((resolve, reject) => {
|
|
186
|
-
const child = spawn(command, {
|
|
187
|
-
cwd,
|
|
188
|
-
shell: true,
|
|
189
|
-
windowsHide: true,
|
|
190
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
191
|
-
});
|
|
192
|
-
let stdout = "";
|
|
193
|
-
let stderr = "";
|
|
194
|
-
const decoder = getWinDecoder();
|
|
195
|
-
child.stdout?.on("data", (d) => {
|
|
196
|
-
stdout += decoder.decode(d, { stream: true });
|
|
197
|
-
});
|
|
198
|
-
child.stderr?.on("data", (d) => {
|
|
199
|
-
stderr += decoder.decode(d, { stream: true });
|
|
200
|
-
});
|
|
201
|
-
const timer = setTimeout(() => {
|
|
202
|
-
child.kill();
|
|
203
|
-
reject(new Error(`Command timed out after ${timeoutMs}ms`));
|
|
204
|
-
}, timeoutMs);
|
|
205
|
-
child.on("error", (err) => {
|
|
206
|
-
clearTimeout(timer);
|
|
207
|
-
reject(err);
|
|
208
|
-
});
|
|
209
|
-
child.on("close", (code) => {
|
|
210
|
-
clearTimeout(timer);
|
|
211
|
-
stdout += decoder.decode();
|
|
212
|
-
stderr += decoder.decode();
|
|
213
|
-
if (code === 0) {
|
|
214
|
-
resolve({ stdout, stderr });
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
const err = new Error(`Command failed with exit code ${code}`);
|
|
218
|
-
err.stdout = stdout;
|
|
219
|
-
err.stderr = stderr;
|
|
220
|
-
err.status = code;
|
|
221
|
-
reject(err);
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
121
|
export const plugin = new LintOnWritePlugin();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic detection of long-running (server/watch) commands.
|
|
3
|
+
*
|
|
4
|
+
* The explicit `background: true` bash parameter always wins; this list is a
|
|
5
|
+
* convenience so dev servers don't block the agent. It only inspects the raw
|
|
6
|
+
* shell command (a technical process-type decision, not task classification).
|
|
7
|
+
*/
|
|
8
|
+
const LONG_RUNNING_PATTERNS = [
|
|
9
|
+
/\b(npm|pnpm|yarn|bun|npx)\s+(run\s+)?(dev|start|serve|preview|watch|server)\b/i,
|
|
10
|
+
/\b(deno|node)\s+.*\b(run\s+)?(dev|serve|watch|server)\b/i,
|
|
11
|
+
/(^|\s)--watch\b/i,
|
|
12
|
+
/(^|\s)-w\b/i,
|
|
13
|
+
/\bnodemon\b/i,
|
|
14
|
+
/\btsx watch\b/i,
|
|
15
|
+
/\b(ts-node|tsc)\s+.*--watch\b/i,
|
|
16
|
+
/\bvite(?!\s+(build|create))\b/i,
|
|
17
|
+
/\bwebpack(-dev-server|\s+serve)\b/i,
|
|
18
|
+
/\bastro dev\b/i,
|
|
19
|
+
/\bnext (dev|start)\b/i,
|
|
20
|
+
/\bnuxt (dev|start)\b/i,
|
|
21
|
+
/\bsvelte-kit (dev|preview)\b/i,
|
|
22
|
+
/\bgatsby develop\b/i,
|
|
23
|
+
/\bdocker(-compose)?\s+.*\bup\b/i,
|
|
24
|
+
/\buvicorn\b|\bgunicorn\b/i,
|
|
25
|
+
/\bpython\s+-m\s+http\.server\b/i,
|
|
26
|
+
/\bflask run\b/i,
|
|
27
|
+
/\bphp artisan serve\b/i,
|
|
28
|
+
/\brails (server|s)\b/i,
|
|
29
|
+
/\bvitest watch\b/i,
|
|
30
|
+
/\bjest --watch\b/i,
|
|
31
|
+
];
|
|
32
|
+
export function isLongRunningCommand(command) {
|
|
33
|
+
return LONG_RUNNING_PATTERNS.some((pattern) => pattern.test(command));
|
|
34
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { spawn
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
2
|
import { platform } from "os";
|
|
3
|
-
const MAX_LOG_LINES =
|
|
3
|
+
const MAX_LOG_LINES = 300;
|
|
4
4
|
const MAX_KEPT_PROCESSES = 20;
|
|
5
5
|
let seq = 0;
|
|
6
6
|
function killTree(child) {
|
|
@@ -8,16 +8,10 @@ function killTree(child) {
|
|
|
8
8
|
if (!pid)
|
|
9
9
|
return;
|
|
10
10
|
if (platform() === "win32") {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
timeout: 3000,
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
/* already dead */
|
|
20
|
-
}
|
|
11
|
+
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
12
|
+
windowsHide: true,
|
|
13
|
+
stdio: "ignore",
|
|
14
|
+
});
|
|
21
15
|
return;
|
|
22
16
|
}
|
|
23
17
|
try {
|
|
@@ -35,10 +29,9 @@ function killTree(child) {
|
|
|
35
29
|
class ProcessRegistry {
|
|
36
30
|
procs = new Map();
|
|
37
31
|
children = new Map();
|
|
38
|
-
exitWaiters = new Map();
|
|
39
32
|
/**
|
|
40
|
-
* Start a
|
|
41
|
-
*
|
|
33
|
+
* Start a background process. Returns immediately with the entry; output is
|
|
34
|
+
* buffered into `entry.log` as it arrives.
|
|
42
35
|
*/
|
|
43
36
|
start(command, cwd, sessionId) {
|
|
44
37
|
const id = `proc_${Date.now()}_${++seq}`;
|
|
@@ -55,98 +48,46 @@ class ProcessRegistry {
|
|
|
55
48
|
};
|
|
56
49
|
this.trimOldEntries();
|
|
57
50
|
this.procs.set(id, entry);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
catch (e) {
|
|
69
|
-
entry.status = "killed";
|
|
70
|
-
entry.spawnError = e?.message || String(e);
|
|
71
|
-
entry.log.push(`[process error] ${entry.spawnError}`);
|
|
72
|
-
return entry;
|
|
73
|
-
}
|
|
51
|
+
const child = spawn(command, {
|
|
52
|
+
cwd,
|
|
53
|
+
shell: true,
|
|
54
|
+
windowsHide: true,
|
|
55
|
+
detached: platform() !== "win32",
|
|
56
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
57
|
+
});
|
|
74
58
|
this.children.set(id, child);
|
|
75
59
|
entry.pid = child.pid ?? 0;
|
|
76
|
-
|
|
77
|
-
// NOT UTF-8. Detect the active code page via chcp.com; fall back to
|
|
78
|
-
// ibm866 (Russian CP866) when detection fails — still correct for the
|
|
79
|
-
// majority of users, and always better than the UTF-8 default.
|
|
80
|
-
const decoder = platform() === "win32"
|
|
81
|
-
? (() => {
|
|
82
|
-
try {
|
|
83
|
-
const cpOut = require("child_process")
|
|
84
|
-
.execSync("chcp.com", {
|
|
85
|
-
encoding: "buffer",
|
|
86
|
-
timeout: 2000,
|
|
87
|
-
windowsHide: true,
|
|
88
|
-
})
|
|
89
|
-
.toString("latin1");
|
|
90
|
-
const m = cpOut.match(/(\d+)/);
|
|
91
|
-
if (m)
|
|
92
|
-
return new TextDecoder("ibm" + m[1]);
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
/* ignore */
|
|
96
|
-
}
|
|
97
|
-
return new TextDecoder("ibm866");
|
|
98
|
-
})()
|
|
99
|
-
: new TextDecoder("utf-8");
|
|
100
|
-
let buf = "";
|
|
101
|
-
const pushLine = (line) => {
|
|
102
|
-
if (entry.log.length >= MAX_LOG_LINES) {
|
|
103
|
-
entry.log.shift();
|
|
104
|
-
}
|
|
105
|
-
entry.log.push(line);
|
|
106
|
-
};
|
|
60
|
+
let partial = "";
|
|
107
61
|
const append = (chunk) => {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
buf = lines.pop() ?? "";
|
|
62
|
+
const text = partial + chunk.toString();
|
|
63
|
+
const lines = text.split(/\r?\n/);
|
|
64
|
+
partial = lines.pop() ?? "";
|
|
112
65
|
for (const line of lines) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
buf += decoder.decode();
|
|
118
|
-
if (buf) {
|
|
119
|
-
pushLine(buf);
|
|
120
|
-
buf = "";
|
|
66
|
+
if (entry.log.length >= MAX_LOG_LINES) {
|
|
67
|
+
entry.log.shift();
|
|
68
|
+
}
|
|
69
|
+
entry.log.push(line);
|
|
121
70
|
}
|
|
122
71
|
};
|
|
123
|
-
const resolveExit = () => {
|
|
124
|
-
this.exitWaiters.get(id)?.resolve();
|
|
125
|
-
};
|
|
126
72
|
child.stdout?.on("data", append);
|
|
127
73
|
child.stderr?.on("data", append);
|
|
128
74
|
child.on("error", (err) => {
|
|
129
75
|
if (entry.status === "running") {
|
|
130
76
|
entry.status = "killed";
|
|
131
77
|
}
|
|
132
|
-
entry.spawnError = err.message;
|
|
133
78
|
append(Buffer.from(`[process error] ${err.message}`));
|
|
134
|
-
resolveExit();
|
|
135
79
|
});
|
|
136
80
|
child.on("close", (code) => {
|
|
137
|
-
flush();
|
|
138
81
|
if (entry.status === "running") {
|
|
139
82
|
entry.status = "exited";
|
|
140
83
|
}
|
|
141
84
|
entry.exitCode = code;
|
|
142
85
|
this.children.delete(id);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
resolve = r;
|
|
86
|
+
if (partial) {
|
|
87
|
+
entry.log.push(partial);
|
|
88
|
+
partial = "";
|
|
89
|
+
}
|
|
148
90
|
});
|
|
149
|
-
this.exitWaiters.set(id, { promise, resolve });
|
|
150
91
|
return entry;
|
|
151
92
|
}
|
|
152
93
|
list(sessionId) {
|
|
@@ -187,51 +128,20 @@ class ProcessRegistry {
|
|
|
187
128
|
}
|
|
188
129
|
return targets.length;
|
|
189
130
|
}
|
|
190
|
-
/**
|
|
191
|
-
* Resolve `true` when the process exits (or fails to spawn) within
|
|
192
|
-
* `timeoutMs`, otherwise `false` (still running → a background process).
|
|
193
|
-
*/
|
|
194
|
-
waitForExit(id, timeoutMs) {
|
|
195
|
-
const entry = this.procs.get(id);
|
|
196
|
-
const waiter = this.exitWaiters.get(id);
|
|
197
|
-
if (!entry || entry.status !== "running" || !waiter) {
|
|
198
|
-
return Promise.resolve(true);
|
|
199
|
-
}
|
|
200
|
-
return new Promise((resolve) => {
|
|
201
|
-
const timer = setTimeout(() => resolve(false), timeoutMs);
|
|
202
|
-
waiter.promise.then(() => {
|
|
203
|
-
clearTimeout(timer);
|
|
204
|
-
resolve(true);
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Remove a finished entry (foreground results do not linger). A still-running
|
|
210
|
-
* entry is killed first so nothing is orphaned.
|
|
211
|
-
*/
|
|
212
|
-
remove(id) {
|
|
213
|
-
const entry = this.procs.get(id);
|
|
214
|
-
if (!entry)
|
|
215
|
-
return false;
|
|
216
|
-
if (entry.status === "running") {
|
|
217
|
-
const child = this.children.get(id);
|
|
218
|
-
if (child) {
|
|
219
|
-
killTree(child);
|
|
220
|
-
}
|
|
221
|
-
entry.status = "killed";
|
|
222
|
-
}
|
|
223
|
-
this.procs.delete(id);
|
|
224
|
-
this.children.delete(id);
|
|
225
|
-
this.exitWaiters.delete(id);
|
|
226
|
-
return true;
|
|
227
|
-
}
|
|
228
131
|
trimOldEntries() {
|
|
229
132
|
if (this.procs.size < MAX_KEPT_PROCESSES)
|
|
230
133
|
return;
|
|
231
134
|
const sorted = Array.from(this.procs.values()).sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
232
135
|
const toRemove = sorted.slice(0, sorted.length - MAX_KEPT_PROCESSES + 1);
|
|
233
136
|
for (const entry of toRemove) {
|
|
234
|
-
|
|
137
|
+
if (entry.status === "running") {
|
|
138
|
+
const child = this.children.get(entry.id);
|
|
139
|
+
if (child) {
|
|
140
|
+
killTree(child);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
this.procs.delete(entry.id);
|
|
144
|
+
this.children.delete(entry.id);
|
|
235
145
|
}
|
|
236
146
|
}
|
|
237
147
|
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { platform } from "os";
|
|
1
3
|
const activeChildren = new Map();
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export function unregisterKillable(callId) {
|
|
8
|
-
activeChildren.delete(callId);
|
|
9
|
-
}
|
|
10
|
-
/** Invoke (and drop) the kill callback registered for `callId`. */
|
|
4
|
+
/**
|
|
5
|
+
* Kill a foreground command previously registered by `callId` (used by the
|
|
6
|
+
* tool executor when the execution timeout fires). Returns true when a child
|
|
7
|
+
* was found and killed.
|
|
8
|
+
*/
|
|
11
9
|
export function killByCallId(callId) {
|
|
12
10
|
const entry = activeChildren.get(callId);
|
|
13
11
|
if (!entry)
|
|
@@ -21,3 +19,106 @@ export function killByCallId(callId) {
|
|
|
21
19
|
}
|
|
22
20
|
return true;
|
|
23
21
|
}
|
|
22
|
+
function killTree(child) {
|
|
23
|
+
const pid = child.pid;
|
|
24
|
+
if (!pid)
|
|
25
|
+
return;
|
|
26
|
+
if (platform() === "win32") {
|
|
27
|
+
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
28
|
+
windowsHide: true,
|
|
29
|
+
stdio: "ignore",
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
// `detached: true` makes the child a process-group leader on POSIX,
|
|
35
|
+
// so killing -pid terminates the whole tree.
|
|
36
|
+
process.kill(-pid, "SIGTERM");
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
try {
|
|
40
|
+
child.kill("SIGKILL");
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
/* already dead */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Run a shell command asynchronously (non-blocking event loop) and collect
|
|
49
|
+
* stdout/stderr. The child is killed on timeout and can be killed externally
|
|
50
|
+
* via `killByCallId` (used by the tool executor timeout).
|
|
51
|
+
*/
|
|
52
|
+
export function runCommand(command, options = {}) {
|
|
53
|
+
const { cwd, callId, timeoutMs = 120_000, maxBuffer = 10 * 1024 * 1024, } = options;
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
let timedOut = false;
|
|
56
|
+
let stdout = "";
|
|
57
|
+
let stderr = "";
|
|
58
|
+
const child = spawn(command, {
|
|
59
|
+
cwd,
|
|
60
|
+
shell: true,
|
|
61
|
+
windowsHide: true,
|
|
62
|
+
detached: platform() !== "win32",
|
|
63
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
64
|
+
});
|
|
65
|
+
const stdoutRef = { value: stdout };
|
|
66
|
+
const stderrRef = { value: stderr };
|
|
67
|
+
// On Windows, child processes spawned via cmd.exe output in the OEM code
|
|
68
|
+
// page (typically CP866 for Russian), NOT UTF-8 — even when the parent
|
|
69
|
+
// console reports code page 65001 via `chcp`. Always decode with ibm866
|
|
70
|
+
// so that Cyrillic and other non-ASCII text is not garbled.
|
|
71
|
+
const decoder = platform() === "win32" ? new TextDecoder("ibm866") : null;
|
|
72
|
+
const decodeChunk = (chunk) => decoder ? decoder.decode(chunk, { stream: true }) : chunk.toString("utf-8");
|
|
73
|
+
child.stdout?.on("data", (chunk) => {
|
|
74
|
+
const text = decodeChunk(chunk);
|
|
75
|
+
const next = stdoutRef.value.length + text.length;
|
|
76
|
+
if (next > maxBuffer) {
|
|
77
|
+
stdoutRef.value =
|
|
78
|
+
stdoutRef.value.slice(stdoutRef.value.length + text.length - maxBuffer) + text;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
stdoutRef.value = stdoutRef.value + text;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
child.stderr?.on("data", (chunk) => {
|
|
85
|
+
const text = decodeChunk(chunk);
|
|
86
|
+
const next = stderrRef.value.length + text.length;
|
|
87
|
+
if (next > maxBuffer) {
|
|
88
|
+
stderrRef.value =
|
|
89
|
+
stderrRef.value.slice(stderrRef.value.length + text.length - maxBuffer) + text;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
stderrRef.value = stderrRef.value + text;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
const entry = {
|
|
96
|
+
kill: () => killTree(child),
|
|
97
|
+
};
|
|
98
|
+
if (callId) {
|
|
99
|
+
activeChildren.set(callId, entry);
|
|
100
|
+
}
|
|
101
|
+
const timer = setTimeout(() => {
|
|
102
|
+
timedOut = true;
|
|
103
|
+
killTree(child);
|
|
104
|
+
}, timeoutMs);
|
|
105
|
+
child.on("error", () => {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
if (callId)
|
|
108
|
+
activeChildren.delete(callId);
|
|
109
|
+
resolve({
|
|
110
|
+
stdout: stdoutRef.value,
|
|
111
|
+
stderr: stderrRef.value,
|
|
112
|
+
code: null,
|
|
113
|
+
signal: null,
|
|
114
|
+
timedOut,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
child.on("close", (code, signal) => {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
if (callId)
|
|
120
|
+
activeChildren.delete(callId);
|
|
121
|
+
resolve({ stdout: stdoutRef.value, stderr: stderrRef.value, code, signal, timedOut });
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|