@ottocode/sdk 0.1.236 → 0.1.237

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/sdk",
3
- "version": "0.1.236",
3
+ "version": "0.1.237",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -0,0 +1,83 @@
1
+ export function normalizeLineEndings(text: string): string {
2
+ return text.replace(/\r\n/g, '\n');
3
+ }
4
+
5
+ export function detectLineEnding(text: string): '\n' | '\r\n' {
6
+ return text.includes('\r\n') ? '\r\n' : '\n';
7
+ }
8
+
9
+ export function convertToLineEnding(
10
+ text: string,
11
+ lineEnding: '\n' | '\r\n',
12
+ ): string {
13
+ if (lineEnding === '\n') return text;
14
+ return text.replace(/\n/g, '\r\n');
15
+ }
16
+
17
+ function countOccurrences(content: string, search: string): number {
18
+ if (!search) return 0;
19
+ let count = 0;
20
+ let start = 0;
21
+ while (true) {
22
+ const index = content.indexOf(search, start);
23
+ if (index === -1) return count;
24
+ count += 1;
25
+ start = index + search.length;
26
+ }
27
+ }
28
+
29
+ export function applyStringEdit(
30
+ content: string,
31
+ oldString: string,
32
+ newString: string,
33
+ replaceAll = false,
34
+ ): { content: string; occurrences: number } {
35
+ if (oldString.length === 0) {
36
+ throw new Error(
37
+ 'oldString must not be empty. Use write to create files or apply_patch for structural insertions.',
38
+ );
39
+ }
40
+ if (oldString === newString) {
41
+ throw new Error(
42
+ 'No changes to apply: oldString and newString are identical.',
43
+ );
44
+ }
45
+
46
+ const lineEnding = detectLineEnding(content);
47
+ const normalizedOld = convertToLineEnding(
48
+ normalizeLineEndings(oldString),
49
+ lineEnding,
50
+ );
51
+ const normalizedNew = convertToLineEnding(
52
+ normalizeLineEndings(newString),
53
+ lineEnding,
54
+ );
55
+
56
+ const occurrences = countOccurrences(content, normalizedOld);
57
+ if (occurrences === 0) {
58
+ throw new Error(
59
+ 'oldString not found in content. Read the file again and copy the exact text, including whitespace.',
60
+ );
61
+ }
62
+ if (occurrences > 1 && !replaceAll) {
63
+ throw new Error(
64
+ 'Found multiple matches for oldString. Provide more surrounding lines to make it unique or set replaceAll to true.',
65
+ );
66
+ }
67
+
68
+ if (replaceAll) {
69
+ return {
70
+ content: content.split(normalizedOld).join(normalizedNew),
71
+ occurrences,
72
+ };
73
+ }
74
+
75
+ const index = content.indexOf(normalizedOld);
76
+ return {
77
+ content:
78
+ content.slice(0, index) +
79
+ normalizedNew +
80
+ content.slice(index + normalizedOld.length),
81
+ occurrences,
82
+ };
83
+ }
@@ -0,0 +1,129 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { tool, type Tool } from 'ai';
3
+ import { z } from 'zod/v3';
4
+ import DESCRIPTION from './edit.txt' with { type: 'text' };
5
+ import { buildWriteArtifact, isAbsoluteLike, resolveSafePath } from './util.ts';
6
+ import { applyStringEdit } from './edit-shared.ts';
7
+ import { assertFreshRead, rememberFileWrite } from './read-tracker.ts';
8
+ import { createToolError, type ToolResponse } from '../../error.ts';
9
+
10
+ export function buildEditTool(projectRoot: string): {
11
+ name: string;
12
+ tool: Tool;
13
+ } {
14
+ const edit = tool({
15
+ description: DESCRIPTION,
16
+ inputSchema: z.object({
17
+ path: z
18
+ .string()
19
+ .describe(
20
+ 'Relative file path within the project. Absolute paths are not allowed.',
21
+ ),
22
+ oldString: z.string().describe('Exact text to replace'),
23
+ newString: z.string().describe('Replacement text'),
24
+ replaceAll: z
25
+ .boolean()
26
+ .optional()
27
+ .default(false)
28
+ .describe(
29
+ 'Replace every matching occurrence instead of requiring a unique match',
30
+ ),
31
+ }),
32
+ async execute({
33
+ path,
34
+ oldString,
35
+ newString,
36
+ replaceAll = false,
37
+ }: {
38
+ path: string;
39
+ oldString: string;
40
+ newString: string;
41
+ replaceAll?: boolean;
42
+ }): Promise<
43
+ ToolResponse<{
44
+ path: string;
45
+ occurrences: number;
46
+ bytes: number;
47
+ artifact: unknown;
48
+ }>
49
+ > {
50
+ if (!path || path.trim().length === 0) {
51
+ return createToolError(
52
+ 'Missing required parameter: path',
53
+ 'validation',
54
+ {
55
+ parameter: 'path',
56
+ value: path,
57
+ suggestion: 'Provide a relative file path to edit',
58
+ },
59
+ );
60
+ }
61
+ if (isAbsoluteLike(path)) {
62
+ return createToolError(
63
+ `Refusing to edit outside project root: ${path}`,
64
+ 'permission',
65
+ {
66
+ parameter: 'path',
67
+ value: path,
68
+ suggestion: 'Use a relative path within the project',
69
+ },
70
+ );
71
+ }
72
+
73
+ const abs = resolveSafePath(projectRoot, path);
74
+ try {
75
+ await assertFreshRead(projectRoot, abs, path);
76
+ const original = await readFile(abs, 'utf-8');
77
+ const updated = applyStringEdit(
78
+ original,
79
+ oldString,
80
+ newString,
81
+ replaceAll,
82
+ );
83
+ if (updated.content === original) {
84
+ return createToolError('No changes applied.', 'validation', {
85
+ suggestion:
86
+ 'Adjust oldString/newString so the file content actually changes',
87
+ });
88
+ }
89
+
90
+ await writeFile(abs, updated.content, 'utf-8');
91
+ await rememberFileWrite(projectRoot, abs);
92
+ const artifact = await buildWriteArtifact(
93
+ path,
94
+ true,
95
+ original,
96
+ updated.content,
97
+ );
98
+ return {
99
+ ok: true,
100
+ path,
101
+ occurrences: updated.occurrences,
102
+ bytes: updated.content.length,
103
+ artifact,
104
+ };
105
+ } catch (error: unknown) {
106
+ const isEnoent =
107
+ error &&
108
+ typeof error === 'object' &&
109
+ 'code' in error &&
110
+ error.code === 'ENOENT';
111
+ return createToolError(
112
+ isEnoent
113
+ ? `File not found: ${path}`
114
+ : `Failed to edit file: ${error instanceof Error ? error.message : String(error)}`,
115
+ isEnoent ? 'not_found' : 'execution',
116
+ {
117
+ parameter: 'path',
118
+ value: path,
119
+ suggestion: isEnoent
120
+ ? 'Use read or ls to confirm the file path first'
121
+ : undefined,
122
+ },
123
+ );
124
+ }
125
+ },
126
+ });
127
+
128
+ return { name: 'edit', tool: edit };
129
+ }
@@ -0,0 +1,9 @@
1
+ Replace an exact text block in an existing file.
2
+
3
+ Use this for targeted edits instead of apply_patch whenever possible.
4
+
5
+ Rules:
6
+ - You must read the file first in the current session before editing it.
7
+ - `oldString` must match the file exactly, including whitespace.
8
+ - If `oldString` appears multiple times, provide more context or set `replaceAll: true`.
9
+ - Use `write` for new files and `apply_patch` for structural multi-file diffs.
@@ -1,5 +1,7 @@
1
1
  import type { Tool } from 'ai';
2
+ import { buildEditTool } from './edit.ts';
2
3
  import { buildReadTool } from './read.ts';
4
+ import { buildMultiEditTool } from './multiedit.ts';
3
5
  import { buildWriteTool } from './write.ts';
4
6
  import { buildLsTool } from './ls.ts';
5
7
  import { buildTreeTool } from './tree.ts';
@@ -11,6 +13,8 @@ export function buildFsTools(
11
13
  ): Array<{ name: string; tool: Tool }> {
12
14
  const out: Array<{ name: string; tool: Tool }> = [];
13
15
  out.push(buildReadTool(projectRoot));
16
+ out.push(buildEditTool(projectRoot));
17
+ out.push(buildMultiEditTool(projectRoot));
14
18
  out.push(buildWriteTool(projectRoot));
15
19
  out.push(buildLsTool(projectRoot));
16
20
  out.push(buildTreeTool(projectRoot));
@@ -0,0 +1,137 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { tool, type Tool } from 'ai';
3
+ import { z } from 'zod/v3';
4
+ import DESCRIPTION from './multiedit.txt' with { type: 'text' };
5
+ import { buildWriteArtifact, isAbsoluteLike, resolveSafePath } from './util.ts';
6
+ import { applyStringEdit } from './edit-shared.ts';
7
+ import { assertFreshRead, rememberFileWrite } from './read-tracker.ts';
8
+ import { createToolError, type ToolResponse } from '../../error.ts';
9
+
10
+ const multiEditSchema = z.object({
11
+ path: z
12
+ .string()
13
+ .describe(
14
+ 'Relative file path within the project. Absolute paths are not allowed.',
15
+ ),
16
+ edits: z
17
+ .array(
18
+ z.object({
19
+ oldString: z.string().describe('Exact text to replace'),
20
+ newString: z.string().describe('Replacement text'),
21
+ replaceAll: z
22
+ .boolean()
23
+ .optional()
24
+ .default(false)
25
+ .describe('Replace every matching occurrence for this edit'),
26
+ }),
27
+ )
28
+ .min(1)
29
+ .describe('Edits to apply sequentially to the same file'),
30
+ });
31
+
32
+ export function buildMultiEditTool(projectRoot: string): {
33
+ name: string;
34
+ tool: Tool;
35
+ } {
36
+ const multiedit = tool({
37
+ description: DESCRIPTION,
38
+ inputSchema: multiEditSchema,
39
+ async execute({ path, edits }: z.infer<typeof multiEditSchema>): Promise<
40
+ ToolResponse<{
41
+ path: string;
42
+ editsApplied: number;
43
+ bytes: number;
44
+ artifact: unknown;
45
+ }>
46
+ > {
47
+ if (!path || path.trim().length === 0) {
48
+ return createToolError(
49
+ 'Missing required parameter: path',
50
+ 'validation',
51
+ {
52
+ parameter: 'path',
53
+ value: path,
54
+ suggestion: 'Provide a relative file path to edit',
55
+ },
56
+ );
57
+ }
58
+ if (isAbsoluteLike(path)) {
59
+ return createToolError(
60
+ `Refusing to edit outside project root: ${path}`,
61
+ 'permission',
62
+ {
63
+ parameter: 'path',
64
+ value: path,
65
+ suggestion: 'Use a relative path within the project',
66
+ },
67
+ );
68
+ }
69
+
70
+ const abs = resolveSafePath(projectRoot, path);
71
+ try {
72
+ await assertFreshRead(projectRoot, abs, path);
73
+ const original = await readFile(abs, 'utf-8');
74
+ let nextContent = original;
75
+ for (let i = 0; i < edits.length; i++) {
76
+ const edit = edits[i];
77
+ try {
78
+ nextContent = applyStringEdit(
79
+ nextContent,
80
+ edit.oldString,
81
+ edit.newString,
82
+ edit.replaceAll,
83
+ ).content;
84
+ } catch (error: unknown) {
85
+ throw new Error(
86
+ `Edit ${i + 1} failed: ${error instanceof Error ? error.message : String(error)}`,
87
+ );
88
+ }
89
+ }
90
+
91
+ if (nextContent === original) {
92
+ return createToolError('No changes applied.', 'validation', {
93
+ suggestion:
94
+ 'Adjust your edits so the file content actually changes',
95
+ });
96
+ }
97
+
98
+ await writeFile(abs, nextContent, 'utf-8');
99
+ await rememberFileWrite(projectRoot, abs);
100
+ const artifact = await buildWriteArtifact(
101
+ path,
102
+ true,
103
+ original,
104
+ nextContent,
105
+ );
106
+ return {
107
+ ok: true,
108
+ path,
109
+ editsApplied: edits.length,
110
+ bytes: nextContent.length,
111
+ artifact,
112
+ };
113
+ } catch (error: unknown) {
114
+ const isEnoent =
115
+ error &&
116
+ typeof error === 'object' &&
117
+ 'code' in error &&
118
+ error.code === 'ENOENT';
119
+ return createToolError(
120
+ isEnoent
121
+ ? `File not found: ${path}`
122
+ : `Failed to edit file: ${error instanceof Error ? error.message : String(error)}`,
123
+ isEnoent ? 'not_found' : 'execution',
124
+ {
125
+ parameter: 'path',
126
+ value: path,
127
+ suggestion: isEnoent
128
+ ? 'Use read or ls to confirm the file path first'
129
+ : undefined,
130
+ },
131
+ );
132
+ }
133
+ },
134
+ });
135
+
136
+ return { name: 'multiedit', tool: multiedit };
137
+ }
@@ -0,0 +1,9 @@
1
+ Apply multiple exact text replacements to a single existing file atomically.
2
+
3
+ Use this when you need several edits in one file.
4
+
5
+ Rules:
6
+ - Read the file first before editing.
7
+ - Each edit uses exact `oldString` -> `newString` replacement.
8
+ - All edits must succeed or none are written.
9
+ - Use `replaceAll: true` only when every occurrence should change.
@@ -0,0 +1,72 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { resolve as resolvePath } from 'node:path';
3
+
4
+ type FileStamp = {
5
+ readAt: number;
6
+ mtimeMs?: number;
7
+ ctimeMs?: number;
8
+ size?: number;
9
+ };
10
+
11
+ const readState = new Map<string, Map<string, FileStamp>>();
12
+
13
+ function getProjectState(projectRoot: string): Map<string, FileStamp> {
14
+ const key = resolvePath(projectRoot);
15
+ let state = readState.get(key);
16
+ if (!state) {
17
+ state = new Map<string, FileStamp>();
18
+ readState.set(key, state);
19
+ }
20
+ return state;
21
+ }
22
+
23
+ async function captureFileStamp(absPath: string): Promise<FileStamp> {
24
+ const stats = await stat(absPath);
25
+ return {
26
+ readAt: Date.now(),
27
+ mtimeMs: Number.isFinite(stats.mtimeMs) ? stats.mtimeMs : undefined,
28
+ ctimeMs: Number.isFinite(stats.ctimeMs) ? stats.ctimeMs : undefined,
29
+ size: typeof stats.size === 'number' ? stats.size : undefined,
30
+ };
31
+ }
32
+
33
+ export async function rememberFileRead(
34
+ projectRoot: string,
35
+ absPath: string,
36
+ ): Promise<void> {
37
+ const state = getProjectState(projectRoot);
38
+ state.set(absPath, await captureFileStamp(absPath));
39
+ }
40
+
41
+ export async function rememberFileWrite(
42
+ projectRoot: string,
43
+ absPath: string,
44
+ ): Promise<void> {
45
+ const state = getProjectState(projectRoot);
46
+ state.set(absPath, await captureFileStamp(absPath));
47
+ }
48
+
49
+ export async function assertFreshRead(
50
+ projectRoot: string,
51
+ absPath: string,
52
+ displayPath: string,
53
+ ): Promise<void> {
54
+ const state = getProjectState(projectRoot);
55
+ const previous = state.get(absPath);
56
+ if (!previous) {
57
+ throw new Error(
58
+ `You must read file ${displayPath} before editing it. Use the read tool first.`,
59
+ );
60
+ }
61
+
62
+ const current = await captureFileStamp(absPath);
63
+ const changed =
64
+ current.mtimeMs !== previous.mtimeMs ||
65
+ current.ctimeMs !== previous.ctimeMs ||
66
+ current.size !== previous.size;
67
+ if (!changed) return;
68
+
69
+ throw new Error(
70
+ `File ${displayPath} has changed since it was last read. Read it again before editing.`,
71
+ );
72
+ }
@@ -2,6 +2,7 @@ import { tool, type Tool } from 'ai';
2
2
  import { z } from 'zod/v3';
3
3
  import { readFile } from 'node:fs/promises';
4
4
  import { expandTilde, isAbsoluteLike, resolveSafePath } from './util.ts';
5
+ import { rememberFileRead } from './read-tracker.ts';
5
6
  import DESCRIPTION from './read.txt' with { type: 'text' };
6
7
  import { createToolError, type ToolResponse } from '../../error.ts';
7
8
 
@@ -108,6 +109,7 @@ export function buildReadTool(projectRoot: string): {
108
109
  try {
109
110
  let content = await readFile(abs, 'utf-8');
110
111
  const indent = detectIndentation(content);
112
+ await rememberFileRead(projectRoot, abs);
111
113
 
112
114
  if (startLine !== undefined && endLine !== undefined) {
113
115
  const lines = content.split('\n');
@@ -8,6 +8,7 @@ import {
8
8
  expandTilde,
9
9
  isAbsoluteLike,
10
10
  } from './util.ts';
11
+ import { rememberFileWrite } from './read-tracker.ts';
11
12
  import DESCRIPTION from './write.txt' with { type: 'text' };
12
13
  import { createToolError, type ToolResponse } from '../../error.ts';
13
14
 
@@ -79,6 +80,7 @@ export function buildWriteTool(projectRoot: string): {
79
80
  existed = true;
80
81
  } catch {}
81
82
  await writeFile(abs, content);
83
+ await rememberFileWrite(projectRoot, abs);
82
84
  const artifact = await buildWriteArtifact(
83
85
  req,
84
86
  existed,
@@ -6,6 +6,6 @@
6
6
  Usage tips:
7
7
  - Use for creating NEW files
8
8
  - Use when replacing >70% of a file's content (almost complete rewrite)
9
- - NEVER use for partial/targeted edits - use apply_patch instead
9
+ - NEVER use for partial/targeted edits - use edit/multiedit first, or apply_patch for structural changes
10
10
  - Using write for partial edits wastes output tokens and risks hallucinating unchanged parts
11
11
  - Prefer idempotent writes by providing the full intended content when you do use write
@@ -12,6 +12,13 @@ You help with coding and build tasks.
12
12
  - Use `terminal(operation: "write", input: "\u0003")` or `terminal(operation: "interrupt")` to stop a process before resorting to `kill`.
13
13
  - Summarize active terminals (purpose, key command, port) in your updates so collaborators know what's running.
14
14
 
15
+ ## Preferred Editing Order
16
+
17
+ - Use `edit` for one exact replacement in an existing file.
18
+ - Use `multiedit` for several exact replacements in the same file.
19
+ - Use `apply_patch` for structural or multi-file changes, file add/delete/rename, or edits that are awkward as exact replacements.
20
+ - Use `write` for new files or near-total rewrites.
21
+
15
22
  ## apply_patch — Mandatory Rules
16
23
 
17
24
  These rules apply EVERY time you use the `apply_patch` tool. Violations cause patch failures.
@@ -211,7 +218,7 @@ Key points:
211
218
  - Wastes output tokens and risks hallucinating unchanged parts
212
219
 
213
220
  ## Never
214
- - Use `write` for partial file edits (use `apply_patch` instead)
221
+ - Use `write` for partial file edits (use `edit`/`multiedit` first, or `apply_patch` for structural diffs)
215
222
  - Make multiple separate `apply_patch` calls for the same file (use multiple hunks with @@ headers instead)
216
223
  - Assume file content remains unchanged between operations
217
- - Use `bash` with `sed`/`awk` for programmatic file editing (use `apply_patch` instead)
224
+ - Use `bash` with `sed`/`awk` for programmatic file editing (use `edit`, `multiedit`, or `apply_patch` instead)
@@ -21,10 +21,13 @@ You MUST call the `finish` tool at the end of every response to signal completio
21
21
  **IMPORTANT**: Do NOT call `finish` before streaming your response. Always stream your message first, then call `finish`. If you forget to call `finish`, the system will hang and not complete properly.
22
22
 
23
23
  File Editing Best Practices:
24
- - ALWAYS read a file immediately before using apply_patch never patch from memory
25
- - The `read` tool returns an `indentation` field (e.g., "tabs", "2 spaces") — use it to match the file's indentation style in your patch
26
- - Copy context lines CHARACTER-FOR-CHARACTER from the read output never reconstruct from memory
27
- - When making multiple edits to the same file, use multiple `@@` hunks in a single `apply_patch` call
28
- - Never assume file content remains unchanged between separate apply_patch operations
29
- - If a patch fails, read the file AGAIN and copy the exact lines
30
- - If `apply_patch` fails repeatedly on the same file, fall back to `write` only when a full-file rewrite is appropriate
24
+ - Prefer `edit` for one exact replacement in an existing file
25
+ - Prefer `multiedit` for several exact replacements in the same file
26
+ - Use `apply_patch` for structural diffs, file add/delete/rename, or changes that are awkward as exact replacements
27
+ - ALWAYS read a file immediately before using `edit`, `multiedit`, or `apply_patch`
28
+ - For `edit` / `multiedit`, copy the exact text including whitespace from the latest `read` output
29
+ - For `apply_patch`, use the `indentation` field from `read` and copy context lines CHARACTER-FOR-CHARACTER
30
+ - When making multiple edits to the same file with `apply_patch`, use multiple `@@` hunks in a single call
31
+ - Never assume file content remains unchanged between separate edit operations
32
+ - If an edit tool fails, read the file AGAIN and copy the exact lines
33
+ - If `apply_patch` fails repeatedly on the same file, prefer `edit` / `multiedit` when possible and fall back to `write` only for a full-file rewrite
@@ -20,7 +20,9 @@ You have access to a rich set of specialized tools optimized for coding tasks:
20
20
  **File Reading & Editing**:
21
21
  - `read`: Read file contents (supports line ranges)
22
22
  - `write`: Write complete file contents
23
- - `apply_patch`: Apply unified diff patches (RECOMMENDED for targeted edits)
23
+ - `edit`: Exact text replacement in one file
24
+ - `multiedit`: Multiple exact replacements in one file
25
+ - `apply_patch`: Apply diff/enveloped patches (best for structural or multi-file changes)
24
26
 
25
27
  **Version Control**:
26
28
  - `git_status`, `git_diff`
@@ -34,12 +36,13 @@ You have access to a rich set of specialized tools optimized for coding tasks:
34
36
  ### Tool Usage Best Practices:
35
37
 
36
38
  1. **Batch Independent Operations**: Make all independent tool calls in one turn
37
- 2. **File Editing**: Prefer `apply_patch` for targeted edits to avoid rewriting entire files
38
- 3. **Combine Edits**: When editing the same file multiple times, use multiple `@@` hunks in ONE `apply_patch` call
39
- 4. **Search First**: Use `glob` to find files before reading them
40
- 5. **Progress Updates**: Call `progress_update` at major milestones (planning, discovering, writing, verifying)
41
- 6. **Plan Tracking**: Use `update_todos` to show task breakdown and progress
42
- 7. **Finish Required**: Always call `finish` tool when complete
39
+ 2. **File Editing**: Prefer `edit` for one targeted replacement and `multiedit` for several exact replacements in the same file
40
+ 3. **Structural Changes**: Use `apply_patch` for multi-file diffs, file add/delete/rename, or edits that are awkward as exact replacements
41
+ 4. **Patch Consolidation**: When you do use `apply_patch` multiple times on the same file, use multiple `@@` hunks in ONE `apply_patch` call
42
+ 5. **Search First**: Use `glob` to find files before reading them
43
+ 6. **Progress Updates**: Call `progress_update` at major milestones (planning, discovering, writing, verifying)
44
+ 7. **Plan Tracking**: Use `update_todos` to show task breakdown and progress
45
+ 8. **Finish Required**: Always call `finish` tool when complete
43
46
 
44
47
  ### Tool Failure Handling
45
48
 
@@ -49,10 +52,18 @@ You have access to a rich set of specialized tools optimized for coding tasks:
49
52
 
50
53
  ## File Editing Best Practices
51
54
 
52
- **Using the `apply_patch` Tool** (Recommended):
55
+ **Using the `edit` / `multiedit` Tools** (Recommended):
56
+ - Use `edit` for one exact replacement in an existing file
57
+ - Use `multiedit` for several exact replacements in the same file
58
+ - These tools are the default choice for targeted edits because they avoid patch-formatting mistakes
59
+ - Read the file first, then copy the exact text including whitespace
60
+ - If the text appears multiple times, include more surrounding context or use `replaceAll: true`
61
+ - Use `apply_patch` only when the change is structural, spans multiple files, or cannot be expressed as an exact replacement
62
+
63
+ **Using the `apply_patch` Tool** (Structural / Advanced):
53
64
  - **CRITICAL**: ALWAYS read the target file immediately before creating a patch - never patch from memory
54
65
  - The `read` tool returns an `indentation` field (e.g., "tabs", "2 spaces") — use it to match the file's indent style in your patch
55
- - Primary choice for targeted file edits - avoids rewriting entire files
66
+ - Use when the change is structural, multi-file, or awkward as an exact replacement
56
67
  - Preferred format is the enveloped patch (`*** Begin Patch` ...); standard unified diffs (`---/+++`) are also accepted and auto-converted if provided
57
68
  - Only requires the specific lines you want to change
58
69
  - Format: `*** Begin Patch` ... `*** Update File: path` ... `-old` / `+new` ... `*** End Patch`
@@ -63,8 +74,8 @@ You have access to a rich set of specialized tools optimized for coding tasks:
63
74
  - If patch fails, it means the file content doesn't match - read it again and retry
64
75
  - If you suspect parts of the patch might be stale, set `allowRejects: true` so the tool applies what it can and reports the skipped hunks with reasons
65
76
  - The tool quietly skips removal lines that are already gone and additions that already exist, so you don't need to resend the same change
66
- - **Best for**: Small, surgical edits to code files (< 50 line changes per file)
67
- - **Struggles with**: Large restructures (> 50 lines), major section reorganizations
77
+ - **Best for**: Structural diffs, file add/delete/rename, or edits that don't fit exact string replacement cleanly
78
+ - **Struggles with**: Small targeted edits where `edit` or `multiedit` would be simpler and less error-prone
68
79
 
69
80
  **Patch Format Reminder**:
70
81
  ```
@@ -83,10 +94,10 @@ You have access to a rich set of specialized tools optimized for coding tasks:
83
94
  - Wastes output tokens and risks hallucinating unchanged parts
84
95
 
85
96
  **Never**:
86
- - Use `write` for partial file edits (use `apply_patch` instead)
97
+ - Use `write` for partial file edits (use `edit`/`multiedit` first, or `apply_patch` for structural diffs)
87
98
  - Make multiple separate `apply_patch` calls for the same file (use multiple hunks with @@ headers instead)
88
99
  - Assume file content remains unchanged between operations
89
- - Use `bash` with `sed`/`awk` for programmatic file editing (use `apply_patch` instead)
100
+ - Use `bash` with `sed`/`awk` for programmatic file editing (use `edit`, `multiedit`, or `apply_patch` instead)
90
101
 
91
102
  ## Direct File References
92
103
 
@@ -264,7 +275,7 @@ You MUST adhere to the following criteria when solving queries:
264
275
  - Working on the repo(s) in the current environment is allowed, even if they are proprietary.
265
276
  - Analyzing code for vulnerabilities is allowed.
266
277
  - Showing user code and tool call details is allowed.
267
- - Use the `apply_patch` tool to edit files (NEVER try `applypatch` or `apply-patch`, only `apply_patch`): {"command":["apply_patch","*** Begin Patch\\n*** Update File: path/to/file.py\\n def example():\\n- pass\\n+ return 123\\n*** End Patch"]}
278
+ - Prefer `edit` and `multiedit` for targeted changes in existing files. Use `apply_patch` for structural diffs or file add/delete/rename. When using patches, NEVER try `applypatch` or `apply-patch`, only `apply_patch`.
268
279
 
269
280
  If completing the user's task requires writing or modifying files, your code and final answer should follow these coding guidelines, though user instructions (i.e. AGENTS.md) may override these guidelines:
270
281