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.
Files changed (58) hide show
  1. package/bin/openusage +91 -0
  2. package/package.json +33 -0
  3. package/plugins/amp/icon.svg +6 -0
  4. package/plugins/amp/plugin.js +175 -0
  5. package/plugins/amp/plugin.json +20 -0
  6. package/plugins/amp/plugin.test.js +365 -0
  7. package/plugins/antigravity/icon.svg +3 -0
  8. package/plugins/antigravity/plugin.js +484 -0
  9. package/plugins/antigravity/plugin.json +17 -0
  10. package/plugins/antigravity/plugin.test.js +1356 -0
  11. package/plugins/claude/icon.svg +3 -0
  12. package/plugins/claude/plugin.js +565 -0
  13. package/plugins/claude/plugin.json +28 -0
  14. package/plugins/claude/plugin.test.js +1012 -0
  15. package/plugins/codex/icon.svg +3 -0
  16. package/plugins/codex/plugin.js +673 -0
  17. package/plugins/codex/plugin.json +30 -0
  18. package/plugins/codex/plugin.test.js +1071 -0
  19. package/plugins/copilot/icon.svg +3 -0
  20. package/plugins/copilot/plugin.js +264 -0
  21. package/plugins/copilot/plugin.json +20 -0
  22. package/plugins/copilot/plugin.test.js +529 -0
  23. package/plugins/cursor/icon.svg +3 -0
  24. package/plugins/cursor/plugin.js +526 -0
  25. package/plugins/cursor/plugin.json +24 -0
  26. package/plugins/cursor/plugin.test.js +1168 -0
  27. package/plugins/factory/icon.svg +1 -0
  28. package/plugins/factory/plugin.js +407 -0
  29. package/plugins/factory/plugin.json +19 -0
  30. package/plugins/factory/plugin.test.js +833 -0
  31. package/plugins/gemini/icon.svg +4 -0
  32. package/plugins/gemini/plugin.js +413 -0
  33. package/plugins/gemini/plugin.json +20 -0
  34. package/plugins/gemini/plugin.test.js +735 -0
  35. package/plugins/jetbrains-ai-assistant/icon.svg +3 -0
  36. package/plugins/jetbrains-ai-assistant/plugin.js +357 -0
  37. package/plugins/jetbrains-ai-assistant/plugin.json +17 -0
  38. package/plugins/jetbrains-ai-assistant/plugin.test.js +338 -0
  39. package/plugins/kimi/icon.svg +3 -0
  40. package/plugins/kimi/plugin.js +358 -0
  41. package/plugins/kimi/plugin.json +19 -0
  42. package/plugins/kimi/plugin.test.js +619 -0
  43. package/plugins/minimax/icon.svg +4 -0
  44. package/plugins/minimax/plugin.js +388 -0
  45. package/plugins/minimax/plugin.json +17 -0
  46. package/plugins/minimax/plugin.test.js +943 -0
  47. package/plugins/perplexity/icon.svg +1 -0
  48. package/plugins/perplexity/plugin.js +378 -0
  49. package/plugins/perplexity/plugin.json +15 -0
  50. package/plugins/perplexity/plugin.test.js +602 -0
  51. package/plugins/windsurf/icon.svg +3 -0
  52. package/plugins/windsurf/plugin.js +218 -0
  53. package/plugins/windsurf/plugin.json +16 -0
  54. package/plugins/windsurf/plugin.test.js +455 -0
  55. package/plugins/zai/icon.svg +5 -0
  56. package/plugins/zai/plugin.js +156 -0
  57. package/plugins/zai/plugin.json +18 -0
  58. package/plugins/zai/plugin.test.js +396 -0
@@ -0,0 +1,338 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest"
2
+ import { makeCtx } from "../test-helpers.js"
3
+
4
+ const DARWIN_PATH = "~/Library/Application Support/JetBrains/WebStorm2025.3/options/AIAssistantQuotaManager2.xml"
5
+ const LINUX_PATH = "~/.config/JetBrains/IntelliJIdea2025.3/options/AIAssistantQuotaManager2.xml"
6
+
7
+ const loadPlugin = async () => {
8
+ await import("./plugin.js")
9
+ return globalThis.__openusage_plugin
10
+ }
11
+
12
+ function encodeXmlValue(value) {
13
+ return JSON.stringify(value, null, 4)
14
+ .replace(/&/g, "&")
15
+ .replace(/"/g, """)
16
+ .replace(/\n/g, "
")
17
+ }
18
+
19
+ function makeQuotaXml({ quotaInfo, nextRefill }) {
20
+ return [
21
+ "<application>",
22
+ ' <component name="AIAssistantQuotaManager2">',
23
+ ` <option name="nextRefill" value="${encodeXmlValue(nextRefill)}" />`,
24
+ ` <option name="quotaInfo" value="${encodeXmlValue(quotaInfo)}" />`,
25
+ " </component>",
26
+ "</application>",
27
+ ].join("\n")
28
+ }
29
+
30
+ describe("jetbrains-ai-assistant plugin", () => {
31
+ beforeEach(() => {
32
+ delete globalThis.__openusage_plugin
33
+ vi.resetModules()
34
+ })
35
+
36
+ it("parses quota xml and emits quota + remaining lines", async () => {
37
+ const ctx = makeCtx()
38
+ ctx.host.fs.writeText(
39
+ DARWIN_PATH,
40
+ makeQuotaXml({
41
+ quotaInfo: {
42
+ type: "Available",
43
+ current: "75",
44
+ maximum: "100",
45
+ available: "25",
46
+ until: "2099-01-31T00:00:00Z",
47
+ },
48
+ nextRefill: {
49
+ type: "Known",
50
+ next: "2099-01-01T00:00:00Z",
51
+ tariff: { amount: "100", duration: "PT720H" },
52
+ },
53
+ })
54
+ )
55
+
56
+ const plugin = await loadPlugin()
57
+ const result = plugin.probe(ctx)
58
+
59
+ const quota = result.lines.find((line) => line.label === "Quota")
60
+ const used = result.lines.find((line) => line.label === "Used")
61
+ const remaining = result.lines.find((line) => line.label === "Remaining")
62
+
63
+ expect(quota && quota.used).toBe(75)
64
+ expect(quota && quota.limit).toBe(100)
65
+ expect(quota && quota.resetsAt).toBe("2099-01-01T00:00:00.000Z")
66
+ expect(quota && quota.periodDurationMs).toBe(2592000000)
67
+ expect(used && used.value).toBe("75")
68
+ expect(remaining && remaining.value).toBe("25")
69
+ })
70
+
71
+ it("falls back to quota until when nextRefill is missing", async () => {
72
+ const ctx = makeCtx()
73
+ ctx.host.fs.writeText(
74
+ DARWIN_PATH,
75
+ makeQuotaXml({
76
+ quotaInfo: {
77
+ type: "Available",
78
+ current: "50",
79
+ maximum: "100",
80
+ available: "50",
81
+ until: "2099-01-31T00:00:00Z",
82
+ },
83
+ nextRefill: null,
84
+ })
85
+ )
86
+
87
+ const plugin = await loadPlugin()
88
+ const result = plugin.probe(ctx)
89
+ const quota = result.lines.find((line) => line.label === "Quota")
90
+ expect(quota && quota.resetsAt).toBe("2099-01-31T00:00:00.000Z")
91
+ })
92
+
93
+ it("prefers the quota state with the latest until window", async () => {
94
+ const ctx = makeCtx()
95
+ ctx.app.platform = "macos"
96
+
97
+ ctx.host.fs.writeText(
98
+ DARWIN_PATH,
99
+ makeQuotaXml({
100
+ quotaInfo: {
101
+ type: "Available",
102
+ current: "10",
103
+ maximum: "100",
104
+ available: "90",
105
+ until: "2099-01-01T00:00:00Z",
106
+ },
107
+ nextRefill: { type: "Known", next: "2099-01-01T00:00:00Z", tariff: { amount: "0", duration: "PT720H" } },
108
+ })
109
+ )
110
+
111
+ // Unknown platforms probe all base paths. Write second valid file in Linux path to verify latest-until selection.
112
+ ctx.app.platform = "unknown"
113
+ ctx.host.fs.writeText(
114
+ LINUX_PATH,
115
+ makeQuotaXml({
116
+ quotaInfo: {
117
+ type: "Available",
118
+ current: "20",
119
+ maximum: "100",
120
+ available: "80",
121
+ until: "2099-02-01T00:00:00Z",
122
+ },
123
+ nextRefill: { type: "Known", next: "2099-02-01T00:00:00Z", tariff: { amount: "0", duration: "PT720H" } },
124
+ })
125
+ )
126
+
127
+ const plugin = await loadPlugin()
128
+ const result = plugin.probe(ctx)
129
+ const quota = result.lines.find((line) => line.label === "Quota")
130
+ expect(quota && quota.used).toBe(20)
131
+ })
132
+
133
+ it("computes remaining when available is missing", async () => {
134
+ const ctx = makeCtx()
135
+ ctx.host.fs.writeText(
136
+ DARWIN_PATH,
137
+ makeQuotaXml({
138
+ quotaInfo: {
139
+ type: "Available",
140
+ current: "60",
141
+ maximum: "100",
142
+ until: "2099-01-31T00:00:00Z",
143
+ },
144
+ nextRefill: { type: "Known", next: "2099-01-01T00:00:00Z", tariff: { amount: "0", duration: "P30D" } },
145
+ })
146
+ )
147
+
148
+ const plugin = await loadPlugin()
149
+ const result = plugin.probe(ctx)
150
+ const remaining = result.lines.find((line) => line.label === "Remaining")
151
+ expect(remaining && remaining.value).toBe("40")
152
+ })
153
+
154
+ it("prefers explicit tariff/topUp available over maximum-current math", async () => {
155
+ const ctx = makeCtx()
156
+ ctx.host.fs.writeText(
157
+ DARWIN_PATH,
158
+ makeQuotaXml({
159
+ quotaInfo: {
160
+ type: "Available",
161
+ current: "90",
162
+ maximum: "100",
163
+ // Intentionally inconsistent to verify explicit available is preferred.
164
+ tariffQuota: { current: "90", maximum: "100", available: "12.5" },
165
+ topUpQuota: { current: "0", maximum: "0", available: "3.5" },
166
+ until: "2099-01-31T00:00:00Z",
167
+ },
168
+ nextRefill: { type: "Known", next: "2099-01-01T00:00:00Z", tariff: { amount: "0", duration: "P30D" } },
169
+ })
170
+ )
171
+
172
+ const plugin = await loadPlugin()
173
+ const result = plugin.probe(ctx)
174
+ const remaining = result.lines.find((line) => line.label === "Remaining")
175
+ expect(remaining && remaining.value).toBe("16")
176
+ })
177
+
178
+ it("breaks equal-until ties by higher used ratio", async () => {
179
+ const ctx = makeCtx()
180
+ ctx.app.platform = "unknown"
181
+
182
+ ctx.host.fs.writeText(
183
+ DARWIN_PATH,
184
+ makeQuotaXml({
185
+ quotaInfo: {
186
+ type: "Available",
187
+ current: "80",
188
+ maximum: "100",
189
+ available: "20",
190
+ until: "2099-01-31T00:00:00Z",
191
+ },
192
+ nextRefill: { type: "Known", next: "2099-01-01T00:00:00Z", tariff: { amount: "0", duration: "PT720H" } },
193
+ })
194
+ )
195
+ ctx.host.fs.writeText(
196
+ LINUX_PATH,
197
+ makeQuotaXml({
198
+ quotaInfo: {
199
+ type: "Available",
200
+ current: "30",
201
+ maximum: "100",
202
+ available: "70",
203
+ until: "2099-01-31T00:00:00Z",
204
+ },
205
+ nextRefill: { type: "Known", next: "2099-01-01T00:00:00Z", tariff: { amount: "0", duration: "PT720H" } },
206
+ })
207
+ )
208
+
209
+ const plugin = await loadPlugin()
210
+ const result = plugin.probe(ctx)
211
+ const used = result.lines.find((line) => line.label === "Used")
212
+ expect(used && used.value).toBe("80")
213
+ })
214
+
215
+ it("converts JetBrains raw quota units to credits for display", async () => {
216
+ const ctx = makeCtx()
217
+ ctx.host.fs.writeText(
218
+ DARWIN_PATH,
219
+ makeQuotaXml({
220
+ quotaInfo: {
221
+ type: "Available",
222
+ current: "1981684.92",
223
+ maximum: "2367648.941",
224
+ tariffQuota: { current: "1981684.92", maximum: "2367648.941", available: "385964.21" },
225
+ topUpQuota: { current: "0", maximum: "0", available: "0" },
226
+ until: "2026-04-30T21:00:00Z",
227
+ },
228
+ nextRefill: {
229
+ type: "Known",
230
+ next: "2026-03-14T06:00:54.020Z",
231
+ tariff: { amount: "2000000", duration: "PT720H" },
232
+ },
233
+ })
234
+ )
235
+
236
+ const plugin = await loadPlugin()
237
+ const result = plugin.probe(ctx)
238
+ const used = result.lines.find((line) => line.label === "Used")
239
+ const remaining = result.lines.find((line) => line.label === "Remaining")
240
+
241
+ expect(used && used.value).toBe("19.82 / 23.68 credits")
242
+ expect(remaining && remaining.value).toBe("3.86 credits")
243
+ })
244
+
245
+ it("throws when no quota file is detected", async () => {
246
+ const ctx = makeCtx()
247
+ const plugin = await loadPlugin()
248
+ expect(() => plugin.probe(ctx)).toThrow("JetBrains AI Assistant not detected")
249
+ })
250
+
251
+ it("throws when quota payload is present but invalid", async () => {
252
+ const ctx = makeCtx()
253
+ ctx.host.fs.writeText(
254
+ DARWIN_PATH,
255
+ makeQuotaXml({
256
+ quotaInfo: {
257
+ type: "Available",
258
+ current: "0",
259
+ maximum: "0",
260
+ until: "2099-01-31T00:00:00Z",
261
+ },
262
+ nextRefill: { type: "Known", next: "2099-01-01T00:00:00Z", tariff: { amount: "0", duration: "PT720H" } },
263
+ })
264
+ )
265
+
266
+ const plugin = await loadPlugin()
267
+ expect(() => plugin.probe(ctx)).toThrow("quota data unavailable")
268
+ })
269
+
270
+ it("discovers quota on windows", async () => {
271
+ const ctx = makeCtx()
272
+ ctx.app.platform = "windows"
273
+ ctx.host.fs.writeText(
274
+ "~/AppData/Roaming/JetBrains/WebStorm2025.3/options/AIAssistantQuotaManager2.xml",
275
+ makeQuotaXml({
276
+ quotaInfo: { current: "50", maximum: "100", available: "50", until: "2099-01-31T00:00:00Z" },
277
+ nextRefill: { type: "Known", next: "2099-01-01T00:00:00Z", tariff: { amount: "100", duration: "PT720H" } },
278
+ })
279
+ )
280
+
281
+ const plugin = await loadPlugin()
282
+ const result = plugin.probe(ctx)
283
+ const quota = result.lines.find((line) => line.label === "Quota")
284
+ expect(quota && quota.used).toBe(50)
285
+ })
286
+
287
+ it("continues gracefully when listDir throws", async () => {
288
+ const ctx = makeCtx()
289
+ ctx.host.fs.writeText(DARWIN_PATH, makeQuotaXml({
290
+ quotaInfo: { current: "50", maximum: "100", available: "50", until: "2099-01-31T00:00:00Z" },
291
+ nextRefill: null,
292
+ }))
293
+ ctx.host.fs.listDir = () => { throw new Error("permission denied") }
294
+
295
+ const plugin = await loadPlugin()
296
+ expect(() => plugin.probe(ctx)).toThrow("JetBrains AI Assistant not detected")
297
+ })
298
+
299
+ it("throws when quota file exists but quotaInfo element is absent", async () => {
300
+ const ctx = makeCtx()
301
+ ctx.host.fs.writeText(
302
+ DARWIN_PATH,
303
+ '<application><component name="AIAssistantQuotaManager2"></component></application>'
304
+ )
305
+
306
+ const plugin = await loadPlugin()
307
+ expect(() => plugin.probe(ctx)).toThrow("quota data unavailable")
308
+ })
309
+
310
+ it("parses P30D and P4W durations", async () => {
311
+ const ctx = makeCtx()
312
+ ctx.host.fs.writeText(
313
+ DARWIN_PATH,
314
+ makeQuotaXml({
315
+ quotaInfo: { current: "50", maximum: "100", available: "50", until: "2099-01-31T00:00:00Z" },
316
+ nextRefill: { type: "Known", next: "2099-01-01T00:00:00Z", tariff: { amount: "100", duration: "P30D" } },
317
+ })
318
+ )
319
+ const plugin = await loadPlugin()
320
+ const result = plugin.probe(ctx)
321
+ const quota = result.lines.find((line) => line.label === "Quota")
322
+ expect(quota && quota.periodDurationMs).toBe(30 * 24 * 60 * 60 * 1000)
323
+
324
+ delete globalThis.__openusage_plugin
325
+ vi.resetModules()
326
+ ctx.host.fs.writeText(
327
+ DARWIN_PATH,
328
+ makeQuotaXml({
329
+ quotaInfo: { current: "50", maximum: "100", available: "50", until: "2099-01-31T00:00:00Z" },
330
+ nextRefill: { type: "Known", next: "2099-01-01T00:00:00Z", tariff: { amount: "100", duration: "P4W" } },
331
+ })
332
+ )
333
+ const plugin2 = await loadPlugin()
334
+ const result2 = plugin2.probe(ctx)
335
+ const quota2 = result2.lines.find((line) => line.label === "Quota")
336
+ expect(quota2 && quota2.periodDurationMs).toBe(4 * 7 * 24 * 60 * 60 * 1000)
337
+ })
338
+ })
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
2
+ <path d="M11.065 11.199l7.257-7.2c.137-.136.06-.41-.116-.41H14.3a.164.164 0 0 0-.117.051l-7.82 7.756c-.122.12-.302.013-.302-.179V3.82c0-.127-.083-.23-.185-.23H3.186c-.103 0-.186.103-.186.23V19.77c0 .128.083.23.186.23h2.69c.103 0 .186-.102.186-.23v-3.25c0-.069.025-.135.069-.178l2.424-2.406a.158.158 0 0 1 .205-.023l6.484 4.772a7.677 7.677 0 0 0 3.453 1.283c.108.012.2-.095.2-.23v-3.06c0-.117-.07-.212-.164-.227a5.028 5.028 0 0 1-2.027-.807l-5.613-4.064c-.117-.078-.132-.279-.028-.381z" />
3
+ </svg>
@@ -0,0 +1,358 @@
1
+ (function () {
2
+ const CRED_PATH = "~/.kimi/credentials/kimi-code.json"
3
+ const USAGE_URL = "https://api.kimi.com/coding/v1/usages"
4
+ const REFRESH_URL = "https://auth.kimi.com/api/oauth/token"
5
+ const CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098"
6
+ const REFRESH_BUFFER_SEC = 5 * 60
7
+
8
+ function readNumber(value) {
9
+ const n = Number(value)
10
+ return Number.isFinite(n) ? n : null
11
+ }
12
+
13
+ function titleCaseWords(value) {
14
+ return String(value)
15
+ .trim()
16
+ .toLowerCase()
17
+ .replace(/\b[a-z]/g, function (c) {
18
+ return c.toUpperCase()
19
+ })
20
+ }
21
+
22
+ function parsePlanLabel(data) {
23
+ const level =
24
+ data &&
25
+ data.user &&
26
+ data.user.membership &&
27
+ typeof data.user.membership.level === "string"
28
+ ? data.user.membership.level
29
+ : null
30
+ if (!level) return null
31
+
32
+ const cleaned = level.replace(/^LEVEL_/, "").replace(/_/g, " ")
33
+ const label = titleCaseWords(cleaned)
34
+ return label || null
35
+ }
36
+
37
+ function loadCredentials(ctx) {
38
+ if (!ctx.host.fs.exists(CRED_PATH)) {
39
+ ctx.host.log.warn("credentials file not found: " + CRED_PATH)
40
+ return null
41
+ }
42
+
43
+ try {
44
+ const text = ctx.host.fs.readText(CRED_PATH)
45
+ const parsed = ctx.util.tryParseJson(text)
46
+ if (!parsed || typeof parsed !== "object") {
47
+ ctx.host.log.warn("credentials file is not valid json")
48
+ return null
49
+ }
50
+ if (!parsed.access_token && !parsed.refresh_token) {
51
+ ctx.host.log.warn("credentials missing access_token and refresh_token")
52
+ return null
53
+ }
54
+ return parsed
55
+ } catch (e) {
56
+ ctx.host.log.warn("credentials read failed: " + String(e))
57
+ return null
58
+ }
59
+ }
60
+
61
+ function saveCredentials(ctx, creds) {
62
+ try {
63
+ ctx.host.fs.writeText(CRED_PATH, JSON.stringify(creds))
64
+ } catch (e) {
65
+ ctx.host.log.warn("failed to persist credentials: " + String(e))
66
+ }
67
+ }
68
+
69
+ function needsRefresh(creds, nowSec) {
70
+ if (!creds.access_token) return true
71
+ const expiresAt = readNumber(creds.expires_at)
72
+ if (expiresAt === null) return true
73
+ return nowSec + REFRESH_BUFFER_SEC >= expiresAt
74
+ }
75
+
76
+ function refreshToken(ctx, creds) {
77
+ if (!creds.refresh_token) {
78
+ ctx.host.log.warn("refresh skipped: no refresh token")
79
+ return null
80
+ }
81
+
82
+ ctx.host.log.info("attempting token refresh")
83
+ let resp
84
+ try {
85
+ resp = ctx.util.request({
86
+ method: "POST",
87
+ url: REFRESH_URL,
88
+ headers: {
89
+ "Content-Type": "application/x-www-form-urlencoded",
90
+ Accept: "application/json",
91
+ },
92
+ bodyText:
93
+ "client_id=" +
94
+ encodeURIComponent(CLIENT_ID) +
95
+ "&grant_type=refresh_token" +
96
+ "&refresh_token=" +
97
+ encodeURIComponent(creds.refresh_token),
98
+ timeoutMs: 15000,
99
+ })
100
+ } catch (e) {
101
+ ctx.host.log.error("refresh exception: " + String(e))
102
+ return null
103
+ }
104
+
105
+ if (ctx.util.isAuthStatus(resp.status)) {
106
+ throw "Session expired. Run `kimi login` to authenticate."
107
+ }
108
+
109
+ if (resp.status < 200 || resp.status >= 300) {
110
+ ctx.host.log.warn("refresh returned unexpected status: " + resp.status)
111
+ return null
112
+ }
113
+
114
+ const body = ctx.util.tryParseJson(resp.bodyText)
115
+ if (!body || !body.access_token) {
116
+ ctx.host.log.warn("refresh response missing access_token")
117
+ return null
118
+ }
119
+
120
+ creds.access_token = body.access_token
121
+ if (body.refresh_token) creds.refresh_token = body.refresh_token
122
+ if (typeof body.expires_in === "number") {
123
+ creds.expires_at = Date.now() / 1000 + body.expires_in
124
+ }
125
+ if (typeof body.scope === "string") creds.scope = body.scope
126
+ if (typeof body.token_type === "string") creds.token_type = body.token_type
127
+
128
+ saveCredentials(ctx, creds)
129
+ return creds.access_token
130
+ }
131
+
132
+ function fetchUsage(ctx, accessToken) {
133
+ return ctx.util.request({
134
+ method: "GET",
135
+ url: USAGE_URL,
136
+ headers: {
137
+ Authorization: "Bearer " + accessToken,
138
+ Accept: "application/json",
139
+ "User-Agent": "OpenUsage",
140
+ },
141
+ timeoutMs: 10000,
142
+ })
143
+ }
144
+
145
+ function parseWindowPeriodMs(window) {
146
+ if (!window || typeof window !== "object") return null
147
+ const duration = readNumber(window.duration)
148
+ if (duration === null || duration <= 0) return null
149
+
150
+ const unit = String(window.timeUnit || window.time_unit || "").toUpperCase()
151
+ if (unit.indexOf("MINUTE") !== -1) return duration * 60 * 1000
152
+ if (unit.indexOf("HOUR") !== -1) return duration * 60 * 60 * 1000
153
+ if (unit.indexOf("DAY") !== -1) return duration * 24 * 60 * 60 * 1000
154
+ if (unit.indexOf("SECOND") !== -1) return duration * 1000
155
+ return null
156
+ }
157
+
158
+ function parseQuota(row, ctx) {
159
+ if (!row || typeof row !== "object") return null
160
+
161
+ const limit = readNumber(row.limit)
162
+ if (limit === null || limit <= 0) return null
163
+
164
+ let used = readNumber(row.used)
165
+ if (used === null) {
166
+ const remaining = readNumber(row.remaining)
167
+ if (remaining !== null) {
168
+ used = limit - remaining
169
+ }
170
+ }
171
+ if (used === null) return null
172
+
173
+ return {
174
+ used,
175
+ limit,
176
+ resetsAt: ctx.util.toIso(row.resetTime || row.reset_at || row.resetAt || row.reset_time),
177
+ }
178
+ }
179
+
180
+ function toPercentUsage(quota) {
181
+ if (!quota || quota.limit <= 0) return null
182
+ const usedPercent = (quota.used / quota.limit) * 100
183
+ if (!Number.isFinite(usedPercent)) return null
184
+ return {
185
+ used: Math.round(Math.max(0, usedPercent) * 10) / 10,
186
+ limit: 100,
187
+ resetsAt: quota.resetsAt,
188
+ }
189
+ }
190
+
191
+ function collectLimitCandidates(ctx, data) {
192
+ const limits = Array.isArray(data && data.limits) ? data.limits : []
193
+ const out = []
194
+
195
+ for (let i = 0; i < limits.length; i += 1) {
196
+ const item = limits[i]
197
+ const detail = item && typeof item.detail === "object" ? item.detail : item
198
+ const quota = parseQuota(detail, ctx)
199
+ if (!quota) continue
200
+
201
+ const periodMs = parseWindowPeriodMs(item && item.window)
202
+ out.push({ quota, periodMs })
203
+ }
204
+
205
+ return out
206
+ }
207
+
208
+ function pickSessionCandidate(candidates) {
209
+ if (!candidates.length) return null
210
+ const sorted = candidates.slice().sort(function (a, b) {
211
+ const aKnown = typeof a.periodMs === "number"
212
+ const bKnown = typeof b.periodMs === "number"
213
+ if (aKnown && bKnown) return a.periodMs - b.periodMs
214
+ if (aKnown) return -1
215
+ if (bKnown) return 1
216
+ return 0
217
+ })
218
+ return sorted[0]
219
+ }
220
+
221
+ function pickLargestByPeriod(candidates) {
222
+ if (!candidates.length) return null
223
+ let best = candidates[0]
224
+ for (let i = 1; i < candidates.length; i += 1) {
225
+ const cur = candidates[i]
226
+ const curMs = typeof cur.periodMs === "number" ? cur.periodMs : -1
227
+ const bestMs = typeof best.periodMs === "number" ? best.periodMs : -1
228
+ if (curMs > bestMs) best = cur
229
+ }
230
+ return best
231
+ }
232
+
233
+ function sameQuota(a, b) {
234
+ if (!a || !b) return false
235
+ return (
236
+ a.quota.used === b.quota.used &&
237
+ a.quota.limit === b.quota.limit &&
238
+ (a.quota.resetsAt || null) === (b.quota.resetsAt || null)
239
+ )
240
+ }
241
+
242
+ function probe(ctx) {
243
+ const creds = loadCredentials(ctx)
244
+ if (!creds) {
245
+ throw "Not logged in. Run `kimi login` to authenticate."
246
+ }
247
+
248
+ const nowSec = Date.now() / 1000
249
+ let accessToken = creds.access_token || ""
250
+
251
+ if (needsRefresh(creds, nowSec)) {
252
+ const refreshed = refreshToken(ctx, creds)
253
+ if (refreshed) {
254
+ accessToken = refreshed
255
+ } else if (!accessToken) {
256
+ throw "Not logged in. Run `kimi login` to authenticate."
257
+ }
258
+ }
259
+
260
+ let didRefresh = false
261
+ let resp
262
+ try {
263
+ resp = ctx.util.retryOnceOnAuth({
264
+ request: function (token) {
265
+ return fetchUsage(ctx, token || accessToken)
266
+ },
267
+ refresh: function () {
268
+ didRefresh = true
269
+ const refreshed = refreshToken(ctx, creds)
270
+ if (refreshed) accessToken = refreshed
271
+ return refreshed
272
+ },
273
+ })
274
+ } catch (e) {
275
+ if (typeof e === "string") throw e
276
+ if (didRefresh) {
277
+ throw "Usage request failed after refresh. Try again."
278
+ }
279
+ throw "Usage request failed. Check your connection."
280
+ }
281
+
282
+ if (ctx.util.isAuthStatus(resp.status)) {
283
+ throw "Token expired. Run `kimi login` to authenticate."
284
+ }
285
+ if (resp.status < 200 || resp.status >= 300) {
286
+ throw "Usage request failed (HTTP " + String(resp.status) + "). Try again later."
287
+ }
288
+
289
+ const data = ctx.util.tryParseJson(resp.bodyText)
290
+ if (!data || typeof data !== "object") {
291
+ throw "Usage response invalid. Try again later."
292
+ }
293
+
294
+ const lines = []
295
+ const candidates = collectLimitCandidates(ctx, data)
296
+ const sessionCandidate = pickSessionCandidate(candidates)
297
+
298
+ let weeklyCandidate = null
299
+ const usageQuota = parseQuota(data.usage, ctx)
300
+ if (usageQuota) {
301
+ weeklyCandidate = { quota: usageQuota, periodMs: null }
302
+ } else {
303
+ const withoutSession = candidates.filter(function (candidate) {
304
+ return candidate !== sessionCandidate
305
+ })
306
+ weeklyCandidate = pickLargestByPeriod(withoutSession)
307
+ }
308
+
309
+ if (sessionCandidate) {
310
+ const sessionPercent = toPercentUsage(sessionCandidate.quota)
311
+ if (sessionPercent) {
312
+ lines.push(
313
+ ctx.line.progress({
314
+ label: "Session",
315
+ used: sessionPercent.used,
316
+ limit: sessionPercent.limit,
317
+ format: { kind: "percent" },
318
+ resetsAt: sessionPercent.resetsAt || undefined,
319
+ periodDurationMs:
320
+ typeof sessionCandidate.periodMs === "number"
321
+ ? sessionCandidate.periodMs
322
+ : undefined,
323
+ })
324
+ )
325
+ }
326
+ }
327
+
328
+ if (weeklyCandidate && !sameQuota(weeklyCandidate, sessionCandidate)) {
329
+ const weeklyPercent = toPercentUsage(weeklyCandidate.quota)
330
+ if (weeklyPercent) {
331
+ lines.push(
332
+ ctx.line.progress({
333
+ label: "Weekly",
334
+ used: weeklyPercent.used,
335
+ limit: weeklyPercent.limit,
336
+ format: { kind: "percent" },
337
+ resetsAt: weeklyPercent.resetsAt || undefined,
338
+ periodDurationMs:
339
+ typeof weeklyCandidate.periodMs === "number"
340
+ ? weeklyCandidate.periodMs
341
+ : undefined,
342
+ })
343
+ )
344
+ }
345
+ }
346
+
347
+ if (lines.length === 0) {
348
+ lines.push(ctx.line.badge({ label: "Status", text: "No usage data", color: "#a3a3a3" }))
349
+ }
350
+
351
+ return {
352
+ plan: parsePlanLabel(data),
353
+ lines,
354
+ }
355
+ }
356
+
357
+ globalThis.__openusage_plugin = { id: "kimi", probe }
358
+ })()