@oh-my-pi/pi-coding-agent 14.7.0 → 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 (61) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +12 -12
  3. package/src/cli/grep-cli.ts +1 -1
  4. package/src/config/model-equivalence.ts +1 -0
  5. package/src/config/model-registry.ts +108 -22
  6. package/src/config/settings-schema.ts +46 -1
  7. package/src/config/settings.ts +71 -1
  8. package/src/dap/client.ts +1 -0
  9. package/src/discovery/builtin.ts +34 -9
  10. package/src/discovery/helpers.ts +4 -3
  11. package/src/edit/index.ts +1 -0
  12. package/src/edit/modes/hashline.ts +212 -63
  13. package/src/eval/py/gateway-coordinator.ts +2 -3
  14. package/src/eval/py/runtime.ts +1 -0
  15. package/src/internal-urls/docs-index.generated.ts +2 -2
  16. package/src/lsp/index.ts +2 -0
  17. package/src/main.ts +10 -15
  18. package/src/mcp/discoverable-tool-metadata.ts +24 -202
  19. package/src/modes/components/extensions/extension-dashboard.ts +26 -2
  20. package/src/modes/components/extensions/state-manager.ts +41 -0
  21. package/src/modes/controllers/selector-controller.ts +3 -0
  22. package/src/modes/interactive-mode.ts +45 -13
  23. package/src/prompts/system/plan-mode-active.md +7 -3
  24. package/src/prompts/system/plan-mode-approved.md +5 -0
  25. package/src/prompts/tools/search-tool-bm25.md +14 -14
  26. package/src/prompts/tools/todo-write.md +1 -0
  27. package/src/sdk.ts +69 -8
  28. package/src/session/agent-session.ts +177 -1
  29. package/src/slash-commands/builtin-registry.ts +13 -2
  30. package/src/task/index.ts +2 -0
  31. package/src/task/isolation-backend.ts +22 -0
  32. package/src/tool-discovery/tool-index.ts +377 -0
  33. package/src/tools/ask.ts +2 -0
  34. package/src/tools/ast-edit.ts +2 -0
  35. package/src/tools/ast-grep.ts +2 -0
  36. package/src/tools/bash.ts +1 -0
  37. package/src/tools/browser.ts +2 -0
  38. package/src/tools/calculator.ts +2 -0
  39. package/src/tools/checkpoint.ts +4 -0
  40. package/src/tools/debug.ts +2 -0
  41. package/src/tools/eval.ts +2 -0
  42. package/src/tools/find.ts +2 -0
  43. package/src/tools/gh.ts +2 -0
  44. package/src/tools/hindsight-recall.ts +2 -0
  45. package/src/tools/hindsight-reflect.ts +2 -0
  46. package/src/tools/hindsight-retain.ts +2 -0
  47. package/src/tools/index.ts +74 -14
  48. package/src/tools/inspect-image.ts +2 -0
  49. package/src/tools/irc.ts +2 -1
  50. package/src/tools/job.ts +2 -1
  51. package/src/tools/notebook.ts +2 -0
  52. package/src/tools/read.ts +7 -1
  53. package/src/tools/recipe/index.ts +2 -0
  54. package/src/tools/render-mermaid.ts +2 -0
  55. package/src/tools/search-tool-bm25.ts +128 -42
  56. package/src/tools/search.ts +2 -0
  57. package/src/tools/ssh.ts +2 -0
  58. package/src/tools/todo-write.ts +2 -1
  59. package/src/tools/write.ts +2 -0
  60. package/src/web/search/index.ts +2 -0
  61. package/src/web/search/providers/searxng.ts +8 -0
package/CHANGELOG.md CHANGED
@@ -1,9 +1,33 @@
1
1
  # Changelog
2
2
 
3
3
  ## [Unreleased]
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
+
20
+ ## [14.7.1] - 2026-05-06
21
+
4
22
  ### Added
5
23
 
6
24
  - Added `pr_create` operation to the GitHub tool to create pull requests with title/body (or `fill`), base/head branch, draft, reviewer, assignee, and label options and return a summarized result including the new PR URL
25
+ - Added `read.summarize.prose` setting to keep Markdown and plain-text reads out of the structural summarizer by default.
26
+
27
+ ### Changed
28
+
29
+ - Changed the `PI_GREP_WORKERS` environment variable help text to state that it sets filesystem walker workers, defaults to 4, and uses `0` for automatic worker selection
30
+ - Changed hashline replacement and pure-insert auto-absorb to also drop a single duplicated structural-closing line (`}`, `);`, `]`, etc.) on either boundary when keeping it would unbalance brackets. The pure-insert variant fires regardless of `edit.hashlineAutoDropPureInsertDuplicates`, while the existing 2+ line generic absorb stays gated on that setting.
7
31
 
8
32
  ## [14.7.0] - 2026-05-04
9
33
  ### Breaking Changes
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.0",
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",
@@ -44,16 +44,17 @@
44
44
  "generate-template": "bun scripts/generate-template.ts"
45
45
  },
46
46
  "dependencies": {
47
- "@agentclientprotocol/sdk": "0.20.0",
47
+ "@agentclientprotocol/sdk": "0.21.0",
48
48
  "@mozilla/readability": "^0.6.0",
49
- "@oh-my-pi/omp-stats": "14.7.0",
50
- "@oh-my-pi/pi-agent-core": "14.7.0",
51
- "@oh-my-pi/pi-ai": "14.7.0",
52
- "@oh-my-pi/pi-natives": "14.7.0",
53
- "@oh-my-pi/pi-tui": "14.7.0",
54
- "@oh-my-pi/pi-utils": "14.7.0",
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
+ "@types/turndown": "5.0.6",
57
58
  "@xterm/headless": "^6.0.0",
58
59
  "ajv": "^8.20.0",
59
60
  "chalk": "^5.6.2",
@@ -61,16 +62,15 @@
61
62
  "fflate": "0.8.2",
62
63
  "handlebars": "^4.7.9",
63
64
  "linkedom": "^0.18.12",
64
- "lru-cache": "11.3.5",
65
+ "lru-cache": "11.3.6",
65
66
  "markit-ai": "0.5.3",
66
67
  "puppeteer-core": "^24.42.0",
67
68
  "turndown": "7.2.4",
68
69
  "turndown-plugin-gfm": "1.0.2",
69
- "zod": "4.3.6"
70
+ "zod": "4.4.3"
70
71
  },
71
72
  "devDependencies": {
72
- "@types/bun": "^1.3",
73
- "@types/turndown": "5.0.6"
73
+ "@types/bun": "^1.3.13"
74
74
  },
75
75
  "engines": {
76
76
  "bun": ">=1.3.7"
@@ -150,7 +150,7 @@ ${chalk.bold("Options:")}
150
150
  --no-gitignore Include files excluded by .gitignore
151
151
 
152
152
  ${chalk.bold("Environment:")}
153
- PI_GREP_WORKERS=0 Disable worker pool (use single-threaded mode)
153
+ PI_GREP_WORKERS=N Set filesystem walker workers (default 4, 0 = auto)
154
154
 
155
155
  ${chalk.bold("Examples:")}
156
156
  ${APP_NAME} grep "import" src/
@@ -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,
@@ -1485,6 +1485,16 @@ export const SETTINGS_SCHEMA = {
1485
1485
  },
1486
1486
  },
1487
1487
 
1488
+ "read.summarize.prose": {
1489
+ type: "boolean",
1490
+ default: false,
1491
+ ui: {
1492
+ tab: "editing",
1493
+ label: "Prose Summaries",
1494
+ description: "Return structural summaries for Markdown and plain text reads",
1495
+ },
1496
+ },
1497
+
1488
1498
  "read.summarize.minBodyLines": {
1489
1499
  type: "number",
1490
1500
  default: 4,
@@ -1945,6 +1955,30 @@ export const SETTINGS_SCHEMA = {
1945
1955
  default: 60_000,
1946
1956
  },
1947
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
+
1948
1982
  // MCP
1949
1983
  "mcp.enableProjectConfig": {
1950
1984
  type: "boolean",
@@ -1996,6 +2030,17 @@ export const SETTINGS_SCHEMA = {
1996
2030
  // Tasks
1997
2031
  // ────────────────────────────────────────────────────────────────────────
1998
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
+
1999
2044
  // Delegation
2000
2045
  "task.isolation.mode": {
2001
2046
  type: "enum",
@@ -2268,7 +2313,7 @@ export const SETTINGS_SCHEMA = {
2268
2313
  { value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY and Kagi Search API beta access" },
2269
2314
  { value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
2270
2315
  { value: "parallel", label: "Parallel", description: "Requires PARALLEL_API_KEY" },
2271
- { value: "searxng", label: "SearXNG", description: "Requires searxng.endpoint" },
2316
+ { value: "searxng", label: "SearXNG", description: "Requires SEARXNG_ENDPOINT or searxng.endpoint" },
2272
2317
  ],
2273
2318
  },
2274
2319
  },
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import * as fs from "node:fs";
15
+ import * as os from "node:os";
15
16
  import * as path from "node:path";
16
17
  import {
17
18
  getAgentDbPath,
@@ -98,6 +99,74 @@ function setByPath(obj: RawSettings, segments: string[], value: unknown): void {
98
99
  current[segments[segments.length - 1]] = value;
99
100
  }
100
101
 
102
+ const PATH_SCOPED_ARRAY_SETTINGS = new Set<SettingPath>(["enabledModels", "disabledProviders"]);
103
+
104
+ type PathScopedStringArrayEntry = {
105
+ path?: unknown;
106
+ paths?: unknown;
107
+ pathPrefix?: unknown;
108
+ pathPrefixes?: unknown;
109
+ values?: unknown;
110
+ items?: unknown;
111
+ models?: unknown;
112
+ providers?: unknown;
113
+ };
114
+
115
+ function normalizePathPrefix(prefix: string): string {
116
+ const expanded =
117
+ prefix === "~" ? os.homedir() : prefix.startsWith("~/") ? path.join(os.homedir(), prefix.slice(2)) : prefix;
118
+ return path.resolve(expanded);
119
+ }
120
+
121
+ function pathMatchesPrefix(cwd: string, prefix: string): boolean {
122
+ const relative = path.relative(normalizePathPrefix(prefix), path.resolve(cwd));
123
+ return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
124
+ }
125
+
126
+ function stringArrayFromUnknown(value: unknown): string[] {
127
+ if (typeof value === "string") return [value];
128
+ if (Array.isArray(value)) return value.filter((item): item is string => typeof item === "string");
129
+ return [];
130
+ }
131
+
132
+ function resolvePathScopedStringArray(settingPath: SettingPath, value: unknown, cwd: string): string[] | undefined {
133
+ if (!PATH_SCOPED_ARRAY_SETTINGS.has(settingPath) || !Array.isArray(value)) return undefined;
134
+
135
+ const resolved: string[] = [];
136
+ for (const entry of value) {
137
+ if (typeof entry === "string") {
138
+ resolved.push(entry);
139
+ continue;
140
+ }
141
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
142
+
143
+ const scoped = entry as PathScopedStringArrayEntry;
144
+ const prefixes = [
145
+ ...stringArrayFromUnknown(scoped.path),
146
+ ...stringArrayFromUnknown(scoped.paths),
147
+ ...stringArrayFromUnknown(scoped.pathPrefix),
148
+ ...stringArrayFromUnknown(scoped.pathPrefixes),
149
+ ];
150
+ if (prefixes.length === 0 || !prefixes.some(prefix => pathMatchesPrefix(cwd, prefix))) continue;
151
+
152
+ const values =
153
+ settingPath === "enabledModels"
154
+ ? [
155
+ ...stringArrayFromUnknown(scoped.values),
156
+ ...stringArrayFromUnknown(scoped.items),
157
+ ...stringArrayFromUnknown(scoped.models),
158
+ ]
159
+ : [
160
+ ...stringArrayFromUnknown(scoped.values),
161
+ ...stringArrayFromUnknown(scoped.items),
162
+ ...stringArrayFromUnknown(scoped.providers),
163
+ ];
164
+ resolved.push(...values);
165
+ }
166
+
167
+ return resolved;
168
+ }
169
+
101
170
  // ═══════════════════════════════════════════════════════════════════════════
102
171
  // Settings Class
103
172
  // ═══════════════════════════════════════════════════════════════════════════
@@ -201,7 +270,8 @@ export class Settings {
201
270
  const segments = path.split(".");
202
271
  const value = getByPath(this.#merged, segments);
203
272
  if (value !== undefined) {
204
- return value as SettingValue<P>;
273
+ const pathScopedValue = resolvePathScopedStringArray(path, value, this.#cwd);
274
+ return (pathScopedValue ?? value) as SettingValue<P>;
205
275
  }
206
276
  return getDefault(path);
207
277
  }
package/src/dap/client.ts CHANGED
@@ -584,6 +584,7 @@ function socketToSink(socket: Bun.Socket<undefined>): DapWriteSink {
584
584
  },
585
585
  flush() {
586
586
  socket.flush();
587
+ return undefined;
587
588
  },
588
589
  };
589
590
  }
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import * as path from "node:path";
7
7
  import { logger, parseFrontmatter, tryParseJson } from "@oh-my-pi/pi-utils";
8
+ import { YAML } from "bun";
8
9
  import { registerProvider } from "../capability";
9
10
  import { type ContextFile, contextFileCapability } from "../capability/context-file";
10
11
  import { type Extension, type ExtensionManifest, extensionCapability } from "../capability/extension";
@@ -778,22 +779,46 @@ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
778
779
  const items: Settings[] = [];
779
780
  const warnings: string[] = [];
780
781
 
782
+ const parseYamlSettings = (content: string, filePath: string): Record<string, unknown> | null => {
783
+ try {
784
+ const data = YAML.parse(content);
785
+ if (!data || typeof data !== "object" || Array.isArray(data)) return {};
786
+ return data as Record<string, unknown>;
787
+ } catch {
788
+ warnings.push(`Failed to parse ${filePath}`);
789
+ return null;
790
+ }
791
+ };
792
+
781
793
  for (const { dir, level } of await getConfigDirs(ctx)) {
782
794
  const settingsPath = path.join(dir, "settings.json");
783
- const content = await readFile(settingsPath);
784
- if (!content) continue;
785
-
786
- const data = tryParseJson<Record<string, unknown>>(content);
787
- if (!data) {
788
- warnings.push(`Failed to parse ${settingsPath}`);
789
- continue;
795
+ const settingsContent = await readFile(settingsPath);
796
+ if (settingsContent) {
797
+ const data = tryParseJson<Record<string, unknown>>(settingsContent);
798
+ if (data) {
799
+ items.push({
800
+ path: settingsPath,
801
+ data,
802
+ level,
803
+ _source: createSourceMeta(PROVIDER_ID, settingsPath, level),
804
+ });
805
+ } else {
806
+ warnings.push(`Failed to parse ${settingsPath}`);
807
+ }
790
808
  }
791
809
 
810
+ const configPath = path.join(dir, "config.yml");
811
+ const configContent = await readFile(configPath);
812
+ if (!configContent) continue;
813
+
814
+ const data = parseYamlSettings(configContent, configPath);
815
+ if (!data) continue;
816
+
792
817
  items.push({
793
- path: settingsPath,
818
+ path: configPath,
794
819
  data,
795
820
  level,
796
- _source: createSourceMeta(PROVIDER_ID, settingsPath, level),
821
+ _source: createSourceMeta(PROVIDER_ID, configPath, level),
797
822
  });
798
823
  }
799
824
 
@@ -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;