@hieplp/pi-account-switcher 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -10
- package/USAGE.md +80 -40
- package/package.json +3 -2
- package/src/commands/accounts/add.ts +9 -1
- package/src/commands/accounts/edit.ts +15 -1
- package/src/commands/accounts/index.ts +8 -2
- package/src/commands/accounts/list.ts +43 -25
- package/src/commands/accounts/peers.ts +59 -0
- package/src/commands/accounts/set-subagent-account.ts +64 -0
- package/src/commands/accounts/shared/prompts.ts +35 -10
- package/src/commands/accounts/subagent.ts +50 -0
- package/src/commands/accounts/switch.ts +16 -24
- package/src/commands/accounts/verify.ts +2 -1
- package/src/commands/dirs/dirs.ts +176 -0
- package/src/commands/dirs/index.ts +9 -0
- package/src/commands/index.ts +2 -0
- package/src/constants/commands.ts +5 -1
- package/src/constants/providers.ts +60 -5
- package/src/extension.ts +37 -5
- package/src/index.ts +8 -2
- package/src/runtime/account-switcher-runtime.ts +99 -31
- package/src/runtime/account-switcher.ts +2 -1
- package/src/schemas/accounts.ts +1 -0
- package/src/schemas/config.ts +3 -1
- package/src/services/accounts.ts +53 -6
- package/src/storage/accounts.ts +27 -9
- package/src/storage/state.ts +122 -4
- package/src/types/accounts.ts +2 -0
- package/src/types/config.ts +5 -1
- package/src/utils/accounts.ts +66 -7
- package/src/utils/common.ts +27 -0
- package/src/utils/filterable-selector.ts +10 -1
- package/src/utils/providers.ts +3 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AccountSwitcher } from "@/runtime";
|
|
3
|
+
import type { AccountSwitcherContext } from "@/types";
|
|
4
|
+
import { errorUtil } from "@/utils";
|
|
5
|
+
import { AccountCommand } from "./shared";
|
|
6
|
+
|
|
7
|
+
export const useSubagentAccountCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
8
|
+
new SubagentAccountCommand(pi, runtime).register();
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
class SubagentAccountCommand extends AccountCommand {
|
|
12
|
+
constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
|
|
13
|
+
super(pi, runtime, {
|
|
14
|
+
name: "accounts:subagent",
|
|
15
|
+
description: "Set the account to use for the next spawned subagent (one-shot or persistent)",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async handler(ctx: AccountSwitcherContext, args?: string): Promise<void> {
|
|
20
|
+
try {
|
|
21
|
+
await this.runtime.load();
|
|
22
|
+
const accounts = this.runtime.getAccounts();
|
|
23
|
+
|
|
24
|
+
if (accounts.length === 0) {
|
|
25
|
+
ctx.ui.notify("No accounts configured.", "info");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Interactive: pick an account
|
|
30
|
+
const account = await this.pickGroupedAccount(ctx, accounts, "Account for subagent");
|
|
31
|
+
if (!account) return;
|
|
32
|
+
|
|
33
|
+
// Ask about oneshot (default: yes)
|
|
34
|
+
const oneshot = await ctx.ui.confirm(
|
|
35
|
+
"Apply to next subagent only?",
|
|
36
|
+
"Yes = one-shot (next subagent only). No = persistent (all subagents until changed).",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const varName = oneshot !== false ? "PI_ACCOUNT_SWITCHER_NEXT_ID" : "PI_ACCOUNT_SWITCHER_ACTIVE_ID";
|
|
40
|
+
process.env[varName] = account.id;
|
|
41
|
+
|
|
42
|
+
ctx.ui.notify(
|
|
43
|
+
`Subagent account set to: ${account.label} (${oneshot !== false ? "next subagent only" : "persistent"}).`,
|
|
44
|
+
"info",
|
|
45
|
+
);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
ctx.ui.notify(`Failed to set subagent account: ${errorUtil.format(e)}`, "error");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -2,7 +2,7 @@ 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 {
|
|
5
|
+
import { errorUtil } from "@/utils";
|
|
6
6
|
import { AccountCommand } from "./shared";
|
|
7
7
|
|
|
8
8
|
export const useSwitchAccountCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
@@ -14,38 +14,30 @@ class SwitchAccountCommand extends AccountCommand {
|
|
|
14
14
|
super(pi, runtime, COMMANDS.accounts.switch);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
async handler(ctx: AccountSwitcherContext): Promise<void> {
|
|
17
|
+
async handler(ctx: AccountSwitcherContext, args?: string): Promise<void> {
|
|
18
18
|
try {
|
|
19
19
|
await this.runtime.load();
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
21
|
+
// With args: activate by ID directly (agent-facing, any provider)
|
|
22
|
+
if (args) {
|
|
23
|
+
const account = this.runtime.getAccounts().find((a) => a.id === args.trim());
|
|
24
|
+
if (!account) {
|
|
25
|
+
ctx.ui.notify(`Account not found: "${args.trim()}". Use the list_accounts tool to see available accounts.`, "error");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const applied = await this.runtime.activateAccount(account, ctx);
|
|
29
|
+
ctx.ui.notify(`Switched to ${account.label} (${applied}).`, "info");
|
|
27
30
|
return;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
);
|
|
35
|
-
const peers = this.runtime
|
|
36
|
-
.getAccounts()
|
|
37
|
-
.filter(
|
|
38
|
-
(a) =>
|
|
39
|
-
providerUtil.normalizeProviderWithCustom(a.piAuth?.provider ?? a.provider, providers) ===
|
|
40
|
-
normalizedActive && a.id !== active.id,
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
if (peers.length === 0) {
|
|
44
|
-
ctx.ui.notify(`No other accounts for provider "${active.provider}".`, "info");
|
|
33
|
+
// Without args: interactive picker from all accounts
|
|
34
|
+
const accounts = this.runtime.getAccounts();
|
|
35
|
+
if (accounts.length === 0) {
|
|
36
|
+
ctx.ui.notify("No accounts configured. Use accounts:add to create one.", "info");
|
|
45
37
|
return;
|
|
46
38
|
}
|
|
47
39
|
|
|
48
|
-
const account = await this.pickGroupedAccount(ctx,
|
|
40
|
+
const account = await this.pickGroupedAccount(ctx, accounts, "Pick account to activate");
|
|
49
41
|
if (!account) return;
|
|
50
42
|
|
|
51
43
|
const applied = await this.runtime.activateAccount(account, ctx);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { AuthCredential, ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { AccountSwitcher } from "@/runtime";
|
|
4
4
|
import type { AccountConfig, AccountSwitcherContext, ProviderConfig, SecretSource } from "@/types";
|
|
@@ -209,6 +209,7 @@ class VerifyAccountsCommand extends AccountCommand {
|
|
|
209
209
|
try {
|
|
210
210
|
ctx.ui.notify(`${prefix} ping: sending request via ${model.provider}/${model.id}...`, "info");
|
|
211
211
|
|
|
212
|
+
const { completeSimple } = await import("@earendil-works/pi-ai");
|
|
212
213
|
const response = await completeSimple(
|
|
213
214
|
model,
|
|
214
215
|
{
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AccountSwitcher } from "@/runtime";
|
|
3
|
+
import type { AccountConfig, AccountSwitcherContext } from "@/types";
|
|
4
|
+
import { COMMANDS } from "@/constants";
|
|
5
|
+
import { BaseCommand } from "../base";
|
|
6
|
+
import { buildGroupedItems } from "../accounts/shared/select";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join, dirname } from "node:path";
|
|
9
|
+
import type { Dirent } from "node:fs";
|
|
10
|
+
|
|
11
|
+
export const useDirsCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
12
|
+
new DirsCommand(pi, runtime).register();
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
class DirsCommand extends BaseCommand {
|
|
16
|
+
constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
|
|
17
|
+
super(pi, runtime, COMMANDS.accounts.dirs);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async handler(ctx: AccountSwitcherContext): Promise<void> {
|
|
21
|
+
await this.runtime.load();
|
|
22
|
+
const accounts = this.runtime.getAccounts();
|
|
23
|
+
if (accounts.length === 0) {
|
|
24
|
+
ctx.ui.notify("No accounts configured.", "info");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Build entry options: auto-save if active account + CWD both exist
|
|
29
|
+
const activeAccount = this.runtime.getActiveAccount();
|
|
30
|
+
const canAutoSave = Boolean(ctx.cwd && activeAccount);
|
|
31
|
+
const entryOptions = canAutoSave
|
|
32
|
+
? ["Auto-save current folder", "Select an account to configure"]
|
|
33
|
+
: ["Select an account to configure"];
|
|
34
|
+
|
|
35
|
+
const entry = await ctx.ui.select("Directory auto-select", entryOptions);
|
|
36
|
+
if (!entry) return;
|
|
37
|
+
|
|
38
|
+
if (entry === "Auto-save current folder") {
|
|
39
|
+
await this.autoSave(ctx, activeAccount!);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Manual: pick account → add/remove
|
|
44
|
+
await this.manualConfig(ctx, accounts);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private async autoSave(ctx: AccountSwitcherContext, account: AccountConfig): Promise<void> {
|
|
48
|
+
const cwd = ctx.cwd!;
|
|
49
|
+
const resolved = cwd;
|
|
50
|
+
|
|
51
|
+
// Check if already present
|
|
52
|
+
const current = this.runtime.getAccounts().find((a) => a.id === account.id) ?? account;
|
|
53
|
+
const dirs = current.dirs ?? [];
|
|
54
|
+
|
|
55
|
+
if (dirs.includes(resolved)) {
|
|
56
|
+
ctx.ui.notify(`Directory already configured for ${current.label}.`, "info");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const updated: AccountConfig = { ...current, dirs: [...dirs, resolved] };
|
|
61
|
+
await this.runtime.editAccount(current, updated);
|
|
62
|
+
ctx.ui.notify(`Added dir: ${resolved}`, "info");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private async manualConfig(ctx: AccountSwitcherContext, accounts: AccountConfig[]): Promise<void> {
|
|
66
|
+
const account = await this.pickAccount(ctx, accounts);
|
|
67
|
+
if (!account) return;
|
|
68
|
+
|
|
69
|
+
await this.manageDirs(ctx, account);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async pickAccount(
|
|
73
|
+
ctx: AccountSwitcherContext,
|
|
74
|
+
accounts: AccountConfig[],
|
|
75
|
+
): Promise<AccountConfig | undefined> {
|
|
76
|
+
const items = buildGroupedItems(accounts, this.runtime.getProviders(), this.runtime.getActiveAccount()?.id);
|
|
77
|
+
const labels: string[] = [];
|
|
78
|
+
const values: Array<AccountConfig | null> = [];
|
|
79
|
+
for (const item of items) {
|
|
80
|
+
if (item.type === "header") {
|
|
81
|
+
labels.push(item.provider);
|
|
82
|
+
values.push(null);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
labels.push(` ${this.isActiveAccount(item.account) ? `${item.account.label} (active)` : item.account.label}`);
|
|
86
|
+
values.push(item.account);
|
|
87
|
+
}
|
|
88
|
+
return this.pickGrouped(ctx, "Pick account to configure dirs", labels, values);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private async manageDirs(ctx: AccountSwitcherContext, account: AccountConfig): Promise<void> {
|
|
92
|
+
const current = this.runtime.getAccounts().find((a) => a.id === account.id) ?? account;
|
|
93
|
+
const dirs = current.dirs ?? [];
|
|
94
|
+
const dirsDisplay = dirs.length > 0 ? dirs.join(", ") : "(none)";
|
|
95
|
+
ctx.ui.notify(`Account: ${current.label} | Dirs: ${dirsDisplay}`, "info");
|
|
96
|
+
|
|
97
|
+
const options = ["Add directory", "Remove directory"];
|
|
98
|
+
const action = await ctx.ui.select("Directory auto-select", options);
|
|
99
|
+
if (!action) return;
|
|
100
|
+
|
|
101
|
+
if (action === "Add directory") {
|
|
102
|
+
const picked = await this.pickDirectory(ctx, homedir());
|
|
103
|
+
if (!picked) return;
|
|
104
|
+
const resolved = picked;
|
|
105
|
+
if (dirs.includes(resolved)) {
|
|
106
|
+
ctx.ui.notify(`Directory already configured for ${current.label}.`, "info");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const updated: AccountConfig = { ...current, dirs: [...dirs, resolved] };
|
|
110
|
+
await this.runtime.editAccount(current, updated);
|
|
111
|
+
ctx.ui.notify(`Added dir: ${resolved}`, "info");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (action === "Remove directory") {
|
|
116
|
+
if (dirs.length === 0) {
|
|
117
|
+
ctx.ui.notify("No directories configured.", "info");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const selected = await ctx.ui.select("Remove directory", dirs);
|
|
121
|
+
if (!selected) return;
|
|
122
|
+
|
|
123
|
+
const updatedDirs = dirs.filter((d) => d !== selected);
|
|
124
|
+
const updated: AccountConfig = { ...current, dirs: updatedDirs.length > 0 ? updatedDirs : undefined };
|
|
125
|
+
await this.runtime.editAccount(current, updated);
|
|
126
|
+
ctx.ui.notify(`Removed dir: ${selected}`, "info");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async pickDirectory(ctx: AccountSwitcherContext, startPath: string): Promise<string | undefined> {
|
|
132
|
+
let currentPath = startPath;
|
|
133
|
+
while (true) {
|
|
134
|
+
const entries = this.listDirectories(currentPath);
|
|
135
|
+
const displayPath = currentPath.replace(homedir(), "~");
|
|
136
|
+
|
|
137
|
+
const options: Array<{ label: string; value: string }> = [
|
|
138
|
+
{ label: "✅ Select this directory", value: "__select__" },
|
|
139
|
+
{ label: "⬆ ..", value: "__up__" },
|
|
140
|
+
...entries.map((e) => ({ label: `${e}/`, value: e })),
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
const labels = options.map((o) => o.label);
|
|
144
|
+
const selected = await ctx.ui.select(`Navigate: ${displayPath}`, labels);
|
|
145
|
+
if (!selected) return undefined;
|
|
146
|
+
|
|
147
|
+
const option = options.find((o) => o.label === selected);
|
|
148
|
+
if (!option) continue;
|
|
149
|
+
|
|
150
|
+
if (option.value === "__select__") return currentPath;
|
|
151
|
+
if (option.value === "__up__") {
|
|
152
|
+
const parent = dirname(currentPath);
|
|
153
|
+
if (parent === currentPath) continue; // at root, can't go up
|
|
154
|
+
currentPath = parent;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
// Navigate into subdirectory
|
|
158
|
+
currentPath = join(currentPath, option.value);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private listDirectories(dirPath: string): string[] {
|
|
163
|
+
const result: string[] = [];
|
|
164
|
+
try {
|
|
165
|
+
const entries: Dirent[] = require("fs").readdirSync(dirPath, { withFileTypes: true });
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
168
|
+
result.push(entry.name);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
/* permission denied, return what we have */
|
|
173
|
+
}
|
|
174
|
+
return result.sort((a, b) => a.localeCompare(b));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AccountSwitcher } from "@/runtime";
|
|
3
|
+
import { useDirsCommand } from "./dirs";
|
|
4
|
+
|
|
5
|
+
const useDirsCommands = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
6
|
+
useDirsCommand(pi, runtime);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default useDirsCommands;
|
package/src/commands/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ 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";
|
|
5
|
+
import useDirsCommands from "./dirs";
|
|
5
6
|
import useModelCommands from "./models";
|
|
6
7
|
import useSystemCommands from "./system";
|
|
7
8
|
|
|
@@ -10,6 +11,7 @@ export { BaseCommand } from "./base";
|
|
|
10
11
|
|
|
11
12
|
export function registerAllCommands(pi: ExtensionAPI, runtime: AccountSwitcher) {
|
|
12
13
|
useAccountCommands(pi, runtime);
|
|
14
|
+
useDirsCommands(pi, runtime);
|
|
13
15
|
useProviderCommands(pi, runtime);
|
|
14
16
|
useModelCommands(pi, runtime);
|
|
15
17
|
useSystemCommands(pi, runtime);
|
|
@@ -6,7 +6,7 @@ export const COMMANDS = {
|
|
|
6
6
|
},
|
|
7
7
|
list: {
|
|
8
8
|
name: "accounts:list",
|
|
9
|
-
description: "
|
|
9
|
+
description: "(agent tool: use list_accounts tool instead for structured output",
|
|
10
10
|
},
|
|
11
11
|
edit: {
|
|
12
12
|
name: "accounts:edit",
|
|
@@ -29,6 +29,10 @@ export const COMMANDS = {
|
|
|
29
29
|
description:
|
|
30
30
|
"Verify secrets for one or all accounts without activating them (pass 'all'; add 'ping' to send a test request)",
|
|
31
31
|
},
|
|
32
|
+
dirs: {
|
|
33
|
+
name: "accounts:dirs",
|
|
34
|
+
description: "Manage working directories for CWD-based auto-select",
|
|
35
|
+
},
|
|
32
36
|
},
|
|
33
37
|
providers: {
|
|
34
38
|
add: {
|
|
@@ -14,11 +14,41 @@ export const OAUTH_PROVIDER_IDS = [
|
|
|
14
14
|
"anthropic",
|
|
15
15
|
"openai-codex",
|
|
16
16
|
"github-copilot",
|
|
17
|
-
"google-antigravity",
|
|
18
|
-
"custom",
|
|
19
17
|
] as const;
|
|
20
18
|
|
|
21
|
-
export const BUILT_IN_PROVIDER_IDS = [
|
|
19
|
+
export const BUILT_IN_PROVIDER_IDS = [
|
|
20
|
+
"amazon-bedrock",
|
|
21
|
+
"anthropic",
|
|
22
|
+
"azure-openai-responses",
|
|
23
|
+
"cerebras",
|
|
24
|
+
"cloudflare-ai-gateway",
|
|
25
|
+
"cloudflare-workers-ai",
|
|
26
|
+
"deepseek",
|
|
27
|
+
"fireworks",
|
|
28
|
+
"github-copilot",
|
|
29
|
+
"google",
|
|
30
|
+
"google-vertex",
|
|
31
|
+
"groq",
|
|
32
|
+
"huggingface",
|
|
33
|
+
"kimi-coding",
|
|
34
|
+
"minimax",
|
|
35
|
+
"minimax-cn",
|
|
36
|
+
"mistral",
|
|
37
|
+
"moonshotai",
|
|
38
|
+
"moonshotai-cn",
|
|
39
|
+
"openai",
|
|
40
|
+
"openai-codex",
|
|
41
|
+
"opencode",
|
|
42
|
+
"opencode-go",
|
|
43
|
+
"openrouter",
|
|
44
|
+
"vercel-ai-gateway",
|
|
45
|
+
"xai",
|
|
46
|
+
"xiaomi",
|
|
47
|
+
"xiaomi-token-plan-ams",
|
|
48
|
+
"xiaomi-token-plan-cn",
|
|
49
|
+
"xiaomi-token-plan-sgp",
|
|
50
|
+
"zai",
|
|
51
|
+
] as const;
|
|
22
52
|
|
|
23
53
|
export const PROVIDER_ALIASES: Record<string, string> = {
|
|
24
54
|
claude: "anthropic",
|
|
@@ -27,10 +57,35 @@ export const PROVIDER_ALIASES: Record<string, string> = {
|
|
|
27
57
|
};
|
|
28
58
|
|
|
29
59
|
export const PROVIDER_ENV_KEYS: Record<string, string[]> = {
|
|
30
|
-
anthropic: ["ANTHROPIC_API_KEY"],
|
|
60
|
+
anthropic: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
|
31
61
|
openai: ["OPENAI_API_KEY"],
|
|
32
62
|
"openai-codex": ["OPENAI_API_KEY"],
|
|
33
|
-
google: ["
|
|
63
|
+
google: ["GEMINI_API_KEY"],
|
|
64
|
+
"google-vertex": ["GOOGLE_CLOUD_API_KEY"],
|
|
65
|
+
"azure-openai-responses": ["AZURE_OPENAI_API_KEY"],
|
|
66
|
+
deepseek: ["DEEPSEEK_API_KEY"],
|
|
67
|
+
groq: ["GROQ_API_KEY"],
|
|
68
|
+
cerebras: ["CEREBRAS_API_KEY"],
|
|
34
69
|
xai: ["XAI_API_KEY"],
|
|
35
70
|
openrouter: ["OPENROUTER_API_KEY"],
|
|
71
|
+
"vercel-ai-gateway": ["AI_GATEWAY_API_KEY"],
|
|
72
|
+
zai: ["ZAI_API_KEY"],
|
|
73
|
+
mistral: ["MISTRAL_API_KEY"],
|
|
74
|
+
minimax: ["MINIMAX_API_KEY"],
|
|
75
|
+
"minimax-cn": ["MINIMAX_CN_API_KEY"],
|
|
76
|
+
moonshotai: ["MOONSHOT_API_KEY"],
|
|
77
|
+
"moonshotai-cn": ["MOONSHOT_API_KEY"],
|
|
78
|
+
huggingface: ["HF_TOKEN"],
|
|
79
|
+
fireworks: ["FIREWORKS_API_KEY"],
|
|
80
|
+
opencode: ["OPENCODE_API_KEY"],
|
|
81
|
+
"opencode-go": ["OPENCODE_API_KEY"],
|
|
82
|
+
"kimi-coding": ["KIMI_API_KEY"],
|
|
83
|
+
"cloudflare-workers-ai": ["CLOUDFLARE_API_KEY"],
|
|
84
|
+
"cloudflare-ai-gateway": ["CLOUDFLARE_API_KEY"],
|
|
85
|
+
xiaomi: ["XIAOMI_API_KEY"],
|
|
86
|
+
"xiaomi-token-plan-cn": ["XIAOMI_TOKEN_PLAN_CN_API_KEY"],
|
|
87
|
+
"xiaomi-token-plan-ams": ["XIAOMI_TOKEN_PLAN_AMS_API_KEY"],
|
|
88
|
+
"xiaomi-token-plan-sgp": ["XIAOMI_TOKEN_PLAN_SGP_API_KEY"],
|
|
89
|
+
"github-copilot": ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"],
|
|
90
|
+
"amazon-bedrock": [],
|
|
36
91
|
};
|
package/src/extension.ts
CHANGED
|
@@ -1,15 +1,47 @@
|
|
|
1
1
|
import { createJiti } from "@mariozechner/jiti";
|
|
2
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
3
4
|
import { dirname } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
|
|
6
7
|
export default async function accountSwitcherBootstrap(pi: ExtensionAPI) {
|
|
7
8
|
const srcDir = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
|
|
10
|
+
// Resolve @earendil-works/* from the pi loader's context so the same
|
|
11
|
+
// module instances are used (avoids duplicate singletons / instanceof mismatches).
|
|
12
|
+
const loaderRequire = createRequire(import.meta.url);
|
|
13
|
+
const resolveOrUndefined = (id: string): string | undefined => {
|
|
14
|
+
try {
|
|
15
|
+
return loaderRequire.resolve(id);
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const piAiEntry = resolveOrUndefined("@earendil-works/pi-ai");
|
|
22
|
+
const piCodingAgentEntry = resolveOrUndefined("@earendil-works/pi-coding-agent");
|
|
23
|
+
const piAgentCoreEntry = resolveOrUndefined("@earendil-works/pi-agent-core");
|
|
24
|
+
const piTuiEntry = resolveOrUndefined("@earendil-works/pi-tui");
|
|
25
|
+
|
|
26
|
+
const alias: Record<string, string> = { "@": srcDir };
|
|
27
|
+
if (piAiEntry) {
|
|
28
|
+
alias["@earendil-works/pi-ai"] = piAiEntry;
|
|
29
|
+
alias["@mariozechner/pi-ai"] = piAiEntry;
|
|
30
|
+
}
|
|
31
|
+
if (piCodingAgentEntry) {
|
|
32
|
+
alias["@earendil-works/pi-coding-agent"] = piCodingAgentEntry;
|
|
33
|
+
alias["@mariozechner/pi-coding-agent"] = piCodingAgentEntry;
|
|
34
|
+
}
|
|
35
|
+
if (piAgentCoreEntry) {
|
|
36
|
+
alias["@earendil-works/pi-agent-core"] = piAgentCoreEntry;
|
|
37
|
+
alias["@mariozechner/pi-agent-core"] = piAgentCoreEntry;
|
|
38
|
+
}
|
|
39
|
+
if (piTuiEntry) {
|
|
40
|
+
alias["@earendil-works/pi-tui"] = piTuiEntry;
|
|
41
|
+
alias["@mariozechner/pi-tui"] = piTuiEntry;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const jiti = createJiti(import.meta.url, { alias });
|
|
13
45
|
|
|
14
46
|
const extension = await jiti.import<(pi: ExtensionAPI) => void | Promise<void>>("./index", {
|
|
15
47
|
default: true,
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcherContext } from "@/types";
|
|
3
|
-
import { useAccountSwitcher, type AccountSwitcher } from "
|
|
4
|
-
import { registerAllCommands } from "
|
|
3
|
+
import { useAccountSwitcher, type AccountSwitcher } from "@/runtime";
|
|
4
|
+
import { registerAllCommands } from "@/commands";
|
|
5
5
|
|
|
6
6
|
async function accountSwitcher(pi: ExtensionAPI) {
|
|
7
7
|
const runtime: AccountSwitcher = useAccountSwitcher(pi);
|
|
@@ -10,6 +10,12 @@ async function accountSwitcher(pi: ExtensionAPI) {
|
|
|
10
10
|
await runtime.init(ctx as AccountSwitcherContext);
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
+
// Re-assert account status on agent/turn lifecycle so it persists
|
|
14
|
+
// across TUI redraws, reloads, and powerline updates.
|
|
15
|
+
pi.on("agent_start", async (_, ctx) => {
|
|
16
|
+
runtime.refreshStatus(ctx as AccountSwitcherContext);
|
|
17
|
+
});
|
|
18
|
+
|
|
13
19
|
pi.on("model_select", async (event, ctx) => {
|
|
14
20
|
await runtime.onModelSelect(event.model.provider, ctx as AccountSwitcherContext);
|
|
15
21
|
});
|