@oh-my-pi/pi-coding-agent 16.0.4 → 16.0.6

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 (270) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/cli.js +2027 -1396
  3. package/dist/types/advisor/advise-tool.d.ts +31 -19
  4. package/dist/types/autoresearch/tools/init-experiment.d.ts +13 -17
  5. package/dist/types/autoresearch/tools/log-experiment.d.ts +17 -19
  6. package/dist/types/autoresearch/tools/run-experiment.d.ts +3 -4
  7. package/dist/types/autoresearch/tools/update-notes.d.ts +4 -5
  8. package/dist/types/cli/args.d.ts +1 -0
  9. package/dist/types/cli/bench-cli.d.ts +6 -0
  10. package/dist/types/cli/ttsr-cli.d.ts +39 -0
  11. package/dist/types/commands/launch.d.ts +3 -0
  12. package/dist/types/commands/ttsr.d.ts +57 -0
  13. package/dist/types/commit/agentic/tools/analyze-file.d.ts +4 -5
  14. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +4 -5
  15. package/dist/types/commit/agentic/tools/git-hunk.d.ts +5 -6
  16. package/dist/types/commit/agentic/tools/git-overview.d.ts +4 -5
  17. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +23 -24
  18. package/dist/types/commit/agentic/tools/propose-commit.d.ts +11 -32
  19. package/dist/types/commit/agentic/tools/recent-commits.d.ts +3 -4
  20. package/dist/types/commit/agentic/tools/schemas.d.ts +6 -27
  21. package/dist/types/commit/agentic/tools/split-commit.d.ts +28 -49
  22. package/dist/types/commit/changelog/generate.d.ts +12 -13
  23. package/dist/types/commit/shared-llm.d.ts +10 -37
  24. package/dist/types/config/config-file.d.ts +4 -4
  25. package/dist/types/config/keybindings.d.ts +5 -0
  26. package/dist/types/config/models-config-schema.d.ts +625 -990
  27. package/dist/types/config/models-config.d.ts +229 -217
  28. package/dist/types/config/settings-schema.d.ts +144 -25
  29. package/dist/types/edit/hashline/params.d.ts +7 -11
  30. package/dist/types/edit/index.d.ts +2 -1
  31. package/dist/types/edit/modes/apply-patch.d.ts +4 -5
  32. package/dist/types/edit/modes/patch.d.ts +15 -24
  33. package/dist/types/edit/modes/replace.d.ts +16 -17
  34. package/dist/types/eval/js/index.d.ts +1 -0
  35. package/dist/types/extensibility/custom-commands/types.d.ts +6 -3
  36. package/dist/types/extensibility/custom-tools/types.d.ts +8 -5
  37. package/dist/types/extensibility/extensions/runner.d.ts +5 -2
  38. package/dist/types/extensibility/extensions/types.d.ts +14 -10
  39. package/dist/types/extensibility/hooks/types.d.ts +7 -4
  40. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +13 -5
  41. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +17 -0
  42. package/dist/types/extensibility/shared-events.d.ts +22 -1
  43. package/dist/types/extensibility/typebox.d.ts +80 -58
  44. package/dist/types/goals/tools/goal-tool.d.ts +11 -24
  45. package/dist/types/index.d.ts +2 -0
  46. package/dist/types/lsp/index.d.ts +11 -26
  47. package/dist/types/lsp/types.d.ts +12 -28
  48. package/dist/types/main.d.ts +1 -0
  49. package/dist/types/mcp/client.d.ts +8 -0
  50. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  51. package/dist/types/modes/components/custom-editor.d.ts +3 -1
  52. package/dist/types/modes/components/status-line/component.d.ts +1 -1
  53. package/dist/types/modes/components/status-line/context-thresholds.d.ts +0 -1
  54. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  55. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  56. package/dist/types/modes/interactive-mode.d.ts +3 -0
  57. package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
  58. package/dist/types/modes/setup-wizard/index.d.ts +1 -0
  59. package/dist/types/modes/setup-wizard/startup-splash.d.ts +7 -0
  60. package/dist/types/modes/theme/theme.d.ts +1 -1
  61. package/dist/types/modes/types.d.ts +3 -0
  62. package/dist/types/modes/utils/context-usage.d.ts +12 -0
  63. package/dist/types/sdk.d.ts +8 -1
  64. package/dist/types/session/agent-session.d.ts +24 -0
  65. package/dist/types/session/session-persistence.d.ts +4 -0
  66. package/dist/types/startup-splash.d.ts +12 -0
  67. package/dist/types/task/types.d.ts +47 -48
  68. package/dist/types/tools/ask.d.ts +26 -27
  69. package/dist/types/tools/ast-edit.d.ts +17 -17
  70. package/dist/types/tools/ast-grep.d.ts +12 -13
  71. package/dist/types/tools/bash.d.ts +20 -17
  72. package/dist/types/tools/browser.d.ts +46 -71
  73. package/dist/types/tools/checkpoint.d.ts +14 -15
  74. package/dist/types/tools/debug.d.ts +82 -145
  75. package/dist/types/tools/eval.d.ts +30 -40
  76. package/dist/types/tools/find.d.ts +17 -18
  77. package/dist/types/tools/gh.d.ts +49 -78
  78. package/dist/types/tools/image-gen.d.ts +20 -36
  79. package/dist/types/tools/inspect-image.d.ts +10 -11
  80. package/dist/types/tools/irc.d.ts +22 -33
  81. package/dist/types/tools/job.d.ts +11 -12
  82. package/dist/types/tools/learn.d.ts +21 -28
  83. package/dist/types/tools/manage-skill.d.ts +13 -22
  84. package/dist/types/tools/memory-edit.d.ts +15 -24
  85. package/dist/types/tools/memory-recall.d.ts +7 -8
  86. package/dist/types/tools/memory-reflect.d.ts +9 -10
  87. package/dist/types/tools/memory-retain.d.ts +13 -14
  88. package/dist/types/tools/read.d.ts +8 -8
  89. package/dist/types/tools/resolve.d.ts +11 -18
  90. package/dist/types/tools/review.d.ts +9 -15
  91. package/dist/types/tools/search-tool-bm25.d.ts +9 -10
  92. package/dist/types/tools/search.d.ts +16 -17
  93. package/dist/types/tools/ssh.d.ts +14 -15
  94. package/dist/types/tools/todo.d.ts +27 -43
  95. package/dist/types/tools/tts.d.ts +8 -9
  96. package/dist/types/tools/write.d.ts +9 -10
  97. package/dist/types/tui/code-cell.d.ts +2 -0
  98. package/dist/types/tui/index.d.ts +1 -0
  99. package/dist/types/tui/width-aware-text.d.ts +23 -0
  100. package/dist/types/utils/image-vision-fallback.d.ts +28 -0
  101. package/dist/types/utils/markit.d.ts +10 -1
  102. package/dist/types/web/search/index.d.ts +17 -28
  103. package/dist/types/web/search/providers/base.d.ts +1 -0
  104. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  105. package/dist/types/web/search/providers/perplexity.d.ts +0 -2
  106. package/dist/types/web/search/types.d.ts +32 -26
  107. package/package.json +14 -13
  108. package/scripts/omp +1 -1
  109. package/src/advisor/__tests__/advisor.test.ts +103 -1
  110. package/src/advisor/advise-tool.ts +47 -11
  111. package/src/autoresearch/tools/init-experiment.ts +13 -16
  112. package/src/autoresearch/tools/log-experiment.ts +15 -18
  113. package/src/autoresearch/tools/run-experiment.ts +3 -3
  114. package/src/autoresearch/tools/update-notes.ts +4 -4
  115. package/src/cli/args.ts +1 -0
  116. package/src/cli/bench-cli.ts +30 -7
  117. package/src/cli/flag-tables.ts +8 -0
  118. package/src/cli/ttsr-cli.ts +995 -0
  119. package/src/cli-commands.ts +1 -0
  120. package/src/cli.ts +7 -1
  121. package/src/collab/host.ts +2 -2
  122. package/src/commands/launch.ts +3 -0
  123. package/src/commands/ttsr.ts +125 -0
  124. package/src/commit/agentic/tools/analyze-file.ts +4 -4
  125. package/src/commit/agentic/tools/git-file-diff.ts +4 -4
  126. package/src/commit/agentic/tools/git-hunk.ts +7 -5
  127. package/src/commit/agentic/tools/git-overview.ts +4 -4
  128. package/src/commit/agentic/tools/propose-changelog.ts +18 -15
  129. package/src/commit/agentic/tools/propose-commit.ts +6 -6
  130. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  131. package/src/commit/agentic/tools/schemas.ts +8 -20
  132. package/src/commit/agentic/tools/split-commit.ts +19 -23
  133. package/src/commit/analysis/summary.ts +7 -5
  134. package/src/commit/changelog/generate.ts +15 -11
  135. package/src/commit/shared-llm.ts +17 -24
  136. package/src/config/config-file.ts +13 -15
  137. package/src/config/keybindings.ts +6 -0
  138. package/src/config/models-config-schema.ts +206 -179
  139. package/src/config/settings-schema.ts +118 -2
  140. package/src/discovery/builtin-rules/index.ts +2 -0
  141. package/src/discovery/builtin-rules/ts-import-type.md +2 -2
  142. package/src/discovery/builtin-rules/ts-no-any.md +11 -2
  143. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +55 -0
  144. package/src/edit/hashline/params.ts +12 -11
  145. package/src/edit/index.ts +5 -4
  146. package/src/edit/modes/apply-patch.ts +4 -4
  147. package/src/edit/modes/patch.ts +15 -18
  148. package/src/edit/modes/replace.ts +13 -17
  149. package/src/edit/renderer.ts +0 -1
  150. package/src/eval/agent-bridge.ts +11 -13
  151. package/src/eval/completion-bridge.ts +25 -17
  152. package/src/eval/js/context-manager.ts +17 -2
  153. package/src/eval/js/index.ts +1 -1
  154. package/src/eval/py/executor.ts +2 -2
  155. package/src/eval/py/runner.py +44 -0
  156. package/src/extensibility/custom-commands/loader.ts +5 -3
  157. package/src/extensibility/custom-commands/types.ts +6 -3
  158. package/src/extensibility/custom-tools/loader.ts +4 -2
  159. package/src/extensibility/custom-tools/types.ts +8 -5
  160. package/src/extensibility/extensions/loader.ts +4 -2
  161. package/src/extensibility/extensions/runner.ts +20 -2
  162. package/src/extensibility/extensions/types.ts +22 -8
  163. package/src/extensibility/hooks/loader.ts +5 -2
  164. package/src/extensibility/hooks/types.ts +7 -4
  165. package/src/extensibility/legacy-pi-ai-shim.ts +42 -5
  166. package/src/extensibility/legacy-pi-coding-agent-shim.ts +113 -0
  167. package/src/extensibility/plugins/legacy-pi-compat.ts +13 -13
  168. package/src/extensibility/shared-events.ts +24 -0
  169. package/src/extensibility/tool-proxy.ts +4 -1
  170. package/src/extensibility/typebox.ts +778 -251
  171. package/src/goals/guided-setup.ts +12 -3
  172. package/src/goals/tools/goal-tool.ts +6 -6
  173. package/src/index.ts +2 -0
  174. package/src/internal-urls/docs-index.generated.ts +15 -13
  175. package/src/lsp/types.ts +13 -27
  176. package/src/main.ts +29 -21
  177. package/src/mcp/client.ts +38 -13
  178. package/src/mcp/render.ts +102 -89
  179. package/src/modes/components/agent-hub.ts +11 -4
  180. package/src/modes/components/branch-summary-message.ts +1 -0
  181. package/src/modes/components/btw-panel.ts +5 -1
  182. package/src/modes/components/collab-prompt-message.ts +9 -7
  183. package/src/modes/components/compaction-summary-message.ts +1 -0
  184. package/src/modes/components/custom-editor.ts +18 -0
  185. package/src/modes/components/custom-message.ts +1 -0
  186. package/src/modes/components/footer.ts +6 -5
  187. package/src/modes/components/hook-message.ts +1 -0
  188. package/src/modes/components/read-tool-group.ts +9 -3
  189. package/src/modes/components/skill-message.ts +1 -0
  190. package/src/modes/components/status-line/component.ts +139 -15
  191. package/src/modes/components/status-line/context-thresholds.ts +0 -1
  192. package/src/modes/components/todo-reminder.ts +1 -0
  193. package/src/modes/components/tool-execution.ts +17 -10
  194. package/src/modes/components/ttsr-notification.ts +1 -0
  195. package/src/modes/components/user-message.ts +6 -6
  196. package/src/modes/controllers/btw-controller.ts +69 -1
  197. package/src/modes/controllers/event-controller.ts +2 -7
  198. package/src/modes/controllers/input-controller.ts +29 -0
  199. package/src/modes/controllers/selector-controller.ts +10 -3
  200. package/src/modes/interactive-mode.ts +42 -10
  201. package/src/modes/rpc/rpc-types.ts +1 -1
  202. package/src/modes/setup-wizard/index.ts +1 -0
  203. package/src/modes/setup-wizard/scenes/sign-in.ts +77 -5
  204. package/src/modes/setup-wizard/startup-splash.ts +107 -0
  205. package/src/modes/theme/theme.ts +133 -143
  206. package/src/modes/types.ts +3 -0
  207. package/src/modes/utils/context-usage.ts +37 -20
  208. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  209. package/src/prompts/system/system-prompt.md +1 -0
  210. package/src/prompts/tools/image-attachment-describe-system.md +8 -0
  211. package/src/prompts/tools/image-attachment-describe.md +10 -0
  212. package/src/sdk.ts +35 -22
  213. package/src/session/agent-session.ts +715 -255
  214. package/src/session/session-history-format.ts +11 -2
  215. package/src/session/session-loader.ts +19 -32
  216. package/src/session/session-persistence.ts +27 -11
  217. package/src/session/snapcompact-inline.ts +1 -1
  218. package/src/slash-commands/builtin-registry.ts +4 -11
  219. package/src/ssh/connection-manager.ts +3 -2
  220. package/src/startup-splash.ts +19 -0
  221. package/src/task/executor.ts +12 -7
  222. package/src/task/types.ts +44 -41
  223. package/src/tool-discovery/tool-index.ts +17 -4
  224. package/src/tools/ask.ts +14 -14
  225. package/src/tools/ast-edit.ts +17 -14
  226. package/src/tools/ast-grep.ts +10 -9
  227. package/src/tools/bash.ts +15 -10
  228. package/src/tools/browser/launch.ts +13 -0
  229. package/src/tools/browser.ts +26 -32
  230. package/src/tools/checkpoint.ts +7 -7
  231. package/src/tools/debug.ts +72 -69
  232. package/src/tools/eval.ts +18 -19
  233. package/src/tools/find.ts +20 -13
  234. package/src/tools/gh.ts +29 -49
  235. package/src/tools/image-gen.ts +94 -57
  236. package/src/tools/inspect-image.ts +8 -9
  237. package/src/tools/irc.ts +12 -12
  238. package/src/tools/job.ts +6 -6
  239. package/src/tools/learn.ts +11 -14
  240. package/src/tools/manage-skill.ts +19 -23
  241. package/src/tools/memory-edit.ts +8 -8
  242. package/src/tools/memory-recall.ts +4 -4
  243. package/src/tools/memory-reflect.ts +5 -5
  244. package/src/tools/memory-retain.ts +9 -11
  245. package/src/tools/puppeteer/02_stealth_hairline.txt +1 -1
  246. package/src/tools/puppeteer/04_stealth_iframe.txt +4 -4
  247. package/src/tools/puppeteer/05_stealth_webgl.txt +1 -1
  248. package/src/tools/puppeteer/10_stealth_plugins.txt +6 -4
  249. package/src/tools/puppeteer/12_stealth_codecs.txt +2 -2
  250. package/src/tools/puppeteer/13_stealth_worker.txt +1 -1
  251. package/src/tools/read.ts +197 -19
  252. package/src/tools/report-tool-issue.ts +6 -6
  253. package/src/tools/resolve.ts +6 -6
  254. package/src/tools/review.ts +10 -12
  255. package/src/tools/search-tool-bm25.ts +5 -5
  256. package/src/tools/search.ts +20 -29
  257. package/src/tools/ssh.ts +8 -8
  258. package/src/tools/todo.ts +16 -19
  259. package/src/tools/tts.ts +16 -15
  260. package/src/tools/write.ts +5 -5
  261. package/src/tui/code-cell.ts +44 -3
  262. package/src/tui/index.ts +1 -0
  263. package/src/tui/width-aware-text.ts +58 -0
  264. package/src/utils/image-vision-fallback.ts +197 -0
  265. package/src/utils/markit.ts +17 -2
  266. package/src/web/search/index.ts +21 -9
  267. package/src/web/search/providers/base.ts +1 -0
  268. package/src/web/search/providers/gemini.ts +56 -18
  269. package/src/web/search/providers/perplexity.ts +373 -126
  270. package/src/web/search/types.ts +28 -48
package/src/tools/read.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Database } from "bun:sqlite";
2
2
  import * as fs from "node:fs/promises";
3
+ import * as os from "node:os";
3
4
  import * as path from "node:path";
4
5
  import { formatHashlineHeader, formatNumberedLine, formatNumberedLines } from "@oh-my-pi/hashline";
5
6
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
@@ -8,8 +9,8 @@ import { glob, type SummaryResult, summarizeCode } from "@oh-my-pi/pi-natives";
8
9
  import type { Component } from "@oh-my-pi/pi-tui";
9
10
  import { Text } from "@oh-my-pi/pi-tui";
10
11
  import { getRemoteDir, logger, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
12
+ import { type } from "arktype";
11
13
  import { LRUCache } from "lru-cache/raw";
12
- import { z } from "zod/v4";
13
14
  import {
14
15
  canonicalSnapshotKey,
15
16
  getFileSnapshotStore,
@@ -661,18 +662,40 @@ function prependSuffixResolutionNotice(text: string, suffixResolution?: { from:
661
662
  const notice = `[Path '${suffixResolution.from}' not found; resolved to '${suffixResolution.to}' via suffix match]`;
662
663
  return text ? `${notice}\n${text}` : notice;
663
664
  }
665
+ const PDF_IMAGE_PLACEHOLDER_RE = /<!--\s*image:\s*([^\s<>]+)(.*?)-->/g;
666
+ const PDF_IMAGE_MEMBER_RE = /^(.*\.pdf):(.*)$/i;
667
+ const PDF_IMAGE_MEMBER_EXTENSION_RE = /\.png$/i;
664
668
 
665
- const readSchema = z
666
- .object({
667
- path: z
668
- .string()
669
- .describe(
670
- 'Local path, internal URI (e.g. "omp://", "issue://123", "pr://123"), or URL; append :<sel> for line ranges or raw mode (e.g. "src/foo.ts:50-100")',
671
- ),
672
- })
673
- .strict();
669
+ function pdfImageMemberPath(pdfPath: string, imageId: string): string {
670
+ const member = PDF_IMAGE_MEMBER_EXTENSION_RE.test(imageId) ? imageId : `${imageId}.png`;
671
+ return `${pdfPath}:${member}`;
672
+ }
673
+
674
+ function rewritePdfImagePlaceholders(markdown: string, pdfPath: string): string {
675
+ return markdown.replace(PDF_IMAGE_PLACEHOLDER_RE, (_match: string, imageId: string, metadataText: string) => {
676
+ const metadata = metadataText.trim();
677
+ const suffix = metadata.length > 0 ? ` (${metadata})` : "";
678
+ return `Image ${imageId}${suffix}: read \`${pdfImageMemberPath(pdfPath, imageId)}\``;
679
+ });
680
+ }
681
+
682
+ function splitPdfImageMemberReadPath(readPath: string): { pdfPath: string; member: string } | null {
683
+ const match = PDF_IMAGE_MEMBER_RE.exec(readPath);
684
+ if (!match) return null;
685
+ const pdfPath = match[1];
686
+ const member = match[2];
687
+ if (pdfPath === undefined || member === undefined) return null;
688
+ if (member.length !== 0 && !PDF_IMAGE_MEMBER_EXTENSION_RE.test(member)) return null;
689
+ return { pdfPath, member };
690
+ }
691
+
692
+ const readSchema = type({
693
+ path: type("string").describe(
694
+ 'Local path, internal URI (e.g. "omp://", "issue://123", "pr://123"), or URL; append :<sel> for line ranges or raw mode (e.g. "src/foo.ts:50-100")',
695
+ ),
696
+ });
674
697
 
675
- export type ReadToolInput = z.infer<typeof readSchema>;
698
+ export type ReadToolInput = typeof readSchema.infer;
676
699
 
677
700
  export interface ReadToolDetails {
678
701
  kind?: "file" | "url";
@@ -689,7 +712,11 @@ export interface ReadToolDetails {
689
712
  /** Raw text + start line for user-visible TUI rendering, set when content is text-like.
690
713
  * Mirrors the same lines the model receives but without hashline/line-number prefixes,
691
714
  * so the TUI can render the file content with its own gutter without re-parsing the formatted text. */
692
- displayContent?: { text: string; startLine: number };
715
+ displayContent?: {
716
+ text: string;
717
+ startLine: number;
718
+ lineNumbers?: Array<number | null>;
719
+ };
693
720
  summary?: { lines: number; elidedSpans: number; elidedLines: number };
694
721
  /** Number of unresolved git conflicts surfaced by this read (TUI uses for inline `⚠ N` badge). */
695
722
  conflictCount?: number;
@@ -979,6 +1006,113 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
979
1006
  return null;
980
1007
  }
981
1008
 
1009
+ #pdfImageCacheDir(absolutePdfPath: string): string {
1010
+ const artifactsDir = this.session.getArtifactsDir?.();
1011
+ let root = artifactsDir ?? undefined;
1012
+ if (root === undefined) {
1013
+ const sessionFile = this.session.getSessionFile();
1014
+ root = sessionFile?.endsWith(".jsonl")
1015
+ ? sessionFile.slice(0, -6)
1016
+ : path.join(os.tmpdir(), "omp-read-pdf-images");
1017
+ }
1018
+ const basename = path.basename(absolutePdfPath).replace(/[^A-Za-z0-9._-]/g, "_");
1019
+ return path.join(root, "read-pdf-images", `${basename}-${Bun.hash(absolutePdfPath).toString(36)}`);
1020
+ }
1021
+
1022
+ async #listPdfImageMembers(imageDir: string): Promise<string[]> {
1023
+ try {
1024
+ const entries = await fs.readdir(imageDir, { withFileTypes: true });
1025
+ const members: string[] = [];
1026
+ for (const entry of entries) {
1027
+ if (entry.isFile() && PDF_IMAGE_MEMBER_EXTENSION_RE.test(entry.name)) members.push(entry.name);
1028
+ }
1029
+ return members.sort();
1030
+ } catch (error) {
1031
+ if (isNotFoundError(error)) return [];
1032
+ throw error;
1033
+ }
1034
+ }
1035
+
1036
+ async #ensurePdfImageCache(absolutePdfPath: string, signal?: AbortSignal): Promise<string> {
1037
+ const imageDir = this.#pdfImageCacheDir(absolutePdfPath);
1038
+ const markerPath = path.join(imageDir, ".extracted");
1039
+ try {
1040
+ await fs.stat(markerPath);
1041
+ return imageDir;
1042
+ } catch (error) {
1043
+ if (!isNotFoundError(error)) throw error;
1044
+ }
1045
+
1046
+ await fs.rm(imageDir, { recursive: true, force: true });
1047
+ await fs.mkdir(imageDir, { recursive: true });
1048
+ const result = await convertFileWithMarkit(absolutePdfPath, signal, { imageDir });
1049
+ if (!result.ok) {
1050
+ await fs.rm(imageDir, { recursive: true, force: true });
1051
+ throw new ToolError(`Cannot extract images from PDF: ${result.error ?? "conversion failed"}`);
1052
+ }
1053
+ await Bun.write(markerPath, "ok");
1054
+ return imageDir;
1055
+ }
1056
+
1057
+ async #readPdfImageMember(
1058
+ absolutePdfPath: string,
1059
+ pdfDisplayPath: string,
1060
+ member: string,
1061
+ suffixResolution: { from: string; to: string } | undefined,
1062
+ signal?: AbortSignal,
1063
+ ): Promise<AgentToolResult<ReadToolDetails>> {
1064
+ const imageDir = await this.#ensurePdfImageCache(absolutePdfPath, signal);
1065
+ const members = await this.#listPdfImageMembers(imageDir);
1066
+ if (member.length === 0) {
1067
+ const text =
1068
+ members.length === 0
1069
+ ? "No extractable PDF image members found."
1070
+ : `Extractable PDF image members:\n${members
1071
+ .map(imageMember => `- read \`${pdfDisplayPath}:${imageMember}\``)
1072
+ .join("\n")}`;
1073
+ return toolResult<ReadToolDetails>({ resolvedPath: absolutePdfPath, suffixResolution })
1074
+ .text(prependSuffixResolutionNotice(text, suffixResolution))
1075
+ .sourcePath(absolutePdfPath)
1076
+ .done();
1077
+ }
1078
+
1079
+ if (!members.includes(member)) {
1080
+ const available = members.length === 0 ? "(none)" : members.join(", ");
1081
+ throw new ToolError(`PDF image member '${member}' not found. Available members: ${available}`);
1082
+ }
1083
+
1084
+ const imagePath = path.join(imageDir, member);
1085
+ const imageStat = await Bun.file(imagePath).stat();
1086
+ if (imageStat.size > MAX_IMAGE_SIZE) {
1087
+ const sizeStr = formatBytes(imageStat.size);
1088
+ const maxStr = formatBytes(MAX_IMAGE_SIZE);
1089
+ throw new ToolError(`Image file too large: ${sizeStr} exceeds ${maxStr} limit.`);
1090
+ }
1091
+ const metadata = await readImageMetadata(imagePath);
1092
+ const mimeType = metadata?.mimeType;
1093
+ if (!mimeType) throw new ToolError(`PDF image member '${member}' is not a supported image.`);
1094
+ const imageInput = await loadImageInput({
1095
+ path: `${pdfDisplayPath}:${member}`,
1096
+ cwd: this.session.cwd,
1097
+ autoResize: this.#autoResizeImages,
1098
+ maxBytes: MAX_IMAGE_SIZE,
1099
+ resolvedPath: imagePath,
1100
+ detectedMimeType: mimeType,
1101
+ excludeWebP: webpExclusionForModel(this.session.getActiveModel?.()),
1102
+ });
1103
+ if (!imageInput) {
1104
+ throw new ToolError(`Read image file [${mimeType}] failed: unsupported image format.`);
1105
+ }
1106
+ const textNote = prependSuffixResolutionNotice(imageInput.textNote, suffixResolution);
1107
+ return toolResult<ReadToolDetails>({ resolvedPath: absolutePdfPath, suffixResolution })
1108
+ .content([
1109
+ { type: "text", text: textNote },
1110
+ { type: "image", data: imageInput.data, mimeType: imageInput.mimeType },
1111
+ ])
1112
+ .sourcePath(imageInput.resolvedPath)
1113
+ .done();
1114
+ }
1115
+
982
1116
  #buildInMemoryTextResult(
983
1117
  text: string,
984
1118
  offset: number | undefined,
@@ -1058,7 +1192,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1058
1192
  let emittedHashlineHeader = false;
1059
1193
  let seenLines: number[] | undefined;
1060
1194
  const formatText = (content: string, startNum: number): string => {
1061
- details.displayContent = { text: content, startLine: startNum };
1195
+ const lineCount = countTextLines(content);
1196
+ details.displayContent = {
1197
+ text: content,
1198
+ startLine: startNum,
1199
+ lineNumbers: Array.from({ length: lineCount }, (_, i) => startNum + i),
1200
+ };
1062
1201
  if (shouldAddHashLines) seenLines = contiguousLineNumbers(startNum, countTextLines(content));
1063
1202
  const formatted = formatTextWithMode(content, startNum, shouldAddHashLines, shouldAddLineNumbers);
1064
1203
  if (!hashContext || emittedHashlineHeader) return formatted;
@@ -1070,6 +1209,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1070
1209
  details.displayContent = {
1071
1210
  text: lineEntriesToPlainText(entries, BRACKET_CONTEXT_ELLIPSIS),
1072
1211
  startLine: firstLine?.kind === "line" ? firstLine.lineNumber : startNum,
1212
+ lineNumbers: entries.map(entry => (entry.kind === "line" ? entry.lineNumber : null)),
1073
1213
  };
1074
1214
  if (shouldAddHashLines) seenLines = lineNumbersFromEntries(entries);
1075
1215
  const formatted = formatLineEntriesWithMode(entries, shouldAddHashLines, shouldAddLineNumbers);
@@ -1218,6 +1358,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1218
1358
  details.displayContent = {
1219
1359
  text: lineEntriesToPlainText(entries, BRACKET_CONTEXT_ELLIPSIS),
1220
1360
  startLine: firstLine.lineNumber,
1361
+ lineNumbers: entries.map(entry => (entry.kind === "line" ? entry.lineNumber : null)),
1221
1362
  };
1222
1363
  }
1223
1364
  const formatted = formatLineEntriesWithMode(entries, shouldAddHashLines, shouldAddLineNumbers);
@@ -1255,7 +1396,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1255
1396
  ): Promise<{
1256
1397
  outputText: string;
1257
1398
  columnTruncated: number;
1258
- displayContent?: { text: string; startLine: number };
1399
+ displayContent?: { text: string; startLine: number; lineNumbers?: Array<number | null> };
1259
1400
  bridgeResult?: AgentToolResult<ReadToolDetails>;
1260
1401
  }> {
1261
1402
  const rawSelector = isRawSelector(parsed);
@@ -1292,7 +1433,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1292
1433
  const displayLineByNumber = new Map<number, string>();
1293
1434
  const fullLines = rawSelector ? undefined : await readBracketContextFullLines(absolutePath, fileSize);
1294
1435
  let columnTruncated = 0;
1295
- let displayContent: { text: string; startLine: number } | undefined;
1436
+ let displayContent: { text: string; startLine: number; lineNumbers?: Array<number | null> } | undefined;
1296
1437
 
1297
1438
  for (const range of ranges) {
1298
1439
  const rangeStart = range.startLine - 1; // 0-indexed
@@ -1375,6 +1516,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1375
1516
  displayContent = {
1376
1517
  text: lineEntriesToPlainText(entries, BRACKET_CONTEXT_ELLIPSIS),
1377
1518
  startLine: firstLine?.kind === "line" ? firstLine.lineNumber : (visibleSpans[0]?.startLine ?? 1),
1519
+ lineNumbers: entries.map(entry => (entry.kind === "line" ? entry.lineNumber : null)),
1378
1520
  };
1379
1521
  outputText = formatLineEntriesWithMode(entries, shouldAddHashLines, shouldAddLineNumbers);
1380
1522
  } else {
@@ -1893,6 +2035,30 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1893
2035
  return this.#readSqlite(sqlitePath, signal);
1894
2036
  }
1895
2037
 
2038
+ const pdfImageMemberPath = splitPdfImageMemberReadPath(readPath);
2039
+ if (pdfImageMemberPath) {
2040
+ let absolutePdfPath = resolveReadPath(pdfImageMemberPath.pdfPath, this.session.cwd);
2041
+ let suffixResolution: { from: string; to: string } | undefined;
2042
+ try {
2043
+ const stat = await Bun.file(absolutePdfPath).stat();
2044
+ if (stat.isDirectory())
2045
+ throw new ToolError(`Path '${pdfImageMemberPath.pdfPath}' is a directory, not a PDF file`);
2046
+ } catch (error) {
2047
+ if (!isNotFoundError(error) || isRemoteMountPath(absolutePdfPath)) throw error;
2048
+ const suffixMatch = await this.#findSuffixMatchCached(suffixCache, pdfImageMemberPath.pdfPath, signal);
2049
+ if (!suffixMatch) throw new ToolError(`Path '${pdfImageMemberPath.pdfPath}' not found`);
2050
+ absolutePdfPath = suffixMatch.absolutePath;
2051
+ suffixResolution = { from: pdfImageMemberPath.pdfPath, to: suffixMatch.displayPath };
2052
+ }
2053
+ return this.#readPdfImageMember(
2054
+ absolutePdfPath,
2055
+ pdfImageMemberPath.pdfPath,
2056
+ pdfImageMemberPath.member,
2057
+ suffixResolution,
2058
+ signal,
2059
+ );
2060
+ }
2061
+
1896
2062
  const localTarget = splitPathAndSel(readPath);
1897
2063
  const localReadPath = localTarget.path;
1898
2064
  const parsed = parseSel(localTarget.sel);
@@ -2044,20 +2210,22 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2044
2210
  // Convert document via markit.
2045
2211
  const result = await convertFileWithMarkit(absolutePath, signal);
2046
2212
  if (result.ok) {
2213
+ const renderedContent =
2214
+ ext === ".pdf" ? rewritePdfImagePlaceholders(result.content, localReadPath) : result.content;
2047
2215
  // Route the converted markdown through the in-memory text builder
2048
2216
  // so line-range selectors (`file.pdf:50-100`, `:5-16,40-80`) and
2049
2217
  // raw mode apply against the converted output. Without this,
2050
2218
  // `file.pdf:50-100` silently returned the head of the document
2051
2219
  // because only `truncateHead` was being applied.
2052
2220
  if (isMultiRange(parsed) && parsed.kind === "lines") {
2053
- return this.#buildInMemoryMultiRangeResult(result.content, parsed.ranges, {
2221
+ return this.#buildInMemoryMultiRangeResult(renderedContent, parsed.ranges, {
2054
2222
  details: { resolvedPath: absolutePath },
2055
2223
  sourcePath: absolutePath,
2056
2224
  entityLabel: "document",
2057
2225
  });
2058
2226
  }
2059
2227
  const { offset, limit } = selToOffsetLimit(parsed);
2060
- return this.#buildInMemoryTextResult(result.content, offset, limit, {
2228
+ return this.#buildInMemoryTextResult(renderedContent, offset, limit, {
2061
2229
  details: { resolvedPath: absolutePath },
2062
2230
  sourcePath: absolutePath,
2063
2231
  entityLabel: "document",
@@ -2292,10 +2460,17 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2292
2460
  }
2293
2461
  }
2294
2462
 
2295
- let capturedDisplayContent: { text: string; startLine: number } | undefined;
2463
+ let capturedDisplayContent:
2464
+ | { text: string; startLine: number; lineNumbers?: Array<number | null> }
2465
+ | undefined;
2296
2466
  let emittedHashlineHeader = false;
2297
2467
  const formatText = (text: string, startNum: number): string => {
2298
- capturedDisplayContent = { text, startLine: startNum };
2468
+ const lineCount = countTextLines(text);
2469
+ capturedDisplayContent = {
2470
+ text,
2471
+ startLine: startNum,
2472
+ lineNumbers: Array.from({ length: lineCount }, (_, i) => startNum + i),
2473
+ };
2299
2474
  const formatted = formatTextWithMode(text, startNum, shouldAddHashLines, shouldAddLineNumbers);
2300
2475
  if (!hashContext || emittedHashlineHeader) return formatted;
2301
2476
  emittedHashlineHeader = true;
@@ -2322,6 +2497,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2322
2497
  capturedDisplayContent = {
2323
2498
  text: lineEntriesToPlainText(entries, BRACKET_CONTEXT_ELLIPSIS),
2324
2499
  startLine: firstLine?.kind === "line" ? firstLine.lineNumber : startLineDisplay,
2500
+ lineNumbers: entries.map(entry => (entry.kind === "line" ? entry.lineNumber : null)),
2325
2501
  };
2326
2502
  const formatted = formatLineEntriesWithMode(entries, shouldAddHashLines, shouldAddLineNumbers);
2327
2503
  if (!hashContext || emittedHashlineHeader) return formatted;
@@ -2928,6 +3104,8 @@ export const readToolRenderer = {
2928
3104
  status: "complete",
2929
3105
  output: warningLines.length > 0 ? warningLines.join("\n") : undefined,
2930
3106
  expanded,
3107
+ codeStartLine: details?.displayContent?.startLine,
3108
+ codeLineNumbers: details?.displayContent?.lineNumbers,
2931
3109
  width,
2932
3110
  },
2933
3111
  uiTheme,
@@ -23,7 +23,7 @@ import { Database } from "bun:sqlite";
23
23
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
24
24
  import type { FetchImpl } from "@oh-my-pi/pi-ai";
25
25
  import { $env, $flag, getAutoQaDbDir, getInstallId, logger, VERSION } from "@oh-my-pi/pi-utils";
26
- import { z } from "zod/v4";
26
+ import { type } from "arktype";
27
27
  import type { Settings } from "..";
28
28
  import type { ToolSession } from "./index";
29
29
 
@@ -31,12 +31,12 @@ function buildReportToolIssueParams(activeBuiltinNames: readonly string[]) {
31
31
  // Enum gives the model a tight schema; the runtime check in `execute` is the
32
32
  // source of truth (handles models that ignore the enum and the empty-list
33
33
  // fallback used by call sites that don't know the active set yet).
34
- const toolSchema = activeBuiltinNames.length > 0 ? z.enum(activeBuiltinNames as [string, ...string[]]) : z.string();
35
- return z.object({
34
+ const toolSchema = activeBuiltinNames.length > 0 ? type.enumerated(...activeBuiltinNames) : type("string");
35
+ return type({
36
36
  tool: toolSchema.describe("tool name"),
37
- report: z
38
- .string()
39
- .describe("unexpected behavior; generic, NEVER PII (paths, file contents, identifiers, prompt text)"),
37
+ report: type("string").describe(
38
+ "unexpected behavior; generic, NEVER PII (paths, file contents, identifiers, prompt text)",
39
+ ),
40
40
  });
41
41
  }
42
42
 
@@ -2,7 +2,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { Text } from "@oh-my-pi/pi-tui";
4
4
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
5
- import { z } from "zod/v4";
5
+ import { type } from "arktype";
6
6
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
7
7
  import type { Theme } from "../modes/theme/theme";
8
8
  import resolveDescription from "../prompts/tools/resolve.md" with { type: "text" };
@@ -11,13 +11,13 @@ import type { ToolSession } from ".";
11
11
  import { replaceTabs } from "./render-utils";
12
12
  import { ToolError } from "./tool-errors";
13
13
 
14
- const resolveSchema = z.object({
15
- action: z.enum(["apply", "discard"]),
16
- reason: z.string().describe("reason for action"),
17
- extra: z.record(z.string(), z.unknown()).optional().describe("free-form metadata"),
14
+ const resolveSchema = type({
15
+ action: "'apply' | 'discard'",
16
+ reason: type("string").describe("reason for action"),
17
+ "extra?": type("Record<string, unknown>").describe("free-form metadata"),
18
18
  });
19
19
 
20
- type ResolveParams = z.infer<typeof resolveSchema>;
20
+ type ResolveParams = typeof resolveSchema.infer;
21
21
 
22
22
  export interface ResolveToolDetails {
23
23
  action: "apply" | "discard";
@@ -12,7 +12,7 @@ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
12
12
  import type { Component } from "@oh-my-pi/pi-tui";
13
13
  import { Container, Text } from "@oh-my-pi/pi-tui";
14
14
  import { isRecord } from "@oh-my-pi/pi-utils";
15
- import { z } from "zod/v4";
15
+ import { type } from "arktype";
16
16
  import type { Theme, ThemeColor } from "../modes/theme/theme";
17
17
  import { subprocessToolRegistry } from "../task/subprocess-tool-registry";
18
18
  import type { ReviewFinding } from "../task/types";
@@ -52,17 +52,15 @@ function getPriorityDisplay(
52
52
 
53
53
  // report_finding schema
54
54
  // report_finding schema
55
- const ReportFindingParams = z
56
- .object({
57
- title: z.string().describe("prefixed imperative title"),
58
- body: z.string().describe("problem explanation"),
59
- priority: z.enum(["P0", "P1", "P2", "P3"] as const).describe("priority 0-3"),
60
- confidence: z.number().min(0).max(1).describe("confidence score"),
61
- file_path: z.string().describe("file path"),
62
- line_start: z.number().describe("start line"),
63
- line_end: z.number().describe("end line"),
64
- })
65
- .strict();
55
+ const ReportFindingParams = type({
56
+ title: type("string").describe("prefixed imperative title"),
57
+ body: type("string").describe("problem explanation"),
58
+ priority: type("'P0' | 'P1' | 'P2' | 'P3'").describe("priority 0-3"),
59
+ confidence: type("number >= 0 & number <= 1").describe("confidence score"),
60
+ file_path: type("string").describe("file path"),
61
+ line_start: type("number").describe("start line"),
62
+ line_end: type("number").describe("end line"),
63
+ });
66
64
 
67
65
  interface ReportFindingDetails {
68
66
  title: string;
@@ -2,7 +2,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { Text } from "@oh-my-pi/pi-tui";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
- import { z } from "zod/v4";
5
+ import { type } from "arktype";
6
6
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
7
7
  import type { Theme } from "../modes/theme/theme";
8
8
  import searchToolBm25Description from "../prompts/tools/search-tool-bm25.md" with { type: "text" };
@@ -27,12 +27,12 @@ const COLLAPSED_MATCH_LIMIT = 5;
27
27
  const MATCH_LABEL_LEN = 72;
28
28
  const MATCH_DESCRIPTION_LEN = 96;
29
29
 
30
- const searchToolBm25Schema = z.object({
31
- query: z.string().describe("tool search query"),
32
- limit: z.number().int().min(1).optional().describe("max matches"),
30
+ const searchToolBm25Schema = type({
31
+ query: type("string").describe("tool search query"),
32
+ "limit?": type("number>0").describe("max matches"),
33
33
  });
34
34
 
35
- type SearchToolBm25Params = z.infer<typeof searchToolBm25Schema>;
35
+ type SearchToolBm25Params = typeof searchToolBm25Schema.infer;
36
36
 
37
37
  interface SearchToolBm25Match {
38
38
  name: string;
@@ -7,7 +7,7 @@ import { type GrepMatch, GrepOutputMode, type GrepResult, grep } from "@oh-my-pi
7
7
  import type { Component } from "@oh-my-pi/pi-tui";
8
8
  import { Text } from "@oh-my-pi/pi-tui";
9
9
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
10
- import { z } from "zod/v4";
10
+ import { type } from "arktype";
11
11
  import { recordFileSnapshot, recordSeenLinesFromBody } from "../edit/file-snapshot-store";
12
12
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
13
13
  import type { LocalProtocolOptions } from "../internal-urls/local-protocol";
@@ -65,31 +65,24 @@ import {
65
65
  import { ToolError } from "./tool-errors";
66
66
  import { toolResult } from "./tool-result";
67
67
 
68
- const searchPathEntrySchema = z
69
- .string()
70
- .describe(
71
- 'file, directory, glob, internal URL, or "<file>:<lines>" selector (e.g. "src/foo.ts:50-100", "src/foo.ts:50+10", "src/foo.ts:50-100,200-300")',
72
- );
73
- const searchSchema = z
74
- .object({
75
- pattern: z.string().describe("regex pattern"),
76
- paths: z
77
- .union([searchPathEntrySchema, z.array(searchPathEntrySchema)])
78
- .optional()
79
- .describe(
80
- 'file, directory, glob, internal URL, or array of those to search; append `:<lines>` to scope a file to specific line ranges. Omitted or empty -> searches the workspace root (".")',
81
- ),
82
- i: z.boolean().optional().describe("case-insensitive search"),
83
- gitignore: z.boolean().optional().describe("respect gitignore"),
84
- skip: z
85
- .number()
86
- .nullable()
87
- .optional()
88
- .describe("files to skip before collecting results — use to paginate when the prior call hit the file limit"),
89
- })
90
- .strict();
91
-
92
- export type SearchToolInput = z.infer<typeof searchSchema>;
68
+ const searchPathEntry = type("string").describe(
69
+ 'file, directory, glob, internal URL, or "<file>:<lines>" selector (e.g. "src/foo.ts:50-100", "src/foo.ts:50+10", "src/foo.ts:50-100,200-300")',
70
+ );
71
+ const searchSchema = type({
72
+ pattern: type("string").describe("regex pattern"),
73
+ "paths?": searchPathEntry
74
+ .or(searchPathEntry.array())
75
+ .describe(
76
+ 'file, directory, glob, internal URL, or array of those to search; append `:<lines>` to scope a file to specific line ranges. Omitted or empty -> searches the workspace root (".")',
77
+ ),
78
+ "i?": type("boolean").describe("case-insensitive search"),
79
+ "gitignore?": type("boolean").describe("respect gitignore"),
80
+ "skip?": type("number")
81
+ .or("null")
82
+ .describe("files to skip before collecting results — use to paginate when the prior call hit the file limit"),
83
+ });
84
+
85
+ export type SearchToolInput = typeof searchSchema.infer;
93
86
  export function toPathList(input: string | string[] | undefined): string[] {
94
87
  return typeof input === "string" ? [input] : (input ?? []);
95
88
  }
@@ -665,7 +658,7 @@ export interface SearchToolDetails {
665
658
  missingPaths?: string[];
666
659
  }
667
660
 
668
- type SearchParams = z.infer<typeof searchSchema>;
661
+ type SearchParams = typeof searchSchema.infer;
669
662
 
670
663
  export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDetails> {
671
664
  readonly name = "search";
@@ -884,7 +877,6 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
884
877
  multiline: effectiveMultiline,
885
878
  hidden: true,
886
879
  gitignore: useGitignore,
887
- cache: false,
888
880
  maxCount: INTERNAL_TOTAL_CAP,
889
881
  contextBefore: normalizedContextBefore,
890
882
  contextAfter: normalizedContextAfter,
@@ -932,7 +924,6 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
932
924
  multiline: effectiveMultiline,
933
925
  hidden: true,
934
926
  gitignore: useGitignore,
935
- cache: false,
936
927
  maxCount: INTERNAL_TOTAL_CAP,
937
928
  contextBefore: normalizedContextBefore,
938
929
  contextAfter: normalizedContextAfter,
package/src/tools/ssh.ts CHANGED
@@ -2,7 +2,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
2
2
  import type { ToolExample } from "@oh-my-pi/pi-ai";
3
3
  import type { Component } from "@oh-my-pi/pi-tui";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
- import { z } from "zod/v4";
5
+ import { type } from "arktype";
6
6
  import type { SSHHost } from "../capability/ssh";
7
7
  import { sshCapability } from "../capability/ssh";
8
8
  import { loadCapability } from "../discovery";
@@ -23,11 +23,11 @@ import { ToolError } from "./tool-errors";
23
23
  import { toolResult } from "./tool-result";
24
24
  import { clampTimeout } from "./tool-timeouts";
25
25
 
26
- const sshSchema = z.object({
27
- host: z.string().describe("ssh host"),
28
- command: z.string().describe("remote command"),
29
- cwd: z.string().optional().describe("remote working directory"),
30
- timeout: z.number().optional().describe("timeout in seconds").default(60),
26
+ const sshSchema = type({
27
+ host: type("string").describe("ssh host"),
28
+ command: type("string").describe("remote command"),
29
+ "cwd?": type("string").describe("remote working directory"),
30
+ "timeout?": type("number").describe("timeout in seconds"),
31
31
  });
32
32
 
33
33
  export interface SSHToolDetails {
@@ -118,7 +118,7 @@ async function loadHosts(session: ToolSession): Promise<{
118
118
  return { hostNames, hostsByName };
119
119
  }
120
120
 
121
- type SshToolParams = z.infer<typeof sshSchema>;
121
+ type SshToolParams = typeof sshSchema.infer;
122
122
 
123
123
  export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
124
124
  readonly name = "ssh";
@@ -136,7 +136,7 @@ export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
136
136
  readonly concurrency = "exclusive";
137
137
  readonly strict = true;
138
138
 
139
- readonly examples: readonly ToolExample<z.input<typeof sshSchema>>[] = [
139
+ readonly examples: readonly ToolExample<SshToolParams>[] = [
140
140
  {
141
141
  caption: "List files: Linux (on server1 (10.0.0.1) | linux/bash)",
142
142
  call: { host: "server1", command: "ls -la /home/user" },
package/src/tools/todo.ts CHANGED
@@ -3,8 +3,8 @@ import type { ToolExample } from "@oh-my-pi/pi-ai";
3
3
  import type { Component } from "@oh-my-pi/pi-tui";
4
4
  import { Text } from "@oh-my-pi/pi-tui";
5
5
  import { prompt } from "@oh-my-pi/pi-utils";
6
+ import { type } from "arktype";
6
7
  import chalk from "chalk";
7
- import { z } from "zod/v4";
8
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
9
9
  import type { Theme } from "../modes/theme/theme";
10
10
  import todoDescription from "../prompts/tools/todo.md" with { type: "text" };
@@ -44,30 +44,27 @@ export interface TodoToolDetails {
44
44
  // Schema
45
45
  // =============================================================================
46
46
 
47
- const TodoOp = z
48
- .enum(["init", "start", "done", "rm", "drop", "append", "view"] as const)
49
- .describe("operation to apply");
47
+ const TodoOp = type('"init" | "start" | "done" | "rm" | "drop" | "append" | "view"').describe("operation to apply");
50
48
 
51
- const InitListEntry = z.object({
52
- phase: z.string().describe("phase name"),
53
- items: z.array(z.string().describe("task content")).min(1).describe("tasks for this phase"),
49
+ const InitListEntry = type({
50
+ phase: type("string").describe("phase name"),
51
+ items: type("string").describe("task content").array().atLeastLength(1).describe("tasks for this phase"),
54
52
  });
55
53
 
56
- const TodoOpEntry = z.object({
54
+ const TodoOpEntry = type({
57
55
  op: TodoOp,
58
- list: z.array(InitListEntry).optional().describe("phased task list (init)"),
59
- task: z.string().optional().describe("task content"),
60
- phase: z.string().optional().describe("phase name"),
61
- items: z.array(z.string().describe("task content")).min(1).optional().describe("tasks to append"),
56
+ "list?": InitListEntry.array().describe("phased task list (init)"),
57
+ "task?": type("string").describe("task content"),
58
+ "phase?": type("string").describe("phase name"),
59
+ "items?": type("string").describe("task content").array().atLeastLength(1).describe("tasks to append"),
62
60
  });
63
61
 
64
- const todoSchema = z
65
- .object({
66
- ops: z.array(TodoOpEntry).min(1).describe("ordered todo operations"),
67
- })
68
- .describe("apply ordered todo operations");
62
+ const todoSchema = type({
63
+ ops: TodoOpEntry.array().atLeastLength(1).describe("ordered todo operations"),
64
+ }).describe("apply ordered todo operations");
69
65
 
70
- type TodoParams = z.infer<typeof todoSchema>;
66
+ type TodoParams = TodoSchema;
67
+ type TodoSchema = typeof todoSchema.infer;
71
68
  type TodoOpEntryValue = TodoParams["ops"][number];
72
69
 
73
70
  // =============================================================================
@@ -565,7 +562,7 @@ export class TodoTool implements AgentTool<typeof todoSchema, TodoToolDetails> {
565
562
  readonly concurrency = "exclusive";
566
563
  readonly strict = true;
567
564
 
568
- readonly examples: readonly ToolExample<z.input<typeof todoSchema>>[] = [
565
+ readonly examples: readonly ToolExample<typeof todoSchema.infer>[] = [
569
566
  {
570
567
  caption: "Initial setup (multi-phase)",
571
568
  call: {