@oh-my-pi/pi-coding-agent 14.7.1 → 14.7.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 (52) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +7 -7
  3. package/src/config/model-equivalence.ts +1 -0
  4. package/src/config/model-registry.ts +108 -22
  5. package/src/config/settings-schema.ts +36 -1
  6. package/src/discovery/helpers.ts +4 -3
  7. package/src/edit/index.ts +1 -0
  8. package/src/eval/py/gateway-coordinator.ts +2 -3
  9. package/src/eval/py/runtime.ts +1 -0
  10. package/src/internal-urls/docs-index.generated.ts +1 -1
  11. package/src/lsp/index.ts +2 -0
  12. package/src/mcp/discoverable-tool-metadata.ts +24 -202
  13. package/src/modes/components/extensions/extension-dashboard.ts +26 -2
  14. package/src/modes/components/extensions/state-manager.ts +41 -0
  15. package/src/modes/controllers/selector-controller.ts +3 -0
  16. package/src/modes/interactive-mode.ts +26 -1
  17. package/src/prompts/tools/search-tool-bm25.md +14 -14
  18. package/src/prompts/tools/todo-write.md +1 -0
  19. package/src/sdk.ts +69 -8
  20. package/src/session/agent-session.ts +177 -1
  21. package/src/slash-commands/builtin-registry.ts +11 -0
  22. package/src/task/index.ts +2 -0
  23. package/src/tool-discovery/tool-index.ts +377 -0
  24. package/src/tools/ask.ts +2 -0
  25. package/src/tools/ast-edit.ts +2 -0
  26. package/src/tools/ast-grep.ts +2 -0
  27. package/src/tools/bash.ts +1 -0
  28. package/src/tools/browser.ts +2 -0
  29. package/src/tools/calculator.ts +2 -0
  30. package/src/tools/checkpoint.ts +4 -0
  31. package/src/tools/debug.ts +2 -0
  32. package/src/tools/eval.ts +2 -0
  33. package/src/tools/find.ts +2 -0
  34. package/src/tools/gh.ts +2 -0
  35. package/src/tools/hindsight-recall.ts +2 -0
  36. package/src/tools/hindsight-reflect.ts +2 -0
  37. package/src/tools/hindsight-retain.ts +2 -0
  38. package/src/tools/index.ts +74 -14
  39. package/src/tools/inspect-image.ts +2 -0
  40. package/src/tools/irc.ts +2 -1
  41. package/src/tools/job.ts +2 -1
  42. package/src/tools/notebook.ts +2 -0
  43. package/src/tools/read.ts +1 -0
  44. package/src/tools/recipe/index.ts +2 -0
  45. package/src/tools/render-mermaid.ts +2 -0
  46. package/src/tools/search-tool-bm25.ts +128 -42
  47. package/src/tools/search.ts +2 -0
  48. package/src/tools/ssh.ts +2 -0
  49. package/src/tools/todo-write.ts +2 -1
  50. package/src/tools/write.ts +2 -0
  51. package/src/web/search/index.ts +2 -0
  52. package/src/web/search/providers/searxng.ts +8 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [14.7.2] - 2026-05-06
6
+ ### Breaking Changes
7
+
8
+ - Removed the exported `BUILTIN_TOOL_METADATA` API, including `BuiltinEntry`-style metadata exports and discoverable-built-in helper exports, which will break consumers relying on those symbols
9
+
10
+ ### Changed
11
+
12
+ - Updated discoverable tool search (`search_tool_bm25` and related discovery metadata) to read each tool’s own `summary` field when present, improving discoverability descriptions for built-in tools
13
+
14
+ ### Fixed
15
+
16
+ - Fixed SearXNG web search Basic Auth validation to reject RFC 7617 control characters and clarified the equivalent `config.yml` and environment variable settings.
17
+ - Fixed extension commands that return without starting a model turn leaving the interactive `Working…` spinner active indefinitely. (#927)
18
+ - Fixed `authHeader: true` provider overrides without custom `models` so built-in model transport headers receive `Authorization: Bearer <resolved-key>` (#929).
19
+
5
20
  ## [14.7.1] - 2026-05-06
6
21
 
7
22
  ### Added
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "14.7.1",
4
+ "version": "14.7.2",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -46,12 +46,12 @@
46
46
  "dependencies": {
47
47
  "@agentclientprotocol/sdk": "0.21.0",
48
48
  "@mozilla/readability": "^0.6.0",
49
- "@oh-my-pi/omp-stats": "14.7.1",
50
- "@oh-my-pi/pi-agent-core": "14.7.1",
51
- "@oh-my-pi/pi-ai": "14.7.1",
52
- "@oh-my-pi/pi-natives": "14.7.1",
53
- "@oh-my-pi/pi-tui": "14.7.1",
54
- "@oh-my-pi/pi-utils": "14.7.1",
49
+ "@oh-my-pi/omp-stats": "14.7.2",
50
+ "@oh-my-pi/pi-agent-core": "14.7.2",
51
+ "@oh-my-pi/pi-ai": "14.7.2",
52
+ "@oh-my-pi/pi-natives": "14.7.2",
53
+ "@oh-my-pi/pi-tui": "14.7.2",
54
+ "@oh-my-pi/pi-utils": "14.7.2",
55
55
  "@puppeteer/browsers": "^2.13.0",
56
56
  "@sinclair/typebox": "^0.34.49",
57
57
  "@types/turndown": "5.0.6",
@@ -50,6 +50,7 @@ const TRAILING_CANONICAL_MARKERS = [
50
50
  "minimal",
51
51
  "xhigh",
52
52
  "free",
53
+ "cloud",
53
54
  "exacto",
54
55
  "nitro",
55
56
  "original",
@@ -421,6 +421,7 @@ interface ProviderOverride {
421
421
  baseUrl?: string;
422
422
  headers?: Record<string, string>;
423
423
  apiKey?: string;
424
+ authHeader?: boolean;
424
425
  compat?: Model<Api>["compat"];
425
426
  }
426
427
 
@@ -667,14 +668,20 @@ function mergeCustomModelHeaders(
667
668
  authHeader: boolean | undefined,
668
669
  apiKeyConfig: string | undefined,
669
670
  ): Record<string, string> | undefined {
670
- let headers = providerHeaders || modelHeaders ? { ...providerHeaders, ...modelHeaders } : undefined;
671
- if (authHeader && apiKeyConfig) {
672
- const resolvedKey = resolveApiKeyConfig(apiKeyConfig);
673
- if (resolvedKey) {
674
- headers = { ...headers, Authorization: `Bearer ${resolvedKey}` };
675
- }
671
+ return mergeAuthHeader({ ...providerHeaders, ...modelHeaders }, authHeader, apiKeyConfig);
672
+ }
673
+
674
+ function mergeAuthHeader(
675
+ headers: Record<string, string> | undefined,
676
+ authHeader: boolean | undefined,
677
+ apiKeyConfig: string | undefined,
678
+ ): Record<string, string> | undefined {
679
+ const nextHeaders = headers && Object.keys(headers).length > 0 ? { ...headers } : undefined;
680
+ if (!authHeader || !apiKeyConfig) {
681
+ return nextHeaders;
676
682
  }
677
- return headers;
683
+ const resolvedKey = resolveApiKeyConfig(apiKeyConfig);
684
+ return resolvedKey ? { ...nextHeaders, Authorization: `Bearer ${resolvedKey}` } : nextHeaders;
678
685
  }
679
686
 
680
687
  /**
@@ -726,6 +733,69 @@ function buildCustomModelOverlay(
726
733
  };
727
734
  }
728
735
 
736
+ // Custom provider entries often front a known upstream model through a local proxy.
737
+ // Use bundled metadata for missing pricing/capability fields, but keep the custom transport.
738
+ function shouldReplaceCustomReference(existing: Model<Api> | undefined, candidate: Model<Api>): boolean {
739
+ if (!existing) return true;
740
+ if (candidate.contextWindow !== existing.contextWindow) {
741
+ return candidate.contextWindow > existing.contextWindow;
742
+ }
743
+ if (candidate.maxTokens !== existing.maxTokens) {
744
+ return candidate.maxTokens > existing.maxTokens;
745
+ }
746
+ const existingHasCachePricing = existing.cost.cacheRead > 0 || existing.cost.cacheWrite > 0;
747
+ const candidateHasCachePricing = candidate.cost.cacheRead > 0 || candidate.cost.cacheWrite > 0;
748
+ if (candidateHasCachePricing !== existingHasCachePricing) {
749
+ return candidateHasCachePricing;
750
+ }
751
+ return existing.provider !== "openai" && candidate.provider === "openai";
752
+ }
753
+
754
+ function buildCustomReferenceMap(): Map<string, Model<Api>> {
755
+ const references = new Map<string, Model<Api>>();
756
+ for (const provider of getBundledProviders()) {
757
+ for (const model of getBundledModels(provider as Parameters<typeof getBundledModels>[0])) {
758
+ const candidate = model as Model<Api>;
759
+ if (shouldReplaceCustomReference(references.get(candidate.id), candidate)) {
760
+ references.set(candidate.id, candidate);
761
+ }
762
+ }
763
+ }
764
+ return references;
765
+ }
766
+
767
+ const customReferenceMap = buildCustomReferenceMap();
768
+
769
+ function getCustomReferenceCandidateIds(modelId: string): string[] {
770
+ const candidates = new Set<string>();
771
+ const queue = [modelId];
772
+ for (let index = 0; index < queue.length; index += 1) {
773
+ const candidate = queue[index]?.trim();
774
+ if (!candidate || candidates.has(candidate)) continue;
775
+ candidates.add(candidate);
776
+
777
+ for (const suffix of [":cloud", "-cloud"] as const) {
778
+ if (candidate.toLowerCase().endsWith(suffix)) {
779
+ queue.push(candidate.slice(0, -suffix.length));
780
+ }
781
+ }
782
+
783
+ const colonToDash = candidate.replace(/:/g, "-");
784
+ if (colonToDash !== candidate) {
785
+ queue.push(colonToDash);
786
+ }
787
+ }
788
+ return [...candidates];
789
+ }
790
+
791
+ function resolveCustomModelReference(modelId: string): Model<Api> | undefined {
792
+ for (const candidate of getCustomReferenceCandidateIds(modelId)) {
793
+ const reference = customReferenceMap.get(candidate);
794
+ if (reference) return reference;
795
+ }
796
+ return undefined;
797
+ }
798
+
729
799
  function applyStandaloneCustomModelPolicies(model: CustomModelOverlay): CustomModelOverlay {
730
800
  if (model.id !== "gpt-5.4" || model.provider === "github-copilot" || model.contextWindow !== undefined) {
731
801
  return model;
@@ -735,23 +805,27 @@ function applyStandaloneCustomModelPolicies(model: CustomModelOverlay): CustomMo
735
805
 
736
806
  function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuildOptions): Model<Api> {
737
807
  const resolvedModel = options.useDefaults ? applyStandaloneCustomModelPolicies(model) : model;
808
+ const reference = options.useDefaults ? resolveCustomModelReference(resolvedModel.id) : undefined;
738
809
  const cost =
739
- resolvedModel.cost ?? (options.useDefaults ? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } : undefined);
740
- const input = resolvedModel.input ?? (options.useDefaults ? ["text"] : undefined);
810
+ resolvedModel.cost ??
811
+ reference?.cost ??
812
+ (options.useDefaults ? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } : undefined);
813
+ const input = resolvedModel.input ?? reference?.input ?? (options.useDefaults ? ["text"] : undefined);
741
814
  return enrichModelThinking({
742
815
  id: resolvedModel.id,
743
816
  name: resolvedModel.name ?? (options.useDefaults ? resolvedModel.id : undefined),
744
817
  api: resolvedModel.api,
745
818
  provider: resolvedModel.provider,
746
819
  baseUrl: resolvedModel.baseUrl,
747
- reasoning: resolvedModel.reasoning ?? (options.useDefaults ? false : undefined),
748
- thinking: resolvedModel.thinking,
820
+ reasoning: resolvedModel.reasoning ?? reference?.reasoning ?? (options.useDefaults ? false : undefined),
821
+ thinking: resolvedModel.thinking ?? reference?.thinking,
749
822
  input: input as ("text" | "image")[],
750
823
  cost,
751
- contextWindow: resolvedModel.contextWindow ?? (options.useDefaults ? 128000 : undefined),
752
- maxTokens: resolvedModel.maxTokens ?? (options.useDefaults ? 16384 : undefined),
824
+ contextWindow:
825
+ resolvedModel.contextWindow ?? reference?.contextWindow ?? (options.useDefaults ? 128000 : undefined),
826
+ maxTokens: resolvedModel.maxTokens ?? reference?.maxTokens ?? (options.useDefaults ? 16384 : undefined),
753
827
  headers: resolvedModel.headers,
754
- compat: resolvedModel.compat,
828
+ compat: mergeCompat(reference?.compat, resolvedModel.compat),
755
829
  contextPromotionTarget: resolvedModel.contextPromotionTarget,
756
830
  premiumMultiplier: resolvedModel.premiumMultiplier,
757
831
  isOAuth: resolvedModel.isOAuth,
@@ -954,10 +1028,9 @@ export class ModelRegistry {
954
1028
 
955
1029
  return models.map(m => {
956
1030
  if (!providerOverride) return m;
1031
+ const withTransportOverride = this.#applyProviderTransportOverride(m, providerOverride);
957
1032
  return {
958
- ...m,
959
- baseUrl: providerOverride.baseUrl ?? m.baseUrl,
960
- headers: providerOverride.headers ? { ...m.headers, ...providerOverride.headers } : m.headers,
1033
+ ...withTransportOverride,
961
1034
  compat: mergeCompat(m.compat, providerOverride.compat),
962
1035
  };
963
1036
  });
@@ -1143,11 +1216,12 @@ export class ModelRegistry {
1143
1216
  const configuredProviders = new Set(Object.keys(value.providers ?? {}));
1144
1217
 
1145
1218
  for (const [providerName, providerConfig] of providerEntries) {
1146
- // Always set overrides when baseUrl/headers/apiKey/compat/disableStrictTools are present
1219
+ // Always set overrides when baseUrl/headers/apiKey/authHeader/compat/disableStrictTools are present
1147
1220
  if (
1148
1221
  providerConfig.baseUrl ||
1149
1222
  providerConfig.headers ||
1150
1223
  providerConfig.apiKey ||
1224
+ providerConfig.authHeader !== undefined ||
1151
1225
  providerConfig.compat ||
1152
1226
  providerConfig.disableStrictTools
1153
1227
  ) {
@@ -1156,6 +1230,7 @@ export class ModelRegistry {
1156
1230
  baseUrl: providerConfig.baseUrl,
1157
1231
  headers: providerConfig.headers,
1158
1232
  apiKey: providerConfig.apiKey,
1233
+ authHeader: providerConfig.authHeader,
1159
1234
  compat: mergeCompat(providerConfig.compat, disableStrictCompat),
1160
1235
  });
1161
1236
  }
@@ -1738,18 +1813,24 @@ export class ModelRegistry {
1738
1813
  return {
1739
1814
  baseUrl: override.baseUrl ?? baseOverride?.baseUrl,
1740
1815
  apiKey: override.apiKey ?? baseOverride?.apiKey,
1816
+ authHeader: override.authHeader ?? baseOverride?.authHeader,
1741
1817
  headers: override.headers ? { ...(baseOverride?.headers ?? {}), ...override.headers } : baseOverride?.headers,
1742
1818
  compat: override.compat ? mergeCompat(baseOverride?.compat, override.compat) : baseOverride?.compat,
1743
1819
  };
1744
1820
  }
1745
1821
  #applyProviderTransportOverride<T extends { baseUrl?: string; headers?: Record<string, string> }>(
1746
1822
  entry: T,
1747
- override: Pick<ProviderOverride, "baseUrl" | "headers">,
1823
+ override: Pick<ProviderOverride, "baseUrl" | "headers" | "authHeader" | "apiKey">,
1748
1824
  ): T {
1825
+ const headers = mergeAuthHeader(
1826
+ override.headers ? { ...entry.headers, ...override.headers } : entry.headers,
1827
+ override.authHeader,
1828
+ override.apiKey,
1829
+ );
1749
1830
  return {
1750
1831
  ...entry,
1751
1832
  baseUrl: override.baseUrl ?? entry.baseUrl,
1752
- headers: override.headers ? { ...entry.headers, ...override.headers } : entry.headers,
1833
+ headers,
1753
1834
  };
1754
1835
  }
1755
1836
  #applyRuntimeProviderOverrides(models: Model<Api>[]): Model<Api>[] {
@@ -2206,8 +2287,13 @@ export class ModelRegistry {
2206
2287
  return;
2207
2288
  }
2208
2289
 
2209
- if (config.baseUrl || config.headers) {
2210
- const transportOverride = { baseUrl: config.baseUrl, headers: config.headers };
2290
+ if (config.baseUrl || config.headers || config.apiKey || config.authHeader !== undefined) {
2291
+ const transportOverride = {
2292
+ baseUrl: config.baseUrl,
2293
+ headers: config.headers,
2294
+ apiKey: config.apiKey,
2295
+ authHeader: config.authHeader,
2296
+ };
2211
2297
  const nextRuntimeOverride = this.#mergeProviderOverride(
2212
2298
  this.#runtimeProviderOverrides.get(providerName),
2213
2299
  transportOverride,
@@ -1955,6 +1955,30 @@ export const SETTINGS_SCHEMA = {
1955
1955
  default: 60_000,
1956
1956
  },
1957
1957
 
1958
+ // Tool Discovery
1959
+ "tools.discoveryMode": {
1960
+ type: "enum",
1961
+ values: ["off", "mcp-only", "all"] as const,
1962
+ default: "off",
1963
+ ui: {
1964
+ tab: "tools",
1965
+ label: "Tool Discovery",
1966
+ description:
1967
+ "Hide tools behind a search tool to save tokens. 'mcp-only' hides MCP tools; 'all' hides all non-essential built-ins too.",
1968
+ },
1969
+ },
1970
+
1971
+ "tools.essentialOverride": {
1972
+ type: "array",
1973
+ default: [] as string[],
1974
+ ui: {
1975
+ tab: "tools",
1976
+ label: "Essential Tools Override",
1977
+ description:
1978
+ "Override the always-loaded built-in tools (default: read, bash, edit). Leave empty to use defaults.",
1979
+ },
1980
+ },
1981
+
1958
1982
  // MCP
1959
1983
  "mcp.enableProjectConfig": {
1960
1984
  type: "boolean",
@@ -2006,6 +2030,17 @@ export const SETTINGS_SCHEMA = {
2006
2030
  // Tasks
2007
2031
  // ────────────────────────────────────────────────────────────────────────
2008
2032
 
2033
+ // Plan mode
2034
+ "plan.enabled": {
2035
+ type: "boolean",
2036
+ default: true,
2037
+ ui: {
2038
+ tab: "tasks",
2039
+ label: "Plan Mode",
2040
+ description: "Enable plan mode for read-only exploration and planning before execution",
2041
+ },
2042
+ },
2043
+
2009
2044
  // Delegation
2010
2045
  "task.isolation.mode": {
2011
2046
  type: "enum",
@@ -2278,7 +2313,7 @@ export const SETTINGS_SCHEMA = {
2278
2313
  { value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY and Kagi Search API beta access" },
2279
2314
  { value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
2280
2315
  { value: "parallel", label: "Parallel", description: "Requires PARALLEL_API_KEY" },
2281
- { value: "searxng", label: "SearXNG", description: "Requires searxng.endpoint" },
2316
+ { value: "searxng", label: "SearXNG", description: "Requires SEARXNG_ENDPOINT or searxng.endpoint" },
2282
2317
  ],
2283
2318
  },
2284
2319
  },
@@ -811,9 +811,10 @@ export async function listClaudePluginRoots(
811
811
 
812
812
  // ── OMP installed plugins registry ───────────────────────────────────────
813
813
  // OMP registry is authoritative: its entries replace Claude's entries for the same plugin ID.
814
- // getPluginsDir() resolves to the same path the marketplace writer uses
815
- // (XDG-aware via the dir resolver), so reads and writes always agree.
816
- const ompRegistryPath = path.join(getPluginsDir(), "installed_plugins.json");
814
+ // In production `home` is `os.homedir()`, so `getPluginsDir(home)` resolves to the
815
+ // same XDG-aware path the marketplace writer uses (reads and writes always agree).
816
+ // Tests pass a temp dir, which short-circuits the resolver for deterministic isolation.
817
+ const ompRegistryPath = path.join(getPluginsDir(home), "installed_plugins.json");
817
818
  const ompContent = await readFile(ompRegistryPath);
818
819
  if (ompContent) {
819
820
  const ompRegistry = parseClaudePluginsRegistry(ompContent);
package/src/edit/index.ts CHANGED
@@ -246,6 +246,7 @@ async function executeSinglePathEntries(
246
246
  export class EditTool implements AgentTool<TInput> {
247
247
  readonly name = "edit";
248
248
  readonly label = "Edit";
249
+ readonly loadMode = "essential";
249
250
  readonly nonAbortable = true;
250
251
  readonly concurrency = "exclusive";
251
252
  readonly strict = true;
@@ -2,13 +2,12 @@ import * as fs from "node:fs";
2
2
  import { createServer } from "node:net";
3
3
  import * as path from "node:path";
4
4
  import { Process } from "@oh-my-pi/pi-natives";
5
- import { getAgentDir, isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
5
+ import { getPythonGatewayDir, isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
6
6
  import type { Subprocess } from "bun";
7
7
  import { Settings } from "../../config/settings";
8
8
  import { getOrCreateSnapshot } from "../../utils/shell-snapshot";
9
9
  import { filterEnv, resolvePythonRuntime } from "./runtime";
10
10
 
11
- const GATEWAY_DIR_NAME = "python-gateway";
12
11
  const GATEWAY_INFO_FILE = "gateway.json";
13
12
  const GATEWAY_LOCK_FILE = "gateway.lock";
14
13
  const GATEWAY_STARTUP_TIMEOUT_MS = 30000;
@@ -66,7 +65,7 @@ async function allocatePort(): Promise<number> {
66
65
  }
67
66
 
68
67
  function getGatewayDir(): string {
69
- return path.join(getAgentDir(), GATEWAY_DIR_NAME);
68
+ return getPythonGatewayDir();
70
69
  }
71
70
 
72
71
  function getGatewayInfoPath(): string {
@@ -34,6 +34,7 @@ const DEFAULT_ENV_ALLOWLIST = new Set([
34
34
  "CONDA_DEFAULT_ENV",
35
35
  "VIRTUAL_ENV",
36
36
  "PYTHONPATH",
37
+ "LD_LIBRARY_PATH",
37
38
  ]);
38
39
 
39
40
  const WINDOWS_ENV_ALLOWLIST = new Set([