@jameslovespancakes/pi-plus 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +190 -190
  3. package/config/pi-plus.example.json +60 -60
  4. package/config/skills/model-routing/SKILL.md +86 -86
  5. package/images/pi-plus.svg +10 -10
  6. package/package.json +67 -67
  7. package/server/board-server.mjs +641 -641
  8. package/server/package.json +17 -17
  9. package/src/core/accounts/registry.ts +93 -93
  10. package/src/core/anthropic/client-identity.ts +241 -241
  11. package/src/core/catalog/quality.ts +314 -314
  12. package/src/core/config.ts +169 -169
  13. package/src/core/env.ts +58 -58
  14. package/src/core/exec/process.ts +146 -146
  15. package/src/core/exec/ssh-config.ts +157 -157
  16. package/src/core/policy/policy.ts +183 -183
  17. package/src/core/quota/pool.ts +64 -64
  18. package/src/core/quota/usage-source.ts +289 -289
  19. package/src/core/store.ts +43 -43
  20. package/src/domains/agents/board-setup.ts +409 -409
  21. package/src/domains/agents/index.ts +462 -462
  22. package/src/domains/models/catalog-tool.ts +361 -361
  23. package/src/domains/models/index.ts +14 -14
  24. package/src/domains/models/policy-gate.ts +169 -169
  25. package/src/domains/models/provider-picker.ts +207 -207
  26. package/src/domains/remote/config-path.ts +41 -41
  27. package/src/domains/remote/index.ts +866 -866
  28. package/src/domains/remote/setup.ts +425 -425
  29. package/src/domains/setup/index.ts +220 -220
  30. package/src/domains/subscriptions/accounts.ts +242 -242
  31. package/src/domains/subscriptions/footer.ts +182 -182
  32. package/src/domains/subscriptions/index.ts +42 -42
  33. package/src/domains/subscriptions/provider.ts +219 -219
  34. package/src/domains/subscriptions/providers/anthropic.ts +149 -149
  35. package/src/domains/subscriptions/providers/codex.ts +148 -148
  36. package/src/domains/subscriptions/routing.ts +72 -72
  37. package/src/services/usage-service.ts +186 -186
  38. package/src/ui/format.ts +73 -73
  39. package/src/ui/usage-bars.ts +154 -154
  40. package/src/vendor/anthropic.ts +109 -109
@@ -1,242 +1,242 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { accountRows, openAccountsPicker, type AccountState } from "./accounts-picker.ts";
3
- import {
4
- accountProvider,
5
- accountProviders,
6
- type AccountContext,
7
- type AccountProvider,
8
- type ManagedAccount,
9
- } from "../../core/accounts/registry.ts";
10
-
11
- /**
12
- * `/accounts` is provider-agnostic subscription account management.
13
- *
14
- * /accounts every provider's accounts, toggled enabled/disabled
15
- * /accounts add pick a provider, name the account, authorize
16
- * /accounts reauth reauthorize an existing account
17
- *
18
- * No provider-specific logic lives here; adapters are registered in
19
- * core/accounts/registry.ts, so a new provider is an adapter rather than a
20
- * new command.
21
- */
22
-
23
- async function openBrowser(pi: ExtensionAPI, url: string): Promise<void> {
24
- if (process.platform === "win32") {
25
- await pi.exec("rundll32.exe", ["url.dll,FileProtocolHandler", url]);
26
- return;
27
- }
28
- if (process.platform === "darwin") {
29
- await pi.exec("open", [url]);
30
- return;
31
- }
32
- await pi.exec("xdg-open", [url]);
33
- }
34
-
35
- function bridge(pi: ExtensionAPI, ctx: any): AccountContext {
36
- return {
37
- hasUI: ctx.hasUI,
38
- ui: {
39
- input: (title, placeholder) => ctx.ui.input(title, placeholder),
40
- confirm: (title, message) => ctx.ui.confirm(title, message),
41
- notify: (message, type) => ctx.ui.notify(message, type ?? "info"),
42
- },
43
- openBrowser: (url) => openBrowser(pi, url),
44
- };
45
- }
46
-
47
- function describe(account: ManagedAccount): string {
48
- const state = !account.enabled
49
- ? "disabled"
50
- : account.expiresAt !== undefined && account.expiresAt < Date.now() ? "expired" : "active";
51
- return `${account.label.padEnd(20)} ${state}`;
52
- }
53
-
54
- async function listAll(ctx: any): Promise<void> {
55
- const providers = accountProviders();
56
- if (providers.length === 0) {
57
- ctx.ui.notify("No account providers are registered.", "warning");
58
- return;
59
- }
60
-
61
- const blocks: string[] = [];
62
- for (const provider of providers) {
63
- try {
64
- const accounts = await provider.list();
65
- const routing = provider.routing ? ` · routing: ${await provider.routing.get()}` : "";
66
- blocks.push(
67
- `${provider.label} (${provider.id})${routing}`,
68
- ...(accounts.length > 0
69
- ? accounts.map((account) => ` ${describe(account)}`)
70
- : [` no additional accounts, add one with /account ${provider.id} add`]),
71
- );
72
- } catch (error) {
73
- blocks.push(`${provider.label} (${provider.id}): ${error instanceof Error ? error.message : String(error)}`);
74
- }
75
- }
76
- ctx.ui.notify(blocks.join("\n"), "info");
77
- }
78
-
79
-
80
-
81
- async function pickAccount(ctx: any, provider: AccountProvider): Promise<string | undefined> {
82
- const accounts = await provider.list();
83
- if (accounts.length === 0) {
84
- ctx.ui.notify(`No ${provider.label} accounts yet. Add one with /account ${provider.id} add.`, "info");
85
- return undefined;
86
- }
87
- if (!ctx.hasUI) {
88
- ctx.ui.notify(`Specify an account: /account ${provider.id} reauth <label>`, "warning");
89
- return undefined;
90
- }
91
-
92
- const labels = accounts.map(describe);
93
- const choice = await ctx.ui.select(`Reauthorize which ${provider.label} account?`, labels);
94
- if (!choice) return undefined;
95
- return accounts[labels.indexOf(choice)]?.id;
96
- }
97
-
98
- export function registerAccountCommands(pi: ExtensionAPI): void {
99
- /** Toggles one account and returns the state it ended in. */
100
- const toggle = (providerId: string, accountId: string): AccountState => {
101
- const provider = accountProvider(providerId);
102
- if (!provider?.setEnabled) throw new Error(`${providerId} accounts cannot be disabled.`);
103
- const next = pendingState.get(`${providerId}:${accountId}`) === "enabled" ? "disabled" : "enabled";
104
- pendingState.set(`${providerId}:${accountId}`, next);
105
- // Adapters persist asynchronously; the picker needs the answer now, so the
106
- // write is fired off and failures surface as a notification.
107
- void provider.setEnabled(accountId, next === "enabled").catch(() => {});
108
- return next;
109
- };
110
-
111
- /** Mirror of on-disk state, so the synchronous toggle can answer instantly. */
112
- const pendingState = new Map<string, AccountState>();
113
-
114
- const rows = async () => {
115
- const list = await accountRows(accountProviders());
116
- pendingState.clear();
117
- for (const row of list) pendingState.set(row.id, row.state);
118
- return list;
119
- };
120
-
121
- /** Adds an account: choose provider, name it, authorize. */
122
- async function addAccount(ctx: any, providerId?: string): Promise<void> {
123
- if (!ctx.hasUI) {
124
- ctx.ui.notify("Adding an account requires interactive Pi mode.", "error");
125
- return;
126
- }
127
-
128
- const providers = accountProviders();
129
- if (providers.length === 0) {
130
- ctx.ui.notify("No subscription providers are registered.", "error");
131
- return;
132
- }
133
-
134
- let provider = providerId ? accountProvider(providerId) : undefined;
135
- if (!provider) {
136
- if (providers.length === 1) {
137
- provider = providers[0];
138
- } else {
139
- const labels = providers.map((p) => `${p.label} (${p.id})`);
140
- const picked = await ctx.ui.select("Add an account for which subscription?", labels);
141
- if (!picked) return;
142
- provider = providers[labels.indexOf(picked)];
143
- }
144
- }
145
- if (!provider) return;
146
-
147
- const label = await ctx.ui.input(`${provider.label} account name`, "Work, Personal, etc.");
148
- if (!label?.trim()) return;
149
-
150
- try {
151
- const added = await provider.add(bridge(pi, ctx), label.trim());
152
- if (added) ctx.ui.notify(`${provider.label} account “${added}” added.`, "info");
153
- } catch (error) {
154
- ctx.ui.notify(`Could not add account: ${error instanceof Error ? error.message : String(error)}`, "error");
155
- }
156
- }
157
-
158
- /** Rename wizard: pick an account, type a new name. */
159
- async function renameAccount(ctx: any): Promise<void> {
160
- if (!ctx.hasUI) {
161
- ctx.ui.notify("Renaming an account requires interactive Pi mode.", "error");
162
- return;
163
- }
164
- const list = await accountRows(accountProviders());
165
- const renameable = list.filter((row) => accountProvider(row.providerId)?.rename);
166
- if (renameable.length === 0) {
167
- ctx.ui.notify("No accounts can be renamed.", "info");
168
- return;
169
- }
170
-
171
- const labels = renameable.map((row) => `${row.providerLabel} ${row.label}`);
172
- const picked = await ctx.ui.select("Rename which account?", labels);
173
- if (!picked) return;
174
- const row = renameable[labels.indexOf(picked)];
175
- if (!row) return;
176
-
177
- const next = await ctx.ui.input("New name", row.label);
178
- if (!next?.trim() || next.trim() === row.label) return;
179
-
180
- const provider = accountProvider(row.providerId);
181
- try {
182
- // Row ids are "<providerId>:<accountId>"; strip the prefix.
183
- await provider!.rename!(row.id.slice(row.providerId.length + 1), next.trim());
184
- ctx.ui.notify(`Renamed to “${next.trim()}”.`, "info");
185
- } catch (error) {
186
- ctx.ui.notify(`Could not rename: ${error instanceof Error ? error.message : String(error)}`, "error");
187
- }
188
- }
189
-
190
- pi.registerCommand("accounts", {
191
- description: "Subscription accounts (/accounts [add|rename|reauth])",
192
- getArgumentCompletions: (prefix) => {
193
- const parts = prefix.trim().split(/\s+/).filter(Boolean);
194
- if (parts.length <= 1) {
195
- return ["add", "rename", "reauth"]
196
- .filter((option) => option.startsWith(parts[0] ?? ""))
197
- .map((option) => ({ value: option, label: option }));
198
- }
199
- return accountProviders()
200
- .map((p) => p.id)
201
- .filter((id) => id.startsWith(parts[1] ?? ""))
202
- .map((id) => ({ value: `${parts[0]} ${id}`, label: id }));
203
- },
204
- handler: async (args, ctx) => {
205
- const [action, providerId] = args.trim().split(/\s+/).filter(Boolean);
206
-
207
- if (action === "add") return addAccount(ctx, providerId);
208
- if (action === "rename") return renameAccount(ctx);
209
-
210
- if (action === "reauth") {
211
- const provider = providerId ? accountProvider(providerId) : accountProviders()[0];
212
- if (!provider) { ctx.ui.notify("No subscription providers are registered.", "error"); return; }
213
- const target = await pickAccount(ctx, provider);
214
- if (!target) return;
215
- try {
216
- const done = await provider.reauth(bridge(pi, ctx), target);
217
- if (done) ctx.ui.notify(`${provider.label} account “${done}” reauthorized.`, "info");
218
- } catch (error) {
219
- ctx.ui.notify(`Could not reauthorize: ${error instanceof Error ? error.message : String(error)}`, "error");
220
- }
221
- return;
222
- }
223
-
224
- if (action) {
225
- ctx.ui.notify(`Unknown action “${action}”. Use: /accounts [add|rename|reauth]`, "warning");
226
- return;
227
- }
228
-
229
- // Bare /accounts: the picker interactively, plain text headless.
230
- if (!ctx.hasUI) return listAll(ctx);
231
-
232
- // The picker closes to run a wizard, then reopens so the result is
233
- // visible immediately. Bounded so a misbehaving action cannot spin.
234
- for (let step = 0; step < 24; step++) {
235
- const requested = await openAccountsPicker(ctx, { rows, toggle });
236
- if (!requested) return;
237
- if (requested.kind === "add") await addAccount(ctx);
238
- else if (requested.kind === "rename") await renameAccount(ctx);
239
- }
240
- },
241
- });
242
- }
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { accountRows, openAccountsPicker, type AccountState } from "./accounts-picker.ts";
3
+ import {
4
+ accountProvider,
5
+ accountProviders,
6
+ type AccountContext,
7
+ type AccountProvider,
8
+ type ManagedAccount,
9
+ } from "../../core/accounts/registry.ts";
10
+
11
+ /**
12
+ * `/accounts` is provider-agnostic subscription account management.
13
+ *
14
+ * /accounts every provider's accounts, toggled enabled/disabled
15
+ * /accounts add pick a provider, name the account, authorize
16
+ * /accounts reauth reauthorize an existing account
17
+ *
18
+ * No provider-specific logic lives here; adapters are registered in
19
+ * core/accounts/registry.ts, so a new provider is an adapter rather than a
20
+ * new command.
21
+ */
22
+
23
+ async function openBrowser(pi: ExtensionAPI, url: string): Promise<void> {
24
+ if (process.platform === "win32") {
25
+ await pi.exec("rundll32.exe", ["url.dll,FileProtocolHandler", url]);
26
+ return;
27
+ }
28
+ if (process.platform === "darwin") {
29
+ await pi.exec("open", [url]);
30
+ return;
31
+ }
32
+ await pi.exec("xdg-open", [url]);
33
+ }
34
+
35
+ function bridge(pi: ExtensionAPI, ctx: any): AccountContext {
36
+ return {
37
+ hasUI: ctx.hasUI,
38
+ ui: {
39
+ input: (title, placeholder) => ctx.ui.input(title, placeholder),
40
+ confirm: (title, message) => ctx.ui.confirm(title, message),
41
+ notify: (message, type) => ctx.ui.notify(message, type ?? "info"),
42
+ },
43
+ openBrowser: (url) => openBrowser(pi, url),
44
+ };
45
+ }
46
+
47
+ function describe(account: ManagedAccount): string {
48
+ const state = !account.enabled
49
+ ? "disabled"
50
+ : account.expiresAt !== undefined && account.expiresAt < Date.now() ? "expired" : "active";
51
+ return `${account.label.padEnd(20)} ${state}`;
52
+ }
53
+
54
+ async function listAll(ctx: any): Promise<void> {
55
+ const providers = accountProviders();
56
+ if (providers.length === 0) {
57
+ ctx.ui.notify("No account providers are registered.", "warning");
58
+ return;
59
+ }
60
+
61
+ const blocks: string[] = [];
62
+ for (const provider of providers) {
63
+ try {
64
+ const accounts = await provider.list();
65
+ const routing = provider.routing ? ` · routing: ${await provider.routing.get()}` : "";
66
+ blocks.push(
67
+ `${provider.label} (${provider.id})${routing}`,
68
+ ...(accounts.length > 0
69
+ ? accounts.map((account) => ` ${describe(account)}`)
70
+ : [` no additional accounts, add one with /account ${provider.id} add`]),
71
+ );
72
+ } catch (error) {
73
+ blocks.push(`${provider.label} (${provider.id}): ${error instanceof Error ? error.message : String(error)}`);
74
+ }
75
+ }
76
+ ctx.ui.notify(blocks.join("\n"), "info");
77
+ }
78
+
79
+
80
+
81
+ async function pickAccount(ctx: any, provider: AccountProvider): Promise<string | undefined> {
82
+ const accounts = await provider.list();
83
+ if (accounts.length === 0) {
84
+ ctx.ui.notify(`No ${provider.label} accounts yet. Add one with /account ${provider.id} add.`, "info");
85
+ return undefined;
86
+ }
87
+ if (!ctx.hasUI) {
88
+ ctx.ui.notify(`Specify an account: /account ${provider.id} reauth <label>`, "warning");
89
+ return undefined;
90
+ }
91
+
92
+ const labels = accounts.map(describe);
93
+ const choice = await ctx.ui.select(`Reauthorize which ${provider.label} account?`, labels);
94
+ if (!choice) return undefined;
95
+ return accounts[labels.indexOf(choice)]?.id;
96
+ }
97
+
98
+ export function registerAccountCommands(pi: ExtensionAPI): void {
99
+ /** Toggles one account and returns the state it ended in. */
100
+ const toggle = (providerId: string, accountId: string): AccountState => {
101
+ const provider = accountProvider(providerId);
102
+ if (!provider?.setEnabled) throw new Error(`${providerId} accounts cannot be disabled.`);
103
+ const next = pendingState.get(`${providerId}:${accountId}`) === "enabled" ? "disabled" : "enabled";
104
+ pendingState.set(`${providerId}:${accountId}`, next);
105
+ // Adapters persist asynchronously; the picker needs the answer now, so the
106
+ // write is fired off and failures surface as a notification.
107
+ void provider.setEnabled(accountId, next === "enabled").catch(() => {});
108
+ return next;
109
+ };
110
+
111
+ /** Mirror of on-disk state, so the synchronous toggle can answer instantly. */
112
+ const pendingState = new Map<string, AccountState>();
113
+
114
+ const rows = async () => {
115
+ const list = await accountRows(accountProviders());
116
+ pendingState.clear();
117
+ for (const row of list) pendingState.set(row.id, row.state);
118
+ return list;
119
+ };
120
+
121
+ /** Adds an account: choose provider, name it, authorize. */
122
+ async function addAccount(ctx: any, providerId?: string): Promise<void> {
123
+ if (!ctx.hasUI) {
124
+ ctx.ui.notify("Adding an account requires interactive Pi mode.", "error");
125
+ return;
126
+ }
127
+
128
+ const providers = accountProviders();
129
+ if (providers.length === 0) {
130
+ ctx.ui.notify("No subscription providers are registered.", "error");
131
+ return;
132
+ }
133
+
134
+ let provider = providerId ? accountProvider(providerId) : undefined;
135
+ if (!provider) {
136
+ if (providers.length === 1) {
137
+ provider = providers[0];
138
+ } else {
139
+ const labels = providers.map((p) => `${p.label} (${p.id})`);
140
+ const picked = await ctx.ui.select("Add an account for which subscription?", labels);
141
+ if (!picked) return;
142
+ provider = providers[labels.indexOf(picked)];
143
+ }
144
+ }
145
+ if (!provider) return;
146
+
147
+ const label = await ctx.ui.input(`${provider.label} account name`, "Work, Personal, etc.");
148
+ if (!label?.trim()) return;
149
+
150
+ try {
151
+ const added = await provider.add(bridge(pi, ctx), label.trim());
152
+ if (added) ctx.ui.notify(`${provider.label} account “${added}” added.`, "info");
153
+ } catch (error) {
154
+ ctx.ui.notify(`Could not add account: ${error instanceof Error ? error.message : String(error)}`, "error");
155
+ }
156
+ }
157
+
158
+ /** Rename wizard: pick an account, type a new name. */
159
+ async function renameAccount(ctx: any): Promise<void> {
160
+ if (!ctx.hasUI) {
161
+ ctx.ui.notify("Renaming an account requires interactive Pi mode.", "error");
162
+ return;
163
+ }
164
+ const list = await accountRows(accountProviders());
165
+ const renameable = list.filter((row) => accountProvider(row.providerId)?.rename);
166
+ if (renameable.length === 0) {
167
+ ctx.ui.notify("No accounts can be renamed.", "info");
168
+ return;
169
+ }
170
+
171
+ const labels = renameable.map((row) => `${row.providerLabel} ${row.label}`);
172
+ const picked = await ctx.ui.select("Rename which account?", labels);
173
+ if (!picked) return;
174
+ const row = renameable[labels.indexOf(picked)];
175
+ if (!row) return;
176
+
177
+ const next = await ctx.ui.input("New name", row.label);
178
+ if (!next?.trim() || next.trim() === row.label) return;
179
+
180
+ const provider = accountProvider(row.providerId);
181
+ try {
182
+ // Row ids are "<providerId>:<accountId>"; strip the prefix.
183
+ await provider!.rename!(row.id.slice(row.providerId.length + 1), next.trim());
184
+ ctx.ui.notify(`Renamed to “${next.trim()}”.`, "info");
185
+ } catch (error) {
186
+ ctx.ui.notify(`Could not rename: ${error instanceof Error ? error.message : String(error)}`, "error");
187
+ }
188
+ }
189
+
190
+ pi.registerCommand("accounts", {
191
+ description: "Subscription accounts (/accounts [add|rename|reauth])",
192
+ getArgumentCompletions: (prefix) => {
193
+ const parts = prefix.trim().split(/\s+/).filter(Boolean);
194
+ if (parts.length <= 1) {
195
+ return ["add", "rename", "reauth"]
196
+ .filter((option) => option.startsWith(parts[0] ?? ""))
197
+ .map((option) => ({ value: option, label: option }));
198
+ }
199
+ return accountProviders()
200
+ .map((p) => p.id)
201
+ .filter((id) => id.startsWith(parts[1] ?? ""))
202
+ .map((id) => ({ value: `${parts[0]} ${id}`, label: id }));
203
+ },
204
+ handler: async (args, ctx) => {
205
+ const [action, providerId] = args.trim().split(/\s+/).filter(Boolean);
206
+
207
+ if (action === "add") return addAccount(ctx, providerId);
208
+ if (action === "rename") return renameAccount(ctx);
209
+
210
+ if (action === "reauth") {
211
+ const provider = providerId ? accountProvider(providerId) : accountProviders()[0];
212
+ if (!provider) { ctx.ui.notify("No subscription providers are registered.", "error"); return; }
213
+ const target = await pickAccount(ctx, provider);
214
+ if (!target) return;
215
+ try {
216
+ const done = await provider.reauth(bridge(pi, ctx), target);
217
+ if (done) ctx.ui.notify(`${provider.label} account “${done}” reauthorized.`, "info");
218
+ } catch (error) {
219
+ ctx.ui.notify(`Could not reauthorize: ${error instanceof Error ? error.message : String(error)}`, "error");
220
+ }
221
+ return;
222
+ }
223
+
224
+ if (action) {
225
+ ctx.ui.notify(`Unknown action “${action}”. Use: /accounts [add|rename|reauth]`, "warning");
226
+ return;
227
+ }
228
+
229
+ // Bare /accounts: the picker interactively, plain text headless.
230
+ if (!ctx.hasUI) return listAll(ctx);
231
+
232
+ // The picker closes to run a wizard, then reopens so the result is
233
+ // visible immediately. Bounded so a misbehaving action cannot spin.
234
+ for (let step = 0; step < 24; step++) {
235
+ const requested = await openAccountsPicker(ctx, { rows, toggle });
236
+ if (!requested) return;
237
+ if (requested.kind === "add") await addAccount(ctx);
238
+ else if (requested.kind === "rename") await renameAccount(ctx);
239
+ }
240
+ },
241
+ });
242
+ }