@oh-my-pi/pi-coding-agent 13.5.8 → 13.6.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 (49) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/package.json +7 -7
  3. package/src/cli/args.ts +7 -0
  4. package/src/cli/stats-cli.ts +5 -0
  5. package/src/cli/update-cli.ts +127 -67
  6. package/src/config/model-registry.ts +100 -22
  7. package/src/config/settings-schema.ts +22 -2
  8. package/src/extensibility/extensions/types.ts +2 -0
  9. package/src/internal-urls/docs-index.generated.ts +2 -2
  10. package/src/internal-urls/index.ts +2 -1
  11. package/src/internal-urls/mcp-protocol.ts +156 -0
  12. package/src/internal-urls/router.ts +1 -1
  13. package/src/internal-urls/types.ts +3 -3
  14. package/src/mcp/client.ts +235 -2
  15. package/src/mcp/index.ts +1 -1
  16. package/src/mcp/manager.ts +399 -5
  17. package/src/mcp/oauth-flow.ts +26 -1
  18. package/src/mcp/smithery-auth.ts +104 -0
  19. package/src/mcp/smithery-connect.ts +145 -0
  20. package/src/mcp/smithery-registry.ts +455 -0
  21. package/src/mcp/types.ts +140 -0
  22. package/src/modes/components/footer.ts +10 -4
  23. package/src/modes/components/settings-defs.ts +15 -1
  24. package/src/modes/components/status-line/git-utils.ts +42 -0
  25. package/src/modes/components/status-line/presets.ts +6 -6
  26. package/src/modes/components/status-line/segments.ts +27 -4
  27. package/src/modes/components/status-line/types.ts +2 -0
  28. package/src/modes/components/status-line-segment-editor.ts +1 -0
  29. package/src/modes/components/status-line.ts +109 -5
  30. package/src/modes/controllers/command-controller.ts +12 -2
  31. package/src/modes/controllers/extension-ui-controller.ts +12 -21
  32. package/src/modes/controllers/mcp-command-controller.ts +577 -14
  33. package/src/modes/controllers/selector-controller.ts +5 -0
  34. package/src/modes/theme/theme.ts +6 -0
  35. package/src/prompts/tools/hashline.md +4 -3
  36. package/src/sdk.ts +115 -3
  37. package/src/session/agent-session.ts +19 -4
  38. package/src/session/session-manager.ts +17 -5
  39. package/src/slash-commands/builtin-registry.ts +10 -0
  40. package/src/task/executor.ts +37 -3
  41. package/src/task/index.ts +37 -5
  42. package/src/task/isolation-backend.ts +72 -0
  43. package/src/task/render.ts +6 -1
  44. package/src/task/types.ts +1 -0
  45. package/src/task/worktree.ts +67 -5
  46. package/src/tools/index.ts +1 -1
  47. package/src/tools/path-utils.ts +2 -1
  48. package/src/tools/read.ts +3 -7
  49. package/src/utils/open.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.6.1] - 2026-03-03
6
+
7
+ ### Fixed
8
+
9
+ - Fixed `omp update` silently succeeding without actually updating the binary when the update channel (bun global vs compiled binary) doesn't match the installation method ([#247](https://github.com/can1357/oh-my-pi/issues/247))
10
+ - Added post-update verification that checks the resolved `omp` binary reports the expected version, with actionable warnings on mismatch
11
+ - `omp update` now detects when the `omp` in PATH is not managed by bun and falls back to binary replacement instead of updating the wrong location
12
+ ## [13.6.0] - 2026-03-03
13
+ ### Added
14
+
15
+ - Added `mcp://` internal URL protocol for reading MCP server resources directly via the read tool (e.g., `read(path="mcp://resource-uri")`)
16
+ - Added LM Studio integration to the model registry and discovery flow.
17
+ - Added support for authenticating with LM Studio using the `/login lm-studio` command.
18
+ - Added `fuse-projfs` task isolation mode for Windows ProjFS-backed overlays.
19
+ - Added `/mcp registry search <keyword>` integration with Smithery, including interactive result selection, editable server naming before deploy, Smithery `configSchema` prompts, and immediate runtime reload so selected MCP tools are available without restarting
20
+ - Added OAuth failure fallback in `/mcp registry search` deploy flow to prompt for manual bearer tokens and validate them before saving configuration
21
+ - Added Smithery auth support for `/mcp registry search` with cached API key login (`/mcp registry login`, `/mcp registry logout`) and automatic login prompt/retry on auth or rate-limit responses
22
+
23
+ ### Changed
24
+
25
+ - Updated MCP resource update notifications to recommend using `read(path="mcp://<uri>")` instead of the deprecated `read_resource` tool
26
+ - Updated Anthropic Foundry environment variable documentation and CLI help text to the canonical names: `CLAUDE_CODE_USE_FOUNDRY`, `CLAUDE_CODE_CLIENT_CERT`, and `CLAUDE_CODE_CLIENT_KEY`
27
+ - Documented Foundry-specific Anthropic runtime configuration (`FOUNDRY_BASE_URL`, `ANTHROPIC_FOUNDRY_API_KEY`, `ANTHROPIC_CUSTOM_HEADERS`, `NODE_EXTRA_CA_CERTS`) in environment variable reference docs
28
+ - `fuse-overlay` task isolation now targets `fuse-overlayfs` on Unix hosts only; on Windows it falls back to `worktree` with a `<system-notification>` suggesting `fuse-projfs`.
29
+ - `fuse-projfs` now performs Windows ProjFS preflight checks and falls back to `worktree` when host or repository prerequisites are unavailable.
30
+ - Cross-repo patch capture now uses the platform null device (`NUL` on Windows, `/dev/null` elsewhere) for `git diff --no-index`.
31
+
32
+ ### Removed
33
+
34
+ - Removed `read_resource` tool; MCP resource reading is now integrated into the `read` tool via `mcp://` URLs
35
+
36
+ ### Fixed
37
+
38
+ - Fixed MCP resource subscription handling to prevent unsubscribing when notifications are re-enabled after being disabled
39
+ - Fixed LM Studio base URL validation to preserve invalid configured URLs instead of silently falling back to localhost
40
+ - Fixed URI template matching to correctly handle expressions that expand to empty strings
41
+
5
42
  ## [13.5.6] - 2026-03-01
6
43
  ### Changed
7
44
 
@@ -1051,7 +1088,6 @@
1051
1088
  - Improved error reporting in fetch tool to include HTTP status codes when URL fetching fails
1052
1089
  - Fixed fetch tool to preserve actual response metadata (finalUrl, contentType) instead of defaults when requests fail
1053
1090
 
1054
- ||||||| parent of a70a34c8b (fix(coding-agent/debug): Sanitized debug log rendering)
1055
1091
 
1056
1092
  ## [12.1.0] - 2026-02-13
1057
1093
 
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": "13.5.8",
4
+ "version": "13.6.1",
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",
@@ -41,12 +41,12 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@mozilla/readability": "^0.6",
44
- "@oh-my-pi/omp-stats": "13.5.8",
45
- "@oh-my-pi/pi-agent-core": "13.5.8",
46
- "@oh-my-pi/pi-ai": "13.5.8",
47
- "@oh-my-pi/pi-natives": "13.5.8",
48
- "@oh-my-pi/pi-tui": "13.5.8",
49
- "@oh-my-pi/pi-utils": "13.5.8",
44
+ "@oh-my-pi/omp-stats": "13.6.1",
45
+ "@oh-my-pi/pi-agent-core": "13.6.1",
46
+ "@oh-my-pi/pi-ai": "13.6.1",
47
+ "@oh-my-pi/pi-natives": "13.6.1",
48
+ "@oh-my-pi/pi-tui": "13.6.1",
49
+ "@oh-my-pi/pi-utils": "13.6.1",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
package/src/cli/args.ts CHANGED
@@ -191,6 +191,13 @@ export function getExtraHelpText(): string {
191
191
  ${chalk.dim("# Core Providers")}
192
192
  ANTHROPIC_API_KEY - Anthropic Claude models
193
193
  ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth (takes precedence over API key)
194
+ CLAUDE_CODE_USE_FOUNDRY - Enable Anthropic Foundry mode (uses Foundry endpoint + mTLS)
195
+ FOUNDRY_BASE_URL - Anthropic Foundry base URL (e.g., https://<foundry-host>)
196
+ ANTHROPIC_FOUNDRY_API_KEY - Anthropic token used as Authorization: Bearer <token> in Foundry mode
197
+ ANTHROPIC_CUSTOM_HEADERS - Extra Foundry headers (e.g., "user-id: USERNAME")
198
+ CLAUDE_CODE_CLIENT_CERT - Client certificate (PEM path or inline PEM) for mTLS
199
+ CLAUDE_CODE_CLIENT_KEY - Client private key (PEM path or inline PEM) for mTLS
200
+ NODE_EXTRA_CA_CERTS - CA bundle path (or inline PEM) for server certificate validation
194
201
  OPENAI_API_KEY - OpenAI GPT models
195
202
  GEMINI_API_KEY - Google Gemini models
196
203
  GITHUB_TOKEN - GitHub Copilot (or GH_TOKEN, COPILOT_GITHUB_TOKEN)
@@ -59,6 +59,10 @@ function formatCost(n: number): string {
59
59
  return `$${n.toFixed(2)}`;
60
60
  }
61
61
 
62
+ function normalizePremiumRequests(n: number): number {
63
+ return Math.round((n + Number.EPSILON) * 100) / 100;
64
+ }
65
+
62
66
  // =============================================================================
63
67
  // Command Handler
64
68
  // =============================================================================
@@ -120,6 +124,7 @@ async function printStatsSummary(): Promise<void> {
120
124
  console.log(` Total Tokens: ${formatNumber(overall.totalInputTokens + overall.totalOutputTokens)}`);
121
125
  console.log(` Cache Rate: ${formatPercent(overall.cacheRate)}`);
122
126
  console.log(` Total Cost: ${formatCost(overall.totalCost)}`);
127
+ console.log(` Premium Requests: ${formatNumber(normalizePremiumRequests(overall.totalPremiumRequests ?? 0))}`);
123
128
  console.log(` Avg Duration: ${overall.avgDuration !== null ? formatDuration(overall.avgDuration) : "-"}`);
124
129
  console.log(` Avg TTFT: ${overall.avgTtft !== null ? formatDuration(overall.avgTtft) : "-"}`);
125
130
  if (overall.avgTokensPerSecond !== null) {
@@ -4,29 +4,20 @@
4
4
  * Handles `omp update` to check for and install updates.
5
5
  * Uses bun if available, otherwise downloads binary from GitHub releases.
6
6
  */
7
- import { execSync, spawnSync } from "node:child_process";
8
7
  import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
9
  import { pipeline } from "node:stream/promises";
10
10
  import { APP_NAME, isEnoent, VERSION } from "@oh-my-pi/pi-utils";
11
+ import { $ } from "bun";
11
12
  import chalk from "chalk";
12
13
  import { theme } from "../modes/theme/theme";
13
14
 
14
- /**
15
- * Detect if we're running as a Bun compiled binary.
16
- */
17
- const isBunBinary =
18
- Bun.env.PI_COMPILED ||
19
- import.meta.url.includes("$bunfs") ||
20
- import.meta.url.includes("~BUN") ||
21
- import.meta.url.includes("%7EBUN");
22
-
23
15
  const REPO = "can1357/oh-my-pi";
24
16
  const PACKAGE = "@oh-my-pi/pi-coding-agent";
25
17
 
26
18
  interface ReleaseInfo {
27
19
  tag: string;
28
20
  version: string;
29
- assets: Array<{ name: string; url: string }>;
30
21
  }
31
22
 
32
23
  /**
@@ -44,18 +35,59 @@ export function parseUpdateArgs(args: string[]): { force: boolean; check: boolea
44
35
  };
45
36
  }
46
37
 
47
- /**
48
- * Check if bun is available in PATH.
49
- */
50
- function hasBun(): boolean {
38
+ async function getBunGlobalBinDir(): Promise<string | undefined> {
39
+ if (!Bun.which("bun")) return undefined;
51
40
  try {
52
- const result = spawnSync("bun", ["--version"], { encoding: "utf-8", stdio: "pipe" });
53
- return result.status === 0;
41
+ const result = await $`bun pm bin -g`.quiet().nothrow();
42
+ if (result.exitCode !== 0) return undefined;
43
+ const output = result.text().trim();
44
+ return output.length > 0 ? output : undefined;
54
45
  } catch {
55
- return false;
46
+ return undefined;
56
47
  }
57
48
  }
58
49
 
50
+ function getRealPathOrOriginal(filePath: string): string {
51
+ try {
52
+ return fs.realpathSync(filePath);
53
+ } catch {
54
+ return filePath;
55
+ }
56
+ }
57
+
58
+ function normalizePathForComparison(filePath: string): string {
59
+ const normalized = path.normalize(filePath);
60
+ if (process.platform === "win32") return normalized.toLowerCase();
61
+ return normalized;
62
+ }
63
+
64
+ function isPathInDirectory(filePath: string, directoryPath: string): boolean {
65
+ const normalizedPath = normalizePathForComparison(getRealPathOrOriginal(filePath));
66
+ const normalizedDirectory = normalizePathForComparison(getRealPathOrOriginal(directoryPath));
67
+ const relativePath = path.relative(normalizedDirectory, normalizedPath);
68
+ return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
69
+ }
70
+
71
+ interface UpdateTarget {
72
+ method: "bun" | "binary";
73
+ path: string;
74
+ }
75
+
76
+ function resolveUpdateMethod(ompPath: string, bunBinDir: string | undefined): "bun" | "binary" {
77
+ if (!bunBinDir) return "binary";
78
+ return isPathInDirectory(ompPath, bunBinDir) ? "bun" : "binary";
79
+ }
80
+
81
+ export function _resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined): "bun" | "binary" {
82
+ return resolveUpdateMethod(ompPath, bunBinDir);
83
+ }
84
+ async function resolveUpdateTarget(): Promise<UpdateTarget> {
85
+ const ompPath = resolveOmpPath() ?? process.execPath;
86
+ const bunBinDir = await getBunGlobalBinDir();
87
+ const method = resolveUpdateMethod(ompPath, bunBinDir);
88
+ return { method, path: ompPath };
89
+ }
90
+
59
91
  /**
60
92
  * Get the latest release info from the npm registry.
61
93
  * Uses npm instead of GitHub API to avoid unauthenticated rate limiting.
@@ -70,15 +102,9 @@ async function getLatestRelease(): Promise<ReleaseInfo> {
70
102
  const version = data.version;
71
103
  const tag = `v${version}`;
72
104
 
73
- // Construct deterministic GitHub release download URLs for the current platform
74
- const makeAsset = (name: string) => ({
75
- name,
76
- url: `https://github.com/${REPO}/releases/download/${tag}/${name}`,
77
- });
78
105
  return {
79
106
  tag,
80
107
  version,
81
- assets: [makeAsset(getBinaryName())],
82
108
  };
83
109
  }
84
110
 
@@ -140,60 +166,93 @@ function getBinaryName(): string {
140
166
  return `${APP_NAME}-${os}-${archName}`;
141
167
  }
142
168
 
169
+ /**
170
+ * Resolve the path that `omp` maps to in the user's PATH.
171
+ */
172
+ function resolveOmpPath(): string | undefined {
173
+ return Bun.which(APP_NAME) ?? undefined;
174
+ }
175
+
176
+ /**
177
+ * Run the resolved omp binary and check if it reports the expected version.
178
+ */
179
+ async function verifyInstalledVersion(
180
+ expectedVersion: string,
181
+ ): Promise<{ ok: boolean; actual?: string; path?: string }> {
182
+ const ompPath = resolveOmpPath();
183
+ if (!ompPath) return { ok: false };
184
+ try {
185
+ const result = await $`${ompPath} --version`.quiet().nothrow();
186
+ if (result.exitCode !== 0) return { ok: false, path: ompPath };
187
+ const output = result.text().trim();
188
+ // Output format: "omp/X.Y.Z"
189
+ const match = output.match(/\/(\d+\.\d+\.\d+)/);
190
+ const actual = match?.[1];
191
+ return { ok: actual === expectedVersion, actual, path: ompPath };
192
+ } catch {
193
+ return { ok: false, path: ompPath };
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Print post-update verification result.
199
+ */
200
+ async function printVerification(expectedVersion: string): Promise<void> {
201
+ const result = await verifyInstalledVersion(expectedVersion);
202
+ if (result.ok) {
203
+ console.log(chalk.green(`\n${theme.status.success} Updated to ${expectedVersion}`));
204
+ return;
205
+ }
206
+ if (result.actual) {
207
+ console.log(
208
+ chalk.yellow(
209
+ `\nWarning: ${APP_NAME} at ${result.path} still reports ${result.actual} (expected ${expectedVersion})`,
210
+ ),
211
+ );
212
+ } else {
213
+ console.log(
214
+ chalk.yellow(`\nWarning: could not verify updated version${result.path ? ` at ${result.path}` : ""}`),
215
+ );
216
+ }
217
+ console.log(
218
+ chalk.yellow(
219
+ `You may need to reinstall: curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | bash`,
220
+ ),
221
+ );
222
+ }
223
+
143
224
  /**
144
225
  * Update via bun package manager.
145
226
  */
146
227
  async function updateViaBun(expectedVersion: string): Promise<void> {
147
228
  console.log(chalk.dim("Updating via bun..."));
148
- try {
149
- execSync(`bun install -g ${PACKAGE}@${expectedVersion}`, { stdio: "inherit" });
150
- } catch (error) {
151
- throw new Error("bun install failed", { cause: error });
229
+ const result = await $`bun install -g ${PACKAGE}@${expectedVersion}`.nothrow();
230
+ if (result.exitCode !== 0) {
231
+ throw new Error(`bun install failed with exit code ${result.exitCode}`);
152
232
  }
153
233
 
154
- // Verify the update actually took effect
155
- try {
156
- const result = spawnSync("bun", ["pm", "ls", "-g"], { encoding: "utf-8", stdio: "pipe" });
157
- const output = result.stdout || "";
158
- const match = output.match(new RegExp(`${PACKAGE.replace("/", "\\/")}@(\\S+)`));
159
- if (match) {
160
- const installedVersion = match[1];
161
- if (compareVersions(installedVersion, expectedVersion) < 0) {
162
- console.log(
163
- chalk.yellow(`\nWarning: bun reports ${installedVersion} installed, expected ${expectedVersion}`),
164
- );
165
- console.log(chalk.yellow(`Try: bun install -g ${PACKAGE}@latest`));
166
- return;
167
- }
168
- }
169
- } catch {
170
- // Verification is best-effort, don't fail the update
171
- }
172
- console.log(chalk.green(`\n${theme.status.success} Update complete`));
234
+ await printVerification(expectedVersion);
173
235
  }
174
236
 
175
237
  /**
176
- * Update by downloading binary from GitHub releases.
238
+ * Download a release binary to a target path, replacing an existing file.
177
239
  */
178
- async function updateViaBinary(release: ReleaseInfo): Promise<void> {
240
+ async function updateViaBinaryAt(targetPath: string, expectedVersion: string): Promise<void> {
179
241
  const binaryName = getBinaryName();
180
- const asset = release.assets.find(a => a.name === binaryName);
181
- if (!asset) {
182
- throw new Error(`No binary found for ${binaryName}`);
183
- }
184
- const execPath = process.execPath;
185
- const tempPath = `${execPath}.new`;
186
- const backupPath = `${execPath}.bak`;
242
+ const tag = `v${expectedVersion}`;
243
+ const url = `https://github.com/${REPO}/releases/download/${tag}/${binaryName}`;
244
+
245
+ const tempPath = `${targetPath}.new`;
246
+ const backupPath = `${targetPath}.bak`;
187
247
  console.log(chalk.dim(`Downloading ${binaryName}…`));
188
248
 
189
- // Download binary to temp file
190
- const response = await fetch(asset.url, { redirect: "follow" });
249
+ const response = await fetch(url, { redirect: "follow" });
191
250
  if (!response.ok || !response.body) {
192
251
  throw new Error(`Download failed: ${response.statusText}`);
193
252
  }
194
253
  const fileStream = fs.createWriteStream(tempPath, { mode: 0o755 });
195
254
  await pipeline(response.body, fileStream);
196
- // Replace current binary
255
+
197
256
  console.log(chalk.dim("Installing update..."));
198
257
  try {
199
258
  try {
@@ -201,15 +260,15 @@ async function updateViaBinary(release: ReleaseInfo): Promise<void> {
201
260
  } catch (err) {
202
261
  if (!isEnoent(err)) throw err;
203
262
  }
204
- await fs.promises.rename(execPath, backupPath);
205
- await fs.promises.rename(tempPath, execPath);
263
+ await fs.promises.rename(targetPath, backupPath);
264
+ await fs.promises.rename(tempPath, targetPath);
206
265
  await fs.promises.unlink(backupPath);
207
266
 
208
- console.log(chalk.green(`\n${theme.status.success} Updated to ${release.version}`));
267
+ await printVerification(expectedVersion);
209
268
  console.log(chalk.dim(`Restart ${APP_NAME} to use the new version`));
210
269
  } catch (err) {
211
- if (fs.existsSync(backupPath) && !fs.existsSync(execPath)) {
212
- await fs.promises.rename(backupPath, execPath);
270
+ if (fs.existsSync(backupPath) && !fs.existsSync(targetPath)) {
271
+ await fs.promises.rename(backupPath, targetPath);
213
272
  }
214
273
  if (fs.existsSync(tempPath)) {
215
274
  await fs.promises.unlink(tempPath);
@@ -251,12 +310,13 @@ export async function runUpdateCommand(opts: { force: boolean; check: boolean })
251
310
  return;
252
311
  }
253
312
 
254
- // Choose update method
313
+ // Choose update method based on the prioritized omp binary in PATH
255
314
  try {
256
- if (!isBunBinary && hasBun()) {
315
+ const target = await resolveUpdateTarget();
316
+ if (target.method === "bun") {
257
317
  await updateViaBun(release.version);
258
318
  } else {
259
- await updateViaBinary(release);
319
+ await updateViaBinaryAt(target.path, release.version);
260
320
  }
261
321
  } catch (err) {
262
322
  console.error(chalk.red(`Update failed: ${err}`));
@@ -3,15 +3,14 @@ import {
3
3
  type AssistantMessageEventStream,
4
4
  type Context,
5
5
  createModelManager,
6
+ DEFAULT_LOCAL_TOKEN,
6
7
  getBundledModels,
7
8
  getBundledProviders,
8
- getGitHubCopilotBaseUrl,
9
9
  googleAntigravityModelManagerOptions,
10
10
  googleGeminiCliModelManagerOptions,
11
11
  type Model,
12
12
  type ModelManagerOptions,
13
13
  type ModelRefreshStrategy,
14
- normalizeDomain,
15
14
  type OAuthCredentials,
16
15
  type OAuthLoginCallbacks,
17
16
  openaiCodexModelManagerOptions,
@@ -99,6 +98,7 @@ const ModelDefinitionSchema = Type.Object({
99
98
  cacheWrite: Type.Number(),
100
99
  }),
101
100
  ),
101
+ premiumMultiplier: Type.Optional(Type.Number()),
102
102
  contextWindow: Type.Optional(Type.Number()),
103
103
  maxTokens: Type.Optional(Type.Number()),
104
104
  headers: Type.Optional(Type.Record(Type.String(), Type.String())),
@@ -119,6 +119,7 @@ const ModelOverrideSchema = Type.Object({
119
119
  cacheWrite: Type.Optional(Type.Number()),
120
120
  }),
121
121
  ),
122
+ premiumMultiplier: Type.Optional(Type.Number()),
122
123
  contextWindow: Type.Optional(Type.Number()),
123
124
  maxTokens: Type.Optional(Type.Number()),
124
125
  headers: Type.Optional(Type.Record(Type.String(), Type.String())),
@@ -129,7 +130,7 @@ const ModelOverrideSchema = Type.Object({
129
130
  type ModelOverride = Static<typeof ModelOverrideSchema>;
130
131
 
131
132
  const ProviderDiscoverySchema = Type.Object({
132
- type: Type.Union([Type.Literal("ollama")]),
133
+ type: Type.Union([Type.Literal("ollama"), Type.Literal("lm-studio")]),
133
134
  });
134
135
 
135
136
  const ProviderAuthSchema = Type.Union([Type.Literal("apiKey"), Type.Literal("none")]);
@@ -378,6 +379,7 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
378
379
  if (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;
379
380
  if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
380
381
  if (override.contextPromotionTarget !== undefined) result.contextPromotionTarget = override.contextPromotionTarget;
382
+ if (override.premiumMultiplier !== undefined) result.premiumMultiplier = override.premiumMultiplier;
381
383
  if (override.cost) {
382
384
  result.cost = {
383
385
  input: override.cost.input ?? model.cost.input,
@@ -405,6 +407,7 @@ interface CustomModelDefinitionLike {
405
407
  headers?: Record<string, string>;
406
408
  compat?: Model<Api>["compat"];
407
409
  contextPromotionTarget?: string;
410
+ premiumMultiplier?: number;
408
411
  }
409
412
 
410
413
  interface CustomModelBuildOptions {
@@ -456,6 +459,7 @@ function buildCustomModel(
456
459
  headers: mergeCustomModelHeaders(providerHeaders, modelDef.headers, authHeader, providerApiKey),
457
460
  compat: modelDef.compat,
458
461
  contextPromotionTarget: modelDef.contextPromotionTarget,
462
+ premiumMultiplier: modelDef.premiumMultiplier,
459
463
  } as Model<Api>;
460
464
  }
461
465
 
@@ -533,17 +537,7 @@ export class ModelRegistry {
533
537
  const builtInModels = this.#loadBuiltInModels(overrides, modelOverrides);
534
538
  const combined = this.#mergeCustomModels(builtInModels, customModels);
535
539
 
536
- // Update github-copilot base URL based on OAuth credentials
537
- const copilotCred = this.authStorage.getOAuthCredential("github-copilot");
538
- if (copilotCred) {
539
- const domain = copilotCred.enterpriseUrl
540
- ? (normalizeDomain(copilotCred.enterpriseUrl) ?? undefined)
541
- : undefined;
542
- const baseUrl = getGitHubCopilotBaseUrl(copilotCred.access, domain);
543
- this.#models = combined.map(m => (m.provider === "github-copilot" ? { ...m, baseUrl } : m));
544
- } else {
545
- this.#models = combined;
546
- }
540
+ this.#models = combined;
547
541
  }
548
542
 
549
543
  /** Load built-in models, applying provider and per-model overrides */
@@ -589,14 +583,24 @@ export class ModelRegistry {
589
583
  }
590
584
 
591
585
  #addImplicitDiscoverableProviders(configuredProviders: Set<string>): void {
592
- if (configuredProviders.has("ollama")) return;
593
- this.#discoverableProviders.push({
594
- provider: "ollama",
595
- api: "openai-completions",
596
- baseUrl: Bun.env.OLLAMA_BASE_URL || "http://127.0.0.1:11434",
597
- discovery: { type: "ollama" },
598
- });
599
- this.#keylessProviders.add("ollama");
586
+ if (!configuredProviders.has("ollama")) {
587
+ this.#discoverableProviders.push({
588
+ provider: "ollama",
589
+ api: "openai-completions",
590
+ baseUrl: Bun.env.OLLAMA_BASE_URL || "http://127.0.0.1:11434",
591
+ discovery: { type: "ollama" },
592
+ });
593
+ this.#keylessProviders.add("ollama");
594
+ }
595
+ if (!configuredProviders.has("lm-studio")) {
596
+ this.#discoverableProviders.push({
597
+ provider: "lm-studio",
598
+ api: "openai-completions",
599
+ baseUrl: Bun.env.LM_STUDIO_BASE_URL || "http://127.0.0.1:1234/v1",
600
+ discovery: { type: "lm-studio" },
601
+ });
602
+ this.#keylessProviders.add("lm-studio");
603
+ }
600
604
  }
601
605
 
602
606
  #loadCustomModels(): CustomModelsResult {
@@ -719,6 +723,8 @@ export class ModelRegistry {
719
723
  switch (providerConfig.discovery.type) {
720
724
  case "ollama":
721
725
  return this.#discoverOllamaModels(providerConfig);
726
+ case "lm-studio":
727
+ return this.#discoverLmStudioModels(providerConfig);
722
728
  }
723
729
  }
724
730
 
@@ -872,6 +878,77 @@ export class ModelRegistry {
872
878
  }
873
879
  }
874
880
 
881
+ async #discoverLmStudioModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
882
+ const baseUrl = this.#normalizeLmStudioBaseUrl(providerConfig.baseUrl);
883
+ const modelsUrl = `${baseUrl}/models`;
884
+
885
+ const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
886
+ const apiKey = await this.authStorage.getApiKey("lm-studio");
887
+ if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
888
+ headers.Authorization = `Bearer ${apiKey}`;
889
+ }
890
+
891
+ try {
892
+ const response = await fetch(modelsUrl, {
893
+ headers,
894
+ signal: AbortSignal.timeout(3000),
895
+ });
896
+ if (!response.ok) {
897
+ logger.warn("model discovery failed for provider", {
898
+ provider: providerConfig.provider,
899
+ status: response.status,
900
+ url: modelsUrl,
901
+ });
902
+ return [];
903
+ }
904
+ const payload = (await response.json()) as { data?: Array<{ id: string }> };
905
+ const models = payload.data ?? [];
906
+ const discovered: Model<Api>[] = [];
907
+ for (const item of models) {
908
+ const id = item.id;
909
+ if (!id) continue;
910
+ discovered.push({
911
+ id,
912
+ name: id,
913
+ api: providerConfig.api,
914
+ provider: providerConfig.provider,
915
+ baseUrl,
916
+ reasoning: false,
917
+ input: ["text"],
918
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
919
+ contextWindow: 128000,
920
+ maxTokens: 8192,
921
+ headers,
922
+ compat: {
923
+ supportsStore: false,
924
+ supportsDeveloperRole: false,
925
+ supportsReasoningEffort: false,
926
+ },
927
+ });
928
+ }
929
+ return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
930
+ } catch (error) {
931
+ logger.warn("model discovery failed for provider", {
932
+ provider: providerConfig.provider,
933
+ url: modelsUrl,
934
+ error: error instanceof Error ? error.message : String(error),
935
+ });
936
+ return [];
937
+ }
938
+ }
939
+
940
+ #normalizeLmStudioBaseUrl(baseUrl?: string): string {
941
+ const defaultBaseUrl = "http://127.0.0.1:1234/v1";
942
+ const raw = baseUrl || defaultBaseUrl;
943
+ try {
944
+ const parsed = new URL(raw);
945
+ const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
946
+ parsed.pathname = trimmedPath.endsWith("/v1") ? trimmedPath || "/v1" : `${trimmedPath}/v1`;
947
+ return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
948
+ } catch {
949
+ return raw;
950
+ }
951
+ }
875
952
  #normalizeOllamaBaseUrl(baseUrl?: string): string {
876
953
  const raw = baseUrl || "http://127.0.0.1:11434";
877
954
  try {
@@ -1136,5 +1213,6 @@ export interface ProviderConfigInput {
1136
1213
  headers?: Record<string, string>;
1137
1214
  compat?: Model<Api>["compat"];
1138
1215
  contextPromotionTarget?: string;
1216
+ premiumMultiplier?: number;
1139
1217
  }>;
1140
1218
  }
@@ -63,6 +63,7 @@ export type StatusLineSegmentId =
63
63
  | "plan_mode"
64
64
  | "path"
65
65
  | "git"
66
+ | "pr"
66
67
  | "subagents"
67
68
  | "token_in"
68
69
  | "token_out"
@@ -569,12 +570,13 @@ export const SETTINGS_SCHEMA = {
569
570
  // ─────────────────────────────────────────────────────────────────────────
570
571
  "task.isolation.mode": {
571
572
  type: "enum",
572
- values: ["none", "worktree", "fuse-overlay"] as const,
573
+ values: ["none", "worktree", "fuse-overlay", "fuse-projfs"] as const,
573
574
  default: "none",
574
575
  ui: {
575
576
  tab: "tools",
576
577
  label: "Task isolation",
577
- description: "Isolation mode for subagents (none, git worktree, or fuse-overlay)",
578
+ description:
579
+ "Isolation mode for subagents (none, git worktree, fuse-overlayfs on Unix, or ProjFS on Windows via fuse-projfs; unsupported modes fall back to worktree)",
578
580
  submenu: true,
579
581
  },
580
582
  },
@@ -858,6 +860,24 @@ export const SETTINGS_SCHEMA = {
858
860
  default: true,
859
861
  ui: { tab: "tools", label: "MCP project config", description: "Load .mcp.json/mcp.json from project root" },
860
862
  },
863
+ "mcp.notifications": {
864
+ type: "boolean",
865
+ default: false,
866
+ ui: {
867
+ tab: "tools",
868
+ label: "MCP update injection",
869
+ description: "Inject MCP resource updates into the agent conversation",
870
+ },
871
+ },
872
+ "mcp.notificationDebounceMs": {
873
+ type: "number",
874
+ default: 500,
875
+ ui: {
876
+ tab: "tools",
877
+ label: "MCP notification debounce (ms)",
878
+ description: "Debounce window for MCP resource update notifications before injecting into conversation",
879
+ },
880
+ },
861
881
 
862
882
  // ─────────────────────────────────────────────────────────────────────────
863
883
  // LSP settings
@@ -1144,6 +1144,8 @@ export interface ProviderModelConfig {
1144
1144
  input: ("text" | "image")[];
1145
1145
  /** Cost per million tokens. */
1146
1146
  cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
1147
+ /** Premium Copilot requests charged per user-initiated request. */
1148
+ premiumMultiplier?: number;
1147
1149
  /** Maximum context window size in tokens. */
1148
1150
  contextWindow: number;
1149
1151
  /** Maximum output tokens. */