@hieplp/pi-account-switcher 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -21
- 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 +6 -6
- 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 +51 -0
- package/src/commands/system/import.ts +102 -0
- package/src/commands/system/index.ts +5 -1
- package/src/commands/system/reset.ts +1 -1
- package/src/constants/commands.ts +13 -0
- package/src/constants/env.ts +1 -0
- package/src/constants/index.ts +1 -0
- package/src/constants/paths.ts +2 -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
|
@@ -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";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { AccountSwitcher } from "@/runtime";
|
|
4
|
+
import { COMMANDS, DEFAULT_EXPORT_PATH, STATE_PATH } from "@/constants";
|
|
5
|
+
import type { AccountSwitcherContext } from "@/types";
|
|
6
|
+
import { BaseCommand } from "../base";
|
|
7
|
+
import { errorUtil, fileUtil } from "@/utils";
|
|
8
|
+
|
|
9
|
+
export const useExportCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
10
|
+
new ExportCommand(pi, runtime).register();
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
class ExportCommand extends BaseCommand {
|
|
14
|
+
constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
|
|
15
|
+
super(pi, runtime, COMMANDS.system.export);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async handler(ctx: AccountSwitcherContext, args?: string): Promise<void> {
|
|
19
|
+
try {
|
|
20
|
+
const target = args?.trim() || (await ctx.ui.input("Export file (blank for default)", DEFAULT_EXPORT_PATH));
|
|
21
|
+
if (target === undefined) {
|
|
22
|
+
ctx.ui.notify("Export cancelled.", "info");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const state = await loadState();
|
|
27
|
+
const exportData = {
|
|
28
|
+
version: 1,
|
|
29
|
+
exportedAt: new Date().toISOString(),
|
|
30
|
+
accounts: this.runtime.getAccounts(),
|
|
31
|
+
providers: this.runtime.getProviders(),
|
|
32
|
+
state,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const path = fileUtil.expandHome(target.trim() || DEFAULT_EXPORT_PATH);
|
|
36
|
+
await fileUtil.writePrivateJson(path, exportData);
|
|
37
|
+
ctx.ui.notify(`Exported account switcher data to ${path}.`, "info");
|
|
38
|
+
} catch (e) {
|
|
39
|
+
ctx.ui.notify(`Failed to export: ${errorUtil.format(e)}`, "error");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function loadState(): Promise<unknown> {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(await readFile(STATE_PATH, "utf8"));
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (fileUtil.isMissingFileError(error)) return {};
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { AccountSwitcher } from "@/runtime";
|
|
5
|
+
import { ACCOUNTS_PATH, COMMANDS, DEFAULT_EXPORT_PATH, PROVIDERS_PATH, STATE_PATH } from "@/constants";
|
|
6
|
+
import { accountSchema, providerSchema } from "@/schemas";
|
|
7
|
+
import type { AccountConfig, AccountSwitcherContext, ProviderConfig } from "@/types";
|
|
8
|
+
import { BaseCommand } from "../base";
|
|
9
|
+
import { errorUtil, fileUtil, uiUtil } from "@/utils";
|
|
10
|
+
|
|
11
|
+
const importStateSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
activeAccountId: z.string().optional(),
|
|
14
|
+
activeModelId: z.string().optional(),
|
|
15
|
+
activeModelProvider: z.string().optional(),
|
|
16
|
+
})
|
|
17
|
+
.default({});
|
|
18
|
+
|
|
19
|
+
const exportBundleSchema = z.object({
|
|
20
|
+
version: z.number().optional(),
|
|
21
|
+
accounts: z.array(accountSchema).default([]),
|
|
22
|
+
providers: z.array(providerSchema.extend({ id: z.string().min(1) })).default([]),
|
|
23
|
+
state: importStateSchema,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
type ImportBundle = {
|
|
27
|
+
accounts: AccountConfig[];
|
|
28
|
+
providers: ProviderConfig[];
|
|
29
|
+
state: z.infer<typeof importStateSchema>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const useImportCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
33
|
+
new ImportCommand(pi, runtime).register();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
class ImportCommand extends BaseCommand {
|
|
37
|
+
constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
|
|
38
|
+
super(pi, runtime, COMMANDS.system.import);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async handler(ctx: AccountSwitcherContext, args?: string): Promise<void> {
|
|
42
|
+
try {
|
|
43
|
+
const source = args?.trim() || (await ctx.ui.input("Import file (blank for default)", DEFAULT_EXPORT_PATH));
|
|
44
|
+
if (source === undefined) {
|
|
45
|
+
ctx.ui.notify("Import cancelled.", "info");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const path = fileUtil.expandHome(source.trim() || DEFAULT_EXPORT_PATH);
|
|
50
|
+
const bundle = parseImportBundle(JSON.parse(await readFile(path, "utf8")));
|
|
51
|
+
|
|
52
|
+
const confirmed = await ctx.ui.confirm(
|
|
53
|
+
"Import account switcher data?",
|
|
54
|
+
`This will replace all existing accounts, providers, and state with data from ${path}.`,
|
|
55
|
+
);
|
|
56
|
+
if (!confirmed) {
|
|
57
|
+
ctx.ui.notify("Import cancelled.", "info");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await fileUtil.writePrivateJson(ACCOUNTS_PATH, { accounts: bundle.accounts });
|
|
62
|
+
await fileUtil.writePrivateJson(PROVIDERS_PATH, { providers: bundle.providers });
|
|
63
|
+
await fileUtil.writePrivateJson(STATE_PATH, bundle.state);
|
|
64
|
+
|
|
65
|
+
await this.runtime.load();
|
|
66
|
+
await this.runtime.init(ctx);
|
|
67
|
+
uiUtil.setAccountStatus(ctx.ui, this.runtime.getActiveAccount()?.label);
|
|
68
|
+
|
|
69
|
+
ctx.ui.notify(
|
|
70
|
+
`Imported ${bundle.accounts.length} accounts and ${bundle.providers.length} providers from ${path}.`,
|
|
71
|
+
"info",
|
|
72
|
+
);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
ctx.ui.notify(`Failed to import: ${errorUtil.format(e)}`, "error");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseImportBundle(raw: unknown): ImportBundle {
|
|
80
|
+
const parsed = exportBundleSchema.parse(raw);
|
|
81
|
+
assertNoDuplicateIds(
|
|
82
|
+
"account",
|
|
83
|
+
parsed.accounts.map((account) => account.id),
|
|
84
|
+
);
|
|
85
|
+
assertNoDuplicateIds(
|
|
86
|
+
"provider",
|
|
87
|
+
parsed.providers.map((provider) => provider.id),
|
|
88
|
+
);
|
|
89
|
+
return parsed;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function assertNoDuplicateIds(kind: string, ids: string[]): void {
|
|
93
|
+
const seen = new Set<string>();
|
|
94
|
+
const duplicates = new Set<string>();
|
|
95
|
+
for (const id of ids) {
|
|
96
|
+
if (seen.has(id)) duplicates.add(id);
|
|
97
|
+
else seen.add(id);
|
|
98
|
+
}
|
|
99
|
+
if (duplicates.size > 0) {
|
|
100
|
+
throw new Error(`Duplicate ${kind} ids: ${Array.from(duplicates).sort().join(", ")}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
|
+
import { useExportCommand } from "./export";
|
|
4
|
+
import { useImportCommand } from "./import";
|
|
3
5
|
import { useResetCommand } from "./reset";
|
|
4
6
|
|
|
5
7
|
export default function useSystemCommands(pi: ExtensionAPI, runtime: AccountSwitcher) {
|
|
6
8
|
useResetCommand(pi, runtime);
|
|
9
|
+
useExportCommand(pi, runtime);
|
|
10
|
+
useImportCommand(pi, runtime);
|
|
7
11
|
}
|
|
@@ -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: {
|
|
@@ -62,5 +67,13 @@ export const COMMANDS = {
|
|
|
62
67
|
name: "system:reset",
|
|
63
68
|
description: "Reset all extension data (accounts, providers, state) to factory defaults",
|
|
64
69
|
},
|
|
70
|
+
export: {
|
|
71
|
+
name: "system:export",
|
|
72
|
+
description: "Export all extension data (accounts, providers, state) to a JSON file",
|
|
73
|
+
},
|
|
74
|
+
import: {
|
|
75
|
+
name: "system:import",
|
|
76
|
+
description: "Import extension data (accounts, providers, state) from a JSON file",
|
|
77
|
+
},
|
|
65
78
|
},
|
|
66
79
|
} as const;
|
|
@@ -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
|
@@ -5,4 +5,5 @@ export const APP_DIR = join(homedir(), ".pi", "account-switcher");
|
|
|
5
5
|
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
|
-
export const PI_AUTH_PATH = join(homedir(), ".pi", "agent", "auth.json");
|
|
8
|
+
export const PI_AUTH_PATH = join(homedir(), ".pi", "agent", "auth.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;
|