@tomsun28/pizza 0.0.1
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.
- package/.claude/settings.local.json +14 -0
- package/dist/main.js +91 -0
- package/package.json +33 -0
- package/src/agent.ts +44 -0
- package/src/cli-convert.ts +104 -0
- package/src/cli-stream.ts +291 -0
- package/src/main.ts +103 -0
- package/src/system-prompt.ts +94 -0
- package/src/tools/bash.ts +124 -0
- package/src/tools/edit.ts +50 -0
- package/src/tools/grep.ts +124 -0
- package/src/tools/index.ts +25 -0
- package/src/tools/ls.ts +52 -0
- package/src/tools/read.ts +69 -0
- package/src/tools/write.ts +31 -0
- package/src/ui/render.ts +131 -0
- package/system-prompt.txt +55 -0
- package/tsconfig.json +15 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { createPizzaAgent } from "./agent.js";
|
|
4
|
+
import { createRenderer } from "./ui/render.js";
|
|
5
|
+
// ANSI helpers
|
|
6
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
7
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
8
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
9
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
10
|
+
// Parse CLI args
|
|
11
|
+
function parseArgs() {
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const result = {};
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
if (args[i] === "--provider" && args[i + 1]) {
|
|
16
|
+
result.provider = args[++i];
|
|
17
|
+
}
|
|
18
|
+
else if (args[i] === "--model" && args[i + 1]) {
|
|
19
|
+
result.model = args[++i];
|
|
20
|
+
}
|
|
21
|
+
else if (args[i] === "--api-key" && args[i + 1]) {
|
|
22
|
+
result.apiKey = args[++i];
|
|
23
|
+
}
|
|
24
|
+
else if (args[i] === "--help" || args[i] === "-h") {
|
|
25
|
+
console.log(`
|
|
26
|
+
${bold("pizza")} - A minimal coding agent
|
|
27
|
+
|
|
28
|
+
${bold("Usage:")}
|
|
29
|
+
pizza [options]
|
|
30
|
+
|
|
31
|
+
${bold("Options:")}
|
|
32
|
+
--provider <name> LLM provider (default: zai)
|
|
33
|
+
--model <id> Model ID (default: glm-4.5)
|
|
34
|
+
--api-key <key> API key (default: from env)
|
|
35
|
+
-h, --help Show this help
|
|
36
|
+
|
|
37
|
+
${bold("Environment:")}
|
|
38
|
+
ZAI_API_KEY ZAI API key
|
|
39
|
+
ANTHROPIC_API_KEY Anthropic API key
|
|
40
|
+
OPENAI_API_KEY OpenAI API key
|
|
41
|
+
`);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
async function main() {
|
|
48
|
+
const args = parseArgs();
|
|
49
|
+
console.log(`\n${bold(green("🍕 pizza"))} ${dim("coding agent")}`);
|
|
50
|
+
console.log(dim(`provider: ${args.provider ?? "zai"} | model: ${args.model ?? "glm-4.7"}`));
|
|
51
|
+
console.log(dim(`cwd: ${process.cwd()}`));
|
|
52
|
+
console.log(dim(`Type your message, or "exit" to quit.\n`));
|
|
53
|
+
const { agent, model } = createPizzaAgent({
|
|
54
|
+
provider: args.provider,
|
|
55
|
+
model: args.model,
|
|
56
|
+
apiKey: args.apiKey,
|
|
57
|
+
});
|
|
58
|
+
const render = createRenderer();
|
|
59
|
+
agent.subscribe(render);
|
|
60
|
+
const rl = createInterface({
|
|
61
|
+
input: process.stdin,
|
|
62
|
+
output: process.stdout,
|
|
63
|
+
});
|
|
64
|
+
const prompt = () => {
|
|
65
|
+
rl.question(`${cyan(">")} `, async (input) => {
|
|
66
|
+
const trimmed = input.trim();
|
|
67
|
+
if (!trimmed) {
|
|
68
|
+
prompt();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (trimmed === "exit" || trimmed === "quit" || trimmed === "/exit" || trimmed === "/quit") {
|
|
72
|
+
console.log(dim("\nBye!"));
|
|
73
|
+
rl.close();
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
await agent.prompt(trimmed);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
console.error(`\n\x1b[31mError: ${err.message}\x1b[0m\n`);
|
|
81
|
+
}
|
|
82
|
+
prompt();
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
prompt();
|
|
86
|
+
}
|
|
87
|
+
main().catch((err) => {
|
|
88
|
+
console.error("Fatal:", err);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|
|
91
|
+
//# sourceMappingURL=main.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tomsun28/pizza",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Pizza - CLI is all you need",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pizza": "dist/main.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/main.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "tsx src/main.ts",
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"start": "node dist/main.js"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@mariozechner/pi-agent-core": "^0.62.0",
|
|
17
|
+
"@mariozechner/pi-ai": "^0.62.0",
|
|
18
|
+
"@mariozechner/pi-tui": "^0.62.0",
|
|
19
|
+
"@sinclair/typebox": "^0.34.41"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.0.0",
|
|
23
|
+
"tsx": "^4.19.0",
|
|
24
|
+
"typescript": "^5.7.3"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20.0.0"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"author": "tomsun28"
|
|
33
|
+
}
|
package/src/agent.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Agent, type AgentEvent } from "@mariozechner/pi-agent-core";
|
|
2
|
+
import { getModel, type Model } from "@mariozechner/pi-ai";
|
|
3
|
+
import { createAllTools } from "./tools/index.js";
|
|
4
|
+
import { buildSystemPrompt } from "./system-prompt.js";
|
|
5
|
+
import { createCLIStreamFn } from "./cli-stream.js";
|
|
6
|
+
import { convertToLlmWithCLI } from "./cli-convert.js";
|
|
7
|
+
|
|
8
|
+
export interface PizzaOptions {
|
|
9
|
+
provider?: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
cwd?: string;
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PizzaAgent {
|
|
16
|
+
agent: Agent;
|
|
17
|
+
model: Model<any>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createPizzaAgent(options: PizzaOptions = {}): PizzaAgent {
|
|
21
|
+
const cwd = options.cwd ?? process.cwd();
|
|
22
|
+
const provider = options.provider ?? "zai";
|
|
23
|
+
const modelId = options.model ?? "glm-4.7";
|
|
24
|
+
|
|
25
|
+
const model = getModel(provider as any, modelId as any);
|
|
26
|
+
const tools = createAllTools(cwd);
|
|
27
|
+
const systemPrompt = buildSystemPrompt(cwd);
|
|
28
|
+
|
|
29
|
+
const agent = new Agent({
|
|
30
|
+
initialState: {
|
|
31
|
+
systemPrompt,
|
|
32
|
+
model,
|
|
33
|
+
thinkingLevel: "off",
|
|
34
|
+
tools,
|
|
35
|
+
},
|
|
36
|
+
streamFn: createCLIStreamFn(),
|
|
37
|
+
convertToLlm: convertToLlmWithCLI,
|
|
38
|
+
getApiKey: async () => {
|
|
39
|
+
return options.apiKey ?? process.env.ZAI_API_KEY ?? process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY;
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return { agent, model };
|
|
44
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
});
|