pi-chatgpt-pool 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 +8 -3
- package/package.json +1 -1
- package/src/index.ts +57 -9
- package/src/panel.ts +11 -6
- package/src/pool.ts +5 -1
package/README.md
CHANGED
|
@@ -12,15 +12,16 @@ Requires pi 0.99.1 or later. The plugin uses pi's own Sign in with ChatGPT flow,
|
|
|
12
12
|
|
|
13
13
|
## Set up accounts
|
|
14
14
|
|
|
15
|
-
Run `/chatgpt-pool`
|
|
15
|
+
Run `/chatgpt-pool`, press `a` to add an account, then Enter to sign in. Or from the editor:
|
|
16
16
|
|
|
17
17
|
```text
|
|
18
18
|
/chatgpt-pool add personal
|
|
19
19
|
/chatgpt-pool add work
|
|
20
|
-
/login
|
|
21
20
|
```
|
|
22
21
|
|
|
23
|
-
`/login
|
|
22
|
+
After adding an account, pi fills in `/login chatgpt-<n>` for it; press Enter to open the ChatGPT sign-in. Sign in to each account with a different ChatGPT account. `/chatgpt-pool login <label>` does the same for an existing account.
|
|
23
|
+
|
|
24
|
+
`/login` lists each account as `ChatGPT (<label>)`, or under a name you choose with `/chatgpt-pool name`. Renaming keeps the account signed in. Sign in to each one with a different ChatGPT account. Then pick a pooled model:
|
|
24
25
|
|
|
25
26
|
```text
|
|
26
27
|
/model chatgpt/gpt-5.6-sol
|
|
@@ -34,6 +35,8 @@ The pool offers the ChatGPT subscription models: the OpenAI models that pi also
|
|
|
34
35
|
| --- | --- |
|
|
35
36
|
| `/chatgpt-pool` | Open the pool panel (see below) |
|
|
36
37
|
| `/chatgpt-pool add <label>` | Add an account slot. Sign in to it with `/login` |
|
|
38
|
+
| `/chatgpt-pool login <label>` | Fill in `/login` for that account, so Enter starts its sign-in |
|
|
39
|
+
| `/chatgpt-pool name <label> <name>` | Set the name the account shows in `/login`, for example `Work (jal@mastra.ai)`. Leave the name empty to go back to `ChatGPT (<label>)` |
|
|
37
40
|
| `/chatgpt-pool remove <label>` | Remove an account. Run `/logout` for it first to delete its token |
|
|
38
41
|
| `/chatgpt-pool spend <label>` | Spend one banked reset on that account, after you confirm |
|
|
39
42
|
| `/chatgpt-pool strategy <name>` | Set the routing strategy: `fill-first`, `round-robin`, `least-used`, or `use-it-or-lose-it` |
|
|
@@ -48,8 +51,10 @@ The command autocompletes its actions and account labels.
|
|
|
48
51
|
| Key | Action |
|
|
49
52
|
| --- | --- |
|
|
50
53
|
| `↑` `↓` or `j` `k` | Select an account |
|
|
54
|
+
| `enter` | Sign in to the selected account (fills in `/login` for it; press Enter again) |
|
|
51
55
|
| `s` | Spend a banked reset on the selected account, after you confirm |
|
|
52
56
|
| `c` | Clear the selected account's recorded limit |
|
|
57
|
+
| `n` | Set the name the selected account shows in `/login` |
|
|
53
58
|
| `d` | Remove the selected account |
|
|
54
59
|
| `a` | Add an account |
|
|
55
60
|
| `t` | Choose the strategy |
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isStrategy,
|
|
9
9
|
failoverMessage,
|
|
10
10
|
limitCooldown,
|
|
11
|
+
loginName,
|
|
11
12
|
nextAccountId,
|
|
12
13
|
orderAccounts,
|
|
13
14
|
parseResetAt,
|
|
@@ -79,7 +80,7 @@ function subscriptionModels(providers: readonly Provider[]): Model<Api>[] {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
function accountProvider(openai: Provider, account: Account, onReset: ResetListener): Provider {
|
|
82
|
-
const name =
|
|
83
|
+
const name = loginName(account);
|
|
83
84
|
const signIn = openai.auth.oauth;
|
|
84
85
|
if (!signIn) throw new Error("pi's openai provider has no Sign in with ChatGPT flow; update pi to 0.99.1 or later");
|
|
85
86
|
const models: Model<Api>[] = subscriptionModels(builtinProviders()).map((model) => ({
|
|
@@ -283,7 +284,7 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
283
284
|
|
|
284
285
|
const describe = ({ account, signedIn, usage, error }: AccountStatus): string => {
|
|
285
286
|
const limitedUntil = account.resetsAt && account.resetsAt > Date.now() ? account.resetsAt : undefined;
|
|
286
|
-
if (!signedIn) return `${account.label} · not signed in, run /login
|
|
287
|
+
if (!signedIn) return `${account.label} · not signed in, run /chatgpt-pool login ${account.label}`;
|
|
287
288
|
const state = limitedUntil ? `limited until ${formatReset(limitedUntil)}` : "ready";
|
|
288
289
|
if (!usage) return `${account.label} · ${state} · usage unavailable${error ? ` (${error})` : ""}`;
|
|
289
290
|
const windows = usage.windows.map(
|
|
@@ -305,7 +306,34 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
305
306
|
update((config) => ({ ...config, accounts: [...config.accounts, account] }));
|
|
306
307
|
registerPooledModels();
|
|
307
308
|
pi.registerProvider(accountProvider(openai, account, onReset));
|
|
308
|
-
ctx.ui.notify(`Added
|
|
309
|
+
ctx.ui.notify(`Added ${loginName(account)}.`, "info");
|
|
310
|
+
startLogin(ctx, account);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const setLoginName = (ctx: ExtensionContext, account: Account, name: string) => {
|
|
314
|
+
const next = { ...account, name: name.trim() || undefined };
|
|
315
|
+
update((config) => ({
|
|
316
|
+
...config,
|
|
317
|
+
accounts: config.accounts.map((candidate) => (candidate.id === account.id ? next : candidate)),
|
|
318
|
+
}));
|
|
319
|
+
pi.registerProvider(accountProvider(openai, next, onReset));
|
|
320
|
+
ctx.ui.notify(`${account.label} now shows as "${loginName(next)}" in /login. Its sign-in is unchanged.`, "info");
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const askLoginName = async (ctx: ExtensionContext, account: Account) => {
|
|
324
|
+
const name = await ctx.ui.input(
|
|
325
|
+
`Name shown in /login for ${account.label} (empty for "ChatGPT (${account.label})")`,
|
|
326
|
+
loginName(account),
|
|
327
|
+
);
|
|
328
|
+
if (name !== undefined) setLoginName(ctx, account, name);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const startLogin = (ctx: ExtensionContext, account: Account) => {
|
|
332
|
+
if (ctx.mode !== "tui") {
|
|
333
|
+
return ctx.ui.notify(`Run /login ${account.id} to sign in to ${loginName(account)}.`, "info");
|
|
334
|
+
}
|
|
335
|
+
ctx.ui.setEditorText(`/login ${account.id}`);
|
|
336
|
+
ctx.ui.notify(`Press Enter to sign in to ${loginName(account)}.`, "info");
|
|
309
337
|
};
|
|
310
338
|
|
|
311
339
|
const removeAccount = (ctx: ExtensionContext, account: Account) => {
|
|
@@ -379,13 +407,16 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
379
407
|
reload = true;
|
|
380
408
|
} else if (action.type === "add") {
|
|
381
409
|
const label = (await ctx.ui.input("Label for the new ChatGPT account", "work"))?.trim();
|
|
382
|
-
if (label) addAccount(ctx, label);
|
|
383
|
-
reload = !!label;
|
|
410
|
+
if (label) return addAccount(ctx, label);
|
|
384
411
|
} else if (action.type === "strategy") {
|
|
385
412
|
await chooseStrategy(ctx);
|
|
386
413
|
} else if (status && action.type === "spend") {
|
|
387
414
|
await spend(ctx, status.account, status.usage);
|
|
388
415
|
reload = true;
|
|
416
|
+
} else if (status && action.type === "login") {
|
|
417
|
+
return startLogin(ctx, status.account);
|
|
418
|
+
} else if (status && action.type === "name") {
|
|
419
|
+
await askLoginName(ctx, status.account);
|
|
389
420
|
} else if (status && action.type === "clear") {
|
|
390
421
|
setResetsAt(status.account.id, undefined);
|
|
391
422
|
ctx.ui.notify(`ChatGPT (${status.account.label}) is back in rotation.`, "info");
|
|
@@ -441,12 +472,14 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
441
472
|
{ value: "add", label: "add", description: "Add an account slot" },
|
|
442
473
|
{ value: "remove", label: "remove", description: "Remove an account" },
|
|
443
474
|
{ value: "spend", label: "spend", description: "Spend a banked reset, after confirming" },
|
|
475
|
+
{ value: "login", label: "login", description: "Sign in to an account" },
|
|
476
|
+
{ value: "name", label: "name", description: "Set the name an account shows in /login" },
|
|
444
477
|
{ value: "reset", label: "reset", description: "Forget recorded limits" },
|
|
445
478
|
{ value: "strategy", label: "strategy", description: "Choose how new conversations pick an account" },
|
|
446
479
|
];
|
|
447
480
|
|
|
448
481
|
pi.registerCommand("chatgpt-pool", {
|
|
449
|
-
description: "Manage pooled ChatGPT accounts: usage, banked resets, strategy, add, remove, spend, reset",
|
|
482
|
+
description: "Manage pooled ChatGPT accounts: usage, banked resets, strategy, add, login, remove, name, spend, reset",
|
|
450
483
|
getArgumentCompletions(prefix) {
|
|
451
484
|
const [action, ...rest] = prefix.split(" ");
|
|
452
485
|
if (rest.length === 0) return SUBCOMMANDS.filter((item) => item.value.startsWith(action));
|
|
@@ -455,7 +488,8 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
455
488
|
.filter(([name]) => name.startsWith(rest.join(" ")))
|
|
456
489
|
.map(([name, description]) => ({ value: `strategy ${name}`, label: name, description }));
|
|
457
490
|
}
|
|
458
|
-
if (
|
|
491
|
+
if (!["remove", "spend", "name", "login"].includes(action)) return null;
|
|
492
|
+
if (action === "name" && rest.length > 1) return null;
|
|
459
493
|
const partial = rest.join(" ");
|
|
460
494
|
return accounts
|
|
461
495
|
.filter((account) => account.label.startsWith(partial))
|
|
@@ -476,8 +510,22 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
476
510
|
return removeAccount(ctx, account);
|
|
477
511
|
}
|
|
478
512
|
|
|
513
|
+
if (action === "login") {
|
|
514
|
+
const account = findAccount(label);
|
|
515
|
+
if (!account) return ctx.ui.notify("Usage: /chatgpt-pool login <label>", "warning");
|
|
516
|
+
return startLogin(ctx, account);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (action === "name") {
|
|
520
|
+
const [target = "", ...nameParts] = rest;
|
|
521
|
+
const account = findAccount(target);
|
|
522
|
+
if (!account) return ctx.ui.notify("Usage: /chatgpt-pool name <label> <name shown in /login>", "warning");
|
|
523
|
+
if (nameParts.length > 0 || !ctx.hasUI) return setLoginName(ctx, account, nameParts.join(" "));
|
|
524
|
+
return askLoginName(ctx, account);
|
|
525
|
+
}
|
|
526
|
+
|
|
479
527
|
if (action === "reset") {
|
|
480
|
-
update((config) => ({ ...config, accounts: config.accounts.map((
|
|
528
|
+
update((config) => ({ ...config, accounts: config.accounts.map((account) => ({ ...account, resetsAt: undefined })) }));
|
|
481
529
|
updateStatus(ctx);
|
|
482
530
|
return ctx.ui.notify("Cleared recorded limits. Every signed-in account is back in rotation.", "info");
|
|
483
531
|
}
|
|
@@ -502,7 +550,7 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
502
550
|
return spend(ctx, account);
|
|
503
551
|
}
|
|
504
552
|
|
|
505
|
-
if (action) return ctx.ui.notify(`Unknown action "${action}". Use add, remove, spend, reset, or strategy.`, "warning");
|
|
553
|
+
if (action) return ctx.ui.notify(`Unknown action "${action}". Use add, login, remove, name, spend, reset, or strategy.`, "warning");
|
|
506
554
|
if (accounts.length === 0) {
|
|
507
555
|
const label = (await ctx.ui.input("No ChatGPT accounts yet. Label for the first one", "personal"))?.trim();
|
|
508
556
|
if (label) addAccount(ctx, label);
|
package/src/panel.ts
CHANGED
|
@@ -11,12 +11,13 @@ export type PanelAction =
|
|
|
11
11
|
| { type: "refresh" }
|
|
12
12
|
| { type: "add" }
|
|
13
13
|
| { type: "strategy" }
|
|
14
|
-
| { type: "spend" | "clear" | "remove"; accountId: string };
|
|
14
|
+
| { type: "spend" | "clear" | "remove" | "name" | "login"; accountId: string };
|
|
15
15
|
|
|
16
|
-
const ACCOUNT_KEYS = new Map<string, "spend" | "clear" | "remove">([
|
|
16
|
+
const ACCOUNT_KEYS = new Map<string, "spend" | "clear" | "remove" | "name">([
|
|
17
17
|
["s", "spend"],
|
|
18
18
|
["c", "clear"],
|
|
19
19
|
["d", "remove"],
|
|
20
|
+
["n", "name"],
|
|
20
21
|
]);
|
|
21
22
|
const POOL_KEYS = new Map<string, "refresh" | "add" | "strategy">([
|
|
22
23
|
["r", "refresh"],
|
|
@@ -25,9 +26,9 @@ const POOL_KEYS = new Map<string, "refresh" | "add" | "strategy">([
|
|
|
25
26
|
]);
|
|
26
27
|
const BAR_WIDTH = 16;
|
|
27
28
|
const HINTS = [
|
|
28
|
-
"↑↓ select · s spend reset · c clear limit · d remove · a add · t strategy · r refresh · esc close",
|
|
29
|
-
"↑↓ · s spend · c clear · d remove · a add · t strategy ·
|
|
30
|
-
"s spend ·
|
|
29
|
+
"↑↓ select · enter sign in · s spend reset · c clear limit · n login name · d remove · a add · t strategy · r refresh · esc close",
|
|
30
|
+
"↑↓ · enter sign in · s spend · c clear · n name · d remove · a add · t strategy · esc",
|
|
31
|
+
"enter sign in · s spend · n name · d del · a add · esc",
|
|
31
32
|
];
|
|
32
33
|
|
|
33
34
|
export class PoolPanel implements Component {
|
|
@@ -46,6 +47,10 @@ export class PoolPanel implements Component {
|
|
|
46
47
|
if (matchesKey(data, Key.escape) || data === "q") return this.done({ type: "close" });
|
|
47
48
|
if (matchesKey(data, Key.up) || data === "k") return this.move(-1);
|
|
48
49
|
if (matchesKey(data, Key.down) || data === "j") return this.move(1);
|
|
50
|
+
if (matchesKey(data, Key.enter)) {
|
|
51
|
+
const account = this.statuses[this.selected]?.account;
|
|
52
|
+
return account ? this.done({ type: "login", accountId: account.id }) : undefined;
|
|
53
|
+
}
|
|
49
54
|
const poolAction = POOL_KEYS.get(data);
|
|
50
55
|
const accountAction = ACCOUNT_KEYS.get(data);
|
|
51
56
|
const account = this.statuses[this.selected]?.account;
|
|
@@ -95,7 +100,7 @@ export class PoolPanel implements Component {
|
|
|
95
100
|
: "";
|
|
96
101
|
const header = `${pointer}${name}${active} ${state}${banked}`;
|
|
97
102
|
|
|
98
|
-
if (!signedIn) return [header, theme.fg("dim",
|
|
103
|
+
if (!signedIn) return [header, theme.fg("dim", " press enter to sign in")];
|
|
99
104
|
if (!usage) return [header, theme.fg("dim", ` usage unavailable${error ? `: ${error}` : ""}`)];
|
|
100
105
|
return [
|
|
101
106
|
header,
|
package/src/pool.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
export type Account = { id: string; label: string; resetsAt?: number };
|
|
1
|
+
export type Account = { id: string; label: string; name?: string; resetsAt?: number };
|
|
2
|
+
|
|
3
|
+
export function loginName(account: Account): string {
|
|
4
|
+
return account.name || `ChatGPT (${account.label})`;
|
|
5
|
+
}
|
|
2
6
|
|
|
3
7
|
const USAGE_LIMIT = /usage.?limit|subscription_sharing_usage_limit_exceeded|insufficient_quota|quota/i;
|
|
4
8
|
const RATE_LIMIT = /rate.?limit|too many requests|\b429\b/i;
|