@oh-my-pi/pi-coding-agent 14.2.1 → 14.4.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.
Files changed (137) hide show
  1. package/CHANGELOG.md +143 -1
  2. package/package.json +19 -19
  3. package/src/autoresearch/prompt.md +1 -1
  4. package/src/cli/args.ts +10 -1
  5. package/src/cli/shell-cli.ts +15 -3
  6. package/src/commit/agentic/prompts/analyze-file.md +1 -1
  7. package/src/config/model-registry.ts +67 -15
  8. package/src/config/prompt-templates.ts +5 -5
  9. package/src/config/settings-schema.ts +63 -4
  10. package/src/cursor.ts +3 -8
  11. package/src/debug/system-info.ts +6 -2
  12. package/src/discovery/claude.ts +58 -36
  13. package/src/discovery/helpers.ts +3 -3
  14. package/src/discovery/opencode.ts +20 -2
  15. package/src/edit/diff.ts +50 -47
  16. package/src/edit/index.ts +87 -57
  17. package/src/edit/line-hash.ts +735 -19
  18. package/src/edit/modes/apply-patch.ts +0 -9
  19. package/src/edit/modes/atom.ts +658 -0
  20. package/src/edit/modes/chunk.ts +144 -78
  21. package/src/edit/modes/hashline.ts +223 -146
  22. package/src/edit/modes/patch.ts +5 -9
  23. package/src/edit/modes/replace.ts +6 -11
  24. package/src/edit/renderer.ts +112 -143
  25. package/src/edit/streaming.ts +385 -0
  26. package/src/exec/bash-executor.ts +58 -5
  27. package/src/export/html/template.generated.ts +1 -1
  28. package/src/export/html/template.js +4 -12
  29. package/src/extensibility/custom-tools/types.ts +2 -0
  30. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  31. package/src/internal-urls/docs-index.generated.ts +7 -7
  32. package/src/internal-urls/pi-protocol.ts +0 -2
  33. package/src/lsp/client.ts +8 -1
  34. package/src/lsp/defaults.json +2 -1
  35. package/src/lsp/index.ts +1 -1
  36. package/src/mcp/render.ts +1 -8
  37. package/src/modes/acp/acp-agent.ts +76 -2
  38. package/src/modes/components/assistant-message.ts +5 -34
  39. package/src/modes/components/diff.ts +23 -14
  40. package/src/modes/components/footer.ts +21 -16
  41. package/src/modes/components/hook-editor.ts +1 -1
  42. package/src/modes/components/settings-defs.ts +6 -1
  43. package/src/modes/components/todo-reminder.ts +1 -8
  44. package/src/modes/components/tool-execution.ts +112 -105
  45. package/src/modes/controllers/input-controller.ts +1 -1
  46. package/src/modes/controllers/selector-controller.ts +1 -1
  47. package/src/modes/interactive-mode.ts +0 -2
  48. package/src/modes/print-mode.ts +8 -0
  49. package/src/modes/theme/mermaid-cache.ts +13 -52
  50. package/src/modes/theme/theme.ts +2 -2
  51. package/src/prompts/agents/librarian.md +1 -1
  52. package/src/prompts/agents/reviewer.md +4 -4
  53. package/src/prompts/ci-green-request.md +1 -1
  54. package/src/prompts/review-request.md +1 -1
  55. package/src/prompts/system/subagent-system-prompt.md +3 -3
  56. package/src/prompts/system/subagent-yield-reminder.md +11 -0
  57. package/src/prompts/system/system-prompt.md +4 -1
  58. package/src/prompts/tools/ask.md +3 -2
  59. package/src/prompts/tools/ast-edit.md +15 -19
  60. package/src/prompts/tools/ast-grep.md +18 -24
  61. package/src/prompts/tools/atom.md +96 -0
  62. package/src/prompts/tools/browser.md +1 -0
  63. package/src/prompts/tools/chunk-edit.md +58 -179
  64. package/src/prompts/tools/debug.md +4 -5
  65. package/src/prompts/tools/exit-plan-mode.md +4 -5
  66. package/src/prompts/tools/find.md +4 -8
  67. package/src/prompts/tools/github.md +18 -0
  68. package/src/prompts/tools/grep.md +8 -8
  69. package/src/prompts/tools/hashline.md +22 -89
  70. package/src/prompts/tools/{gemini-image.md → image-gen.md} +1 -1
  71. package/src/prompts/tools/inspect-image.md +6 -6
  72. package/src/prompts/tools/lsp.md +6 -0
  73. package/src/prompts/tools/patch.md +12 -19
  74. package/src/prompts/tools/python.md +3 -2
  75. package/src/prompts/tools/read-chunk.md +46 -8
  76. package/src/prompts/tools/read.md +9 -6
  77. package/src/prompts/tools/ssh.md +8 -17
  78. package/src/prompts/tools/todo-write.md +54 -41
  79. package/src/sdk.ts +22 -14
  80. package/src/session/agent-session.ts +61 -22
  81. package/src/session/session-manager.ts +228 -57
  82. package/src/session/streaming-output.ts +11 -0
  83. package/src/system-prompt.ts +7 -2
  84. package/src/task/executor.ts +44 -48
  85. package/src/task/render.ts +11 -13
  86. package/src/tools/ask.ts +7 -7
  87. package/src/tools/ast-edit.ts +45 -41
  88. package/src/tools/ast-grep.ts +77 -85
  89. package/src/tools/bash.ts +21 -9
  90. package/src/tools/browser.ts +32 -30
  91. package/src/tools/calculator.ts +4 -4
  92. package/src/tools/cancel-job.ts +1 -1
  93. package/src/tools/checkpoint.ts +2 -2
  94. package/src/tools/debug.ts +41 -37
  95. package/src/tools/exit-plan-mode.ts +1 -1
  96. package/src/tools/find.ts +4 -4
  97. package/src/tools/gh-renderer.ts +12 -4
  98. package/src/tools/gh.ts +514 -712
  99. package/src/tools/grep.ts +115 -130
  100. package/src/tools/{gemini-image.ts → image-gen.ts} +459 -60
  101. package/src/tools/index.ts +14 -32
  102. package/src/tools/inspect-image.ts +3 -3
  103. package/src/tools/json-tree.ts +114 -114
  104. package/src/tools/match-line-format.ts +9 -8
  105. package/src/tools/notebook.ts +8 -7
  106. package/src/tools/poll-tool.ts +2 -1
  107. package/src/tools/python.ts +9 -23
  108. package/src/tools/read.ts +32 -21
  109. package/src/tools/render-mermaid.ts +1 -1
  110. package/src/tools/render-utils.ts +18 -0
  111. package/src/tools/renderers.ts +2 -2
  112. package/src/tools/report-tool-issue.ts +3 -2
  113. package/src/tools/resolve.ts +1 -1
  114. package/src/tools/review.ts +12 -10
  115. package/src/tools/search-tool-bm25.ts +2 -4
  116. package/src/tools/sqlite-reader.ts +116 -3
  117. package/src/tools/ssh.ts +4 -4
  118. package/src/tools/todo-write.ts +172 -147
  119. package/src/tools/vim.ts +14 -15
  120. package/src/tools/write.ts +4 -4
  121. package/src/tools/{submit-result.ts → yield.ts} +11 -13
  122. package/src/utils/edit-mode.ts +2 -1
  123. package/src/utils/file-display-mode.ts +10 -5
  124. package/src/utils/git.ts +9 -5
  125. package/src/utils/shell-snapshot.ts +2 -3
  126. package/src/vim/render.ts +4 -4
  127. package/src/web/search/providers/codex.ts +129 -6
  128. package/src/prompts/system/subagent-submit-reminder.md +0 -11
  129. package/src/prompts/tools/gh-issue-view.md +0 -11
  130. package/src/prompts/tools/gh-pr-checkout.md +0 -12
  131. package/src/prompts/tools/gh-pr-diff.md +0 -12
  132. package/src/prompts/tools/gh-pr-push.md +0 -11
  133. package/src/prompts/tools/gh-pr-view.md +0 -11
  134. package/src/prompts/tools/gh-repo-view.md +0 -11
  135. package/src/prompts/tools/gh-run-watch.md +0 -12
  136. package/src/prompts/tools/gh-search-issues.md +0 -11
  137. package/src/prompts/tools/gh-search-prs.md +0 -11
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as nodePath from "node:path";
3
3
  import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
4
- import { StringEnum } from "@oh-my-pi/pi-coding-agent";
4
+ import { StringEnum } from "@oh-my-pi/pi-ai";
5
5
  import {
6
6
  ChunkAnchorStyle,
7
7
  ChunkEditOp,
@@ -25,6 +25,7 @@ import { invalidateFsScanAfterWrite } from "../../tools/fs-cache-invalidation";
25
25
  import { outputMeta } from "../../tools/output-meta";
26
26
  import { enforcePlanModeWrite, resolvePlanPath } from "../../tools/plan-mode-guard";
27
27
  import { generateUnifiedDiffString } from "../diff";
28
+ import { HASHLINE_BIGRAMS } from "../line-hash";
28
29
  import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "../normalize";
29
30
  import type { EditToolDetails, LspBatchRequest } from "../renderer";
30
31
 
@@ -32,7 +33,6 @@ export type { ChunkReadTarget };
32
33
 
33
34
  export type ChunkEditOperation =
34
35
  | { op: "put"; sel?: string; content: string }
35
- | { op: "replace"; sel?: string; content: string; find: string }
36
36
  | { op: "delete"; sel?: string }
37
37
  | { op: "before"; sel?: string; content: string }
38
38
  | { op: "after"; sel?: string; content: string }
@@ -120,6 +120,8 @@ type ChunkSourceContext = {
120
120
  chunkLanguage: string | undefined;
121
121
  };
122
122
 
123
+ type ChunkSourceIntent = "read" | "write";
124
+
123
125
  function normalizeLanguage(language: string | undefined): string {
124
126
  return language?.trim().toLowerCase() || "";
125
127
  }
@@ -140,11 +142,17 @@ function fileLanguageTag(filePath: string, language?: string): string | undefine
140
142
  return ext.length > 0 ? ext : undefined;
141
143
  }
142
144
 
143
- async function resolveChunkSourceContext(session: ToolSession, path: string): Promise<ChunkSourceContext> {
145
+ async function resolveChunkSourceContext(
146
+ session: ToolSession,
147
+ path: string,
148
+ options?: { intent?: ChunkSourceIntent },
149
+ ): Promise<ChunkSourceContext> {
144
150
  const resolvedPath = resolvePlanPath(session, path);
145
151
  const sourceFile = Bun.file(resolvedPath);
146
152
  const sourceExists = await sourceFile.exists();
147
- enforcePlanModeWrite(session, path, { op: sourceExists ? "update" : "create" });
153
+ if ((options?.intent ?? "write") === "write") {
154
+ enforcePlanModeWrite(session, path, { op: sourceExists ? "update" : "create" });
155
+ }
148
156
 
149
157
  let rawContent = "";
150
158
  if (sourceExists) {
@@ -161,6 +169,57 @@ async function resolveChunkSourceContext(session: ToolSession, path: string): Pr
161
169
  };
162
170
  }
163
171
 
172
+ /**
173
+ * Preview-safe loader: read raw source without plan-mode enforcement or
174
+ * editable-file guards. Used by streaming diff previews that must not throw
175
+ * side-effecting errors while args are still being streamed.
176
+ */
177
+ export async function loadChunkSource(params: {
178
+ cwd: string;
179
+ path: string;
180
+ }): Promise<{ resolvedPath: string; rawContent: string; language: string | undefined; exists: boolean }> {
181
+ const resolvedPath = nodePath.isAbsolute(params.path) ? params.path : nodePath.resolve(params.cwd, params.path);
182
+ const sourceFile = Bun.file(resolvedPath);
183
+ const exists = await sourceFile.exists();
184
+ const rawContent = exists ? await sourceFile.text() : "";
185
+ return { resolvedPath, rawContent, language: getLanguageFromPath(resolvedPath), exists };
186
+ }
187
+
188
+ /**
189
+ * Compute a unified diff preview for a chunk edit without applying it.
190
+ * Used for streaming previews while args are still arriving. Returns
191
+ * `{ error }` on any failure so callers can decide whether to surface it.
192
+ */
193
+ export async function computeChunkDiff(
194
+ input: { path: string; edits: ChunkToolEdit[] },
195
+ cwd: string,
196
+ options?: { anchorStyle?: ChunkAnchorStyle; signal?: AbortSignal },
197
+ ): Promise<{ diff: string; firstChangedLine: number | undefined } | { error: string }> {
198
+ try {
199
+ options?.signal?.throwIfAborted?.();
200
+ const { filePath } = parseChunkEditPath(input.path);
201
+ if (!filePath) return { error: "chunk edit path is empty" };
202
+ const { resolvedPath, rawContent, language } = await loadChunkSource({ cwd, path: filePath });
203
+ options?.signal?.throwIfAborted?.();
204
+ const { operations } = normalizeChunkEditOperations(input.edits);
205
+ const result = applyChunkEdits({
206
+ source: rawContent,
207
+ language,
208
+ cwd,
209
+ filePath: resolvedPath,
210
+ operations,
211
+ anchorStyle: options?.anchorStyle,
212
+ });
213
+ options?.signal?.throwIfAborted?.();
214
+ if (!result.changed) {
215
+ return { diff: "", firstChangedLine: undefined };
216
+ }
217
+ return generateUnifiedDiffString(result.diffSourceBefore, result.diffSourceAfter);
218
+ } catch (err) {
219
+ return { error: err instanceof Error ? err.message : String(err) };
220
+ }
221
+ }
222
+
164
223
  function normalizeChunkRegionSyntax(text: string): string {
165
224
  return text.replaceAll("@body", "~").replaceAll("@head", "^");
166
225
  }
@@ -189,6 +248,20 @@ function chunkReadPathSeparatorIndex(readPath: string): number {
189
248
  if (/^[a-zA-Z]:[/\\]/.test(readPath)) {
190
249
  return readPath.indexOf(":", 2);
191
250
  }
251
+ const urlMatch = readPath.match(/^([a-z][a-z0-9+.-]*):\/\//i);
252
+ if (urlMatch) {
253
+ const scheme = urlMatch[1].toLowerCase();
254
+ const urlPrefixEnd = urlMatch[0].length;
255
+ if (scheme === "local") {
256
+ const index = readPath.lastIndexOf(":");
257
+ return index >= urlPrefixEnd ? index : -1;
258
+ }
259
+
260
+ const pathStart = readPath.indexOf("/", urlPrefixEnd);
261
+ if (pathStart === -1) return -1;
262
+ const index = readPath.lastIndexOf(":");
263
+ return index >= pathStart ? index : -1;
264
+ }
192
265
  return readPath.indexOf(":");
193
266
  }
194
267
 
@@ -300,11 +373,13 @@ export async function describeChunkedGrepMatch(params: {
300
373
  };
301
374
  }
302
375
 
303
- const CHUNK_CHECKSUM_ALPHABET = "ZPMQVRWSNKTXJBYH";
376
+ const CHUNK_CHECKSUM_BIGRAMS = new Set<string>(HASHLINE_BIGRAMS);
304
377
  type NativeChunkRegion = "head" | "body";
305
378
 
306
379
  function isChunkChecksumToken(value: string): boolean {
307
- return value.length === 4 && Array.from(value).every(ch => CHUNK_CHECKSUM_ALPHABET.includes(ch.toUpperCase()));
380
+ if (value.length !== 4) return false;
381
+ const lower = value.toLowerCase();
382
+ return CHUNK_CHECKSUM_BIGRAMS.has(lower.slice(0, 2)) && CHUNK_CHECKSUM_BIGRAMS.has(lower.slice(2, 4));
308
383
  }
309
384
 
310
385
  function parseChunkEditSelector(selector: string | undefined): {
@@ -334,11 +409,11 @@ function parseChunkEditSelector(selector: string | undefined): {
334
409
  if (hashIndex >= 0) {
335
410
  const suffix = selectorPart.slice(hashIndex + 1).trim();
336
411
  if (isChunkChecksumToken(suffix)) {
337
- crc = suffix.toUpperCase();
412
+ crc = suffix.toLowerCase();
338
413
  selectorPart = selectorPart.slice(0, hashIndex).trimEnd();
339
414
  }
340
415
  } else if (isChunkChecksumToken(selectorPart)) {
341
- crc = selectorPart.toUpperCase();
416
+ crc = selectorPart.toLowerCase();
342
417
  selectorPart = "";
343
418
  }
344
419
 
@@ -376,15 +451,6 @@ function toNativeEditOperation(
376
451
  region: nativeRegion,
377
452
  content: operation.content,
378
453
  };
379
- case "replace":
380
- return {
381
- op: ChunkEditOp.Replace,
382
- sel: selector,
383
- crc,
384
- region: nativeRegion,
385
- find: operation.find,
386
- content: operation.content,
387
- };
388
454
  case "before":
389
455
  return { op: ChunkEditOp.Before, sel: selector, crc, region: nativeRegion, content: operation.content };
390
456
  case "after":
@@ -486,22 +552,21 @@ export function missingChunkReadTarget(selector: string): ChunkReadTarget {
486
552
 
487
553
  export const chunkToolEditSchema = Type.Object(
488
554
  {
489
- path: Type.String({
490
- description: "File path with chunk selector. Examples: 'src/app.ts:fn_foo#ABCD~', 'src/app.ts:class_Bar'.",
491
- }),
555
+ path: Type.Optional(
556
+ Type.String({
557
+ description: "File path with chunk selector. Examples: 'src/app.ts:fn_foo#thth~', 'src/app.ts:class_Bar'.",
558
+ }),
559
+ ),
492
560
  write: Type.Optional(
493
561
  Type.Union([Type.String(), Type.Null()], {
494
- description: "Write complete new content to the targeted region. Use null to delete the chunk.",
562
+ description:
563
+ "Write complete new content to the targeted region. Null is rejected; use delete: true for deletion.",
495
564
  }),
496
565
  ),
497
- replace: Type.Optional(
498
- Type.Object(
499
- {
500
- old: Type.String({ description: "Literal substring to find. Must match exactly once." }),
501
- new: Type.String({ description: "Replacement text." }),
502
- },
503
- { description: "Find and replace a substring within the chunk." },
504
- ),
566
+ delete: Type.Optional(
567
+ Type.Boolean({
568
+ description: "Explicitly delete the targeted chunk. Must be true; include the current chunk ID.",
569
+ }),
505
570
  ),
506
571
  insert: Type.Optional(
507
572
  Type.Object(
@@ -517,6 +582,7 @@ export const chunkToolEditSchema = Type.Object(
517
582
  );
518
583
  export const chunkEditParamsSchema = Type.Object(
519
584
  {
585
+ path: Type.Optional(Type.String({ description: "Default file path used when an edit omits its own `path`" })),
520
586
  edits: Type.Array(chunkToolEditSchema, {
521
587
  description: "Chunk edits",
522
588
  minItems: 1,
@@ -538,25 +604,6 @@ export interface ExecuteChunkSingleOptions {
538
604
  beginDeferredDiagnosticsForPath: (path: string) => WritethroughDeferredHandle;
539
605
  }
540
606
 
541
- export function isChunkParams(params: unknown): params is ChunkParams {
542
- if (
543
- typeof params !== "object" ||
544
- params === null ||
545
- !("edits" in params) ||
546
- !Array.isArray(params.edits) ||
547
- params.edits.length === 0
548
- ) {
549
- return false;
550
- }
551
- const first = params.edits[0];
552
- // Accept a bare `{ path }` entry: it is interpreted downstream as a chunk
553
- // delete. Some providers strip `null` values from tool-call JSON, so a
554
- // documented `{ path, write: null }` delete can arrive here as just
555
- // `{ path }`. Rejecting that surfaced as a misleading
556
- // "Invalid edit parameters for chunk mode." error.
557
- return typeof first === "object" && first !== null && "path" in first;
558
- }
559
-
560
607
  /** Auto-correct indentation for content targeting a body region (`~`) when autoIndent is on.
561
608
  * Handles two patterns:
562
609
  * 1. Tab-based over-indentation: models include the function's base \t indent.
@@ -605,6 +652,29 @@ function autoCorrectBodyIndent(content: string, index: number): { content: strin
605
652
  return { content, warnings };
606
653
  }
607
654
 
655
+ function chunkEditOperationFields(edit: ChunkToolEdit): string[] {
656
+ const fields: string[] = [];
657
+ if (edit.write !== undefined) fields.push("write");
658
+ if (edit.insert != null) fields.push("insert");
659
+ if (edit.delete === true) fields.push("delete");
660
+ return fields;
661
+ }
662
+
663
+ function assertSingleChunkOperation(edit: ChunkToolEdit, index: number): string {
664
+ const fields = chunkEditOperationFields(edit);
665
+ if (fields.length === 0) {
666
+ throw new Error(
667
+ `Edit ${index + 1}: no operation specified. Use write:"..." to replace, insert:{loc,body} to insert, or delete:true to delete. Use the open tool to inspect chunks.`,
668
+ );
669
+ }
670
+ if (fields.length > 1) {
671
+ throw new Error(
672
+ `Edit ${index + 1}: multiple operation fields set (${fields.join(", ")}). Each chunk edit entry must have exactly one operation.`,
673
+ );
674
+ }
675
+ return fields[0];
676
+ }
677
+
608
678
  function normalizeChunkEditOperations(edits: ChunkToolEdit[]): {
609
679
  operations: ChunkEditOperation[];
610
680
  warnings: string[];
@@ -612,26 +682,22 @@ function normalizeChunkEditOperations(edits: ChunkToolEdit[]): {
612
682
  const warnings: string[] = [];
613
683
  const operations = edits.map((edit, index): ChunkEditOperation => {
614
684
  const { selector } = parseChunkEditPath(edit.path);
615
- // When multiple ops are present (model confusion), prefer write (total replacement) as the
616
- // safest default, then replace (surgical), then insert (additive), then delete.
617
- const hasInsert = edit.insert != null && typeof edit.insert.body === "string" && edit.insert.body.length > 0;
618
- const hasReplace =
619
- edit.replace != null &&
620
- ((typeof edit.replace.old === "string" && edit.replace.old.length > 0) ||
621
- (typeof edit.replace.new === "string" && edit.replace.new.length > 0));
622
- const hasWrite = typeof edit.write === "string" && edit.write.length > 0;
623
- const opCount = [hasInsert, hasReplace, hasWrite].filter(Boolean).length;
624
- if (opCount > 1) {
625
- const chosen = hasWrite ? "write" : hasReplace ? "replace" : "insert";
626
- const present = [hasWrite && "write", hasReplace && "replace", hasInsert && "insert"]
627
- .filter(Boolean)
628
- .join(", ");
629
- warnings.push(
630
- `Edit ${index + 1}: multiple operation fields set (${present}). Each edit entry must have exactly ONE of write/replace/insert — not multiple. Used "${chosen}", ignored the rest.`,
631
- );
632
- }
633
- if (hasWrite) {
634
- let writeContent = edit.write!;
685
+ const operation = assertSingleChunkOperation(edit, index);
686
+ if (operation === "write") {
687
+ if (edit.write === null) {
688
+ throw new Error(
689
+ `Edit ${index + 1}: write:null no longer deletes chunks. Use delete:true to delete, or open the chunk to inspect its content without modifying the file.`,
690
+ );
691
+ }
692
+ if (typeof edit.write !== "string") {
693
+ throw new Error(`Edit ${index + 1}: write must be a string.`);
694
+ }
695
+ if (edit.write.length === 0) {
696
+ throw new Error(
697
+ `Edit ${index + 1}: write:"" is a destructive empty replacement. Use delete:true to delete the chunk, or open the chunk to inspect its content without modifying the file.`,
698
+ );
699
+ }
700
+ let writeContent = edit.write;
635
701
  if (selector?.endsWith("~")) {
636
702
  const corrected = autoCorrectBodyIndent(writeContent, index);
637
703
  writeContent = corrected.content;
@@ -639,15 +705,12 @@ function normalizeChunkEditOperations(edits: ChunkToolEdit[]): {
639
705
  }
640
706
  return { op: "put", sel: selector, content: writeContent };
641
707
  }
642
- if (typeof edit.write === "string" && !hasInsert && !hasReplace) {
643
- return { op: "put", sel: selector, content: edit.write };
644
- }
645
- if (hasReplace) {
646
- return { op: "replace", sel: selector, content: edit.replace!.new, find: edit.replace!.old };
647
- }
648
- if (hasInsert) {
649
- const op = edit.insert!.loc === "prepend" ? "before" : "after";
650
- let insertContent = edit.insert!.body;
708
+ if (operation === "insert") {
709
+ if (edit.insert == null || typeof edit.insert.body !== "string" || edit.insert.body.length === 0) {
710
+ throw new Error(`Edit ${index + 1}: insert.body must be a non-empty string.`);
711
+ }
712
+ const op = edit.insert.loc === "prepend" ? "before" : "after";
713
+ let insertContent = edit.insert.body;
651
714
  if (selector?.endsWith("~")) {
652
715
  const corrected = autoCorrectBodyIndent(insertContent, index);
653
716
  insertContent = corrected.content;
@@ -655,7 +718,9 @@ function normalizeChunkEditOperations(edits: ChunkToolEdit[]): {
655
718
  }
656
719
  return { op, sel: selector, content: insertContent };
657
720
  }
658
- // write: null or no op specified → delete
721
+ if (operation !== "delete") {
722
+ throw new Error(`Edit ${index + 1}: unsupported chunk edit operation "${operation}".`);
723
+ }
659
724
  return { op: "delete", sel: selector };
660
725
  });
661
726
  return { operations, warnings };
@@ -717,6 +782,7 @@ export async function executeChunkSingle(
717
782
  const { resolvedPath, sourceFile, sourceExists, rawContent, chunkLanguage } = await resolveChunkSourceContext(
718
783
  session,
719
784
  path,
785
+ { intent: "write" },
720
786
  );
721
787
  const parentDir = nodePath.dirname(resolvedPath);
722
788
  if (parentDir && parentDir !== ".") {