@hieplp/pi-account-switcher 0.2.0 → 0.2.2

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 (56) hide show
  1. package/README.md +53 -21
  2. package/package.json +10 -7
  3. package/src/commands/accounts/add.ts +1 -1
  4. package/src/commands/accounts/edit.ts +1 -1
  5. package/src/commands/accounts/index.ts +3 -1
  6. package/src/commands/accounts/list.ts +1 -1
  7. package/src/commands/accounts/oauth.ts +1 -1
  8. package/src/commands/accounts/remove.ts +1 -1
  9. package/src/commands/accounts/shared/base.ts +34 -3
  10. package/src/commands/accounts/shared/prompts.ts +1 -1
  11. package/src/commands/accounts/switch.ts +6 -3
  12. package/src/commands/accounts/verify.ts +298 -0
  13. package/src/commands/base.ts +6 -6
  14. package/src/commands/index.ts +1 -1
  15. package/src/commands/models/add.ts +1 -1
  16. package/src/commands/models/index.ts +1 -1
  17. package/src/commands/models/list.ts +1 -1
  18. package/src/commands/models/remove.ts +1 -1
  19. package/src/commands/models/shared/base.ts +1 -1
  20. package/src/commands/models/shared/prompts.ts +1 -1
  21. package/src/commands/models/shared/select.ts +1 -1
  22. package/src/commands/providers/add.ts +1 -1
  23. package/src/commands/providers/edit.ts +1 -1
  24. package/src/commands/providers/index.ts +1 -1
  25. package/src/commands/providers/list.ts +5 -2
  26. package/src/commands/providers/remove.ts +1 -1
  27. package/src/commands/providers/shared/base.ts +1 -1
  28. package/src/commands/providers/shared/prompts.ts +1 -1
  29. package/src/commands/providers/shared/select.ts +1 -1
  30. package/src/commands/system/export.ts +51 -0
  31. package/src/commands/system/import.ts +102 -0
  32. package/src/commands/system/index.ts +5 -1
  33. package/src/commands/system/reset.ts +1 -1
  34. package/src/constants/commands.ts +13 -0
  35. package/src/constants/env.ts +1 -0
  36. package/src/constants/index.ts +1 -0
  37. package/src/constants/paths.ts +2 -1
  38. package/src/extension.ts +2 -4
  39. package/src/index.ts +1 -1
  40. package/src/runtime/account-switcher-runtime.ts +6 -2
  41. package/src/runtime/account-switcher.ts +2 -1
  42. package/src/runtime/index.ts +2 -4
  43. package/src/schemas/accounts.ts +1 -2
  44. package/src/schemas/providers.ts +2 -6
  45. package/src/services/models.ts +2 -2
  46. package/src/services/providers.ts +3 -9
  47. package/src/types/context.ts +1 -1
  48. package/src/types/index.ts +1 -1
  49. package/src/utils/accounts.ts +2 -2
  50. package/src/utils/commands.ts +10 -0
  51. package/src/utils/common.ts +15 -0
  52. package/src/utils/errors.ts +1 -3
  53. package/src/utils/filterable-selector.ts +108 -3
  54. package/src/utils/index.ts +1 -0
  55. package/src/utils/models.ts +1 -1
  56. package/src/utils/ui.ts +36 -2
package/README.md CHANGED
@@ -48,43 +48,55 @@ After installing, reload Pi and add your first account:
48
48
  /accounts:add
49
49
  ```
50
50
 
51
+ ### Local development command prefix
52
+
53
+ If you have the npm package installed and also run a local checkout, set `PI_ACCOUNT_SWITCHER_COMMAND_PREFIX` before launching Pi to avoid command-name collisions:
54
+
55
+ ```bash
56
+ PI_ACCOUNT_SWITCHER_COMMAND_PREFIX=dev pi -e ./src/extension.ts
57
+ ```
58
+
59
+ The local commands will be registered as `/dev:accounts:list`, `/dev:accounts:add`, etc. The prefix may include the trailing colon (`dev:`) or omit it (`dev`).
60
+
51
61
  ---
52
62
 
53
63
  ## Commands
54
64
 
55
65
  ### Accounts
56
66
 
57
- | Command | Description |
58
- |---|---|
59
- | `/accounts:add` | Add a new account interactively |
60
- | `/accounts:list` | List all accounts and activate the selected one |
61
- | `/accounts:switch` | Switch to another account within the current provider |
62
- | `/accounts:edit` | Edit label, provider, id, or credential source |
63
- | `/accounts:remove` | Delete an account |
64
- | `/accounts:oauth` | Import the current Pi `/login` OAuth session as a named account |
67
+ | Command | Description |
68
+ | ------------------ | --------------------------------------------------------------- |
69
+ | `/accounts:add` | Add a new account interactively |
70
+ | `/accounts:list` | List all accounts and activate the selected one |
71
+ | `/accounts:switch` | Switch to another account within the current provider |
72
+ | `/accounts:edit` | Edit label, provider, id, or credential source |
73
+ | `/accounts:remove` | Delete an account |
74
+ | `/accounts:oauth` | Import the current Pi `/login` OAuth session as a named account |
65
75
 
66
76
  ### Providers
67
77
 
68
- | Command | Description |
69
- |---|---|
70
- | `/providers:add` | Add a reusable custom provider |
71
- | `/providers:list` | List custom providers |
72
- | `/providers:edit` | Edit a custom provider |
78
+ | Command | Description |
79
+ | ------------------- | -------------------------------- |
80
+ | `/providers:add` | Add a reusable custom provider |
81
+ | `/providers:list` | List custom providers |
82
+ | `/providers:edit` | Edit a custom provider |
73
83
  | `/providers:remove` | Remove an unused custom provider |
74
84
 
75
85
  ### Models
76
86
 
77
- | Command | Description |
78
- |---|---|
79
- | `/models:list` | List all available models and switch to the selected one |
80
- | `/models:add` | Add a custom model config to the current provider |
81
- | `/models:remove` | Remove a custom model config |
87
+ | Command | Description |
88
+ | ---------------- | -------------------------------------------------------- |
89
+ | `/models:list` | List all available models and switch to the selected one |
90
+ | `/models:add` | Add a custom model config to the current provider |
91
+ | `/models:remove` | Remove a custom model config |
82
92
 
83
93
  ### System
84
94
 
85
- | Command | Description |
86
- |---|---|
87
- | `/system:reset` | Delete all accounts, providers, and state |
95
+ | Command | Description |
96
+ | ---------------- | -------------------------------------------------------- |
97
+ | `/system:reset` | Delete all accounts, providers, and state |
98
+ | `/system:export` | Export all accounts, providers, and state to a JSON file |
99
+ | `/system:import` | Import accounts, providers, and state from a JSON file |
88
100
 
89
101
  ---
90
102
 
@@ -157,6 +169,26 @@ Custom providers are stored at `~/.pi/account-switcher/providers.json` and suppo
157
169
 
158
170
  ---
159
171
 
172
+ ## Export / Import
173
+
174
+ Back up or migrate your full configuration with two commands:
175
+
176
+ ```
177
+ /system:export # prompts for a path, defaults to ~/pi-account-switcher-export.json
178
+ /system:export ~/backup.json # export to a specific path
179
+ ```
180
+
181
+ The export file contains all accounts, providers, and active-selection state as a single JSON bundle. To restore on another machine (or after a reset):
182
+
183
+ ```
184
+ /system:import # prompts for a path, defaults to ~/pi-account-switcher-export.json
185
+ /system:import ~/backup.json # import from a specific path
186
+ ```
187
+
188
+ > **Warning:** import replaces all existing data. A confirmation prompt is shown before anything is written.
189
+
190
+ ---
191
+
160
192
  ## Config Reference
161
193
 
162
194
  ### Accounts — `~/.pi/account-switcher/accounts.json`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hieplp/pi-account-switcher",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Pi extension for quickly switching between multiple accounts/API keys per provider.",
6
6
  "license": "MIT",
@@ -27,21 +27,24 @@
27
27
  ],
28
28
  "scripts": {
29
29
  "typecheck": "tsc --noEmit",
30
- "test": "vitest run"
30
+ "test": "vitest run",
31
+ "format": "prettier --write .",
32
+ "format:check": "prettier --check ."
31
33
  },
32
34
  "dependencies": {
35
+ "@earendil-works/pi-tui": "^0.74.0",
33
36
  "@mariozechner/jiti": "^2.6.5",
34
- "@mariozechner/pi-tui": "^0.73.1",
35
37
  "zod": "^4.4.3"
36
38
  },
37
39
  "peerDependencies": {
38
- "@mariozechner/pi-ai": "*",
39
- "@mariozechner/pi-coding-agent": "*"
40
+ "@earendil-works/pi-ai": "*",
41
+ "@earendil-works/pi-coding-agent": "*"
40
42
  },
41
43
  "devDependencies": {
42
- "@mariozechner/pi-ai": "latest",
43
- "@mariozechner/pi-coding-agent": "latest",
44
+ "@earendil-works/pi-ai": "latest",
45
+ "@earendil-works/pi-coding-agent": "latest",
44
46
  "@types/node": "latest",
47
+ "prettier": "^3.8.3",
45
48
  "typescript": "latest",
46
49
  "vitest": "latest"
47
50
  },
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { AccountSwitcher } from "@/runtime";
3
3
  import type { AccountConfig, AccountSwitcherContext } from "@/types";
4
4
  import { COMMANDS } from "@/constants";
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { AccountSwitcher } from "@/runtime";
3
3
  import type { AccountSwitcherContext } from "@/types";
4
4
  import { COMMANDS } from "@/constants";
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { AccountSwitcher } from "@/runtime";
3
3
  import { useAddAccountCommand } from "./add";
4
4
  import { useEditAccountCommand } from "./edit";
@@ -6,6 +6,7 @@ import { useListAccountsCommand } from "./list";
6
6
  import { useOAuthImportCommand } from "./oauth";
7
7
  import { useRemoveAccountCommand } from "./remove";
8
8
  import { useSwitchAccountCommand } from "./switch";
9
+ import { useVerifyAccountsCommand } from "./verify";
9
10
 
10
11
  const useAccountCommands = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
11
12
  useAddAccountCommand(pi, runtime);
@@ -14,6 +15,7 @@ const useAccountCommands = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
14
15
  useOAuthImportCommand(pi, runtime);
15
16
  useRemoveAccountCommand(pi, runtime);
16
17
  useSwitchAccountCommand(pi, runtime);
18
+ useVerifyAccountsCommand(pi, runtime);
17
19
  };
18
20
 
19
21
  export default useAccountCommands;
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { AccountSwitcher } from "@/runtime";
3
3
  import type { AccountSwitcherContext } from "@/types";
4
4
  import { COMMANDS } from "@/constants";
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { AccountSwitcher } from "@/runtime";
3
3
  import type { AccountConfig, AccountSwitcherContext } from "@/types";
4
4
  import { COMMANDS, OAUTH_PROVIDER_IDS, PI_AUTH_PATH } from "@/constants";
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { AccountSwitcher } from "@/runtime";
3
3
  import type { AccountSwitcherContext } from "@/types";
4
4
  import { COMMANDS } from "@/constants";
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { AccountSwitcher } from "@/runtime";
3
3
  import type { AccountConfig, AccountSwitcherContext } from "@/types";
4
4
  import { uiUtil } from "@/utils";
@@ -69,6 +69,31 @@ export abstract class AccountCommand extends BaseCommand {
69
69
  accounts: AccountConfig[],
70
70
  label = "Pick account",
71
71
  ): Promise<AccountConfig | undefined> {
72
+ const { labels, values } = this.buildGroupedAccountSelectItems(accounts, true);
73
+ return this.pickGrouped(ctx, label, labels, values);
74
+ }
75
+
76
+ protected async pickGroupedAccounts(
77
+ ctx: AccountSwitcherContext,
78
+ accounts: AccountConfig[],
79
+ label = "Pick accounts",
80
+ ): Promise<AccountConfig[] | undefined> {
81
+ const activeId = this.runtime.getActiveAccount()?.id;
82
+ const { labels, values } = this.buildGroupedAccountSelectItems(accounts, false);
83
+ const firstAccountIndex = values.findIndex((value) => value !== null);
84
+ const initialChecked = values.map((value, index) =>
85
+ activeId ? value?.id === activeId : firstAccountIndex !== -1 && index === firstAccountIndex,
86
+ );
87
+ return uiUtil.multiGroupedSelect(ctx.ui, label, labels, values, initialChecked);
88
+ }
89
+
90
+ private buildGroupedAccountSelectItems(
91
+ accounts: AccountConfig[],
92
+ includeActiveMarker: boolean,
93
+ ): {
94
+ labels: string[];
95
+ values: Array<AccountConfig | null>;
96
+ } {
72
97
  const items = buildGroupedItems(accounts, this.runtime.getProviders(), this.runtime.getActiveAccount()?.id);
73
98
 
74
99
  const labels: string[] = [];
@@ -79,10 +104,16 @@ export abstract class AccountCommand extends BaseCommand {
79
104
  values.push(null);
80
105
  continue;
81
106
  }
82
- labels.push(formatAccountItem(item));
107
+ labels.push(includeActiveMarker ? formatAccountItem(item) : this.formatMultiAccountItem(item));
83
108
  values.push(item.account);
84
109
  }
85
110
 
86
- return this.pickGrouped(ctx, label, labels, values);
111
+ return { labels, values };
112
+ }
113
+
114
+ private formatMultiAccountItem(
115
+ item: Extract<ReturnType<typeof buildGroupedItems>[number], { type: "account" }>,
116
+ ): string {
117
+ return item.active ? `${item.account.label} (active)` : item.account.label;
87
118
  }
88
119
  }
@@ -1,4 +1,4 @@
1
- import type { ExtensionUIContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { AccountConfig, ProviderConfig, SecretSource } from "@/types";
3
3
  import { commonUtil, providerUtil, uiUtil } from "@/utils";
4
4
  import { ACCOUNTS_PATH } from "@/constants";
@@ -1,8 +1,8 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { AccountSwitcher } from "@/runtime";
3
3
  import type { AccountSwitcherContext } from "@/types";
4
4
  import { COMMANDS } from "@/constants";
5
- import { errorUtil, providerUtil } from "@/utils";
5
+ import { commandUtil, errorUtil, providerUtil } from "@/utils";
6
6
  import { AccountCommand } from "./shared";
7
7
 
8
8
  export const useSwitchAccountCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
@@ -20,7 +20,10 @@ class SwitchAccountCommand extends AccountCommand {
20
20
 
21
21
  const active = this.runtime.getActiveAccount();
22
22
  if (!active) {
23
- ctx.ui.notify("No active account. Use accounts:list to activate one first.", "info");
23
+ ctx.ui.notify(
24
+ `No active account. Use ${commandUtil.name(COMMANDS.accounts.list.name)} to activate one first.`,
25
+ "info",
26
+ );
24
27
  return;
25
28
  }
26
29
 
@@ -0,0 +1,298 @@
1
+ import { completeSimple, type Api, type Model } from "@earendil-works/pi-ai";
2
+ import type { AuthCredential, ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ import type { AccountSwitcher } from "@/runtime";
4
+ import type { AccountConfig, AccountSwitcherContext, ProviderConfig, SecretSource } from "@/types";
5
+ import { COMMANDS } from "@/constants";
6
+ import { accountUtil, commonUtil, errorUtil, providerUtil, uiUtil } from "@/utils";
7
+ import { AccountCommand } from "./shared";
8
+
9
+ export const useVerifyAccountsCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
10
+ new VerifyAccountsCommand(pi, runtime).register();
11
+ };
12
+
13
+ type VerifyTestPlan = {
14
+ secrets: boolean;
15
+ ping: boolean;
16
+ };
17
+
18
+ type CheckResult = {
19
+ ok: boolean;
20
+ line: string;
21
+ };
22
+
23
+ type AccountVerifyReport = {
24
+ account: AccountConfig;
25
+ ok: boolean;
26
+ lines: string[];
27
+ };
28
+
29
+ const TEST_OPTIONS = {
30
+ secrets: "Verify secrets",
31
+ ping: "Send ping",
32
+ } as const;
33
+
34
+ const MAX_PARALLEL_VERIFY = 3;
35
+
36
+ class VerifyAccountsCommand extends AccountCommand {
37
+ constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
38
+ super(pi, runtime, COMMANDS.accounts.verify);
39
+ }
40
+
41
+ async handler(ctx: AccountSwitcherContext, args?: string): Promise<void> {
42
+ try {
43
+ const accounts = await this.loadAccounts(ctx);
44
+ if (!accounts) return;
45
+
46
+ const parts = (args ?? "").trim().toLowerCase().split(/\s+/).filter(Boolean);
47
+ const verifyAll = parts.includes("all");
48
+ const pingArg = parts.includes("ping") || parts.includes("probe") || parts.includes("connect");
49
+ const targets = verifyAll
50
+ ? accounts
51
+ : await this.pickGroupedAccounts(ctx, accounts, pingArg ? "Verify and ping accounts" : "Verify accounts");
52
+
53
+ if (!targets || targets.length === 0) return;
54
+
55
+ const plan = pingArg ? { secrets: true, ping: true } : await this.pickTestPlan(ctx);
56
+ if (!plan) return;
57
+
58
+ const selectedTests = this.formatSelectedTests(plan);
59
+ ctx.ui.notify(
60
+ `Testing ${targets.length} account${targets.length === 1 ? "" : "s"} (${selectedTests})...`,
61
+ "info",
62
+ );
63
+
64
+ const concurrency = plan.ping ? 1 : MAX_PARALLEL_VERIFY;
65
+ const reports = await commonUtil.runWithConcurrency(targets, concurrency, (account) =>
66
+ this.verifyAccount(ctx, account, plan),
67
+ );
68
+ const failed = reports.filter((report) => !report.ok).length;
69
+
70
+ ctx.ui.setEditorText(this.formatReport(plan, reports));
71
+ ctx.ui.notify(
72
+ `Finished testing ${targets.length} account${targets.length === 1 ? "" : "s"}${
73
+ failed ? ` — ${failed} failed` : " — all checks passed"
74
+ }.`,
75
+ failed ? "warning" : "info",
76
+ );
77
+ } catch (error) {
78
+ ctx.ui.notify(`accounts:verify failed: ${errorUtil.format(error)}`, "error");
79
+ }
80
+ }
81
+
82
+ private async pickTestPlan(ctx: AccountSwitcherContext): Promise<VerifyTestPlan | undefined> {
83
+ const selected = await uiUtil.multiSelect(
84
+ ctx.ui,
85
+ "What should be tested?",
86
+ [TEST_OPTIONS.secrets, TEST_OPTIONS.ping],
87
+ [true, false],
88
+ );
89
+
90
+ if (!selected) return undefined;
91
+
92
+ const plan = {
93
+ secrets: selected.includes(TEST_OPTIONS.secrets),
94
+ ping: selected.includes(TEST_OPTIONS.ping),
95
+ };
96
+
97
+ if (!plan.secrets && !plan.ping) {
98
+ ctx.ui.notify("No tests selected.", "info");
99
+ return undefined;
100
+ }
101
+
102
+ return plan;
103
+ }
104
+
105
+ private async verifyAccount(
106
+ ctx: AccountSwitcherContext,
107
+ account: AccountConfig,
108
+ plan: VerifyTestPlan,
109
+ ): Promise<AccountVerifyReport> {
110
+ const lines: string[] = [];
111
+ let ok = true;
112
+
113
+ if (plan.secrets) {
114
+ let anyChecked = false;
115
+
116
+ if (account.piAuth) {
117
+ lines.push("✓ secrets: using stored OAuth/piAuth credentials");
118
+ }
119
+
120
+ if (account.usesProviderApiKey && !account.providerApiKey) {
121
+ lines.push("✓ secrets: using provider config apiKey");
122
+ }
123
+
124
+ const secretChecks: Array<[string, SecretSource]> = [];
125
+ if (account.providerApiKey) secretChecks.push(["providerApiKey", account.providerApiKey]);
126
+ if (!account.piAuth && account.env) {
127
+ for (const [envName, source] of Object.entries(account.env)) secretChecks.push([envName, source]);
128
+ }
129
+
130
+ if (secretChecks.length > 0) {
131
+ const results = await Promise.all(secretChecks.map(([key, source]) => this.verifySecret(key, source)));
132
+ for (const result of results) {
133
+ lines.push(result.line);
134
+ ok &&= result.ok;
135
+ }
136
+ } else if (!account.piAuth && !account.usesProviderApiKey) {
137
+ lines.push("ℹ secrets: no secrets configured");
138
+ }
139
+ }
140
+
141
+ if (plan.ping) {
142
+ const result = await this.pingAccount(ctx, account);
143
+ lines.push(result.line);
144
+ ok &&= result.ok;
145
+ }
146
+
147
+ return { account, ok, lines };
148
+ }
149
+
150
+ private async pingAccount(ctx: AccountSwitcherContext, account: AccountConfig): Promise<CheckResult> {
151
+ const prefix = `[${account.label}]`;
152
+ const authProvider = this.resolveAuthProvider(account);
153
+ const model = this.resolveProbeModel(ctx, account, authProvider);
154
+ if (!model) {
155
+ return { ok: false, line: `✗ ping: skipped — no model found for provider ${authProvider}` };
156
+ }
157
+
158
+ const envBackup = new Map<string, string | undefined>();
159
+ let authBackup: AuthCredential | undefined;
160
+ let hadAuth = false;
161
+ let providerToRestore: ProviderConfig | undefined;
162
+
163
+ let requestAuth!: Awaited<ReturnType<typeof ctx.modelRegistry.getApiKeyAndHeaders>>;
164
+ try {
165
+ if (!account.piAuth && account.env) {
166
+ const resolved = await accountUtil.resolveAccountEnv(account);
167
+ for (const [envName, value] of resolved) {
168
+ envBackup.set(envName, process.env[envName]);
169
+ process.env[envName] = value;
170
+ }
171
+ }
172
+
173
+ if (account.piAuth) {
174
+ hadAuth = ctx.modelRegistry.authStorage.has(authProvider);
175
+ authBackup = ctx.modelRegistry.authStorage.get(authProvider);
176
+ ctx.modelRegistry.authStorage.set(authProvider, account.piAuth.entry);
177
+ ctx.modelRegistry.authStorage.reload();
178
+ }
179
+
180
+ if (account.providerApiKey) {
181
+ const provider = providerUtil.findProvider(account.provider, this.runtime.getProviders());
182
+ if (provider) {
183
+ providerToRestore = provider;
184
+ const apiKey = await accountUtil.resolveSecret(account.providerApiKey);
185
+ if (!apiKey) throw new Error("Resolved empty providerApiKey for ping");
186
+ this.runtime.registerProvider({ ...provider, apiKey });
187
+ }
188
+ }
189
+
190
+ requestAuth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
191
+ if (!requestAuth.ok) throw new Error(requestAuth.error);
192
+ } catch (err) {
193
+ return { ok: false, line: `✗ ping: unable to prepare credentials — ${errorUtil.format(err)}` };
194
+ } finally {
195
+ for (const [envName, previous] of envBackup) {
196
+ if (previous === undefined) delete process.env[envName];
197
+ else process.env[envName] = previous;
198
+ }
199
+ if (account.piAuth) {
200
+ if (hadAuth && authBackup) ctx.modelRegistry.authStorage.set(authProvider, authBackup);
201
+ else ctx.modelRegistry.authStorage.remove(authProvider);
202
+ ctx.modelRegistry.authStorage.reload();
203
+ }
204
+ if (providerToRestore) {
205
+ this.runtime.registerProvider(providerToRestore);
206
+ }
207
+ }
208
+
209
+ try {
210
+ ctx.ui.notify(`${prefix} ping: sending request via ${model.provider}/${model.id}...`, "info");
211
+
212
+ const response = await completeSimple(
213
+ model,
214
+ {
215
+ systemPrompt: "You are a health-check endpoint. Follow the user instruction exactly.",
216
+ messages: [
217
+ {
218
+ role: "user",
219
+ content: "Health check: reply with exactly OK.",
220
+ timestamp: Date.now(),
221
+ },
222
+ ],
223
+ },
224
+ {
225
+ apiKey: requestAuth.apiKey,
226
+ headers: requestAuth.headers,
227
+ maxTokens: 16,
228
+ timeoutMs: 30_000,
229
+ maxRetries: 0,
230
+ reasoning: "minimal",
231
+ },
232
+ );
233
+
234
+ if (response.stopReason === "error") throw new Error(response.errorMessage ?? "model returned an error");
235
+ const text = response.content.find((block) => block.type === "text")?.text?.trim();
236
+ return { ok: true, line: `✓ ping: OK via ${model.provider}/${model.id}${text ? ` — ${text}` : ""}` };
237
+ } catch (err) {
238
+ return { ok: false, line: `✗ ping: failed — ${errorUtil.format(err)}` };
239
+ }
240
+ }
241
+
242
+ private resolveAuthProvider(account: AccountConfig): string {
243
+ if (account.piAuth?.provider) return account.piAuth.provider;
244
+ const provider = providerUtil.findProvider(account.provider, this.runtime.getProviders());
245
+ return provider?.piAuthProvider ?? providerUtil.normalizeProvider(account.provider);
246
+ }
247
+
248
+ private resolveProbeModel(
249
+ ctx: AccountSwitcherContext,
250
+ account: AccountConfig,
251
+ authProvider: string,
252
+ ): Model<Api> | undefined {
253
+ const providers = this.runtime.getProviders();
254
+ const normalized = providerUtil.normalizeProviderWithCustom(authProvider, providers);
255
+ if (account.model) {
256
+ const configured = ctx.modelRegistry.find(authProvider, account.model);
257
+ if (configured) return configured;
258
+ }
259
+ if (ctx.model && providerUtil.normalizeProviderWithCustom(ctx.model.provider, providers) === normalized) {
260
+ return ctx.model;
261
+ }
262
+ return ctx.modelRegistry
263
+ .getAll()
264
+ .find((model) => providerUtil.normalizeProviderWithCustom(model.provider, providers) === normalized);
265
+ }
266
+
267
+ private async verifySecret(key: string, source: SecretSource): Promise<CheckResult> {
268
+ try {
269
+ const value = await accountUtil.resolveSecret(source);
270
+ if (!value) throw new Error("resolved to empty value");
271
+ return { ok: true, line: `✓ secrets: ${key} OK` };
272
+ } catch (err) {
273
+ return { ok: false, line: `✗ secrets: ${key} failed — ${errorUtil.format(err)}` };
274
+ }
275
+ }
276
+
277
+ private formatReport(plan: VerifyTestPlan, reports: AccountVerifyReport[]): string {
278
+ const selectedTests = this.formatSelectedTests(plan);
279
+ const passed = reports.filter((report) => report.ok).length;
280
+
281
+ return [
282
+ "Account verify results",
283
+ "",
284
+ `Tests: ${selectedTests}`,
285
+ `Accounts: ${reports.length} (${passed} passed, ${reports.length - passed} failed)`,
286
+ "",
287
+ ...reports.flatMap((report) => [
288
+ `${report.ok ? "✓" : "✗"} ${report.account.label}`,
289
+ ...report.lines.map((line) => ` ${line}`),
290
+ "",
291
+ ]),
292
+ ].join("\n");
293
+ }
294
+
295
+ private formatSelectedTests(plan: VerifyTestPlan): string {
296
+ return [plan.secrets ? "secrets" : undefined, plan.ping ? "ping" : undefined].filter(Boolean).join(" + ");
297
+ }
298
+ }
@@ -1,7 +1,7 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { AccountSwitcher } from "@/runtime";
3
3
  import type { AccountConfig, AccountSwitcherContext, ProviderConfig } from "@/types";
4
- import { providerUtil, uiUtil } from "@/utils";
4
+ import { commandUtil, providerUtil, uiUtil } from "@/utils";
5
5
 
6
6
  function deduplicateLabels(labels: string[]): string[] {
7
7
  const seen = new Map<string, number>();
@@ -19,7 +19,7 @@ export interface CommandMeta {
19
19
 
20
20
  export interface Command extends CommandMeta {
21
21
  register(): void;
22
- handler(ctx: AccountSwitcherContext): Promise<void>;
22
+ handler(ctx: AccountSwitcherContext, args?: string): Promise<void>;
23
23
  }
24
24
 
25
25
  export abstract class BaseCommand implements Command {
@@ -31,14 +31,14 @@ export abstract class BaseCommand implements Command {
31
31
  protected readonly runtime: AccountSwitcher,
32
32
  meta: CommandMeta,
33
33
  ) {
34
- this.name = meta.name;
34
+ this.name = commandUtil.name(meta.name);
35
35
  this.description = meta.description;
36
36
  }
37
37
 
38
38
  register(): void {
39
39
  this.pi.registerCommand(this.name, {
40
40
  description: this.description,
41
- handler: (_, ctx) => this.handler(ctx),
41
+ handler: (args, ctx) => this.handler(ctx, args),
42
42
  });
43
43
  }
44
44
 
@@ -77,5 +77,5 @@ export abstract class BaseCommand implements Command {
77
77
  return ctx.model?.id === modelId;
78
78
  }
79
79
 
80
- abstract handler(ctx: AccountSwitcherContext): Promise<void>;
80
+ abstract handler(ctx: AccountSwitcherContext, args?: string): Promise<void>;
81
81
  }
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { AccountSwitcher } from "@/runtime";
3
3
  import useProviderCommands from "./providers";
4
4
  import useAccountCommands from "./accounts";
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { AccountSwitcher } from "@/runtime";
3
3
  import type { AccountSwitcherContext } from "@/types";
4
4
  import { COMMANDS } from "@/constants";
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { AccountSwitcher } from "@/runtime";
3
3
  import { useListModelsCommand } from "./list";
4
4
  import { useAddModelCommand } from "./add";
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { AccountSwitcher } from "@/runtime";
3
3
  import type { AccountSwitcherContext } from "@/types";
4
4
  import { COMMANDS } from "@/constants";
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { AccountSwitcher } from "@/runtime";
3
3
  import type { AccountSwitcherContext } from "@/types";
4
4
  import { COMMANDS } from "@/constants";