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,388 @@
1
+ (function () {
2
+ const GLOBAL_PRIMARY_USAGE_URL = "https://api.minimax.io/v1/api/openplatform/coding_plan/remains"
3
+ const GLOBAL_FALLBACK_USAGE_URLS = [
4
+ "https://api.minimax.io/v1/coding_plan/remains",
5
+ "https://www.minimax.io/v1/api/openplatform/coding_plan/remains",
6
+ ]
7
+ const CN_PRIMARY_USAGE_URL = "https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains"
8
+ const CN_FALLBACK_USAGE_URLS = ["https://api.minimaxi.com/v1/coding_plan/remains"]
9
+ const GLOBAL_API_KEY_ENV_VARS = ["MINIMAX_API_KEY", "MINIMAX_API_TOKEN"]
10
+ const CN_API_KEY_ENV_VARS = ["MINIMAX_CN_API_KEY", "MINIMAX_API_KEY", "MINIMAX_API_TOKEN"]
11
+ const CODING_PLAN_WINDOW_MS = 5 * 60 * 60 * 1000
12
+ const CODING_PLAN_WINDOW_TOLERANCE_MS = 10 * 60 * 1000
13
+ // GLOBAL plan tiers (based on prompt limits)
14
+ const GLOBAL_PROMPT_LIMIT_TO_PLAN = {
15
+ 100: "Starter",
16
+ 300: "Plus",
17
+ 1000: "Max",
18
+ 2000: "Ultra",
19
+ }
20
+ // CN plan tiers (based on model call counts = prompts × 15)
21
+ // Starter: 40 prompts = 600, Plus: 100 prompts = 1500, Max: 300 prompts = 4500
22
+ const CN_PROMPT_LIMIT_TO_PLAN = {
23
+ 600: "Starter",
24
+ 1500: "Plus",
25
+ 4500: "Max",
26
+ }
27
+ const MODEL_CALLS_PER_PROMPT = 15
28
+
29
+ function readString(value) {
30
+ if (typeof value !== "string") return null
31
+ const trimmed = value.trim()
32
+ return trimmed ? trimmed : null
33
+ }
34
+
35
+ function readNumber(value) {
36
+ if (typeof value === "number") return Number.isFinite(value) ? value : null
37
+ if (typeof value !== "string") return null
38
+ const trimmed = value.trim()
39
+ if (!trimmed) return null
40
+ const n = Number(trimmed)
41
+ return Number.isFinite(n) ? n : null
42
+ }
43
+
44
+ function pickFirstString(values) {
45
+ for (let i = 0; i < values.length; i += 1) {
46
+ const value = readString(values[i])
47
+ if (value) return value
48
+ }
49
+ return null
50
+ }
51
+
52
+ function normalizePlanName(value) {
53
+ const raw = readString(value)
54
+ if (!raw) return null
55
+ const compact = raw.replace(/\s+/g, " ").trim()
56
+ const withoutPrefix = compact.replace(/^minimax\s+coding\s+plan\b[:\-]?\s*/i, "").trim()
57
+ if (withoutPrefix) return withoutPrefix
58
+ if (/coding\s+plan/i.test(compact)) return "Coding Plan"
59
+ return compact
60
+ }
61
+
62
+ function inferPlanNameFromLimit(totalCount, endpointSelection) {
63
+ const n = readNumber(totalCount)
64
+ if (n === null || n <= 0) return null
65
+
66
+ const normalized = Math.round(n)
67
+ if (endpointSelection === "CN") {
68
+ // CN totals are model-call counts; only exact known CN tiers should infer.
69
+ return CN_PROMPT_LIMIT_TO_PLAN[normalized] || null
70
+ }
71
+
72
+ if (GLOBAL_PROMPT_LIMIT_TO_PLAN[normalized]) return GLOBAL_PROMPT_LIMIT_TO_PLAN[normalized]
73
+
74
+ if (normalized % MODEL_CALLS_PER_PROMPT !== 0) return null
75
+ const inferredPromptLimit = normalized / MODEL_CALLS_PER_PROMPT
76
+ return GLOBAL_PROMPT_LIMIT_TO_PLAN[inferredPromptLimit] || null
77
+ }
78
+
79
+ function epochToMs(epoch) {
80
+ const n = readNumber(epoch)
81
+ if (n === null) return null
82
+ return Math.abs(n) < 1e10 ? n * 1000 : n
83
+ }
84
+
85
+ function inferRemainsMs(remainsRaw, endMs, nowMs) {
86
+ if (remainsRaw === null || remainsRaw <= 0) return null
87
+
88
+ const asSecondsMs = remainsRaw * 1000
89
+ const asMillisecondsMs = remainsRaw
90
+
91
+ // If end_time exists, infer remains_time unit by whichever aligns best.
92
+ if (endMs !== null) {
93
+ const toEndMs = endMs - nowMs
94
+ if (toEndMs > 0) {
95
+ const secDelta = Math.abs(asSecondsMs - toEndMs)
96
+ const msDelta = Math.abs(asMillisecondsMs - toEndMs)
97
+ return secDelta <= msDelta ? asSecondsMs : asMillisecondsMs
98
+ }
99
+ }
100
+
101
+ // Coding Plan resets every 5h. Use that constraint before defaulting.
102
+ const maxExpectedMs = CODING_PLAN_WINDOW_MS + CODING_PLAN_WINDOW_TOLERANCE_MS
103
+ const secondsLooksValid = asSecondsMs <= maxExpectedMs
104
+ const millisecondsLooksValid = asMillisecondsMs <= maxExpectedMs
105
+
106
+ if (secondsLooksValid && !millisecondsLooksValid) return asSecondsMs
107
+ if (millisecondsLooksValid && !secondsLooksValid) return asMillisecondsMs
108
+ if (secondsLooksValid && millisecondsLooksValid) return asSecondsMs
109
+
110
+ const secOverflow = Math.abs(asSecondsMs - maxExpectedMs)
111
+ const msOverflow = Math.abs(asMillisecondsMs - maxExpectedMs)
112
+ return secOverflow <= msOverflow ? asSecondsMs : asMillisecondsMs
113
+ }
114
+
115
+ function loadApiKey(ctx, endpointSelection) {
116
+ const envVars = endpointSelection === "CN" ? CN_API_KEY_ENV_VARS : GLOBAL_API_KEY_ENV_VARS
117
+ for (let i = 0; i < envVars.length; i += 1) {
118
+ const name = envVars[i]
119
+ let value = null
120
+ try {
121
+ value = ctx.host.env.get(name)
122
+ } catch (e) {
123
+ ctx.host.log.warn("env read failed for " + name + ": " + String(e))
124
+ }
125
+ const key = readString(value)
126
+ if (key) {
127
+ ctx.host.log.info("api key loaded from " + name)
128
+ return { value: key, source: name }
129
+ }
130
+ }
131
+ return null
132
+ }
133
+
134
+ function getUsageUrls(endpointSelection) {
135
+ if (endpointSelection === "CN") {
136
+ return [CN_PRIMARY_USAGE_URL].concat(CN_FALLBACK_USAGE_URLS)
137
+ }
138
+ return [GLOBAL_PRIMARY_USAGE_URL].concat(GLOBAL_FALLBACK_USAGE_URLS)
139
+ }
140
+
141
+ function endpointAttempts(ctx) {
142
+ // AUTO: if CN key exists, try CN first; otherwise try GLOBAL first.
143
+ let cnApiKeyValue = null
144
+ try {
145
+ cnApiKeyValue = ctx.host.env.get("MINIMAX_CN_API_KEY")
146
+ } catch (e) {
147
+ ctx.host.log.warn("env read failed for MINIMAX_CN_API_KEY: " + String(e))
148
+ }
149
+ if (readString(cnApiKeyValue)) return ["CN", "GLOBAL"]
150
+ return ["GLOBAL", "CN"]
151
+ }
152
+
153
+ function formatAuthError() {
154
+ return "Session expired. Check your MiniMax API key."
155
+ }
156
+
157
+ /**
158
+ * Tries multiple URL candidates and returns the first successful response.
159
+ * @returns {object} parsed JSON response
160
+ * @throws {string} error message
161
+ */
162
+ function tryUrls(ctx, urls, apiKey) {
163
+ let lastStatus = null
164
+ let hadNetworkError = false
165
+ let authStatusCount = 0
166
+
167
+ for (let i = 0; i < urls.length; i += 1) {
168
+ const url = urls[i]
169
+ let resp
170
+ try {
171
+ resp = ctx.util.request({
172
+ method: "GET",
173
+ url: url,
174
+ headers: {
175
+ Authorization: "Bearer " + apiKey,
176
+ "Content-Type": "application/json",
177
+ Accept: "application/json",
178
+ },
179
+ timeoutMs: 15000,
180
+ })
181
+ } catch (e) {
182
+ hadNetworkError = true
183
+ ctx.host.log.warn("request failed (" + url + "): " + String(e))
184
+ continue
185
+ }
186
+
187
+ if (ctx.util.isAuthStatus(resp.status)) {
188
+ authStatusCount += 1
189
+ ctx.host.log.warn("request returned auth status " + resp.status + " (" + url + ")")
190
+ continue
191
+ }
192
+ if (resp.status < 200 || resp.status >= 300) {
193
+ lastStatus = resp.status
194
+ ctx.host.log.warn("request returned status " + resp.status + " (" + url + ")")
195
+ continue
196
+ }
197
+
198
+ const parsed = ctx.util.tryParseJson(resp.bodyText)
199
+ if (!parsed || typeof parsed !== "object") {
200
+ ctx.host.log.warn("request returned invalid JSON (" + url + ")")
201
+ continue
202
+ }
203
+
204
+ return parsed
205
+ }
206
+
207
+ if (authStatusCount > 0 && lastStatus === null && !hadNetworkError) {
208
+ throw formatAuthError()
209
+ }
210
+ if (lastStatus !== null) throw "Request failed (HTTP " + lastStatus + "). Try again later."
211
+ if (hadNetworkError) throw "Request failed. Check your connection."
212
+ throw "Could not parse usage data."
213
+ }
214
+
215
+ function parsePayloadShape(ctx, payload, endpointSelection) {
216
+ if (!payload || typeof payload !== "object") return null
217
+
218
+ const data = payload.data && typeof payload.data === "object" ? payload.data : payload
219
+ const baseResp = (data && data.base_resp) || payload.base_resp || null
220
+ const statusCode = readNumber(baseResp && baseResp.status_code)
221
+ const statusMessage = readString(baseResp && baseResp.status_msg)
222
+
223
+ if (statusCode !== null && statusCode !== 0) {
224
+ const normalized = (statusMessage || "").toLowerCase()
225
+ if (
226
+ statusCode === 1004 ||
227
+ normalized.includes("cookie") ||
228
+ normalized.includes("log in") ||
229
+ normalized.includes("login")
230
+ ) {
231
+ throw formatAuthError()
232
+ }
233
+ throw statusMessage
234
+ ? "MiniMax API error: " + statusMessage
235
+ : "MiniMax API error (status " + statusCode + ")."
236
+ }
237
+
238
+ const modelRemains =
239
+ (Array.isArray(data.model_remains) && data.model_remains) ||
240
+ (Array.isArray(payload.model_remains) && payload.model_remains) ||
241
+ (Array.isArray(data.modelRemains) && data.modelRemains) ||
242
+ (Array.isArray(payload.modelRemains) && payload.modelRemains) ||
243
+ null
244
+
245
+ if (!modelRemains || modelRemains.length === 0) return null
246
+
247
+ let chosen = modelRemains[0]
248
+ for (let i = 0; i < modelRemains.length; i += 1) {
249
+ const item = modelRemains[i]
250
+ if (!item || typeof item !== "object") continue
251
+ const total = readNumber(item.current_interval_total_count ?? item.currentIntervalTotalCount)
252
+ if (total !== null && total > 0) {
253
+ chosen = item
254
+ break
255
+ }
256
+ }
257
+
258
+ if (!chosen || typeof chosen !== "object") return null
259
+
260
+ const total = readNumber(chosen.current_interval_total_count ?? chosen.currentIntervalTotalCount)
261
+ if (total === null || total <= 0) return null
262
+
263
+ const usageFieldCount = readNumber(chosen.current_interval_usage_count ?? chosen.currentIntervalUsageCount)
264
+ const remainingCount = readNumber(
265
+ chosen.current_interval_remaining_count ??
266
+ chosen.currentIntervalRemainingCount ??
267
+ chosen.current_interval_remains_count ??
268
+ chosen.currentIntervalRemainsCount ??
269
+ chosen.current_interval_remain_count ??
270
+ chosen.currentIntervalRemainCount ??
271
+ chosen.remaining_count ??
272
+ chosen.remainingCount ??
273
+ chosen.remains_count ??
274
+ chosen.remainsCount ??
275
+ chosen.remaining ??
276
+ chosen.remains ??
277
+ chosen.left_count ??
278
+ chosen.leftCount
279
+ )
280
+ // MiniMax "coding_plan/remains" commonly returns remaining prompts in current_interval_usage_count.
281
+ const inferredRemainingCount = remainingCount !== null ? remainingCount : usageFieldCount
282
+ const explicitUsed = readNumber(
283
+ chosen.current_interval_used_count ??
284
+ chosen.currentIntervalUsedCount ??
285
+ chosen.used_count ??
286
+ chosen.used
287
+ )
288
+ let used = explicitUsed
289
+
290
+ if (used === null && inferredRemainingCount !== null) used = total - inferredRemainingCount
291
+ if (used === null) return null
292
+ if (used < 0) used = 0
293
+ if (used > total) used = total
294
+
295
+ const startMs = epochToMs(chosen.start_time ?? chosen.startTime)
296
+ const endMs = epochToMs(chosen.end_time ?? chosen.endTime)
297
+ const remainsRaw = readNumber(chosen.remains_time ?? chosen.remainsTime)
298
+ const nowMs = Date.now()
299
+ const remainsMs = inferRemainsMs(remainsRaw, endMs, nowMs)
300
+
301
+ let resetsAt = endMs !== null ? ctx.util.toIso(endMs) : null
302
+ if (!resetsAt && remainsMs !== null) {
303
+ resetsAt = ctx.util.toIso(nowMs + remainsMs)
304
+ }
305
+
306
+ let periodDurationMs = null
307
+ if (startMs !== null && endMs !== null && endMs > startMs) {
308
+ periodDurationMs = endMs - startMs
309
+ }
310
+
311
+ const explicitPlanName = normalizePlanName(pickFirstString([
312
+ data.current_subscribe_title,
313
+ data.plan_name,
314
+ data.plan,
315
+ data.current_plan_title,
316
+ data.combo_title,
317
+ payload.current_subscribe_title,
318
+ payload.plan_name,
319
+ payload.plan,
320
+ ]))
321
+ const inferredPlanName = inferPlanNameFromLimit(total, endpointSelection)
322
+ const planName = explicitPlanName || inferredPlanName
323
+
324
+ return {
325
+ planName,
326
+ used,
327
+ total,
328
+ resetsAt,
329
+ periodDurationMs,
330
+ }
331
+ }
332
+
333
+ function fetchUsagePayload(ctx, apiKey, endpointSelection) {
334
+ return tryUrls(ctx, getUsageUrls(endpointSelection), apiKey)
335
+ }
336
+
337
+ function probe(ctx) {
338
+ const attempts = endpointAttempts(ctx)
339
+ let lastError = null
340
+ let parsed = null
341
+ let successfulEndpoint = null
342
+
343
+ for (let i = 0; i < attempts.length; i += 1) {
344
+ const endpoint = attempts[i]
345
+ const apiKeyInfo = loadApiKey(ctx, endpoint)
346
+ if (!apiKeyInfo) continue
347
+ try {
348
+ const payload = fetchUsagePayload(ctx, apiKeyInfo.value, endpoint)
349
+ parsed = parsePayloadShape(ctx, payload, endpoint)
350
+ if (parsed) {
351
+ successfulEndpoint = endpoint
352
+ break
353
+ }
354
+ if (!lastError) lastError = "Could not parse usage data."
355
+ } catch (e) {
356
+ if (!lastError) lastError = String(e)
357
+ }
358
+ }
359
+
360
+ if (!parsed) {
361
+ if (lastError) throw lastError
362
+ throw "MiniMax API key missing. Set MINIMAX_API_KEY or MINIMAX_CN_API_KEY."
363
+ }
364
+
365
+ // CN API returns model call counts (needs division by 15 for prompts)
366
+ // GLOBAL API returns prompt counts directly
367
+ const isCnEndpoint = successfulEndpoint === "CN"
368
+ const displayMultiplier = isCnEndpoint ? 1 / MODEL_CALLS_PER_PROMPT : 1
369
+
370
+ const line = {
371
+ label: "Session",
372
+ used: Math.round(parsed.used * displayMultiplier),
373
+ limit: Math.round(parsed.total * displayMultiplier),
374
+ format: { kind: "count", suffix: "prompts" },
375
+ }
376
+ if (parsed.resetsAt) line.resetsAt = parsed.resetsAt
377
+ if (parsed.periodDurationMs !== null) line.periodDurationMs = parsed.periodDurationMs
378
+
379
+ const result = { lines: [ctx.line.progress(line)] }
380
+ if (parsed.planName) {
381
+ const regionLabel = successfulEndpoint === "CN" ? " (CN)" : " (GLOBAL)"
382
+ result.plan = parsed.planName + regionLabel
383
+ }
384
+ return result
385
+ }
386
+
387
+ globalThis.__openusage_plugin = { id: "minimax", probe }
388
+ })()
@@ -0,0 +1,17 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "minimax",
4
+ "name": "MiniMax",
5
+ "version": "0.0.1",
6
+ "entry": "plugin.js",
7
+ "icon": "icon.svg",
8
+ "brandColor": "#F5433C",
9
+ "cli": {
10
+ "category": "env",
11
+ "envVarNames": ["MINIMAX_API_KEY", "MINIMAX_CN_API_KEY"],
12
+ "envKeyLabel": "MiniMax API Key"
13
+ },
14
+ "lines": [
15
+ { "type": "progress", "label": "Session", "scope": "overview", "primaryOrder": 1 }
16
+ ]
17
+ }