@jameslovespancakes/pi-plus 1.0.0 → 1.0.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.
Files changed (41) 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/images/usage_demo.png +0 -0
  7. package/package.json +67 -67
  8. package/server/board-server.mjs +641 -641
  9. package/server/package.json +17 -17
  10. package/src/core/accounts/registry.ts +93 -93
  11. package/src/core/anthropic/client-identity.ts +241 -241
  12. package/src/core/catalog/quality.ts +314 -314
  13. package/src/core/config.ts +169 -169
  14. package/src/core/env.ts +58 -58
  15. package/src/core/exec/process.ts +146 -146
  16. package/src/core/exec/ssh-config.ts +157 -157
  17. package/src/core/policy/policy.ts +183 -183
  18. package/src/core/quota/pool.ts +64 -64
  19. package/src/core/quota/usage-source.ts +289 -289
  20. package/src/core/store.ts +43 -43
  21. package/src/domains/agents/board-setup.ts +409 -409
  22. package/src/domains/agents/index.ts +462 -462
  23. package/src/domains/models/catalog-tool.ts +361 -361
  24. package/src/domains/models/index.ts +14 -14
  25. package/src/domains/models/policy-gate.ts +169 -169
  26. package/src/domains/models/provider-picker.ts +207 -207
  27. package/src/domains/remote/config-path.ts +41 -41
  28. package/src/domains/remote/index.ts +866 -866
  29. package/src/domains/remote/setup.ts +425 -425
  30. package/src/domains/setup/index.ts +220 -220
  31. package/src/domains/subscriptions/accounts.ts +242 -242
  32. package/src/domains/subscriptions/footer.ts +182 -182
  33. package/src/domains/subscriptions/index.ts +42 -42
  34. package/src/domains/subscriptions/provider.ts +219 -219
  35. package/src/domains/subscriptions/providers/anthropic.ts +149 -149
  36. package/src/domains/subscriptions/providers/codex.ts +148 -148
  37. package/src/domains/subscriptions/routing.ts +72 -72
  38. package/src/services/usage-service.ts +186 -186
  39. package/src/ui/format.ts +73 -73
  40. package/src/ui/usage-bars.ts +154 -154
  41. package/src/vendor/anthropic.ts +109 -109
@@ -1,220 +1,220 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { existsSync } from "node:fs";
3
- import { env } from "../../core/env.ts";
4
- import { agentPath, readJson } from "../../core/store.ts";
5
- import { configPath, readConfig } from "../../core/config.ts";
6
- import { accountProviders } from "../../core/accounts/registry.ts";
7
- import { readSshHosts } from "../../core/exec/ssh-config.ts";
8
-
9
- /**
10
- * `/pi-plus` is the status modal for the pack; `/pi-plus help` explains it.
11
- *
12
- * The modal lists every feature with its live state. Selecting an unconfigured
13
- * one runs its setup command; selecting a ready one opens its hub. `help` hands
14
- * the detected state to the model so the explanation is specific to this
15
- * machine rather than a static README dump.
16
- *
17
- * Only `core/` is read, so this stays inside the layering rules: it never
18
- * reaches into another domain.
19
- */
20
-
21
- interface Feature {
22
- name: string;
23
- ready: boolean;
24
- detail: string;
25
- commands: string[];
26
- /** Command that configures this feature, when it is not ready. */
27
- setup?: string;
28
- /** Command that opens this feature once it is ready. */
29
- open: string;
30
- }
31
-
32
- async function inspect(ctx: any): Promise<Feature[]> {
33
- const config = readConfig();
34
- const features: Feature[] = [];
35
-
36
- /* subscriptions */
37
- const providers = accountProviders();
38
- const accountSummary: string[] = [];
39
- let anySignedIn = false;
40
- for (const provider of providers) {
41
- // pi holds the primary credential itself; the adapter only knows about the
42
- // extra pooled accounts. Being signed in at all is what makes this usable.
43
- let primary = false;
44
- try {
45
- primary = !!(await ctx.modelRegistry.getProviderAuth(provider.id))?.auth?.apiKey;
46
- } catch { /* provider not configured */ }
47
-
48
- try {
49
- const accounts = await provider.list();
50
- const routing = provider.routing ? await provider.routing.get() : "n/a";
51
- const pooled = accounts.length + (primary ? 1 : 0);
52
- anySignedIn ||= primary || accounts.length > 0;
53
- accountSummary.push(
54
- primary || accounts.length > 0
55
- ? `${provider.id}: ${pooled} account(s), routing=${routing}`
56
- : `${provider.id}: not signed in`,
57
- );
58
- } catch {
59
- accountSummary.push(`${provider.id}: unreadable`);
60
- }
61
- }
62
- features.push({
63
- name: "Subscriptions",
64
- ready: anySignedIn,
65
- detail: accountSummary.join("; ") || "no account providers registered",
66
- commands: ["/account", "/account <provider> add", "/routing standard|optimal", "/usage"],
67
- setup: anySignedIn ? undefined : "/account anthropic add",
68
- open: "/account",
69
- });
70
-
71
- /* benchmarks */
72
- const hasKey = !!env("ARTIFICIAL_ANALYSIS_API_KEY");
73
- const cache = readJson<{ records?: Record<string, unknown>; checkedAt?: number; fetchedAt?: number }>(
74
- agentPath("model-quality.json"),
75
- {},
76
- );
77
- const records = Object.keys(cache.records ?? {}).length;
78
- // Caches written before the store rewrite carry `fetchedAt`.
79
- const refreshedAt = cache.checkedAt ?? cache.fetchedAt;
80
- const ageHours = refreshedAt ? Math.round((Date.now() - refreshedAt) / 3.6e6) : undefined;
81
- features.push({
82
- name: "Model Information",
83
- ready: hasKey && records > 0,
84
- detail: hasKey
85
- ? `${records} models cached${ageHours !== undefined ? `, refreshed ${ageHours}h ago` : ""}`
86
- : "no Artificial Analysis API key",
87
- commands: ["/models", "/model-info <id>", "/model-info refresh", "list_models (tool)"],
88
- setup: hasKey ? undefined : "/model-info setup",
89
- open: "/models",
90
- });
91
-
92
- /* spend policy */
93
- features.push({
94
- name: "Providers",
95
- ready: config.policy.requireApproval.length > 0,
96
- detail: `${config.policy.requireApproval.length} gated pattern(s), ${config.policy.autoApprove.length} auto-approved`,
97
- commands: ["/provider", "/provider list", "/provider approve <name>"],
98
- open: "/provider",
99
- });
100
-
101
- /* agent board */
102
- const boardUrl = env("AGENT_BOARD_URL");
103
- const boardReady = !!boardUrl && !!env("AGENT_BOARD_TOKEN");
104
- // A board configured before /board setup existed has no recorded mode; it is
105
- // externally managed by definition.
106
- const mode = env("AGENT_BOARD_MODE") ?? (boardReady ? "external" : "none");
107
- features.push({
108
- name: "Agent Board",
109
- ready: boardReady,
110
- detail: boardUrl ? `${mode} at ${boardUrl}` : "no board configured",
111
- commands: ["/board", "/board setup", "/board restart", "/board clear", "agent_board (tool)"],
112
- setup: boardReady ? undefined : "/board setup",
113
- open: "/board status",
114
- });
115
-
116
- /* remote workers */
117
- const workers = config.remote.workers ?? [];
118
- const enabled = workers.filter((worker) => (worker as { enabled?: boolean }).enabled !== false);
119
- let sshHosts = 0;
120
- try {
121
- sshHosts = readSshHosts().length;
122
- } catch { /* no ssh config */ }
123
- features.push({
124
- name: "Remote Workers",
125
- ready: enabled.length > 0,
126
- detail: enabled.length > 0
127
- ? `${enabled.length} enabled of ${workers.length} configured`
128
- : `none enabled${sshHosts > 0 ? ` (${sshHosts} host(s) available in ~/.ssh/config)` : ""}`,
129
- commands: ["/remote setup", "/remote add", "remote_test (tool)", "remote_status (tool)"],
130
- setup: enabled.length > 0 ? undefined : "/remote setup",
131
- open: "/remote setup",
132
- });
133
-
134
- return features;
135
- }
136
-
137
- /** Unconfigured features sort first: the work to do is what you see first. */
138
- function ordered(features: Feature[]): Feature[] {
139
- return [...features].sort((a, b) => Number(a.ready) - Number(b.ready));
140
- }
141
-
142
- function renderRow(feature: Feature, width: number): string {
143
- return `[${feature.ready ? "✓" : " "}] ${feature.name.padEnd(width)} ${feature.detail}`;
144
- }
145
-
146
- function buildBrief(features: Feature[]): string {
147
- const lines = features.map((feature) => {
148
- const parts = [
149
- `- ${feature.name}: ${feature.ready ? "READY" : "NOT SET UP"}`,
150
- ` state: ${feature.detail}`,
151
- ` commands: ${feature.commands.join(", ")}`,
152
- ];
153
- if (feature.setup) parts.push(` to enable: ${feature.setup}`);
154
- return parts.join("\n");
155
- });
156
-
157
- const pending = features.filter((feature) => !feature.ready);
158
-
159
- return [
160
- "The user just ran /pi-plus. Give them a short, friendly orientation to the pi-plus extension pack.",
161
- "",
162
- "Detected state on this machine:",
163
- "",
164
- ...lines,
165
- "",
166
- `Config file: ${configPath()}${existsSync(configPath()) ? "" : " (not created yet)"}`,
167
- "",
168
- "Write the reply yourself, in chat. Requirements:",
169
- "1. One short sentence on what pi-plus is: four capabilities in one package.",
170
- "2. A compact list of the capabilities, each with one line on what it does and the command to try. Mark which are already working.",
171
- pending.length > 0
172
- ? `3. Then a short 'Set these up next' section covering ONLY the ones marked NOT SET UP (${pending.map((f) => f.name).join(", ")}), each with the single command to run and one line on what it will ask for.`
173
- : "3. Note that everything is already configured, and suggest one or two commands worth trying.",
174
- "4. Keep it under ~250 words. No preamble, no headings deeper than one level, no invented features.",
175
- "5. Do not call any tools. Just answer.",
176
- ].join("\n");
177
- }
178
-
179
- export default function setupGuide(pi: ExtensionAPI) {
180
- pi.registerCommand("pi-plus", {
181
- description: "Status for every pi-plus feature, or `help` for an explanation",
182
- getArgumentCompletions: (prefix) =>
183
- "help".startsWith(prefix) ? [{ value: "help", label: "help: explain the extension and what is missing" }] : [],
184
- handler: async (args, ctx) => {
185
- const features = await inspect(ctx);
186
-
187
- // `/pi-plus help` asks the model to explain the pack and what is missing.
188
- if (args.trim().toLowerCase() === "help") {
189
- await pi.sendUserMessage(buildBrief(features));
190
- return;
191
- }
192
-
193
- // Everything else is the status modal. ui.select needs a TTY, so headless
194
- // sessions fall back to the same information as plain text.
195
- const rows = ordered(features);
196
- if (!ctx.hasUI) {
197
- ctx.ui.notify(
198
- rows
199
- .map((feature) => `${feature.ready ? "[ready]" : "[setup]"} ${feature.name}\n ${feature.detail}`)
200
- .join("\n"),
201
- "info",
202
- );
203
- return;
204
- }
205
-
206
- const width = Math.max(...rows.map((feature) => feature.name.length));
207
- const labels = rows.map((feature) => renderRow(feature, width));
208
- const choice = await ctx.ui.select("pi-plus", labels);
209
- if (!choice) return;
210
-
211
- const picked = rows[labels.indexOf(choice)];
212
- if (!picked) return;
213
-
214
- // Dispatch through the command pipeline rather than importing another
215
- // domain, which would break the no-cross-domain-imports rule.
216
- const command = picked.ready ? picked.open : (picked.setup ?? picked.open);
217
- await pi.sendUserMessage(command, { expandPromptTemplates: true });
218
- },
219
- });
220
- }
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { existsSync } from "node:fs";
3
+ import { env } from "../../core/env.ts";
4
+ import { agentPath, readJson } from "../../core/store.ts";
5
+ import { configPath, readConfig } from "../../core/config.ts";
6
+ import { accountProviders } from "../../core/accounts/registry.ts";
7
+ import { readSshHosts } from "../../core/exec/ssh-config.ts";
8
+
9
+ /**
10
+ * `/pi-plus` is the status modal for the pack; `/pi-plus help` explains it.
11
+ *
12
+ * The modal lists every feature with its live state. Selecting an unconfigured
13
+ * one runs its setup command; selecting a ready one opens its hub. `help` hands
14
+ * the detected state to the model so the explanation is specific to this
15
+ * machine rather than a static README dump.
16
+ *
17
+ * Only `core/` is read, so this stays inside the layering rules: it never
18
+ * reaches into another domain.
19
+ */
20
+
21
+ interface Feature {
22
+ name: string;
23
+ ready: boolean;
24
+ detail: string;
25
+ commands: string[];
26
+ /** Command that configures this feature, when it is not ready. */
27
+ setup?: string;
28
+ /** Command that opens this feature once it is ready. */
29
+ open: string;
30
+ }
31
+
32
+ async function inspect(ctx: any): Promise<Feature[]> {
33
+ const config = readConfig();
34
+ const features: Feature[] = [];
35
+
36
+ /* subscriptions */
37
+ const providers = accountProviders();
38
+ const accountSummary: string[] = [];
39
+ let anySignedIn = false;
40
+ for (const provider of providers) {
41
+ // pi holds the primary credential itself; the adapter only knows about the
42
+ // extra pooled accounts. Being signed in at all is what makes this usable.
43
+ let primary = false;
44
+ try {
45
+ primary = !!(await ctx.modelRegistry.getProviderAuth(provider.id))?.auth?.apiKey;
46
+ } catch { /* provider not configured */ }
47
+
48
+ try {
49
+ const accounts = await provider.list();
50
+ const routing = provider.routing ? await provider.routing.get() : "n/a";
51
+ const pooled = accounts.length + (primary ? 1 : 0);
52
+ anySignedIn ||= primary || accounts.length > 0;
53
+ accountSummary.push(
54
+ primary || accounts.length > 0
55
+ ? `${provider.id}: ${pooled} account(s), routing=${routing}`
56
+ : `${provider.id}: not signed in`,
57
+ );
58
+ } catch {
59
+ accountSummary.push(`${provider.id}: unreadable`);
60
+ }
61
+ }
62
+ features.push({
63
+ name: "Subscriptions",
64
+ ready: anySignedIn,
65
+ detail: accountSummary.join("; ") || "no account providers registered",
66
+ commands: ["/account", "/account <provider> add", "/routing standard|optimal", "/usage"],
67
+ setup: anySignedIn ? undefined : "/account anthropic add",
68
+ open: "/account",
69
+ });
70
+
71
+ /* benchmarks */
72
+ const hasKey = !!env("ARTIFICIAL_ANALYSIS_API_KEY");
73
+ const cache = readJson<{ records?: Record<string, unknown>; checkedAt?: number; fetchedAt?: number }>(
74
+ agentPath("model-quality.json"),
75
+ {},
76
+ );
77
+ const records = Object.keys(cache.records ?? {}).length;
78
+ // Caches written before the store rewrite carry `fetchedAt`.
79
+ const refreshedAt = cache.checkedAt ?? cache.fetchedAt;
80
+ const ageHours = refreshedAt ? Math.round((Date.now() - refreshedAt) / 3.6e6) : undefined;
81
+ features.push({
82
+ name: "Model Information",
83
+ ready: hasKey && records > 0,
84
+ detail: hasKey
85
+ ? `${records} models cached${ageHours !== undefined ? `, refreshed ${ageHours}h ago` : ""}`
86
+ : "no Artificial Analysis API key",
87
+ commands: ["/models", "/model-info <id>", "/model-info refresh", "list_models (tool)"],
88
+ setup: hasKey ? undefined : "/model-info setup",
89
+ open: "/models",
90
+ });
91
+
92
+ /* spend policy */
93
+ features.push({
94
+ name: "Providers",
95
+ ready: config.policy.requireApproval.length > 0,
96
+ detail: `${config.policy.requireApproval.length} gated pattern(s), ${config.policy.autoApprove.length} auto-approved`,
97
+ commands: ["/provider", "/provider list", "/provider approve <name>"],
98
+ open: "/provider",
99
+ });
100
+
101
+ /* agent board */
102
+ const boardUrl = env("AGENT_BOARD_URL");
103
+ const boardReady = !!boardUrl && !!env("AGENT_BOARD_TOKEN");
104
+ // A board configured before /board setup existed has no recorded mode; it is
105
+ // externally managed by definition.
106
+ const mode = env("AGENT_BOARD_MODE") ?? (boardReady ? "external" : "none");
107
+ features.push({
108
+ name: "Agent Board",
109
+ ready: boardReady,
110
+ detail: boardUrl ? `${mode} at ${boardUrl}` : "no board configured",
111
+ commands: ["/board", "/board setup", "/board restart", "/board clear", "agent_board (tool)"],
112
+ setup: boardReady ? undefined : "/board setup",
113
+ open: "/board status",
114
+ });
115
+
116
+ /* remote workers */
117
+ const workers = config.remote.workers ?? [];
118
+ const enabled = workers.filter((worker) => (worker as { enabled?: boolean }).enabled !== false);
119
+ let sshHosts = 0;
120
+ try {
121
+ sshHosts = readSshHosts().length;
122
+ } catch { /* no ssh config */ }
123
+ features.push({
124
+ name: "Remote Workers",
125
+ ready: enabled.length > 0,
126
+ detail: enabled.length > 0
127
+ ? `${enabled.length} enabled of ${workers.length} configured`
128
+ : `none enabled${sshHosts > 0 ? ` (${sshHosts} host(s) available in ~/.ssh/config)` : ""}`,
129
+ commands: ["/remote setup", "/remote add", "remote_test (tool)", "remote_status (tool)"],
130
+ setup: enabled.length > 0 ? undefined : "/remote setup",
131
+ open: "/remote setup",
132
+ });
133
+
134
+ return features;
135
+ }
136
+
137
+ /** Unconfigured features sort first: the work to do is what you see first. */
138
+ function ordered(features: Feature[]): Feature[] {
139
+ return [...features].sort((a, b) => Number(a.ready) - Number(b.ready));
140
+ }
141
+
142
+ function renderRow(feature: Feature, width: number): string {
143
+ return `[${feature.ready ? "✓" : " "}] ${feature.name.padEnd(width)} ${feature.detail}`;
144
+ }
145
+
146
+ function buildBrief(features: Feature[]): string {
147
+ const lines = features.map((feature) => {
148
+ const parts = [
149
+ `- ${feature.name}: ${feature.ready ? "READY" : "NOT SET UP"}`,
150
+ ` state: ${feature.detail}`,
151
+ ` commands: ${feature.commands.join(", ")}`,
152
+ ];
153
+ if (feature.setup) parts.push(` to enable: ${feature.setup}`);
154
+ return parts.join("\n");
155
+ });
156
+
157
+ const pending = features.filter((feature) => !feature.ready);
158
+
159
+ return [
160
+ "The user just ran /pi-plus. Give them a short, friendly orientation to the pi-plus extension pack.",
161
+ "",
162
+ "Detected state on this machine:",
163
+ "",
164
+ ...lines,
165
+ "",
166
+ `Config file: ${configPath()}${existsSync(configPath()) ? "" : " (not created yet)"}`,
167
+ "",
168
+ "Write the reply yourself, in chat. Requirements:",
169
+ "1. One short sentence on what pi-plus is: four capabilities in one package.",
170
+ "2. A compact list of the capabilities, each with one line on what it does and the command to try. Mark which are already working.",
171
+ pending.length > 0
172
+ ? `3. Then a short 'Set these up next' section covering ONLY the ones marked NOT SET UP (${pending.map((f) => f.name).join(", ")}), each with the single command to run and one line on what it will ask for.`
173
+ : "3. Note that everything is already configured, and suggest one or two commands worth trying.",
174
+ "4. Keep it under ~250 words. No preamble, no headings deeper than one level, no invented features.",
175
+ "5. Do not call any tools. Just answer.",
176
+ ].join("\n");
177
+ }
178
+
179
+ export default function setupGuide(pi: ExtensionAPI) {
180
+ pi.registerCommand("pi-plus", {
181
+ description: "Status for every pi-plus feature, or `help` for an explanation",
182
+ getArgumentCompletions: (prefix) =>
183
+ "help".startsWith(prefix) ? [{ value: "help", label: "help: explain the extension and what is missing" }] : [],
184
+ handler: async (args, ctx) => {
185
+ const features = await inspect(ctx);
186
+
187
+ // `/pi-plus help` asks the model to explain the pack and what is missing.
188
+ if (args.trim().toLowerCase() === "help") {
189
+ await pi.sendUserMessage(buildBrief(features));
190
+ return;
191
+ }
192
+
193
+ // Everything else is the status modal. ui.select needs a TTY, so headless
194
+ // sessions fall back to the same information as plain text.
195
+ const rows = ordered(features);
196
+ if (!ctx.hasUI) {
197
+ ctx.ui.notify(
198
+ rows
199
+ .map((feature) => `${feature.ready ? "[ready]" : "[setup]"} ${feature.name}\n ${feature.detail}`)
200
+ .join("\n"),
201
+ "info",
202
+ );
203
+ return;
204
+ }
205
+
206
+ const width = Math.max(...rows.map((feature) => feature.name.length));
207
+ const labels = rows.map((feature) => renderRow(feature, width));
208
+ const choice = await ctx.ui.select("pi-plus", labels);
209
+ if (!choice) return;
210
+
211
+ const picked = rows[labels.indexOf(choice)];
212
+ if (!picked) return;
213
+
214
+ // Dispatch through the command pipeline rather than importing another
215
+ // domain, which would break the no-cross-domain-imports rule.
216
+ const command = picked.ready ? picked.open : (picked.setup ?? picked.open);
217
+ await pi.sendUserMessage(command, { expandPromptTemplates: true });
218
+ },
219
+ });
220
+ }