micro-models-agent 0.9.0 → 0.10.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/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.replace(`{${k}}`, String(v));
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(() => reject(new Error(t("tool.timeout", {
4840
- name: call.name,
4841
- seconds: TOOL_EXECUTION_TIMEOUT_MS / 1000
4842
- }))), TOOL_EXECUTION_TIMEOUT_MS);
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 (platform() !== "win32")
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 output = execSync2(command, {
6342
+ const res = await runCommand(command, {
6025
6343
  cwd: workdir,
6026
- encoding: "utf-8",
6027
- maxBuffer: 10 * 1024 * 1024,
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), true, `Working directory: ${workdir}, Output length: ${output.length}`);
6359
+ logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), res.code === 0, `Working directory: ${workdir}, Output length: ${res.stdout.length}`);
6032
6360
  }
6033
- return { success: true, output: output.trimEnd() };
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: ${errorOutput.slice(0, 100)}`);
6364
+ logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), false, `Working directory: ${workdir}, Error: ${e.message?.slice(0, 100) || ""}`);
6040
6365
  }
6041
- return { success: false, output: errorOutput };
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 execSync3 } from "child_process";
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
- execSync3(`bun run ${scriptName}`, {
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
- execSync3("npx tsc --noEmit", {
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
- execSync3(`npx tsc --noEmit --skipLibCheck ${filePath}`, { stdio: "pipe", timeout: 1e4 });
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
- execSync3(`node --check ${filePath}`, { stdio: "pipe", timeout: 5000 });
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 = spawn(this.config.command, this.config.args || [], {
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 execSync4 } from "child_process";
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
- execSync4(`npx tsc --noEmit --skipLibCheck "${filePath}"`, { cwd: baseDir, stdio: "pipe", timeout: 1e4 });
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
- execSync4(`node --check "${filePath}"`, { cwd: baseDir, stdio: "pipe", timeout: 5000 });
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
- execSync4(lintScript, { cwd: ctx.baseDir, stdio: "pipe" });
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
- execSync4(`npx tsc --noEmit --skipLibCheck`, { cwd: baseDir, stdio: "pipe", timeout: 30000 });
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 execSync5 } from "child_process";
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
- execSync5(`npx tsc --noEmit --skipLibCheck`, { cwd: this.baseDir, stdio: "pipe", timeout: 30000 });
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 platform2, type } from "os";
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: platform2(),
12570
+ platform: platform4(),
12136
12571
  os: `${type()} ${hostname()}`,
12137
12572
  hostname: hostname(),
12138
12573
  shell: env.SHELL || env.ComSpec || "unknown",