@toninho09/opencode-usage 1.0.0

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,185 @@
1
+ import { fetchWithTimeout } from "../../shared/utils";
2
+ import { readAuthConfig, type AuthProvider } from "../../shared/auth";
3
+ import type { CopilotUsageResponse, CopilotTokenResponse } from "./types";
4
+
5
+ const GITHUB_API_BASE_URL = "https://api.github.com";
6
+
7
+ const COPILOT_VERSION = "0.35.0";
8
+ const EDITOR_VERSION = "vscode/1.107.0";
9
+ const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
10
+ const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
11
+
12
+ const COPILOT_HEADERS = {
13
+ "User-Agent": USER_AGENT,
14
+ "Editor-Version": EDITOR_VERSION,
15
+ "Editor-Plugin-Version": EDITOR_PLUGIN_VERSION,
16
+ "Copilot-Integration-Id": "vscode-chat",
17
+ };
18
+
19
+ export class CopilotClient {
20
+ private readonly apiBaseUrl = GITHUB_API_BASE_URL;
21
+
22
+ /**
23
+ * Builds headers for authentication with Bearer token
24
+ */
25
+ private buildGitHubHeaders(token: string): Record<string, string> {
26
+ return {
27
+ "Content-Type": "application/json",
28
+ Accept: "application/json",
29
+ Authorization: `Bearer ${token}`,
30
+ ...COPILOT_HEADERS,
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Builds headers for legacy authentication (token prefix)
36
+ */
37
+ private buildLegacyHeaders(token: string): Record<string, string> {
38
+ return {
39
+ "Content-Type": "application/json",
40
+ Accept: "application/json",
41
+ Authorization: `token ${token}`,
42
+ ...COPILOT_HEADERS,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Exchanges OAuth token for specific Copilot token
48
+ */
49
+ private async exchangeForCopilotToken(oauthToken: string): Promise<string | null> {
50
+ try {
51
+ const response = await fetchWithTimeout(
52
+ `${this.apiBaseUrl}/copilot_internal/v2/token`,
53
+ {
54
+ headers: {
55
+ Accept: "application/json",
56
+ Authorization: `Bearer ${oauthToken}`,
57
+ ...COPILOT_HEADERS,
58
+ },
59
+ },
60
+ );
61
+
62
+ if (!response.ok) {
63
+ return null;
64
+ }
65
+
66
+ const tokenData: CopilotTokenResponse = await response.json();
67
+ return tokenData.token;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Strategy 1: Try with cached token if still valid
75
+ */
76
+ private async fetchWithCachedToken(
77
+ cachedAccessToken: string,
78
+ tokenExpiry: number,
79
+ oauthToken: string,
80
+ ): Promise<CopilotUsageResponse | null> {
81
+ if (!cachedAccessToken || cachedAccessToken === oauthToken || tokenExpiry <= Date.now()) {
82
+ return null;
83
+ }
84
+
85
+ const response = await fetchWithTimeout(
86
+ `${this.apiBaseUrl}/copilot_internal/user`,
87
+ { headers: this.buildGitHubHeaders(cachedAccessToken) },
88
+ );
89
+
90
+ if (response.ok) {
91
+ return response.json() as Promise<CopilotUsageResponse>;
92
+ }
93
+
94
+ return null;
95
+ }
96
+
97
+ /**
98
+ * Strategy 2: Try with direct OAuth token (legacy format)
99
+ */
100
+ private async fetchWithLegacyToken(oauthToken: string): Promise<CopilotUsageResponse | null> {
101
+ const response = await fetchWithTimeout(
102
+ `${this.apiBaseUrl}/copilot_internal/user`,
103
+ { headers: this.buildLegacyHeaders(oauthToken) },
104
+ );
105
+
106
+ if (response.ok) {
107
+ return response.json() as Promise<CopilotUsageResponse>;
108
+ }
109
+
110
+ return null;
111
+ }
112
+
113
+ /**
114
+ * Strategy 3: Exchange OAuth token for Copilot token and fetch
115
+ */
116
+ private async fetchWithExchangedToken(oauthToken: string): Promise<CopilotUsageResponse> {
117
+ const copilotToken = await this.exchangeForCopilotToken(oauthToken);
118
+
119
+ if (!copilotToken) {
120
+ throw new Error("Failed to exchange OAuth token for Copilot token");
121
+ }
122
+
123
+ const response = await fetchWithTimeout(
124
+ `${this.apiBaseUrl}/copilot_internal/user`,
125
+ { headers: this.buildGitHubHeaders(copilotToken) },
126
+ );
127
+
128
+ if (!response.ok) {
129
+ const errorText = await response.text();
130
+ throw new Error(`GitHub API Error ${response.status}: ${errorText}`);
131
+ }
132
+
133
+ return response.json() as Promise<CopilotUsageResponse>;
134
+ }
135
+
136
+ /**
137
+ * Tries to fetch usage data using multiple authentication strategies
138
+ */
139
+ private async fetchWithAuthStrategies(authData: AuthProvider): Promise<CopilotUsageResponse> {
140
+ const oauthToken = authData.refresh || authData.access;
141
+ if (!oauthToken) {
142
+ throw new Error("No OAuth token found in auth data");
143
+ }
144
+
145
+ const cachedAccessToken = authData.access || "";
146
+ const tokenExpiry = authData.expires || 0;
147
+
148
+ // Strategy 1: Try with cached token
149
+ const cachedResult = await this.fetchWithCachedToken(cachedAccessToken, tokenExpiry, oauthToken);
150
+ if (cachedResult) {
151
+ return cachedResult;
152
+ }
153
+
154
+ // Strategy 2: Try with direct OAuth token
155
+ const legacyResult = await this.fetchWithLegacyToken(oauthToken);
156
+ if (legacyResult) {
157
+ return legacyResult;
158
+ }
159
+
160
+ // Strategy 3: Exchange token and try again
161
+ return this.fetchWithExchangedToken(oauthToken);
162
+ }
163
+
164
+ /**
165
+ * Fetches Copilot usage data
166
+ */
167
+ async fetchUsage(): Promise<CopilotUsageResponse | null> {
168
+ const auth = readAuthConfig();
169
+ const copilotAuth = auth?.["github-copilot"];
170
+
171
+ if (!copilotAuth?.refresh) {
172
+ return null;
173
+ }
174
+
175
+ return this.fetchWithAuthStrategies(copilotAuth);
176
+ }
177
+
178
+ /**
179
+ * Checks if Copilot is configured
180
+ */
181
+ isConfigured(): boolean {
182
+ const auth = readAuthConfig();
183
+ return !!auth?.["github-copilot"]?.refresh;
184
+ }
185
+ }
@@ -0,0 +1,65 @@
1
+ import { formatResetLine } from "../../shared/formatting";
2
+ import { createUsedProgressBar } from "../../shared/utils";
3
+ import type { CopilotUsageResponse, QuotaDetail } from "./types";
4
+
5
+ export class CopilotFormatter {
6
+ /**
7
+ * Formats a Copilot quota line with progress bar
8
+ */
9
+ private formatQuotaLine(name: string, quota: QuotaDetail | undefined): string {
10
+ if (!quota) {
11
+ return "";
12
+ }
13
+
14
+ if (quota.unlimited) {
15
+ return `${name.padEnd(16)} Unlimited`;
16
+ }
17
+
18
+ const total = quota.entitlement;
19
+ const used = total - quota.remaining;
20
+ const percentUsed = Math.round((used / total) * 100);
21
+ const progressBar = createUsedProgressBar(percentUsed, 20);
22
+
23
+ return `${name.padEnd(16)} ${progressBar} ${percentUsed}% (${used}/${total})`;
24
+ }
25
+
26
+ /**
27
+ * Formats Copilot usage data for display
28
+ */
29
+ format(data: CopilotUsageResponse): string {
30
+ const lines: string[] = [];
31
+
32
+ lines.push("╔════════════════════════════════════════╗");
33
+ lines.push("║ GITHUB COPILOT ║");
34
+ lines.push("╚════════════════════════════════════════╝");
35
+ lines.push(`Plan: ${data.copilot_plan}`);
36
+
37
+ const premium = data.quota_snapshots.premium_interactions;
38
+ if (premium) {
39
+ const premiumLine = this.formatQuotaLine("Premium:", premium);
40
+ if (premiumLine) {
41
+ lines.push(premiumLine);
42
+ }
43
+ }
44
+
45
+ const chat = data.quota_snapshots.chat;
46
+ if (chat) {
47
+ const chatLine = this.formatQuotaLine("Chat:", chat);
48
+ if (chatLine) {
49
+ lines.push(chatLine);
50
+ }
51
+ }
52
+
53
+ const completions = data.quota_snapshots.completions;
54
+ if (completions) {
55
+ const completionsLine = this.formatQuotaLine("Completions:", completions);
56
+ if (completionsLine) {
57
+ lines.push(completionsLine);
58
+ }
59
+ }
60
+
61
+ lines.push(formatResetLine("Quota Resets:", data.quota_reset_date, 16));
62
+
63
+ return lines.join("\n");
64
+ }
65
+ }
@@ -0,0 +1,36 @@
1
+ import type { UsageProvider, ProviderMessage } from "../base";
2
+ import { CopilotClient } from "./client";
3
+ import { CopilotFormatter } from "./formatter";
4
+
5
+ const client = new CopilotClient();
6
+ const formatter = new CopilotFormatter();
7
+
8
+ export const copilotProvider: UsageProvider = {
9
+ name: "GitHub Copilot",
10
+ id: "copilot",
11
+ description: "Monitoramento de uso do GitHub Copilot",
12
+
13
+ async getUsageData(): Promise<ProviderMessage | null> {
14
+ try {
15
+ const data = await client.fetchUsage();
16
+ if (!data) {
17
+ return null;
18
+ }
19
+
20
+ return {
21
+ content: formatter.format(data),
22
+ };
23
+ } catch (error) {
24
+ return {
25
+ content: "",
26
+ error: error instanceof Error ? error.message : String(error),
27
+ };
28
+ }
29
+ },
30
+
31
+ isConfigured(): boolean {
32
+ return client.isConfigured();
33
+ },
34
+ };
35
+
36
+ export default copilotProvider;
@@ -0,0 +1,38 @@
1
+ export interface QuotaDetail {
2
+ entitlement: number;
3
+ overage_count: number;
4
+ overage_permitted: boolean;
5
+ percent_remaining: number;
6
+ quota_id: string;
7
+ quota_remaining: number;
8
+ remaining: number;
9
+ unlimited: boolean;
10
+ }
11
+
12
+ export interface QuotaSnapshots {
13
+ chat?: QuotaDetail;
14
+ completions?: QuotaDetail;
15
+ premium_interactions: QuotaDetail;
16
+ }
17
+
18
+ export interface CopilotUsageResponse {
19
+ access_type_sku: string;
20
+ analytics_tracking_id: string;
21
+ assigned_date: string;
22
+ can_signup_for_limited: boolean;
23
+ chat_enabled: boolean;
24
+ copilot_plan: string;
25
+ organization_login_list: unknown[];
26
+ organization_list: unknown[];
27
+ quota_reset_date: string;
28
+ quota_snapshots: QuotaSnapshots;
29
+ }
30
+
31
+ export interface CopilotTokenResponse {
32
+ token: string;
33
+ expires_at: number;
34
+ refresh_in: number;
35
+ endpoints: {
36
+ api: string;
37
+ };
38
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Exporta todos os providers e componentes principais
3
+ */
4
+ export { copilotProvider } from "./copilot";
5
+ export { claudeProvider } from "./claude";
6
+ export { zaiProvider } from "./zai";
7
+ export { registry, ProviderRegistry } from "./registry";
8
+ export type { UsageProvider, ProviderMessage } from "./base";
@@ -0,0 +1,56 @@
1
+ import type { UsageProvider } from "./base";
2
+ import { copilotProvider } from "./copilot";
3
+ import { claudeProvider } from "./claude";
4
+ import { zaiProvider } from "./zai";
5
+
6
+ /**
7
+ * Centralized registry of all usage providers
8
+ */
9
+ export class ProviderRegistry {
10
+ private providers: Map<string, UsageProvider> = new Map();
11
+
12
+ /**
13
+ * Registers a new provider
14
+ */
15
+ register(provider: UsageProvider): void {
16
+ this.providers.set(provider.id, provider);
17
+ }
18
+
19
+ /**
20
+ * Returns all registered providers
21
+ */
22
+ getAll(): UsageProvider[] {
23
+ return Array.from(this.providers.values());
24
+ }
25
+
26
+ /**
27
+ * Returns a specific provider by ID
28
+ */
29
+ getById(id: string): UsageProvider | undefined {
30
+ return this.providers.get(id);
31
+ }
32
+
33
+ /**
34
+ * Returns all configured providers
35
+ */
36
+ getConfigured(): UsageProvider[] {
37
+ return this.getAll().filter((provider) => provider.isConfigured());
38
+ }
39
+
40
+ /**
41
+ * Checks if any provider is configured
42
+ */
43
+ hasConfigured(): boolean {
44
+ return this.getConfigured().length > 0;
45
+ }
46
+ }
47
+
48
+ // Creates singleton instance of registry
49
+ export const registry = new ProviderRegistry();
50
+
51
+ // Auto-registers all providers
52
+ registry.register(copilotProvider);
53
+ registry.register(claudeProvider);
54
+ registry.register(zaiProvider);
55
+
56
+ export default registry;
@@ -0,0 +1,97 @@
1
+ import { fetchWithTimeout } from "../../shared/utils";
2
+ import { readAuthConfig, type AuthProvider } from "../../shared/auth";
3
+ import type { ZaiUsageResponse } from "./types";
4
+
5
+ const ZAI_API_BASE_URL = "https://api.z.ai";
6
+
7
+ export class ZaiClient {
8
+ private readonly apiBaseUrl = ZAI_API_BASE_URL;
9
+
10
+ /**
11
+ * Builds headers for authentication with Z.ai API
12
+ */
13
+ private buildZaiHeaders(apiKey: string): Record<string, string> {
14
+ return {
15
+ "Authorization": apiKey,
16
+ "Content-Type": "application/json",
17
+ "User-Agent": "OpenCode-Status-Plugin/1.0",
18
+ };
19
+ }
20
+
21
+ /**
22
+ * Fetches usage data from Z.ai API
23
+ */
24
+ private async fetchZaiUsage(authData: AuthProvider): Promise<ZaiUsageResponse> {
25
+ const apiKey = authData.key;
26
+
27
+ if (!apiKey) {
28
+ throw new Error("No API key found in Z.ai auth data");
29
+ }
30
+
31
+ const url = `${this.apiBaseUrl}/api/monitor/usage/quota/limit`;
32
+ const response = await fetchWithTimeout(url, {
33
+ method: "GET",
34
+ headers: this.buildZaiHeaders(apiKey),
35
+ });
36
+
37
+ if (!response.ok) {
38
+ const errorText = await response.text();
39
+ throw new Error(`Z.ai API Error ${response.status}: ${errorText}`);
40
+ }
41
+
42
+ const data: ZaiUsageResponse = await response.json();
43
+
44
+ if (!data.success || data.code !== 200) {
45
+ throw new Error(`Z.ai API Error ${data.code}: ${data.msg || "Unknown error"}`);
46
+ }
47
+
48
+ return data;
49
+ }
50
+
51
+ /**
52
+ * Fetches Z.ai usage data
53
+ */
54
+ async fetchUsage(): Promise<ZaiUsageResponse | null> {
55
+ const authConfig = readAuthConfig();
56
+
57
+ if (!authConfig) {
58
+ return null;
59
+ }
60
+
61
+ const authData = authConfig["zai-coding-plan"];
62
+
63
+ if (!authData || authData.type !== "api" || !authData.key) {
64
+ return null;
65
+ }
66
+
67
+ return this.fetchZaiUsage(authData);
68
+ }
69
+
70
+ /**
71
+ * Returns the Z.ai API key (for use in formatter)
72
+ */
73
+ getApiKey(): string | null {
74
+ const authConfig = readAuthConfig();
75
+
76
+ if (!authConfig) {
77
+ return null;
78
+ }
79
+
80
+ const authData = authConfig["zai-coding-plan"];
81
+
82
+ if (!authData || authData.type !== "api" || !authData.key) {
83
+ return null;
84
+ }
85
+
86
+ return authData.key;
87
+ }
88
+
89
+ /**
90
+ * Checks if Z.ai is configured
91
+ */
92
+ isConfigured(): boolean {
93
+ const authConfig = readAuthConfig();
94
+ const authData = authConfig?.["zai-coding-plan"];
95
+ return !!(authData && authData.type === "api" && authData.key);
96
+ }
97
+ }
@@ -0,0 +1,77 @@
1
+ import { formatResetLine, formatTokens, maskApiKey } from "../../shared/formatting";
2
+ import { createUsedProgressBar } from "../../shared/utils";
3
+ import type { ZaiUsageResponse, UsageLimitItem } from "./types";
4
+
5
+ export class ZaiFormatter {
6
+ /**
7
+ * Calculates usage display based on available fields from API
8
+ */
9
+ private calculateUsageDisplay(limit: UsageLimitItem): string {
10
+ const { usage, currentValue, remaining } = limit;
11
+
12
+ // Case 1: API returns usage and currentValue (when already used)
13
+ if (currentValue !== undefined && usage !== undefined) {
14
+ return ` (${formatTokens(currentValue)}/${formatTokens(usage)})`;
15
+ }
16
+
17
+ // Case 2: API returns remaining and usage (calculate currentValue)
18
+ if (remaining !== undefined && usage !== undefined) {
19
+ const used = usage - remaining;
20
+ return ` (${formatTokens(used)}/${formatTokens(usage)})`;
21
+ }
22
+
23
+ // Case 3: Only remaining available
24
+ if (remaining !== undefined) {
25
+ return ` (0/${formatTokens(remaining)})`;
26
+ }
27
+
28
+ return "";
29
+ }
30
+
31
+ /**
32
+ * Formats Z.ai usage data for display
33
+ */
34
+ format(data: ZaiUsageResponse, apiKey: string): string {
35
+ const lines: string[] = [];
36
+ const limits = data.data.limits;
37
+
38
+ const maskedKey = maskApiKey(apiKey);
39
+ lines.push("╔════════════════════════════════════════╗");
40
+ lines.push("║ Z.AI CODING PLAN ║");
41
+ lines.push("╚════════════════════════════════════════╝");
42
+ lines.push(`Account: ${maskedKey} (Z.AI Coding Plan)`);
43
+
44
+ if (!limits || limits.length === 0) {
45
+ lines.push("No quota data available");
46
+ return lines.join("\n");
47
+ }
48
+
49
+ const tokensLimit = limits.find((l) => l.type === "TOKENS_LIMIT");
50
+ if (tokensLimit) {
51
+ const percentUsed = tokensLimit.percentage;
52
+ const progressBar = createUsedProgressBar(percentUsed, 20);
53
+ const usageDisplay = this.calculateUsageDisplay(tokensLimit);
54
+
55
+ lines.push(`Tokens: ${progressBar} ${percentUsed}%${usageDisplay}`);
56
+
57
+ if (tokensLimit.nextResetTime) {
58
+ lines.push(formatResetLine("5h Resets:", tokensLimit.nextResetTime, 16));
59
+ }
60
+ }
61
+
62
+ const timeLimit = limits.find((l) => l.type === "TIME_LIMIT");
63
+ if (timeLimit) {
64
+ const percentUsed = timeLimit.percentage;
65
+ const progressBar = createUsedProgressBar(percentUsed, 20);
66
+
67
+ const currentValue = timeLimit.currentValue ?? 0;
68
+ const total = timeLimit.usage ?? (timeLimit.remaining ?? 0);
69
+
70
+ lines.push(
71
+ `MCP Searches: ${progressBar} ${percentUsed}% (${currentValue}/${total})`,
72
+ );
73
+ }
74
+
75
+ return lines.join("\n");
76
+ }
77
+ }
@@ -0,0 +1,41 @@
1
+ import type { UsageProvider, ProviderMessage } from "../base";
2
+ import { ZaiClient } from "./client";
3
+ import { ZaiFormatter } from "./formatter";
4
+
5
+ const client = new ZaiClient();
6
+ const formatter = new ZaiFormatter();
7
+
8
+ export const zaiProvider: UsageProvider = {
9
+ name: "Z.ai Coding Plan",
10
+ id: "zai",
11
+ description: "Monitoramento de uso do Z.ai Coding Plan",
12
+
13
+ async getUsageData(): Promise<ProviderMessage | null> {
14
+ try {
15
+ const data = await client.fetchUsage();
16
+ if (!data) {
17
+ return null;
18
+ }
19
+
20
+ const apiKey = client.getApiKey();
21
+ if (!apiKey) {
22
+ return null;
23
+ }
24
+
25
+ return {
26
+ content: formatter.format(data, apiKey),
27
+ };
28
+ } catch (error) {
29
+ return {
30
+ content: "",
31
+ error: error instanceof Error ? error.message : String(error),
32
+ };
33
+ }
34
+ },
35
+
36
+ isConfigured(): boolean {
37
+ return client.isConfigured();
38
+ },
39
+ };
40
+
41
+ export default zaiProvider;
@@ -0,0 +1,23 @@
1
+ export interface UsageLimitItem {
2
+ type: "TIME_LIMIT" | "TOKENS_LIMIT";
3
+ usage?: number;
4
+ currentValue?: number;
5
+ remaining?: number;
6
+ percentage: number;
7
+ nextResetTime?: number;
8
+ unit?: number;
9
+ number?: number;
10
+ usageDetails?: Array<{
11
+ modelCode: string;
12
+ usage: number;
13
+ }>;
14
+ }
15
+
16
+ export interface ZaiUsageResponse {
17
+ code: number;
18
+ msg: string;
19
+ data: {
20
+ limits: UsageLimitItem[];
21
+ };
22
+ success: boolean;
23
+ }
@@ -0,0 +1,49 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+
5
+ const AUTH_CONFIG_PATH = path.join(
6
+ os.homedir(),
7
+ ".local",
8
+ "share",
9
+ "opencode",
10
+ "auth.json",
11
+ );
12
+
13
+ export interface AuthProvider {
14
+ type?: string;
15
+ access?: string;
16
+ refresh?: string;
17
+ expires?: number;
18
+ username?: string;
19
+ key?: string;
20
+ }
21
+
22
+ export interface AuthConfig {
23
+ "github-copilot"?: AuthProvider;
24
+ "anthropic"?: AuthProvider;
25
+ "zai-coding-plan"?: AuthProvider;
26
+ }
27
+
28
+ /**
29
+ * Reads the OpenCode authentication configuration file
30
+ * Returns null if file doesn't exist or error reading
31
+ */
32
+ export function readAuthConfig(): AuthConfig | null {
33
+ try {
34
+ if (!fs.existsSync(AUTH_CONFIG_PATH)) {
35
+ return null;
36
+ }
37
+ const content = fs.readFileSync(AUTH_CONFIG_PATH, "utf-8");
38
+ return JSON.parse(content) as AuthConfig;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Returns the authentication configuration file path
46
+ */
47
+ export function getAuthConfigPath(): string {
48
+ return AUTH_CONFIG_PATH;
49
+ }