@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,14 +1,14 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { registerCatalogTool } from "./catalog-tool.ts";
3
- import { registerPolicyGate } from "./policy-gate.ts";
4
-
5
- /**
6
- * Models domain: which models exist, how good they are, and what they cost.
7
- *
8
- * Combines the old `model-catalog.ts` and `model-policy-gate.ts`: the catalogue
9
- * ranks models and the gate decides whether a given one is allowed to bill.
10
- */
11
- export default function models(pi: ExtensionAPI) {
12
- registerCatalogTool(pi);
13
- registerPolicyGate(pi);
14
- }
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { registerCatalogTool } from "./catalog-tool.ts";
3
+ import { registerPolicyGate } from "./policy-gate.ts";
4
+
5
+ /**
6
+ * Models domain: which models exist, how good they are, and what they cost.
7
+ *
8
+ * Combines the old `model-catalog.ts` and `model-policy-gate.ts`: the catalogue
9
+ * ranks models and the gate decides whether a given one is allowed to bill.
10
+ */
11
+ export default function models(pi: ExtensionAPI) {
12
+ registerCatalogTool(pi);
13
+ registerPolicyGate(pi);
14
+ }
@@ -1,169 +1,169 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import {
3
- approve,
4
- checkModel,
5
- gatedProviders,
6
- isApproved,
7
- loadPolicy,
8
- providerState,
9
- revoke,
10
- toggleProvider,
11
- } from "../../core/policy/policy.ts";
12
- import { openProviderPicker, STATE_TEXT, type ProviderRow, type StateKey } from "./provider-picker.ts";
13
-
14
- /**
15
- * Enforces the approval policy at the provider boundary, so it also covers
16
- * workflow subagents rather than relying on prompt instructions.
17
- */
18
-
19
- class ModelPolicyError extends Error {
20
- code = "MODEL_POLICY_BLOCKED";
21
- }
22
-
23
- async function providerRows(ctx: any): Promise<ProviderRow[]> {
24
- const ids = new Set<string>();
25
- try {
26
- for (const model of await ctx.modelRegistry.getAvailable()) ids.add(model.provider);
27
- } catch { /* registry unavailable */ }
28
-
29
- // The copy is deliberate: the body deletes from `ids` while iterating, which
30
- // is unsafe without it. oxlint cannot see the mutation below.
31
- // oxlint-disable-next-line unicorn/no-useless-spread
32
- for (const id of [...ids]) {
33
- try {
34
- if (!ctx.modelRegistry.getProviderAuthStatus(id)?.configured) ids.delete(id);
35
- } catch { /* keep it if the status cannot be read */ }
36
- }
37
-
38
- // Gated providers stay listed without credentials so the policy stays visible.
39
- for (const id of gatedProviders()) ids.add(id);
40
-
41
- return [...ids].sort().map((provider) => {
42
- let display = provider;
43
- try {
44
- display = cleanName(ctx.modelRegistry.getProviderDisplayName(provider) || provider);
45
- } catch { /* fall back to the id */ }
46
- return {
47
- id: provider,
48
- provider,
49
- display,
50
- state: providerState(provider) as ProviderRow["state"],
51
- };
52
- });
53
- }
54
-
55
- /**
56
- * Every provider the user actually has credentials for, plus any the policy
57
- * gates. Derived at call time so a newly authenticated provider shows up
58
- * without touching config.
59
- */
60
- /**
61
- * Providers are free to decorate their own name. The CortexKit package calls
62
- * itself "Anthropic (CortexKit OAuth)". The implementation detail is noise in a
63
- * policy list, so the parenthetical is dropped.
64
- */
65
- function cleanName(name: string): string {
66
- return name.replace(/\s*\([^)]*\)\s*$/, "").trim() || name;
67
- }
68
-
69
-
70
- export function registerPolicyGate(pi: ExtensionAPI): void {
71
- let wrapped = false;
72
-
73
- const wrapProviders = (ctx: any) => {
74
- if (wrapped) return;
75
- for (const providerId of gatedProviders()) {
76
- const provider = ctx.modelRegistry.getProvider(providerId);
77
- if (!provider) continue;
78
-
79
- const guard = (model: any) => {
80
- const decision = checkModel(providerId, model?.id ?? "unknown");
81
- if (!decision.allowed) throw new ModelPolicyError(decision.message);
82
- };
83
-
84
- const originalStream = provider.stream?.bind(provider);
85
- const originalStreamSimple = provider.streamSimple?.bind(provider);
86
-
87
- pi.registerProvider({
88
- ...provider,
89
- ...(originalStream && {
90
- stream: (model: any, ...rest: any[]) => {
91
- guard(model);
92
- return originalStream(model, ...rest);
93
- },
94
- }),
95
- ...(originalStreamSimple && {
96
- streamSimple: (model: any, ...rest: any[]) => {
97
- guard(model);
98
- return originalStreamSimple(model, ...rest);
99
- },
100
- }),
101
- });
102
- }
103
- wrapped = true;
104
- };
105
-
106
- pi.on("session_start", async (_event, ctx) => {
107
- loadPolicy();
108
- wrapProviders(ctx);
109
- });
110
-
111
- pi.registerCommand("provider", {
112
- description: "Toggle which providers may be used (approve | remove <name>)",
113
- getArgumentCompletions: (prefix) => {
114
- const [action, name = ""] = prefix.split(/\s+/);
115
- if (!prefix.includes(" ")) {
116
- return ["approve", "remove"]
117
- .filter((option) => option.startsWith(action))
118
- .map((option) => ({ value: option, label: option }));
119
- }
120
- if (action !== "approve" && action !== "remove") return [];
121
- return gatedProviders()
122
- .filter((provider) => provider.startsWith(name))
123
- .map((provider) => ({ value: `${action} ${provider}`, label: provider }));
124
- },
125
- handler: async (args, ctx) => {
126
- const [action, ...rest] = args.trim().split(/\s+/).filter(Boolean);
127
- const name = rest.join(" ");
128
-
129
- if (action === "approve" || action === "remove") {
130
- if (!name) {
131
- ctx.ui.notify(`Usage: /provider ${action} <provider>`, "warning");
132
- return;
133
- }
134
- if (!gatedProviders().includes(name)) {
135
- ctx.ui.notify(
136
- `“${name}” is not a gated provider. Gated: ${gatedProviders().join(", ") || "none"}`,
137
- "warning",
138
- );
139
- return;
140
- }
141
- if (action === "approve") approve(name);
142
- else revoke(name);
143
- ctx.ui.notify(`${name} is now ${isApproved(name) ? "approved" : "blocked"}.`, "info");
144
- return;
145
- }
146
-
147
- if (action) {
148
- ctx.ui.notify(`Unknown action “${action}”. Use: /provider [approve|remove <name>]`, "warning");
149
- return;
150
- }
151
-
152
- const rows = await providerRows(ctx);
153
-
154
- // Headless: plain text, no cursor to draw.
155
- if (!ctx.hasUI) {
156
- ctx.ui.notify(
157
- rows.map((row) => `${row.state === "auto" || row.state === "approved" ? "[on] " : "[off]"} ${row.display}: ${STATE_TEXT[row.state]}`).join("\n"),
158
- "info",
159
- );
160
- return;
161
- }
162
-
163
- await openProviderPicker(ctx, {
164
- rows: () => providerRows(ctx),
165
- toggle: (provider) => toggleProvider(provider) as StateKey,
166
- });
167
- },
168
- });
169
- }
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ approve,
4
+ checkModel,
5
+ gatedProviders,
6
+ isApproved,
7
+ loadPolicy,
8
+ providerState,
9
+ revoke,
10
+ toggleProvider,
11
+ } from "../../core/policy/policy.ts";
12
+ import { openProviderPicker, STATE_TEXT, type ProviderRow, type StateKey } from "./provider-picker.ts";
13
+
14
+ /**
15
+ * Enforces the approval policy at the provider boundary, so it also covers
16
+ * workflow subagents rather than relying on prompt instructions.
17
+ */
18
+
19
+ class ModelPolicyError extends Error {
20
+ code = "MODEL_POLICY_BLOCKED";
21
+ }
22
+
23
+ async function providerRows(ctx: any): Promise<ProviderRow[]> {
24
+ const ids = new Set<string>();
25
+ try {
26
+ for (const model of await ctx.modelRegistry.getAvailable()) ids.add(model.provider);
27
+ } catch { /* registry unavailable */ }
28
+
29
+ // The copy is deliberate: the body deletes from `ids` while iterating, which
30
+ // is unsafe without it. oxlint cannot see the mutation below.
31
+ // oxlint-disable-next-line unicorn/no-useless-spread
32
+ for (const id of [...ids]) {
33
+ try {
34
+ if (!ctx.modelRegistry.getProviderAuthStatus(id)?.configured) ids.delete(id);
35
+ } catch { /* keep it if the status cannot be read */ }
36
+ }
37
+
38
+ // Gated providers stay listed without credentials so the policy stays visible.
39
+ for (const id of gatedProviders()) ids.add(id);
40
+
41
+ return [...ids].sort().map((provider) => {
42
+ let display = provider;
43
+ try {
44
+ display = cleanName(ctx.modelRegistry.getProviderDisplayName(provider) || provider);
45
+ } catch { /* fall back to the id */ }
46
+ return {
47
+ id: provider,
48
+ provider,
49
+ display,
50
+ state: providerState(provider) as ProviderRow["state"],
51
+ };
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Every provider the user actually has credentials for, plus any the policy
57
+ * gates. Derived at call time so a newly authenticated provider shows up
58
+ * without touching config.
59
+ */
60
+ /**
61
+ * Providers are free to decorate their own name. The CortexKit package calls
62
+ * itself "Anthropic (CortexKit OAuth)". The implementation detail is noise in a
63
+ * policy list, so the parenthetical is dropped.
64
+ */
65
+ function cleanName(name: string): string {
66
+ return name.replace(/\s*\([^)]*\)\s*$/, "").trim() || name;
67
+ }
68
+
69
+
70
+ export function registerPolicyGate(pi: ExtensionAPI): void {
71
+ let wrapped = false;
72
+
73
+ const wrapProviders = (ctx: any) => {
74
+ if (wrapped) return;
75
+ for (const providerId of gatedProviders()) {
76
+ const provider = ctx.modelRegistry.getProvider(providerId);
77
+ if (!provider) continue;
78
+
79
+ const guard = (model: any) => {
80
+ const decision = checkModel(providerId, model?.id ?? "unknown");
81
+ if (!decision.allowed) throw new ModelPolicyError(decision.message);
82
+ };
83
+
84
+ const originalStream = provider.stream?.bind(provider);
85
+ const originalStreamSimple = provider.streamSimple?.bind(provider);
86
+
87
+ pi.registerProvider({
88
+ ...provider,
89
+ ...(originalStream && {
90
+ stream: (model: any, ...rest: any[]) => {
91
+ guard(model);
92
+ return originalStream(model, ...rest);
93
+ },
94
+ }),
95
+ ...(originalStreamSimple && {
96
+ streamSimple: (model: any, ...rest: any[]) => {
97
+ guard(model);
98
+ return originalStreamSimple(model, ...rest);
99
+ },
100
+ }),
101
+ });
102
+ }
103
+ wrapped = true;
104
+ };
105
+
106
+ pi.on("session_start", async (_event, ctx) => {
107
+ loadPolicy();
108
+ wrapProviders(ctx);
109
+ });
110
+
111
+ pi.registerCommand("provider", {
112
+ description: "Toggle which providers may be used (approve | remove <name>)",
113
+ getArgumentCompletions: (prefix) => {
114
+ const [action, name = ""] = prefix.split(/\s+/);
115
+ if (!prefix.includes(" ")) {
116
+ return ["approve", "remove"]
117
+ .filter((option) => option.startsWith(action))
118
+ .map((option) => ({ value: option, label: option }));
119
+ }
120
+ if (action !== "approve" && action !== "remove") return [];
121
+ return gatedProviders()
122
+ .filter((provider) => provider.startsWith(name))
123
+ .map((provider) => ({ value: `${action} ${provider}`, label: provider }));
124
+ },
125
+ handler: async (args, ctx) => {
126
+ const [action, ...rest] = args.trim().split(/\s+/).filter(Boolean);
127
+ const name = rest.join(" ");
128
+
129
+ if (action === "approve" || action === "remove") {
130
+ if (!name) {
131
+ ctx.ui.notify(`Usage: /provider ${action} <provider>`, "warning");
132
+ return;
133
+ }
134
+ if (!gatedProviders().includes(name)) {
135
+ ctx.ui.notify(
136
+ `“${name}” is not a gated provider. Gated: ${gatedProviders().join(", ") || "none"}`,
137
+ "warning",
138
+ );
139
+ return;
140
+ }
141
+ if (action === "approve") approve(name);
142
+ else revoke(name);
143
+ ctx.ui.notify(`${name} is now ${isApproved(name) ? "approved" : "blocked"}.`, "info");
144
+ return;
145
+ }
146
+
147
+ if (action) {
148
+ ctx.ui.notify(`Unknown action “${action}”. Use: /provider [approve|remove <name>]`, "warning");
149
+ return;
150
+ }
151
+
152
+ const rows = await providerRows(ctx);
153
+
154
+ // Headless: plain text, no cursor to draw.
155
+ if (!ctx.hasUI) {
156
+ ctx.ui.notify(
157
+ rows.map((row) => `${row.state === "auto" || row.state === "approved" ? "[on] " : "[off]"} ${row.display}: ${STATE_TEXT[row.state]}`).join("\n"),
158
+ "info",
159
+ );
160
+ return;
161
+ }
162
+
163
+ await openProviderPicker(ctx, {
164
+ rows: () => providerRows(ctx),
165
+ toggle: (provider) => toggleProvider(provider) as StateKey,
166
+ });
167
+ },
168
+ });
169
+ }