@howaboua/opencode-usage-plugin 0.1.3 → 0.1.4-dev.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/providers/proxy/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAM5C,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAyI5C,eAAO,MAAM,aAAa,EAAE,aAAa,CAAC,IAAI,CAwB7C,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/providers/proxy/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAM5C,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAkM5C,eAAO,MAAM,aAAa,EAAE,aAAa,CAAC,IAAI,CAwB7C,CAAA"}
@@ -39,6 +39,49 @@ function normalizeTier(tier) {
39
39
  return "paid";
40
40
  return "free";
41
41
  }
42
+ function parseQuotaGroupsFromAggregation(quotaGroups) {
43
+ if (!quotaGroups)
44
+ return [];
45
+ const tiers = {
46
+ paid: new Map(),
47
+ free: new Map(),
48
+ };
49
+ for (const [groupName, groupData] of Object.entries(quotaGroups)) {
50
+ const mappedName = GROUP_MAPPING[groupName];
51
+ if (!mappedName)
52
+ continue;
53
+ const windows = groupData.windows || {};
54
+ const windowPriority = ["daily", "5h", "1h", "15m"];
55
+ let bestWindowName = null;
56
+ for (const windowName of windowPriority) {
57
+ if (windows[windowName]) {
58
+ bestWindowName = windowName;
59
+ break;
60
+ }
61
+ }
62
+ if (!bestWindowName && Object.keys(windows).length > 0) {
63
+ bestWindowName = Object.keys(windows)[0];
64
+ }
65
+ if (!bestWindowName)
66
+ continue;
67
+ const window = windows[bestWindowName];
68
+ // Since these are already aggregated by provider, we split by tier if possible.
69
+ // However, the aggregate API provides tier-agnostic totals.
70
+ // For now, we put them into "paid" as the default high-level view.
71
+ tiers.paid.set(mappedName, {
72
+ name: mappedName,
73
+ remaining: window.total_remaining,
74
+ max: window.total_max,
75
+ remainingPct: Math.round(window.remaining_pct),
76
+ resetTime: null, // Aggregated windows don't have a single reset_at
77
+ });
78
+ }
79
+ const result = [];
80
+ if (tiers.paid.size > 0) {
81
+ result.push({ tier: "paid", quotaGroups: sortQuotaGroups(Array.from(tiers.paid.values())) });
82
+ }
83
+ return result;
84
+ }
42
85
  function parseQuotaGroupsFromCredential(groupUsage) {
43
86
  if (!groupUsage)
44
87
  return [];
@@ -72,6 +115,11 @@ function parseQuotaGroupsFromCredential(groupUsage) {
72
115
  return Array.from(result.values());
73
116
  }
74
117
  function aggregateByProvider(provider) {
118
+ // Try aggregated quota groups first
119
+ if (provider.quota_groups && Object.keys(provider.quota_groups).length > 0) {
120
+ return parseQuotaGroupsFromAggregation(provider.quota_groups);
121
+ }
122
+ // Fallback to manual aggregation of credentials
75
123
  const tiers = {
76
124
  paid: new Map(),
77
125
  free: new Map(),
package/dist/types.d.ts CHANGED
@@ -64,6 +64,8 @@ export interface UsageSnapshot {
64
64
  copilotQuota?: CopilotQuota;
65
65
  updatedAt: number;
66
66
  isMissing?: boolean;
67
+ missingReason?: string;
68
+ missingDetails?: string[];
67
69
  }
68
70
  export interface UsageEntry {
69
71
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -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;AA4KD,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,218 +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)
88
- return ["→ [proxy] No data"];
89
- const lines = ["→ [Google] Mirrowel Proxy"];
90
- for (const provider of proxy.providers) {
91
- lines.push("");
92
- lines.push(` ${provider.name}:`);
93
- for (const tierInfo of provider.tiers) {
94
- const tierLabel = tierInfo.tier === "paid" ? "Paid" : "Free";
95
- lines.push(` ${tierLabel}:`);
96
- for (const group of tierInfo.quotaGroups) {
97
- const resetSuffix = group.resetTime ? formatResetSuffixISO(group.resetTime) : "";
98
- const label = `${group.name}:`.padEnd(9);
99
- lines.push(` ${label} ${formatBar(group.remainingPct)} ${group.remaining}/${group.max}${resetSuffix}`);
100
- }
101
- }
102
- }
103
- return lines;
104
- }
105
- function formatCopilotSnapshot(snapshot) {
106
- const copilot = snapshot.copilotQuota;
107
- if (!copilot)
108
- return ["→ [copilot] No data"];
109
- const lines = ["→ [GITHUB] Copilot"];
110
- const resetSuffix = copilot.resetTime ? formatResetSuffixISO(copilot.resetTime) : "";
111
- const totalLabel = copilot.total === -1 ? "∞" : copilot.total.toString();
112
- const chatLabel = "Chat:".padEnd(13);
113
- const chatRemaining = copilot.used;
114
- const chatPct = copilot.percentRemaining;
115
- lines.push(` ${chatLabel} ${formatBar(chatPct)} ${chatRemaining}/${totalLabel}${resetSuffix}`);
116
- if (copilot.completionsUsed !== undefined && copilot.completionsTotal !== undefined) {
117
- const compLabel = "Completions:".padEnd(13);
118
- const compPct = copilot.completionsTotal > 0
119
- ? Math.round((copilot.completionsUsed / copilot.completionsTotal) * 100)
120
- : 0;
121
- lines.push(` ${compLabel} ${formatBar(compPct)} ${copilot.completionsUsed}/${copilot.completionsTotal}`);
122
- }
123
- return lines;
124
- }
125
- function getAppDataPath() {
126
- const home = homedir();
127
- const plat = platform();
128
- if (plat === "darwin")
129
- return join(home, ".config", "opencode");
130
- if (plat === "win32")
131
- return join(process.env.APPDATA || join(home, "AppData", "Roaming"), "opencode");
132
- return join(process.env.XDG_CONFIG_HOME || join(home, ".config"), "opencode");
133
- }
134
- function formatMissingSnapshot(snapshot) {
135
- const provider = snapshot.provider;
136
- const label = provider === "codex" ? "codex" : provider === "proxy" ? "proxy" : "gh";
137
- const configPath = join(getAppDataPath(), "usage-config.jsonc");
138
- let providerInstruction = "";
139
- if (provider === "codex") {
140
- providerInstruction = "if you dont have codex oauth, please set your usage-config.jsonc to openai: false";
141
- }
142
- else if (provider === "proxy") {
143
- providerInstruction = "if you are not running Mirrowel's proxy, please set your usage-config.jsonc to proxy: false";
144
- }
145
- else if (provider === "copilot") {
146
- providerInstruction = "if you are not running GitHub Copilot, please set your usage-config.jsonc to copilot: false";
147
- }
148
- return [
149
- `→ [${provider.toUpperCase()}] - ${providerInstruction}`,
150
- "",
151
- `The file can be found in ${configPath}. Please read the comments in the file or visit the repo for the readme.`,
152
- "",
153
- "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!",
154
- ];
155
- }
156
28
  function formatSnapshot(snapshot) {
157
- if (snapshot.isMissing) {
29
+ if (snapshot.isMissing)
158
30
  return formatMissingSnapshot(snapshot);
159
- }
160
- if (snapshot.provider === "proxy" && snapshot.proxyQuota) {
31
+ if (snapshot.provider === "proxy")
161
32
  return formatProxySnapshot(snapshot);
162
- }
163
- if (snapshot.provider === "copilot" && snapshot.copilotQuota) {
33
+ if (snapshot.provider === "copilot")
164
34
  return formatCopilotSnapshot(snapshot);
165
- }
166
- const plan = snapshot.planType ? ` (${formatPlanType(snapshot.planType)})` : "";
35
+ const plan = snapshot.planType ? ` (${snapshot.planType.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase())})` : "";
167
36
  const lines = [`→ [${snapshot.provider.toUpperCase()}]${plan}`];
168
- const primary = snapshot.primary;
169
- if (primary) {
170
- const remainingPct = 100 - primary.usedPercent;
171
- const label = "Hourly:".padEnd(13);
172
- lines.push(` ${label} ${formatBar(remainingPct)} ${remainingPct.toFixed(0)}% left${formatResetSuffix(primary.resetsAt)}`);
173
- }
174
- const secondary = snapshot.secondary;
175
- if (secondary) {
176
- const remainingPct = 100 - secondary.usedPercent;
177
- const label = "Weekly:".padEnd(13);
178
- lines.push(` ${label} ${formatBar(remainingPct)} ${remainingPct.toFixed(0)}% left${formatResetSuffix(secondary.resetsAt)}`);
179
- }
180
- const codeReview = snapshot.codeReview;
181
- if (codeReview) {
182
- const remainingPct = 100 - codeReview.usedPercent;
183
- const label = "Code Review:".padEnd(13);
184
- 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
+ }
185
49
  }
186
50
  if (snapshot.credits?.hasCredits) {
187
51
  lines.push(` Credits: ${snapshot.credits.balance}`);
52
+ hasData = true;
188
53
  }
189
- return lines;
54
+ return hasData ? lines : formatMissingSnapshot(snapshot);
190
55
  }
191
56
  export async function renderUsageStatus(options) {
192
57
  if (options.snapshots.length === 0) {
193
58
  const filterMsg = options.filter ? ` for "${options.filter}"` : "";
194
- await sendStatusMessage({
195
- client: options.client,
196
- state: options.state,
197
- sessionID: options.sessionID,
198
- text: `▣ Usage | No data received${filterMsg}.`,
199
- });
200
- return;
59
+ return sendStatusMessage({ ...options, text: `▣ Usage | No data received${filterMsg}.` });
201
60
  }
202
61
  const lines = ["▣ Usage Status", ""];
203
- options.snapshots.forEach((snapshot, index) => {
204
- const snapshotLines = formatSnapshot(snapshot);
205
- for (const line of snapshotLines)
206
- lines.push(line);
207
- if (index < options.snapshots.length - 1) {
208
- lines.push("");
209
- lines.push("---");
210
- }
211
- });
212
- await sendStatusMessage({
213
- client: options.client,
214
- state: options.state,
215
- sessionID: options.sessionID,
216
- 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("", "---");
217
66
  });
67
+ await sendStatusMessage({ ...options, text: lines.join("\n") });
218
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,11 +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>;
7
+ export declare function resolveProviderFilter(filter?: string): string | undefined;
8
+ export declare function loadAuths(): Promise<import("./auth/loader").AuthRecord>;
11
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;AAe5C,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,CA4EnF;AAED,wBAAsB,SAAS,IAAI,OAAO,CAAC,UAAU,CAAC,CA8BrD"}
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,CAoCnF;AAWD,wBAAgB,qBAAqB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEzE;AA6BD,wBAAsB,SAAS,gDAG9B"}
@@ -1,162 +1,83 @@
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
- })
18
- .passthrough();
19
- const authRecordSchema = z.record(z.string(), authEntrySchema);
20
- export const providerAliases = {
21
- codex: "codex",
22
- openai: "codex",
23
- gpt: "codex",
24
- proxy: "proxy",
25
- agy: "proxy",
26
- antigravity: "proxy",
27
- gemini: "proxy",
28
- copilot: "copilot",
29
- gh: "copilot",
30
- github: "copilot",
31
- };
32
- export function resolveProviderFilter(filter) {
33
- if (!filter)
34
- return undefined;
35
- const normalized = filter.toLowerCase().trim();
36
- return providerAliases[normalized];
37
- }
9
+ const CORE_PROVIDERS = ["codex", "proxy", "copilot"];
38
10
  export async function fetchUsageSnapshots(filter) {
39
- const targetProvider = resolveProviderFilter(filter);
40
- const usageConfig = await loadUsageConfig().catch(() => null);
41
- const providerToggles = usageConfig?.providers ?? {};
42
- const isProviderEnabled = (id) => {
11
+ const target = resolveFilter(filter);
12
+ const config = await loadUsageConfig().catch(() => null);
13
+ const toggles = config?.providers ?? {};
14
+ const isEnabled = (id) => {
43
15
  if (id === "codex")
44
- return providerToggles.openai !== false;
45
- if (id === "proxy")
46
- return providerToggles.proxy !== false;
47
- if (id === "copilot")
48
- return providerToggles.copilot !== false;
49
- return true;
16
+ return toggles.openai !== false;
17
+ return toggles[id] !== false;
50
18
  };
51
- const auths = await loadAuths();
19
+ const { auths, codexDiagnostics } = await loadMergedAuths();
52
20
  const entries = resolveProviderAuths(auths, null);
53
21
  const snapshots = [];
54
- const coreProviders = ["codex", "proxy", "copilot"];
55
- const fetchedProviders = new Set();
22
+ const fetched = new Set();
56
23
  const fetches = entries
57
- .filter((entry) => !targetProvider || entry.providerID === targetProvider)
58
- .filter((entry) => isProviderEnabled(entry.providerID))
59
- .map(async (entry) => {
60
- const provider = providers[entry.providerID];
61
- if (!provider?.fetchUsage)
62
- return;
63
- try {
64
- const snapshot = await provider.fetchUsage(entry.auth);
65
- if (snapshot) {
66
- snapshots.push(snapshot);
67
- fetchedProviders.add(entry.providerID);
68
- }
69
- }
70
- 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
+ snapshots.push(snap);
29
+ fetched.add(e.providerID);
71
30
  }
72
31
  });
73
- const specialProviders = ["proxy", "copilot"];
74
- for (const id of specialProviders) {
75
- if ((!targetProvider || targetProvider === id) && isProviderEnabled(id)) {
32
+ // Handle special/default fetches
33
+ for (const id of ["proxy", "copilot"]) {
34
+ if ((!target || target === id) && isEnabled(id) && !fetched.has(id)) {
76
35
  const provider = providers[id];
77
36
  if (provider?.fetchUsage) {
78
- fetches.push(provider
79
- .fetchUsage(undefined)
80
- .then((snapshot) => {
81
- if (snapshot) {
82
- snapshots.push(snapshot);
83
- fetchedProviders.add(id);
37
+ fetches.push(provider.fetchUsage(undefined).then(s => {
38
+ if (s) {
39
+ snapshots.push(s);
40
+ fetched.add(id);
84
41
  }
85
- })
86
- .catch(() => { }));
87
- }
88
- }
89
- }
90
- await Promise.race([Promise.all(fetches), timeout(5000)]);
91
- for (const id of coreProviders) {
92
- if (isProviderEnabled(id) && !fetchedProviders.has(id)) {
93
- if (!targetProvider || targetProvider === id) {
94
- snapshots.push({
95
- timestamp: Date.now(),
96
- provider: id,
97
- planType: null,
98
- primary: null,
99
- secondary: null,
100
- codeReview: null,
101
- credits: null,
102
- updatedAt: Date.now(),
103
- isMissing: true,
104
- });
42
+ }).catch(() => { }));
105
43
  }
106
44
  }
107
45
  }
108
- return snapshots;
46
+ await Promise.race([Promise.all(fetches), new Promise(r => setTimeout(r, 5000))]);
47
+ return appendMissingStates(snapshots, fetched, isEnabled, target, codexDiagnostics);
109
48
  }
110
- export async function loadAuths() {
111
- const possiblePaths = getPossibleAuthPaths();
112
- const mergedAuth = {};
113
- for (const authPath of possiblePaths.reverse()) {
114
- try {
115
- const file = Bun.file(authPath);
116
- if (!(await file.exists()))
117
- continue;
118
- const data = await file.json();
119
- if (data && typeof data === "object") {
120
- if (authPath.includes(".codex")) {
121
- const codexAuth = transformCodexAuth(data);
122
- if (codexAuth) {
123
- Object.assign(mergedAuth, codexAuth);
124
- }
125
- continue;
126
- }
127
- const parsed = authRecordSchema.safeParse(data);
128
- if (parsed.success) {
129
- Object.assign(mergedAuth, parsed.data);
130
- }
131
- }
132
- }
133
- catch {
134
- continue;
49
+ function resolveFilter(f) {
50
+ const aliases = {
51
+ codex: "codex", openai: "codex", gpt: "codex",
52
+ proxy: "proxy", agy: "proxy", gemini: "proxy",
53
+ copilot: "copilot", github: "copilot"
54
+ };
55
+ return f ? aliases[f.toLowerCase().trim()] : undefined;
56
+ }
57
+ export function resolveProviderFilter(filter) {
58
+ return resolveFilter(filter);
59
+ }
60
+ function appendMissingStates(snaps, fetched, isEnabled, target, diagnostics) {
61
+ for (const id of CORE_PROVIDERS) {
62
+ if (isEnabled(id) && !fetched.has(id) && (!target || target === id)) {
63
+ snaps.push({
64
+ timestamp: Date.now(),
65
+ provider: id,
66
+ planType: null,
67
+ primary: null,
68
+ secondary: null,
69
+ codeReview: null,
70
+ credits: null,
71
+ updatedAt: Date.now(),
72
+ isMissing: true,
73
+ missingReason: id === "codex" ? "Auth resolution failed" : undefined,
74
+ missingDetails: id === "codex" ? diagnostics : undefined
75
+ });
135
76
  }
136
77
  }
137
- return mergedAuth;
78
+ return snaps;
138
79
  }
139
- function transformCodexAuth(data) {
140
- const codexAuthSchema = z.object({
141
- tokens: z.object({
142
- access_token: z.string(),
143
- account_id: z.string().optional(),
144
- refresh_token: z.string().optional(),
145
- }),
146
- });
147
- const parsed = codexAuthSchema.safeParse(data);
148
- if (!parsed.success)
149
- return null;
150
- const { access_token, account_id, refresh_token } = parsed.data.tokens;
151
- return {
152
- openai: {
153
- type: "oauth",
154
- access: access_token,
155
- accountId: account_id,
156
- refresh: refresh_token,
157
- },
158
- };
159
- }
160
- function timeout(ms) {
161
- return new Promise((resolve) => setTimeout(resolve, ms));
80
+ export async function loadAuths() {
81
+ const { auths } = await loadMergedAuths();
82
+ return auths;
162
83
  }
@@ -2,17 +2,22 @@
2
2
  * Resolves provider auth entries for usage snapshots.
3
3
  */
4
4
  import type { CodexAuth } from "../providers/codex";
5
+ import type { CopilotAuthData } from "../providers/copilot/types";
5
6
  export type AuthEntry = {
6
7
  type?: string;
7
8
  access?: string;
8
9
  refresh?: string;
9
10
  enterpriseUrl?: string;
10
11
  accountId?: string;
12
+ key?: string;
11
13
  };
12
14
  export type AuthRecord = Record<string, AuthEntry>;
13
15
  type ProviderAuthEntry = {
14
16
  providerID: "codex";
15
17
  auth: CodexAuth;
18
+ } | {
19
+ providerID: "copilot";
20
+ auth: CopilotAuthData;
16
21
  };
17
22
  export declare function resolveProviderAuths(auths: AuthRecord, usageToken: string | null): ProviderAuthEntry[];
18
23
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/usage/registry.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEnD,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;AAElD,KAAK,iBAAiB,GAClB;IAAE,UAAU,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAA;AAqB5C,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,iBAAiB,EAAE,CActG"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/usage/registry.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAEjE,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,GAAG,CAAC,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;AAElD,KAAK,iBAAiB,GAClB;IAAE,UAAU,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GACxC;IAAE,UAAU,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,eAAe,CAAA;CAAE,CAAA;AA8BpD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,iBAAiB,EAAE,CActG"}
@@ -7,10 +7,19 @@ const providerDescriptors = [
7
7
  authKeys: ["codex", "openai"],
8
8
  requiresOAuth: true,
9
9
  buildAuth: (entry) => ({
10
- access: entry.access,
10
+ access: entry.access || entry.key,
11
11
  accountId: entry.accountId,
12
12
  }),
13
13
  },
14
+ {
15
+ id: "copilot",
16
+ authKeys: ["copilot", "github-copilot"],
17
+ requiresOAuth: true,
18
+ buildAuth: (entry) => ({
19
+ access: entry.access,
20
+ refresh: entry.refresh,
21
+ }),
22
+ },
14
23
  ];
15
24
  export function resolveProviderAuths(auths, usageToken) {
16
25
  const entries = [];
@@ -21,7 +30,7 @@ export function resolveProviderAuths(auths, usageToken) {
21
30
  const auth = auths[matched];
22
31
  if (!auth)
23
32
  continue;
24
- if (descriptor.requiresOAuth && auth.type && auth.type !== "oauth")
33
+ if (descriptor.requiresOAuth && auth.type && auth.type !== "oauth" && auth.type !== "token")
25
34
  continue;
26
35
  const built = descriptor.buildAuth(auth, usageToken);
27
36
  entries.push({ providerID: descriptor.id, auth: built });
@@ -1 +1 @@
1
- {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,wBAAgB,cAAc,IAAI,MAAM,CAiBvC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,EAAE,CAyB/C;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAYxC"}
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,wBAAgB,cAAc,IAAI,MAAM,CAUvC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,EAAE,CAe/C;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAYxC"}
@@ -8,17 +8,11 @@ import { existsSync } from "fs";
8
8
  export function getAppDataPath() {
9
9
  const plat = platform();
10
10
  const home = homedir();
11
- if (plat === "darwin") {
12
- return join(home, "Library", "Application Support", "opencode");
13
- }
14
11
  if (plat === "win32") {
15
12
  return join(process.env.APPDATA || join(home, "AppData", "Roaming"), "opencode");
16
13
  }
17
- const xdgData = process.env.XDG_DATA_HOME;
18
- if (xdgData) {
19
- return join(xdgData, "opencode");
20
- }
21
- return join(home, ".local", "share", "opencode");
14
+ const dataHome = process.env.XDG_DATA_HOME || join(home, ".local", "share");
15
+ return join(dataHome, "opencode");
22
16
  }
23
17
  /**
24
18
  * Returns all possible auth file paths for the current platform.
@@ -28,24 +22,13 @@ export function getPossibleAuthPaths() {
28
22
  const plat = platform();
29
23
  const home = homedir();
30
24
  const pathSet = new Set();
31
- if (plat === "darwin") {
32
- // OpenCode on macOS uses Linux-style paths
33
- pathSet.add(join(home, ".local", "share", "opencode", "auth.json"));
34
- // Standard macOS location (fallback)
35
- pathSet.add(join(home, "Library", "Application Support", "opencode", "auth.json"));
36
- // Codex-specific auth (fallback)
37
- pathSet.add(join(home, ".codex", "auth.json"));
38
- }
39
- else if (plat === "win32") {
25
+ if (plat === "win32") {
40
26
  pathSet.add(join(process.env.APPDATA || join(home, "AppData", "Roaming"), "opencode", "auth.json"));
41
27
  }
42
28
  else {
43
29
  // Linux/other
44
- const xdgData = process.env.XDG_DATA_HOME;
45
- if (xdgData) {
46
- pathSet.add(join(xdgData, "opencode", "auth.json"));
47
- }
48
- pathSet.add(join(home, ".local", "share", "opencode", "auth.json"));
30
+ const dataHome = process.env.XDG_DATA_HOME || join(home, ".local", "share");
31
+ pathSet.add(join(dataHome, "opencode", "auth.json"));
49
32
  pathSet.add(join(home, ".codex", "auth.json"));
50
33
  }
51
34
  return Array.from(pathSet);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/opencode-usage-plugin",
3
- "version": "0.1.3",
3
+ "version": "0.1.4-dev.1",
4
4
  "description": "opencode plugin for tracking AI provider usage, rate limits, and quotas",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,7 +31,9 @@
31
31
  "scripts": {
32
32
  "clean": "rm -rf dist",
33
33
  "build": "npm run clean && tsc",
34
- "prepublishOnly": "npm run build"
34
+ "prepublishOnly": "npm run build",
35
+ "publish:dev": "npm publish --tag dev",
36
+ "release:dev": "npm version prerelease --preid dev && npm publish --tag dev"
35
37
  },
36
38
  "publishConfig": {
37
39
  "access": "public"