@codex-infinity/pi-infinity 0.64.3 → 0.65.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 (87) hide show
  1. package/CHANGELOG.md +54 -34
  2. package/README.md +1 -1
  3. package/dist/cli/args.d.ts +7 -4
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +37 -15
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/core/agent-session-runtime.d.ts +51 -104
  8. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  9. package/dist/core/agent-session-runtime.js +103 -141
  10. package/dist/core/agent-session-runtime.js.map +1 -1
  11. package/dist/core/agent-session-services.d.ts +86 -0
  12. package/dist/core/agent-session-services.d.ts.map +1 -0
  13. package/dist/core/agent-session-services.js +116 -0
  14. package/dist/core/agent-session-services.js.map +1 -0
  15. package/dist/core/agent-session.d.ts.map +1 -1
  16. package/dist/core/agent-session.js +1 -1
  17. package/dist/core/agent-session.js.map +1 -1
  18. package/dist/core/extensions/index.d.ts +1 -1
  19. package/dist/core/extensions/index.d.ts.map +1 -1
  20. package/dist/core/extensions/index.js.map +1 -1
  21. package/dist/core/extensions/types.d.ts +1 -13
  22. package/dist/core/extensions/types.d.ts.map +1 -1
  23. package/dist/core/extensions/types.js.map +1 -1
  24. package/dist/core/index.d.ts +2 -1
  25. package/dist/core/index.d.ts.map +1 -1
  26. package/dist/core/index.js +2 -1
  27. package/dist/core/index.js.map +1 -1
  28. package/dist/core/keybindings.d.ts +4 -1
  29. package/dist/core/keybindings.d.ts.map +1 -1
  30. package/dist/core/keybindings.js +3 -14
  31. package/dist/core/keybindings.js.map +1 -1
  32. package/dist/core/package-manager.d.ts +20 -0
  33. package/dist/core/package-manager.d.ts.map +1 -1
  34. package/dist/core/package-manager.js +32 -0
  35. package/dist/core/package-manager.js.map +1 -1
  36. package/dist/core/resource-loader.d.ts.map +1 -1
  37. package/dist/core/resource-loader.js +21 -0
  38. package/dist/core/resource-loader.js.map +1 -1
  39. package/dist/core/sdk.d.ts +1 -1
  40. package/dist/core/sdk.d.ts.map +1 -1
  41. package/dist/core/sdk.js +1 -1
  42. package/dist/core/sdk.js.map +1 -1
  43. package/dist/core/settings-manager.d.ts +1 -1
  44. package/dist/core/settings-manager.d.ts.map +1 -1
  45. package/dist/core/settings-manager.js +2 -1
  46. package/dist/core/settings-manager.js.map +1 -1
  47. package/dist/index.d.ts +1 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +2 -2
  50. package/dist/index.js.map +1 -1
  51. package/dist/main.d.ts.map +1 -1
  52. package/dist/main.js +202 -457
  53. package/dist/main.js.map +1 -1
  54. package/dist/migrations.d.ts.map +1 -1
  55. package/dist/migrations.js +20 -0
  56. package/dist/migrations.js.map +1 -1
  57. package/dist/modes/interactive/interactive-mode.d.ts +3 -2
  58. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  59. package/dist/modes/interactive/interactive-mode.js +56 -29
  60. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  61. package/dist/modes/print-mode.d.ts +2 -2
  62. package/dist/modes/print-mode.d.ts.map +1 -1
  63. package/dist/modes/print-mode.js +4 -0
  64. package/dist/modes/print-mode.js.map +1 -1
  65. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  66. package/dist/modes/rpc/rpc-client.js +1 -0
  67. package/dist/modes/rpc/rpc-client.js.map +1 -1
  68. package/dist/modes/rpc/rpc-mode.d.ts +2 -2
  69. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  70. package/dist/modes/rpc/rpc-mode.js +23 -15
  71. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  72. package/dist/package-manager-cli.d.ts +4 -0
  73. package/dist/package-manager-cli.d.ts.map +1 -0
  74. package/dist/package-manager-cli.js +234 -0
  75. package/dist/package-manager-cli.js.map +1 -0
  76. package/docs/extensions.md +2 -26
  77. package/docs/sdk.md +97 -37
  78. package/docs/settings.md +1 -1
  79. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  80. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  81. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  82. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  83. package/examples/extensions/with-deps/package-lock.json +2 -2
  84. package/examples/extensions/with-deps/package.json +1 -1
  85. package/examples/sdk/13-session-runtime.ts +30 -12
  86. package/examples/sdk/README.md +2 -0
  87. package/package.json +4 -4
package/dist/main.js CHANGED
@@ -9,21 +9,17 @@ import { modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai";
9
9
  import chalk from "chalk";
10
10
  import { createInterface } from "readline";
11
11
  import { parseArgs, printHelp } from "./cli/args.js";
12
- import { selectConfig } from "./cli/config-selector.js";
13
12
  import { processFileArguments } from "./cli/file-processor.js";
14
13
  import { buildInitialMessage } from "./cli/initial-message.js";
15
14
  import { listModels } from "./cli/list-models.js";
16
15
  import { selectSession } from "./cli/session-picker.js";
17
- import { APP_NAME, getAgentDir, getModelsPath, VERSION } from "./config.js";
18
- import { AgentSessionRuntimeHost, createAgentSessionRuntime, } from "./core/agent-session-runtime.js";
16
+ import { getAgentDir, getModelsPath, VERSION } from "./config.js";
17
+ import { createAgentSessionRuntime } from "./core/agent-session-runtime.js";
18
+ import { createAgentSessionFromServices, createAgentSessionServices, } from "./core/agent-session-services.js";
19
19
  import { AuthStorage } from "./core/auth-storage.js";
20
20
  import { exportFromFile } from "./core/export-html/index.js";
21
- import { migrateKeybindingsConfigFile } from "./core/keybindings.js";
22
- import { ModelRegistry } from "./core/model-registry.js";
23
21
  import { resolveCliModel, resolveModelScope } from "./core/model-resolver.js";
24
22
  import { restoreStdout, takeOverStdout } from "./core/output-guard.js";
25
- import { DefaultPackageManager } from "./core/package-manager.js";
26
- import { DefaultResourceLoader } from "./core/resource-loader.js";
27
23
  import { SessionManager } from "./core/session-manager.js";
28
24
  import { SettingsManager } from "./core/settings-manager.js";
29
25
  import { printTimings, resetTimings, time } from "./core/timings.js";
@@ -31,6 +27,7 @@ import { allTools } from "./core/tools/index.js";
31
27
  import { runMigrations, showDeprecationWarnings } from "./migrations.js";
32
28
  import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js";
33
29
  import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js";
30
+ import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.js";
34
31
  /**
35
32
  * Read all content from piped stdin.
36
33
  * Returns undefined if stdin is a TTY (interactive terminal).
@@ -52,13 +49,17 @@ async function readPipedStdin() {
52
49
  process.stdin.resume();
53
50
  });
54
51
  }
55
- function reportSettingsErrors(settingsManager, context) {
56
- const errors = settingsManager.drainErrors();
57
- for (const { scope, error } of errors) {
58
- console.error(chalk.yellow(`Warning (${context}, ${scope} settings): ${error.message}`));
59
- if (error.stack) {
60
- console.error(chalk.dim(error.stack));
61
- }
52
+ function collectSettingsDiagnostics(settingsManager, context) {
53
+ return settingsManager.drainErrors().map(({ scope, error }) => ({
54
+ type: "warning",
55
+ message: `(${context}, ${scope} settings) ${error.message}`,
56
+ }));
57
+ }
58
+ function reportDiagnostics(diagnostics) {
59
+ for (const diagnostic of diagnostics) {
60
+ const color = diagnostic.type === "error" ? chalk.red : diagnostic.type === "warning" ? chalk.yellow : chalk.dim;
61
+ const prefix = diagnostic.type === "error" ? "Error: " : diagnostic.type === "warning" ? "Warning: " : "";
62
+ console.error(color(`${prefix}${diagnostic.message}`));
62
63
  }
63
64
  }
64
65
  function isTruthyEnvFlag(value) {
@@ -66,212 +67,20 @@ function isTruthyEnvFlag(value) {
66
67
  return false;
67
68
  return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes";
68
69
  }
69
- function getPackageCommandUsage(command) {
70
- switch (command) {
71
- case "install":
72
- return `${APP_NAME} install <source> [-l]`;
73
- case "remove":
74
- return `${APP_NAME} remove <source> [-l]`;
75
- case "update":
76
- return `${APP_NAME} update [source]`;
77
- case "list":
78
- return `${APP_NAME} list`;
79
- }
80
- }
81
- function printPackageCommandHelp(command) {
82
- switch (command) {
83
- case "install":
84
- console.log(`${chalk.bold("Usage:")}
85
- ${getPackageCommandUsage("install")}
86
-
87
- Install a package and add it to settings.
88
-
89
- Options:
90
- -l, --local Install project-locally (.pi/settings.json)
91
-
92
- Examples:
93
- ${APP_NAME} install npm:@foo/bar
94
- ${APP_NAME} install git:github.com/user/repo
95
- ${APP_NAME} install git:git@github.com:user/repo
96
- ${APP_NAME} install https://github.com/user/repo
97
- ${APP_NAME} install ssh://git@github.com/user/repo
98
- ${APP_NAME} install ./local/path
99
- `);
100
- return;
101
- case "remove":
102
- console.log(`${chalk.bold("Usage:")}
103
- ${getPackageCommandUsage("remove")}
104
-
105
- Remove a package and its source from settings.
106
- Alias: ${APP_NAME} uninstall <source> [-l]
107
-
108
- Options:
109
- -l, --local Remove from project settings (.pi/settings.json)
110
-
111
- Examples:
112
- ${APP_NAME} remove npm:@foo/bar
113
- ${APP_NAME} uninstall npm:@foo/bar
114
- `);
115
- return;
116
- case "update":
117
- console.log(`${chalk.bold("Usage:")}
118
- ${getPackageCommandUsage("update")}
119
-
120
- Update installed packages.
121
- If <source> is provided, only that package is updated.
122
- `);
123
- return;
124
- case "list":
125
- console.log(`${chalk.bold("Usage:")}
126
- ${getPackageCommandUsage("list")}
127
-
128
- List installed packages from user and project settings.
129
- `);
130
- return;
131
- }
132
- }
133
- function parsePackageCommand(args) {
134
- const [rawCommand, ...rest] = args;
135
- let command;
136
- if (rawCommand === "uninstall") {
137
- command = "remove";
70
+ function resolveAppMode(parsed, stdinIsTTY) {
71
+ if (parsed.mode === "rpc") {
72
+ return "rpc";
138
73
  }
139
- else if (rawCommand === "install" || rawCommand === "remove" || rawCommand === "update" || rawCommand === "list") {
140
- command = rawCommand;
74
+ if (parsed.mode === "json") {
75
+ return "json";
141
76
  }
142
- if (!command) {
143
- return undefined;
144
- }
145
- let local = false;
146
- let help = false;
147
- let invalidOption;
148
- let source;
149
- for (const arg of rest) {
150
- if (arg === "-h" || arg === "--help") {
151
- help = true;
152
- continue;
153
- }
154
- if (arg === "-l" || arg === "--local") {
155
- if (command === "install" || command === "remove") {
156
- local = true;
157
- }
158
- else {
159
- invalidOption = invalidOption ?? arg;
160
- }
161
- continue;
162
- }
163
- if (arg.startsWith("-")) {
164
- invalidOption = invalidOption ?? arg;
165
- continue;
166
- }
167
- if (!source) {
168
- source = arg;
169
- }
77
+ if (parsed.print || !stdinIsTTY) {
78
+ return "print";
170
79
  }
171
- return { command, source, local, help, invalidOption };
80
+ return "interactive";
172
81
  }
173
- async function handlePackageCommand(args) {
174
- const options = parsePackageCommand(args);
175
- if (!options) {
176
- return false;
177
- }
178
- if (options.help) {
179
- printPackageCommandHelp(options.command);
180
- return true;
181
- }
182
- if (options.invalidOption) {
183
- console.error(chalk.red(`Unknown option ${options.invalidOption} for "${options.command}".`));
184
- console.error(chalk.dim(`Use "${APP_NAME} --help" or "${getPackageCommandUsage(options.command)}".`));
185
- process.exitCode = 1;
186
- return true;
187
- }
188
- const source = options.source;
189
- if ((options.command === "install" || options.command === "remove") && !source) {
190
- console.error(chalk.red(`Missing ${options.command} source.`));
191
- console.error(chalk.dim(`Usage: ${getPackageCommandUsage(options.command)}`));
192
- process.exitCode = 1;
193
- return true;
194
- }
195
- const cwd = process.cwd();
196
- const agentDir = getAgentDir();
197
- const settingsManager = SettingsManager.create(cwd, agentDir);
198
- reportSettingsErrors(settingsManager, "package command");
199
- const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
200
- packageManager.setProgressCallback((event) => {
201
- if (event.type === "start") {
202
- process.stdout.write(chalk.dim(`${event.message}\n`));
203
- }
204
- });
205
- try {
206
- switch (options.command) {
207
- case "install":
208
- await packageManager.install(source, { local: options.local });
209
- packageManager.addSourceToSettings(source, { local: options.local });
210
- console.log(chalk.green(`Installed ${source}`));
211
- return true;
212
- case "remove": {
213
- await packageManager.remove(source, { local: options.local });
214
- const removed = packageManager.removeSourceFromSettings(source, { local: options.local });
215
- if (!removed) {
216
- console.error(chalk.red(`No matching package found for ${source}`));
217
- process.exitCode = 1;
218
- return true;
219
- }
220
- console.log(chalk.green(`Removed ${source}`));
221
- return true;
222
- }
223
- case "list": {
224
- const globalSettings = settingsManager.getGlobalSettings();
225
- const projectSettings = settingsManager.getProjectSettings();
226
- const globalPackages = globalSettings.packages ?? [];
227
- const projectPackages = projectSettings.packages ?? [];
228
- if (globalPackages.length === 0 && projectPackages.length === 0) {
229
- console.log(chalk.dim("No packages installed."));
230
- return true;
231
- }
232
- const formatPackage = (pkg, scope) => {
233
- const source = typeof pkg === "string" ? pkg : pkg.source;
234
- const filtered = typeof pkg === "object";
235
- const display = filtered ? `${source} (filtered)` : source;
236
- console.log(` ${display}`);
237
- const path = packageManager.getInstalledPath(source, scope);
238
- if (path) {
239
- console.log(chalk.dim(` ${path}`));
240
- }
241
- };
242
- if (globalPackages.length > 0) {
243
- console.log(chalk.bold("User packages:"));
244
- for (const pkg of globalPackages) {
245
- formatPackage(pkg, "user");
246
- }
247
- }
248
- if (projectPackages.length > 0) {
249
- if (globalPackages.length > 0)
250
- console.log();
251
- console.log(chalk.bold("Project packages:"));
252
- for (const pkg of projectPackages) {
253
- formatPackage(pkg, "project");
254
- }
255
- }
256
- return true;
257
- }
258
- case "update":
259
- await packageManager.update(source);
260
- if (source) {
261
- console.log(chalk.green(`Updated ${source}`));
262
- }
263
- else {
264
- console.log(chalk.green("Updated packages"));
265
- }
266
- return true;
267
- }
268
- }
269
- catch (error) {
270
- const message = error instanceof Error ? error.message : "Unknown package command error";
271
- console.error(chalk.red(`Error: ${message}`));
272
- process.exitCode = 1;
273
- return true;
274
- }
82
+ function toPrintOutputMode(appMode) {
83
+ return appMode === "json" ? "json" : "text";
275
84
  }
276
85
  async function prepareInitialMessage(parsed, autoResizeImages, stdinContent) {
277
86
  if (parsed.fileArgs.length === 0) {
@@ -323,29 +132,6 @@ async function promptConfirm(message) {
323
132
  });
324
133
  });
325
134
  }
326
- /** Helper to call CLI-only session_directory handlers before the initial session manager is created */
327
- async function callSessionDirectoryHook(extensions, cwd) {
328
- let customSessionDir;
329
- for (const ext of extensions.extensions) {
330
- const handlers = ext.handlers.get("session_directory");
331
- if (!handlers || handlers.length === 0)
332
- continue;
333
- for (const handler of handlers) {
334
- try {
335
- const event = { type: "session_directory", cwd };
336
- const result = (await handler(event));
337
- if (result?.sessionDir) {
338
- customSessionDir = result.sessionDir;
339
- }
340
- }
341
- catch (err) {
342
- const message = err instanceof Error ? err.message : String(err);
343
- console.error(chalk.red(`Extension "${ext.path}" session_directory handler failed: ${message}`));
344
- }
345
- }
346
- }
347
- return customSessionDir;
348
- }
349
135
  function validateForkFlags(parsed) {
350
136
  if (!parsed.fork)
351
137
  return;
@@ -370,62 +156,65 @@ function forkSessionOrExit(sourcePath, cwd, sessionDir) {
370
156
  process.exit(1);
371
157
  }
372
158
  }
373
- async function createSessionManager(parsed, cwd, extensions, settingsManager) {
159
+ async function createSessionManager(parsed, cwd, sessionDir, settingsManager) {
374
160
  if (parsed.noSession) {
375
161
  return SessionManager.inMemory();
376
162
  }
377
- // Priority: CLI flag > settings.json > extension hook
378
- const effectiveSessionDir = parsed.sessionDir ?? settingsManager.getSessionDir() ?? (await callSessionDirectoryHook(extensions, cwd));
379
163
  if (parsed.fork) {
380
- const resolved = await resolveSessionPath(parsed.fork, cwd, effectiveSessionDir);
164
+ const resolved = await resolveSessionPath(parsed.fork, cwd, sessionDir);
381
165
  switch (resolved.type) {
382
166
  case "path":
383
167
  case "local":
384
168
  case "global":
385
- return forkSessionOrExit(resolved.path, cwd, effectiveSessionDir);
169
+ return forkSessionOrExit(resolved.path, cwd, sessionDir);
386
170
  case "not_found":
387
171
  console.error(chalk.red(`No session found matching '${resolved.arg}'`));
388
172
  process.exit(1);
389
173
  }
390
174
  }
391
175
  if (parsed.session) {
392
- const resolved = await resolveSessionPath(parsed.session, cwd, effectiveSessionDir);
176
+ const resolved = await resolveSessionPath(parsed.session, cwd, sessionDir);
393
177
  switch (resolved.type) {
394
178
  case "path":
395
179
  case "local":
396
- return SessionManager.open(resolved.path, effectiveSessionDir);
180
+ return SessionManager.open(resolved.path, sessionDir);
397
181
  case "global": {
398
- // Session found in different project - ask user if they want to fork
399
182
  console.log(chalk.yellow(`Session found in different project: ${resolved.cwd}`));
400
183
  const shouldFork = await promptConfirm("Fork this session into current directory?");
401
184
  if (!shouldFork) {
402
185
  console.log(chalk.dim("Aborted."));
403
186
  process.exit(0);
404
187
  }
405
- return forkSessionOrExit(resolved.path, cwd, effectiveSessionDir);
188
+ return forkSessionOrExit(resolved.path, cwd, sessionDir);
406
189
  }
407
190
  case "not_found":
408
191
  console.error(chalk.red(`No session found matching '${resolved.arg}'`));
409
192
  process.exit(1);
410
193
  }
411
194
  }
412
- if (parsed.continue) {
413
- return SessionManager.continueRecent(cwd, effectiveSessionDir);
195
+ if (parsed.resume) {
196
+ initTheme(settingsManager.getTheme(), true);
197
+ try {
198
+ const selectedPath = await selectSession((onProgress) => SessionManager.list(cwd, sessionDir, onProgress), SessionManager.listAll);
199
+ if (!selectedPath) {
200
+ console.log(chalk.dim("No session selected"));
201
+ process.exit(0);
202
+ }
203
+ return SessionManager.open(selectedPath, sessionDir);
204
+ }
205
+ finally {
206
+ stopThemeWatcher();
207
+ }
414
208
  }
415
- // --resume is handled separately (needs picker UI)
416
- // If effective session dir is set, create new session there
417
- if (effectiveSessionDir) {
418
- return SessionManager.create(cwd, effectiveSessionDir);
209
+ if (parsed.continue) {
210
+ return SessionManager.continueRecent(cwd, sessionDir);
419
211
  }
420
- // Default case (new session) returns undefined, SDK will create one
421
- return undefined;
212
+ return SessionManager.create(cwd, sessionDir);
422
213
  }
423
- function buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry, settingsManager) {
214
+ function buildSessionOptions(parsed, scopedModels, hasExistingSession, modelRegistry, settingsManager) {
424
215
  const options = {};
216
+ const diagnostics = [];
425
217
  let cliThinkingFromModel = false;
426
- if (sessionManager) {
427
- options.sessionManager = sessionManager;
428
- }
429
218
  // Model from CLI
430
219
  // - supports --provider <name> --model <pattern>
431
220
  // - supports --model <provider>/<pattern>
@@ -436,11 +225,10 @@ function buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry
436
225
  modelRegistry,
437
226
  });
438
227
  if (resolved.warning) {
439
- console.warn(chalk.yellow(`Warning: ${resolved.warning}`));
228
+ diagnostics.push({ type: "warning", message: resolved.warning });
440
229
  }
441
230
  if (resolved.error) {
442
- console.error(chalk.red(resolved.error));
443
- process.exit(1);
231
+ diagnostics.push({ type: "error", message: resolved.error });
444
232
  }
445
233
  if (resolved.model) {
446
234
  options.model = resolved.model;
@@ -452,7 +240,7 @@ function buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry
452
240
  }
453
241
  }
454
242
  }
455
- if (!options.model && scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
243
+ if (!options.model && scopedModels.length > 0 && !hasExistingSession) {
456
244
  // Check if saved default is in scoped models - use it if so, otherwise first scoped model
457
245
  const savedProvider = settingsManager.getDefaultProvider();
458
246
  const savedModelId = settingsManager.getDefaultModel();
@@ -509,52 +297,11 @@ function buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry
509
297
  if (parsed.autoNextIdea) {
510
298
  options.autoNextIdea = true;
511
299
  }
512
- return { options, cliThinkingFromModel };
300
+ return { options, cliThinkingFromModel, diagnostics };
513
301
  }
514
302
  function resolveCliPaths(cwd, paths) {
515
303
  return paths?.map((value) => resolve(cwd, value));
516
304
  }
517
- function buildRuntimeBootstrap(parsed, cwd, agentDir, authStorage, sessionOptions) {
518
- return {
519
- agentDir,
520
- authStorage,
521
- model: sessionOptions.model,
522
- thinkingLevel: sessionOptions.thinkingLevel,
523
- scopedModels: sessionOptions.scopedModels,
524
- tools: sessionOptions.tools,
525
- customTools: sessionOptions.customTools,
526
- resourceLoader: {
527
- additionalExtensionPaths: resolveCliPaths(cwd, parsed.extensions),
528
- additionalSkillPaths: resolveCliPaths(cwd, parsed.skills),
529
- additionalPromptTemplatePaths: resolveCliPaths(cwd, parsed.promptTemplates),
530
- additionalThemePaths: resolveCliPaths(cwd, parsed.themes),
531
- noExtensions: parsed.noExtensions,
532
- noSkills: parsed.noSkills,
533
- noPromptTemplates: parsed.noPromptTemplates,
534
- noThemes: parsed.noThemes,
535
- systemPrompt: parsed.systemPrompt,
536
- appendSystemPrompt: parsed.appendSystemPrompt,
537
- },
538
- };
539
- }
540
- async function handleConfigCommand(args) {
541
- if (args[0] !== "config") {
542
- return false;
543
- }
544
- const cwd = process.cwd();
545
- const agentDir = getAgentDir();
546
- const settingsManager = SettingsManager.create(cwd, agentDir);
547
- reportSettingsErrors(settingsManager, "config command");
548
- const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
549
- const resolvedPaths = await packageManager.resolve();
550
- await selectConfig({
551
- resolvedPaths,
552
- settingsManager,
553
- cwd,
554
- agentDir,
555
- });
556
- process.exit(0);
557
- }
558
305
  export async function main(args) {
559
306
  resetTimings();
560
307
  const offlineMode = args.includes("--offline") || isTruthyEnvFlag(process.env.PI_OFFLINE);
@@ -568,93 +315,26 @@ export async function main(args) {
568
315
  if (await handleConfigCommand(args)) {
569
316
  return;
570
317
  }
571
- // First pass: parse args to get --extension paths
572
- const firstPass = parseArgs(args);
573
- time("parseArgs.firstPass");
574
- const shouldTakeOverStdout = firstPass.mode !== undefined || firstPass.print || !process.stdin.isTTY;
575
- if (shouldTakeOverStdout) {
576
- takeOverStdout();
577
- }
578
- // Run migrations (pass cwd for project-local migrations)
579
- const { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(process.cwd());
580
- time("runMigrations");
581
- // Early load extensions to discover their CLI flags
582
- const cwd = process.cwd();
583
- const agentDir = getAgentDir();
584
- const settingsManager = SettingsManager.create(cwd, agentDir);
585
- reportSettingsErrors(settingsManager, "startup");
586
- const authStorage = AuthStorage.create();
587
- const modelRegistry = ModelRegistry.create(authStorage, getModelsPath());
588
- const resourceLoader = new DefaultResourceLoader({
589
- cwd,
590
- agentDir,
591
- settingsManager,
592
- additionalExtensionPaths: firstPass.extensions,
593
- additionalSkillPaths: firstPass.skills,
594
- additionalPromptTemplatePaths: firstPass.promptTemplates,
595
- additionalThemePaths: firstPass.themes,
596
- noExtensions: firstPass.noExtensions,
597
- noSkills: firstPass.noSkills,
598
- noPromptTemplates: firstPass.noPromptTemplates,
599
- noThemes: firstPass.noThemes,
600
- systemPrompt: firstPass.systemPrompt,
601
- appendSystemPrompt: firstPass.appendSystemPrompt,
602
- });
603
- time("createResourceLoader");
604
- await resourceLoader.reload();
605
- time("resourceLoader.reload");
606
- const extensionsResult = resourceLoader.getExtensions();
607
- for (const { path, error } of extensionsResult.errors) {
608
- console.error(chalk.red(`Failed to load extension "${path}": ${error}`));
609
- }
610
- // Apply pending provider registrations from extensions immediately
611
- // so they're available for model resolution before AgentSession is created
612
- for (const { name, config, extensionPath } of extensionsResult.runtime.pendingProviderRegistrations) {
613
- try {
614
- modelRegistry.registerProvider(name, config);
318
+ const parsed = parseArgs(args);
319
+ if (parsed.diagnostics.length > 0) {
320
+ for (const d of parsed.diagnostics) {
321
+ const color = d.type === "error" ? chalk.red : chalk.yellow;
322
+ console.error(color(`${d.type === "error" ? "Error" : "Warning"}: ${d.message}`));
615
323
  }
616
- catch (error) {
617
- const message = error instanceof Error ? error.message : String(error);
618
- console.error(chalk.red(`Extension "${extensionPath}" error: ${message}`));
619
- }
620
- }
621
- extensionsResult.runtime.pendingProviderRegistrations = [];
622
- const extensionFlags = new Map();
623
- for (const ext of extensionsResult.extensions) {
624
- for (const [name, flag] of ext.flags) {
625
- extensionFlags.set(name, { type: flag.type });
324
+ if (parsed.diagnostics.some((d) => d.type === "error")) {
325
+ process.exit(1);
626
326
  }
627
327
  }
628
- // Second pass: parse args with extension flags
629
- const parsed = parseArgs(args, extensionFlags);
630
- time("parseArgs.secondPass");
631
- // Pass flag values to extensions via runtime
632
- for (const [name, value] of parsed.unknownFlags) {
633
- extensionsResult.runtime.flagValues.set(name, value);
328
+ time("parseArgs");
329
+ let appMode = resolveAppMode(parsed, process.stdin.isTTY);
330
+ const shouldTakeOverStdout = appMode !== "interactive";
331
+ if (shouldTakeOverStdout) {
332
+ takeOverStdout();
634
333
  }
635
334
  if (parsed.version) {
636
335
  console.log(VERSION);
637
336
  process.exit(0);
638
337
  }
639
- if (parsed.help) {
640
- printHelp();
641
- process.exit(0);
642
- }
643
- if (parsed.listModels !== undefined) {
644
- const searchPattern = typeof parsed.listModels === "string" ? parsed.listModels : undefined;
645
- await listModels(modelRegistry, searchPattern);
646
- process.exit(0);
647
- }
648
- // Read piped stdin content (if any) - skip for RPC mode which uses stdin for JSON-RPC
649
- let stdinContent;
650
- if (parsed.mode !== "rpc") {
651
- stdinContent = await readPipedStdin();
652
- if (stdinContent !== undefined) {
653
- // Force print mode since interactive mode requires a TTY for keyboard input
654
- parsed.print = true;
655
- }
656
- }
657
- time("readPipedStdin");
658
338
  if (parsed.export) {
659
339
  let result;
660
340
  try {
@@ -669,98 +349,163 @@ export async function main(args) {
669
349
  console.log(`Exported to: ${result}`);
670
350
  process.exit(0);
671
351
  }
672
- migrateKeybindingsConfigFile(agentDir);
673
- time("migrateKeybindingsConfigFile");
674
352
  if (parsed.mode === "rpc" && parsed.fileArgs.length > 0) {
675
353
  console.error(chalk.red("Error: @file arguments are not supported in RPC mode"));
676
354
  process.exit(1);
677
355
  }
678
356
  validateForkFlags(parsed);
357
+ // Run migrations (pass cwd for project-local migrations)
358
+ const { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(process.cwd());
359
+ time("runMigrations");
360
+ const cwd = process.cwd();
361
+ const agentDir = getAgentDir();
362
+ const startupSettingsManager = SettingsManager.create(cwd, agentDir);
363
+ reportDiagnostics(collectSettingsDiagnostics(startupSettingsManager, "startup session lookup"));
364
+ // Decide the final runtime cwd before creating cwd-bound runtime services.
365
+ // --session and --resume may select a session from another project, so project-local
366
+ // settings, resources, provider registrations, and models must be resolved only after
367
+ // the target session cwd is known. The startup-cwd settings manager is used only for
368
+ // sessionDir lookup during session selection.
369
+ const sessionManager = await createSessionManager(parsed, cwd, parsed.sessionDir ?? startupSettingsManager.getSessionDir(), startupSettingsManager);
370
+ time("createSessionManager");
371
+ const resolvedExtensionPaths = resolveCliPaths(cwd, parsed.extensions);
372
+ const resolvedSkillPaths = resolveCliPaths(cwd, parsed.skills);
373
+ const resolvedPromptTemplatePaths = resolveCliPaths(cwd, parsed.promptTemplates);
374
+ const resolvedThemePaths = resolveCliPaths(cwd, parsed.themes);
375
+ const authStorage = AuthStorage.create();
376
+ const createRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent, }) => {
377
+ const services = await createAgentSessionServices({
378
+ cwd,
379
+ agentDir,
380
+ authStorage,
381
+ extensionFlagValues: parsed.unknownFlags,
382
+ resourceLoaderOptions: {
383
+ additionalExtensionPaths: resolvedExtensionPaths,
384
+ additionalSkillPaths: resolvedSkillPaths,
385
+ additionalPromptTemplatePaths: resolvedPromptTemplatePaths,
386
+ additionalThemePaths: resolvedThemePaths,
387
+ noExtensions: parsed.noExtensions,
388
+ noSkills: parsed.noSkills,
389
+ noPromptTemplates: parsed.noPromptTemplates,
390
+ noThemes: parsed.noThemes,
391
+ systemPrompt: parsed.systemPrompt,
392
+ appendSystemPrompt: parsed.appendSystemPrompt,
393
+ },
394
+ });
395
+ const { settingsManager, modelRegistry, resourceLoader } = services;
396
+ const diagnostics = [
397
+ ...services.diagnostics,
398
+ ...collectSettingsDiagnostics(settingsManager, "runtime creation"),
399
+ ...resourceLoader.getExtensions().errors.map(({ path, error }) => ({
400
+ type: "error",
401
+ message: `Failed to load extension "${path}": ${error}`,
402
+ })),
403
+ ];
404
+ const modelPatterns = parsed.models ?? settingsManager.getEnabledModels();
405
+ const scopedModels = modelPatterns && modelPatterns.length > 0 ? await resolveModelScope(modelPatterns, modelRegistry) : [];
406
+ const { options: sessionOptions, cliThinkingFromModel, diagnostics: sessionOptionDiagnostics, } = buildSessionOptions(parsed, scopedModels, sessionManager.buildSessionContext().messages.length > 0, modelRegistry, settingsManager);
407
+ diagnostics.push(...sessionOptionDiagnostics);
408
+ if (parsed.apiKey) {
409
+ if (!sessionOptions.model) {
410
+ diagnostics.push({
411
+ type: "error",
412
+ message: "--api-key requires a model to be specified via --model, --provider/--model, or --models",
413
+ });
414
+ }
415
+ else {
416
+ authStorage.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey);
417
+ }
418
+ }
419
+ const created = await createAgentSessionFromServices({
420
+ services,
421
+ sessionManager,
422
+ sessionStartEvent,
423
+ model: sessionOptions.model,
424
+ thinkingLevel: sessionOptions.thinkingLevel,
425
+ scopedModels: sessionOptions.scopedModels,
426
+ tools: sessionOptions.tools,
427
+ customTools: sessionOptions.customTools,
428
+ });
429
+ const cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel;
430
+ if (created.session.model && cliThinkingOverride) {
431
+ let effectiveThinking = created.session.thinkingLevel;
432
+ if (!created.session.model.reasoning) {
433
+ effectiveThinking = "off";
434
+ }
435
+ else if (effectiveThinking === "xhigh" && !supportsXhigh(created.session.model)) {
436
+ effectiveThinking = "high";
437
+ }
438
+ if (effectiveThinking !== created.session.thinkingLevel) {
439
+ created.session.setThinkingLevel(effectiveThinking);
440
+ }
441
+ }
442
+ return {
443
+ ...created,
444
+ services,
445
+ diagnostics,
446
+ };
447
+ };
448
+ time("createRuntime");
449
+ const runtime = await createAgentSessionRuntime(createRuntime, {
450
+ cwd: sessionManager.getCwd(),
451
+ agentDir,
452
+ sessionManager,
453
+ });
454
+ const { services, session, modelFallbackMessage } = runtime;
455
+ const { settingsManager, modelRegistry, resourceLoader } = services;
456
+ if (parsed.help) {
457
+ const extensionFlags = resourceLoader
458
+ .getExtensions()
459
+ .extensions.flatMap((extension) => Array.from(extension.flags.values()));
460
+ printHelp(extensionFlags);
461
+ process.exit(0);
462
+ }
463
+ if (parsed.listModels !== undefined) {
464
+ const searchPattern = typeof parsed.listModels === "string" ? parsed.listModels : undefined;
465
+ await listModels(modelRegistry, searchPattern);
466
+ process.exit(0);
467
+ }
468
+ // Read piped stdin content (if any) - skip for RPC mode which uses stdin for JSON-RPC
469
+ let stdinContent;
470
+ if (appMode !== "rpc") {
471
+ stdinContent = await readPipedStdin();
472
+ if (stdinContent !== undefined) {
473
+ appMode = "print";
474
+ }
475
+ }
476
+ time("readPipedStdin");
679
477
  const { initialMessage, initialImages } = await prepareInitialMessage(parsed, settingsManager.getImageAutoResize(), stdinContent);
680
478
  time("prepareInitialMessage");
681
- const isInteractive = !parsed.print && parsed.mode === undefined;
682
- const startupBenchmark = isTruthyEnvFlag(process.env.PI_STARTUP_BENCHMARK);
683
- if (startupBenchmark && !isInteractive) {
684
- console.error(chalk.red("Error: PI_STARTUP_BENCHMARK only supports interactive mode"));
685
- process.exit(1);
686
- }
687
- const mode = parsed.mode || "text";
688
- initTheme(settingsManager.getTheme(), isInteractive);
479
+ initTheme(settingsManager.getTheme(), appMode === "interactive");
689
480
  time("initTheme");
690
481
  // Show deprecation warnings in interactive mode
691
- if (isInteractive && deprecationWarnings.length > 0) {
482
+ if (appMode === "interactive" && deprecationWarnings.length > 0) {
692
483
  await showDeprecationWarnings(deprecationWarnings);
693
484
  }
694
- let scopedModels = [];
695
- const modelPatterns = parsed.models ?? settingsManager.getEnabledModels();
696
- if (modelPatterns && modelPatterns.length > 0) {
697
- scopedModels = await resolveModelScope(modelPatterns, modelRegistry);
698
- }
485
+ const scopedModels = [...session.scopedModels];
699
486
  time("resolveModelScope");
700
- // Create session manager based on CLI flags
701
- let sessionManager = await createSessionManager(parsed, cwd, extensionsResult, settingsManager);
702
- time("createSessionManager");
703
- // Handle --resume: show session picker
704
- if (parsed.resume) {
705
- // Compute effective session dir for resume (same logic as createSessionManager)
706
- const effectiveSessionDir = parsed.sessionDir ??
707
- settingsManager.getSessionDir() ??
708
- (await callSessionDirectoryHook(extensionsResult, cwd));
709
- const selectedPath = await selectSession((onProgress) => SessionManager.list(cwd, effectiveSessionDir, onProgress), SessionManager.listAll);
710
- if (!selectedPath) {
711
- console.log(chalk.dim("No session selected"));
712
- stopThemeWatcher();
713
- process.exit(0);
714
- }
715
- sessionManager = SessionManager.open(selectedPath, effectiveSessionDir);
716
- }
717
- const { options: sessionOptions, cliThinkingFromModel } = buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry, settingsManager);
718
- if (parsed.apiKey) {
719
- if (!sessionOptions.model) {
720
- console.error(chalk.red("--api-key requires a model to be specified via --model, --provider/--model, or --models"));
721
- process.exit(1);
722
- }
723
- authStorage.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey);
724
- }
725
- const runtimeBootstrap = buildRuntimeBootstrap(parsed, cwd, agentDir, authStorage, sessionOptions);
726
- const runtime = await createAgentSessionRuntime(runtimeBootstrap, {
727
- cwd: sessionManager?.getCwd() ?? cwd,
728
- sessionManager,
729
- resourceLoader,
730
- });
731
- if (process.cwd() !== runtime.cwd) {
732
- process.chdir(runtime.cwd);
487
+ reportDiagnostics(runtime.diagnostics);
488
+ if (runtime.diagnostics.some((diagnostic) => diagnostic.type === "error")) {
489
+ process.exit(1);
733
490
  }
734
- const runtimeHost = new AgentSessionRuntimeHost(runtimeBootstrap, runtime);
735
- const { session, modelFallbackMessage } = runtime;
736
491
  time("createAgentSession");
737
- if (!isInteractive && !session.model) {
492
+ if (appMode !== "interactive" && !session.model) {
738
493
  console.error(chalk.red("No models available."));
739
494
  console.error(chalk.yellow("\nSet an API key environment variable:"));
740
495
  console.error(" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc.");
741
496
  console.error(chalk.yellow(`\nOr create ${getModelsPath()}`));
742
497
  process.exit(1);
743
498
  }
744
- // Clamp thinking level to model capabilities for CLI-provided thinking levels.
745
- // This covers both --thinking <level> and --model <pattern>:<thinking>.
746
- const cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel;
747
- if (session.model && cliThinkingOverride) {
748
- let effectiveThinking = session.thinkingLevel;
749
- if (!session.model.reasoning) {
750
- effectiveThinking = "off";
751
- }
752
- else if (effectiveThinking === "xhigh" && !supportsXhigh(session.model)) {
753
- effectiveThinking = "high";
754
- }
755
- if (effectiveThinking !== session.thinkingLevel) {
756
- session.setThinkingLevel(effectiveThinking);
757
- }
499
+ const startupBenchmark = isTruthyEnvFlag(process.env.PI_STARTUP_BENCHMARK);
500
+ if (startupBenchmark && appMode !== "interactive") {
501
+ console.error(chalk.red("Error: PI_STARTUP_BENCHMARK only supports interactive mode"));
502
+ process.exit(1);
758
503
  }
759
- if (mode === "rpc") {
504
+ if (appMode === "rpc") {
760
505
  printTimings();
761
- await runRpcMode(runtimeHost);
506
+ await runRpcMode(runtime);
762
507
  }
763
- else if (isInteractive) {
508
+ else if (appMode === "interactive") {
764
509
  if (scopedModels.length > 0 && (parsed.verbose || !settingsManager.getQuietStartup())) {
765
510
  const modelList = scopedModels
766
511
  .map((sm) => {
@@ -770,7 +515,7 @@ export async function main(args) {
770
515
  .join(", ");
771
516
  console.log(chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`));
772
517
  }
773
- const interactiveMode = new InteractiveMode(runtimeHost, {
518
+ const interactiveMode = new InteractiveMode(runtime, {
774
519
  migratedProviders,
775
520
  modelFallbackMessage,
776
521
  initialMessage,
@@ -797,8 +542,8 @@ export async function main(args) {
797
542
  }
798
543
  else {
799
544
  printTimings();
800
- const exitCode = await runPrintMode(runtimeHost, {
801
- mode,
545
+ const exitCode = await runPrintMode(runtime, {
546
+ mode: toPrintOutputMode(appMode),
802
547
  messages: parsed.messages,
803
548
  initialMessage,
804
549
  initialImages,