@hieplp/pi-account-switcher 0.2.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.
Files changed (77) hide show
  1. package/INSTALL_AS_PI_PACKAGE.md +78 -0
  2. package/LICENSE +21 -0
  3. package/README.md +214 -0
  4. package/USAGE.md +446 -0
  5. package/package.json +53 -0
  6. package/src/commands/accounts/add.ts +63 -0
  7. package/src/commands/accounts/edit.ts +31 -0
  8. package/src/commands/accounts/index.ts +19 -0
  9. package/src/commands/accounts/list.ts +31 -0
  10. package/src/commands/accounts/oauth.ts +59 -0
  11. package/src/commands/accounts/remove.ts +40 -0
  12. package/src/commands/accounts/shared/base.ts +88 -0
  13. package/src/commands/accounts/shared/index.ts +3 -0
  14. package/src/commands/accounts/shared/prompts.ts +226 -0
  15. package/src/commands/accounts/shared/select.ts +37 -0
  16. package/src/commands/accounts/switch.ts +54 -0
  17. package/src/commands/base.ts +81 -0
  18. package/src/commands/index.ts +16 -0
  19. package/src/commands/models/add.ts +30 -0
  20. package/src/commands/models/index.ts +13 -0
  21. package/src/commands/models/list.ts +45 -0
  22. package/src/commands/models/remove.ts +41 -0
  23. package/src/commands/models/shared/base.ts +44 -0
  24. package/src/commands/models/shared/index.ts +3 -0
  25. package/src/commands/models/shared/prompts.ts +36 -0
  26. package/src/commands/models/shared/select.ts +37 -0
  27. package/src/commands/providers/add.ts +28 -0
  28. package/src/commands/providers/edit.ts +29 -0
  29. package/src/commands/providers/index.ts +15 -0
  30. package/src/commands/providers/list.ts +38 -0
  31. package/src/commands/providers/remove.ts +46 -0
  32. package/src/commands/providers/shared/base.ts +30 -0
  33. package/src/commands/providers/shared/index.ts +3 -0
  34. package/src/commands/providers/shared/prompts.ts +172 -0
  35. package/src/commands/providers/shared/select.ts +24 -0
  36. package/src/commands/system/index.ts +7 -0
  37. package/src/commands/system/reset.ts +36 -0
  38. package/src/constants/commands.ts +66 -0
  39. package/src/constants/config.ts +6 -0
  40. package/src/constants/index.ts +4 -0
  41. package/src/constants/paths.ts +8 -0
  42. package/src/constants/providers.ts +36 -0
  43. package/src/extension.ts +21 -0
  44. package/src/index.ts +20 -0
  45. package/src/runtime/account-switcher-runtime.ts +194 -0
  46. package/src/runtime/account-switcher.ts +32 -0
  47. package/src/runtime/index.ts +11 -0
  48. package/src/schemas/accounts.ts +50 -0
  49. package/src/schemas/common.ts +3 -0
  50. package/src/schemas/config.ts +7 -0
  51. package/src/schemas/index.ts +4 -0
  52. package/src/schemas/providers.ts +57 -0
  53. package/src/services/accounts.ts +116 -0
  54. package/src/services/index.ts +4 -0
  55. package/src/services/models.ts +27 -0
  56. package/src/services/pi-auth.ts +23 -0
  57. package/src/services/providers.ts +123 -0
  58. package/src/storage/accounts.ts +109 -0
  59. package/src/storage/index.ts +4 -0
  60. package/src/storage/paths.ts +8 -0
  61. package/src/storage/pi-auth.ts +37 -0
  62. package/src/storage/providers.ts +85 -0
  63. package/src/storage/state.ts +43 -0
  64. package/src/types/accounts.ts +37 -0
  65. package/src/types/config.ts +6 -0
  66. package/src/types/context.ts +3 -0
  67. package/src/types/index.ts +4 -0
  68. package/src/types/providers.ts +53 -0
  69. package/src/utils/accounts.ts +99 -0
  70. package/src/utils/common.ts +74 -0
  71. package/src/utils/errors.ts +16 -0
  72. package/src/utils/files.ts +25 -0
  73. package/src/utils/filterable-selector.ts +114 -0
  74. package/src/utils/index.ts +7 -0
  75. package/src/utils/models.ts +76 -0
  76. package/src/utils/providers.ts +49 -0
  77. package/src/utils/ui.ts +49 -0
@@ -0,0 +1,36 @@
1
+ export const PROVIDER_API_TYPES = [
2
+ "openai-completions",
3
+ "openai-responses",
4
+ "anthropic-messages",
5
+ "azure-openai-responses",
6
+ "openai-codex-responses",
7
+ "mistral-conversations",
8
+ "google-generative-ai",
9
+ "google-vertex",
10
+ "bedrock-converse-stream",
11
+ ] as const;
12
+
13
+ export const OAUTH_PROVIDER_IDS = [
14
+ "anthropic",
15
+ "openai-codex",
16
+ "github-copilot",
17
+ "google-antigravity",
18
+ "custom",
19
+ ] as const;
20
+
21
+ export const BUILT_IN_PROVIDER_IDS = ["anthropic", "openai", "openai-codex", "google", "xai", "openrouter"] as const;
22
+
23
+ export const PROVIDER_ALIASES: Record<string, string> = {
24
+ claude: "anthropic",
25
+ codex: "openai-codex",
26
+ gemini: "google",
27
+ };
28
+
29
+ export const PROVIDER_ENV_KEYS: Record<string, string[]> = {
30
+ anthropic: ["ANTHROPIC_API_KEY"],
31
+ openai: ["OPENAI_API_KEY"],
32
+ "openai-codex": ["OPENAI_API_KEY"],
33
+ google: ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
34
+ xai: ["XAI_API_KEY"],
35
+ openrouter: ["OPENROUTER_API_KEY"],
36
+ };
@@ -0,0 +1,21 @@
1
+ import { createJiti } from "@mariozechner/jiti";
2
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
+ import { dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ export default async function accountSwitcherBootstrap(pi: ExtensionAPI) {
7
+ const srcDir = dirname(fileURLToPath(import.meta.url));
8
+ const jiti = createJiti(import.meta.url, {
9
+ alias: {
10
+ "@": srcDir,
11
+ },
12
+ });
13
+
14
+ const extension = await jiti.import<
15
+ (pi: ExtensionAPI) => void | Promise<void>
16
+ >("./index", {
17
+ default: true,
18
+ });
19
+
20
+ await extension(pi);
21
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { AccountSwitcherContext } from "@/types";
3
+ import { useAccountSwitcher, type AccountSwitcher } from "./runtime";
4
+ import { registerAllCommands } from "./commands";
5
+
6
+ async function accountSwitcher(pi: ExtensionAPI) {
7
+ const runtime: AccountSwitcher = useAccountSwitcher(pi);
8
+
9
+ pi.on("session_start", async (_, ctx) => {
10
+ await runtime.init(ctx as AccountSwitcherContext);
11
+ });
12
+
13
+ pi.on("model_select", async (event, ctx) => {
14
+ await runtime.onModelSelect(event.model.provider, ctx as AccountSwitcherContext);
15
+ });
16
+
17
+ registerAllCommands(pi, runtime);
18
+ }
19
+
20
+ export default accountSwitcher;
@@ -0,0 +1,194 @@
1
+ import AccountSwitcher from "./account-switcher";
2
+ import { ACCOUNTS_PATH, PROVIDERS_PATH, STATE_PATH } from "@/constants";
3
+ import type { Api, Model } from "@mariozechner/pi-ai";
4
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
5
+ import type { AccountConfig, AccountSwitcherContext, PiAuthEntry, ProviderConfig } from "@/types";
6
+ import type { AccountService, ModelService, PiAuthService, ProviderService } from "@/services";
7
+ import { useAccountService, useModelService, usePiAuthService, useProviderService } from "@/services";
8
+ import { accountUtil, modelUtil, providerUtil, uiUtil } from "@/utils";
9
+
10
+ function resolveAuthProvider(account: AccountConfig, providers: ProviderConfig[]): string {
11
+ if (account.piAuth?.provider) return account.piAuth.provider;
12
+ const provider = providerUtil.findProvider(account.provider, providers);
13
+ return provider?.piAuthProvider ?? providerUtil.normalizeProvider(account.provider);
14
+ }
15
+
16
+ export default class AccountSwitcherRuntime implements AccountSwitcher {
17
+ private accountService: AccountService;
18
+ private modelService: ModelService;
19
+ private piAuthService: PiAuthService;
20
+ private providerService: ProviderService;
21
+
22
+ constructor(private readonly pi: Pick<ExtensionAPI, "registerProvider" | "setModel">) {
23
+ this.providerService = useProviderService(this.pi as ExtensionAPI, PROVIDERS_PATH);
24
+ this.accountService = useAccountService(ACCOUNTS_PATH, STATE_PATH);
25
+ this.modelService = useModelService(this.pi);
26
+ this.piAuthService = usePiAuthService();
27
+ }
28
+
29
+ // ===============================================================================================
30
+ // Core
31
+ // ===============================================================================================
32
+
33
+ async init(ctx: AccountSwitcherContext): Promise<void> {
34
+ await this.load();
35
+
36
+ const active = this.accountService.getActiveAccount();
37
+ uiUtil.setAccountStatus(ctx.ui, active?.label);
38
+
39
+ // Re-apply saved account credentials so env vars and OAuth auth storage are
40
+ // populated on session start, not only after the first explicit switch.
41
+ if (active) {
42
+ const providers = this.providerService.getProviders();
43
+ await this.applyProviderApiKey(active, providers);
44
+ await accountUtil.applyAccountEnv(active, ctx.modelRegistry, resolveAuthProvider(active, providers));
45
+ }
46
+
47
+ // Restore the last active model. modelRegistry.find returns undefined if the
48
+ // model is no longer available (e.g. provider was removed), in which case we
49
+ // leave Pi's default model selection untouched.
50
+ const modelState = this.accountService.getActiveModelState();
51
+ if (modelState) {
52
+ const model = ctx.modelRegistry.find(modelState.provider, modelState.id);
53
+ if (model) await this.modelService.applyModel(model, ctx);
54
+ }
55
+ }
56
+
57
+ async load(): Promise<void> {
58
+ await this.accountService.load();
59
+ await this.providerService.load();
60
+ }
61
+
62
+ async onModelSelect(provider: string, ctx: AccountSwitcherContext): Promise<void> {
63
+ const matchingAccount = this.findAccountsByProvider(provider)[0];
64
+ const activeAccount = this.accountService.getActiveAccount();
65
+ if (matchingAccount && matchingAccount.id !== activeAccount?.id) {
66
+ await this.activateAccount(matchingAccount, ctx);
67
+ }
68
+ }
69
+
70
+ // ===============================================================================================
71
+ // Pi Auth
72
+ // ===============================================================================================
73
+
74
+ async getPiAuthEntry(provider: string): Promise<PiAuthEntry | undefined> {
75
+ return this.piAuthService.getEntry(provider);
76
+ }
77
+
78
+ isOAuthEntry(entry: PiAuthEntry | undefined): boolean {
79
+ return this.piAuthService.isOAuthEntry(entry);
80
+ }
81
+
82
+ // ===============================================================================================
83
+ // Account
84
+ // ===============================================================================================
85
+
86
+ getAccounts(): AccountConfig[] {
87
+ return this.accountService.getAccounts();
88
+ }
89
+
90
+ findAccountById(id: string): AccountConfig | undefined {
91
+ return this.accountService.getAccounts().find((a) => a.id === id);
92
+ }
93
+
94
+ findAccountsByProvider(provider: string): AccountConfig[] {
95
+ const providers = this.providerService.getProviders();
96
+ const normalized = providerUtil.normalizeProviderWithCustom(provider, providers);
97
+ return this.accountService.getAccounts().filter((a) => {
98
+ const accountProvider = providerUtil.normalizeProviderWithCustom(resolveAuthProvider(a, providers), providers);
99
+ return accountProvider === normalized;
100
+ });
101
+ }
102
+
103
+ getActiveAccount(): AccountConfig | undefined {
104
+ return this.accountService.getActiveAccount();
105
+ }
106
+
107
+ async addAccount(account: AccountConfig): Promise<void> {
108
+ return this.accountService.addAccount(account);
109
+ }
110
+
111
+ async editAccount(original: AccountConfig, updated: AccountConfig): Promise<void> {
112
+ return this.accountService.editAccount(original, updated);
113
+ }
114
+
115
+ async removeAccount(account: AccountConfig): Promise<void> {
116
+ return this.accountService.removeAccount(account);
117
+ }
118
+
119
+ async activateAccount(account: AccountConfig, ctx: AccountSwitcherContext): Promise<string> {
120
+ const providers = this.providerService.getProviders();
121
+ const providerApiKey = await this.applyProviderApiKey(account, providers);
122
+ const result = await this.accountService.activateAccount(account, ctx, resolveAuthProvider(account, providers));
123
+
124
+ // piAuth accounts authenticate via a separate provider (e.g. github-copilot),
125
+ // so use that for model lookup rather than the account's own provider field.
126
+ const accountProvider = providerUtil.normalizeProviderWithCustom(
127
+ resolveAuthProvider(account, providers),
128
+ providers,
129
+ );
130
+ const currentProvider = ctx.model
131
+ ? providerUtil.normalizeProviderWithCustom(ctx.model.provider, providers)
132
+ : undefined;
133
+
134
+ // Skip model selection if the active model already belongs to the same provider.
135
+ if (accountProvider !== currentProvider) {
136
+ const model = await modelUtil.pickModel(ctx, account, providers);
137
+ if (model) await this.applyModel(model, ctx);
138
+ }
139
+
140
+ return providerApiKey ? `provider apiKey (${providerApiKey})` : result;
141
+ }
142
+
143
+ private async applyProviderApiKey(account: AccountConfig, providers: ProviderConfig[]): Promise<string | undefined> {
144
+ if (!account.providerApiKey && !account.usesProviderApiKey) return undefined;
145
+
146
+ const provider = providerUtil.findProvider(account.provider, providers);
147
+ if (!provider) throw new Error(`Custom provider not found for account ${account.id}: ${account.provider}`);
148
+
149
+ if (account.providerApiKey) {
150
+ const apiKey = await accountUtil.resolveSecret(account.providerApiKey);
151
+ if (!apiKey) throw new Error(`Resolved empty providerApiKey for account ${account.id}`);
152
+ this.providerService.registerProvider({ ...provider, apiKey });
153
+ return provider.id;
154
+ }
155
+
156
+ this.providerService.registerProvider(provider);
157
+ return provider.id;
158
+ }
159
+
160
+ // ===============================================================================================
161
+ // Model
162
+ // ===============================================================================================
163
+
164
+ async applyModel(model: Model<Api>, ctx: AccountSwitcherContext): Promise<void> {
165
+ await this.modelService.applyModel(model, ctx);
166
+ await this.accountService.saveActiveModel(model.id, model.provider);
167
+ }
168
+
169
+ // ===============================================================================================
170
+ // Provider
171
+ // ===============================================================================================
172
+
173
+ getProviders(): ProviderConfig[] {
174
+ return this.providerService.getProviders();
175
+ }
176
+
177
+ async addProvider(provider: ProviderConfig): Promise<void> {
178
+ return this.providerService.addProvider(provider);
179
+ }
180
+
181
+ async editProvider(original: ProviderConfig, updated: ProviderConfig): Promise<void> {
182
+ return this.providerService.editProvider(original, updated);
183
+ }
184
+
185
+ async removeProvider(provider: ProviderConfig): Promise<void> {
186
+ const providers = this.providerService.getProviders();
187
+ const dependents = this.accountService.findAccountsByProvider(provider.id, providers);
188
+ if (dependents.length > 0) {
189
+ const names = dependents.map((a) => `"${a.label ?? a.id}"`).join(", ");
190
+ throw new Error(`Cannot remove: ${names} ${dependents.length === 1 ? "uses" : "use"} this provider`);
191
+ }
192
+ return this.providerService.removeProvider(provider);
193
+ }
194
+ }
@@ -0,0 +1,32 @@
1
+ import type { Api, Model } from "@mariozechner/pi-ai";
2
+ import type { AccountConfig, AccountSwitcherContext, PiAuthEntry, ProviderConfig } from "@/types";
3
+
4
+ export default interface AccountSwitcher {
5
+ // Core
6
+ init(ctx: AccountSwitcherContext): Promise<void>;
7
+ load(): Promise<void>;
8
+ onModelSelect(provider: string, ctx: AccountSwitcherContext): Promise<void>;
9
+
10
+ // Pi Auth
11
+ getPiAuthEntry(provider: string): Promise<PiAuthEntry | undefined>;
12
+ isOAuthEntry(entry: PiAuthEntry | undefined): boolean;
13
+
14
+ // Account
15
+ getAccounts(): AccountConfig[];
16
+ findAccountById(id: string): AccountConfig | undefined;
17
+ findAccountsByProvider(provider: string): AccountConfig[];
18
+ getActiveAccount(): AccountConfig | undefined;
19
+ addAccount(account: AccountConfig): Promise<void>;
20
+ editAccount(original: AccountConfig, updated: AccountConfig): Promise<void>;
21
+ removeAccount(account: AccountConfig): Promise<void>;
22
+ activateAccount(account: AccountConfig, ctx: AccountSwitcherContext): Promise<string>;
23
+
24
+ // Model
25
+ applyModel(model: Model<Api>, ctx: AccountSwitcherContext): Promise<void>;
26
+
27
+ // Provider
28
+ getProviders(): ProviderConfig[];
29
+ addProvider(config: ProviderConfig): Promise<void>;
30
+ editProvider(original: ProviderConfig, updated: ProviderConfig): Promise<void>;
31
+ removeProvider(provider: ProviderConfig): Promise<void>;
32
+ }
@@ -0,0 +1,11 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type AccountSwitcher from "./account-switcher";
3
+ import AccountSwitcherRuntime from "./account-switcher-runtime";
4
+
5
+ function useAccountSwitcher(
6
+ pi: Pick<ExtensionAPI, "registerProvider" | "setModel">,
7
+ ): AccountSwitcher {
8
+ return new AccountSwitcherRuntime(pi);
9
+ }
10
+
11
+ export { useAccountSwitcher, type AccountSwitcher };
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+
3
+ export const secretSourceSchema = z.union([
4
+ z.string().min(1),
5
+ z.object({ type: z.literal("literal"), value: z.string().min(1) }),
6
+ z.object({ type: z.literal("env"), name: z.string().min(1) }),
7
+ z.object({ type: z.literal("file"), path: z.string().min(1) }),
8
+ z.object({ type: z.literal("command"), command: z.string().min(1) }),
9
+ z.object({ type: z.literal("op"), reference: z.string().min(1) }),
10
+ ]);
11
+
12
+ export const piAuthEntrySchema = z.union([
13
+ z.object({ type: z.literal("api_key"), key: z.string().min(1) }),
14
+ z
15
+ .object({
16
+ type: z.literal("oauth"),
17
+ refresh: z.string().min(1),
18
+ access: z.string().min(1),
19
+ expires: z.number(),
20
+ })
21
+ .passthrough(),
22
+ ]);
23
+
24
+ export const accountSchema = z
25
+ .object({
26
+ id: z.string().min(1),
27
+ label: z.string().min(1),
28
+ provider: z.string().min(1),
29
+ model: z.string().min(1).optional(),
30
+ env: z.record(z.string().min(1), secretSourceSchema).optional(),
31
+ providerApiKey: secretSourceSchema.optional(),
32
+ usesProviderApiKey: z.boolean().optional(),
33
+ piAuth: z
34
+ .object({
35
+ provider: z.string().min(1),
36
+ entry: piAuthEntrySchema,
37
+ })
38
+ .optional(),
39
+ })
40
+ .refine(
41
+ (account) =>
42
+ (account.env && Object.keys(account.env).length > 0) ||
43
+ account.providerApiKey ||
44
+ account.usesProviderApiKey ||
45
+ account.piAuth,
46
+ {
47
+ message:
48
+ "Account must define env credentials, providerApiKey, provider apiKey, or piAuth credentials",
49
+ },
50
+ );
@@ -0,0 +1,3 @@
1
+ import z from "zod";
2
+
3
+ export const jsonRecordSchema = z.record(z.string(), z.unknown());
@@ -0,0 +1,7 @@
1
+ import z from "zod";
2
+ import { accountSchema } from "./accounts";
3
+
4
+ export const configSchema = z.object({
5
+ accounts: z.array(accountSchema).default([]),
6
+ switchMode: z.literal("env").optional().default("env"),
7
+ });
@@ -0,0 +1,4 @@
1
+ export * from "./common";
2
+ export * from "./accounts";
3
+ export * from "./config";
4
+ export * from "./providers";
@@ -0,0 +1,57 @@
1
+ import z from "zod";
2
+ import { jsonRecordSchema } from "@/schemas/common";
3
+
4
+ export const providerModelSchema = z
5
+ .object({
6
+ id: z.string().min(1),
7
+ name: z.string().min(1).optional(),
8
+ api: z.string().min(1).optional(),
9
+ baseUrl: z.string().min(1).optional(),
10
+ reasoning: z.boolean().optional(),
11
+ input: z.array(z.enum(["text", "image"])).optional(),
12
+ contextWindow: z.number().int().positive().optional(),
13
+ maxTokens: z.number().int().positive().optional(),
14
+ cost: z
15
+ .object({
16
+ input: z.number(),
17
+ output: z.number(),
18
+ cacheRead: z.number(),
19
+ cacheWrite: z.number(),
20
+ })
21
+ .optional(),
22
+ compat: jsonRecordSchema.optional(),
23
+ thinkingLevelMap: z
24
+ .record(z.string(), z.union([z.string(), z.null()]))
25
+ .optional(),
26
+ headers: z.record(z.string(), z.string()).optional(),
27
+ })
28
+ .passthrough();
29
+
30
+ export const providerSchema = z
31
+ .object({
32
+ id: z.string().min(1).optional(),
33
+ label: z.string().min(1).optional(),
34
+ name: z.string().min(1).optional(),
35
+ envKeys: z.array(z.string().min(1)).optional(),
36
+ aliases: z.array(z.string().min(1)).optional(),
37
+ piAuthProvider: z.string().min(1).optional(),
38
+ baseUrl: z.string().min(1).optional(),
39
+ api: z.string().min(1).optional(),
40
+ apiKey: z.string().min(1).optional(),
41
+ headers: z.record(z.string(), z.string()).optional(),
42
+ authHeader: z.boolean().optional(),
43
+ compat: jsonRecordSchema.optional(),
44
+ models: z.array(providerModelSchema).optional(),
45
+ modelOverrides: z
46
+ .record(z.string(), providerModelSchema.partial())
47
+ .optional(),
48
+ })
49
+ .passthrough();
50
+
51
+ export const providerCatalogRecordSchema = z.object({
52
+ providers: z.record(z.string().min(1), providerSchema).default({}),
53
+ });
54
+
55
+ export const providerCatalogArraySchema = z.object({
56
+ providers: z.array(providerSchema.extend({ id: z.string().min(1) })).default([]),
57
+ });
@@ -0,0 +1,116 @@
1
+ import { type AccountStore, useAccountStore, type StateStore, useStateStore } from "@/storage";
2
+ import type { AccountConfig, AccountSwitcherContext, ProviderConfig } from "@/types";
3
+ import { accountUtil, providerUtil, uiUtil } from "@/utils";
4
+
5
+ export interface AccountService {
6
+ load(): Promise<void>;
7
+ getAccounts(): AccountConfig[];
8
+ findAccountsByProvider(provider: string, providers: ProviderConfig[]): AccountConfig[];
9
+ getActiveAccount(): AccountConfig | undefined;
10
+ addAccount(account: AccountConfig): Promise<void>;
11
+ editAccount(original: AccountConfig, updated: AccountConfig): Promise<void>;
12
+ removeAccount(account: AccountConfig): Promise<void>;
13
+ activateAccount(account: AccountConfig, ctx: AccountSwitcherContext, authProvider?: string): Promise<string>;
14
+ getActiveModelState(): { id: string; provider: string } | undefined;
15
+ saveActiveModel(id: string, provider: string): Promise<void>;
16
+ }
17
+
18
+ export function useAccountService(accountsPath: string, statePath?: string): AccountService {
19
+ return new AccountServiceImpl(useAccountStore(accountsPath), useStateStore(statePath));
20
+ }
21
+
22
+ // ===============================================================================================
23
+ // Account Service
24
+ // ===============================================================================================
25
+
26
+ class AccountServiceImpl implements AccountService {
27
+ private accounts: AccountConfig[] = [];
28
+ private activeAccountId: string | undefined;
29
+ private activeModelId: string | undefined;
30
+ private activeModelProvider: string | undefined;
31
+
32
+ constructor(
33
+ private readonly store: AccountStore,
34
+ private readonly stateStore: StateStore,
35
+ ) {}
36
+
37
+ async load(): Promise<void> {
38
+ this.accounts = await this.store.load();
39
+ const state = await this.stateStore.load();
40
+ this.activeAccountId = state.activeAccountId;
41
+ this.activeModelId = state.activeModelId;
42
+ this.activeModelProvider = state.activeModelProvider;
43
+ }
44
+
45
+ getAccounts(): AccountConfig[] {
46
+ return this.accounts;
47
+ }
48
+
49
+ findAccountsByProvider(provider: string, providers: ProviderConfig[]): AccountConfig[] {
50
+ const normalized = providerUtil.normalizeProviderWithCustom(provider, providers);
51
+ return this.accounts.filter(
52
+ (account) => providerUtil.normalizeProviderWithCustom(account.provider, providers) === normalized,
53
+ );
54
+ }
55
+
56
+ getActiveAccount(): AccountConfig | undefined {
57
+ return this.accounts.find((a) => a.id === this.activeAccountId);
58
+ }
59
+
60
+ getActiveModelState(): { id: string; provider: string } | undefined {
61
+ if (!this.activeModelId || !this.activeModelProvider) return undefined;
62
+ return { id: this.activeModelId, provider: this.activeModelProvider };
63
+ }
64
+
65
+ async saveActiveModel(id: string, provider: string): Promise<void> {
66
+ this.activeModelId = id;
67
+ this.activeModelProvider = provider;
68
+ await this.flushState();
69
+ }
70
+
71
+ async addAccount(account: AccountConfig): Promise<void> {
72
+ this.accounts = await this.store.addAccount(account);
73
+ }
74
+
75
+ async editAccount(original: AccountConfig, updated: AccountConfig): Promise<void> {
76
+ this.accounts = await this.store.replaceAccount(original.id, updated);
77
+ if (this.activeAccountId === original.id) {
78
+ this.activeAccountId = updated.id;
79
+ await this.flushState();
80
+ }
81
+ }
82
+
83
+ async removeAccount(account: AccountConfig): Promise<void> {
84
+ this.accounts = await this.store.removeAccount(account.id);
85
+ if (this.activeAccountId === account.id) {
86
+ this.activeAccountId = undefined;
87
+ await this.flushState();
88
+ }
89
+ }
90
+
91
+ async activateAccount(account: AccountConfig, ctx: AccountSwitcherContext, authProvider?: string): Promise<string> {
92
+ const previous = this.getActiveAccount();
93
+ let applied: string[] = [];
94
+ if (account.piAuth) {
95
+ if (previous) await accountUtil.clearAccountEnv(previous, ctx.modelRegistry);
96
+ applied = await accountUtil.applyAccountEnv(account, ctx.modelRegistry, authProvider);
97
+ } else {
98
+ const resolved = await accountUtil.resolveAccountEnv(account);
99
+ if (previous) await accountUtil.clearAccountEnv(previous, ctx.modelRegistry);
100
+ applied = accountUtil.applyResolvedAccountEnv(account, resolved, ctx.modelRegistry, authProvider);
101
+ }
102
+ this.activeAccountId = account.id;
103
+ await this.flushState();
104
+ uiUtil.setAccountStatus(ctx.ui, account.label);
105
+ if (account.piAuth) return "via OAuth";
106
+ return applied.length > 0 ? applied.join(", ") : "";
107
+ }
108
+
109
+ private async flushState(): Promise<void> {
110
+ await this.stateStore.save({
111
+ activeAccountId: this.activeAccountId,
112
+ activeModelId: this.activeModelId,
113
+ activeModelProvider: this.activeModelProvider,
114
+ });
115
+ }
116
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./accounts";
2
+ export * from "./models";
3
+ export * from "./pi-auth";
4
+ export * from "./providers";
@@ -0,0 +1,27 @@
1
+ import type { Api, Model } from "@mariozechner/pi-ai";
2
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
+ import type { AccountSwitcherContext } from "@/types";
4
+
5
+ type ProviderModel = Model<Api>;
6
+
7
+ export interface ModelService {
8
+ applyModel(model: ProviderModel, ctx: AccountSwitcherContext): Promise<void>;
9
+ }
10
+
11
+ export function useModelService(pi: Pick<ExtensionAPI, "setModel">): ModelService {
12
+ return new ModelServiceImpl(pi);
13
+ }
14
+
15
+ class ModelServiceImpl implements ModelService {
16
+ constructor(private readonly pi: Pick<ExtensionAPI, "setModel">) {}
17
+
18
+ async applyModel(model: ProviderModel, ctx: AccountSwitcherContext): Promise<void> {
19
+ const ok = await this.pi.setModel(model);
20
+ if (!ok) {
21
+ ctx.ui.notify(
22
+ `Account switched, but Pi refused model ${model.provider}/${model.id}. Check credentials.`,
23
+ "warning",
24
+ );
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,23 @@
1
+ import { type PiAuthStore, usePiAuthStore, isOAuthEntry } from "@/storage";
2
+ import type { PiAuthEntry } from "@/types";
3
+
4
+ export interface PiAuthService {
5
+ getEntry(provider: string): Promise<PiAuthEntry | undefined>;
6
+ isOAuthEntry(entry: PiAuthEntry | undefined): boolean;
7
+ }
8
+
9
+ export function usePiAuthService(path?: string): PiAuthService {
10
+ return new PiAuthServiceImpl(usePiAuthStore(path));
11
+ }
12
+
13
+ class PiAuthServiceImpl implements PiAuthService {
14
+ constructor(private readonly store: PiAuthStore) {}
15
+
16
+ async getEntry(provider: string): Promise<PiAuthEntry | undefined> {
17
+ return this.store.getEntry(provider);
18
+ }
19
+
20
+ isOAuthEntry(entry: PiAuthEntry | undefined): boolean {
21
+ return isOAuthEntry(entry);
22
+ }
23
+ }