@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,183 +1,183 @@
1
- import { readConfig, updateConfig } from "../config.ts";
2
-
3
- /**
4
- * Approval policy for model selection.
5
- *
6
- * Subscription providers are free to use. Metered providers (OpenRouter and
7
- * anything else that bills per token) require an explicit approval before a
8
- * request is allowed to leave the machine.
9
- */
10
-
11
- export interface PolicyFile {
12
- autoApprove: string[];
13
- requireApproval: string[];
14
- deny: string[];
15
- }
16
-
17
- const DEFAULT_POLICY: PolicyFile = {
18
- autoApprove: ["anthropic/*", "openai-codex/*"],
19
- requireApproval: ["openrouter/*", "google/*", "openai/*", "xai/*"],
20
- deny: [],
21
- };
22
-
23
- export type Decision =
24
- | { allowed: true; reason: "auto" | "approved" }
25
- | { allowed: false; reason: "denied" | "needs-approval"; message: string };
26
-
27
- const approvals = new Map<string, number>();
28
-
29
- export function loadPolicy(): PolicyFile {
30
- return readConfig().policy;
31
- }
32
-
33
- export function savePolicy(next: PolicyFile): void {
34
- updateConfig((config) => {
35
- config.policy = {
36
- autoApprove: next.autoApprove ?? DEFAULT_POLICY.autoApprove,
37
- requireApproval: next.requireApproval ?? DEFAULT_POLICY.requireApproval,
38
- deny: next.deny ?? DEFAULT_POLICY.deny,
39
- };
40
- });
41
- }
42
-
43
- function matches(pattern: string, value: string): boolean {
44
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
45
- return new RegExp(`^${escaped}$`).test(value);
46
- }
47
-
48
- function matchesAny(patterns: string[], value: string): boolean {
49
- return patterns.some((pattern) => matches(pattern, value));
50
- }
51
-
52
- /**
53
- * Provider ids that require approval, derived from the policy patterns so the
54
- * toggle list always reflects the configured file rather than a hardcoded set.
55
- */
56
- export function gatedProviders(): string[] {
57
- const names = loadPolicy().requireApproval
58
- .map((pattern) => pattern.split("/")[0])
59
- .filter((name) => name && !name.includes("*"));
60
- return [...new Set(names)].sort();
61
- }
62
-
63
- export type ProviderState = "auto" | "approved" | "blocked" | "denied";
64
-
65
- /** How the policy currently treats a provider, for display and toggling. */
66
- export function providerState(provider: string): ProviderState {
67
- const current = loadPolicy();
68
- if (matchesAny(current.deny, `${provider}/*`) || matchesAny(current.deny, provider)) return "denied";
69
- if (matchesAny(current.requireApproval, `${provider}/*`)) {
70
- return isApproved(provider) ? "approved" : "blocked";
71
- }
72
- return "auto";
73
- }
74
-
75
- /**
76
- * Flips whether a provider may be used.
77
- *
78
- * Auto-approved providers are moved into `requireApproval` so the switch is
79
- * reversible; gated ones just gain or lose their session grant. Denied
80
- * providers are left alone; `deny` is an explicit, deliberate block.
81
- */
82
- export function toggleProvider(provider: string): ProviderState {
83
- const state = providerState(provider);
84
- if (state === "denied") return state;
85
-
86
- if (state === "auto") {
87
- const current = loadPolicy();
88
- savePolicy({
89
- ...current,
90
- autoApprove: current.autoApprove.filter((pattern) => !matches(pattern, `${provider}/*`) && pattern !== `${provider}/*`),
91
- requireApproval: [...new Set([...current.requireApproval, `${provider}/*`])],
92
- });
93
- revoke(provider);
94
- return "blocked";
95
- }
96
-
97
- if (state === "approved") {
98
- revoke(provider);
99
- return "blocked";
100
- }
101
-
102
- approve(provider);
103
- return "approved";
104
- }
105
-
106
- /** Grant approval for a provider until `untilMs` (omitted means this session). */
107
- export function approve(provider: string, durationMs?: number): void {
108
- approvals.set(provider, durationMs ? Date.now() + durationMs : Number.MAX_SAFE_INTEGER);
109
- }
110
-
111
- export function revoke(provider: string): void {
112
- approvals.delete(provider);
113
- }
114
-
115
- export function isApproved(provider: string): boolean {
116
- const until = approvals.get(provider);
117
- return until !== undefined && Date.now() < until;
118
- }
119
-
120
- /** Flips a provider's approval and reports the resulting state. */
121
- export function toggleApproval(provider: string): boolean {
122
- if (isApproved(provider)) {
123
- revoke(provider);
124
- return false;
125
- }
126
- approve(provider);
127
- return true;
128
- }
129
-
130
- export interface ProviderApproval {
131
- provider: string;
132
- approved: boolean;
133
- until?: number;
134
- }
135
-
136
- /** Current approval state for every gated provider, for display. */
137
- export function approvalStates(): ProviderApproval[] {
138
- return gatedProviders().map((provider) => {
139
- const until = approvals.get(provider);
140
- const approved = until !== undefined && Date.now() < until;
141
- return { provider, approved, until: approved && until !== Number.MAX_SAFE_INTEGER ? until : undefined };
142
- });
143
- }
144
-
145
- export function checkModel(provider: string, modelId: string): Decision {
146
- const current = loadPolicy();
147
- const ref = `${provider}/${modelId}`;
148
-
149
- if (matchesAny(current.deny, ref) || matchesAny(current.deny, `${provider}/*`)) {
150
- return { allowed: false, reason: "denied", message: `${ref} is denied by model policy.` };
151
- }
152
- if (matchesAny(current.autoApprove, ref)) return { allowed: true, reason: "auto" };
153
-
154
- const gated = matchesAny(current.requireApproval, ref);
155
- if (!gated) return { allowed: true, reason: "auto" };
156
-
157
- if (!isApproved(provider)) {
158
- return {
159
- allowed: false,
160
- reason: "needs-approval",
161
- message:
162
- `${ref} is a metered (pay-per-token) model and is not approved in this session. `
163
- + `Use a subscription model such as anthropic/* or openai-codex/*, or ask the user to run `
164
- + `/provider approve ${provider}.`,
165
- };
166
- }
167
-
168
- return { allowed: true, reason: "approved" };
169
- }
170
-
171
- export function policySummary(): string {
172
- const current = loadPolicy();
173
- const active = approvalStates()
174
- .filter((entry) => entry.approved)
175
- .map((entry) => `${entry.provider}${entry.until ? ` (until ${new Date(entry.until).toLocaleTimeString()})` : " (session)"}`);
176
- return [
177
- "Model approval policy",
178
- ` auto-approved: ${current.autoApprove.join(", ") || "none"}`,
179
- ` needs approval: ${current.requireApproval.join(", ") || "none"}`,
180
- ` denied: ${current.deny.join(", ") || "none"}`,
181
- ` approved now: ${active.join(", ") || "none"}`,
182
- ].join("\n");
183
- }
1
+ import { readConfig, updateConfig } from "../config.ts";
2
+
3
+ /**
4
+ * Approval policy for model selection.
5
+ *
6
+ * Subscription providers are free to use. Metered providers (OpenRouter and
7
+ * anything else that bills per token) require an explicit approval before a
8
+ * request is allowed to leave the machine.
9
+ */
10
+
11
+ export interface PolicyFile {
12
+ autoApprove: string[];
13
+ requireApproval: string[];
14
+ deny: string[];
15
+ }
16
+
17
+ const DEFAULT_POLICY: PolicyFile = {
18
+ autoApprove: ["anthropic/*", "openai-codex/*"],
19
+ requireApproval: ["openrouter/*", "google/*", "openai/*", "xai/*"],
20
+ deny: [],
21
+ };
22
+
23
+ export type Decision =
24
+ | { allowed: true; reason: "auto" | "approved" }
25
+ | { allowed: false; reason: "denied" | "needs-approval"; message: string };
26
+
27
+ const approvals = new Map<string, number>();
28
+
29
+ export function loadPolicy(): PolicyFile {
30
+ return readConfig().policy;
31
+ }
32
+
33
+ export function savePolicy(next: PolicyFile): void {
34
+ updateConfig((config) => {
35
+ config.policy = {
36
+ autoApprove: next.autoApprove ?? DEFAULT_POLICY.autoApprove,
37
+ requireApproval: next.requireApproval ?? DEFAULT_POLICY.requireApproval,
38
+ deny: next.deny ?? DEFAULT_POLICY.deny,
39
+ };
40
+ });
41
+ }
42
+
43
+ function matches(pattern: string, value: string): boolean {
44
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
45
+ return new RegExp(`^${escaped}$`).test(value);
46
+ }
47
+
48
+ function matchesAny(patterns: string[], value: string): boolean {
49
+ return patterns.some((pattern) => matches(pattern, value));
50
+ }
51
+
52
+ /**
53
+ * Provider ids that require approval, derived from the policy patterns so the
54
+ * toggle list always reflects the configured file rather than a hardcoded set.
55
+ */
56
+ export function gatedProviders(): string[] {
57
+ const names = loadPolicy().requireApproval
58
+ .map((pattern) => pattern.split("/")[0])
59
+ .filter((name) => name && !name.includes("*"));
60
+ return [...new Set(names)].sort();
61
+ }
62
+
63
+ export type ProviderState = "auto" | "approved" | "blocked" | "denied";
64
+
65
+ /** How the policy currently treats a provider, for display and toggling. */
66
+ export function providerState(provider: string): ProviderState {
67
+ const current = loadPolicy();
68
+ if (matchesAny(current.deny, `${provider}/*`) || matchesAny(current.deny, provider)) return "denied";
69
+ if (matchesAny(current.requireApproval, `${provider}/*`)) {
70
+ return isApproved(provider) ? "approved" : "blocked";
71
+ }
72
+ return "auto";
73
+ }
74
+
75
+ /**
76
+ * Flips whether a provider may be used.
77
+ *
78
+ * Auto-approved providers are moved into `requireApproval` so the switch is
79
+ * reversible; gated ones just gain or lose their session grant. Denied
80
+ * providers are left alone; `deny` is an explicit, deliberate block.
81
+ */
82
+ export function toggleProvider(provider: string): ProviderState {
83
+ const state = providerState(provider);
84
+ if (state === "denied") return state;
85
+
86
+ if (state === "auto") {
87
+ const current = loadPolicy();
88
+ savePolicy({
89
+ ...current,
90
+ autoApprove: current.autoApprove.filter((pattern) => !matches(pattern, `${provider}/*`) && pattern !== `${provider}/*`),
91
+ requireApproval: [...new Set([...current.requireApproval, `${provider}/*`])],
92
+ });
93
+ revoke(provider);
94
+ return "blocked";
95
+ }
96
+
97
+ if (state === "approved") {
98
+ revoke(provider);
99
+ return "blocked";
100
+ }
101
+
102
+ approve(provider);
103
+ return "approved";
104
+ }
105
+
106
+ /** Grant approval for a provider until `untilMs` (omitted means this session). */
107
+ export function approve(provider: string, durationMs?: number): void {
108
+ approvals.set(provider, durationMs ? Date.now() + durationMs : Number.MAX_SAFE_INTEGER);
109
+ }
110
+
111
+ export function revoke(provider: string): void {
112
+ approvals.delete(provider);
113
+ }
114
+
115
+ export function isApproved(provider: string): boolean {
116
+ const until = approvals.get(provider);
117
+ return until !== undefined && Date.now() < until;
118
+ }
119
+
120
+ /** Flips a provider's approval and reports the resulting state. */
121
+ export function toggleApproval(provider: string): boolean {
122
+ if (isApproved(provider)) {
123
+ revoke(provider);
124
+ return false;
125
+ }
126
+ approve(provider);
127
+ return true;
128
+ }
129
+
130
+ export interface ProviderApproval {
131
+ provider: string;
132
+ approved: boolean;
133
+ until?: number;
134
+ }
135
+
136
+ /** Current approval state for every gated provider, for display. */
137
+ export function approvalStates(): ProviderApproval[] {
138
+ return gatedProviders().map((provider) => {
139
+ const until = approvals.get(provider);
140
+ const approved = until !== undefined && Date.now() < until;
141
+ return { provider, approved, until: approved && until !== Number.MAX_SAFE_INTEGER ? until : undefined };
142
+ });
143
+ }
144
+
145
+ export function checkModel(provider: string, modelId: string): Decision {
146
+ const current = loadPolicy();
147
+ const ref = `${provider}/${modelId}`;
148
+
149
+ if (matchesAny(current.deny, ref) || matchesAny(current.deny, `${provider}/*`)) {
150
+ return { allowed: false, reason: "denied", message: `${ref} is denied by model policy.` };
151
+ }
152
+ if (matchesAny(current.autoApprove, ref)) return { allowed: true, reason: "auto" };
153
+
154
+ const gated = matchesAny(current.requireApproval, ref);
155
+ if (!gated) return { allowed: true, reason: "auto" };
156
+
157
+ if (!isApproved(provider)) {
158
+ return {
159
+ allowed: false,
160
+ reason: "needs-approval",
161
+ message:
162
+ `${ref} is a metered (pay-per-token) model and is not approved in this session. `
163
+ + `Use a subscription model such as anthropic/* or openai-codex/*, or ask the user to run `
164
+ + `/provider approve ${provider}.`,
165
+ };
166
+ }
167
+
168
+ return { allowed: true, reason: "approved" };
169
+ }
170
+
171
+ export function policySummary(): string {
172
+ const current = loadPolicy();
173
+ const active = approvalStates()
174
+ .filter((entry) => entry.approved)
175
+ .map((entry) => `${entry.provider}${entry.until ? ` (until ${new Date(entry.until).toLocaleTimeString()})` : " (session)"}`);
176
+ return [
177
+ "Model approval policy",
178
+ ` auto-approved: ${current.autoApprove.join(", ") || "none"}`,
179
+ ` needs approval: ${current.requireApproval.join(", ") || "none"}`,
180
+ ` denied: ${current.deny.join(", ") || "none"}`,
181
+ ` approved now: ${active.join(", ") || "none"}`,
182
+ ].join("\n");
183
+ }
@@ -1,64 +1,64 @@
1
- export type UsageRow = {
2
- group: string;
3
- label: string;
4
- remaining: number;
5
- resetAt?: number;
6
- checkedAt?: number;
7
- stale?: boolean;
8
- capacity?: number;
9
- };
10
-
11
- /*
12
- * How old a usage sample may be and still be poolable.
13
- *
14
- * This was 6 minutes when every refresh fetched the usage endpoint live.
15
- * Quota now comes from response headers, backed by a poll at most once per
16
- * 10 minutes, so a 6 minute bound marked idle accounts stale almost all the
17
- * time. It must stay comfortably above that poll interval; the underlying
18
- * windows are 5 hours and 7 days, so a sample minutes old is still accurate.
19
- */
20
- export const CLAUDE_FRESH_MS = 12 * 60_000;
21
- export const isClaudeAccount = (row: UsageRow) => row.group.startsWith("Claude ") && !row.group.startsWith("Claude pool ×");
22
- export const isFresh = (row: UsageRow, now = Date.now()) => !row.stale && !!row.checkedAt
23
- && now - row.checkedAt < CLAUDE_FRESH_MS && (!row.resetAt || row.resetAt > now);
24
-
25
- /** Percent of combined capacity, not a claim that quota transfers between accounts.
26
- * Without published capacities this is explicitly an equal-account estimate.
27
- */
28
- export function combinedWindow(rows: UsageRow[], label: string, expected: number, now = Date.now(), allowPartial = false) {
29
- const matching = rows.filter((r) => isClaudeAccount(r) && r.label === label && isFresh(r, now));
30
- const partial = matching.length !== expected;
31
- if (!expected || !matching.length || (partial && !allowPartial)) return undefined;
32
- const weighted = matching.every((r) => typeof r.capacity === "number" && r.capacity > 0);
33
- const total = matching.reduce((sum, r) => sum + (weighted ? r.capacity! : 1), 0);
34
- const remaining = matching.reduce((sum, r) => sum + r.remaining * (weighted ? r.capacity! : 1), 0) / total;
35
- const resets = matching.map((r) => r.resetAt).filter((t): t is number => !!t && t > now);
36
- return { label, remaining, resetAt: resets.length ? Math.min(...resets) : undefined, estimated: !weighted, partial };
37
- }
38
-
39
- export function scopedLabels(rows: UsageRow[], modelId?: string): string[] {
40
- const labels = [...new Set(rows.filter(isClaudeAccount).map((r) => r.label).filter((l) => l.startsWith("7d ")))];
41
- const model = modelId?.toLowerCase() ?? "";
42
- return labels.sort((a, b) => Number(matchesScope(b, model)) - Number(matchesScope(a, model)) || a.localeCompare(b));
43
- }
44
-
45
- function matchesScope(label: string, model: string): boolean {
46
- const family = label.slice(3).toLowerCase();
47
- return model.includes(family) || (family === "fable" && model.includes("mythos"));
48
- }
49
-
50
- /** An account must pass ALL applicable windows; independent averages cannot answer this. */
51
- export function poolAvailability(rows: UsageRow[], expected: number, modelId?: string, now = Date.now()) {
52
- const groups = [...new Set(rows.filter(isClaudeAccount).map((r) => r.group))];
53
- let ready = 0;
54
- let unknown = Math.max(0, expected - groups.length);
55
- for (const group of groups) {
56
- const account = rows.filter((r) => r.group === group);
57
- const required = ["5h", "7d", ...scopedLabels(account).filter((l) => matchesScope(l, modelId?.toLowerCase() ?? ""))];
58
- const windows = required.map((label) => account.find((r) => r.label === label));
59
- if (windows.some((r) => r && isFresh(r, now) && r.remaining <= 0)) continue;
60
- if (windows.some((r) => !r || !isFresh(r, now))) { unknown++; continue; }
61
- ready++;
62
- }
63
- return { ready, unknown, total: expected };
64
- }
1
+ export type UsageRow = {
2
+ group: string;
3
+ label: string;
4
+ remaining: number;
5
+ resetAt?: number;
6
+ checkedAt?: number;
7
+ stale?: boolean;
8
+ capacity?: number;
9
+ };
10
+
11
+ /*
12
+ * How old a usage sample may be and still be poolable.
13
+ *
14
+ * This was 6 minutes when every refresh fetched the usage endpoint live.
15
+ * Quota now comes from response headers, backed by a poll at most once per
16
+ * 10 minutes, so a 6 minute bound marked idle accounts stale almost all the
17
+ * time. It must stay comfortably above that poll interval; the underlying
18
+ * windows are 5 hours and 7 days, so a sample minutes old is still accurate.
19
+ */
20
+ export const CLAUDE_FRESH_MS = 12 * 60_000;
21
+ export const isClaudeAccount = (row: UsageRow) => row.group.startsWith("Claude ") && !row.group.startsWith("Claude pool ×");
22
+ export const isFresh = (row: UsageRow, now = Date.now()) => !row.stale && !!row.checkedAt
23
+ && now - row.checkedAt < CLAUDE_FRESH_MS && (!row.resetAt || row.resetAt > now);
24
+
25
+ /** Percent of combined capacity, not a claim that quota transfers between accounts.
26
+ * Without published capacities this is explicitly an equal-account estimate.
27
+ */
28
+ export function combinedWindow(rows: UsageRow[], label: string, expected: number, now = Date.now(), allowPartial = false) {
29
+ const matching = rows.filter((r) => isClaudeAccount(r) && r.label === label && isFresh(r, now));
30
+ const partial = matching.length !== expected;
31
+ if (!expected || !matching.length || (partial && !allowPartial)) return undefined;
32
+ const weighted = matching.every((r) => typeof r.capacity === "number" && r.capacity > 0);
33
+ const total = matching.reduce((sum, r) => sum + (weighted ? r.capacity! : 1), 0);
34
+ const remaining = matching.reduce((sum, r) => sum + r.remaining * (weighted ? r.capacity! : 1), 0) / total;
35
+ const resets = matching.map((r) => r.resetAt).filter((t): t is number => !!t && t > now);
36
+ return { label, remaining, resetAt: resets.length ? Math.min(...resets) : undefined, estimated: !weighted, partial };
37
+ }
38
+
39
+ export function scopedLabels(rows: UsageRow[], modelId?: string): string[] {
40
+ const labels = [...new Set(rows.filter(isClaudeAccount).map((r) => r.label).filter((l) => l.startsWith("7d ")))];
41
+ const model = modelId?.toLowerCase() ?? "";
42
+ return labels.sort((a, b) => Number(matchesScope(b, model)) - Number(matchesScope(a, model)) || a.localeCompare(b));
43
+ }
44
+
45
+ function matchesScope(label: string, model: string): boolean {
46
+ const family = label.slice(3).toLowerCase();
47
+ return model.includes(family) || (family === "fable" && model.includes("mythos"));
48
+ }
49
+
50
+ /** An account must pass ALL applicable windows; independent averages cannot answer this. */
51
+ export function poolAvailability(rows: UsageRow[], expected: number, modelId?: string, now = Date.now()) {
52
+ const groups = [...new Set(rows.filter(isClaudeAccount).map((r) => r.group))];
53
+ let ready = 0;
54
+ let unknown = Math.max(0, expected - groups.length);
55
+ for (const group of groups) {
56
+ const account = rows.filter((r) => r.group === group);
57
+ const required = ["5h", "7d", ...scopedLabels(account).filter((l) => matchesScope(l, modelId?.toLowerCase() ?? ""))];
58
+ const windows = required.map((label) => account.find((r) => r.label === label));
59
+ if (windows.some((r) => r && isFresh(r, now) && r.remaining <= 0)) continue;
60
+ if (windows.some((r) => !r || !isFresh(r, now))) { unknown++; continue; }
61
+ ready++;
62
+ }
63
+ return { ready, unknown, total: expected };
64
+ }