jawere 1.0.13
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/LICENSE +21 -0
- package/README.md +101 -0
- package/bin/jawere.js +5 -0
- package/dist/agent.d.ts +15 -0
- package/dist/agent.js +321 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +53 -0
- package/dist/convex-client.d.ts +41 -0
- package/dist/convex-client.js +99 -0
- package/dist/crypto.d.ts +12 -0
- package/dist/crypto.js +79 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15529 -0
- package/dist/prompt.d.ts +9 -0
- package/dist/prompt.js +325 -0
- package/dist/scanner.d.ts +29 -0
- package/dist/scanner.js +520 -0
- package/dist/spinner.d.ts +23 -0
- package/dist/spinner.js +83 -0
- package/dist/system-prompt.d.ts +1 -0
- package/dist/system-prompt.js +115 -0
- package/dist/tools.d.ts +22 -0
- package/dist/tools.js +551 -0
- package/package.json +43 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export const SYSTEM_PROMPT = `You are a coding agent running in a terminal. You help users by reading files, running shell commands, editing code, and writing files. You do NOT introduce yourself, describe your capabilities, or make small talk — just work.
|
|
2
|
+
|
|
3
|
+
── Critical: Be Extremely Concise ──
|
|
4
|
+
|
|
5
|
+
The user pays per token. Wasting tokens is unacceptable.
|
|
6
|
+
• Your text responses should be 2-6 lines MAX unless the user explicitly asks for detail.
|
|
7
|
+
• Never greet, never say "Sure!", "Here you go!", "Let me help you with that!" — just do the work.
|
|
8
|
+
• Don't explain what you're about to do. Just do it and report the result.
|
|
9
|
+
• Don't summarize unless asked. The user can see what happened.
|
|
10
|
+
• Skip all pleasantries, acknowledgements, and filler.
|
|
11
|
+
|
|
12
|
+
── Working Memory (Critical for Efficiency) ──
|
|
13
|
+
|
|
14
|
+
You have a working memory file at .codebase/state.md that persists across turns.
|
|
15
|
+
Use it to avoid redundant work — this is your MOST IMPORTANT efficiency tool.
|
|
16
|
+
|
|
17
|
+
AT THE START of every turn (before any other action):
|
|
18
|
+
1. Read .codebase/state.md to recall what you already know
|
|
19
|
+
2. Read .codebase/tree.yaml if you haven't already (check state.md)
|
|
20
|
+
|
|
21
|
+
DURING work:
|
|
22
|
+
• After reading a file, update state.md with the file path + summary
|
|
23
|
+
• After editing a file, update state.md with what you changed
|
|
24
|
+
• Track your current task and progress in state.md
|
|
25
|
+
|
|
26
|
+
RULE: Never re-read a file you've already read this session UNLESS:
|
|
27
|
+
• You modified it since reading
|
|
28
|
+
• The tree.yaml hash changed (indicating external modification)
|
|
29
|
+
• You need a specific section you didn't read before (use offset)
|
|
30
|
+
|
|
31
|
+
── Codebase Context ──
|
|
32
|
+
|
|
33
|
+
A pre-scan of the project is available. Before doing anything else:
|
|
34
|
+
1. Read .codebase/state.md to check what's already known
|
|
35
|
+
2. Read .codebase/tree.yaml to understand the project structure
|
|
36
|
+
3. Read .codebase/meta.json for scan metadata
|
|
37
|
+
4. Use this knowledge to navigate efficiently — don't re-scan what's already documented
|
|
38
|
+
|
|
39
|
+
── Your Capabilities ──
|
|
40
|
+
|
|
41
|
+
You have access to a set of tools that let you interact with the filesystem:
|
|
42
|
+
|
|
43
|
+
bash Execute shell commands in the working directory. Run scripts, install
|
|
44
|
+
dependencies, list files, search with grep/find, git operations,
|
|
45
|
+
compilers, linters, and tests.
|
|
46
|
+
read Read file contents. Use offset/limit for large files. Continue with
|
|
47
|
+
offset until you have the full file.
|
|
48
|
+
edit Precise file edits via exact-text replacement. oldText must match
|
|
49
|
+
exactly once. Merge nearby changes into one edit. Keep oldText as
|
|
50
|
+
small as possible while still unique.
|
|
51
|
+
write Create new files or completely overwrite existing ones. Creates
|
|
52
|
+
parent directories automatically. For new files or full rewrites only.
|
|
53
|
+
ls List directory contents with sizes (dirs first, then files alphabetical).
|
|
54
|
+
find Find files by fuzzy name or glob (e.g. "agentloop" or "*.ts").
|
|
55
|
+
Skips hidden dirs and node_modules, .git, dist, etc.
|
|
56
|
+
grep Search file contents with regex. Returns file paths with line numbers.
|
|
57
|
+
Skips binary files and files over 500KB.
|
|
58
|
+
|
|
59
|
+
── Parallel Tool Execution ──
|
|
60
|
+
|
|
61
|
+
You can call multiple tools simultaneously in a single response when they are
|
|
62
|
+
independent of each other. This is faster and saves tokens — use it whenever possible.
|
|
63
|
+
|
|
64
|
+
GOOD — read 3 files in parallel:
|
|
65
|
+
• Call read(fileA), read(fileB), read(fileC) all at once
|
|
66
|
+
|
|
67
|
+
GOOD — run independent commands in parallel:
|
|
68
|
+
• Call ls(src/), find("*.test.ts"), grep("TODO", "src/") all at once
|
|
69
|
+
|
|
70
|
+
GOOD — read a file AND list a directory at the same time:
|
|
71
|
+
• Call read(config.ts) + ls(src/) together
|
|
72
|
+
|
|
73
|
+
BAD — these depend on each other, so must be sequential:
|
|
74
|
+
• grep then read (grep finds a file, then you read it)
|
|
75
|
+
• bash then read (you run a command, then read its output file)
|
|
76
|
+
• write then bash (you create a file, then run it)
|
|
77
|
+
|
|
78
|
+
Rule of thumb: if tool B's input depends on tool A's output, they must be
|
|
79
|
+
sequential. Otherwise, batch them together in one response.
|
|
80
|
+
|
|
81
|
+
── Response Style ──
|
|
82
|
+
|
|
83
|
+
You are running in a terminal. Use clean plain text — never markdown.
|
|
84
|
+
• No **bold**, no ## headings, no [links](url), no \`code spans\`
|
|
85
|
+
• Use ── section separators for structure
|
|
86
|
+
• Use indentation for lists and code blocks
|
|
87
|
+
• Show file paths like this: path/to/file.ts
|
|
88
|
+
• Wrap code snippets with 2-space indent, not triple backticks
|
|
89
|
+
• Keep responses tight — skip filler and pleasantries
|
|
90
|
+
|
|
91
|
+
── Rules ──
|
|
92
|
+
|
|
93
|
+
1. Think before acting. Reason internally about the best approach.
|
|
94
|
+
2. Use bash first for exploration (ls, find, grep) before editing.
|
|
95
|
+
3. Edit precisely — minimal oldText, unique matches only.
|
|
96
|
+
4. Merge nearby edits into a single call.
|
|
97
|
+
5. Write for new files or complete rewrites only.
|
|
98
|
+
6. Stay safe. Never run rm -rf, force-push to main, etc. without confirmation.
|
|
99
|
+
7. Work in the current directory. Use relative paths.
|
|
100
|
+
8. When done, give a short summary (1-3 lines) of what you changed.
|
|
101
|
+
|
|
102
|
+
── Final Output Format ──
|
|
103
|
+
|
|
104
|
+
Your final message (when all tool calls are complete) MUST be a brief summary:
|
|
105
|
+
|
|
106
|
+
── Changes Made ──
|
|
107
|
+
• file/path.ts — what you changed (one line)
|
|
108
|
+
──
|
|
109
|
+
|
|
110
|
+
Only include files you actually modified. Do NOT include:
|
|
111
|
+
- Markdown formatting, long explanations, or step-by-step walkthroughs
|
|
112
|
+
- Tool-by-tool recaps of what commands you ran
|
|
113
|
+
- Pleasantries, filler, or "let me know if you need anything else"
|
|
114
|
+
|
|
115
|
+
Keep it tight. The user sees tool calls live — they just want the summary.`;
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface ToolCall {
|
|
2
|
+
id: string;
|
|
3
|
+
function: {
|
|
4
|
+
name: string;
|
|
5
|
+
arguments: string;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export interface ToolResult {
|
|
9
|
+
tool_call_id: string;
|
|
10
|
+
role: 'tool';
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
export type OpenAITool = {
|
|
14
|
+
type: 'function';
|
|
15
|
+
function: {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
parameters: Record<string, unknown>;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export declare const TOOL_DEFS: OpenAITool[];
|
|
22
|
+
export declare function executeTool(call: ToolCall, workDir: string): Promise<ToolResult>;
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { readFile, writeFile, mkdir, access } from 'fs/promises';
|
|
3
|
+
import { constants } from 'fs';
|
|
4
|
+
import { resolve, dirname } from 'path';
|
|
5
|
+
// ── Tool definitions ────────────────────────────────────────────────
|
|
6
|
+
export const TOOL_DEFS = [
|
|
7
|
+
{
|
|
8
|
+
type: 'function',
|
|
9
|
+
function: {
|
|
10
|
+
name: 'bash',
|
|
11
|
+
description: 'Execute a bash command in the working directory. Returns stdout and stderr. ' +
|
|
12
|
+
'Output is truncated to 2000 lines or 50KB (whichever is hit first). ' +
|
|
13
|
+
'Optionally provide a timeout in seconds (max 300s).',
|
|
14
|
+
parameters: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
command: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'Bash command to execute',
|
|
20
|
+
},
|
|
21
|
+
timeout: {
|
|
22
|
+
type: 'number',
|
|
23
|
+
description: 'Timeout in seconds (optional, max 300)',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: ['command'],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
type: 'function',
|
|
32
|
+
function: {
|
|
33
|
+
name: 'read',
|
|
34
|
+
description: 'Read the contents of a file. Supports text files. ' +
|
|
35
|
+
'Output is truncated to 2000 lines or 50KB (whichever is hit first). ' +
|
|
36
|
+
'Use offset/limit for large files. When you need the full file, continue with offset until complete.',
|
|
37
|
+
parameters: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
path: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
description: 'Path to the file to read (relative or absolute)',
|
|
43
|
+
},
|
|
44
|
+
offset: {
|
|
45
|
+
type: 'number',
|
|
46
|
+
description: 'Line number to start reading from (1-indexed)',
|
|
47
|
+
},
|
|
48
|
+
limit: {
|
|
49
|
+
type: 'number',
|
|
50
|
+
description: 'Maximum number of lines to read',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
required: ['path'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'function',
|
|
59
|
+
function: {
|
|
60
|
+
name: 'edit',
|
|
61
|
+
description: 'Edit a file using exact text replacement. Every edits[].oldText must match a unique, ' +
|
|
62
|
+
'non-overlapping region of the original file. If two changes affect the same block or ' +
|
|
63
|
+
'nearby lines, merge them into one edit. Do not include large unchanged regions.',
|
|
64
|
+
parameters: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
path: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: 'Path to the file to edit (relative or absolute)',
|
|
70
|
+
},
|
|
71
|
+
edits: {
|
|
72
|
+
type: 'array',
|
|
73
|
+
items: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
oldText: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: 'Exact text to replace (must be unique in file)',
|
|
79
|
+
},
|
|
80
|
+
newText: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
description: 'Replacement text',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
required: ['oldText', 'newText'],
|
|
86
|
+
},
|
|
87
|
+
description: 'One or more targeted replacements',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
required: ['path', 'edits'],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
type: 'function',
|
|
96
|
+
function: {
|
|
97
|
+
name: 'write',
|
|
98
|
+
description: 'Write content to a file. Creates the file if it doesn\'t exist, overwrites if it does. ' +
|
|
99
|
+
'Automatically creates parent directories.',
|
|
100
|
+
parameters: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
path: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: 'Path to the file to write (relative or absolute)',
|
|
106
|
+
},
|
|
107
|
+
content: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
description: 'Content to write to the file',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
required: ['path', 'content'],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
type: 'function',
|
|
118
|
+
function: {
|
|
119
|
+
name: 'ls',
|
|
120
|
+
description: 'List directory contents. Shows files and directories with sizes, sorted (dirs first, then files alphabetically).',
|
|
121
|
+
parameters: {
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties: {
|
|
124
|
+
path: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
description: 'Directory to list (defaults to working directory)',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
type: 'function',
|
|
134
|
+
function: {
|
|
135
|
+
name: 'find',
|
|
136
|
+
description: 'Find files by name. Supports fuzzy matching (e.g. "agentloop" matches "agent-loop.ts") ' +
|
|
137
|
+
'and glob patterns (e.g. "*.ts"). Skips hidden dirs and common large directories ' +
|
|
138
|
+
'(node_modules, .git, dist, etc.).',
|
|
139
|
+
parameters: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
properties: {
|
|
142
|
+
pattern: {
|
|
143
|
+
type: 'string',
|
|
144
|
+
description: 'Search query (fuzzy or glob like *.ts)',
|
|
145
|
+
},
|
|
146
|
+
path: {
|
|
147
|
+
type: 'string',
|
|
148
|
+
description: 'Directory to search in (defaults to working directory)',
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
required: ['pattern'],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
type: 'function',
|
|
157
|
+
function: {
|
|
158
|
+
name: 'grep',
|
|
159
|
+
description: 'Search file contents with regex. Returns matching file paths with line numbers and content. ' +
|
|
160
|
+
'Skips binary files and files over 500KB.',
|
|
161
|
+
parameters: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
pattern: {
|
|
165
|
+
type: 'string',
|
|
166
|
+
description: 'Regex pattern to search for',
|
|
167
|
+
},
|
|
168
|
+
path: {
|
|
169
|
+
type: 'string',
|
|
170
|
+
description: 'Directory or file to search in (defaults to working directory)',
|
|
171
|
+
},
|
|
172
|
+
include: {
|
|
173
|
+
type: 'string',
|
|
174
|
+
description: 'File glob filter (e.g. *.ts)',
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
required: ['pattern'],
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
// ── Tool implementations ────────────────────────────────────────────
|
|
183
|
+
const MAX_OUTPUT_LINES = 2000;
|
|
184
|
+
const MAX_OUTPUT_BYTES = 50 * 1024; // 50KB
|
|
185
|
+
function truncateOutput(text) {
|
|
186
|
+
const lines = text.split('\n');
|
|
187
|
+
const byteLen = Buffer.byteLength(text, 'utf-8');
|
|
188
|
+
if (lines.length <= MAX_OUTPUT_LINES && byteLen <= MAX_OUTPUT_BYTES) {
|
|
189
|
+
return text;
|
|
190
|
+
}
|
|
191
|
+
let truncated = '';
|
|
192
|
+
if (lines.length > MAX_OUTPUT_LINES) {
|
|
193
|
+
truncated = lines.slice(0, MAX_OUTPUT_LINES).join('\n');
|
|
194
|
+
truncated += `\n\n[Truncated: ${lines.length - MAX_OUTPUT_LINES} more lines]`;
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
truncated = text;
|
|
198
|
+
}
|
|
199
|
+
if (Buffer.byteLength(truncated, 'utf-8') > MAX_OUTPUT_BYTES) {
|
|
200
|
+
const buf = Buffer.from(truncated, 'utf-8');
|
|
201
|
+
truncated = buf.subarray(0, MAX_OUTPUT_BYTES).toString('utf-8');
|
|
202
|
+
truncated += '\n\n[Truncated: output exceeded 50KB]';
|
|
203
|
+
}
|
|
204
|
+
return truncated;
|
|
205
|
+
}
|
|
206
|
+
async function execBash(command, workDir, timeoutSec) {
|
|
207
|
+
const timeout = Math.min(timeoutSec ?? 120, 300) * 1000;
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
const child = exec(command, {
|
|
210
|
+
cwd: workDir,
|
|
211
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
212
|
+
timeout,
|
|
213
|
+
shell: '/bin/bash',
|
|
214
|
+
}, (error, stdout, stderr) => {
|
|
215
|
+
let result = '';
|
|
216
|
+
const outStr = typeof stdout === 'string' ? stdout : stdout?.toString() ?? '';
|
|
217
|
+
const errStr = typeof stderr === 'string' ? stderr : stderr?.toString() ?? '';
|
|
218
|
+
if (outStr.trim())
|
|
219
|
+
result += outStr.trim();
|
|
220
|
+
if (errStr.trim()) {
|
|
221
|
+
if (result)
|
|
222
|
+
result += '\n';
|
|
223
|
+
result += errStr.trim();
|
|
224
|
+
}
|
|
225
|
+
if (error && !result) {
|
|
226
|
+
result = error.message;
|
|
227
|
+
}
|
|
228
|
+
resolve(truncateOutput(result) || '(no output)');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
async function readFileTool(path, workDir, offset, limit) {
|
|
233
|
+
const fullPath = resolve(workDir, path);
|
|
234
|
+
// Safety: ensure path is within workDir or is a reasonable absolute path
|
|
235
|
+
try {
|
|
236
|
+
await access(fullPath, constants.R_OK);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return `Error: File not found or not readable: ${fullPath}`;
|
|
240
|
+
}
|
|
241
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
242
|
+
let lines = content.split('\n');
|
|
243
|
+
const start = offset ? offset - 1 : 0;
|
|
244
|
+
const end = limit ? start + limit : undefined;
|
|
245
|
+
lines = lines.slice(start, end);
|
|
246
|
+
let result = lines.join('\n');
|
|
247
|
+
if (result.length === 0 && content.length > 0) {
|
|
248
|
+
result = content; // fallback for non-newline files
|
|
249
|
+
}
|
|
250
|
+
return truncateOutput(result);
|
|
251
|
+
}
|
|
252
|
+
async function editFileTool(path, edits, workDir) {
|
|
253
|
+
const fullPath = resolve(workDir, path);
|
|
254
|
+
let content;
|
|
255
|
+
try {
|
|
256
|
+
content = await readFile(fullPath, 'utf-8');
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// If file doesn't exist, create it if we have exactly one "empty" edit
|
|
260
|
+
if (edits.length === 1 && edits[0].oldText === '') {
|
|
261
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
262
|
+
await writeFile(fullPath, edits[0].newText, 'utf-8');
|
|
263
|
+
return `Created new file: ${fullPath}`;
|
|
264
|
+
}
|
|
265
|
+
return `Error: File not found: ${fullPath}`;
|
|
266
|
+
}
|
|
267
|
+
let modified = content;
|
|
268
|
+
const errors = [];
|
|
269
|
+
for (const edit of edits) {
|
|
270
|
+
const { oldText, newText } = edit;
|
|
271
|
+
if (oldText === '') {
|
|
272
|
+
errors.push(`Edit with empty oldText — use write tool for new files`);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const count = modified.split(oldText).length - 1;
|
|
276
|
+
if (count === 0) {
|
|
277
|
+
errors.push(`oldText not found in file: "${oldText.slice(0, 80)}..."`);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (count > 1) {
|
|
281
|
+
errors.push(`oldText matches ${count} times (must be unique): "${oldText.slice(0, 80)}..."`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
modified = modified.replace(oldText, newText);
|
|
285
|
+
}
|
|
286
|
+
if (errors.length > 0 && modified === content) {
|
|
287
|
+
return `Edit failed:\n${errors.map((e) => ` - ${e}`).join('\n')}`;
|
|
288
|
+
}
|
|
289
|
+
await writeFile(fullPath, modified, 'utf-8');
|
|
290
|
+
const result = errors.length > 0
|
|
291
|
+
? `File edited with warnings:\n${errors.map((e) => ` - ${e}`).join('\n')}`
|
|
292
|
+
: `File edited successfully: ${fullPath}`;
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
async function writeFileTool(path, content, workDir) {
|
|
296
|
+
const fullPath = resolve(workDir, path);
|
|
297
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
298
|
+
await writeFile(fullPath, content, 'utf-8');
|
|
299
|
+
return `Successfully wrote ${Buffer.byteLength(content, 'utf-8')} bytes to ${fullPath}`;
|
|
300
|
+
}
|
|
301
|
+
// ── New tools: ls, find, grep ──────────────────────────────────────
|
|
302
|
+
async function lsTool(path, workDir) {
|
|
303
|
+
const dir = resolve(workDir, path || '.');
|
|
304
|
+
const { readdir, stat } = await import('fs/promises');
|
|
305
|
+
try {
|
|
306
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
307
|
+
if (entries.length === 0)
|
|
308
|
+
return `(empty directory: ${dir})`;
|
|
309
|
+
// Get sizes and sort: dirs first, then files alphabetically
|
|
310
|
+
const withMeta = await Promise.all(entries.map(async (e) => {
|
|
311
|
+
const full = resolve(dir, e.name);
|
|
312
|
+
let size = '';
|
|
313
|
+
try {
|
|
314
|
+
const s = await stat(full);
|
|
315
|
+
if (s.isFile()) {
|
|
316
|
+
const kb = s.size / 1024;
|
|
317
|
+
size = kb >= 1000 ? `${(kb / 1024).toFixed(1)}M` : kb >= 1 ? `${kb.toFixed(0)}K` : `${s.size}B`;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch { /* skip */ }
|
|
321
|
+
return {
|
|
322
|
+
name: e.name,
|
|
323
|
+
isDir: e.isDirectory(),
|
|
324
|
+
size,
|
|
325
|
+
};
|
|
326
|
+
}));
|
|
327
|
+
withMeta.sort((a, b) => {
|
|
328
|
+
if (a.isDir !== b.isDir)
|
|
329
|
+
return a.isDir ? -1 : 1;
|
|
330
|
+
return a.name.localeCompare(b.name);
|
|
331
|
+
});
|
|
332
|
+
const lines = withMeta.map((e) => {
|
|
333
|
+
const type = e.isDir ? '/' : '';
|
|
334
|
+
const sizeStr = e.size ? ` (${e.size})` : '';
|
|
335
|
+
return `${e.name}${type}${sizeStr}`;
|
|
336
|
+
});
|
|
337
|
+
return truncateOutput(`${dir}:\n${lines.join('\n')}`);
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
return `Error listing ${dir}: ${err.message}`;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function findTool(pattern, searchPath, workDir) {
|
|
344
|
+
const base = resolve(workDir, searchPath || '.');
|
|
345
|
+
const SKIP_DIRS = new Set([
|
|
346
|
+
'node_modules', '.git', 'dist', 'build', '__pycache__', '.venv', 'venv',
|
|
347
|
+
'.next', '.cache', 'coverage', '.convex', '_generated',
|
|
348
|
+
]);
|
|
349
|
+
// Detect if pattern looks like a glob
|
|
350
|
+
const isGlob = /[*?[\]{}]/.test(pattern);
|
|
351
|
+
const results = [];
|
|
352
|
+
const MAX_RESULTS = 200;
|
|
353
|
+
async function walk(dir, depth) {
|
|
354
|
+
if (depth > 8 || results.length >= MAX_RESULTS)
|
|
355
|
+
return;
|
|
356
|
+
let entries;
|
|
357
|
+
try {
|
|
358
|
+
const { readdir } = await import('fs/promises');
|
|
359
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
for (const e of entries) {
|
|
365
|
+
if (results.length >= MAX_RESULTS)
|
|
366
|
+
break;
|
|
367
|
+
if (e.isDirectory()) {
|
|
368
|
+
if (SKIP_DIRS.has(e.name) || e.name.startsWith('.'))
|
|
369
|
+
continue;
|
|
370
|
+
await walk(resolve(dir, e.name), depth + 1);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
if (isGlob) {
|
|
374
|
+
// Simple glob matching
|
|
375
|
+
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
|
|
376
|
+
if (regex.test(e.name)) {
|
|
377
|
+
results.push(resolve(dir, e.name).replace(base + '/', ''));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
// Fuzzy matching: all pattern chars appear in order in filename
|
|
382
|
+
const lower = e.name.toLowerCase();
|
|
383
|
+
const pat = pattern.toLowerCase();
|
|
384
|
+
let pi = 0;
|
|
385
|
+
for (let i = 0; i < lower.length && pi < pat.length; i++) {
|
|
386
|
+
if (lower[i] === pat[pi])
|
|
387
|
+
pi++;
|
|
388
|
+
}
|
|
389
|
+
if (pi === pat.length) {
|
|
390
|
+
results.push(resolve(dir, e.name).replace(base + '/', ''));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
await walk(base, 0);
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
return `Error finding files: ${err.message}`;
|
|
401
|
+
}
|
|
402
|
+
if (results.length === 0)
|
|
403
|
+
return `No files matching "${pattern}" found.`;
|
|
404
|
+
return truncateOutput(`Found ${results.length} file${results.length !== 1 ? 's' : ''} matching "${pattern}":\n${results.join('\n')}`);
|
|
405
|
+
}
|
|
406
|
+
async function grepTool(pattern, searchPath, include, workDir) {
|
|
407
|
+
const base = resolve(workDir, searchPath || '.');
|
|
408
|
+
const SKIP_DIRS = new Set([
|
|
409
|
+
'node_modules', '.git', 'dist', 'build', '__pycache__', '.venv', 'venv',
|
|
410
|
+
'.next', '.cache', '.convex', '_generated',
|
|
411
|
+
]);
|
|
412
|
+
const MAX_FILE_SIZE = 500 * 1024;
|
|
413
|
+
const MAX_MATCHES = 100;
|
|
414
|
+
let regex;
|
|
415
|
+
try {
|
|
416
|
+
regex = new RegExp(pattern, 'gi');
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
return `Error: Invalid regex pattern: ${err.message}`;
|
|
420
|
+
}
|
|
421
|
+
const results = [];
|
|
422
|
+
async function walk(dir, depth) {
|
|
423
|
+
if (depth > 8 || results.length >= MAX_MATCHES)
|
|
424
|
+
return;
|
|
425
|
+
let entries;
|
|
426
|
+
try {
|
|
427
|
+
const { readdir, stat } = await import('fs/promises');
|
|
428
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
for (const e of entries) {
|
|
434
|
+
if (results.length >= MAX_MATCHES)
|
|
435
|
+
break;
|
|
436
|
+
const full = resolve(dir, e.name);
|
|
437
|
+
if (e.isDirectory()) {
|
|
438
|
+
if (SKIP_DIRS.has(e.name) || e.name.startsWith('.'))
|
|
439
|
+
continue;
|
|
440
|
+
await walk(full, depth + 1);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
// Include filter
|
|
444
|
+
if (include) {
|
|
445
|
+
const incRegex = new RegExp('^' + include.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
|
|
446
|
+
if (!incRegex.test(e.name))
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
// Size check
|
|
450
|
+
try {
|
|
451
|
+
const { stat } = await import('fs/promises');
|
|
452
|
+
const s = await stat(full);
|
|
453
|
+
if (s.size > MAX_FILE_SIZE)
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
// Read and search
|
|
460
|
+
try {
|
|
461
|
+
const content = await readFile(full, 'utf-8');
|
|
462
|
+
const lines = content.split('\n');
|
|
463
|
+
for (let i = 0; i < lines.length && results.length < MAX_MATCHES; i++) {
|
|
464
|
+
if (regex.test(lines[i])) {
|
|
465
|
+
regex.lastIndex = 0;
|
|
466
|
+
const relPath = full.replace(base + '/', '');
|
|
467
|
+
results.push(`${relPath}:${i + 1}: ${lines[i].trim().slice(0, 120)}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
// Skip binary/unreadable files
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const s = await (await import('fs/promises')).stat(base);
|
|
479
|
+
if (s.isFile()) {
|
|
480
|
+
// Single file mode
|
|
481
|
+
const content = await readFile(base, 'utf-8');
|
|
482
|
+
const lines = content.split('\n');
|
|
483
|
+
for (let i = 0; i < lines.length && results.length < MAX_MATCHES; i++) {
|
|
484
|
+
if (regex.test(lines[i])) {
|
|
485
|
+
regex.lastIndex = 0;
|
|
486
|
+
results.push(`L${i + 1}: ${lines[i].trim().slice(0, 120)}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
await walk(base, 0);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
return `Error searching: ${err.message}`;
|
|
496
|
+
}
|
|
497
|
+
if (results.length === 0)
|
|
498
|
+
return `No matches for "${pattern}" found.`;
|
|
499
|
+
return truncateOutput(`${results.length} match${results.length !== 1 ? 'es' : ''} for "${pattern}":\n${results.join('\n')}`);
|
|
500
|
+
}
|
|
501
|
+
// ── Tool dispatcher ─────────────────────────────────────────────────
|
|
502
|
+
export async function executeTool(call, workDir) {
|
|
503
|
+
const { id, function: fn } = call;
|
|
504
|
+
let args;
|
|
505
|
+
try {
|
|
506
|
+
args = JSON.parse(fn.arguments);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
return {
|
|
510
|
+
tool_call_id: id,
|
|
511
|
+
role: 'tool',
|
|
512
|
+
content: `Error: Invalid JSON arguments: ${fn.arguments}`,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
let result;
|
|
516
|
+
try {
|
|
517
|
+
switch (fn.name) {
|
|
518
|
+
case 'bash':
|
|
519
|
+
result = await execBash(args.command, workDir, args.timeout);
|
|
520
|
+
break;
|
|
521
|
+
case 'read':
|
|
522
|
+
result = await readFileTool(args.path, workDir, args.offset, args.limit);
|
|
523
|
+
break;
|
|
524
|
+
case 'edit':
|
|
525
|
+
result = await editFileTool(args.path, args.edits, workDir);
|
|
526
|
+
break;
|
|
527
|
+
case 'write':
|
|
528
|
+
result = await writeFileTool(args.path, args.content, workDir);
|
|
529
|
+
break;
|
|
530
|
+
case 'ls':
|
|
531
|
+
result = await lsTool(args.path, workDir);
|
|
532
|
+
break;
|
|
533
|
+
case 'find':
|
|
534
|
+
result = await findTool(args.pattern, args.path, workDir);
|
|
535
|
+
break;
|
|
536
|
+
case 'grep':
|
|
537
|
+
result = await grepTool(args.pattern, args.path, args.include, workDir);
|
|
538
|
+
break;
|
|
539
|
+
default:
|
|
540
|
+
result = `Error: Unknown tool: ${fn.name}`;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch (err) {
|
|
544
|
+
result = `Error executing ${fn.name}: ${err instanceof Error ? err.message : String(err)}`;
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
tool_call_id: id,
|
|
548
|
+
role: 'tool',
|
|
549
|
+
content: result,
|
|
550
|
+
};
|
|
551
|
+
}
|