@xtruder/opencode-claude-max-plugin 0.1.6 → 0.1.8
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/build/plugin.d.ts +14 -0
- package/build/plugin.d.ts.map +1 -0
- package/build/plugin.js +175 -0
- package/package.json +7 -2
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Plugin entry point for @xtruder/opencode-claude-max-plugin.
|
|
3
|
+
*
|
|
4
|
+
* Registers a /usage command that fetches Claude subscription usage directly
|
|
5
|
+
* from the Anthropic OAuth API — no bash, no markdown file required.
|
|
6
|
+
*
|
|
7
|
+
* Register in opencode.json:
|
|
8
|
+
* "plugin": ["@xtruder/opencode-claude-max-plugin/plugin"]
|
|
9
|
+
*/
|
|
10
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
11
|
+
declare const UsagePlugin: Plugin;
|
|
12
|
+
export default UsagePlugin;
|
|
13
|
+
export { UsagePlugin };
|
|
14
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AA+CjD,QAAA,MAAM,WAAW,EAAE,MAgClB,CAAA;AAED,eAAe,WAAW,CAAA;AAC1B,OAAO,EAAE,WAAW,EAAE,CAAA"}
|
package/build/plugin.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// src/credentials.ts
|
|
2
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
function getCredentialsPath() {
|
|
6
|
+
return join(homedir(), ".claude", ".credentials.json");
|
|
7
|
+
}
|
|
8
|
+
function readClaudeCredentials(path) {
|
|
9
|
+
const credPath = path ?? getCredentialsPath();
|
|
10
|
+
if (!existsSync(credPath))
|
|
11
|
+
return null;
|
|
12
|
+
try {
|
|
13
|
+
const raw = readFileSync(credPath, "utf-8");
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
if (!parsed.claudeAiOauth?.accessToken)
|
|
16
|
+
return null;
|
|
17
|
+
return parsed.claudeAiOauth;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function isExpired(creds) {
|
|
23
|
+
const buffer = 5 * 60 * 1000;
|
|
24
|
+
return Date.now() > creds.expiresAt - buffer;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/usage.ts
|
|
28
|
+
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
29
|
+
async function fetchUsage(credentialsPath) {
|
|
30
|
+
const creds = readClaudeCredentials(credentialsPath);
|
|
31
|
+
if (!creds)
|
|
32
|
+
return null;
|
|
33
|
+
if (isExpired(creds))
|
|
34
|
+
return null;
|
|
35
|
+
const resp = await fetch(USAGE_URL, {
|
|
36
|
+
headers: {
|
|
37
|
+
authorization: `Bearer ${creds.accessToken}`,
|
|
38
|
+
"anthropic-beta": "claude-code-20250219,oauth-2025-04-20",
|
|
39
|
+
"content-type": "application/json",
|
|
40
|
+
"user-agent": "claude-cli/2.1.81 (external, sdk-cli)",
|
|
41
|
+
"x-app": "cli"
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
if (!resp.ok)
|
|
45
|
+
return null;
|
|
46
|
+
return resp.json();
|
|
47
|
+
}
|
|
48
|
+
function progressBar(utilization, width = 50) {
|
|
49
|
+
const filled = Math.round(utilization / 100 * width);
|
|
50
|
+
const bar = "█".repeat(filled) + " ".repeat(width - filled);
|
|
51
|
+
return bar;
|
|
52
|
+
}
|
|
53
|
+
function formatReset(resetsAt) {
|
|
54
|
+
if (!resetsAt)
|
|
55
|
+
return "";
|
|
56
|
+
const reset = new Date(resetsAt);
|
|
57
|
+
const now = new Date;
|
|
58
|
+
const diffMs = reset.getTime() - now.getTime();
|
|
59
|
+
if (diffMs <= 0)
|
|
60
|
+
return "Reset now";
|
|
61
|
+
const diffMin = Math.floor(diffMs / 60000);
|
|
62
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
63
|
+
const timeStr = reset.toLocaleTimeString("en-US", {
|
|
64
|
+
hour: "numeric",
|
|
65
|
+
minute: "2-digit",
|
|
66
|
+
timeZoneName: "short"
|
|
67
|
+
});
|
|
68
|
+
const dateStr = reset.toLocaleDateString("en-US", {
|
|
69
|
+
month: "short",
|
|
70
|
+
day: "numeric"
|
|
71
|
+
});
|
|
72
|
+
if (diffHour < 24) {
|
|
73
|
+
return `Resets ${timeStr}`;
|
|
74
|
+
}
|
|
75
|
+
return `Resets ${dateStr}, ${timeStr}`;
|
|
76
|
+
}
|
|
77
|
+
function formatUsage(data) {
|
|
78
|
+
const lines = [];
|
|
79
|
+
if (data.five_hour) {
|
|
80
|
+
const pct = Math.round(data.five_hour.utilization);
|
|
81
|
+
lines.push(" Current session");
|
|
82
|
+
lines.push(` ${progressBar(pct)} ${pct}% used`);
|
|
83
|
+
lines.push(` ${formatReset(data.five_hour.resets_at)}`);
|
|
84
|
+
lines.push("");
|
|
85
|
+
}
|
|
86
|
+
if (data.seven_day) {
|
|
87
|
+
const pct = Math.round(data.seven_day.utilization);
|
|
88
|
+
lines.push(" Current week (all models)");
|
|
89
|
+
lines.push(` ${progressBar(pct)} ${pct}% used`);
|
|
90
|
+
lines.push(` ${formatReset(data.seven_day.resets_at)}`);
|
|
91
|
+
lines.push("");
|
|
92
|
+
}
|
|
93
|
+
if (data.seven_day_sonnet) {
|
|
94
|
+
const pct = Math.round(data.seven_day_sonnet.utilization);
|
|
95
|
+
lines.push(" Current week (Sonnet only)");
|
|
96
|
+
lines.push(` ${progressBar(pct)} ${pct}% used`);
|
|
97
|
+
if (data.seven_day_sonnet.resets_at) {
|
|
98
|
+
lines.push(` ${formatReset(data.seven_day_sonnet.resets_at)}`);
|
|
99
|
+
}
|
|
100
|
+
lines.push("");
|
|
101
|
+
}
|
|
102
|
+
if (data.seven_day_opus) {
|
|
103
|
+
const pct = Math.round(data.seven_day_opus.utilization);
|
|
104
|
+
lines.push(" Current week (Opus only)");
|
|
105
|
+
lines.push(` ${progressBar(pct)} ${pct}% used`);
|
|
106
|
+
if (data.seven_day_opus.resets_at) {
|
|
107
|
+
lines.push(` ${formatReset(data.seven_day_opus.resets_at)}`);
|
|
108
|
+
}
|
|
109
|
+
lines.push("");
|
|
110
|
+
}
|
|
111
|
+
if (data.extra_usage) {
|
|
112
|
+
if (data.extra_usage.is_enabled) {
|
|
113
|
+
const pct = Math.round(data.extra_usage.utilization ?? 0);
|
|
114
|
+
lines.push(" Extra usage");
|
|
115
|
+
lines.push(` ${progressBar(pct)} ${pct}% used`);
|
|
116
|
+
if (data.extra_usage.monthly_limit != null) {
|
|
117
|
+
lines.push(` $${data.extra_usage.used_credits?.toFixed(2) ?? "0.00"} / $${data.extra_usage.monthly_limit.toFixed(2)}`);
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
lines.push(" Extra usage: disabled");
|
|
121
|
+
}
|
|
122
|
+
lines.push("");
|
|
123
|
+
}
|
|
124
|
+
return lines.join(`
|
|
125
|
+
`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/plugin.ts
|
|
129
|
+
var COMMAND_NAME = "usage";
|
|
130
|
+
async function buildUsageText(credentialsPath) {
|
|
131
|
+
const data = await fetchUsage(credentialsPath);
|
|
132
|
+
if (!data) {
|
|
133
|
+
return `Display this message verbatim to the user:
|
|
134
|
+
|
|
135
|
+
` + " Could not fetch Claude usage. " + "Make sure you are logged into Claude Code (~/.claude/.credentials.json).";
|
|
136
|
+
}
|
|
137
|
+
return "Display the following Claude subscription usage verbatim to the user. " + `Do not add any commentary, formatting changes, or explanation:
|
|
138
|
+
|
|
139
|
+
` + ` Claude Subscription Usage
|
|
140
|
+
` + " " + "─".repeat(52) + `
|
|
141
|
+
` + formatUsage(data);
|
|
142
|
+
}
|
|
143
|
+
function replaceTextInParts(parts, text) {
|
|
144
|
+
const idx = parts.findIndex((p) => p.type === "text");
|
|
145
|
+
if (idx >= 0) {
|
|
146
|
+
parts[idx] = { ...parts[idx], text };
|
|
147
|
+
} else if (parts.length > 0) {
|
|
148
|
+
parts[0] = { ...parts[0], text };
|
|
149
|
+
} else {
|
|
150
|
+
parts.push({ type: "text", text });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
var UsagePlugin = async (_ctx) => {
|
|
154
|
+
return {
|
|
155
|
+
"command.execute.before": async (input, output) => {
|
|
156
|
+
if (input.command.toLowerCase() !== COMMAND_NAME)
|
|
157
|
+
return;
|
|
158
|
+
const text = await buildUsageText();
|
|
159
|
+
replaceTextInParts(output.parts, text);
|
|
160
|
+
},
|
|
161
|
+
"chat.message": async (_input, output) => {
|
|
162
|
+
const parts = output.parts;
|
|
163
|
+
const idx = parts.findIndex((p) => p.type === "text" && typeof p.text === "string" && p.text.trim().toLowerCase().startsWith(`/${COMMAND_NAME}`));
|
|
164
|
+
if (idx < 0)
|
|
165
|
+
return;
|
|
166
|
+
const text = await buildUsageText();
|
|
167
|
+
parts[idx] = { ...parts[idx], text };
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
var plugin_default = UsagePlugin;
|
|
172
|
+
export {
|
|
173
|
+
plugin_default as default,
|
|
174
|
+
UsagePlugin
|
|
175
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xtruder/opencode-claude-max-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "OpenCode plugin for Claude Max/Pro subscription via @anthropic-ai/sdk - uses Claude Code OAuth credentials",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
"./agent": {
|
|
14
14
|
"import": "./build/agent-index.js",
|
|
15
15
|
"types": "./build/agent-index.d.ts"
|
|
16
|
+
},
|
|
17
|
+
"./plugin": {
|
|
18
|
+
"import": "./build/plugin.js",
|
|
19
|
+
"types": "./build/plugin.d.ts"
|
|
16
20
|
}
|
|
17
21
|
},
|
|
18
22
|
"bin": {
|
|
@@ -22,7 +26,7 @@
|
|
|
22
26
|
"build"
|
|
23
27
|
],
|
|
24
28
|
"scripts": {
|
|
25
|
-
"build": "bun build ./src/index.ts ./src/agent-index.ts ./src/usage-cli.ts --outdir ./build --target node --format esm --packages external && bun x tsc --emitDeclarationOnly --declaration --outDir build",
|
|
29
|
+
"build": "bun build ./src/index.ts ./src/agent-index.ts ./src/usage-cli.ts ./src/plugin.ts --outdir ./build --target node --format esm --packages external && bun x tsc --emitDeclarationOnly --declaration --outDir build",
|
|
26
30
|
"test": "bun run src/test.ts"
|
|
27
31
|
},
|
|
28
32
|
"dependencies": {
|
|
@@ -35,6 +39,7 @@
|
|
|
35
39
|
"devDependencies": {
|
|
36
40
|
"@ai-sdk/provider": "2.0.1",
|
|
37
41
|
"@ai-sdk/provider-utils": "3.0.2",
|
|
42
|
+
"@opencode-ai/plugin": "^1.3.0",
|
|
38
43
|
"ai": "5.0.124",
|
|
39
44
|
"bun-types": "^1.3.11",
|
|
40
45
|
"typescript": "^5.7.0"
|