@mariozechner/pi-coding-agent 0.64.0 → 0.65.0

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