@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
package/README.md
CHANGED
|
@@ -64,14 +64,16 @@ The local commands will be registered as `/dev:accounts:list`, `/dev:accounts:ad
|
|
|
64
64
|
|
|
65
65
|
### Accounts
|
|
66
66
|
|
|
67
|
-
| Command
|
|
68
|
-
|
|
|
69
|
-
| `/accounts:add`
|
|
70
|
-
| `/accounts:
|
|
71
|
-
| `/accounts:
|
|
72
|
-
| `/accounts:
|
|
73
|
-
| `/accounts:
|
|
74
|
-
| `/accounts:
|
|
67
|
+
| Command | Description |
|
|
68
|
+
| -------------------- | --------------------------------------------------------------- |
|
|
69
|
+
| `/accounts:add` | Add a new account interactively |
|
|
70
|
+
| `/accounts:switch` | Switch to any account (interactive picker or by ID) |
|
|
71
|
+
| `/accounts:peers` | Switch to another account within the current provider |
|
|
72
|
+
| `/accounts:subagent` | Set account for the next spawned subagent |
|
|
73
|
+
| `/accounts:edit` | Edit label, provider, id, or credential source |
|
|
74
|
+
| `/accounts:remove` | Delete an account |
|
|
75
|
+
| `/accounts:oauth` | Import the current Pi `/login` OAuth session as a named account |
|
|
76
|
+
| `/accounts:dirs` | Manage working directories for CWD-based auto-select |
|
|
75
77
|
|
|
76
78
|
### Providers
|
|
77
79
|
|
|
@@ -133,12 +135,20 @@ Complete browser/device login, then:
|
|
|
133
135
|
/accounts:oauth
|
|
134
136
|
```
|
|
135
137
|
|
|
136
|
-
Give it a label like `Claude — Work`. Repeat for as many accounts as you need — each gets its own saved credentials. Switch between them any time with `/accounts:
|
|
138
|
+
Give it a label like `Claude — Work`. Repeat for as many accounts as you need — each gets its own saved credentials. Switch between them any time with `/accounts:switch`.
|
|
137
139
|
|
|
138
140
|
OAuth credentials are read from `~/.pi/agent/auth.json` and written back to Pi's live auth storage on switch.
|
|
139
141
|
|
|
140
142
|
---
|
|
141
143
|
|
|
144
|
+
## Directory-based Auto-Select
|
|
145
|
+
|
|
146
|
+
The extension can automatically activate the right account based on your current working directory. Each account can list directory paths (`dirs`) — the longest prefix match wins. A `defaultAccountId` at the config level serves as the fallback. Use `/accounts:dirs` to manage directories interactively.
|
|
147
|
+
|
|
148
|
+
See **USAGE.md** for full details on the activation cascade, configuration, and examples.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
142
152
|
## Custom Providers
|
|
143
153
|
|
|
144
154
|
Define a provider once, reuse it across accounts:
|
|
@@ -196,11 +206,13 @@ The export file contains all accounts, providers, and active-selection state as
|
|
|
196
206
|
```json
|
|
197
207
|
{
|
|
198
208
|
"switchMode": "env",
|
|
209
|
+
"defaultAccountId": "claude-work",
|
|
199
210
|
"accounts": [
|
|
200
211
|
{
|
|
201
212
|
"id": "claude-work",
|
|
202
213
|
"label": "Claude — Work",
|
|
203
214
|
"provider": "anthropic",
|
|
215
|
+
"dirs": ["/home/user/Development/Work"],
|
|
204
216
|
"env": {
|
|
205
217
|
"ANTHROPIC_API_KEY": { "type": "env", "name": "ANTHROPIC_WORK_API_KEY" }
|
|
206
218
|
}
|
|
@@ -209,6 +221,7 @@ The export file contains all accounts, providers, and active-selection state as
|
|
|
209
221
|
"id": "openai-personal",
|
|
210
222
|
"label": "OpenAI — Personal",
|
|
211
223
|
"provider": "openai",
|
|
224
|
+
"dirs": ["/home/user/Projects/Client-A"],
|
|
212
225
|
"env": {
|
|
213
226
|
"OPENAI_API_KEY": { "type": "file", "path": "~/.keys/openai-personal.txt" }
|
|
214
227
|
}
|
|
@@ -231,7 +244,22 @@ A plain string is treated as a literal; strings starting with `op://` are resolv
|
|
|
231
244
|
|
|
232
245
|
### State — `~/.pi/account-switcher/state.json`
|
|
233
246
|
|
|
234
|
-
Tracks the selected account and model
|
|
247
|
+
Tracks the selected account and model **per Pi session**. Each Pi session gets its own key — no global active-account state.
|
|
248
|
+
|
|
249
|
+
```json
|
|
250
|
+
{
|
|
251
|
+
"sessions": {
|
|
252
|
+
"abc123": { "activeAccountId": "claude-work" },
|
|
253
|
+
"def456": { "activeAccountId": "openai-personal", "activeModelId": "gpt-4", "activeModelProvider": "openai" }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
When Pi starts, the extension derives a session key from Pi's session file and restores that session's saved state. Sessions with no saved state fall back to CWD-based auto-select or `defaultAccountId`.
|
|
259
|
+
|
|
260
|
+
Legacy flat-format state (`{ "activeAccountId": "..." }`) is automatically migrated to the session-keyed format on first load.
|
|
261
|
+
|
|
262
|
+
Session entries are automatically cleaned up: entries inactive for `stateCleanupDays` (default 30, configurable in `accounts.json`) are pruned, with a hard cap of 500 entries.
|
|
235
263
|
|
|
236
264
|
---
|
|
237
265
|
|
package/USAGE.md
CHANGED
|
@@ -109,7 +109,7 @@ To add another OAuth account for the same provider, run `/login` again with the
|
|
|
109
109
|
Switch OAuth accounts with:
|
|
110
110
|
|
|
111
111
|
```txt
|
|
112
|
-
/accounts:
|
|
112
|
+
/accounts:switch
|
|
113
113
|
```
|
|
114
114
|
|
|
115
115
|
OAuth credentials are captured from Pi's auth file:
|
|
@@ -191,12 +191,14 @@ Example config:
|
|
|
191
191
|
|
|
192
192
|
```json
|
|
193
193
|
{
|
|
194
|
-
"
|
|
194
|
+
"defaultAccountId": "claude-work",
|
|
195
|
+
"stateCleanupDays": 30,
|
|
195
196
|
"accounts": [
|
|
196
197
|
{
|
|
197
198
|
"id": "claude-work",
|
|
198
199
|
"label": "Claude — Work",
|
|
199
200
|
"provider": "anthropic",
|
|
201
|
+
"dirs": ["/home/user/Development/Work"],
|
|
200
202
|
"env": {
|
|
201
203
|
"ANTHROPIC_API_KEY": { "type": "env", "name": "ANTHROPIC_WORK_API_KEY" }
|
|
202
204
|
}
|
|
@@ -273,26 +275,13 @@ A plain string is treated as a literal value, except strings beginning with `op:
|
|
|
273
275
|
|
|
274
276
|
## 9. Commands
|
|
275
277
|
|
|
276
|
-
###
|
|
277
|
-
|
|
278
|
-
```txt
|
|
279
|
-
/accounts:list
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
The extension tries to detect the current model provider and shows matching accounts.
|
|
283
|
-
|
|
284
|
-
### Pick account for a specific provider
|
|
285
|
-
|
|
286
|
-
```txt
|
|
287
|
-
/accounts:list
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
Useful if Pi cannot detect the active provider.
|
|
291
|
-
|
|
292
|
-
### List accounts
|
|
278
|
+
### Switch accounts
|
|
293
279
|
|
|
294
280
|
```txt
|
|
295
|
-
/accounts:
|
|
281
|
+
/accounts:switch # interactive picker from all accounts
|
|
282
|
+
/accounts:switch <id> # activate by ID directly (agent-facing)
|
|
283
|
+
/accounts:peers # picker from same-provider accounts
|
|
284
|
+
/accounts:subagent # set account for next spawned subagent
|
|
296
285
|
```
|
|
297
286
|
|
|
298
287
|
### Import current Pi OAuth login
|
|
@@ -366,44 +355,102 @@ Typical usage:
|
|
|
366
355
|
3. Later, switch accounts:
|
|
367
356
|
|
|
368
357
|
```txt
|
|
369
|
-
/accounts:
|
|
358
|
+
/accounts:switch
|
|
370
359
|
```
|
|
371
360
|
|
|
372
361
|
Alternative manual config flow: edit `~/.pi/account-switcher/accounts.json` directly, then reload Pi:
|
|
373
362
|
|
|
374
|
-
|
|
363
|
+
1. If needed, reload Pi runtime:
|
|
375
364
|
|
|
376
365
|
```txt
|
|
377
366
|
/reload
|
|
378
367
|
```
|
|
379
368
|
|
|
380
|
-
## 11.
|
|
369
|
+
## 11. Directory-based Auto-Select
|
|
381
370
|
|
|
382
|
-
|
|
371
|
+
The extension can automatically activate the right account based on your current working directory.
|
|
372
|
+
|
|
373
|
+
### `dirs` on accounts
|
|
374
|
+
|
|
375
|
+
Add directory paths to an account in `accounts.json`. The longest matching prefix of the current working directory wins. On tie, the first account in the array wins.
|
|
376
|
+
|
|
377
|
+
```json
|
|
378
|
+
{
|
|
379
|
+
"id": "claude-work",
|
|
380
|
+
"label": "Claude — Work",
|
|
381
|
+
"provider": "anthropic",
|
|
382
|
+
"dirs": ["/home/user/Development/Work"],
|
|
383
|
+
"env": {
|
|
384
|
+
"ANTHROPIC_API_KEY": { "type": "env", "name": "ANTHROPIC_WORK_API_KEY" }
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### `defaultAccountId` config fallback
|
|
390
|
+
|
|
391
|
+
If no session state exists and no directory matches, the extension falls back to `defaultAccountId` at the top of `accounts.json`:
|
|
392
|
+
|
|
393
|
+
```json
|
|
394
|
+
{
|
|
395
|
+
"switchMode": "env",
|
|
396
|
+
"defaultAccountId": "claude-work",
|
|
397
|
+
"accounts": [...]
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Activation cascade (session start)
|
|
402
|
+
|
|
403
|
+
1. **Saved session state** — account previously selected for this Pi session
|
|
404
|
+
2. **CWD-based auto-select** — longest matching directory prefix
|
|
405
|
+
3. **`defaultAccountId`** — config-level fallback
|
|
406
|
+
4. **None** — no account activated until you pick one
|
|
407
|
+
|
|
408
|
+
### Manage dirs with `/accounts:dirs`
|
|
409
|
+
|
|
410
|
+
The `/accounts:dirs` command opens an interactive wizard. When an active account and current working directory are detected, it offers **Auto-save** (one-step: saves current dir to active account) and **Manual** (pick account, then add via recursive directory browser or remove configured dirs).
|
|
411
|
+
|
|
412
|
+
Dirs are stored per account in `~/.pi/account-switcher/accounts.json`.
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## 12. State Persistence
|
|
417
|
+
|
|
418
|
+
Selected accounts are saved **per Pi session** at:
|
|
383
419
|
|
|
384
420
|
```txt
|
|
385
421
|
~/.pi/account-switcher/state.json
|
|
386
422
|
```
|
|
387
423
|
|
|
388
|
-
|
|
424
|
+
Each Pi session gets its own key — no global active-account state:
|
|
389
425
|
|
|
390
426
|
```json
|
|
391
427
|
{
|
|
392
|
-
"
|
|
393
|
-
|
|
394
|
-
|
|
428
|
+
"sessions": {
|
|
429
|
+
"abc123": { "activeAccountId": "claude-work" },
|
|
430
|
+
"def456": { "activeAccountId": "openai-personal", "activeModelId": "gpt-4", "activeModelProvider": "openai" }
|
|
431
|
+
}
|
|
395
432
|
}
|
|
396
433
|
```
|
|
397
434
|
|
|
398
|
-
On Pi session start, the extension restores
|
|
435
|
+
On Pi session start, the extension derives a session key and restores that session's saved state. Sessions with no saved state fall back to CWD-based auto-select or `defaultAccountId`. Legacy flat-format state (`{ "activeAccountId": "..." }`) is automatically migrated on first load.
|
|
436
|
+
|
|
437
|
+
Session entries auto-accumulate as you use Pi. To prevent unbounded growth, the extension automatically prunes entries that haven't been active in `stateCleanupDays` days (default: 30). If there are still more than 500 entries after the TTL sweep, the oldest ones are removed.
|
|
438
|
+
|
|
439
|
+
Configure the TTL in `~/.pi/account-switcher/accounts.json`:
|
|
440
|
+
|
|
441
|
+
```json
|
|
442
|
+
{ "accounts": [...], "stateCleanupDays": 60 }
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Set to a higher value (e.g. 90) if you frequently resume sessions from weeks ago, or a lower value (e.g. 7) for tighter cleanup. Entries without a `lastActive` timestamp (from previous versions) are automatically timestamped on first write after upgrade.
|
|
399
446
|
|
|
400
|
-
##
|
|
447
|
+
## 13. Important Note About Credential Caching
|
|
401
448
|
|
|
402
449
|
The extension updates `process.env`, Pi's live runtime API-key overrides, and Pi's live OAuth auth storage when those hooks are available.
|
|
403
450
|
|
|
404
451
|
If a provider still keeps old credentials cached, run `/reload` or restart Pi.
|
|
405
452
|
|
|
406
|
-
##
|
|
453
|
+
## 14. Troubleshooting
|
|
407
454
|
|
|
408
455
|
### No accounts configured
|
|
409
456
|
|
|
@@ -414,17 +461,10 @@ Run `/accounts:add` to create one interactively, or create `~/.pi/account-switch
|
|
|
414
461
|
Run explicitly:
|
|
415
462
|
|
|
416
463
|
```txt
|
|
417
|
-
/accounts:
|
|
464
|
+
/accounts:switch
|
|
418
465
|
```
|
|
419
466
|
|
|
420
|
-
Also check that account `provider` values match supported providers:
|
|
421
|
-
|
|
422
|
-
- `anthropic` / `claude`
|
|
423
|
-
- `openai`
|
|
424
|
-
- `openai-codex` / `codex`
|
|
425
|
-
- `google` / `gemini`
|
|
426
|
-
- `xai`
|
|
427
|
-
- `openrouter`
|
|
467
|
+
Also check that account `provider` values match a supported built-in provider or a custom provider added via `/providers:add`. Built-in providers include anthropic, openai, openai-codex, google, xai, openrouter, opencode, opencode-go, github-copilot, amazon-bedrock, and others. Use the `list_accounts` tool or check `@/constants/providers.ts` for the full list.
|
|
428
468
|
|
|
429
469
|
### Secret resolves empty
|
|
430
470
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hieplp/pi-account-switcher",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi extension for quickly switching between multiple accounts/API keys per provider.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,12 +45,13 @@
|
|
|
45
45
|
"@earendil-works/pi-coding-agent": "latest",
|
|
46
46
|
"@types/node": "latest",
|
|
47
47
|
"prettier": "^3.8.3",
|
|
48
|
+
"typebox": "^1.2.14",
|
|
48
49
|
"typescript": "latest",
|
|
49
50
|
"vitest": "latest"
|
|
50
51
|
},
|
|
51
52
|
"pi": {
|
|
52
53
|
"extensions": [
|
|
53
|
-
"./src/
|
|
54
|
+
"./src/index.ts"
|
|
54
55
|
]
|
|
55
56
|
},
|
|
56
57
|
"packageManager": "pnpm@11.5.0+sha512.dbfcc4f81cf48597afd4bc391ffdf12c11f1a9fb83a395bfa6b0a2d9cc2fd8ffebafdb1ccbd529632153f793904c2615b7f09fe1a345473fd1c35845172a8eb1"
|
|
@@ -20,7 +20,15 @@ class AddAccountCommand extends AccountCommand {
|
|
|
20
20
|
await this.runtime.load();
|
|
21
21
|
const providers = this.runtime.getProviders();
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// Get all provider IDs registered in Pi from the model registry
|
|
24
|
+
const piModels = ctx.modelRegistry.getAll();
|
|
25
|
+
const piProviderIds = [...new Set(piModels.map((m) => m.provider))];
|
|
26
|
+
|
|
27
|
+
const getOAuthEntry = async (p: string) => {
|
|
28
|
+
return this.runtime.getPiAuthEntry(p);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const account = await new AccountConfigBuilder(ctx.ui, providers, piProviderIds, getOAuthEntry).collect();
|
|
24
32
|
if (!account) return;
|
|
25
33
|
|
|
26
34
|
await this.saveProvider(ctx, account);
|
|
@@ -19,7 +19,21 @@ class EditAccountCommand extends AccountCommand {
|
|
|
19
19
|
const original = await this.loadAndSelectAccount(ctx, "Select account to edit");
|
|
20
20
|
if (!original) return;
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// Pass dynamic providers from Pi so editing can also see all options
|
|
23
|
+
const piModels = ctx.modelRegistry.getAll();
|
|
24
|
+
const piProviderIds = [...new Set(piModels.map((m) => m.provider))];
|
|
25
|
+
|
|
26
|
+
const getOAuthEntry = async (p: string) => {
|
|
27
|
+
return this.runtime.getPiAuthEntry(p);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const updated = await new AccountConfigBuilder(
|
|
31
|
+
ctx.ui,
|
|
32
|
+
this.runtime.getProviders(),
|
|
33
|
+
piProviderIds,
|
|
34
|
+
getOAuthEntry,
|
|
35
|
+
original,
|
|
36
|
+
).collect(true);
|
|
23
37
|
if (!updated) return;
|
|
24
38
|
|
|
25
39
|
await this.runtime.editAccount(original, updated);
|
|
@@ -2,19 +2,25 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import { useAddAccountCommand } from "./add";
|
|
4
4
|
import { useEditAccountCommand } from "./edit";
|
|
5
|
-
import {
|
|
5
|
+
import { useListAccountsTool } from "./list";
|
|
6
6
|
import { useOAuthImportCommand } from "./oauth";
|
|
7
7
|
import { useRemoveAccountCommand } from "./remove";
|
|
8
8
|
import { useSwitchAccountCommand } from "./switch";
|
|
9
|
+
import { useSubagentAccountCommand } from "./subagent";
|
|
10
|
+
import { usePeersCommand } from "./peers";
|
|
11
|
+
import { useSetSubagentAccountTool } from "./set-subagent-account";
|
|
9
12
|
import { useVerifyAccountsCommand } from "./verify";
|
|
10
13
|
|
|
11
14
|
const useAccountCommands = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
12
15
|
useAddAccountCommand(pi, runtime);
|
|
13
16
|
useEditAccountCommand(pi, runtime);
|
|
14
|
-
|
|
17
|
+
useListAccountsTool(pi, runtime);
|
|
15
18
|
useOAuthImportCommand(pi, runtime);
|
|
16
19
|
useRemoveAccountCommand(pi, runtime);
|
|
17
20
|
useSwitchAccountCommand(pi, runtime);
|
|
21
|
+
useSubagentAccountCommand(pi, runtime);
|
|
22
|
+
useSetSubagentAccountTool(pi, runtime);
|
|
23
|
+
usePeersCommand(pi, runtime);
|
|
18
24
|
useVerifyAccountsCommand(pi, runtime);
|
|
19
25
|
};
|
|
20
26
|
|
|
@@ -1,31 +1,49 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
1
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
|
-
import type {
|
|
4
|
-
import { COMMANDS } from "@/constants";
|
|
5
|
-
import { errorUtil } from "@/utils";
|
|
6
|
-
import { AccountCommand } from "./shared";
|
|
4
|
+
import type { AccountConfig } from "@/types";
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
class ListAccountsCommand extends AccountCommand {
|
|
13
|
-
constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
|
|
14
|
-
super(pi, runtime, COMMANDS.accounts.list);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async handler(ctx: AccountSwitcherContext): Promise<void> {
|
|
18
|
-
try {
|
|
19
|
-
const accounts = await this.loadAccounts(ctx);
|
|
20
|
-
if (!accounts) return;
|
|
6
|
+
/** Tool name for agent-facing account discovery */
|
|
7
|
+
export const LIST_ACCOUNTS_TOOL = "list_accounts";
|
|
21
8
|
|
|
22
|
-
|
|
23
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Format a list of accounts as structured text for agent consumption.
|
|
11
|
+
* Output format: `id | label | provider | status` (one line per account, sorted by label).
|
|
12
|
+
*/
|
|
13
|
+
export function formatAccountList(accounts: AccountConfig[], active?: AccountConfig): string {
|
|
14
|
+
if (accounts.length === 0) return "No accounts configured.";
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
const sorted = [...accounts].sort((a, b) => a.label.localeCompare(b.label));
|
|
17
|
+
return sorted
|
|
18
|
+
.map((account) => {
|
|
19
|
+
const status = active && active.id === account.id ? "active" : "inactive";
|
|
20
|
+
return `${account.id} | ${account.label} | ${account.provider} | ${status}`;
|
|
21
|
+
})
|
|
22
|
+
.join("\n");
|
|
31
23
|
}
|
|
24
|
+
|
|
25
|
+
export const useListAccountsTool = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
26
|
+
pi.registerTool({
|
|
27
|
+
name: LIST_ACCOUNTS_TOOL,
|
|
28
|
+
label: "List Accounts",
|
|
29
|
+
description:
|
|
30
|
+
"List all configured accounts. Returns ID, label, provider, and active/inactive status for each account. " +
|
|
31
|
+
"Use the ID with accounts:switch <id> to activate a specific account.",
|
|
32
|
+
promptSnippet: "List my configured accounts",
|
|
33
|
+
promptGuidelines: [
|
|
34
|
+
"Use list_accounts when the user asks what accounts are configured or wants to switch accounts.",
|
|
35
|
+
"The output shows one line per account: id | label | provider | status.",
|
|
36
|
+
],
|
|
37
|
+
parameters: Type.Object({}),
|
|
38
|
+
execute: async (_toolCallId, _params, _signal, _onUpdate, _ctx) => {
|
|
39
|
+
await runtime.load();
|
|
40
|
+
const accounts = runtime.getAccounts();
|
|
41
|
+
const active = runtime.getActiveAccount();
|
|
42
|
+
const output = formatAccountList(accounts, active);
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: "text", text: output }],
|
|
45
|
+
details: {},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AccountSwitcher } from "@/runtime";
|
|
3
|
+
import type { AccountSwitcherContext, AccountConfig, ProviderConfig } from "@/types";
|
|
4
|
+
import { COMMANDS } from "@/constants";
|
|
5
|
+
import { commandUtil, errorUtil, providerUtil } from "@/utils";
|
|
6
|
+
import { AccountCommand } from "./shared";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Filter accounts to those sharing the same auth provider as the active account,
|
|
10
|
+
* excluding the active account itself.
|
|
11
|
+
*/
|
|
12
|
+
export function filterPeers(accounts: AccountConfig[], active: AccountConfig): AccountConfig[] {
|
|
13
|
+
const normalize = (a: AccountConfig): string =>
|
|
14
|
+
providerUtil.normalizeProvider(a.piAuth?.provider ?? a.provider);
|
|
15
|
+
|
|
16
|
+
return accounts.filter((a) => normalize(a) === normalize(active) && a.id !== active.id);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const usePeersCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
20
|
+
new PeersCommand(pi, runtime).register();
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
class PeersCommand extends AccountCommand {
|
|
24
|
+
constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
|
|
25
|
+
super(pi, runtime, {
|
|
26
|
+
name: "accounts:peers",
|
|
27
|
+
description: "Switch to another account sharing the same provider",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async handler(ctx: AccountSwitcherContext): Promise<void> {
|
|
32
|
+
try {
|
|
33
|
+
await this.runtime.load();
|
|
34
|
+
|
|
35
|
+
const active = this.runtime.getActiveAccount();
|
|
36
|
+
if (!active) {
|
|
37
|
+
ctx.ui.notify(
|
|
38
|
+
`No active account. Use ${commandUtil.name("accounts:switch")} to activate one first.`,
|
|
39
|
+
"info",
|
|
40
|
+
);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const peers = filterPeers(this.runtime.getAccounts(), active);
|
|
45
|
+
if (peers.length === 0) {
|
|
46
|
+
ctx.ui.notify(`No other accounts for provider "${active.provider}".`, "info");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const account = await this.pickGroupedAccount(ctx, peers, `Switch account (${active.provider})`);
|
|
51
|
+
if (!account) return;
|
|
52
|
+
|
|
53
|
+
const applied = await this.runtime.activateAccount(account, ctx);
|
|
54
|
+
ctx.ui.notify(`Switched to ${account.label} (${applied}).`, "info");
|
|
55
|
+
} catch (e) {
|
|
56
|
+
ctx.ui.notify(`Failed to switch account: ${errorUtil.format(e)}`, "error");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { AccountSwitcher } from "@/runtime";
|
|
4
|
+
|
|
5
|
+
export const SET_SUBAGENT_ACCOUNT_TOOL = "set_subagent_account";
|
|
6
|
+
|
|
7
|
+
export const useSetSubagentAccountTool = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
8
|
+
pi.registerTool({
|
|
9
|
+
name: SET_SUBAGENT_ACCOUNT_TOOL,
|
|
10
|
+
label: "Set Subagent Account",
|
|
11
|
+
description:
|
|
12
|
+
"Set the account for spawned subagents. Does not affect the current session. " +
|
|
13
|
+
"By default (oneshot=true), the override applies only to the next subagent and is consumed. " +
|
|
14
|
+
"With oneshot=false, the override persists until cleared. " +
|
|
15
|
+
"Provide the account ID from list_accounts. Pass empty string to clear.",
|
|
16
|
+
promptSnippet: "Set the account for subagents",
|
|
17
|
+
promptGuidelines: [
|
|
18
|
+
"Use set_subagent_account before spawning a subagent when it needs a specific account.",
|
|
19
|
+
"With oneshot=true (default): the next subagent uses this account, then reverts.",
|
|
20
|
+
"With oneshot=false: all subsequent subagents use this account until cleared.",
|
|
21
|
+
"Use list_accounts first to discover available account IDs.",
|
|
22
|
+
],
|
|
23
|
+
parameters: Type.Object({
|
|
24
|
+
id: Type.String({ description: "Account ID to use for subagents. Empty string to clear." }),
|
|
25
|
+
oneshot: Type.Optional(Type.Boolean({ default: true, description: "If true (default), applies only to the next subagent. If false, persists until cleared." })),
|
|
26
|
+
}),
|
|
27
|
+
execute: async (_toolCallId, params: { id: string; oneshot?: boolean }, _signal, _onUpdate, _ctx) => {
|
|
28
|
+
const isOneShot = params.oneshot !== false;
|
|
29
|
+
|
|
30
|
+
if (!params.id) {
|
|
31
|
+
delete process.env.PI_ACCOUNT_SWITCHER_NEXT_ID;
|
|
32
|
+
delete process.env.PI_ACCOUNT_SWITCHER_ACTIVE_ID;
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: "Subagent override cleared. Subagents will inherit the parent's active account." }],
|
|
35
|
+
details: {},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const accounts = runtime.getAccounts();
|
|
40
|
+
const match = accounts.find((a) => a.id === params.id);
|
|
41
|
+
if (!match) {
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: "text", text: `Account not found: "${params.id}". Use the list_accounts tool to see available accounts.` }],
|
|
44
|
+
isError: true,
|
|
45
|
+
details: {},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (isOneShot) {
|
|
50
|
+
process.env.PI_ACCOUNT_SWITCHER_NEXT_ID = params.id;
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: "text", text: `One-shot set to: ${match.label} (${params.id}). The next subagent will use this account, then revert.` }],
|
|
53
|
+
details: {},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
process.env.PI_ACCOUNT_SWITCHER_ACTIVE_ID = params.id;
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: "text", text: `Persistent override set to: ${match.label} (${params.id}). All subagents will use this account until cleared.` }],
|
|
60
|
+
details: {},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { AccountConfig, ProviderConfig, SecretSource } from "@/types";
|
|
2
|
+
import type { AccountConfig, PiAuthEntry, ProviderConfig, SecretSource } from "@/types";
|
|
3
3
|
import { commonUtil, providerUtil, uiUtil } from "@/utils";
|
|
4
|
-
import { ACCOUNTS_PATH } from "@/constants";
|
|
4
|
+
import { ACCOUNTS_PATH, OAUTH_PROVIDER_IDS } from "@/constants";
|
|
5
5
|
|
|
6
6
|
export const SECRET_SOURCE_CHOICES = {
|
|
7
7
|
literal: "Paste API key now (stored in config)",
|
|
@@ -25,6 +25,8 @@ export class AccountConfigBuilder {
|
|
|
25
25
|
constructor(
|
|
26
26
|
private readonly ui: ExtensionUIContext,
|
|
27
27
|
private readonly customProviders: ProviderConfig[] = [],
|
|
28
|
+
private readonly piProviderIds: string[] = [],
|
|
29
|
+
private readonly getOAuthEntry?: (provider: string) => Promise<PiAuthEntry | undefined>,
|
|
28
30
|
original?: Partial<AccountConfig>,
|
|
29
31
|
) {
|
|
30
32
|
this.prompt = uiUtil.prompt(ui);
|
|
@@ -37,7 +39,7 @@ export class AccountConfigBuilder {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
async withProvider(): Promise<this> {
|
|
40
|
-
const choices = providerUtil.providerChoices(this.customProviders);
|
|
42
|
+
const choices = providerUtil.providerChoices(this.customProviders, this.piProviderIds);
|
|
41
43
|
const choice = await uiUtil.filteredSelect(this.ui, "Provider", choices);
|
|
42
44
|
if (!choice) return this;
|
|
43
45
|
|
|
@@ -65,14 +67,22 @@ export class AccountConfigBuilder {
|
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
async withId(): Promise<this> {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
// Auto-generate ID from label in add mode to reduce UX friction
|
|
71
|
+
if (!this.config.id) {
|
|
72
|
+
const suggested = commonUtil.slugify(this.config.label ?? "");
|
|
73
|
+
if (suggested) {
|
|
74
|
+
this.config.id = suggested;
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
// Fallback: prompt when label produces empty slug
|
|
78
|
+
const id = (await this.prompt("Account id", "account").asText()) || "account";
|
|
79
|
+
if (!id) {
|
|
80
|
+
throw new Error("Account id is required");
|
|
81
|
+
}
|
|
82
|
+
this.config.id = id;
|
|
83
|
+
return this;
|
|
73
84
|
}
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
// Edit mode: keep existing id
|
|
76
86
|
return this;
|
|
77
87
|
}
|
|
78
88
|
|
|
@@ -122,6 +132,21 @@ export class AccountConfigBuilder {
|
|
|
122
132
|
}
|
|
123
133
|
}
|
|
124
134
|
|
|
135
|
+
// OAuth-capable provider: check if Pi has stored credentials
|
|
136
|
+
if (!hasExistingCredentials && this.getOAuthEntry && (OAUTH_PROVIDER_IDS as readonly string[]).includes(provider)) {
|
|
137
|
+
const entry = await this.getOAuthEntry(provider);
|
|
138
|
+
if (entry) {
|
|
139
|
+
const useOAuth = await this.ui.confirm(
|
|
140
|
+
"Import Pi OAuth?",
|
|
141
|
+
`Pi has OAuth credentials for ${provider}. Import and use them for this account?`,
|
|
142
|
+
);
|
|
143
|
+
if (useOAuth) {
|
|
144
|
+
this.config.piAuth = { provider, entry };
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
125
150
|
const envKeys = providerUtil.requiredEnvKeysForProvider(provider, this.customProviders);
|
|
126
151
|
const envChoice = await this.ui.select("Credential env var", [
|
|
127
152
|
...envKeys,
|