@mariozechner/pi-coding-agent 0.64.0 → 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 (137) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +12 -6
  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 +236 -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/bash-executor.d.ts.map +1 -1
  20. package/dist/core/bash-executor.js +19 -7
  21. package/dist/core/bash-executor.js.map +1 -1
  22. package/dist/core/export-html/tool-renderer.d.ts +2 -0
  23. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  24. package/dist/core/export-html/tool-renderer.js +2 -2
  25. package/dist/core/export-html/tool-renderer.js.map +1 -1
  26. package/dist/core/extensions/index.d.ts +2 -2
  27. package/dist/core/extensions/index.d.ts.map +1 -1
  28. package/dist/core/extensions/index.js +1 -1
  29. package/dist/core/extensions/index.js.map +1 -1
  30. package/dist/core/extensions/types.d.ts +16 -28
  31. package/dist/core/extensions/types.d.ts.map +1 -1
  32. package/dist/core/extensions/types.js +10 -0
  33. package/dist/core/extensions/types.js.map +1 -1
  34. package/dist/core/footer-data-provider.d.ts +5 -1
  35. package/dist/core/footer-data-provider.d.ts.map +1 -1
  36. package/dist/core/footer-data-provider.js +70 -8
  37. package/dist/core/footer-data-provider.js.map +1 -1
  38. package/dist/core/index.d.ts +3 -1
  39. package/dist/core/index.d.ts.map +1 -1
  40. package/dist/core/index.js +3 -1
  41. package/dist/core/index.js.map +1 -1
  42. package/dist/core/keybindings.d.ts +14 -1
  43. package/dist/core/keybindings.d.ts.map +1 -1
  44. package/dist/core/keybindings.js +13 -14
  45. package/dist/core/keybindings.js.map +1 -1
  46. package/dist/core/package-manager.d.ts +20 -0
  47. package/dist/core/package-manager.d.ts.map +1 -1
  48. package/dist/core/package-manager.js +55 -9
  49. package/dist/core/package-manager.js.map +1 -1
  50. package/dist/core/resource-loader.d.ts.map +1 -1
  51. package/dist/core/resource-loader.js +25 -3
  52. package/dist/core/resource-loader.js.map +1 -1
  53. package/dist/core/sdk.d.ts +4 -1
  54. package/dist/core/sdk.d.ts.map +1 -1
  55. package/dist/core/sdk.js +4 -1
  56. package/dist/core/sdk.js.map +1 -1
  57. package/dist/core/session-cwd.d.ts +19 -0
  58. package/dist/core/session-cwd.d.ts.map +1 -0
  59. package/dist/core/session-cwd.js +38 -0
  60. package/dist/core/session-cwd.js.map +1 -0
  61. package/dist/core/session-manager.d.ts +5 -1
  62. package/dist/core/session-manager.d.ts.map +1 -1
  63. package/dist/core/session-manager.js +16 -8
  64. package/dist/core/session-manager.js.map +1 -1
  65. package/dist/core/settings-manager.d.ts +1 -1
  66. package/dist/core/settings-manager.d.ts.map +1 -1
  67. package/dist/core/settings-manager.js +2 -1
  68. package/dist/core/settings-manager.js.map +1 -1
  69. package/dist/core/tools/bash.d.ts.map +1 -1
  70. package/dist/core/tools/bash.js +19 -9
  71. package/dist/core/tools/bash.js.map +1 -1
  72. package/dist/index.d.ts +3 -3
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +3 -3
  75. package/dist/index.js.map +1 -1
  76. package/dist/main.d.ts.map +1 -1
  77. package/dist/main.js +245 -426
  78. package/dist/main.js.map +1 -1
  79. package/dist/migrations.d.ts.map +1 -1
  80. package/dist/migrations.js +20 -0
  81. package/dist/migrations.js.map +1 -1
  82. package/dist/modes/interactive/components/footer.d.ts +1 -0
  83. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  84. package/dist/modes/interactive/components/footer.js +4 -1
  85. package/dist/modes/interactive/components/footer.js.map +1 -1
  86. package/dist/modes/interactive/components/tree-selector.d.ts +4 -2
  87. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  88. package/dist/modes/interactive/components/tree-selector.js +48 -15
  89. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  90. package/dist/modes/interactive/interactive-mode.d.ts +10 -4
  91. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  92. package/dist/modes/interactive/interactive-mode.js +160 -94
  93. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  94. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  95. package/dist/modes/interactive/theme/theme.js +15 -11
  96. package/dist/modes/interactive/theme/theme.js.map +1 -1
  97. package/dist/modes/print-mode.d.ts +2 -2
  98. package/dist/modes/print-mode.d.ts.map +1 -1
  99. package/dist/modes/print-mode.js +41 -36
  100. package/dist/modes/print-mode.js.map +1 -1
  101. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  102. package/dist/modes/rpc/rpc-client.js +1 -0
  103. package/dist/modes/rpc/rpc-client.js.map +1 -1
  104. package/dist/modes/rpc/rpc-mode.d.ts +2 -2
  105. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  106. package/dist/modes/rpc/rpc-mode.js +92 -64
  107. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  108. package/dist/package-manager-cli.d.ts +4 -0
  109. package/dist/package-manager-cli.d.ts.map +1 -0
  110. package/dist/package-manager-cli.js +234 -0
  111. package/dist/package-manager-cli.js.map +1 -0
  112. package/dist/utils/paths.d.ts +7 -0
  113. package/dist/utils/paths.d.ts.map +1 -0
  114. package/dist/utils/paths.js +19 -0
  115. package/dist/utils/paths.js.map +1 -0
  116. package/docs/extensions.md +72 -40
  117. package/docs/keybindings.md +2 -0
  118. package/docs/sdk.md +227 -74
  119. package/docs/settings.md +1 -1
  120. package/docs/tree.md +6 -3
  121. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  122. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  123. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  124. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  125. package/examples/extensions/doom-overlay/doom/build.sh +2 -2
  126. package/examples/extensions/hello.ts +18 -17
  127. package/examples/extensions/hidden-thinking-label.ts +0 -4
  128. package/examples/extensions/rpc-demo.ts +3 -9
  129. package/examples/extensions/status-line.ts +0 -8
  130. package/examples/extensions/todo.ts +0 -2
  131. package/examples/extensions/tools.ts +0 -5
  132. package/examples/extensions/widget-placement.ts +4 -12
  133. package/examples/extensions/with-deps/package-lock.json +2 -2
  134. package/examples/extensions/with-deps/package.json +1 -1
  135. package/examples/sdk/13-session-runtime.ts +67 -0
  136. package/examples/sdk/README.md +4 -1
  137. package/package.json +4 -4
package/dist/main.js CHANGED
@@ -4,32 +4,35 @@
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";
9
+ import { ProcessTerminal, setKeybindings, TUI } from "@mariozechner/pi-tui";
8
10
  import chalk from "chalk";
9
11
  import { createInterface } from "readline";
10
12
  import { parseArgs, printHelp } from "./cli/args.js";
11
- import { selectConfig } from "./cli/config-selector.js";
12
13
  import { processFileArguments } from "./cli/file-processor.js";
13
14
  import { buildInitialMessage } from "./cli/initial-message.js";
14
15
  import { listModels } from "./cli/list-models.js";
15
16
  import { selectSession } from "./cli/session-picker.js";
16
- import { APP_NAME, getAgentDir, getModelsPath, VERSION } from "./config.js";
17
+ import { getAgentDir, getModelsPath, VERSION } from "./config.js";
18
+ import { createAgentSessionRuntime } from "./core/agent-session-runtime.js";
19
+ import { createAgentSessionFromServices, createAgentSessionServices, } from "./core/agent-session-services.js";
17
20
  import { AuthStorage } from "./core/auth-storage.js";
18
21
  import { exportFromFile } from "./core/export-html/index.js";
19
- import { migrateKeybindingsConfigFile } from "./core/keybindings.js";
20
- import { ModelRegistry } from "./core/model-registry.js";
22
+ import { KeybindingsManager } from "./core/keybindings.js";
21
23
  import { resolveCliModel, resolveModelScope } from "./core/model-resolver.js";
22
24
  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";
25
+ import { formatMissingSessionCwdPrompt, getMissingSessionCwdIssue, MissingSessionCwdError, } from "./core/session-cwd.js";
26
26
  import { SessionManager } from "./core/session-manager.js";
27
27
  import { SettingsManager } from "./core/settings-manager.js";
28
28
  import { printTimings, resetTimings, time } from "./core/timings.js";
29
29
  import { allTools } from "./core/tools/index.js";
30
30
  import { runMigrations, showDeprecationWarnings } from "./migrations.js";
31
31
  import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js";
32
+ import { ExtensionSelectorComponent } from "./modes/interactive/components/extension-selector.js";
32
33
  import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js";
34
+ import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.js";
35
+ import { isLocalPath } from "./utils/paths.js";
33
36
  /**
34
37
  * Read all content from piped stdin.
35
38
  * Returns undefined if stdin is a TTY (interactive terminal).
@@ -51,13 +54,17 @@ async function readPipedStdin() {
51
54
  process.stdin.resume();
52
55
  });
53
56
  }
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
- }
57
+ function collectSettingsDiagnostics(settingsManager, context) {
58
+ return settingsManager.drainErrors().map(({ scope, error }) => ({
59
+ type: "warning",
60
+ message: `(${context}, ${scope} settings) ${error.message}`,
61
+ }));
62
+ }
63
+ function reportDiagnostics(diagnostics) {
64
+ for (const diagnostic of diagnostics) {
65
+ const color = diagnostic.type === "error" ? chalk.red : diagnostic.type === "warning" ? chalk.yellow : chalk.dim;
66
+ const prefix = diagnostic.type === "error" ? "Error: " : diagnostic.type === "warning" ? "Warning: " : "";
67
+ console.error(color(`${prefix}${diagnostic.message}`));
61
68
  }
62
69
  }
63
70
  function isTruthyEnvFlag(value) {
@@ -65,212 +72,20 @@ function isTruthyEnvFlag(value) {
65
72
  return false;
66
73
  return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes";
67
74
  }
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";
75
+ function resolveAppMode(parsed, stdinIsTTY) {
76
+ if (parsed.mode === "rpc") {
77
+ return "rpc";
137
78
  }
138
- else if (rawCommand === "install" || rawCommand === "remove" || rawCommand === "update" || rawCommand === "list") {
139
- command = rawCommand;
79
+ if (parsed.mode === "json") {
80
+ return "json";
140
81
  }
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
- }
82
+ if (parsed.print || !stdinIsTTY) {
83
+ return "print";
169
84
  }
170
- return { command, source, local, help, invalidOption };
85
+ return "interactive";
171
86
  }
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
- }
87
+ function toPrintOutputMode(appMode) {
88
+ return appMode === "json" ? "json" : "text";
274
89
  }
275
90
  async function prepareInitialMessage(parsed, autoResizeImages, stdinContent) {
276
91
  if (parsed.fileArgs.length === 0) {
@@ -322,29 +137,6 @@ async function promptConfirm(message) {
322
137
  });
323
138
  });
324
139
  }
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
140
  function validateForkFlags(parsed) {
349
141
  if (!parsed.fork)
350
142
  return;
@@ -369,62 +161,65 @@ function forkSessionOrExit(sourcePath, cwd, sessionDir) {
369
161
  process.exit(1);
370
162
  }
371
163
  }
372
- async function createSessionManager(parsed, cwd, extensions, settingsManager) {
164
+ async function createSessionManager(parsed, cwd, sessionDir, settingsManager) {
373
165
  if (parsed.noSession) {
374
166
  return SessionManager.inMemory();
375
167
  }
376
- // Priority: CLI flag > settings.json > extension hook
377
- const effectiveSessionDir = parsed.sessionDir ?? settingsManager.getSessionDir() ?? (await callSessionDirectoryHook(extensions, cwd));
378
168
  if (parsed.fork) {
379
- const resolved = await resolveSessionPath(parsed.fork, cwd, effectiveSessionDir);
169
+ const resolved = await resolveSessionPath(parsed.fork, cwd, sessionDir);
380
170
  switch (resolved.type) {
381
171
  case "path":
382
172
  case "local":
383
173
  case "global":
384
- return forkSessionOrExit(resolved.path, cwd, effectiveSessionDir);
174
+ return forkSessionOrExit(resolved.path, cwd, sessionDir);
385
175
  case "not_found":
386
176
  console.error(chalk.red(`No session found matching '${resolved.arg}'`));
387
177
  process.exit(1);
388
178
  }
389
179
  }
390
180
  if (parsed.session) {
391
- const resolved = await resolveSessionPath(parsed.session, cwd, effectiveSessionDir);
181
+ const resolved = await resolveSessionPath(parsed.session, cwd, sessionDir);
392
182
  switch (resolved.type) {
393
183
  case "path":
394
184
  case "local":
395
- return SessionManager.open(resolved.path, effectiveSessionDir);
185
+ return SessionManager.open(resolved.path, sessionDir);
396
186
  case "global": {
397
- // Session found in different project - ask user if they want to fork
398
187
  console.log(chalk.yellow(`Session found in different project: ${resolved.cwd}`));
399
188
  const shouldFork = await promptConfirm("Fork this session into current directory?");
400
189
  if (!shouldFork) {
401
190
  console.log(chalk.dim("Aborted."));
402
191
  process.exit(0);
403
192
  }
404
- return forkSessionOrExit(resolved.path, cwd, effectiveSessionDir);
193
+ return forkSessionOrExit(resolved.path, cwd, sessionDir);
405
194
  }
406
195
  case "not_found":
407
196
  console.error(chalk.red(`No session found matching '${resolved.arg}'`));
408
197
  process.exit(1);
409
198
  }
410
199
  }
411
- if (parsed.continue) {
412
- return SessionManager.continueRecent(cwd, effectiveSessionDir);
200
+ if (parsed.resume) {
201
+ initTheme(settingsManager.getTheme(), true);
202
+ try {
203
+ const selectedPath = await selectSession((onProgress) => SessionManager.list(cwd, sessionDir, onProgress), SessionManager.listAll);
204
+ if (!selectedPath) {
205
+ console.log(chalk.dim("No session selected"));
206
+ process.exit(0);
207
+ }
208
+ return SessionManager.open(selectedPath, sessionDir);
209
+ }
210
+ finally {
211
+ stopThemeWatcher();
212
+ }
413
213
  }
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);
214
+ if (parsed.continue) {
215
+ return SessionManager.continueRecent(cwd, sessionDir);
418
216
  }
419
- // Default case (new session) returns undefined, SDK will create one
420
- return undefined;
217
+ return SessionManager.create(cwd, sessionDir);
421
218
  }
422
- function buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry, settingsManager) {
219
+ function buildSessionOptions(parsed, scopedModels, hasExistingSession, modelRegistry, settingsManager) {
423
220
  const options = {};
221
+ const diagnostics = [];
424
222
  let cliThinkingFromModel = false;
425
- if (sessionManager) {
426
- options.sessionManager = sessionManager;
427
- }
428
223
  // Model from CLI
429
224
  // - supports --provider <name> --model <pattern>
430
225
  // - supports --model <provider>/<pattern>
@@ -435,11 +230,10 @@ function buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry
435
230
  modelRegistry,
436
231
  });
437
232
  if (resolved.warning) {
438
- console.warn(chalk.yellow(`Warning: ${resolved.warning}`));
233
+ diagnostics.push({ type: "warning", message: resolved.warning });
439
234
  }
440
235
  if (resolved.error) {
441
- console.error(chalk.red(resolved.error));
442
- process.exit(1);
236
+ diagnostics.push({ type: "error", message: resolved.error });
443
237
  }
444
238
  if (resolved.model) {
445
239
  options.model = resolved.model;
@@ -451,7 +245,7 @@ function buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry
451
245
  }
452
246
  }
453
247
  }
454
- if (!options.model && scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
248
+ if (!options.model && scopedModels.length > 0 && !hasExistingSession) {
455
249
  // Check if saved default is in scoped models - use it if so, otherwise first scoped model
456
250
  const savedProvider = settingsManager.getDefaultProvider();
457
251
  const savedModelId = settingsManager.getDefaultModel();
@@ -501,25 +295,31 @@ function buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry
501
295
  else if (parsed.tools) {
502
296
  options.tools = parsed.tools.map((name) => allTools[name]);
503
297
  }
504
- return { options, cliThinkingFromModel };
298
+ return { options, cliThinkingFromModel, diagnostics };
505
299
  }
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,
300
+ function resolveCliPaths(cwd, paths) {
301
+ return paths?.map((value) => (isLocalPath(value) ? resolve(cwd, value) : value));
302
+ }
303
+ async function promptForMissingSessionCwd(issue, settingsManager) {
304
+ initTheme(settingsManager.getTheme());
305
+ setKeybindings(KeybindingsManager.create());
306
+ return new Promise((resolve) => {
307
+ const ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor());
308
+ ui.setClearOnShrink(settingsManager.getClearOnShrink());
309
+ let settled = false;
310
+ const finish = (result) => {
311
+ if (settled) {
312
+ return;
313
+ }
314
+ settled = true;
315
+ ui.stop();
316
+ resolve(result);
317
+ };
318
+ const selector = new ExtensionSelectorComponent(formatMissingSessionCwdPrompt(issue), ["Continue", "Cancel"], (option) => finish(option === "Continue" ? issue.fallbackCwd : undefined), () => finish(undefined), { tui: ui });
319
+ ui.addChild(selector);
320
+ ui.setFocus(selector);
321
+ ui.start();
521
322
  });
522
- process.exit(0);
523
323
  }
524
324
  export async function main(args) {
525
325
  resetTimings();
@@ -534,93 +334,26 @@ export async function main(args) {
534
334
  if (await handleConfigCommand(args)) {
535
335
  return;
536
336
  }
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);
337
+ const parsed = parseArgs(args);
338
+ if (parsed.diagnostics.length > 0) {
339
+ for (const d of parsed.diagnostics) {
340
+ const color = d.type === "error" ? chalk.red : chalk.yellow;
341
+ console.error(color(`${d.type === "error" ? "Error" : "Warning"}: ${d.message}`));
581
342
  }
582
- catch (error) {
583
- const message = error instanceof Error ? error.message : String(error);
584
- console.error(chalk.red(`Extension "${extensionPath}" error: ${message}`));
585
- }
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 });
343
+ if (parsed.diagnostics.some((d) => d.type === "error")) {
344
+ process.exit(1);
592
345
  }
593
346
  }
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);
347
+ time("parseArgs");
348
+ let appMode = resolveAppMode(parsed, process.stdin.isTTY);
349
+ const shouldTakeOverStdout = appMode !== "interactive";
350
+ if (shouldTakeOverStdout) {
351
+ takeOverStdout();
600
352
  }
601
353
  if (parsed.version) {
602
354
  console.log(VERSION);
603
355
  process.exit(0);
604
356
  }
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
357
  if (parsed.export) {
625
358
  let result;
626
359
  try {
@@ -635,92 +368,178 @@ export async function main(args) {
635
368
  console.log(`Exported to: ${result}`);
636
369
  process.exit(0);
637
370
  }
638
- migrateKeybindingsConfigFile(agentDir);
639
- time("migrateKeybindingsConfigFile");
640
371
  if (parsed.mode === "rpc" && parsed.fileArgs.length > 0) {
641
372
  console.error(chalk.red("Error: @file arguments are not supported in RPC mode"));
642
373
  process.exit(1);
643
374
  }
644
375
  validateForkFlags(parsed);
376
+ // Run migrations (pass cwd for project-local migrations)
377
+ const { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(process.cwd());
378
+ time("runMigrations");
379
+ const cwd = process.cwd();
380
+ const agentDir = getAgentDir();
381
+ const startupSettingsManager = SettingsManager.create(cwd, agentDir);
382
+ reportDiagnostics(collectSettingsDiagnostics(startupSettingsManager, "startup session lookup"));
383
+ // Decide the final runtime cwd before creating cwd-bound runtime services.
384
+ // --session and --resume may select a session from another project, so project-local
385
+ // settings, resources, provider registrations, and models must be resolved only after
386
+ // the target session cwd is known. The startup-cwd settings manager is used only for
387
+ // sessionDir lookup during session selection.
388
+ const sessionDir = parsed.sessionDir ?? startupSettingsManager.getSessionDir();
389
+ let sessionManager = await createSessionManager(parsed, cwd, sessionDir, startupSettingsManager);
390
+ const missingSessionCwdIssue = getMissingSessionCwdIssue(sessionManager, cwd);
391
+ if (missingSessionCwdIssue) {
392
+ if (appMode === "interactive") {
393
+ const selectedCwd = await promptForMissingSessionCwd(missingSessionCwdIssue, startupSettingsManager);
394
+ if (!selectedCwd) {
395
+ process.exit(0);
396
+ }
397
+ sessionManager = SessionManager.open(missingSessionCwdIssue.sessionFile, sessionDir, selectedCwd);
398
+ }
399
+ else {
400
+ console.error(chalk.red(new MissingSessionCwdError(missingSessionCwdIssue).message));
401
+ process.exit(1);
402
+ }
403
+ }
404
+ time("createSessionManager");
405
+ const resolvedExtensionPaths = resolveCliPaths(cwd, parsed.extensions);
406
+ const resolvedSkillPaths = resolveCliPaths(cwd, parsed.skills);
407
+ const resolvedPromptTemplatePaths = resolveCliPaths(cwd, parsed.promptTemplates);
408
+ const resolvedThemePaths = resolveCliPaths(cwd, parsed.themes);
409
+ const authStorage = AuthStorage.create();
410
+ const createRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent, }) => {
411
+ const services = await createAgentSessionServices({
412
+ cwd,
413
+ agentDir,
414
+ authStorage,
415
+ extensionFlagValues: parsed.unknownFlags,
416
+ resourceLoaderOptions: {
417
+ additionalExtensionPaths: resolvedExtensionPaths,
418
+ additionalSkillPaths: resolvedSkillPaths,
419
+ additionalPromptTemplatePaths: resolvedPromptTemplatePaths,
420
+ additionalThemePaths: resolvedThemePaths,
421
+ noExtensions: parsed.noExtensions,
422
+ noSkills: parsed.noSkills,
423
+ noPromptTemplates: parsed.noPromptTemplates,
424
+ noThemes: parsed.noThemes,
425
+ systemPrompt: parsed.systemPrompt,
426
+ appendSystemPrompt: parsed.appendSystemPrompt,
427
+ },
428
+ });
429
+ const { settingsManager, modelRegistry, resourceLoader } = services;
430
+ const diagnostics = [
431
+ ...services.diagnostics,
432
+ ...collectSettingsDiagnostics(settingsManager, "runtime creation"),
433
+ ...resourceLoader.getExtensions().errors.map(({ path, error }) => ({
434
+ type: "error",
435
+ message: `Failed to load extension "${path}": ${error}`,
436
+ })),
437
+ ];
438
+ const modelPatterns = parsed.models ?? settingsManager.getEnabledModels();
439
+ const scopedModels = modelPatterns && modelPatterns.length > 0 ? await resolveModelScope(modelPatterns, modelRegistry) : [];
440
+ const { options: sessionOptions, cliThinkingFromModel, diagnostics: sessionOptionDiagnostics, } = buildSessionOptions(parsed, scopedModels, sessionManager.buildSessionContext().messages.length > 0, modelRegistry, settingsManager);
441
+ diagnostics.push(...sessionOptionDiagnostics);
442
+ if (parsed.apiKey) {
443
+ if (!sessionOptions.model) {
444
+ diagnostics.push({
445
+ type: "error",
446
+ message: "--api-key requires a model to be specified via --model, --provider/--model, or --models",
447
+ });
448
+ }
449
+ else {
450
+ authStorage.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey);
451
+ }
452
+ }
453
+ const created = await createAgentSessionFromServices({
454
+ services,
455
+ sessionManager,
456
+ sessionStartEvent,
457
+ model: sessionOptions.model,
458
+ thinkingLevel: sessionOptions.thinkingLevel,
459
+ scopedModels: sessionOptions.scopedModels,
460
+ tools: sessionOptions.tools,
461
+ customTools: sessionOptions.customTools,
462
+ });
463
+ const cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel;
464
+ if (created.session.model && cliThinkingOverride) {
465
+ let effectiveThinking = created.session.thinkingLevel;
466
+ if (!created.session.model.reasoning) {
467
+ effectiveThinking = "off";
468
+ }
469
+ else if (effectiveThinking === "xhigh" && !supportsXhigh(created.session.model)) {
470
+ effectiveThinking = "high";
471
+ }
472
+ if (effectiveThinking !== created.session.thinkingLevel) {
473
+ created.session.setThinkingLevel(effectiveThinking);
474
+ }
475
+ }
476
+ return {
477
+ ...created,
478
+ services,
479
+ diagnostics,
480
+ };
481
+ };
482
+ time("createRuntime");
483
+ const runtime = await createAgentSessionRuntime(createRuntime, {
484
+ cwd: sessionManager.getCwd(),
485
+ agentDir,
486
+ sessionManager,
487
+ });
488
+ const { services, session, modelFallbackMessage } = runtime;
489
+ const { settingsManager, modelRegistry, resourceLoader } = services;
490
+ if (parsed.help) {
491
+ const extensionFlags = resourceLoader
492
+ .getExtensions()
493
+ .extensions.flatMap((extension) => Array.from(extension.flags.values()));
494
+ printHelp(extensionFlags);
495
+ process.exit(0);
496
+ }
497
+ if (parsed.listModels !== undefined) {
498
+ const searchPattern = typeof parsed.listModels === "string" ? parsed.listModels : undefined;
499
+ await listModels(modelRegistry, searchPattern);
500
+ process.exit(0);
501
+ }
502
+ // Read piped stdin content (if any) - skip for RPC mode which uses stdin for JSON-RPC
503
+ let stdinContent;
504
+ if (appMode !== "rpc") {
505
+ stdinContent = await readPipedStdin();
506
+ if (stdinContent !== undefined && appMode === "interactive") {
507
+ appMode = "print";
508
+ }
509
+ }
510
+ time("readPipedStdin");
645
511
  const { initialMessage, initialImages } = await prepareInitialMessage(parsed, settingsManager.getImageAutoResize(), stdinContent);
646
512
  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);
513
+ initTheme(settingsManager.getTheme(), appMode === "interactive");
655
514
  time("initTheme");
656
515
  // Show deprecation warnings in interactive mode
657
- if (isInteractive && deprecationWarnings.length > 0) {
516
+ if (appMode === "interactive" && deprecationWarnings.length > 0) {
658
517
  await showDeprecationWarnings(deprecationWarnings);
659
518
  }
660
- let scopedModels = [];
661
- const modelPatterns = parsed.models ?? settingsManager.getEnabledModels();
662
- if (modelPatterns && modelPatterns.length > 0) {
663
- scopedModels = await resolveModelScope(modelPatterns, modelRegistry);
664
- }
519
+ const scopedModels = [...session.scopedModels];
665
520
  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);
521
+ reportDiagnostics(runtime.diagnostics);
522
+ if (runtime.diagnostics.some((diagnostic) => diagnostic.type === "error")) {
523
+ process.exit(1);
694
524
  }
695
- const { session, modelFallbackMessage } = await createAgentSession(sessionOptions);
696
525
  time("createAgentSession");
697
- if (!isInteractive && !session.model) {
526
+ if (appMode !== "interactive" && !session.model) {
698
527
  console.error(chalk.red("No models available."));
699
528
  console.error(chalk.yellow("\nSet an API key environment variable:"));
700
529
  console.error(" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc.");
701
530
  console.error(chalk.yellow(`\nOr create ${getModelsPath()}`));
702
531
  process.exit(1);
703
532
  }
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
- }
533
+ const startupBenchmark = isTruthyEnvFlag(process.env.PI_STARTUP_BENCHMARK);
534
+ if (startupBenchmark && appMode !== "interactive") {
535
+ console.error(chalk.red("Error: PI_STARTUP_BENCHMARK only supports interactive mode"));
536
+ process.exit(1);
718
537
  }
719
- if (mode === "rpc") {
538
+ if (appMode === "rpc") {
720
539
  printTimings();
721
- await runRpcMode(session);
540
+ await runRpcMode(runtime);
722
541
  }
723
- else if (isInteractive) {
542
+ else if (appMode === "interactive") {
724
543
  if (scopedModels.length > 0 && (parsed.verbose || !settingsManager.getQuietStartup())) {
725
544
  const modelList = scopedModels
726
545
  .map((sm) => {
@@ -730,7 +549,7 @@ export async function main(args) {
730
549
  .join(", ");
731
550
  console.log(chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`));
732
551
  }
733
- const interactiveMode = new InteractiveMode(session, {
552
+ const interactiveMode = new InteractiveMode(runtime, {
734
553
  migratedProviders,
735
554
  modelFallbackMessage,
736
555
  initialMessage,
@@ -757,8 +576,8 @@ export async function main(args) {
757
576
  }
758
577
  else {
759
578
  printTimings();
760
- const exitCode = await runPrintMode(session, {
761
- mode,
579
+ const exitCode = await runPrintMode(runtime, {
580
+ mode: toPrintOutputMode(appMode),
762
581
  messages: parsed.messages,
763
582
  initialMessage,
764
583
  initialImages,