@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,71 @@
1
+ import { formatFriendlyDate } from "./utils";
2
+
3
+ /**
4
+ * Formats a countdown line for quota reset
5
+ * Accepts both ISO string and timestamp in milliseconds
6
+ */
7
+ export function formatResetCountdown(resetDate: string | number | undefined): string {
8
+ if (!resetDate) {
9
+ return "N/A";
10
+ }
11
+
12
+ const reset = new Date(resetDate);
13
+ const now = new Date();
14
+ const diffMs = reset.getTime() - now.getTime();
15
+
16
+ if (diffMs <= 0) {
17
+ return "Resets soon";
18
+ }
19
+
20
+ const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
21
+ const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
22
+
23
+ if (days > 0) {
24
+ return `${days}d ${hours}h`;
25
+ }
26
+ return `${hours}h`;
27
+ }
28
+
29
+ /**
30
+ * Formats a complete reset line with friendly date
31
+ */
32
+ export function formatResetLine(
33
+ label: string,
34
+ resetDate: string | number | undefined,
35
+ labelWidth: number = 15,
36
+ ): string {
37
+ if (!resetDate) {
38
+ return `${label.padEnd(labelWidth)} N/A`;
39
+ }
40
+
41
+ const countdown = formatResetCountdown(resetDate);
42
+ const friendlyDate = formatFriendlyDate(resetDate);
43
+
44
+ return `${label.padEnd(labelWidth)} ${countdown} (${friendlyDate})`;
45
+ }
46
+
47
+ /**
48
+ * Formats large numbers as tokens (1000 -> 1K, 1000000 -> 1M)
49
+ */
50
+ export function formatTokens(value: number): string {
51
+ if (value >= 1000000000) {
52
+ return `${(value / 1000000000).toFixed(1)}B`;
53
+ }
54
+ if (value >= 1000000) {
55
+ return `${(value / 1000000).toFixed(1)}M`;
56
+ }
57
+ if (value >= 1000) {
58
+ return `${(value / 1000).toFixed(0)}K`;
59
+ }
60
+ return value.toString();
61
+ }
62
+
63
+ /**
64
+ * Masks an API key showing only the first 8 characters
65
+ */
66
+ export function maskApiKey(apiKey: string): string {
67
+ if (apiKey.length <= 8) {
68
+ return apiKey;
69
+ }
70
+ return apiKey.substring(0, 8) + "............";
71
+ }
@@ -0,0 +1,28 @@
1
+ export async function sendIgnoredMessage(
2
+ client: any,
3
+ sessionID: string,
4
+ text: string,
5
+ params: any,
6
+ ): Promise<void> {
7
+ const agent = params.agent || undefined
8
+ const variant = params.variant || undefined
9
+ const model = params.providerId && params.modelId ? {
10
+ providerID: params.providerId,
11
+ modelID: params.modelId,
12
+ } : undefined
13
+
14
+ try {
15
+ await client.session.prompt({
16
+ path: { id: sessionID },
17
+ body: {
18
+ noReply: true,
19
+ agent,
20
+ model,
21
+ variant,
22
+ parts: [{ type: "text", text, ignored: true }],
23
+ },
24
+ })
25
+ } catch (error: any) {
26
+ console.error("Failed to send notification:", error.message)
27
+ }
28
+ }
@@ -0,0 +1,43 @@
1
+ export async function fetchWithTimeout(
2
+ url: string | URL | Request,
3
+ options: RequestInit & { timeout?: number } = {},
4
+ ): Promise<Response> {
5
+ const timeout = options.timeout ?? 10000;
6
+
7
+ const controller = new AbortController();
8
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
9
+
10
+ try {
11
+ const response = await fetch(url, {
12
+ ...options,
13
+ signal: controller.signal,
14
+ });
15
+ return response;
16
+ } finally {
17
+ clearTimeout(timeoutId);
18
+ }
19
+ }
20
+
21
+ export function createUsedProgressBar(percentUsed: number, width: number = 20): string {
22
+ const filled = Math.round((percentUsed / 100) * width);
23
+ const empty = width - filled;
24
+ return "[" + "#".repeat(filled) + " ".repeat(empty) + "]";
25
+ }
26
+
27
+ export function formatFriendlyDate(dateInput: string | number | Date): string {
28
+ const date = new Date(dateInput);
29
+
30
+ const year = date.getFullYear();
31
+ const month = String(date.getMonth() + 1).padStart(2, "0");
32
+ const day = String(date.getDate()).padStart(2, "0");
33
+ const hours = String(date.getHours()).padStart(2, "0");
34
+ const minutes = String(date.getMinutes()).padStart(2, "0");
35
+
36
+ const offsetMinutes = -date.getTimezoneOffset();
37
+ const offsetHours = Math.floor(Math.abs(offsetMinutes) / 60);
38
+ const offsetMins = Math.abs(offsetMinutes) % 60;
39
+ const offsetSign = offsetMinutes >= 0 ? "+" : "-";
40
+ const timezone = `UTC${offsetSign}${String(offsetHours).padStart(2, "0")}:${String(offsetMins).padStart(2, "0")}`;
41
+
42
+ return `${year}-${month}-${day} ${hours}:${minutes} ${timezone}`;
43
+ }
@@ -0,0 +1,55 @@
1
+ import { sendIgnoredMessage } from "./shared/notification";
2
+ import { registry } from "./providers/registry";
3
+
4
+ interface UsageContext {
5
+ client: any;
6
+ sessionID: string;
7
+ params: any;
8
+ }
9
+
10
+ /**
11
+ * Main handler for the /usage command
12
+ * Fetches data from all registered providers and displays to user
13
+ */
14
+ export async function handleUsageCommand(ctx: UsageContext): Promise<void> {
15
+ const { client, sessionID, params } = ctx;
16
+
17
+ // Gets all registered providers
18
+ const providers = registry.getAll();
19
+
20
+ // Fetches data from all providers in parallel
21
+ const results = await Promise.all(
22
+ providers.map(async (provider) => {
23
+ try {
24
+ const result = await provider.getUsageData();
25
+ if (result && result.content) {
26
+ return {
27
+ provider: provider.name,
28
+ content: result.content,
29
+ };
30
+ }
31
+ return null;
32
+ } catch (error) {
33
+ console.error(`[${provider.name} Error]`, error);
34
+ return null;
35
+ }
36
+ }),
37
+ );
38
+
39
+ // Filters only providers with valid data
40
+ const validResults = results.filter((r): r is { provider: string; content: string } => r !== null);
41
+
42
+ // If no provider returned data, shows error message
43
+ if (validResults.length === 0) {
44
+ const message =
45
+ "No usage service configured.\nConfigure GitHub Copilot, Claude Code or Z.ai in your auth file.";
46
+ await sendIgnoredMessage(client, sessionID, message, params);
47
+ return;
48
+ }
49
+
50
+ // Joins all provider messages
51
+ const message = validResults.map((r) => r.content).join("\n\n");
52
+
53
+ // Sends final message to user
54
+ await sendIgnoredMessage(client, sessionID, message, params);
55
+ }
package/opencode.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "plugin": ["./"]
4
+ }
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@toninho09/opencode-usage",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "index.ts",
6
+ "dependencies": {
7
+ "@opencode-ai/plugin": "latest",
8
+ "@types/node": "^25.2.2",
9
+ "openai": "^6.19.0"
10
+ },
11
+ "devDependencies": {
12
+ "typescript": "^5.9.3"
13
+ }
14
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "noEmit": true
10
+ },
11
+ "include": ["*.ts", "lib/**/*.ts"]
12
+ }