@eminent337/aery 0.1.116 → 0.1.119

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 (65) hide show
  1. package/CHANGELOG.md +9 -9
  2. package/README.md +1 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +20 -13
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +2 -2
  8. package/dist/config.js.map +1 -1
  9. package/dist/core/auth-storage.d.ts +1 -2
  10. package/dist/core/auth-storage.d.ts.map +1 -1
  11. package/dist/core/auth-storage.js +2 -2
  12. package/dist/core/auth-storage.js.map +1 -1
  13. package/dist/core/extensions/loader.d.ts +0 -1
  14. package/dist/core/extensions/loader.d.ts.map +1 -1
  15. package/dist/core/extensions/loader.js +32 -17
  16. package/dist/core/extensions/loader.js.map +1 -1
  17. package/dist/core/extensions/types.d.ts +14 -8
  18. package/dist/core/extensions/types.d.ts.map +1 -1
  19. package/dist/core/extensions/types.js.map +1 -1
  20. package/dist/core/model-registry.d.ts +4 -6
  21. package/dist/core/model-registry.d.ts.map +1 -1
  22. package/dist/core/model-registry.js +17 -78
  23. package/dist/core/model-registry.js.map +1 -1
  24. package/dist/core/model-resolver.d.ts.map +1 -1
  25. package/dist/core/model-resolver.js +0 -2
  26. package/dist/core/model-resolver.js.map +1 -1
  27. package/dist/core/package-manager.d.ts +0 -1
  28. package/dist/core/package-manager.d.ts.map +1 -1
  29. package/dist/core/package-manager.js +26 -15
  30. package/dist/core/package-manager.js.map +1 -1
  31. package/dist/core/provider-display-names.d.ts.map +1 -1
  32. package/dist/core/provider-display-names.js +0 -2
  33. package/dist/core/provider-display-names.js.map +1 -1
  34. package/dist/core/sdk.d.ts.map +1 -1
  35. package/dist/core/sdk.js.map +1 -1
  36. package/dist/core/slash-commands.d.ts.map +1 -1
  37. package/dist/core/slash-commands.js +0 -2
  38. package/dist/core/slash-commands.js.map +1 -1
  39. package/dist/main.d.ts.map +1 -1
  40. package/dist/main.js +8 -31
  41. package/dist/main.js.map +1 -1
  42. package/dist/migrations.d.ts +4 -4
  43. package/dist/migrations.d.ts.map +1 -1
  44. package/dist/migrations.js +25 -29
  45. package/dist/migrations.js.map +1 -1
  46. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  47. package/dist/modes/interactive/components/login-dialog.js +0 -2
  48. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  49. package/dist/modes/interactive/interactive-mode.d.ts +16 -12
  50. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  51. package/dist/modes/interactive/interactive-mode.js +143 -304
  52. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  53. package/dist/package-manager-cli.d.ts +0 -1
  54. package/dist/package-manager-cli.d.ts.map +1 -1
  55. package/dist/package-manager-cli.js +57 -111
  56. package/dist/package-manager-cli.js.map +1 -1
  57. package/docs/development.md +1 -1
  58. package/docs/packages.md +1 -1
  59. package/docs/rpc.md +4 -4
  60. package/docs/usage.md +1 -1
  61. package/examples/extensions/preset.ts +1 -1
  62. package/examples/extensions/provider-payload.ts +1 -1
  63. package/examples/extensions/sandbox/index.ts +1 -1
  64. package/examples/extensions/subagent/agents.ts +1 -1
  65. package/package.json +94 -96
@@ -6,30 +6,28 @@ import * as crypto from "node:crypto";
6
6
  import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
- import { getProviders } from "@eminent337/aery-ai";
10
- import { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@eminent337/aery-tui";
9
+ import { getProviders, } from "@eminent337/aery-ai";
10
+ import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@eminent337/aery-tui";
11
11
  import { spawn, spawnSync } from "child_process";
12
- import { collectCapabilitiesReport, formatCapabilitiesReport } from "../../cli/capabilities.js";
13
- import { formatCurrentCoreExtensionsReport } from "../../cli/doctor.js";
14
- import { APP_NAME, getAgentDir, getAuthPath, getDebugLogPath, getModelsPath, getShareViewerUrl, getUpdateInstruction, VERSION, } from "../../config.js";
12
+ import { APP_NAME, APP_TITLE, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, VERSION, } from "../../config.js";
15
13
  import { parseSkillBlock } from "../../core/agent-session.js";
16
14
  import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
17
- import { CUSTOM_OPENAI_COMPATIBLE_PROVIDER_ID, saveCustomOpenAICompatibleProvider, } from "../../core/custom-openai-compatible.js";
18
15
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
19
16
  import { KeybindingsManager } from "../../core/keybindings.js";
20
17
  import { createCompactionSummaryMessage } from "../../core/messages.js";
21
18
  import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
22
19
  import { DefaultPackageManager } from "../../core/package-manager.js";
23
- import { checkProviderSetup } from "../../core/provider-setup-check.js";
20
+ import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
24
21
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
25
22
  import { SessionManager } from "../../core/session-manager.js";
26
23
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
27
24
  import { isInstallTelemetryEnabled } from "../../core/telemetry.js";
28
- import { wireCoreExtensions } from "../../migrations.js";
29
25
  import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
30
26
  import { copyToClipboard } from "../../utils/clipboard.js";
31
27
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
32
28
  import { parseGitUrl } from "../../utils/git.js";
29
+ import { getCwdRelativePath } from "../../utils/paths.js";
30
+ import { getPiUserAgent } from "../../utils/pi-user-agent.js";
33
31
  import { killTrackedDetachedChildren } from "../../utils/shell.js";
34
32
  import { ensureTool } from "../../utils/tools-manager.js";
35
33
  import { checkForNewPiVersion } from "../../utils/version-check.js";
@@ -49,7 +47,7 @@ import { ExtensionEditorComponent } from "./components/extension-editor.js";
49
47
  import { ExtensionInputComponent } from "./components/extension-input.js";
50
48
  import { ExtensionSelectorComponent } from "./components/extension-selector.js";
51
49
  import { FooterComponent } from "./components/footer.js";
52
- import { keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
50
+ import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
53
51
  import { LoginDialogComponent } from "./components/login-dialog.js";
54
52
  import { ModelSelectorComponent } from "./components/model-selector.js";
55
53
  import { OAuthSelectorComponent } from "./components/oauth-selector.js";
@@ -95,47 +93,17 @@ function isUnknownModel(model) {
95
93
  function hasDefaultModelProvider(providerId) {
96
94
  return providerId in defaultModelPerProvider;
97
95
  }
98
- const API_KEY_PROVIDER_NAMES = {
99
- anthropic: "Anthropic",
100
- "azure-openai-responses": "Azure OpenAI Responses",
101
- cerebras: "Cerebras",
102
- "cloudflare-workers-ai": "Cloudflare Workers AI",
103
- fireworks: "Fireworks",
104
- google: "Google Gemini",
105
- "google-vertex": "Google Vertex AI",
106
- groq: "Groq",
107
- huggingface: "Hugging Face",
108
- "kimi-coding": "Kimi For Coding",
109
- mistral: "Mistral",
110
- minimax: "MiniMax",
111
- "minimax-cn": "MiniMax (China)",
112
- opencode: "OpenCode Zen",
113
- "opencode-go": "OpenCode Go",
114
- openai: "OpenAI",
115
- openrouter: "OpenRouter",
116
- "vercel-ai-gateway": "Vercel AI Gateway",
117
- xai: "xAI",
118
- zai: "ZAI",
119
- };
120
- const API_KEY_LOGIN_PROVIDER_BLOCKLIST = new Set(["amazon-bedrock", "llama.cpp", "lmstudio", "ollama"]);
121
- const CUSTOM_OPENAI_COMPATIBLE_PROVIDER_LABEL = "Custom OpenAI-compatible";
122
- const AERY_GATEWAY_PROVIDER_ID = "__aery-gateway__";
123
- const AERY_GATEWAY_BASE_URL = "https://aery-gateway.eminent337.workers.dev/v1";
124
- const AERY_GATEWAY_PROVIDER_APIS = {
125
- anthropic: "anthropic-messages",
126
- "google-generative-ai": "google-generative-ai",
127
- "openai-responses": "openai-responses",
128
- };
129
- export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProviderIds = new Set(getProviders())) {
130
- if (API_KEY_PROVIDER_NAMES[providerId])
96
+ const BEDROCK_PROVIDER_ID = "amazon-bedrock";
97
+ const BUILT_IN_MODEL_PROVIDERS = new Set(getProviders());
98
+ export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProviderIds = BUILT_IN_MODEL_PROVIDERS) {
99
+ if (BUILT_IN_PROVIDER_DISPLAY_NAMES[providerId]) {
131
100
  return true;
132
- if (builtInProviderIds.has(providerId))
101
+ }
102
+ if (builtInProviderIds.has(providerId)) {
133
103
  return false;
104
+ }
134
105
  return !oauthProviderIds.has(providerId);
135
106
  }
136
- export function getApiKeyProviderDisplayName(providerId) {
137
- return API_KEY_PROVIDER_NAMES[providerId] ?? providerId;
138
- }
139
107
  export class InteractiveMode {
140
108
  options;
141
109
  runtimeHost;
@@ -145,7 +113,7 @@ export class InteractiveMode {
145
113
  statusContainer;
146
114
  defaultEditor;
147
115
  editor;
148
- customEditorFactory;
116
+ editorComponentFactory;
149
117
  autocompleteProvider;
150
118
  autocompleteProviderWrappers = [];
151
119
  fdPath;
@@ -457,7 +425,7 @@ export class InteractiveMode {
457
425
  hint("app.tools.expand", "more"),
458
426
  ].join(theme.fg("muted", " · "));
459
427
  const compactOnboarding = theme.fg("dim", `Press ${keyText("app.tools.expand")} to show full startup help and loaded resources.`);
460
- const onboarding = theme.fg("dim", `Aery can explain its own features and look up its docs. Ask it how to use or extend Aery.`);
428
+ const onboarding = theme.fg("dim", `Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.`);
461
429
  this.builtInHeader = new ExpandableText(() => `${logo}\n${compactInstructions}\n${compactOnboarding}\n\n${onboarding}`, () => `${logo}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
462
430
  // Setup UI layout
463
431
  this.headerContainer.addChild(new Spacer(1));
@@ -507,10 +475,10 @@ export class InteractiveMode {
507
475
  const cwdBasename = path.basename(this.sessionManager.getCwd());
508
476
  const sessionName = this.sessionManager.getSessionName();
509
477
  if (sessionName) {
510
- this.ui.terminal.setTitle(`⌬ aery - ${sessionName} - ${cwdBasename}`);
478
+ this.ui.terminal.setTitle(`${APP_TITLE} - ${sessionName} - ${cwdBasename}`);
511
479
  }
512
480
  else {
513
- this.ui.terminal.setTitle(`⌬ aery - ${cwdBasename}`);
481
+ this.ui.terminal.setTitle(`${APP_TITLE} - ${cwdBasename}`);
514
482
  }
515
483
  }
516
484
  /**
@@ -637,7 +605,7 @@ export class InteractiveMode {
637
605
  return "tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys on` to ~/.tmux.conf and restart tmux.";
638
606
  }
639
607
  if (extendedKeysFormat === "xterm") {
640
- return "tmux extended-keys-format is xterm. Aery works best with csi-u. Add `set -g extended-keys-format csi-u` to ~/.tmux.conf and restart tmux.";
608
+ return "tmux extended-keys-format is xterm. Pi works best with csi-u. Add `set -g extended-keys-format csi-u` to ~/.tmux.conf and restart tmux.";
641
609
  }
642
610
  return undefined;
643
611
  }
@@ -657,8 +625,6 @@ export class InteractiveMode {
657
625
  // Fresh install - record the version, send telemetry, don't show changelog
658
626
  this.settingsManager.setLastChangelogVersion(VERSION);
659
627
  this.reportInstallTelemetry(VERSION);
660
- // Auto-install core extension pack on fresh install
661
- this.installCorePackIfNeeded();
662
628
  return undefined;
663
629
  }
664
630
  const newEntries = getNewEntries(entries, lastVersion);
@@ -676,37 +642,15 @@ export class InteractiveMode {
676
642
  if (!isInstallTelemetryEnabled(this.settingsManager)) {
677
643
  return;
678
644
  }
679
- void fetch(`https://aery.dev/install?version=${encodeURIComponent(version)}`, {
645
+ void fetch(`https://eminent337.github.io/api/report-install?version=${encodeURIComponent(version)}`, {
646
+ headers: {
647
+ "User-Agent": getPiUserAgent(version),
648
+ },
680
649
  signal: AbortSignal.timeout(5000),
681
650
  })
682
651
  .then(() => undefined)
683
652
  .catch(() => undefined);
684
653
  }
685
- installCorePackIfNeeded() {
686
- // Fire-and-forget: install core extension pack on fresh install
687
- void (async () => {
688
- try {
689
- const { execFile } = await import("node:child_process");
690
- const { promisify } = await import("node:util");
691
- const { existsSync, readFileSync, writeFileSync } = await import("node:fs");
692
- const { join } = await import("node:path");
693
- const { homedir } = await import("node:os");
694
- const exec = promisify(execFile);
695
- await exec("aery", ["install", "https://github.com/eminent337/aery-extensions"], {
696
- timeout: 30000,
697
- });
698
- // Wire core extensions into settings.json
699
- const repoPath = join(homedir(), ".aery", "agent", "git", "github.com", "eminent337", "aery-extensions");
700
- const settingsPath = join(homedir(), ".aery", "agent", "settings.json");
701
- wireCoreExtensions(repoPath, settingsPath);
702
- const settings = existsSync(settingsPath) ? JSON.parse(readFileSync(settingsPath, "utf-8")) : {};
703
- writeFileSync(settingsPath, `${JSON.stringify({ ...settings, quietStartup: true }, null, 2)}\n`);
704
- }
705
- catch {
706
- // Silent fail — user can install manually
707
- }
708
- })();
709
- }
710
654
  getMarkdownThemeWithSettings() {
711
655
  return {
712
656
  ...getMarkdownTheme(),
@@ -733,13 +677,9 @@ export class InteractiveMode {
733
677
  formatContextPath(p) {
734
678
  const cwd = path.resolve(this.sessionManager.getCwd());
735
679
  const absolutePath = path.isAbsolute(p) ? path.resolve(p) : path.resolve(cwd, p);
736
- const relativePath = path.relative(cwd, absolutePath);
737
- const isInsideCwd = relativePath === "" ||
738
- (!relativePath.startsWith("..") &&
739
- !relativePath.startsWith(`..${path.sep}`) &&
740
- !path.isAbsolute(relativePath));
741
- if (isInsideCwd) {
742
- return relativePath || ".";
680
+ const relativePath = getCwdRelativePath(absolutePath, cwd);
681
+ if (relativePath !== undefined) {
682
+ return relativePath;
743
683
  }
744
684
  return this.formatDisplayPath(absolutePath);
745
685
  }
@@ -1601,7 +1541,7 @@ export class InteractiveMode {
1601
1541
  this.setupAutocompleteProvider();
1602
1542
  },
1603
1543
  setEditorComponent: (factory) => this.setCustomEditorComponent(factory),
1604
- getEditorComponent: () => this.customEditorFactory,
1544
+ getEditorComponent: () => this.editorComponentFactory,
1605
1545
  get theme() {
1606
1546
  return theme;
1607
1547
  },
@@ -1648,7 +1588,7 @@ export class InteractiveMode {
1648
1588
  opts?.signal?.removeEventListener("abort", onAbort);
1649
1589
  this.hideExtensionSelector();
1650
1590
  resolve(undefined);
1651
- }, { tui: this.ui, timeout: opts?.timeout });
1591
+ }, { tui: this.ui, timeout: opts?.timeout, onToggleToolsExpanded: () => this.toggleToolOutputExpansion() });
1652
1592
  this.editorContainer.clear();
1653
1593
  this.editorContainer.addChild(this.extensionSelector);
1654
1594
  this.ui.setFocus(this.extensionSelector);
@@ -1750,7 +1690,7 @@ export class InteractiveMode {
1750
1690
  * Pass undefined to restore the default editor.
1751
1691
  */
1752
1692
  setCustomEditorComponent(factory) {
1753
- this.customEditorFactory = factory;
1693
+ this.editorComponentFactory = factory;
1754
1694
  // Save text from current editor before switching
1755
1695
  const currentText = this.editor.getText();
1756
1696
  this.editorContainer.clear();
@@ -1986,7 +1926,7 @@ export class InteractiveMode {
1986
1926
  // Write to temp file
1987
1927
  const tmpDir = os.tmpdir();
1988
1928
  const ext = extensionForImageMimeType(image.mimeType) ?? "png";
1989
- const fileName = `aery-clipboard-${crypto.randomUUID()}.${ext}`;
1929
+ const fileName = `pi-clipboard-${crypto.randomUUID()}.${ext}`;
1990
1930
  const filePath = path.join(tmpDir, fileName);
1991
1931
  fs.writeFileSync(filePath, Buffer.from(image.bytes));
1992
1932
  // Insert file path directly
@@ -2049,11 +1989,6 @@ export class InteractiveMode {
2049
1989
  this.editor.setText("");
2050
1990
  return;
2051
1991
  }
2052
- if (text === "/capabilities") {
2053
- this.handleCapabilitiesCommand();
2054
- this.editor.setText("");
2055
- return;
2056
- }
2057
1992
  if (text === "/changelog") {
2058
1993
  this.handleChangelogCommand();
2059
1994
  this.editor.setText("");
@@ -2079,11 +2014,6 @@ export class InteractiveMode {
2079
2014
  this.editor.setText("");
2080
2015
  return;
2081
2016
  }
2082
- if (text === "/extensions doctor" || text === "/extensions") {
2083
- this.handleExtensionsDoctorCommand();
2084
- this.editor.setText("");
2085
- return;
2086
- }
2087
2017
  if (text === "/login") {
2088
2018
  this.showOAuthSelector("login");
2089
2019
  this.editor.setText("");
@@ -2195,6 +2125,7 @@ export class InteractiveMode {
2195
2125
  this.footer.invalidate();
2196
2126
  switch (event.type) {
2197
2127
  case "agent_start":
2128
+ this.pendingTools.clear();
2198
2129
  if (this.settingsManager.getShowTerminalProgress()) {
2199
2130
  this.ui.terminal.setProgress(true);
2200
2131
  }
@@ -2228,6 +2159,10 @@ export class InteractiveMode {
2228
2159
  this.footer.invalidate();
2229
2160
  this.ui.requestRender();
2230
2161
  break;
2162
+ case "thinking_level_changed":
2163
+ this.footer.invalidate();
2164
+ this.updateEditorBorderColor();
2165
+ break;
2231
2166
  case "message_start":
2232
2167
  if (event.message.role === "custom") {
2233
2168
  this.addMessageToChat(event.message);
@@ -2581,6 +2516,7 @@ export class InteractiveMode {
2581
2516
  */
2582
2517
  renderSessionContext(sessionContext, options = {}) {
2583
2518
  this.pendingTools.clear();
2519
+ const renderedPendingTools = new Map();
2584
2520
  if (options.updateFooter) {
2585
2521
  this.footer.invalidate();
2586
2522
  this.updateEditorBorderColor();
@@ -2613,17 +2549,17 @@ export class InteractiveMode {
2613
2549
  component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
2614
2550
  }
2615
2551
  else {
2616
- this.pendingTools.set(content.id, component);
2552
+ renderedPendingTools.set(content.id, component);
2617
2553
  }
2618
2554
  }
2619
2555
  }
2620
2556
  }
2621
2557
  else if (message.role === "toolResult") {
2622
2558
  // Match tool results to pending tool components
2623
- const component = this.pendingTools.get(message.toolCallId);
2559
+ const component = renderedPendingTools.get(message.toolCallId);
2624
2560
  if (component) {
2625
2561
  component.updateResult(message);
2626
- this.pendingTools.delete(message.toolCallId);
2562
+ renderedPendingTools.delete(message.toolCallId);
2627
2563
  }
2628
2564
  }
2629
2565
  else {
@@ -2631,6 +2567,9 @@ export class InteractiveMode {
2631
2567
  this.addMessageToChat(message, options);
2632
2568
  }
2633
2569
  }
2570
+ for (const [toolCallId, component] of renderedPendingTools) {
2571
+ this.pendingTools.set(toolCallId, component);
2572
+ }
2634
2573
  this.ui.requestRender();
2635
2574
  }
2636
2575
  renderInitialMessages() {
@@ -2704,6 +2643,38 @@ export class InteractiveMode {
2704
2643
  // extension cleanup can write restore sequences and re-trigger EIO.
2705
2644
  process.exit(129);
2706
2645
  }
2646
+ /**
2647
+ * Last-resort handler for uncaught exceptions. The TUI puts stdin into raw
2648
+ * mode and hides the cursor; without this handler, an uncaught throw from
2649
+ * anywhere (e.g. an extension's async `ChildProcess.on("exit")` callback)
2650
+ * tears down the process while leaving the terminal in raw mode with no
2651
+ * cursor, requiring `stty sane && reset` to recover.
2652
+ *
2653
+ * Unlike emergencyTerminalExit, the terminal is still alive here, so we
2654
+ * call ui.stop() to restore cooked mode, the cursor, and disable bracketed
2655
+ * paste / Kitty / modifyOtherKeys sequences.
2656
+ */
2657
+ uncaughtCrash(error) {
2658
+ if (this.isShuttingDown) {
2659
+ process.exit(1);
2660
+ }
2661
+ this.isShuttingDown = true;
2662
+ try {
2663
+ this.unregisterSignalHandlers();
2664
+ }
2665
+ catch { }
2666
+ try {
2667
+ killTrackedDetachedChildren();
2668
+ }
2669
+ catch { }
2670
+ try {
2671
+ this.ui.stop();
2672
+ }
2673
+ catch { }
2674
+ console.error("pi exiting due to uncaughtException:");
2675
+ console.error(error);
2676
+ process.exit(1);
2677
+ }
2707
2678
  /**
2708
2679
  * Check if shutdown was requested and perform shutdown if so.
2709
2680
  */
@@ -2739,6 +2710,12 @@ export class InteractiveMode {
2739
2710
  process.stderr.on("error", terminalErrorHandler);
2740
2711
  this.signalCleanupHandlers.push(() => process.stdout.off("error", terminalErrorHandler));
2741
2712
  this.signalCleanupHandlers.push(() => process.stderr.off("error", terminalErrorHandler));
2713
+ // Restore the terminal before the process dies on any uncaught throw.
2714
+ // Without this, an unhandled exception from extension code (or anywhere
2715
+ // in pi) leaves the terminal in raw mode with no cursor.
2716
+ const uncaughtExceptionHandler = (error) => this.uncaughtCrash(error);
2717
+ process.prependListener("uncaughtException", uncaughtExceptionHandler);
2718
+ this.signalCleanupHandlers.push(() => process.off("uncaughtException", uncaughtExceptionHandler));
2742
2719
  }
2743
2720
  unregisterSignalHandlers() {
2744
2721
  for (const cleanup of this.signalCleanupHandlers) {
@@ -2805,6 +2782,7 @@ export class InteractiveMode {
2805
2782
  }
2806
2783
  // If not streaming, Alt+Enter acts like regular Enter (trigger onSubmit)
2807
2784
  else if (this.editor.onSubmit) {
2785
+ this.editor.setText("");
2808
2786
  this.editor.onSubmit(text);
2809
2787
  }
2810
2788
  }
@@ -2895,7 +2873,7 @@ export class InteractiveMode {
2895
2873
  return;
2896
2874
  }
2897
2875
  const currentText = this.editor.getExpandedText?.() ?? this.editor.getText();
2898
- const tmpFile = path.join(os.tmpdir(), `aery-editor-${Date.now()}.aery.md`);
2876
+ const tmpFile = path.join(os.tmpdir(), `pi-editor-${Date.now()}.pi.md`);
2899
2877
  try {
2900
2878
  // Write current content to temp file
2901
2879
  fs.writeFileSync(tmpFile, currentText, "utf-8");
@@ -2939,6 +2917,7 @@ export class InteractiveMode {
2939
2917
  showError(errorMessage) {
2940
2918
  this.chatContainer.addChild(new Spacer(1));
2941
2919
  this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
2920
+ this.chatContainer.addChild(new Spacer(1));
2942
2921
  this.ui.requestRender();
2943
2922
  }
2944
2923
  showWarning(warningMessage) {
@@ -2947,10 +2926,13 @@ export class InteractiveMode {
2947
2926
  this.ui.requestRender();
2948
2927
  }
2949
2928
  showNewVersionNotification(newVersion) {
2950
- const action = theme.fg("accent", getUpdateInstruction("@eminent337/aery"));
2951
- const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. `) + action;
2952
- const changelogUrl = theme.fg("accent", "https://github.com/eminent337/aery/blob/main/packages/coding-agent/CHANGELOG.md");
2953
- const changelogLine = theme.fg("muted", "Changelog: ") + changelogUrl;
2929
+ const action = theme.fg("accent", `${APP_NAME} update`);
2930
+ const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. Run `) + action;
2931
+ const changelogUrl = "https://github.com/eminent337/aery/blob/main/packages/coding-agent/CHANGELOG.md";
2932
+ const changelogLink = getCapabilities().hyperlinks
2933
+ ? hyperlink(theme.fg("accent", "open changelog"), changelogUrl)
2934
+ : theme.fg("accent", changelogUrl);
2935
+ const changelogLine = theme.fg("muted", "Changelog: ") + changelogLink;
2954
2936
  this.chatContainer.addChild(new Spacer(1));
2955
2937
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2956
2938
  this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}\n${changelogLine}`, 1, 0));
@@ -3689,24 +3671,12 @@ export class InteractiveMode {
3689
3671
  }));
3690
3672
  const modelProviders = new Set(this.session.modelRegistry.getAll().map((model) => model.provider));
3691
3673
  for (const providerId of modelProviders) {
3692
- if (oauthProviderIds.has(providerId) || API_KEY_LOGIN_PROVIDER_BLOCKLIST.has(providerId)) {
3674
+ if (!isApiKeyLoginProvider(providerId, oauthProviderIds)) {
3693
3675
  continue;
3694
3676
  }
3695
3677
  options.push({
3696
3678
  id: providerId,
3697
- name: getApiKeyProviderDisplayName(providerId),
3698
- authType: "api_key",
3699
- });
3700
- }
3701
- if (!authType || authType === "api_key") {
3702
- options.push({
3703
- id: AERY_GATEWAY_PROVIDER_ID,
3704
- name: "Aery Gateway",
3705
- authType: "api_key",
3706
- });
3707
- options.push({
3708
- id: CUSTOM_OPENAI_COMPATIBLE_PROVIDER_ID,
3709
- name: CUSTOM_OPENAI_COMPATIBLE_PROVIDER_LABEL,
3679
+ name: this.session.modelRegistry.getProviderDisplayName(providerId),
3710
3680
  authType: "api_key",
3711
3681
  });
3712
3682
  }
@@ -3715,7 +3685,6 @@ export class InteractiveMode {
3715
3685
  }
3716
3686
  getLogoutProviderOptions() {
3717
3687
  const authStorage = this.session.modelRegistry.authStorage;
3718
- const oauthNameById = new Map(authStorage.getOAuthProviders().map((provider) => [provider.id, provider.name]));
3719
3688
  const options = [];
3720
3689
  for (const providerId of authStorage.list()) {
3721
3690
  const credential = authStorage.get(providerId);
@@ -3724,9 +3693,7 @@ export class InteractiveMode {
3724
3693
  }
3725
3694
  options.push({
3726
3695
  id: providerId,
3727
- name: credential.type === "oauth"
3728
- ? (oauthNameById.get(providerId) ?? providerId)
3729
- : getApiKeyProviderDisplayName(providerId),
3696
+ name: this.session.modelRegistry.getProviderDisplayName(providerId),
3730
3697
  authType: credential.type,
3731
3698
  });
3732
3699
  }
@@ -3760,22 +3727,19 @@ export class InteractiveMode {
3760
3727
  if (!providerOption) {
3761
3728
  return;
3762
3729
  }
3763
- if (providerOption.id === AERY_GATEWAY_PROVIDER_ID) {
3764
- await this.showAeryGatewayLoginDialog();
3765
- }
3766
- else if (providerOption.id === CUSTOM_OPENAI_COMPATIBLE_PROVIDER_ID) {
3767
- await this.showCustomOpenAICompatibleLoginDialog();
3768
- }
3769
- else if (providerOption.authType === "oauth") {
3730
+ if (providerOption.authType === "oauth") {
3770
3731
  await this.showLoginDialog(providerOption.id, providerOption.name);
3771
3732
  }
3733
+ else if (providerOption.id === BEDROCK_PROVIDER_ID) {
3734
+ this.showBedrockSetupDialog(providerOption.id, providerOption.name);
3735
+ }
3772
3736
  else {
3773
3737
  await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
3774
3738
  }
3775
3739
  }, () => {
3776
3740
  done();
3777
3741
  this.showLoginAuthTypeSelector();
3778
- });
3742
+ }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId));
3779
3743
  return { component: selector, focus: selector };
3780
3744
  });
3781
3745
  }
@@ -3786,7 +3750,7 @@ export class InteractiveMode {
3786
3750
  }
3787
3751
  const providerOptions = this.getLogoutProviderOptions();
3788
3752
  if (providerOptions.length === 0) {
3789
- this.showStatus("No providers logged in. Use /login first.");
3753
+ this.showStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
3790
3754
  return;
3791
3755
  }
3792
3756
  this.showSelector((done) => {
@@ -3802,7 +3766,7 @@ export class InteractiveMode {
3802
3766
  await this.updateAvailableProviderCount();
3803
3767
  const message = providerOption.authType === "oauth"
3804
3768
  ? `Logged out of ${providerOption.name}`
3805
- : `Removed API key for ${providerOption.name}`;
3769
+ : `Removed stored API key for ${providerOption.name}. Environment variables and models.json config are unchanged.`;
3806
3770
  this.showStatus(message);
3807
3771
  }
3808
3772
  catch (error) {
@@ -3864,117 +3828,31 @@ export class InteractiveMode {
3864
3828
  void this.maybeWarnAboutAnthropicSubscriptionAuth();
3865
3829
  }
3866
3830
  }
3867
- const providerCheck = checkProviderSetup(providerId, providerName, this.session.modelRegistry);
3868
- if (providerCheck.level === "ok") {
3869
- this.showStatus(providerCheck.message);
3870
- }
3871
- else if (providerCheck.level === "warning") {
3872
- this.showWarning(providerCheck.message);
3873
- }
3874
- else {
3875
- this.showError(providerCheck.message);
3876
- }
3877
3831
  }
3878
- async showApiKeyLoginDialog(providerId, providerName) {
3879
- const previousModel = this.session.model;
3880
- const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
3881
- // Completion handled below
3882
- }, providerName);
3883
- this.editorContainer.clear();
3884
- this.editorContainer.addChild(dialog);
3885
- this.ui.setFocus(dialog);
3886
- this.ui.requestRender();
3832
+ showBedrockSetupDialog(providerId, providerName) {
3887
3833
  const restoreEditor = () => {
3888
3834
  this.editorContainer.clear();
3889
3835
  this.editorContainer.addChild(this.editor);
3890
3836
  this.ui.setFocus(this.editor);
3891
3837
  this.ui.requestRender();
3892
3838
  };
3893
- try {
3894
- const apiKeyPrompt = providerId === "cloudflare-workers-ai" ? "Enter Cloudflare API token:" : "Enter API key:";
3895
- const apiKey = (await dialog.showPrompt(apiKeyPrompt)).trim();
3896
- if (!apiKey) {
3897
- throw new Error("API key cannot be empty.");
3898
- }
3899
- if (providerId === "cloudflare-workers-ai") {
3900
- const accountId = (await dialog.showPrompt("Enter Cloudflare account ID:")).trim();
3901
- if (!accountId) {
3902
- throw new Error("Cloudflare account ID cannot be empty.");
3903
- }
3904
- this.session.modelRegistry.authStorage.set(providerId, { type: "api_key", key: apiKey, accountId });
3905
- this.session.modelRegistry.refresh();
3906
- }
3907
- else {
3908
- this.session.modelRegistry.authStorage.set(providerId, { type: "api_key", key: apiKey });
3909
- }
3910
- restoreEditor();
3911
- await this.completeProviderAuthentication(providerId, providerName, "api_key", previousModel);
3912
- }
3913
- catch (error) {
3914
- restoreEditor();
3915
- const errorMsg = error instanceof Error ? error.message : String(error);
3916
- if (errorMsg !== "Login cancelled") {
3917
- this.showError(`Failed to save API key for ${providerName}: ${errorMsg}`);
3918
- }
3919
- }
3920
- }
3921
- async showAeryGatewayLoginDialog() {
3922
- const dialog = new LoginDialogComponent(this.ui, AERY_GATEWAY_PROVIDER_ID, (_success, _message) => { }, "Connect to Aery Gateway");
3839
+ const dialog = new LoginDialogComponent(this.ui, providerId, () => restoreEditor(), providerName, "Amazon Bedrock setup");
3840
+ dialog.showInfo([
3841
+ theme.fg("text", "Amazon Bedrock uses AWS credentials instead of a single API key."),
3842
+ theme.fg("text", "Configure an AWS profile, IAM keys, bearer token, or role-based credentials."),
3843
+ theme.fg("muted", "See:"),
3844
+ theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`),
3845
+ ]);
3923
3846
  this.editorContainer.clear();
3924
3847
  this.editorContainer.addChild(dialog);
3925
3848
  this.ui.setFocus(dialog);
3926
3849
  this.ui.requestRender();
3927
- const restoreEditor = () => {
3928
- this.editorContainer.clear();
3929
- this.editorContainer.addChild(this.editor);
3930
- this.ui.setFocus(this.editor);
3931
- this.ui.requestRender();
3932
- };
3933
- try {
3934
- const aeryKey = (await dialog.showPrompt("Enter your Aery key:", "aery_...")).trim();
3935
- if (!aeryKey)
3936
- throw new Error("Aery key cannot be empty.");
3937
- const provider = (await dialog.showPrompt("Provider to route through (e.g. anthropic, openai, openrouter):", "anthropic")).trim();
3938
- if (!provider)
3939
- throw new Error("Provider cannot be empty.");
3940
- const modelId = (await dialog.showPrompt("Model ID:", "claude-sonnet-4-5")).trim();
3941
- if (!modelId)
3942
- throw new Error("Model ID cannot be empty.");
3943
- const baseUrl = `${AERY_GATEWAY_BASE_URL}/${provider}`;
3944
- const saved = saveCustomOpenAICompatibleProvider({
3945
- modelsPath: getModelsPath(),
3946
- baseUrl,
3947
- modelId,
3948
- api: AERY_GATEWAY_PROVIDER_APIS[provider] ?? "openai-completions",
3949
- });
3950
- this.session.modelRegistry.authStorage.set(saved.providerId, { type: "api_key", key: aeryKey });
3951
- this.session.modelRegistry.refresh();
3952
- restoreEditor();
3953
- await this.updateAvailableProviderCount();
3954
- this.footer.invalidate();
3955
- this.updateEditorBorderColor();
3956
- const model = this.session.modelRegistry.find(saved.providerId, saved.modelId);
3957
- if (model) {
3958
- try {
3959
- await this.session.setModel(model);
3960
- this.showStatus(`Connected to Aery Gateway → ${provider}/${modelId}.`);
3961
- }
3962
- catch {
3963
- this.showStatus(`Aery Gateway configured. Use /model to select ${saved.providerId}/${modelId}.`);
3964
- }
3965
- }
3966
- }
3967
- catch (error) {
3968
- restoreEditor();
3969
- const errorMsg = error instanceof Error ? error.message : String(error);
3970
- if (errorMsg !== "Login cancelled") {
3971
- this.showError(`Failed to connect to Aery Gateway: ${errorMsg}`);
3972
- }
3973
- }
3974
3850
  }
3975
- async showCustomOpenAICompatibleLoginDialog() {
3851
+ async showApiKeyLoginDialog(providerId, providerName) {
3976
3852
  const previousModel = this.session.model;
3977
- const dialog = new LoginDialogComponent(this.ui, CUSTOM_OPENAI_COMPATIBLE_PROVIDER_ID, (_success, _message) => { }, `Configure ${CUSTOM_OPENAI_COMPATIBLE_PROVIDER_LABEL}`);
3853
+ const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
3854
+ // Completion handled below
3855
+ }, providerName);
3978
3856
  this.editorContainer.clear();
3979
3857
  this.editorContainer.addChild(dialog);
3980
3858
  this.ui.setFocus(dialog);
@@ -3986,58 +3864,44 @@ export class InteractiveMode {
3986
3864
  this.ui.requestRender();
3987
3865
  };
3988
3866
  try {
3989
- const baseUrl = (await dialog.showPrompt("Enter base URL:", "https://api.example.com/v1")).trim();
3990
- if (!baseUrl) {
3991
- throw new Error("Base URL cannot be empty.");
3992
- }
3993
- const modelId = (await dialog.showPrompt("Enter model ID:", "gpt-4o-mini")).trim();
3994
- if (!modelId) {
3995
- throw new Error("Model ID cannot be empty.");
3996
- }
3997
- const apiKey = (await dialog.showPrompt("Enter API key:", "sk-...")).trim();
3867
+ const apiKey = (await dialog.showPrompt("Enter API key:")).trim();
3998
3868
  if (!apiKey) {
3999
3869
  throw new Error("API key cannot be empty.");
4000
3870
  }
4001
- const saved = saveCustomOpenAICompatibleProvider({
4002
- modelsPath: getModelsPath(),
4003
- baseUrl,
4004
- modelId,
4005
- });
4006
- this.session.modelRegistry.authStorage.set(saved.providerId, { type: "api_key", key: apiKey });
4007
- this.session.modelRegistry.refresh();
3871
+ this.session.modelRegistry.authStorage.set(providerId, { type: "api_key", key: apiKey });
4008
3872
  restoreEditor();
4009
- await this.updateAvailableProviderCount();
4010
- this.footer.invalidate();
4011
- this.updateEditorBorderColor();
4012
- const model = this.session.modelRegistry.find(saved.providerId, saved.modelId);
4013
- let selectedModel = false;
4014
- if (model) {
4015
- try {
4016
- await this.session.setModel(model);
4017
- selectedModel = true;
4018
- }
4019
- catch (error) {
4020
- this.showError(`Saved ${saved.providerId}/${saved.modelId}, but selecting it failed: ${error instanceof Error ? error.message : String(error)}. Use /model to select it manually.`);
4021
- }
4022
- }
4023
- if (selectedModel) {
4024
- this.showStatus(`Configured ${saved.providerId}/${saved.modelId}. Provider saved to ${saved.modelsPath}; API key saved to ${getAuthPath()}.`);
4025
- }
4026
- else {
4027
- this.showStatus(`Configured ${saved.providerId}/${saved.modelId}. Provider saved to ${saved.modelsPath}; API key saved to ${getAuthPath()}. Use /model to select it.`);
4028
- }
4029
- if (isUnknownModel(previousModel) && !selectedModel) {
4030
- this.showWarning(`Custom provider ${saved.providerId} is available, but no model was selected automatically.`);
4031
- }
3873
+ await this.completeProviderAuthentication(providerId, providerName, "api_key", previousModel);
4032
3874
  }
4033
3875
  catch (error) {
4034
3876
  restoreEditor();
4035
3877
  const errorMsg = error instanceof Error ? error.message : String(error);
4036
3878
  if (errorMsg !== "Login cancelled") {
4037
- this.showError(`Failed to configure ${CUSTOM_OPENAI_COMPATIBLE_PROVIDER_LABEL}: ${errorMsg}`);
3879
+ this.showError(`Failed to save API key for ${providerName}: ${errorMsg}`);
4038
3880
  }
4039
3881
  }
4040
3882
  }
3883
+ showOAuthLoginSelect(dialog, prompt) {
3884
+ return new Promise((resolve) => {
3885
+ const restoreDialog = () => {
3886
+ this.editorContainer.clear();
3887
+ this.editorContainer.addChild(dialog);
3888
+ this.ui.setFocus(dialog);
3889
+ this.ui.requestRender();
3890
+ };
3891
+ const labels = prompt.options.map((option) => option.label);
3892
+ const selector = new ExtensionSelectorComponent(prompt.message, labels, (optionLabel) => {
3893
+ restoreDialog();
3894
+ resolve(prompt.options.find((option) => option.label === optionLabel)?.id);
3895
+ }, () => {
3896
+ restoreDialog();
3897
+ resolve(undefined);
3898
+ });
3899
+ this.editorContainer.clear();
3900
+ this.editorContainer.addChild(selector);
3901
+ this.ui.setFocus(selector);
3902
+ this.ui.requestRender();
3903
+ });
3904
+ }
4041
3905
  async showLoginDialog(providerId, providerName) {
4042
3906
  const providerInfo = this.session.modelRegistry.authStorage
4043
3907
  .getOAuthProviders()
@@ -4075,7 +3939,7 @@ export class InteractiveMode {
4075
3939
  if (usesCallbackServer) {
4076
3940
  // Show input for manual paste, racing with callback
4077
3941
  dialog
4078
- .showManualInput("Open the URL in your browser, log in, then paste the redirect URL here.\n(WSL/remote users: after login, copy the full URL from the browser address bar and paste it below)")
3942
+ .showManualInput("Paste redirect URL below, or complete login in browser:")
4079
3943
  .then((value) => {
4080
3944
  if (value && manualCodeResolve) {
4081
3945
  manualCodeResolve(value);
@@ -4101,6 +3965,7 @@ export class InteractiveMode {
4101
3965
  onProgress: (message) => {
4102
3966
  dialog.showProgress(message);
4103
3967
  },
3968
+ onSelect: (prompt) => this.showOAuthLoginSelect(dialog, prompt),
4104
3969
  onManualCodeInput: () => manualCodePromise,
4105
3970
  signal: dialog.signal,
4106
3971
  });
@@ -4451,43 +4316,17 @@ export class InteractiveMode {
4451
4316
  this.chatContainer.addChild(new DynamicBorder());
4452
4317
  this.ui.requestRender();
4453
4318
  }
4454
- handleExtensionsDoctorCommand() {
4455
- this.chatContainer.addChild(new Spacer(1));
4456
- this.chatContainer.addChild(new Text(formatCurrentCoreExtensionsReport(), 1, 0));
4457
- this.ui.requestRender();
4458
- }
4459
- handleCapabilitiesCommand() {
4460
- const report = collectCapabilitiesReport({
4461
- session: this.session,
4462
- services: this.runtimeHost.services,
4463
- });
4464
- this.chatContainer.addChild(new Spacer(1));
4465
- this.chatContainer.addChild(new Text(formatCapabilitiesReport(report), 1, 0));
4466
- this.ui.requestRender();
4467
- }
4468
- /**
4469
- * Capitalize keybinding for display (e.g., "ctrl+c" -> "Ctrl+C").
4470
- */
4471
- capitalizeKey(key) {
4472
- return key
4473
- .split("/")
4474
- .map((k) => k
4475
- .split("+")
4476
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
4477
- .join("+"))
4478
- .join("/");
4479
- }
4480
4319
  /**
4481
4320
  * Get capitalized display string for an app keybinding action.
4482
4321
  */
4483
4322
  getAppKeyDisplay(action) {
4484
- return this.capitalizeKey(keyText(action));
4323
+ return keyDisplayText(action);
4485
4324
  }
4486
4325
  /**
4487
4326
  * Get capitalized display string for an editor keybinding action.
4488
4327
  */
4489
4328
  getEditorKeyDisplay(action) {
4490
- return this.capitalizeKey(keyText(action));
4329
+ return keyDisplayText(action);
4491
4330
  }
4492
4331
  handleHotkeysCommand() {
4493
4332
  // Navigation keybindings
@@ -4586,7 +4425,7 @@ export class InteractiveMode {
4586
4425
  `;
4587
4426
  for (const [key, shortcut] of shortcuts) {
4588
4427
  const description = shortcut.description ?? shortcut.extensionPath;
4589
- const keyDisplay = key.replace(/\b\w/g, (c) => c.toUpperCase());
4428
+ const keyDisplay = formatKeyText(key, { capitalize: true });
4590
4429
  hotkeys += `| \`${keyDisplay}\` | ${description} |\n`;
4591
4430
  }
4592
4431
  }