Files
CodeNomad/packages/opencode-config/plugin/lib/background-process.ts
2025-12-24 01:07:56 +00:00

161 lines
5.4 KiB
TypeScript

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 <T>(path: string, init?: RequestInit): Promise<T> => {
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<BackgroundProcess>("", {
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<BackgroundProcess>(`/${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<void>(`/${args.id}/terminate`, { method: "POST" })
return `Terminated background process ${args.id} and removed its output.`
},
}),
}
}
function normalizeHeaders(headers: HeadersInit | undefined): Record<string, string> {
const output: Record<string, string> = {}
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 }
}