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,526 @@
1
+ (function () {
2
+ const STATE_DB =
3
+ "~/Library/Application Support/Cursor/User/globalStorage/state.vscdb"
4
+ const KEYCHAIN_ACCESS_TOKEN_SERVICE = "cursor-access-token"
5
+ const KEYCHAIN_REFRESH_TOKEN_SERVICE = "cursor-refresh-token"
6
+ const BASE_URL = "https://api2.cursor.sh"
7
+ const USAGE_URL = BASE_URL + "/aiserver.v1.DashboardService/GetCurrentPeriodUsage"
8
+ const PLAN_URL = BASE_URL + "/aiserver.v1.DashboardService/GetPlanInfo"
9
+ const REFRESH_URL = BASE_URL + "/oauth/token"
10
+ const CREDITS_URL = BASE_URL + "/aiserver.v1.DashboardService/GetCreditGrantsBalance"
11
+ const REST_USAGE_URL = "https://cursor.com/api/usage"
12
+ const CLIENT_ID = "KbZUR41cY7W6zRSdpSUJ7I7mLYBKOCmB"
13
+ const REFRESH_BUFFER_MS = 5 * 60 * 1000 // refresh 5 minutes before expiration
14
+ const LOGIN_HINT = "Sign in via Cursor app or run `agent login`."
15
+
16
+ function readStateValue(ctx, key) {
17
+ try {
18
+ const sql =
19
+ "SELECT value FROM ItemTable WHERE key = '" + key + "' LIMIT 1;"
20
+ const json = ctx.host.sqlite.query(STATE_DB, sql)
21
+ const rows = ctx.util.tryParseJson(json)
22
+ if (!Array.isArray(rows)) {
23
+ throw new Error("sqlite returned invalid json")
24
+ }
25
+ if (rows.length > 0 && rows[0].value) {
26
+ return rows[0].value
27
+ }
28
+ } catch (e) {
29
+ ctx.host.log.warn("sqlite read failed for " + key + ": " + String(e))
30
+ }
31
+ return null
32
+ }
33
+
34
+ function writeStateValue(ctx, key, value) {
35
+ try {
36
+ // Escape single quotes in value for SQL
37
+ const escaped = String(value).replace(/'/g, "''")
38
+ const sql =
39
+ "INSERT OR REPLACE INTO ItemTable (key, value) VALUES ('" +
40
+ key +
41
+ "', '" +
42
+ escaped +
43
+ "');"
44
+ ctx.host.sqlite.exec(STATE_DB, sql)
45
+ return true
46
+ } catch (e) {
47
+ ctx.host.log.warn("sqlite write failed for " + key + ": " + String(e))
48
+ return false
49
+ }
50
+ }
51
+
52
+ function readKeychainValue(ctx, service) {
53
+ if (!ctx.host.keychain || typeof ctx.host.keychain.readGenericPassword !== "function") {
54
+ return null
55
+ }
56
+ try {
57
+ const value = ctx.host.keychain.readGenericPassword(service)
58
+ if (typeof value !== "string") return null
59
+ const trimmed = value.trim()
60
+ return trimmed || null
61
+ } catch (e) {
62
+ ctx.host.log.info("keychain read failed for " + service + ": " + String(e))
63
+ return null
64
+ }
65
+ }
66
+
67
+ function writeKeychainValue(ctx, service, value) {
68
+ if (!ctx.host.keychain || typeof ctx.host.keychain.writeGenericPassword !== "function") {
69
+ ctx.host.log.warn("keychain write unsupported")
70
+ return false
71
+ }
72
+ try {
73
+ ctx.host.keychain.writeGenericPassword(service, String(value))
74
+ return true
75
+ } catch (e) {
76
+ ctx.host.log.warn("keychain write failed for " + service + ": " + String(e))
77
+ return false
78
+ }
79
+ }
80
+
81
+ function loadAuthState(ctx) {
82
+ const sqliteAccessToken = readStateValue(ctx, "cursorAuth/accessToken")
83
+ const sqliteRefreshToken = readStateValue(ctx, "cursorAuth/refreshToken")
84
+ if (sqliteAccessToken || sqliteRefreshToken) {
85
+ return {
86
+ accessToken: sqliteAccessToken,
87
+ refreshToken: sqliteRefreshToken,
88
+ source: "sqlite",
89
+ }
90
+ }
91
+
92
+ const keychainAccessToken = readKeychainValue(ctx, KEYCHAIN_ACCESS_TOKEN_SERVICE)
93
+ const keychainRefreshToken = readKeychainValue(ctx, KEYCHAIN_REFRESH_TOKEN_SERVICE)
94
+ if (keychainAccessToken || keychainRefreshToken) {
95
+ return {
96
+ accessToken: keychainAccessToken,
97
+ refreshToken: keychainRefreshToken,
98
+ source: "keychain",
99
+ }
100
+ }
101
+
102
+ return {
103
+ accessToken: null,
104
+ refreshToken: null,
105
+ source: null,
106
+ }
107
+ }
108
+
109
+ function persistAccessToken(ctx, source, accessToken) {
110
+ if (source === "keychain") {
111
+ return writeKeychainValue(ctx, KEYCHAIN_ACCESS_TOKEN_SERVICE, accessToken)
112
+ }
113
+ return writeStateValue(ctx, "cursorAuth/accessToken", accessToken)
114
+ }
115
+
116
+ function getTokenExpiration(ctx, token) {
117
+ const payload = ctx.jwt.decodePayload(token)
118
+ if (!payload || typeof payload.exp !== "number") return null
119
+ return payload.exp * 1000 // Convert to milliseconds
120
+ }
121
+
122
+ function needsRefresh(ctx, accessToken, nowMs) {
123
+ if (!accessToken) return true
124
+ const expiresAt = getTokenExpiration(ctx, accessToken)
125
+ return ctx.util.needsRefreshByExpiry({
126
+ nowMs,
127
+ expiresAtMs: expiresAt,
128
+ bufferMs: REFRESH_BUFFER_MS,
129
+ })
130
+ }
131
+
132
+ function refreshToken(ctx, refreshTokenValue, source) {
133
+ if (!refreshTokenValue) {
134
+ ctx.host.log.warn("refresh skipped: no refresh token")
135
+ return null
136
+ }
137
+
138
+ ctx.host.log.info("attempting token refresh")
139
+ try {
140
+ const resp = ctx.util.request({
141
+ method: "POST",
142
+ url: REFRESH_URL,
143
+ headers: { "Content-Type": "application/json" },
144
+ bodyText: JSON.stringify({
145
+ grant_type: "refresh_token",
146
+ client_id: CLIENT_ID,
147
+ refresh_token: refreshTokenValue,
148
+ }),
149
+ timeoutMs: 15000,
150
+ })
151
+
152
+ if (resp.status === 400 || resp.status === 401) {
153
+ let errorInfo = null
154
+ errorInfo = ctx.util.tryParseJson(resp.bodyText)
155
+ const shouldLogout = errorInfo && errorInfo.shouldLogout === true
156
+ ctx.host.log.error("refresh failed: status=" + resp.status + " shouldLogout=" + shouldLogout)
157
+ if (shouldLogout) {
158
+ throw "Session expired. " + LOGIN_HINT
159
+ }
160
+ throw "Token expired. " + LOGIN_HINT
161
+ }
162
+
163
+ if (resp.status < 200 || resp.status >= 300) {
164
+ ctx.host.log.warn("refresh returned unexpected status: " + resp.status)
165
+ return null
166
+ }
167
+
168
+ const body = ctx.util.tryParseJson(resp.bodyText)
169
+ if (!body) {
170
+ ctx.host.log.warn("refresh response not valid JSON")
171
+ return null
172
+ }
173
+
174
+ // Check if server wants us to logout
175
+ if (body.shouldLogout === true) {
176
+ ctx.host.log.error("refresh response indicates shouldLogout=true")
177
+ throw "Session expired. " + LOGIN_HINT
178
+ }
179
+
180
+ const newAccessToken = body.access_token
181
+ if (!newAccessToken) {
182
+ ctx.host.log.warn("refresh response missing access_token")
183
+ return null
184
+ }
185
+
186
+ // Persist updated access token to source where auth was loaded from.
187
+ persistAccessToken(ctx, source, newAccessToken)
188
+ ctx.host.log.info("refresh succeeded, token persisted")
189
+
190
+ // Note: Cursor refresh returns access_token which is used as both
191
+ // access and refresh token in some flows
192
+ return newAccessToken
193
+ } catch (e) {
194
+ if (typeof e === "string") throw e
195
+ ctx.host.log.error("refresh exception: " + String(e))
196
+ return null
197
+ }
198
+ }
199
+
200
+ function connectPost(ctx, url, token) {
201
+ return ctx.util.request({
202
+ method: "POST",
203
+ url: url,
204
+ headers: {
205
+ Authorization: "Bearer " + token,
206
+ "Content-Type": "application/json",
207
+ "Connect-Protocol-Version": "1",
208
+ },
209
+ bodyText: "{}",
210
+ timeoutMs: 10000,
211
+ })
212
+ }
213
+
214
+ function buildSessionToken(ctx, accessToken) {
215
+ var payload = ctx.jwt.decodePayload(accessToken)
216
+ if (!payload || !payload.sub) return null
217
+ var parts = String(payload.sub).split("|")
218
+ var userId = parts.length > 1 ? parts[1] : parts[0]
219
+ if (!userId) return null
220
+ return { userId: userId, sessionToken: userId + "%3A%3A" + accessToken }
221
+ }
222
+
223
+ function fetchEnterpriseUsage(ctx, accessToken) {
224
+ var session = buildSessionToken(ctx, accessToken)
225
+ if (!session) {
226
+ ctx.host.log.warn("enterprise: cannot build session token")
227
+ return null
228
+ }
229
+ try {
230
+ var resp = ctx.util.request({
231
+ method: "GET",
232
+ url: REST_USAGE_URL + "?user=" + encodeURIComponent(session.userId),
233
+ headers: {
234
+ Cookie: "WorkosCursorSessionToken=" + session.sessionToken,
235
+ },
236
+ timeoutMs: 10000,
237
+ })
238
+ if (resp.status < 200 || resp.status >= 300) {
239
+ ctx.host.log.warn("enterprise usage returned status=" + resp.status)
240
+ return null
241
+ }
242
+ return ctx.util.tryParseJson(resp.bodyText)
243
+ } catch (e) {
244
+ ctx.host.log.warn("enterprise usage fetch failed: " + String(e))
245
+ return null
246
+ }
247
+ }
248
+
249
+ function buildEnterpriseResult(ctx, accessToken, planName, usage) {
250
+ var requestUsage = fetchEnterpriseUsage(ctx, accessToken)
251
+ var lines = []
252
+
253
+ if (requestUsage) {
254
+ var gpt4 = requestUsage["gpt-4"]
255
+ if (gpt4 && typeof gpt4.maxRequestUsage === "number" && gpt4.maxRequestUsage > 0) {
256
+ var used = gpt4.numRequests || 0
257
+ var limit = gpt4.maxRequestUsage
258
+
259
+ var billingPeriodMs = 30 * 24 * 60 * 60 * 1000
260
+ var cycleStart = requestUsage.startOfMonth
261
+ ? ctx.util.parseDateMs(requestUsage.startOfMonth)
262
+ : null
263
+ var cycleEndMs = cycleStart ? cycleStart + billingPeriodMs : null
264
+
265
+ lines.push(ctx.line.progress({
266
+ label: "Requests",
267
+ used: used,
268
+ limit: limit,
269
+ format: { kind: "count", suffix: "requests" },
270
+ resetsAt: ctx.util.toIso(cycleEndMs),
271
+ periodDurationMs: billingPeriodMs,
272
+ }))
273
+ }
274
+ }
275
+
276
+ if (lines.length === 0) {
277
+ ctx.host.log.warn("enterprise: no usage data available")
278
+ throw "Enterprise usage data unavailable. Try again later."
279
+ }
280
+
281
+ var plan = null
282
+ if (planName) {
283
+ var planLabel = ctx.fmt.planLabel(planName)
284
+ if (planLabel) plan = planLabel
285
+ }
286
+
287
+ return { plan: plan, lines: lines }
288
+ }
289
+
290
+ function probe(ctx) {
291
+ const authState = loadAuthState(ctx)
292
+ let accessToken = authState.accessToken
293
+ const refreshTokenValue = authState.refreshToken
294
+ const authSource = authState.source
295
+
296
+ if (!accessToken && !refreshTokenValue) {
297
+ ctx.host.log.error("probe failed: no access or refresh token in sqlite/keychain")
298
+ throw "Not logged in. " + LOGIN_HINT
299
+ }
300
+
301
+ ctx.host.log.info("tokens loaded from " + authSource + ": accessToken=" + (accessToken ? "yes" : "no") + " refreshToken=" + (refreshTokenValue ? "yes" : "no"))
302
+
303
+ const nowMs = Date.now()
304
+
305
+ // Proactively refresh if token is expired or about to expire
306
+ if (needsRefresh(ctx, accessToken, nowMs)) {
307
+ ctx.host.log.info("token needs refresh (expired or expiring soon)")
308
+ let refreshed = null
309
+ try {
310
+ refreshed = refreshToken(ctx, refreshTokenValue, authSource)
311
+ } catch (e) {
312
+ // If refresh fails but we have an access token, try it anyway
313
+ ctx.host.log.warn("refresh failed but have access token, will try: " + String(e))
314
+ if (!accessToken) throw e
315
+ }
316
+ if (refreshed) {
317
+ accessToken = refreshed
318
+ } else if (!accessToken) {
319
+ ctx.host.log.error("refresh failed and no access token available")
320
+ throw "Not logged in. " + LOGIN_HINT
321
+ }
322
+ }
323
+
324
+ let usageResp
325
+ let didRefresh = false
326
+ try {
327
+ usageResp = ctx.util.retryOnceOnAuth({
328
+ request: (token) => {
329
+ try {
330
+ return connectPost(ctx, USAGE_URL, token || accessToken)
331
+ } catch (e) {
332
+ ctx.host.log.error("usage request exception: " + String(e))
333
+ if (didRefresh) {
334
+ throw "Usage request failed after refresh. Try again."
335
+ }
336
+ throw "Usage request failed. Check your connection."
337
+ }
338
+ },
339
+ refresh: () => {
340
+ ctx.host.log.info("usage returned 401, attempting refresh")
341
+ didRefresh = true
342
+ const refreshed = refreshToken(ctx, refreshTokenValue, authSource)
343
+ if (refreshed) accessToken = refreshed
344
+ return refreshed
345
+ },
346
+ })
347
+ } catch (e) {
348
+ if (typeof e === "string") throw e
349
+ ctx.host.log.error("usage request failed: " + String(e))
350
+ throw "Usage request failed. Check your connection."
351
+ }
352
+
353
+ if (ctx.util.isAuthStatus(usageResp.status)) {
354
+ ctx.host.log.error("usage returned auth error after all retries: status=" + usageResp.status)
355
+ throw "Token expired. " + LOGIN_HINT
356
+ }
357
+
358
+ if (usageResp.status < 200 || usageResp.status >= 300) {
359
+ ctx.host.log.error("usage returned error: status=" + usageResp.status)
360
+ throw "Usage request failed (HTTP " + String(usageResp.status) + "). Try again later."
361
+ }
362
+
363
+ ctx.host.log.info("usage fetch succeeded")
364
+
365
+ const usage = ctx.util.tryParseJson(usageResp.bodyText)
366
+ if (usage === null) {
367
+ throw "Usage response invalid. Try again later."
368
+ }
369
+
370
+ // Fetch plan info early (needed for Enterprise detection)
371
+ let planName = ""
372
+ try {
373
+ const planResp = connectPost(ctx, PLAN_URL, accessToken)
374
+ if (planResp.status >= 200 && planResp.status < 300) {
375
+ const plan = ctx.util.tryParseJson(planResp.bodyText)
376
+ if (plan && plan.planInfo && plan.planInfo.planName) {
377
+ planName = plan.planInfo.planName
378
+ }
379
+ }
380
+ } catch (e) {
381
+ ctx.host.log.warn("plan info fetch failed: " + String(e))
382
+ }
383
+
384
+ // Enterprise accounts return no planUsage from the Connect API.
385
+ // Detect Enterprise and use the REST usage API instead.
386
+ const isEnterprise = !usage.planUsage && planName.toLowerCase() === "enterprise"
387
+ if (isEnterprise) {
388
+ ctx.host.log.info("detected enterprise account, using REST usage API")
389
+ return buildEnterpriseResult(ctx, accessToken, planName, usage)
390
+ }
391
+
392
+ // Team plans may omit `enabled` even with valid plan usage data.
393
+ if (usage.enabled === false || !usage.planUsage) {
394
+ throw "No active Cursor subscription."
395
+ }
396
+
397
+ let creditGrants = null
398
+ try {
399
+ const creditsResp = connectPost(ctx, CREDITS_URL, accessToken)
400
+ if (creditsResp.status >= 200 && creditsResp.status < 300) {
401
+ creditGrants = ctx.util.tryParseJson(creditsResp.bodyText)
402
+ }
403
+ } catch (e) {
404
+ ctx.host.log.warn("credit grants fetch failed: " + String(e))
405
+ }
406
+
407
+ let plan = null
408
+ if (planName) {
409
+ const planLabel = ctx.fmt.planLabel(planName)
410
+ if (planLabel) {
411
+ plan = planLabel
412
+ }
413
+ }
414
+
415
+ const lines = []
416
+ const pu = usage.planUsage
417
+
418
+ // Credits first (if available) - highest priority primary metric
419
+ if (creditGrants && creditGrants.hasCreditGrants === true) {
420
+ const total = parseInt(creditGrants.totalCents, 10)
421
+ const used = parseInt(creditGrants.usedCents, 10)
422
+ if (total > 0 && !isNaN(total) && !isNaN(used)) {
423
+ lines.push(ctx.line.progress({
424
+ label: "Credits",
425
+ used: ctx.fmt.dollars(used),
426
+ limit: ctx.fmt.dollars(total),
427
+ format: { kind: "dollars" },
428
+ }))
429
+ }
430
+ }
431
+
432
+ // Total usage (always present) - fallback primary metric
433
+ if (typeof pu.limit !== "number") {
434
+ throw "Total usage limit missing from API response."
435
+ }
436
+ const planUsed = typeof pu.totalSpend === "number"
437
+ ? pu.totalSpend
438
+ : pu.limit - (pu.remaining ?? 0)
439
+ const computedPercentUsed = pu.limit > 0
440
+ ? (planUsed / pu.limit) * 100
441
+ : 0
442
+ const totalUsagePercent = Number.isFinite(pu.totalPercentUsed)
443
+ ? pu.totalPercentUsed
444
+ : computedPercentUsed
445
+
446
+ // Calculate billing cycle period duration
447
+ var billingPeriodMs = 30 * 24 * 60 * 60 * 1000 // 30 days default
448
+ var cycleStart = Number(usage.billingCycleStart)
449
+ var cycleEnd = Number(usage.billingCycleEnd)
450
+ if (Number.isFinite(cycleStart) && Number.isFinite(cycleEnd) && cycleEnd > cycleStart) {
451
+ billingPeriodMs = cycleEnd - cycleStart // already in ms
452
+ }
453
+
454
+ const su = usage.spendLimitUsage
455
+ const isTeamAccount = (
456
+ (typeof planName === "string" && planName.toLowerCase() === "team") ||
457
+ (su && su.limitType === "team") ||
458
+ (su && typeof su.pooledLimit === "number")
459
+ )
460
+
461
+ if (isTeamAccount) {
462
+ lines.push(ctx.line.progress({
463
+ label: "Total usage",
464
+ used: ctx.fmt.dollars(planUsed),
465
+ limit: ctx.fmt.dollars(pu.limit),
466
+ format: { kind: "dollars" },
467
+ resetsAt: ctx.util.toIso(usage.billingCycleEnd),
468
+ periodDurationMs: billingPeriodMs
469
+ }))
470
+
471
+ if (typeof pu.bonusSpend === "number" && pu.bonusSpend > 0) {
472
+ lines.push(ctx.line.text({ label: "Bonus spend", value: "$" + String(ctx.fmt.dollars(pu.bonusSpend)) }))
473
+ }
474
+ } else {
475
+ lines.push(ctx.line.progress({
476
+ label: "Total usage",
477
+ used: totalUsagePercent,
478
+ limit: 100,
479
+ format: { kind: "percent" },
480
+ resetsAt: ctx.util.toIso(usage.billingCycleEnd),
481
+ periodDurationMs: billingPeriodMs
482
+ }))
483
+ }
484
+
485
+ if (typeof pu.autoPercentUsed === "number" && Number.isFinite(pu.autoPercentUsed)) {
486
+ lines.push(ctx.line.progress({
487
+ label: "Auto usage",
488
+ used: pu.autoPercentUsed,
489
+ limit: 100,
490
+ format: { kind: "percent" },
491
+ resetsAt: ctx.util.toIso(usage.billingCycleEnd),
492
+ periodDurationMs: billingPeriodMs
493
+ }))
494
+ }
495
+
496
+ if (typeof pu.apiPercentUsed === "number" && Number.isFinite(pu.apiPercentUsed)) {
497
+ lines.push(ctx.line.progress({
498
+ label: "API usage",
499
+ used: pu.apiPercentUsed,
500
+ limit: 100,
501
+ format: { kind: "percent" },
502
+ resetsAt: ctx.util.toIso(usage.billingCycleEnd),
503
+ periodDurationMs: billingPeriodMs
504
+ }))
505
+ }
506
+
507
+ // On-demand (if available) - not a primary candidate
508
+ if (su) {
509
+ const limit = su.individualLimit ?? su.pooledLimit ?? 0
510
+ const remaining = su.individualRemaining ?? su.pooledRemaining ?? 0
511
+ if (limit > 0) {
512
+ const used = limit - remaining
513
+ lines.push(ctx.line.progress({
514
+ label: "On-demand",
515
+ used: ctx.fmt.dollars(used),
516
+ limit: ctx.fmt.dollars(limit),
517
+ format: { kind: "dollars" },
518
+ }))
519
+ }
520
+ }
521
+
522
+ return { plan: plan, lines: lines }
523
+ }
524
+
525
+ globalThis.__openusage_plugin = { id: "cursor", probe }
526
+ })()
@@ -0,0 +1,24 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "cursor",
4
+ "name": "Cursor",
5
+ "version": "0.0.1",
6
+ "entry": "plugin.js",
7
+ "icon": "icon.svg",
8
+ "brandColor": "#000000",
9
+ "cli": {
10
+ "category": "ide"
11
+ },
12
+ "links": [
13
+ { "label": "Status", "url": "https://status.cursor.com/" },
14
+ { "label": "Dashboard", "url": "https://www.cursor.com/dashboard" }
15
+ ],
16
+ "lines": [
17
+ { "type": "progress", "label": "Credits", "scope": "overview", "primaryOrder": 1 },
18
+ { "type": "progress", "label": "Total usage", "scope": "overview", "primaryOrder": 2 },
19
+ { "type": "progress", "label": "Requests", "scope": "overview", "primaryOrder": 3 },
20
+ { "type": "progress", "label": "Auto usage", "scope": "detail" },
21
+ { "type": "progress", "label": "API usage", "scope": "detail" },
22
+ { "type": "progress", "label": "On-demand", "scope": "detail" }
23
+ ]
24
+ }