summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src/tools/run-shell.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-04 21:21:20 +0900
committerAdam Malczewski <[email protected]>2026-06-04 21:21:20 +0900
commit394f1ed37ce860da6fdc385769bf29f9737105cd (patch)
tree4b825dc642cb6eb9a060e54bf8d69288fbee4904 /packages/core/src/tools/run-shell.ts
parent81a9cdbadf8c9d940d4fe9a2a0de607dee1f5f1a (diff)
downloaddispatch-394f1ed37ce860da6fdc385769bf29f9737105cd.tar.gz
dispatch-394f1ed37ce860da6fdc385769bf29f9737105cd.zip
chore: genesis — remove all files to rebuild from scratch (arch rewrite)
Diffstat (limited to 'packages/core/src/tools/run-shell.ts')
-rw-r--r--packages/core/src/tools/run-shell.ts176
1 files changed, 0 insertions, 176 deletions
diff --git a/packages/core/src/tools/run-shell.ts b/packages/core/src/tools/run-shell.ts
deleted file mode 100644
index ec2db9c..0000000
--- a/packages/core/src/tools/run-shell.ts
+++ /dev/null
@@ -1,176 +0,0 @@
-import { spawn } from "node:child_process";
-import { randomUUID } from "node:crypto";
-import { z } from "zod";
-import type { ToolDefinition, ToolExecuteContext } from "../types/index.js";
-
-const DEFAULT_TIMEOUT = 2 * 60 * 1000; // 2 minutes
-
-export interface BackgroundShellJob {
- command: string;
- stdout: string;
- stderr: string;
- /** Resolves when the process exits */
- completion: Promise<{ stdout: string; stderr: string; exitCode: number; error?: string }>;
-}
-
-/** Shared store for shell commands that were backgrounded due to user interrupt */
-export class BackgroundShellStore {
- private jobs = new Map<string, BackgroundShellJob>();
-
- register(job: BackgroundShellJob): string {
- const id = `run_shell_${randomUUID()}`;
- this.jobs.set(id, job);
- // Auto-cleanup after completion + 10 minutes
- job.completion.finally(() => {
- setTimeout(() => this.jobs.delete(id), 10 * 60 * 1000);
- });
- return id;
- }
-
- async getResult(
- id: string,
- ): Promise<{ status: "done"; result: string } | { status: "error"; error: string }> {
- const job = this.jobs.get(id);
- if (!job) {
- return { status: "error", error: `No background shell job found with id '${id}'` };
- }
- const result = await job.completion;
- return { status: "done", result: JSON.stringify(result) };
- }
-
- has(id: string): boolean {
- return this.jobs.has(id);
- }
-}
-
-export function createRunShellTool(
- workingDirectory: string,
- shellStore?: BackgroundShellStore,
-): ToolDefinition {
- return {
- name: "run_shell",
- description:
- "Execute a shell command in the working directory. Returns stdout, stderr, and exit code. Use for running tests, builds, git operations, package management, and other development tasks. If the user interrupts while a command is running, the command continues in the background and you receive a job ID. Use the retrieve tool with that ID to get the result later.",
- parameters: z.object({
- command: z.string().describe("The shell command to execute"),
- timeout: z.number().optional().describe("Timeout in milliseconds (default 2 minutes)"),
- background: z
- .boolean()
- .optional()
- .describe(
- "If true, the command starts in the background and a job_id is returned immediately. Use the retrieve tool with the job_id to get the result later.",
- ),
- }),
- execute: async (
- args: Record<string, unknown>,
- context?: ToolExecuteContext,
- ): Promise<string> => {
- const command = args.command as string;
- const timeout = (args.timeout as number | undefined) ?? DEFAULT_TIMEOUT;
- const background = (args.background as boolean | undefined) ?? false;
-
- const [shell, shellArgs] = getShell();
- const child = spawn(shell, [...shellArgs, command], {
- cwd: workingDirectory,
- env: process.env,
- timeout,
- stdio: ["ignore", "pipe", "pipe"],
- });
-
- let stdout = "";
- let stderr = "";
-
- const completionPromise = new Promise<{
- stdout: string;
- stderr: string;
- exitCode: number;
- error?: string;
- }>((resolve) => {
- child.stdout?.on("data", (data: Buffer) => {
- const chunk = data.toString();
- stdout += chunk;
- context?.onOutput?.(chunk, "stdout");
- });
- child.stderr?.on("data", (data: Buffer) => {
- const chunk = data.toString();
- stderr += chunk;
- context?.onOutput?.(chunk, "stderr");
- });
-
- child.on("close", (exitCode) => {
- resolve({ stdout, stderr, exitCode: exitCode ?? 1 });
- });
-
- child.on("error", (err) => {
- resolve({ stdout, stderr, exitCode: 1, error: err.message });
- });
- });
-
- // If background mode requested, register immediately and return job ID
- if (background && shellStore) {
- const jobId = shellStore.register({
- command,
- stdout,
- stderr,
- completion: completionPromise,
- });
- return [
- `Command started in background.`,
- `job_id: ${jobId}`,
- `command: ${command}`,
- ``,
- `Use the retrieve tool with this job_id to get the result when ready.`,
- ].join("\n");
- }
-
- const queueCallbacks = context?.queueCallbacks;
-
- if (queueCallbacks && shellStore) {
- const { promise: queuePromise, cancel: cancelQueueWait } =
- queueCallbacks.waitForQueuedMessage();
- const queueSignal = queuePromise.then(() => "QUEUE_INTERRUPT" as const);
-
- const raceResult = await Promise.race([completionPromise, queueSignal]);
-
- if (raceResult === "QUEUE_INTERRUPT") {
- // Background the still-running process
- const jobId = shellStore.register({
- command,
- stdout,
- stderr,
- completion: completionPromise,
- });
-
- const queuedMsgs = queueCallbacks.dequeueMessages();
- const userMessages = queuedMsgs.map((m) => m.message).join("\n---\n");
-
- return [
- `Command backgrounded — still running.`,
- `job_id: ${jobId}`,
- `command: ${command}`,
- `stdout so far: ${stdout.slice(-500) || "(none)"}`,
- `stderr so far: ${stderr.slice(-500) || "(none)"}`,
- ``,
- `Use the retrieve tool with this job_id to get the final result when ready.`,
- ``,
- `[USER INTERRUPT]`,
- `The user has sent you message(s) while you were working. You MUST address these before continuing with your current task:`,
- ``,
- userMessages,
- ].join("\n");
- }
-
- // Command finished before interrupt — clean up queue listener
- cancelQueueWait();
- return JSON.stringify(raceResult);
- }
-
- const result = await completionPromise;
- return JSON.stringify(result);
- },
- };
-}
-
-function getShell(): [string, string[]] {
- return process.platform === "win32" ? ["powershell", ["-Command"]] : ["bash", ["-c"]];
-}