Creates, edits and manages files and directories. Reading and searching live in filesystem_read; LLM-backed edits live in file_ai.
edit — exact text. old must occur exactly once in the file; new replaces it. Read the file first and copy old verbatim, including indentation. This is the default for almost every change: it either applies exactly or fails loudly.edit_lines — line numbers. Use operations when you know the exact lines. Lines are 1-based and inclusive. Deterministic, no AI call.insert — add text after a known line number (after: 0 means before line 1). A simpler form of one insert operation.write — create a file, or rewrite it entirely. It replaces everything you did not retype.file_ai edit — only when the change cannot be expressed as exact text or line numbers (for example, "rename this symbol everywhere").| Action | Use it for |
|---|---|
write |
create a file or replace its whole content |
append |
add text to the end of a file |
edit |
exact-once text replacement (old → new) |
edit_lines |
one or more line-based operations |
insert |
insert text after line after |
mkdir |
create a directory and any missing parents |
move |
move a file or directory |
copy |
copy a file or directory |
delete |
delete a file or an empty directory; recursive: true for a non-empty tree |
{"action": "edit", "path": "src/app.py", "old": " return None", "new": " return {}"}
{"action": "edit", "path": "src/app.py", "old": "process", "new": "handle", "replace_all": true}
If old is not found, the error shows the closest lines with their numbers and tells you to copy the exact text (or to drop the line-number column if you copied a numbered read). If it occurs more than once, the error lists the line numbers; include more surrounding context, use edit_lines, or set replace_all: true deliberately.
A successful edit returns the changed lines with a little context, so you can verify the result without another read. The full diff is shown in the UI.
edit_lines takes an array of operations. Each operation is one of:
{"op": "replace", "start": 15, "end": 15, "content": " limit = 50"}
{"op": "delete", "start": 20, "end": 22}
{"op": "insert", "after": 30, "content": " # new section"}
start/end are 1-based and inclusive.insert uses after: 0 inserts before line 1, len(lines) appends.For a single insertion, insert is shorter:
{"action": "insert", "path": "src/app.py", "after": 15, "content": " # note"}
write creates missing parent directories. On an existing file it reports the old and new line counts; if the new content is less than half the old, it warns that the file may have been truncated.write with empty content over an existing file is refused unless you pass force: true.move and copy refuse to replace an existing destination unless you pass overwrite: true.delete refuses a non-empty directory unless you pass recursive: true.mkdir creates intermediate directories.{"action": "write", "path": "out/report.md", "content": "# Report\n"}
{"action": "mkdir", "path": "out/parts"}
{"action": "copy", "path": "a.scad", "destination": "b.scad"}
{"action": "move", "path": "old.txt", "destination": "archive/old.txt", "overwrite": true}
{"action": "delete", "path": "build", "recursive": true}
In multi-user mode every path resolves inside user_data/<user_id>/ or the current session directory. A path outside both returns Access denied. Do not work around a refusal by copying files into the sandbox or /tmp; ask the user to upload the file.
edit with an old string that appears twice — the call is refused, correctly. Include more surrounding context, or set replace_all: true only when you really mean every occurrence.old copied from a numbered read — the 12: column is not part of the file.write to change one line — it replaces the whole file. Use edit.edit_lines ranges that overlap — merge them into one operation.delete on a directory without recursive: true — it refuses; this is a guard, not a failure to retry blindly.