@tomsun28/pizza 0.0.1 → 0.0.5
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/README.md +154 -0
- package/dist/extension/core.d.ts +53 -0
- package/dist/extension/core.js +466 -0
- package/dist/extension/core.js.map +1 -0
- package/dist/extension/index.d.ts +15 -0
- package/dist/extension/index.js +260 -0
- package/dist/extension/index.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +90 -66
- package/dist/main.js.map +1 -0
- package/package.json +14 -6
- package/themes/pizza.json +87 -0
- package/.claude/settings.local.json +0 -14
- package/src/agent.ts +0 -44
- package/src/cli-convert.ts +0 -104
- package/src/cli-stream.ts +0 -291
- package/src/main.ts +0 -103
- package/src/system-prompt.ts +0 -94
- package/src/tools/bash.ts +0 -124
- package/src/tools/edit.ts +0 -50
- package/src/tools/grep.ts +0 -124
- package/src/tools/index.ts +0 -25
- package/src/tools/ls.ts +0 -52
- package/src/tools/read.ts +0 -69
- package/src/tools/write.ts +0 -31
- package/src/ui/render.ts +0 -131
- package/system-prompt.txt +0 -55
- package/tsconfig.json +0 -15
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# 🍕 Pizza
|
|
2
|
+
|
|
3
|
+
> **CLI is all you need**
|
|
4
|
+
|
|
5
|
+
Pizza is a command-line AI assistant that replaces traditional JSON tool calls with a unique `[CLI]` tag format. Built on top of [pi-coding-agent](https://github.com/badlogic/pi-coding-agent), it provides developers with an efficient terminal interaction experience.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **[CLI] Tag Format** — Invoke tools using natural text tags, no complex JSON structures needed
|
|
12
|
+
- **Multi-Tool Support** — Built-in commands: `ls`, `read`, `write`, `edit`, `grep`, `bash`
|
|
13
|
+
- **Interactive TUI** — Beautiful terminal interface based on pi-tui with keyboard shortcuts
|
|
14
|
+
- **Multi-Model Support** — Compatible with OpenAI, Anthropic, and other LLM providers
|
|
15
|
+
- **Session Management** — Automatic saving and management of conversation history
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g @tomsun28/pizza
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Interactive mode
|
|
33
|
+
pizza
|
|
34
|
+
|
|
35
|
+
# Execute a single command
|
|
36
|
+
pizza "analyze the current project structure"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Command Line Options
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
pizza [options] [message]
|
|
43
|
+
|
|
44
|
+
OPTIONS:
|
|
45
|
+
-p, --provider <name> LLM provider (reads from config by default)
|
|
46
|
+
-m, --model <id> Model ID
|
|
47
|
+
-h, --help Show help information
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Keyboard Shortcuts
|
|
51
|
+
|
|
52
|
+
| Shortcut | Function |
|
|
53
|
+
|----------|----------|
|
|
54
|
+
| `Ctrl+P` | Switch model |
|
|
55
|
+
| `Ctrl+T` | Think mode |
|
|
56
|
+
| `Ctrl+C` | Stop current operation |
|
|
57
|
+
|
|
58
|
+
### In-Session Commands
|
|
59
|
+
|
|
60
|
+
- `/cli-mode` — Toggle CLI tag mode (enabled by default)
|
|
61
|
+
- `/help` — Show help
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## [CLI] Tag Format
|
|
66
|
+
|
|
67
|
+
Pizza's core innovation replaces traditional JSON tool calls with `[CLI]` tags:
|
|
68
|
+
|
|
69
|
+
### Format
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
[CLI id=1] ls src[/CLI]
|
|
73
|
+
[CLI id=2] read package.json[/CLI]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Result Return
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
[RESULT id=1]
|
|
80
|
+
file 1.2KB index.ts
|
|
81
|
+
file 2.5KB utils.ts
|
|
82
|
+
dir - components
|
|
83
|
+
[/RESULT]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Supported Commands
|
|
87
|
+
|
|
88
|
+
| Command | Usage | Description |
|
|
89
|
+
|---------|-------|-------------|
|
|
90
|
+
| `ls` | `[CLI]ls <path>[/CLI]` | List directory contents |
|
|
91
|
+
| `read` | `[CLI]read <path> [--offset N] [--limit N][/CLI]` | Read file |
|
|
92
|
+
| `write` | `[CLI]write --path <path> --content <content>[/CLI]` | Write file |
|
|
93
|
+
| `edit` | `[CLI]edit --path <path> --old <text> --new <text>[/CLI]` | Edit file |
|
|
94
|
+
| `grep` | `[CLI]grep <pattern> --path <dir> --include <glob>[/CLI]` | Search files |
|
|
95
|
+
| `bash` | `[CLI]bash <command>[/CLI]` | Execute shell command |
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Development
|
|
100
|
+
|
|
101
|
+
### Project Structure
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
.
|
|
105
|
+
├── src/
|
|
106
|
+
│ ├── main.ts # Entry file
|
|
107
|
+
│ └── extension/
|
|
108
|
+
│ ├── index.ts # Extension main logic
|
|
109
|
+
│ └── core.ts # Core tool implementations
|
|
110
|
+
├── themes/
|
|
111
|
+
│ └── pizza.json # Theme configuration
|
|
112
|
+
├── package.json
|
|
113
|
+
└── tsconfig.json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Local Development
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# Clone the repository
|
|
120
|
+
git clone <repo-url>
|
|
121
|
+
cd pizza
|
|
122
|
+
|
|
123
|
+
# Install dependencies
|
|
124
|
+
pnpm install
|
|
125
|
+
|
|
126
|
+
# Development mode
|
|
127
|
+
pnpm dev
|
|
128
|
+
|
|
129
|
+
# Build
|
|
130
|
+
pnpm build
|
|
131
|
+
|
|
132
|
+
# Run
|
|
133
|
+
pnpm start
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Dependencies
|
|
139
|
+
|
|
140
|
+
- [pi-coding-agent](https://github.com/badlogic/pi-coding-agent) — Agent session management and extension system
|
|
141
|
+
- [pi-ai](https://github.com/badlogic/pi-coding-agent) — AI model interface
|
|
142
|
+
- [pi-tui](https://github.com/badlogic/pi-coding-agent) — Terminal user interface
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Configuration
|
|
147
|
+
|
|
148
|
+
Pizza uses the `.pizza` directory to store configuration and session data. It is created automatically on first run.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core logic for pizza-mode: [CLI] tag parsing, tool execution helpers,
|
|
3
|
+
* and conversation history rewriting.
|
|
4
|
+
*/
|
|
5
|
+
export declare const MAX_CALLS_PER_TURN = 5;
|
|
6
|
+
export declare const MAX_OUTPUT_BYTES: number;
|
|
7
|
+
export declare const MAX_OUTPUT_LINES = 1000;
|
|
8
|
+
export declare const MAX_FILE_BYTES: number;
|
|
9
|
+
export declare const MAX_FILE_LINES = 2000;
|
|
10
|
+
export declare const MAX_GREP_RESULTS = 200;
|
|
11
|
+
export declare const MAX_GREP_FILE_SIZE: number;
|
|
12
|
+
export interface ParsedCLICall {
|
|
13
|
+
id: number;
|
|
14
|
+
toolName: string;
|
|
15
|
+
rawArgs: string;
|
|
16
|
+
}
|
|
17
|
+
export interface CLIToolResult {
|
|
18
|
+
output: string;
|
|
19
|
+
isError: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parse [CLI] toolName args[/CLI] blocks from model text output.
|
|
23
|
+
* Returns all calls found and the text before the first call.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseCLICalls(text: string): {
|
|
26
|
+
calls: ParsedCLICall[];
|
|
27
|
+
textBeforeCalls: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Parse a shell-style args string into a Record.
|
|
31
|
+
* Handles: --key "value", --key value, --flag, positional args.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseCliArgs(raw: string): Record<string, any>;
|
|
34
|
+
/**
|
|
35
|
+
* Map parsed args to tool-specific argument schema.
|
|
36
|
+
*/
|
|
37
|
+
export declare function mapArgsToTool(toolName: string, rawArgs: string): Record<string, any>;
|
|
38
|
+
/**
|
|
39
|
+
* Convert tool call arguments back to CLI flag string for history reconstruction.
|
|
40
|
+
*/
|
|
41
|
+
export declare function argsToCliString(toolName: string, arguments_: Record<string, any>): string;
|
|
42
|
+
export declare function execLs(cwd: string, args: Record<string, any>): Promise<CLIToolResult>;
|
|
43
|
+
export declare function execRead(cwd: string, args: Record<string, any>): Promise<CLIToolResult>;
|
|
44
|
+
export declare function execWrite(cwd: string, args: Record<string, any>): Promise<CLIToolResult>;
|
|
45
|
+
export declare function execEdit(cwd: string, args: Record<string, any>): Promise<CLIToolResult>;
|
|
46
|
+
export declare function execGrep(cwd: string, args: Record<string, any>): Promise<CLIToolResult>;
|
|
47
|
+
export declare function execBash(cwd: string, args: Record<string, any>, signal?: AbortSignal): Promise<CLIToolResult>;
|
|
48
|
+
export declare function dispatchTool(toolName: string, rawArgs: string, cwd: string, signal?: AbortSignal): Promise<CLIToolResult>;
|
|
49
|
+
export declare function buildResultBlock(output: string, isError: boolean, id?: number, total?: number, cliCommand?: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Reconstruct the [CLI] text that the model originally emitted for a tool call.
|
|
52
|
+
*/
|
|
53
|
+
export declare function reconstructCliText(toolName: string, arguments_: Record<string, any>): string;
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core logic for pizza-mode: [CLI] tag parsing, tool execution helpers,
|
|
3
|
+
* and conversation history rewriting.
|
|
4
|
+
*/
|
|
5
|
+
import { readFile, writeFile, mkdir, readdir, stat } from "node:fs/promises";
|
|
6
|
+
import { resolve, isAbsolute, dirname, join, relative } from "node:path";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
9
|
+
export const MAX_CALLS_PER_TURN = 5;
|
|
10
|
+
export const MAX_OUTPUT_BYTES = 128 * 1024;
|
|
11
|
+
export const MAX_OUTPUT_LINES = 1000;
|
|
12
|
+
export const MAX_FILE_BYTES = 256 * 1024;
|
|
13
|
+
export const MAX_FILE_LINES = 2000;
|
|
14
|
+
export const MAX_GREP_RESULTS = 200;
|
|
15
|
+
export const MAX_GREP_FILE_SIZE = 1024 * 1024;
|
|
16
|
+
// ─── [CLI] tag parsing ────────────────────────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* Parse [CLI] toolName args[/CLI] blocks from model text output.
|
|
19
|
+
* Returns all calls found and the text before the first call.
|
|
20
|
+
*/
|
|
21
|
+
export function parseCLICalls(text) {
|
|
22
|
+
const regex = /\[CLI(?:\s+id=(\d+))?\]([\s\S]*?)\[\/CLI\]/g;
|
|
23
|
+
const calls = [];
|
|
24
|
+
let firstMatchStart = -1;
|
|
25
|
+
let autoId = 1;
|
|
26
|
+
let match;
|
|
27
|
+
while ((match = regex.exec(text)) !== null) {
|
|
28
|
+
if (firstMatchStart === -1)
|
|
29
|
+
firstMatchStart = match.index;
|
|
30
|
+
const explicitId = match[1] ? parseInt(match[1], 10) : undefined;
|
|
31
|
+
const inner = match[2].trim();
|
|
32
|
+
const spaceIdx = inner.search(/\s/);
|
|
33
|
+
const toolName = spaceIdx === -1 ? inner : inner.slice(0, spaceIdx);
|
|
34
|
+
const rawArgs = spaceIdx === -1 ? "" : inner.slice(spaceIdx + 1).trim();
|
|
35
|
+
if (toolName) {
|
|
36
|
+
calls.push({ id: explicitId ?? autoId, toolName, rawArgs });
|
|
37
|
+
autoId++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const textBeforeCalls = firstMatchStart >= 0 ? text.substring(0, firstMatchStart).trimEnd() : text;
|
|
41
|
+
return { calls, textBeforeCalls };
|
|
42
|
+
}
|
|
43
|
+
// ─── Shell-style arg parser ───────────────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Parse a shell-style args string into a Record.
|
|
46
|
+
* Handles: --key "value", --key value, --flag, positional args.
|
|
47
|
+
*/
|
|
48
|
+
export function parseCliArgs(raw) {
|
|
49
|
+
const args = {};
|
|
50
|
+
const positional = [];
|
|
51
|
+
const tokens = [];
|
|
52
|
+
let i = 0;
|
|
53
|
+
while (i < raw.length) {
|
|
54
|
+
while (i < raw.length && /\s/.test(raw[i]))
|
|
55
|
+
i++;
|
|
56
|
+
if (i >= raw.length)
|
|
57
|
+
break;
|
|
58
|
+
if (raw[i] === '"' || raw[i] === "'") {
|
|
59
|
+
const quote = raw[i++];
|
|
60
|
+
let val = "";
|
|
61
|
+
while (i < raw.length && raw[i] !== quote) {
|
|
62
|
+
if (raw[i] === "\\" && i + 1 < raw.length) {
|
|
63
|
+
i++;
|
|
64
|
+
val += raw[i++];
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
val += raw[i++];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
i++;
|
|
71
|
+
tokens.push(val);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
let val = "";
|
|
75
|
+
while (i < raw.length && !/\s/.test(raw[i])) {
|
|
76
|
+
val += raw[i++];
|
|
77
|
+
}
|
|
78
|
+
tokens.push(val);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
let ti = 0;
|
|
82
|
+
while (ti < tokens.length) {
|
|
83
|
+
const tok = tokens[ti];
|
|
84
|
+
if (tok.startsWith("--")) {
|
|
85
|
+
const key = tok.slice(2);
|
|
86
|
+
if (ti + 1 < tokens.length && !tokens[ti + 1].startsWith("--")) {
|
|
87
|
+
args[key] = tokens[ti + 1];
|
|
88
|
+
ti += 2;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
args[key] = true;
|
|
92
|
+
ti++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
positional.push(tok);
|
|
97
|
+
ti++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (positional.length > 0) {
|
|
101
|
+
if (!("path" in args) && !("command" in args) && !("pattern" in args)) {
|
|
102
|
+
args._positional = positional.join(" ");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return args;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Map parsed args to tool-specific argument schema.
|
|
109
|
+
*/
|
|
110
|
+
export function mapArgsToTool(toolName, rawArgs) {
|
|
111
|
+
const args = parseCliArgs(rawArgs);
|
|
112
|
+
const pos = args._positional;
|
|
113
|
+
delete args._positional;
|
|
114
|
+
switch (toolName) {
|
|
115
|
+
case "ls":
|
|
116
|
+
if (pos && !args.path)
|
|
117
|
+
args.path = pos;
|
|
118
|
+
break;
|
|
119
|
+
case "read":
|
|
120
|
+
if (pos && !args.path)
|
|
121
|
+
args.path = pos;
|
|
122
|
+
break;
|
|
123
|
+
case "write":
|
|
124
|
+
// --path and --content required
|
|
125
|
+
break;
|
|
126
|
+
case "edit":
|
|
127
|
+
if (args.old) {
|
|
128
|
+
args.oldText = args.old;
|
|
129
|
+
delete args.old;
|
|
130
|
+
}
|
|
131
|
+
if (args.new) {
|
|
132
|
+
args.newText = args.new;
|
|
133
|
+
delete args.new;
|
|
134
|
+
}
|
|
135
|
+
if (pos && !args.path)
|
|
136
|
+
args.path = pos;
|
|
137
|
+
break;
|
|
138
|
+
case "grep":
|
|
139
|
+
if (pos && !args.pattern)
|
|
140
|
+
args.pattern = pos;
|
|
141
|
+
break;
|
|
142
|
+
case "bash":
|
|
143
|
+
if (rawArgs && !args.command)
|
|
144
|
+
args.command = rawArgs;
|
|
145
|
+
break;
|
|
146
|
+
default:
|
|
147
|
+
if (pos)
|
|
148
|
+
args.input = pos;
|
|
149
|
+
}
|
|
150
|
+
return args;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Convert tool call arguments back to CLI flag string for history reconstruction.
|
|
154
|
+
*/
|
|
155
|
+
export function argsToCliString(toolName, arguments_) {
|
|
156
|
+
if (toolName === "bash") {
|
|
157
|
+
return arguments_.command ?? "";
|
|
158
|
+
}
|
|
159
|
+
if (toolName === "ls" || toolName === "read") {
|
|
160
|
+
const path = arguments_.path;
|
|
161
|
+
const rest = Object.entries(arguments_)
|
|
162
|
+
.filter(([k]) => k !== "path")
|
|
163
|
+
.map(([k, v]) => `--${k} "${String(v).replace(/"/g, '\\"')}"`)
|
|
164
|
+
.join(" ");
|
|
165
|
+
return path ? (rest ? `${path} ${rest}` : path) : rest;
|
|
166
|
+
}
|
|
167
|
+
if (toolName === "edit") {
|
|
168
|
+
const mapped = { ...arguments_ };
|
|
169
|
+
if (mapped.oldText !== undefined) {
|
|
170
|
+
mapped.old = mapped.oldText;
|
|
171
|
+
delete mapped.oldText;
|
|
172
|
+
}
|
|
173
|
+
if (mapped.newText !== undefined) {
|
|
174
|
+
mapped.new = mapped.newText;
|
|
175
|
+
delete mapped.newText;
|
|
176
|
+
}
|
|
177
|
+
return Object.entries(mapped)
|
|
178
|
+
.map(([k, v]) => {
|
|
179
|
+
if (v === true)
|
|
180
|
+
return `--${k}`;
|
|
181
|
+
const s = String(v);
|
|
182
|
+
return s.includes(" ") || s.includes('"') || s.includes("\n")
|
|
183
|
+
? `--${k} "${s.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`
|
|
184
|
+
: `--${k} ${s}`;
|
|
185
|
+
})
|
|
186
|
+
.join(" ");
|
|
187
|
+
}
|
|
188
|
+
return Object.entries(arguments_)
|
|
189
|
+
.map(([k, v]) => {
|
|
190
|
+
if (v === true)
|
|
191
|
+
return `--${k}`;
|
|
192
|
+
const s = String(v);
|
|
193
|
+
return s.includes(" ") || s.includes('"') || s.includes("\n")
|
|
194
|
+
? `--${k} "${s.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`
|
|
195
|
+
: `--${k} ${s}`;
|
|
196
|
+
})
|
|
197
|
+
.join(" ");
|
|
198
|
+
}
|
|
199
|
+
// ─── Tool implementations ─────────────────────────────────────────────────────
|
|
200
|
+
export async function execLs(cwd, args) {
|
|
201
|
+
const dirPath = args.path
|
|
202
|
+
? isAbsolute(args.path) ? args.path : resolve(cwd, args.path)
|
|
203
|
+
: cwd;
|
|
204
|
+
const entries = await readdir(dirPath);
|
|
205
|
+
const lines = [];
|
|
206
|
+
for (const entry of entries.sort()) {
|
|
207
|
+
try {
|
|
208
|
+
const fullPath = join(dirPath, entry);
|
|
209
|
+
const s = await stat(fullPath);
|
|
210
|
+
const type = s.isDirectory() ? "dir" : "file";
|
|
211
|
+
const size = s.isDirectory() ? "-" : formatSize(s.size);
|
|
212
|
+
lines.push(`${type}\t${size}\t${entry}`);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
lines.push(`?\t?\t${entry}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
output: lines.length === 0 ? "(empty directory)" : lines.join("\n"),
|
|
220
|
+
isError: false,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
export async function execRead(cwd, args) {
|
|
224
|
+
const absolutePath = args.path
|
|
225
|
+
? (isAbsolute(args.path) ? args.path : resolve(cwd, args.path))
|
|
226
|
+
: resolve(cwd, ".");
|
|
227
|
+
const buffer = await readFile(absolutePath);
|
|
228
|
+
const text = buffer.toString("utf-8");
|
|
229
|
+
const allLines = text.split("\n");
|
|
230
|
+
const totalLines = allLines.length;
|
|
231
|
+
const offset = args.offset ? Number(args.offset) : undefined;
|
|
232
|
+
const limit = args.limit ? Number(args.limit) : undefined;
|
|
233
|
+
const startLine = offset ? Math.max(0, offset - 1) : 0;
|
|
234
|
+
if (startLine >= totalLines) {
|
|
235
|
+
throw new Error(`Offset ${offset} is beyond end of file (${totalLines} lines)`);
|
|
236
|
+
}
|
|
237
|
+
let selected = limit !== undefined
|
|
238
|
+
? allLines.slice(startLine, startLine + limit)
|
|
239
|
+
: allLines.slice(startLine);
|
|
240
|
+
if (selected.length > MAX_FILE_LINES) {
|
|
241
|
+
selected = selected.slice(0, MAX_FILE_LINES);
|
|
242
|
+
}
|
|
243
|
+
let output = selected.join("\n");
|
|
244
|
+
if (Buffer.byteLength(output, "utf-8") > MAX_FILE_BYTES) {
|
|
245
|
+
const buf = Buffer.from(output, "utf-8");
|
|
246
|
+
output = buf.subarray(0, MAX_FILE_BYTES).toString("utf-8");
|
|
247
|
+
}
|
|
248
|
+
const shownLines = output.split("\n").length;
|
|
249
|
+
const remaining = totalLines - startLine - shownLines;
|
|
250
|
+
if (remaining > 0) {
|
|
251
|
+
const nextOffset = startLine + shownLines + 1;
|
|
252
|
+
output += `\n\n[Showing ${shownLines} of ${totalLines} lines. Use --offset ${nextOffset} to continue.]`;
|
|
253
|
+
}
|
|
254
|
+
return { output, isError: false };
|
|
255
|
+
}
|
|
256
|
+
export async function execWrite(cwd, args) {
|
|
257
|
+
if (!args.path)
|
|
258
|
+
throw new Error("write requires --path");
|
|
259
|
+
if (args.content === undefined)
|
|
260
|
+
throw new Error("write requires --content");
|
|
261
|
+
const absolutePath = isAbsolute(args.path) ? args.path : resolve(cwd, args.path);
|
|
262
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
263
|
+
await writeFile(absolutePath, args.content, "utf-8");
|
|
264
|
+
return {
|
|
265
|
+
output: `Wrote ${String(args.content).length} bytes to ${args.path}`,
|
|
266
|
+
isError: false,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
export async function execEdit(cwd, args) {
|
|
270
|
+
if (!args.path)
|
|
271
|
+
throw new Error("edit requires --path");
|
|
272
|
+
if (args.oldText === undefined)
|
|
273
|
+
throw new Error("edit requires --old");
|
|
274
|
+
if (args.newText === undefined)
|
|
275
|
+
throw new Error("edit requires --new");
|
|
276
|
+
const absolutePath = isAbsolute(args.path) ? args.path : resolve(cwd, args.path);
|
|
277
|
+
const content = await readFile(absolutePath, "utf-8");
|
|
278
|
+
const index = content.indexOf(args.oldText);
|
|
279
|
+
if (index === -1) {
|
|
280
|
+
throw new Error(`Could not find the exact text in ${args.path}. The --old text must match exactly including whitespace.`);
|
|
281
|
+
}
|
|
282
|
+
const second = content.indexOf(args.oldText, index + 1);
|
|
283
|
+
if (second !== -1) {
|
|
284
|
+
throw new Error(`Found multiple occurrences of the text in ${args.path}. Provide more context to make it unique.`);
|
|
285
|
+
}
|
|
286
|
+
const newContent = content.substring(0, index) + args.newText + content.substring(index + args.oldText.length);
|
|
287
|
+
await writeFile(absolutePath, newContent, "utf-8");
|
|
288
|
+
return { output: `Edited ${args.path}`, isError: false };
|
|
289
|
+
}
|
|
290
|
+
export async function execGrep(cwd, args) {
|
|
291
|
+
if (!args.pattern)
|
|
292
|
+
throw new Error("grep requires a pattern");
|
|
293
|
+
const searchDir = args.path
|
|
294
|
+
? (isAbsolute(args.path) ? args.path : resolve(cwd, args.path))
|
|
295
|
+
: cwd;
|
|
296
|
+
let regex;
|
|
297
|
+
try {
|
|
298
|
+
regex = new RegExp(args.pattern, "g");
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
throw new Error(`Invalid regex pattern: ${e.message}`);
|
|
302
|
+
}
|
|
303
|
+
const extFilter = args.include ? parseExtFilter(args.include) : null;
|
|
304
|
+
const results = [];
|
|
305
|
+
await grepDirectory(searchDir, regex, extFilter, results, cwd);
|
|
306
|
+
if (results.length === 0) {
|
|
307
|
+
return { output: "No matches found.", isError: false };
|
|
308
|
+
}
|
|
309
|
+
let output = results.join("\n");
|
|
310
|
+
if (results.length >= MAX_GREP_RESULTS) {
|
|
311
|
+
output += `\n\n[Truncated at ${MAX_GREP_RESULTS} results]`;
|
|
312
|
+
}
|
|
313
|
+
return { output, isError: false };
|
|
314
|
+
}
|
|
315
|
+
export async function execBash(cwd, args, signal) {
|
|
316
|
+
const command = args.command;
|
|
317
|
+
if (!command)
|
|
318
|
+
throw new Error("bash requires a command");
|
|
319
|
+
return new Promise((resolve_, reject) => {
|
|
320
|
+
const shell = process.env.SHELL || "/bin/bash";
|
|
321
|
+
const child = spawn(shell, ["-c", command], {
|
|
322
|
+
cwd,
|
|
323
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
324
|
+
env: { ...process.env },
|
|
325
|
+
});
|
|
326
|
+
const chunks = [];
|
|
327
|
+
const onAbort = () => {
|
|
328
|
+
child.kill("SIGTERM");
|
|
329
|
+
setTimeout(() => child.kill("SIGKILL"), 2000);
|
|
330
|
+
};
|
|
331
|
+
if (signal) {
|
|
332
|
+
if (signal.aborted) {
|
|
333
|
+
onAbort();
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
child.stdout?.on("data", (d) => chunks.push(d));
|
|
340
|
+
child.stderr?.on("data", (d) => chunks.push(d));
|
|
341
|
+
child.on("close", (code) => {
|
|
342
|
+
if (signal)
|
|
343
|
+
signal.removeEventListener("abort", onAbort);
|
|
344
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
345
|
+
const output = truncateOutput(raw);
|
|
346
|
+
const isError = code !== 0 && code !== null;
|
|
347
|
+
resolve_({ output: output || "(no output)", isError });
|
|
348
|
+
});
|
|
349
|
+
child.on("error", (err) => {
|
|
350
|
+
if (signal)
|
|
351
|
+
signal.removeEventListener("abort", onAbort);
|
|
352
|
+
reject(err);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
// ─── Dispatch ─────────────────────────────────────────────────────────────────
|
|
357
|
+
export async function dispatchTool(toolName, rawArgs, cwd, signal) {
|
|
358
|
+
const args = mapArgsToTool(toolName, rawArgs);
|
|
359
|
+
switch (toolName) {
|
|
360
|
+
case "ls": return execLs(cwd, args);
|
|
361
|
+
case "read": return execRead(cwd, args);
|
|
362
|
+
case "write": return execWrite(cwd, args);
|
|
363
|
+
case "edit": return execEdit(cwd, args);
|
|
364
|
+
case "grep": return execGrep(cwd, args);
|
|
365
|
+
case "bash": return execBash(cwd, args, signal);
|
|
366
|
+
default: {
|
|
367
|
+
// Fallback: treat unknown commands as bash
|
|
368
|
+
const bashCmd = `${toolName}${rawArgs ? " " + rawArgs : ""}`;
|
|
369
|
+
return execBash(cwd, { command: bashCmd }, signal);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// ─── Result formatting ────────────────────────────────────────────────────────
|
|
374
|
+
export function buildResultBlock(output, isError, id, total, cliCommand) {
|
|
375
|
+
const idAttr = id !== undefined ? ` id=${id}` : "";
|
|
376
|
+
const cliAttr = cliCommand ? ` cli="${cliCommand.replace(/"/g, '\\"')}"` : "";
|
|
377
|
+
const errAttr = isError ? " error=true" : "";
|
|
378
|
+
return `[RESULT${idAttr}${cliAttr}${errAttr}]\n${output}\n[/RESULT]`;
|
|
379
|
+
}
|
|
380
|
+
// ─── History rewriting ────────────────────────────────────────────────────────
|
|
381
|
+
/**
|
|
382
|
+
* Reconstruct the [CLI] text that the model originally emitted for a tool call.
|
|
383
|
+
*/
|
|
384
|
+
export function reconstructCliText(toolName, arguments_) {
|
|
385
|
+
const cliArgs = argsToCliString(toolName, arguments_);
|
|
386
|
+
const inner = cliArgs ? `${toolName} ${cliArgs}` : toolName;
|
|
387
|
+
return `[CLI] ${inner}[/CLI]`;
|
|
388
|
+
}
|
|
389
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
390
|
+
function formatSize(bytes) {
|
|
391
|
+
if (bytes < 1024)
|
|
392
|
+
return `${bytes}B`;
|
|
393
|
+
if (bytes < 1024 * 1024)
|
|
394
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
395
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
396
|
+
}
|
|
397
|
+
function parseExtFilter(include) {
|
|
398
|
+
if (include.startsWith("*."))
|
|
399
|
+
return include.substring(1);
|
|
400
|
+
return include;
|
|
401
|
+
}
|
|
402
|
+
const SKIP_DIRS = new Set([
|
|
403
|
+
"node_modules", ".git", "dist", "build", ".next", "__pycache__",
|
|
404
|
+
".venv", "venv", ".cache", "coverage",
|
|
405
|
+
]);
|
|
406
|
+
async function grepDirectory(dir, regex, extFilter, results, cwd) {
|
|
407
|
+
if (results.length >= MAX_GREP_RESULTS)
|
|
408
|
+
return;
|
|
409
|
+
let entries;
|
|
410
|
+
try {
|
|
411
|
+
entries = await readdir(dir);
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
for (const entry of entries) {
|
|
417
|
+
if (results.length >= MAX_GREP_RESULTS)
|
|
418
|
+
return;
|
|
419
|
+
const fullPath = join(dir, entry);
|
|
420
|
+
let s;
|
|
421
|
+
try {
|
|
422
|
+
s = await stat(fullPath);
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (s.isDirectory()) {
|
|
428
|
+
if (!SKIP_DIRS.has(entry)) {
|
|
429
|
+
await grepDirectory(fullPath, regex, extFilter, results, cwd);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else if (s.isFile()) {
|
|
433
|
+
if (s.size > MAX_GREP_FILE_SIZE)
|
|
434
|
+
continue;
|
|
435
|
+
if (extFilter && !entry.endsWith(extFilter))
|
|
436
|
+
continue;
|
|
437
|
+
try {
|
|
438
|
+
const content = await readFile(fullPath, "utf-8");
|
|
439
|
+
const lines = content.split("\n");
|
|
440
|
+
const relPath = relative(cwd, fullPath);
|
|
441
|
+
for (let i = 0; i < lines.length; i++) {
|
|
442
|
+
regex.lastIndex = 0;
|
|
443
|
+
if (regex.test(lines[i])) {
|
|
444
|
+
results.push(`${relPath}:${i + 1}: ${lines[i].trimEnd()}`);
|
|
445
|
+
if (results.length >= MAX_GREP_RESULTS)
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
catch { /* skip binary/unreadable */ }
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function truncateOutput(text) {
|
|
455
|
+
const lines = text.split("\n");
|
|
456
|
+
if (lines.length > MAX_OUTPUT_LINES) {
|
|
457
|
+
const kept = lines.slice(lines.length - MAX_OUTPUT_LINES);
|
|
458
|
+
return `[${lines.length - MAX_OUTPUT_LINES} lines truncated]\n${kept.join("\n")}`;
|
|
459
|
+
}
|
|
460
|
+
if (Buffer.byteLength(text, "utf-8") > MAX_OUTPUT_BYTES) {
|
|
461
|
+
const buf = Buffer.from(text, "utf-8");
|
|
462
|
+
return buf.subarray(buf.length - MAX_OUTPUT_BYTES).toString("utf-8");
|
|
463
|
+
}
|
|
464
|
+
return text;
|
|
465
|
+
}
|
|
466
|
+
//# sourceMappingURL=core.js.map
|