@oh-my-pi/pi-coding-agent 15.1.3 → 15.1.5

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.
@@ -21,7 +21,7 @@
21
21
  * `@sinclair/typebox` directly in their own package.
22
22
  */
23
23
 
24
- import { areJsonValuesEqual } from "@oh-my-pi/pi-ai/utils/schema";
24
+ import { areJsonValuesEqual, zodToWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
25
25
  import {
26
26
  type ZodArray,
27
27
  type ZodEnum,
@@ -104,19 +104,46 @@ interface ObjectOpts extends Meta {
104
104
  // Helpers
105
105
  // ---------------------------------------------------------------------------
106
106
 
107
+ /**
108
+ * Stamp a non-enumerable `toJSON()` on a schema so `JSON.stringify(schema)`
109
+ * yields a clean draft 2020-12 JSON Schema — matching real TypeBox semantics
110
+ * where the schema object IS already a JSON Schema. Without this, an extension
111
+ * author who serialises the schema across any JSON boundary (worker
112
+ * postMessage, MCP transport, config persistence, network hop, structuredClone
113
+ * fallback) ships the raw Zod internals (`def`, `_zod`, object-shaped `enum`,
114
+ * `"type":"enum"`) — neither valid JSON Schema nor parseable Zod. See
115
+ * issue #1101 for the symptoms when this leaks into a tool's `input_schema`.
116
+ *
117
+ * Idempotent: re-stamping the same instance is a no-op.
118
+ */
119
+ function wire<T extends ZodType>(schema: T): T {
120
+ if (!Object.hasOwn(schema as object, "toJSON")) {
121
+ Object.defineProperty(schema as object, "toJSON", {
122
+ value: function toJSON(this: ZodType) {
123
+ return zodToWireSchema(this);
124
+ },
125
+ enumerable: false,
126
+ writable: true,
127
+ configurable: true,
128
+ });
129
+ }
130
+ return schema;
131
+ }
132
+
107
133
  function withMeta<T extends ZodType>(schema: T, opts: Meta | undefined): T {
108
- if (!opts) return schema;
109
134
  let out: ZodType = schema;
110
- if (typeof opts.description === "string") out = out.describe(opts.description);
111
- if ("default" in opts) out = out.default(opts.default as never) as unknown as ZodType;
135
+ if (opts) {
136
+ if (typeof opts.description === "string") out = out.describe(opts.description);
137
+ if ("default" in opts) out = out.default(opts.default as never) as unknown as ZodType;
112
138
 
113
- const metadata: Record<string, unknown> = {};
114
- for (const [key, value] of Object.entries(opts)) {
115
- if (key === "description" || key === "default" || key === "additionalProperties") continue;
116
- metadata[key] = value;
139
+ const metadata: Record<string, unknown> = {};
140
+ for (const key in opts) {
141
+ if (key === "description" || key === "default" || key === "additionalProperties") continue;
142
+ metadata[key] = opts[key];
143
+ }
144
+ if (Object.keys(metadata).length > 0) out = out.meta(metadata);
117
145
  }
118
- if (Object.keys(metadata).length > 0) out = out.meta(metadata);
119
- return out as T;
146
+ return wire(out as T);
120
147
  }
121
148
 
122
149
  // ---------------------------------------------------------------------------
@@ -313,7 +340,8 @@ function tRecord<V extends ZodType>(key: ZodType, value: V, opts?: Meta): ZodTyp
313
340
  }
314
341
 
315
342
  function tOptional<E extends ZodType>(schema: E, _opts?: Meta): ZodOptional<E> {
316
- return isOptional(schema) ? (schema as unknown as ZodOptional<E>) : (schema.optional() as ZodOptional<E>);
343
+ if (isOptional(schema)) return wire(schema as unknown as ZodOptional<E>);
344
+ return wire(schema.optional() as ZodOptional<E>);
317
345
  }
318
346
 
319
347
  function tNullable<E extends ZodType>(schema: E, opts?: Meta): ZodType {
@@ -322,27 +350,26 @@ function tNullable<E extends ZodType>(schema: E, opts?: Meta): ZodType {
322
350
 
323
351
  function tReadonly<E extends ZodType>(schema: E): E {
324
352
  // TypeBox's `Type.Readonly` is purely a marker; runtime parsing is identical.
325
- return schema;
353
+ return wire(schema);
326
354
  }
327
355
 
328
356
  function tPartial<P extends ZodRawShape>(obj: ZodObject<P>): ZodObject<P> {
329
- return obj.partial() as unknown as ZodObject<P>;
357
+ return wire(obj.partial() as unknown as ZodObject<P>);
330
358
  }
331
359
 
332
360
  function tRequired<P extends ZodRawShape>(obj: ZodObject<P>): ZodObject<P> {
333
- return obj.required() as unknown as ZodObject<P>;
361
+ return wire(obj.required() as unknown as ZodObject<P>);
334
362
  }
335
363
 
336
364
  function tPick<P extends ZodRawShape, K extends keyof P>(obj: ZodObject<P>, keys: readonly K[]): ZodObject<Pick<P, K>> {
337
365
  const mask = Object.fromEntries(keys.map(k => [k as string, true]));
338
- return obj.pick(mask as never) as unknown as ZodObject<Pick<P, K>>;
366
+ return wire(obj.pick(mask as never) as unknown as ZodObject<Pick<P, K>>);
339
367
  }
340
368
 
341
369
  function tOmit<P extends ZodRawShape, K extends keyof P>(obj: ZodObject<P>, keys: readonly K[]): ZodObject<Omit<P, K>> {
342
370
  const mask = Object.fromEntries(keys.map(k => [k as string, true]));
343
- return obj.omit(mask as never) as unknown as ZodObject<Omit<P, K>>;
371
+ return wire(obj.omit(mask as never) as unknown as ZodObject<Omit<P, K>>);
344
372
  }
345
-
346
373
  function tComposite(objects: readonly ZodObject<ZodRawShape>[], opts?: Meta): ZodObject<ZodRawShape> {
347
374
  // `Type.Composite([...])` flattens every object schema into one object schema
348
375
  // rather than producing an intersection. Mirror that via repeated `extend`.
package/src/main.ts CHANGED
@@ -49,8 +49,14 @@ import type { MCPManager } from "./mcp";
49
49
  import { InteractiveMode, runAcpMode, runPrintMode, runRpcMode } from "./modes";
50
50
  import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
51
51
  import type { SubmittedUserInput } from "./modes/types";
52
- import { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage } from "./sdk";
52
+ import {
53
+ type CreateAgentSessionOptions,
54
+ type CreateAgentSessionResult,
55
+ createAgentSession,
56
+ discoverAuthStorage,
57
+ } from "./sdk";
53
58
  import type { AgentSession } from "./session/agent-session";
59
+ import type { AuthStorage } from "./session/auth-storage";
54
60
  import { resolveResumableSession, type SessionInfo, SessionManager } from "./session/session-manager";
55
61
  import { resolvePromptInput } from "./system-prompt";
56
62
  import type { LspStartupServerInfo } from "./tools";
@@ -102,9 +108,9 @@ const RPC_DEFAULTED_SETTING_PATHS: SettingPath[] = [
102
108
  "memories.enabled",
103
109
  ];
104
110
 
105
- function applyRpcDefaultSettingOverrides(): void {
111
+ function applyRpcDefaultSettingOverrides(targetSettings: Settings = settings): void {
106
112
  for (const settingPath of RPC_DEFAULTED_SETTING_PATHS) {
107
- settings.override(settingPath, getDefault(settingPath));
113
+ targetSettings.override(settingPath, getDefault(settingPath));
108
114
  }
109
115
  }
110
116
 
@@ -160,6 +166,73 @@ export async function submitInteractiveInput(
160
166
  }
161
167
  }
162
168
 
169
+ function applyExtensionFlagValues(session: AgentSession, rawArgs: string[]): Map<string, boolean | string> {
170
+ const extensionRunner = session.extensionRunner;
171
+ if (!extensionRunner) {
172
+ return new Map();
173
+ }
174
+
175
+ const extFlags = extensionRunner.getFlags();
176
+ if (extFlags.size > 0) {
177
+ for (let i = 0; i < rawArgs.length; i++) {
178
+ const arg = rawArgs[i];
179
+ if (!arg.startsWith("--")) {
180
+ continue;
181
+ }
182
+ const flagName = arg.slice(2);
183
+ const extFlag = extFlags.get(flagName);
184
+ if (!extFlag) {
185
+ continue;
186
+ }
187
+ if (extFlag.type === "boolean") {
188
+ extensionRunner.setFlagValue(flagName, true);
189
+ continue;
190
+ }
191
+ if (i + 1 < rawArgs.length) {
192
+ extensionRunner.setFlagValue(flagName, rawArgs[++i]);
193
+ }
194
+ }
195
+ }
196
+
197
+ return extensionRunner.getFlagValues();
198
+ }
199
+
200
+ type AcpSessionFactory = (cwd: string) => Promise<AgentSession>;
201
+
202
+ interface AcpSessionFactoryOptions {
203
+ baseOptions: CreateAgentSessionOptions;
204
+ settings: Settings;
205
+ sessionDir?: string;
206
+ authStorage: AuthStorage;
207
+ modelRegistry: ModelRegistry;
208
+ parsedArgs: Pick<Args, "apiKey">;
209
+ rawArgs: string[];
210
+ createSession: (options: CreateAgentSessionOptions) => Promise<CreateAgentSessionResult>;
211
+ }
212
+
213
+ function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSessionFactory {
214
+ return async cwd => {
215
+ const nextSettings = await args.settings.cloneForCwd(cwd);
216
+ const nextSessionManager = SessionManager.create(cwd, args.sessionDir);
217
+ const agentId = `acp:${nextSessionManager.getSessionId()}`;
218
+ const { session: nextSession } = await args.createSession({
219
+ ...args.baseOptions,
220
+ cwd,
221
+ sessionManager: nextSessionManager,
222
+ settings: nextSettings,
223
+ authStorage: args.authStorage,
224
+ modelRegistry: args.modelRegistry,
225
+ agentId,
226
+ hasUI: false,
227
+ });
228
+ if (args.parsedArgs.apiKey && !args.baseOptions.model && nextSession.model) {
229
+ args.authStorage.setRuntimeApiKey(nextSession.model.provider, args.parsedArgs.apiKey);
230
+ }
231
+ applyExtensionFlagValues(nextSession, args.rawArgs);
232
+ return nextSession;
233
+ };
234
+ }
235
+
163
236
  async function runInteractiveMode(
164
237
  session: AgentSession,
165
238
  version: string,
@@ -290,7 +363,11 @@ async function flushChangelogVersion(): Promise<void> {
290
363
  }
291
364
  }
292
365
 
293
- async function createSessionManager(parsed: Args, cwd: string): Promise<SessionManager | undefined> {
366
+ async function createSessionManager(
367
+ parsed: Args,
368
+ cwd: string,
369
+ activeSettings: Settings = settings,
370
+ ): Promise<SessionManager | undefined> {
294
371
  if (parsed.fork) {
295
372
  if (parsed.noSession) {
296
373
  throw new Error("--fork requires session persistence");
@@ -343,7 +420,7 @@ async function createSessionManager(parsed: Args, cwd: string): Promise<SessionM
343
420
  // session exists. When a prior session is resumed, mark parsed.continue so
344
421
  // buildSessionOptions restores the session's model/thinking instead of
345
422
  // overriding them with CLI defaults.
346
- if (settings.get("autoResume")) {
423
+ if (activeSettings.get("autoResume")) {
347
424
  const manager = await SessionManager.continueRecent(cwd, parsed.sessionDir);
348
425
  if (manager.getEntries().length > 0) {
349
426
  parsed.continue = true;
@@ -437,6 +514,7 @@ async function buildSessionOptions(
437
514
  scopedModels: ScopedModel[],
438
515
  sessionManager: SessionManager | undefined,
439
516
  modelRegistry: ModelRegistry,
517
+ activeSettings: Settings,
440
518
  ): Promise<{ options: CreateAgentSessionOptions }> {
441
519
  const options: CreateAgentSessionOptions = {
442
520
  cwd: parsed.cwd ?? getProjectDir(),
@@ -459,7 +537,7 @@ async function buildSessionOptions(
459
537
  // - supports --provider <name> --model <pattern>
460
538
  // - supports --model <provider>/<pattern>
461
539
  const modelMatchPreferences = {
462
- usageOrder: settings.getStorage()?.getModelUsageOrder(),
540
+ usageOrder: activeSettings.getStorage()?.getModelUsageOrder(),
463
541
  };
464
542
  if (parsed.model) {
465
543
  const resolved = resolveCliModel({
@@ -482,7 +560,7 @@ async function buildSessionOptions(
482
560
  }
483
561
  } else if (resolved.model) {
484
562
  options.model = resolved.model;
485
- settings.overrideModelRoles({
563
+ activeSettings.overrideModelRoles({
486
564
  default: resolved.selector ?? `${resolved.model.provider}/${resolved.model.id}`,
487
565
  });
488
566
  if (!parsed.thinking && resolved.thinkingLevel) {
@@ -490,13 +568,13 @@ async function buildSessionOptions(
490
568
  }
491
569
  }
492
570
  } else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
493
- const remembered = settings.getModelRole("default");
571
+ const remembered = activeSettings.getModelRole("default");
494
572
  if (remembered) {
495
573
  const rememberedSpec = resolveModelRoleValue(
496
574
  remembered,
497
575
  scopedModels.map(scopedModel => scopedModel.model),
498
576
  {
499
- settings,
577
+ settings: activeSettings,
500
578
  matchPreferences: modelMatchPreferences,
501
579
  modelRegistry,
502
580
  },
@@ -534,7 +612,7 @@ async function buildSessionOptions(
534
612
 
535
613
  // Scoped models for Ctrl+P cycling - fill in default thinking levels when not explicit
536
614
  if (scopedModels.length > 0) {
537
- const defaultThinkingLevel = settings.get("defaultThinkingLevel");
615
+ const defaultThinkingLevel = activeSettings.get("defaultThinkingLevel");
538
616
  options.scopedModels = scopedModels.map(scopedModel => ({
539
617
  model: scopedModel.model,
540
618
  thinkingLevel: scopedModel.explicitThinkingLevel
@@ -571,7 +649,7 @@ async function buildSessionOptions(
571
649
  options.skills = [];
572
650
  } else if (parsed.skills && parsed.skills.length > 0) {
573
651
  // Override includeSkills for this session
574
- settings.override("skills.includeSkills", parsed.skills as string[]);
652
+ activeSettings.override("skills.includeSkills", parsed.skills as string[]);
575
653
  }
576
654
 
577
655
  // Rules
@@ -593,7 +671,18 @@ async function buildSessionOptions(
593
671
  return { options };
594
672
  }
595
673
 
596
- export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<void> {
674
+ interface RunRootCommandDependencies {
675
+ createAgentSession?: typeof createAgentSession;
676
+ discoverAuthStorage?: typeof discoverAuthStorage;
677
+ runAcpMode?: typeof runAcpMode;
678
+ settings?: Settings;
679
+ }
680
+
681
+ export async function runRootCommand(
682
+ parsed: Args,
683
+ rawArgs: string[],
684
+ deps: RunRootCommandDependencies = {},
685
+ ): Promise<void> {
597
686
  logger.startTiming();
598
687
 
599
688
  // Initialize theme early with defaults (CLI commands need symbols)
@@ -606,7 +695,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
606
695
  const notifs: (InteractiveModeNotify | null)[] = [];
607
696
 
608
697
  // Create AuthStorage and ModelRegistry upfront
609
- const authStorage = await logger.time("discoverModels", discoverAuthStorage);
698
+ const authStorage = await logger.time("discoverModels", deps.discoverAuthStorage ?? discoverAuthStorage);
610
699
  const modelRegistry = new ModelRegistry(authStorage);
611
700
 
612
701
  if (parsedArgs.version) {
@@ -668,9 +757,9 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
668
757
  pluginPreloadPromise.catch(() => {});
669
758
 
670
759
  const cwd = getProjectDir();
671
- const settingsInstance = await logger.time("settings:init", Settings.init, { cwd });
760
+ const settingsInstance = deps.settings ?? (await logger.time("settings:init", Settings.init, { cwd }));
672
761
  if (parsedArgs.mode === "rpc" || parsedArgs.mode === "rpc-ui" || parsedArgs.mode === "acp") {
673
- applyRpcDefaultSettingOverrides();
762
+ applyRpcDefaultSettingOverrides(settingsInstance);
674
763
  }
675
764
  if (parsedArgs.noPty || parsedArgs.mode === "rpc-ui") {
676
765
  Bun.env.PI_NO_PTY = "1";
@@ -684,7 +773,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
684
773
  return { pipedInput, fileText: undefined, fileImages: undefined };
685
774
  }
686
775
  const processed = await processFileArguments(parsedArgs.fileArgs, {
687
- autoResizeImages: settings.get("images.autoResize"),
776
+ autoResizeImages: settingsInstance.get("images.autoResize"),
688
777
  });
689
778
  return { pipedInput, fileText: processed.text, fileImages: processed.images };
690
779
  });
@@ -699,14 +788,14 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
699
788
  const mode = parsedArgs.mode || "text";
700
789
 
701
790
  // Initialize discovery system with settings for provider persistence
702
- logger.time("initializeWithSettings", initializeWithSettings, settings);
791
+ logger.time("initializeWithSettings", initializeWithSettings, settingsInstance);
703
792
 
704
793
  // Apply model role overrides from CLI args or env vars (ephemeral, not persisted)
705
794
  const smolModel = parsedArgs.smol ?? $env.PI_SMOL_MODEL;
706
795
  const slowModel = parsedArgs.slow ?? $env.PI_SLOW_MODEL;
707
796
  const planModel = parsedArgs.plan ?? $env.PI_PLAN_MODEL;
708
797
  if (smolModel || slowModel || planModel) {
709
- settings.overrideModelRoles({
798
+ settingsInstance.overrideModelRoles({
710
799
  smol: smolModel,
711
800
  slow: slowModel,
712
801
  plan: planModel,
@@ -717,16 +806,16 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
717
806
  "initTheme:final",
718
807
  initTheme,
719
808
  isInteractive,
720
- settings.get("symbolPreset"),
721
- settings.get("colorBlindMode"),
722
- settings.get("theme.dark"),
723
- settings.get("theme.light"),
809
+ settingsInstance.get("symbolPreset"),
810
+ settingsInstance.get("colorBlindMode"),
811
+ settingsInstance.get("theme.dark"),
812
+ settingsInstance.get("theme.light"),
724
813
  );
725
814
 
726
815
  let scopedModels: ScopedModel[] = [];
727
- const modelPatterns = parsedArgs.models ?? settings.get("enabledModels");
816
+ const modelPatterns = parsedArgs.models ?? settingsInstance.get("enabledModels");
728
817
  const modelMatchPreferences = {
729
- usageOrder: settings.getStorage()?.getModelUsageOrder(),
818
+ usageOrder: settingsInstance.getStorage()?.getModelUsageOrder(),
730
819
  };
731
820
  if (modelPatterns && modelPatterns.length > 0) {
732
821
  scopedModels = await logger.time(
@@ -739,7 +828,13 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
739
828
  }
740
829
 
741
830
  // Create session manager based on CLI flags
742
- let sessionManager = await logger.time("createSessionManager", createSessionManager, parsedArgs, cwd);
831
+ let sessionManager = await logger.time(
832
+ "createSessionManager",
833
+ createSessionManager,
834
+ parsedArgs,
835
+ cwd,
836
+ settingsInstance,
837
+ );
743
838
 
744
839
  // Handle --resume (no value): show session picker
745
840
  if (parsedArgs.resume === true && !parsedArgs.fork) {
@@ -759,7 +854,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
759
854
  await pluginPreloadPromise;
760
855
 
761
856
  // Background marketplace auto-update — never blocks startup.
762
- const autoUpdate = settings.get("marketplace.autoUpdate");
857
+ const autoUpdate = settingsInstance.get("marketplace.autoUpdate");
763
858
  if (autoUpdate !== "off") {
764
859
  void (async () => {
765
860
  try {
@@ -793,6 +888,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
793
888
  scopedModels,
794
889
  sessionManager,
795
890
  modelRegistry,
891
+ settingsInstance,
796
892
  );
797
893
  sessionOptions.authStorage = authStorage;
798
894
  sessionOptions.modelRegistry = modelRegistry;
@@ -812,140 +908,111 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
812
908
  }
813
909
  }
814
910
 
815
- const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager, eventBus } = await logger.time(
816
- "createAgentSession",
817
- createAgentSession,
818
- sessionOptions,
819
- );
820
- // Kick off background model discovery only after createAgentSession finishes its parallel
821
- // discovery arms; running these concurrently contends for the event loop and stretches
822
- // every parallel arm by ~30ms.
823
- modelRegistry.refreshInBackground();
824
- if (parsedArgs.apiKey && !sessionOptions.model && session.model) {
825
- authStorage.setRuntimeApiKey(session.model.provider, parsedArgs.apiKey);
826
- }
827
-
828
- if (modelFallbackMessage) {
829
- notifs.push({ kind: "warn", message: modelFallbackMessage });
830
- }
831
-
832
- const modelRegistryError = modelRegistry.getError();
833
- if (modelRegistryError) {
834
- notifs.push({ kind: "error", message: modelRegistryError.message });
835
- }
911
+ const createAgentSessionImpl = deps.createAgentSession ?? createAgentSession;
912
+ const createSession = async (options: CreateAgentSessionOptions): Promise<CreateAgentSessionResult> => {
913
+ const result = await logger.time("createAgentSession", createAgentSessionImpl, options);
914
+ // Kick off background model discovery only after createAgentSession finishes its parallel
915
+ // discovery arms; running these concurrently contends for the event loop and stretches
916
+ // every parallel arm by ~30ms.
917
+ modelRegistry.refreshInBackground();
918
+ return result;
919
+ };
836
920
 
837
- // Re-parse CLI args with extension flags and apply values
838
- if (session.extensionRunner) {
839
- const extFlags = session.extensionRunner.getFlags();
840
- if (extFlags.size > 0) {
841
- for (let i = 0; i < rawArgs.length; i++) {
842
- const arg = rawArgs[i];
843
- if (!arg.startsWith("--")) {
844
- continue;
845
- }
846
- const flagName = arg.slice(2);
847
- const extFlag = extFlags.get(flagName);
848
- if (!extFlag) {
849
- continue;
850
- }
851
- if (extFlag.type === "boolean") {
852
- session.extensionRunner.setFlagValue(flagName, true);
853
- continue;
854
- }
855
- if (i + 1 < rawArgs.length) {
856
- session.extensionRunner.setFlagValue(flagName, rawArgs[++i]);
857
- }
858
- }
921
+ if (mode === "acp") {
922
+ const createAcpSession = createAcpSessionFactory({
923
+ baseOptions: sessionOptions,
924
+ settings: settingsInstance,
925
+ sessionDir: parsedArgs.sessionDir,
926
+ authStorage,
927
+ modelRegistry,
928
+ parsedArgs,
929
+ rawArgs,
930
+ createSession,
931
+ });
932
+ await (deps.runAcpMode ?? runAcpMode)(createAcpSession);
933
+ } else {
934
+ const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager, eventBus } =
935
+ await createSession(sessionOptions);
936
+ if (parsedArgs.apiKey && !sessionOptions.model && session.model) {
937
+ authStorage.setRuntimeApiKey(session.model.provider, parsedArgs.apiKey);
859
938
  }
860
- }
861
939
 
862
- if (!isInteractive && parsedArgs.mode !== "acp" && !session.model) {
863
940
  if (modelFallbackMessage) {
864
- process.stderr.write(`${chalk.red(modelFallbackMessage)}\n`);
865
- } else {
866
- process.stderr.write(`${chalk.red("No models available.")}\n`);
941
+ notifs.push({ kind: "warn", message: modelFallbackMessage });
867
942
  }
868
- process.stderr.write(`${chalk.yellow("\nSet an API key environment variable:")}\n`);
869
- process.stderr.write(" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc.\n");
870
- process.stderr.write(`${chalk.yellow(`\nOr create ${ModelsConfigFile.path()}`)}\n`);
871
- process.exit(1);
872
- }
873
943
 
874
- const extensionFlagValues = session.extensionRunner?.getFlagValues() ?? new Map<string, boolean | string>();
875
- const createAcpSession = async (cwd: string) => {
876
- const nextSettings = await session.settings.cloneForCwd(cwd);
877
- const nextSessionManager = SessionManager.create(cwd, parsedArgs.sessionDir);
878
- const { session: nextSession } = await createAgentSession({
879
- ...sessionOptions,
880
- cwd,
881
- sessionManager: nextSessionManager,
882
- settings: nextSettings,
883
- authStorage,
884
- modelRegistry,
885
- hasUI: false,
886
- });
887
- if (nextSession.extensionRunner) {
888
- for (const [flagName, value] of extensionFlagValues) {
889
- nextSession.extensionRunner.setFlagValue(flagName, value);
890
- }
944
+ const modelRegistryError = modelRegistry.getError();
945
+ if (modelRegistryError) {
946
+ notifs.push({ kind: "error", message: modelRegistryError.message });
891
947
  }
892
- return nextSession;
893
- };
894
948
 
895
- if (mode === "rpc" || mode === "rpc-ui") {
896
- await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined);
897
- } else if (mode === "acp") {
898
- await runAcpMode(session, createAcpSession);
899
- } else if (isInteractive) {
900
- const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
901
- const changelogMarkdown = await logger.time("main:getChangelogForDisplay", getChangelogForDisplay, parsedArgs);
902
-
903
- const scopedModelsForDisplay = sessionOptions.scopedModels ?? scopedModels;
904
- if (scopedModelsForDisplay.length > 0) {
905
- const modelList = scopedModelsForDisplay
906
- .map(scopedModel => {
907
- const thinkingStr = !scopedModel.thinkingLevel ? `:${scopedModel.thinkingLevel}` : "";
908
- return `${scopedModel.model.id}${thinkingStr}`;
909
- })
910
- .join(", ");
911
- process.stdout.write(`${chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`)}\n`);
912
- }
913
-
914
- if ($env.PI_TIMING) {
915
- logger.printTimings();
916
- if ($env.PI_TIMING === "x") {
917
- process.exit(0);
949
+ applyExtensionFlagValues(session, rawArgs);
950
+
951
+ if (!isInteractive && !session.model) {
952
+ if (modelFallbackMessage) {
953
+ process.stderr.write(`${chalk.red(modelFallbackMessage)}\n`);
954
+ } else {
955
+ process.stderr.write(`${chalk.red("No models available.")}\n`);
918
956
  }
957
+ process.stderr.write(`${chalk.yellow("\nSet an API key environment variable:")}\n`);
958
+ process.stderr.write(" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc.\n");
959
+ process.stderr.write(`${chalk.yellow(`\nOr create ${ModelsConfigFile.path()}`)}\n`);
960
+ process.exit(1);
919
961
  }
920
962
 
921
- logger.endTiming();
922
- await runInteractiveMode(
923
- session,
924
- VERSION,
925
- changelogMarkdown,
926
- notifs,
927
- versionCheckPromise,
928
- parsedArgs.messages,
929
- setToolUIContext,
930
- lspServers,
931
- mcpManager,
932
- eventBus,
933
- initialMessage,
934
- initialImages,
935
- );
936
- } else {
937
- await runPrintMode(session, {
938
- mode,
939
- messages: parsedArgs.messages,
940
- initialMessage,
941
- initialImages,
942
- });
943
- if ($env.PI_TIMING) {
944
- logger.printTimings();
963
+ if (mode === "rpc" || mode === "rpc-ui") {
964
+ await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined);
965
+ } else if (isInteractive) {
966
+ const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
967
+ const changelogMarkdown = await logger.time("main:getChangelogForDisplay", getChangelogForDisplay, parsedArgs);
968
+
969
+ const scopedModelsForDisplay = sessionOptions.scopedModels ?? scopedModels;
970
+ if (scopedModelsForDisplay.length > 0) {
971
+ const modelList = scopedModelsForDisplay
972
+ .map(scopedModel => {
973
+ const thinkingStr = !scopedModel.thinkingLevel ? `:${scopedModel.thinkingLevel}` : "";
974
+ return `${scopedModel.model.id}${thinkingStr}`;
975
+ })
976
+ .join(", ");
977
+ process.stdout.write(`${chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`)}\n`);
978
+ }
979
+
980
+ if ($env.PI_TIMING) {
981
+ logger.printTimings();
982
+ if ($env.PI_TIMING === "x") {
983
+ process.exit(0);
984
+ }
985
+ }
986
+
987
+ logger.endTiming();
988
+ await runInteractiveMode(
989
+ session,
990
+ VERSION,
991
+ changelogMarkdown,
992
+ notifs,
993
+ versionCheckPromise,
994
+ parsedArgs.messages,
995
+ setToolUIContext,
996
+ lspServers,
997
+ mcpManager,
998
+ eventBus,
999
+ initialMessage,
1000
+ initialImages,
1001
+ );
1002
+ } else {
1003
+ await runPrintMode(session, {
1004
+ mode,
1005
+ messages: parsedArgs.messages,
1006
+ initialMessage,
1007
+ initialImages,
1008
+ });
1009
+ if ($env.PI_TIMING) {
1010
+ logger.printTimings();
1011
+ }
1012
+ await session.dispose();
1013
+ stopThemeWatcher();
1014
+ await postmortem.quit(0);
945
1015
  }
946
- await session.dispose();
947
- stopThemeWatcher();
948
- await postmortem.quit(0);
949
1016
  }
950
1017
  }
951
1018