Skip to content

Commit c360562

Browse files
committed
feat(files): add anchor-based file edits
1 parent 3b155c3 commit c360562

34 files changed

Lines changed: 1068 additions & 529 deletions

File tree

apps/docs/content/docs/cli/files.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,10 @@ sim files delete <fileId> [options]
174174

175175
</CommandTable>
176176

177-
## Edit file content
177+
## Apply one exact or anchor-based edit to a text file
178178

179179
```bash
180-
sim files content update <fileId> [options]
180+
sim files edit <fileId> [options]
181181
```
182182

183183
**Arguments**
@@ -196,7 +196,7 @@ sim files content update <fileId> [options]
196196

197197
| Option | Required | Description |
198198
| --- | --- | --- |
199-
| `--edit <json\|@file>` | Yes | The change to apply. (JSON, or @path / @- to read a file or stdin). |
199+
| `--edit <json\|@file>` | Yes | One edit object: &#123;"mode":"search_replace","search":"old","content":"new","replaceAll":false&#125;, &#123;"mode":"replace_between","beforeAnchor":"start line","afterAnchor":"end line","content":"new"&#125;, &#123;"mode":"insert_after","anchor":"line","content":"new"&#125;, or &#123;"mode":"delete_between","startAnchor":"first line deleted","endAnchor":"ending line kept"&#125;. Anchored modes also accept occurrence starting at 1 (JSON, or @path / @- to read a file or stdin). |
200200

201201
</CommandTable>
202202

apps/docs/content/docs/cli/reference.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -860,12 +860,12 @@ sim files delete <fileId> [options]
860860

861861
</CommandTable>
862862

863-
### sim files content update
863+
### sim files edit
864864

865-
Edit File Content
865+
Apply one exact or anchor-based edit to a text file
866866

867867
```bash
868-
sim files content update <fileId> [options]
868+
sim files edit <fileId> [options]
869869
```
870870

871871
**Arguments**
@@ -884,7 +884,7 @@ sim files content update <fileId> [options]
884884

885885
| Option | Required | Description |
886886
| --- | --- | --- |
887-
| `--edit <json\|@file>` | Yes | The change to apply. (JSON, or @path / @- to read a file or stdin). |
887+
| `--edit <json\|@file>` | Yes | One edit object: &#123;"mode":"search_replace","search":"old","content":"new","replaceAll":false&#125;, &#123;"mode":"replace_between","beforeAnchor":"start line","afterAnchor":"end line","content":"new"&#125;, &#123;"mode":"insert_after","anchor":"line","content":"new"&#125;, or &#123;"mode":"delete_between","startAnchor":"first line deleted","endAnchor":"ending line kept"&#125;. Anchored modes also accept occurrence starting at 1 (JSON, or @path / @- to read a file or stdin). |
888888

889889
</CommandTable>
890890

apps/docs/content/docs/integrations/file.mdx

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,9 @@ Append content to an existing workspace file. The file must already exist. Conte
172172
| `size` | number | File size in bytes |
173173
| `url` | string | URL to access the file |
174174

175-
### File Edit
175+
### Apply File Edit
176176

177-
Replace one exact piece of text in an existing workspace file, leaving the rest untouched. Use this to correct a fact in place instead of rewriting the whole file. The search text must match exactly once: if it appears several times the edit is refused and the matching line numbers are returned, so include enough surrounding text to be unique. Only plain-text files can be edited.
177+
Apply one precise edit to an existing text file without rewriting it. Use search_replace for verbatim replacement, optionally with replaceAll. Use replace_between, insert_after, or delete_between for complete trimmed-line anchors that stay stable when line numbers move. Folder scope can disambiguate a name or constrain a file ID.
178178

179179
#### Input
180180

@@ -183,33 +183,17 @@ Replace one exact piece of text in an existing workspace file, leaving the rest
183183
| `fileName` | string | Yes | Name or ID of the workspace file to edit. |
184184
| `folderPath` | string | No | Single folder in which to resolve the file name or validate the file ID. Canonical folder path, percent-encoded, e.g. "/memory/user-a/people". The workspace root is "/". Use folderPaths for multiple folders; do not provide both fields. |
185185
| `folderPaths` | array | No | Folders to search for the named file or validate the file ID against. The name must resolve to exactly one file across the selected scopes. Do not provide folderPath as well. |
186-
| `includeSubfolders` | boolean | No | Whether the folder is searched for the file recursively. Defaults to true; set false to match only its direct contents, which is how a name shared with a file in a nested folder is disambiguated. |
187-
| `oldString` | string | Yes | The exact text to replace, matched verbatim including whitespace and line breaks. It must appear exactly once in the file. |
188-
| `newString` | string | Yes | The text to put in its place. Pass an empty string to delete the old text. |
189-
190-
#### Output
191-
192-
| Parameter | Type | Description |
193-
| --------- | ---- | ----------- |
194-
| `id` | string | File ID |
195-
| `name` | string | File name |
196-
| `size` | number | File size in bytes |
197-
| `lineCount` | number | Lines in the file after the edit |
198-
199-
### File Insert
200-
201-
Insert new lines into an existing workspace file at a given line, leaving the rest untouched. Use this to add an entry under a heading without rewriting the file. Line numbers are the ones returned by search and by a ranged read. Only plain-text files can be edited.
202-
203-
#### Input
204-
205-
| Parameter | Type | Required | Description |
206-
| --------- | ---- | -------- | ----------- |
207-
| `fileName` | string | Yes | Name or ID of the workspace file to insert into. |
208-
| `folderPath` | string | No | Single folder in which to resolve the file name or validate the file ID. Canonical folder path, percent-encoded, e.g. "/memory/user-a/people". The workspace root is "/". Use folderPaths for multiple folders; do not provide both fields. |
209-
| `folderPaths` | array | No | Folders to search for the named file or validate the file ID against. The name must resolve to exactly one file across the selected scopes. Do not provide folderPath as well. |
210-
| `includeSubfolders` | boolean | No | Whether the folder is searched for the file recursively. Defaults to true; set false to match only its direct contents, which is how a name shared with a file in a nested folder is disambiguated. |
211-
| `afterLine` | number | Yes | The 1-based line to insert after. Use 0 to insert at the top of the file. A line past the end of the file is refused rather than appended, so read or search first if you are not sure how long the file is. |
212-
| `content` | string | Yes | The text to insert. Multiple lines are inserted as multiple lines; no trailing newline is needed. |
186+
| `includeSubfolders` | boolean | No | Whether selected folders are searched recursively. Defaults to true; false matches only their direct contents. |
187+
| `mode` | string | Yes | Edit type: search_replace, replace_between, insert_after, or delete_between. |
188+
| `search` | string | No | For search_replace, the exact text to replace, including whitespace and line breaks. It must be unique unless replaceAll is true. |
189+
| `content` | string | No | Replacement or inserted text. Pass an empty string to delete a search match or clear the text between anchors. Omit only for delete_between. |
190+
| `replaceAll` | boolean | No | For search_replace, replace every non-overlapping match. Defaults to false, which refuses an ambiguous match. |
191+
| `beforeAnchor` | string | No | For replace_between, the complete line before the content to replace. Leading and trailing whitespace is ignored. |
192+
| `afterAnchor` | string | No | For replace_between, the complete line after the content to replace. The anchor lines remain in the file. |
193+
| `anchor` | string | No | For insert_after, the complete line after which content is inserted. |
194+
| `startAnchor` | string | No | For delete_between, the complete first line to delete. The start anchor is removed. |
195+
| `endAnchor` | string | No | For delete_between, the complete ending boundary line. The end anchor remains in the file. |
196+
| `occurrence` | number | No | For anchored edits, which matching anchor occurrence to use, starting at 1. Defaults to 1. |
213197

214198
#### Output
215199

apps/docs/openapi-v2-files-audit.json

Lines changed: 91 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1950,7 +1950,7 @@
19501950
"patch": {
19511951
"operationId": "editFileContent",
19521952
"summary": "Edit File Content",
1953-
"description": "Change part of a text file in place, leaving the rest untouched. `PUT` on this path replaces the whole file; this is the partial counterpart, so one line can be corrected without regenerating everything around it. `replace_string` requires `oldString` to match **exactly once** — several matches answer `400` naming the lines they sit on, because replacing an arbitrary one of them would be a silent write to a line the caller never chose. `insert_lines` inserts after a 1-based line, with `0` meaning the top; a line past the end of the file answers `400` rather than appending. Line numbers are the ones `GET /api/v2/files/{fileId}/text` and file search report. Only files whose stored bytes are UTF-8 text can be edited: editing works on the bytes, never on parsed text, so a PDF or DOCX answers `400`. A concurrent write answers `409`, and retrying means re-reading first.",
1953+
"description": "Change part of a text file in place, leaving the rest untouched. `PUT` on this path replaces the whole file; this is the partial counterpart. `search_replace` matches exact text and requires one match unless `replaceAll` is true. `replace_between`, `insert_after`, and `delete_between` match complete lines after trimming surrounding whitespace, so edits remain stable when unrelated changes move the target to another line. Anchored replacement preserves both boundary lines; insertion preserves its anchor; deletion removes the start anchor and preserves the end anchor. Use `occurrence` when an anchor line repeats. Only files whose stored bytes are UTF-8 text can be edited: a PDF or DOCX answers `400`. A concurrent write answers `409`, and retrying means re-reading first.",
19541954
"tags": ["Files"],
19551955
"parameters": [
19561956
{
@@ -4600,46 +4600,116 @@
46004600
"properties": {
46014601
"mode": {
46024602
"type": "string",
4603-
"const": "replace_string",
4604-
"description": "Replace one exact piece of text with another."
4603+
"const": "search_replace",
4604+
"description": "Replace exact text."
46054605
},
4606-
"oldString": {
4606+
"search": {
46074607
"type": "string",
46084608
"minLength": 1,
4609-
"description": "Exact text to replace, matched verbatim. It must appear exactly once: several matches are refused with `400` naming the lines they sit on, so include enough surrounding text to be unique."
4609+
"description": "Exact text to replace, matched verbatim. It must appear once unless replaceAll is true."
46104610
},
4611-
"newString": {
4611+
"content": {
46124612
"type": "string",
46134613
"description": "Text to put in its place. An empty string deletes the matched text."
4614+
},
4615+
"replaceAll": {
4616+
"description": "Replace every non-overlapping match. Defaults to false.",
4617+
"type": "boolean"
46144618
}
46154619
},
4616-
"required": ["mode", "oldString", "newString"],
4620+
"required": ["mode", "search", "content"],
46174621
"additionalProperties": false
46184622
},
46194623
{
46204624
"type": "object",
46214625
"properties": {
46224626
"mode": {
46234627
"type": "string",
4624-
"const": "insert_lines",
4625-
"description": "Insert new lines at a given line."
4628+
"const": "replace_between",
4629+
"description": "Replace content between two complete-line anchors."
4630+
},
4631+
"beforeAnchor": {
4632+
"type": "string",
4633+
"minLength": 1,
4634+
"description": "Boundary line before the replaced content. This line remains."
4635+
},
4636+
"afterAnchor": {
4637+
"type": "string",
4638+
"minLength": 1,
4639+
"description": "Boundary line after the replaced content. This line remains."
4640+
},
4641+
"content": {
4642+
"type": "string",
4643+
"description": "Replacement text. An empty string clears the interior."
46264644
},
4627-
"afterLine": {
4645+
"occurrence": {
4646+
"description": "Matching anchor occurrence, starting at 1. Defaults to 1.",
46284647
"type": "integer",
4629-
"minimum": 0,
4630-
"maximum": 9007199254740991,
4631-
"description": "The 1-based line to insert after; 0 inserts at the top. A line past the end is refused rather than appended."
4648+
"minimum": 1,
4649+
"maximum": 9007199254740991
4650+
}
4651+
},
4652+
"required": ["mode", "beforeAnchor", "afterAnchor", "content"],
4653+
"additionalProperties": false
4654+
},
4655+
{
4656+
"type": "object",
4657+
"properties": {
4658+
"mode": {
4659+
"type": "string",
4660+
"const": "insert_after",
4661+
"description": "Insert content after a complete-line anchor."
4662+
},
4663+
"anchor": {
4664+
"type": "string",
4665+
"minLength": 1,
4666+
"description": "Complete line after which content is inserted."
46324667
},
46334668
"content": {
46344669
"type": "string",
4635-
"description": "Text to insert. Multiple lines insert as multiple lines."
4670+
"minLength": 1,
4671+
"description": "Text to insert."
4672+
},
4673+
"occurrence": {
4674+
"description": "Matching anchor occurrence, starting at 1. Defaults to 1.",
4675+
"type": "integer",
4676+
"minimum": 1,
4677+
"maximum": 9007199254740991
4678+
}
4679+
},
4680+
"required": ["mode", "anchor", "content"],
4681+
"additionalProperties": false
4682+
},
4683+
{
4684+
"type": "object",
4685+
"properties": {
4686+
"mode": {
4687+
"type": "string",
4688+
"const": "delete_between",
4689+
"description": "Delete from one complete-line anchor to another."
4690+
},
4691+
"startAnchor": {
4692+
"type": "string",
4693+
"minLength": 1,
4694+
"description": "First line to delete. This start anchor is removed."
4695+
},
4696+
"endAnchor": {
4697+
"type": "string",
4698+
"minLength": 1,
4699+
"description": "Ending boundary line. This end anchor remains."
4700+
},
4701+
"occurrence": {
4702+
"description": "Matching anchor occurrence, starting at 1. Defaults to 1.",
4703+
"type": "integer",
4704+
"minimum": 1,
4705+
"maximum": 9007199254740991
46364706
}
46374707
},
4638-
"required": ["mode", "afterLine", "content"],
4708+
"required": ["mode", "startAnchor", "endAnchor"],
46394709
"additionalProperties": false
46404710
}
46414711
],
4642-
"description": "The change to apply."
4712+
"description": "One exact or anchor-based edit: search_replace, replace_between, insert_after, or delete_between."
46434713
}
46444714
},
46454715
"required": ["workspaceId", "edit"],
@@ -4650,16 +4720,16 @@
46504720
{
46514721
"workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
46524722
"edit": {
4653-
"mode": "replace_string",
4654-
"oldString": "- based in NYC",
4655-
"newString": "- based in SF"
4723+
"mode": "search_replace",
4724+
"search": "- based in NYC",
4725+
"content": "- based in SF"
46564726
}
46574727
},
46584728
{
46594729
"workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
46604730
"edit": {
4661-
"mode": "insert_lines",
4662-
"afterLine": 4,
4731+
"mode": "insert_after",
4732+
"anchor": "## Preferences",
46634733
"content": "- prefers async updates"
46644734
}
46654735
}

apps/sim/app/api/v2/files/[fileId]/content/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ export const PUT = defineV2JsonRoute({
4848
* Correcting one line without regenerating everything around it is the point:
4949
* a whole-file rewrite of a long document both costs more and drifts.
5050
*
51-
* A string replacement must match exactly once. Several matches are refused
52-
* with `400` naming the lines, because replacing an arbitrary one of them is a
53-
* silent write to a line the caller never chose.
51+
* Exact replacement refuses ambiguity unless `replaceAll` is explicit.
52+
* Anchored edits match complete trimmed lines, so they remain stable when
53+
* unrelated changes move the target to a different line number.
5454
*/
5555
export const PATCH = defineV2JsonRoute({
5656
contract: v2EditFileContentContract,

apps/sim/blocks/blocks.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,6 @@ describe.concurrent('Blocks Module', () => {
176176
'file_write',
177177
'file_append',
178178
'file_edit',
179-
'file_insert',
180179
'file_compress',
181180
'file_decompress',
182181
'file_manage_sharing',

0 commit comments

Comments
 (0)