mcp-server-commands 0.7.4 → 0.8.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/README.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## `runProcess` tool
2
+
3
+ The `runProcess` tool runs processes on the host machine. There are two mutually exclusive ways to invoke it:
4
+
5
+ 1. **`command_line`** (string) — Executed via the system's default shell (just like typing into `bash`/`fish`/`pwsh`/etc). Shell features like pipes, redirects, and variable expansion all work.
6
+ 2. **`argv`** (string array) — Direct executable invocation. `argv[0]` is the executable, the rest are arguments. No shell interpretation.
7
+
8
+ You cannot pass both. The tool infers whether to use a shell from which parameter you provide.
9
+
10
+ If you want your model to use specific shell(s) on a system, I would list them in your system prompt. Or, maybe in your tool instructions, though models tend to pay better attention to examples in a system prompt.
11
+
12
+ Let me know if you encounter problems!
13
+
1
14
  ## Tools
2
15
 
3
16
  Tools are for LLMs to request. Claude Sonnet 3.5 intelligently uses `run_process`. And, initial testing shows promising results with [Groq Desktop with MCP](https://github.com/groq/groq-desktop-beta) and `llama4` models.
@@ -1,17 +1,82 @@
1
1
  // TODO cleanup exec usages once spawn is ready
2
2
  import { spawn } from "child_process";
3
- export async function spawn_wrapped(command, args, stdin, options) {
4
- return new Promise((resolve, reject) => {
3
+ import { performance } from "perf_hooks";
4
+ import { is_verbose, verbose_log } from "./logging.js";
5
+ import { resultFor } from "./messages.js";
6
+ import { errorResult } from "./messages.js";
7
+ export function runProcess(runProcessArgs) {
8
+ const startTime = performance.now();
9
+ const options = {
10
+ // spawn options: https://nodejs.org/api/child_process.html#child_processspawncommand-args-options
11
+ encoding: "utf8"
12
+ };
13
+ if (runProcessArgs?.cwd) {
14
+ options.cwd = String(runProcessArgs.cwd);
15
+ }
16
+ const stdin = runProcessArgs?.stdin ? String(runProcessArgs.stdin) : undefined;
17
+ // ---------------------------------------------------------------------
18
+ // RunProcess argument handling – determine the actual command and args.
19
+ // ---------------------------------------------------------------------
20
+ const isShellMode = Boolean(runProcessArgs?.command_line);
21
+ const isExecutableMode = Array.isArray(runProcessArgs?.argv) && (runProcessArgs?.argv).length > 0;
22
+ if (isShellMode && isExecutableMode) {
23
+ return Promise.resolve(errorResult("Cannot pass both 'command_line' and 'argv'. Use one or the other."));
24
+ }
25
+ if (!isShellMode && !isExecutableMode) {
26
+ return Promise.resolve(errorResult("Either 'command_line' (string) or 'argv' (array) is required."));
27
+ }
28
+ // Resolve the command/args based on the mode.
29
+ let execCommand = "";
30
+ let execArgs = [];
31
+ if (isShellMode) {
32
+ options.shell = true;
33
+ execCommand = String(runProcessArgs.command_line);
34
+ execArgs = [];
35
+ }
36
+ else {
37
+ options.shell = false;
38
+ const argv = runProcessArgs?.argv;
39
+ execCommand = argv[0];
40
+ execArgs = argv.slice(1);
41
+ }
42
+ const logWithElapsedTime = (msg, ...rest) => {
43
+ if (!is_verbose)
44
+ return;
45
+ const elapsed = ((performance.now() - startTime) / 1000).toFixed(3);
46
+ verbose_log(`[${elapsed}s] ${msg}`, ...rest, execCommand, execArgs);
47
+ };
48
+ let child_pid;
49
+ const promise = new Promise((resolve, reject) => {
5
50
  if (!stdin) {
6
- // FYI default is all 'pipe' (works when stdin is provided)
7
- // 'ignore' attaches /dev/null
8
- // order: [STDIN, STDOUT, STDERR]
51
+ // PRN windowsHide on Windows, signal, killSignal
52
+ // FYI spawn_options.stdio => default is perfect ['pipe', 'pipe', 'pipe']
53
+ // order: [STDIN, STDOUT, STDERR]
54
+ // https://nodejs.org/api/child_process.html#optionsstdio
55
+ // 'ignore' attaches /dev/null
56
+ // do not set 'inherit' (causes ripgrep to see STDIN socket and search it, thus hanging)
9
57
  options.stdio = ['ignore', 'pipe', 'pipe'];
10
- // ? I wonder if this was related to fishWorkaround issue w/ STDIN (see commit history)... I was using base64 encoding to pass STDIN
11
58
  }
12
- const child = spawn(command, args, options);
13
- // PRN return pid to callers?
14
- // console.log(`child.pid: ${child.pid}`);
59
+ // remove timeout on spawn options (if set) so the built‑in spawn timeout does not interfere
60
+ delete options.timeout;
61
+ // Use a detached child so we can kill the entire process group.
62
+ options.detached = true;
63
+ let settled = false;
64
+ const settle = (result, isError) => {
65
+ if (settled)
66
+ return;
67
+ settled = true;
68
+ if (timer)
69
+ clearTimeout(timer);
70
+ if (isError) {
71
+ resolve(resultFor(result));
72
+ }
73
+ else {
74
+ resolve(resultFor(result));
75
+ }
76
+ };
77
+ const child = spawn(execCommand, execArgs, options);
78
+ logWithElapsedTime(`START SPAWN child.pid: ${child.pid}`);
79
+ child_pid = child.pid;
15
80
  let stdout = "";
16
81
  let stderr = "";
17
82
  if (child.stdin && stdin) {
@@ -28,45 +93,94 @@ export async function spawn_wrapped(command, args, stdin, options) {
28
93
  stderr += chunk.toString();
29
94
  });
30
95
  }
31
- let errored = false;
96
+ child.on("exit", (code, signal) => {
97
+ // child process streams MAY still be open when EXIT is emitted (use close if need to ensure they're closed)
98
+ // "close" will come after "exit" once process is terminated + streams are closed
99
+ // so for now use "close" to determine if process was terminated too, that way you can access STDOUT/STDERR reliably for returning full output to agent
100
+ logWithElapsedTime("EXIT", { code, signal });
101
+ });
102
+ child.on("spawn", () => {
103
+ // emitted after child process starts successfully
104
+ // if child doesn't start, error emitted instead
105
+ // emitted BEFORE any data received via stdout/stderr
106
+ logWithElapsedTime("SPAWN");
107
+ });
108
+ // Timeout handling – kill the whole process group after the supplied timeout.
109
+ let timer = null;
110
+ let timeoutMs = Number(runProcessArgs?.timeout_ms);
111
+ if (Number.isNaN(timeoutMs)) {
112
+ timeoutMs = 30_000;
113
+ }
114
+ timer = setTimeout(() => {
115
+ if (process.platform !== "win32") {
116
+ if (child.pid) {
117
+ try {
118
+ process.kill(-child.pid, "SIGTERM");
119
+ }
120
+ catch (_) { }
121
+ }
122
+ }
123
+ else {
124
+ child.kill("SIGTERM");
125
+ }
126
+ const killTimeout = setTimeout(() => {
127
+ if (process.platform !== "win32") {
128
+ if (child.pid) {
129
+ try {
130
+ process.kill(-child.pid, "SIGKILL");
131
+ }
132
+ catch (_) { }
133
+ }
134
+ }
135
+ else {
136
+ child.kill("SIGKILL");
137
+ }
138
+ }, 2000);
139
+ const clearKill = () => clearTimeout(killTimeout);
140
+ child.once("exit", clearKill);
141
+ child.once("close", clearKill);
142
+ }, timeoutMs);
32
143
  child.on("error", (err) => {
144
+ logWithElapsedTime("ERROR");
33
145
  // ChildProcess 'error' docs: https://nodejs.org/api/child_process.html#event-error
34
146
  // error running process
35
147
  // IIUC not just b/c of command failed w/ non-zero exit code
36
148
  const result = {
37
- stdout: stdout,
38
- stderr: stderr,
149
+ stdout,
150
+ stderr,
39
151
  //
40
- code: err.code,
41
- signal: err.signal,
152
+ // one of these will always be non-null
153
+ code: err.code, // set if process exited, else null
154
+ signal: err.signal, // set if process was terminated by signal, else null
42
155
  //
43
156
  message: err.message,
44
- // killed: (err as any).killed,
45
- cmd: command, // TODO does error have .cmd on it? is it the composite result of processing in spawn too? (or same as I passed)
157
+ // ? killed: (err as any).killed
46
158
  };
47
- // console.log("ON_ERROR", result);
48
- errored = true;
49
- reject(result);
159
+ logWithElapsedTime("ERROR_RESULT", result);
160
+ settle(result, true);
50
161
  });
51
162
  child.on("close", (code, signal) => {
163
+ logWithElapsedTime("CLOSE");
52
164
  // ChildProcess 'close' docs: https://nodejs.org/api/child_process.html#event-close
53
165
  // 'close' is after child process ends AND stdio streams are closed
54
166
  // - after 'exit' or 'error'
55
- //
56
- // * do not resolve if 'error' already called (promise is already resolved)
57
- if (errored)
58
- return;
59
167
  // either code is set, or signal, but NOT BOTH
60
168
  // signal if process killed
61
169
  // FYI close does not mean code==0
62
170
  const result = {
63
- stdout: stdout,
64
- stderr: stderr,
171
+ stdout,
172
+ stderr,
173
+ //
65
174
  code: code ?? undefined,
66
175
  signal: signal ?? undefined,
176
+ //
67
177
  };
68
- // console.log("ON_CLOSE", result);
69
- resolve(result);
178
+ logWithElapsedTime("CLOSE_RESULT", result);
179
+ settle(result, false);
70
180
  });
71
181
  });
182
+ // FYI later (when needed) I can map this onto the promise that comes back from runProcess too (and tie into that unit test I have that needs pid to terminate it)
183
+ // Resolve the underlying spawn result, then map to CallToolResult including PID.
184
+ promise.pid = child_pid;
185
+ return promise;
72
186
  }
package/build/index.js CHANGED
@@ -4,7 +4,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  import { createRequire } from "module";
6
6
  import { registerPrompts } from "./prompts.js";
7
- import { reisterTools } from "./tools.js";
7
+ import { registerTools } from "./tools.js";
8
8
  const require = createRequire(import.meta.url);
9
9
  const { name: package_name, version: package_version, } = require("../package.json");
10
10
  const server = new Server({
@@ -19,7 +19,7 @@ const server = new Server({
19
19
  //logging: {}, // for logging messages that don't seem to work yet or I am doing them wrong
20
20
  },
21
21
  });
22
- reisterTools(server);
22
+ registerTools(server);
23
23
  registerPrompts(server);
24
24
  async function main() {
25
25
  const transport = new StdioServerTransport();
@@ -0,0 +1,51 @@
1
+ import fs from 'fs';
2
+ export let is_verbose = false;
3
+ if (process.argv.includes("--verbose")) {
4
+ is_verbose = true;
5
+ }
6
+ const isJest = typeof process !== 'undefined' && !!process.env.JEST_WORKER_ID;
7
+ if (isJest) {
8
+ // is_verbose = true; // comment out to disable verbose logging in JEST test runner
9
+ }
10
+ if (is_verbose) {
11
+ always_log("INFO: verbose logging enabled");
12
+ }
13
+ export function verbose_log(message, data) {
14
+ if (is_verbose) {
15
+ always_log(message, data);
16
+ }
17
+ }
18
+ export function always_log(message, data) {
19
+ // LOGGING NOTES:
20
+ // https://modelcontextprotocol.io/docs/tools/debugging#implementing-logging
21
+ // * STDIO transport => STDERR captured by host app (i.e. Claude Desktop which writes to ~/Library/Logs/Claude/mcp.log)
22
+ // * Streamable HTTP transport => suggests server side capture (i.e. log file)
23
+ // * and/or log to MCP client regardless of transport
24
+ // server.sendLoggingMessage({
25
+ // level: "info",
26
+ // data: message,
27
+ // });
28
+ // // sends a message like:
29
+ // server.notification({
30
+ // method: "notifications/message",
31
+ // params: {
32
+ // level: "warning",
33
+ // logger: "mcp-server-commands",
34
+ // data: "ListToolsRequest2",
35
+ // },
36
+ // });
37
+ if (isJest) {
38
+ // * JEST => log to console so I can see in test runner output
39
+ console.log(`${message}${data ? ": " + JSON.stringify(data) : ""}`);
40
+ return;
41
+ }
42
+ // * all transports => server side log file
43
+ const timestamp = new Date().toISOString();
44
+ const logMessage = `[${timestamp}] ${message}${data ? ": " + JSON.stringify(data) : ""}`;
45
+ const shareDir = process.env.HOME + "/.local/share/mcp-server-commands/";
46
+ const logFile = shareDir + "/commands.log";
47
+ fs.mkdirSync(shareDir, { recursive: true });
48
+ fs.appendFileSync(logFile, logMessage + "\n");
49
+ // TODO any hail mary if this logging fails? just use console.error then?
50
+ // else still can get seemingly hung tool calls
51
+ }
package/build/prompts.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { GetPromptRequestSchema, ListPromptsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
2
- import { verbose_log } from "./always_log.js";
2
+ import { verbose_log } from "./logging.js";
3
3
  import { exec } from "node:child_process";
4
4
  import { promisify } from "node:util";
5
5
  const execAsync = promisify(exec);
@@ -20,10 +20,8 @@ export function registerPrompts(server) {
20
20
  description: "Include command output in the prompt. " +
21
21
  "This is effectively a user tool call.",
22
22
  arguments: [
23
- { name: "mode", },
24
23
  { name: "command_line", },
25
24
  { name: "argv", },
26
- { name: "dry_run", },
27
25
  // ? other args?
28
26
  ],
29
27
  },
@@ -41,8 +39,6 @@ export function registerPrompts(server) {
41
39
  throw new Error("run_process not yet ported to prompts");
42
40
  const command_line = String(request.params.arguments?.command_line);
43
41
  const argv = String(request.params.arguments?.argv);
44
- const mode = String(request.params.arguments?.mode);
45
- const dry_run = Boolean(request.params.arguments?.dry_run);
46
42
  // Is it possible/feasible to pass a path for the workdir when running the command?
47
43
  // - currently it uses / (yikez)
48
44
  // - IMO makes more sense to have it be based on the Zed workdir of each project
package/build/tools.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import os from "os";
2
2
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
3
- import { verbose_log } from "./always_log.js";
4
- import { runProcess } from "./run_process.js";
5
- export function reisterTools(server) {
3
+ import { verbose_log } from "./logging.js";
4
+ import { runProcess } from "./exec-utils.js";
5
+ export function registerTools(server) {
6
6
  server.setRequestHandler(ListToolsRequestSchema, async () => {
7
7
  verbose_log("INFO: ListTools");
8
8
  return {
@@ -10,6 +10,8 @@ export function reisterTools(server) {
10
10
  // https://modelcontextprotocol.io/docs/learn/architecture#understanding-the-tool-execution-request // tool request/response
11
11
  // typescript SDK docs:
12
12
  // servers: https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/server.md
13
+ // TODO upgrade to newer version AND check if STDIO delimiter style has changed to include content-length "header" before responses?
14
+ // OR is there an opt-in for this style vs what I get with my simple nvim uv.spawn nvim client... where \n terminates/delimits each message
13
15
  tools: [
14
16
  {
15
17
  // TODO RUN_PROCESS MIGRATION! provide examples in system message, that way it is very clear how to use these!
@@ -19,23 +21,15 @@ export function reisterTools(server) {
19
21
  type: "object",
20
22
  properties: {
21
23
  // ListToolsResult => Tool type (in protocol) => https://modelcontextprotocol.io/specification/2025-06-18/schema#tool
22
- // * appears I can do w/e I want to describe properties (as long as give a string key (name)) => hence enum below
23
- mode: {
24
- enum: ["shell", "executable"], // * I made this up
25
- description: "What are you running, two choices: 'shell' or 'executable' (required, no default)",
26
- // FYI only use default system shell, if the model wants a different shell, can explicitly run it with command_line/argv
27
- // DO NOT duplicate the mechanism of specifying what runs by adding yet another executable field!
28
- // spawn's options.shell is bool/string... string is command_name/path... that duplicates and complicates needlessly!
29
- },
30
24
  command_line: {
31
25
  type: "string",
32
- description: "Shell command line. Required when mode='shell'. Forbidden when mode='executable'."
26
+ description: "Shell mode: a shell command line executed via the system's default shell. Supports pipes, redirects, globbing. Cannot be combined with 'argv'."
33
27
  },
34
28
  argv: {
35
29
  minItems: 1, // * made up too
36
30
  type: "array",
37
31
  items: { type: "string" },
38
- description: "Executable and arguments. argv[0] is the executable. Required when mode='executable'. Forbidden when mode='shell'."
32
+ description: "Executable mode: directly spawn a process. argv[0] is the executable, followed by arguments passed verbatim (no shell interpretation). Cannot be combined with 'command_line'."
39
33
  },
40
34
  cwd: {
41
35
  // or "workdir" like before? => eval model behavior w/ each name?
@@ -49,33 +43,10 @@ export function reisterTools(server) {
49
43
  timeout_ms: {
50
44
  type: "number",
51
45
  description: "Optional timeout in milliseconds, defaults to 30,000ms",
52
- },
53
- dry_run: {
54
- type: "boolean",
55
- description: "Optional explain what would run, defaults to False."
56
- // FYI this can help avoid the need for logging this information, I can call this as a user too!
57
- // TODO, dump info about shell (if applicable) and program versions!
58
- // - explain the node calls too, i.e. spawn('echo foo')
59
- // - show paths of resolved resources (i.e. shell)
60
- // - i.e. could run command --version to see its version (attempt various flags until one works, and/or have a lookup of common tools to know how to find their version)
61
46
  }
62
- // MAYBEs:
63
- // - env - obscure cases where command takes a param only via an env var?
64
47
  },
65
- required: ["mode"],
66
- // PRN add proprietary constraints explanation? only if model seems to struggle and there isn't a better fix...
67
- // oneOf: [
68
- // {
69
- // properties: { mode: { const: "shell" } },
70
- // required: ["command_line"],
71
- // not: { required: ["argv"] }
72
- // },
73
- // {
74
- // properties: { mode: { const: "executable" } },
75
- // required: ["argv"],
76
- // not: { required: ["command_line"] }
77
- // }
78
- // ]
48
+ // FYI no required arg top level and I am not gonna fret about specifiying one or the other, the tool definition is fine with that distinction in the descriptions, plus it is intuitive.
49
+ // and back when I had mode=shell/executable required, models would still forget to add it so why bother with a huge complexity in tool definition
79
50
  },
80
51
  },
81
52
  ],
@@ -85,7 +56,13 @@ export function reisterTools(server) {
85
56
  verbose_log("INFO: ToolRequest", request);
86
57
  switch (request.params.name) {
87
58
  case "run_process": {
88
- return await runProcess(request.params.arguments);
59
+ if (!request.params.arguments) {
60
+ throw new Error("Missing arguments for run_process");
61
+ }
62
+ const result = await runProcess(request.params.arguments);
63
+ // FYI logging this response is INVALUABLE! found a problem with my neovim MCP STDIO client!
64
+ verbose_log("INFO: ToolResponse", result);
65
+ return result;
89
66
  }
90
67
  default:
91
68
  throw new Error("Unknown tool");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-server-commands",
3
- "version": "0.7.4",
3
+ "version": "0.8.0",
4
4
  "description": "An MCP server to run arbitrary commands",
5
5
  "private": false,
6
6
  "type": "module",
@@ -1,52 +0,0 @@
1
- let verbose = false;
2
- // check CLI args:
3
- if (process.argv.includes("--verbose")) {
4
- verbose = true;
5
- }
6
- if (verbose) {
7
- always_log("INFO: verbose logging enabled");
8
- }
9
- export function verbose_log(message, data) {
10
- // https://modelcontextprotocol.io/docs/tools/debugging - mentions various ways to debug/troubleshoot (including dev tools)
11
- //
12
- // remember STDIO transport means can't log over STDOUT (client expects JSON messages per the spec)
13
- // https://modelcontextprotocol.io/docs/tools/debugging#implementing-logging
14
- // mentions STDERR is captured by the host app (i.e. Claude Desktop app)
15
- // server.sendLoggingMessage is captured by MCP client (not Claude Desktop app)
16
- // * SO, IIUC use STDERR for logging into Claude Desktop app logs in:
17
- // '~/Library/Logs/Claude/mcp.log'
18
- if (verbose) {
19
- always_log(message, data);
20
- }
21
- // inspector, catches these logs and shows them on left hand side of screen (sidebar)
22
- // TODO add verbose parameter (CLI arg?)
23
- // IF I wanted to log via MCP client logs (not sure what those are/do):
24
- // I do not see inspector catching these logs :(, there is a server notifications section and it remains empty
25
- //server.sendLoggingMessage({
26
- // level: "info",
27
- // data: message,
28
- //});
29
- // which results in something like:
30
- //server.notification({
31
- // method: "notifications/message",
32
- // params: {
33
- // level: "warning",
34
- // logger: "mcp-server-commands",
35
- // data: "ListToolsRequest2",
36
- // },
37
- //});
38
- //
39
- // FYI client should also requets a log level from the server, so that needs to be here at some point too
40
- }
41
- export function always_log(message, data) {
42
- const isJest = typeof process !== 'undefined' && !!process.env.JEST_WORKER_ID;
43
- if (isJest) {
44
- return;
45
- }
46
- if (data) {
47
- console.error(message + ": " + JSON.stringify(data));
48
- }
49
- else {
50
- console.error(message);
51
- }
52
- }
@@ -1,90 +0,0 @@
1
- import { spawn_wrapped } from "./exec-utils.js";
2
- import { always_log } from "./always_log.js";
3
- import { errorResult, messagesFor, resultFor } from "./messages.js";
4
- export async function runProcess(args) {
5
- const command_line = args?.command_line;
6
- const argv = args?.argv;
7
- const mode = String(args?.mode);
8
- if (!mode) {
9
- return errorResult("Mode is required");
10
- }
11
- const isShell = mode === "shell";
12
- const isExecutable = mode === "executable";
13
- if (!isShell && !isExecutable) {
14
- return errorResult(`Invalid mode '${mode}'. Allowed values are 'shell' or 'executable'.`);
15
- }
16
- // * shared args
17
- const spawn_options = {
18
- // spawn options: https://nodejs.org/api/child_process.html#child_processspawncommand-args-options
19
- encoding: "utf8"
20
- };
21
- if (args?.cwd) {
22
- spawn_options.cwd = String(args.cwd);
23
- }
24
- // PRN args.env
25
- if (args?.timeout_ms) {
26
- spawn_options.timeout = Number(args.timeout_ms);
27
- }
28
- else {
29
- // default timeout after 30s
30
- spawn_options.timeout = 30000;
31
- }
32
- // PRN windowsHide on Windows, signal, killSignal
33
- // FYI spawn_options.stdio => default is perfect ['pipe', 'pipe', 'pipe'] https://nodejs.org/api/child_process.html#optionsstdio
34
- // do not set inherit (this is what causes ripgrep to see STDIN socket and search it, thus hanging)
35
- const stdin = args?.stdin ? String(args.stdin) : undefined; // TODO
36
- const dryRun = Boolean(args?.dry_run);
37
- try {
38
- if (dryRun) {
39
- // Build a descriptive plan without executing anything
40
- let plan = '';
41
- if (isShell) {
42
- const cmd = String(args?.command_line);
43
- const shell = process.env.SHELL || (process.platform === 'win32' ? 'cmd.exe' : '/bin/sh');
44
- plan = `Shell mode: will execute command_line via ${shell}: ${cmd}`;
45
- }
46
- else if (isExecutable) {
47
- const argv = args?.argv;
48
- plan = `Executable mode: will spawn '${argv[0]}' with arguments ${JSON.stringify(argv.slice(1))}`;
49
- }
50
- return { content: [{ name: "PLAN", type: "text", text: plan }] };
51
- }
52
- if (isShell) {
53
- if (!args?.command_line) {
54
- return errorResult("Mode 'shell' requires a non‑empty 'command_line' parameter.");
55
- }
56
- if (args?.argv) {
57
- return errorResult("Mode 'shell' does not accept an 'argv' parameter.");
58
- }
59
- spawn_options.shell = true;
60
- const command_line = String(args?.command_line);
61
- const result = await spawn_wrapped(command_line, [], stdin, spawn_options);
62
- return resultFor(result);
63
- }
64
- if (isExecutable) {
65
- const argv = args?.argv;
66
- if (!Array.isArray(argv) || argv.length === 0) {
67
- return errorResult("Mode 'executable' requires a non‑empty 'argv' array.");
68
- }
69
- if (args?.command_line) {
70
- return errorResult("Mode 'executable' does not accept a 'command_line' parameter.");
71
- }
72
- spawn_options.shell = false;
73
- const command = argv[0];
74
- const commandArgs = argv.slice(1);
75
- const result = await spawn_wrapped(command, commandArgs, stdin, spawn_options);
76
- return resultFor(result);
77
- }
78
- // TODO... fish shell workaround (see exec-utils.ts) - ADD or FIX via a TEST FIRST
79
- return errorResult("Unexpected execution path in runProcess, should not be possible");
80
- }
81
- catch (error) {
82
- // TODO check if not SpawnFailure => i.e. test aborting/killing
83
- const response = {
84
- isError: true,
85
- content: messagesFor(error),
86
- };
87
- always_log("WARN: run_process failed", error);
88
- return response;
89
- }
90
- }