@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.
@@ -0,0 +1,14 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Write",
5
+ "Bash(npm:*)",
6
+ "Bash(npx:*)",
7
+ "Bash(echo:*)",
8
+ "mcp__zread__get_repo_structure"
9
+ ]
10
+ },
11
+ "enabledPlugins": {
12
+ "skill-creator@claude-plugins-official": true
13
+ }
14
+ }
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
+ });