@jameslovespancakes/pi-plus 1.0.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -0
  3. package/config/pi-plus.example.json +60 -0
  4. package/config/skills/model-routing/SKILL.md +86 -0
  5. package/images/board_demo.png +0 -0
  6. package/images/pi-plus.svg +10 -0
  7. package/images/pi-plus_demo.png +0 -0
  8. package/images/provider_demo.png +0 -0
  9. package/images/remote_demo.png +0 -0
  10. package/images/usage_demo.png +0 -0
  11. package/package.json +67 -0
  12. package/server/board-server.mjs +641 -0
  13. package/server/package.json +17 -0
  14. package/src/core/accounts/registry.ts +93 -0
  15. package/src/core/anthropic/client-identity.ts +241 -0
  16. package/src/core/anthropic/models.ts +69 -0
  17. package/src/core/anthropic/oauth.ts +208 -0
  18. package/src/core/anthropic/quota.ts +253 -0
  19. package/src/core/anthropic/routing.ts +168 -0
  20. package/src/core/anthropic/store.ts +225 -0
  21. package/src/core/anthropic/vendor/README.md +36 -0
  22. package/src/core/anthropic/vendor/xxhash-wasm.LICENSE.md +25 -0
  23. package/src/core/anthropic/vendor/xxhash-wasm.js +2 -0
  24. package/src/core/anthropic/xxhash64.ts +33 -0
  25. package/src/core/catalog/quality.ts +314 -0
  26. package/src/core/codex/oauth.ts +129 -0
  27. package/src/core/codex/quota.ts +88 -0
  28. package/src/core/codex/store.ts +97 -0
  29. package/src/core/config.ts +169 -0
  30. package/src/core/env.ts +58 -0
  31. package/src/core/exec/process.ts +146 -0
  32. package/src/core/exec/ssh-config.ts +157 -0
  33. package/src/core/oauth/pkce.ts +88 -0
  34. package/src/core/policy/policy.ts +183 -0
  35. package/src/core/quota/pool.ts +64 -0
  36. package/src/core/quota/usage-source.ts +289 -0
  37. package/src/core/store.ts +43 -0
  38. package/src/domains/agents/board-setup.ts +409 -0
  39. package/src/domains/agents/index.ts +462 -0
  40. package/src/domains/models/catalog-tool.ts +361 -0
  41. package/src/domains/models/index.ts +14 -0
  42. package/src/domains/models/policy-gate.ts +169 -0
  43. package/src/domains/models/provider-picker.ts +208 -0
  44. package/src/domains/remote/config-path.ts +41 -0
  45. package/src/domains/remote/index.ts +866 -0
  46. package/src/domains/remote/setup.ts +425 -0
  47. package/src/domains/setup/index.ts +220 -0
  48. package/src/domains/subscriptions/accounts-picker.ts +178 -0
  49. package/src/domains/subscriptions/accounts.ts +242 -0
  50. package/src/domains/subscriptions/footer.ts +182 -0
  51. package/src/domains/subscriptions/index.ts +42 -0
  52. package/src/domains/subscriptions/provider.ts +219 -0
  53. package/src/domains/subscriptions/providers/anthropic.ts +149 -0
  54. package/src/domains/subscriptions/providers/codex.ts +148 -0
  55. package/src/domains/subscriptions/routing.ts +72 -0
  56. package/src/services/usage-service.ts +186 -0
  57. package/src/ui/format.ts +73 -0
  58. package/src/ui/usage-bars.ts +154 -0
  59. package/src/vendor/anthropic.ts +109 -0
@@ -0,0 +1,409 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { spawn } from "node:child_process";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "node:fs";
4
+ import { randomBytes } from "node:crypto";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { agentPath } from "../../core/store.ts";
8
+ import { env, setEnv } from "../../core/env.ts";
9
+ import { runSshCommand } from "../../core/exec/process.ts";
10
+ import { readRemote } from "../remote/config-path.ts";
11
+
12
+ /**
13
+ * `/board setup | restart | clear`
14
+ *
15
+ * Two deployment shapes:
16
+ * local pi owns the lifecycle and starts the server on session start
17
+ * remote a native service (launchd / systemd / Task Scheduler) owns it, so
18
+ * it returns by itself when the host reboots
19
+ *
20
+ * An externally managed board is also supported: supply a URL and token and
21
+ * pi-plus will only ever connect to it.
22
+ */
23
+
24
+ const PID_FILE = "board-server.pid";
25
+ const DEFAULT_PORT = 8787;
26
+
27
+ function serverEntry(): string {
28
+ // src/domains/agents/ -> repo root -> server/
29
+ return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "server", "board-server.mjs");
30
+ }
31
+
32
+ type Mode = "local" | "remote" | "external";
33
+
34
+ function mode(): Mode {
35
+ const value = env("AGENT_BOARD_MODE");
36
+ return value === "local" || value === "remote" ? value : "external";
37
+ }
38
+
39
+ function boardUrl(): string | undefined {
40
+ return env("AGENT_BOARD_URL");
41
+ }
42
+
43
+ function httpBase(): string | undefined {
44
+ const url = boardUrl();
45
+ if (!url) return undefined;
46
+ return url.replace(/^ws(s)?:/, "http$1:").replace(/\/ws\/?$/, "");
47
+ }
48
+
49
+ /* ---------------------------------- local --------------------------------- */
50
+
51
+ function pidPath(): string {
52
+ return agentPath(PID_FILE);
53
+ }
54
+
55
+ function readPid(): number | undefined {
56
+ try {
57
+ const pid = Number(readFileSync(pidPath(), "utf8").trim());
58
+ if (!Number.isInteger(pid) || pid <= 0) return undefined;
59
+ // Signal 0 tests for existence without touching the process.
60
+ process.kill(pid, 0);
61
+ return pid;
62
+ } catch {
63
+ return undefined;
64
+ }
65
+ }
66
+
67
+ function stopLocal(): boolean {
68
+ const pid = readPid();
69
+ if (!pid) return false;
70
+ try {
71
+ process.kill(pid, "SIGTERM");
72
+ } catch { /* already gone */ }
73
+ try {
74
+ rmSync(pidPath());
75
+ } catch { /* nothing to remove */ }
76
+ return true;
77
+ }
78
+
79
+ /** Starts a detached server and records its pid. Safe to call when running. */
80
+ export function startLocal(): { started: boolean; reason?: string } {
81
+ if (mode() !== "local") return { started: false, reason: "not a local board" };
82
+ if (readPid()) return { started: false, reason: "already running" };
83
+
84
+ const entry = serverEntry();
85
+ if (!existsSync(entry)) return { started: false, reason: `server not found at ${entry}` };
86
+
87
+ const token = env("AGENT_BOARD_TOKEN");
88
+ if (!token) return { started: false, reason: "AGENT_BOARD_TOKEN is not set" };
89
+
90
+ const port = new URL(boardUrl() ?? `ws://127.0.0.1:${DEFAULT_PORT}/ws`).port || String(DEFAULT_PORT);
91
+ mkdirSync(agentPath("board-data"), { recursive: true });
92
+
93
+ const child = spawn(process.execPath, [entry], {
94
+ detached: true,
95
+ stdio: ["ignore", "ignore", "ignore"],
96
+ windowsHide: true,
97
+ env: {
98
+ ...process.env,
99
+ AGENT_BOARD_TOKEN: token,
100
+ AGENT_BOARD_PORT: port,
101
+ AGENT_BOARD_HOST: "127.0.0.1",
102
+ AGENT_BOARD_DB: join(agentPath("board-data"), "board.sqlite"),
103
+ },
104
+ });
105
+
106
+ if (!child.pid) return { started: false, reason: "spawn failed" };
107
+ writeFileSync(pidPath(), String(child.pid), "utf8");
108
+ child.unref();
109
+ return { started: true };
110
+ }
111
+
112
+ /* --------------------------------- remote --------------------------------- */
113
+
114
+ /** Install script per platform. Each makes the board restart at boot. */
115
+ function installScript(platform: string, token: string, port: string): string {
116
+ const common = `set -e
117
+ mkdir -p ~/.pi-board
118
+ cat > ~/.pi-board/board-server.mjs <<'PI_PLUS_BOARD_EOF'
119
+ __SERVER__
120
+ PI_PLUS_BOARD_EOF
121
+ cd ~/.pi-board
122
+ if [ ! -d node_modules/ws ]; then npm install ws@^8 --no-audit --no-fund --silent; fi
123
+ `;
124
+
125
+ if (platform === "darwin") {
126
+ return `${common}
127
+ cat > ~/Library/LaunchAgents/com.pi-plus.board.plist <<EOF
128
+ <?xml version="1.0" encoding="UTF-8"?>
129
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
130
+ <plist version="1.0"><dict>
131
+ <key>Label</key><string>com.pi-plus.board</string>
132
+ <key>ProgramArguments</key><array>
133
+ <string>$(command -v node)</string><string>$HOME/.pi-board/board-server.mjs</string>
134
+ </array>
135
+ <key>WorkingDirectory</key><string>$HOME/.pi-board</string>
136
+ <key>EnvironmentVariables</key><dict>
137
+ <key>AGENT_BOARD_TOKEN</key><string>${token}</string>
138
+ <key>AGENT_BOARD_PORT</key><string>${port}</string>
139
+ <key>AGENT_BOARD_DB</key><string>$HOME/.pi-board/board.sqlite</string>
140
+ </dict>
141
+ <key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
142
+ <key>StandardOutPath</key><string>$HOME/.pi-board/board.log</string>
143
+ <key>StandardErrorPath</key><string>$HOME/.pi-board/board.error.log</string>
144
+ </dict></plist>
145
+ EOF
146
+ launchctl unload ~/Library/LaunchAgents/com.pi-plus.board.plist 2>/dev/null || true
147
+ launchctl load ~/Library/LaunchAgents/com.pi-plus.board.plist
148
+ echo INSTALLED=launchd`;
149
+ }
150
+
151
+ return `${common}
152
+ mkdir -p ~/.config/systemd/user
153
+ cat > ~/.config/systemd/user/pi-plus-board.service <<EOF
154
+ [Unit]
155
+ Description=pi-plus agent board
156
+ After=network.target
157
+
158
+ [Service]
159
+ ExecStart=$(command -v node) %h/.pi-board/board-server.mjs
160
+ WorkingDirectory=%h/.pi-board
161
+ Environment=AGENT_BOARD_TOKEN=${token}
162
+ Environment=AGENT_BOARD_PORT=${port}
163
+ Environment=AGENT_BOARD_DB=%h/.pi-board/board.sqlite
164
+ Restart=always
165
+ RestartSec=5
166
+
167
+ [Install]
168
+ WantedBy=default.target
169
+ EOF
170
+ systemctl --user daemon-reload
171
+ systemctl --user enable --now pi-plus-board.service
172
+ # Survive logout so the board returns after a reboot.
173
+ loginctl enable-linger "$USER" 2>/dev/null || true
174
+ echo INSTALLED=systemd`;
175
+ }
176
+
177
+ async function detectPlatform(host: string): Promise<string> {
178
+ const result = await runSshCommand(host, "uname -s", { timeoutSeconds: 15 });
179
+ const name = result.stdout.trim().toLowerCase();
180
+ return name.includes("darwin") ? "darwin" : "linux";
181
+ }
182
+
183
+ async function installRemote(ctx: any, host: string, token: string, port: string): Promise<boolean> {
184
+ const entry = serverEntry();
185
+ if (!existsSync(entry)) {
186
+ ctx.ui.notify(`Server source missing at ${entry}`, "error");
187
+ return false;
188
+ }
189
+
190
+ ctx.ui.notify(`Detecting platform on ${host}…`, "info");
191
+ const platform = await detectPlatform(host);
192
+ const source = readFileSync(entry, "utf8");
193
+ const script = installScript(platform, token, port).replace("__SERVER__", source);
194
+
195
+ ctx.ui.notify(`Installing board on ${host} (${platform})…`, "info");
196
+ const result = await runSshCommand(host, "bash -s", { input: script, timeoutSeconds: 180 });
197
+ if (result.code !== 0) {
198
+ ctx.ui.notify(`Install failed:\n${result.stderr.trim() || result.stdout.trim()}`, "error");
199
+ return false;
200
+ }
201
+ ctx.ui.notify(`Installed (${result.stdout.match(/INSTALLED=(\w+)/)?.[1] ?? "ok"}). It will restart automatically on reboot.`, "info");
202
+ return true;
203
+ }
204
+
205
+ async function restartRemote(host: string): Promise<string> {
206
+ const platform = await detectPlatform(host);
207
+ const command = platform === "darwin"
208
+ ? "launchctl kickstart -k gui/$(id -u)/com.pi-plus.board && echo restarted"
209
+ : "systemctl --user restart pi-plus-board.service && echo restarted";
210
+ const result = await runSshCommand(host, command, { timeoutSeconds: 60 });
211
+ return result.code === 0 ? "restarted" : (result.stderr.trim() || `exit ${result.code}`);
212
+ }
213
+
214
+ /* ---------------------------------- probes -------------------------------- */
215
+
216
+ async function health(): Promise<{ ok: boolean; detail: string }> {
217
+ const base = httpBase();
218
+ if (!base) return { ok: false, detail: "no board URL configured" };
219
+ try {
220
+ const response = await fetch(`${base}/health`, { signal: AbortSignal.timeout(5_000) });
221
+ if (!response.ok) return { ok: false, detail: `HTTP ${response.status}` };
222
+ const body = await response.json() as { activeAgents?: number };
223
+ return { ok: true, detail: `${body.activeAgents ?? 0} agent(s) connected` };
224
+ } catch (error) {
225
+ return { ok: false, detail: error instanceof Error ? error.message : String(error) };
226
+ }
227
+ }
228
+
229
+ async function clearBoard(all: boolean): Promise<string> {
230
+ const base = httpBase();
231
+ const token = env("AGENT_BOARD_TOKEN");
232
+ if (!base || !token) return "no board configured";
233
+ const response = await fetch(`${base}${all ? "/clear-all" : "/clear"}`, {
234
+ method: "POST",
235
+ headers: { authorization: `Bearer ${token}` },
236
+ signal: AbortSignal.timeout(15_000),
237
+ });
238
+ if (!response.ok) return `HTTP ${response.status}`;
239
+ const body = await response.json() as { cleared?: number };
240
+ return `cleared ${body.cleared ?? 0} message(s)`;
241
+ }
242
+
243
+ /* --------------------------------- command -------------------------------- */
244
+
245
+ async function setup(pi: ExtensionAPI, ctx: any): Promise<void> {
246
+ const choice = await ctx.ui.select("Agent board setup", [
247
+ "Run locally (pi starts it each session)",
248
+ "Install on a server (restarts itself on reboot)",
249
+ "Connect to existing (URL + token)",
250
+ ]);
251
+ if (!choice) return;
252
+
253
+ if (choice.startsWith("Connect")) {
254
+ const url = await ctx.ui.input("Board URL", "ws://host:8787/ws");
255
+ if (!url) return;
256
+ const token = await ctx.ui.input("Board token", "shared secret");
257
+ if (!token) return;
258
+ setEnv("AGENT_BOARD_URL", url.trim());
259
+ setEnv("AGENT_BOARD_TOKEN", token.trim());
260
+ setEnv("AGENT_BOARD_MODE", "external");
261
+ const probe = await health();
262
+ ctx.ui.notify(probe.ok ? `Connected. ${probe.detail}. Restart pi to join.` : `Saved, but not reachable: ${probe.detail}`, probe.ok ? "info" : "warning");
263
+ return;
264
+ }
265
+
266
+ // Both remaining paths need a token; reuse the existing one so already
267
+ // connected agents are not locked out.
268
+ let token = env("AGENT_BOARD_TOKEN");
269
+ if (!token) {
270
+ token = randomBytes(32).toString("base64url");
271
+ setEnv("AGENT_BOARD_TOKEN", token);
272
+ }
273
+
274
+ if (choice.startsWith("Run locally")) {
275
+ const port = (await ctx.ui.input("Port", String(DEFAULT_PORT)))?.trim() || String(DEFAULT_PORT);
276
+ setEnv("AGENT_BOARD_URL", `ws://127.0.0.1:${port}/ws`);
277
+ setEnv("AGENT_BOARD_MODE", "local");
278
+ stopLocal();
279
+ const started = startLocal();
280
+ if (!started.started) {
281
+ ctx.ui.notify(`Could not start: ${started.reason}`, "error");
282
+ return;
283
+ }
284
+ await new Promise((done) => setTimeout(done, 800));
285
+ const probe = await health();
286
+ ctx.ui.notify(
287
+ probe.ok
288
+ ? `Local board running on port ${port}. pi will start it automatically each session.`
289
+ : `Started, but health check failed: ${probe.detail}`,
290
+ probe.ok ? "info" : "warning",
291
+ );
292
+ return;
293
+ }
294
+
295
+ // Remote: offer configured workers first, then free-form.
296
+ const workers = readRemote().workers.filter((worker) => worker.enabled !== false);
297
+ const options = [...workers.map((worker) => `${worker.name} (${worker.ssh})`), "Other host…"];
298
+ const picked = await ctx.ui.select("Install on which host?", options);
299
+ if (!picked) return;
300
+
301
+ let host: string;
302
+ if (picked === "Other host…") {
303
+ const entered = await ctx.ui.input("SSH host", "user@host");
304
+ if (!entered) return;
305
+ host = entered.trim();
306
+ } else {
307
+ host = workers[options.indexOf(picked)].ssh;
308
+ }
309
+
310
+ const port = (await ctx.ui.input("Port", String(DEFAULT_PORT)))?.trim() || String(DEFAULT_PORT);
311
+ if (!(await installRemote(ctx, host, token, port))) return;
312
+
313
+ const hostname = host.includes("@") ? host.split("@")[1] : host;
314
+ setEnv("AGENT_BOARD_URL", `ws://${hostname}:${port}/ws`);
315
+ setEnv("AGENT_BOARD_MODE", "remote");
316
+ setEnv("AGENT_BOARD_SSH", host);
317
+
318
+ await new Promise((done) => setTimeout(done, 1_500));
319
+ const probe = await health();
320
+ ctx.ui.notify(
321
+ probe.ok ? `Board reachable. ${probe.detail}. Restart pi to join.` : `Installed, but not reachable yet: ${probe.detail}`,
322
+ probe.ok ? "info" : "warning",
323
+ );
324
+ }
325
+
326
+ /** Wires the session hook that keeps a local board alive. */
327
+ export function registerBoardLifecycle(pi: ExtensionAPI): void {
328
+ // A local board is pi's responsibility: bring it up with the session.
329
+ pi.on("session_start", async () => {
330
+ if (mode() === "local") startLocal();
331
+ });
332
+ }
333
+
334
+ /** Admin verbs for `/board`. Returns false when `args` is not one of them. */
335
+ export async function handleBoardAdmin(pi: ExtensionAPI, ctx: any, args: string): Promise<boolean> {
336
+ {
337
+ {
338
+ const [action, flag] = args.trim().toLowerCase().split(/\s+/).filter(Boolean);
339
+ if (!["setup", "restart", "clear", "status"].includes(action ?? "")) return false;
340
+
341
+ if (action === "setup") {
342
+ if (!ctx.hasUI) ctx.ui.notify("/board setup requires an interactive session.", "error");
343
+ else await setup(pi, ctx);
344
+ return true;
345
+ }
346
+
347
+ if (action === "restart") {
348
+ const current = mode();
349
+ if (current === "local") {
350
+ stopLocal();
351
+ const started = startLocal();
352
+ await new Promise((done) => setTimeout(done, 800));
353
+ const probe = await health();
354
+ ctx.ui.notify(
355
+ started.started && probe.ok ? `Local board restarted. ${probe.detail}.` : `Restart issue: ${started.reason ?? probe.detail}`,
356
+ started.started && probe.ok ? "info" : "warning",
357
+ );
358
+ return true;
359
+ }
360
+ if (current === "remote") {
361
+ const host = env("AGENT_BOARD_SSH");
362
+ if (!host) {
363
+ ctx.ui.notify("No SSH host recorded for the remote board. Run /board setup again.", "error");
364
+ return true;
365
+ }
366
+ ctx.ui.notify(`Restarting board on ${host}…`, "info");
367
+ const detail = await restartRemote(host);
368
+ const probe = await health();
369
+ ctx.ui.notify(`${detail}${probe.ok ? `. ${probe.detail}` : ""}`, probe.ok ? "info" : "warning");
370
+ return true;
371
+ }
372
+ ctx.ui.notify("This board is externally managed; restart it where it runs.", "warning");
373
+ return true;
374
+ }
375
+
376
+ if (action === "clear") {
377
+ const all = flag === "all";
378
+ const ok = await ctx.ui.confirm(
379
+ all ? "Clear everything?" : "Clear the board?",
380
+ all
381
+ ? "Deletes all messages, threads, coordinator links and presence history."
382
+ : "Deletes all messages and threads. Coordinator links are kept.",
383
+ );
384
+ if (!ok) return true;
385
+ try {
386
+ ctx.ui.notify(await clearBoard(all), "info");
387
+ } catch (error) {
388
+ ctx.ui.notify(`Clear failed: ${error instanceof Error ? error.message : String(error)}`, "error");
389
+ }
390
+ return true;
391
+ }
392
+
393
+ // /board status
394
+ const probe = await health();
395
+ ctx.ui.notify(
396
+ [
397
+ `mode: ${mode()}`,
398
+ `url: ${boardUrl() ?? "not configured"}`,
399
+ mode() === "local" ? `pid: ${readPid() ?? "not running"}` : "",
400
+ `health: ${probe.ok ? probe.detail : probe.detail}`,
401
+ "",
402
+ "/board setup · /board restart · /board clear [all]",
403
+ ].filter(Boolean).join("\n"),
404
+ probe.ok ? "info" : "warning",
405
+ );
406
+ return true;
407
+ }
408
+ }
409
+ }