@oh-my-pi/pi-coding-agent 16.2.12 → 16.3.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 (171) hide show
  1. package/CHANGELOG.md +115 -7
  2. package/dist/cli.js +5996 -5945
  3. package/dist/types/advisor/config.d.ts +4 -2
  4. package/dist/types/collab/host.d.ts +16 -0
  5. package/dist/types/collab/protocol.d.ts +9 -3
  6. package/dist/types/commit/model-selection.d.ts +5 -0
  7. package/dist/types/config/model-resolver.d.ts +12 -10
  8. package/dist/types/config/models-config-schema.d.ts +12 -0
  9. package/dist/types/config/models-config.d.ts +9 -0
  10. package/dist/types/config/settings-schema.d.ts +28 -5
  11. package/dist/types/edit/modes/patch.d.ts +11 -0
  12. package/dist/types/eval/agent-bridge.d.ts +5 -1
  13. package/dist/types/exec/bash-executor.d.ts +1 -0
  14. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +86 -2
  15. package/dist/types/extensibility/skills.d.ts +2 -1
  16. package/dist/types/mnemopi/state.d.ts +12 -0
  17. package/dist/types/modes/components/agent-hub.d.ts +9 -5
  18. package/dist/types/modes/components/status-line/component.test.d.ts +1 -0
  19. package/dist/types/modes/components/usage-row.d.ts +1 -1
  20. package/dist/types/modes/controllers/command-controller.d.ts +0 -1
  21. package/dist/types/modes/controllers/extension-ui-controller.d.ts +6 -0
  22. package/dist/types/modes/controllers/streaming-reveal.d.ts +17 -1
  23. package/dist/types/modes/controllers/tool-args-reveal.d.ts +30 -3
  24. package/dist/types/modes/interactive-mode.d.ts +12 -7
  25. package/dist/types/modes/rpc/rpc-client.d.ts +5 -0
  26. package/dist/types/modes/rpc/rpc-mode.d.ts +64 -1
  27. package/dist/types/modes/session-teardown.d.ts +63 -0
  28. package/dist/types/modes/session-teardown.test.d.ts +1 -0
  29. package/dist/types/modes/theme/theme.d.ts +9 -4
  30. package/dist/types/modes/types.d.ts +2 -3
  31. package/dist/types/modes/utils/copy-targets.d.ts +2 -0
  32. package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +1 -0
  33. package/dist/types/sdk.d.ts +9 -0
  34. package/dist/types/session/agent-session.d.ts +46 -13
  35. package/dist/types/session/exit-diagnostics.d.ts +48 -0
  36. package/dist/types/session/session-loader.d.ts +5 -0
  37. package/dist/types/task/executor.d.ts +12 -5
  38. package/dist/types/task/parallel.d.ts +1 -0
  39. package/dist/types/task/render.test.d.ts +1 -0
  40. package/dist/types/thinking.d.ts +2 -0
  41. package/dist/types/tools/checkpoint.d.ts +8 -0
  42. package/dist/types/tools/eval-render.d.ts +1 -0
  43. package/dist/types/tools/fetch.d.ts +11 -1
  44. package/dist/types/tools/index.d.ts +3 -1
  45. package/dist/types/tools/irc.d.ts +1 -0
  46. package/dist/types/tools/output-meta.d.ts +7 -0
  47. package/dist/types/tools/output-schema-validator.d.ts +7 -4
  48. package/dist/types/tools/path-utils.d.ts +16 -0
  49. package/dist/types/tools/render-utils.d.ts +4 -1
  50. package/dist/types/utils/git.d.ts +47 -2
  51. package/dist/types/web/search/provider.d.ts +7 -0
  52. package/dist/types/web/search/types.d.ts +1 -1
  53. package/package.json +12 -12
  54. package/src/advisor/config.ts +4 -2
  55. package/src/cli/bench-cli.ts +7 -2
  56. package/src/collab/guest.ts +94 -5
  57. package/src/collab/host.ts +80 -3
  58. package/src/collab/protocol.ts +9 -2
  59. package/src/commit/model-selection.ts +8 -2
  60. package/src/config/keybindings.ts +3 -1
  61. package/src/config/model-discovery.ts +66 -2
  62. package/src/config/model-registry.ts +7 -3
  63. package/src/config/model-resolver.ts +51 -23
  64. package/src/config/models-config-schema.ts +13 -0
  65. package/src/config/settings-schema.ts +44 -14
  66. package/src/edit/hashline/diff.ts +13 -2
  67. package/src/edit/index.ts +58 -8
  68. package/src/edit/modes/patch.ts +53 -18
  69. package/src/edit/streaming.ts +7 -6
  70. package/src/eval/__tests__/agent-bridge.test.ts +57 -1
  71. package/src/eval/agent-bridge.ts +15 -5
  72. package/src/exec/bash-executor.ts +7 -12
  73. package/src/extensibility/legacy-pi-coding-agent-shim.ts +534 -16
  74. package/src/extensibility/skills.ts +29 -6
  75. package/src/goals/guided-setup.ts +4 -3
  76. package/src/internal-urls/docs-index.generated.txt +1 -1
  77. package/src/lsp/client.ts +11 -14
  78. package/src/main.ts +38 -10
  79. package/src/mnemopi/state.ts +43 -7
  80. package/src/modes/acp/acp-agent.ts +1 -1
  81. package/src/modes/components/agent-hub.ts +10 -2
  82. package/src/modes/components/agent-transcript-viewer.ts +39 -7
  83. package/src/modes/components/assistant-message.ts +16 -10
  84. package/src/modes/components/chat-transcript-builder.ts +11 -1
  85. package/src/modes/components/custom-editor.test.ts +20 -0
  86. package/src/modes/components/hook-selector.ts +44 -25
  87. package/src/modes/components/model-selector.ts +1 -1
  88. package/src/modes/components/status-line/component.test.ts +44 -0
  89. package/src/modes/components/status-line/component.ts +9 -1
  90. package/src/modes/components/status-line/segments.ts +3 -3
  91. package/src/modes/components/usage-row.ts +16 -2
  92. package/src/modes/controllers/command-controller.ts +6 -21
  93. package/src/modes/controllers/event-controller.ts +11 -9
  94. package/src/modes/controllers/extension-ui-controller.ts +84 -3
  95. package/src/modes/controllers/input-controller.ts +16 -13
  96. package/src/modes/controllers/selector-controller.ts +3 -8
  97. package/src/modes/controllers/streaming-reveal.ts +75 -53
  98. package/src/modes/controllers/tool-args-reveal.ts +340 -10
  99. package/src/modes/interactive-mode.ts +122 -79
  100. package/src/modes/rpc/rpc-client.ts +21 -0
  101. package/src/modes/rpc/rpc-mode.ts +197 -46
  102. package/src/modes/session-teardown.test.ts +219 -0
  103. package/src/modes/session-teardown.ts +82 -0
  104. package/src/modes/setup-wizard/scenes/theme.ts +2 -2
  105. package/src/modes/skill-command.ts +7 -20
  106. package/src/modes/theme/theme.ts +29 -15
  107. package/src/modes/types.ts +2 -3
  108. package/src/modes/utils/copy-targets.ts +12 -0
  109. package/src/modes/utils/ui-helpers.ts +19 -2
  110. package/src/plan-mode/approved-plan-prompt.test.ts +36 -0
  111. package/src/prompts/advisor/system.md +1 -1
  112. package/src/prompts/agents/tester.md +6 -2
  113. package/src/prompts/skills/autoload.md +8 -0
  114. package/src/prompts/skills/user-invocation.md +11 -0
  115. package/src/prompts/steering/parent-irc.md +5 -0
  116. package/src/prompts/system/irc-incoming.md +2 -0
  117. package/src/prompts/system/mid-run-todo-nudge.md +3 -0
  118. package/src/prompts/system/plan-mode-approved.md +7 -10
  119. package/src/prompts/system/plan-mode-compact-instructions.md +2 -1
  120. package/src/prompts/system/plan-mode-reference.md +3 -4
  121. package/src/prompts/system/rewind-report.md +6 -0
  122. package/src/prompts/system/subagent-system-prompt.md +3 -0
  123. package/src/prompts/tools/irc.md +2 -1
  124. package/src/prompts/tools/job.md +2 -2
  125. package/src/prompts/tools/rewind.md +3 -2
  126. package/src/prompts/tools/ssh.md +1 -0
  127. package/src/prompts/tools/task.md +4 -0
  128. package/src/sdk.ts +18 -4
  129. package/src/session/agent-session.ts +660 -114
  130. package/src/session/exit-diagnostics.ts +202 -0
  131. package/src/session/session-context.ts +25 -13
  132. package/src/session/session-loader.ts +58 -25
  133. package/src/session/session-manager.ts +0 -1
  134. package/src/session/session-persistence.ts +30 -4
  135. package/src/session/settings-stream-fn.ts +14 -0
  136. package/src/slash-commands/builtin-registry.ts +35 -3
  137. package/src/system-prompt.ts +15 -1
  138. package/src/task/executor.ts +31 -8
  139. package/src/task/index.ts +31 -9
  140. package/src/task/isolation-runner.ts +18 -2
  141. package/src/task/parallel.ts +7 -2
  142. package/src/task/render.test.ts +121 -0
  143. package/src/task/render.ts +48 -2
  144. package/src/task/worktree.ts +12 -13
  145. package/src/task/yield-assembly.ts +8 -5
  146. package/src/thinking.ts +5 -0
  147. package/src/tools/ask.ts +188 -9
  148. package/src/tools/ast-edit.ts +7 -0
  149. package/src/tools/ast-grep.ts +11 -0
  150. package/src/tools/bash.ts +6 -30
  151. package/src/tools/checkpoint.ts +15 -1
  152. package/src/tools/eval-render.ts +15 -0
  153. package/src/tools/fetch.ts +82 -18
  154. package/src/tools/gh.ts +1 -1
  155. package/src/tools/grep.ts +45 -27
  156. package/src/tools/index.ts +3 -1
  157. package/src/tools/irc.ts +24 -9
  158. package/src/tools/output-meta.ts +50 -0
  159. package/src/tools/output-schema-validator.ts +152 -15
  160. package/src/tools/path-utils.ts +55 -10
  161. package/src/tools/read.ts +1 -1
  162. package/src/tools/render-utils.ts +5 -3
  163. package/src/tools/ssh.ts +8 -1
  164. package/src/tools/yield.ts +41 -1
  165. package/src/utils/commit-message-generator.ts +2 -2
  166. package/src/utils/git.ts +271 -29
  167. package/src/utils/thinking-display.ts +13 -0
  168. package/src/web/search/index.ts +9 -28
  169. package/src/web/search/provider.ts +26 -1
  170. package/src/web/search/providers/duckduckgo.ts +1 -1
  171. package/src/web/search/types.ts +5 -1
@@ -3,7 +3,6 @@
3
3
  *
4
4
  * Uses brush-core via native bindings for shell execution.
5
5
  */
6
- import * as fs from "node:fs/promises";
7
6
  import { ExponentialYield } from "@oh-my-pi/pi-agent-core/utils/yield";
8
7
  import { executeShell, type MinimizerOptions, Shell, type ShellRunResult } from "@oh-my-pi/pi-natives";
9
8
  import { isExecutable, type ShellConfig } from "@oh-my-pi/pi-utils/procmgr";
@@ -51,6 +50,7 @@ export interface BashResult {
51
50
  outputLines: number;
52
51
  outputBytes: number;
53
52
  artifactId?: string;
53
+ workingDir?: string;
54
54
  }
55
55
 
56
56
  const shellSessions = new Map<string, Shell>();
@@ -115,16 +115,10 @@ function quarantineShellSession(
115
115
  .catch(() => undefined);
116
116
  }
117
117
 
118
- async function resolveShellCwd(cwd: string | undefined): Promise<string | undefined> {
119
- if (!cwd) return undefined;
120
-
121
- try {
122
- // Brush preserves the working directory string verbatim, so resolve symlinks
123
- // up front to keep `pwd` aligned with tools like `git worktree list`.
124
- return await fs.realpath(cwd);
125
- } catch {
126
- return cwd;
127
- }
118
+ function resolveShellCwd(cwd: string | undefined): string | undefined {
119
+ // Preserve the caller's logical cwd string. Brush uses this value to update `PWD` and its
120
+ // internal working directory, so realpathing here collapses symlinks before the shell sees them.
121
+ return cwd;
128
122
  }
129
123
 
130
124
  /** Translate `ShellMinimizerSettings` into native `MinimizerOptions`, or `undefined` when disabled. */
@@ -219,7 +213,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
219
213
 
220
214
  const minimizer = buildMinimizerOptions(settings.getGroup("shellMinimizer"));
221
215
 
222
- const commandCwd = await resolveShellCwd(options?.cwd);
216
+ const commandCwd = resolveShellCwd(options?.cwd);
223
217
  const commandEnv = buildNonInteractiveEnv(options?.env);
224
218
 
225
219
  // Apply command prefix if configured
@@ -429,6 +423,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
429
423
  return {
430
424
  exitCode: winner.result.exitCode,
431
425
  cancelled: false,
426
+ workingDir: winner.result.workingDir,
432
427
  ...(await sink.dump()),
433
428
  };
434
429
  } catch (err) {
@@ -12,21 +12,140 @@
12
12
  * the same module identity as a direct `@oh-my-pi/pi-coding-agent` import.
13
13
  */
14
14
 
15
- import type { AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
15
+ import * as fs from "node:fs/promises";
16
+ import * as path from "node:path";
17
+ import type { AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
16
18
  import type { TSchema } from "@oh-my-pi/pi-ai";
19
+ import { Text } from "@oh-my-pi/pi-tui";
17
20
  import { parseFrontmatter as parseOmpFrontmatter } from "@oh-my-pi/pi-utils";
18
- import { Settings } from "../config/settings";
19
- import { BUILTIN_TOOLS, type Tool, type ToolSession } from "../tools";
21
+ import { type SettingPath, Settings } from "../config/settings";
22
+ import { EditTool } from "../edit";
23
+ import {
24
+ DEFAULT_MAX_BYTES,
25
+ DEFAULT_MAX_LINES,
26
+ type TruncationResult,
27
+ truncateHead,
28
+ truncateTail,
29
+ } from "../session/streaming-output";
30
+ import type { Tool, ToolSession } from "../tools";
31
+ import { BashTool } from "../tools/bash";
32
+ import { GlobTool } from "../tools/glob";
33
+ import { GrepTool } from "../tools/grep";
34
+ import { ReadTool } from "../tools/read";
35
+ import { formatBytes } from "../tools/render-utils";
36
+ import { WriteTool } from "../tools/write";
20
37
  import type { ToolDefinition } from "./extensions/types";
21
38
  import { Type } from "./typebox";
22
39
 
23
40
  const TOOL_DEFINITION_MARKER = "__isToolDefinition";
24
41
  const LEGACY_BUILTIN_TOOL_MARKER = "__ompLegacyBuiltinTool";
25
42
  const LEGACY_CODING_TOOL_NAMES = ["read", "bash", "edit", "write"] as const;
43
+ const LEGACY_READ_ONLY_TOOL_NAMES = ["read", "grep", "find", "ls"] as const;
26
44
 
27
45
  type LegacyCodingToolName = (typeof LEGACY_CODING_TOOL_NAMES)[number];
46
+ type LegacyRegistryToolName = LegacyCodingToolName | "grep" | "glob";
28
47
  type LegacyBuiltinToolDefinition = ToolDefinition & { [LEGACY_BUILTIN_TOOL_MARKER]: true };
29
48
 
49
+ type LegacySettingOverrides = Partial<Record<SettingPath, unknown>>;
50
+
51
+ interface LegacyThemeLike {
52
+ fg(color: string, text: string): string;
53
+ bold(text: string): string;
54
+ }
55
+
56
+ export interface BashSpawnContext {
57
+ command: string;
58
+ cwd: string;
59
+ env: NodeJS.ProcessEnv;
60
+ }
61
+
62
+ export type BashSpawnHook = (context: BashSpawnContext) => BashSpawnContext;
63
+
64
+ export interface BashOperations {
65
+ exec: (
66
+ command: string,
67
+ cwd: string,
68
+ options: {
69
+ onData: (data: Buffer) => void;
70
+ signal?: AbortSignal;
71
+ timeout?: number;
72
+ env?: NodeJS.ProcessEnv;
73
+ },
74
+ ) => Promise<{ exitCode: number | null }>;
75
+ }
76
+
77
+ export interface BashToolOptions {
78
+ operations?: BashOperations;
79
+ commandPrefix?: string;
80
+ spawnHook?: BashSpawnHook;
81
+ }
82
+
83
+ export interface ReadToolOptions {
84
+ /** Auto-resize large images; maps onto the `images.autoResize` setting. Default: true. */
85
+ autoResizeImages?: boolean;
86
+ }
87
+
88
+ export interface GrepToolOptions {
89
+ /**
90
+ * Unsupported. The historical grep operations seam (isDirectory/readFile for
91
+ * context lines) never delegated the search itself — ripgrep always ran
92
+ * locally — and the built-in native grep tool exposes no filesystem seam at
93
+ * all. Supplying operations throws at tool creation instead of silently
94
+ * searching the local filesystem.
95
+ */
96
+ operations?: unknown;
97
+ }
98
+
99
+ export interface FindOperations {
100
+ exists: (absolutePath: string) => Promise<boolean> | boolean;
101
+ glob: (pattern: string, cwd: string, options: { ignore: string[]; limit: number }) => Promise<string[]> | string[];
102
+ }
103
+
104
+ export interface FindToolOptions {
105
+ operations?: FindOperations;
106
+ }
107
+
108
+ export interface LsOperations {
109
+ exists: (absolutePath: string) => Promise<boolean> | boolean;
110
+ stat: (absolutePath: string) => Promise<{ isDirectory(): boolean }> | { isDirectory(): boolean };
111
+ readdir: (absolutePath: string) => Promise<string[]> | string[];
112
+ }
113
+
114
+ export interface LsToolOptions {
115
+ operations?: LsOperations;
116
+ }
117
+
118
+ const legacyBashSchema = Type.Object({
119
+ command: Type.String({ description: "Bash command to execute" }),
120
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds" })),
121
+ });
122
+
123
+ const legacyReadSchema = Type.Object({
124
+ path: Type.String({ description: "Path to read" }),
125
+ offset: Type.Optional(Type.Number({ description: "1-based line offset" })),
126
+ limit: Type.Optional(Type.Number({ description: "Maximum lines to read" })),
127
+ });
128
+
129
+ const legacyGrepSchema = Type.Object({
130
+ pattern: Type.String({ description: "Search pattern" }),
131
+ path: Type.Optional(Type.String({ description: "Directory or file to search" })),
132
+ glob: Type.Optional(Type.String({ description: "Glob filter" })),
133
+ ignoreCase: Type.Optional(Type.Boolean({ description: "Case-insensitive search" })),
134
+ literal: Type.Optional(Type.Boolean({ description: "Treat pattern as a literal string" })),
135
+ context: Type.Optional(Type.Number({ description: "Context lines" })),
136
+ });
137
+
138
+ const legacyFindSchema = Type.Object({
139
+ pattern: Type.String({ description: "Glob pattern to match files" }),
140
+ path: Type.Optional(Type.String({ description: "Directory to search" })),
141
+ limit: Type.Optional(Type.Number({ description: "Maximum results" })),
142
+ });
143
+
144
+ const legacyLsSchema = Type.Object({
145
+ path: Type.Optional(Type.String({ description: "Directory to list" })),
146
+ limit: Type.Optional(Type.Number({ description: "Maximum entries" })),
147
+ });
148
+
30
149
  function markToolDefinition<TParams extends TSchema, TDetails>(
31
150
  tool: ToolDefinition<TParams, TDetails>,
32
151
  ): ToolDefinition<TParams, TDetails> {
@@ -38,25 +157,37 @@ function markToolDefinition<TParams extends TSchema, TDetails>(
38
157
  });
39
158
  return tool;
40
159
  }
41
- function legacyToolSession(cwd: string): ToolSession {
160
+
161
+ function legacyToolSession(cwd: string, settingOverrides?: LegacySettingOverrides): ToolSession {
42
162
  return {
43
163
  cwd,
44
164
  hasUI: false,
45
165
  getSessionFile: () => null,
46
166
  getSessionSpawns: () => null,
47
- settings: Settings.isolated(),
167
+ settings: Settings.isolated(settingOverrides),
48
168
  };
49
169
  }
50
170
 
51
- function createBuiltinTool(cwd: string, name: LegacyCodingToolName): Tool {
52
- const tool = BUILTIN_TOOLS[name](legacyToolSession(cwd));
53
- if (tool instanceof Promise) {
54
- throw new Error(`Built-in ${name} tool factory unexpectedly returned a promise.`);
55
- }
56
- if (!tool) {
57
- throw new Error(`Built-in ${name} tool is unavailable.`);
171
+ function createRegistryTool(
172
+ cwd: string,
173
+ name: LegacyRegistryToolName,
174
+ settingOverrides?: LegacySettingOverrides,
175
+ ): Tool {
176
+ const session = legacyToolSession(cwd, settingOverrides);
177
+ switch (name) {
178
+ case "bash":
179
+ return new BashTool(session);
180
+ case "edit":
181
+ return new EditTool(session);
182
+ case "glob":
183
+ return new GlobTool(session);
184
+ case "grep":
185
+ return new GrepTool(session);
186
+ case "read":
187
+ return new ReadTool(session);
188
+ case "write":
189
+ return new WriteTool(session);
58
190
  }
59
- return tool;
60
191
  }
61
192
 
62
193
  async function executeBuiltinTool(
@@ -67,12 +198,12 @@ async function executeBuiltinTool(
67
198
  signal: AbortSignal | undefined,
68
199
  onUpdate: AgentToolUpdateCallback | undefined,
69
200
  ) {
70
- const tool = createBuiltinTool(cwd, name);
201
+ const tool = createRegistryTool(cwd, name);
71
202
  return tool.execute(toolCallId, params, signal, onUpdate);
72
203
  }
73
204
 
74
205
  function legacyBuiltinTool(cwd: string, name: LegacyCodingToolName): ToolDefinition {
75
- const tool = createBuiltinTool(cwd, name);
206
+ const tool = createRegistryTool(cwd, name);
76
207
  const definition: LegacyBuiltinToolDefinition = {
77
208
  name: tool.name,
78
209
  label: tool.label,
@@ -88,11 +219,144 @@ function legacyBuiltinTool(cwd: string, name: LegacyCodingToolName): ToolDefinit
88
219
  return markToolDefinition(definition);
89
220
  }
90
221
 
222
+ function stringField(value: unknown, key: string): string | undefined {
223
+ if (value === null || typeof value !== "object") return undefined;
224
+ const field = Reflect.get(value, key);
225
+ return typeof field === "string" ? field : undefined;
226
+ }
227
+
228
+ function numberField(value: unknown, key: string): number | undefined {
229
+ if (value === null || typeof value !== "object") return undefined;
230
+ const field = Reflect.get(value, key);
231
+ return typeof field === "number" ? field : undefined;
232
+ }
233
+
234
+ function booleanField(value: unknown, key: string): boolean | undefined {
235
+ if (value === null || typeof value !== "object") return undefined;
236
+ const field = Reflect.get(value, key);
237
+ return typeof field === "boolean" ? field : undefined;
238
+ }
239
+
240
+ function isLegacyThemeLike(value: unknown): value is LegacyThemeLike {
241
+ if (value === null || typeof value !== "object") return false;
242
+ return typeof Reflect.get(value, "fg") === "function" && typeof Reflect.get(value, "bold") === "function";
243
+ }
244
+
245
+ function renderTheme(second: unknown, third: unknown): LegacyThemeLike | undefined {
246
+ if (isLegacyThemeLike(second)) return second;
247
+ if (isLegacyThemeLike(third)) return third;
248
+ return undefined;
249
+ }
250
+
251
+ function themedTitle(theme: LegacyThemeLike | undefined, title: string): string {
252
+ return theme ? theme.fg("toolTitle", theme.bold(title)) : title;
253
+ }
254
+
255
+ function themedMuted(theme: LegacyThemeLike | undefined, text: string): string {
256
+ return theme ? theme.fg("toolOutput", text) : text;
257
+ }
258
+
259
+ function textResult(result: AgentToolResult<unknown> | undefined): string {
260
+ return result?.content.find(block => block.type === "text")?.text ?? "";
261
+ }
262
+
263
+ function legacyRenderResult(result: AgentToolResult<unknown>, _options: unknown, themeArg: unknown): Text {
264
+ const theme = renderTheme(themeArg, undefined);
265
+ const output = textResult(result);
266
+ return new Text(output ? `\n${themedMuted(theme, output)}` : "", 0, 0);
267
+ }
268
+
269
+ function lineRangePath(readPath: string, offset: number | undefined, limit: number | undefined): string {
270
+ if (offset === undefined && limit === undefined) return readPath;
271
+ const start = Math.max(1, Math.floor(offset ?? 1));
272
+ if (limit === undefined) return `${readPath}:${start}`;
273
+ const end = Math.max(start, start + Math.max(1, Math.floor(limit)) - 1);
274
+ return `${readPath}:${start}-${end}`;
275
+ }
276
+
277
+ function escapeRegexLiteral(value: string): string {
278
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
279
+ }
280
+
281
+ function joinLegacyGlob(searchPath: string, pattern: string): string {
282
+ if (path.isAbsolute(pattern)) return pattern;
283
+ if (!searchPath || searchPath === ".") return pattern;
284
+ return path.join(searchPath, pattern);
285
+ }
286
+
287
+ function normalizeLegacyLimit(limit: number | undefined, fallback: number): number {
288
+ if (limit === undefined || !Number.isFinite(limit)) return fallback;
289
+ return Math.max(1, Math.floor(limit));
290
+ }
291
+
292
+ function appendStatus(text: string, status: string): string {
293
+ return text ? `${text}\n\n${status}` : status;
294
+ }
295
+
296
+ function legacyBashSnapshot(output: string): { text: string; details?: { truncation: TruncationResult } } {
297
+ const truncation = truncateTail(output, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
298
+ if (!truncation.truncated) {
299
+ return { text: truncation.content };
300
+ }
301
+ const startLine = truncation.totalLines - (truncation.outputLines ?? 0) + 1;
302
+ const note =
303
+ truncation.truncatedBy === "lines"
304
+ ? `Showing lines ${startLine}-${truncation.totalLines} of ${truncation.totalLines}`
305
+ : `Showing lines ${startLine}-${truncation.totalLines} of ${truncation.totalLines} (${formatBytes(DEFAULT_MAX_BYTES)} limit)`;
306
+ return {
307
+ text: `${truncation.content}\n\n[${note}]`,
308
+ details: { truncation },
309
+ };
310
+ }
311
+
312
+ async function executeLegacyBashOperations(
313
+ operations: BashOperations,
314
+ spawn: BashSpawnContext,
315
+ timeout: number | undefined,
316
+ signal: AbortSignal | undefined,
317
+ onUpdate: AgentToolUpdateCallback | undefined,
318
+ ): Promise<AgentToolResult> {
319
+ let output = "";
320
+ const onData = (data: Buffer) => {
321
+ output += data.toString("utf8");
322
+ if (onUpdate) {
323
+ const snapshot = legacyBashSnapshot(output);
324
+ onUpdate({ content: [{ type: "text", text: snapshot.text }], details: snapshot.details });
325
+ }
326
+ };
327
+ try {
328
+ const result = await operations.exec(spawn.command, spawn.cwd, {
329
+ onData,
330
+ signal,
331
+ timeout,
332
+ env: spawn.env,
333
+ });
334
+ const snapshot = legacyBashSnapshot(output);
335
+ const text = snapshot.text || "(no output)";
336
+ if (result.exitCode !== 0 && result.exitCode !== null) {
337
+ throw new Error(appendStatus(text, `Command exited with code ${result.exitCode}`));
338
+ }
339
+ return { content: [{ type: "text", text }], details: snapshot.details };
340
+ } catch (err) {
341
+ const snapshot = legacyBashSnapshot(output);
342
+ const text = snapshot.text;
343
+ if (err instanceof Error && err.message === "aborted") {
344
+ throw new Error(appendStatus(text, "Command aborted"));
345
+ }
346
+ if (err instanceof Error && err.message.startsWith("timeout:")) {
347
+ throw new Error(appendStatus(text, `Command timed out after ${err.message.slice("timeout:".length)} seconds`));
348
+ }
349
+ throw err;
350
+ }
351
+ }
352
+
353
+ /** Parse frontmatter using the historical Pi package-root helper. */
91
354
  export interface ParsedFrontmatter<T extends Record<string, unknown> = Record<string, unknown>> {
92
355
  frontmatter: T;
93
356
  body: string;
94
357
  }
95
358
 
359
+ /** Parse YAML frontmatter and throw on invalid metadata. */
96
360
  export function parseFrontmatter<T extends Record<string, unknown> = Record<string, unknown>>(
97
361
  content: string,
98
362
  ): ParsedFrontmatter<T> {
@@ -100,20 +364,273 @@ export function parseFrontmatter<T extends Record<string, unknown> = Record<stri
100
364
  return { frontmatter: frontmatter as T, body };
101
365
  }
102
366
 
367
+ /** Return content without YAML frontmatter. */
103
368
  export function stripFrontmatter(content: string): string {
104
369
  return parseFrontmatter(content).body;
105
370
  }
106
371
 
372
+ /** Mark an extension-authored tool as a Pi-compatible tool definition. */
107
373
  export function defineTool<TParams extends TSchema = TSchema, TDetails = unknown>(
108
374
  tool: ToolDefinition<TParams, TDetails>,
109
375
  ): ToolDefinition<TParams, TDetails> {
110
376
  return markToolDefinition(tool);
111
377
  }
112
378
 
379
+ /** Create the legacy read tool definition. */
380
+ export function createReadToolDefinition(cwd: string, options?: ReadToolOptions): ToolDefinition {
381
+ const tool = createRegistryTool(
382
+ cwd,
383
+ "read",
384
+ options?.autoResizeImages === undefined ? undefined : { "images.autoResize": options.autoResizeImages },
385
+ );
386
+ return markToolDefinition({
387
+ name: "read",
388
+ label: "Read",
389
+ description: tool.description,
390
+ parameters: legacyReadSchema,
391
+ approval: "read",
392
+ renderCall: (params, options, themeArg) => {
393
+ const theme = renderTheme(options, themeArg);
394
+ const readPath = stringField(params, "path") ?? "";
395
+ return new Text(`${themedTitle(theme, "read")} ${themedMuted(theme, readPath)}`, 0, 0);
396
+ },
397
+ renderResult: legacyRenderResult,
398
+ execute: (toolCallId, params, signal, onUpdate) => {
399
+ const readPath = stringField(params, "path") ?? "";
400
+ const pathWithRange = lineRangePath(readPath, numberField(params, "offset"), numberField(params, "limit"));
401
+ return tool.execute(toolCallId, { path: pathWithRange }, signal, onUpdate);
402
+ },
403
+ });
404
+ }
405
+
406
+ /** Create the legacy read tool. */
407
+ export function createReadTool(cwd: string, options?: ReadToolOptions): ToolDefinition {
408
+ return createReadToolDefinition(cwd, options);
409
+ }
410
+
411
+ /** Create the legacy bash tool definition. */
412
+ export function createBashToolDefinition(cwd: string, options?: BashToolOptions): ToolDefinition {
413
+ const tool = createRegistryTool(cwd, "bash");
414
+ return markToolDefinition({
415
+ name: "bash",
416
+ label: "Bash",
417
+ description: tool.description,
418
+ parameters: legacyBashSchema,
419
+ approval: "exec",
420
+ renderCall: (params, optionsArg, themeArg) => {
421
+ const theme = renderTheme(optionsArg, themeArg);
422
+ const command = stringField(params, "command") ?? "";
423
+ return new Text(`${themedTitle(theme, "bash")} ${themedMuted(theme, command)}`, 0, 0);
424
+ },
425
+ renderResult: legacyRenderResult,
426
+ execute: (toolCallId, params, signal, onUpdate) => {
427
+ const rawCommand = stringField(params, "command") ?? "";
428
+ const command = options?.commandPrefix ? `${options.commandPrefix}\n${rawCommand}` : rawCommand;
429
+ const timeout = numberField(params, "timeout");
430
+ const spawn = options?.spawnHook?.({ command, cwd, env: process.env });
431
+ if (options?.operations) {
432
+ return executeLegacyBashOperations(
433
+ options.operations,
434
+ { command: spawn?.command ?? command, cwd: spawn?.cwd ?? cwd, env: spawn?.env ?? process.env },
435
+ timeout,
436
+ signal,
437
+ onUpdate,
438
+ );
439
+ }
440
+ return tool.execute(
441
+ toolCallId,
442
+ {
443
+ command: spawn?.command ?? command,
444
+ cwd: spawn?.cwd ?? cwd,
445
+ env: spawn?.env,
446
+ timeout,
447
+ },
448
+ signal,
449
+ onUpdate,
450
+ );
451
+ },
452
+ });
453
+ }
454
+
455
+ /** Create the legacy bash tool. */
456
+ export function createBashTool(cwd: string, options?: BashToolOptions): ToolDefinition {
457
+ return createBashToolDefinition(cwd, options);
458
+ }
459
+
460
+ /** Create the legacy grep tool definition. */
461
+ export function createGrepToolDefinition(cwd: string, options?: GrepToolOptions): ToolDefinition {
462
+ if (options?.operations) {
463
+ throw new Error(
464
+ "Legacy GrepToolOptions.operations is not supported: the built-in grep tool searches the local " +
465
+ "filesystem natively and exposes no pluggable filesystem seam (the historical seam only customized " +
466
+ "context-line reads; the search itself always ran locally). Register a custom grep tool via " +
467
+ "defineTool() instead of passing operations to createGrepTool()/createGrepToolDefinition().",
468
+ );
469
+ }
470
+ const tool = createRegistryTool(cwd, "grep");
471
+ return markToolDefinition({
472
+ name: "grep",
473
+ label: "grep",
474
+ description: "Search file contents for a pattern.",
475
+ parameters: legacyGrepSchema,
476
+ approval: "read",
477
+ renderCall: (params, optionsArg, themeArg) => {
478
+ const theme = renderTheme(optionsArg, themeArg);
479
+ const pattern = stringField(params, "pattern") ?? "";
480
+ const searchPath = stringField(params, "path") ?? ".";
481
+ return new Text(`${themedTitle(theme, "grep")} ${themedMuted(theme, `/${pattern}/ in ${searchPath}`)}`, 0, 0);
482
+ },
483
+ renderResult: legacyRenderResult,
484
+ execute: (toolCallId, params, signal, onUpdate) => {
485
+ const rawPattern = stringField(params, "pattern") ?? "";
486
+ const pattern = booleanField(params, "literal") ? escapeRegexLiteral(rawPattern) : rawPattern;
487
+ const searchPath = stringField(params, "path") ?? ".";
488
+ const glob = stringField(params, "glob");
489
+ const context = numberField(params, "context");
490
+ // The new grep reads context from settings fixed at construction; build a
491
+ // per-call tool when the model passes an explicit legacy `context`.
492
+ const grepTool =
493
+ context === undefined
494
+ ? tool
495
+ : createRegistryTool(cwd, "grep", {
496
+ "grep.contextBefore": Math.max(0, Math.floor(context)),
497
+ "grep.contextAfter": Math.max(0, Math.floor(context)),
498
+ });
499
+ return grepTool.execute(
500
+ toolCallId,
501
+ {
502
+ pattern,
503
+ paths: glob ? joinLegacyGlob(searchPath, glob) : searchPath,
504
+ case: booleanField(params, "ignoreCase") ? false : undefined,
505
+ },
506
+ signal,
507
+ onUpdate,
508
+ );
509
+ },
510
+ });
511
+ }
512
+
513
+ /** Create the legacy grep tool. */
514
+ export function createGrepTool(cwd: string, options?: GrepToolOptions): ToolDefinition {
515
+ return createGrepToolDefinition(cwd, options);
516
+ }
517
+
518
+ /** Create the legacy find tool definition. */
519
+ export function createFindToolDefinition(cwd: string, options?: FindToolOptions): ToolDefinition {
520
+ const tool = createRegistryTool(cwd, "glob");
521
+ return markToolDefinition({
522
+ name: "find",
523
+ label: "find",
524
+ description: "Find files by glob pattern.",
525
+ parameters: legacyFindSchema,
526
+ approval: "read",
527
+ renderCall: (params, optionsArg, themeArg) => {
528
+ const theme = renderTheme(optionsArg, themeArg);
529
+ const pattern = stringField(params, "pattern") ?? "";
530
+ const searchPath = stringField(params, "path") ?? ".";
531
+ return new Text(`${themedTitle(theme, "find")} ${themedMuted(theme, `${pattern} in ${searchPath}`)}`, 0, 0);
532
+ },
533
+ renderResult: legacyRenderResult,
534
+ execute: async (toolCallId, params, signal, onUpdate) => {
535
+ const pattern = stringField(params, "pattern") ?? "*";
536
+ const searchPath = stringField(params, "path") ?? ".";
537
+ const limit = normalizeLegacyLimit(numberField(params, "limit"), 1000);
538
+ const absolutePath = path.resolve(cwd, searchPath);
539
+ if (options?.operations) {
540
+ if (!(await options.operations.exists(absolutePath))) {
541
+ throw new Error(`Path not found: ${absolutePath}`);
542
+ }
543
+ const matches = await options.operations.glob(pattern, absolutePath, {
544
+ ignore: ["**/node_modules/**", "**/.git/**"],
545
+ limit,
546
+ });
547
+ const output = matches
548
+ .map(match => {
549
+ const rel = path.isAbsolute(match) ? path.relative(absolutePath, match) : match;
550
+ return rel.split(path.sep).join("/");
551
+ })
552
+ .join("\n");
553
+ const truncation = truncateHead(output, { maxLines: Number.MAX_SAFE_INTEGER });
554
+ return {
555
+ content: [{ type: "text", text: truncation.content || "No files found matching pattern" }],
556
+ details: truncation.truncated ? { truncation } : undefined,
557
+ };
558
+ }
559
+ return tool.execute(
560
+ toolCallId,
561
+ { paths: [joinLegacyGlob(searchPath, pattern)], hidden: true, gitignore: true, limit },
562
+ signal,
563
+ onUpdate,
564
+ );
565
+ },
566
+ });
567
+ }
568
+
569
+ /** Create the legacy find tool. */
570
+ export function createFindTool(cwd: string, options?: FindToolOptions): ToolDefinition {
571
+ return createFindToolDefinition(cwd, options);
572
+ }
573
+
574
+ /** Create the legacy ls tool definition. */
575
+ export function createLsToolDefinition(cwd: string, options?: LsToolOptions): ToolDefinition {
576
+ return markToolDefinition({
577
+ name: "ls",
578
+ label: "ls",
579
+ description: "List directory entries.",
580
+ parameters: legacyLsSchema,
581
+ approval: "read",
582
+ renderCall: (params, optionsArg, themeArg) => {
583
+ const theme = renderTheme(optionsArg, themeArg);
584
+ return new Text(`${themedTitle(theme, "ls")} ${themedMuted(theme, stringField(params, "path") ?? ".")}`, 0, 0);
585
+ },
586
+ renderResult: legacyRenderResult,
587
+ execute: async (_toolCallId, params, _signal, _onUpdate) => {
588
+ const rawPath = stringField(params, "path") ?? ".";
589
+ const limit = normalizeLegacyLimit(numberField(params, "limit"), 500);
590
+ const absolutePath = path.resolve(cwd, rawPath);
591
+ const ops = options?.operations;
592
+ const exists = ops
593
+ ? await ops.exists(absolutePath)
594
+ : await fs.stat(absolutePath).then(
595
+ () => true,
596
+ () => false,
597
+ );
598
+ if (!exists) throw new Error(`Path not found: ${absolutePath}`);
599
+ const stat = ops ? await ops.stat(absolutePath) : await fs.stat(absolutePath);
600
+ if (!stat.isDirectory()) {
601
+ return { content: [{ type: "text", text: rawPath }] };
602
+ }
603
+ const entries = ops ? await ops.readdir(absolutePath) : await fs.readdir(absolutePath);
604
+ const sorted = [...entries].sort((a, b) => a.localeCompare(b));
605
+ const limited = sorted.slice(0, limit);
606
+ const output = limited.join("\n");
607
+ const details = sorted.length > limited.length ? { entryLimitReached: limit } : undefined;
608
+ const suffix = details ? `\n\n[${limit} entries limit reached]` : "";
609
+ return { content: [{ type: "text", text: `${output}${suffix}` }], details };
610
+ },
611
+ });
612
+ }
613
+
614
+ /** Create the legacy ls tool. */
615
+ export function createLsTool(cwd: string, options?: LsToolOptions): ToolDefinition {
616
+ return createLsToolDefinition(cwd, options);
617
+ }
618
+
619
+ /** Create legacy read, bash, edit, and write tools. */
113
620
  export function createCodingTools(cwd: string): ToolDefinition[] {
114
621
  return LEGACY_CODING_TOOL_NAMES.map(name => legacyBuiltinTool(cwd, name));
115
622
  }
116
623
 
624
+ /** Create legacy read, grep, find, and ls tools. */
625
+ export function createReadOnlyTools(cwd: string): ToolDefinition[] {
626
+ return LEGACY_READ_ONLY_TOOL_NAMES.map(name => {
627
+ if (name === "read") return createReadTool(cwd);
628
+ if (name === "grep") return createGrepTool(cwd);
629
+ if (name === "find") return createFindTool(cwd);
630
+ return createLsTool(cwd);
631
+ });
632
+ }
633
+
117
634
  export const SettingsManager = {
118
635
  create(cwd: string, agentDir?: string): Promise<Settings> {
119
636
  return Settings.init({ cwd, agentDir });
@@ -125,4 +642,5 @@ export const SettingsManager = {
125
642
  } as const;
126
643
 
127
644
  export * from "../index";
128
- export { Type };
645
+ export { formatBytes as formatSize } from "../tools/render-utils";
646
+ export { Type } from "./typebox";