@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,44 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { AccountSwitcher } from "@/runtime";
3
+ import type { AccountSwitcherContext, ProviderConfig } from "@/types";
4
+ import { BaseCommand, type CommandMeta } from "../../base";
5
+ import { providerUtil } from "@/utils";
6
+ import type { ProviderModel } from "./select";
7
+
8
+ export abstract class ModelCommand extends BaseCommand {
9
+ constructor(pi: ExtensionAPI, runtime: AccountSwitcher, meta: CommandMeta) {
10
+ super(pi, runtime, meta);
11
+ }
12
+
13
+ protected async loadProvider(ctx: AccountSwitcherContext): Promise<ProviderConfig | undefined> {
14
+ await this.runtime.load();
15
+
16
+ const provider = ctx.model?.provider;
17
+ if (!provider) {
18
+ ctx.ui.notify("No active model. Use models:list to select one first.", "info");
19
+ return undefined;
20
+ }
21
+
22
+ const config = providerUtil.findProvider(provider, this.runtime.getProviders());
23
+ if (!config) {
24
+ ctx.ui.notify(`"${provider}" is a built-in provider. This command only works with custom providers.`, "info");
25
+ return undefined;
26
+ }
27
+
28
+ return config;
29
+ }
30
+
31
+ protected getModels(ctx: AccountSwitcherContext, provider: string): ProviderModel[] {
32
+ const providers = this.runtime.getProviders();
33
+ const normalized = providerUtil.normalizeProviderWithCustom(provider, providers);
34
+ const seen = new Set<string>();
35
+ const result: ProviderModel[] = [];
36
+ for (const m of [...ctx.modelRegistry.getAvailable(), ...ctx.modelRegistry.getAll()]) {
37
+ const key = `${m.provider}/${m.id}`;
38
+ if (seen.has(key)) continue;
39
+ seen.add(key);
40
+ if (providerUtil.normalizeProviderWithCustom(m.provider, providers) === normalized) result.push(m);
41
+ }
42
+ return result;
43
+ }
44
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./base";
2
+ export * from "./prompts";
3
+ export * from "./select";
@@ -0,0 +1,36 @@
1
+ import type { ExtensionUIContext } from "@mariozechner/pi-coding-agent";
2
+ import type { ProviderModelConfig } from "@/types";
3
+ import { uiUtil } from "@/utils";
4
+
5
+ export class ModelConfigBuilder {
6
+ private readonly prompt: ReturnType<typeof uiUtil.prompt>;
7
+ private config: Partial<ProviderModelConfig> = {};
8
+
9
+ constructor(readonly ui: ExtensionUIContext) {
10
+ this.prompt = uiUtil.prompt(ui);
11
+ }
12
+
13
+ async withId(): Promise<this> {
14
+ const raw = await this.prompt("Model id", "my-model").asText();
15
+ if (!raw) throw new Error("Model id is required");
16
+ this.config.id = raw.trim();
17
+ return this;
18
+ }
19
+
20
+ async withName(): Promise<this> {
21
+ this.config.name = await this.prompt("Display name (optional)", this.config.id).asText();
22
+ return this;
23
+ }
24
+
25
+ build(): ProviderModelConfig {
26
+ const { id } = this.config;
27
+ if (!id) throw new Error("Model id is required");
28
+ return { ...this.config, id };
29
+ }
30
+
31
+ async collect(): Promise<ProviderModelConfig> {
32
+ await this.withId();
33
+ await this.withName();
34
+ return this.build();
35
+ }
36
+ }
@@ -0,0 +1,37 @@
1
+ import type { Api, Model } from "@mariozechner/pi-ai";
2
+ import type { ProviderConfig } from "@/types";
3
+ import { providerUtil } from "@/utils";
4
+
5
+ export type ProviderModel = Model<Api>;
6
+
7
+ export type ModelItem = { type: "header"; provider: string } | { type: "model"; model: ProviderModel; active: boolean };
8
+
9
+ export function buildGroupedModelItems(
10
+ models: ProviderModel[],
11
+ providers: ProviderConfig[],
12
+ activeId: string | undefined,
13
+ ): ModelItem[] {
14
+ const sorted = [...models].sort((a, b) => {
15
+ const pa = providerUtil.normalizeProviderWithCustom(a.provider, providers);
16
+ const pb = providerUtil.normalizeProviderWithCustom(b.provider, providers);
17
+ return pa.localeCompare(pb) || a.id.localeCompare(b.id);
18
+ });
19
+
20
+ const items: ModelItem[] = [];
21
+ let prevProvider: string | undefined;
22
+
23
+ for (const model of sorted) {
24
+ const provider = providerUtil.normalizeProviderWithCustom(model.provider, providers);
25
+ if (prevProvider !== provider) {
26
+ items.push({ type: "header", provider });
27
+ prevProvider = provider;
28
+ }
29
+ items.push({ type: "model", model, active: model.id === activeId });
30
+ }
31
+ return items;
32
+ }
33
+
34
+ export function formatModelItem(item: Extract<ModelItem, { type: "model" }>): string {
35
+ const marker = item.active ? "✓" : "·";
36
+ return ` ${marker} ${item.model.id}`;
37
+ }
@@ -0,0 +1,28 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { AccountSwitcher } from "@/runtime";
3
+ import { COMMANDS } from "@/constants";
4
+ import type { AccountSwitcherContext } from "@/types";
5
+ import { ProviderConfigBuilder } from "./shared";
6
+ import { BaseCommand } from "../base";
7
+ import { errorUtil } from "@/utils";
8
+
9
+ export const useAddProviderCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
10
+ new AddProviderCommand(pi, runtime).register();
11
+ };
12
+
13
+ class AddProviderCommand extends BaseCommand {
14
+ constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
15
+ super(pi, runtime, COMMANDS.providers.add);
16
+ }
17
+
18
+ async handler(ctx: AccountSwitcherContext): Promise<void> {
19
+ try {
20
+ await this.runtime.load();
21
+ const provider = await new ProviderConfigBuilder(ctx.ui).collect();
22
+ await this.runtime.addProvider(provider);
23
+ ctx.ui.notify(`Provider "${provider.id}" added.`, "info");
24
+ } catch (e) {
25
+ ctx.ui.notify(`Failed to add provider: ${errorUtil.format(e)}`, "error");
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,29 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { AccountSwitcher } from "@/runtime";
3
+ import { COMMANDS } from "@/constants";
4
+ import type { AccountSwitcherContext } from "@/types";
5
+ import { ProviderConfigBuilder, ProviderCommand } from "./shared";
6
+ import { errorUtil } from "@/utils";
7
+
8
+ export const useEditProviderCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
9
+ new EditProviderCommand(pi, runtime).register();
10
+ };
11
+
12
+ class EditProviderCommand extends ProviderCommand {
13
+ constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
14
+ super(pi, runtime, COMMANDS.providers.edit);
15
+ }
16
+
17
+ async handler(ctx: AccountSwitcherContext): Promise<void> {
18
+ try {
19
+ const original = await this.loadAndSelectProvider(ctx, "Select provider to edit");
20
+ if (!original) return;
21
+
22
+ const updated = await new ProviderConfigBuilder(ctx.ui, original).collect();
23
+ await this.runtime.editProvider(original, updated);
24
+ ctx.ui.notify(`Provider "${updated.id}" updated.`, "info");
25
+ } catch (e) {
26
+ ctx.ui.notify(`Failed to edit provider: ${errorUtil.format(e)}`, "error");
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,15 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { AccountSwitcher } from "@/runtime";
3
+ import { useAddProviderCommand } from "./add";
4
+ import { useEditProviderCommand } from "./edit";
5
+ import { useListProvidersCommand } from "./list";
6
+ import { useRemoveProviderCommand } from "./remove";
7
+
8
+ const useProviderCommands = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
9
+ useAddProviderCommand(pi, runtime);
10
+ useEditProviderCommand(pi, runtime);
11
+ useListProvidersCommand(pi, runtime);
12
+ useRemoveProviderCommand(pi, runtime);
13
+ };
14
+
15
+ export default useProviderCommands;
@@ -0,0 +1,38 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { AccountSwitcher } from "@/runtime";
3
+ import { COMMANDS } from "@/constants";
4
+ import { ProviderCommand } from "./shared";
5
+ import type { AccountSwitcherContext, ProviderConfig } from "@/types";
6
+ import { errorUtil } from "@/utils";
7
+
8
+ export const useListProvidersCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
9
+ new ListProvidersCommand(pi, runtime).register();
10
+ };
11
+
12
+ class ListProvidersCommand extends ProviderCommand {
13
+ constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
14
+ super(pi, runtime, COMMANDS.providers.list);
15
+ }
16
+
17
+ async handler(ctx: AccountSwitcherContext): Promise<void> {
18
+ try {
19
+ const providers = await this.loadProviders(ctx);
20
+ if (!providers) return;
21
+ await ctx.ui.select("Providers", providers.map((p) => this.format(p)));
22
+ } catch (e) {
23
+ ctx.ui.notify(`Failed to list providers: ${errorUtil.format(e)}`, "error");
24
+ }
25
+ }
26
+
27
+ private format(p: ProviderConfig): string {
28
+ const details = [
29
+ p.envKeys?.length ? `env: ${p.envKeys.join(", ")}` : undefined,
30
+ p.baseUrl && `baseUrl: ${p.baseUrl}`,
31
+ p.api && `api: ${p.api}`,
32
+ p.models && `models: ${p.models.length}`,
33
+ ]
34
+ .filter(Boolean)
35
+ .join("; ");
36
+ return `custom — ${p.label ?? p.id} (${p.id})${details ? ` ${details}` : ""}`;
37
+ }
38
+ }
@@ -0,0 +1,46 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { AccountSwitcher } from "@/runtime";
3
+ import { COMMANDS } from "@/constants";
4
+ import type { AccountSwitcherContext } from "@/types";
5
+ import { errorUtil, providerUtil } from "@/utils";
6
+ import { ProviderCommand, selectProvider } from "./shared";
7
+
8
+ export const useRemoveProviderCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
9
+ new RemoveProviderCommand(pi, runtime).register();
10
+ };
11
+
12
+ class RemoveProviderCommand extends ProviderCommand {
13
+ constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
14
+ super(pi, runtime, COMMANDS.providers.remove);
15
+ }
16
+
17
+ async handler(ctx: AccountSwitcherContext): Promise<void> {
18
+ try {
19
+ const providers = await this.loadProviders(ctx);
20
+ if (!providers) return;
21
+
22
+ const accounts = this.runtime.getAccounts();
23
+ const removable = providers.filter(
24
+ (p) => !accounts.some((a) => providerUtil.normalizeProviderWithCustom(a.provider, providers) === p.id),
25
+ );
26
+ if (removable.length === 0) {
27
+ ctx.ui.notify("No providers to remove.", "info");
28
+ return;
29
+ }
30
+
31
+ const provider = await selectProvider(ctx.ui, "Remove provider", removable);
32
+ if (!provider) return;
33
+
34
+ const confirmed = await ctx.ui.confirm(
35
+ "Remove provider?",
36
+ `Remove "${provider.label ?? provider.id}" (${provider.id})?`,
37
+ );
38
+ if (!confirmed) return;
39
+
40
+ await this.runtime.removeProvider(provider);
41
+ ctx.ui.notify(`Removed provider "${provider.label ?? provider.id}".`, "info");
42
+ } catch (e) {
43
+ ctx.ui.notify(`Failed to remove provider: ${errorUtil.format(e)}`, "error");
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,30 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { AccountSwitcher } from "@/runtime";
3
+ import type { AccountSwitcherContext, ProviderConfig } from "@/types";
4
+ import { BaseCommand, type CommandMeta } from "../../base";
5
+ import { selectProvider } from ".";
6
+
7
+ export abstract class ProviderCommand extends BaseCommand {
8
+ constructor(pi: ExtensionAPI, runtime: AccountSwitcher, meta: CommandMeta) {
9
+ super(pi, runtime, meta);
10
+ }
11
+
12
+ protected async loadProviders(ctx: AccountSwitcherContext): Promise<ProviderConfig[] | undefined> {
13
+ await this.runtime.load();
14
+ const providers = this.runtime.getProviders();
15
+ if (providers.length === 0) {
16
+ ctx.ui.notify("No custom providers configured.", "info");
17
+ return undefined;
18
+ }
19
+ return providers;
20
+ }
21
+
22
+ protected async loadAndSelectProvider(
23
+ ctx: AccountSwitcherContext,
24
+ label: string,
25
+ ): Promise<ProviderConfig | undefined> {
26
+ const providers = await this.loadProviders(ctx);
27
+ if (!providers) return undefined;
28
+ return selectProvider(ctx.ui, label, providers);
29
+ }
30
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./prompts";
2
+ export * from "./select";
3
+ export * from "./base";
@@ -0,0 +1,172 @@
1
+ import type { ExtensionUIContext } from "@mariozechner/pi-coding-agent";
2
+ import type { ProviderApi, ProviderConfig } from "@/types";
3
+ import { PROVIDER_API_TYPES } from "@/constants";
4
+ import { providerUtil, uiUtil } from "@/utils";
5
+
6
+ const DEFAULTS = {
7
+ id: "my-provider",
8
+ baseUrl: "https://api.example.com/v1",
9
+ api: "openai-completions",
10
+ envKey: "PROVIDER_API_KEY",
11
+ } as const;
12
+
13
+ export class ProviderConfigBuilder {
14
+ private readonly prompt: ReturnType<typeof uiUtil.prompt>;
15
+ private readonly defaults: {
16
+ id: string;
17
+ label?: string;
18
+ baseUrl: string;
19
+ api: string;
20
+ apiKey?: string;
21
+ envKeys?: string[];
22
+ aliases: string[];
23
+ models?: ProviderConfig["models"];
24
+ compat?: ProviderConfig["compat"];
25
+ piAuthProvider?: string;
26
+ };
27
+
28
+ private config: Partial<ProviderConfig> = {};
29
+
30
+ constructor(
31
+ private readonly ui: ExtensionUIContext,
32
+ original?: ProviderConfig,
33
+ ) {
34
+ this.prompt = uiUtil.prompt(ui);
35
+ this.defaults = {
36
+ id: original?.id ?? DEFAULTS.id,
37
+ label: original?.label ?? original?.name,
38
+ baseUrl: original?.baseUrl ?? DEFAULTS.baseUrl,
39
+ api: original?.api ?? DEFAULTS.api,
40
+ apiKey: original?.apiKey,
41
+ envKeys: original?.envKeys,
42
+ aliases: original?.aliases ?? [],
43
+ models: original?.models,
44
+ compat: original?.compat,
45
+ piAuthProvider: original?.piAuthProvider,
46
+ };
47
+ }
48
+
49
+ async withId(): Promise<this> {
50
+ const raw = await this.prompt("Provider id", this.defaults.id).asText();
51
+ if (!raw) throw new Error("Provider id is required");
52
+ this.config.id = providerUtil.normalizeProvider(raw);
53
+ return this;
54
+ }
55
+
56
+ async withLabel(): Promise<this> {
57
+ const id = this.config.id ?? "";
58
+ this.config.label = await this.prompt("Provider label", this.defaults.label ?? id).asText();
59
+ return this;
60
+ }
61
+
62
+ async withBaseUrl(): Promise<this> {
63
+ this.config.baseUrl = await this.prompt(
64
+ "Base URL (blank for account-only provider)",
65
+ this.defaults.baseUrl,
66
+ ).asText();
67
+ return this;
68
+ }
69
+
70
+ async withApi(): Promise<this> {
71
+ const current = this.defaults.api ?? DEFAULTS.api;
72
+ const options = (PROVIDER_API_TYPES as readonly string[]).includes(current)
73
+ ? [...PROVIDER_API_TYPES]
74
+ : [current, ...PROVIDER_API_TYPES];
75
+ this.config.api = (await uiUtil.filteredSelect(this.ui, "Pi API type", options)) ?? current;
76
+ return this;
77
+ }
78
+
79
+ async withApiKey(): Promise<this> {
80
+ const id = this.config.id ?? "";
81
+ this.config.apiKey = await this.prompt(
82
+ "Pi apiKey env var/name or raw key",
83
+ this.defaults.apiKey ?? `${id.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`,
84
+ ).asText();
85
+ return this;
86
+ }
87
+
88
+ async withEnvKeys(): Promise<this> {
89
+ const { apiKey } = this.config;
90
+ this.config.envKeys = await this.prompt(
91
+ "Env key suggestions (comma-separated)",
92
+ (this.defaults.envKeys ?? (apiKey ? [apiKey] : [DEFAULTS.envKey])).join(", "),
93
+ ).asCsv();
94
+ return this;
95
+ }
96
+
97
+ async withAliases(): Promise<this> {
98
+ this.config.aliases = (
99
+ await this.prompt("Aliases (comma-separated, optional)", this.defaults.aliases.join(", ")).asCsv()
100
+ ).map(providerUtil.normalizeProvider);
101
+ return this;
102
+ }
103
+
104
+ async withModels(): Promise<this> {
105
+ const rawModels = await this.prompt(
106
+ "Models JSON array (optional)",
107
+ this.defaults.models ? JSON.stringify(this.defaults.models) : "",
108
+ ).asJsonArray("models");
109
+ if (rawModels?.some((item) => typeof item !== "object" || item === null || Array.isArray(item))) {
110
+ throw new Error("models must be an array of objects");
111
+ }
112
+ this.config.models = rawModels as ProviderConfig["models"];
113
+ return this;
114
+ }
115
+
116
+ async withCompat(): Promise<this> {
117
+ this.config.compat = await this.prompt(
118
+ "Compat JSON object (optional)",
119
+ this.defaults.compat ? JSON.stringify(this.defaults.compat) : "",
120
+ ).asJsonRecord("compat");
121
+ return this;
122
+ }
123
+
124
+ async withPiAuthProvider(): Promise<this> {
125
+ const id = this.config.id ?? this.defaults.id;
126
+ this.config.piAuthProvider = (await this.ui.confirm(
127
+ "Configure Pi OAuth provider id?",
128
+ "Only choose yes if this provider maps to a Pi /login auth entry.",
129
+ ))
130
+ ? await this.prompt("Pi auth provider id", this.defaults.piAuthProvider ?? id).asText()
131
+ : this.defaults.piAuthProvider;
132
+ return this;
133
+ }
134
+
135
+ build(): ProviderConfig {
136
+ const { id, label, envKeys, aliases, baseUrl, api, apiKey, models, compat, piAuthProvider } = this.config;
137
+
138
+ if (!id) {
139
+ throw new Error("Provider id is required");
140
+ }
141
+
142
+ const resolvedLabel = label ?? id;
143
+ const resolvedApi = api || (baseUrl || apiKey || models ? DEFAULTS.api : undefined);
144
+ return {
145
+ id,
146
+ label: resolvedLabel,
147
+ name: resolvedLabel,
148
+ envKeys: envKeys ?? [],
149
+ aliases: aliases ?? [],
150
+ ...(baseUrl && { baseUrl }),
151
+ ...(resolvedApi && { api: resolvedApi }),
152
+ ...(apiKey && { apiKey }),
153
+ ...(models && { models }),
154
+ ...(compat && { compat }),
155
+ ...(piAuthProvider && { piAuthProvider }),
156
+ };
157
+ }
158
+
159
+ async collect(): Promise<ProviderConfig> {
160
+ await this.withId();
161
+ await this.withLabel();
162
+ await this.withBaseUrl();
163
+ await this.withApi();
164
+ await this.withApiKey();
165
+ await this.withEnvKeys();
166
+ await this.withAliases();
167
+ await this.withModels();
168
+ await this.withCompat();
169
+ await this.withPiAuthProvider();
170
+ return this.build();
171
+ }
172
+ }
@@ -0,0 +1,24 @@
1
+ import type { ExtensionUIContext } from "@mariozechner/pi-coding-agent";
2
+ import type { ProviderConfig } from "@/types";
3
+
4
+ export async function selectProvider(
5
+ ui: ExtensionUIContext,
6
+ title: string,
7
+ providers: ProviderConfig[],
8
+ ): Promise<ProviderConfig | undefined> {
9
+ const labelCounts = new Map<string, number>();
10
+ for (const p of providers) {
11
+ labelCounts.set(p.label ?? p.id, (labelCounts.get(p.label ?? p.id) ?? 0) + 1);
12
+ }
13
+
14
+ const labelMap = new Map<string, ProviderConfig>();
15
+ for (const p of providers) {
16
+ const base = p.label ?? p.id;
17
+ const display = (labelCounts.get(base) ?? 0) > 1 ? `${base} (${p.id})` : base;
18
+ labelMap.set(display, p);
19
+ }
20
+
21
+ const choice = await ui.select(title, [...labelMap.keys()]);
22
+ if (!choice) return undefined;
23
+ return labelMap.get(choice);
24
+ }
@@ -0,0 +1,7 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { AccountSwitcher } from "@/runtime";
3
+ import { useResetCommand } from "./reset";
4
+
5
+ export default function useSystemCommands(pi: ExtensionAPI, runtime: AccountSwitcher) {
6
+ useResetCommand(pi, runtime);
7
+ }
@@ -0,0 +1,36 @@
1
+ import { rm } from "node:fs/promises";
2
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
+ import type { AccountSwitcher } from "@/runtime";
4
+ import { ACCOUNTS_PATH, COMMANDS, PROVIDERS_PATH, STATE_PATH } from "@/constants";
5
+ import type { AccountSwitcherContext } from "@/types";
6
+ import { BaseCommand } from "../base";
7
+ import { errorUtil, uiUtil } from "@/utils";
8
+
9
+ export const useResetCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
10
+ new ResetCommand(pi, runtime).register();
11
+ };
12
+
13
+ class ResetCommand extends BaseCommand {
14
+ constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
15
+ super(pi, runtime, COMMANDS.system.reset);
16
+ }
17
+
18
+ async handler(ctx: AccountSwitcherContext): Promise<void> {
19
+ try {
20
+ const confirmed = await ctx.ui.confirm(
21
+ "Reset all extension data?",
22
+ "This will permanently delete all accounts, providers, and state. This cannot be undone.",
23
+ );
24
+ if (!confirmed) return;
25
+
26
+ await Promise.all([ACCOUNTS_PATH, PROVIDERS_PATH, STATE_PATH].map((path) => rm(path, { force: true })));
27
+
28
+ await this.runtime.load();
29
+ uiUtil.setAccountStatus(ctx.ui, undefined);
30
+
31
+ ctx.ui.notify("Extension data reset to factory defaults.", "info");
32
+ } catch (e) {
33
+ ctx.ui.notify(`Failed to reset: ${errorUtil.format(e)}`, "error");
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,66 @@
1
+ export const COMMANDS = {
2
+ accounts: {
3
+ add: {
4
+ name: "accounts:add",
5
+ description: "Add a new provider account from inside Pi",
6
+ },
7
+ list: {
8
+ name: "accounts:list",
9
+ description: "List configured accounts and activate the selected account",
10
+ },
11
+ edit: {
12
+ name: "accounts:edit",
13
+ description: "Edit a configured account",
14
+ },
15
+ remove: {
16
+ name: "accounts:remove",
17
+ description: "Remove a configured account",
18
+ },
19
+ switch: {
20
+ name: "accounts:switch",
21
+ description: "Switch to another account within the current provider",
22
+ },
23
+ oauth: {
24
+ name: "accounts:oauth",
25
+ description: "Import Pi /login OAuth credentials as a switchable account",
26
+ },
27
+ },
28
+ providers: {
29
+ add: {
30
+ name: "providers:add",
31
+ description: "Add a reusable custom provider",
32
+ },
33
+ edit: {
34
+ name: "providers:edit",
35
+ description: "Edit a configured custom provider",
36
+ },
37
+ list: {
38
+ name: "providers:list",
39
+ description: "List configured custom providers",
40
+ },
41
+ remove: {
42
+ name: "providers:remove",
43
+ description: "Remove a configured custom provider",
44
+ },
45
+ },
46
+ models: {
47
+ list: {
48
+ name: "models:list",
49
+ description: "List all available models and switch to the selected one",
50
+ },
51
+ add: {
52
+ name: "models:add",
53
+ description: "Add a custom model config to the current provider",
54
+ },
55
+ remove: {
56
+ name: "models:remove",
57
+ description: "Remove a custom model config from the current provider",
58
+ },
59
+ },
60
+ system: {
61
+ reset: {
62
+ name: "system:reset",
63
+ description: "Reset all extension data (accounts, providers, state) to factory defaults",
64
+ },
65
+ },
66
+ } as const;
@@ -0,0 +1,6 @@
1
+ import type { AccountSwitcherConfig } from "@/types";
2
+
3
+ export const DEFAULT_CONFIG: AccountSwitcherConfig = {
4
+ accounts: [],
5
+ switchMode: "env",
6
+ };
@@ -0,0 +1,4 @@
1
+ export * from "./commands";
2
+ export * from "./config";
3
+ export * from "./paths";
4
+ export * from "./providers";
@@ -0,0 +1,8 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+
4
+ export const APP_DIR = join(homedir(), ".pi", "account-switcher");
5
+ export const ACCOUNTS_PATH = join(APP_DIR, "accounts.json");
6
+ export const PROVIDERS_PATH = join(APP_DIR, "providers.json");
7
+ export const STATE_PATH = join(APP_DIR, "state.json");
8
+ export const PI_AUTH_PATH = join(homedir(), ".pi", "agent", "auth.json");