-
Notifications
You must be signed in to change notification settings - Fork 61
Expand file tree
/
Copy pathcli.ts
More file actions
108 lines (91 loc) · 2.78 KB
/
cli.ts
File metadata and controls
108 lines (91 loc) · 2.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import { spawn } from "child_process";
import type { AgentTool, AgentToolUpdateCallback } from "@mariozechner/pi-agent-core";
import { cliSchema, MAX_OUTPUT, DEFAULT_TIMEOUT } from "./shared.js";
export function createCliTool(cwd: string, defaultTimeout?: number): AgentTool<typeof cliSchema> {
const baseTimeout = defaultTimeout ?? DEFAULT_TIMEOUT;
return {
name: "cli",
label: "cli",
description:
"Execute a shell command. Returns stdout and stderr combined. Output is truncated if it exceeds ~100KB. Default timeout is 120 seconds.",
parameters: cliSchema,
execute: async (
_toolCallId: string,
{ command, timeout }: { command: string; timeout?: number },
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback,
) => {
const timeoutSecs = timeout ?? baseTimeout;
return new Promise((resolve, reject) => {
if (signal?.aborted) {
reject(new Error("Operation aborted"));
return;
}
const isWin = process.platform === "win32";
const child = spawn(isWin ? "cmd" : "sh", isWin ? ["/c", command] : ["-c", command], {
cwd,
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env },
});
let output = "";
let timedOut = false;
const timeoutHandle = setTimeout(() => {
timedOut = true;
child.kill("SIGTERM");
}, timeoutSecs * 1000);
const onData = (data: Buffer) => {
output += data.toString("utf-8");
if (onUpdate && output.length <= MAX_OUTPUT) {
onUpdate({
content: [{ type: "text", text: output }],
details: undefined,
});
}
};
child.stdout?.on("data", onData);
child.stderr?.on("data", onData);
const onAbort = () => {
child.kill("SIGTERM");
};
if (signal) {
signal.addEventListener("abort", onAbort, { once: true });
}
child.on("error", (err) => {
clearTimeout(timeoutHandle);
if (signal) signal.removeEventListener("abort", onAbort);
reject(err);
});
child.on("close", (code) => {
clearTimeout(timeoutHandle);
if (signal) signal.removeEventListener("abort", onAbort);
if (signal?.aborted) {
reject(new Error("Operation aborted"));
return;
}
if (timedOut) {
reject(new Error(`Command timed out after ${timeoutSecs} seconds\n${output}`));
return;
}
// Truncate if needed
let text = output;
if (text.length > MAX_OUTPUT) {
text = text.slice(-MAX_OUTPUT);
text = `[output truncated, showing last ~100KB]\n${text}`;
}
if (!text) {
text = "(no output)";
}
if (code !== 0 && code !== null) {
text += `\n\nExit code: ${code}`;
reject(new Error(text));
} else {
resolve({
content: [{ type: "text", text }],
details: { exitCode: code },
});
}
});
});
},
};
}