@oh-my-pi/pi-coding-agent 12.18.3 → 12.19.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 (231) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/package.json +7 -7
  3. package/src/async/index.ts +1 -0
  4. package/src/async/job-manager.ts +341 -0
  5. package/src/cli/file-processor.ts +3 -3
  6. package/src/cli/list-models.ts +3 -17
  7. package/src/cli/stats-cli.ts +3 -22
  8. package/src/cli/web-search-cli.ts +8 -16
  9. package/src/commit/agentic/agent.ts +6 -9
  10. package/src/commit/agentic/index.ts +44 -50
  11. package/src/commit/agentic/state.ts +0 -9
  12. package/src/commit/agentic/tools/propose-commit.ts +1 -30
  13. package/src/commit/agentic/tools/schemas.ts +31 -0
  14. package/src/commit/agentic/tools/split-commit.ts +1 -30
  15. package/src/commit/agentic/validation.ts +1 -18
  16. package/src/commit/analysis/conventional.ts +3 -50
  17. package/src/commit/analysis/summary.ts +2 -13
  18. package/src/commit/changelog/detect.ts +4 -1
  19. package/src/commit/changelog/generate.ts +2 -25
  20. package/src/commit/changelog/index.ts +1 -2
  21. package/src/commit/cli.ts +4 -12
  22. package/src/commit/map-reduce/reduce-phase.ts +2 -43
  23. package/src/commit/pipeline.ts +7 -15
  24. package/src/commit/utils.ts +44 -0
  25. package/src/config/prompt-templates.ts +1 -81
  26. package/src/config/settings-schema.ts +20 -1
  27. package/src/config.ts +2 -3
  28. package/src/debug/index.ts +1 -6
  29. package/src/debug/system-info.ts +2 -6
  30. package/src/discovery/builtin.ts +5 -9
  31. package/src/discovery/helpers.ts +0 -26
  32. package/src/discovery/ssh.ts +1 -8
  33. package/src/exa/company.ts +8 -39
  34. package/src/exa/factory.ts +64 -0
  35. package/src/exa/index.ts +0 -16
  36. package/src/exa/linkedin.ts +8 -39
  37. package/src/exa/mcp-client.ts +0 -64
  38. package/src/exa/researcher.ts +17 -59
  39. package/src/exa/search.ts +30 -154
  40. package/src/extensibility/custom-tools/loader.ts +3 -41
  41. package/src/extensibility/extensions/loader.ts +2 -9
  42. package/src/extensibility/hooks/loader.ts +3 -20
  43. package/src/extensibility/hooks/runner.ts +3 -19
  44. package/src/extensibility/plugins/installer.ts +2 -1
  45. package/src/extensibility/plugins/loader.ts +29 -117
  46. package/src/extensibility/skills.ts +2 -89
  47. package/src/extensibility/slash-commands.ts +1 -63
  48. package/src/extensibility/utils.ts +38 -0
  49. package/src/index.ts +9 -25
  50. package/src/internal-urls/index.ts +1 -0
  51. package/src/internal-urls/jobs-protocol.ts +118 -0
  52. package/src/ipy/kernel.ts +2 -0
  53. package/src/lsp/config.ts +1 -5
  54. package/src/lsp/lspmux.ts +0 -17
  55. package/src/lsp/utils.ts +2 -24
  56. package/src/main.ts +16 -24
  57. package/src/mcp/client.ts +1 -46
  58. package/src/mcp/render.ts +8 -1
  59. package/src/mcp/tool-cache.ts +1 -5
  60. package/src/mcp/transports/http.ts +2 -7
  61. package/src/mcp/transports/stdio.ts +2 -7
  62. package/src/modes/components/bash-execution.ts +2 -16
  63. package/src/modes/components/extensions/inspector-panel.ts +8 -18
  64. package/src/modes/components/footer.ts +10 -50
  65. package/src/modes/components/model-selector.ts +2 -21
  66. package/src/modes/components/python-execution.ts +2 -16
  67. package/src/modes/components/settings-selector.ts +1 -10
  68. package/src/modes/components/status-line/segments.ts +8 -25
  69. package/src/modes/components/status-line.ts +14 -31
  70. package/src/modes/components/tool-execution.ts +8 -2
  71. package/src/modes/controllers/command-controller.ts +71 -30
  72. package/src/modes/controllers/event-controller.ts +34 -4
  73. package/src/modes/controllers/mcp-command-controller.ts +3 -34
  74. package/src/modes/controllers/selector-controller.ts +2 -2
  75. package/src/modes/controllers/ssh-command-controller.ts +3 -34
  76. package/src/modes/interactive-mode.ts +6 -2
  77. package/src/modes/rpc/rpc-client.ts +1 -5
  78. package/src/modes/shared.ts +73 -0
  79. package/src/modes/types.ts +1 -0
  80. package/src/modes/utils/ui-helpers.ts +26 -2
  81. package/src/patch/index.ts +4 -4
  82. package/src/patch/normalize.ts +22 -65
  83. package/src/patch/shared.ts +16 -16
  84. package/src/prompts/system/custom-system-prompt.md +0 -10
  85. package/src/prompts/system/system-prompt.md +69 -89
  86. package/src/prompts/tools/async-result.md +5 -0
  87. package/src/prompts/tools/bash.md +5 -0
  88. package/src/prompts/tools/cancel-job.md +7 -0
  89. package/src/prompts/tools/poll-jobs.md +7 -0
  90. package/src/prompts/tools/task.md +4 -0
  91. package/src/sdk.ts +70 -6
  92. package/src/session/agent-session.ts +40 -6
  93. package/src/session/agent-storage.ts +69 -278
  94. package/src/session/auth-storage.ts +14 -1430
  95. package/src/session/session-manager.ts +69 -5
  96. package/src/session/session-storage.ts +1 -5
  97. package/src/session/streaming-output.ts +637 -76
  98. package/src/slash-commands/builtin-registry.ts +8 -0
  99. package/src/ssh/connection-manager.ts +4 -12
  100. package/src/ssh/sshfs-mount.ts +3 -7
  101. package/src/ssh/utils.ts +8 -0
  102. package/src/system-prompt.ts +24 -90
  103. package/src/task/executor.ts +11 -1
  104. package/src/task/index.ts +258 -13
  105. package/src/task/parallel.ts +32 -0
  106. package/src/task/render.ts +15 -7
  107. package/src/task/types.ts +5 -0
  108. package/src/tools/ask.ts +4 -7
  109. package/src/tools/bash-interactive.ts +4 -5
  110. package/src/tools/bash.ts +125 -41
  111. package/src/tools/cancel-job.ts +93 -0
  112. package/src/tools/fetch.ts +7 -27
  113. package/src/tools/find.ts +3 -3
  114. package/src/tools/gemini-image.ts +15 -14
  115. package/src/tools/grep.ts +3 -3
  116. package/src/tools/index.ts +13 -29
  117. package/src/tools/json-tree.ts +12 -1
  118. package/src/tools/jtd-to-json-schema.ts +10 -74
  119. package/src/tools/jtd-to-typescript.ts +10 -72
  120. package/src/tools/jtd-utils.ts +102 -0
  121. package/src/tools/notebook.ts +4 -9
  122. package/src/tools/output-meta.ts +52 -26
  123. package/src/tools/path-utils.ts +13 -7
  124. package/src/tools/poll-jobs.ts +178 -0
  125. package/src/tools/python.ts +32 -35
  126. package/src/tools/read.ts +61 -82
  127. package/src/tools/render-utils.ts +8 -159
  128. package/src/tools/ssh.ts +7 -20
  129. package/src/tools/submit-result.ts +1 -1
  130. package/src/tools/tool-errors.ts +0 -30
  131. package/src/tools/tool-result.ts +1 -2
  132. package/src/tools/write.ts +8 -10
  133. package/src/tui/code-cell.ts +8 -3
  134. package/src/tui/status-line.ts +4 -4
  135. package/src/tui/types.ts +0 -1
  136. package/src/tui/utils.ts +1 -14
  137. package/src/utils/command-args.ts +76 -0
  138. package/src/utils/file-mentions.ts +15 -19
  139. package/src/utils/frontmatter.ts +5 -10
  140. package/src/utils/shell-snapshot.ts +0 -11
  141. package/src/utils/title-generator.ts +0 -12
  142. package/src/web/scrapers/artifacthub.ts +7 -16
  143. package/src/web/scrapers/arxiv.ts +3 -8
  144. package/src/web/scrapers/aur.ts +8 -22
  145. package/src/web/scrapers/biorxiv.ts +5 -14
  146. package/src/web/scrapers/bluesky.ts +13 -36
  147. package/src/web/scrapers/brew.ts +5 -10
  148. package/src/web/scrapers/cheatsh.ts +2 -12
  149. package/src/web/scrapers/chocolatey.ts +63 -26
  150. package/src/web/scrapers/choosealicense.ts +3 -18
  151. package/src/web/scrapers/cisa-kev.ts +4 -18
  152. package/src/web/scrapers/clojars.ts +6 -33
  153. package/src/web/scrapers/coingecko.ts +25 -33
  154. package/src/web/scrapers/crates-io.ts +7 -26
  155. package/src/web/scrapers/crossref.ts +4 -18
  156. package/src/web/scrapers/devto.ts +11 -41
  157. package/src/web/scrapers/discogs.ts +7 -10
  158. package/src/web/scrapers/discourse.ts +6 -31
  159. package/src/web/scrapers/dockerhub.ts +12 -35
  160. package/src/web/scrapers/fdroid.ts +8 -33
  161. package/src/web/scrapers/firefox-addons.ts +10 -34
  162. package/src/web/scrapers/flathub.ts +7 -24
  163. package/src/web/scrapers/github-gist.ts +2 -12
  164. package/src/web/scrapers/github.ts +9 -47
  165. package/src/web/scrapers/gitlab.ts +130 -185
  166. package/src/web/scrapers/go-pkg.ts +12 -22
  167. package/src/web/scrapers/hackage.ts +88 -43
  168. package/src/web/scrapers/hackernews.ts +25 -45
  169. package/src/web/scrapers/hex.ts +19 -36
  170. package/src/web/scrapers/huggingface.ts +26 -91
  171. package/src/web/scrapers/iacr.ts +3 -8
  172. package/src/web/scrapers/jetbrains-marketplace.ts +9 -20
  173. package/src/web/scrapers/lemmy.ts +5 -23
  174. package/src/web/scrapers/lobsters.ts +16 -28
  175. package/src/web/scrapers/mastodon.ts +24 -43
  176. package/src/web/scrapers/maven.ts +6 -21
  177. package/src/web/scrapers/mdn.ts +7 -11
  178. package/src/web/scrapers/metacpan.ts +9 -41
  179. package/src/web/scrapers/musicbrainz.ts +4 -28
  180. package/src/web/scrapers/npm.ts +8 -25
  181. package/src/web/scrapers/nuget.ts +14 -37
  182. package/src/web/scrapers/nvd.ts +6 -28
  183. package/src/web/scrapers/ollama.ts +7 -34
  184. package/src/web/scrapers/open-vsx.ts +5 -19
  185. package/src/web/scrapers/opencorporates.ts +30 -14
  186. package/src/web/scrapers/openlibrary.ts +49 -33
  187. package/src/web/scrapers/orcid.ts +4 -18
  188. package/src/web/scrapers/osv.ts +7 -24
  189. package/src/web/scrapers/packagist.ts +9 -24
  190. package/src/web/scrapers/pub-dev.ts +7 -50
  191. package/src/web/scrapers/pubmed.ts +54 -21
  192. package/src/web/scrapers/pypi.ts +8 -26
  193. package/src/web/scrapers/rawg.ts +11 -19
  194. package/src/web/scrapers/readthedocs.ts +4 -9
  195. package/src/web/scrapers/reddit.ts +5 -15
  196. package/src/web/scrapers/repology.ts +8 -20
  197. package/src/web/scrapers/rfc.ts +5 -14
  198. package/src/web/scrapers/rubygems.ts +6 -21
  199. package/src/web/scrapers/searchcode.ts +8 -36
  200. package/src/web/scrapers/sec-edgar.ts +4 -18
  201. package/src/web/scrapers/semantic-scholar.ts +15 -35
  202. package/src/web/scrapers/snapcraft.ts +5 -19
  203. package/src/web/scrapers/sourcegraph.ts +5 -43
  204. package/src/web/scrapers/spdx.ts +4 -18
  205. package/src/web/scrapers/spotify.ts +4 -23
  206. package/src/web/scrapers/stackoverflow.ts +8 -13
  207. package/src/web/scrapers/terraform.ts +9 -37
  208. package/src/web/scrapers/tldr.ts +3 -7
  209. package/src/web/scrapers/twitter.ts +3 -7
  210. package/src/web/scrapers/types.ts +105 -27
  211. package/src/web/scrapers/utils.ts +97 -103
  212. package/src/web/scrapers/vimeo.ts +7 -27
  213. package/src/web/scrapers/vscode-marketplace.ts +8 -17
  214. package/src/web/scrapers/w3c.ts +6 -14
  215. package/src/web/scrapers/wikidata.ts +5 -19
  216. package/src/web/scrapers/wikipedia.ts +2 -12
  217. package/src/web/scrapers/youtube.ts +5 -34
  218. package/src/web/search/index.ts +0 -9
  219. package/src/web/search/providers/anthropic.ts +3 -2
  220. package/src/web/search/providers/brave.ts +3 -18
  221. package/src/web/search/providers/exa.ts +1 -12
  222. package/src/web/search/providers/kimi.ts +5 -44
  223. package/src/web/search/providers/perplexity.ts +1 -12
  224. package/src/web/search/providers/synthetic.ts +3 -26
  225. package/src/web/search/providers/utils.ts +36 -0
  226. package/src/web/search/providers/zai.ts +9 -50
  227. package/src/web/search/types.ts +0 -28
  228. package/src/web/search/utils.ts +17 -0
  229. package/src/tools/output-utils.ts +0 -63
  230. package/src/tools/truncate.ts +0 -385
  231. package/src/web/search/auth.ts +0 -178
@@ -1,7 +1,7 @@
1
1
  import * as os from "node:os";
2
2
  import * as path from "node:path";
3
3
  import { getAntigravityHeaders, getEnvApiKey, StringEnum } from "@oh-my-pi/pi-ai";
4
- import { $env, ptree, readSseJson, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
4
+ import { $env, isEnoent, ptree, readSseJson, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
5
5
  import { type Static, Type } from "@sinclair/typebox";
6
6
  import type { ModelRegistry } from "../config/model-registry";
7
7
  import { renderPromptTemplate } from "../config/prompt-templates";
@@ -428,21 +428,22 @@ async function findImageApiKey(modelRegistry?: ModelRegistry): Promise<ImageApiK
428
428
 
429
429
  async function loadImageFromPath(imagePath: string, cwd: string): Promise<InlineImageData> {
430
430
  const resolved = resolveReadPath(imagePath, cwd);
431
- const file = Bun.file(resolved);
432
- if (!(await file.exists())) {
433
- throw new Error(`Image file not found: ${imagePath}`);
434
- }
435
- if (file.size > MAX_IMAGE_SIZE) {
436
- throw new Error(`Image file too large: ${imagePath}`);
437
- }
431
+ try {
432
+ const buffer = await Bun.file(resolved).bytes();
433
+ if (buffer.length > MAX_IMAGE_SIZE) {
434
+ throw new Error(`Image file too large: ${imagePath}`);
435
+ }
438
436
 
439
- const mimeType = await detectSupportedImageMimeTypeFromFile(resolved);
440
- if (!mimeType) {
441
- throw new Error(`Unsupported image type: ${imagePath}`);
442
- }
437
+ const mimeType = await detectSupportedImageMimeTypeFromFile(resolved);
438
+ if (!mimeType) {
439
+ throw new Error(`Unsupported image type: ${imagePath}`);
440
+ }
443
441
 
444
- const buffer = await file.bytes();
445
- return { data: buffer.toBase64(), mimeType };
442
+ return { data: buffer.toBase64(), mimeType };
443
+ } catch (err) {
444
+ if (isEnoent(err)) throw new Error(`Image file not found: ${imagePath}`);
445
+ throw err;
446
+ }
446
447
  }
447
448
 
448
449
  async function resolveInputImage(input: ImageInput, cwd: string): Promise<InlineImageData> {
package/src/tools/grep.ts CHANGED
@@ -11,15 +11,15 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
11
  import type { Theme } from "../modes/theme/theme";
12
12
  import { computeLineHash } from "../patch/hashline";
13
13
  import grepDescription from "../prompts/tools/grep.md" with { type: "text" };
14
+ import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead } from "../session/streaming-output";
14
15
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
15
16
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
16
17
  import type { ToolSession } from ".";
17
- import type { OutputMeta } from "./output-meta";
18
+ import { formatFullOutputReference, type OutputMeta } from "./output-meta";
18
19
  import { resolveToCwd } from "./path-utils";
19
20
  import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
20
21
  import { ToolError } from "./tool-errors";
21
22
  import { toolResult } from "./tool-result";
22
- import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead } from "./truncate";
23
23
 
24
24
  const grepSchema = Type.Object({
25
25
  pattern: Type.String({ description: "Regex pattern to search for" }),
@@ -452,7 +452,7 @@ export const grepToolRenderer = {
452
452
  if (limits?.resultLimit) truncationReasons.push(`limit ${limits.resultLimit.reached} results`);
453
453
  if (truncation) truncationReasons.push(truncation.truncatedBy === "lines" ? "line limit" : "size limit");
454
454
  if (limits?.columnTruncated) truncationReasons.push(`line length ${limits.columnTruncated.maxColumn}`);
455
- if (truncation?.artifactId) truncationReasons.push(`full output: artifact://${truncation.artifactId}`);
455
+ if (truncation?.artifactId) truncationReasons.push(formatFullOutputReference(truncation.artifactId));
456
456
 
457
457
  const extraLines =
458
458
  truncationReasons.length > 0 ? [uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`)] : [];
@@ -1,5 +1,6 @@
1
1
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
2
  import { $env, logger } from "@oh-my-pi/pi-utils";
3
+ import type { AsyncJobManager } from "../async";
3
4
  import type { PromptTemplate } from "../config/prompt-templates";
4
5
  import type { Settings } from "../config/settings";
5
6
  import type { Skill } from "../extensibility/skills";
@@ -9,7 +10,6 @@ import { checkPythonKernelAvailability } from "../ipy/kernel";
9
10
  import { LspTool } from "../lsp";
10
11
  import { EditTool } from "../patch";
11
12
  import type { PlanModeState } from "../plan-mode/state";
12
- import type { ArtifactManager } from "../session/artifacts";
13
13
  import { TaskTool } from "../task";
14
14
  import type { AgentOutputManager } from "../task/output-manager";
15
15
  import type { EventBus } from "../utils/event-bus";
@@ -18,12 +18,14 @@ import { AskTool } from "./ask";
18
18
  import { BashTool } from "./bash";
19
19
  import { BrowserTool } from "./browser";
20
20
  import { CalculatorTool } from "./calculator";
21
+ import { CancelJobTool } from "./cancel-job";
21
22
  import { ExitPlanModeTool } from "./exit-plan-mode";
22
23
  import { FetchTool } from "./fetch";
23
24
  import { FindTool } from "./find";
24
25
  import { GrepTool } from "./grep";
25
26
  import { NotebookTool } from "./notebook";
26
27
  import { wrapToolWithMetaNotice } from "./output-meta";
28
+ import { PollJobsTool } from "./poll-jobs";
27
29
  import { PythonTool } from "./python";
28
30
  import { ReadTool } from "./read";
29
31
  import { reportFindingTool } from "./review";
@@ -48,49 +50,27 @@ export {
48
50
  warmupLspServers,
49
51
  } from "../lsp";
50
52
  export { EditTool, type EditToolDetails } from "../patch";
53
+ export * from "../session/streaming-output";
51
54
  export { BUNDLED_AGENTS, TaskTool } from "../task";
52
- export {
53
- companySearchTools,
54
- exaSearchTools,
55
- getSearchTools,
56
- type SearchProvider,
57
- type SearchResponse,
58
- SearchTool,
59
- type SearchToolsOptions,
60
- setPreferredSearchProvider,
61
- webSearchCodeContextTool,
62
- webSearchCompanyTool,
63
- webSearchCrawlTool,
64
- webSearchCustomTool,
65
- webSearchDeepTool,
66
- webSearchLinkedinTool,
67
- } from "../web/search";
55
+ export * from "../web/search";
68
56
  export { AskTool, type AskToolDetails } from "./ask";
69
57
  export { BashTool, type BashToolDetails, type BashToolInput, type BashToolOptions } from "./bash";
70
58
  export { BrowserTool, type BrowserToolDetails } from "./browser";
71
59
  export { CalculatorTool, type CalculatorToolDetails } from "./calculator";
60
+ export { CancelJobTool, type CancelJobToolDetails } from "./cancel-job";
72
61
  export { type ExitPlanModeDetails, ExitPlanModeTool } from "./exit-plan-mode";
73
62
  export { FetchTool, type FetchToolDetails } from "./fetch";
74
63
  export { type FindOperations, FindTool, type FindToolDetails, type FindToolInput, type FindToolOptions } from "./find";
75
64
  export { setPreferredImageProvider } from "./gemini-image";
76
65
  export { GrepTool, type GrepToolDetails, type GrepToolInput } from "./grep";
77
66
  export { NotebookTool, type NotebookToolDetails } from "./notebook";
67
+ export { PollJobsTool, type PollJobsToolDetails } from "./poll-jobs";
78
68
  export { PythonTool, type PythonToolDetails, type PythonToolOptions } from "./python";
79
69
  export { ReadTool, type ReadToolDetails, type ReadToolInput } from "./read";
80
70
  export { reportFindingTool, type SubmitReviewDetails } from "./review";
81
71
  export { loadSshTool, type SSHToolDetails, SshTool } from "./ssh";
82
72
  export { SubmitResultTool } from "./submit-result";
83
73
  export { type TodoItem, TodoWriteTool, type TodoWriteToolDetails } from "./todo-write";
84
- export {
85
- DEFAULT_MAX_BYTES,
86
- DEFAULT_MAX_LINES,
87
- formatSize,
88
- type TruncationOptions,
89
- type TruncationResult,
90
- truncateHead,
91
- truncateLine,
92
- truncateTail,
93
- } from "./truncate";
94
74
  export { WriteTool, type WriteToolDetails, type WriteToolInput } from "./write";
95
75
 
96
76
  /** Tool type (AgentTool from pi-ai) */
@@ -132,10 +112,10 @@ export interface ToolSession {
132
112
  getSessionFile: () => string | null;
133
113
  /** Get session ID */
134
114
  getSessionId?: () => string | null;
135
- /** Cached artifact manager (allocated per ToolSession) */
136
- artifactManager?: ArtifactManager;
137
115
  /** Get artifacts directory for artifact:// URLs and $ARTIFACTS env var */
138
116
  getArtifactsDir?: () => string | null;
117
+ /** Allocate a new artifact path and ID for session-scoped truncated output. */
118
+ allocateOutputArtifact?: (toolType: string) => Promise<{ id?: string; path?: string }>;
139
119
  /** Get session spawns */
140
120
  getSessionSpawns: () => string | null;
141
121
  /** Get resolved model string if explicitly set for this session */
@@ -152,6 +132,8 @@ export interface ToolSession {
152
132
  internalRouter?: InternalUrlRouter;
153
133
  /** Agent output manager for unique agent:// IDs across task invocations */
154
134
  agentOutputManager?: AgentOutputManager;
135
+ /** Async background job manager for bash/task async execution */
136
+ asyncJobManager?: AsyncJobManager;
155
137
  /** Settings instance for passing to subagents */
156
138
  settings: Settings;
157
139
  /** Plan mode state (if active) */
@@ -176,6 +158,8 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
176
158
  read: s => new ReadTool(s),
177
159
  browser: s => new BrowserTool(s),
178
160
  task: TaskTool.create,
161
+ cancel_job: CancelJobTool.createIf,
162
+ poll_jobs: PollJobsTool.createIf,
179
163
  todo_write: s => new TodoWriteTool(s),
180
164
  fetch: s => new FetchTool(s),
181
165
  web_search: s => new SearchTool(s),
@@ -12,6 +12,17 @@ export const JSON_TREE_MAX_LINES_EXPANDED = 200;
12
12
  export const JSON_TREE_SCALAR_LEN_COLLAPSED = 60;
13
13
  export const JSON_TREE_SCALAR_LEN_EXPANDED = 2000;
14
14
 
15
+ /** Keys injected by the harness that should not be displayed to users */
16
+ const HIDDEN_ARG_KEYS = new Set(["agent__intent"]);
17
+
18
+ /** Strip harness-internal keys from tool args for display */
19
+ export function stripInternalArgs(args: Record<string, unknown>): Record<string, unknown> {
20
+ const result: Record<string, unknown> = {};
21
+ for (const [key, value] of Object.entries(args)) {
22
+ if (!HIDDEN_ARG_KEYS.has(key)) result[key] = value;
23
+ }
24
+ return result;
25
+ }
15
26
  /**
16
27
  * Format a scalar value for inline display.
17
28
  */
@@ -37,7 +48,7 @@ export function formatScalar(value: unknown, maxLen: number): string {
37
48
  * Format args inline for collapsed view.
38
49
  */
39
50
  export function formatArgsInline(args: Record<string, unknown>, maxWidth: number): string {
40
- const entries = Object.entries(args);
51
+ const entries = Object.entries(args).filter(([k]) => !HIDDEN_ARG_KEYS.has(k));
41
52
  if (entries.length === 0) return "";
42
53
 
43
54
  // Single arg: show key=value
@@ -8,52 +8,16 @@
8
8
  * @see https://datatracker.ietf.org/doc/html/rfc8927
9
9
  */
10
10
 
11
- type JTDPrimitive =
12
- | "boolean"
13
- | "string"
14
- | "timestamp"
15
- | "float32"
16
- | "float64"
17
- | "int8"
18
- | "uint8"
19
- | "int16"
20
- | "uint16"
21
- | "int32"
22
- | "uint32";
23
-
24
- interface JTDType {
25
- type: JTDPrimitive;
26
- }
27
-
28
- interface JTDEnum {
29
- enum: string[];
30
- }
31
-
32
- interface JTDElements {
33
- elements: JTDSchema;
34
- }
35
-
36
- interface JTDValues {
37
- values: JTDSchema;
38
- }
39
-
40
- interface JTDProperties {
41
- properties?: Record<string, JTDSchema>;
42
- optionalProperties?: Record<string, JTDSchema>;
43
- }
44
-
45
- interface JTDDiscriminator {
46
- discriminator: string;
47
- mapping: Record<string, JTDProperties>;
48
- }
49
-
50
- interface JTDRef {
51
- ref: string;
52
- }
53
-
54
- interface JTDEmpty {}
55
-
56
- type JTDSchema = JTDType | JTDEnum | JTDElements | JTDValues | JTDProperties | JTDDiscriminator | JTDRef | JTDEmpty;
11
+ import type { JTDPrimitive } from "./jtd-utils.js";
12
+ import {
13
+ isJTDDiscriminator,
14
+ isJTDElements,
15
+ isJTDEnum,
16
+ isJTDProperties,
17
+ isJTDRef,
18
+ isJTDType,
19
+ isJTDValues,
20
+ } from "./jtd-utils.js";
57
21
 
58
22
  const primitiveMap: Record<JTDPrimitive, string> = {
59
23
  boolean: "boolean",
@@ -69,34 +33,6 @@ const primitiveMap: Record<JTDPrimitive, string> = {
69
33
  uint32: "integer",
70
34
  };
71
35
 
72
- function isJTDType(schema: unknown): schema is JTDType {
73
- return typeof schema === "object" && schema !== null && "type" in schema;
74
- }
75
-
76
- function isJTDEnum(schema: unknown): schema is JTDEnum {
77
- return typeof schema === "object" && schema !== null && "enum" in schema;
78
- }
79
-
80
- function isJTDElements(schema: unknown): schema is JTDElements {
81
- return typeof schema === "object" && schema !== null && "elements" in schema;
82
- }
83
-
84
- function isJTDValues(schema: unknown): schema is JTDValues {
85
- return typeof schema === "object" && schema !== null && "values" in schema;
86
- }
87
-
88
- function isJTDProperties(schema: unknown): schema is JTDProperties {
89
- return typeof schema === "object" && schema !== null && ("properties" in schema || "optionalProperties" in schema);
90
- }
91
-
92
- function isJTDDiscriminator(schema: unknown): schema is JTDDiscriminator {
93
- return typeof schema === "object" && schema !== null && "discriminator" in schema;
94
- }
95
-
96
- function isJTDRef(schema: unknown): schema is JTDRef {
97
- return typeof schema === "object" && schema !== null && "ref" in schema;
98
- }
99
-
100
36
  function convertSchema(schema: unknown): unknown {
101
37
  if (schema === null || typeof schema !== "object") {
102
38
  return {};
@@ -5,50 +5,16 @@
5
5
  * helping models understand expected output structure.
6
6
  */
7
7
 
8
- type JTDPrimitive =
9
- | "boolean"
10
- | "string"
11
- | "timestamp"
12
- | "float32"
13
- | "float64"
14
- | "int8"
15
- | "uint8"
16
- | "int16"
17
- | "uint16"
18
- | "int32"
19
- | "uint32";
20
-
21
- interface JTDType {
22
- type: JTDPrimitive;
23
- }
24
-
25
- interface JTDEnum {
26
- enum: string[];
27
- }
28
-
29
- interface JTDElements {
30
- elements: JTDSchema;
31
- }
32
-
33
- interface JTDValues {
34
- values: JTDSchema;
35
- }
36
-
37
- interface JTDProperties {
38
- properties?: Record<string, JTDSchema>;
39
- optionalProperties?: Record<string, JTDSchema>;
40
- }
41
-
42
- interface JTDDiscriminator {
43
- discriminator: string;
44
- mapping: Record<string, JTDProperties>;
45
- }
46
-
47
- interface JTDRef {
48
- ref: string;
49
- }
50
-
51
- type JTDSchema = JTDType | JTDEnum | JTDElements | JTDValues | JTDProperties | JTDDiscriminator | JTDRef | object;
8
+ import type { JTDPrimitive } from "./jtd-utils.js";
9
+ import {
10
+ isJTDDiscriminator,
11
+ isJTDElements,
12
+ isJTDEnum,
13
+ isJTDProperties,
14
+ isJTDRef,
15
+ isJTDType,
16
+ isJTDValues,
17
+ } from "./jtd-utils.js";
52
18
 
53
19
  const primitiveMap: Record<JTDPrimitive, string> = {
54
20
  boolean: "boolean",
@@ -64,34 +30,6 @@ const primitiveMap: Record<JTDPrimitive, string> = {
64
30
  uint32: "number",
65
31
  };
66
32
 
67
- function isJTDType(schema: unknown): schema is JTDType {
68
- return typeof schema === "object" && schema !== null && "type" in schema;
69
- }
70
-
71
- function isJTDEnum(schema: unknown): schema is JTDEnum {
72
- return typeof schema === "object" && schema !== null && "enum" in schema && Array.isArray((schema as JTDEnum).enum);
73
- }
74
-
75
- function isJTDElements(schema: unknown): schema is JTDElements {
76
- return typeof schema === "object" && schema !== null && "elements" in schema;
77
- }
78
-
79
- function isJTDValues(schema: unknown): schema is JTDValues {
80
- return typeof schema === "object" && schema !== null && "values" in schema;
81
- }
82
-
83
- function isJTDProperties(schema: unknown): schema is JTDProperties {
84
- return typeof schema === "object" && schema !== null && ("properties" in schema || "optionalProperties" in schema);
85
- }
86
-
87
- function isJTDDiscriminator(schema: unknown): schema is JTDDiscriminator {
88
- return typeof schema === "object" && schema !== null && "discriminator" in schema && "mapping" in schema;
89
- }
90
-
91
- function isJTDRef(schema: unknown): schema is JTDRef {
92
- return typeof schema === "object" && schema !== null && "ref" in schema;
93
- }
94
-
95
33
  function convertToTypeScript(schema: unknown, inline = false): string {
96
34
  if (schema === null || schema === undefined || (typeof schema === "object" && Object.keys(schema).length === 0)) {
97
35
  return "unknown";
@@ -0,0 +1,102 @@
1
+ /**
2
+ * JSON Type Definition (JTD) utility types and guards.
3
+ *
4
+ * Shared type definitions and type guard functions for JTD schema validation.
5
+ *
6
+ * @see https://jsontypedef.com/
7
+ * @see https://datatracker.ietf.org/doc/html/rfc8927
8
+ */
9
+
10
+ export type JTDPrimitive =
11
+ | "boolean"
12
+ | "string"
13
+ | "timestamp"
14
+ | "float32"
15
+ | "float64"
16
+ | "int8"
17
+ | "uint8"
18
+ | "int16"
19
+ | "uint16"
20
+ | "int32"
21
+ | "uint32";
22
+
23
+ export interface JTDType {
24
+ type: JTDPrimitive;
25
+ }
26
+
27
+ export interface JTDEnum {
28
+ enum: string[];
29
+ }
30
+
31
+ export interface JTDElements {
32
+ elements: JTDSchema;
33
+ }
34
+
35
+ export interface JTDValues {
36
+ values: JTDSchema;
37
+ }
38
+
39
+ export interface JTDProperties {
40
+ properties?: Record<string, JTDSchema>;
41
+ optionalProperties?: Record<string, JTDSchema>;
42
+ }
43
+
44
+ export interface JTDDiscriminator {
45
+ discriminator: string;
46
+ mapping: Record<string, JTDProperties>;
47
+ }
48
+
49
+ export interface JTDRef {
50
+ ref: string;
51
+ }
52
+
53
+ export interface JTDEmpty {}
54
+
55
+ export type JTDSchema =
56
+ | JTDType
57
+ | JTDEnum
58
+ | JTDElements
59
+ | JTDValues
60
+ | JTDProperties
61
+ | JTDDiscriminator
62
+ | JTDRef
63
+ | JTDEmpty;
64
+
65
+ // Type guards
66
+
67
+ export function isJTDType(schema: unknown): schema is JTDType {
68
+ return typeof schema === "object" && schema !== null && "type" in schema;
69
+ }
70
+
71
+ export function isJTDEnum(schema: unknown): schema is JTDEnum {
72
+ return typeof schema === "object" && schema !== null && "enum" in schema && Array.isArray(schema.enum);
73
+ }
74
+
75
+ export function isJTDElements(schema: unknown): schema is JTDElements {
76
+ return typeof schema === "object" && schema !== null && "elements" in schema;
77
+ }
78
+
79
+ export function isJTDValues(schema: unknown): schema is JTDValues {
80
+ return typeof schema === "object" && schema !== null && "values" in schema;
81
+ }
82
+
83
+ export function isJTDProperties(schema: unknown): schema is JTDProperties {
84
+ return typeof schema === "object" && schema !== null && ("properties" in schema || "optionalProperties" in schema);
85
+ }
86
+
87
+ export function isJTDDiscriminator(schema: unknown): schema is JTDDiscriminator {
88
+ return (
89
+ typeof schema === "object" &&
90
+ schema !== null &&
91
+ "discriminator" in schema &&
92
+ "mapping" in schema &&
93
+ typeof schema.discriminator === "string" &&
94
+ typeof schema.mapping === "object" &&
95
+ schema.mapping !== null &&
96
+ !Array.isArray(schema.mapping)
97
+ );
98
+ }
99
+
100
+ export function isJTDRef(schema: unknown): schema is JTDRef {
101
+ return typeof schema === "object" && schema !== null && "ref" in schema;
102
+ }
@@ -2,7 +2,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
2
2
  import { StringEnum } 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
- import { untilAborted } from "@oh-my-pi/pi-utils";
5
+ import { isEnoent, untilAborted } from "@oh-my-pi/pi-utils";
6
6
  import { type Static, Type } from "@sinclair/typebox";
7
7
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
8
  import type { Theme } from "../modes/theme/theme";
@@ -80,17 +80,12 @@ export class NotebookTool implements AgentTool<typeof notebookSchema, NotebookTo
80
80
  const absolutePath = resolveToCwd(notebook_path, this.session.cwd);
81
81
 
82
82
  return untilAborted(signal, async () => {
83
- // Check if file exists
84
- const file = Bun.file(absolutePath);
85
- if (!(await file.exists())) {
86
- throw new Error(`Notebook not found: ${notebook_path}`);
87
- }
88
-
89
83
  // Read and parse notebook
90
84
  let notebook: Notebook;
91
85
  try {
92
- notebook = await file.json();
93
- } catch {
86
+ notebook = await Bun.file(absolutePath).json();
87
+ } catch (err) {
88
+ if (isEnoent(err)) throw new Error(`Notebook not found: ${notebook_path}`);
94
89
  throw new Error(`Invalid JSON in notebook: ${notebook_path}`);
95
90
  }
96
91
 
@@ -12,10 +12,10 @@ import type {
12
12
  AgentToolUpdateCallback,
13
13
  } from "@oh-my-pi/pi-agent-core";
14
14
  import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
15
- import type { OutputSummary } from "../session/streaming-output";
15
+ import type { Theme } from "../modes/theme/theme";
16
+ import type { OutputSummary, TruncationResult } from "../session/streaming-output";
17
+ import { formatBytes, wrapBrackets } from "./render-utils";
16
18
  import { renderError } from "./tool-errors";
17
- import type { TruncationResult } from "./truncate";
18
- import { formatSize } from "./truncate";
19
19
 
20
20
  /**
21
21
  * Truncation metadata for the output notice.
@@ -313,6 +313,44 @@ export function outputMeta(): OutputMetaBuilder {
313
313
  // Notice formatting
314
314
  // =============================================================================
315
315
 
316
+ export function formatFullOutputReference(artifactId: string): string {
317
+ return `Full output: artifact://${artifactId}`;
318
+ }
319
+
320
+ export function formatTruncationMetaNotice(truncation: TruncationMeta): string {
321
+ const range = truncation.shownRange;
322
+ let notice: string;
323
+
324
+ if (range && range.end >= range.start) {
325
+ notice = `Showing lines ${range.start}-${range.end} of ${truncation.totalLines}`;
326
+ } else {
327
+ notice = `Showing ${truncation.outputLines} of ${truncation.totalLines} lines`;
328
+ }
329
+
330
+ if (truncation.truncatedBy === "bytes") {
331
+ const maxBytes = truncation.maxBytes ?? truncation.outputBytes;
332
+ notice += ` (${formatBytes(maxBytes)} limit)`;
333
+ }
334
+
335
+ if (truncation.nextOffset != null) {
336
+ notice += `. Use offset=${truncation.nextOffset} to continue`;
337
+ }
338
+
339
+ if (truncation.artifactId != null) {
340
+ notice += `. ${formatFullOutputReference(truncation.artifactId)}`;
341
+ }
342
+
343
+ return notice;
344
+ }
345
+
346
+ /**
347
+ * Format styled artifact reference with warning color and brackets.
348
+ * For TUI rendering of truncation warnings.
349
+ */
350
+ export function formatStyledArtifactReference(artifactId: string, theme: Theme): string {
351
+ return theme.fg("warning", formatFullOutputReference(artifactId));
352
+ }
353
+
316
354
  /**
317
355
  * Format notices from OutputMeta for LLM consumption.
318
356
  * Returns empty string if no notices needed.
@@ -324,29 +362,7 @@ export function formatOutputNotice(meta: OutputMeta | undefined): string {
324
362
 
325
363
  // Truncation notice
326
364
  if (meta.truncation) {
327
- const t = meta.truncation;
328
- const range = t.shownRange;
329
- let notice: string;
330
-
331
- if (range && range.end >= range.start) {
332
- notice = `Showing lines ${range.start}-${range.end} of ${t.totalLines}`;
333
- } else {
334
- notice = `Showing ${t.outputLines} of ${t.totalLines} lines`;
335
- }
336
-
337
- if (t.truncatedBy === "bytes") {
338
- const maxBytes = t.maxBytes ?? t.outputBytes;
339
- notice += ` (${formatSize(maxBytes)} limit)`;
340
- }
341
-
342
- if (t.nextOffset != null) {
343
- notice += `. Use offset=${t.nextOffset} to continue`;
344
- }
345
- if (t.artifactId != null) {
346
- notice += `. Full: artifact://${t.artifactId}`;
347
- }
348
-
349
- parts.push(notice);
365
+ parts.push(formatTruncationMetaNotice(meta.truncation));
350
366
  }
351
367
 
352
368
  // Limit notices
@@ -377,6 +393,16 @@ export function formatOutputNotice(meta: OutputMeta | undefined): string {
377
393
  return notice + diagnosticsNotice;
378
394
  }
379
395
 
396
+ /**
397
+ * Format a styled truncation warning message.
398
+ * Returns null if no truncation metadata present.
399
+ */
400
+ export function formatStyledTruncationWarning(meta: OutputMeta | undefined, theme: Theme): string | null {
401
+ if (!meta?.truncation) return null;
402
+ const message = formatTruncationMetaNotice(meta.truncation);
403
+ return theme.fg("warning", wrapBrackets(message, theme));
404
+ }
405
+
380
406
  // =============================================================================
381
407
  // Tool wrapper
382
408
  // =============================================================================
@@ -58,15 +58,21 @@ function normalizeAtPrefix(filePath: string): string {
58
58
  return filePath;
59
59
  }
60
60
 
61
- export function expandPath(filePath: string): string {
62
- const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
63
- if (normalized === "~") {
64
- return os.homedir();
61
+ export function expandTilde(filePath: string, home?: string): string {
62
+ const h = home ?? os.homedir();
63
+ if (filePath === "~") return h;
64
+ if (filePath.startsWith("~/") || filePath.startsWith("~\\")) {
65
+ return h + filePath.slice(1);
65
66
  }
66
- if (normalized.startsWith("~/")) {
67
- return os.homedir() + normalized.slice(1);
67
+ if (filePath.startsWith("~")) {
68
+ return path.join(h, filePath.slice(1));
68
69
  }
69
- return normalized;
70
+ return filePath;
71
+ }
72
+
73
+ export function expandPath(filePath: string): string {
74
+ const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
75
+ return expandTilde(normalized);
70
76
  }
71
77
 
72
78
  /**