@oh-my-pi/pi-coding-agent 17.0.0 → 17.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/cli.js +4006 -3966
  3. package/dist/types/advisor/advise-tool.d.ts +5 -2
  4. package/dist/types/advisor/config.d.ts +14 -6
  5. package/dist/types/advisor/runtime.d.ts +20 -11
  6. package/dist/types/autolearn/controller.d.ts +1 -0
  7. package/dist/types/cli-commands.d.ts +14 -1
  8. package/dist/types/config/model-discovery.d.ts +17 -0
  9. package/dist/types/config/model-registry.d.ts +4 -0
  10. package/dist/types/config/model-resolver.d.ts +6 -2
  11. package/dist/types/config/models-config-schema.d.ts +10 -0
  12. package/dist/types/config/models-config.d.ts +6 -0
  13. package/dist/types/config/settings-schema.d.ts +32 -7
  14. package/dist/types/config/settings.d.ts +50 -0
  15. package/dist/types/cursor.d.ts +11 -0
  16. package/dist/types/discovery/helpers.d.ts +5 -2
  17. package/dist/types/discovery/substitute-plugin-root.d.ts +20 -7
  18. package/dist/types/exec/bash-executor.d.ts +2 -0
  19. package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
  20. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  21. package/dist/types/extensibility/extensions/types.d.ts +18 -0
  22. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +40 -0
  23. package/dist/types/extensibility/shared-events.d.ts +6 -0
  24. package/dist/types/extensibility/utils.d.ts +2 -2
  25. package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
  26. package/dist/types/lib/xai-http.d.ts +11 -0
  27. package/dist/types/mcp/transports/stdio.d.ts +13 -1
  28. package/dist/types/modes/acp/acp-agent.d.ts +3 -0
  29. package/dist/types/modes/acp/acp-mode.d.ts +6 -2
  30. package/dist/types/modes/components/advisor-config.d.ts +8 -1
  31. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  32. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +1 -0
  33. package/dist/types/modes/components/custom-editor.d.ts +23 -1
  34. package/dist/types/modes/components/hook-editor.d.ts +16 -2
  35. package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
  36. package/dist/types/modes/components/model-hub.d.ts +5 -2
  37. package/dist/types/modes/components/session-selector.d.ts +4 -0
  38. package/dist/types/modes/components/tool-execution.d.ts +8 -1
  39. package/dist/types/modes/components/tool-execution.test.d.ts +1 -0
  40. package/dist/types/modes/controllers/command-controller.d.ts +8 -0
  41. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  42. package/dist/types/modes/interactive-mode.d.ts +6 -0
  43. package/dist/types/modes/noninteractive-dispose.test.d.ts +1 -0
  44. package/dist/types/modes/print-mode.d.ts +1 -1
  45. package/dist/types/modes/types.d.ts +10 -0
  46. package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
  47. package/dist/types/modes/warp-events.d.ts +24 -0
  48. package/dist/types/modes/warp-events.test.d.ts +1 -0
  49. package/dist/types/plan-mode/model-transition.d.ts +47 -0
  50. package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
  51. package/dist/types/registry/agent-lifecycle.d.ts +26 -1
  52. package/dist/types/sdk.d.ts +13 -2
  53. package/dist/types/session/agent-session.d.ts +43 -11
  54. package/dist/types/session/session-history-format.d.ts +10 -0
  55. package/dist/types/session/session-manager.d.ts +14 -0
  56. package/dist/types/session/streaming-output.d.ts +2 -0
  57. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
  58. package/dist/types/slash-commands/types.d.ts +4 -4
  59. package/dist/types/system-prompt.d.ts +1 -1
  60. package/dist/types/task/label.d.ts +1 -1
  61. package/dist/types/telemetry-export.d.ts +34 -9
  62. package/dist/types/tools/approval.d.ts +8 -0
  63. package/dist/types/tools/bash.d.ts +2 -0
  64. package/dist/types/tools/browser/registry.d.ts +7 -3
  65. package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
  66. package/dist/types/tools/essential-tools.d.ts +29 -0
  67. package/dist/types/tools/image-gen.d.ts +2 -1
  68. package/dist/types/tools/index.d.ts +4 -1
  69. package/dist/types/tools/manage-skill.d.ts +2 -0
  70. package/dist/types/tools/xdev.d.ts +11 -2
  71. package/dist/types/utils/title-generator.d.ts +2 -1
  72. package/dist/types/web/search/index.d.ts +2 -0
  73. package/dist/types/web/search/providers/base.d.ts +3 -0
  74. package/dist/types/web/search/providers/kimi.d.ts +4 -1
  75. package/dist/types/web/search/types.d.ts +1 -1
  76. package/package.json +21 -16
  77. package/src/advisor/__tests__/advisor.test.ts +1358 -86
  78. package/src/advisor/__tests__/config.test.ts +58 -2
  79. package/src/advisor/advise-tool.ts +7 -3
  80. package/src/advisor/config.ts +76 -24
  81. package/src/advisor/runtime.ts +445 -92
  82. package/src/autolearn/controller.ts +23 -28
  83. package/src/cli/bench-cli.ts +4 -1
  84. package/src/cli/grep-cli.ts +2 -1
  85. package/src/cli-commands.ts +57 -23
  86. package/src/cli.ts +5 -1
  87. package/src/config/model-discovery.ts +81 -21
  88. package/src/config/model-registry.ts +34 -6
  89. package/src/config/model-resolver.ts +57 -12
  90. package/src/config/models-config-schema.ts +1 -0
  91. package/src/config/settings-schema.ts +43 -6
  92. package/src/config/settings.ts +405 -25
  93. package/src/cursor.ts +20 -3
  94. package/src/debug/report-bundle.ts +40 -4
  95. package/src/discovery/codex.ts +14 -4
  96. package/src/discovery/helpers.ts +28 -5
  97. package/src/discovery/substitute-plugin-root.ts +23 -7
  98. package/src/exec/bash-executor.ts +14 -5
  99. package/src/export/html/template.js +2 -0
  100. package/src/extensibility/custom-tools/loader.ts +3 -3
  101. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  102. package/src/extensibility/extensions/loader.ts +3 -3
  103. package/src/extensibility/extensions/managed-timers.ts +83 -0
  104. package/src/extensibility/extensions/runner.ts +26 -0
  105. package/src/extensibility/extensions/types.ts +18 -0
  106. package/src/extensibility/extensions/wrapper.ts +2 -1
  107. package/src/extensibility/hooks/loader.ts +3 -3
  108. package/src/extensibility/legacy-pi-coding-agent-shim.ts +96 -1
  109. package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
  110. package/src/extensibility/plugins/manager.ts +2 -2
  111. package/src/extensibility/shared-events.ts +6 -0
  112. package/src/extensibility/utils.ts +91 -25
  113. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
  114. package/src/internal-urls/agent-protocol.ts +73 -39
  115. package/src/irc/bus.ts +22 -3
  116. package/src/launch/broker.ts +3 -2
  117. package/src/launch/client.ts +2 -2
  118. package/src/launch/presence.ts +2 -2
  119. package/src/lib/xai-http.ts +28 -2
  120. package/src/lsp/client.ts +1 -1
  121. package/src/main.ts +11 -8
  122. package/src/mcp/manager.ts +9 -3
  123. package/src/mcp/transports/stdio.ts +103 -23
  124. package/src/modes/acp/acp-agent.ts +28 -9
  125. package/src/modes/acp/acp-event-mapper.ts +8 -0
  126. package/src/modes/acp/acp-mode.ts +18 -4
  127. package/src/modes/components/advisor-config.ts +65 -3
  128. package/src/modes/components/ask-dialog.ts +1 -1
  129. package/src/modes/components/assistant-message.ts +35 -14
  130. package/src/modes/components/chat-transcript-builder.ts +2 -0
  131. package/src/modes/components/custom-editor-plugin-ctor.test.ts +36 -0
  132. package/src/modes/components/custom-editor.ts +62 -1
  133. package/src/modes/components/hook-editor.ts +35 -5
  134. package/src/modes/components/login-dialog.test.ts +56 -0
  135. package/src/modes/components/login-dialog.ts +7 -3
  136. package/src/modes/components/model-hub.ts +138 -42
  137. package/src/modes/components/plan-review-overlay.ts +51 -10
  138. package/src/modes/components/session-selector.ts +10 -0
  139. package/src/modes/components/status-line/component.test.ts +1 -0
  140. package/src/modes/components/status-line/component.ts +2 -4
  141. package/src/modes/components/status-line/segments.ts +21 -6
  142. package/src/modes/components/tool-execution.test.ts +101 -0
  143. package/src/modes/components/tool-execution.ts +117 -9
  144. package/src/modes/components/transcript-container.ts +30 -2
  145. package/src/modes/controllers/command-controller-shared.ts +1 -1
  146. package/src/modes/controllers/command-controller.ts +168 -48
  147. package/src/modes/controllers/event-controller.ts +6 -0
  148. package/src/modes/controllers/extension-ui-controller.ts +4 -22
  149. package/src/modes/controllers/input-controller.ts +12 -12
  150. package/src/modes/controllers/selector-controller.ts +208 -46
  151. package/src/modes/interactive-mode.ts +205 -65
  152. package/src/modes/noninteractive-dispose.test.ts +60 -0
  153. package/src/modes/print-mode.ts +10 -5
  154. package/src/modes/rpc/host-tools.ts +2 -1
  155. package/src/modes/rpc/rpc-mode.ts +31 -7
  156. package/src/modes/theme/theme.ts +2 -2
  157. package/src/modes/types.ts +10 -0
  158. package/src/modes/utils/interactive-context-helpers.ts +3 -1
  159. package/src/modes/utils/transcript-render-helpers.ts +3 -2
  160. package/src/modes/warp-events.test.ts +794 -0
  161. package/src/modes/warp-events.ts +232 -0
  162. package/src/plan-mode/model-transition.test.ts +60 -0
  163. package/src/plan-mode/model-transition.ts +51 -0
  164. package/src/prompts/system/system-prompt.md +1 -1
  165. package/src/registry/agent-lifecycle.ts +133 -18
  166. package/src/sdk.ts +227 -44
  167. package/src/session/agent-session.ts +1364 -382
  168. package/src/session/session-history-format.ts +20 -5
  169. package/src/session/session-listing.ts +6 -5
  170. package/src/session/session-loader.ts +9 -0
  171. package/src/session/session-manager.ts +48 -0
  172. package/src/session/session-persistence.ts +11 -0
  173. package/src/session/streaming-output.ts +3 -1
  174. package/src/slash-commands/builtin-registry.ts +9 -0
  175. package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
  176. package/src/slash-commands/types.ts +4 -4
  177. package/src/system-prompt.ts +2 -2
  178. package/src/task/executor.ts +1 -1
  179. package/src/task/label.ts +2 -0
  180. package/src/telemetry-export.ts +453 -97
  181. package/src/tools/approval.ts +11 -0
  182. package/src/tools/bash.ts +71 -38
  183. package/src/tools/browser/registry.ts +9 -2
  184. package/src/tools/browser/tab-supervisor.ts +65 -10
  185. package/src/tools/browser.ts +4 -3
  186. package/src/tools/essential-tools.ts +45 -0
  187. package/src/tools/eval.ts +3 -0
  188. package/src/tools/gh.ts +169 -2
  189. package/src/tools/image-gen.ts +624 -513
  190. package/src/tools/index.ts +10 -6
  191. package/src/tools/manage-skill.ts +5 -3
  192. package/src/tools/output-meta.ts +7 -0
  193. package/src/tools/path-utils.ts +8 -5
  194. package/src/tools/read.ts +48 -3
  195. package/src/tools/write.ts +22 -4
  196. package/src/tools/xdev.ts +14 -3
  197. package/src/utils/title-generator.ts +15 -4
  198. package/src/web/search/index.ts +21 -6
  199. package/src/web/search/providers/base.ts +3 -0
  200. package/src/web/search/providers/kimi.ts +18 -12
  201. package/src/web/search/providers/xai.ts +40 -9
  202. package/src/web/search/types.ts +6 -1
@@ -74,6 +74,7 @@ export * from "./bash";
74
74
  export * from "./browser";
75
75
  export * from "./checkpoint";
76
76
  export * from "./debug";
77
+ export * from "./essential-tools";
77
78
  export * from "./eval";
78
79
  export * from "./eval-backends";
79
80
  export * from "./gh";
@@ -160,7 +161,9 @@ export interface ToolSession {
160
161
  /** Pre-loaded workspace tree (forwarded to subagents to skip re-scanning) */
161
162
  workspaceTree?: WorkspaceTree;
162
163
  /** Pre-loaded skills */
163
- skills?: Skill[];
164
+ skills?: readonly Skill[];
165
+ /** Rediscover live session skills after a tool mutates their backing files. */
166
+ refreshSkills?: () => Promise<void>;
164
167
  /** Pre-loaded prompt templates */
165
168
  promptTemplates?: PromptTemplate[];
166
169
  /** Pre-loaded rules (forwarded to subagents to skip re-discovery). */
@@ -566,15 +569,16 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
566
569
  );
567
570
  let tools = baseResults.filter((r): r is Tool => r !== null);
568
571
 
569
- // xd:// mounting: unmount discoverable built-ins from the tools array and
570
- // expose them as virtual device URLs driven through read/write. Active for
571
- // default tool sets when `tools.xdev` is enabled.
572
- const xdevEnabled = requestedTools === undefined && session.settings.get("tools.xdev");
572
+ // Always create the xd:// registry when enabled so SDK assembly can mount
573
+ // discoverable custom/MCP tools later. Explicitly requested built-ins keep
574
+ // their top-level presentation; default tool sets mount discoverable built-ins.
575
+ const xdevEnabled = session.settings.get("tools.xdev");
576
+ const mountBuiltinTools = requestedTools === undefined;
573
577
  if (xdevEnabled) {
574
578
  const mounted: Tool[] = [];
575
579
  const kept: Tool[] = [];
576
580
  for (const tool of tools) {
577
- const mountable = isMountableUnderXdev(tool) && tool.name in BUILTIN_TOOLS;
581
+ const mountable = mountBuiltinTools && isMountableUnderXdev(tool) && tool.name in BUILTIN_TOOLS;
578
582
  (mountable ? mounted : kept).push(tool);
579
583
  }
580
584
  session.xdevRegistry = new XdevRegistry(mounted);
@@ -45,16 +45,17 @@ export class ManageSkillTool implements AgentTool<typeof manageSkillSchema> {
45
45
  readonly loadMode = "essential" as const;
46
46
  readonly summary = "Create, update, or delete an isolated managed skill";
47
47
 
48
- // No session state needed: createIf reads settings; writes target the
49
- // home-based managed-skills dir directly.
48
+ constructor(private readonly refreshSkills?: () => Promise<void>) {}
49
+
50
50
  static createIf(session: ToolSession): ManageSkillTool | null {
51
51
  if (!session.settings.get("autolearn.enabled")) return null;
52
- return new ManageSkillTool();
52
+ return new ManageSkillTool(session.refreshSkills);
53
53
  }
54
54
 
55
55
  async execute(_id: string, params: ManageSkillParams): Promise<AgentToolResult> {
56
56
  if (params.action === "delete") {
57
57
  await deleteManagedSkill(params.name);
58
+ await this.refreshSkills?.();
58
59
  return {
59
60
  content: [{ type: "text", text: `Deleted managed skill "${params.name}".` }],
60
61
  details: { action: "delete", name: params.name },
@@ -90,6 +91,7 @@ export class ManageSkillTool implements AgentTool<typeof manageSkillSchema> {
90
91
  description: params.description,
91
92
  body: params.body,
92
93
  });
94
+ await this.refreshSkills?.();
93
95
  const relativePath = path.relative(getManagedSkillsDir(), skillPath);
94
96
  const verb = params.action === "create" ? "Created" : "Updated";
95
97
  return {
@@ -191,6 +191,13 @@ export class OutputMetaBuilder {
191
191
 
192
192
  /** Add truncation info from OutputSummary. No-op if not truncated. */
193
193
  truncationFromSummary(summary: OutputSummary, options: TruncationSummaryOptions): this {
194
+ // A per-line column cap only trims individual lines (with a `…` marker);
195
+ // it is not a window/byte truncation, so surface it as its own limit
196
+ // notice rather than a "Showing lines X-Y … limit" range. This runs even
197
+ // when the output is otherwise complete (`truncated === false`).
198
+ if (summary.columnMax != null && summary.columnMax > 0 && (summary.columnTruncatedLines ?? 0) > 0) {
199
+ this.columnTruncated(summary.columnMax);
200
+ }
194
201
  if (!summary.truncated) return this;
195
202
 
196
203
  const { direction, startLine = 1, totalFileLines } = options;
@@ -148,11 +148,14 @@ export function expandTilde(filePath: string, home?: string): string {
148
148
 
149
149
  export function expandPath(filePath: string): string {
150
150
  // Some models intermittently prefix an otherwise-valid path with a stray
151
- // `:` (e.g. `:/abs/path`, `:../rel`). No real path starts with `:` and it
152
- // never begins a selector against an absolute/relative path, so strip it
153
- // before resolution mirroring the `@`-prefix normalization above and the
154
- // implicit stripping `write` already tolerates (issue #5508).
155
- const deColoned = /^:(?=[/~]|\.\.?\/)/.test(filePath) ? filePath.slice(1) : filePath;
151
+ // `:` (e.g. `:/abs/path`, `:../rel`, or the Windows forms `:C:\repo\file`
152
+ // and `:.\src`). No real path starts with `:` and it never begins a
153
+ // selector against an absolute/relative path, so strip it before
154
+ // resolution mirroring the `@`-prefix normalization above and the
155
+ // implicit stripping `write` already tolerates (issues #5508, #5624). The
156
+ // lookahead admits POSIX (`/`, `~`, `./`, `../`) and Windows (`\`, `.\`,
157
+ // `..\`, drive-letter `C:`) path shapes.
158
+ const deColoned = /^:(?=[/\\~]|\.\.?[/\\]|[A-Za-z]:)/.test(filePath) ? filePath.slice(1) : filePath;
156
159
  const normalized = stripWindowsExtendedLengthPathPrefix(
157
160
  stripFileUrl(normalizeUnicodeSpaces(normalizeAtPrefix(deColoned))),
158
161
  );
package/src/tools/read.ts CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  import { normalizeToLF } from "../edit/normalize";
37
37
  import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
38
38
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
39
- import { InternalUrlRouter, resolveLocalUrlToFile } from "../internal-urls";
39
+ import { InternalUrlRouter, resolveLocalUrlToFile, resolveLocalUrlToPath } from "../internal-urls";
40
40
  import { type ResolvedArtifactFile, resolveArtifactFile } from "../internal-urls/artifact-protocol";
41
41
  import { parseInternalUrl } from "../internal-urls/parse";
42
42
  import type { InternalUrl } from "../internal-urls/types";
@@ -920,6 +920,32 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
920
920
  });
921
921
  }
922
922
 
923
+ /**
924
+ * Recover the active approved plan when a model rewrites its `local://` URL
925
+ * as a same-basename path in the working-directory root.
926
+ *
927
+ * Only missing cwd-root paths qualify, so a real working-tree file always
928
+ * wins and unrelated paths cannot escape into the session artifact sandbox.
929
+ */
930
+ #approvedPlanAlias(missingAbsolutePath: string): string | undefined {
931
+ const planReferencePath = this.session.getPlanReferencePath?.();
932
+ if (!planReferencePath?.startsWith("local:")) return undefined;
933
+
934
+ const requestedPath = path.resolve(missingAbsolutePath);
935
+ if (path.dirname(requestedPath) !== path.resolve(this.session.cwd)) return undefined;
936
+
937
+ const localProtocolOptions = this.session.localProtocolOptions ?? {
938
+ getArtifactsDir: () => this.session.getArtifactsDir?.() ?? null,
939
+ getSessionId: () => this.session.getSessionId?.() ?? null,
940
+ };
941
+ try {
942
+ const approvedPlanPath = resolveLocalUrlToPath(planReferencePath, localProtocolOptions);
943
+ return path.basename(requestedPath) === path.basename(approvedPlanPath) ? approvedPlanPath : undefined;
944
+ } catch {
945
+ return undefined;
946
+ }
947
+ }
948
+
923
949
  async #tryReadDelimitedPaths(
924
950
  readPath: string,
925
951
  signal?: AbortSignal,
@@ -2292,7 +2318,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2292
2318
  isDirectory = stat.isDirectory();
2293
2319
  } catch (error) {
2294
2320
  if (isNotFoundError(error)) {
2295
- // Attempt unique suffix resolution before falling back to fuzzy suggestions
2321
+ // Attempt unique suffix resolution before falling back to the approved-plan
2322
+ // alias or fuzzy suggestions. Existing workspace files retain precedence.
2296
2323
  if (!isRemoteMountPath(absolutePath)) {
2297
2324
  const suffixMatch = await this.#findSuffixMatchCached(suffixCache, localReadPath, signal);
2298
2325
  if (suffixMatch) {
@@ -2303,12 +2330,30 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2303
2330
  isDirectory = retryStat.isDirectory();
2304
2331
  suffixResolution = { from: localReadPath, to: suffixMatch.displayPath };
2305
2332
  } catch {
2306
- // Suffix match candidate no longer stats — fall through to error path
2333
+ // Suffix match candidate no longer stats — continue through
2334
+ // approved-plan recovery and the original not-found error.
2307
2335
  }
2308
2336
  }
2309
2337
  }
2310
2338
 
2339
+ let recoveredApprovedPlan = false;
2311
2340
  if (!suffixResolution) {
2341
+ const approvedPlanPath = this.#approvedPlanAlias(absolutePath);
2342
+ if (approvedPlanPath) {
2343
+ try {
2344
+ const approvedPlanStat = await Bun.file(approvedPlanPath).stat();
2345
+ absolutePath = approvedPlanPath;
2346
+ fileSize = approvedPlanStat.size;
2347
+ isDirectory = approvedPlanStat.isDirectory();
2348
+ recoveredApprovedPlan = true;
2349
+ } catch {
2350
+ // The referenced plan disappeared after resolution; continue through
2351
+ // the ordinary delimited-path fallback and not-found error.
2352
+ }
2353
+ }
2354
+ }
2355
+
2356
+ if (!recoveredApprovedPlan && !suffixResolution) {
2312
2357
  const delimitedResult = await this.#tryReadDelimitedPaths(readPath, signal);
2313
2358
  if (delimitedResult) return delimitedResult;
2314
2359
  throw new ToolError(`Path '${localReadPath}' not found`);
@@ -36,7 +36,7 @@ import {
36
36
  writeArchive,
37
37
  } from "../utils/zip";
38
38
  import { routeWriteThroughBridge } from "./acp-bridge";
39
- import { truncateForPrompt } from "./approval";
39
+ import { resolveToolTier, truncateForPrompt } from "./approval";
40
40
  import { assertEditableFile } from "./auto-generated-guard";
41
41
  import {
42
42
  type ConflictEntry,
@@ -398,9 +398,27 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
398
398
  if (xdevTarget.name === REPORT_ISSUE_DEVICE_NAME) return "write";
399
399
  if (xdevTarget.name && isResolutionDeviceName(xdevTarget.name)) return "read";
400
400
  const inst = xdevTarget.name ? this.session.xdevRegistry?.get(xdevTarget.name) : undefined;
401
- const decision = typeof inst?.approval === "function" ? undefined : inst?.approval;
402
- const tier = typeof decision === "object" ? decision?.tier : decision;
403
- return tier ?? "exec";
401
+ if (!inst) return "exec";
402
+ // Decode the device JSON payload and evaluate the mounted tool's own
403
+ // approval (which may be argument-dependent, e.g. ast_edit is read-tier
404
+ // for internal-URL paths, debug is read-tier for inspection actions).
405
+ // Malformed JSON, non-object payloads, missing content, and approval
406
+ // functions that reject schema-invalid objects stay exec so the gate
407
+ // fails closed — the dispatch itself rejects invalid arguments too.
408
+ const rawContent = (args as Partial<WriteParams>).content;
409
+ if (typeof rawContent !== "string") return "exec";
410
+ let parsed: unknown;
411
+ try {
412
+ parsed = JSON.parse(rawContent);
413
+ } catch {
414
+ return "exec";
415
+ }
416
+ if (!isRecord(parsed)) return "exec";
417
+ try {
418
+ return resolveToolTier(inst, parsed);
419
+ } catch {
420
+ return "exec";
421
+ }
404
422
  }
405
423
  // Remote SSH writes open an outbound connection and run a remote shell —
406
424
  // gate them like the exec-tier `ssh` tool, ahead of the handler-write
package/src/tools/xdev.ts CHANGED
@@ -44,15 +44,26 @@ import { ToolError } from "./tool-errors";
44
44
  */
45
45
  export const XDEV_KEEP_TOP_LEVEL: Record<string, true> = { todo: true, ask: true, grep: true };
46
46
 
47
+ /**
48
+ * Tools that carry the `xd://` transport itself and therefore can never be
49
+ * mounted as devices: `read xd://` lists/documents devices and
50
+ * `write xd://<tool>` executes them. Demoting either leaves every mounted
51
+ * device unreachable (issue #5764), so they stay top-level regardless of a
52
+ * declared `loadMode`.
53
+ */
54
+ export const XDEV_TRANSPORT_TOOLS: Record<string, true> = { read: true, write: true };
55
+
47
56
  /**
48
57
  * Whether an enabled tool is presented under `xd://` (rather than top-level)
49
58
  * while the `xd://` transport is active. Discoverable tools mount unless they
50
- * are pinned top-level by {@link XDEV_KEEP_TOP_LEVEL}; essential tools never do.
51
- * The caller gates this on the transport being active (a session-owned
59
+ * are pinned top-level by {@link XDEV_KEEP_TOP_LEVEL} or carry the transport
60
+ * itself ({@link XDEV_TRANSPORT_TOOLS}); essential tools never do. The caller
61
+ * gates this on the transport being active (a session-owned
52
62
  * {@link XdevRegistry} existing).
53
63
  */
54
64
  export function isMountableUnderXdev(tool: { name: string; loadMode?: ToolLoadMode }): boolean {
55
- return tool.loadMode === "discoverable" && !(tool.name in XDEV_KEEP_TOP_LEVEL);
65
+ if (tool.name in XDEV_TRANSPORT_TOOLS || tool.name in XDEV_KEEP_TOP_LEVEL) return false;
66
+ return tool.loadMode === "discoverable";
56
67
  }
57
68
 
58
69
  /** Dispatch metadata carried on write-tool details for renderer delegation. */
@@ -68,6 +68,7 @@ function getTitleModel(registry: ModelRegistry, settings: Settings, currentModel
68
68
  * resolver instead of a pre-evaluated value ensures the metadata's account_uuid
69
69
  * reflects the credential actually selected for this request.
70
70
  * @param customSystemPrompt Optional title-specific system prompt override
71
+ * @param signal Session-lifecycle cancellation for background title requests
71
72
  */
72
73
  export async function generateSessionTitle(
73
74
  firstMessage: string,
@@ -77,6 +78,7 @@ export async function generateSessionTitle(
77
78
  currentModel?: Model<Api>,
78
79
  metadataResolver?: (provider: string) => Record<string, unknown> | undefined,
79
80
  customSystemPrompt?: string,
81
+ signal?: AbortSignal,
80
82
  ): Promise<string | null> {
81
83
  // Defer titling for greetings / acknowledgements / empty input. The default
82
84
  // tiny title model can't reliably decline trivial input, so this happens
@@ -97,7 +99,7 @@ export async function generateSessionTitle(
97
99
  sessionId,
98
100
  currentModel,
99
101
  metadataResolver,
100
- undefined,
102
+ signal,
101
103
  titleSystemPrompt,
102
104
  );
103
105
  }
@@ -117,9 +119,18 @@ export async function generateSessionTitle(
117
119
  return null;
118
120
  }
119
121
  try {
120
- const localTitle = titleSystemPrompt
121
- ? await tinyTitleClient.generate(tinyModel, firstMessage, { systemPrompt: titleSystemPrompt })
122
- : await tinyTitleClient.generate(tinyModel, firstMessage);
122
+ let localTitle: string | null;
123
+ if (signal) {
124
+ localTitle = await tinyTitleClient.generate(
125
+ tinyModel,
126
+ firstMessage,
127
+ titleSystemPrompt ? { signal, systemPrompt: titleSystemPrompt } : { signal },
128
+ );
129
+ } else if (titleSystemPrompt) {
130
+ localTitle = await tinyTitleClient.generate(tinyModel, firstMessage, { systemPrompt: titleSystemPrompt });
131
+ } else {
132
+ localTitle = await tinyTitleClient.generate(tinyModel, firstMessage);
133
+ }
123
134
  if (!localTitle) {
124
135
  logger.warn("title-generator: local tiny model produced no title; skipping (no online fallback)", {
125
136
  sessionId,
@@ -8,6 +8,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
8
8
  import type { AuthStorage } from "@oh-my-pi/pi-ai";
9
9
  import { prompt } from "@oh-my-pi/pi-utils";
10
10
  import { type } from "arktype";
11
+ import { ModelRegistry } from "../../config/model-registry";
11
12
  import { settings } from "../../config/settings";
12
13
  import type { CustomTool, CustomToolContext, RenderResultOptions } from "../../extensibility/custom-tools/types";
13
14
  import type { Theme } from "../../modes/theme/theme";
@@ -118,6 +119,7 @@ function hasRenderableSearchContent(response: SearchResponse): boolean {
118
119
 
119
120
  interface ExecuteSearchOptions {
120
121
  authStorage: AuthStorage;
122
+ modelRegistry?: ModelRegistry;
121
123
  sessionId?: string;
122
124
  signal?: AbortSignal;
123
125
  }
@@ -128,7 +130,7 @@ async function executeSearch(
128
130
  params: SearchQueryParams,
129
131
  options: ExecuteSearchOptions,
130
132
  ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
131
- const { authStorage, sessionId, signal } = options;
133
+ const { authStorage, modelRegistry, sessionId, signal } = options;
132
134
  const explicitProvider = params.provider;
133
135
  let candidates: SearchProviderCandidate[];
134
136
  if (explicitProvider && explicitProvider !== "auto") {
@@ -187,6 +189,7 @@ async function executeSearch(
187
189
  temperature: params.temperature,
188
190
  signal,
189
191
  authStorage,
192
+ modelRegistry,
190
193
  sessionId,
191
194
  antigravityEndpointMode,
192
195
  geminiModel,
@@ -246,16 +249,18 @@ async function executeSearch(
246
249
  */
247
250
  export async function runSearchQuery(
248
251
  params: SearchQueryParams,
249
- options: { authStorage?: AuthStorage; sessionId?: string; signal?: AbortSignal } = {},
252
+ options: { authStorage?: AuthStorage; modelRegistry?: ModelRegistry; sessionId?: string; signal?: AbortSignal } = {},
250
253
  ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
251
- const createdAuthStorage = options.authStorage ? undefined : await discoverAuthStorage();
252
- const authStorage = options.authStorage ?? createdAuthStorage;
254
+ const createdAuthStorage = options.authStorage || options.modelRegistry ? undefined : await discoverAuthStorage();
255
+ const authStorage = options.authStorage ?? options.modelRegistry?.authStorage ?? createdAuthStorage;
253
256
  if (!authStorage) {
254
257
  throw new Error("Failed to initialize authentication storage");
255
258
  }
259
+ const modelRegistry = options.modelRegistry ?? (createdAuthStorage ? new ModelRegistry(authStorage) : undefined);
256
260
  try {
257
261
  return await executeSearch("cli-web-search", params, {
258
262
  authStorage,
263
+ modelRegistry,
259
264
  sessionId: options.sessionId,
260
265
  signal: options.signal,
261
266
  });
@@ -295,7 +300,12 @@ export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRe
295
300
  ): Promise<AgentToolResult<SearchRenderDetails>> {
296
301
  const authStorage = this.#session.authStorage ?? (await discoverAuthStorage());
297
302
  const sessionId = this.#session.getSessionId?.() ?? undefined;
298
- return executeSearch(_toolCallId, params, { authStorage, sessionId, signal });
303
+ return executeSearch(_toolCallId, params, {
304
+ authStorage,
305
+ modelRegistry: this.#session.modelRegistry,
306
+ sessionId,
307
+ signal,
308
+ });
299
309
  }
300
310
  }
301
311
 
@@ -316,7 +326,12 @@ export const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRende
316
326
  ) {
317
327
  const authStorage = ctx.modelRegistry?.authStorage ?? (await discoverAuthStorage());
318
328
  const sessionId = ctx.sessionManager.getSessionId();
319
- return executeSearch(toolCallId, params, { authStorage, sessionId, signal });
329
+ return executeSearch(toolCallId, params, {
330
+ authStorage,
331
+ modelRegistry: ctx.modelRegistry,
332
+ sessionId,
333
+ signal,
334
+ });
320
335
  },
321
336
 
322
337
  renderCall(args: SearchToolParams, options: RenderResultOptions, theme: Theme) {
@@ -1,4 +1,5 @@
1
1
  import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai";
2
+ import type { ModelRegistry } from "../../../config/model-registry";
2
3
  import type { SearchProviderId, SearchResponse } from "../types";
3
4
 
4
5
  /**
@@ -45,6 +46,8 @@ export interface SearchParams {
45
46
  * the per-credential single-flight refresh.
46
47
  */
47
48
  authStorage: AuthStorage;
49
+ /** Provider/model transport settings used by native search endpoints. */
50
+ modelRegistry?: ModelRegistry;
48
51
  /**
49
52
  * Optional session id used as the round-robin / sticky key when selecting
50
53
  * among multiple credentials for the same provider. Pass through from the
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * Kimi Web Search Provider
3
3
  *
4
- * Uses Moonshot Kimi Code search API to retrieve web results.
4
+ * Uses the Kimi Code search API to retrieve web results. This is the Kimi Code
5
+ * membership service, distinct from the Moonshot Open Platform — it requires a
6
+ * Kimi Code Console credential (`omp /login kimi-code` or an explicit
7
+ * `MOONSHOT_SEARCH_API_KEY` / `KIMI_SEARCH_API_KEY`), not `MOONSHOT_API_KEY`.
5
8
  * Endpoint: POST https://api.kimi.com/coding/v1/search
6
9
  */
7
10
  import { type ApiKey, type AuthStorage, type FetchImpl, withAuth } from "@oh-my-pi/pi-ai";
@@ -58,11 +61,17 @@ function resolveBaseUrl(): string {
58
61
  }
59
62
 
60
63
  /**
61
- * Resolve the Kimi search credential. Highest precedence is the static env key;
62
- * otherwise an AuthStorage-backed resolver for whichever stored provider id
63
- * holds a key (`moonshot` first, then `kimi-code`), so a stale token triggers
64
- * the central force-refresh / sibling-rotate retry. Returns `undefined` when
65
- * neither is configured.
64
+ * Resolve the Kimi Code search credential. Highest precedence is the explicit
65
+ * search-key env override; otherwise an AuthStorage-backed resolver for a
66
+ * stored `kimi-code` credential (from `omp /login kimi-code`), so a stale token
67
+ * triggers the central force-refresh / sibling-rotate retry. Returns
68
+ * `undefined` when neither is configured.
69
+ *
70
+ * The endpoint (`https://api.kimi.com/coding/v1/search`) is the Kimi Code
71
+ * membership service, which has a different credential system from the Moonshot
72
+ * Open Platform (`https://api.moonshot.ai`). A stored `moonshot` credential
73
+ * (or `MOONSHOT_API_KEY`) is NOT accepted here — it 401s against Kimi Code
74
+ * (issue #5762).
66
75
  */
67
76
  async function resolveKey(
68
77
  authStorage: AuthStorage,
@@ -72,10 +81,8 @@ async function resolveKey(
72
81
  const envKey = asTrimmed($env.MOONSHOT_SEARCH_API_KEY) ?? asTrimmed($env.KIMI_SEARCH_API_KEY);
73
82
  if (envKey) return envKey;
74
83
 
75
- for (const provider of ["moonshot", "kimi-code"] as const) {
76
- const stored = await authStorage.getApiKey(provider, sessionId, { signal });
77
- if (stored) return authStorage.resolver(provider, { sessionId });
78
- }
84
+ const stored = await authStorage.getApiKey("kimi-code", sessionId, { signal });
85
+ if (stored) return authStorage.resolver("kimi-code", { sessionId });
79
86
  return undefined;
80
87
  }
81
88
 
@@ -127,7 +134,7 @@ export async function searchKimi(params: KimiSearchParams): Promise<SearchRespon
127
134
  const keyOrResolver = await resolveKey(params.authStorage, params.sessionId, params.signal);
128
135
  if (!keyOrResolver) {
129
136
  throw new Error(
130
- "Kimi search credentials not found. Set MOONSHOT_SEARCH_API_KEY, KIMI_SEARCH_API_KEY, MOONSHOT_API_KEY, or login with 'omp /login moonshot'.",
137
+ "Kimi search credentials not found. Kimi web search uses the Kimi Code service (api.kimi.com); set MOONSHOT_SEARCH_API_KEY / KIMI_SEARCH_API_KEY to a Kimi Code Console key, or login with 'omp /login kimi-code'. A Moonshot Open Platform key (MOONSHOT_API_KEY) is not accepted here.",
131
138
  );
132
139
  }
133
140
 
@@ -176,7 +183,6 @@ export class KimiProvider extends SearchProvider {
176
183
  return (
177
184
  !!asTrimmed($env.MOONSHOT_SEARCH_API_KEY) ||
178
185
  !!asTrimmed($env.KIMI_SEARCH_API_KEY) ||
179
- authStorage.hasAuth("moonshot") ||
180
186
  authStorage.hasAuth("kimi-code")
181
187
  );
182
188
  }
@@ -1,5 +1,6 @@
1
1
  import { type ApiKey, type ApiKeyResolver, type AuthStorage, withAuth } from "@oh-my-pi/pi-ai";
2
2
  import { $env } from "@oh-my-pi/pi-utils";
3
+ import { resolveXAIHttpTransport, type XAIHttpProvider, type XAIHttpTransport } from "../../../lib/xai-http";
3
4
  import type { SearchCitation, SearchResponse, SearchSource, SearchUsage } from "../../../web/search/types";
4
5
  import { SearchProviderError } from "../../../web/search/types";
5
6
  import { clampNumResults } from "../utils";
@@ -7,7 +8,7 @@ import type { SearchParams } from "./base";
7
8
  import { SearchProvider } from "./base";
8
9
  import { classifyProviderHttpError, withHardTimeout } from "./utils";
9
10
 
10
- const XAI_RESPONSES_URL = "https://api.x.ai/v1/responses";
11
+ const XAI_DEFAULT_BASE_URL = "https://api.x.ai/v1";
11
12
  const XAI_WEB_SEARCH_MODEL = "grok-4.3";
12
13
  const DEFAULT_NUM_RESULTS = 10;
13
14
  const MAX_NUM_RESULTS = 30;
@@ -75,10 +76,12 @@ async function postXAIResponses(
75
76
  apiKey: string,
76
77
  params: SearchParams,
77
78
  body: Record<string, unknown>,
79
+ transport: XAIHttpTransport,
78
80
  ): Promise<Response> {
79
- return (params.fetch ?? fetch)(XAI_RESPONSES_URL, {
81
+ return (params.fetch ?? fetch)(`${transport.baseURL.replace(/\/+$/, "")}/responses`, {
80
82
  method: "POST",
81
83
  headers: {
84
+ ...transport.headers,
82
85
  "Content-Type": "application/json",
83
86
  Authorization: `Bearer ${apiKey}`,
84
87
  },
@@ -93,9 +96,13 @@ function throwXAIResponsesError(status: number, errorText: string): never {
93
96
  throw new SearchProviderError("xai", `xAI Responses API error (${status}): ${errorText}`, status);
94
97
  }
95
98
 
96
- async function callXAIResponses(apiKey: string, params: SearchParams): Promise<XAIResponsesResponse> {
99
+ async function callXAIResponses(
100
+ apiKey: string,
101
+ params: SearchParams,
102
+ transport: XAIHttpTransport,
103
+ ): Promise<XAIResponsesResponse> {
97
104
  const requestBody = buildRequestBody(params);
98
- const response = await postXAIResponses(apiKey, params, requestBody);
105
+ const response = await postXAIResponses(apiKey, params, requestBody, transport);
99
106
 
100
107
  if (!response.ok) {
101
108
  throwXAIResponsesError(response.status, await response.text());
@@ -235,19 +242,24 @@ function shouldPreferXAIOAuth(authStorage: AuthStorage): boolean {
235
242
  return true;
236
243
  }
237
244
 
238
- function resolveXAIWebSearchApiKey(params: SearchParams): ApiKeyResolver {
245
+ interface XAIWebSearchAuth {
246
+ provider: XAIHttpProvider;
247
+ keyOrResolver: ApiKey;
248
+ }
249
+
250
+ function resolveXAIWebSearchAuth(params: SearchParams): XAIWebSearchAuth {
239
251
  const xaiResolver = params.authStorage.resolver("xai", {
240
252
  sessionId: params.sessionId,
241
253
  });
242
254
  const xaiOAuthOrigin = params.authStorage.getCredentialOrigin("xai-oauth");
243
255
  if (!shouldPreferXAIOAuth(params.authStorage)) {
244
- return xaiResolver;
256
+ return { provider: "xai", keyOrResolver: xaiResolver };
245
257
  }
246
258
 
247
259
  const xaiOAuthResolver = params.authStorage.resolver("xai-oauth", {
248
260
  sessionId: params.sessionId,
249
261
  });
250
- return async ctx => {
262
+ const keyOrResolver: ApiKeyResolver = async ctx => {
251
263
  const xaiOAuthKey = await xaiOAuthResolver(ctx);
252
264
  if (xaiOAuthKey) {
253
265
  const borrowedSharedEnvKey =
@@ -259,14 +271,33 @@ function resolveXAIWebSearchApiKey(params: SearchParams): ApiKeyResolver {
259
271
  }
260
272
  return xaiResolver(ctx);
261
273
  };
274
+ return { provider: "xai-oauth", keyOrResolver };
262
275
  }
263
276
 
264
277
  /** Execute xAI Responses API web search. */
265
278
  export async function searchXAI(params: SearchParams): Promise<SearchResponse> {
266
- const keyOrResolver: ApiKey = resolveXAIWebSearchApiKey(params);
279
+ const auth = resolveXAIWebSearchAuth(params);
280
+ const transport = params.modelRegistry
281
+ ? resolveXAIHttpTransport(params.modelRegistry, auth.provider, XAI_WEB_SEARCH_MODEL)
282
+ : { baseURL: XAI_DEFAULT_BASE_URL };
283
+ const customEndpoint = transport.baseURL.replace(/\/+$/, "") !== XAI_DEFAULT_BASE_URL;
284
+ const credentialOrigin = params.authStorage.getCredentialOrigin(auth.provider);
285
+ if (
286
+ customEndpoint &&
287
+ auth.provider === "xai-oauth" &&
288
+ (credentialOrigin?.kind === "oauth" || credentialOrigin?.kind === "env")
289
+ ) {
290
+ throw new SearchProviderError(
291
+ "xai",
292
+ `Refusing to send official xAI OAuth credentials to custom endpoint ${transport.baseURL}. Configure an API key for provider "xai-oauth".`,
293
+ );
294
+ }
295
+ const keyOrResolver: ApiKey = customEndpoint
296
+ ? params.authStorage.resolver(auth.provider, { sessionId: params.sessionId })
297
+ : auth.keyOrResolver;
267
298
 
268
299
  const resultCap = clampNumResults(params.numSearchResults ?? params.limit, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
269
- const response = await withAuth(keyOrResolver, (key: string) => callXAIResponses(key, params), {
300
+ const response = await withAuth(keyOrResolver, (key: string) => callXAIResponses(key, params, transport), {
270
301
  signal: params.signal,
271
302
  missingKeyMessage: 'xAI credentials not found. Set XAI_API_KEY or configure an API key for provider "xai".',
272
303
  });
@@ -39,7 +39,12 @@ export const SEARCH_PROVIDER_OPTIONS = [
39
39
  { value: "tavily", label: "Tavily", description: "Requires TAVILY_API_KEY" },
40
40
  { value: "firecrawl", label: "Firecrawl", description: "Requires FIRECRAWL_API_KEY" },
41
41
  { value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" },
42
- { value: "kimi", label: "Kimi", description: "Requires MOONSHOT_SEARCH_API_KEY or MOONSHOT_API_KEY" },
42
+ {
43
+ value: "kimi",
44
+ label: "Kimi",
45
+ description:
46
+ "Kimi Code search (requires a Kimi Code Console key via KIMI_SEARCH_API_KEY/MOONSHOT_SEARCH_API_KEY or /login kimi-code; not MOONSHOT_API_KEY)",
47
+ },
43
48
  { value: "parallel", label: "Parallel", description: "Requires PARALLEL_API_KEY" },
44
49
  { value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
45
50
  { value: "searxng", label: "SearXNG", description: "Requires SEARXNG_ENDPOINT or searxng.endpoint" },