@hieplp/pi-account-switcher 0.2.1 → 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.
- package/README.md +32 -22
- package/package.json +10 -7
- package/src/commands/accounts/add.ts +1 -1
- package/src/commands/accounts/edit.ts +1 -1
- package/src/commands/accounts/index.ts +3 -1
- package/src/commands/accounts/list.ts +1 -1
- package/src/commands/accounts/oauth.ts +1 -1
- package/src/commands/accounts/remove.ts +1 -1
- package/src/commands/accounts/shared/base.ts +34 -3
- package/src/commands/accounts/shared/prompts.ts +1 -1
- package/src/commands/accounts/switch.ts +6 -3
- package/src/commands/accounts/verify.ts +298 -0
- package/src/commands/base.ts +3 -3
- package/src/commands/index.ts +1 -1
- package/src/commands/models/add.ts +1 -1
- package/src/commands/models/index.ts +1 -1
- package/src/commands/models/list.ts +1 -1
- package/src/commands/models/remove.ts +1 -1
- package/src/commands/models/shared/base.ts +1 -1
- package/src/commands/models/shared/prompts.ts +1 -1
- package/src/commands/models/shared/select.ts +1 -1
- package/src/commands/providers/add.ts +1 -1
- package/src/commands/providers/edit.ts +1 -1
- package/src/commands/providers/index.ts +1 -1
- package/src/commands/providers/list.ts +5 -2
- package/src/commands/providers/remove.ts +1 -1
- package/src/commands/providers/shared/base.ts +1 -1
- package/src/commands/providers/shared/prompts.ts +1 -1
- package/src/commands/providers/shared/select.ts +1 -1
- package/src/commands/system/export.ts +1 -2
- package/src/commands/system/import.ts +9 -3
- package/src/commands/system/index.ts +1 -1
- package/src/commands/system/reset.ts +1 -1
- package/src/constants/commands.ts +5 -0
- package/src/constants/env.ts +1 -0
- package/src/constants/index.ts +1 -0
- package/src/constants/paths.ts +1 -1
- package/src/extension.ts +2 -4
- package/src/index.ts +1 -1
- package/src/runtime/account-switcher-runtime.ts +6 -2
- package/src/runtime/account-switcher.ts +2 -1
- package/src/runtime/index.ts +2 -4
- package/src/schemas/accounts.ts +1 -2
- package/src/schemas/providers.ts +2 -6
- package/src/services/models.ts +2 -2
- package/src/services/providers.ts +3 -9
- package/src/types/context.ts +1 -1
- package/src/types/index.ts +1 -1
- package/src/utils/accounts.ts +2 -2
- package/src/utils/commands.ts +10 -0
- package/src/utils/common.ts +15 -0
- package/src/utils/errors.ts +1 -3
- package/src/utils/filterable-selector.ts +108 -3
- package/src/utils/index.ts +1 -0
- package/src/utils/models.ts +1 -1
- package/src/utils/ui.ts +36 -2
package/README.md
CHANGED
|
@@ -48,45 +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
|
|
58
|
-
|
|
59
|
-
| `/accounts:add`
|
|
60
|
-
| `/accounts:list`
|
|
61
|
-
| `/accounts:switch` | Switch to another account within the current provider
|
|
62
|
-
| `/accounts:edit`
|
|
63
|
-
| `/accounts:remove` | Delete an account
|
|
64
|
-
| `/accounts:oauth`
|
|
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
|
|
69
|
-
|
|
70
|
-
| `/providers:add`
|
|
71
|
-
| `/providers:list`
|
|
72
|
-
| `/providers:edit`
|
|
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
|
|
78
|
-
|
|
79
|
-
| `/models:list`
|
|
80
|
-
| `/models:add`
|
|
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
|
|
86
|
-
|
|
87
|
-
| `/system:reset`
|
|
95
|
+
| Command | Description |
|
|
96
|
+
| ---------------- | -------------------------------------------------------- |
|
|
97
|
+
| `/system:reset` | Delete all accounts, providers, and state |
|
|
88
98
|
| `/system:export` | Export all accounts, providers, and state to a JSON file |
|
|
89
|
-
| `/system:import` | Import accounts, providers, and state from a JSON file
|
|
99
|
+
| `/system:import` | Import accounts, providers, and state from a JSON file |
|
|
90
100
|
|
|
91
101
|
---
|
|
92
102
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hieplp/pi-account-switcher",
|
|
3
|
-
"version": "0.2.
|
|
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
|
-
"@
|
|
39
|
-
"@
|
|
40
|
+
"@earendil-works/pi-ai": "*",
|
|
41
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
40
42
|
},
|
|
41
43
|
"devDependencies": {
|
|
42
|
-
"@
|
|
43
|
-
"@
|
|
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 "@
|
|
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 "@
|
|
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 "@
|
|
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 "@
|
|
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 "@
|
|
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 "@
|
|
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 "@
|
|
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
|
|
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 "@
|
|
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 "@
|
|
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(
|
|
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
|
+
}
|
package/src/commands/base.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
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>();
|
|
@@ -31,7 +31,7 @@ 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
|
|
package/src/commands/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
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 "@
|
|
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 "@
|
|
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 "@
|
|
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 "@
|
|
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 "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import type { AccountSwitcherContext, ProviderConfig } from "@/types";
|
|
4
4
|
import { BaseCommand, type CommandMeta } from "../../base";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import { COMMANDS } from "@/constants";
|
|
4
4
|
import type { AccountSwitcherContext } from "@/types";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import { COMMANDS } from "@/constants";
|
|
4
4
|
import type { AccountSwitcherContext } from "@/types";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import { useAddProviderCommand } from "./add";
|
|
4
4
|
import { useEditProviderCommand } from "./edit";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import { COMMANDS } from "@/constants";
|
|
4
4
|
import { ProviderCommand } from "./shared";
|
|
@@ -18,7 +18,10 @@ class ListProvidersCommand extends ProviderCommand {
|
|
|
18
18
|
try {
|
|
19
19
|
const providers = await this.loadProviders(ctx);
|
|
20
20
|
if (!providers) return;
|
|
21
|
-
await ctx.ui.select(
|
|
21
|
+
await ctx.ui.select(
|
|
22
|
+
"Providers",
|
|
23
|
+
providers.map((p) => this.format(p)),
|
|
24
|
+
);
|
|
22
25
|
} catch (e) {
|
|
23
26
|
ctx.ui.notify(`Failed to list providers: ${errorUtil.format(e)}`, "error");
|
|
24
27
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import { COMMANDS } from "@/constants";
|
|
4
4
|
import type { AccountSwitcherContext } from "@/types";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import type { AccountSwitcherContext, ProviderConfig } from "@/types";
|
|
4
4
|
import { BaseCommand, type CommandMeta } from "../../base";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionUIContext } from "@
|
|
1
|
+
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { ProviderApi, ProviderConfig } from "@/types";
|
|
3
3
|
import { PROVIDER_API_TYPES } from "@/constants";
|
|
4
4
|
import { providerUtil, uiUtil } from "@/utils";
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import type { ExtensionAPI } from "@
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { AccountSwitcher } from "@/runtime";
|
|
4
4
|
import { COMMANDS, DEFAULT_EXPORT_PATH, STATE_PATH } from "@/constants";
|
|
5
5
|
import type { AccountSwitcherContext } from "@/types";
|
|
6
6
|
import { BaseCommand } from "../base";
|
|
7
7
|
import { errorUtil, fileUtil } from "@/utils";
|
|
8
8
|
|
|
9
|
-
|
|
10
9
|
export const useExportCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
11
10
|
new ExportCommand(pi, runtime).register();
|
|
12
11
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import z from "zod";
|
|
3
|
-
import type { ExtensionAPI } from "@
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { AccountSwitcher } from "@/runtime";
|
|
5
5
|
import { ACCOUNTS_PATH, COMMANDS, DEFAULT_EXPORT_PATH, PROVIDERS_PATH, STATE_PATH } from "@/constants";
|
|
6
6
|
import { accountSchema, providerSchema } from "@/schemas";
|
|
@@ -78,8 +78,14 @@ class ImportCommand extends BaseCommand {
|
|
|
78
78
|
|
|
79
79
|
function parseImportBundle(raw: unknown): ImportBundle {
|
|
80
80
|
const parsed = exportBundleSchema.parse(raw);
|
|
81
|
-
assertNoDuplicateIds(
|
|
82
|
-
|
|
81
|
+
assertNoDuplicateIds(
|
|
82
|
+
"account",
|
|
83
|
+
parsed.accounts.map((account) => account.id),
|
|
84
|
+
);
|
|
85
|
+
assertNoDuplicateIds(
|
|
86
|
+
"provider",
|
|
87
|
+
parsed.providers.map((provider) => provider.id),
|
|
88
|
+
);
|
|
83
89
|
return parsed;
|
|
84
90
|
}
|
|
85
91
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import { useExportCommand } from "./export";
|
|
4
4
|
import { useImportCommand } from "./import";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { rm } from "node:fs/promises";
|
|
2
|
-
import type { ExtensionAPI } from "@
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { AccountSwitcher } from "@/runtime";
|
|
4
4
|
import { ACCOUNTS_PATH, COMMANDS, PROVIDERS_PATH, STATE_PATH } from "@/constants";
|
|
5
5
|
import type { AccountSwitcherContext } from "@/types";
|
|
@@ -24,6 +24,11 @@ export const COMMANDS = {
|
|
|
24
24
|
name: "accounts:oauth",
|
|
25
25
|
description: "Import Pi /login OAuth credentials as a switchable account",
|
|
26
26
|
},
|
|
27
|
+
verify: {
|
|
28
|
+
name: "accounts:verify",
|
|
29
|
+
description:
|
|
30
|
+
"Verify secrets for one or all accounts without activating them (pass 'all'; add 'ping' to send a test request)",
|
|
31
|
+
},
|
|
27
32
|
},
|
|
28
33
|
providers: {
|
|
29
34
|
add: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const COMMAND_PREFIX_ENV = "PI_ACCOUNT_SWITCHER_COMMAND_PREFIX";
|
package/src/constants/index.ts
CHANGED
package/src/constants/paths.ts
CHANGED
|
@@ -6,4 +6,4 @@ export const ACCOUNTS_PATH = join(APP_DIR, "accounts.json");
|
|
|
6
6
|
export const PROVIDERS_PATH = join(APP_DIR, "providers.json");
|
|
7
7
|
export const STATE_PATH = join(APP_DIR, "state.json");
|
|
8
8
|
export const PI_AUTH_PATH = join(homedir(), ".pi", "agent", "auth.json");
|
|
9
|
-
export const DEFAULT_EXPORT_PATH = "~/pi-account-switcher-export.json";
|
|
9
|
+
export const DEFAULT_EXPORT_PATH = "~/pi-account-switcher-export.json";
|
package/src/extension.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createJiti } from "@mariozechner/jiti";
|
|
2
|
-
import type { ExtensionAPI } from "@
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
@@ -11,9 +11,7 @@ export default async function accountSwitcherBootstrap(pi: ExtensionAPI) {
|
|
|
11
11
|
},
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
const extension = await jiti.import<
|
|
15
|
-
(pi: ExtensionAPI) => void | Promise<void>
|
|
16
|
-
>("./index", {
|
|
14
|
+
const extension = await jiti.import<(pi: ExtensionAPI) => void | Promise<void>>("./index", {
|
|
17
15
|
default: true,
|
|
18
16
|
});
|
|
19
17
|
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcherContext } from "@/types";
|
|
3
3
|
import { useAccountSwitcher, type AccountSwitcher } from "./runtime";
|
|
4
4
|
import { registerAllCommands } from "./commands";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import AccountSwitcher from "./account-switcher";
|
|
2
2
|
import { ACCOUNTS_PATH, PROVIDERS_PATH, STATE_PATH } from "@/constants";
|
|
3
|
-
import type { Api, Model } from "@
|
|
4
|
-
import type { ExtensionAPI } from "@
|
|
3
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
4
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import type { AccountConfig, AccountSwitcherContext, PiAuthEntry, ProviderConfig } from "@/types";
|
|
6
6
|
import type { AccountService, ModelService, PiAuthService, ProviderService } from "@/services";
|
|
7
7
|
import { useAccountService, useModelService, usePiAuthService, useProviderService } from "@/services";
|
|
@@ -174,6 +174,10 @@ export default class AccountSwitcherRuntime implements AccountSwitcher {
|
|
|
174
174
|
return this.providerService.getProviders();
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
registerProvider(provider: ProviderConfig): void {
|
|
178
|
+
this.providerService.registerProvider(provider);
|
|
179
|
+
}
|
|
180
|
+
|
|
177
181
|
async addProvider(provider: ProviderConfig): Promise<void> {
|
|
178
182
|
return this.providerService.addProvider(provider);
|
|
179
183
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Api, Model } from "@
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { AccountConfig, AccountSwitcherContext, PiAuthEntry, ProviderConfig } from "@/types";
|
|
3
3
|
|
|
4
4
|
export default interface AccountSwitcher {
|
|
@@ -26,6 +26,7 @@ export default interface AccountSwitcher {
|
|
|
26
26
|
|
|
27
27
|
// Provider
|
|
28
28
|
getProviders(): ProviderConfig[];
|
|
29
|
+
registerProvider(provider: ProviderConfig): void;
|
|
29
30
|
addProvider(config: ProviderConfig): Promise<void>;
|
|
30
31
|
editProvider(original: ProviderConfig, updated: ProviderConfig): Promise<void>;
|
|
31
32
|
removeProvider(provider: ProviderConfig): Promise<void>;
|
package/src/runtime/index.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type AccountSwitcher from "./account-switcher";
|
|
3
3
|
import AccountSwitcherRuntime from "./account-switcher-runtime";
|
|
4
4
|
|
|
5
|
-
function useAccountSwitcher(
|
|
6
|
-
pi: Pick<ExtensionAPI, "registerProvider" | "setModel">,
|
|
7
|
-
): AccountSwitcher {
|
|
5
|
+
function useAccountSwitcher(pi: Pick<ExtensionAPI, "registerProvider" | "setModel">): AccountSwitcher {
|
|
8
6
|
return new AccountSwitcherRuntime(pi);
|
|
9
7
|
}
|
|
10
8
|
|
package/src/schemas/accounts.ts
CHANGED
|
@@ -44,7 +44,6 @@ export const accountSchema = z
|
|
|
44
44
|
account.usesProviderApiKey ||
|
|
45
45
|
account.piAuth,
|
|
46
46
|
{
|
|
47
|
-
message:
|
|
48
|
-
"Account must define env credentials, providerApiKey, provider apiKey, or piAuth credentials",
|
|
47
|
+
message: "Account must define env credentials, providerApiKey, provider apiKey, or piAuth credentials",
|
|
49
48
|
},
|
|
50
49
|
);
|
package/src/schemas/providers.ts
CHANGED
|
@@ -20,9 +20,7 @@ export const providerModelSchema = z
|
|
|
20
20
|
})
|
|
21
21
|
.optional(),
|
|
22
22
|
compat: jsonRecordSchema.optional(),
|
|
23
|
-
thinkingLevelMap: z
|
|
24
|
-
.record(z.string(), z.union([z.string(), z.null()]))
|
|
25
|
-
.optional(),
|
|
23
|
+
thinkingLevelMap: z.record(z.string(), z.union([z.string(), z.null()])).optional(),
|
|
26
24
|
headers: z.record(z.string(), z.string()).optional(),
|
|
27
25
|
})
|
|
28
26
|
.passthrough();
|
|
@@ -42,9 +40,7 @@ export const providerSchema = z
|
|
|
42
40
|
authHeader: z.boolean().optional(),
|
|
43
41
|
compat: jsonRecordSchema.optional(),
|
|
44
42
|
models: z.array(providerModelSchema).optional(),
|
|
45
|
-
modelOverrides: z
|
|
46
|
-
.record(z.string(), providerModelSchema.partial())
|
|
47
|
-
.optional(),
|
|
43
|
+
modelOverrides: z.record(z.string(), providerModelSchema.partial()).optional(),
|
|
48
44
|
})
|
|
49
45
|
.passthrough();
|
|
50
46
|
|
package/src/services/models.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Api, Model } from "@
|
|
2
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { AccountSwitcherContext } from "@/types";
|
|
4
4
|
|
|
5
5
|
type ProviderModel = Model<Api>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type ProviderStore, useProviderStore } from "@/storage";
|
|
3
3
|
import type { ProviderConfig, ProviderModelConfig } from "@/types";
|
|
4
4
|
import { commonUtil } from "@/utils";
|
|
@@ -72,10 +72,7 @@ class ProviderServiceImpl implements ProviderService {
|
|
|
72
72
|
registerProvider(provider: ProviderConfig): void {
|
|
73
73
|
const config = this.toPiProvider(provider);
|
|
74
74
|
if (!config) return;
|
|
75
|
-
this.pi.registerProvider(
|
|
76
|
-
provider.id,
|
|
77
|
-
config as Parameters<ExtensionAPI["registerProvider"]>[1],
|
|
78
|
-
);
|
|
75
|
+
this.pi.registerProvider(provider.id, config as Parameters<ExtensionAPI["registerProvider"]>[1]);
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
private toPiProvider(provider: ProviderConfig): Record<string, unknown> | undefined {
|
|
@@ -104,10 +101,7 @@ class ProviderServiceImpl implements ProviderService {
|
|
|
104
101
|
});
|
|
105
102
|
}
|
|
106
103
|
|
|
107
|
-
private toPiModel(
|
|
108
|
-
provider: ProviderConfig,
|
|
109
|
-
model: ProviderModelConfig,
|
|
110
|
-
): Record<string, unknown> | undefined {
|
|
104
|
+
private toPiModel(provider: ProviderConfig, model: ProviderModelConfig): Record<string, unknown> | undefined {
|
|
111
105
|
return commonUtil.omitUndefined({
|
|
112
106
|
...model,
|
|
113
107
|
api: model.api ?? provider.api,
|
package/src/types/context.ts
CHANGED
package/src/types/index.ts
CHANGED
package/src/utils/accounts.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as piAi from "@
|
|
1
|
+
import * as piAi from "@earendil-works/pi-ai";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import type { ModelRegistry } from "@
|
|
3
|
+
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { AccountConfig, SecretSource } from "@/types";
|
|
5
5
|
import { commonUtil } from "./common";
|
|
6
6
|
import { fileUtil } from "./files";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { COMMAND_PREFIX_ENV } from "@/constants";
|
|
2
|
+
|
|
3
|
+
export const commandUtil = {
|
|
4
|
+
name: (name: string): string => {
|
|
5
|
+
const prefix = process.env[COMMAND_PREFIX_ENV]?.trim();
|
|
6
|
+
if (!prefix) return name;
|
|
7
|
+
|
|
8
|
+
return `${prefix.endsWith(":") ? prefix : `${prefix}:`}${name}`;
|
|
9
|
+
},
|
|
10
|
+
};
|
package/src/utils/common.ts
CHANGED
|
@@ -71,4 +71,19 @@ export const commonUtil = {
|
|
|
71
71
|
});
|
|
72
72
|
return stdout.trim();
|
|
73
73
|
},
|
|
74
|
+
|
|
75
|
+
runWithConcurrency: async <T, R>(items: T[], concurrency: number, worker: (item: T) => Promise<R>): Promise<R[]> => {
|
|
76
|
+
const results = new Array<R>(items.length);
|
|
77
|
+
let nextIndex = 0;
|
|
78
|
+
|
|
79
|
+
const runNext = async (): Promise<void> => {
|
|
80
|
+
const index = nextIndex++;
|
|
81
|
+
if (index >= items.length) return;
|
|
82
|
+
results[index] = await worker(items[index]);
|
|
83
|
+
await runNext();
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, runNext));
|
|
87
|
+
return results;
|
|
88
|
+
},
|
|
74
89
|
};
|
package/src/utils/errors.ts
CHANGED
|
@@ -3,9 +3,7 @@ import z from "zod";
|
|
|
3
3
|
export const errorUtil = {
|
|
4
4
|
format: (error: unknown): string => {
|
|
5
5
|
if (error instanceof z.ZodError) {
|
|
6
|
-
return error.issues
|
|
7
|
-
.map((issue) => `${errorUtil.formatPath(issue.path)}: ${issue.message}`)
|
|
8
|
-
.join("; ");
|
|
6
|
+
return error.issues.map((issue) => `${errorUtil.formatPath(issue.path)}: ${issue.message}`).join("; ");
|
|
9
7
|
}
|
|
10
8
|
return error instanceof Error ? error.message : String(error);
|
|
11
9
|
},
|
|
@@ -1,9 +1,114 @@
|
|
|
1
|
-
import { DynamicBorder } from "@
|
|
2
|
-
import { Container, fuzzyFilter, getKeybindings, Input, Spacer, Text, type Focusable } from "@
|
|
3
|
-
import type { ThemeColor } from "@
|
|
1
|
+
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Container, fuzzyFilter, getKeybindings, Input, Spacer, Text, type Focusable } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
5
|
type Theme = { fg: (color: ThemeColor, text: string) => string; bold: (text: string) => string };
|
|
6
6
|
|
|
7
|
+
export class FilterableMultiSelectComponent extends Container implements Focusable {
|
|
8
|
+
private readonly listContainer: Container;
|
|
9
|
+
private readonly theme: Theme;
|
|
10
|
+
private selectedIndex = 0;
|
|
11
|
+
private checked: boolean[];
|
|
12
|
+
|
|
13
|
+
_focused = false;
|
|
14
|
+
get focused() {
|
|
15
|
+
return this._focused;
|
|
16
|
+
}
|
|
17
|
+
set focused(value: boolean) {
|
|
18
|
+
this._focused = value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
title: string,
|
|
23
|
+
private readonly options: string[],
|
|
24
|
+
initialChecked: boolean[],
|
|
25
|
+
private readonly onDone: (selected: string[] | undefined) => void,
|
|
26
|
+
theme: Theme,
|
|
27
|
+
private readonly disabled: boolean[] = [],
|
|
28
|
+
) {
|
|
29
|
+
super();
|
|
30
|
+
this.theme = theme;
|
|
31
|
+
this.checked = options.map((_, i) => !this.disabled[i] && (initialChecked[i] ?? false));
|
|
32
|
+
this.selectedIndex = Math.max(
|
|
33
|
+
0,
|
|
34
|
+
this.disabled.findIndex((value) => !value),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const borderColor = (str: string) => theme.fg("border", str);
|
|
38
|
+
this.addChild(new DynamicBorder(borderColor));
|
|
39
|
+
this.addChild(new Spacer(1));
|
|
40
|
+
this.addChild(new Text(theme.fg("accent", theme.bold(title)), 1, 0));
|
|
41
|
+
this.addChild(new Spacer(1));
|
|
42
|
+
|
|
43
|
+
this.listContainer = new Container();
|
|
44
|
+
this.addChild(this.listContainer);
|
|
45
|
+
this.addChild(new Spacer(1));
|
|
46
|
+
|
|
47
|
+
const hint = (key: string, desc: string) => theme.fg("dim", key) + theme.fg("muted", ` ${desc}`);
|
|
48
|
+
this.addChild(
|
|
49
|
+
new Text(
|
|
50
|
+
hint("↑↓", "navigate") +
|
|
51
|
+
" " +
|
|
52
|
+
hint("Space", "toggle") +
|
|
53
|
+
" " +
|
|
54
|
+
hint("Enter", "run") +
|
|
55
|
+
" " +
|
|
56
|
+
hint("Esc", "cancel"),
|
|
57
|
+
1,
|
|
58
|
+
0,
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
this.addChild(new Spacer(1));
|
|
62
|
+
this.addChild(new DynamicBorder(borderColor));
|
|
63
|
+
|
|
64
|
+
this.updateList();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
handleInput(keyData: string) {
|
|
68
|
+
const kb = getKeybindings();
|
|
69
|
+
if (kb.matches(keyData, "tui.select.up")) {
|
|
70
|
+
this.moveSelection(-1);
|
|
71
|
+
} else if (kb.matches(keyData, "tui.select.down")) {
|
|
72
|
+
this.moveSelection(1);
|
|
73
|
+
} else if (keyData === " ") {
|
|
74
|
+
this.toggleSelected();
|
|
75
|
+
} else if (kb.matches(keyData, "tui.select.confirm")) {
|
|
76
|
+
this.onDone(this.options.filter((_, i) => this.checked[i] && !this.disabled[i]));
|
|
77
|
+
} else if (kb.matches(keyData, "tui.select.cancel")) {
|
|
78
|
+
this.onDone(undefined);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private moveSelection(delta: number) {
|
|
83
|
+
if (this.options.length === 0) return;
|
|
84
|
+
for (let step = 0; step < this.options.length; step++) {
|
|
85
|
+
this.selectedIndex = (this.selectedIndex + delta + this.options.length) % this.options.length;
|
|
86
|
+
if (!this.disabled[this.selectedIndex]) break;
|
|
87
|
+
}
|
|
88
|
+
this.updateList();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private toggleSelected() {
|
|
92
|
+
if (this.disabled[this.selectedIndex]) return;
|
|
93
|
+
this.checked[this.selectedIndex] = !this.checked[this.selectedIndex];
|
|
94
|
+
this.updateList();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private updateList() {
|
|
98
|
+
this.listContainer.clear();
|
|
99
|
+
for (let i = 0; i < this.options.length; i++) {
|
|
100
|
+
const isSelected = i === this.selectedIndex;
|
|
101
|
+
if (this.disabled[i]) {
|
|
102
|
+
this.listContainer.addChild(new Text(this.theme.fg("muted", ` ${this.options[i]}`), 0, 0));
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const marker = this.checked[i] ? "[x]" : "[ ]";
|
|
106
|
+
const line = `${isSelected ? "›" : " "} ${marker} ${this.options[i]}`;
|
|
107
|
+
this.listContainer.addChild(new Text(isSelected ? this.theme.fg("accent", line) : line, 0, 0));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
7
112
|
export class FilterableExtensionSelectorComponent extends Container implements Focusable {
|
|
8
113
|
private readonly searchInput: Input;
|
|
9
114
|
private readonly listContainer: Container;
|
package/src/utils/index.ts
CHANGED
package/src/utils/models.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Api, Model } from "@
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { AccountConfig, AccountSwitcherContext, ProviderConfig } from "@/types";
|
|
3
3
|
import { providerUtil } from "./providers";
|
|
4
4
|
import { uiUtil } from "./ui";
|
package/src/utils/ui.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ExtensionUIContext } from "@
|
|
1
|
+
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { commonUtil } from "./common.js";
|
|
3
|
-
import { FilterableExtensionSelectorComponent } from "./filterable-selector.js";
|
|
3
|
+
import { FilterableExtensionSelectorComponent, FilterableMultiSelectComponent } from "./filterable-selector.js";
|
|
4
4
|
|
|
5
5
|
function deduplicateLabels(labels: string[]): string[] {
|
|
6
6
|
const seen = new Map<string, number>();
|
|
@@ -46,4 +46,38 @@ export const uiUtil = {
|
|
|
46
46
|
(_tui, theme, _keybindings, done) => new FilterableExtensionSelectorComponent(title, options, done, theme),
|
|
47
47
|
);
|
|
48
48
|
},
|
|
49
|
+
|
|
50
|
+
/** Show a checkbox-style multi-select component. */
|
|
51
|
+
multiSelect: (
|
|
52
|
+
ui: ExtensionUIContext,
|
|
53
|
+
title: string,
|
|
54
|
+
options: string[],
|
|
55
|
+
initialChecked: boolean[] = [],
|
|
56
|
+
disabled: boolean[] = [],
|
|
57
|
+
): Promise<string[] | undefined> => {
|
|
58
|
+
return ui.custom<string[] | undefined>(
|
|
59
|
+
(_tui, theme, _keybindings, done) =>
|
|
60
|
+
new FilterableMultiSelectComponent(title, options, initialChecked, done, theme, disabled),
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/** Like multiSelect but skips items where the corresponding value is null (used for group headers). */
|
|
65
|
+
multiGroupedSelect: async <T>(
|
|
66
|
+
ui: ExtensionUIContext,
|
|
67
|
+
title: string,
|
|
68
|
+
labels: string[],
|
|
69
|
+
values: Array<T | null>,
|
|
70
|
+
initialChecked: boolean[] = [],
|
|
71
|
+
): Promise<T[] | undefined> => {
|
|
72
|
+
const deduped = deduplicateLabels(labels);
|
|
73
|
+
const selected = await uiUtil.multiSelect(
|
|
74
|
+
ui,
|
|
75
|
+
title,
|
|
76
|
+
deduped,
|
|
77
|
+
initialChecked,
|
|
78
|
+
values.map((value) => value === null),
|
|
79
|
+
);
|
|
80
|
+
if (!selected) return undefined;
|
|
81
|
+
return selected.map((label) => values[deduped.indexOf(label)]).filter((value): value is T => value !== null);
|
|
82
|
+
},
|
|
49
83
|
};
|