@oh-my-pi/pi-coding-agent 13.19.0 → 14.0.2

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 (202) hide show
  1. package/CHANGELOG.md +266 -1
  2. package/package.json +86 -20
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +91 -0
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +83 -125
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -5
  28. package/src/commit/agentic/index.ts +3 -4
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/validation.ts +1 -1
  31. package/src/commit/analysis/conventional.ts +4 -4
  32. package/src/commit/analysis/summary.ts +3 -3
  33. package/src/commit/changelog/generate.ts +4 -4
  34. package/src/commit/map-reduce/map-phase.ts +4 -4
  35. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  36. package/src/commit/pipeline.ts +3 -4
  37. package/src/config/prompt-templates.ts +44 -226
  38. package/src/config/resolve-config-value.ts +4 -2
  39. package/src/config/settings-schema.ts +54 -2
  40. package/src/config/settings.ts +25 -26
  41. package/src/dap/client.ts +674 -0
  42. package/src/dap/config.ts +150 -0
  43. package/src/dap/defaults.json +211 -0
  44. package/src/dap/index.ts +4 -0
  45. package/src/dap/session.ts +1255 -0
  46. package/src/dap/types.ts +600 -0
  47. package/src/debug/log-viewer.ts +3 -2
  48. package/src/discovery/builtin.ts +1 -2
  49. package/src/discovery/codex.ts +2 -2
  50. package/src/discovery/github.ts +2 -1
  51. package/src/discovery/helpers.ts +2 -2
  52. package/src/discovery/opencode.ts +2 -2
  53. package/src/edit/diff.ts +818 -0
  54. package/src/edit/index.ts +309 -0
  55. package/src/edit/line-hash.ts +67 -0
  56. package/src/edit/modes/chunk.ts +454 -0
  57. package/src/{patch → edit/modes}/hashline.ts +741 -361
  58. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  59. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  60. package/src/{patch → edit}/normalize.ts +97 -76
  61. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  62. package/src/exec/bash-executor.ts +4 -2
  63. package/src/exec/idle-timeout-watchdog.ts +126 -0
  64. package/src/exec/non-interactive-env.ts +5 -0
  65. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +2 -2
  66. package/src/extensibility/custom-commands/bundled/review/index.ts +2 -2
  67. package/src/extensibility/custom-commands/loader.ts +1 -2
  68. package/src/extensibility/custom-tools/loader.ts +34 -11
  69. package/src/extensibility/extensions/loader.ts +9 -4
  70. package/src/extensibility/extensions/runner.ts +24 -1
  71. package/src/extensibility/extensions/types.ts +1 -1
  72. package/src/extensibility/hooks/loader.ts +5 -6
  73. package/src/extensibility/hooks/types.ts +1 -1
  74. package/src/extensibility/plugins/doctor.ts +2 -1
  75. package/src/extensibility/slash-commands.ts +3 -7
  76. package/src/index.ts +2 -1
  77. package/src/internal-urls/docs-index.generated.ts +11 -11
  78. package/src/ipy/executor.ts +58 -17
  79. package/src/ipy/gateway-coordinator.ts +6 -4
  80. package/src/ipy/kernel.ts +45 -22
  81. package/src/ipy/runtime.ts +2 -2
  82. package/src/lsp/client.ts +7 -4
  83. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  84. package/src/lsp/config.ts +2 -2
  85. package/src/lsp/defaults.json +688 -154
  86. package/src/lsp/index.ts +234 -45
  87. package/src/lsp/lspmux.ts +2 -2
  88. package/src/lsp/startup-events.ts +13 -0
  89. package/src/lsp/types.ts +12 -1
  90. package/src/lsp/utils.ts +8 -1
  91. package/src/main.ts +102 -46
  92. package/src/memories/index.ts +4 -5
  93. package/src/modes/acp/acp-agent.ts +563 -163
  94. package/src/modes/acp/acp-event-mapper.ts +9 -1
  95. package/src/modes/acp/acp-mode.ts +4 -2
  96. package/src/modes/components/agent-dashboard.ts +3 -4
  97. package/src/modes/components/diff.ts +6 -7
  98. package/src/modes/components/read-tool-group.ts +6 -12
  99. package/src/modes/components/settings-defs.ts +5 -0
  100. package/src/modes/components/tool-execution.ts +1 -1
  101. package/src/modes/components/welcome.ts +1 -1
  102. package/src/modes/controllers/btw-controller.ts +2 -2
  103. package/src/modes/controllers/command-controller.ts +3 -2
  104. package/src/modes/controllers/input-controller.ts +12 -8
  105. package/src/modes/index.ts +20 -2
  106. package/src/modes/interactive-mode.ts +94 -37
  107. package/src/modes/rpc/host-tools.ts +186 -0
  108. package/src/modes/rpc/rpc-client.ts +178 -13
  109. package/src/modes/rpc/rpc-mode.ts +73 -3
  110. package/src/modes/rpc/rpc-types.ts +53 -1
  111. package/src/modes/theme/theme.ts +80 -8
  112. package/src/modes/types.ts +2 -2
  113. package/src/prompts/system/system-prompt.md +2 -1
  114. package/src/prompts/tools/chunk-edit.md +219 -0
  115. package/src/prompts/tools/debug.md +43 -0
  116. package/src/prompts/tools/grep.md +3 -0
  117. package/src/prompts/tools/lsp.md +5 -5
  118. package/src/prompts/tools/read-chunk.md +17 -0
  119. package/src/prompts/tools/read.md +19 -5
  120. package/src/sdk.ts +190 -154
  121. package/src/secrets/obfuscator.ts +1 -1
  122. package/src/session/agent-session.ts +306 -256
  123. package/src/session/agent-storage.ts +12 -12
  124. package/src/session/compaction/branch-summarization.ts +3 -3
  125. package/src/session/compaction/compaction.ts +5 -6
  126. package/src/session/compaction/utils.ts +3 -3
  127. package/src/session/history-storage.ts +62 -19
  128. package/src/session/messages.ts +3 -3
  129. package/src/session/session-dump-format.ts +203 -0
  130. package/src/session/session-storage.ts +4 -2
  131. package/src/session/streaming-output.ts +1 -1
  132. package/src/session/tool-choice-queue.ts +213 -0
  133. package/src/slash-commands/builtin-registry.ts +56 -8
  134. package/src/ssh/connection-manager.ts +2 -2
  135. package/src/ssh/sshfs-mount.ts +5 -5
  136. package/src/stt/downloader.ts +4 -4
  137. package/src/stt/recorder.ts +4 -4
  138. package/src/stt/transcriber.ts +2 -2
  139. package/src/system-prompt.ts +21 -13
  140. package/src/task/agents.ts +5 -6
  141. package/src/task/commands.ts +2 -5
  142. package/src/task/executor.ts +4 -4
  143. package/src/task/index.ts +3 -4
  144. package/src/task/template.ts +2 -2
  145. package/src/task/worktree.ts +4 -4
  146. package/src/tools/ask.ts +2 -3
  147. package/src/tools/ast-edit.ts +7 -7
  148. package/src/tools/ast-grep.ts +7 -7
  149. package/src/tools/auto-generated-guard.ts +36 -41
  150. package/src/tools/await-tool.ts +2 -2
  151. package/src/tools/bash.ts +5 -23
  152. package/src/tools/browser.ts +4 -5
  153. package/src/tools/calculator.ts +2 -3
  154. package/src/tools/cancel-job.ts +2 -2
  155. package/src/tools/checkpoint.ts +3 -3
  156. package/src/tools/debug.ts +1007 -0
  157. package/src/tools/exit-plan-mode.ts +2 -3
  158. package/src/tools/fetch.ts +67 -3
  159. package/src/tools/find.ts +4 -5
  160. package/src/tools/fs-cache-invalidation.ts +5 -0
  161. package/src/tools/gemini-image.ts +13 -5
  162. package/src/tools/gh.ts +10 -11
  163. package/src/tools/grep.ts +57 -9
  164. package/src/tools/index.ts +44 -22
  165. package/src/tools/inspect-image.ts +4 -4
  166. package/src/tools/output-meta.ts +1 -1
  167. package/src/tools/python.ts +19 -6
  168. package/src/tools/read.ts +198 -67
  169. package/src/tools/render-mermaid.ts +2 -3
  170. package/src/tools/render-utils.ts +20 -6
  171. package/src/tools/renderers.ts +3 -1
  172. package/src/tools/report-tool-issue.ts +80 -0
  173. package/src/tools/resolve.ts +70 -39
  174. package/src/tools/search-tool-bm25.ts +2 -2
  175. package/src/tools/ssh.ts +2 -2
  176. package/src/tools/todo-write.ts +2 -2
  177. package/src/tools/tool-timeouts.ts +1 -0
  178. package/src/tools/write.ts +5 -6
  179. package/src/tui/tree-list.ts +3 -1
  180. package/src/utils/clipboard.ts +80 -0
  181. package/src/utils/commit-message-generator.ts +2 -3
  182. package/src/utils/edit-mode.ts +49 -0
  183. package/src/utils/file-display-mode.ts +6 -5
  184. package/src/utils/file-mentions.ts +8 -7
  185. package/src/utils/git.ts +4 -4
  186. package/src/utils/image-loading.ts +98 -0
  187. package/src/utils/title-generator.ts +2 -3
  188. package/src/utils/tools-manager.ts +6 -6
  189. package/src/web/scrapers/choosealicense.ts +1 -1
  190. package/src/web/search/index.ts +3 -3
  191. package/src/autoresearch/command-initialize.md +0 -34
  192. package/src/patch/diff.ts +0 -433
  193. package/src/patch/index.ts +0 -888
  194. package/src/patch/parser.ts +0 -532
  195. package/src/patch/types.ts +0 -292
  196. package/src/prompts/agents/oracle.md +0 -77
  197. package/src/tools/pending-action.ts +0 -49
  198. package/src/utils/child-process.ts +0 -88
  199. package/src/utils/frontmatter.ts +0 -117
  200. package/src/utils/image-input.ts +0 -274
  201. package/src/utils/mime.ts +0 -53
  202. package/src/utils/prompt-format.ts +0 -170
@@ -3,7 +3,7 @@ import * as fs from "node:fs/promises";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { projfsOverlayStart, projfsOverlayStop } from "@oh-my-pi/pi-natives";
6
- import { getWorktreeDir, isEnoent, logger, Snowflake } from "@oh-my-pi/pi-utils";
6
+ import { $which, getWorktreeDir, isEnoent, logger, Snowflake } from "@oh-my-pi/pi-utils";
7
7
  import { $ } from "bun";
8
8
  import * as git from "../utils/git";
9
9
 
@@ -348,7 +348,7 @@ export async function ensureFuseOverlay(baseCwd: string, id: string): Promise<st
348
348
  const mergedDir = path.join(baseDir, "merged");
349
349
 
350
350
  // Clean up any stale mount at this path (linux only)
351
- const fusermount = Bun.which("fusermount3") ?? Bun.which("fusermount");
351
+ const fusermount = $which("fusermount3") ?? $which("fusermount");
352
352
  if (fusermount) {
353
353
  await $`${fusermount} -u ${mergedDir}`.quiet().nothrow();
354
354
  }
@@ -358,7 +358,7 @@ export async function ensureFuseOverlay(baseCwd: string, id: string): Promise<st
358
358
  await fs.mkdir(workDir, { recursive: true });
359
359
  await fs.mkdir(mergedDir, { recursive: true });
360
360
 
361
- const binary = Bun.which("fuse-overlayfs");
361
+ const binary = $which("fuse-overlayfs");
362
362
  if (!binary) {
363
363
  await fs.rm(baseDir, { recursive: true, force: true });
364
364
  throw new Error(
@@ -380,7 +380,7 @@ export async function ensureFuseOverlay(baseCwd: string, id: string): Promise<st
380
380
 
381
381
  export async function cleanupFuseOverlay(mergedDir: string): Promise<void> {
382
382
  try {
383
- const fusermount = Bun.which("fusermount3") ?? Bun.which("fusermount");
383
+ const fusermount = $which("fusermount3") ?? $which("fusermount");
384
384
  if (fusermount) {
385
385
  await $`${fusermount} -u ${mergedDir}`.quiet().nothrow();
386
386
  }
package/src/tools/ask.ts CHANGED
@@ -17,9 +17,8 @@
17
17
 
18
18
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
19
19
  import { type Component, Container, Markdown, renderInlineMarkdown, TERMINAL, Text } from "@oh-my-pi/pi-tui";
20
- import { untilAborted } from "@oh-my-pi/pi-utils";
20
+ import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
21
21
  import { type Static, Type } from "@sinclair/typebox";
22
- import { renderPromptTemplate } from "../config/prompt-templates";
23
22
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
24
23
  import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
25
24
  import askDescription from "../prompts/tools/ask.md" with { type: "text" };
@@ -385,7 +384,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
385
384
  readonly strict = true;
386
385
 
387
386
  constructor(private readonly session: ToolSession) {
388
- this.description = renderPromptTemplate(askDescription);
387
+ this.description = prompt.render(askDescription);
389
388
  }
390
389
 
391
390
  static createIf(session: ToolSession): AskTool | null {
@@ -3,12 +3,11 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
3
3
  import { type AstReplaceChange, astEdit } from "@oh-my-pi/pi-natives";
4
4
  import type { Component } from "@oh-my-pi/pi-tui";
5
5
  import { Text } from "@oh-my-pi/pi-tui";
6
- import { untilAborted } from "@oh-my-pi/pi-utils";
6
+ import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
7
  import { type Static, Type } from "@sinclair/typebox";
8
- import { renderPromptTemplate } from "../config/prompt-templates";
8
+ import { computeLineHash } from "../edit/line-hash";
9
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
10
  import type { Theme } from "../modes/theme/theme";
11
- import { computeLineHash } from "../patch/hashline";
12
11
  import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
13
12
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
14
13
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
@@ -31,6 +30,7 @@ import {
31
30
  PARSE_ERRORS_LIMIT,
32
31
  PREVIEW_LIMITS,
33
32
  } from "./render-utils";
33
+ import { queueResolveHandler } from "./resolve";
34
34
  import { ToolError } from "./tool-errors";
35
35
  import { toolResult } from "./tool-result";
36
36
 
@@ -71,7 +71,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
71
71
  readonly strict = true;
72
72
  readonly deferrable = true;
73
73
  constructor(private readonly session: ToolSession) {
74
- this.description = renderPromptTemplate(astEditDescription);
74
+ this.description = prompt.render(astEditDescription);
75
75
  }
76
76
 
77
77
  async execute(
@@ -201,7 +201,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
201
201
  filesSearched: result.filesSearched,
202
202
  applied: result.applied,
203
203
  limitReached: result.limitReached,
204
- parseErrors: dedupedParseErrors,
204
+ ...(dedupedParseErrors.length > 0 ? { parseErrors: dedupedParseErrors } : {}),
205
205
  scopePath,
206
206
  files: fileList,
207
207
  fileReplacements: [],
@@ -289,7 +289,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
289
289
  if (!result.applied && result.totalReplacements > 0) {
290
290
  const previewReplacementPlural = result.totalReplacements !== 1 ? "s" : "";
291
291
  const previewFilePlural = result.filesTouched !== 1 ? "s" : "";
292
- this.session.pendingActionStore?.push({
292
+ queueResolveHandler(this.session, {
293
293
  label: `AST Edit: ${result.totalReplacements} replacement${previewReplacementPlural} in ${result.filesTouched} file${previewFilePlural}`,
294
294
  sourceToolName: this.name,
295
295
  apply: async (_reason: string) => {
@@ -311,7 +311,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
311
311
  filesSearched: applyResult.filesSearched,
312
312
  applied: applyResult.applied,
313
313
  limitReached: applyResult.limitReached,
314
- parseErrors: dedupedApplyParseErrors,
314
+ ...(dedupedApplyParseErrors.length > 0 ? { parseErrors: dedupedApplyParseErrors } : {}),
315
315
  scopePath,
316
316
  files: fileList,
317
317
  fileReplacements,
@@ -3,12 +3,11 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
3
3
  import { type AstFindMatch, astGrep } from "@oh-my-pi/pi-natives";
4
4
  import type { Component } from "@oh-my-pi/pi-tui";
5
5
  import { Text } from "@oh-my-pi/pi-tui";
6
- import { untilAborted } from "@oh-my-pi/pi-utils";
6
+ import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
7
  import { type Static, Type } from "@sinclair/typebox";
8
- import { renderPromptTemplate } from "../config/prompt-templates";
8
+ import { computeLineHash } from "../edit/line-hash";
9
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
10
  import type { Theme } from "../modes/theme/theme";
11
- import { computeLineHash } from "../patch/hashline";
12
11
  import astGrepDescription from "../prompts/tools/ast-grep.md" with { type: "text" };
13
12
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
14
13
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
@@ -65,7 +64,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
65
64
  readonly strict = true;
66
65
 
67
66
  constructor(private readonly session: ToolSession) {
68
- this.description = renderPromptTemplate(astGrepDescription);
67
+ this.description = prompt.render(astGrepDescription);
69
68
  }
70
69
 
71
70
  async execute(
@@ -188,7 +187,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
188
187
  fileCount: result.filesWithMatches,
189
188
  filesSearched: result.filesSearched,
190
189
  limitReached: result.limitReached,
191
- parseErrors: dedupedParseErrors,
190
+ ...(dedupedParseErrors.length > 0 ? { parseErrors: dedupedParseErrors } : {}),
192
191
  scopePath,
193
192
  files: fileList,
194
193
  fileMatches: [],
@@ -213,12 +212,13 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
213
212
  const lineNumbers = matchLines.map((_, index) => match.startLine + index);
214
213
  const lineWidth = Math.max(...lineNumbers.map(value => value.toString().length));
215
214
  const formatLine = (lineNumber: number, line: string, isMatch: boolean): string => {
215
+ const separator = isMatch ? ":" : "-";
216
216
  if (useHashLines) {
217
217
  const ref = `${lineNumber}#${computeLineHash(lineNumber, line)}`;
218
- return isMatch ? `>>${ref}:${line}` : ` ${ref}:${line}`;
218
+ return `${ref}${separator}${line}`;
219
219
  }
220
220
  const padded = lineNumber.toString().padStart(lineWidth, " ");
221
- return isMatch ? `>>${padded}:${line}` : ` ${padded}:${line}`;
221
+ return `${padded}${separator}${line}`;
222
222
  };
223
223
  for (let index = 0; index < matchLines.length; index++) {
224
224
  outputLines.push(formatLine(match.startLine + index, matchLines[index], index === 0));
@@ -4,8 +4,9 @@
4
4
  * Prevents editing of files that appear to be automatically generated
5
5
  * by code generation tools (protoc, sqlc, buf, swagger, etc.).
6
6
  */
7
- import * as fs from "node:fs/promises";
8
7
  import * as path from "node:path";
8
+ import { isEnoent, peekFile } from "@oh-my-pi/pi-utils";
9
+ import { LRUCache } from "lru-cache/raw";
9
10
  import { settings } from "../config/settings";
10
11
  import { ToolError } from "./tool-errors";
11
12
 
@@ -209,40 +210,19 @@ function isAutoGeneratedFileName(filePath: string): boolean {
209
210
 
210
211
  /**
211
212
  * Check if leading header comments contain auto-generated markers.
212
- * Returns the matched marker text if found, null otherwise.
213
+ * Returns the matched marker text if found, undefined otherwise.
213
214
  */
214
- function detectAutoGeneratedMarker(content: string, filePath: string): string | null {
215
+ function detectAutoGeneratedMarker(content: string, filePath: string): string | undefined {
215
216
  const commentStyles = getCommentStylesForPath(filePath);
216
217
  const headerCommentText = extractLeadingHeaderCommentText(content, commentStyles);
217
- if (!headerCommentText) return null;
218
+ if (!headerCommentText) return undefined;
218
219
 
219
220
  for (const markerPattern of AUTO_GENERATED_HEADER_MARKERS) {
220
221
  const match = markerPattern.exec(headerCommentText);
221
222
  if (match?.[0]) return match[0];
222
223
  }
223
224
 
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
- }
225
+ return undefined;
246
226
  }
247
227
 
248
228
  /**
@@ -259,6 +239,33 @@ function buildAutoGeneratedError(displayPath: string, detected: string): ToolErr
259
239
  );
260
240
  }
261
241
 
242
+ const decoder = new TextDecoder("utf-8");
243
+
244
+ const autoGeneratedMap = new LRUCache<string, { marker: string | undefined }>({ max: 10 });
245
+
246
+ async function getAutoGeneratedMarker(filePath: string): Promise<string | undefined> {
247
+ if (isAutoGeneratedFileName(filePath)) {
248
+ return filePath.split("/").pop() ?? "";
249
+ }
250
+
251
+ const cached = autoGeneratedMap.get(filePath);
252
+ if (cached) return cached.marker;
253
+
254
+ let marker: string | undefined;
255
+ try {
256
+ const content = await peekFile(filePath, CHECK_BYTE_COUNT, header => decoder.decode(header));
257
+ marker = detectAutoGeneratedMarker(content, filePath);
258
+ } catch (err) {
259
+ if (isEnoent(err)) {
260
+ return undefined;
261
+ }
262
+ throw err;
263
+ }
264
+
265
+ autoGeneratedMap.set(filePath, { marker });
266
+ return marker;
267
+ }
268
+
262
269
  /**
263
270
  * Check if a file is auto-generated by examining its content.
264
271
  * Throws ToolError if the file appears to be auto-generated.
@@ -266,24 +273,12 @@ function buildAutoGeneratedError(displayPath: string, detected: string): ToolErr
266
273
  * @param absolutePath - Absolute path to the file
267
274
  * @param displayPath - Path to show in error messages (relative or as provided)
268
275
  */
269
- export async function checkAutoGeneratedFile(absolutePath: string, displayPath?: string): Promise<void> {
276
+ export async function assertEditableFile(absolutePath: string, displayPath?: string) {
270
277
  if (!settings.get("edit.blockAutoGenerated")) {
271
278
  return;
272
279
  }
273
-
274
280
  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);
281
+ const marker = await getAutoGeneratedMarker(absolutePath);
287
282
  if (marker) {
288
283
  throw buildAutoGeneratedError(pathForDisplay, marker);
289
284
  }
@@ -297,7 +292,7 @@ export async function checkAutoGeneratedFile(absolutePath: string, displayPath?:
297
292
  * @param content - File content to check (can be full content or prefix)
298
293
  * @param displayPath - Path to show in error messages
299
294
  */
300
- export async function checkAutoGeneratedFileContent(content: string, displayPath: string): Promise<void> {
295
+ export function assertEditableFileContent(content: string, displayPath: string): void {
301
296
  if (!settings.get("edit.blockAutoGenerated")) {
302
297
  return;
303
298
  }
@@ -1,6 +1,6 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
+ import { prompt } from "@oh-my-pi/pi-utils";
2
3
  import { type Static, Type } from "@sinclair/typebox";
3
- import { renderPromptTemplate } from "../config/prompt-templates";
4
4
  import awaitDescription from "../prompts/tools/await.md" with { type: "text" };
5
5
  import type { ToolSession } from "./index";
6
6
 
@@ -36,7 +36,7 @@ export class AwaitTool implements AgentTool<typeof awaitSchema, AwaitToolDetails
36
36
  readonly strict = true;
37
37
 
38
38
  constructor(private readonly session: ToolSession) {
39
- this.description = renderPromptTemplate(awaitDescription);
39
+ this.description = prompt.render(awaitDescription);
40
40
  }
41
41
 
42
42
  static createIf(session: ToolSession): AwaitTool | null {
package/src/tools/bash.ts CHANGED
@@ -1,11 +1,9 @@
1
1
  import * as fs from "node:fs";
2
- import * as path from "node:path";
3
2
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
3
  import type { Component } from "@oh-my-pi/pi-tui";
5
4
  import { ImageProtocol, TERMINAL, Text } from "@oh-my-pi/pi-tui";
6
- import { $env, getProjectDir, isEnoent } from "@oh-my-pi/pi-utils";
5
+ import { $env, getProjectDir, isEnoent, prompt } from "@oh-my-pi/pi-utils";
7
6
  import { Type } from "@sinclair/typebox";
8
- import { renderPromptTemplate } from "../config/prompt-templates";
9
7
  import { type BashResult, executeBash } from "../exec/bash-executor";
10
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
9
  import { truncateToVisualLines } from "../modes/components/visual-truncate";
@@ -22,7 +20,7 @@ import { applyHeadTail } from "./bash-normalize";
22
20
  import { expandInternalUrls, type InternalUrlExpansionOptions } from "./bash-skill-urls";
23
21
  import { formatStyledTruncationWarning, type OutputMeta } from "./output-meta";
24
22
  import { resolveToCwd } from "./path-utils";
25
- import { replaceTabs } from "./render-utils";
23
+ import { formatToolWorkingDirectory, replaceTabs } from "./render-utils";
26
24
  import { ToolAbortError, ToolError } from "./tool-errors";
27
25
  import { toolResult } from "./tool-result";
28
26
  import { clampTimeout } from "./tool-timeouts";
@@ -218,7 +216,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
218
216
  constructor(private readonly session: ToolSession) {
219
217
  this.#asyncEnabled = this.session.settings.get("async.enabled");
220
218
  this.parameters = this.#asyncEnabled ? bashSchemaWithAsync : bashSchemaBase;
221
- this.description = renderPromptTemplate(bashDescription, {
219
+ this.description = prompt.render(bashDescription, {
222
220
  asyncEnabled: this.#asyncEnabled,
223
221
  hasAstGrep: this.session.settings.get("astGrep.enabled"),
224
222
  hasAstEdit: this.session.settings.get("astEdit.enabled"),
@@ -481,26 +479,10 @@ interface BashRenderContext {
481
479
  }
482
480
 
483
481
  function formatBashCommand(args: BashRenderArgs): string {
484
- const command = args.command || "…";
482
+ const command = replaceTabs(args.command || "…");
485
483
  const prompt = "$";
486
484
  const cwd = getProjectDir();
487
- let displayWorkdir = args.cwd;
488
-
489
- if (displayWorkdir) {
490
- const resolvedCwd = path.resolve(cwd);
491
- const resolvedWorkdir = path.resolve(displayWorkdir);
492
- if (resolvedWorkdir === resolvedCwd) {
493
- displayWorkdir = undefined;
494
- } else {
495
- const relativePath = path.relative(resolvedCwd, resolvedWorkdir);
496
- const isWithinCwd =
497
- relativePath && !relativePath.startsWith("..") && !relativePath.startsWith(`..${path.sep}`);
498
- if (isWithinCwd) {
499
- displayWorkdir = relativePath;
500
- }
501
- }
502
- }
503
-
485
+ const displayWorkdir = formatToolWorkingDirectory(args.cwd, cwd);
504
486
  const renderedCommand = [formatBashEnvAssignments(getBashEnvForDisplay(args)), command].filter(Boolean).join(" ");
505
487
  return displayWorkdir ? `${prompt} cd ${displayWorkdir} && ${renderedCommand}` : `${prompt} ${renderedCommand}`;
506
488
  }
@@ -4,7 +4,7 @@ import * as path from "node:path";
4
4
  import { Readability } from "@mozilla/readability";
5
5
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
6
6
  import { StringEnum } from "@oh-my-pi/pi-ai";
7
- import { getPuppeteerDir, logger, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
7
+ import { $which, getPuppeteerDir, logger, prompt, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
8
8
  import { type Static, Type } from "@sinclair/typebox";
9
9
  import { type HTMLElement, parseHTML } from "linkedom";
10
10
  import type {
@@ -16,7 +16,6 @@ import type {
16
16
  default as Puppeteer,
17
17
  SerializedAXNode,
18
18
  } from "puppeteer";
19
- import { renderPromptTemplate } from "../config/prompt-templates";
20
19
  import browserDescription from "../prompts/tools/browser.md" with { type: "text" };
21
20
  import type { ToolSession } from "../sdk";
22
21
  import { resizeImage } from "../utils/image-resize";
@@ -88,8 +87,8 @@ function resolveSystemChromium(): string | undefined {
88
87
  return undefined;
89
88
  }
90
89
  const candidates = [
91
- Bun.which("chromium"),
92
- Bun.which("chromium-browser"),
90
+ $which("chromium"),
91
+ $which("chromium-browser"),
93
92
  path.join(os.homedir(), ".nix-profile/bin/chromium"),
94
93
  "/run/current-system/sw/bin/chromium",
95
94
  ];
@@ -539,7 +538,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
539
538
  readonly #patchedClients = new WeakSet<object>();
540
539
 
541
540
  constructor(private readonly session: ToolSession) {
542
- this.description = renderPromptTemplate(browserDescription, {});
541
+ this.description = prompt.render(browserDescription, {});
543
542
  }
544
543
 
545
544
  async #closeBrowser(): Promise<void> {
@@ -1,9 +1,8 @@
1
1
  import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { Text } from "@oh-my-pi/pi-tui";
4
- import { untilAborted } from "@oh-my-pi/pi-utils";
4
+ import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
5
5
  import { type Static, Type } from "@sinclair/typebox";
6
- import { renderPromptTemplate } from "../config/prompt-templates";
7
6
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
7
  import type { Theme } from "../modes/theme/theme";
9
8
  import calculatorDescription from "../prompts/tools/calculator.md" with { type: "text" };
@@ -402,7 +401,7 @@ export class CalculatorTool implements AgentTool<typeof calculatorSchema, Calcul
402
401
  readonly strict = true;
403
402
 
404
403
  constructor(_session: ToolSession) {
405
- this.description = renderPromptTemplate(calculatorDescription);
404
+ this.description = prompt.render(calculatorDescription);
406
405
  }
407
406
 
408
407
  async execute(
@@ -1,6 +1,6 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
+ import { prompt } from "@oh-my-pi/pi-utils";
2
3
  import { type Static, Type } from "@sinclair/typebox";
3
- import { renderPromptTemplate } from "../config/prompt-templates";
4
4
  import cancelJobDescription from "../prompts/tools/cancel-job.md" with { type: "text" };
5
5
  import type { ToolSession } from "./index";
6
6
 
@@ -23,7 +23,7 @@ export class CancelJobTool implements AgentTool<typeof cancelJobSchema, CancelJo
23
23
  readonly strict = true;
24
24
 
25
25
  constructor(private readonly session: ToolSession) {
26
- this.description = renderPromptTemplate(cancelJobDescription);
26
+ this.description = prompt.render(cancelJobDescription);
27
27
  }
28
28
 
29
29
  static createIf(session: ToolSession): CancelJobTool | null {
@@ -1,6 +1,6 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
+ import { prompt } from "@oh-my-pi/pi-utils";
2
3
  import { type Static, Type } from "@sinclair/typebox";
3
- import { renderPromptTemplate } from "../config/prompt-templates";
4
4
  import checkpointDescription from "../prompts/tools/checkpoint.md" with { type: "text" };
5
5
  import rewindDescription from "../prompts/tools/rewind.md" with { type: "text" };
6
6
  import type { ToolSession } from ".";
@@ -54,7 +54,7 @@ export class CheckpointTool implements AgentTool<typeof checkpointSchema, Checkp
54
54
  readonly strict = true;
55
55
 
56
56
  constructor(private readonly session: ToolSession) {
57
- this.description = renderPromptTemplate(checkpointDescription);
57
+ this.description = prompt.render(checkpointDescription);
58
58
  }
59
59
 
60
60
  static createIf(session: ToolSession): CheckpointTool | null {
@@ -96,7 +96,7 @@ export class RewindTool implements AgentTool<typeof rewindSchema, RewindToolDeta
96
96
  readonly strict = true;
97
97
 
98
98
  constructor(private readonly session: ToolSession) {
99
- this.description = renderPromptTemplate(rewindDescription);
99
+ this.description = prompt.render(rewindDescription);
100
100
  }
101
101
 
102
102
  static createIf(session: ToolSession): RewindTool | null {