@howaboua/opencode-usage-plugin 0.1.4-dev.0 → 0.1.4-dev.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.
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Formats GitHub Copilot usage snapshots.
3
+ * Handles chat and completion quotas with progress bars and reset times.
4
+ */
5
+ import type { UsageSnapshot } from "../../types";
6
+ export declare function formatCopilotSnapshot(snapshot: UsageSnapshot): string[];
7
+ //# sourceMappingURL=copilot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copilot.d.ts","sourceRoot":"","sources":["../../../src/ui/formatters/copilot.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAGhD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,EAAE,CAkBvE"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Formats GitHub Copilot usage snapshots.
3
+ * Handles chat and completion quotas with progress bars and reset times.
4
+ */
5
+ import { formatBar, formatResetSuffixISO, formatMissingSnapshot } from "./shared";
6
+ export function formatCopilotSnapshot(snapshot) {
7
+ const copilot = snapshot.copilotQuota;
8
+ if (!copilot)
9
+ return formatMissingSnapshot(snapshot);
10
+ const lines = ["→ [GITHUB] Copilot"];
11
+ const reset = copilot.resetTime ? formatResetSuffixISO(copilot.resetTime) : "";
12
+ const total = copilot.total === -1 ? "∞" : copilot.total.toString();
13
+ lines.push(` ${"Chat:".padEnd(13)} ${formatBar(copilot.percentRemaining)} ${copilot.used}/${total}${reset}`);
14
+ if (copilot.completionsUsed !== undefined && copilot.completionsTotal !== undefined) {
15
+ const pct = copilot.completionsTotal > 0
16
+ ? Math.round((copilot.completionsUsed / copilot.completionsTotal) * 100)
17
+ : 0;
18
+ lines.push(` ${"Completions:".padEnd(13)} ${formatBar(pct)} ${copilot.completionsUsed}/${copilot.completionsTotal}`);
19
+ }
20
+ return lines;
21
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Formats Mirrowel Proxy usage snapshots.
3
+ * Handles multi-provider and multi-tier quota reporting with progress bars.
4
+ */
5
+ import type { UsageSnapshot } from "../../types";
6
+ export declare function formatProxySnapshot(snapshot: UsageSnapshot): string[];
7
+ //# sourceMappingURL=proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../../src/ui/formatters/proxy.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAGhD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,EAAE,CAcrE"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Formats Mirrowel Proxy usage snapshots.
3
+ * Handles multi-provider and multi-tier quota reporting with progress bars.
4
+ */
5
+ import { formatBar, formatResetSuffixISO, formatMissingSnapshot } from "./shared";
6
+ export function formatProxySnapshot(snapshot) {
7
+ const proxy = snapshot.proxyQuota;
8
+ if (!proxy?.providers?.length)
9
+ return formatMissingSnapshot(snapshot);
10
+ const lines = ["→ [Google] Mirrowel Proxy"];
11
+ for (const provider of proxy.providers) {
12
+ const providerLines = formatProxyProvider(provider);
13
+ if (providerLines.length) {
14
+ lines.push("", ` ${provider.name}:`, ...providerLines);
15
+ }
16
+ }
17
+ return lines;
18
+ }
19
+ function formatProxyProvider(provider) {
20
+ const lines = [];
21
+ for (const tier of provider.tiers) {
22
+ if (!tier.quotaGroups?.length)
23
+ continue;
24
+ lines.push(` ${tier.tier === "paid" ? "Paid" : "Free"}:`);
25
+ for (const group of tier.quotaGroups) {
26
+ const reset = group.resetTime ? formatResetSuffixISO(group.resetTime) : "";
27
+ lines.push(` ${group.name.padEnd(9)} ${formatBar(group.remainingPct)} ${group.remaining}/${group.max}${reset}`);
28
+ }
29
+ }
30
+ return lines;
31
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Formats various usage snapshots (Proxy, Copilot, Codex) into human-readable text.
3
+ * Provides specialized formatting for progress bars, reset times, and missing data states.
4
+ */
5
+ import type { UsageSnapshot } from "../../types";
6
+ export declare function formatBar(pct: number): string;
7
+ export declare function formatResetSuffix(resetAt: number | null): string;
8
+ export declare function formatResetSuffixISO(iso: string): string;
9
+ export declare function formatMissingSnapshot(snapshot: UsageSnapshot): string[];
10
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../src/ui/formatters/shared.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEhD,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAK7C;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAGhE;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKxD;AAWD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,EAAE,CAgBvE"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Formats various usage snapshots (Proxy, Copilot, Codex) into human-readable text.
3
+ * Provides specialized formatting for progress bars, reset times, and missing data states.
4
+ */
5
+ import { platform, homedir } from "os";
6
+ import { join } from "path";
7
+ export function formatBar(pct) {
8
+ const clamped = Math.max(0, Math.min(100, pct));
9
+ const size = 15;
10
+ const filled = Math.round((clamped / 100) * size);
11
+ return `${"█".repeat(filled)}${"░".repeat(size - filled)}`;
12
+ }
13
+ export function formatResetSuffix(resetAt) {
14
+ if (!resetAt)
15
+ return "";
16
+ return ` (resets in ${formatTimeDelta(resetAt)})`;
17
+ }
18
+ export function formatResetSuffixISO(iso) {
19
+ try {
20
+ const at = Math.floor(new Date(iso).getTime() / 1000);
21
+ return ` (resets in ${formatTimeDelta(at)})`;
22
+ }
23
+ catch {
24
+ return "";
25
+ }
26
+ }
27
+ function formatTimeDelta(at) {
28
+ const diff = at - Math.floor(Date.now() / 1000);
29
+ if (diff <= 0)
30
+ return "now";
31
+ if (diff < 60)
32
+ return `${diff}s`;
33
+ if (diff < 3600)
34
+ return `${Math.ceil(diff / 60)}m`;
35
+ if (diff < 84400)
36
+ return `${Math.round(diff / 3600)}h`;
37
+ return `${Math.round(diff / 86400)}d`;
38
+ }
39
+ export function formatMissingSnapshot(snapshot) {
40
+ const { provider } = snapshot;
41
+ const configPath = getConfigPath();
42
+ const instructions = {
43
+ codex: "if you dont have codex oauth, please set your usage-config.jsonc to openai: false",
44
+ proxy: "if you are not running Mirrowel's proxy, please set your usage-config.jsonc to proxy: false",
45
+ copilot: "if you are not running GitHub Copilot, please set your usage-config.jsonc to copilot: false"
46
+ };
47
+ const lines = [`→ [${provider.toUpperCase()}] - ${instructions[provider] || ""}`];
48
+ if (snapshot.missingReason)
49
+ lines.push("", `Reason: ${snapshot.missingReason}`);
50
+ if (snapshot.missingDetails?.length) {
51
+ lines.push("", "Details:", ...snapshot.missingDetails.map((d) => `- ${d}`));
52
+ }
53
+ return [...lines, "", `File: ${configPath}`, "", "Issue? https://github.com/IgorWarzocha/opencode-usage-plugin/issues"];
54
+ }
55
+ function getConfigPath() {
56
+ const home = homedir();
57
+ const plat = platform();
58
+ if (plat === "darwin")
59
+ return join(home, ".config", "opencode", "usage-config.jsonc");
60
+ if (plat === "win32")
61
+ return join(process.env.APPDATA || join(home, "AppData", "Roaming"), "opencode", "usage-config.jsonc");
62
+ return join(process.env.XDG_CONFIG_HOME || join(home, ".config"), "opencode", "usage-config.jsonc");
63
+ }
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Renders usage snapshots into readable status text.
3
+ * Dispatches to specialized formatters and manages session messaging.
3
4
  */
4
5
  import type { PluginInput } from "@opencode-ai/plugin";
5
6
  import type { UsageSnapshot } from "../types";
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/ui/status.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAGtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAE1C,KAAK,WAAW,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAA;AAExC,wBAAsB,iBAAiB,CAAC,OAAO,EAAE;IAC/C,MAAM,EAAE,WAAW,CAAA;IACnB,KAAK,EAAE,UAAU,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;CACb,GAAG,OAAO,CAAC,IAAI,CAAC,CAqChB;AAkMD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE;IAC/C,MAAM,EAAE,WAAW,CAAA;IACnB,KAAK,EAAE,UAAU,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,aAAa,EAAE,CAAA;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6BhB"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/ui/status.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAK1C,KAAK,WAAW,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAA;AAExC,wBAAsB,iBAAiB,CAAC,OAAO,EAAE;IAC/C,MAAM,EAAE,WAAW,CAAA;IACnB,KAAK,EAAE,UAAU,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;CACb,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBhB;AAiCD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE;IAC/C,MAAM,EAAE,WAAW,CAAA;IACnB,KAAK,EAAE,UAAU,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,aAAa,EAAE,CAAA;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,GAAG,OAAO,CAAC,IAAI,CAAC,CAahB"}
package/dist/ui/status.js CHANGED
@@ -1,231 +1,68 @@
1
1
  /**
2
2
  * Renders usage snapshots into readable status text.
3
+ * Dispatches to specialized formatters and manages session messaging.
3
4
  */
4
- import { platform, homedir } from "os";
5
- import { join } from "path";
5
+ import { formatProxySnapshot } from "./formatters/proxy";
6
+ import { formatCopilotSnapshot } from "./formatters/copilot";
7
+ import { formatBar, formatResetSuffix, formatMissingSnapshot } from "./formatters/shared";
6
8
  export async function sendStatusMessage(options) {
7
- // @ts-ignore
8
9
  const bus = options.client.bus;
9
10
  if (bus) {
10
11
  try {
11
12
  await bus.publish({
12
13
  topic: "companion.projection",
13
- body: {
14
- key: "usage",
15
- kind: "markdown",
16
- content: options.text,
17
- },
14
+ body: { key: "usage", kind: "markdown", content: options.text },
18
15
  });
19
16
  }
20
17
  catch { }
21
18
  }
22
- await options.client.session
23
- .prompt({
19
+ await options.client.session.prompt({
24
20
  path: { id: options.sessionID },
25
- body: {
26
- noReply: true,
27
- parts: [
28
- {
29
- type: "text",
30
- text: options.text,
31
- ignored: true,
32
- },
33
- ],
34
- },
35
- })
36
- .catch(async () => {
37
- await options.client.tui
38
- .showToast({
21
+ body: { noReply: true, parts: [{ type: "text", text: options.text, ignored: true }] },
22
+ }).catch(async () => {
23
+ await options.client.tui.showToast({
39
24
  body: { title: "Usage Status", message: options.text, variant: "info" },
40
- })
41
- .catch(() => { });
25
+ }).catch(() => { });
42
26
  });
43
27
  }
44
- function formatBar(remainingPercent) {
45
- const clamped = Math.max(0, Math.min(100, remainingPercent));
46
- const size = 15;
47
- const filled = Math.round((clamped / 100) * size);
48
- const empty = size - filled;
49
- return `${"█".repeat(filled)}${"░".repeat(empty)}`;
50
- }
51
- function formatPlanType(planType) {
52
- return planType
53
- .replace(/_/g, " ")
54
- .split(" ")
55
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
56
- .join(" ");
57
- }
58
- function formatResetTime(resetAt) {
59
- const now = Math.floor(Date.now() / 1000);
60
- const diff = resetAt - now;
61
- if (diff <= 0)
62
- return "now";
63
- if (diff < 60)
64
- return `${diff}s`;
65
- if (diff < 3600)
66
- return `${Math.ceil(diff / 60)}m`;
67
- if (diff < 86400)
68
- return `${Math.round(diff / 3600)}h`;
69
- return `${Math.round(diff / 86400)}d`;
70
- }
71
- function formatResetSuffix(resetAt) {
72
- if (!resetAt)
73
- return "";
74
- return ` (resets in ${formatResetTime(resetAt)})`;
75
- }
76
- function formatResetSuffixISO(isoString) {
77
- try {
78
- const resetAt = Math.floor(new Date(isoString).getTime() / 1000);
79
- return ` (resets in ${formatResetTime(resetAt)})`;
80
- }
81
- catch {
82
- return "";
83
- }
84
- }
85
- function formatProxySnapshot(snapshot) {
86
- const proxy = snapshot.proxyQuota;
87
- if (!proxy || !proxy.providers || proxy.providers.length === 0)
88
- return ["→ [proxy] No data"];
89
- const lines = ["→ [Google] Mirrowel Proxy"];
90
- for (const provider of proxy.providers) {
91
- const providerLines = [];
92
- for (const tierInfo of provider.tiers) {
93
- if (!tierInfo.quotaGroups || tierInfo.quotaGroups.length === 0)
94
- continue;
95
- const tierLabel = tierInfo.tier === "paid" ? "Paid" : "Free";
96
- providerLines.push(` ${tierLabel}:`);
97
- for (const group of tierInfo.quotaGroups) {
98
- const resetSuffix = group.resetTime ? formatResetSuffixISO(group.resetTime) : "";
99
- const label = `${group.name}:`.padEnd(9);
100
- providerLines.push(` ${label} ${formatBar(group.remainingPct)} ${group.remaining}/${group.max}${resetSuffix}`);
101
- }
102
- }
103
- if (providerLines.length > 0) {
104
- lines.push("");
105
- lines.push(` ${provider.name}:`);
106
- for (const line of providerLines)
107
- lines.push(line);
108
- }
109
- }
110
- return lines;
111
- }
112
- function formatCopilotSnapshot(snapshot) {
113
- const copilot = snapshot.copilotQuota;
114
- if (!copilot)
115
- return ["→ [copilot] No data"];
116
- const lines = ["→ [GITHUB] Copilot"];
117
- const resetSuffix = copilot.resetTime ? formatResetSuffixISO(copilot.resetTime) : "";
118
- const totalLabel = copilot.total === -1 ? "∞" : copilot.total.toString();
119
- const chatLabel = "Chat:".padEnd(13);
120
- const chatRemaining = copilot.used;
121
- const chatPct = copilot.percentRemaining;
122
- lines.push(` ${chatLabel} ${formatBar(chatPct)} ${chatRemaining}/${totalLabel}${resetSuffix}`);
123
- if (copilot.completionsUsed !== undefined && copilot.completionsTotal !== undefined) {
124
- const compLabel = "Completions:".padEnd(13);
125
- const compPct = copilot.completionsTotal > 0
126
- ? Math.round((copilot.completionsUsed / copilot.completionsTotal) * 100)
127
- : 0;
128
- lines.push(` ${compLabel} ${formatBar(compPct)} ${copilot.completionsUsed}/${copilot.completionsTotal}`);
129
- }
130
- return lines;
131
- }
132
- function getAppDataPath() {
133
- const home = homedir();
134
- const plat = platform();
135
- if (plat === "darwin")
136
- return join(home, ".config", "opencode");
137
- if (plat === "win32")
138
- return join(process.env.APPDATA || join(home, "AppData", "Roaming"), "opencode");
139
- return join(process.env.XDG_CONFIG_HOME || join(home, ".config"), "opencode");
140
- }
141
- function formatMissingSnapshot(snapshot) {
142
- const provider = snapshot.provider;
143
- const configPath = join(getAppDataPath(), "usage-config.jsonc");
144
- let providerInstruction = "";
145
- if (provider === "codex") {
146
- providerInstruction = "if you dont have codex oauth, please set your usage-config.jsonc to openai: false";
147
- }
148
- else if (provider === "proxy") {
149
- providerInstruction = "if you are not running Mirrowel's proxy, please set your usage-config.jsonc to proxy: false";
150
- }
151
- else if (provider === "copilot") {
152
- providerInstruction = "if you are not running GitHub Copilot, please set your usage-config.jsonc to copilot: false";
153
- }
154
- const lines = [
155
- `→ [${provider.toUpperCase()}] - ${providerInstruction}`,
156
- ];
157
- if (snapshot.missingReason) {
158
- lines.push("", `Reason: ${snapshot.missingReason}`);
159
- }
160
- if (snapshot.missingDetails && snapshot.missingDetails.length > 0) {
161
- lines.push("", "Details:");
162
- for (const detail of snapshot.missingDetails) {
163
- lines.push(`- ${detail}`);
164
- }
165
- }
166
- lines.push("", `The file can be found in ${configPath}. Please read the comments in the file or visit the repo for the readme.`, "", "If you are seeing empty usages or errors, despite having everything set up correctly, please fire an issue at https://github.com/IgorWarzocha/opencode-usage-plugin/issues - thank you!");
167
- return lines;
168
- }
169
28
  function formatSnapshot(snapshot) {
170
- if (snapshot.isMissing) {
29
+ if (snapshot.isMissing)
171
30
  return formatMissingSnapshot(snapshot);
172
- }
173
- if (snapshot.provider === "proxy" && snapshot.proxyQuota) {
31
+ if (snapshot.provider === "proxy")
174
32
  return formatProxySnapshot(snapshot);
175
- }
176
- if (snapshot.provider === "copilot" && snapshot.copilotQuota) {
33
+ if (snapshot.provider === "copilot")
177
34
  return formatCopilotSnapshot(snapshot);
178
- }
179
- const plan = snapshot.planType ? ` (${formatPlanType(snapshot.planType)})` : "";
35
+ const plan = snapshot.planType ? ` (${snapshot.planType.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase())})` : "";
180
36
  const lines = [`→ [${snapshot.provider.toUpperCase()}]${plan}`];
181
- const primary = snapshot.primary;
182
- if (primary) {
183
- const remainingPct = 100 - primary.usedPercent;
184
- const label = "Hourly:".padEnd(13);
185
- lines.push(` ${label} ${formatBar(remainingPct)} ${remainingPct.toFixed(0)}% left${formatResetSuffix(primary.resetsAt)}`);
186
- }
187
- const secondary = snapshot.secondary;
188
- if (secondary) {
189
- const remainingPct = 100 - secondary.usedPercent;
190
- const label = "Weekly:".padEnd(13);
191
- lines.push(` ${label} ${formatBar(remainingPct)} ${remainingPct.toFixed(0)}% left${formatResetSuffix(secondary.resetsAt)}`);
192
- }
193
- const codeReview = snapshot.codeReview;
194
- if (codeReview) {
195
- const remainingPct = 100 - codeReview.usedPercent;
196
- const label = "Code Review:".padEnd(13);
197
- lines.push(` ${label} ${formatBar(remainingPct)} ${remainingPct.toFixed(0)}% left${formatResetSuffix(codeReview.resetsAt)}`);
37
+ const metrics = [
38
+ { label: "Hourly:", data: snapshot.primary },
39
+ { label: "Weekly:", data: snapshot.secondary },
40
+ { label: "Code Review:", data: snapshot.codeReview }
41
+ ];
42
+ let hasData = false;
43
+ for (const m of metrics) {
44
+ if (m.data) {
45
+ const pct = 100 - m.data.usedPercent;
46
+ lines.push(` ${m.label.padEnd(13)} ${formatBar(pct)} ${pct.toFixed(0)}% left${formatResetSuffix(m.data.resetsAt)}`);
47
+ hasData = true;
48
+ }
198
49
  }
199
50
  if (snapshot.credits?.hasCredits) {
200
51
  lines.push(` Credits: ${snapshot.credits.balance}`);
52
+ hasData = true;
201
53
  }
202
- return lines;
54
+ return hasData ? lines : formatMissingSnapshot(snapshot);
203
55
  }
204
56
  export async function renderUsageStatus(options) {
205
57
  if (options.snapshots.length === 0) {
206
58
  const filterMsg = options.filter ? ` for "${options.filter}"` : "";
207
- await sendStatusMessage({
208
- client: options.client,
209
- state: options.state,
210
- sessionID: options.sessionID,
211
- text: `▣ Usage | No data received${filterMsg}.`,
212
- });
213
- return;
59
+ return sendStatusMessage({ ...options, text: `▣ Usage | No data received${filterMsg}.` });
214
60
  }
215
61
  const lines = ["▣ Usage Status", ""];
216
- options.snapshots.forEach((snapshot, index) => {
217
- const snapshotLines = formatSnapshot(snapshot);
218
- for (const line of snapshotLines)
219
- lines.push(line);
220
- if (index < options.snapshots.length - 1) {
221
- lines.push("");
222
- lines.push("---");
223
- }
224
- });
225
- await sendStatusMessage({
226
- client: options.client,
227
- state: options.state,
228
- sessionID: options.sessionID,
229
- text: lines.join("\n"),
62
+ options.snapshots.forEach((s, i) => {
63
+ lines.push(...formatSnapshot(s));
64
+ if (i < options.snapshots.length - 1)
65
+ lines.push("", "---");
230
66
  });
67
+ await sendStatusMessage({ ...options, text: lines.join("\n") });
231
68
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Utility for loading and merging authentication records from multiple system paths.
3
+ * Handles platform-specific path resolution and specific provider auth transformations.
4
+ */
5
+ import z from "zod";
6
+ declare const authEntrySchema: z.ZodObject<{
7
+ type: z.ZodOptional<z.ZodString>;
8
+ access: z.ZodOptional<z.ZodString>;
9
+ refresh: z.ZodOptional<z.ZodString>;
10
+ enterpriseUrl: z.ZodOptional<z.ZodString>;
11
+ accountId: z.ZodOptional<z.ZodString>;
12
+ key: z.ZodOptional<z.ZodString>;
13
+ }, z.core.$loose>;
14
+ export type AuthEntry = z.infer<typeof authEntrySchema>;
15
+ export type AuthRecord = Record<string, AuthEntry>;
16
+ export declare function loadMergedAuths(): Promise<{
17
+ auths: AuthRecord;
18
+ codexDiagnostics: string[];
19
+ }>;
20
+ export {};
21
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/usage/auth/loader.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,CAAC,MAAM,KAAK,CAAA;AAGnB,QAAA,MAAM,eAAe;;;;;;;iBAOL,CAAA;AAIhB,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAA;AACvD,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;AAElD,wBAAsB,eAAe,IAAI,OAAO,CAAC;IAC/C,KAAK,EAAE,UAAU,CAAA;IACjB,gBAAgB,EAAE,MAAM,EAAE,CAAA;CAC3B,CAAC,CAYD"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Utility for loading and merging authentication records from multiple system paths.
3
+ * Handles platform-specific path resolution and specific provider auth transformations.
4
+ */
5
+ import z from "zod";
6
+ import { getPossibleAuthPaths } from "../../utils";
7
+ const authEntrySchema = z.object({
8
+ type: z.string().optional(),
9
+ access: z.string().optional(),
10
+ refresh: z.string().optional(),
11
+ enterpriseUrl: z.string().optional(),
12
+ accountId: z.string().optional(),
13
+ key: z.string().optional(),
14
+ }).passthrough();
15
+ const authRecordSchema = z.record(z.string(), authEntrySchema);
16
+ export async function loadMergedAuths() {
17
+ const possiblePaths = getPossibleAuthPaths();
18
+ const mergedAuth = {};
19
+ const codexDiagnostics = [`Auth paths checked: ${possiblePaths.join(", ")}`];
20
+ const orderedPaths = [...possiblePaths].reverse();
21
+ for (const authPath of orderedPaths) {
22
+ const diagnostics = await processAuthPath(authPath, mergedAuth);
23
+ codexDiagnostics.push(...diagnostics);
24
+ }
25
+ return { auths: mergedAuth, codexDiagnostics };
26
+ }
27
+ async function processAuthPath(authPath, mergedAuth) {
28
+ const diagnostics = [];
29
+ try {
30
+ const file = Bun.file(authPath);
31
+ if (!(await file.exists()))
32
+ return [`Missing auth file: ${authPath}`];
33
+ const data = await file.json();
34
+ if (typeof data !== "object" || data === null)
35
+ return [`Auth file is not a JSON object: ${authPath}`];
36
+ if (authPath.includes(".codex")) {
37
+ const parsed = parseCodexAuth(data, authPath);
38
+ if (parsed.auth)
39
+ Object.assign(mergedAuth, parsed.auth);
40
+ return parsed.diagnostics;
41
+ }
42
+ const parsed = authRecordSchema.safeParse(data);
43
+ if (!parsed.success)
44
+ return [`Auth file failed schema validation: ${authPath}`];
45
+ Object.assign(mergedAuth, parsed.data);
46
+ diagnostics.push(`Loaded auth from ${authPath}`);
47
+ }
48
+ catch (e) {
49
+ diagnostics.push(`Failed to read auth file ${authPath}: ${e.message}`);
50
+ }
51
+ return diagnostics;
52
+ }
53
+ function parseCodexAuth(data, path) {
54
+ const tokens = data.tokens;
55
+ if (!tokens?.access_token)
56
+ return { auth: null, diagnostics: [`Invalid Codex auth in ${path}`] };
57
+ return {
58
+ auth: {
59
+ openai: {
60
+ type: "oauth",
61
+ access: tokens.access_token,
62
+ accountId: tokens.account_id,
63
+ refresh: tokens.refresh_token,
64
+ },
65
+ },
66
+ diagnostics: [`Codex CLI auth loaded from ${path}`],
67
+ };
68
+ }
@@ -1,15 +1,9 @@
1
1
  /**
2
- * Loads auth data and fetches live usage snapshots from providers.
3
- * Supports filtering by provider alias.
2
+ * Orchestrates the fetching of usage snapshots from multiple providers.
3
+ * Manages provider filtering, concurrency, and fallback for missing data.
4
4
  */
5
5
  import type { UsageSnapshot } from "../types";
6
- import type { AuthRecord } from "./registry";
7
- export declare const providerAliases: Record<string, string>;
8
- export declare function resolveProviderFilter(filter?: string): string | undefined;
9
6
  export declare function fetchUsageSnapshots(filter?: string): Promise<UsageSnapshot[]>;
10
- export declare function loadAuths(): Promise<AuthRecord>;
11
- export declare function loadAuthsWithDiagnostics(): Promise<{
12
- auths: AuthRecord;
13
- codexDiagnostics: string[];
14
- }>;
7
+ export declare function resolveProviderFilter(filter?: string): string | undefined;
8
+ export declare function loadAuths(): Promise<import("./auth/loader").AuthRecord>;
15
9
  //# sourceMappingURL=fetch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/usage/fetch.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAI7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAgB5C,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAWlD,CAAA;AAED,wBAAgB,qBAAqB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIzE;AAED,wBAAsB,mBAAmB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CA+EnF;AAED,wBAAsB,SAAS,IAAI,OAAO,CAAC,UAAU,CAAC,CAGrD;AAED,wBAAsB,wBAAwB,IAAI,OAAO,CAAC;IACxD,KAAK,EAAE,UAAU,CAAA;IACjB,gBAAgB,EAAE,MAAM,EAAE,CAAA;CAC3B,CAAC,CAsED"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/usage/fetch.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAQ7C,wBAAsB,mBAAmB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CA2CnF;AAWD,wBAAgB,qBAAqB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEzE;AA6BD,wBAAsB,SAAS,gDAG9B"}
@@ -1,215 +1,84 @@
1
1
  /**
2
- * Loads auth data and fetches live usage snapshots from providers.
3
- * Supports filtering by provider alias.
2
+ * Orchestrates the fetching of usage snapshots from multiple providers.
3
+ * Manages provider filtering, concurrency, and fallback for missing data.
4
4
  */
5
- import z from "zod";
6
5
  import { providers } from "../providers";
7
6
  import { loadUsageConfig } from "./config";
8
- import { getPossibleAuthPaths } from "../utils";
7
+ import { loadMergedAuths } from "./auth/loader";
9
8
  import { resolveProviderAuths } from "./registry";
10
- const authEntrySchema = z
11
- .object({
12
- type: z.string().optional(),
13
- access: z.string().optional(),
14
- refresh: z.string().optional(),
15
- enterpriseUrl: z.string().optional(),
16
- accountId: z.string().optional(),
17
- key: z.string().optional(),
18
- })
19
- .passthrough();
20
- const authRecordSchema = z.record(z.string(), authEntrySchema);
21
- export const providerAliases = {
22
- codex: "codex",
23
- openai: "codex",
24
- gpt: "codex",
25
- proxy: "proxy",
26
- agy: "proxy",
27
- antigravity: "proxy",
28
- gemini: "proxy",
29
- copilot: "copilot",
30
- gh: "copilot",
31
- github: "copilot",
32
- };
33
- export function resolveProviderFilter(filter) {
34
- if (!filter)
35
- return undefined;
36
- const normalized = filter.toLowerCase().trim();
37
- return providerAliases[normalized];
38
- }
9
+ const CORE_PROVIDERS = ["codex", "proxy", "copilot"];
39
10
  export async function fetchUsageSnapshots(filter) {
40
- const targetProvider = resolveProviderFilter(filter);
41
- const usageConfig = await loadUsageConfig().catch(() => null);
42
- const providerToggles = usageConfig?.providers ?? {};
43
- const isProviderEnabled = (id) => {
11
+ const target = resolveFilter(filter);
12
+ const config = await loadUsageConfig().catch(() => null);
13
+ const toggles = config?.providers ?? {};
14
+ const isEnabled = (id) => {
44
15
  if (id === "codex")
45
- return providerToggles.openai !== false;
46
- if (id === "proxy")
47
- return providerToggles.proxy !== false;
48
- if (id === "copilot")
49
- return providerToggles.copilot !== false;
50
- return true;
16
+ return toggles.openai !== false;
17
+ return toggles[id] !== false;
51
18
  };
52
- const { auths, codexDiagnostics } = await loadAuthsWithDiagnostics();
19
+ const { auths, codexDiagnostics } = await loadMergedAuths();
53
20
  const entries = resolveProviderAuths(auths, null);
54
- const snapshots = [];
55
- const coreProviders = ["codex", "proxy", "copilot"];
56
- const fetchedProviders = new Set();
21
+ const snapshotsMap = new Map();
22
+ const fetched = new Set();
57
23
  const fetches = entries
58
- .filter((entry) => !targetProvider || entry.providerID === targetProvider)
59
- .filter((entry) => isProviderEnabled(entry.providerID))
60
- .map(async (entry) => {
61
- const provider = providers[entry.providerID];
62
- if (!provider?.fetchUsage)
63
- return;
64
- try {
65
- const snapshot = await provider.fetchUsage(entry.auth);
66
- if (snapshot) {
67
- snapshots.push(snapshot);
68
- fetchedProviders.add(entry.providerID);
69
- }
70
- }
71
- catch {
24
+ .filter(e => (!target || e.providerID === target) && isEnabled(e.providerID))
25
+ .map(async (e) => {
26
+ const snap = await providers[e.providerID]?.fetchUsage?.(e.auth).catch(() => null);
27
+ if (snap) {
28
+ snapshotsMap.set(e.providerID, snap);
29
+ fetched.add(e.providerID);
72
30
  }
73
31
  });
74
- const specialProviders = ["proxy", "copilot"];
75
- for (const id of specialProviders) {
76
- if ((!targetProvider || targetProvider === id) && isProviderEnabled(id) && !fetchedProviders.has(id)) {
32
+ // Handle special/default fetches
33
+ for (const id of ["proxy", "copilot"]) {
34
+ if ((!target || target === id) && isEnabled(id) && !fetched.has(id)) {
77
35
  const provider = providers[id];
78
36
  if (provider?.fetchUsage) {
79
- fetches.push(provider
80
- .fetchUsage(undefined)
81
- .then((snapshot) => {
82
- if (snapshot) {
83
- snapshots.push(snapshot);
84
- fetchedProviders.add(id);
37
+ fetches.push(provider.fetchUsage(undefined).then(s => {
38
+ if (s) {
39
+ snapshotsMap.set(id, s);
40
+ fetched.add(id);
85
41
  }
86
- })
87
- .catch(() => { }));
42
+ }).catch(() => { }));
88
43
  }
89
44
  }
90
45
  }
91
- await Promise.race([Promise.all(fetches), timeout(5000)]);
92
- for (const id of coreProviders) {
93
- if (isProviderEnabled(id) && !fetchedProviders.has(id)) {
94
- if (!targetProvider || targetProvider === id) {
95
- const codexDetails = id === "codex" && codexDiagnostics.length > 0 ? codexDiagnostics : undefined;
96
- snapshots.push({
97
- timestamp: Date.now(),
98
- provider: id,
99
- planType: null,
100
- primary: null,
101
- secondary: null,
102
- codeReview: null,
103
- credits: null,
104
- updatedAt: Date.now(),
105
- isMissing: true,
106
- missingReason: codexDetails ? "Codex auth was not resolved from auth.json." : undefined,
107
- missingDetails: codexDetails,
108
- });
109
- }
110
- }
111
- }
112
- return snapshots;
46
+ await Promise.race([Promise.all(fetches), new Promise(r => setTimeout(r, 5000))]);
47
+ const snapshots = Array.from(snapshotsMap.values());
48
+ return appendMissingStates(snapshots, fetched, isEnabled, target, codexDiagnostics);
113
49
  }
114
- export async function loadAuths() {
115
- const { auths } = await loadAuthsWithDiagnostics();
116
- return auths;
50
+ function resolveFilter(f) {
51
+ const aliases = {
52
+ codex: "codex", openai: "codex", gpt: "codex",
53
+ proxy: "proxy", agy: "proxy", gemini: "proxy",
54
+ copilot: "copilot", github: "copilot"
55
+ };
56
+ return f ? aliases[f.toLowerCase().trim()] : undefined;
117
57
  }
118
- export async function loadAuthsWithDiagnostics() {
119
- const possiblePaths = getPossibleAuthPaths();
120
- const mergedAuth = {};
121
- const codexDiagnostics = [];
122
- const orderedPaths = [...possiblePaths].reverse();
123
- codexDiagnostics.push(`Auth paths checked: ${possiblePaths.join(", ")}`);
124
- for (const authPath of orderedPaths) {
125
- try {
126
- const file = Bun.file(authPath);
127
- const exists = await file.exists();
128
- if (!exists) {
129
- codexDiagnostics.push(`Missing auth file: ${authPath}`);
130
- continue;
131
- }
132
- const data = await file.json();
133
- if (!isRecord(data)) {
134
- codexDiagnostics.push(`Auth file is not a JSON object: ${authPath}`);
135
- continue;
136
- }
137
- if (authPath.includes(".codex")) {
138
- const parsed = parseCodexAuth(data, authPath);
139
- codexDiagnostics.push(...parsed.diagnostics);
140
- if (parsed.auth)
141
- Object.assign(mergedAuth, parsed.auth);
142
- continue;
143
- }
144
- const parsed = authRecordSchema.safeParse(data);
145
- if (!parsed.success) {
146
- codexDiagnostics.push(`Auth file failed schema validation: ${authPath}`);
147
- continue;
148
- }
149
- Object.assign(mergedAuth, parsed.data);
150
- const openaiEntry = parsed.data.openai ?? parsed.data.codex;
151
- if (!openaiEntry) {
152
- codexDiagnostics.push(`No "openai" or "codex" entry in auth file: ${authPath}`);
153
- continue;
154
- }
155
- codexDiagnostics.push(`Found OpenAI/Codex entry in auth file: ${authPath}`);
156
- if (openaiEntry.type && openaiEntry.type !== "oauth" && openaiEntry.type !== "token") {
157
- codexDiagnostics.push(`OpenAI/Codex entry has unsupported type "${openaiEntry.type}" in ${authPath}`);
158
- }
159
- if (!openaiEntry.access && !openaiEntry.key) {
160
- codexDiagnostics.push(`OpenAI/Codex entry missing "access" or "key" in ${authPath}`);
161
- }
162
- }
163
- catch (error) {
164
- const message = error instanceof Error ? error.message : String(error);
165
- codexDiagnostics.push(`Failed to read auth file ${authPath}: ${message}`);
166
- continue;
167
- }
168
- }
169
- const mergedEntry = mergedAuth.openai ?? mergedAuth.codex;
170
- if (!mergedEntry) {
171
- codexDiagnostics.push("Merged auth record has no OpenAI/Codex entry.");
172
- }
173
- else {
174
- if (mergedEntry.type && mergedEntry.type !== "oauth" && mergedEntry.type !== "token") {
175
- codexDiagnostics.push(`Merged OpenAI/Codex entry has unsupported type "${mergedEntry.type}".`);
176
- }
177
- if (!mergedEntry.access && !mergedEntry.key) {
178
- codexDiagnostics.push("Merged OpenAI/Codex entry missing access or key.");
179
- }
180
- }
181
- return { auths: mergedAuth, codexDiagnostics };
58
+ export function resolveProviderFilter(filter) {
59
+ return resolveFilter(filter);
182
60
  }
183
- function parseCodexAuth(data, authPath) {
184
- const diagnostics = [];
185
- const tokens = data.tokens;
186
- if (!isRecord(tokens)) {
187
- diagnostics.push(`Codex auth missing "tokens" object in ${authPath}`);
188
- return { auth: null, diagnostics };
189
- }
190
- const accessToken = tokens.access_token;
191
- if (typeof accessToken !== "string" || accessToken.length === 0) {
192
- diagnostics.push(`Codex auth missing tokens.access_token in ${authPath}`);
193
- return { auth: null, diagnostics };
61
+ function appendMissingStates(snaps, fetched, isEnabled, target, diagnostics) {
62
+ for (const id of CORE_PROVIDERS) {
63
+ if (isEnabled(id) && !fetched.has(id) && (!target || target === id)) {
64
+ snaps.push({
65
+ timestamp: Date.now(),
66
+ provider: id,
67
+ planType: null,
68
+ primary: null,
69
+ secondary: null,
70
+ codeReview: null,
71
+ credits: null,
72
+ updatedAt: Date.now(),
73
+ isMissing: true,
74
+ missingReason: id === "codex" ? "Auth resolution failed" : undefined,
75
+ missingDetails: id === "codex" ? diagnostics : undefined
76
+ });
77
+ }
194
78
  }
195
- const accountId = tokens.account_id;
196
- const refreshToken = tokens.refresh_token;
197
- diagnostics.push(`Codex CLI auth loaded from ${authPath}`);
198
- return {
199
- auth: {
200
- openai: {
201
- type: "oauth",
202
- access: accessToken,
203
- accountId: typeof accountId === "string" ? accountId : undefined,
204
- refresh: typeof refreshToken === "string" ? refreshToken : undefined,
205
- },
206
- },
207
- diagnostics,
208
- };
79
+ return snaps;
209
80
  }
210
- function isRecord(value) {
211
- return typeof value === "object" && value !== null;
212
- }
213
- function timeout(ms) {
214
- return new Promise((resolve) => setTimeout(resolve, ms));
81
+ export async function loadAuths() {
82
+ const { auths } = await loadMergedAuths();
83
+ return auths;
215
84
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/opencode-usage-plugin",
3
- "version": "0.1.4-dev.0",
3
+ "version": "0.1.4-dev.2",
4
4
  "description": "opencode plugin for tracking AI provider usage, rate limits, and quotas",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",