import { tool } from "@opencode-ai/plugin/tool" type BackgroundProcess = { id: string title: string command: string status: "running" | "stopped" | "error" startedAt: string stoppedAt?: string exitCode?: number outputSizeBytes?: number } type CodeNomadConfig = { instanceId: string baseUrl: string } export function createBackgroundProcessTools(config: CodeNomadConfig) { const request = async (path: string, init?: RequestInit): Promise => { const base = config.baseUrl.replace(/\/+$/, "") const url = `${base}/workspaces/${config.instanceId}/plugin/background-processes${path}` const headers = normalizeHeaders(init?.headers) if (init?.body !== undefined) { headers["Content-Type"] = "application/json" } const response = await fetch(url, { ...init, headers, }) if (!response.ok) { const message = await response.text() throw new Error(message || `Request failed with ${response.status}`) } if (response.status === 204) { return undefined as T } return (await response.json()) as T } return { run_background_process: tool({ description: "Run a long-lived background process (dev servers, DBs, watchers) so it keeps running while you do other tasks. Use it for running processes that timeout otherwise or produce a lot of output.", args: { title: tool.schema.string().describe("Short label for the process (e.g. Dev server, DB server)"), command: tool.schema.string().describe("Shell command to run in the workspace"), }, async execute(args) { const process = await request("", { method: "POST", body: JSON.stringify({ title: args.title, command: args.command }), }) return `Started background process ${process.id} (${process.title})\nStatus: ${process.status}\nCommand: ${process.command}` }, }), list_background_processes: tool({ description: "List background processes running for this workspace.", args: {}, async execute() { const response = await request<{ processes: BackgroundProcess[] }>("") if (response.processes.length === 0) { return "No background processes running." } return response.processes .map((process) => { const status = process.status === "running" ? "running" : process.status const exit = process.exitCode !== undefined ? ` (exit ${process.exitCode})` : "" const size = typeof process.outputSizeBytes === "number" ? ` | ${Math.round(process.outputSizeBytes / 1024)}KB` : "" return `- ${process.id} | ${process.title} | ${status}${exit}${size}\n ${process.command}` }) .join("\n") }, }), read_background_process_output: tool({ description: "Read output from a background process. Use full, grep, head, or tail.", args: { id: tool.schema.string().describe("Background process ID"), method: tool.schema .enum(["full", "grep", "head", "tail"]) .default("full") .describe("Method to read output"), pattern: tool.schema.string().optional().describe("Pattern for grep method"), lines: tool.schema.number().optional().describe("Number of lines for head/tail methods"), }, async execute(args) { if (args.method === "grep" && !args.pattern) { return "Pattern is required for grep method." } const params = new URLSearchParams({ method: args.method }) if (args.pattern) { params.set("pattern", args.pattern) } if (args.lines) { params.set("lines", String(args.lines)) } const response = await request<{ id: string; content: string; truncated: boolean; sizeBytes: number }>( `/${args.id}/output?${params.toString()}`, ) const header = response.truncated ? `Output (truncated, ${Math.round(response.sizeBytes / 1024)}KB):` : `Output (${Math.round(response.sizeBytes / 1024)}KB):` return `${header}\n\n${response.content}` }, }), stop_background_process: tool({ description: "Stop a background process (SIGTERM) but keep its output and entry.", args: { id: tool.schema.string().describe("Background process ID"), }, async execute(args) { const process = await request(`/${args.id}/stop`, { method: "POST" }) return `Stopped background process ${process.id} (${process.title}). Status: ${process.status}` }, }), terminate_background_process: tool({ description: "Terminate a background process and delete its output + entry.", args: { id: tool.schema.string().describe("Background process ID"), }, async execute(args) { await request(`/${args.id}/terminate`, { method: "POST" }) return `Terminated background process ${args.id} and removed its output.` }, }), } } function normalizeHeaders(headers: HeadersInit | undefined): Record { const output: Record = {} if (!headers) return output if (headers instanceof Headers) { headers.forEach((value, key) => { output[key] = value }) return output } if (Array.isArray(headers)) { for (const [key, value] of headers) { output[key] = value } return output } return { ...headers } }