@tomsun28/pizza 0.0.1 → 0.0.4

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.
@@ -1,104 +0,0 @@
1
- /**
2
- * Custom convertToLlm that transforms tool call/result history into
3
- * [CLI]/[RESULT] text format that the model can understand.
4
- *
5
- * The model never sees native tool_use/tool_result messages.
6
- * Instead, its own [CLI] calls and our [RESULT] responses appear as
7
- * plain text in the conversation history.
8
- */
9
-
10
- import type { Message, AssistantMessage, ToolResultMessage, UserMessage } from "@mariozechner/pi-ai";
11
- import type { AgentMessage } from "@mariozechner/pi-agent-core";
12
- import { argsToCliString } from "./cli-stream.js";
13
-
14
- /**
15
- * Convert AgentMessage[] to LLM Message[].
16
- *
17
- * - User messages: pass through
18
- * - Assistant messages with toolCalls: reconstruct the [CLI] tag text
19
- * - ToolResult messages: convert to user messages with [RESULT] tags
20
- */
21
- export function convertToLlmWithCLI(messages: AgentMessage[]): Message[] {
22
- const result: Message[] = [];
23
- let pendingResults: ToolResultMessage[] = [];
24
-
25
- for (const msg of messages) {
26
- if (msg.role === "user") {
27
- // Flush any pending tool results first
28
- flushResults(pendingResults, result);
29
- pendingResults = [];
30
- result.push(msg as UserMessage);
31
- } else if (msg.role === "assistant") {
32
- // Flush any pending tool results first
33
- flushResults(pendingResults, result);
34
- pendingResults = [];
35
-
36
- const assistantMsg = msg as AssistantMessage;
37
- const textParts: string[] = [];
38
- const toolCalls = assistantMsg.content.filter(c => c.type === "toolCall");
39
- const textBlocks = assistantMsg.content.filter(c => c.type === "text");
40
-
41
- // Add text content
42
- for (const block of textBlocks) {
43
- if (block.type === "text" && block.text.trim()) {
44
- textParts.push(block.text);
45
- }
46
- }
47
-
48
- // Reconstruct [CLI] tags from tool calls
49
- for (const tc of toolCalls) {
50
- if (tc.type === "toolCall") {
51
- const cliArgs = argsToCliString(tc.name, tc.arguments as Record<string, any>);
52
- const inner = cliArgs ? `${tc.name} ${cliArgs}` : tc.name;
53
- textParts.push(`[CLI] ${inner}[/CLI]`);
54
- }
55
- }
56
-
57
- if (textParts.length > 0) {
58
- result.push({
59
- role: "assistant",
60
- content: [{ type: "text", text: textParts.join("\n\n") }],
61
- api: assistantMsg.api,
62
- provider: assistantMsg.provider,
63
- model: assistantMsg.model,
64
- usage: assistantMsg.usage,
65
- stopReason: toolCalls.length > 0 ? "stop" : assistantMsg.stopReason,
66
- timestamp: assistantMsg.timestamp,
67
- } as AssistantMessage);
68
- }
69
- } else if (msg.role === "toolResult") {
70
- pendingResults.push(msg as ToolResultMessage);
71
- }
72
- }
73
-
74
- // Flush remaining results
75
- flushResults(pendingResults, result);
76
-
77
- return result;
78
- }
79
-
80
- /**
81
- * Convert accumulated tool results into a single user message with [RESULT] tags.
82
- */
83
- function flushResults(results: ToolResultMessage[], output: Message[]): void {
84
- if (results.length === 0) return;
85
-
86
- const parts: string[] = [];
87
- for (let i = 0; i < results.length; i++) {
88
- const r = results[i];
89
- const text = r.content
90
- .filter(c => c.type === "text")
91
- .map(c => (c as any).text)
92
- .join("\n");
93
-
94
- const id = results.length > 1 ? ` id=${i + 1}` : "";
95
- const errorAttr = r.isError ? " error=true" : "";
96
- parts.push(`[RESULT${id}${errorAttr}]\n${text}\n[/RESULT]`);
97
- }
98
-
99
- output.push({
100
- role: "user",
101
- content: parts.join("\n\n"),
102
- timestamp: results[0].timestamp,
103
- } as UserMessage);
104
- }
package/src/cli-stream.ts DELETED
@@ -1,291 +0,0 @@
1
- /**
2
- * Custom stream function that bypasses LLM's native tool calling.
3
- *
4
- * New format: [CLI] toolName [args][/CLI]
5
- * Examples:
6
- * [CLI] ls .[/CLI]
7
- * [CLI] read src/main.ts[/CLI]
8
- * [CLI] write --path "file.ts" --content "hello"[/CLI]
9
- * [CLI] bash git status[/CLI]
10
- */
11
-
12
- import {
13
- type AssistantMessage,
14
- type AssistantMessageEventStream,
15
- createAssistantMessageEventStream,
16
- type Context,
17
- type Model,
18
- type SimpleStreamOptions,
19
- streamSimple,
20
- type ToolCall,
21
- } from "@mariozechner/pi-ai";
22
-
23
- interface ParsedCLICall {
24
- toolName: string;
25
- rawArgs: string;
26
- }
27
-
28
- /**
29
- * Parse [CLI] toolName args[/CLI] tags from text.
30
- */
31
- function parseCLICalls(text: string): { calls: ParsedCLICall[]; textBeforeCalls: string } {
32
- const regex = /\[CLI\]([\s\S]*?)\[\/CLI\]/g;
33
- const calls: ParsedCLICall[] = [];
34
- let firstMatchStart = -1;
35
-
36
- let match: RegExpExecArray | null;
37
- while ((match = regex.exec(text)) !== null) {
38
- if (firstMatchStart === -1) firstMatchStart = match.index;
39
- const inner = match[1].trim();
40
- const spaceIdx = inner.search(/\s/);
41
- const toolName = spaceIdx === -1 ? inner : inner.slice(0, spaceIdx);
42
- const rawArgs = spaceIdx === -1 ? "" : inner.slice(spaceIdx + 1).trim();
43
- if (toolName) {
44
- calls.push({ toolName, rawArgs });
45
- }
46
- }
47
-
48
- const textBeforeCalls = firstMatchStart >= 0 ? text.substring(0, firstMatchStart).trimEnd() : text;
49
- return { calls, textBeforeCalls };
50
- }
51
-
52
- /**
53
- * Parse shell-style args string into a Record.
54
- *
55
- * Handles:
56
- * --key "value" -> { key: "value" }
57
- * --key value -> { key: "value" }
58
- * --flag -> { flag: true }
59
- * positional -> first positional goes to "path", rest to "_args"
60
- *
61
- * Quoted strings support escaped quotes inside.
62
- */
63
- export function parseCliArgs(raw: string): Record<string, any> {
64
- const args: Record<string, any> = {};
65
- const positional: string[] = [];
66
-
67
- // Tokenize respecting quotes
68
- const tokens: string[] = [];
69
- let i = 0;
70
- while (i < raw.length) {
71
- // Skip whitespace
72
- while (i < raw.length && /\s/.test(raw[i])) i++;
73
- if (i >= raw.length) break;
74
-
75
- if (raw[i] === '"' || raw[i] === "'") {
76
- // Quoted token
77
- const quote = raw[i++];
78
- let val = "";
79
- while (i < raw.length && raw[i] !== quote) {
80
- if (raw[i] === "\\" && i + 1 < raw.length) {
81
- i++;
82
- val += raw[i++];
83
- } else {
84
- val += raw[i++];
85
- }
86
- }
87
- i++; // closing quote
88
- tokens.push(val);
89
- } else {
90
- // Unquoted token — read until whitespace
91
- let val = "";
92
- while (i < raw.length && !/\s/.test(raw[i])) {
93
- val += raw[i++];
94
- }
95
- tokens.push(val);
96
- }
97
- }
98
-
99
- // Parse tokens into args
100
- let ti = 0;
101
- while (ti < tokens.length) {
102
- const tok = tokens[ti];
103
- if (tok.startsWith("--")) {
104
- const key = tok.slice(2);
105
- // Next token is value if it doesn't start with --
106
- if (ti + 1 < tokens.length && !tokens[ti + 1].startsWith("--")) {
107
- args[key] = tokens[ti + 1];
108
- ti += 2;
109
- } else {
110
- args[key] = true;
111
- ti++;
112
- }
113
- } else {
114
- positional.push(tok);
115
- ti++;
116
- }
117
- }
118
-
119
- // Map positional args to tool params
120
- if (positional.length > 0) {
121
- // First positional is usually a path or command
122
- if (!("path" in args) && !("command" in args) && !("pattern" in args)) {
123
- args._positional = positional.join(" ");
124
- }
125
- }
126
-
127
- return args;
128
- }
129
-
130
- /**
131
- * Map parsed args to tool-specific argument schema.
132
- * Each tool has a known primary positional argument.
133
- */
134
- function mapArgsToTool(toolName: string, rawArgs: string): Record<string, any> {
135
- const args = parseCliArgs(rawArgs);
136
- const pos = args._positional as string | undefined;
137
- delete args._positional;
138
-
139
- switch (toolName) {
140
- case "ls":
141
- if (pos && !args.path) args.path = pos;
142
- break;
143
- case "read":
144
- if (pos && !args.path) args.path = pos;
145
- break;
146
- case "write":
147
- // --path and --content are required; no positional shorthand
148
- break;
149
- case "edit":
150
- // --path, --old, --new
151
- if (args.old) { args.oldText = args.old; delete args.old; }
152
- if (args.new) { args.newText = args.new; delete args.new; }
153
- if (pos && !args.path) args.path = pos;
154
- break;
155
- case "grep":
156
- if (pos && !args.pattern) args.pattern = pos;
157
- break;
158
- case "bash":
159
- // Everything is the command
160
- if (rawArgs && !args.command) args.command = rawArgs;
161
- break;
162
- default:
163
- if (pos) args.input = pos;
164
- }
165
-
166
- return args;
167
- }
168
-
169
- /**
170
- * Convert tool call arguments back to CLI flag string for history reconstruction.
171
- */
172
- export function argsToCliString(toolName: string, arguments_: Record<string, any>): string {
173
- if (toolName === "bash") {
174
- return (arguments_.command as string) ?? "";
175
- }
176
- if (toolName === "ls" || toolName === "read") {
177
- const path = arguments_.path;
178
- const rest = Object.entries(arguments_)
179
- .filter(([k]) => k !== "path")
180
- .map(([k, v]) => `--${k} "${String(v).replace(/"/g, '\\"')}"`)
181
- .join(" ");
182
- return path ? (rest ? `${path} ${rest}` : path) : rest;
183
- }
184
- // Generic: all args as --key "value"
185
- return Object.entries(arguments_)
186
- .map(([k, v]) => {
187
- if (v === true) return `--${k}`;
188
- const s = String(v);
189
- // Quote if contains spaces or special chars
190
- return s.includes(" ") || s.includes('"') || s.includes("\n")
191
- ? `--${k} "${s.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`
192
- : `--${k} ${s}`;
193
- })
194
- .join(" ");
195
- }
196
-
197
- let callIdCounter = 0;
198
- function nextCallId(): string {
199
- return `cli_${Date.now()}_${callIdCounter++}`;
200
- }
201
-
202
- export function createCLIStreamFn() {
203
- return function cliStreamFn(
204
- model: Model<any>,
205
- context: Context,
206
- options?: SimpleStreamOptions,
207
- ): AssistantMessageEventStream {
208
- const output = createAssistantMessageEventStream();
209
-
210
- void (async () => {
211
- try {
212
- const contextWithoutTools: Context = { ...context, tools: undefined };
213
- const upstream = streamSimple(model, contextWithoutTools, options);
214
- let fullText = "";
215
-
216
- for await (const event of upstream) {
217
- if (event.type === "text_delta") {
218
- fullText += event.delta;
219
- }
220
-
221
- if (
222
- event.type === "start" ||
223
- event.type === "text_start" ||
224
- event.type === "text_delta" ||
225
- event.type === "text_end" ||
226
- event.type === "thinking_start" ||
227
- event.type === "thinking_delta" ||
228
- event.type === "thinking_end"
229
- ) {
230
- output.push(event);
231
- continue;
232
- }
233
-
234
- if (event.type === "error") {
235
- output.push(event);
236
- continue;
237
- }
238
-
239
- if (event.type === "done") {
240
- const { calls, textBeforeCalls } = parseCLICalls(fullText);
241
-
242
- if (calls.length === 0) {
243
- output.push(event);
244
- } else {
245
- const toolCalls: ToolCall[] = calls.map((call) => ({
246
- type: "toolCall",
247
- id: nextCallId(),
248
- name: call.toolName,
249
- arguments: mapArgsToTool(call.toolName, call.rawArgs),
250
- }));
251
-
252
- const rewritten: AssistantMessage = {
253
- ...event.message,
254
- content: [
255
- ...(textBeforeCalls ? [{ type: "text" as const, text: textBeforeCalls }] : []),
256
- ...toolCalls,
257
- ],
258
- stopReason: "toolUse",
259
- };
260
-
261
- for (let i = 0; i < toolCalls.length; i++) {
262
- const tc = toolCalls[i];
263
- const contentIndex = (textBeforeCalls ? 1 : 0) + i;
264
- output.push({ type: "toolcall_start", contentIndex, partial: { ...rewritten } });
265
- output.push({ type: "toolcall_end", contentIndex, toolCall: tc, partial: { ...rewritten } });
266
- }
267
-
268
- output.push({ type: "done", reason: "toolUse", message: rewritten });
269
- }
270
- continue;
271
- }
272
- }
273
- } catch (err: any) {
274
- const errorMessage: AssistantMessage = {
275
- role: "assistant",
276
- content: [{ type: "text", text: "" }],
277
- api: model.api,
278
- provider: model.provider,
279
- model: model.id,
280
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
281
- stopReason: "error",
282
- errorMessage: err.message || String(err),
283
- timestamp: Date.now(),
284
- };
285
- output.push({ type: "error", reason: "error", error: errorMessage });
286
- }
287
- })();
288
-
289
- return output;
290
- };
291
- }
package/src/main.ts DELETED
@@ -1,103 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { createInterface } from "node:readline";
4
- import { createPizzaAgent } from "./agent.js";
5
- import { createRenderer } from "./ui/render.js";
6
-
7
- // ANSI helpers
8
- const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
9
- const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
10
- const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
11
- const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
12
-
13
- // Parse CLI args
14
- function parseArgs(): { provider?: string; model?: string; apiKey?: string } {
15
- const args = process.argv.slice(2);
16
- const result: Record<string, string> = {};
17
-
18
- for (let i = 0; i < args.length; i++) {
19
- if (args[i] === "--provider" && args[i + 1]) {
20
- result.provider = args[++i];
21
- } else if (args[i] === "--model" && args[i + 1]) {
22
- result.model = args[++i];
23
- } else if (args[i] === "--api-key" && args[i + 1]) {
24
- result.apiKey = args[++i];
25
- } else if (args[i] === "--help" || args[i] === "-h") {
26
- console.log(`
27
- ${bold("pizza")} - A minimal coding agent
28
-
29
- ${bold("Usage:")}
30
- pizza [options]
31
-
32
- ${bold("Options:")}
33
- --provider <name> LLM provider (default: zai)
34
- --model <id> Model ID (default: glm-4.5)
35
- --api-key <key> API key (default: from env)
36
- -h, --help Show this help
37
-
38
- ${bold("Environment:")}
39
- ZAI_API_KEY ZAI API key
40
- ANTHROPIC_API_KEY Anthropic API key
41
- OPENAI_API_KEY OpenAI API key
42
- `);
43
- process.exit(0);
44
- }
45
- }
46
-
47
- return result;
48
- }
49
-
50
- async function main() {
51
- const args = parseArgs();
52
-
53
- console.log(`\n${bold(green("🍕 pizza"))} ${dim("coding agent")}`);
54
- console.log(dim(`provider: ${args.provider ?? "zai"} | model: ${args.model ?? "glm-4.7"}`));
55
- console.log(dim(`cwd: ${process.cwd()}`));
56
- console.log(dim(`Type your message, or "exit" to quit.\n`));
57
-
58
- const { agent, model } = createPizzaAgent({
59
- provider: args.provider,
60
- model: args.model,
61
- apiKey: args.apiKey,
62
- });
63
-
64
- const render = createRenderer();
65
- agent.subscribe(render);
66
-
67
- const rl = createInterface({
68
- input: process.stdin,
69
- output: process.stdout,
70
- });
71
-
72
- const prompt = () => {
73
- rl.question(`${cyan(">")} `, async (input) => {
74
- const trimmed = input.trim();
75
-
76
- if (!trimmed) {
77
- prompt();
78
- return;
79
- }
80
-
81
- if (trimmed === "exit" || trimmed === "quit" || trimmed === "/exit" || trimmed === "/quit") {
82
- console.log(dim("\nBye!"));
83
- rl.close();
84
- process.exit(0);
85
- }
86
-
87
- try {
88
- await agent.prompt(trimmed);
89
- } catch (err: any) {
90
- console.error(`\n\x1b[31mError: ${err.message}\x1b[0m\n`);
91
- }
92
-
93
- prompt();
94
- });
95
- };
96
-
97
- prompt();
98
- }
99
-
100
- main().catch((err) => {
101
- console.error("Fatal:", err);
102
- process.exit(1);
103
- });
@@ -1,94 +0,0 @@
1
- export function buildSystemPrompt(cwd: string): string {
2
- const date = new Date().toISOString().slice(0, 10);
3
-
4
- return [
5
- `You are pizza, a coding assistant running in the user's terminal. CLI is all you need.`,
6
- ``,
7
- `When you need to perform an action, wrap a CLI command in [CLI] ... [/CLI] tags. You can use multiple tags in one response. Results are returned in [RESULT]...[/RESULT] tags.`,
8
- ``,
9
- `## Available Commands`,
10
- ``,
11
- `[CLI] ls <path>[/CLI]`,
12
- `[CLI] read <path>[/CLI]`,
13
- `[CLI] read <path> --offset <n> --limit <n>[/CLI]`,
14
- `[CLI] write --path <path> --content <content>[/CLI]`,
15
- `[CLI] edit --path <path> --old <oldText> --new <newText>[/CLI]`,
16
- `[CLI] grep <pattern> --path <dir> --include <glob>[/CLI]`,
17
- `[CLI] bash <any shell command>[/CLI]`,
18
- ``,
19
- `## Examples`,
20
- ``,
21
- `User: What files are in this project?`,
22
- ``,
23
- `Assistant: Let me check.`,
24
- ``,
25
- `[CLI] ls .[/CLI]`,
26
- ``,
27
- `[RESULT]`,
28
- `src/`,
29
- `package.json`,
30
- `tsconfig.json`,
31
- `[/RESULT]`,
32
- ``,
33
- `The project has a src/ directory, package.json, and tsconfig.json.`,
34
- ``,
35
- `---`,
36
- ``,
37
- `User: Read package.json and list src/`,
38
- ``,
39
- `Assistant: I'll do both at once.`,
40
- ``,
41
- `[CLI] read package.json[/CLI]`,
42
- `[CLI] ls src[/CLI]`,
43
- ``,
44
- `[RESULT id=1]`,
45
- `{"name":"my-app","version":"1.0.0"}`,
46
- `[/RESULT]`,
47
- ``,
48
- `[RESULT id=2]`,
49
- `index.ts`,
50
- `utils.ts`,
51
- `[/RESULT]`,
52
- ``,
53
- `The project is called "my-app". src/ contains index.ts and utils.ts.`,
54
- ``,
55
- `---`,
56
- ``,
57
- `User: Add a hello function to src/utils.ts`,
58
- ``,
59
- `Assistant: Let me read the file first.`,
60
- ``,
61
- `[CLI] read src/utils.ts[/CLI]`,
62
- ``,
63
- `[RESULT]`,
64
- `export function add(a: number, b: number): number {`,
65
- ` return a + b;`,
66
- `}`,
67
- `[/RESULT]`,
68
- ``,
69
- `[CLI] edit --path src/utils.ts --old "return a + b;
70
- }" --new "return a + b;
71
- }
72
-
73
- export function hello(name: string): string {
74
- return "Hello, " + name;
75
- }"[/CLI]`,
76
- ``,
77
- `[RESULT]`,
78
- `File edited successfully.`,
79
- `[/RESULT]`,
80
- ``,
81
- `Done. Added a \`hello\` function to src/utils.ts.`,
82
- ``,
83
- `---`,
84
- ``,
85
- `User: Run git status and install deps`,
86
- ``,
87
- `Assistant:`,
88
- ``,
89
- `[CLI] bash git status[/CLI]`,
90
- `[CLI] bash npm install[/CLI]`,
91
- `Current directory: ${cwd}`,
92
- `Date: ${date}`,
93
- ].join("\n");
94
- }
package/src/tools/bash.ts DELETED
@@ -1,124 +0,0 @@
1
- import type { AgentTool } from "@mariozechner/pi-agent-core";
2
- import { Type } from "@sinclair/typebox";
3
- import { spawn } from "node:child_process";
4
-
5
- const MAX_OUTPUT_BYTES = 256 * 1024;
6
- const MAX_OUTPUT_LINES = 2000;
7
-
8
- const schema = Type.Object({
9
- command: Type.String({ description: "Bash command to execute" }),
10
- timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional)" })),
11
- });
12
-
13
- export function createBashTool(cwd: string): AgentTool<typeof schema> {
14
- return {
15
- name: "bash",
16
- label: "bash",
17
- description: `Execute a bash command. Returns stdout and stderr. Output truncated to ${MAX_OUTPUT_LINES} lines or ${MAX_OUTPUT_BYTES / 1024}KB.`,
18
- parameters: schema,
19
- async execute(_toolCallId, { command, timeout }, signal, onUpdate) {
20
- return new Promise((resolve, reject) => {
21
- const shell = process.env.SHELL || "/bin/bash";
22
- const child = spawn(shell, ["-c", command], {
23
- cwd,
24
- stdio: ["ignore", "pipe", "pipe"],
25
- env: { ...process.env },
26
- });
27
-
28
- const chunks: Buffer[] = [];
29
- let totalBytes = 0;
30
- let timedOut = false;
31
- let timeoutHandle: NodeJS.Timeout | undefined;
32
-
33
- if (timeout !== undefined && timeout > 0) {
34
- timeoutHandle = setTimeout(() => {
35
- timedOut = true;
36
- child.kill("SIGTERM");
37
- setTimeout(() => child.kill("SIGKILL"), 2000);
38
- }, timeout * 1000);
39
- }
40
-
41
- const handleData = (data: Buffer) => {
42
- chunks.push(data);
43
- totalBytes += data.length;
44
-
45
- if (onUpdate) {
46
- const text = Buffer.concat(chunks).toString("utf-8");
47
- const truncated = truncateOutput(text);
48
- onUpdate({
49
- content: [{ type: "text", text: truncated }],
50
- details: {},
51
- });
52
- }
53
- };
54
-
55
- child.stdout?.on("data", handleData);
56
- child.stderr?.on("data", handleData);
57
-
58
- const onAbort = () => {
59
- child.kill("SIGTERM");
60
- setTimeout(() => child.kill("SIGKILL"), 2000);
61
- };
62
-
63
- if (signal) {
64
- if (signal.aborted) {
65
- onAbort();
66
- } else {
67
- signal.addEventListener("abort", onAbort, { once: true });
68
- }
69
- }
70
-
71
- child.on("close", (code) => {
72
- if (timeoutHandle) clearTimeout(timeoutHandle);
73
- if (signal) signal.removeEventListener("abort", onAbort);
74
-
75
- const output = Buffer.concat(chunks).toString("utf-8");
76
- const truncated = truncateOutput(output);
77
-
78
- if (signal?.aborted) {
79
- reject(new Error(truncated ? `${truncated}\n\nCommand aborted` : "Command aborted"));
80
- return;
81
- }
82
-
83
- if (timedOut) {
84
- reject(new Error(truncated ? `${truncated}\n\nCommand timed out after ${timeout}s` : `Command timed out after ${timeout}s`));
85
- return;
86
- }
87
-
88
- if (code !== 0 && code !== null) {
89
- reject(new Error(`${truncated || "(no output)"}\n\nExit code ${code}`));
90
- return;
91
- }
92
-
93
- resolve({
94
- content: [{ type: "text", text: truncated || "(no output)" }],
95
- details: {},
96
- });
97
- });
98
-
99
- child.on("error", (err) => {
100
- if (timeoutHandle) clearTimeout(timeoutHandle);
101
- if (signal) signal.removeEventListener("abort", onAbort);
102
- reject(err);
103
- });
104
- });
105
- },
106
- };
107
- }
108
-
109
- function truncateOutput(text: string): string {
110
- const lines = text.split("\n");
111
-
112
- if (lines.length > MAX_OUTPUT_LINES) {
113
- const kept = lines.slice(lines.length - MAX_OUTPUT_LINES);
114
- const skipped = lines.length - MAX_OUTPUT_LINES;
115
- return `[${skipped} lines truncated]\n${kept.join("\n")}`;
116
- }
117
-
118
- if (Buffer.byteLength(text, "utf-8") > MAX_OUTPUT_BYTES) {
119
- const buf = Buffer.from(text, "utf-8");
120
- return buf.subarray(buf.length - MAX_OUTPUT_BYTES).toString("utf-8");
121
- }
122
-
123
- return text;
124
- }