@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.
- package/LICENSE +21 -21
- package/README.md +190 -190
- package/config/pi-plus.example.json +60 -60
- package/config/skills/model-routing/SKILL.md +86 -86
- package/images/pi-plus.svg +10 -10
- package/package.json +67 -67
- package/server/board-server.mjs +641 -641
- package/server/package.json +17 -17
- package/src/core/accounts/registry.ts +93 -93
- package/src/core/anthropic/client-identity.ts +241 -241
- package/src/core/catalog/quality.ts +314 -314
- package/src/core/config.ts +169 -169
- package/src/core/env.ts +58 -58
- package/src/core/exec/process.ts +146 -146
- package/src/core/exec/ssh-config.ts +157 -157
- package/src/core/policy/policy.ts +183 -183
- package/src/core/quota/pool.ts +64 -64
- package/src/core/quota/usage-source.ts +289 -289
- package/src/core/store.ts +43 -43
- package/src/domains/agents/board-setup.ts +409 -409
- package/src/domains/agents/index.ts +462 -462
- package/src/domains/models/catalog-tool.ts +361 -361
- package/src/domains/models/index.ts +14 -14
- package/src/domains/models/policy-gate.ts +169 -169
- package/src/domains/models/provider-picker.ts +207 -207
- package/src/domains/remote/config-path.ts +41 -41
- package/src/domains/remote/index.ts +866 -866
- package/src/domains/remote/setup.ts +425 -425
- package/src/domains/setup/index.ts +220 -220
- package/src/domains/subscriptions/accounts.ts +242 -242
- package/src/domains/subscriptions/footer.ts +182 -182
- package/src/domains/subscriptions/index.ts +42 -42
- package/src/domains/subscriptions/provider.ts +219 -219
- package/src/domains/subscriptions/providers/anthropic.ts +149 -149
- package/src/domains/subscriptions/providers/codex.ts +148 -148
- package/src/domains/subscriptions/routing.ts +72 -72
- package/src/services/usage-service.ts +186 -186
- package/src/ui/format.ts +73 -73
- package/src/ui/usage-bars.ts +154 -154
- package/src/vendor/anthropic.ts +109 -109
package/src/ui/usage-bars.ts
CHANGED
|
@@ -1,154 +1,154 @@
|
|
|
1
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
-
import { combinedWindow, isClaudeAccount, poolAvailability, scopedLabels, type UsageRow } from "../core/quota/pool.ts";
|
|
3
|
-
import type { UsageState } from "../services/usage-service.ts";
|
|
4
|
-
import { formatReset, formatShortReset, hasTruecolor, levelColor, themeLevel } from "./format.ts";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Renders the Claude/Codex quota bars. Takes state as an argument rather than
|
|
8
|
-
* importing the service, so the renderer stays a pure function of its input.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
type Cell = { label: string; remaining?: number; resetAt?: number; partial?: boolean };
|
|
12
|
-
|
|
13
|
-
function pooled(state: UsageState, label: string): Cell | undefined {
|
|
14
|
-
return combinedWindow(state.rows, label, state.accounts, Date.now(), true);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function codexCell(state: UsageState, display: string, match: (label: string) => boolean): Cell | undefined {
|
|
18
|
-
const row = state.rows.find((candidate) => candidate.group === "Codex" && match(candidate.label));
|
|
19
|
-
return row ? { label: display, remaining: row.remaining, resetAt: row.resetAt } : { label: display };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Turns a scoped limit id into something that fits the label column.
|
|
24
|
-
*
|
|
25
|
-
* Anthropic names these with an internal id, e.g.
|
|
26
|
-
* `7d claude-weekly-scoped-fable`. Rendered raw it was truncated to the 6
|
|
27
|
-
* column label width and came out as "claude", which named neither the window
|
|
28
|
-
* nor the model. The trailing segment is the model family, so that is what is
|
|
29
|
-
* shown.
|
|
30
|
-
*/
|
|
31
|
-
function scopedDisplayName(label: string): string {
|
|
32
|
-
const family = label.replace(/^7d\s+/, "").split("-").pop() ?? label;
|
|
33
|
-
return family.charAt(0).toUpperCase() + family.slice(1);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function buildColumns(state: UsageState, modelId?: string): { claude: Cell[]; codex: Cell[] } {
|
|
37
|
-
// Three fixed tiers, so the block keeps its shape whether or not a scoped
|
|
38
|
-
// limit is currently reported.
|
|
39
|
-
const scoped = scopedLabels(state.rows, modelId)[0];
|
|
40
|
-
const scopedCell = scoped
|
|
41
|
-
? { ...(pooled(state, scoped) ?? { label: scoped }), label: scopedDisplayName(scoped) }
|
|
42
|
-
: { label: "Fable" };
|
|
43
|
-
|
|
44
|
-
const claude: Cell[] = [
|
|
45
|
-
{ ...(pooled(state, "5h") ?? { label: "5h" }), label: "5h" },
|
|
46
|
-
{ ...(pooled(state, "7d") ?? { label: "7d" }), label: "weekly" },
|
|
47
|
-
scopedCell,
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
// Codex reports only the two windows; it has no scoped equivalent.
|
|
51
|
-
const codex: Cell[] = [
|
|
52
|
-
codexCell(state, "5h", (label) => label === "5h") ?? { label: "5h" },
|
|
53
|
-
codexCell(state, "weekly", (label) => label === "weekly") ?? { label: "weekly" },
|
|
54
|
-
];
|
|
55
|
-
return { claude, codex };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** `Work 61% · Personal 88%`. Empty when there is nothing extra to say. */
|
|
59
|
-
function renderAccountSummary(state: UsageState, cellWidth: number): string | undefined {
|
|
60
|
-
const stamps = state.lastUsedAt ?? {};
|
|
61
|
-
const groups = [...new Set(state.rows.filter(isClaudeAccount).map((row) => row.group))];
|
|
62
|
-
if (groups.length < 2) return undefined;
|
|
63
|
-
|
|
64
|
-
const ordered = groups.sort((a, b) => (stamps[b] ?? 0) - (stamps[a] ?? 0) || a.localeCompare(b));
|
|
65
|
-
const shown = ordered.slice(0, 2);
|
|
66
|
-
|
|
67
|
-
const parts = shown.map((group) => {
|
|
68
|
-
const row = state.rows.find((candidate) => candidate.group === group && candidate.label === "5h");
|
|
69
|
-
const name = group.replace(/^Claude /, "");
|
|
70
|
-
if (!row) return `${name} -`;
|
|
71
|
-
return `${name} ${Math.round(row.remaining)}%${row.stale ? "*" : ""}`;
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const hidden = ordered.length - shown.length;
|
|
75
|
-
const text = parts.join(" · ") + (hidden > 0 ? ` +${hidden}` : "");
|
|
76
|
-
return text.length > cellWidth * 2 ? text.slice(0, cellWidth * 2) : text;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function renderCell(theme: any, cell: Cell, labelWidth: number, cellWidth: number): string {
|
|
80
|
-
const label = cell.label.slice(0, labelWidth).padEnd(labelWidth);
|
|
81
|
-
const reset = formatShortReset(cell.resetAt);
|
|
82
|
-
const resetWidth = 4;
|
|
83
|
-
const barWidth = Math.max(4, cellWidth - labelWidth - 6 - resetWidth - 3);
|
|
84
|
-
|
|
85
|
-
if (cell.remaining === undefined) {
|
|
86
|
-
return `${theme.fg("muted", label)} ${theme.fg("dim", "·".repeat(barWidth))} ${theme.fg("dim", " n/a")}${" ".repeat(resetWidth + 1)}`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const filled = Math.round((cell.remaining / 100) * barWidth);
|
|
90
|
-
const percentText = `${cell.partial ? "~" : ""}${Math.round(cell.remaining)}%`.padStart(5);
|
|
91
|
-
let bar: string;
|
|
92
|
-
let percent: string;
|
|
93
|
-
|
|
94
|
-
if (hasTruecolor()) {
|
|
95
|
-
// One smooth hue per bar: green when full, amber mid-way, red as it empties.
|
|
96
|
-
const paint = levelColor(cell.remaining);
|
|
97
|
-
bar = paint("█".repeat(filled)) + theme.fg("dim", "░".repeat(barWidth - filled));
|
|
98
|
-
percent = paint(percentText);
|
|
99
|
-
} else {
|
|
100
|
-
const color = themeLevel(cell.remaining);
|
|
101
|
-
bar = theme.fg(color, "█".repeat(filled)) + theme.fg("dim", "░".repeat(barWidth - filled));
|
|
102
|
-
percent = theme.fg(color, percentText);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return `${theme.fg("muted", label)} ${bar} ${percent} ${theme.fg("dim", reset.padEnd(resetWidth))}`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function renderUsageLines(state: UsageState, theme: any, width: number, modelId?: string): string[] {
|
|
109
|
-
if (state.loading) return [theme.fg("dim", " usage: loading…")];
|
|
110
|
-
if (state.rows.length === 0) {
|
|
111
|
-
if (state.errors.length > 0) return state.errors.map((error) => theme.fg("warning", ` ${error}`));
|
|
112
|
-
return [theme.fg("dim", " usage: unavailable")];
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const gap = 3;
|
|
116
|
-
const cellWidth = Math.max(22, Math.floor((width - 2 - gap) / 2));
|
|
117
|
-
const labelWidth = 6;
|
|
118
|
-
const { claude, codex } = buildColumns(state, modelId);
|
|
119
|
-
const availability = poolAvailability(state.rows, state.accounts, modelId);
|
|
120
|
-
const status = availability.ready
|
|
121
|
-
? `${availability.ready}/${availability.total} ready`
|
|
122
|
-
: availability.unknown ? "unknown/stale" : "exhausted";
|
|
123
|
-
const partial = claude.some((cell) => cell.partial);
|
|
124
|
-
const claudeTitle = `Claude Σ${state.accounts} · ${status}${partial ? " · partial" : ""}`;
|
|
125
|
-
const codexTitle = state.codexPlan ? `Codex · ${state.codexPlan}` : "Codex";
|
|
126
|
-
const lines = [` ${theme.fg("accent", claudeTitle.padEnd(cellWidth))}${" ".repeat(gap)}${theme.fg("accent", codexTitle)}`];
|
|
127
|
-
|
|
128
|
-
for (let index = 0; index < Math.max(claude.length, codex.length); index += 1) {
|
|
129
|
-
const left = claude[index] ? renderCell(theme, claude[index], labelWidth, cellWidth) : " ".repeat(cellWidth);
|
|
130
|
-
const right = codex[index] ? renderCell(theme, codex[index], labelWidth, cellWidth) : "";
|
|
131
|
-
lines.push(` ${left}${" ".repeat(gap)}${right}`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Per-account detail. With more than two accounts only the two most recently
|
|
135
|
-
// used are shown, so the footer stays two lines regardless of pool size.
|
|
136
|
-
const accountLine = renderAccountSummary(state, cellWidth);
|
|
137
|
-
if (accountLine) lines.push(` ${theme.fg("dim", accountLine)}`);
|
|
138
|
-
|
|
139
|
-
for (const error of state.errors) lines.push(theme.fg("warning", ` ${error}`));
|
|
140
|
-
return lines.map((line) => truncateToWidth(line, width, ""));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function usageSummaryText(state: UsageState): string {
|
|
144
|
-
const combined = ["5h", "7d", ...scopedLabels(state.rows)].map((label) => {
|
|
145
|
-
const pool = combinedWindow(state.rows, label, state.accounts, Date.now(), true);
|
|
146
|
-
return `Claude combined ${label}: ${pool
|
|
147
|
-
? `${pool.partial ? "~" : ""}${Math.round(pool.remaining)}% left${pool.partial ? " (partial: reporting accounts only)" : ""} ${formatReset(pool.resetAt)}`
|
|
148
|
-
: "unknown/stale"}`;
|
|
149
|
-
});
|
|
150
|
-
const summary = [...combined, ...state.rows.map(
|
|
151
|
-
(row: UsageRow) => `${row.group} ${row.label}: ${Math.round(row.remaining)}% left ${formatReset(row.resetAt)}${row.stale ? " (stale)" : ""}`.trim(),
|
|
152
|
-
)];
|
|
153
|
-
return [...summary, ...state.errors].join("\n") || "No usage data";
|
|
154
|
-
}
|
|
1
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { combinedWindow, isClaudeAccount, poolAvailability, scopedLabels, type UsageRow } from "../core/quota/pool.ts";
|
|
3
|
+
import type { UsageState } from "../services/usage-service.ts";
|
|
4
|
+
import { formatReset, formatShortReset, hasTruecolor, levelColor, themeLevel } from "./format.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Renders the Claude/Codex quota bars. Takes state as an argument rather than
|
|
8
|
+
* importing the service, so the renderer stays a pure function of its input.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
type Cell = { label: string; remaining?: number; resetAt?: number; partial?: boolean };
|
|
12
|
+
|
|
13
|
+
function pooled(state: UsageState, label: string): Cell | undefined {
|
|
14
|
+
return combinedWindow(state.rows, label, state.accounts, Date.now(), true);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function codexCell(state: UsageState, display: string, match: (label: string) => boolean): Cell | undefined {
|
|
18
|
+
const row = state.rows.find((candidate) => candidate.group === "Codex" && match(candidate.label));
|
|
19
|
+
return row ? { label: display, remaining: row.remaining, resetAt: row.resetAt } : { label: display };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Turns a scoped limit id into something that fits the label column.
|
|
24
|
+
*
|
|
25
|
+
* Anthropic names these with an internal id, e.g.
|
|
26
|
+
* `7d claude-weekly-scoped-fable`. Rendered raw it was truncated to the 6
|
|
27
|
+
* column label width and came out as "claude", which named neither the window
|
|
28
|
+
* nor the model. The trailing segment is the model family, so that is what is
|
|
29
|
+
* shown.
|
|
30
|
+
*/
|
|
31
|
+
function scopedDisplayName(label: string): string {
|
|
32
|
+
const family = label.replace(/^7d\s+/, "").split("-").pop() ?? label;
|
|
33
|
+
return family.charAt(0).toUpperCase() + family.slice(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildColumns(state: UsageState, modelId?: string): { claude: Cell[]; codex: Cell[] } {
|
|
37
|
+
// Three fixed tiers, so the block keeps its shape whether or not a scoped
|
|
38
|
+
// limit is currently reported.
|
|
39
|
+
const scoped = scopedLabels(state.rows, modelId)[0];
|
|
40
|
+
const scopedCell = scoped
|
|
41
|
+
? { ...(pooled(state, scoped) ?? { label: scoped }), label: scopedDisplayName(scoped) }
|
|
42
|
+
: { label: "Fable" };
|
|
43
|
+
|
|
44
|
+
const claude: Cell[] = [
|
|
45
|
+
{ ...(pooled(state, "5h") ?? { label: "5h" }), label: "5h" },
|
|
46
|
+
{ ...(pooled(state, "7d") ?? { label: "7d" }), label: "weekly" },
|
|
47
|
+
scopedCell,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// Codex reports only the two windows; it has no scoped equivalent.
|
|
51
|
+
const codex: Cell[] = [
|
|
52
|
+
codexCell(state, "5h", (label) => label === "5h") ?? { label: "5h" },
|
|
53
|
+
codexCell(state, "weekly", (label) => label === "weekly") ?? { label: "weekly" },
|
|
54
|
+
];
|
|
55
|
+
return { claude, codex };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `Work 61% · Personal 88%`. Empty when there is nothing extra to say. */
|
|
59
|
+
function renderAccountSummary(state: UsageState, cellWidth: number): string | undefined {
|
|
60
|
+
const stamps = state.lastUsedAt ?? {};
|
|
61
|
+
const groups = [...new Set(state.rows.filter(isClaudeAccount).map((row) => row.group))];
|
|
62
|
+
if (groups.length < 2) return undefined;
|
|
63
|
+
|
|
64
|
+
const ordered = groups.sort((a, b) => (stamps[b] ?? 0) - (stamps[a] ?? 0) || a.localeCompare(b));
|
|
65
|
+
const shown = ordered.slice(0, 2);
|
|
66
|
+
|
|
67
|
+
const parts = shown.map((group) => {
|
|
68
|
+
const row = state.rows.find((candidate) => candidate.group === group && candidate.label === "5h");
|
|
69
|
+
const name = group.replace(/^Claude /, "");
|
|
70
|
+
if (!row) return `${name} -`;
|
|
71
|
+
return `${name} ${Math.round(row.remaining)}%${row.stale ? "*" : ""}`;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const hidden = ordered.length - shown.length;
|
|
75
|
+
const text = parts.join(" · ") + (hidden > 0 ? ` +${hidden}` : "");
|
|
76
|
+
return text.length > cellWidth * 2 ? text.slice(0, cellWidth * 2) : text;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function renderCell(theme: any, cell: Cell, labelWidth: number, cellWidth: number): string {
|
|
80
|
+
const label = cell.label.slice(0, labelWidth).padEnd(labelWidth);
|
|
81
|
+
const reset = formatShortReset(cell.resetAt);
|
|
82
|
+
const resetWidth = 4;
|
|
83
|
+
const barWidth = Math.max(4, cellWidth - labelWidth - 6 - resetWidth - 3);
|
|
84
|
+
|
|
85
|
+
if (cell.remaining === undefined) {
|
|
86
|
+
return `${theme.fg("muted", label)} ${theme.fg("dim", "·".repeat(barWidth))} ${theme.fg("dim", " n/a")}${" ".repeat(resetWidth + 1)}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const filled = Math.round((cell.remaining / 100) * barWidth);
|
|
90
|
+
const percentText = `${cell.partial ? "~" : ""}${Math.round(cell.remaining)}%`.padStart(5);
|
|
91
|
+
let bar: string;
|
|
92
|
+
let percent: string;
|
|
93
|
+
|
|
94
|
+
if (hasTruecolor()) {
|
|
95
|
+
// One smooth hue per bar: green when full, amber mid-way, red as it empties.
|
|
96
|
+
const paint = levelColor(cell.remaining);
|
|
97
|
+
bar = paint("█".repeat(filled)) + theme.fg("dim", "░".repeat(barWidth - filled));
|
|
98
|
+
percent = paint(percentText);
|
|
99
|
+
} else {
|
|
100
|
+
const color = themeLevel(cell.remaining);
|
|
101
|
+
bar = theme.fg(color, "█".repeat(filled)) + theme.fg("dim", "░".repeat(barWidth - filled));
|
|
102
|
+
percent = theme.fg(color, percentText);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return `${theme.fg("muted", label)} ${bar} ${percent} ${theme.fg("dim", reset.padEnd(resetWidth))}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function renderUsageLines(state: UsageState, theme: any, width: number, modelId?: string): string[] {
|
|
109
|
+
if (state.loading) return [theme.fg("dim", " usage: loading…")];
|
|
110
|
+
if (state.rows.length === 0) {
|
|
111
|
+
if (state.errors.length > 0) return state.errors.map((error) => theme.fg("warning", ` ${error}`));
|
|
112
|
+
return [theme.fg("dim", " usage: unavailable")];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const gap = 3;
|
|
116
|
+
const cellWidth = Math.max(22, Math.floor((width - 2 - gap) / 2));
|
|
117
|
+
const labelWidth = 6;
|
|
118
|
+
const { claude, codex } = buildColumns(state, modelId);
|
|
119
|
+
const availability = poolAvailability(state.rows, state.accounts, modelId);
|
|
120
|
+
const status = availability.ready
|
|
121
|
+
? `${availability.ready}/${availability.total} ready`
|
|
122
|
+
: availability.unknown ? "unknown/stale" : "exhausted";
|
|
123
|
+
const partial = claude.some((cell) => cell.partial);
|
|
124
|
+
const claudeTitle = `Claude Σ${state.accounts} · ${status}${partial ? " · partial" : ""}`;
|
|
125
|
+
const codexTitle = state.codexPlan ? `Codex · ${state.codexPlan}` : "Codex";
|
|
126
|
+
const lines = [` ${theme.fg("accent", claudeTitle.padEnd(cellWidth))}${" ".repeat(gap)}${theme.fg("accent", codexTitle)}`];
|
|
127
|
+
|
|
128
|
+
for (let index = 0; index < Math.max(claude.length, codex.length); index += 1) {
|
|
129
|
+
const left = claude[index] ? renderCell(theme, claude[index], labelWidth, cellWidth) : " ".repeat(cellWidth);
|
|
130
|
+
const right = codex[index] ? renderCell(theme, codex[index], labelWidth, cellWidth) : "";
|
|
131
|
+
lines.push(` ${left}${" ".repeat(gap)}${right}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Per-account detail. With more than two accounts only the two most recently
|
|
135
|
+
// used are shown, so the footer stays two lines regardless of pool size.
|
|
136
|
+
const accountLine = renderAccountSummary(state, cellWidth);
|
|
137
|
+
if (accountLine) lines.push(` ${theme.fg("dim", accountLine)}`);
|
|
138
|
+
|
|
139
|
+
for (const error of state.errors) lines.push(theme.fg("warning", ` ${error}`));
|
|
140
|
+
return lines.map((line) => truncateToWidth(line, width, ""));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function usageSummaryText(state: UsageState): string {
|
|
144
|
+
const combined = ["5h", "7d", ...scopedLabels(state.rows)].map((label) => {
|
|
145
|
+
const pool = combinedWindow(state.rows, label, state.accounts, Date.now(), true);
|
|
146
|
+
return `Claude combined ${label}: ${pool
|
|
147
|
+
? `${pool.partial ? "~" : ""}${Math.round(pool.remaining)}% left${pool.partial ? " (partial: reporting accounts only)" : ""} ${formatReset(pool.resetAt)}`
|
|
148
|
+
: "unknown/stale"}`;
|
|
149
|
+
});
|
|
150
|
+
const summary = [...combined, ...state.rows.map(
|
|
151
|
+
(row: UsageRow) => `${row.group} ${row.label}: ${Math.round(row.remaining)}% left ${formatReset(row.resetAt)}${row.stale ? " (stale)" : ""}`.trim(),
|
|
152
|
+
)];
|
|
153
|
+
return [...summary, ...state.errors].join("\n") || "No usage data";
|
|
154
|
+
}
|
package/src/vendor/anthropic.ts
CHANGED
|
@@ -1,109 +1,109 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The single import point for `@cortexkit/anthropic-auth-core`.
|
|
7
|
-
*
|
|
8
|
-
* Nothing else in this repo may reach into the pi-managed npm tree. The old
|
|
9
|
-
* code hardcoded `../npm/node_modules/@cortexkit/...` relative to the
|
|
10
|
-
* extension file, which broke whenever an extension moved directory depth.
|
|
11
|
-
* Resolving from the agent dir at runtime makes placement irrelevant.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
function agentDir(): string {
|
|
15
|
-
return process.env.PI_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function coreEntry(): string {
|
|
19
|
-
return pathToFileURL(
|
|
20
|
-
join(agentDir(), "npm", "node_modules", "@cortexkit", "anthropic-auth-core", "dist", "index.js"),
|
|
21
|
-
).href;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface AnthropicAccount {
|
|
25
|
-
id: string;
|
|
26
|
-
label?: string;
|
|
27
|
-
type: string;
|
|
28
|
-
enabled?: boolean;
|
|
29
|
-
access?: string;
|
|
30
|
-
refresh?: string;
|
|
31
|
-
expires?: number;
|
|
32
|
-
addedAt?: number;
|
|
33
|
-
lastRefreshedAt?: number;
|
|
34
|
-
authLineageId?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface AccountStorage {
|
|
38
|
-
accounts: AnthropicAccount[];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface AnthropicCore {
|
|
42
|
-
addAccountPersistent(account: AnthropicAccount, path: string): Promise<void>;
|
|
43
|
-
loadAccounts(path: string): Promise<AccountStorage | undefined>;
|
|
44
|
-
authorize(mode: string): Promise<{ url: string; verifier: string; redirectUri: string; state: string }>;
|
|
45
|
-
exchange(
|
|
46
|
-
callback: string,
|
|
47
|
-
verifier: string,
|
|
48
|
-
redirectUri: string,
|
|
49
|
-
state: string,
|
|
50
|
-
): Promise<{ type: string; access: string; refresh: string; expires: number }>;
|
|
51
|
-
refreshClaudeOAuthToken(options: {
|
|
52
|
-
refreshToken: string;
|
|
53
|
-
maxRetries?: number;
|
|
54
|
-
fetchImpl?: typeof fetch;
|
|
55
|
-
}): Promise<{ access: string; refresh: string; expires: number }>;
|
|
56
|
-
getRoutingMode(storage: unknown): string;
|
|
57
|
-
setRoutingMode(mode: string, path: string): Promise<unknown>;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
let cached: Promise<AnthropicCore> | undefined;
|
|
61
|
-
|
|
62
|
-
/** Lazily loads and memoizes the vendor core module. */
|
|
63
|
-
export function core(): Promise<AnthropicCore> {
|
|
64
|
-
cached ??= import(coreEntry()).then((module) => module as unknown as AnthropicCore).catch((error) => {
|
|
65
|
-
cached = undefined;
|
|
66
|
-
throw new Error(
|
|
67
|
-
`@cortexkit/anthropic-auth-core is unavailable (${error instanceof Error ? error.message : String(error)}). `
|
|
68
|
-
+ "Reinstall with: pi package add @cortexkit/pi-anthropic-auth",
|
|
69
|
-
);
|
|
70
|
-
});
|
|
71
|
-
return cached;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Where the multi-account OAuth store lives. */
|
|
75
|
-
export function accountStoragePath(): string {
|
|
76
|
-
return process.env.PI_ANTHROPIC_AUTH_FILE ?? join(agentDir(), "anthropic-auth.json");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export async function loadAccounts(): Promise<AccountStorage | undefined> {
|
|
80
|
-
return (await core()).loadAccounts(accountStoragePath());
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export async function saveAccount(account: AnthropicAccount): Promise<void> {
|
|
84
|
-
return (await core()).addAccountPersistent(account, accountStoragePath());
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export async function refreshToken(
|
|
88
|
-
refreshTokenValue: string,
|
|
89
|
-
options: { maxRetries?: number; timeoutMs?: number } = {},
|
|
90
|
-
): Promise<{ access: string; refresh: string; expires: number }> {
|
|
91
|
-
const { refreshClaudeOAuthToken } = await core();
|
|
92
|
-
return refreshClaudeOAuthToken({
|
|
93
|
-
refreshToken: refreshTokenValue,
|
|
94
|
-
maxRetries: options.maxRetries,
|
|
95
|
-
fetchImpl: options.timeoutMs
|
|
96
|
-
? (input, init) => fetch(input, { ...init, signal: AbortSignal.timeout(options.timeoutMs!) })
|
|
97
|
-
: undefined,
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export async function routingMode(): Promise<string> {
|
|
102
|
-
const { getRoutingMode } = await core();
|
|
103
|
-
return getRoutingMode(await loadAccounts());
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export async function setRoutingMode(mode: "sticky-balanced" | "main-first"): Promise<string> {
|
|
107
|
-
const { setRoutingMode: apply, getRoutingMode } = await core();
|
|
108
|
-
return getRoutingMode(await apply(mode, accountStoragePath()));
|
|
109
|
-
}
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The single import point for `@cortexkit/anthropic-auth-core`.
|
|
7
|
+
*
|
|
8
|
+
* Nothing else in this repo may reach into the pi-managed npm tree. The old
|
|
9
|
+
* code hardcoded `../npm/node_modules/@cortexkit/...` relative to the
|
|
10
|
+
* extension file, which broke whenever an extension moved directory depth.
|
|
11
|
+
* Resolving from the agent dir at runtime makes placement irrelevant.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function agentDir(): string {
|
|
15
|
+
return process.env.PI_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function coreEntry(): string {
|
|
19
|
+
return pathToFileURL(
|
|
20
|
+
join(agentDir(), "npm", "node_modules", "@cortexkit", "anthropic-auth-core", "dist", "index.js"),
|
|
21
|
+
).href;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AnthropicAccount {
|
|
25
|
+
id: string;
|
|
26
|
+
label?: string;
|
|
27
|
+
type: string;
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
access?: string;
|
|
30
|
+
refresh?: string;
|
|
31
|
+
expires?: number;
|
|
32
|
+
addedAt?: number;
|
|
33
|
+
lastRefreshedAt?: number;
|
|
34
|
+
authLineageId?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AccountStorage {
|
|
38
|
+
accounts: AnthropicAccount[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface AnthropicCore {
|
|
42
|
+
addAccountPersistent(account: AnthropicAccount, path: string): Promise<void>;
|
|
43
|
+
loadAccounts(path: string): Promise<AccountStorage | undefined>;
|
|
44
|
+
authorize(mode: string): Promise<{ url: string; verifier: string; redirectUri: string; state: string }>;
|
|
45
|
+
exchange(
|
|
46
|
+
callback: string,
|
|
47
|
+
verifier: string,
|
|
48
|
+
redirectUri: string,
|
|
49
|
+
state: string,
|
|
50
|
+
): Promise<{ type: string; access: string; refresh: string; expires: number }>;
|
|
51
|
+
refreshClaudeOAuthToken(options: {
|
|
52
|
+
refreshToken: string;
|
|
53
|
+
maxRetries?: number;
|
|
54
|
+
fetchImpl?: typeof fetch;
|
|
55
|
+
}): Promise<{ access: string; refresh: string; expires: number }>;
|
|
56
|
+
getRoutingMode(storage: unknown): string;
|
|
57
|
+
setRoutingMode(mode: string, path: string): Promise<unknown>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let cached: Promise<AnthropicCore> | undefined;
|
|
61
|
+
|
|
62
|
+
/** Lazily loads and memoizes the vendor core module. */
|
|
63
|
+
export function core(): Promise<AnthropicCore> {
|
|
64
|
+
cached ??= import(coreEntry()).then((module) => module as unknown as AnthropicCore).catch((error) => {
|
|
65
|
+
cached = undefined;
|
|
66
|
+
throw new Error(
|
|
67
|
+
`@cortexkit/anthropic-auth-core is unavailable (${error instanceof Error ? error.message : String(error)}). `
|
|
68
|
+
+ "Reinstall with: pi package add @cortexkit/pi-anthropic-auth",
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
return cached;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Where the multi-account OAuth store lives. */
|
|
75
|
+
export function accountStoragePath(): string {
|
|
76
|
+
return process.env.PI_ANTHROPIC_AUTH_FILE ?? join(agentDir(), "anthropic-auth.json");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function loadAccounts(): Promise<AccountStorage | undefined> {
|
|
80
|
+
return (await core()).loadAccounts(accountStoragePath());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function saveAccount(account: AnthropicAccount): Promise<void> {
|
|
84
|
+
return (await core()).addAccountPersistent(account, accountStoragePath());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function refreshToken(
|
|
88
|
+
refreshTokenValue: string,
|
|
89
|
+
options: { maxRetries?: number; timeoutMs?: number } = {},
|
|
90
|
+
): Promise<{ access: string; refresh: string; expires: number }> {
|
|
91
|
+
const { refreshClaudeOAuthToken } = await core();
|
|
92
|
+
return refreshClaudeOAuthToken({
|
|
93
|
+
refreshToken: refreshTokenValue,
|
|
94
|
+
maxRetries: options.maxRetries,
|
|
95
|
+
fetchImpl: options.timeoutMs
|
|
96
|
+
? (input, init) => fetch(input, { ...init, signal: AbortSignal.timeout(options.timeoutMs!) })
|
|
97
|
+
: undefined,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function routingMode(): Promise<string> {
|
|
102
|
+
const { getRoutingMode } = await core();
|
|
103
|
+
return getRoutingMode(await loadAccounts());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function setRoutingMode(mode: "sticky-balanced" | "main-first"): Promise<string> {
|
|
107
|
+
const { setRoutingMode: apply, getRoutingMode } = await core();
|
|
108
|
+
return getRoutingMode(await apply(mode, accountStoragePath()));
|
|
109
|
+
}
|