@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.
- package/INSTALL_AS_PI_PACKAGE.md +78 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/USAGE.md +446 -0
- package/package.json +53 -0
- package/src/commands/accounts/add.ts +63 -0
- package/src/commands/accounts/edit.ts +31 -0
- package/src/commands/accounts/index.ts +19 -0
- package/src/commands/accounts/list.ts +31 -0
- package/src/commands/accounts/oauth.ts +59 -0
- package/src/commands/accounts/remove.ts +40 -0
- package/src/commands/accounts/shared/base.ts +88 -0
- package/src/commands/accounts/shared/index.ts +3 -0
- package/src/commands/accounts/shared/prompts.ts +226 -0
- package/src/commands/accounts/shared/select.ts +37 -0
- package/src/commands/accounts/switch.ts +54 -0
- package/src/commands/base.ts +81 -0
- package/src/commands/index.ts +16 -0
- package/src/commands/models/add.ts +30 -0
- package/src/commands/models/index.ts +13 -0
- package/src/commands/models/list.ts +45 -0
- package/src/commands/models/remove.ts +41 -0
- package/src/commands/models/shared/base.ts +44 -0
- package/src/commands/models/shared/index.ts +3 -0
- package/src/commands/models/shared/prompts.ts +36 -0
- package/src/commands/models/shared/select.ts +37 -0
- package/src/commands/providers/add.ts +28 -0
- package/src/commands/providers/edit.ts +29 -0
- package/src/commands/providers/index.ts +15 -0
- package/src/commands/providers/list.ts +38 -0
- package/src/commands/providers/remove.ts +46 -0
- package/src/commands/providers/shared/base.ts +30 -0
- package/src/commands/providers/shared/index.ts +3 -0
- package/src/commands/providers/shared/prompts.ts +172 -0
- package/src/commands/providers/shared/select.ts +24 -0
- package/src/commands/system/index.ts +7 -0
- package/src/commands/system/reset.ts +36 -0
- package/src/constants/commands.ts +66 -0
- package/src/constants/config.ts +6 -0
- package/src/constants/index.ts +4 -0
- package/src/constants/paths.ts +8 -0
- package/src/constants/providers.ts +36 -0
- package/src/extension.ts +21 -0
- package/src/index.ts +20 -0
- package/src/runtime/account-switcher-runtime.ts +194 -0
- package/src/runtime/account-switcher.ts +32 -0
- package/src/runtime/index.ts +11 -0
- package/src/schemas/accounts.ts +50 -0
- package/src/schemas/common.ts +3 -0
- package/src/schemas/config.ts +7 -0
- package/src/schemas/index.ts +4 -0
- package/src/schemas/providers.ts +57 -0
- package/src/services/accounts.ts +116 -0
- package/src/services/index.ts +4 -0
- package/src/services/models.ts +27 -0
- package/src/services/pi-auth.ts +23 -0
- package/src/services/providers.ts +123 -0
- package/src/storage/accounts.ts +109 -0
- package/src/storage/index.ts +4 -0
- package/src/storage/paths.ts +8 -0
- package/src/storage/pi-auth.ts +37 -0
- package/src/storage/providers.ts +85 -0
- package/src/storage/state.ts +43 -0
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +6 -0
- package/src/types/context.ts +3 -0
- package/src/types/index.ts +4 -0
- package/src/types/providers.ts +53 -0
- package/src/utils/accounts.ts +99 -0
- package/src/utils/common.ts +74 -0
- package/src/utils/errors.ts +16 -0
- package/src/utils/files.ts +25 -0
- package/src/utils/filterable-selector.ts +114 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/models.ts +76 -0
- package/src/utils/providers.ts +49 -0
- package/src/utils/ui.ts +49 -0
package/USAGE.md
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
# Pi Account Switcher — Install & Usage
|
|
2
|
+
|
|
3
|
+
This guide explains how to install, run, and use this extension in Pi.
|
|
4
|
+
|
|
5
|
+
## 1. Install Dependencies
|
|
6
|
+
|
|
7
|
+
From this repository:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Optional sanity check:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm run typecheck
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 2. Run Temporarily for Testing
|
|
20
|
+
|
|
21
|
+
The fastest way to test the extension is with Pi's `-e` / `--extension` flag:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pi -e ./src/extension.ts
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then, inside Pi, add your first account:
|
|
28
|
+
|
|
29
|
+
```txt
|
|
30
|
+
/accounts:add
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
To reload after manually editing the config file, use Pi's built-in:
|
|
34
|
+
|
|
35
|
+
```txt
|
|
36
|
+
/reload
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 3. Install as a Project-local Pi Extension
|
|
40
|
+
|
|
41
|
+
To make the extension auto-load for this project, place it under `.pi/extensions/` or configure it as a package.
|
|
42
|
+
|
|
43
|
+
Recommended project-local setup:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
mkdir -p .pi/extensions/account-switcher
|
|
47
|
+
cp -R src package.json package-lock.json tsconfig.json .pi/extensions/account-switcher/
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then start Pi from the project directory:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pi
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If Pi is already running, use:
|
|
57
|
+
|
|
58
|
+
```txt
|
|
59
|
+
/reload
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Pi auto-discovers extensions from:
|
|
63
|
+
|
|
64
|
+
```txt
|
|
65
|
+
.pi/extensions/*.ts
|
|
66
|
+
.pi/extensions/*/index.ts
|
|
67
|
+
~/.pi/agent/extensions/*.ts
|
|
68
|
+
~/.pi/agent/extensions/*/index.ts
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The easiest dev command is:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pi -e ./src/extension.ts
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 4. Install Globally for All Pi Projects
|
|
78
|
+
|
|
79
|
+
To use the extension globally:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
mkdir -p ~/.pi/agent/extensions/account-switcher
|
|
83
|
+
cp -R src package.json package-lock.json tsconfig.json ~/.pi/agent/extensions/account-switcher/
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then start Pi anywhere:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pi
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Or reload an existing Pi session:
|
|
93
|
+
|
|
94
|
+
```txt
|
|
95
|
+
/reload
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## 5. OAuth Login Like Pi `/login`
|
|
99
|
+
|
|
100
|
+
For subscription/OAuth providers, use Pi's built-in login first, then import that login as a named switchable account.
|
|
101
|
+
|
|
102
|
+
```txt
|
|
103
|
+
/login
|
|
104
|
+
/accounts:oauth
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
To add another OAuth account for the same provider, run `/login` again with the other browser account, then run `/accounts:oauth` again with a different label.
|
|
108
|
+
|
|
109
|
+
Switch OAuth accounts with:
|
|
110
|
+
|
|
111
|
+
```txt
|
|
112
|
+
/accounts:list
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
OAuth credentials are captured from Pi's auth file:
|
|
116
|
+
|
|
117
|
+
```txt
|
|
118
|
+
~/.pi/agent/auth.json
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
When switching OAuth accounts, this extension applies the stored credentials to Pi's live auth storage and clears cached provider sessions when Pi exposes cleanup hooks.
|
|
122
|
+
|
|
123
|
+
## 6. Configure API-key Accounts from Inside Pi
|
|
124
|
+
|
|
125
|
+
You can add API-key accounts directly from Pi without hand-writing JSON.
|
|
126
|
+
|
|
127
|
+
### Add an account
|
|
128
|
+
|
|
129
|
+
```txt
|
|
130
|
+
/accounts:add
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
This opens a wizard for provider, label, id, credential env var, and secret source, then optionally activates the new account. If the id already exists, choose replace, enter a new id, or cancel. For custom model providers, choose a default model, then paste an account API key override or leave it blank to use the provider-level `apiKey`. Switching to that account re-registers the provider key and switches Pi to the account model. If you enter a free-text custom provider, Pi can save it as a reusable provider.
|
|
134
|
+
|
|
135
|
+
The wizard supports secret sources from pasted API key, env var, file, shell command, or 1Password `op://` reference.
|
|
136
|
+
|
|
137
|
+
Warning: if you choose `Paste API key now`, the key is written as plain text to:
|
|
138
|
+
|
|
139
|
+
```txt
|
|
140
|
+
~/.pi/account-switcher/accounts.json
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Prefer env vars, files with restricted permissions, or 1Password references.
|
|
144
|
+
|
|
145
|
+
### Manage custom providers
|
|
146
|
+
|
|
147
|
+
```txt
|
|
148
|
+
/providers:list
|
|
149
|
+
/providers:add
|
|
150
|
+
/providers:edit
|
|
151
|
+
/providers:remove
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Custom providers are stored separately from accounts:
|
|
155
|
+
|
|
156
|
+
```txt
|
|
157
|
+
~/.pi/account-switcher/providers.json
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Built-in providers are read-only. Removing a custom provider is blocked while accounts still use it. Provider entries may also include Pi model-provider fields like `baseUrl`, `api`, `apiKey`, `compat`, and `models`; account-switcher registers those providers with Pi.
|
|
161
|
+
|
|
162
|
+
`apiKey` can be an env var name, shell command (`!op read ...`), or raw key. Raw keys work, but they are stored in plaintext in `providers.json`; prefer env/file/1Password when possible.
|
|
163
|
+
|
|
164
|
+
Example provider config:
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"providers": {
|
|
169
|
+
"acme": {
|
|
170
|
+
"name": "Acme AI",
|
|
171
|
+
"baseUrl": "https://api.acme.test/v1",
|
|
172
|
+
"api": "openai-completions",
|
|
173
|
+
"apiKey": "ACME_API_KEY",
|
|
174
|
+
"envKeys": ["ACME_API_KEY"],
|
|
175
|
+
"aliases": ["acme-ai"],
|
|
176
|
+
"models": [{ "id": "acme-coder", "name": "Acme Coder" }]
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## 7. Configure Accounts Manually
|
|
183
|
+
|
|
184
|
+
Account config lives at:
|
|
185
|
+
|
|
186
|
+
```txt
|
|
187
|
+
~/.pi/account-switcher/accounts.json
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Example config:
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"switchMode": "env",
|
|
195
|
+
"accounts": [
|
|
196
|
+
{
|
|
197
|
+
"id": "claude-work",
|
|
198
|
+
"label": "Claude — Work",
|
|
199
|
+
"provider": "anthropic",
|
|
200
|
+
"env": {
|
|
201
|
+
"ANTHROPIC_API_KEY": { "type": "env", "name": "ANTHROPIC_WORK_API_KEY" }
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"id": "claude-personal",
|
|
206
|
+
"label": "Claude — Personal",
|
|
207
|
+
"provider": "anthropic",
|
|
208
|
+
"env": {
|
|
209
|
+
"ANTHROPIC_API_KEY": { "type": "file", "path": "~/.keys/claude-personal.txt" }
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"id": "codex-client-a",
|
|
214
|
+
"label": "Codex — Client A",
|
|
215
|
+
"provider": "openai",
|
|
216
|
+
"env": {
|
|
217
|
+
"OPENAI_API_KEY": { "type": "op", "reference": "op://AI/CodexClientA/api-key" }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## 8. Supported Secret Sources
|
|
225
|
+
|
|
226
|
+
### Literal value
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"OPENAI_API_KEY": { "type": "literal", "value": "sk-..." }
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Existing environment variable
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"ANTHROPIC_API_KEY": { "type": "env", "name": "ANTHROPIC_WORK_API_KEY" }
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### File
|
|
243
|
+
|
|
244
|
+
```json
|
|
245
|
+
{
|
|
246
|
+
"ANTHROPIC_API_KEY": { "type": "file", "path": "~/.keys/claude-work.txt" }
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Shell command
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"ANTHROPIC_API_KEY": {
|
|
255
|
+
"type": "command",
|
|
256
|
+
"command": "op read op://AI/ClaudeWork/api-key"
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### 1Password reference
|
|
262
|
+
|
|
263
|
+
```json
|
|
264
|
+
{
|
|
265
|
+
"OPENAI_API_KEY": {
|
|
266
|
+
"type": "op",
|
|
267
|
+
"reference": "op://AI/CodexClientA/api-key"
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
A plain string is treated as a literal value, except strings beginning with `op://` are resolved using `op read`.
|
|
273
|
+
|
|
274
|
+
## 9. Commands
|
|
275
|
+
|
|
276
|
+
### Pick account for current provider
|
|
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
|
|
293
|
+
|
|
294
|
+
```txt
|
|
295
|
+
/accounts:list
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Import current Pi OAuth login
|
|
299
|
+
|
|
300
|
+
```txt
|
|
301
|
+
/accounts:oauth
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Use this after Pi's built-in `/login`.
|
|
305
|
+
|
|
306
|
+
### Add account interactively
|
|
307
|
+
|
|
308
|
+
```txt
|
|
309
|
+
/accounts:add
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Login/add account and activate it
|
|
313
|
+
|
|
314
|
+
```txt
|
|
315
|
+
/accounts:add
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Edit account
|
|
319
|
+
|
|
320
|
+
```txt
|
|
321
|
+
/accounts:edit
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Edit label, provider, id, and env credential source. Blank text input keeps the existing value. Literal secret values are not displayed by default.
|
|
325
|
+
|
|
326
|
+
### Remove account
|
|
327
|
+
|
|
328
|
+
```txt
|
|
329
|
+
/accounts:remove
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Shows a non-secret summary, asks for confirmation, deletes the account, and clears stale saved selections.
|
|
333
|
+
|
|
334
|
+
### Manage custom providers
|
|
335
|
+
|
|
336
|
+
```txt
|
|
337
|
+
/providers:list
|
|
338
|
+
/providers:add
|
|
339
|
+
/providers:edit
|
|
340
|
+
/providers:remove
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## 10. Switching Flow
|
|
344
|
+
|
|
345
|
+
Typical usage:
|
|
346
|
+
|
|
347
|
+
1. Start Pi:
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
pi -e ./src/extension.ts
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
2. For OAuth/subscription accounts, login with Pi and import it:
|
|
354
|
+
|
|
355
|
+
```txt
|
|
356
|
+
/login
|
|
357
|
+
/accounts:oauth
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
For API-key accounts, add and activate an account:
|
|
361
|
+
|
|
362
|
+
```txt
|
|
363
|
+
/accounts:add
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
3. Later, switch accounts:
|
|
367
|
+
|
|
368
|
+
```txt
|
|
369
|
+
/accounts:list
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Alternative manual config flow: edit `~/.pi/account-switcher/accounts.json` directly, then reload Pi:
|
|
373
|
+
|
|
374
|
+
6. If needed, reload Pi runtime:
|
|
375
|
+
|
|
376
|
+
```txt
|
|
377
|
+
/reload
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## 11. State Persistence
|
|
381
|
+
|
|
382
|
+
Selected accounts are saved at:
|
|
383
|
+
|
|
384
|
+
```txt
|
|
385
|
+
~/.pi/account-switcher/state.json
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Example:
|
|
389
|
+
|
|
390
|
+
```json
|
|
391
|
+
{
|
|
392
|
+
"activeAccountId": "claude-work",
|
|
393
|
+
"activeModelId": "claude-sonnet-4",
|
|
394
|
+
"activeModelProvider": "anthropic"
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
On Pi session start, the extension restores the saved active account and model state.
|
|
399
|
+
|
|
400
|
+
## 12. Important Note About Credential Caching
|
|
401
|
+
|
|
402
|
+
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
|
+
|
|
404
|
+
If a provider still keeps old credentials cached, run `/reload` or restart Pi.
|
|
405
|
+
|
|
406
|
+
## 13. Troubleshooting
|
|
407
|
+
|
|
408
|
+
### No accounts configured
|
|
409
|
+
|
|
410
|
+
Run `/accounts:add` to create one interactively, or create `~/.pi/account-switcher/accounts.json` manually.
|
|
411
|
+
|
|
412
|
+
### No accounts for provider
|
|
413
|
+
|
|
414
|
+
Run explicitly:
|
|
415
|
+
|
|
416
|
+
```txt
|
|
417
|
+
/accounts:list
|
|
418
|
+
```
|
|
419
|
+
|
|
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`
|
|
428
|
+
|
|
429
|
+
### Secret resolves empty
|
|
430
|
+
|
|
431
|
+
Check the configured secret source:
|
|
432
|
+
|
|
433
|
+
- env var exists
|
|
434
|
+
- file exists and contains the key
|
|
435
|
+
- command works manually
|
|
436
|
+
- `op` CLI is signed in
|
|
437
|
+
|
|
438
|
+
### Changes do not apply
|
|
439
|
+
|
|
440
|
+
Switch the account again. If the provider still keeps old credentials cached, run:
|
|
441
|
+
|
|
442
|
+
```txt
|
|
443
|
+
/reload
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
If it still uses the old account, restart Pi.
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hieplp/pi-account-switcher",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Pi extension for quickly switching between multiple accounts/API keys per provider.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package",
|
|
9
|
+
"pi-extension",
|
|
10
|
+
"pi",
|
|
11
|
+
"account-switcher"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/hieplp/pi-account-switcher.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/hieplp/pi-account-switcher/issues"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/hieplp/pi-account-switcher#readme",
|
|
21
|
+
"files": [
|
|
22
|
+
"src",
|
|
23
|
+
"!src/**/*.test.ts",
|
|
24
|
+
"README.md",
|
|
25
|
+
"USAGE.md",
|
|
26
|
+
"INSTALL_AS_PI_PACKAGE.md"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"test": "vitest run"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@mariozechner/jiti": "^2.6.5",
|
|
34
|
+
"@mariozechner/pi-tui": "^0.73.1",
|
|
35
|
+
"zod": "^4.4.3"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@mariozechner/pi-ai": "*",
|
|
39
|
+
"@mariozechner/pi-coding-agent": "*"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@mariozechner/pi-ai": "latest",
|
|
43
|
+
"@mariozechner/pi-coding-agent": "latest",
|
|
44
|
+
"@types/node": "latest",
|
|
45
|
+
"typescript": "latest",
|
|
46
|
+
"vitest": "latest"
|
|
47
|
+
},
|
|
48
|
+
"pi": {
|
|
49
|
+
"extensions": [
|
|
50
|
+
"./src/extension.ts"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { AccountSwitcher } from "@/runtime";
|
|
3
|
+
import type { AccountConfig, AccountSwitcherContext } from "@/types";
|
|
4
|
+
import { COMMANDS } from "@/constants";
|
|
5
|
+
import { errorUtil, providerUtil } from "@/utils";
|
|
6
|
+
import { AccountCommand } from "./shared";
|
|
7
|
+
import { AccountConfigBuilder } from "./shared/prompts";
|
|
8
|
+
|
|
9
|
+
export const useAddAccountCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
10
|
+
new AddAccountCommand(pi, runtime).register();
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
class AddAccountCommand extends AccountCommand {
|
|
14
|
+
constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
|
|
15
|
+
super(pi, runtime, COMMANDS.accounts.add);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async handler(ctx: AccountSwitcherContext): Promise<void> {
|
|
19
|
+
try {
|
|
20
|
+
await this.runtime.load();
|
|
21
|
+
const providers = this.runtime.getProviders();
|
|
22
|
+
|
|
23
|
+
const account = await new AccountConfigBuilder(ctx.ui, providers).collect();
|
|
24
|
+
if (!account) return;
|
|
25
|
+
|
|
26
|
+
await this.saveProvider(ctx, account);
|
|
27
|
+
const saved = await this.saveAccount(ctx, account);
|
|
28
|
+
if (!saved) return;
|
|
29
|
+
|
|
30
|
+
ctx.ui.notify(`Added account ${saved.label}.`, "info");
|
|
31
|
+
|
|
32
|
+
const activate = await ctx.ui.confirm(
|
|
33
|
+
"Activate now?",
|
|
34
|
+
`Switch ${providerUtil.normalizeProvider(saved.provider)} to ${saved.label} now?`,
|
|
35
|
+
);
|
|
36
|
+
if (activate) {
|
|
37
|
+
const applied = await this.runtime.activateAccount(saved, ctx);
|
|
38
|
+
const detail = applied ? ` (${applied})` : "";
|
|
39
|
+
ctx.ui.notify(`Activated ${saved.label}${detail}.`, "info");
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
ctx.ui.notify(`Failed to add account: ${errorUtil.format(error)}`, "error");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private async saveProvider(ctx: AccountSwitcherContext, account: AccountConfig): Promise<void> {
|
|
47
|
+
const providerId = providerUtil.normalizeProvider(account.provider);
|
|
48
|
+
|
|
49
|
+
if (providerUtil.isBuiltInProviderId(providerId)) return;
|
|
50
|
+
|
|
51
|
+
if (providerUtil.hasProvider(providerId, this.runtime.getProviders())) return;
|
|
52
|
+
|
|
53
|
+
const save = await ctx.ui.confirm(
|
|
54
|
+
"Save custom provider?",
|
|
55
|
+
`Save ${providerId} as a reusable custom provider for future account setup?`,
|
|
56
|
+
);
|
|
57
|
+
if (!save) return;
|
|
58
|
+
|
|
59
|
+
const provider = { id: providerId, label: providerId, envKeys: Object.keys(account.env ?? {}) };
|
|
60
|
+
await this.runtime.addProvider(provider);
|
|
61
|
+
ctx.ui.notify(`Saved custom provider ${provider.id}.`, "info");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { AccountSwitcher } from "@/runtime";
|
|
3
|
+
import type { AccountSwitcherContext } from "@/types";
|
|
4
|
+
import { COMMANDS } from "@/constants";
|
|
5
|
+
import { errorUtil } from "@/utils";
|
|
6
|
+
import { AccountCommand, AccountConfigBuilder } from "./shared";
|
|
7
|
+
|
|
8
|
+
export const useEditAccountCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
9
|
+
new EditAccountCommand(pi, runtime).register();
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
class EditAccountCommand extends AccountCommand {
|
|
13
|
+
constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
|
|
14
|
+
super(pi, runtime, COMMANDS.accounts.edit);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async handler(ctx: AccountSwitcherContext): Promise<void> {
|
|
18
|
+
try {
|
|
19
|
+
const original = await this.loadAndSelectAccount(ctx, "Select account to edit");
|
|
20
|
+
if (!original) return;
|
|
21
|
+
|
|
22
|
+
const updated = await new AccountConfigBuilder(ctx.ui, this.runtime.getProviders(), original).collect();
|
|
23
|
+
if (!updated) return;
|
|
24
|
+
|
|
25
|
+
await this.runtime.editAccount(original, updated);
|
|
26
|
+
ctx.ui.notify(`Account "${updated.label}" updated.`, "info");
|
|
27
|
+
} catch (e) {
|
|
28
|
+
ctx.ui.notify(`Failed to edit account: ${errorUtil.format(e)}`, "error");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { AccountSwitcher } from "@/runtime";
|
|
3
|
+
import { useAddAccountCommand } from "./add";
|
|
4
|
+
import { useEditAccountCommand } from "./edit";
|
|
5
|
+
import { useListAccountsCommand } from "./list";
|
|
6
|
+
import { useOAuthImportCommand } from "./oauth";
|
|
7
|
+
import { useRemoveAccountCommand } from "./remove";
|
|
8
|
+
import { useSwitchAccountCommand } from "./switch";
|
|
9
|
+
|
|
10
|
+
const useAccountCommands = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
11
|
+
useAddAccountCommand(pi, runtime);
|
|
12
|
+
useEditAccountCommand(pi, runtime);
|
|
13
|
+
useListAccountsCommand(pi, runtime);
|
|
14
|
+
useOAuthImportCommand(pi, runtime);
|
|
15
|
+
useRemoveAccountCommand(pi, runtime);
|
|
16
|
+
useSwitchAccountCommand(pi, runtime);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default useAccountCommands;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { AccountSwitcher } from "@/runtime";
|
|
3
|
+
import type { AccountSwitcherContext } from "@/types";
|
|
4
|
+
import { COMMANDS } from "@/constants";
|
|
5
|
+
import { errorUtil } from "@/utils";
|
|
6
|
+
import { AccountCommand } from "./shared";
|
|
7
|
+
|
|
8
|
+
export const useListAccountsCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
9
|
+
new ListAccountsCommand(pi, runtime).register();
|
|
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;
|
|
21
|
+
|
|
22
|
+
const account = await this.pickGroupedAccount(ctx, accounts, "Pick account to activate");
|
|
23
|
+
if (!account) return;
|
|
24
|
+
|
|
25
|
+
const applied = await this.runtime.activateAccount(account, ctx);
|
|
26
|
+
ctx.ui.notify(`Switched to ${account.label} (${applied}).`, "info");
|
|
27
|
+
} catch (error) {
|
|
28
|
+
ctx.ui.notify(`Failed to list accounts: ${errorUtil.format(error)}`, "error");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { AccountSwitcher } from "@/runtime";
|
|
3
|
+
import type { AccountConfig, AccountSwitcherContext } from "@/types";
|
|
4
|
+
import { COMMANDS, OAUTH_PROVIDER_IDS, PI_AUTH_PATH } from "@/constants";
|
|
5
|
+
import { commonUtil, errorUtil } from "@/utils";
|
|
6
|
+
import { AccountCommand } from "./shared";
|
|
7
|
+
|
|
8
|
+
export const useOAuthImportCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
9
|
+
new OAuthImportCommand(pi, runtime).register();
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
class OAuthImportCommand extends AccountCommand {
|
|
13
|
+
constructor(pi: ExtensionAPI, runtime: AccountSwitcher) {
|
|
14
|
+
super(pi, runtime, COMMANDS.accounts.oauth);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async handler(ctx: AccountSwitcherContext): Promise<void> {
|
|
18
|
+
try {
|
|
19
|
+
await this.runtime.load();
|
|
20
|
+
|
|
21
|
+
const providerChoice = await ctx.ui.select("Provider logged in with Pi /login", [...OAUTH_PROVIDER_IDS]);
|
|
22
|
+
if (!providerChoice) return;
|
|
23
|
+
|
|
24
|
+
const provider =
|
|
25
|
+
providerChoice === "custom"
|
|
26
|
+
? (await ctx.ui.input("Pi auth provider id", "provider-id"))?.trim()
|
|
27
|
+
: providerChoice;
|
|
28
|
+
if (!provider) return;
|
|
29
|
+
|
|
30
|
+
const entry = await this.runtime.getPiAuthEntry(provider);
|
|
31
|
+
if (!entry) {
|
|
32
|
+
ctx.ui.notify(`No Pi auth entry for "${provider}". Run /login ${provider} first, then try again.`, "error");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!this.runtime.isOAuthEntry(entry)) {
|
|
37
|
+
const ok = await ctx.ui.confirm(
|
|
38
|
+
"Import non-OAuth auth entry?",
|
|
39
|
+
`"${provider}" exists in ${PI_AUTH_PATH} but is not marked as OAuth. Import anyway?`,
|
|
40
|
+
);
|
|
41
|
+
if (!ok) return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const label = (await ctx.ui.input("Account label", `${provider} — Work`))?.trim();
|
|
45
|
+
if (!label) return;
|
|
46
|
+
|
|
47
|
+
const suggested = commonUtil.slugify(label);
|
|
48
|
+
const id = (await ctx.ui.input("Account id", suggested))?.trim() || suggested;
|
|
49
|
+
|
|
50
|
+
const account: AccountConfig = { id, label, provider, piAuth: { provider, entry } };
|
|
51
|
+
const saved = await this.saveAccount(ctx, account);
|
|
52
|
+
if (!saved) return;
|
|
53
|
+
|
|
54
|
+
ctx.ui.notify(`Imported OAuth account "${saved.label}".`, "info");
|
|
55
|
+
} catch (error) {
|
|
56
|
+
ctx.ui.notify(`Failed to import OAuth account: ${errorUtil.format(error)}`, "error");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|