openusage 0.1.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/bin/openusage +91 -0
- package/package.json +33 -0
- package/plugins/amp/icon.svg +6 -0
- package/plugins/amp/plugin.js +175 -0
- package/plugins/amp/plugin.json +20 -0
- package/plugins/amp/plugin.test.js +365 -0
- package/plugins/antigravity/icon.svg +3 -0
- package/plugins/antigravity/plugin.js +484 -0
- package/plugins/antigravity/plugin.json +17 -0
- package/plugins/antigravity/plugin.test.js +1356 -0
- package/plugins/claude/icon.svg +3 -0
- package/plugins/claude/plugin.js +565 -0
- package/plugins/claude/plugin.json +28 -0
- package/plugins/claude/plugin.test.js +1012 -0
- package/plugins/codex/icon.svg +3 -0
- package/plugins/codex/plugin.js +673 -0
- package/plugins/codex/plugin.json +30 -0
- package/plugins/codex/plugin.test.js +1071 -0
- package/plugins/copilot/icon.svg +3 -0
- package/plugins/copilot/plugin.js +264 -0
- package/plugins/copilot/plugin.json +20 -0
- package/plugins/copilot/plugin.test.js +529 -0
- package/plugins/cursor/icon.svg +3 -0
- package/plugins/cursor/plugin.js +526 -0
- package/plugins/cursor/plugin.json +24 -0
- package/plugins/cursor/plugin.test.js +1168 -0
- package/plugins/factory/icon.svg +1 -0
- package/plugins/factory/plugin.js +407 -0
- package/plugins/factory/plugin.json +19 -0
- package/plugins/factory/plugin.test.js +833 -0
- package/plugins/gemini/icon.svg +4 -0
- package/plugins/gemini/plugin.js +413 -0
- package/plugins/gemini/plugin.json +20 -0
- package/plugins/gemini/plugin.test.js +735 -0
- package/plugins/jetbrains-ai-assistant/icon.svg +3 -0
- package/plugins/jetbrains-ai-assistant/plugin.js +357 -0
- package/plugins/jetbrains-ai-assistant/plugin.json +17 -0
- package/plugins/jetbrains-ai-assistant/plugin.test.js +338 -0
- package/plugins/kimi/icon.svg +3 -0
- package/plugins/kimi/plugin.js +358 -0
- package/plugins/kimi/plugin.json +19 -0
- package/plugins/kimi/plugin.test.js +619 -0
- package/plugins/minimax/icon.svg +4 -0
- package/plugins/minimax/plugin.js +388 -0
- package/plugins/minimax/plugin.json +17 -0
- package/plugins/minimax/plugin.test.js +943 -0
- package/plugins/perplexity/icon.svg +1 -0
- package/plugins/perplexity/plugin.js +378 -0
- package/plugins/perplexity/plugin.json +15 -0
- package/plugins/perplexity/plugin.test.js +602 -0
- package/plugins/windsurf/icon.svg +3 -0
- package/plugins/windsurf/plugin.js +218 -0
- package/plugins/windsurf/plugin.json +16 -0
- package/plugins/windsurf/plugin.test.js +455 -0
- package/plugins/zai/icon.svg +5 -0
- package/plugins/zai/plugin.js +156 -0
- package/plugins/zai/plugin.json +18 -0
- package/plugins/zai/plugin.test.js +396 -0
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M25.7146 63.2153L41.4393 54.3917L41.7025 53.6226L41.4393 53.1976H40.6705L38.0394 53.0359L29.054 52.7929L21.2624 52.4691L13.7134 52.0644L11.8111 51.6594L10.0303 49.3118L10.2123 48.138L11.8111 47.0657L14.0981 47.2681L19.1574 47.6119L26.7467 48.138L32.2516 48.4618L40.4073 49.3118H41.7025L41.8846 48.7857L41.4393 48.4618L41.0955 48.138L33.243 42.8155L24.7432 37.1894L20.2909 33.9513L17.8824 32.3119L16.6684 30.774L16.1422 27.4147L18.328 25.0062L21.2624 25.2088L22.0112 25.4112L24.9861 27.6979L31.3407 32.616L39.6381 38.7273L40.8525 39.7391L41.3381 39.395L41.399 39.1523L40.8525 38.2415L36.3394 30.0858L31.5227 21.7883L29.3775 18.3478L28.811 16.2837C28.6087 15.4334 28.4669 14.7252 28.4669 13.8549L30.9563 10.4753L32.3321 10.0303L35.6515 10.4756L37.0479 11.6897L39.112 16.4052L42.4513 23.8327L47.6321 33.9313L49.15 36.9265L49.9594 39.6991L50.2632 40.5491H50.7894V40.0632L51.2141 34.3766L52.0035 27.3944L52.7726 18.4087L53.0358 15.8793L54.2905 12.8435L56.7795 11.2041L58.7224 12.135L60.3212 14.422L60.0986 15.899L59.1474 22.0718L57.2857 31.7458L56.0713 38.2218H56.7795L57.5892 37.4121L60.8677 33.061L66.3723 26.18L68.801 23.448L71.6342 20.4325L73.4556 18.9957H76.8962L79.4255 22.7601L78.2926 26.6456L74.7509 31.1384L71.8163 34.943L67.607 40.6097L64.9758 45.1431L65.2188 45.5072L65.8464 45.4466L75.358 43.4228L80.4984 42.4917L86.6304 41.4393L89.4033 42.7346L89.7065 44.0502L88.6135 46.7419L82.0566 48.3607L74.3662 49.8989L62.9118 52.6109L62.77 52.7121L62.9321 52.9144L68.0925 53.4L70.2987 53.5214H75.7021L85.7601 54.2702L88.3912 56.0108L89.9697 58.1358L89.7065 59.7545L85.6589 61.8189L80.1949 60.5236L67.4452 57.4881L63.0735 56.3952H62.4665V56.7596L66.1093 60.3213L72.7877 66.3523L81.1461 74.1236L81.5707 76.0462L80.4984 77.5638L79.3649 77.4021L72.0186 71.8772L69.1854 69.3879L62.77 63.9844H62.3453V64.5509L63.8223 66.7164L71.6342 78.4544L72.0389 82.0567L71.4725 83.2308L69.4487 83.939L67.2222 83.534L62.6485 77.1189L57.9333 69.8937L54.1284 63.4177L53.6631 63.6809L51.4167 87.8651L50.3644 89.0995L47.9356 90.0303L45.9121 88.4924L44.8392 86.0031L45.9118 81.0852L47.2071 74.6701L48.2594 69.5699L49.2106 63.2356L49.7773 61.131L49.7367 60.9892L49.2715 61.0498L44.4954 67.607L37.23 77.4224L31.4825 83.5746L30.1063 84.1211L27.7181 82.8864L27.9408 80.6805L29.2763 78.7177L37.2297 68.5988L42.026 62.3248L45.1227 58.7025L45.1024 58.176H44.9204L23.7917 71.8975L20.0274 72.3831L18.4083 70.8655L18.6106 68.3761L19.3798 67.5664L25.7343 63.195L25.7146 63.2153Z" fill="currentColor"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const CRED_FILE = "~/.claude/.credentials.json"
|
|
3
|
+
const KEYCHAIN_SERVICE = "Claude Code-credentials"
|
|
4
|
+
const USAGE_URL = "https://api.anthropic.com/api/oauth/usage"
|
|
5
|
+
const REFRESH_URL = "https://platform.claude.com/v1/oauth/token"
|
|
6
|
+
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
|
|
7
|
+
const SCOPES = "user:profile user:inference user:sessions:claude_code user:mcp_servers"
|
|
8
|
+
const REFRESH_BUFFER_MS = 5 * 60 * 1000 // refresh 5 minutes before expiration
|
|
9
|
+
|
|
10
|
+
function utf8DecodeBytes(bytes) {
|
|
11
|
+
// Prefer native TextDecoder when available (QuickJS may not expose it).
|
|
12
|
+
if (typeof TextDecoder !== "undefined") {
|
|
13
|
+
try {
|
|
14
|
+
return new TextDecoder("utf-8", { fatal: false }).decode(new Uint8Array(bytes))
|
|
15
|
+
} catch {}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Minimal UTF-8 decoder (replacement char on invalid sequences).
|
|
19
|
+
let out = ""
|
|
20
|
+
for (let i = 0; i < bytes.length; ) {
|
|
21
|
+
const b0 = bytes[i] & 0xff
|
|
22
|
+
if (b0 < 0x80) {
|
|
23
|
+
out += String.fromCharCode(b0)
|
|
24
|
+
i += 1
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2-byte
|
|
29
|
+
if (b0 >= 0xc2 && b0 <= 0xdf) {
|
|
30
|
+
if (i + 1 >= bytes.length) {
|
|
31
|
+
out += "\ufffd"
|
|
32
|
+
break
|
|
33
|
+
}
|
|
34
|
+
const b1 = bytes[i + 1] & 0xff
|
|
35
|
+
if ((b1 & 0xc0) !== 0x80) {
|
|
36
|
+
out += "\ufffd"
|
|
37
|
+
i += 1
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
const cp = ((b0 & 0x1f) << 6) | (b1 & 0x3f)
|
|
41
|
+
out += String.fromCharCode(cp)
|
|
42
|
+
i += 2
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 3-byte
|
|
47
|
+
if (b0 >= 0xe0 && b0 <= 0xef) {
|
|
48
|
+
if (i + 2 >= bytes.length) {
|
|
49
|
+
out += "\ufffd"
|
|
50
|
+
break
|
|
51
|
+
}
|
|
52
|
+
const b1 = bytes[i + 1] & 0xff
|
|
53
|
+
const b2 = bytes[i + 2] & 0xff
|
|
54
|
+
const validCont = (b1 & 0xc0) === 0x80 && (b2 & 0xc0) === 0x80
|
|
55
|
+
const notOverlong = !(b0 === 0xe0 && b1 < 0xa0)
|
|
56
|
+
const notSurrogate = !(b0 === 0xed && b1 >= 0xa0)
|
|
57
|
+
if (!validCont || !notOverlong || !notSurrogate) {
|
|
58
|
+
out += "\ufffd"
|
|
59
|
+
i += 1
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
const cp = ((b0 & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f)
|
|
63
|
+
out += String.fromCharCode(cp)
|
|
64
|
+
i += 3
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 4-byte
|
|
69
|
+
if (b0 >= 0xf0 && b0 <= 0xf4) {
|
|
70
|
+
if (i + 3 >= bytes.length) {
|
|
71
|
+
out += "\ufffd"
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
const b1 = bytes[i + 1] & 0xff
|
|
75
|
+
const b2 = bytes[i + 2] & 0xff
|
|
76
|
+
const b3 = bytes[i + 3] & 0xff
|
|
77
|
+
const validCont = (b1 & 0xc0) === 0x80 && (b2 & 0xc0) === 0x80 && (b3 & 0xc0) === 0x80
|
|
78
|
+
const notOverlong = !(b0 === 0xf0 && b1 < 0x90)
|
|
79
|
+
const notTooHigh = !(b0 === 0xf4 && b1 > 0x8f)
|
|
80
|
+
if (!validCont || !notOverlong || !notTooHigh) {
|
|
81
|
+
out += "\ufffd"
|
|
82
|
+
i += 1
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
const cp =
|
|
86
|
+
((b0 & 0x07) << 18) | ((b1 & 0x3f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f)
|
|
87
|
+
const n = cp - 0x10000
|
|
88
|
+
out += String.fromCharCode(0xd800 + ((n >> 10) & 0x3ff), 0xdc00 + (n & 0x3ff))
|
|
89
|
+
i += 4
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
out += "\ufffd"
|
|
94
|
+
i += 1
|
|
95
|
+
}
|
|
96
|
+
return out
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function tryParseCredentialJSON(ctx, text) {
|
|
100
|
+
if (!text) return null
|
|
101
|
+
const parsed = ctx.util.tryParseJson(text)
|
|
102
|
+
if (parsed) return parsed
|
|
103
|
+
|
|
104
|
+
// Some macOS keychain items are returned by `security ... -w` as hex-encoded UTF-8 bytes.
|
|
105
|
+
// Example prefix: "7b0a" ( "{\\n" ).
|
|
106
|
+
// Support both plain hex and "0x..." forms.
|
|
107
|
+
let hex = String(text).trim()
|
|
108
|
+
if (hex.startsWith("0x") || hex.startsWith("0X")) hex = hex.slice(2)
|
|
109
|
+
if (!hex || hex.length % 2 !== 0) return null
|
|
110
|
+
if (!/^[0-9a-fA-F]+$/.test(hex)) return null
|
|
111
|
+
try {
|
|
112
|
+
const bytes = []
|
|
113
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
114
|
+
bytes.push(parseInt(hex.slice(i, i + 2), 16))
|
|
115
|
+
}
|
|
116
|
+
const decoded = utf8DecodeBytes(bytes)
|
|
117
|
+
const decodedParsed = ctx.util.tryParseJson(decoded)
|
|
118
|
+
if (decodedParsed) return decodedParsed
|
|
119
|
+
} catch {}
|
|
120
|
+
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function loadCredentials(ctx) {
|
|
125
|
+
// Try file first
|
|
126
|
+
if (ctx.host.fs.exists(CRED_FILE)) {
|
|
127
|
+
try {
|
|
128
|
+
const text = ctx.host.fs.readText(CRED_FILE)
|
|
129
|
+
const parsed = tryParseCredentialJSON(ctx, text)
|
|
130
|
+
if (parsed) {
|
|
131
|
+
const oauth = parsed.claudeAiOauth
|
|
132
|
+
if (oauth && oauth.accessToken) {
|
|
133
|
+
ctx.host.log.info("credentials loaded from file")
|
|
134
|
+
return { oauth, source: "file", fullData: parsed }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
ctx.host.log.warn("credentials file exists but no valid oauth data")
|
|
138
|
+
} catch (e) {
|
|
139
|
+
ctx.host.log.warn("credentials file read failed: " + String(e))
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Try keychain fallback
|
|
144
|
+
try {
|
|
145
|
+
const keychainValue = ctx.host.keychain.readGenericPassword(KEYCHAIN_SERVICE)
|
|
146
|
+
if (keychainValue) {
|
|
147
|
+
const parsed = tryParseCredentialJSON(ctx, keychainValue)
|
|
148
|
+
if (parsed) {
|
|
149
|
+
const oauth = parsed.claudeAiOauth
|
|
150
|
+
if (oauth && oauth.accessToken) {
|
|
151
|
+
ctx.host.log.info("credentials loaded from keychain")
|
|
152
|
+
return { oauth, source: "keychain", fullData: parsed }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
ctx.host.log.warn("keychain has data but no valid oauth")
|
|
156
|
+
}
|
|
157
|
+
} catch (e) {
|
|
158
|
+
ctx.host.log.info("keychain read failed (may not exist): " + String(e))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
ctx.host.log.warn("no credentials found")
|
|
162
|
+
return null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function saveCredentials(ctx, source, fullData) {
|
|
166
|
+
// MUST use minified JSON - macOS `security -w` hex-encodes values with newlines,
|
|
167
|
+
// which Claude Code can't read back, causing it to invalidate the session.
|
|
168
|
+
const text = JSON.stringify(fullData)
|
|
169
|
+
if (source === "file") {
|
|
170
|
+
try {
|
|
171
|
+
ctx.host.fs.writeText(CRED_FILE, text)
|
|
172
|
+
} catch (e) {
|
|
173
|
+
ctx.host.log.error("Failed to write Claude credentials file: " + String(e))
|
|
174
|
+
}
|
|
175
|
+
} else if (source === "keychain") {
|
|
176
|
+
try {
|
|
177
|
+
ctx.host.keychain.writeGenericPassword(KEYCHAIN_SERVICE, text)
|
|
178
|
+
} catch (e) {
|
|
179
|
+
ctx.host.log.error("Failed to write Claude credentials keychain: " + String(e))
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function needsRefresh(ctx, oauth, nowMs) {
|
|
185
|
+
return ctx.util.needsRefreshByExpiry({
|
|
186
|
+
nowMs,
|
|
187
|
+
expiresAtMs: oauth.expiresAt,
|
|
188
|
+
bufferMs: REFRESH_BUFFER_MS,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function refreshToken(ctx, creds) {
|
|
193
|
+
const { oauth, source, fullData } = creds
|
|
194
|
+
if (!oauth.refreshToken) {
|
|
195
|
+
ctx.host.log.warn("refresh skipped: no refresh token")
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
ctx.host.log.info("attempting token refresh")
|
|
200
|
+
try {
|
|
201
|
+
const resp = ctx.util.request({
|
|
202
|
+
method: "POST",
|
|
203
|
+
url: REFRESH_URL,
|
|
204
|
+
headers: { "Content-Type": "application/json" },
|
|
205
|
+
bodyText: JSON.stringify({
|
|
206
|
+
grant_type: "refresh_token",
|
|
207
|
+
refresh_token: oauth.refreshToken,
|
|
208
|
+
client_id: CLIENT_ID,
|
|
209
|
+
scope: SCOPES,
|
|
210
|
+
}),
|
|
211
|
+
timeoutMs: 15000,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
if (resp.status === 400 || resp.status === 401) {
|
|
215
|
+
let errorCode = null
|
|
216
|
+
const body = ctx.util.tryParseJson(resp.bodyText)
|
|
217
|
+
if (body) errorCode = body.error || body.error_description
|
|
218
|
+
ctx.host.log.error("refresh failed: status=" + resp.status + " error=" + String(errorCode))
|
|
219
|
+
if (errorCode === "invalid_grant") {
|
|
220
|
+
throw "Session expired. Run `claude` to log in again."
|
|
221
|
+
}
|
|
222
|
+
throw "Token expired. Run `claude` to log in again."
|
|
223
|
+
}
|
|
224
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
225
|
+
ctx.host.log.warn("refresh returned unexpected status: " + resp.status)
|
|
226
|
+
return null
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const body = ctx.util.tryParseJson(resp.bodyText)
|
|
230
|
+
if (!body) {
|
|
231
|
+
ctx.host.log.warn("refresh response not valid JSON")
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
234
|
+
const newAccessToken = body.access_token
|
|
235
|
+
if (!newAccessToken) {
|
|
236
|
+
ctx.host.log.warn("refresh response missing access_token")
|
|
237
|
+
return null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Update oauth credentials
|
|
241
|
+
oauth.accessToken = newAccessToken
|
|
242
|
+
if (body.refresh_token) oauth.refreshToken = body.refresh_token
|
|
243
|
+
if (typeof body.expires_in === "number") {
|
|
244
|
+
oauth.expiresAt = Date.now() + body.expires_in * 1000
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Persist updated credentials
|
|
248
|
+
fullData.claudeAiOauth = oauth
|
|
249
|
+
saveCredentials(ctx, source, fullData)
|
|
250
|
+
|
|
251
|
+
ctx.host.log.info("refresh succeeded, new token expires in " + (body.expires_in || "unknown") + "s")
|
|
252
|
+
return newAccessToken
|
|
253
|
+
} catch (e) {
|
|
254
|
+
if (typeof e === "string") throw e
|
|
255
|
+
ctx.host.log.error("refresh exception: " + String(e))
|
|
256
|
+
return null
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function fetchUsage(ctx, accessToken) {
|
|
261
|
+
return ctx.util.request({
|
|
262
|
+
method: "GET",
|
|
263
|
+
url: USAGE_URL,
|
|
264
|
+
headers: {
|
|
265
|
+
Authorization: "Bearer " + accessToken.trim(),
|
|
266
|
+
Accept: "application/json",
|
|
267
|
+
"Content-Type": "application/json",
|
|
268
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
269
|
+
"User-Agent": "OpenUsage",
|
|
270
|
+
},
|
|
271
|
+
timeoutMs: 10000,
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function queryTokenUsage(ctx) {
|
|
276
|
+
const since = new Date()
|
|
277
|
+
// Inclusive range: today + previous 30 days = 31 calendar days.
|
|
278
|
+
since.setDate(since.getDate() - 30)
|
|
279
|
+
const y = since.getFullYear()
|
|
280
|
+
const m = since.getMonth() + 1
|
|
281
|
+
const d = since.getDate()
|
|
282
|
+
const sinceStr = "" + y + (m < 10 ? "0" : "") + m + (d < 10 ? "0" : "") + d
|
|
283
|
+
|
|
284
|
+
const result = ctx.host.ccusage.query({ since: sinceStr })
|
|
285
|
+
if (!result || typeof result !== "object" || typeof result.status !== "string") {
|
|
286
|
+
return { status: "runner_failed", data: null }
|
|
287
|
+
}
|
|
288
|
+
if (result.status !== "ok") {
|
|
289
|
+
return { status: result.status, data: null }
|
|
290
|
+
}
|
|
291
|
+
if (!result.data || !Array.isArray(result.data.daily)) {
|
|
292
|
+
return { status: "runner_failed", data: null }
|
|
293
|
+
}
|
|
294
|
+
return { status: "ok", data: result.data }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function fmtTokens(n) {
|
|
298
|
+
const abs = Math.abs(n)
|
|
299
|
+
const sign = n < 0 ? "-" : ""
|
|
300
|
+
const units = [
|
|
301
|
+
{ threshold: 1e9, divisor: 1e9, suffix: "B" },
|
|
302
|
+
{ threshold: 1e6, divisor: 1e6, suffix: "M" },
|
|
303
|
+
{ threshold: 1e3, divisor: 1e3, suffix: "K" },
|
|
304
|
+
]
|
|
305
|
+
for (let i = 0; i < units.length; i++) {
|
|
306
|
+
const unit = units[i]
|
|
307
|
+
if (abs >= unit.threshold) {
|
|
308
|
+
const scaled = abs / unit.divisor
|
|
309
|
+
const formatted = scaled >= 10
|
|
310
|
+
? Math.round(scaled).toString()
|
|
311
|
+
: scaled.toFixed(1).replace(/\.0$/, "")
|
|
312
|
+
return sign + formatted + unit.suffix
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return sign + Math.round(abs).toString()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function dayKeyFromDate(date) {
|
|
319
|
+
const year = date.getFullYear()
|
|
320
|
+
const month = date.getMonth() + 1
|
|
321
|
+
const day = date.getDate()
|
|
322
|
+
return year + "-" + (month < 10 ? "0" : "") + month + "-" + (day < 10 ? "0" : "") + day
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function dayKeyFromUsageDate(rawDate) {
|
|
326
|
+
if (typeof rawDate !== "string") return null
|
|
327
|
+
const value = rawDate.trim()
|
|
328
|
+
if (!value) return null
|
|
329
|
+
|
|
330
|
+
const isoMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})$/)
|
|
331
|
+
if (isoMatch) {
|
|
332
|
+
return isoMatch[1] + "-" + isoMatch[2] + "-" + isoMatch[3]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const compactMatch = value.match(/^(\d{4})(\d{2})(\d{2})$/)
|
|
336
|
+
if (compactMatch) {
|
|
337
|
+
return compactMatch[1] + "-" + compactMatch[2] + "-" + compactMatch[3]
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const ms = Date.parse(value)
|
|
341
|
+
if (!Number.isFinite(ms)) return null
|
|
342
|
+
return dayKeyFromDate(new Date(ms))
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function usageCostUsd(day) {
|
|
346
|
+
if (!day || typeof day !== "object") return null
|
|
347
|
+
|
|
348
|
+
if (day.totalCost != null) {
|
|
349
|
+
const totalCost = Number(day.totalCost)
|
|
350
|
+
if (Number.isFinite(totalCost)) return totalCost
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (day.costUSD != null) {
|
|
354
|
+
const costUSD = Number(day.costUSD)
|
|
355
|
+
if (Number.isFinite(costUSD)) return costUSD
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return null
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function costAndTokensLabel(data, opts) {
|
|
362
|
+
const includeZeroTokens = !!(opts && opts.includeZeroTokens)
|
|
363
|
+
const parts = []
|
|
364
|
+
if (data.costUSD != null) parts.push("$" + data.costUSD.toFixed(2))
|
|
365
|
+
if (data.tokens > 0 || (includeZeroTokens && data.tokens === 0)) {
|
|
366
|
+
parts.push(fmtTokens(data.tokens) + " tokens")
|
|
367
|
+
}
|
|
368
|
+
return parts.join(" \u00b7 ")
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function pushDayUsageLine(lines, ctx, label, dayEntry) {
|
|
372
|
+
const tokens = Number(dayEntry && dayEntry.totalTokens) || 0
|
|
373
|
+
const cost = usageCostUsd(dayEntry)
|
|
374
|
+
if (tokens > 0) {
|
|
375
|
+
lines.push(ctx.line.text({
|
|
376
|
+
label: label,
|
|
377
|
+
value: costAndTokensLabel({ tokens: tokens, costUSD: cost })
|
|
378
|
+
}))
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
lines.push(ctx.line.text({
|
|
383
|
+
label: label,
|
|
384
|
+
value: costAndTokensLabel({ tokens: 0, costUSD: 0 }, { includeZeroTokens: true })
|
|
385
|
+
}))
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function probe(ctx) {
|
|
389
|
+
const creds = loadCredentials(ctx)
|
|
390
|
+
if (!creds || !creds.oauth || !creds.oauth.accessToken || !creds.oauth.accessToken.trim()) {
|
|
391
|
+
ctx.host.log.error("probe failed: not logged in")
|
|
392
|
+
throw "Not logged in. Run `claude` to authenticate."
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const nowMs = Date.now()
|
|
396
|
+
let accessToken = creds.oauth.accessToken
|
|
397
|
+
|
|
398
|
+
// Proactively refresh if token is expired or about to expire
|
|
399
|
+
if (needsRefresh(ctx, creds.oauth, nowMs)) {
|
|
400
|
+
ctx.host.log.info("token needs refresh (expired or expiring soon)")
|
|
401
|
+
const refreshed = refreshToken(ctx, creds)
|
|
402
|
+
if (refreshed) {
|
|
403
|
+
accessToken = refreshed
|
|
404
|
+
} else {
|
|
405
|
+
ctx.host.log.warn("proactive refresh failed, trying with existing token")
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let resp
|
|
410
|
+
let didRefresh = false
|
|
411
|
+
try {
|
|
412
|
+
resp = ctx.util.retryOnceOnAuth({
|
|
413
|
+
request: (token) => {
|
|
414
|
+
try {
|
|
415
|
+
return fetchUsage(ctx, token || accessToken)
|
|
416
|
+
} catch (e) {
|
|
417
|
+
ctx.host.log.error("usage request exception: " + String(e))
|
|
418
|
+
if (didRefresh) {
|
|
419
|
+
throw "Usage request failed after refresh. Try again."
|
|
420
|
+
}
|
|
421
|
+
throw "Usage request failed. Check your connection."
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
refresh: () => {
|
|
425
|
+
ctx.host.log.info("usage returned 401, attempting refresh")
|
|
426
|
+
didRefresh = true
|
|
427
|
+
return refreshToken(ctx, creds)
|
|
428
|
+
},
|
|
429
|
+
})
|
|
430
|
+
} catch (e) {
|
|
431
|
+
if (typeof e === "string") throw e
|
|
432
|
+
ctx.host.log.error("usage request failed: " + String(e))
|
|
433
|
+
throw "Usage request failed. Check your connection."
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (ctx.util.isAuthStatus(resp.status)) {
|
|
437
|
+
ctx.host.log.error("usage returned auth error after all retries: status=" + resp.status)
|
|
438
|
+
throw "Token expired. Run `claude` to log in again."
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
442
|
+
ctx.host.log.error("usage returned error: status=" + resp.status)
|
|
443
|
+
throw "Usage request failed (HTTP " + String(resp.status) + "). Try again later."
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
ctx.host.log.info("usage fetch succeeded")
|
|
447
|
+
|
|
448
|
+
let data
|
|
449
|
+
data = ctx.util.tryParseJson(resp.bodyText)
|
|
450
|
+
if (data === null) {
|
|
451
|
+
throw "Usage response invalid. Try again later."
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const lines = []
|
|
455
|
+
let plan = null
|
|
456
|
+
if (creds.oauth.subscriptionType) {
|
|
457
|
+
const planLabel = ctx.fmt.planLabel(creds.oauth.subscriptionType)
|
|
458
|
+
if (planLabel) {
|
|
459
|
+
plan = planLabel
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (data.five_hour && typeof data.five_hour.utilization === "number") {
|
|
464
|
+
lines.push(ctx.line.progress({
|
|
465
|
+
label: "Session",
|
|
466
|
+
used: data.five_hour.utilization,
|
|
467
|
+
limit: 100,
|
|
468
|
+
format: { kind: "percent" },
|
|
469
|
+
resetsAt: ctx.util.toIso(data.five_hour.resets_at),
|
|
470
|
+
periodDurationMs: 5 * 60 * 60 * 1000 // 5 hours
|
|
471
|
+
}))
|
|
472
|
+
}
|
|
473
|
+
if (data.seven_day && typeof data.seven_day.utilization === "number") {
|
|
474
|
+
lines.push(ctx.line.progress({
|
|
475
|
+
label: "Weekly",
|
|
476
|
+
used: data.seven_day.utilization,
|
|
477
|
+
limit: 100,
|
|
478
|
+
format: { kind: "percent" },
|
|
479
|
+
resetsAt: ctx.util.toIso(data.seven_day.resets_at),
|
|
480
|
+
periodDurationMs: 7 * 24 * 60 * 60 * 1000 // 7 days
|
|
481
|
+
}))
|
|
482
|
+
}
|
|
483
|
+
if (data.seven_day_sonnet && typeof data.seven_day_sonnet.utilization === "number") {
|
|
484
|
+
lines.push(ctx.line.progress({
|
|
485
|
+
label: "Sonnet",
|
|
486
|
+
used: data.seven_day_sonnet.utilization,
|
|
487
|
+
limit: 100,
|
|
488
|
+
format: { kind: "percent" },
|
|
489
|
+
resetsAt: ctx.util.toIso(data.seven_day_sonnet.resets_at),
|
|
490
|
+
periodDurationMs: 7 * 24 * 60 * 60 * 1000 // 7 days
|
|
491
|
+
}))
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (data.extra_usage && data.extra_usage.is_enabled) {
|
|
495
|
+
const used = data.extra_usage.used_credits
|
|
496
|
+
const limit = data.extra_usage.monthly_limit
|
|
497
|
+
if (typeof used === "number" && typeof limit === "number" && limit > 0) {
|
|
498
|
+
lines.push(ctx.line.progress({
|
|
499
|
+
label: "Extra usage spent",
|
|
500
|
+
used: ctx.fmt.dollars(used),
|
|
501
|
+
limit: ctx.fmt.dollars(limit),
|
|
502
|
+
format: { kind: "dollars" }
|
|
503
|
+
}))
|
|
504
|
+
} else if (typeof used === "number" && used > 0) {
|
|
505
|
+
lines.push(ctx.line.text({ label: "Extra usage spent", value: "$" + String(ctx.fmt.dollars(used)) }))
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const usageResult = queryTokenUsage(ctx)
|
|
510
|
+
if (usageResult.status === "ok") {
|
|
511
|
+
const usage = usageResult.data
|
|
512
|
+
const now = new Date()
|
|
513
|
+
const todayKey = dayKeyFromDate(now)
|
|
514
|
+
const yesterday = new Date(now.getTime())
|
|
515
|
+
yesterday.setDate(yesterday.getDate() - 1)
|
|
516
|
+
const yesterdayKey = dayKeyFromDate(yesterday)
|
|
517
|
+
|
|
518
|
+
let todayEntry = null
|
|
519
|
+
let yesterdayEntry = null
|
|
520
|
+
for (let i = 0; i < usage.daily.length; i++) {
|
|
521
|
+
const usageDayKey = dayKeyFromUsageDate(usage.daily[i].date)
|
|
522
|
+
if (usageDayKey === todayKey) {
|
|
523
|
+
todayEntry = usage.daily[i]
|
|
524
|
+
continue
|
|
525
|
+
}
|
|
526
|
+
if (usageDayKey === yesterdayKey) {
|
|
527
|
+
yesterdayEntry = usage.daily[i]
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
pushDayUsageLine(lines, ctx, "Today", todayEntry)
|
|
532
|
+
pushDayUsageLine(lines, ctx, "Yesterday", yesterdayEntry)
|
|
533
|
+
|
|
534
|
+
let totalTokens = 0
|
|
535
|
+
let totalCostNanos = 0
|
|
536
|
+
let hasCost = false
|
|
537
|
+
for (let i = 0; i < usage.daily.length; i++) {
|
|
538
|
+
const day = usage.daily[i]
|
|
539
|
+
const dayTokens = Number(day.totalTokens)
|
|
540
|
+
if (Number.isFinite(dayTokens)) {
|
|
541
|
+
totalTokens += dayTokens
|
|
542
|
+
}
|
|
543
|
+
const dayCost = usageCostUsd(day)
|
|
544
|
+
if (dayCost != null) {
|
|
545
|
+
totalCostNanos += Math.round(dayCost * 1e9)
|
|
546
|
+
hasCost = true
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (totalTokens > 0) {
|
|
550
|
+
lines.push(ctx.line.text({
|
|
551
|
+
label: "Last 30 Days",
|
|
552
|
+
value: costAndTokensLabel({ tokens: totalTokens, costUSD: hasCost ? totalCostNanos / 1e9 : null })
|
|
553
|
+
}))
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (lines.length === 0) {
|
|
558
|
+
lines.push(ctx.line.badge({ label: "Status", text: "No usage data", color: "#a3a3a3" }))
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return { plan: plan, lines: lines }
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
globalThis.__openusage_plugin = { id: "claude", probe }
|
|
565
|
+
})()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"id": "claude",
|
|
4
|
+
"name": "Claude",
|
|
5
|
+
"version": "0.0.1",
|
|
6
|
+
"entry": "plugin.js",
|
|
7
|
+
"icon": "icon.svg",
|
|
8
|
+
"brandColor": "#DE7356",
|
|
9
|
+
"cli": {
|
|
10
|
+
"category": "cli",
|
|
11
|
+
"binaryName": "claude",
|
|
12
|
+
"installCmd": "curl -fsSL https://claude.ai/install.sh | sh",
|
|
13
|
+
"loginCmd": "claude auth login"
|
|
14
|
+
},
|
|
15
|
+
"links": [
|
|
16
|
+
{ "label": "Status", "url": "https://status.anthropic.com/" },
|
|
17
|
+
{ "label": "Console", "url": "https://console.anthropic.com/" }
|
|
18
|
+
],
|
|
19
|
+
"lines": [
|
|
20
|
+
{ "type": "progress", "label": "Session", "scope": "overview", "primaryOrder": 1 },
|
|
21
|
+
{ "type": "progress", "label": "Weekly", "scope": "overview" },
|
|
22
|
+
{ "type": "progress", "label": "Sonnet", "scope": "detail" },
|
|
23
|
+
{ "type": "progress", "label": "Extra usage spent", "scope": "detail" },
|
|
24
|
+
{ "type": "text", "label": "Today", "scope": "detail" },
|
|
25
|
+
{ "type": "text", "label": "Yesterday", "scope": "detail" },
|
|
26
|
+
{ "type": "text", "label": "Last 30 Days", "scope": "detail" }
|
|
27
|
+
]
|
|
28
|
+
}
|