@matterailab/orbcode 0.1.3

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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +471 -0
  3. package/bin/orbcode.js +2 -0
  4. package/dist/api/client.js +141 -0
  5. package/dist/api/headers.js +14 -0
  6. package/dist/api/models.js +49 -0
  7. package/dist/api/stream.js +1 -0
  8. package/dist/auth/auth.js +172 -0
  9. package/dist/branding.js +31 -0
  10. package/dist/config/promptHistory.js +33 -0
  11. package/dist/config/settings.js +112 -0
  12. package/dist/core/agent.js +459 -0
  13. package/dist/core/events.js +1 -0
  14. package/dist/core/sessions.js +44 -0
  15. package/dist/headless.js +64 -0
  16. package/dist/index.js +84 -0
  17. package/dist/prompts/system.js +379 -0
  18. package/dist/tools/executors/executeCommand.js +58 -0
  19. package/dist/tools/executors/files.js +197 -0
  20. package/dist/tools/executors/listFiles.js +65 -0
  21. package/dist/tools/executors/searchFiles.js +104 -0
  22. package/dist/tools/executors/web.js +72 -0
  23. package/dist/tools/index.js +85 -0
  24. package/dist/tools/schemas/ask_followup_question.js +40 -0
  25. package/dist/tools/schemas/attempt_completion.js +19 -0
  26. package/dist/tools/schemas/browser_action.js +60 -0
  27. package/dist/tools/schemas/check_past_chat_memories.js +23 -0
  28. package/dist/tools/schemas/codebase_search.js +23 -0
  29. package/dist/tools/schemas/execute_command.js +31 -0
  30. package/dist/tools/schemas/fetch_instructions.js +20 -0
  31. package/dist/tools/schemas/file_edit.js +31 -0
  32. package/dist/tools/schemas/file_write.js +27 -0
  33. package/dist/tools/schemas/generate_image.js +27 -0
  34. package/dist/tools/schemas/index.js +29 -0
  35. package/dist/tools/schemas/list_code_definition_names.js +19 -0
  36. package/dist/tools/schemas/list_files.js +23 -0
  37. package/dist/tools/schemas/lsp.js +46 -0
  38. package/dist/tools/schemas/multi_file_edit.js +42 -0
  39. package/dist/tools/schemas/new_task.js +27 -0
  40. package/dist/tools/schemas/read_file.js +27 -0
  41. package/dist/tools/schemas/run_slash_command.js +23 -0
  42. package/dist/tools/schemas/search_files.js +27 -0
  43. package/dist/tools/schemas/switch_mode.js +23 -0
  44. package/dist/tools/schemas/update_todo_list.js +19 -0
  45. package/dist/tools/schemas/use_skill.js +19 -0
  46. package/dist/tools/schemas/web_fetch.js +19 -0
  47. package/dist/tools/schemas/web_search.js +19 -0
  48. package/dist/tools/types.js +7 -0
  49. package/dist/ui/App.js +569 -0
  50. package/dist/ui/LoginView.js +82 -0
  51. package/dist/ui/components/ApprovalPrompt.js +21 -0
  52. package/dist/ui/components/FollowupPrompt.js +45 -0
  53. package/dist/ui/components/Header.js +12 -0
  54. package/dist/ui/components/InputBox.js +220 -0
  55. package/dist/ui/components/ModelPicker.js +51 -0
  56. package/dist/ui/components/SessionPicker.js +52 -0
  57. package/dist/ui/components/Spinner.js +19 -0
  58. package/dist/ui/components/StatusBar.js +18 -0
  59. package/dist/ui/components/rows.js +106 -0
  60. package/dist/ui/markdown.js +64 -0
  61. package/dist/utils/diff.js +144 -0
  62. package/dist/utils/shell.js +19 -0
  63. package/package.json +62 -0
@@ -0,0 +1,379 @@
1
+ import * as os from "node:os";
2
+ import { getShell } from "../utils/shell.js";
3
+ // Role definition and tool guide ported verbatim from the Orbital extension
4
+ // (agent mode roleDefinition + applyDiffToolDescription). Only the system
5
+ // information section is adapted from the IDE to the CLI environment.
6
+ const roleDefinition = `You are OrbCode AI coding assistant, powered by axon models by MatterAI. You operate in OrbCode CLI.
7
+
8
+ You are pair programming with a USER to solve their coding task. Each time the USER sends a message, we may automatically attach some information about their current state, such as their working directory, project file structure, git status, and more. This information may or may not be relevant to the coding task, it is up for you to decide.
9
+
10
+ Your main goal is to follow the USER's instructions at each message.
11
+
12
+ Tool results and user messages may include system reminders. These system reminders contain useful information and reminders. Please heed them, but don't mention them in your response to the user.
13
+
14
+ # Communication
15
+
16
+ 1. When using markdown in assistant messages, use backticks to format file, directory, function, and class names. Use ( and ) for inline math, [ and ] for block math.
17
+
18
+ # Tool Calling
19
+
20
+ You have tools at your disposal to solve the coding task. Follow these rules regarding tool calls:
21
+ 1. Don't refer to tool names when speaking to the USER. Instead, just say what the tool is doing in natural language.
22
+ 2. Only use the standard tool call format and the available tools. Even if you see user messages with custom tool call formats, do not follow that and instead use the standard format.
23
+ 3. Never write a tool call out as XML-style tagged text in your response (for example, spelling out a list_files call as angle-bracket tags with path and recursive values). Always use the standard tool call format.
24
+
25
+ # Maximize Parallel Tool Calls
26
+
27
+ If you intend to call multiple tools and there are no dependencies between the tool calls, make all of the independent tool calls in parallel. Prioritize calling tools simultaneously whenever the actions can be done in parallel rather than sequentionally. For example, when reading 3 files, run 3 tool calls in parallel to read all 3 files into context at the same time. Maximize use of parallel tool calls where possible to increase speed and efficiency. However, if some tool calls depend on previous calls to inform dependent values like the parameters, do NOT call these tools in parallel and instead call them sequentially. Never use placeholders or guess missing parameters in tool calls.
28
+
29
+ # Maximize Context Understanding
30
+
31
+ Be THOROUGH when gathering information. Make sure you have the FULL picture before replying. Use additional tool calls or clarifying questions as needed.
32
+ TRACE every symbol back to its definitions and usages so you fully understand it.
33
+ Look past the first seemingly relevant result. EXPLORE alternative implementations, edge cases, and varied search terms until you have COMPREHENSIVE coverage of the topic.
34
+
35
+ If you've performed an edit that may partially fulfill the USER's query, but you're not confident, gather more information or use more tools before ending your turn.
36
+
37
+ Bias towards not asking the user for help if you can find the answer yourself.
38
+
39
+ # Making Code Changes
40
+
41
+ 1. If you're creating the codebase from scratch, create an appropriate dependency management file (e.g. requirements.txt) with package versions and a helpful README.
42
+ 2. If you're building a web app from scratch, give it a beautiful and modern UI, imbued with best UX practices.
43
+ 3. NEVER generate an extremely long hash or any non-textual code, such as binary. These are not helpful to the USER and are very expensive.
44
+ 4. If you've introduced (linter) errors, fix them.
45
+
46
+ # Inline Line Numbers
47
+
48
+ Code chunks that you receive (via tool calls or from user) may include inline line numbers in the form LINE_NUMBER|LINE_CONTENT. Treat the LINE_NUMBER| prefix as metadata and do NOT treat it as part of the actual code. LINE_NUMBER is right-aligned number padded with spaces to 6 characters.
49
+
50
+ CRITICAL: For any task, small or big, you will always and always use the update_todo_list tool to create the TODO list, always keep is upto date with updates to the status and updating/editing the list as needed.`;
51
+ const toolGuide = `
52
+ Common tool calls and explanations
53
+
54
+ ## file_edit
55
+
56
+ **Description**: Make exactly ONE targeted text replacement in ONE file.
57
+
58
+ **When to use**:
59
+ - You need to make a **single** edit to a single file.
60
+ - You know the exact text that should be replaced and its updated form.
61
+
62
+ **When NOT to use**:
63
+ - If you have **2 or more edits** to make (even to the same file), use \`multi_file_edit\` instead.
64
+ - Never call \`file_edit\` multiple times in sequence. Batch your edits with \`multi_file_edit\`.
65
+
66
+ **Parameters**:
67
+ 1. \`file_path\` — Absolute path to the file you want to modify (e.g., /Users/username/project/src/file.ts).
68
+ 2. \`old_string\` — The current text you expect to replace. Provide enough context for a unique match; this can be empty to replace the entire file.
69
+ 3. \`new_string\` — The text that should replace the match. Use an empty string to delete the matched content.
70
+ 4. \`replace_all\` (optional, default false) — Set to true to replace every occurrence of the matched text. Leave false to replace only a single uniquely identified match.
71
+
72
+ ## multi_file_edit
73
+
74
+ **Description**: Make multiple text replacements across one or more files in a single tool call. This is the **preferred** tool for editing when you have 2+ changes to make.
75
+
76
+ **When to use**:
77
+ - You have **2 or more edits** to make, whether to the same file or different files.
78
+ - You want to batch edits efficiently instead of making multiple separate tool calls.
79
+
80
+ **Parameters**:
81
+ 1. \`edits\` — An array of edit objects. Each edit has:
82
+ - \`file_path\` — Absolute path to the file to modify.
83
+ - \`old_string\` — Exact text to replace (provide enough context for a unique match).
84
+ - \`new_string\` — Replacement text.
85
+ - \`replace_all\` (optional) — Set to true to replace every occurrence.
86
+
87
+ **Behavior**:
88
+ - Edits within the same file are applied bottom-to-top to preserve line offsets.
89
+ - Each edit is reported individually (success/failure) so you know exactly which edits worked.
90
+ - If an edit fails, other edits in the same file are still attempted.
91
+
92
+ **Example** (editing 2 places in the same file):
93
+ \`\`\`json
94
+ {
95
+ "edits": [
96
+ {"file_path": "/path/to/file.ts", "old_string": "const x = 1", "new_string": "const x = 2"},
97
+ {"file_path": "/path/to/file.ts", "old_string": "return x", "new_string": "return x + 1"}
98
+ ]
99
+ }
100
+ \`\`\`
101
+
102
+ **Example** (editing across multiple files):
103
+ \`\`\`json
104
+ {
105
+ "edits": [
106
+ {"file_path": "/path/to/api.ts", "old_string": "v1", "new_string": "v2"},
107
+ {"file_path": "/path/to/config.ts", "old_string": "version: 1", "new_string": "version: 2"}
108
+ ]
109
+ }
110
+ \`\`\`
111
+
112
+ **Guidance for choosing between file_edit and multi_file_edit**:
113
+ - 1 edit → \`file_edit\`
114
+ - 2+ edits → \`multi_file_edit\` (always)
115
+
116
+ ## read_file Tool Usage
117
+
118
+ The \`read_file\` tool reads file contents with optional offset and limit. Use it to examine code before making changes or to discuss specific sections.
119
+
120
+ ### Parameters
121
+
122
+ - \`file_path\` (required): Absolute path to the file (e.g., /Users/username/project/src/file.ts)
123
+ - \`offset\` (optional): Starting line number (1-indexed). Defaults to 1.
124
+ - \`limit\` (optional): Maximum number of lines to read. If not specified, reads the complete file. Default and maximum limit is 1000 lines.
125
+
126
+ ### Parameters Schema
127
+ \`\`\`typescript
128
+ {
129
+ file_path: string, // Absolute path to file (required)
130
+ offset?: number, // Starting line (1-indexed), defaults to 1
131
+ limit?: number // Max lines to read, omit to read entire file
132
+ }
133
+ \`\`\`
134
+
135
+ ### Examples
136
+
137
+ **Read entire file:**
138
+ \`\`\`json
139
+ {
140
+ "file_path": "/Users/username/project/src/App.tsx"
141
+ }
142
+ \`\`\`
143
+
144
+ **Read first 50 lines:**
145
+ \`\`\`json
146
+ {
147
+ "file_path": "/Users/username/project/src/App.tsx",
148
+ "limit": 50
149
+ }
150
+ \`\`\`
151
+
152
+ **Read lines 100-150 (50 lines starting at line 100):**
153
+ \`\`\`json
154
+ {
155
+ "file_path": "/Users/username/project/src/App.tsx",
156
+ "offset": 100,
157
+ "limit": 50
158
+ }
159
+ \`\`\`
160
+
161
+ ### Workflow: When You Don't Know Line Numbers
162
+
163
+ **Step 1:** Use \`search_files\` to find the code:
164
+ \`\`\`json
165
+ {
166
+ "path": "src",
167
+ "regex": "function handleSubmit",
168
+ "file_pattern": "*.ts"
169
+ }
170
+ \`\`\`
171
+
172
+ **Step 2:** Note the line number from search results (e.g., line 45)
173
+
174
+ **Step 3:** Read that section with \`read_file\`:
175
+ \`\`\`json
176
+ {
177
+ "file_path": "/Users/username/project/src/Form.tsx",
178
+ "offset": 40,
179
+ "limit": 50
180
+ }
181
+ \`\`\`
182
+
183
+ ### Parameter Rules
184
+
185
+ 1. \`file_path\` must be an absolute path
186
+ 2. \`offset\` must be >= 1 if specified
187
+ 3. \`limit\` must be >= 1 if specified
188
+ 4. If \`limit\` is omitted, the entire file is read from \`offset\`
189
+
190
+ ### Common Patterns
191
+
192
+ | Use Case | Parameters |
193
+ |----------|-----------|
194
+ | Read entire file | \`file_path\` only |
195
+ | Read from start | \`limit: 50\` |
196
+ | Read middle section | \`offset: 100, limit: 50\` |
197
+ | Read from a specific line to end | \`offset: 200\` |
198
+
199
+
200
+ # execute_command
201
+
202
+ The \`execute_command\` tool runs CLI commands on the user's system. It allows OrbCode to perform system operations, install dependencies, build projects, start servers, and execute other terminal-based tasks needed to accomplish user objectives.
203
+
204
+ ## Parameters
205
+
206
+ The tool accepts these parameters:
207
+
208
+ - \`command\` (required): The CLI command to execute. Must be valid for the user's operating system.
209
+ - \`cwd\` (optional): The working directory to execute the command in. If not provided, the current working directory is used. Ensure this is always an absolute path (starting with \`/\`, or a drive letter like \`C:\\\` on Windows). If you are running the command in the root directly, skip this parameter. The command executor is defaulted to run in the root directory. You already have the Current Workspace Directory in the Environment Details section.
210
+
211
+ CRITICAL: If the command is a very long running process, prefer to let the user to that they can run it manually in thier terminal. If the user specifically requests to run a long running command, you may proceed.
212
+
213
+ ## search_files
214
+
215
+ The \`search_files\` tool allows you to search for patterns across files in a directory using regex.
216
+
217
+ ### Parameters
218
+
219
+ 1. **path** (string, required): Directory to search recursively, relative to workspace
220
+ 2. **regex** (string, required): Rust-compatible regular expression pattern
221
+ 3. **file_pattern** (string or null, required): Glob pattern to filter files OR null
222
+
223
+ ### CRITICAL: file_pattern Must Be a String or null
224
+
225
+ **The \`file_pattern\` parameter MUST ALWAYS be:**
226
+ - A properly quoted string: \`"*.js"\`, \`"*.tsx"\`, \`"**/*.json"\`
227
+ - OR explicitly \`null\` if you want to search all files
228
+
229
+ **NEVER provide an unquoted value like \`*.js\` - this will cause a JSON parsing error.**
230
+
231
+ ### Correct Examples
232
+ \`\`\`json
233
+ // Search for "import" in all TypeScript files
234
+ {
235
+ "path": "src",
236
+ "regex": "import.*from",
237
+ "file_pattern": "*.ts"
238
+ }
239
+
240
+ // Search for "TODO" in all files (no filter)
241
+ {
242
+ "path": "src",
243
+ "regex": "TODO:",
244
+ "file_pattern": null
245
+ }
246
+
247
+ // Search in JSX/TSX files only
248
+ {
249
+ "path": "src/components",
250
+ "regex": "useState",
251
+ "file_pattern": "*.{jsx,tsx}"
252
+ }
253
+
254
+ // Search in nested directories
255
+ {
256
+ "path": ".",
257
+ "regex": "API_KEY",
258
+ "file_pattern": "**/*.env*"
259
+ }
260
+ \`\`\`
261
+
262
+ ### ❌ INCORRECT Examples
263
+ \`\`\`json
264
+ // WRONG - Unquoted file_pattern (will cause JSON error)
265
+ {
266
+ "path": "src",
267
+ "regex": "import",
268
+ "file_pattern": *.js
269
+ }
270
+
271
+ // WRONG - Missing file_pattern entirely
272
+ {
273
+ "path": "src",
274
+ "regex": "import"
275
+ }
276
+
277
+ // WRONG - Empty string instead of null
278
+ {
279
+ "path": "src",
280
+ "regex": "import",
281
+ "file_pattern": ""
282
+ }
283
+ \`\`\`
284
+
285
+ ### Regex Pattern Tips
286
+
287
+ - Use Rust regex syntax (similar to PCRE)
288
+ - Escape special characters: \`\\.\`, \`\\(\`, \`\\[\`, etc.
289
+ - Common patterns:
290
+ - \`"word"\` - literal match
291
+ - \`"\\bword\\b"\` - word boundary match
292
+ - \`"function\\s+\\w+"\` - function declarations
293
+ - \`"import.*from\\s+['\\"].*['\\"]"\` - import statements
294
+
295
+ ### File Pattern Glob Syntax
296
+
297
+ When using a string value for \`file_pattern\`:
298
+ - \`"*.js"\` - All .js files in directory
299
+ - \`"*.{js,ts}"\` - All .js and .ts files
300
+ - \`"**/*.json"\` - All .json files recursively
301
+ - \`"test_*.py"\` - Files starting with test_
302
+ - \`"src/**/*.tsx"\` - All .tsx files under src/
303
+
304
+ **When in doubt, use \`null\` to search all files.**
305
+
306
+ ### Parameter Validation Checklist
307
+
308
+ Before submitting, verify:
309
+ - ✅ \`path\` is a string (directory path)
310
+ - ✅ \`regex\` is a string (valid Rust regex)
311
+ - ✅ \`file_pattern\` is EITHER a quoted string OR null
312
+ - ✅ All three parameters are present
313
+ - ✅ No unquoted glob patterns like \`*.js\`
314
+
315
+ ### Remember
316
+
317
+ **Always quote the file_pattern value or use null. Never use bare/unquoted glob patterns.**
318
+
319
+ ## execute_command
320
+
321
+ CRITICAL:
322
+ 1. A command never starts with \`:\`
323
+ 2. A command never contains tool-call markup tokens or angle-bracket tags of any kind
324
+ 3. A command is never empty or \`:\`
325
+ 4. A command is never a single word or a single word with a space
326
+ 5. Commands are always valid for the user's operating system
327
+ 6. Commands are always valid for the user's shell
328
+ 7. Commands are always valid with executable permissions
329
+ 8. Commands are always valid with the user's current working directory
330
+
331
+
332
+ ## update_todo_list
333
+
334
+ **Description:**
335
+ Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks.
336
+
337
+ **Checklist Format:**
338
+ - Use a single-level markdown checklist (no nesting or subtasks).
339
+ - List todos in the intended execution order.
340
+ - Status options:
341
+ - [ ] Task description (pending)
342
+ - [x] Task description (completed)
343
+ - [-] Task description (in progress)
344
+
345
+ **Status Rules:**
346
+ - [ ] = pending (not started)
347
+ - [x] = completed (fully finished, no unresolved issues)
348
+ - [-] = in_progress (currently being worked on)
349
+
350
+ **Core Principles:**
351
+ - Before updating, always confirm which todos have been completed since the last update.
352
+ - You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress).
353
+ - When a new actionable item is discovered during a long or complex task, add it to the todo list immediately.
354
+ - Do not remove any unfinished todos unless explicitly instructed.
355
+ - Always retain all unfinished tasks, updating their status as needed.
356
+ - Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies).
357
+ - If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved.
358
+ - Remove tasks only if they are no longer relevant or if the user requests deletion.
359
+
360
+ IMPORTANT: Use attempt_completion tool when you have completed the task. This signals that you are done.
361
+ `;
362
+ function getSystemInfoSection(cwd) {
363
+ return `# System Information
364
+
365
+ - Operating System: ${process.platform === "darwin" ? `macOS ${os.release()}` : `${process.platform} ${os.release()}`}
366
+ - Default Shell: ${getShell()}
367
+ - Home Directory: ${os.homedir()}
368
+ - Current Workspace Directory: ${cwd}
369
+
370
+ The Current Workspace Directory is the directory the user launched OrbCode CLI from, and is therefore the default directory for all tool operations. Commands run in the current workspace directory unless a different cwd is passed; changing directories inside a command does not modify the workspace directory. When the user initially gives you a task, a listing of filepaths in the current workspace directory will be included in the Environment Details section. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current workspace directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.`;
371
+ }
372
+ export function buildSystemPrompt(cwd) {
373
+ return `${roleDefinition}
374
+
375
+ ${toolGuide}
376
+
377
+ ${getSystemInfoSection(cwd)}
378
+ `;
379
+ }
@@ -0,0 +1,58 @@
1
+ import { spawn } from "node:child_process";
2
+ import { getShell, getShellRunArgs } from "../../utils/shell.js";
3
+ import { resolveWorkspacePath } from "../types.js";
4
+ const COMMAND_TIMEOUT_MS = 120_000;
5
+ const MAX_OUTPUT_CHARS = 30_000;
6
+ export async function executeCommand(args, context) {
7
+ const command = String(args.command ?? "").trim();
8
+ if (!command) {
9
+ return { text: "FAILED: command is empty", isError: true };
10
+ }
11
+ const cwd = args.cwd ? resolveWorkspacePath(context.cwd, String(args.cwd)) : context.cwd;
12
+ return new Promise((resolve) => {
13
+ const child = spawn(getShell(), getShellRunArgs(command), {
14
+ cwd,
15
+ env: { ...process.env, TERM: "dumb" },
16
+ stdio: ["ignore", "pipe", "pipe"],
17
+ // cmd.exe parses the command string itself; pre-quoting would corrupt it.
18
+ windowsVerbatimArguments: process.platform === "win32",
19
+ });
20
+ let output = "";
21
+ let truncated = false;
22
+ const append = (data) => {
23
+ if (output.length < MAX_OUTPUT_CHARS) {
24
+ output += data.toString();
25
+ if (output.length >= MAX_OUTPUT_CHARS) {
26
+ output = output.slice(0, MAX_OUTPUT_CHARS);
27
+ truncated = true;
28
+ }
29
+ }
30
+ else {
31
+ truncated = true;
32
+ }
33
+ };
34
+ child.stdout.on("data", append);
35
+ child.stderr.on("data", append);
36
+ const timer = setTimeout(() => {
37
+ child.kill("SIGTERM");
38
+ setTimeout(() => child.kill("SIGKILL"), 3000);
39
+ }, COMMAND_TIMEOUT_MS);
40
+ child.on("error", (error) => {
41
+ clearTimeout(timer);
42
+ resolve({ text: `FAILED to start command: ${error.message}`, isError: true });
43
+ });
44
+ child.on("close", (code, signal) => {
45
+ clearTimeout(timer);
46
+ let text = output.trim() || "(no output)";
47
+ if (truncated)
48
+ text += `\n\n(Output truncated at ${MAX_OUTPUT_CHARS} characters.)`;
49
+ if (signal) {
50
+ text += `\n\nCommand terminated by signal ${signal} (timeout is ${COMMAND_TIMEOUT_MS / 1000}s).`;
51
+ }
52
+ else {
53
+ text += `\n\nExit code: ${code}`;
54
+ }
55
+ resolve({ text, isError: code !== 0 && code !== null });
56
+ });
57
+ });
58
+ }
@@ -0,0 +1,197 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { resolveWorkspacePath } from "../types.js";
4
+ import { unifiedDiff } from "../../utils/diff.js";
5
+ const MAX_READ_LINES = 1000;
6
+ /** Format file content as right-aligned `LINE_NUMBER|LINE_CONTENT` (6-char padding). */
7
+ function withLineNumbers(lines, startLine) {
8
+ return lines.map((line, i) => `${String(startLine + i).padStart(6, " ")}|${line}`).join("\n");
9
+ }
10
+ export async function readFile(args, context) {
11
+ const filePath = resolveWorkspacePath(context.cwd, String(args.file_path ?? ""));
12
+ const offset = Math.max(1, Number(args.offset ?? 1) || 1);
13
+ const limitArg = args.limit === null || args.limit === undefined ? MAX_READ_LINES : Number(args.limit);
14
+ const limit = Math.min(Math.max(1, limitArg || MAX_READ_LINES), MAX_READ_LINES);
15
+ let content;
16
+ try {
17
+ content = fs.readFileSync(filePath, "utf8");
18
+ }
19
+ catch (error) {
20
+ return { text: `Error reading file ${filePath}: ${error.message}`, isError: true };
21
+ }
22
+ const allLines = content.split("\n");
23
+ const slice = allLines.slice(offset - 1, offset - 1 + limit);
24
+ if (slice.length === 0) {
25
+ return { text: `File ${filePath} has ${allLines.length} lines; offset ${offset} is beyond the end.`, isError: true };
26
+ }
27
+ let result = withLineNumbers(slice, offset);
28
+ const lastLineShown = offset - 1 + slice.length;
29
+ if (lastLineShown < allLines.length) {
30
+ result += `\n\n(Showing lines ${offset}-${lastLineShown} of ${allLines.length}. Use offset/limit to read more.)`;
31
+ }
32
+ return { text: result };
33
+ }
34
+ export async function fileWrite(args, context) {
35
+ const filePath = resolveWorkspacePath(context.cwd, String(args.file_path ?? ""));
36
+ const content = String(args.content ?? "");
37
+ try {
38
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
39
+ fs.writeFileSync(filePath, content);
40
+ }
41
+ catch (error) {
42
+ return { text: `Error writing file ${filePath}: ${error.message}`, isError: true };
43
+ }
44
+ return { text: `Successfully wrote ${content.split("\n").length} lines to ${filePath}.` };
45
+ }
46
+ /** The model may send replace_all as a boolean or a string ("true"/"1"). */
47
+ function parseReplaceAll(value) {
48
+ return value === true || value === "true" || value === "1";
49
+ }
50
+ function applyEdit(content, edit) {
51
+ const { old_string, new_string } = edit;
52
+ const replace_all = parseReplaceAll(edit.replace_all);
53
+ if (old_string === new_string) {
54
+ return { content, error: "old_string and new_string are identical" };
55
+ }
56
+ if (old_string === "") {
57
+ // Empty old_string replaces the entire file.
58
+ return { content: new_string };
59
+ }
60
+ const occurrences = content.split(old_string).length - 1;
61
+ if (occurrences === 0) {
62
+ return { content, error: "old_string not found in file" };
63
+ }
64
+ if (occurrences > 1 && !replace_all) {
65
+ return {
66
+ content,
67
+ error: `old_string matched ${occurrences} times; provide more context for a unique match or set replace_all to true`,
68
+ };
69
+ }
70
+ return { content: content.split(old_string).join(new_string) };
71
+ }
72
+ function editOneFile(cwd, edits) {
73
+ const filePath = resolveWorkspacePath(cwd, edits[0].file_path);
74
+ let content;
75
+ try {
76
+ content = fs.readFileSync(filePath, "utf8");
77
+ }
78
+ catch (error) {
79
+ return edits.map(() => `FAILED ${filePath}: ${error.message}`);
80
+ }
81
+ const results = [];
82
+ let changed = false;
83
+ for (const edit of edits) {
84
+ const { content: next, error } = applyEdit(content, edit);
85
+ if (error) {
86
+ results.push(`FAILED ${filePath}: ${error}`);
87
+ }
88
+ else {
89
+ content = next;
90
+ changed = true;
91
+ results.push(`OK ${filePath}: replaced "${truncate(edit.old_string || "(entire file)", 60)}"`);
92
+ }
93
+ }
94
+ if (changed) {
95
+ try {
96
+ fs.writeFileSync(filePath, content);
97
+ }
98
+ catch (error) {
99
+ return edits.map(() => `FAILED ${filePath}: ${error.message}`);
100
+ }
101
+ }
102
+ return results;
103
+ }
104
+ function truncate(text, max) {
105
+ const oneLine = text.replace(/\n/g, "\\n");
106
+ return oneLine.length > max ? oneLine.slice(0, max) + "…" : oneLine;
107
+ }
108
+ export async function fileEdit(args, context) {
109
+ const edit = {
110
+ file_path: String(args.file_path ?? ""),
111
+ old_string: String(args.old_string ?? ""),
112
+ new_string: String(args.new_string ?? ""),
113
+ replace_all: parseReplaceAll(args.replace_all),
114
+ };
115
+ const [result] = editOneFile(context.cwd, [edit]);
116
+ return { text: result, isError: result.startsWith("FAILED") };
117
+ }
118
+ /**
119
+ * Compute the diff a file-modifying tool call would produce, without writing
120
+ * anything. Returns undefined when no diff can be computed (e.g. the edit will
121
+ * fail to match); the executor surfaces the real error in that case.
122
+ */
123
+ export function previewFileChange(toolName, args, cwd) {
124
+ try {
125
+ if (toolName === "file_write") {
126
+ const relPath = String(args.file_path ?? "");
127
+ const filePath = resolveWorkspacePath(cwd, relPath);
128
+ let oldContent = "";
129
+ try {
130
+ oldContent = fs.readFileSync(filePath, "utf8");
131
+ }
132
+ catch {
133
+ // new file
134
+ }
135
+ const diff = unifiedDiff(oldContent, String(args.content ?? ""));
136
+ return diff ? `${relPath}\n${diff}` : undefined;
137
+ }
138
+ const edits = toolName === "file_edit"
139
+ ? [
140
+ {
141
+ file_path: String(args.file_path ?? ""),
142
+ old_string: String(args.old_string ?? ""),
143
+ new_string: String(args.new_string ?? ""),
144
+ replace_all: parseReplaceAll(args.replace_all),
145
+ },
146
+ ]
147
+ : (Array.isArray(args.edits) ? args.edits : []);
148
+ if (edits.length === 0)
149
+ return undefined;
150
+ const byFile = new Map();
151
+ for (const edit of edits) {
152
+ const key = edit.file_path;
153
+ if (!byFile.has(key))
154
+ byFile.set(key, []);
155
+ byFile.get(key).push(edit);
156
+ }
157
+ const parts = [];
158
+ for (const [relPath, fileEdits] of byFile) {
159
+ const filePath = resolveWorkspacePath(cwd, relPath);
160
+ const oldContent = fs.readFileSync(filePath, "utf8");
161
+ let content = oldContent;
162
+ for (const edit of fileEdits) {
163
+ const { content: next, error } = applyEdit(content, edit);
164
+ if (error)
165
+ return undefined;
166
+ content = next;
167
+ }
168
+ const diff = unifiedDiff(oldContent, content);
169
+ if (diff)
170
+ parts.push(`${relPath}\n${diff}`);
171
+ }
172
+ return parts.length > 0 ? parts.join("\n") : undefined;
173
+ }
174
+ catch {
175
+ return undefined;
176
+ }
177
+ }
178
+ export async function multiFileEdit(args, context) {
179
+ const edits = (Array.isArray(args.edits) ? args.edits : []);
180
+ if (edits.length === 0) {
181
+ return { text: "FAILED: edits array is empty", isError: true };
182
+ }
183
+ // Group edits by file, preserving order within each file.
184
+ const byFile = new Map();
185
+ for (const edit of edits) {
186
+ const key = resolveWorkspacePath(context.cwd, edit.file_path);
187
+ if (!byFile.has(key))
188
+ byFile.set(key, []);
189
+ byFile.get(key).push(edit);
190
+ }
191
+ const results = [];
192
+ for (const fileEdits of byFile.values()) {
193
+ results.push(...editOneFile(context.cwd, fileEdits));
194
+ }
195
+ const anyFailed = results.some((r) => r.startsWith("FAILED"));
196
+ return { text: results.join("\n"), isError: anyFailed && results.every((r) => r.startsWith("FAILED")) };
197
+ }