@oh-my-pi/pi-coding-agent 16.1.23 → 16.2.1

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 (260) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/cli.js +3785 -3695
  3. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +1 -0
  4. package/dist/types/advisor/emission-guard.d.ts +73 -0
  5. package/dist/types/advisor/index.d.ts +1 -0
  6. package/dist/types/advisor/runtime.d.ts +7 -0
  7. package/dist/types/advisor/watchdog.d.ts +13 -0
  8. package/dist/types/cli/flag-tables.d.ts +1 -0
  9. package/dist/types/cli/gallery-fixtures/search.d.ts +1 -1
  10. package/dist/types/collab/display-name.d.ts +3 -0
  11. package/dist/types/collab/host.d.ts +0 -2
  12. package/dist/types/commands/worktree.d.ts +0 -3
  13. package/dist/types/config/inline-tool-descriptors-mode.d.ts +13 -0
  14. package/dist/types/config/model-discovery.d.ts +6 -1
  15. package/dist/types/config/model-registry.d.ts +6 -2
  16. package/dist/types/config/model-resolver.d.ts +12 -0
  17. package/dist/types/config/models-config-schema.d.ts +29 -2
  18. package/dist/types/config/models-config.d.ts +22 -1
  19. package/dist/types/config/settings-schema.d.ts +91 -21
  20. package/dist/types/config/settings.d.ts +2 -0
  21. package/dist/types/dap/config.d.ts +1 -1
  22. package/dist/types/edit/hashline/filesystem.d.ts +4 -2
  23. package/dist/types/edit/renderer.d.ts +4 -0
  24. package/dist/types/extensibility/extensions/types.d.ts +17 -17
  25. package/dist/types/extensibility/hooks/types.d.ts +10 -10
  26. package/dist/types/extensibility/plugins/marketplace/types.d.ts +1 -0
  27. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +1 -0
  28. package/dist/types/internal-urls/index.d.ts +1 -0
  29. package/dist/types/internal-urls/registry-helpers.d.ts +2 -0
  30. package/dist/types/internal-urls/router.d.ts +1 -1
  31. package/dist/types/internal-urls/ssh-protocol.d.ts +10 -0
  32. package/dist/types/internal-urls/types.d.ts +19 -2
  33. package/dist/types/irc/bus.d.ts +6 -0
  34. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -0
  35. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +1 -0
  36. package/dist/types/modes/components/custom-editor.d.ts +7 -0
  37. package/dist/types/modes/components/move-overlay.d.ts +23 -0
  38. package/dist/types/modes/components/plugin-selector.d.ts +2 -1
  39. package/dist/types/modes/components/queue-mode-selector.d.ts +2 -1
  40. package/dist/types/modes/components/select-list-mouse-routing.d.ts +2 -0
  41. package/dist/types/modes/components/settings-defs.d.ts +4 -1
  42. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  43. package/dist/types/modes/components/show-images-selector.d.ts +2 -1
  44. package/dist/types/modes/components/status-line/component.d.ts +2 -0
  45. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  46. package/dist/types/modes/components/theme-selector.d.ts +2 -1
  47. package/dist/types/modes/components/thinking-selector.d.ts +2 -1
  48. package/dist/types/modes/controllers/command-controller.d.ts +10 -1
  49. package/dist/types/modes/controllers/streaming-reveal.d.ts +6 -0
  50. package/dist/types/modes/interactive-mode.d.ts +10 -1
  51. package/dist/types/modes/internal-url-autocomplete.d.ts +1 -1
  52. package/dist/types/modes/running-subagent-badge.d.ts +6 -0
  53. package/dist/types/modes/theme/theme.d.ts +1 -1
  54. package/dist/types/modes/types.d.ts +9 -1
  55. package/dist/types/sdk.d.ts +2 -2
  56. package/dist/types/session/agent-session.d.ts +8 -1
  57. package/dist/types/session/blob-store.d.ts +4 -0
  58. package/dist/types/session/session-manager.d.ts +13 -0
  59. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  60. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +1 -0
  61. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +1 -0
  62. package/dist/types/ssh/connection-manager.d.ts +2 -0
  63. package/dist/types/ssh/file-transfer.d.ts +79 -0
  64. package/dist/types/ssh/utils.d.ts +6 -0
  65. package/dist/types/system-prompt.d.ts +3 -0
  66. package/dist/types/task/executor.d.ts +16 -0
  67. package/dist/types/tiny/text.d.ts +1 -1
  68. package/dist/types/tools/builtin-names.d.ts +5 -1
  69. package/dist/types/tools/{find.d.ts → glob.d.ts} +15 -15
  70. package/dist/types/tools/{search.d.ts → grep.d.ts} +14 -14
  71. package/dist/types/tools/index.d.ts +3 -3
  72. package/dist/types/tools/path-utils.d.ts +29 -0
  73. package/dist/types/tools/read.d.ts +2 -2
  74. package/dist/types/tools/render-utils.d.ts +8 -0
  75. package/dist/types/tools/renderers.d.ts +11 -0
  76. package/dist/types/tools/ssh.d.ts +2 -0
  77. package/dist/types/tools/todo.d.ts +0 -16
  78. package/dist/types/tools/write.d.ts +2 -2
  79. package/dist/types/utils/active-repo-context.d.ts +8 -0
  80. package/dist/types/utils/image-resize.d.ts +1 -0
  81. package/dist/types/utils/markit-cache.d.ts +23 -0
  82. package/dist/types/utils/markit.d.ts +5 -1
  83. package/dist/types/utils/prompt-path.d.ts +1 -0
  84. package/dist/types/web/search/providers/duckduckgo.d.ts +14 -0
  85. package/dist/types/web/search/providers/firecrawl.d.ts +28 -0
  86. package/dist/types/web/search/providers/tinyfish.d.ts +29 -0
  87. package/dist/types/web/search/providers/xai.d.ts +13 -0
  88. package/dist/types/web/search/types.d.ts +18 -2
  89. package/package.json +30 -15
  90. package/scripts/build-binary.ts +9 -9
  91. package/scripts/bundle-dist.ts +2 -2
  92. package/src/advisor/__tests__/advisor.test.ts +61 -4
  93. package/src/advisor/__tests__/emission-guard.test.ts +147 -0
  94. package/src/advisor/advise-tool.ts +1 -1
  95. package/src/advisor/emission-guard.ts +172 -0
  96. package/src/advisor/index.ts +1 -0
  97. package/src/advisor/runtime.ts +11 -0
  98. package/src/advisor/watchdog.ts +27 -1
  99. package/src/cli/args.ts +5 -2
  100. package/src/cli/auth-broker-cli.ts +17 -0
  101. package/src/cli/config-cli.ts +4 -0
  102. package/src/cli/flag-tables.ts +7 -4
  103. package/src/cli/gallery-cli.ts +14 -2
  104. package/src/cli/gallery-fixtures/edit.ts +60 -0
  105. package/src/cli/gallery-fixtures/fs.ts +17 -17
  106. package/src/cli/gallery-fixtures/search.ts +4 -4
  107. package/src/cli/usage-cli.ts +20 -1
  108. package/src/cli/web-search-cli.ts +1 -1
  109. package/src/collab/display-name.ts +13 -0
  110. package/src/collab/guest.ts +5 -1
  111. package/src/collab/host.ts +2 -13
  112. package/src/commands/worktree.ts +6 -0
  113. package/src/config/inline-tool-descriptors-mode.ts +27 -0
  114. package/src/config/model-discovery.ts +77 -8
  115. package/src/config/model-registry.ts +89 -11
  116. package/src/config/model-resolver.ts +39 -1
  117. package/src/config/models-config-schema.ts +41 -5
  118. package/src/config/models-config.ts +4 -1
  119. package/src/config/settings-schema.ts +85 -27
  120. package/src/config/settings.ts +172 -1
  121. package/src/cursor.ts +1 -1
  122. package/src/dap/config.ts +152 -8
  123. package/src/dap/session.ts +1 -1
  124. package/src/discovery/claude-plugins.ts +3 -2
  125. package/src/discovery/helpers.ts +3 -1
  126. package/src/edit/hashline/diff.ts +14 -3
  127. package/src/edit/hashline/execute.ts +42 -6
  128. package/src/edit/hashline/filesystem.ts +38 -2
  129. package/src/edit/index.ts +1 -0
  130. package/src/edit/modes/patch.ts +11 -1
  131. package/src/edit/renderer.ts +140 -13
  132. package/src/eval/__tests__/agent-bridge.test.ts +101 -0
  133. package/src/eval/agent-bridge.ts +26 -3
  134. package/src/eval/js/tool-bridge.ts +2 -2
  135. package/src/export/html/index.ts +1 -1
  136. package/src/export/html/template.js +3 -1
  137. package/src/export/html/tool-views.generated.js +20 -20
  138. package/src/extensibility/extensions/types.ts +22 -22
  139. package/src/extensibility/hooks/types.ts +11 -11
  140. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +8 -3
  141. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +25 -10
  142. package/src/extensibility/plugins/marketplace/manager.ts +22 -0
  143. package/src/extensibility/plugins/marketplace/types.ts +1 -0
  144. package/src/internal-urls/__tests__/ssh-protocol.test.ts +331 -0
  145. package/src/internal-urls/docs-index.generated.txt +2 -2
  146. package/src/internal-urls/docs-index.ts +2 -3
  147. package/src/internal-urls/index.ts +1 -0
  148. package/src/internal-urls/registry-helpers.ts +19 -4
  149. package/src/internal-urls/router.ts +5 -3
  150. package/src/internal-urls/ssh-protocol.ts +367 -0
  151. package/src/internal-urls/types.ts +19 -2
  152. package/src/irc/bus.ts +11 -3
  153. package/src/mcp/tool-bridge.ts +32 -2
  154. package/src/memories/index.ts +1 -1
  155. package/src/modes/acp/acp-agent.ts +14 -1
  156. package/src/modes/acp/acp-event-mapper.ts +91 -27
  157. package/src/modes/components/__tests__/move-overlay.test.ts +166 -0
  158. package/src/modes/components/agent-hub.ts +3 -0
  159. package/src/modes/components/agent-transcript-viewer.ts +9 -7
  160. package/src/modes/components/custom-editor.ts +90 -5
  161. package/src/modes/components/move-overlay.ts +282 -0
  162. package/src/modes/components/plan-review-overlay.ts +35 -35
  163. package/src/modes/components/plugin-selector.ts +6 -1
  164. package/src/modes/components/queue-mode-selector.ts +6 -1
  165. package/src/modes/components/select-list-mouse-routing.ts +35 -0
  166. package/src/modes/components/session-selector.ts +11 -10
  167. package/src/modes/components/settings-defs.ts +14 -1
  168. package/src/modes/components/settings-selector.ts +196 -29
  169. package/src/modes/components/show-images-selector.ts +6 -1
  170. package/src/modes/components/status-line/component.ts +108 -28
  171. package/src/modes/components/status-line/segments.ts +5 -1
  172. package/src/modes/components/status-line/types.ts +2 -0
  173. package/src/modes/components/theme-selector.ts +6 -1
  174. package/src/modes/components/thinking-selector.ts +6 -1
  175. package/src/modes/components/tool-execution.ts +25 -9
  176. package/src/modes/components/tree-selector.ts +5 -5
  177. package/src/modes/controllers/command-controller.ts +140 -47
  178. package/src/modes/controllers/event-controller.ts +59 -3
  179. package/src/modes/controllers/input-controller.ts +68 -2
  180. package/src/modes/controllers/selector-controller.ts +5 -2
  181. package/src/modes/controllers/streaming-reveal.ts +17 -0
  182. package/src/modes/controllers/tool-args-reveal.ts +1 -1
  183. package/src/modes/interactive-mode.ts +116 -50
  184. package/src/modes/internal-url-autocomplete.ts +17 -2
  185. package/src/modes/prompt-action-autocomplete.ts +3 -1
  186. package/src/modes/running-subagent-badge.ts +13 -0
  187. package/src/modes/setup-wizard/scenes/glyph.ts +2 -13
  188. package/src/modes/setup-wizard/scenes/providers.ts +2 -1
  189. package/src/modes/setup-wizard/scenes/theme.ts +6 -12
  190. package/src/modes/setup-wizard/scenes/web-search.ts +8 -13
  191. package/src/modes/setup-wizard/wizard-overlay.ts +6 -5
  192. package/src/modes/theme/theme.ts +9 -1
  193. package/src/modes/types.ts +9 -1
  194. package/src/modes/utils/interactive-context-helpers.ts +1 -1
  195. package/src/priority.json +15 -0
  196. package/src/prompts/advisor/active-repo-watchdog.md +6 -0
  197. package/src/prompts/advisor/context-files.md +8 -0
  198. package/src/prompts/advisor/system.md +21 -7
  199. package/src/prompts/agents/designer.md +1 -1
  200. package/src/prompts/agents/explore.md +1 -1
  201. package/src/prompts/agents/librarian.md +2 -2
  202. package/src/prompts/agents/plan.md +2 -2
  203. package/src/prompts/agents/reviewer.md +1 -1
  204. package/src/prompts/agents/task.md +2 -2
  205. package/src/prompts/system/active-repo-context.md +4 -0
  206. package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
  207. package/src/prompts/system/plan-mode-active.md +3 -3
  208. package/src/prompts/system/project-prompt.md +2 -2
  209. package/src/prompts/system/system-prompt.md +6 -6
  210. package/src/prompts/tools/bash.md +2 -2
  211. package/src/prompts/tools/checkpoint.md +1 -1
  212. package/src/prompts/tools/{find.md → glob.md} +1 -1
  213. package/src/prompts/tools/{search.md → grep.md} +3 -3
  214. package/src/prompts/tools/read.md +4 -2
  215. package/src/sdk.ts +65 -25
  216. package/src/session/agent-session.ts +519 -132
  217. package/src/session/blob-store.ts +24 -0
  218. package/src/session/session-history-format.ts +2 -2
  219. package/src/session/session-manager.ts +46 -0
  220. package/src/slash-commands/builtin-registry.ts +138 -20
  221. package/src/slash-commands/helpers/usage-report.ts +23 -2
  222. package/src/ssh/__tests__/connection-manager-args.test.ts +69 -0
  223. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +68 -0
  224. package/src/ssh/connection-manager.ts +12 -15
  225. package/src/ssh/file-transfer.ts +209 -0
  226. package/src/ssh/utils.ts +24 -0
  227. package/src/system-prompt.ts +56 -28
  228. package/src/task/executor.ts +96 -36
  229. package/src/task/index.ts +3 -3
  230. package/src/task/render.ts +14 -13
  231. package/src/task/worktree.ts +38 -1
  232. package/src/tiny/text.ts +49 -4
  233. package/src/tiny/worker.ts +3 -3
  234. package/src/tools/bash.ts +2 -2
  235. package/src/tools/builtin-names.ts +26 -2
  236. package/src/tools/{find.ts → glob.ts} +48 -42
  237. package/src/tools/{search.ts → grep.ts} +311 -129
  238. package/src/tools/index.ts +13 -14
  239. package/src/tools/irc.ts +6 -2
  240. package/src/tools/path-utils.ts +64 -1
  241. package/src/tools/read.ts +35 -6
  242. package/src/tools/render-utils.ts +14 -0
  243. package/src/tools/renderers.ts +15 -4
  244. package/src/tools/ssh.ts +17 -2
  245. package/src/tools/todo.ts +4 -26
  246. package/src/tools/write.ts +25 -9
  247. package/src/utils/active-repo-context.ts +143 -0
  248. package/src/utils/image-resize.ts +88 -7
  249. package/src/utils/lang-from-path.ts +3 -3
  250. package/src/utils/markit-cache.ts +166 -0
  251. package/src/utils/markit.ts +86 -18
  252. package/src/utils/prompt-path.ts +3 -0
  253. package/src/utils/title-generator.ts +1 -1
  254. package/src/web/search/provider.ts +54 -34
  255. package/src/web/search/providers/duckduckgo.ts +140 -0
  256. package/src/web/search/providers/firecrawl.ts +144 -0
  257. package/src/web/search/providers/tinyfish.ts +159 -0
  258. package/src/web/search/providers/xai.ts +292 -0
  259. package/src/web/search/providers/zai.ts +142 -56
  260. package/src/web/search/types.ts +6 -2
@@ -2,8 +2,7 @@
2
2
  * Harness documentation index for the `omp://` protocol.
3
3
  *
4
4
  * Compiled binaries and the prepacked npm bundle inline a compressed index from
5
- * `docs-index.generated.txt` (populated by `scripts/generate-docs-index.ts
6
- * --generate` at build time). The format is two lines:
5
+ * `docs-index.generated.txt` (populated by `gen:docs` at build time). The format is two lines:
7
6
  * 1. a plain JSON array of the sorted doc file names, and
8
7
  * 2. a base64 gzip blob of the index-aligned doc bodies (`string[]`).
9
8
  * Listing/completion (`getDocFilenames`) parses only the small first line and
@@ -84,7 +83,7 @@ function getIndex(): DocsIndex {
84
83
  if (decoded === null) {
85
84
  throw new Error(
86
85
  "Malformed embedded docs index (docs-index.generated.txt): non-empty payload without a newline separator. " +
87
- "Rebuild with `bun --cwd=packages/coding-agent scripts/generate-docs-index.ts --generate`.",
86
+ "Rebuild with `bun run gen:docs`.",
88
87
  );
89
88
  }
90
89
  index = decoded;
@@ -21,5 +21,6 @@ export * from "./parse";
21
21
  export * from "./router";
22
22
  export * from "./rule-protocol";
23
23
  export * from "./skill-protocol";
24
+ export * from "./ssh-protocol";
24
25
  export type * from "./types";
25
26
  export * from "./vault-protocol";
@@ -4,6 +4,19 @@
4
4
  */
5
5
  import { AgentRegistry } from "../registry/agent-registry";
6
6
 
7
+ const extraArtifactsDirs = new Set<string>();
8
+
9
+ export function registerArtifactsDir(dir: string): () => void {
10
+ extraArtifactsDirs.add(dir);
11
+ return () => {
12
+ extraArtifactsDirs.delete(dir);
13
+ };
14
+ }
15
+
16
+ export function resetRegisteredArtifactDirsForTests(): void {
17
+ extraArtifactsDirs.clear();
18
+ }
19
+
7
20
  /**
8
21
  * Snapshot of artifacts dirs for every registered session, deduped.
9
22
  *
@@ -15,11 +28,13 @@ import { AgentRegistry } from "../registry/agent-registry";
15
28
  */
16
29
  export function artifactsDirsFromRegistry(): string[] {
17
30
  const dirs: string[] = [];
18
- for (const ref of AgentRegistry.global().list()) {
19
- const dir =
20
- ref.session?.sessionManager.getArtifactsDir() ?? (ref.sessionFile ? ref.sessionFile.slice(0, -6) : null);
21
- if (!dir) continue;
31
+ const addDir = (dir: string | null | undefined) => {
32
+ if (!dir) return;
22
33
  if (!dirs.includes(dir)) dirs.push(dir);
34
+ };
35
+ for (const ref of AgentRegistry.global().list()) {
36
+ addDir(ref.session?.sessionManager.getArtifactsDir() ?? (ref.sessionFile ? ref.sessionFile.slice(0, -6) : null));
23
37
  }
38
+ for (const dir of extraArtifactsDirs) addDir(dir);
24
39
  return dirs;
25
40
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Internal URL router for internal protocols (`agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, and `vault://`).
2
+ * Internal URL router for internal protocols (`agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, `ssh://`, and `vault://`).
3
3
  *
4
4
  * One process-global router with one handler per scheme. Access via
5
5
  * `InternalUrlRouter.instance()`. Handlers are stateless; per-session and
@@ -16,6 +16,7 @@ import { OmpProtocolHandler } from "./omp-protocol";
16
16
  import { parseInternalUrl } from "./parse";
17
17
  import { RuleProtocolHandler } from "./rule-protocol";
18
18
  import { SkillProtocolHandler } from "./skill-protocol";
19
+ import { SshProtocolHandler } from "./ssh-protocol";
19
20
  import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, UrlCompletion } from "./types";
20
21
  import { VaultProtocolHandler } from "./vault-protocol";
21
22
 
@@ -37,6 +38,7 @@ export class InternalUrlRouter {
37
38
  this.register(new IssueProtocolHandler());
38
39
  this.register(new PrProtocolHandler());
39
40
  this.register(new HistoryProtocolHandler());
41
+ this.register(new SshProtocolHandler());
40
42
  }
41
43
 
42
44
  /** Process-global router instance. */
@@ -81,10 +83,10 @@ export class InternalUrlRouter {
81
83
  * Candidate completions for the host/path portion of `scheme://<query>`.
82
84
  * Returns `null` when the scheme is unknown or does not support completion.
83
85
  */
84
- async complete(scheme: string, query: string): Promise<UrlCompletion[] | null> {
86
+ async complete(scheme: string, query: string, context?: ResolveContext): Promise<UrlCompletion[] | null> {
85
87
  const handler = this.#handlers.get(scheme.toLowerCase());
86
88
  if (!handler?.complete) return null;
87
- return handler.complete(query);
89
+ return handler.complete(query, context);
88
90
  }
89
91
 
90
92
  async resolve(input: string, context?: ResolveContext): Promise<InternalResource> {
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Protocol handler for `ssh://host/path` URLs.
3
+ *
4
+ * Resolves a remote text file or directory listing on a pre-configured SSH host — or any
5
+ * destination OpenSSH can resolve itself (e.g. a `~/.ssh/config` alias) — for
6
+ * the read, search, and write tools, reusing the shared ControlMaster
7
+ * connection in `../ssh/connection-manager`.
8
+ *
9
+ * A remote path resolves to a UTF-8 text file (≤ 1 MiB) or, when it is a
10
+ * directory, a one-level listing. Binary/non-UTF-8 or oversized files are
11
+ * rejected with an explicit error. This handler exposes no `sourcePath`;
12
+ * directory listings carry `isDirectory` so `search` refuses to grep the
13
+ * listing text instead of the directory's real contents.
14
+ *
15
+ * `loadCapability` is imported from `../capability` (not the `../discovery`
16
+ * barrel) on purpose — pulling the barrel here would route
17
+ * `path-utils -> internal-urls -> ssh-protocol -> discovery -> path-utils` and
18
+ * eager-load every provider on any `path-utils` import. Runtime bootstraps the
19
+ * SSH provider via `import "./discovery"` (sdk.ts) / `initializeWithSettings`
20
+ * (main.ts) before any tool resolves.
21
+ */
22
+ import * as capability from "../capability";
23
+ import { type SSHHost, sshCapability } from "../capability/ssh";
24
+ import type { SSHConnectionTarget } from "../ssh/connection-manager";
25
+ import {
26
+ listRemoteDir,
27
+ type RemoteDirEntry,
28
+ type RemotePathKind,
29
+ readRemoteFile,
30
+ statRemotePath,
31
+ writeRemoteFile,
32
+ } from "../ssh/file-transfer";
33
+ import type {
34
+ InternalResource,
35
+ InternalUrl,
36
+ ProtocolHandler,
37
+ ResolveContext,
38
+ UrlCompletion,
39
+ WriteContext,
40
+ } from "./types";
41
+
42
+ /** Largest remote text file `ssh://` will materialize (mirrors the local:// cap). */
43
+ const SSH_TEXT_MAX_BYTES = 1024 * 1024;
44
+
45
+ /** POSIX-aware content type from the last path segment's extension. */
46
+ function contentTypeFor(remotePath: string): InternalResource["contentType"] {
47
+ const slash = remotePath.lastIndexOf("/");
48
+ const base = slash === -1 ? remotePath : remotePath.slice(slash + 1);
49
+ const dot = base.lastIndexOf(".");
50
+ const ext = dot <= 0 ? "" : base.slice(dot).toLowerCase();
51
+ if (ext === ".md") return "text/markdown";
52
+ if (ext === ".json") return "application/json";
53
+ return "text/plain";
54
+ }
55
+
56
+ /** Decode the whole buffer as UTF-8 text, or null if it holds a NUL or invalid byte. */
57
+ function decodeUtf8Text(bytes: Uint8Array): string | null {
58
+ if (bytes.indexOf(0) !== -1) return null;
59
+ try {
60
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Remote absolute path from the URL. Uses `rawPathname` (pre-normalization) so
68
+ * `..`/`//` and percent-escapes survive verbatim to the remote shell; the
69
+ * authority (host/user/port) stays on the WHATWG fields, which preserve case for
70
+ * the non-special `ssh` scheme.
71
+ */
72
+ function remotePathFromUrl(url: InternalUrl): string {
73
+ // `?`/`#` are URL delimiters, so parseInternalUrl strips them from the path
74
+ // (`ssh://h/tmp/a?draft` → `/tmp/a`). Reject the unsupported suffix instead of
75
+ // silently operating on the truncated path; a literal `?`/`#` in a filename
76
+ // must be percent-encoded (`%3F`/`%23`).
77
+ if (url.search) {
78
+ throw new Error(
79
+ `ssh:// does not support URL query strings; percent-encode a literal '?' as %3F in the path: ${url.href}`,
80
+ );
81
+ }
82
+ if (url.hash) {
83
+ throw new Error(
84
+ `ssh:// does not support URL fragments; percent-encode a literal '#' as %23 in the path: ${url.href}`,
85
+ );
86
+ }
87
+ const raw = url.rawPathname ?? url.pathname;
88
+ let decoded: string;
89
+ try {
90
+ decoded = decodeURIComponent(raw);
91
+ } catch {
92
+ throw new Error(`Invalid URL encoding in ssh:// path: ${url.href}`);
93
+ }
94
+ if (!decoded) {
95
+ throw new Error(
96
+ "ssh:// requires an absolute path, e.g. ssh://host/etc/hosts or ssh://host/ for the root directory",
97
+ );
98
+ }
99
+ return decoded;
100
+ }
101
+
102
+ /** Load the configured SSH hosts from the `ssh` capability (managed/project `ssh.json`). */
103
+ async function loadConfiguredHosts(cwd?: string): Promise<SSHHost[]> {
104
+ const { items } = await capability.loadCapability<SSHHost>(sshCapability.id, cwd ? { cwd } : {});
105
+ return items;
106
+ }
107
+
108
+ /** One-line address for a host, e.g. `deploy@10.0.0.1:2222`. */
109
+ function hostAddress(host: SSHHost): string {
110
+ return `${host.username ? `${host.username}@` : ""}${host.host}${host.port ? `:${host.port}` : ""}`;
111
+ }
112
+
113
+ /** Render the configured-host index for a bare `ssh://` read (markdown with per-host links). */
114
+ function formatHostIndex(hosts: readonly SSHHost[]): string {
115
+ if (hosts.length === 0) {
116
+ return "# SSH hosts\n\nNo SSH hosts are configured. Add hosts to an `ssh.json` capability file, or read `ssh://<host>/<path>` with any destination OpenSSH can resolve (e.g. a `~/.ssh/config` alias).\n";
117
+ }
118
+ const lines = hosts.map(host => {
119
+ const addr = hostAddress(host);
120
+ const suffix = addr === host.name ? "" : ` — \`${addr}\``;
121
+ const desc = host.description ? ` (${host.description})` : "";
122
+ return `- [${host.name}](ssh://${encodeURIComponent(host.name)}/)${suffix}${desc}`;
123
+ });
124
+ return `# SSH hosts\n\n${hosts.length} configured host${hosts.length === 1 ? "" : "s"}:\n\n${lines.join("\n")}\n`;
125
+ }
126
+
127
+ /**
128
+ * Resolve the URL authority to an SSH connection target. With no explicit
129
+ * user/port, the full DECODED authority (`url.rawHost`) is matched against a
130
+ * configured host name, so percent-encoded reserved-char aliases (e.g.
131
+ * `alice%40prod` → `alice@prod`) resolve correctly. A literal `user@`/`:port`
132
+ * in the URL is an override: it is rejected on a configured bare name (the
133
+ * ControlMaster/host-info caches key on `name` alone) and otherwise treated as
134
+ * an opaque OpenSSH destination so plain `~/.ssh/config` aliases work.
135
+ */
136
+ async function resolveTarget(url: InternalUrl, cwd?: string): Promise<SSHConnectionTarget> {
137
+ // `parseInternalUrl` falls back to a lenient regex parse when WHATWG `new URL`
138
+ // rejects the input. For ssh:// that only happens on a malformed authority — an
139
+ // invalid or out-of-range port (`prod:abc`, `host:65536`) or a bad IPv6 literal —
140
+ // which would otherwise be mis-read as an opaque host and silently connect to the
141
+ // default port. Reject it before resolving.
142
+ if (!URL.canParse(url.href)) {
143
+ throw new Error(`ssh://: invalid host or port in "${url.href}"; use ssh://host[:1-65535]/<absolute-path>`);
144
+ }
145
+ // WHATWG `hostname` is bracketed only for a *valid* IPv6 literal, so a bracketed
146
+ // host is unambiguously IPv6 — hand OpenSSH the bare address. Percent-encoded
147
+ // bracketed aliases (e.g. `%5Bprod%3A2222%5D`) keep their literal brackets in the
148
+ // decoded `rawHost`, so they are matched and forwarded verbatim, never stripped.
149
+ const bareHost = url.hostname;
150
+ const rawAuthority = url.rawHost || bareHost;
151
+ if (!bareHost && !rawAuthority) {
152
+ throw new Error("ssh:// requires a host: ssh://<host>/<absolute-path>");
153
+ }
154
+ // `decodeOr` fails open, so a malformed percent-escape (`%ZZ`) in the authority
155
+ // would otherwise pass the canonical check below and reach OpenSSH literally.
156
+ // Reject it up front — the path decoder fails closed for the same bad escapes.
157
+ for (const part of [url.username, bareHost]) {
158
+ if (part.includes("%")) {
159
+ try {
160
+ decodeURIComponent(part);
161
+ } catch {
162
+ throw new Error(`ssh://: invalid percent-escape in authority "${url.href}"`);
163
+ }
164
+ }
165
+ }
166
+ if (url.password) {
167
+ throw new Error(
168
+ "ssh://: password authentication is not supported; ssh:// uses key/agent auth — drop the ':<password>' from the URL",
169
+ );
170
+ }
171
+ const isIpv6Literal = bareHost.startsWith("[") && bareHost.endsWith("]");
172
+ const sshHost = isIpv6Literal ? bareHost.slice(1, -1) : bareHost;
173
+ const username = url.username || undefined;
174
+ const port = url.port ? Number(url.port) : undefined;
175
+ if (port === 0) {
176
+ throw new Error("ssh://: port 0 is not a valid SSH port; use ssh://host:<1-65535>/<path> or omit the port");
177
+ }
178
+ // An empty port (`ssh://prod:/path`, `ssh://user@host:/path`, including
179
+ // percent-encoded authority parts) parses cleanly with `url.port === ""`, so it
180
+ // slips past the malformed-authority guard and would be read as "no port" —
181
+ // silently using the default/configured target. `url.rawHost` is the decoded
182
+ // authority and uniquely retains the trailing `:`; comparing it to the decoded
183
+ // host (+ user) catches the empty port, while a percent-encoded alias like
184
+ // `prod%3A` (whose decoded host already ends in `:`) reconstructs to `prod::`
185
+ // and is left alone.
186
+ const decodeOr = (s: string): string => {
187
+ try {
188
+ return decodeURIComponent(s);
189
+ } catch {
190
+ return s;
191
+ }
192
+ };
193
+ if (port === undefined && url.rawHost === `${username ? `${decodeOr(username)}@` : ""}${decodeOr(bareHost)}:`) {
194
+ throw new Error(`ssh://: empty port in "${url.href}"; use ssh://host:<1-65535>/<path> or drop the colon`);
195
+ }
196
+ // A literal but empty userinfo (`ssh://@host`) sets username to "" — WHATWG drops
197
+ // the `@` from hostname, but rawHost keeps the leading `@`. A percent-encoded
198
+ // alias like `%40prod` decodes to `@prod` in rawHost too, but its hostname keeps
199
+ // `%40`, so the reconstruction is `@@prod` and is left alone.
200
+ if (username === undefined && url.rawHost === `@${decodeOr(bareHost)}${port !== undefined ? `:${port}` : ""}`) {
201
+ throw new Error(`ssh://: empty username in "${url.href}"; drop the leading '@' or provide a username before it`);
202
+ }
203
+ // Backstop for any remaining stray/empty authority marker the explicit checks
204
+ // above do not name — notably an empty password (`ssh://user:@host`, `ssh://:@host`,
205
+ // where `url.password === ""`). `rawHost` keeps the literal marker, so it differs
206
+ // from the canonical decoded `[user@]host[:port]` WHATWG actually parsed. Every
207
+ // valid authority — including percent-encoded reserved-char aliases — reconstructs
208
+ // to exactly `rawHost`, so only malformed userinfo trips this.
209
+ const canonicalAuthority = `${url.username ? `${decodeOr(url.username)}@` : ""}${decodeOr(bareHost)}${port !== undefined ? `:${port}` : ""}`;
210
+ if (url.rawHost !== canonicalAuthority) {
211
+ throw new Error(
212
+ `ssh://: unsupported or malformed authority in "${url.href}"; use ssh://[user@]host[:1-65535]/<absolute-path>`,
213
+ );
214
+ }
215
+ const items = await loadConfiguredHosts(cwd);
216
+
217
+ // A literal user/port in the URL is an authority override. A configured alias
218
+ // is addressed only by its (percent-encoded) name, never with a separate
219
+ // user/port — so reject an override on a configured bare name, else opaque.
220
+ if (username || port !== undefined) {
221
+ const decodedBareHost = decodeOr(bareHost);
222
+ if (items.some(entry => entry.name === bareHost || entry.name === decodedBareHost)) {
223
+ throw new Error(
224
+ `ssh://: user/port overrides are not allowed for the configured host "${decodedBareHost}"; use ssh://${bareHost}/<path> or an unconfigured hostname`,
225
+ );
226
+ }
227
+ const sshUser = username ? decodeOr(username) : undefined;
228
+ const sshTargetHost = decodeOr(sshHost);
229
+ const name = `${sshUser ? `${sshUser}@` : ""}${sshTargetHost}${port !== undefined ? `:${port}` : ""}`;
230
+ return { name, host: sshTargetHost, username: sshUser, port };
231
+ }
232
+
233
+ // No explicit user/port: match the full decoded authority against a
234
+ // configured name (so an encoded reserved-char alias resolves correctly).
235
+ const match = items.find(entry => entry.name === rawAuthority) ?? items.find(entry => entry.name === bareHost);
236
+ if (match) {
237
+ return {
238
+ name: match.name,
239
+ host: match.host,
240
+ username: match.username,
241
+ port: match.port,
242
+ keyPath: match.keyPath,
243
+ compat: match.compat,
244
+ };
245
+ }
246
+ // Opaque OpenSSH destination (plain ~/.ssh/config alias, or any resolvable host).
247
+ return { name: rawAuthority, host: isIpv6Literal ? sshHost : rawAuthority };
248
+ }
249
+
250
+ /** Format a one-level remote directory listing — mirrors buildDirectoryResource's plain `name/` lines. */
251
+ function formatDirListing(entries: readonly RemoteDirEntry[]): string {
252
+ if (entries.length === 0) return "(empty directory)";
253
+ return entries.map(entry => `${entry.name}${entry.isDirectory ? "/" : ""}`).join("\n");
254
+ }
255
+
256
+ export class SshProtocolHandler implements ProtocolHandler {
257
+ readonly scheme = "ssh";
258
+ readonly immutable = false;
259
+
260
+ async resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
261
+ // Bare `ssh://` (or `ssh:///`) with no host lists the configured hosts. A
262
+ // host-less URL that still carries a path (`ssh:///etc/hosts`) is malformed —
263
+ // reject it instead of silently dropping the path and listing hosts.
264
+ if (!(url.rawHost || url.hostname)) {
265
+ const rawPath = url.rawPathname ?? url.pathname;
266
+ if (rawPath && rawPath !== "/") {
267
+ throw new Error(
268
+ `ssh:// requires a host before the path: ssh://<host>${rawPath} (host-less ssh://${rawPath} is not valid)`,
269
+ );
270
+ }
271
+ return this.#resolveHostIndex(url, context?.cwd);
272
+ }
273
+ const target = await resolveTarget(url, context?.cwd);
274
+ const remotePath = remotePathFromUrl(url);
275
+ // Classify before reading. A FIFO with no writer would block `head` until the
276
+ // timeout, and a device (e.g. /dev/zero) would stream the whole probe, so a
277
+ // special file must fail fast. Only a regular file is read; a directory lists.
278
+ // `missing`/stat-failure falls through to the read so its original remote stderr
279
+ // (e.g. "No such file or directory") still surfaces.
280
+ let kind: RemotePathKind | undefined;
281
+ try {
282
+ kind = await statRemotePath(target, remotePath, { signal: context?.signal });
283
+ } catch {
284
+ // stat failed (host/connection issue) — fall through; the read gives a clearer error.
285
+ }
286
+ if (kind === "directory") {
287
+ return this.#resolveDirectory(target, remotePath, url, context?.signal, context?.skipDirectoryListing);
288
+ }
289
+ if (kind === "other") {
290
+ throw new Error(
291
+ `ssh://: ${remotePath} is not a regular file (FIFO, socket, or device); ssh:// reads UTF-8 text files only — use the ssh tool for special files`,
292
+ );
293
+ }
294
+ const fileResult = await readRemoteFile(target, remotePath, {
295
+ maxBytes: SSH_TEXT_MAX_BYTES,
296
+ signal: context?.signal,
297
+ });
298
+ if (fileResult.truncated) {
299
+ throw new Error(
300
+ `ssh://: ${remotePath} exceeds the 1 MiB limit; ssh:// supports text files up to 1 MiB — use an sshfs mount for larger files`,
301
+ );
302
+ }
303
+ const content = decodeUtf8Text(fileResult.bytes);
304
+ if (content === null) {
305
+ throw new Error(
306
+ `ssh://: ${remotePath} is a binary or non-UTF-8 file; ssh:// supports UTF-8 text only — use the ssh tool or an sshfs mount`,
307
+ );
308
+ }
309
+ // No `sourcePath`: keeps search on the virtual-resource path so the
310
+ // displayed/searched resource stays `ssh://…` instead of a temp path.
311
+ return {
312
+ url: url.href,
313
+ content,
314
+ contentType: contentTypeFor(remotePath),
315
+ size: fileResult.bytes.length,
316
+ };
317
+ }
318
+
319
+ /** Resolve a remote directory to a one-level listing (no `sourcePath`; `isDirectory` so search refuses it; immutable). */
320
+ async #resolveDirectory(
321
+ target: SSHConnectionTarget,
322
+ remotePath: string,
323
+ url: InternalUrl,
324
+ signal?: AbortSignal,
325
+ skipListing?: boolean,
326
+ ): Promise<InternalResource> {
327
+ // `search`/`find` reject an ssh:// directory outright, so they pass `skipListing`
328
+ // to avoid draining a full remote `ls` we would only discard.
329
+ const content = skipListing ? "" : formatDirListing(await listRemoteDir(target, remotePath, { signal }));
330
+ return {
331
+ url: url.href,
332
+ content,
333
+ contentType: "text/plain",
334
+ size: Buffer.byteLength(content, "utf-8"),
335
+ immutable: true,
336
+ isDirectory: true,
337
+ };
338
+ }
339
+
340
+ /** Resolve a bare `ssh://` to a listing of configured hosts (immutable; plain virtual text, so `search` can still grep host names). */
341
+ async #resolveHostIndex(url: InternalUrl, cwd?: string): Promise<InternalResource> {
342
+ const content = formatHostIndex(await loadConfiguredHosts(cwd));
343
+ return {
344
+ url: url.href,
345
+ content,
346
+ contentType: "text/markdown",
347
+ size: Buffer.byteLength(content, "utf-8"),
348
+ immutable: true,
349
+ };
350
+ }
351
+
352
+ /** Autocomplete the host segment of `ssh://` with the configured SSH hosts. */
353
+ async complete(_query?: string, context?: ResolveContext): Promise<UrlCompletion[]> {
354
+ const hosts = await loadConfiguredHosts(context?.cwd);
355
+ return hosts.map(host => ({
356
+ value: encodeURIComponent(host.name),
357
+ label: host.name,
358
+ description: host.description ?? hostAddress(host),
359
+ }));
360
+ }
361
+
362
+ async write(url: InternalUrl, content: string, context?: WriteContext): Promise<void> {
363
+ const target = await resolveTarget(url, context?.cwd);
364
+ const remotePath = remotePathFromUrl(url);
365
+ await writeRemoteFile(target, remotePath, new TextEncoder().encode(content), { signal: context?.signal });
366
+ }
367
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Types for the internal URL routing system.
3
3
  *
4
- * Internal URLs (`agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, and `vault://`) are resolved by tools like read,
4
+ * Internal URLs (`agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, `ssh://`, and `vault://`) are resolved by tools like read,
5
5
  * providing access to agent outputs and server resources without exposing filesystem paths.
6
6
  */
7
7
 
@@ -33,6 +33,13 @@ export interface InternalResource {
33
33
  * resources. Mutable resources (e.g. local://) behave like editable files.
34
34
  */
35
35
  immutable?: boolean;
36
+ /**
37
+ * True when the resource is a directory listing rather than file content.
38
+ * `search` refuses to grep such a resource when it has no `sourcePath` — a
39
+ * remote `ssh://` listing has no local path to recurse, so its listing text
40
+ * must never be mistaken for the directory's contents.
41
+ */
42
+ isDirectory?: boolean;
36
43
  }
37
44
 
38
45
  /**
@@ -93,6 +100,13 @@ export interface ResolveContext {
93
100
  localProtocolOptions?: LocalProtocolOptions;
94
101
  /** Calling session's loaded skills. Prefer this over process-global skill state. */
95
102
  skills?: readonly Skill[];
103
+ /**
104
+ * When set, handlers that would otherwise materialize an expensive directory
105
+ * listing (e.g. the ssh:// handler draining a full remote `ls`) instead return
106
+ * the directory shape (`isDirectory: true`) with empty content. `search`/`find`
107
+ * reject directory resources, so they never need the listing.
108
+ */
109
+ skipDirectoryListing?: boolean;
96
110
  }
97
111
 
98
112
  /**
@@ -150,6 +164,9 @@ export interface ProtocolHandler {
150
164
  * mcp://) omit it. The caller fuzzy-filters the returned set against the
151
165
  * partially typed `query`, so handlers return their full (bounded) candidate
152
166
  * list; `query` is provided only so handlers can scope expensive enumeration.
167
+ * `context.cwd`/`context.localProtocolOptions` carry the caller's working dir
168
+ * and session, for handlers whose candidates are project- or session-scoped
169
+ * (e.g. ssh:// hosts from a project `ssh.json`, local:// roots per session).
153
170
  */
154
- complete?(query: string): Promise<UrlCompletion[]>;
171
+ complete?(query?: string, context?: ResolveContext): Promise<UrlCompletion[]>;
155
172
  }
package/src/irc/bus.ts CHANGED
@@ -91,8 +91,16 @@ export class IrcBus {
91
91
  * disabled — e.g. blocked in a synchronous task spawn awaiting the
92
92
  * sender's own batch) can generate an ephemeral side-channel auto-reply
93
93
  * instead of stranding the sender until timeout.
94
+ *
95
+ * `opts.suppressRelay` skips the display-only main-UI relay for this leg.
96
+ * Set by broadcast fan-out when the same broadcast also targets the main
97
+ * agent directly: the main agent then already sees the body as its own
98
+ * incoming card, so relaying the sibling legs would duplicate it.
94
99
  */
95
- async send(msg: Omit<IrcMessage, "id" | "ts">, opts?: { expectsReply?: boolean }): Promise<IrcDeliveryReceipt> {
100
+ async send(
101
+ msg: Omit<IrcMessage, "id" | "ts">,
102
+ opts?: { expectsReply?: boolean; suppressRelay?: boolean },
103
+ ): Promise<IrcDeliveryReceipt> {
96
104
  const message: IrcMessage = { ...msg, id: Snowflake.next(), ts: Date.now() };
97
105
  const ref = this.#registry.get(message.to);
98
106
  if (!ref || ref.status === "aborted") {
@@ -127,7 +135,7 @@ export class IrcBus {
127
135
  const waiter = this.#takeMatchingWaiter(message.to, message.from);
128
136
  if (waiter) {
129
137
  waiter.resolve(message);
130
- this.#relayToMainUi(message);
138
+ if (!opts?.suppressRelay) this.#relayToMainUi(message);
131
139
  return { to: message.to, outcome: revived ? "revived" : "injected" };
132
140
  }
133
141
 
@@ -138,7 +146,7 @@ export class IrcBus {
138
146
 
139
147
  try {
140
148
  const delivery = await session.deliverIrcMessage(message, opts);
141
- this.#relayToMainUi(message);
149
+ if (!opts?.suppressRelay) this.#relayToMainUi(message);
142
150
  return { to: message.to, outcome: revived ? "revived" : delivery };
143
151
  } catch (error) {
144
152
  // Live hand-off failed (e.g. recipient disposed mid-shutdown): buffer
@@ -7,6 +7,7 @@ import type { AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
7
7
  import type { TSchema } from "@oh-my-pi/pi-ai";
8
8
  import { normalizeSchemaForMCP } from "@oh-my-pi/pi-ai/utils/schema";
9
9
  import { untilAborted } from "@oh-my-pi/pi-utils";
10
+ import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
10
11
  import type { SourceMeta } from "../capability/types";
11
12
  import type {
12
13
  CustomTool,
@@ -83,6 +84,35 @@ function omitUnusedOptionalArgs(args: MCPToolArgs, inputSchema: MCPToolDefinitio
83
84
  return cleaned ?? args;
84
85
  }
85
86
 
87
+ /**
88
+ * Drop the harness-internal intent field (`INTENT_FIELD`) before forwarding
89
+ * args to an MCP server. The harness injects `i` into every tool's wire
90
+ * schema; the direct model tool-call path strips it via `extractIntent`, but
91
+ * the `eval` `tool.*` bridge and any other in-process caller forwards args
92
+ * verbatim. Strict-schema servers (Linear, anything with
93
+ * `additionalProperties:false` / Zod `.strict()`) reject every call that
94
+ * carries `i`. The MCP boundary is the authoritative guard so callers don't
95
+ * have to pre-strip.
96
+ *
97
+ * Leaves `i` in place when the server's own `inputSchema.properties` declares
98
+ * it, so a server that legitimately uses `i` as a parameter is unaffected.
99
+ */
100
+ function stripHarnessIntent(args: MCPToolArgs, inputSchema: MCPToolDefinition["inputSchema"]): MCPToolArgs {
101
+ if (!Object.hasOwn(args, INTENT_FIELD)) return args;
102
+ if (inputSchema.properties && Object.hasOwn(inputSchema.properties, INTENT_FIELD)) return args;
103
+ const { [INTENT_FIELD]: _intent, ...rest } = args;
104
+ return rest;
105
+ }
106
+
107
+ /**
108
+ * Normalize raw tool params into the outbound `tools/call` arguments: strip
109
+ * the harness intent field, then drop optional empty placeholders the server
110
+ * declares but doesn't require.
111
+ */
112
+ function prepareOutboundArgs(params: unknown, inputSchema: MCPToolDefinition["inputSchema"]): MCPToolArgs {
113
+ return omitUnusedOptionalArgs(stripHarnessIntent(normalizeToolArgs(params), inputSchema), inputSchema);
114
+ }
115
+
86
116
  /** Details included in MCP tool results for rendering */
87
117
  export interface MCPToolDetails {
88
118
  /** Server name */
@@ -286,7 +316,7 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
286
316
  signal?: AbortSignal,
287
317
  ): Promise<CustomToolResult<MCPToolDetails>> {
288
318
  throwIfAborted(signal);
289
- const args = omitUnusedOptionalArgs(normalizeToolArgs(params), this.tool.inputSchema);
319
+ const args = prepareOutboundArgs(params, this.tool.inputSchema);
290
320
  const provider = this.connection._source?.provider;
291
321
  const providerName = this.connection._source?.providerName;
292
322
 
@@ -385,7 +415,7 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
385
415
  signal?: AbortSignal,
386
416
  ): Promise<CustomToolResult<MCPToolDetails>> {
387
417
  throwIfAborted(signal);
388
- const args = omitUnusedOptionalArgs(normalizeToolArgs(params), this.tool.inputSchema);
418
+ const args = prepareOutboundArgs(params, this.tool.inputSchema);
389
419
  const provider = this.#fallbackProvider;
390
420
  const providerName = this.#fallbackProviderName;
391
421
 
@@ -559,7 +559,7 @@ function shouldPersistResponseItemForMemories(message: AgentMessage): boolean {
559
559
  }
560
560
  if (role !== "toolResult") return false;
561
561
  const toolName = (message as { toolName?: string }).toolName;
562
- if (toolName === "bash" || toolName === "eval" || toolName === "read" || toolName === "search") {
562
+ if (toolName === "bash" || toolName === "eval" || toolName === "read" || toolName === "grep") {
563
563
  const text = extractMessageText(message);
564
564
  return text.length > 0 && text.length <= 32_000;
565
565
  }