@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.
- package/AGENTS.md +226 -0
- package/README.md +133 -0
- package/index.ts +31 -0
- package/lib/providers/base.ts +46 -0
- package/lib/providers/claude/client.ts +72 -0
- package/lib/providers/claude/formatter.ts +56 -0
- package/lib/providers/claude/index.ts +36 -0
- package/lib/providers/claude/types.ts +22 -0
- package/lib/providers/copilot/client.ts +185 -0
- package/lib/providers/copilot/formatter.ts +65 -0
- package/lib/providers/copilot/index.ts +36 -0
- package/lib/providers/copilot/types.ts +38 -0
- package/lib/providers/index.ts +8 -0
- package/lib/providers/registry.ts +56 -0
- package/lib/providers/zai/client.ts +97 -0
- package/lib/providers/zai/formatter.ts +77 -0
- package/lib/providers/zai/index.ts +41 -0
- package/lib/providers/zai/types.ts +23 -0
- package/lib/shared/auth.ts +49 -0
- package/lib/shared/formatting.ts +71 -0
- package/lib/shared/notification.ts +28 -0
- package/lib/shared/utils.ts +43 -0
- package/lib/usage-handler.ts +55 -0
- package/opencode.json +4 -0
- package/package.json +14 -0
- package/tsconfig.json +12 -0
|
@@ -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
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