@oh-my-pi/pi-coding-agent 15.10.1 → 15.10.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 (102) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/types/cli/startup-cwd.d.ts +2 -0
  3. package/dist/types/commands/launch.d.ts +3 -0
  4. package/dist/types/config/keybindings.d.ts +2 -2
  5. package/dist/types/config/model-provider-priority.d.ts +1 -0
  6. package/dist/types/config/model-resolver.d.ts +4 -1
  7. package/dist/types/config/settings.d.ts +7 -2
  8. package/dist/types/debug/report-bundle.d.ts +3 -0
  9. package/dist/types/edit/file-snapshot-store.d.ts +18 -10
  10. package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +4 -1
  12. package/dist/types/lsp/client.d.ts +10 -0
  13. package/dist/types/main.d.ts +3 -9
  14. package/dist/types/mcp/tool-bridge.d.ts +2 -0
  15. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  16. package/dist/types/modes/components/status-line.d.ts +2 -0
  17. package/dist/types/modes/controllers/event-controller.d.ts +17 -0
  18. package/dist/types/modes/interactive-mode.d.ts +1 -0
  19. package/dist/types/modes/magic-keywords.d.ts +1 -1
  20. package/dist/types/modes/markdown-prose.d.ts +1 -1
  21. package/dist/types/modes/types.d.ts +3 -0
  22. package/dist/types/modes/workflow.d.ts +3 -3
  23. package/dist/types/session/auth-storage.d.ts +1 -1
  24. package/dist/types/session/session-manager.d.ts +5 -2
  25. package/dist/types/task/executor.d.ts +10 -0
  26. package/dist/types/tools/eval.d.ts +8 -0
  27. package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
  28. package/dist/types/tools/github-cache.d.ts +12 -0
  29. package/dist/types/tools/path-utils.d.ts +8 -0
  30. package/dist/types/tools/search.d.ts +2 -2
  31. package/dist/types/tools/yield.d.ts +8 -0
  32. package/package.json +9 -9
  33. package/src/cli/args.ts +3 -1
  34. package/src/cli/dry-balance-cli.ts +2 -4
  35. package/src/cli/startup-cwd.ts +68 -0
  36. package/src/commands/launch.ts +3 -0
  37. package/src/commit/model-selection.ts +3 -2
  38. package/src/config/model-provider-priority.ts +55 -0
  39. package/src/config/model-registry.ts +4 -22
  40. package/src/config/model-resolver.ts +39 -7
  41. package/src/config/settings.ts +86 -41
  42. package/src/debug/index.ts +8 -0
  43. package/src/debug/raw-sse-buffer.ts +7 -4
  44. package/src/debug/report-bundle.ts +9 -0
  45. package/src/edit/file-snapshot-store.ts +33 -1
  46. package/src/edit/hashline/filesystem.ts +2 -1
  47. package/src/eval/__tests__/llm-bridge.test.ts +20 -0
  48. package/src/eval/js/context-manager.ts +32 -15
  49. package/src/eval/llm-bridge.ts +14 -3
  50. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  51. package/src/eval/py/executor.ts +23 -11
  52. package/src/eval/py/prelude.py +1 -1
  53. package/src/extensibility/extensions/types.ts +10 -1
  54. package/src/internal-urls/docs-index.generated.ts +3 -3
  55. package/src/lsp/client.ts +23 -11
  56. package/src/lsp/config.ts +11 -1
  57. package/src/lsp/index.ts +61 -9
  58. package/src/main.ts +91 -65
  59. package/src/mcp/tool-bridge.ts +2 -0
  60. package/src/memories/index.ts +2 -2
  61. package/src/modes/components/custom-editor.ts +143 -111
  62. package/src/modes/components/model-selector.ts +59 -13
  63. package/src/modes/components/oauth-selector.ts +33 -7
  64. package/src/modes/components/status-line.ts +19 -4
  65. package/src/modes/components/tips.txt +1 -1
  66. package/src/modes/components/user-message.ts +1 -1
  67. package/src/modes/controllers/event-controller.ts +26 -0
  68. package/src/modes/controllers/input-controller.ts +46 -7
  69. package/src/modes/interactive-mode.ts +107 -20
  70. package/src/modes/magic-keywords.ts +1 -1
  71. package/src/modes/markdown-prose.ts +1 -1
  72. package/src/modes/theme/shimmer.ts +20 -9
  73. package/src/modes/types.ts +3 -0
  74. package/src/modes/workflow.ts +10 -10
  75. package/src/prompts/system/workflow-notice.md +1 -1
  76. package/src/prompts/tools/bash.md +9 -0
  77. package/src/prompts/tools/browser.md +1 -1
  78. package/src/prompts/tools/eval.md +2 -1
  79. package/src/prompts/tools/read.md +2 -2
  80. package/src/sdk.ts +26 -9
  81. package/src/session/agent-session.ts +37 -12
  82. package/src/session/auth-storage.ts +2 -0
  83. package/src/session/session-manager.ts +96 -23
  84. package/src/task/executor.ts +71 -36
  85. package/src/task/render.ts +3 -4
  86. package/src/tools/bash.ts +7 -0
  87. package/src/tools/browser/tab-supervisor.ts +13 -1
  88. package/src/tools/browser/tab-worker.ts +33 -4
  89. package/src/tools/eval.ts +13 -2
  90. package/src/tools/find.ts +7 -0
  91. package/src/tools/gh-cache-invalidation.ts +200 -0
  92. package/src/tools/github-cache.ts +25 -0
  93. package/src/tools/inspect-image.ts +2 -2
  94. package/src/tools/path-utils.ts +28 -2
  95. package/src/tools/plan-mode-guard.ts +52 -7
  96. package/src/tools/read.ts +25 -12
  97. package/src/tools/search.ts +38 -3
  98. package/src/tools/write.ts +2 -2
  99. package/src/tools/yield.ts +10 -1
  100. package/src/utils/commit-message-generator.ts +2 -2
  101. package/src/utils/enhanced-paste.ts +30 -2
  102. package/src/web/search/providers/codex.ts +37 -8
package/src/lsp/client.ts CHANGED
@@ -946,18 +946,28 @@ export async function shutdownClient(key: string): Promise<void> {
946
946
  // LSP Protocol Methods
947
947
  // =============================================================================
948
948
 
949
- /** Default timeout for LSP requests (30 seconds) */
949
+ /** Default timeout for LSP requests when no abort signal is provided (30 seconds) */
950
950
  const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
951
951
 
952
952
  /**
953
953
  * Send an LSP request and wait for response.
954
+ *
955
+ * Timeout policy:
956
+ * - If `timeoutMs` is explicitly provided, that value is used.
957
+ * - Else, if `signal` is provided, no internal timer is installed (the caller
958
+ * owns the deadline via the signal — typically a wall-clock `AbortSignal.timeout`
959
+ * from the LSP tool). Installing a second hard-coded 30s timer here used to
960
+ * cause "timed out after 30000ms" errors even when the caller had requested
961
+ * `timeout: 60`.
962
+ * - Else (no signal, no explicit timeout), fall back to `DEFAULT_REQUEST_TIMEOUT_MS`
963
+ * to avoid leaking pending requests forever.
954
964
  */
955
965
  export async function sendRequest(
956
966
  client: LspClient,
957
967
  method: string,
958
968
  params: unknown,
959
969
  signal?: AbortSignal,
960
- timeoutMs: number = DEFAULT_REQUEST_TIMEOUT_MS,
970
+ timeoutMs?: number,
961
971
  ): Promise<unknown> {
962
972
  // Atomically increment and capture request ID
963
973
  const id = ++client.requestId;
@@ -993,15 +1003,17 @@ export async function sendRequest(
993
1003
  reject(reason);
994
1004
  };
995
1005
 
996
- // Set timeout
997
- timeout = setTimeout(() => {
998
- if (client.pendingRequests.has(id)) {
999
- client.pendingRequests.delete(id);
1000
- const err = new Error(`LSP request ${method} timed out after ${timeoutMs}ms`);
1001
- cleanup();
1002
- reject(err);
1003
- }
1004
- }, timeoutMs);
1006
+ const effectiveTimeoutMs = timeoutMs ?? (signal ? undefined : DEFAULT_REQUEST_TIMEOUT_MS);
1007
+ if (effectiveTimeoutMs !== undefined) {
1008
+ timeout = setTimeout(() => {
1009
+ if (client.pendingRequests.has(id)) {
1010
+ client.pendingRequests.delete(id);
1011
+ const err = new Error(`LSP request ${method} timed out after ${effectiveTimeoutMs}ms`);
1012
+ cleanup();
1013
+ reject(err);
1014
+ }
1015
+ }, effectiveTimeoutMs);
1016
+ }
1005
1017
  if (signal) {
1006
1018
  signal.addEventListener("abort", abortHandler, { once: true });
1007
1019
  if (signal.aborted) {
package/src/lsp/config.ts CHANGED
@@ -450,13 +450,23 @@ export function loadConfig(cwd: string): LspConfig {
450
450
  */
451
451
  export function getServersForFile(config: LspConfig, filePath: string): Array<[string, ServerConfig]> {
452
452
  const ext = path.extname(filePath).toLowerCase();
453
+ const extNoDot = ext.startsWith(".") ? ext.slice(1) : ext;
453
454
  const fileName = path.basename(filePath).toLowerCase();
454
455
  const matches: Array<[string, ServerConfig]> = [];
455
456
 
456
457
  for (const [name, serverConfig] of Object.entries(config.servers)) {
457
458
  const supportsFile = serverConfig.fileTypes.some(fileType => {
459
+ // Accept both `.ts` and `ts` forms in user config / fixtures so a
460
+ // missing dot in `fileTypes` doesn't silently exclude the server
461
+ // from extension-based routing (e.g. rename_file's relevance filter).
458
462
  const normalized = fileType.toLowerCase();
459
- return normalized === ext || normalized === fileName;
463
+ const normalizedNoDot = normalized.startsWith(".") ? normalized.slice(1) : normalized;
464
+ return (
465
+ normalized === ext ||
466
+ normalized === fileName ||
467
+ normalizedNoDot === extNoDot ||
468
+ normalizedNoDot === fileName
469
+ );
460
470
  });
461
471
 
462
472
  if (supportsFile) {
package/src/lsp/index.ts CHANGED
@@ -1261,7 +1261,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1261
1261
 
1262
1262
  // Status action doesn't need a file
1263
1263
  if (action === "status") {
1264
- const servers = Object.keys(config.servers);
1264
+ const configuredNames = Object.keys(config.servers);
1265
1265
  const lspmuxState = await detectLspmux();
1266
1266
  const lspmuxStatus = lspmuxState.available
1267
1267
  ? lspmuxState.running
@@ -1269,14 +1269,40 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1269
1269
  : "lspmux: installed but server not running"
1270
1270
  : "";
1271
1271
 
1272
- const serverStatus =
1273
- servers.length > 0
1274
- ? `Active language servers: ${servers.join(", ")}`
1275
- : "No language servers configured for this project";
1272
+ // `Object.keys(config.servers)` reflects what is *configured & resolvable
1273
+ // on PATH* — it does NOT prove the server actually starts. A wrapper
1274
+ // binary that exits immediately (e.g. rustup without the rust-analyzer
1275
+ // component) still appears here. Distinguish "configured" from
1276
+ // "started" (have a live in-process client) so callers cannot mistake
1277
+ // presence-on-PATH for a working server.
1278
+ const startedClients = getActiveClients();
1279
+ const startedByConfigName = new Map<string, LspServerStatus>();
1280
+ // getActiveClients() reports `name = client.config.command` (the
1281
+ // unresolved binary name from defaults.json), so match against
1282
+ // `serverConfig.command`, not the resolved path.
1283
+ for (const [name, serverConfig] of Object.entries(config.servers)) {
1284
+ const matched = startedClients.find(c => c.name === serverConfig.command);
1285
+ if (matched) startedByConfigName.set(name, matched);
1286
+ }
1287
+
1288
+ const lines: string[] = [];
1289
+ if (configuredNames.length === 0) {
1290
+ lines.push("No language servers configured for this project");
1291
+ } else {
1292
+ const labelled = configuredNames.map(name => {
1293
+ const started = startedByConfigName.get(name);
1294
+ if (!started) return `${name} (configured, not started)`;
1295
+ return `${name} (${started.status})`;
1296
+ });
1297
+ lines.push(`Language servers: ${labelled.join(", ")}`);
1298
+ lines.push(
1299
+ " note: 'configured, not started' means the binary resolves on PATH but no request has spawned it yet; 'ready' means a client process is live for this cwd.",
1300
+ );
1301
+ }
1302
+ if (lspmuxStatus) lines.push(lspmuxStatus);
1276
1303
 
1277
- const output = lspmuxStatus ? `${serverStatus}\n${lspmuxStatus}` : serverStatus;
1278
1304
  return {
1279
- content: [{ type: "text", text: output }],
1305
+ content: [{ type: "text", text: lines.join("\n") }],
1280
1306
  details: { action, success: true, request: params },
1281
1307
  };
1282
1308
  }
@@ -1505,7 +1531,26 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1505
1531
  }
1506
1532
 
1507
1533
  const lspParams = { files: pairs };
1508
- const servers = getLspServers(config);
1534
+ // Filter to servers whose fileTypes match either the source or any
1535
+ // destination path. Asking every configured server about a .md/.sql/.txt
1536
+ // rename used to stack up willRenameFiles requests against irrelevant
1537
+ // language servers and hit the wall-clock timeout. A server only has
1538
+ // something useful to say about a rename if it understands one of the
1539
+ // affected file extensions.
1540
+ const allLspServers = getLspServers(config);
1541
+ const relevantNames = new Set<string>();
1542
+ const collectRelevant = (filePath: string) => {
1543
+ for (const [name] of getLspServersForFile(config, filePath)) {
1544
+ relevantNames.add(name);
1545
+ }
1546
+ };
1547
+ collectRelevant(source);
1548
+ collectRelevant(dest);
1549
+ for (const pair of pairs) {
1550
+ collectRelevant(uriToFile(pair.oldUri));
1551
+ collectRelevant(uriToFile(pair.newUri));
1552
+ }
1553
+ const servers = allLspServers.filter(([name]) => relevantNames.has(name));
1509
1554
  const respondingServers = new Set<string>();
1510
1555
  const perServerEdits: Array<{ serverName: string; edit: WorkspaceEdit }> = [];
1511
1556
  const serverNotes: string[] = [];
@@ -1829,8 +1874,15 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1829
1874
  throw new ToolAbortError();
1830
1875
  }
1831
1876
  const msg = err instanceof Error ? err.message : String(err);
1877
+ // Echo a (truncated) preview of the params we sent so the caller can
1878
+ // tell parse / shape errors (e.g. nested args dropped, missing field)
1879
+ // apart from genuine server errors without spinning up another debug call.
1880
+ const previewRaw = JSON.stringify(requestParams ?? null);
1881
+ const preview = previewRaw.length > 400 ? `${previewRaw.slice(0, 397)}...` : previewRaw;
1832
1882
  return {
1833
- content: [{ type: "text", text: `LSP error from ${chosenName} on ${method}: ${msg}` }],
1883
+ content: [
1884
+ { type: "text", text: `LSP error from ${chosenName} on ${method}: ${msg}\n params: ${preview}` },
1885
+ ],
1834
1886
  details: { action, serverName: chosenName, success: false, request: params },
1835
1887
  };
1836
1888
  }
package/src/main.ts CHANGED
@@ -4,10 +4,8 @@
4
4
  * This file handles CLI argument parsing and translates them into
5
5
  * createAgentSession() options. The SDK does the heavy lifting.
6
6
  */
7
-
8
- import * as fs from "node:fs/promises";
7
+ import * as fsSync from "node:fs";
9
8
  import * as os from "node:os";
10
- import * as path from "node:path";
11
9
  import { createInterface } from "node:readline/promises";
12
10
  import { EventLoopKeepalive } from "@oh-my-pi/pi-agent-core";
13
11
  import type { ImageContent } from "@oh-my-pi/pi-ai";
@@ -28,9 +26,16 @@ import { processFileArguments } from "./cli/file-processor";
28
26
  import { buildInitialMessage } from "./cli/initial-message";
29
27
  import { runListModelsCommand } from "./cli/list-models";
30
28
  import { selectSession } from "./cli/session-picker";
29
+ import { applyStartupCwd } from "./cli/startup-cwd";
31
30
  import { findConfigFile } from "./config";
32
31
  import { ModelRegistry, ModelsConfigFile } from "./config/model-registry";
33
- import { resolveCliModel, resolveModelRoleValue, resolveModelScope, type ScopedModel } from "./config/model-resolver";
32
+ import {
33
+ getModelMatchPreferences,
34
+ resolveCliModel,
35
+ resolveModelRoleValue,
36
+ resolveModelScope,
37
+ type ScopedModel,
38
+ } from "./config/model-resolver";
34
39
  import { getDefault, type SettingPath, Settings, settings } from "./config/settings";
35
40
  import { initializeWithSettings } from "./discovery";
36
41
  import {
@@ -344,11 +349,11 @@ async function runInteractiveMode(
344
349
  }
345
350
  }
346
351
 
347
- type ForkSessionPromptResult = "accepted" | "declined" | "unavailable";
352
+ type SessionPromptResult = "accepted" | "declined" | "unavailable";
348
353
 
349
- type ForkSessionPrompt = (session: SessionInfo) => Promise<ForkSessionPromptResult>;
354
+ type SessionPrompt = (session: SessionInfo) => Promise<SessionPromptResult>;
350
355
 
351
- async function promptForkSession(session: SessionInfo): Promise<ForkSessionPromptResult> {
356
+ async function promptForkSession(session: SessionInfo): Promise<SessionPromptResult> {
352
357
  if (!process.stdin.isTTY) {
353
358
  return "unavailable";
354
359
  }
@@ -362,6 +367,52 @@ async function promptForkSession(session: SessionInfo): Promise<ForkSessionPromp
362
367
  }
363
368
  }
364
369
 
370
+ async function promptMoveSession(session: SessionInfo): Promise<SessionPromptResult> {
371
+ if (!process.stdin.isTTY) {
372
+ return "unavailable";
373
+ }
374
+ const message = `Session's directory no longer exists (${session.cwd}). Move (re-root) it into the current directory? [Y/n] `;
375
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
376
+ try {
377
+ const answer = (await rl.question(message)).trim().toLowerCase();
378
+ return answer === "" || answer === "y" || answer === "yes" ? "accepted" : "declined";
379
+ } finally {
380
+ rl.close();
381
+ }
382
+ }
383
+
384
+ type MissingCwdMoveResult =
385
+ | { status: "not-needed" }
386
+ | { status: "declined" }
387
+ | { status: "moved"; manager: SessionManager };
388
+
389
+ async function moveMissingCwdSessionIfNeeded(
390
+ sessionArg: string,
391
+ session: SessionInfo,
392
+ cwd: string,
393
+ sessionDir: string | undefined,
394
+ askToMoveSession: SessionPrompt,
395
+ ): Promise<MissingCwdMoveResult> {
396
+ const sourceCwd = session.cwd;
397
+ if (!sourceCwd || fsSync.existsSync(sourceCwd)) {
398
+ return { status: "not-needed" };
399
+ }
400
+
401
+ const movePromptResult = await askToMoveSession(session);
402
+ if (movePromptResult === "unavailable") {
403
+ throw new Error(
404
+ `Session "${sessionArg}" belongs to a directory that no longer exists (${sourceCwd}); run interactively to move it into the current project.`,
405
+ );
406
+ }
407
+ if (movePromptResult === "declined") {
408
+ return { status: "declined" };
409
+ }
410
+
411
+ const manager = await SessionManager.open(session.path, sessionDir);
412
+ await manager.moveTo(cwd, sessionDir);
413
+ return { status: "moved", manager };
414
+ }
415
+
365
416
  async function getChangelogForDisplay(parsed: Args): Promise<string | undefined> {
366
417
  if (parsed.continue || parsed.resume) {
367
418
  return undefined;
@@ -407,7 +458,8 @@ export async function createSessionManager(
407
458
  parsed: Args,
408
459
  cwd: string,
409
460
  activeSettings: Settings = settings,
410
- askToForkSession: ForkSessionPrompt = promptForkSession,
461
+ askToForkSession: SessionPrompt = promptForkSession,
462
+ askToMoveSession: SessionPrompt = promptMoveSession,
411
463
  ): Promise<SessionManager | undefined> {
412
464
  if (parsed.fork) {
413
465
  if (parsed.noSession) {
@@ -436,10 +488,38 @@ export async function createSessionManager(
436
488
  if (!match) {
437
489
  throw new Error(`Session "${sessionArg}" not found.`);
438
490
  }
491
+ if (match.scope === "local") {
492
+ const moveResult = await moveMissingCwdSessionIfNeeded(
493
+ sessionArg,
494
+ match.session,
495
+ cwd,
496
+ parsed.sessionDir,
497
+ askToMoveSession,
498
+ );
499
+ if (moveResult.status === "moved") {
500
+ return moveResult.manager;
501
+ }
502
+ if (moveResult.status === "declined") {
503
+ return undefined;
504
+ }
505
+ }
439
506
  if (match.scope === "global") {
440
507
  const normalizedCwd = normalizePathForComparison(cwd);
441
508
  const normalizedMatchCwd = normalizePathForComparison(match.session.cwd || cwd);
442
509
  if (normalizedCwd !== normalizedMatchCwd) {
510
+ const moveResult = await moveMissingCwdSessionIfNeeded(
511
+ sessionArg,
512
+ match.session,
513
+ cwd,
514
+ parsed.sessionDir,
515
+ askToMoveSession,
516
+ );
517
+ if (moveResult.status === "moved") {
518
+ return moveResult.manager;
519
+ }
520
+ if (moveResult.status === "declined") {
521
+ return undefined;
522
+ }
443
523
  const forkPromptResult = await askToForkSession(match.session);
444
524
  if (forkPromptResult === "unavailable") {
445
525
  throw new Error(
@@ -480,56 +560,6 @@ export async function createSessionManager(
480
560
  return undefined;
481
561
  }
482
562
 
483
- async function maybeAutoChdir(parsed: Args): Promise<void> {
484
- if (parsed.allowHome || parsed.cwd) {
485
- return;
486
- }
487
-
488
- const home = os.homedir();
489
- if (!home) {
490
- return;
491
- }
492
-
493
- const normalizePath = normalizePathForComparison;
494
-
495
- const cwd = normalizePath(getProjectDir());
496
- const normalizedHome = normalizePath(home);
497
- if (cwd !== normalizedHome) {
498
- return;
499
- }
500
-
501
- const isDirectory = async (p: string) => {
502
- try {
503
- const s = await fs.stat(p);
504
- return s.isDirectory();
505
- } catch {
506
- return false;
507
- }
508
- };
509
-
510
- const candidates = [path.join(home, "tmp"), "/tmp", "/var/tmp"];
511
- for (const candidate of candidates) {
512
- try {
513
- if (!(await isDirectory(candidate))) {
514
- continue;
515
- }
516
- setProjectDir(candidate);
517
- return;
518
- } catch {
519
- // Try next candidate.
520
- }
521
- }
522
-
523
- try {
524
- const fallback = os.tmpdir();
525
- if (fallback && normalizePath(fallback) !== cwd && (await isDirectory(fallback))) {
526
- setProjectDir(fallback);
527
- }
528
- } catch {
529
- // Ignore fallback errors.
530
- }
531
- }
532
-
533
563
  /** Discover SYSTEM.md file if no CLI system prompt was provided */
534
564
  function discoverSystemPromptFile(): string | undefined {
535
565
  // Check project-local first (.omp/SYSTEM.md, .pi/SYSTEM.md legacy)
@@ -586,9 +616,7 @@ async function buildSessionOptions(
586
616
  // Model from CLI
587
617
  // - supports --provider <name> --model <pattern>
588
618
  // - supports --model <provider>/<pattern>
589
- const modelMatchPreferences = {
590
- usageOrder: activeSettings.getStorage()?.getModelUsageOrder(),
591
- };
619
+ const modelMatchPreferences = getModelMatchPreferences(activeSettings);
592
620
  if (parsed.model) {
593
621
  const resolved = resolveCliModel({
594
622
  cliProvider: parsed.provider,
@@ -745,7 +773,7 @@ export async function runRootCommand(
745
773
  await logger.time("initTheme:initial", initTheme);
746
774
 
747
775
  const parsedArgs = parsed;
748
- await logger.time("maybeAutoChdir", maybeAutoChdir, parsedArgs);
776
+ await logger.time("applyStartupCwd", applyStartupCwd, parsedArgs);
749
777
 
750
778
  const notifs: (InteractiveModeNotify | null)[] = [];
751
779
 
@@ -880,9 +908,7 @@ export async function runRootCommand(
880
908
 
881
909
  let scopedModels: ScopedModel[] = [];
882
910
  const modelPatterns = parsedArgs.models ?? settingsInstance.get("enabledModels");
883
- const modelMatchPreferences = {
884
- usageOrder: settingsInstance.getStorage()?.getModelUsageOrder(),
885
- };
911
+ const modelMatchPreferences = getModelMatchPreferences(settingsInstance);
886
912
  if (modelPatterns && modelPatterns.length > 0) {
887
913
  scopedModels = await logger.time(
888
914
  "resolveModelScope",
@@ -220,6 +220,7 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
220
220
  readonly mcpToolName: string;
221
221
  /** Server name */
222
222
  readonly mcpServerName: string;
223
+ readonly approval = "write" as const;
223
224
  /** Render completed MCP calls with the result header replacing the pending call header. */
224
225
  readonly mergeCallAndResult = true;
225
226
 
@@ -305,6 +306,7 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
305
306
  readonly mcpToolName: string;
306
307
  /** Server name */
307
308
  readonly mcpServerName: string;
309
+ readonly approval = "write" as const;
308
310
  /** Render completed MCP calls with the result header replacing the pending call header. */
309
311
  readonly mergeCallAndResult = true;
310
312
 
@@ -7,7 +7,7 @@ import { type ApiKey, clampThinkingLevelForModel, completeSimple, Effort, type M
7
7
  import { getAgentDbPath, getMemoriesDir, logger, parseJsonlLenient, prompt } from "@oh-my-pi/pi-utils";
8
8
 
9
9
  import type { ModelRegistry } from "../config/model-registry";
10
- import { resolveModelRoleValue } from "../config/model-resolver";
10
+ import { getModelMatchPreferences, resolveModelRoleValue } from "../config/model-resolver";
11
11
  import type { Settings } from "../config/settings";
12
12
  import consolidationTemplate from "../prompts/memories/consolidation.md" with { type: "text" };
13
13
  import readPathTemplate from "../prompts/memories/read-path.md" with { type: "text" };
@@ -1088,7 +1088,7 @@ async function resolveMemoryModel(options: {
1088
1088
  if (requestedModel) {
1089
1089
  const resolved = resolveModelRoleValue(requestedModel, modelRegistry.getAll(), {
1090
1090
  settings: session.settings,
1091
- matchPreferences: { usageOrder: session.settings.getStorage()?.getModelUsageOrder() },
1091
+ matchPreferences: getModelMatchPreferences(session.settings),
1092
1092
  modelRegistry,
1093
1093
  });
1094
1094
  if (resolved.model) return resolved.model;