@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,182 +1,182 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
- import { refreshUsage, startPolling, stopPolling, subscribe, usageState } from "../../services/usage-service.ts";
4
- import { renderUsageLines, usageSummaryText } from "../../ui/usage-bars.ts";
5
- import { formatTokens, sanitize } from "../../ui/format.ts";
6
-
7
- /**
8
- * Single-line session footer (no working-directory line) with the subscription
9
- * bars rendered directly underneath it.
10
- *
11
- * The footer no longer owns the poll loop; it subscribes to usage-service and
12
- * re-renders on change. That is what lets non-UI consumers get fresh data.
13
- */
14
-
15
- const SUBSCRIPTION_PROVIDERS = new Set(["anthropic", "openai-codex", "kimi-coding"]);
16
-
17
- export function registerFooter(pi: ExtensionAPI): void {
18
- let showUsage = true;
19
- let requestRender: (() => void) | undefined;
20
- let unsubscribe: (() => void) | undefined;
21
-
22
- const apply = (ctx: any) => {
23
- if (!ctx.hasUI) return;
24
- if (!showUsage) {
25
- ctx.ui.setFooter(undefined);
26
- return;
27
- }
28
-
29
- ctx.ui.setFooter((tui: any, theme: any, footerData: any) => {
30
- requestRender = () => tui.requestRender();
31
- return {
32
- invalidate() {},
33
- render(rawWidth: number): string[] {
34
- const width = Math.max(1, Math.floor(Number(rawWidth) || 0));
35
- const lines: string[] = [];
36
-
37
- let input = 0;
38
- let output = 0;
39
- let cacheRead = 0;
40
- let cacheWrite = 0;
41
- let cost = 0;
42
- let latestHitRate: number | undefined;
43
-
44
- for (const entry of ctx.sessionManager.getEntries()) {
45
- const usage = entry.type === "message"
46
- ? (entry.message.role === "assistant" || entry.message.role === "toolResult" ? entry.message.usage : undefined)
47
- : (entry.type === "branch_summary" || entry.type === "compaction" ? entry.usage : undefined);
48
- if (!usage) continue;
49
- input += usage.input ?? 0;
50
- output += usage.output ?? 0;
51
- cacheRead += usage.cacheRead ?? 0;
52
- cacheWrite += usage.cacheWrite ?? 0;
53
- cost += usage.cost?.total ?? 0;
54
- if (entry.type === "message" && entry.message.role === "assistant") {
55
- const prompt = (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
56
- latestHitRate = prompt > 0 ? ((usage.cacheRead ?? 0) / prompt) * 100 : undefined;
57
- }
58
- }
59
-
60
- const parts: string[] = [];
61
- if (input) parts.push(`↑${formatTokens(input)}`);
62
- if (output) parts.push(`↓${formatTokens(output)}`);
63
- if (cacheRead) parts.push(`R${formatTokens(cacheRead)}`);
64
- if (cacheWrite) parts.push(`W${formatTokens(cacheWrite)}`);
65
- if ((cacheRead || cacheWrite) && latestHitRate !== undefined) parts.push(`CH${latestHitRate.toFixed(1)}%`);
66
-
67
- const model = ctx.model;
68
- const subscription = model ? SUBSCRIPTION_PROVIDERS.has(model.provider) : false;
69
- if (cost || subscription) parts.push(`$${cost.toFixed(3)}${subscription ? " (sub)" : ""}`);
70
-
71
- const contextUsage = ctx.getContextUsage();
72
- const contextWindow = contextUsage?.contextWindow ?? model?.contextWindow ?? 0;
73
- const percentValue = contextUsage?.percent ?? 0;
74
- const percentText = contextUsage?.percent != null ? `${percentValue.toFixed(1)}%` : "?";
75
- const contextText = `${percentText}/${formatTokens(contextWindow)}`;
76
- parts.push(percentValue > 90
77
- ? theme.fg("error", contextText)
78
- : percentValue > 70 ? theme.fg("warning", contextText) : contextText);
79
-
80
- const branch = footerData.getGitBranch?.();
81
- if (branch) parts.push(`⎇ ${branch}`);
82
-
83
- let left = parts.join(" ");
84
- if (visibleWidth(left) > width) left = truncateToWidth(left, width, "...");
85
- let leftWidth = visibleWidth(left);
86
-
87
- let right = model?.id ?? "no-model";
88
- if (model?.reasoning) {
89
- const level = ctx.thinkingLevel || "off";
90
- right = level === "off" ? `${right} • thinking off` : `${right} • ${level}`;
91
- }
92
- if (footerData.getAvailableProviderCount?.() > 1 && model) {
93
- const withProvider = `(${model.provider}) ${right}`;
94
- if (leftWidth + 2 + visibleWidth(withProvider) <= width) right = withProvider;
95
- }
96
-
97
- // Keep the composed line within `width`: shrink right, then left.
98
- let rightWidth = visibleWidth(right);
99
- if (rightWidth > width) {
100
- right = truncateToWidth(right, width, "...");
101
- rightWidth = visibleWidth(right);
102
- }
103
- if (leftWidth + 2 + rightWidth > width) {
104
- const leftBudget = Math.max(0, width - rightWidth - 2);
105
- left = leftBudget > 0 ? truncateToWidth(left, leftBudget, "...") : "";
106
- leftWidth = visibleWidth(left);
107
- }
108
- const padding = " ".repeat(Math.max(0, width - leftWidth - rightWidth));
109
- lines.push(truncateToWidth(theme.fg("dim", left) + theme.fg("dim", padding + right), width, ""));
110
-
111
- const statuses = footerData.getExtensionStatuses?.() as Map<string, string> | undefined;
112
- if (statuses && statuses.size > 0) {
113
- const statusLine = Array.from(statuses.entries())
114
- .sort(([a], [b]) => a.localeCompare(b))
115
- .map(([, text]) => sanitize(text))
116
- .join(" ");
117
- lines.push(truncateToWidth(statusLine, width, theme.fg("dim", "...")));
118
- }
119
-
120
- if (showUsage) {
121
- lines.push(...renderUsageLines(
122
- usageState(),
123
- theme,
124
- width,
125
- model?.provider === "anthropic" ? model.id : undefined,
126
- ));
127
- }
128
-
129
- // Final safety net: never emit a line wider than the terminal.
130
- return lines.map((line) => (visibleWidth(line) > width ? truncateToWidth(line, width, "") : line));
131
- },
132
- };
133
- });
134
- };
135
-
136
- pi.on("session_start", async (_event, ctx) => {
137
- apply(ctx);
138
- unsubscribe ??= subscribe(() => requestRender?.());
139
- // Polling is started even without a UI so headless consumers stay current.
140
- startPolling(ctx);
141
- });
142
-
143
- pi.on("model_select", async (_event, ctx) => {
144
- apply(ctx);
145
- requestRender?.();
146
- });
147
-
148
- pi.on("agent_settled", async (_event, ctx) => {
149
- // Status polling is cosmetic: never hold up agent completion on auth/network.
150
- if (ctx.hasUI && showUsage) void refreshUsage(ctx);
151
- });
152
-
153
- pi.on("session_shutdown", async () => {
154
- unsubscribe?.();
155
- unsubscribe = undefined;
156
- stopPolling();
157
- });
158
-
159
- pi.registerCommand("usage", {
160
- description: "Refresh, hide, or show the subscription bars (on | off | text)",
161
- getArgumentCompletions: (prefix) =>
162
- ["on", "off", "text"]
163
- .filter((option) => option.startsWith(prefix))
164
- .map((option) => ({ value: option, label: option })),
165
- handler: async (args, ctx) => {
166
- const action = args.trim().toLowerCase();
167
- if (action === "off") {
168
- showUsage = false;
169
- apply(ctx);
170
- requestRender?.();
171
- ctx.ui.notify("Usage bars hidden. Use /usage on to restore them.", "info");
172
- return;
173
- }
174
- if (action === "on") showUsage = true;
175
- await refreshUsage(ctx, true);
176
- apply(ctx);
177
- if (action === "text" || !ctx.hasUI) ctx.ui.notify(usageSummaryText(usageState()), "info");
178
- else if (usageState().errors.length > 0) ctx.ui.notify(usageState().errors.join("\n"), "warning");
179
- },
180
- });
181
-
182
- }
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+ import { refreshUsage, startPolling, stopPolling, subscribe, usageState } from "../../services/usage-service.ts";
4
+ import { renderUsageLines, usageSummaryText } from "../../ui/usage-bars.ts";
5
+ import { formatTokens, sanitize } from "../../ui/format.ts";
6
+
7
+ /**
8
+ * Single-line session footer (no working-directory line) with the subscription
9
+ * bars rendered directly underneath it.
10
+ *
11
+ * The footer no longer owns the poll loop; it subscribes to usage-service and
12
+ * re-renders on change. That is what lets non-UI consumers get fresh data.
13
+ */
14
+
15
+ const SUBSCRIPTION_PROVIDERS = new Set(["anthropic", "openai-codex", "kimi-coding"]);
16
+
17
+ export function registerFooter(pi: ExtensionAPI): void {
18
+ let showUsage = true;
19
+ let requestRender: (() => void) | undefined;
20
+ let unsubscribe: (() => void) | undefined;
21
+
22
+ const apply = (ctx: any) => {
23
+ if (!ctx.hasUI) return;
24
+ if (!showUsage) {
25
+ ctx.ui.setFooter(undefined);
26
+ return;
27
+ }
28
+
29
+ ctx.ui.setFooter((tui: any, theme: any, footerData: any) => {
30
+ requestRender = () => tui.requestRender();
31
+ return {
32
+ invalidate() {},
33
+ render(rawWidth: number): string[] {
34
+ const width = Math.max(1, Math.floor(Number(rawWidth) || 0));
35
+ const lines: string[] = [];
36
+
37
+ let input = 0;
38
+ let output = 0;
39
+ let cacheRead = 0;
40
+ let cacheWrite = 0;
41
+ let cost = 0;
42
+ let latestHitRate: number | undefined;
43
+
44
+ for (const entry of ctx.sessionManager.getEntries()) {
45
+ const usage = entry.type === "message"
46
+ ? (entry.message.role === "assistant" || entry.message.role === "toolResult" ? entry.message.usage : undefined)
47
+ : (entry.type === "branch_summary" || entry.type === "compaction" ? entry.usage : undefined);
48
+ if (!usage) continue;
49
+ input += usage.input ?? 0;
50
+ output += usage.output ?? 0;
51
+ cacheRead += usage.cacheRead ?? 0;
52
+ cacheWrite += usage.cacheWrite ?? 0;
53
+ cost += usage.cost?.total ?? 0;
54
+ if (entry.type === "message" && entry.message.role === "assistant") {
55
+ const prompt = (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
56
+ latestHitRate = prompt > 0 ? ((usage.cacheRead ?? 0) / prompt) * 100 : undefined;
57
+ }
58
+ }
59
+
60
+ const parts: string[] = [];
61
+ if (input) parts.push(`↑${formatTokens(input)}`);
62
+ if (output) parts.push(`↓${formatTokens(output)}`);
63
+ if (cacheRead) parts.push(`R${formatTokens(cacheRead)}`);
64
+ if (cacheWrite) parts.push(`W${formatTokens(cacheWrite)}`);
65
+ if ((cacheRead || cacheWrite) && latestHitRate !== undefined) parts.push(`CH${latestHitRate.toFixed(1)}%`);
66
+
67
+ const model = ctx.model;
68
+ const subscription = model ? SUBSCRIPTION_PROVIDERS.has(model.provider) : false;
69
+ if (cost || subscription) parts.push(`$${cost.toFixed(3)}${subscription ? " (sub)" : ""}`);
70
+
71
+ const contextUsage = ctx.getContextUsage();
72
+ const contextWindow = contextUsage?.contextWindow ?? model?.contextWindow ?? 0;
73
+ const percentValue = contextUsage?.percent ?? 0;
74
+ const percentText = contextUsage?.percent != null ? `${percentValue.toFixed(1)}%` : "?";
75
+ const contextText = `${percentText}/${formatTokens(contextWindow)}`;
76
+ parts.push(percentValue > 90
77
+ ? theme.fg("error", contextText)
78
+ : percentValue > 70 ? theme.fg("warning", contextText) : contextText);
79
+
80
+ const branch = footerData.getGitBranch?.();
81
+ if (branch) parts.push(`⎇ ${branch}`);
82
+
83
+ let left = parts.join(" ");
84
+ if (visibleWidth(left) > width) left = truncateToWidth(left, width, "...");
85
+ let leftWidth = visibleWidth(left);
86
+
87
+ let right = model?.id ?? "no-model";
88
+ if (model?.reasoning) {
89
+ const level = ctx.thinkingLevel || "off";
90
+ right = level === "off" ? `${right} • thinking off` : `${right} • ${level}`;
91
+ }
92
+ if (footerData.getAvailableProviderCount?.() > 1 && model) {
93
+ const withProvider = `(${model.provider}) ${right}`;
94
+ if (leftWidth + 2 + visibleWidth(withProvider) <= width) right = withProvider;
95
+ }
96
+
97
+ // Keep the composed line within `width`: shrink right, then left.
98
+ let rightWidth = visibleWidth(right);
99
+ if (rightWidth > width) {
100
+ right = truncateToWidth(right, width, "...");
101
+ rightWidth = visibleWidth(right);
102
+ }
103
+ if (leftWidth + 2 + rightWidth > width) {
104
+ const leftBudget = Math.max(0, width - rightWidth - 2);
105
+ left = leftBudget > 0 ? truncateToWidth(left, leftBudget, "...") : "";
106
+ leftWidth = visibleWidth(left);
107
+ }
108
+ const padding = " ".repeat(Math.max(0, width - leftWidth - rightWidth));
109
+ lines.push(truncateToWidth(theme.fg("dim", left) + theme.fg("dim", padding + right), width, ""));
110
+
111
+ const statuses = footerData.getExtensionStatuses?.() as Map<string, string> | undefined;
112
+ if (statuses && statuses.size > 0) {
113
+ const statusLine = Array.from(statuses.entries())
114
+ .sort(([a], [b]) => a.localeCompare(b))
115
+ .map(([, text]) => sanitize(text))
116
+ .join(" ");
117
+ lines.push(truncateToWidth(statusLine, width, theme.fg("dim", "...")));
118
+ }
119
+
120
+ if (showUsage) {
121
+ lines.push(...renderUsageLines(
122
+ usageState(),
123
+ theme,
124
+ width,
125
+ model?.provider === "anthropic" ? model.id : undefined,
126
+ ));
127
+ }
128
+
129
+ // Final safety net: never emit a line wider than the terminal.
130
+ return lines.map((line) => (visibleWidth(line) > width ? truncateToWidth(line, width, "") : line));
131
+ },
132
+ };
133
+ });
134
+ };
135
+
136
+ pi.on("session_start", async (_event, ctx) => {
137
+ apply(ctx);
138
+ unsubscribe ??= subscribe(() => requestRender?.());
139
+ // Polling is started even without a UI so headless consumers stay current.
140
+ startPolling(ctx);
141
+ });
142
+
143
+ pi.on("model_select", async (_event, ctx) => {
144
+ apply(ctx);
145
+ requestRender?.();
146
+ });
147
+
148
+ pi.on("agent_settled", async (_event, ctx) => {
149
+ // Status polling is cosmetic: never hold up agent completion on auth/network.
150
+ if (ctx.hasUI && showUsage) void refreshUsage(ctx);
151
+ });
152
+
153
+ pi.on("session_shutdown", async () => {
154
+ unsubscribe?.();
155
+ unsubscribe = undefined;
156
+ stopPolling();
157
+ });
158
+
159
+ pi.registerCommand("usage", {
160
+ description: "Refresh, hide, or show the subscription bars (on | off | text)",
161
+ getArgumentCompletions: (prefix) =>
162
+ ["on", "off", "text"]
163
+ .filter((option) => option.startsWith(prefix))
164
+ .map((option) => ({ value: option, label: option })),
165
+ handler: async (args, ctx) => {
166
+ const action = args.trim().toLowerCase();
167
+ if (action === "off") {
168
+ showUsage = false;
169
+ apply(ctx);
170
+ requestRender?.();
171
+ ctx.ui.notify("Usage bars hidden. Use /usage on to restore them.", "info");
172
+ return;
173
+ }
174
+ if (action === "on") showUsage = true;
175
+ await refreshUsage(ctx, true);
176
+ apply(ctx);
177
+ if (action === "text" || !ctx.hasUI) ctx.ui.notify(usageSummaryText(usageState()), "info");
178
+ else if (usageState().errors.length > 0) ctx.ui.notify(usageState().errors.join("\n"), "warning");
179
+ },
180
+ });
181
+
182
+ }
@@ -1,42 +1,42 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { registerAccountProvider } from "../../core/accounts/registry.ts";
3
- import { anthropicAccounts } from "./providers/anthropic.ts";
4
- import { codexAccounts } from "./providers/codex.ts";
5
- import { registerAnthropicProvider } from "./provider.ts";
6
- import { registerAccountCommands } from "./accounts.ts";
7
- import { registerRoutingCommands } from "./routing.ts";
8
- import { registerFooter } from "./footer.ts";
9
-
10
- /**
11
- * Subscriptions domain: Claude OAuth accounts, routing mode, and the quota HUD.
12
- *
13
- * This replaces the old `claude-multi-account.ts` + `compact-footer.ts` pair.
14
- * Account and routing commands are provider-agnostic: `/account <provider>` and
15
- * `/routing` dispatch through core/accounts/registry.ts, so adding a second
16
- * provider means writing one adapter, not new commands.
17
- * Authentication and model catalogue are separate concerns here; they were only
18
- * ever colocated because they shared an OAuth token.
19
- *
20
- * Relationship to @cortexkit/pi-anthropic-auth:
21
- * That package stays installed and keeps ownership of the Anthropic provider,
22
- * its stream implementation, and its own commands (/claude-account,
23
- * /claude-routing, /claude-quota, ...). Absorbing it fully was evaluated and
24
- * rejected, because its `dist/commands.js` exports only `registerCommands(pi)` as a
25
- * unit, so taking over individual commands would mean copying ~130 lines of
26
- * provider/model specs that drift on every vendor upgrade. This domain adds
27
- * the multi-account surface the vendor does not provide, and everything it
28
- * touches goes through src/vendor/anthropic.ts.
29
- */
30
- export default function subscriptions(pi: ExtensionAPI) {
31
- // Adapters register first so the generic commands can see them.
32
- registerAccountProvider(anthropicAccounts);
33
- registerAccountProvider(codexAccounts);
34
-
35
- // Own the Anthropic provider unless explicitly told to defer to cortexkit.
36
- // PI_PLUS_VENDOR_ANTHROPIC=1 restores the vendored package.
37
- if (process.env.PI_PLUS_VENDOR_ANTHROPIC !== "1") registerAnthropicProvider(pi);
38
-
39
- registerAccountCommands(pi);
40
- registerRoutingCommands(pi);
41
- registerFooter(pi);
42
- }
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { registerAccountProvider } from "../../core/accounts/registry.ts";
3
+ import { anthropicAccounts } from "./providers/anthropic.ts";
4
+ import { codexAccounts } from "./providers/codex.ts";
5
+ import { registerAnthropicProvider } from "./provider.ts";
6
+ import { registerAccountCommands } from "./accounts.ts";
7
+ import { registerRoutingCommands } from "./routing.ts";
8
+ import { registerFooter } from "./footer.ts";
9
+
10
+ /**
11
+ * Subscriptions domain: Claude OAuth accounts, routing mode, and the quota HUD.
12
+ *
13
+ * This replaces the old `claude-multi-account.ts` + `compact-footer.ts` pair.
14
+ * Account and routing commands are provider-agnostic: `/account <provider>` and
15
+ * `/routing` dispatch through core/accounts/registry.ts, so adding a second
16
+ * provider means writing one adapter, not new commands.
17
+ * Authentication and model catalogue are separate concerns here; they were only
18
+ * ever colocated because they shared an OAuth token.
19
+ *
20
+ * Relationship to @cortexkit/pi-anthropic-auth:
21
+ * That package stays installed and keeps ownership of the Anthropic provider,
22
+ * its stream implementation, and its own commands (/claude-account,
23
+ * /claude-routing, /claude-quota, ...). Absorbing it fully was evaluated and
24
+ * rejected, because its `dist/commands.js` exports only `registerCommands(pi)` as a
25
+ * unit, so taking over individual commands would mean copying ~130 lines of
26
+ * provider/model specs that drift on every vendor upgrade. This domain adds
27
+ * the multi-account surface the vendor does not provide, and everything it
28
+ * touches goes through src/vendor/anthropic.ts.
29
+ */
30
+ export default function subscriptions(pi: ExtensionAPI) {
31
+ // Adapters register first so the generic commands can see them.
32
+ registerAccountProvider(anthropicAccounts);
33
+ registerAccountProvider(codexAccounts);
34
+
35
+ // Own the Anthropic provider unless explicitly told to defer to cortexkit.
36
+ // PI_PLUS_VENDOR_ANTHROPIC=1 restores the vendored package.
37
+ if (process.env.PI_PLUS_VENDOR_ANTHROPIC !== "1") registerAnthropicProvider(pi);
38
+
39
+ registerAccountCommands(pi);
40
+ registerRoutingCommands(pi);
41
+ registerFooter(pi);
42
+ }