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