@oh-my-pi/pi-coding-agent 13.12.9 → 13.13.0
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/CHANGELOG.md +25 -0
- package/package.json +7 -7
- package/src/config/settings-schema.ts +20 -1
- package/src/ipy/prelude.py +25 -8
- package/src/main.ts +6 -0
- package/src/patch/index.ts +4 -0
- package/src/tools/auto-generated-guard.ts +310 -0
- package/src/tools/write.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.13.0] - 2026-03-18
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `edit.blockAutoGenerated` setting to control whether auto-generated file detection is enforced (enabled by default)
|
|
10
|
+
- Improved auto-generated file detection to use language-specific comment parsing instead of broad regex patterns, reducing false positives
|
|
11
|
+
- Added auto-generated file detection to prevent accidental modification of generated code (protoc, sqlc, buf, swagger, etc.)
|
|
12
|
+
- Added validation in Edit and Write tools to block modifications to files with auto-generated markers or naming patterns
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Enhanced auto-generated marker detection to only scan leading header comments rather than entire file prefix, improving accuracy for files with generated markers in code
|
|
17
|
+
|
|
18
|
+
## [13.12.10] - 2026-03-17
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Added `args` field to ShellResult to capture the executed command
|
|
22
|
+
- Added `exit_code` property to ShellResult as an alias for `returncode`
|
|
23
|
+
- Added `check_returncode()` method to ShellResult to raise CalledProcessError on non-zero exit codes
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Renamed `code` field to `returncode` in ShellResult (accessible via `code` property for backward compatibility)
|
|
28
|
+
- Updated `run()` command documentation to clarify available ShellResult fields
|
|
29
|
+
|
|
5
30
|
## [13.12.9] - 2026-03-17
|
|
6
31
|
### Added
|
|
7
32
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.
|
|
4
|
+
"version": "13.13.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.13.0",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.13.0",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.13.0",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.13.0",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.13.0",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.13.0",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
|
@@ -549,6 +549,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
549
549
|
},
|
|
550
550
|
},
|
|
551
551
|
|
|
552
|
+
"startup.checkUpdate": {
|
|
553
|
+
type: "boolean",
|
|
554
|
+
default: true,
|
|
555
|
+
ui: {
|
|
556
|
+
tab: "interaction",
|
|
557
|
+
label: "Check for Updates",
|
|
558
|
+
description: "If false, skip update check",
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
|
|
552
562
|
collapseChangelog: {
|
|
553
563
|
type: "boolean",
|
|
554
564
|
default: false,
|
|
@@ -849,7 +859,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
849
859
|
},
|
|
850
860
|
},
|
|
851
861
|
|
|
852
|
-
|
|
862
|
+
"edit.blockAutoGenerated": {
|
|
863
|
+
type: "boolean",
|
|
864
|
+
default: true,
|
|
865
|
+
ui: {
|
|
866
|
+
tab: "editing",
|
|
867
|
+
label: "Block Auto-Generated Files",
|
|
868
|
+
description: "Prevent editing of files that appear to be auto-generated (protoc, sqlc, swagger, etc.)",
|
|
869
|
+
},
|
|
870
|
+
},
|
|
871
|
+
|
|
853
872
|
readLineNumbers: {
|
|
854
873
|
type: "boolean",
|
|
855
874
|
default: false,
|
package/src/ipy/prelude.py
CHANGED
|
@@ -305,23 +305,40 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
305
305
|
|
|
306
306
|
class ShellResult:
|
|
307
307
|
"""Result from shell command execution."""
|
|
308
|
-
__slots__ = ("stdout", "stderr", "
|
|
309
|
-
def __init__(self, stdout: str, stderr: str,
|
|
308
|
+
__slots__ = ("args", "stdout", "stderr", "returncode")
|
|
309
|
+
def __init__(self, args: str, stdout: str, stderr: str, returncode: int):
|
|
310
|
+
self.args = args
|
|
310
311
|
self.stdout = stdout
|
|
311
312
|
self.stderr = stderr
|
|
312
|
-
self.
|
|
313
|
+
self.returncode = returncode
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def code(self) -> int:
|
|
317
|
+
return self.returncode
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def exit_code(self) -> int:
|
|
321
|
+
return self.returncode
|
|
322
|
+
|
|
323
|
+
def check_returncode(self) -> None:
|
|
324
|
+
if self.returncode != 0:
|
|
325
|
+
raise subprocess.CalledProcessError(
|
|
326
|
+
self.returncode, self.args, output=self.stdout, stderr=self.stderr
|
|
327
|
+
)
|
|
328
|
+
|
|
313
329
|
def __repr__(self):
|
|
314
|
-
if self.
|
|
330
|
+
if self.returncode == 0:
|
|
315
331
|
return ""
|
|
316
|
-
return f"exit code {self.
|
|
332
|
+
return f"exit code {self.returncode}"
|
|
333
|
+
|
|
317
334
|
def __bool__(self):
|
|
318
|
-
return self.
|
|
335
|
+
return self.returncode == 0
|
|
319
336
|
|
|
320
337
|
def _make_shell_result(proc: subprocess.CompletedProcess[str], cmd: str) -> ShellResult:
|
|
321
338
|
"""Create ShellResult and emit status."""
|
|
322
339
|
output = proc.stdout + proc.stderr if proc.stderr else proc.stdout
|
|
323
340
|
_emit_status("sh", cmd=cmd[:80], code=proc.returncode, output=output[:500])
|
|
324
|
-
return ShellResult(proc.stdout, proc.stderr, proc.returncode)
|
|
341
|
+
return ShellResult(cmd, proc.stdout, proc.stderr, proc.returncode)
|
|
325
342
|
|
|
326
343
|
import signal as _signal
|
|
327
344
|
|
|
@@ -356,7 +373,7 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
356
373
|
|
|
357
374
|
@_category("Shell")
|
|
358
375
|
def run(cmd: str, *, cwd: str | Path | None = None, timeout: int | None = None) -> ShellResult:
|
|
359
|
-
"""Run a shell command."""
|
|
376
|
+
"""Run a shell command. Returns ShellResult with stdout/stderr and returncode/exit_code fields."""
|
|
360
377
|
shell_path = shutil.which("bash") or shutil.which("sh") or "/bin/sh"
|
|
361
378
|
args = [shell_path, "-c", cmd]
|
|
362
379
|
return _run_with_interrupt(args, str(cwd) if cwd else None, timeout, cmd)
|
package/src/main.ts
CHANGED
|
@@ -34,6 +34,9 @@ import { resolvePromptInput } from "./system-prompt";
|
|
|
34
34
|
import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog";
|
|
35
35
|
|
|
36
36
|
async function checkForNewVersion(currentVersion: string): Promise<string | undefined> {
|
|
37
|
+
if (!settings.get("startup.checkUpdate")) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
37
40
|
try {
|
|
38
41
|
const response = await fetch("https://registry.npmjs.org/@oh-my-pi/pi-coding-agent/latest");
|
|
39
42
|
if (!response.ok) return undefined;
|
|
@@ -109,6 +112,9 @@ async function runInteractiveMode(
|
|
|
109
112
|
|
|
110
113
|
versionCheckPromise
|
|
111
114
|
.then(newVersion => {
|
|
115
|
+
if (!settings.get("startup.checkUpdate")) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
112
118
|
if (newVersion) {
|
|
113
119
|
mode.showNewVersionNotification(newVersion);
|
|
114
120
|
}
|
package/src/patch/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ import hashlineDescription from "../prompts/tools/hashline.md" with { type: "tex
|
|
|
25
25
|
import patchDescription from "../prompts/tools/patch.md" with { type: "text" };
|
|
26
26
|
import replaceDescription from "../prompts/tools/replace.md" with { type: "text" };
|
|
27
27
|
import type { ToolSession } from "../tools";
|
|
28
|
+
import { checkAutoGeneratedFile, checkAutoGeneratedFileContent } from "../tools/auto-generated-guard";
|
|
28
29
|
import {
|
|
29
30
|
invalidateFsScanAfterDelete,
|
|
30
31
|
invalidateFsScanAfterRename,
|
|
@@ -546,6 +547,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
546
547
|
const anchorEdits = resolveEditAnchors(edits);
|
|
547
548
|
|
|
548
549
|
const rawContent = await fs.readFile(absolutePath, "utf-8");
|
|
550
|
+
await checkAutoGeneratedFileContent(rawContent, path);
|
|
549
551
|
const { bom, text } = stripBom(rawContent);
|
|
550
552
|
const originalEnding = detectLineEnding(text);
|
|
551
553
|
const originalNormalized = normalizeToLF(text);
|
|
@@ -685,6 +687,8 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
685
687
|
throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
|
|
686
688
|
}
|
|
687
689
|
|
|
690
|
+
await checkAutoGeneratedFile(resolvedPath, path);
|
|
691
|
+
|
|
688
692
|
const input: PatchInput = { path: resolvedPath, op, rename: resolvedRename, diff };
|
|
689
693
|
const fs = new LspFileSystem(this.#writethrough, signal, batchRequest);
|
|
690
694
|
const result = await applyPatch(input, {
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-generated file detection guard.
|
|
3
|
+
*
|
|
4
|
+
* Prevents editing of files that appear to be automatically generated
|
|
5
|
+
* by code generation tools (protoc, sqlc, buf, swagger, etc.).
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs/promises";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { settings } from "../config/settings";
|
|
10
|
+
import { ToolError } from "./tool-errors";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Number of bytes to read from the start of a file for auto-generated detection.
|
|
14
|
+
*/
|
|
15
|
+
const CHECK_BYTE_COUNT = 1024;
|
|
16
|
+
const HEADER_LINE_LIMIT = 40;
|
|
17
|
+
|
|
18
|
+
const KNOWN_GENERATOR_PATTERN =
|
|
19
|
+
"(?:protoc(?:-gen-[\\w-]+)?|sqlc|buf|swagger(?:-codegen)?|openapi(?:-generator)?|grpc-gateway|mockery|stringer|easyjson|deepcopy-gen|defaulter-gen|conversion-gen|client-gen|lister-gen|informer-gen)";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Strong marker patterns for generated-file headers.
|
|
23
|
+
*
|
|
24
|
+
* Keep these strict: broad patterns like /auto-generated/ cause false positives
|
|
25
|
+
* in normal hand-written files (including this guard implementation itself).
|
|
26
|
+
*/
|
|
27
|
+
const AUTO_GENERATED_HEADER_MARKERS: readonly RegExp[] = [
|
|
28
|
+
/@generated\b/i,
|
|
29
|
+
/\bcode\s+generated\s+by\s+[a-z0-9_.-]+/i,
|
|
30
|
+
/\bthis\s+file\s+was\s+automatically\s+generated\b/i,
|
|
31
|
+
new RegExp(`\\bgenerated\\s+by\\s+${KNOWN_GENERATOR_PATTERN}\\b`, "i"),
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
type CommentStyle = "slash" | "hash" | "sql" | "html";
|
|
35
|
+
|
|
36
|
+
const COMMENT_STYLES_BY_EXTENSION = new Map<string, readonly CommentStyle[]>([
|
|
37
|
+
[".c", ["slash"]],
|
|
38
|
+
[".cc", ["slash"]],
|
|
39
|
+
[".cpp", ["slash"]],
|
|
40
|
+
[".cs", ["slash"]],
|
|
41
|
+
[".dart", ["slash"]],
|
|
42
|
+
[".go", ["slash"]],
|
|
43
|
+
[".h", ["slash"]],
|
|
44
|
+
[".hpp", ["slash"]],
|
|
45
|
+
[".java", ["slash"]],
|
|
46
|
+
[".js", ["slash"]],
|
|
47
|
+
[".jsx", ["slash"]],
|
|
48
|
+
[".kt", ["slash"]],
|
|
49
|
+
[".kts", ["slash"]],
|
|
50
|
+
[".mjs", ["slash"]],
|
|
51
|
+
[".cjs", ["slash"]],
|
|
52
|
+
[".php", ["slash"]],
|
|
53
|
+
[".rs", ["slash"]],
|
|
54
|
+
[".scala", ["slash"]],
|
|
55
|
+
[".swift", ["slash"]],
|
|
56
|
+
[".ts", ["slash"]],
|
|
57
|
+
[".tsx", ["slash"]],
|
|
58
|
+
[".py", ["hash"]],
|
|
59
|
+
[".rb", ["hash"]],
|
|
60
|
+
[".sh", ["hash"]],
|
|
61
|
+
[".bash", ["hash"]],
|
|
62
|
+
[".zsh", ["hash"]],
|
|
63
|
+
[".yml", ["hash"]],
|
|
64
|
+
[".yaml", ["hash"]],
|
|
65
|
+
[".toml", ["hash"]],
|
|
66
|
+
[".ini", ["hash"]],
|
|
67
|
+
[".cfg", ["hash"]],
|
|
68
|
+
[".conf", ["hash"]],
|
|
69
|
+
[".env", ["hash"]],
|
|
70
|
+
[".pl", ["hash"]],
|
|
71
|
+
[".r", ["hash"]],
|
|
72
|
+
[".sql", ["sql"]],
|
|
73
|
+
[".html", ["html"]],
|
|
74
|
+
[".htm", ["html"]],
|
|
75
|
+
[".xml", ["html"]],
|
|
76
|
+
[".svg", ["html"]],
|
|
77
|
+
[".xhtml", ["html"]],
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const COMMENT_STYLES_BY_BASENAME = new Map<string, readonly CommentStyle[]>([
|
|
81
|
+
["dockerfile", ["hash"]],
|
|
82
|
+
["makefile", ["hash"]],
|
|
83
|
+
["justfile", ["hash"]],
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* File name patterns that strongly indicate auto-generated files.
|
|
88
|
+
* These are checked against the file name (not content).
|
|
89
|
+
*/
|
|
90
|
+
const AUTO_GENERATED_FILENAME_PATTERNS = [
|
|
91
|
+
/^zz_generated\./,
|
|
92
|
+
/\.pb\.(go|cc|h|c|js|ts)$/,
|
|
93
|
+
/_pb2\.py$/,
|
|
94
|
+
/_pb2_grpc\.py$/,
|
|
95
|
+
/\.gen\.(go|ts|js|py)$/,
|
|
96
|
+
/^generated\.(go|ts|js|py)$/,
|
|
97
|
+
/\.swagger\.json$/,
|
|
98
|
+
/\.openapi\.json$/,
|
|
99
|
+
/\.mock\.(go|ts)$/,
|
|
100
|
+
/\.mocks?\.(go|ts|js)$/,
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
function stripBom(content: string): string {
|
|
104
|
+
if (content.charCodeAt(0) === 0xfeff) {
|
|
105
|
+
return content.slice(1);
|
|
106
|
+
}
|
|
107
|
+
return content;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getCommentStylesForPath(filePath: string): readonly CommentStyle[] {
|
|
111
|
+
const normalizedPath = filePath.toLowerCase();
|
|
112
|
+
const fileName = path.basename(normalizedPath);
|
|
113
|
+
const stylesByName = COMMENT_STYLES_BY_BASENAME.get(fileName);
|
|
114
|
+
if (stylesByName) return stylesByName;
|
|
115
|
+
|
|
116
|
+
const ext = path.extname(fileName);
|
|
117
|
+
const stylesByExt = COMMENT_STYLES_BY_EXTENSION.get(ext);
|
|
118
|
+
return stylesByExt ?? [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extractLeadingHeaderCommentText(content: string, commentStyles: readonly CommentStyle[]): string {
|
|
122
|
+
if (commentStyles.length === 0) return "";
|
|
123
|
+
|
|
124
|
+
const includeSlash = commentStyles.includes("slash");
|
|
125
|
+
const includeHash = commentStyles.includes("hash");
|
|
126
|
+
const includeSql = commentStyles.includes("sql");
|
|
127
|
+
const includeHtml = commentStyles.includes("html");
|
|
128
|
+
|
|
129
|
+
const lines = stripBom(content).split(/\r?\n/);
|
|
130
|
+
const headerLines: string[] = [];
|
|
131
|
+
let started = false;
|
|
132
|
+
let inSlashBlock = false;
|
|
133
|
+
let inHtmlBlock = false;
|
|
134
|
+
|
|
135
|
+
for (let lineIndex = 0; lineIndex < lines.length && lineIndex < HEADER_LINE_LIMIT; lineIndex += 1) {
|
|
136
|
+
const trimmed = lines[lineIndex]?.trim() ?? "";
|
|
137
|
+
if (lineIndex === 0 && trimmed.startsWith("#!")) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (inSlashBlock) {
|
|
142
|
+
headerLines.push(trimmed);
|
|
143
|
+
if (trimmed.includes("*/")) inSlashBlock = false;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (inHtmlBlock) {
|
|
148
|
+
headerLines.push(trimmed);
|
|
149
|
+
if (trimmed.includes("-->")) inHtmlBlock = false;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (trimmed.length === 0) {
|
|
154
|
+
if (started) headerLines.push("");
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (includeSlash && trimmed.startsWith("//")) {
|
|
159
|
+
started = true;
|
|
160
|
+
headerLines.push(trimmed);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (includeSlash && trimmed.startsWith("/*")) {
|
|
165
|
+
started = true;
|
|
166
|
+
headerLines.push(trimmed);
|
|
167
|
+
if (!trimmed.includes("*/")) {
|
|
168
|
+
inSlashBlock = true;
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (includeHash && trimmed.startsWith("#")) {
|
|
174
|
+
started = true;
|
|
175
|
+
headerLines.push(trimmed);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (includeSql && trimmed.startsWith("--")) {
|
|
180
|
+
started = true;
|
|
181
|
+
headerLines.push(trimmed);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (includeHtml && trimmed.startsWith("<!--")) {
|
|
186
|
+
started = true;
|
|
187
|
+
headerLines.push(trimmed);
|
|
188
|
+
if (!trimmed.includes("-->")) {
|
|
189
|
+
inHtmlBlock = true;
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (started) break;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return headerLines.join("\n");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Check if a file name indicates it might be auto-generated.
|
|
203
|
+
* This is a quick pre-check before reading file content.
|
|
204
|
+
*/
|
|
205
|
+
function isAutoGeneratedFileName(filePath: string): boolean {
|
|
206
|
+
const fileName = filePath.split("/").pop() ?? "";
|
|
207
|
+
return AUTO_GENERATED_FILENAME_PATTERNS.some(pattern => pattern.test(fileName));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Check if leading header comments contain auto-generated markers.
|
|
212
|
+
* Returns the matched marker text if found, null otherwise.
|
|
213
|
+
*/
|
|
214
|
+
function detectAutoGeneratedMarker(content: string, filePath: string): string | null {
|
|
215
|
+
const commentStyles = getCommentStylesForPath(filePath);
|
|
216
|
+
const headerCommentText = extractLeadingHeaderCommentText(content, commentStyles);
|
|
217
|
+
if (!headerCommentText) return null;
|
|
218
|
+
|
|
219
|
+
for (const markerPattern of AUTO_GENERATED_HEADER_MARKERS) {
|
|
220
|
+
const match = markerPattern.exec(headerCommentText);
|
|
221
|
+
if (match?.[0]) return match[0];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Read the first N bytes of a file as a UTF-8 string.
|
|
229
|
+
* More efficient than reading the entire file.
|
|
230
|
+
*/
|
|
231
|
+
async function readFilePrefix(filePath: string, bytes: number): Promise<string | null> {
|
|
232
|
+
const handle = await fs.open(filePath, "r").catch(() => null);
|
|
233
|
+
if (!handle) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const buffer = Buffer.allocUnsafe(bytes);
|
|
239
|
+
const { bytesRead } = await handle.read(buffer, 0, bytes, 0);
|
|
240
|
+
return buffer.toString("utf-8", 0, bytesRead);
|
|
241
|
+
} catch {
|
|
242
|
+
return null;
|
|
243
|
+
} finally {
|
|
244
|
+
await handle.close();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Build the error message for an auto-generated file.
|
|
250
|
+
*/
|
|
251
|
+
function buildAutoGeneratedError(displayPath: string, detected: string): ToolError {
|
|
252
|
+
return new ToolError(
|
|
253
|
+
`Cannot modify auto-generated file: ${displayPath}\n\n` +
|
|
254
|
+
`This file appears to be automatically generated (detected marker: "${detected}").\n` +
|
|
255
|
+
`Auto-generated files should not be edited directly. Instead:\n` +
|
|
256
|
+
`1. Find the source file or generator configuration\n` +
|
|
257
|
+
`2. Make changes to the source\n` +
|
|
258
|
+
`3. Regenerate the file`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Check if a file is auto-generated by examining its content.
|
|
264
|
+
* Throws ToolError if the file appears to be auto-generated.
|
|
265
|
+
*
|
|
266
|
+
* @param absolutePath - Absolute path to the file
|
|
267
|
+
* @param displayPath - Path to show in error messages (relative or as provided)
|
|
268
|
+
*/
|
|
269
|
+
export async function checkAutoGeneratedFile(absolutePath: string, displayPath?: string): Promise<void> {
|
|
270
|
+
if (!settings.get("edit.blockAutoGenerated")) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const pathForDisplay = displayPath ?? absolutePath;
|
|
275
|
+
|
|
276
|
+
if (isAutoGeneratedFileName(absolutePath)) {
|
|
277
|
+
const fileName = absolutePath.split("/").pop() ?? "";
|
|
278
|
+
throw buildAutoGeneratedError(pathForDisplay, fileName);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const content = await readFilePrefix(absolutePath, CHECK_BYTE_COUNT);
|
|
282
|
+
if (content === null) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const marker = detectAutoGeneratedMarker(content, absolutePath);
|
|
287
|
+
if (marker) {
|
|
288
|
+
throw buildAutoGeneratedError(pathForDisplay, marker);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Check if file content is auto-generated.
|
|
294
|
+
* Uses only the first CHECK_BYTE_COUNT characters of the content.
|
|
295
|
+
* Throws ToolError if the content appears to be auto-generated.
|
|
296
|
+
*
|
|
297
|
+
* @param content - File content to check (can be full content or prefix)
|
|
298
|
+
* @param displayPath - Path to show in error messages
|
|
299
|
+
*/
|
|
300
|
+
export async function checkAutoGeneratedFileContent(content: string, displayPath: string): Promise<void> {
|
|
301
|
+
if (!settings.get("edit.blockAutoGenerated")) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const prefix = content.slice(0, CHECK_BYTE_COUNT);
|
|
306
|
+
const marker = detectAutoGeneratedMarker(prefix, displayPath);
|
|
307
|
+
if (marker) {
|
|
308
|
+
throw buildAutoGeneratedError(displayPath, marker);
|
|
309
|
+
}
|
|
310
|
+
}
|
package/src/tools/write.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import type {
|
|
2
3
|
AgentTool,
|
|
3
4
|
AgentToolContext,
|
|
@@ -16,6 +17,7 @@ import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
|
16
17
|
import writeDescription from "../prompts/tools/write.md" with { type: "text" };
|
|
17
18
|
import type { ToolSession } from "../sdk";
|
|
18
19
|
import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
|
|
20
|
+
import { checkAutoGeneratedFile } from "./auto-generated-guard";
|
|
19
21
|
import { invalidateFsScanAfterWrite } from "./fs-cache-invalidation";
|
|
20
22
|
import { type OutputMeta, outputMeta } from "./output-meta";
|
|
21
23
|
import { enforcePlanModeWrite, resolvePlanPath } from "./plan-mode-guard";
|
|
@@ -102,6 +104,11 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
102
104
|
const absolutePath = resolvePlanPath(this.session, path);
|
|
103
105
|
const batchRequest = getLspBatchRequest(context?.toolCall);
|
|
104
106
|
|
|
107
|
+
// Check if file exists and is auto-generated before overwriting
|
|
108
|
+
if (await fs.exists(absolutePath)) {
|
|
109
|
+
await checkAutoGeneratedFile(absolutePath, path);
|
|
110
|
+
}
|
|
111
|
+
|
|
105
112
|
const diagnostics = await this.#writethrough(absolutePath, content, signal, undefined, batchRequest);
|
|
106
113
|
invalidateFsScanAfterWrite(absolutePath);
|
|
107
114
|
|