micro-models-agent 0.9.0 → 0.11.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 +46 -0
- package/dist/cli/repl.js +52 -1
- package/dist/core/agent.js +5 -0
- package/dist/i18n/en.json +27 -0
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/ru.json +25 -0
- package/dist/llm/openai-compat.js +26 -0
- package/dist/main.js +474 -39
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +3 -0
- package/dist/modules/processes/registry.js +142 -0
- package/dist/modules/processes/runner.js +109 -0
- package/dist/tools/bash.js +42 -15
- package/dist/tools/executor.js +10 -4
- package/dist/tools/index.js +5 -1
- package/dist/tools/process-kill.js +29 -0
- package/dist/tools/process-list.js +38 -0
- package/dist/tools/process-log.js +39 -0
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -2688,6 +2688,25 @@ var en_default = {
|
|
|
2688
2688
|
"tool.screenshot_unavailable": "[Screenshot captured — image not available for text-only model]",
|
|
2689
2689
|
"tool.timeout": "Tool {name} timed out after {seconds} seconds",
|
|
2690
2690
|
"tool.interactive_disabled": "Interactive tool is disabled in exit-on-complete mode. Proceed without asking the user.",
|
|
2691
|
+
"proc.started": `Started background process {id} (PID {pid}).
|
|
2692
|
+
Command: {command}`,
|
|
2693
|
+
"proc.detected_hint": "[Long-running command detected — started in background]",
|
|
2694
|
+
"proc.manage_hint": "Check output: process_log id={id}. Stop it: process_kill id={id}. List all: process_list.",
|
|
2695
|
+
"proc.none": "No background processes running.",
|
|
2696
|
+
"proc.not_found": "Process not found: {id}",
|
|
2697
|
+
"proc.killed": "Process {id} (PID {pid}) killed.",
|
|
2698
|
+
"proc.kill_failed": "Failed to kill process {id}",
|
|
2699
|
+
"proc.list_header": "Background processes",
|
|
2700
|
+
"proc.log_header": "Process {id} ({status}) output:",
|
|
2701
|
+
"proc.log_empty": "(no output yet)",
|
|
2702
|
+
"proc.timed_out": "Command timed out after {ms} ms and was killed.",
|
|
2703
|
+
"proc.hint": "Manage them with {list}, {log}, {kill}.",
|
|
2704
|
+
"proc.status_running": "running",
|
|
2705
|
+
"proc.status_exited": "exited",
|
|
2706
|
+
"proc.status_killed": "killed",
|
|
2707
|
+
"tool.friendly.process_list": "Listing background processes",
|
|
2708
|
+
"tool.friendly.process_log": "Process output",
|
|
2709
|
+
"tool.friendly.process_kill": "Stopping process",
|
|
2691
2710
|
"plan.created": "Plan created: {title} ({steps} steps)",
|
|
2692
2711
|
"plan.step_done": "Step {n}/{total}: {description} ✓",
|
|
2693
2712
|
"plan.complete": "Task complete: {summary}",
|
|
@@ -3118,6 +3137,25 @@ var ru_default = {
|
|
|
3118
3137
|
"tool.screenshot_unavailable": "[Скриншот сделан — изображение недоступно для текстовой модели]",
|
|
3119
3138
|
"tool.timeout": "Инструмент {name} превысил таймаут ({seconds} сек)",
|
|
3120
3139
|
"tool.interactive_disabled": "Интерактивный инструмент отключён в режиме exit-on-complete. Продолжай без вопроса пользователю.",
|
|
3140
|
+
"proc.started": `Фоновый процесс запущен: {id} (PID {pid}).
|
|
3141
|
+
Команда: {command}`,
|
|
3142
|
+
"proc.detected_hint": "[Обнаружена длительная команда — запущена в фоне]",
|
|
3143
|
+
"proc.manage_hint": "Проверить вывод: process_log id={id}. Остановить: process_kill id={id}. Список всех: process_list.",
|
|
3144
|
+
"proc.none": "Фоновых процессов нет.",
|
|
3145
|
+
"proc.not_found": "Процесс не найден: {id}",
|
|
3146
|
+
"proc.killed": "Процесс {id} (PID {pid}) остановлен.",
|
|
3147
|
+
"proc.kill_failed": "Не удалось остановить процесс {id}",
|
|
3148
|
+
"proc.list_header": "Фоновые процессы",
|
|
3149
|
+
"proc.log_header": "Вывод процесса {id} ({status}):",
|
|
3150
|
+
"proc.log_empty": "(вывода пока нет)",
|
|
3151
|
+
"proc.timed_out": "Команда превысила таймаут {ms} мс и была остановлена.",
|
|
3152
|
+
"proc.hint": "Управление: {list}, {log}, {kill}.",
|
|
3153
|
+
"proc.status_running": "работает",
|
|
3154
|
+
"proc.status_exited": "завершён",
|
|
3155
|
+
"proc.status_killed": "остановлен",
|
|
3156
|
+
"tool.friendly.process_list": "Список фоновых процессов",
|
|
3157
|
+
"tool.friendly.process_log": "Вывод процесса",
|
|
3158
|
+
"tool.friendly.process_kill": "Остановка процесса",
|
|
3121
3159
|
"plan.created": "План создан: {title} ({steps} шагов)",
|
|
3122
3160
|
"plan.step_done": "Шаг {n}/{total}: {description} ✓",
|
|
3123
3161
|
"plan.complete": "Задача выполнена: {summary}",
|
|
@@ -3448,7 +3486,7 @@ function t(key, params) {
|
|
|
3448
3486
|
return key;
|
|
3449
3487
|
if (params) {
|
|
3450
3488
|
for (const [k, v] of Object.entries(params)) {
|
|
3451
|
-
template = template.
|
|
3489
|
+
template = template.replaceAll(`{${k}}`, String(v));
|
|
3452
3490
|
}
|
|
3453
3491
|
}
|
|
3454
3492
|
return template;
|
|
@@ -4791,6 +4829,101 @@ class ToolRegistry {
|
|
|
4791
4829
|
}
|
|
4792
4830
|
}
|
|
4793
4831
|
|
|
4832
|
+
// src/modules/processes/runner.ts
|
|
4833
|
+
import { spawn } from "child_process";
|
|
4834
|
+
import { platform } from "os";
|
|
4835
|
+
var activeChildren = new Map;
|
|
4836
|
+
function killByCallId(callId) {
|
|
4837
|
+
const entry = activeChildren.get(callId);
|
|
4838
|
+
if (!entry)
|
|
4839
|
+
return false;
|
|
4840
|
+
activeChildren.delete(callId);
|
|
4841
|
+
try {
|
|
4842
|
+
entry.kill();
|
|
4843
|
+
} catch {}
|
|
4844
|
+
return true;
|
|
4845
|
+
}
|
|
4846
|
+
function killTree(child) {
|
|
4847
|
+
const pid = child.pid;
|
|
4848
|
+
if (!pid)
|
|
4849
|
+
return;
|
|
4850
|
+
if (platform() === "win32") {
|
|
4851
|
+
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
4852
|
+
windowsHide: true,
|
|
4853
|
+
stdio: "ignore"
|
|
4854
|
+
});
|
|
4855
|
+
return;
|
|
4856
|
+
}
|
|
4857
|
+
try {
|
|
4858
|
+
process.kill(-pid, "SIGTERM");
|
|
4859
|
+
} catch {
|
|
4860
|
+
try {
|
|
4861
|
+
child.kill("SIGKILL");
|
|
4862
|
+
} catch {}
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4865
|
+
function runCommand(command, options = {}) {
|
|
4866
|
+
const {
|
|
4867
|
+
cwd,
|
|
4868
|
+
callId,
|
|
4869
|
+
timeoutMs = 120000,
|
|
4870
|
+
maxBuffer = 10 * 1024 * 1024
|
|
4871
|
+
} = options;
|
|
4872
|
+
return new Promise((resolve) => {
|
|
4873
|
+
let timedOut = false;
|
|
4874
|
+
let stdout = "";
|
|
4875
|
+
let stderr = "";
|
|
4876
|
+
const child = spawn(command, {
|
|
4877
|
+
cwd,
|
|
4878
|
+
shell: true,
|
|
4879
|
+
windowsHide: true,
|
|
4880
|
+
detached: platform() !== "win32",
|
|
4881
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4882
|
+
});
|
|
4883
|
+
const append = (ref, chunk) => {
|
|
4884
|
+
const text = chunk.toString();
|
|
4885
|
+
const next = ref.value.length + text.length;
|
|
4886
|
+
if (next > maxBuffer) {
|
|
4887
|
+
ref.value = ref.value.slice(ref.value.length + text.length - maxBuffer) + text;
|
|
4888
|
+
} else {
|
|
4889
|
+
ref.value = ref.value + text;
|
|
4890
|
+
}
|
|
4891
|
+
};
|
|
4892
|
+
const stdoutRef = { value: stdout };
|
|
4893
|
+
const stderrRef = { value: stderr };
|
|
4894
|
+
child.stdout?.on("data", (chunk) => append(stdoutRef, chunk));
|
|
4895
|
+
child.stderr?.on("data", (chunk) => append(stderrRef, chunk));
|
|
4896
|
+
const entry = {
|
|
4897
|
+
kill: () => killTree(child)
|
|
4898
|
+
};
|
|
4899
|
+
if (callId) {
|
|
4900
|
+
activeChildren.set(callId, entry);
|
|
4901
|
+
}
|
|
4902
|
+
const timer = setTimeout(() => {
|
|
4903
|
+
timedOut = true;
|
|
4904
|
+
killTree(child);
|
|
4905
|
+
}, timeoutMs);
|
|
4906
|
+
child.on("error", () => {
|
|
4907
|
+
clearTimeout(timer);
|
|
4908
|
+
if (callId)
|
|
4909
|
+
activeChildren.delete(callId);
|
|
4910
|
+
resolve({
|
|
4911
|
+
stdout: stdoutRef.value,
|
|
4912
|
+
stderr: stderrRef.value,
|
|
4913
|
+
code: null,
|
|
4914
|
+
signal: null,
|
|
4915
|
+
timedOut
|
|
4916
|
+
});
|
|
4917
|
+
});
|
|
4918
|
+
child.on("close", (code, signal) => {
|
|
4919
|
+
clearTimeout(timer);
|
|
4920
|
+
if (callId)
|
|
4921
|
+
activeChildren.delete(callId);
|
|
4922
|
+
resolve({ stdout: stdoutRef.value, stderr: stderrRef.value, code, signal, timedOut });
|
|
4923
|
+
});
|
|
4924
|
+
});
|
|
4925
|
+
}
|
|
4926
|
+
|
|
4794
4927
|
// src/tools/executor.ts
|
|
4795
4928
|
var TOOL_EXECUTION_TIMEOUT_MS = 60000;
|
|
4796
4929
|
|
|
@@ -4832,14 +4965,18 @@ class ToolExecutor {
|
|
|
4832
4965
|
}
|
|
4833
4966
|
let result;
|
|
4834
4967
|
try {
|
|
4968
|
+
this.ctx.activeCallId = call.id;
|
|
4835
4969
|
if (tool.interactive) {
|
|
4836
4970
|
result = await tool.handler(this.ctx, call.arguments);
|
|
4837
4971
|
} else {
|
|
4838
4972
|
const timeoutPromise = new Promise((_, reject) => {
|
|
4839
|
-
setTimeout(() =>
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4973
|
+
setTimeout(() => {
|
|
4974
|
+
killByCallId(call.id);
|
|
4975
|
+
reject(new Error(t("tool.timeout", {
|
|
4976
|
+
name: call.name,
|
|
4977
|
+
seconds: TOOL_EXECUTION_TIMEOUT_MS / 1000
|
|
4978
|
+
})));
|
|
4979
|
+
}, TOOL_EXECUTION_TIMEOUT_MS);
|
|
4843
4980
|
});
|
|
4844
4981
|
result = await Promise.race([
|
|
4845
4982
|
tool.handler(this.ctx, call.arguments),
|
|
@@ -5889,10 +6026,6 @@ var fileInfoTool = {
|
|
|
5889
6026
|
}
|
|
5890
6027
|
};
|
|
5891
6028
|
|
|
5892
|
-
// src/tools/bash.ts
|
|
5893
|
-
import { execSync as execSync2 } from "child_process";
|
|
5894
|
-
import { platform } from "os";
|
|
5895
|
-
|
|
5896
6029
|
// src/modules/security/command-validator.ts
|
|
5897
6030
|
var FALLBACK_BASH_CONFIG = {
|
|
5898
6031
|
blacklist: ["rm", "dd", "chmod", "wget", "curl", "scp", "ssh", "nc", "netcat"],
|
|
@@ -5976,10 +6109,174 @@ function sanitizeCommandForLog(command) {
|
|
|
5976
6109
|
}
|
|
5977
6110
|
return sanitized;
|
|
5978
6111
|
}
|
|
6112
|
+
// src/modules/processes/registry.ts
|
|
6113
|
+
import { spawn as spawn2 } from "child_process";
|
|
6114
|
+
import { platform as platform2 } from "os";
|
|
6115
|
+
var MAX_LOG_LINES = 300;
|
|
6116
|
+
var MAX_KEPT_PROCESSES = 20;
|
|
6117
|
+
var seq = 0;
|
|
6118
|
+
function killTree2(child) {
|
|
6119
|
+
const pid = child.pid;
|
|
6120
|
+
if (!pid)
|
|
6121
|
+
return;
|
|
6122
|
+
if (platform2() === "win32") {
|
|
6123
|
+
spawn2("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
6124
|
+
windowsHide: true,
|
|
6125
|
+
stdio: "ignore"
|
|
6126
|
+
});
|
|
6127
|
+
return;
|
|
6128
|
+
}
|
|
6129
|
+
try {
|
|
6130
|
+
process.kill(-pid, "SIGTERM");
|
|
6131
|
+
} catch {
|
|
6132
|
+
try {
|
|
6133
|
+
child.kill("SIGKILL");
|
|
6134
|
+
} catch {}
|
|
6135
|
+
}
|
|
6136
|
+
}
|
|
5979
6137
|
|
|
6138
|
+
class ProcessRegistry {
|
|
6139
|
+
procs = new Map;
|
|
6140
|
+
children = new Map;
|
|
6141
|
+
start(command, cwd, sessionId) {
|
|
6142
|
+
const id = `proc_${Date.now()}_${++seq}`;
|
|
6143
|
+
const entry = {
|
|
6144
|
+
id,
|
|
6145
|
+
pid: 0,
|
|
6146
|
+
command,
|
|
6147
|
+
cwd,
|
|
6148
|
+
sessionId,
|
|
6149
|
+
status: "running",
|
|
6150
|
+
exitCode: null,
|
|
6151
|
+
startedAt: new Date().toISOString(),
|
|
6152
|
+
log: []
|
|
6153
|
+
};
|
|
6154
|
+
this.trimOldEntries();
|
|
6155
|
+
this.procs.set(id, entry);
|
|
6156
|
+
const child = spawn2(command, {
|
|
6157
|
+
cwd,
|
|
6158
|
+
shell: true,
|
|
6159
|
+
windowsHide: true,
|
|
6160
|
+
detached: platform2() !== "win32",
|
|
6161
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
6162
|
+
});
|
|
6163
|
+
this.children.set(id, child);
|
|
6164
|
+
entry.pid = child.pid ?? 0;
|
|
6165
|
+
let partial = "";
|
|
6166
|
+
const append = (chunk) => {
|
|
6167
|
+
const text = partial + chunk.toString();
|
|
6168
|
+
const lines = text.split(/\r?\n/);
|
|
6169
|
+
partial = lines.pop() ?? "";
|
|
6170
|
+
for (const line of lines) {
|
|
6171
|
+
if (entry.log.length >= MAX_LOG_LINES) {
|
|
6172
|
+
entry.log.shift();
|
|
6173
|
+
}
|
|
6174
|
+
entry.log.push(line);
|
|
6175
|
+
}
|
|
6176
|
+
};
|
|
6177
|
+
child.stdout?.on("data", append);
|
|
6178
|
+
child.stderr?.on("data", append);
|
|
6179
|
+
child.on("error", (err) => {
|
|
6180
|
+
if (entry.status === "running") {
|
|
6181
|
+
entry.status = "killed";
|
|
6182
|
+
}
|
|
6183
|
+
append(Buffer.from(`[process error] ${err.message}`));
|
|
6184
|
+
});
|
|
6185
|
+
child.on("close", (code) => {
|
|
6186
|
+
if (entry.status === "running") {
|
|
6187
|
+
entry.status = "exited";
|
|
6188
|
+
}
|
|
6189
|
+
entry.exitCode = code;
|
|
6190
|
+
this.children.delete(id);
|
|
6191
|
+
if (partial) {
|
|
6192
|
+
entry.log.push(partial);
|
|
6193
|
+
partial = "";
|
|
6194
|
+
}
|
|
6195
|
+
});
|
|
6196
|
+
return entry;
|
|
6197
|
+
}
|
|
6198
|
+
list(sessionId) {
|
|
6199
|
+
const all = Array.from(this.procs.values());
|
|
6200
|
+
if (!sessionId)
|
|
6201
|
+
return all;
|
|
6202
|
+
return all.filter((p) => p.sessionId === sessionId);
|
|
6203
|
+
}
|
|
6204
|
+
get(id) {
|
|
6205
|
+
return this.procs.get(id);
|
|
6206
|
+
}
|
|
6207
|
+
getLog(id, tail) {
|
|
6208
|
+
const entry = this.procs.get(id);
|
|
6209
|
+
if (!entry)
|
|
6210
|
+
return "";
|
|
6211
|
+
const lines = tail && tail > 0 ? entry.log.slice(-tail) : entry.log;
|
|
6212
|
+
return lines.join(`
|
|
6213
|
+
`);
|
|
6214
|
+
}
|
|
6215
|
+
kill(id) {
|
|
6216
|
+
const entry = this.procs.get(id);
|
|
6217
|
+
if (!entry)
|
|
6218
|
+
return false;
|
|
6219
|
+
if (entry.status === "running") {
|
|
6220
|
+
entry.status = "killed";
|
|
6221
|
+
const child = this.children.get(id);
|
|
6222
|
+
if (child) {
|
|
6223
|
+
killTree2(child);
|
|
6224
|
+
}
|
|
6225
|
+
}
|
|
6226
|
+
return true;
|
|
6227
|
+
}
|
|
6228
|
+
killAll(sessionId) {
|
|
6229
|
+
const targets = this.list(sessionId);
|
|
6230
|
+
for (const entry of targets) {
|
|
6231
|
+
this.kill(entry.id);
|
|
6232
|
+
}
|
|
6233
|
+
return targets.length;
|
|
6234
|
+
}
|
|
6235
|
+
trimOldEntries() {
|
|
6236
|
+
if (this.procs.size < MAX_KEPT_PROCESSES)
|
|
6237
|
+
return;
|
|
6238
|
+
const sorted = Array.from(this.procs.values()).sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
6239
|
+
const toRemove = sorted.slice(0, sorted.length - MAX_KEPT_PROCESSES + 1);
|
|
6240
|
+
for (const entry of toRemove) {
|
|
6241
|
+
this.procs.delete(entry.id);
|
|
6242
|
+
this.children.delete(entry.id);
|
|
6243
|
+
}
|
|
6244
|
+
}
|
|
6245
|
+
}
|
|
6246
|
+
var processRegistry = new ProcessRegistry;
|
|
6247
|
+
// src/modules/processes/detect.ts
|
|
6248
|
+
var LONG_RUNNING_PATTERNS = [
|
|
6249
|
+
/\b(npm|pnpm|yarn|bun|npx)\s+(run\s+)?(dev|start|serve|preview|watch|server)\b/i,
|
|
6250
|
+
/\b(deno|node)\s+.*\b(run\s+)?(dev|serve|watch|server)\b/i,
|
|
6251
|
+
/(^|\s)--watch\b/i,
|
|
6252
|
+
/(^|\s)-w\b/i,
|
|
6253
|
+
/\bnodemon\b/i,
|
|
6254
|
+
/\btsx watch\b/i,
|
|
6255
|
+
/\b(ts-node|tsc)\s+.*--watch\b/i,
|
|
6256
|
+
/\bvite(?!\s+(build|create))\b/i,
|
|
6257
|
+
/\bwebpack(-dev-server|\s+serve)\b/i,
|
|
6258
|
+
/\bastro dev\b/i,
|
|
6259
|
+
/\bnext (dev|start)\b/i,
|
|
6260
|
+
/\bnuxt (dev|start)\b/i,
|
|
6261
|
+
/\bsvelte-kit (dev|preview)\b/i,
|
|
6262
|
+
/\bgatsby develop\b/i,
|
|
6263
|
+
/\bdocker(-compose)?\s+.*\bup\b/i,
|
|
6264
|
+
/\buvicorn\b|\bgunicorn\b/i,
|
|
6265
|
+
/\bpython\s+-m\s+http\.server\b/i,
|
|
6266
|
+
/\bflask run\b/i,
|
|
6267
|
+
/\bphp artisan serve\b/i,
|
|
6268
|
+
/\brails (server|s)\b/i,
|
|
6269
|
+
/\bvitest watch\b/i,
|
|
6270
|
+
/\bjest --watch\b/i
|
|
6271
|
+
];
|
|
6272
|
+
function isLongRunningCommand(command) {
|
|
6273
|
+
return LONG_RUNNING_PATTERNS.some((pattern) => pattern.test(command));
|
|
6274
|
+
}
|
|
5980
6275
|
// src/tools/bash.ts
|
|
6276
|
+
import { platform as platform3 } from "os";
|
|
6277
|
+
var BASH_TIMEOUT_MS = 120000;
|
|
5981
6278
|
function adaptCommandForWindows(command) {
|
|
5982
|
-
if (
|
|
6279
|
+
if (platform3() !== "win32")
|
|
5983
6280
|
return command;
|
|
5984
6281
|
const trimmed = command.trim();
|
|
5985
6282
|
if (trimmed.startsWith("mkdir -p ")) {
|
|
@@ -5992,13 +6289,14 @@ function adaptCommandForWindows(command) {
|
|
|
5992
6289
|
}
|
|
5993
6290
|
var bashTool = {
|
|
5994
6291
|
name: "bash",
|
|
5995
|
-
description: "Execute a shell command and return its output. Use for running tests, build, git, and shell operations.",
|
|
6292
|
+
description: "Execute a shell command and return its output. Use for running tests, build, git, and shell operations. Long-running commands (dev servers, watchers) start in the background and return a process id immediately — manage them with process_list, process_log, process_kill. Set background=true to force background execution.",
|
|
5996
6293
|
tags: ["shell", "code"],
|
|
5997
6294
|
parameters: {
|
|
5998
6295
|
type: "object",
|
|
5999
6296
|
properties: {
|
|
6000
6297
|
command: { type: "string", description: "Shell command to execute" },
|
|
6001
|
-
workdir: { type: "string", description: "Working directory (default: baseDir)" }
|
|
6298
|
+
workdir: { type: "string", description: "Working directory (default: baseDir)" },
|
|
6299
|
+
background: { type: "boolean", description: "Start the command in the background and return immediately with a process id (default: auto-detect long-running commands)" }
|
|
6002
6300
|
},
|
|
6003
6301
|
required: ["command"]
|
|
6004
6302
|
},
|
|
@@ -6020,26 +6318,156 @@ var bashTool = {
|
|
|
6020
6318
|
if (securityConfig?.logCommands) {
|
|
6021
6319
|
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), false, `Working directory: ${workdir}`);
|
|
6022
6320
|
}
|
|
6321
|
+
const background = args.background === true || isLongRunningCommand(command);
|
|
6322
|
+
if (background) {
|
|
6323
|
+
const entry = processRegistry.start(command, workdir, ctx.sessionId);
|
|
6324
|
+
if (securityConfig?.logCommands) {
|
|
6325
|
+
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), true, `Started in background: ${entry.id} (PID ${entry.pid})`);
|
|
6326
|
+
}
|
|
6327
|
+
const explicit = args.background === true;
|
|
6328
|
+
return {
|
|
6329
|
+
success: true,
|
|
6330
|
+
output: `${t("proc.started", {
|
|
6331
|
+
id: entry.id,
|
|
6332
|
+
pid: entry.pid,
|
|
6333
|
+
command
|
|
6334
|
+
})}${explicit ? "" : `
|
|
6335
|
+
${t("proc.detected_hint")}`}
|
|
6336
|
+
${t("proc.manage_hint", {
|
|
6337
|
+
id: entry.id
|
|
6338
|
+
})}`
|
|
6339
|
+
};
|
|
6340
|
+
}
|
|
6023
6341
|
try {
|
|
6024
|
-
const
|
|
6342
|
+
const res = await runCommand(command, {
|
|
6025
6343
|
cwd: workdir,
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
timeout: 120000
|
|
6344
|
+
callId: ctx.activeCallId,
|
|
6345
|
+
timeoutMs: BASH_TIMEOUT_MS
|
|
6029
6346
|
});
|
|
6347
|
+
const parts = [res.stdout.trimEnd(), res.stderr.trimEnd()].filter(Boolean);
|
|
6348
|
+
let output = parts.join(`
|
|
6349
|
+
`);
|
|
6350
|
+
if (res.timedOut) {
|
|
6351
|
+
output = `${output ? output + `
|
|
6352
|
+
` : ""}${t("proc.timed_out", {
|
|
6353
|
+
ms: BASH_TIMEOUT_MS
|
|
6354
|
+
})}`;
|
|
6355
|
+
} else if (!output && res.code !== 0) {
|
|
6356
|
+
output = `(exit code ${res.code})`;
|
|
6357
|
+
}
|
|
6030
6358
|
if (securityConfig?.logCommands) {
|
|
6031
|
-
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command),
|
|
6359
|
+
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), res.code === 0, `Working directory: ${workdir}, Output length: ${res.stdout.length}`);
|
|
6032
6360
|
}
|
|
6033
|
-
return { success:
|
|
6361
|
+
return { success: res.code === 0, output };
|
|
6034
6362
|
} catch (e) {
|
|
6035
|
-
const stderr = e.stderr?.toString() || "";
|
|
6036
|
-
const stdout = e.stdout?.toString() || "";
|
|
6037
|
-
const errorOutput = stderr || stdout || e.message;
|
|
6038
6363
|
if (securityConfig?.logCommands) {
|
|
6039
|
-
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), false, `Working directory: ${workdir}, Error: ${
|
|
6364
|
+
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), false, `Working directory: ${workdir}, Error: ${e.message?.slice(0, 100) || ""}`);
|
|
6040
6365
|
}
|
|
6041
|
-
return { success: false, output:
|
|
6366
|
+
return { success: false, output: e.message || String(e) };
|
|
6367
|
+
}
|
|
6368
|
+
}
|
|
6369
|
+
};
|
|
6370
|
+
|
|
6371
|
+
// src/tools/process-list.ts
|
|
6372
|
+
var processListTool = {
|
|
6373
|
+
name: "process_list",
|
|
6374
|
+
description: "List background processes started via the bash tool (dev servers, watchers, long-running commands). Shows id, pid, command, status, and recent output. Use with process_log and process_kill to inspect or stop them.",
|
|
6375
|
+
tags: ["shell"],
|
|
6376
|
+
parameters: {
|
|
6377
|
+
type: "object",
|
|
6378
|
+
properties: {}
|
|
6379
|
+
},
|
|
6380
|
+
handler: async (ctx) => {
|
|
6381
|
+
const list = processRegistry.list(ctx.sessionId);
|
|
6382
|
+
if (list.length === 0) {
|
|
6383
|
+
return { success: true, output: t("proc.none") };
|
|
6384
|
+
}
|
|
6385
|
+
const statusLabel = (status) => {
|
|
6386
|
+
if (status === "running")
|
|
6387
|
+
return t("proc.status_running");
|
|
6388
|
+
if (status === "exited")
|
|
6389
|
+
return t("proc.status_exited");
|
|
6390
|
+
return t("proc.status_killed");
|
|
6391
|
+
};
|
|
6392
|
+
const lines = [`${t("proc.list_header")} (${list.length}):`];
|
|
6393
|
+
for (const entry of list) {
|
|
6394
|
+
const tail = entry.log.length > 0 ? entry.log[entry.log.length - 1] : "";
|
|
6395
|
+
const detail = tail ? ` — ${tail.slice(0, 80)}${tail.length > 80 ? "…" : ""}` : "";
|
|
6396
|
+
lines.push(` ${entry.id} PID ${entry.pid} ${statusLabel(entry.status)} ${entry.command}${detail}`);
|
|
6397
|
+
}
|
|
6398
|
+
lines.push(`
|
|
6399
|
+
${t("proc.hint", {
|
|
6400
|
+
list: "process_list",
|
|
6401
|
+
log: "process_log",
|
|
6402
|
+
kill: "process_kill"
|
|
6403
|
+
})}`);
|
|
6404
|
+
return { success: true, output: lines.join(`
|
|
6405
|
+
`) };
|
|
6406
|
+
}
|
|
6407
|
+
};
|
|
6408
|
+
|
|
6409
|
+
// src/tools/process-log.ts
|
|
6410
|
+
var processLogTool = {
|
|
6411
|
+
name: "process_log",
|
|
6412
|
+
description: "Show the buffered output of a background process started via bash. Use after starting a dev server to verify it came up without errors, and while it runs to check its state.",
|
|
6413
|
+
tags: ["shell"],
|
|
6414
|
+
parameters: {
|
|
6415
|
+
type: "object",
|
|
6416
|
+
properties: {
|
|
6417
|
+
id: { type: "string", description: "Process id from bash output or process_list" },
|
|
6418
|
+
tail: { type: "number", description: "Number of trailing lines to show (default: all buffered lines, max 300)" }
|
|
6419
|
+
},
|
|
6420
|
+
required: ["id"]
|
|
6421
|
+
},
|
|
6422
|
+
handler: async (ctx, args) => {
|
|
6423
|
+
const id = String(args.id);
|
|
6424
|
+
const entry = processRegistry.get(id);
|
|
6425
|
+
if (!entry) {
|
|
6426
|
+
return { success: false, output: t("proc.not_found", { id }) };
|
|
6427
|
+
}
|
|
6428
|
+
const status = entry.status === "running" ? t("proc.status_running") : entry.status === "exited" ? t("proc.status_exited") : t("proc.status_killed");
|
|
6429
|
+
const tail = typeof args.tail === "number" ? args.tail : undefined;
|
|
6430
|
+
const log = processRegistry.getLog(id, tail);
|
|
6431
|
+
if (!log) {
|
|
6432
|
+
return {
|
|
6433
|
+
success: true,
|
|
6434
|
+
output: `${t("proc.log_header", { id, status })} ${t("proc.log_empty")}`
|
|
6435
|
+
};
|
|
6436
|
+
}
|
|
6437
|
+
return {
|
|
6438
|
+
success: true,
|
|
6439
|
+
output: `${t("proc.log_header", { id, status })}
|
|
6440
|
+
${log}`
|
|
6441
|
+
};
|
|
6442
|
+
}
|
|
6443
|
+
};
|
|
6444
|
+
|
|
6445
|
+
// src/tools/process-kill.ts
|
|
6446
|
+
var processKillTool = {
|
|
6447
|
+
name: "process_kill",
|
|
6448
|
+
description: "Stop a background process started via bash (dev server, watcher). Kills the whole process tree (children included). Use the id returned by bash or process_list.",
|
|
6449
|
+
tags: ["shell"],
|
|
6450
|
+
parameters: {
|
|
6451
|
+
type: "object",
|
|
6452
|
+
properties: {
|
|
6453
|
+
id: { type: "string", description: "Process id from bash output or process_list" }
|
|
6454
|
+
},
|
|
6455
|
+
required: ["id"]
|
|
6456
|
+
},
|
|
6457
|
+
handler: async (ctx, args) => {
|
|
6458
|
+
const id = String(args.id);
|
|
6459
|
+
const entry = processRegistry.get(id);
|
|
6460
|
+
if (!entry) {
|
|
6461
|
+
return { success: false, output: t("proc.not_found", { id }) };
|
|
6462
|
+
}
|
|
6463
|
+
const killed = processRegistry.kill(id);
|
|
6464
|
+
if (!killed) {
|
|
6465
|
+
return { success: false, output: t("proc.kill_failed", { id }) };
|
|
6042
6466
|
}
|
|
6467
|
+
return {
|
|
6468
|
+
success: true,
|
|
6469
|
+
output: t("proc.killed", { id, pid: entry.pid })
|
|
6470
|
+
};
|
|
6043
6471
|
}
|
|
6044
6472
|
};
|
|
6045
6473
|
|
|
@@ -7429,7 +7857,7 @@ class MoEExecutor {
|
|
|
7429
7857
|
// src/modules/execution/verifier.ts
|
|
7430
7858
|
import { existsSync as existsSync16 } from "fs";
|
|
7431
7859
|
import { resolve as resolve13, extname as extname2 } from "path";
|
|
7432
|
-
import { execSync as
|
|
7860
|
+
import { execSync as execSync2 } from "child_process";
|
|
7433
7861
|
class StepVerifier {
|
|
7434
7862
|
baseDir;
|
|
7435
7863
|
constructor(baseDir) {
|
|
@@ -7445,7 +7873,7 @@ class StepVerifier {
|
|
|
7445
7873
|
}
|
|
7446
7874
|
async runScript(scriptName) {
|
|
7447
7875
|
try {
|
|
7448
|
-
|
|
7876
|
+
execSync2(`bun run ${scriptName}`, {
|
|
7449
7877
|
cwd: this.baseDir,
|
|
7450
7878
|
encoding: "utf-8",
|
|
7451
7879
|
timeout: 60000,
|
|
@@ -7462,7 +7890,7 @@ class StepVerifier {
|
|
|
7462
7890
|
return { passed: true, message: "No tsconfig.json found — skipping type check" };
|
|
7463
7891
|
}
|
|
7464
7892
|
try {
|
|
7465
|
-
|
|
7893
|
+
execSync2("npx tsc --noEmit", {
|
|
7466
7894
|
cwd: this.baseDir,
|
|
7467
7895
|
encoding: "utf-8",
|
|
7468
7896
|
timeout: 60000,
|
|
@@ -7545,7 +7973,7 @@ class StepVerifier {
|
|
|
7545
7973
|
const ext = extname2(filePath);
|
|
7546
7974
|
if (ext === ".ts" || ext === ".tsx") {
|
|
7547
7975
|
try {
|
|
7548
|
-
|
|
7976
|
+
execSync2(`npx tsc --noEmit --skipLibCheck ${filePath}`, { stdio: "pipe", timeout: 1e4 });
|
|
7549
7977
|
return true;
|
|
7550
7978
|
} catch (err) {
|
|
7551
7979
|
if (err.status === 127 || err.message.includes("not found") || err.message.includes("ENOENT")) {
|
|
@@ -7560,7 +7988,7 @@ class StepVerifier {
|
|
|
7560
7988
|
}
|
|
7561
7989
|
if (ext === ".js" || ext === ".jsx") {
|
|
7562
7990
|
try {
|
|
7563
|
-
|
|
7991
|
+
execSync2(`node --check ${filePath}`, { stdio: "pipe", timeout: 5000 });
|
|
7564
7992
|
return true;
|
|
7565
7993
|
} catch {
|
|
7566
7994
|
return false;
|
|
@@ -8205,6 +8633,10 @@ ${t("tool.truncated", { tokens: Math.ceil(removedChars / 2) })}`;
|
|
|
8205
8633
|
shutdown() {
|
|
8206
8634
|
const { pluginManager, logger, sessionManager, contextManager } = this.deps;
|
|
8207
8635
|
contextManager.onCompact = null;
|
|
8636
|
+
const killed = processRegistry.killAll();
|
|
8637
|
+
if (killed > 0) {
|
|
8638
|
+
logger.info(`Killed ${killed} background process(es) on shutdown`);
|
|
8639
|
+
}
|
|
8208
8640
|
pluginManager.runOnSessionEnd({
|
|
8209
8641
|
logger,
|
|
8210
8642
|
sessionManager: sessionManager?.getActiveMeta()
|
|
@@ -9692,7 +10124,7 @@ ${logs.join(`
|
|
|
9692
10124
|
};
|
|
9693
10125
|
|
|
9694
10126
|
// src/modules/mcp/client.ts
|
|
9695
|
-
import { spawn } from "child_process";
|
|
10127
|
+
import { spawn as spawn3 } from "child_process";
|
|
9696
10128
|
|
|
9697
10129
|
class MCPClient {
|
|
9698
10130
|
serverName;
|
|
@@ -9799,7 +10231,7 @@ class MCPClient {
|
|
|
9799
10231
|
}
|
|
9800
10232
|
connectStdio() {
|
|
9801
10233
|
return new Promise((resolve14, reject) => {
|
|
9802
|
-
const child =
|
|
10234
|
+
const child = spawn3(this.config.command, this.config.args || [], {
|
|
9803
10235
|
env: { ...process.env, ...this.config.env },
|
|
9804
10236
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9805
10237
|
});
|
|
@@ -10824,6 +11256,9 @@ function registerAllTools(registry2, skillsModule) {
|
|
|
10824
11256
|
fileInfoTool,
|
|
10825
11257
|
bashTool,
|
|
10826
11258
|
subagentTool,
|
|
11259
|
+
processListTool,
|
|
11260
|
+
processLogTool,
|
|
11261
|
+
processKillTool,
|
|
10827
11262
|
webSearchTool,
|
|
10828
11263
|
webFetchTool,
|
|
10829
11264
|
webBrowseTool,
|
|
@@ -10917,7 +11352,7 @@ class PluginLoader {
|
|
|
10917
11352
|
}
|
|
10918
11353
|
|
|
10919
11354
|
// src/modules/plugins/builtin/lint-on-write.ts
|
|
10920
|
-
import { execSync as
|
|
11355
|
+
import { execSync as execSync3 } from "child_process";
|
|
10921
11356
|
import { existsSync as existsSync20, readFileSync as readFileSync10 } from "fs";
|
|
10922
11357
|
import { resolve as resolve14, extname as extname3, join as join13 } from "path";
|
|
10923
11358
|
var projectTypeCheckPromise = null;
|
|
@@ -10954,7 +11389,7 @@ class LintOnWritePlugin {
|
|
|
10954
11389
|
checkSyntax(filePath, ext, baseDir) {
|
|
10955
11390
|
if (ext === ".ts" || ext === ".tsx") {
|
|
10956
11391
|
try {
|
|
10957
|
-
|
|
11392
|
+
execSync3(`npx tsc --noEmit --skipLibCheck "${filePath}"`, { cwd: baseDir, stdio: "pipe", timeout: 1e4 });
|
|
10958
11393
|
return null;
|
|
10959
11394
|
} catch (err) {
|
|
10960
11395
|
if (err.status === 127 || err.message.includes("not found") || err.message.includes("ENOENT")) {
|
|
@@ -10971,7 +11406,7 @@ class LintOnWritePlugin {
|
|
|
10971
11406
|
}
|
|
10972
11407
|
if (ext === ".js" || ext === ".jsx") {
|
|
10973
11408
|
try {
|
|
10974
|
-
|
|
11409
|
+
execSync3(`node --check "${filePath}"`, { cwd: baseDir, stdio: "pipe", timeout: 5000 });
|
|
10975
11410
|
return null;
|
|
10976
11411
|
} catch (err) {
|
|
10977
11412
|
const stderr = err.stderr?.toString() || "";
|
|
@@ -10994,7 +11429,7 @@ class LintOnWritePlugin {
|
|
|
10994
11429
|
return;
|
|
10995
11430
|
}
|
|
10996
11431
|
ctx.logger.debug(`Running lint: ${lintScript}`);
|
|
10997
|
-
|
|
11432
|
+
execSync3(lintScript, { cwd: ctx.baseDir, stdio: "pipe" });
|
|
10998
11433
|
ctx.logger.debug("Lint passed");
|
|
10999
11434
|
} catch (err) {
|
|
11000
11435
|
ctx.logger.warn(`Lint failed: ${err.message}`);
|
|
@@ -11029,7 +11464,7 @@ class LintOnWritePlugin {
|
|
|
11029
11464
|
}
|
|
11030
11465
|
async runTscCheck(baseDir) {
|
|
11031
11466
|
try {
|
|
11032
|
-
|
|
11467
|
+
execSync3(`npx tsc --noEmit --skipLibCheck`, { cwd: baseDir, stdio: "pipe", timeout: 30000 });
|
|
11033
11468
|
return null;
|
|
11034
11469
|
} catch (err) {
|
|
11035
11470
|
if (err.status === 127 || err.message.includes("not found") || err.message.includes("ENOENT")) {
|
|
@@ -11256,7 +11691,7 @@ class StuckDetector {
|
|
|
11256
11691
|
}
|
|
11257
11692
|
|
|
11258
11693
|
// src/modules/execution/auditor.ts
|
|
11259
|
-
import { execSync as
|
|
11694
|
+
import { execSync as execSync4 } from "child_process";
|
|
11260
11695
|
import { existsSync as existsSync21 } from "fs";
|
|
11261
11696
|
import { resolve as resolve15, join as join14 } from "path";
|
|
11262
11697
|
class Auditor {
|
|
@@ -11308,7 +11743,7 @@ class Auditor {
|
|
|
11308
11743
|
return null;
|
|
11309
11744
|
}
|
|
11310
11745
|
try {
|
|
11311
|
-
|
|
11746
|
+
execSync4(`npx tsc --noEmit --skipLibCheck`, { cwd: this.baseDir, stdio: "pipe", timeout: 30000 });
|
|
11312
11747
|
return null;
|
|
11313
11748
|
} catch (err) {
|
|
11314
11749
|
if (err.status === 127 || err.message.includes("not found") || err.message.includes("ENOENT")) {
|
|
@@ -12103,7 +12538,7 @@ class SessionModule {
|
|
|
12103
12538
|
// src/modules/user-profile/profile.ts
|
|
12104
12539
|
import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync25, mkdirSync as mkdirSync11 } from "fs";
|
|
12105
12540
|
import { join as join17 } from "path";
|
|
12106
|
-
import { homedir as homedir7, hostname, platform as
|
|
12541
|
+
import { homedir as homedir7, hostname, platform as platform4, type } from "os";
|
|
12107
12542
|
import { env } from "process";
|
|
12108
12543
|
|
|
12109
12544
|
// src/modules/user-profile/compressor.ts
|
|
@@ -12132,7 +12567,7 @@ class UserProfile {
|
|
|
12132
12567
|
}
|
|
12133
12568
|
collect() {
|
|
12134
12569
|
this.info = {
|
|
12135
|
-
platform:
|
|
12570
|
+
platform: platform4(),
|
|
12136
12571
|
os: `${type()} ${hostname()}`,
|
|
12137
12572
|
hostname: hostname(),
|
|
12138
12573
|
shell: env.SHELL || env.ComSpec || "unknown",
|