@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,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
+ }