@hashimkarim/usagestat-linux-x64-gnu 2.0.2 → 2.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 (156) hide show
  1. package/native-manifest.json +685 -133
  2. package/package.json +94 -2
  3. package/plugins/abliteration/LICENSE +21 -0
  4. package/plugins/abliteration/icon.svg +5 -0
  5. package/plugins/abliteration/plugin.js +89 -0
  6. package/plugins/abliteration/plugin.json +24 -0
  7. package/plugins/aerostack/LICENSE +21 -0
  8. package/plugins/aerostack/icon.svg +1 -0
  9. package/plugins/aerostack/plugin.js +94 -0
  10. package/plugins/aerostack/plugin.json +24 -0
  11. package/plugins/aixy/icon.svg +1 -0
  12. package/plugins/aixy/plugin.js +5 -0
  13. package/plugins/aixy/plugin.json +9 -1
  14. package/plugins/antigravity/plugin.js +19 -10
  15. package/plugins/antigravity-cli/plugin.js +8 -3
  16. package/plugins/antigravity-ide/plugin.js +8 -5
  17. package/plugins/atlascloud/plugin.js +5 -0
  18. package/plugins/atlascloud/plugin.json +2 -1
  19. package/plugins/bifrost/icon.svg +3 -0
  20. package/plugins/bifrost/plugin.js +5 -0
  21. package/plugins/bifrost/plugin.json +9 -1
  22. package/plugins/chatbox/LICENSE +21 -0
  23. package/plugins/chatbox/icon.svg +4 -0
  24. package/plugins/chatbox/plugin.js +208 -0
  25. package/plugins/chatbox/plugin.json +24 -0
  26. package/plugins/claude/plugin.js +39 -2
  27. package/plugins/claudeplatform/LICENSE +21 -0
  28. package/plugins/claudeplatform/icon.svg +3 -0
  29. package/plugins/claudeplatform/plugin.js +244 -0
  30. package/plugins/claudeplatform/plugin.json +25 -0
  31. package/plugins/codex/plugin.js +3 -2
  32. package/plugins/codex/plugin.json +1 -1
  33. package/plugins/cosmic/LICENSE +21 -0
  34. package/plugins/cosmic/icon.svg +1 -0
  35. package/plugins/cosmic/plugin.js +134 -0
  36. package/plugins/cosmic/plugin.json +32 -0
  37. package/plugins/cursor/plugin.js +9 -2
  38. package/plugins/cursor-nightly/plugin.json +1 -1
  39. package/plugins/devpass/icon.svg +3 -0
  40. package/plugins/devpass/plugin.js +5 -0
  41. package/plugins/devpass/plugin.json +2 -1
  42. package/plugins/exa/LICENSE +21 -0
  43. package/plugins/exa/icon-color.svg +1 -0
  44. package/plugins/exa/icon.svg +1 -0
  45. package/plugins/exa/plugin.js +75 -0
  46. package/plugins/exa/plugin.json +32 -0
  47. package/plugins/fal/LICENSE +21 -0
  48. package/plugins/fal/icon-color.svg +1 -0
  49. package/plugins/fal/icon.svg +1 -0
  50. package/plugins/fal/plugin.js +74 -0
  51. package/plugins/fal/plugin.json +24 -0
  52. package/plugins/flora/LICENSE +21 -0
  53. package/plugins/flora/icon.svg +4 -0
  54. package/plugins/flora/plugin.js +93 -0
  55. package/plugins/flora/plugin.json +32 -0
  56. package/plugins/futureagi/LICENSE +21 -0
  57. package/plugins/futureagi/icon.svg +24 -0
  58. package/plugins/futureagi/plugin.js +139 -0
  59. package/plugins/futureagi/plugin.json +32 -0
  60. package/plugins/gemini-apps/LICENSE +1 -2
  61. package/plugins/gemini-apps/plugin.js +136 -465
  62. package/plugins/gemini-apps/plugin.json +13 -7
  63. package/plugins/gitkraken/icon.svg +2 -0
  64. package/plugins/gitkraken/plugin.js +5 -0
  65. package/plugins/gitkraken/plugin.json +9 -1
  66. package/plugins/helmcode/icon.svg +5 -0
  67. package/plugins/helmcode/plugin.js +48 -40
  68. package/plugins/helmcode/plugin.json +10 -2
  69. package/plugins/huggingface/plugin.js +6 -1
  70. package/plugins/huggingface/plugin.json +4 -3
  71. package/plugins/hyper/plugin.js +53 -47
  72. package/plugins/hyper/plugin.json +3 -2
  73. package/plugins/kimi/plugin.js +39 -14
  74. package/plugins/langdock/LICENSE +21 -0
  75. package/plugins/langdock/icon.svg +4 -0
  76. package/plugins/langdock/plugin.js +106 -0
  77. package/plugins/langdock/plugin.json +25 -0
  78. package/plugins/linkup/LICENSE +21 -0
  79. package/plugins/linkup/icon.svg +10 -0
  80. package/plugins/linkup/plugin.js +46 -0
  81. package/plugins/linkup/plugin.json +24 -0
  82. package/plugins/lithosai/plugin.js +5 -0
  83. package/plugins/lithosai/plugin.json +2 -1
  84. package/plugins/llmman/icon.svg +4 -0
  85. package/plugins/llmman/plugin.js +5 -0
  86. package/plugins/llmman/plugin.json +9 -1
  87. package/plugins/munito/LICENSE +21 -0
  88. package/plugins/munito/icon.svg +7 -0
  89. package/plugins/munito/plugin.js +171 -0
  90. package/plugins/munito/plugin.json +24 -0
  91. package/plugins/muse/icon.svg +3 -0
  92. package/plugins/muse/plugin.js +5 -0
  93. package/plugins/muse/plugin.json +9 -1
  94. package/plugins/museai/plugin.js +5 -0
  95. package/plugins/museai/plugin.json +2 -1
  96. package/plugins/nous/plugin.js +5 -0
  97. package/plugins/nous/plugin.json +9 -1
  98. package/plugins/ollama/plugin.js +58 -0
  99. package/plugins/opencode-go/plugin.js +17 -6
  100. package/plugins/parallel/LICENSE +21 -0
  101. package/plugins/parallel/icon.svg +11 -0
  102. package/plugins/parallel/plugin.js +97 -0
  103. package/plugins/parallel/plugin.json +25 -0
  104. package/plugins/qwencloud/LICENSE +21 -0
  105. package/plugins/qwencloud/plugin.js +197 -0
  106. package/plugins/qwencloud/plugin.json +17 -0
  107. package/plugins/raycast/icon.svg +4 -0
  108. package/plugins/raycast/plugin.js +5 -0
  109. package/plugins/raycast/plugin.json +9 -1
  110. package/plugins/replicate/plugin.js +5 -0
  111. package/plugins/replicate/plugin.json +2 -1
  112. package/plugins/sailresearch/LICENSE +21 -0
  113. package/plugins/sailresearch/icon.svg +4 -0
  114. package/plugins/sailresearch/plugin.js +104 -0
  115. package/plugins/sailresearch/plugin.json +24 -0
  116. package/plugins/sofya/LICENSE +21 -0
  117. package/plugins/sofya/icon.svg +3 -0
  118. package/plugins/sofya/plugin.js +86 -0
  119. package/plugins/sofya/plugin.json +24 -0
  120. package/plugins/soniox/LICENSE +21 -0
  121. package/plugins/soniox/icon.svg +3 -0
  122. package/plugins/soniox/plugin.js +103 -0
  123. package/plugins/soniox/plugin.json +24 -0
  124. package/plugins/tavily/LICENSE +21 -0
  125. package/plugins/tavily/icon.svg +3 -0
  126. package/plugins/tavily/plugin.js +61 -0
  127. package/plugins/tavily/plugin.json +24 -0
  128. package/plugins/tinyapi/LICENSE +21 -0
  129. package/plugins/tinyapi/icon.svg +4 -0
  130. package/plugins/tinyapi/plugin.js +73 -0
  131. package/plugins/tinyapi/plugin.json +25 -0
  132. package/plugins/traecn/LICENSE +21 -0
  133. package/plugins/traecn/icon.svg +3 -0
  134. package/plugins/traecn/plugin.js +104 -0
  135. package/plugins/traecn/plugin.json +25 -0
  136. package/plugins/typesafe/icon.svg +4 -0
  137. package/plugins/typesafe/plugin.js +30 -25
  138. package/plugins/typesafe/plugin.json +3 -2
  139. package/plugins/v0/plugin.js +5 -0
  140. package/plugins/v0/plugin.json +9 -1
  141. package/plugins/vercel/plugin.js +147 -0
  142. package/plugins/vercel/plugin.json +34 -3
  143. package/plugins/workbuddy/icon.svg +1 -1
  144. package/plugins/workbuddy/plugin.js +9 -4
  145. package/plugins/workbuddy/plugin.json +20 -2
  146. package/plugins/xai/plugin.js +9 -0
  147. package/plugins/xapi/LICENSE +21 -0
  148. package/plugins/xapi/icon.svg +1 -0
  149. package/plugins/xapi/plugin.js +90 -0
  150. package/plugins/xapi/plugin.json +25 -0
  151. package/plugins/xkiro/icon.svg +1 -0
  152. package/plugins/xkiro/plugin.js +5 -0
  153. package/plugins/xkiro/plugin.json +2 -1
  154. package/usagestat +0 -0
  155. package/usagestatd +0 -0
  156. package/plugins/cursor-nightly/plugin.js +0 -1416
@@ -1,1416 +0,0 @@
1
- globalThis.__OPENUSAGE_PLUGIN_REGISTRATION_ID__ = "cursor-nightly";
2
- (function () {
3
- const KEYCHAIN_ACCESS_TOKEN_SERVICE = "cursor-access-token"
4
- const KEYCHAIN_REFRESH_TOKEN_SERVICE = "cursor-refresh-token"
5
- const BASE_URL = "https://api2.cursor.sh"
6
- const USAGE_URL = BASE_URL + "/aiserver.v1.DashboardService/GetCurrentPeriodUsage"
7
- const GROK_BOT_USAGE_URL = BASE_URL + "/aiserver.v1.DashboardService/GetSandUsageStatus"
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 USAGE_LIMIT_GRANTS_URL =
12
- BASE_URL + "/aiserver.v1.DashboardService/GetUsageLimitStatusAndActiveGrants"
13
- const REST_USAGE_URL = "https://cursor.com/api/usage"
14
- const STRIPE_URL = "https://cursor.com/api/auth/stripe"
15
- const CLIENT_ID = "KbZUR41cY7W6zRSdpSUJ7I7mLYBKOCmB"
16
- /** Must match `buildDevMockProviderCredentials` when `VITE_PROVIDER_ACCOUNT_DEV_MOCK` is enabled. */
17
- const CROSSUSAGE_DEV_MOCK_ACCESS = "crossusage-dev-mock-access-token"
18
- const CROSSUSAGE_DEV_MOCK_REFRESH = "crossusage-dev-mock-refresh-token"
19
- const CROSSUSAGE_DEV_MOCK_SESSION_PREFIX = "crossusage-dev-mock-session:"
20
- const REFRESH_BUFFER_MS = 5 * 60 * 1000 // refresh 5 minutes before expiration
21
- const LOGIN_HINT = "Sign in via Cursor app or run `agent login`."
22
- /** Connect RPC (api2.cursor.sh) — short product UA. */
23
- const CONNECT_CLIENT_USER_AGENT = "Cursor/1.0.0"
24
- /** cursor.com web dashboard style (some endpoints reject non-browser UAs). */
25
- const CURSOR_WEB_USER_AGENT =
26
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
27
- const CONNECT_USAGE_FALLBACK_FAILED_MSG =
28
- "Cursor Connect API rejected usage (HTTP 400/403), and cursor.com/api/usage had no usable data " +
29
- "(no request limits or plan usage). Open Cursor, sign in again, update the app, then retry. " +
30
- LOGIN_HINT
31
-
32
- function extractConnectUsageErrorDetail(ctx, bodyText) {
33
- var j = ctx.util.tryParseJson(bodyText)
34
- if (!j || typeof j !== "object") return null
35
- if (typeof j.message === "string" && j.message.length > 0 && j.message !== "Error") {
36
- return j.message.trim()
37
- }
38
- if (Array.isArray(j.details)) {
39
- for (var i = 0; i < j.details.length; i++) {
40
- var d = j.details[i]
41
- if (!d || typeof d !== "object") continue
42
- if (d.debug && d.debug.details && typeof d.debug.details.detail === "string") {
43
- return d.debug.details.detail.trim()
44
- }
45
- if (d.debug && typeof d.debug.detail === "string") return d.debug.detail.trim()
46
- }
47
- }
48
- return null
49
- }
50
-
51
- function buildConnectFallbackFailedMessage(connectDetail) {
52
- if (
53
- connectDetail &&
54
- String(connectDetail).indexOf("Usage summary is not enabled") >= 0
55
- ) {
56
- return (
57
- "Cursor reports \"Usage summary is not enabled\" for this account (GetCurrentPeriodUsage is gated off). " +
58
- "usagestat tried alternate APIs and cursor.com; check usage in the Cursor app (Settings → Account). " +
59
- LOGIN_HINT
60
- )
61
- }
62
- return CONNECT_USAGE_FALLBACK_FAILED_MSG
63
- }
64
-
65
- /**
66
- * Map undocumented GetUsageLimitStatusAndActiveGrants JSON toward GetCurrentPeriodUsage shape.
67
- */
68
- function normalizeLimitGrantsToUsageShape(raw) {
69
- if (!raw || typeof raw !== "object") return null
70
- var u = raw
71
- if (u.data && typeof u.data === "object") u = u.data
72
- if (u.result && typeof u.result === "object" && !u.planUsage && u.result.planUsage) {
73
- u = u.result
74
- }
75
- if (u.usage && typeof u.usage === "object") {
76
- if (u.usage.planUsage && !u.planUsage) {
77
- u = Object.assign({}, u.usage, { planUsage: u.usage.planUsage })
78
- } else if (!u.planUsage) {
79
- u = u.usage
80
- }
81
- }
82
- if (u.planUsage && typeof u.planUsage === "object") {
83
- coerceUsageNumbers(u)
84
- return u
85
- }
86
- if (u.limitStatus && typeof u.limitStatus === "object") {
87
- var ls = u.limitStatus
88
- var pu = {}
89
- if (ls.limit != null) pu.limit = ls.limit
90
- if (ls.remaining != null) pu.remaining = ls.remaining
91
- if (ls.totalSpend != null) pu.totalSpend = ls.totalSpend
92
- if (ls.totalPercentUsed != null) pu.totalPercentUsed = ls.totalPercentUsed
93
- if (Object.keys(pu).length > 0) {
94
- return { enabled: true, planUsage: pu }
95
- }
96
- }
97
- var pct = readFiniteNumber(u.totalPercentUsed)
98
- if (Number.isFinite(pct)) {
99
- return { enabled: true, planUsage: { totalPercentUsed: pct } }
100
- }
101
- return null
102
- }
103
-
104
- const CURSOR_STATE_DB_REL = "Cursor/User/globalStorage/state.vscdb"
105
- const CURSOR_NIGHTLY_STATE_DB_REL = "Cursor Nightly/User/globalStorage/state.vscdb"
106
-
107
- function cursorBaseProviderId(ctx) {
108
- if (ctx.account && ctx.account.baseProviderId) {
109
- return String(ctx.account.baseProviderId)
110
- }
111
- return globalThis.__OPENUSAGE_PLUGIN_REGISTRATION_ID__ || "cursor"
112
- }
113
-
114
- function getCursorDbPath(ctx) {
115
- if (ctx.host.cursorPaths && typeof ctx.host.cursorPaths.resolveStateDb === "function") {
116
- // The native resolver owns explicit profile selection, including missing
117
- // overrides. Do not undo it by searching another installation.
118
- return ctx.host.cursorPaths.resolveStateDb() || null
119
- }
120
- var rel = cursorBaseProviderId(ctx) === "cursor-nightly" ? CURSOR_NIGHTLY_STATE_DB_REL : CURSOR_STATE_DB_REL
121
- return ctx.host.fs.firstExistingAppSupport(rel) || null
122
- }
123
-
124
- function sharedCredentialsAllowed(ctx) {
125
- if (cursorBaseProviderId(ctx) === "cursor-nightly") return false
126
- return !ctx.host.cursorPaths || ctx.host.cursorPaths.sharedCredentialsAllowed !== false
127
- }
128
-
129
- function readStateValue(ctx, key) {
130
- try {
131
- const dbPath = getCursorDbPath(ctx)
132
- if (!dbPath) return null
133
- const sql =
134
- "SELECT value FROM ItemTable WHERE key = '" + key + "' LIMIT 1;"
135
- const json = ctx.host.sqlite.query(dbPath, sql)
136
- const rows = ctx.util.tryParseJson(json)
137
- if (!Array.isArray(rows)) {
138
- throw new Error("sqlite returned invalid json")
139
- }
140
- if (rows.length > 0 && rows[0].value) {
141
- return rows[0].value
142
- }
143
- } catch (e) {
144
- ctx.host.log.warn("sqlite read failed for " + key + ": " + String(e))
145
- }
146
- return null
147
- }
148
-
149
- function writeStateValue(ctx, key, value) {
150
- try {
151
- const dbPath = getCursorDbPath(ctx)
152
- // Escape single quotes in value for SQL
153
- const escaped = String(value).replace(/'/g, "''")
154
- const sql =
155
- "INSERT OR REPLACE INTO ItemTable (key, value) VALUES ('" +
156
- key +
157
- "', '" +
158
- escaped +
159
- "');"
160
- ctx.host.sqlite.exec(dbPath, sql)
161
- return true
162
- } catch (e) {
163
- ctx.host.log.warn("sqlite write failed for " + key + ": " + String(e))
164
- return false
165
- }
166
- }
167
-
168
- function readKeychainValue(ctx, service) {
169
- if (!ctx.host.keychain || typeof ctx.host.keychain.readGenericPassword !== "function") {
170
- return null
171
- }
172
- try {
173
- const value = ctx.host.keychain.readGenericPassword(service)
174
- if (typeof value !== "string") return null
175
- const trimmed = value.trim()
176
- return trimmed || null
177
- } catch (e) {
178
- ctx.host.log.info("keychain read failed for " + service + ": " + String(e))
179
- return null
180
- }
181
- }
182
-
183
- function writeKeychainValue(ctx, service, value) {
184
- if (!ctx.host.keychain || typeof ctx.host.keychain.writeGenericPassword !== "function") {
185
- ctx.host.log.warn("keychain write unsupported")
186
- return false
187
- }
188
- try {
189
- ctx.host.keychain.writeGenericPassword(service, String(value))
190
- return true
191
- } catch (e) {
192
- ctx.host.log.warn("keychain write failed for " + service + ": " + String(e))
193
- return false
194
- }
195
- }
196
-
197
- function loadAuthState(ctx) {
198
- const injected = readInjectedCredential(ctx)
199
- if (injected) return injected
200
-
201
- const sqliteAccessToken = readStateValue(ctx, "cursorAuth/accessToken")
202
- const sqliteRefreshToken = readStateValue(ctx, "cursorAuth/refreshToken")
203
- if (sqliteAccessToken || sqliteRefreshToken) {
204
- return { accessToken: sqliteAccessToken, refreshToken: sqliteRefreshToken, source: "sqlite" }
205
- }
206
- // Generic CLI keychain targets are shared, not tied to an IDE variant.
207
- // They cannot represent Nightly or an explicitly selected database profile.
208
- const keychainAccessToken = sharedCredentialsAllowed(ctx) ? readKeychainValue(ctx, KEYCHAIN_ACCESS_TOKEN_SERVICE) : null
209
- const keychainRefreshToken = sharedCredentialsAllowed(ctx) ? readKeychainValue(ctx, KEYCHAIN_REFRESH_TOKEN_SERVICE) : null
210
-
211
- if (keychainAccessToken || keychainRefreshToken) {
212
- return {
213
- accessToken: keychainAccessToken,
214
- refreshToken: keychainRefreshToken,
215
- source: "keychain",
216
- }
217
- }
218
-
219
- return {
220
- accessToken: null,
221
- refreshToken: null,
222
- source: null,
223
- }
224
- }
225
-
226
- function readInjectedCredential(ctx) {
227
- try {
228
- if (!ctx.host.credentials || typeof ctx.host.credentials.get !== "function") return null
229
- const raw = ctx.host.credentials.get()
230
- if (!raw) return null
231
- const credential = ctx.util.tryParseJson(String(raw))
232
- if (!credential) return null
233
- const accessToken = String(credential.accessToken || credential.sessionKey || "").trim()
234
- const refreshToken = String(credential.refreshToken || "").trim()
235
- if (!accessToken && !refreshToken) return null
236
- return {
237
- accessToken: accessToken || null,
238
- refreshToken: refreshToken || null,
239
- source: "provider-account",
240
- }
241
- } catch (e) {
242
- ctx.host.log.warn("provider account credential read failed: " + String(e))
243
- return null
244
- }
245
- }
246
-
247
- function isCrossusageDevMockCredential(accessToken, refreshToken) {
248
- const at = accessToken ? String(accessToken).trim() : ""
249
- const rt = refreshToken ? String(refreshToken).trim() : ""
250
- if (at === CROSSUSAGE_DEV_MOCK_ACCESS) return true
251
- if (rt === CROSSUSAGE_DEV_MOCK_REFRESH) return true
252
- if (at.indexOf(CROSSUSAGE_DEV_MOCK_SESSION_PREFIX) === 0) return true
253
- return false
254
- }
255
-
256
- function buildCrossusageDevMockProbeOutput(ctx) {
257
- ctx.host.log.info("crossusage dev mock: skipping Cursor API (no network)")
258
- // Labels must match plugin.json manifest lines so tray + overview filters never go empty.
259
- return {
260
- plan: "Dev mock",
261
- lines: [
262
- ctx.line.text({
263
- label: "Data source",
264
- value: "Mock (CrossUsage dev)",
265
- subtitle: "Placeholder tokens from Settings; api2.cursor.sh is not called.",
266
- }),
267
- ctx.line.progress({
268
- label: "Total usage",
269
- used: 35,
270
- limit: 100,
271
- format: { kind: "percent" },
272
- }),
273
- ctx.line.progress({
274
- label: "Credits",
275
- used: 250,
276
- limit: 1000,
277
- format: { kind: "dollars" },
278
- }),
279
- ctx.line.progress({
280
- label: "Requests",
281
- used: 42,
282
- limit: 1000,
283
- format: { kind: "count", suffix: " req" },
284
- }),
285
- ctx.line.progress({
286
- label: "Cursor Models",
287
- used: 18,
288
- limit: 100,
289
- format: { kind: "percent" },
290
- }),
291
- ctx.line.progress({
292
- label: "Other Models",
293
- used: 7,
294
- limit: 50,
295
- format: { kind: "percent" },
296
- }),
297
- ctx.line.progress({
298
- label: "On-demand",
299
- used: 1,
300
- limit: 10,
301
- format: { kind: "percent" },
302
- }),
303
- ],
304
- }
305
- }
306
-
307
- function getTokenSubject(ctx, token) {
308
- if (!token) return null
309
- const payload = ctx.jwt.decodePayload(token)
310
- if (!payload || typeof payload.sub !== "string") return null
311
- const subject = payload.sub.trim()
312
- return subject || null
313
- }
314
-
315
- function persistAccessToken(ctx, source, accessToken) {
316
- if (source === "provider-account") {
317
- try {
318
- if (ctx.host.credentials && typeof ctx.host.credentials.update === "function") {
319
- const update = { accessToken: accessToken || null }
320
- ctx.host.credentials.update(JSON.stringify(update))
321
- return true
322
- }
323
- } catch (e) {
324
- ctx.host.log.warn("provider account credential update failed: " + String(e))
325
- }
326
- return false
327
- }
328
- if (source === "keychain") {
329
- return writeKeychainValue(ctx, KEYCHAIN_ACCESS_TOKEN_SERVICE, accessToken)
330
- }
331
- return writeStateValue(ctx, "cursorAuth/accessToken", accessToken)
332
- }
333
-
334
- function getTokenExpiration(ctx, token) {
335
- const payload = ctx.jwt.decodePayload(token)
336
- if (!payload || typeof payload.exp !== "number") return null
337
- return payload.exp * 1000 // Convert to milliseconds
338
- }
339
-
340
- function needsRefresh(ctx, accessToken, nowMs) {
341
- if (!accessToken) return true
342
- const expiresAt = getTokenExpiration(ctx, accessToken)
343
- return ctx.util.needsRefreshByExpiry({
344
- nowMs,
345
- expiresAtMs: expiresAt,
346
- bufferMs: REFRESH_BUFFER_MS,
347
- })
348
- }
349
-
350
- function refreshToken(ctx, refreshTokenValue, source) {
351
- if (!refreshTokenValue) {
352
- ctx.host.log.warn("refresh skipped: no refresh token")
353
- return null
354
- }
355
-
356
- ctx.host.log.info("attempting token refresh")
357
- try {
358
- const resp = ctx.util.request({
359
- method: "POST",
360
- url: REFRESH_URL,
361
- headers: { "Content-Type": "application/json" },
362
- bodyText: JSON.stringify({
363
- grant_type: "refresh_token",
364
- client_id: CLIENT_ID,
365
- refresh_token: refreshTokenValue,
366
- }),
367
- timeoutMs: 15000,
368
- })
369
-
370
- if (resp.status === 400 || resp.status === 401) {
371
- let errorInfo = null
372
- errorInfo = ctx.util.tryParseJson(resp.bodyText)
373
- const shouldLogout = errorInfo && errorInfo.shouldLogout === true
374
- ctx.host.log.error("refresh failed: status=" + resp.status + " shouldLogout=" + shouldLogout)
375
- if (shouldLogout) {
376
- throw "Session expired. " + LOGIN_HINT
377
- }
378
- throw "Token expired. " + LOGIN_HINT
379
- }
380
-
381
- if (resp.status < 200 || resp.status >= 300) {
382
- ctx.host.log.warn("refresh returned unexpected status: " + resp.status)
383
- return null
384
- }
385
-
386
- const body = ctx.util.tryParseJson(resp.bodyText)
387
- if (!body) {
388
- ctx.host.log.warn("refresh response not valid JSON")
389
- return null
390
- }
391
-
392
- // Check if server wants us to logout
393
- if (body.shouldLogout === true) {
394
- ctx.host.log.error("refresh response indicates shouldLogout=true")
395
- throw "Session expired. " + LOGIN_HINT
396
- }
397
-
398
- const newAccessToken = body.access_token
399
- if (!newAccessToken) {
400
- ctx.host.log.warn("refresh response missing access_token")
401
- return null
402
- }
403
-
404
- // Persist updated access token to source where auth was loaded from.
405
- persistAccessToken(ctx, source, newAccessToken)
406
- ctx.host.log.info("refresh succeeded, token persisted")
407
-
408
- // Note: Cursor refresh returns access_token which is used as both
409
- // access and refresh token in some flows
410
- return newAccessToken
411
- } catch (e) {
412
- if (typeof e === "string") throw e
413
- ctx.host.log.error("refresh exception: " + String(e))
414
- return null
415
- }
416
- }
417
-
418
- function connectPost(ctx, url, token, timeoutMs) {
419
- return ctx.util.request({
420
- method: "POST",
421
- url: url,
422
- headers: {
423
- Authorization: "Bearer " + token,
424
- "Content-Type": "application/json",
425
- "Connect-Protocol-Version": "1",
426
- Accept: "application/json",
427
- "User-Agent": CONNECT_CLIENT_USER_AGENT,
428
- },
429
- bodyText: "{}",
430
- timeoutMs: timeoutMs || 10000,
431
- })
432
- }
433
-
434
- /** Auth0-style `sub` is often `provider|opaqueId`; use last segment so `a|b|c` maps to `c`. */
435
- function userIdFromJwtSub(sub) {
436
- if (!sub || typeof sub !== "string") return null
437
- var parts = String(sub).trim().split("|")
438
- if (parts.length >= 2) {
439
- var last = parts[parts.length - 1]
440
- return last ? last.trim() : parts[0].trim()
441
- }
442
- return parts[0] ? parts[0].trim() : null
443
- }
444
-
445
- function buildSessionToken(ctx, accessToken) {
446
- var payload = ctx.jwt.decodePayload(accessToken)
447
- if (!payload || !payload.sub) return null
448
- var userId = userIdFromJwtSub(payload.sub)
449
- if (!userId) return null
450
- return { userId: userId, sessionToken: userId + "%3A%3A" + accessToken }
451
- }
452
-
453
- function coerceNumericField(obj, key) {
454
- if (!obj || typeof obj !== "object") return
455
- var v = obj[key]
456
- if (typeof v === "string" && v.trim() !== "") {
457
- var n = parseFloat(v)
458
- if (Number.isFinite(n)) obj[key] = n
459
- }
460
- }
461
-
462
- function coercePlanUsageObject(pu) {
463
- if (!pu || typeof pu !== "object") return
464
- var keys = [
465
- "limit",
466
- "remaining",
467
- "totalSpend",
468
- "includedSpend",
469
- "bonusSpend",
470
- "totalPercentUsed",
471
- "autoPercentUsed",
472
- "apiPercentUsed",
473
- ]
474
- for (var i = 0; i < keys.length; i++) {
475
- coerceNumericField(pu, keys[i])
476
- }
477
- }
478
-
479
- function coerceUsageNumbers(u) {
480
- if (!u || typeof u !== "object") return
481
- coerceNumericField(u, "billingCycleStart")
482
- coerceNumericField(u, "billingCycleEnd")
483
- if (u.planUsage) coercePlanUsageObject(u.planUsage)
484
- if (u.spendLimitUsage && typeof u.spendLimitUsage === "object") {
485
- var su = u.spendLimitUsage
486
- coerceNumericField(su, "individualLimit")
487
- coerceNumericField(su, "individualUsed")
488
- coerceNumericField(su, "individualRemaining")
489
- coerceNumericField(su, "pooledLimit")
490
- coerceNumericField(su, "pooledUsed")
491
- coerceNumericField(su, "pooledRemaining")
492
- }
493
- }
494
-
495
- /** Unwrap `{ data: ... }` / `{ usage: ... }` and coerce numeric strings from the dashboard API. */
496
- function normalizeRestUsagePayload(raw) {
497
- if (!raw || typeof raw !== "object") return null
498
- var u = raw
499
- if (u.data && typeof u.data === "object") u = u.data
500
- if (u.result && typeof u.result === "object" && !u.planUsage && u.result.planUsage) {
501
- u = u.result
502
- } else if (u.usage && typeof u.usage === "object" && !u.planUsage && u.usage.planUsage) {
503
- u = u.usage
504
- }
505
- coerceUsageNumbers(u)
506
- return u
507
- }
508
-
509
- function restUsageLooksPromising(u) {
510
- if (!u || typeof u !== "object") return false
511
- var g4 = u["gpt-4"]
512
- if (g4 && typeof g4.maxRequestUsage === "number" && g4.maxRequestUsage > 0) return true
513
- if (isConnectUsageRestShape(u)) return true
514
- return false
515
- }
516
-
517
- function fetchRequestBasedUsage(ctx, accessToken) {
518
- var session = buildSessionToken(ctx, accessToken)
519
- if (!session) {
520
- ctx.host.log.warn("request-based: cannot build session token")
521
- return null
522
- }
523
- var commonHeaders = {
524
- Authorization: "Bearer " + accessToken,
525
- Cookie: "WorkosCursorSessionToken=" + session.sessionToken,
526
- Accept: "application/json",
527
- Origin: "https://cursor.com",
528
- Referer: "https://cursor.com/dashboard",
529
- "User-Agent": CURSOR_WEB_USER_AGENT,
530
- }
531
- var urls = [
532
- REST_USAGE_URL + "?user=" + encodeURIComponent(session.userId),
533
- REST_USAGE_URL,
534
- ]
535
- var fallback = null
536
- for (var i = 0; i < urls.length; i++) {
537
- try {
538
- var resp = ctx.util.request({
539
- method: "GET",
540
- url: urls[i],
541
- headers: commonHeaders,
542
- timeoutMs: 15000,
543
- })
544
- if (resp.status < 200 || resp.status >= 300) {
545
- ctx.host.log.warn(
546
- "request-based usage returned status=" + resp.status + " url=" + urls[i]
547
- )
548
- continue
549
- }
550
- var parsed = ctx.util.tryParseJson(resp.bodyText)
551
- var norm = normalizeRestUsagePayload(parsed)
552
- if (norm && restUsageLooksPromising(norm)) {
553
- return norm
554
- }
555
- if (norm && !fallback) fallback = norm
556
- } catch (e) {
557
- ctx.host.log.warn("request-based usage fetch failed: " + String(e))
558
- }
559
- }
560
- return fallback
561
- }
562
-
563
- function fetchStripePayload(ctx, accessToken) {
564
- var session = buildSessionToken(ctx, accessToken)
565
- if (!session) {
566
- ctx.host.log.warn("stripe: cannot build session token")
567
- return null
568
- }
569
- try {
570
- var resp = ctx.util.request({
571
- method: "GET",
572
- url: STRIPE_URL,
573
- headers: {
574
- Authorization: "Bearer " + accessToken,
575
- Cookie: "WorkosCursorSessionToken=" + session.sessionToken,
576
- Accept: "application/json",
577
- Origin: "https://cursor.com",
578
- Referer: "https://cursor.com/dashboard",
579
- "User-Agent": CURSOR_WEB_USER_AGENT,
580
- },
581
- timeoutMs: 10000,
582
- })
583
- if (resp.status < 200 || resp.status >= 300) {
584
- ctx.host.log.warn("stripe payload returned status=" + resp.status)
585
- return null
586
- }
587
- return ctx.util.tryParseJson(resp.bodyText)
588
- } catch (e) {
589
- ctx.host.log.warn("stripe payload fetch failed: " + String(e))
590
- return null
591
- }
592
- }
593
-
594
- function fetchStripeBalance(ctx, accessToken) {
595
- var stripe = fetchStripePayload(ctx, accessToken)
596
- if (!stripe) return null
597
- var customerBalanceCents = Number(stripe.customerBalance)
598
- if (!Number.isFinite(customerBalanceCents)) return null
599
- // Stripe stores customer credits as a negative balance.
600
- return customerBalanceCents < 0 ? Math.abs(customerBalanceCents) : 0
601
- }
602
-
603
- function buildPartialStripeSubscriptionResult(ctx, accessToken, connectDetail) {
604
- var stripe = fetchStripePayload(ctx, accessToken)
605
- if (!stripe || typeof stripe !== "object") return null
606
- var mt = stripe.membershipType
607
- var ss = stripe.subscriptionStatus
608
- if (!mt && !ss) return null
609
- var tier = typeof mt === "string" ? mt : "unknown"
610
- var sub = typeof ss === "string" ? ss : "unknown"
611
- var msg = "Plan: " + tier + ", subscription: " + sub + ". "
612
- if (connectDetail && String(connectDetail).indexOf("Usage summary is not enabled") >= 0) {
613
- msg +=
614
- "Cursor does not expose usage summary for this account via API. Open Cursor → Settings → Account to see usage."
615
- } else {
616
- msg += "Usage meters unavailable from API; open Cursor → Account / Usage."
617
- }
618
- return {
619
- plan: null,
620
- lines: [
621
- ctx.line.text({
622
- label: "Account",
623
- value: msg,
624
- }),
625
- ],
626
- }
627
- }
628
-
629
- function readFiniteNumber(v) {
630
- if (typeof v === "number" && Number.isFinite(v)) return v
631
- if (typeof v === "string" && v.trim() !== "") {
632
- var n = parseFloat(v)
633
- if (Number.isFinite(n)) return n
634
- }
635
- return NaN
636
- }
637
-
638
- function getConnectUsageMetricFlags(usage) {
639
- const hasPlanUsage = !!usage.planUsage
640
- const pu = usage.planUsage
641
- const limitN = hasPlanUsage ? readFiniteNumber(pu.limit) : NaN
642
- const pctN = hasPlanUsage ? readFiniteNumber(pu.totalPercentUsed) : NaN
643
- const hasPlanUsageLimit = hasPlanUsage && Number.isFinite(limitN)
644
- const planUsageLimitMissing = hasPlanUsage && !hasPlanUsageLimit
645
- const hasTotalUsagePercent = hasPlanUsage && Number.isFinite(pctN)
646
- return {
647
- hasPlanUsage: hasPlanUsage,
648
- hasPlanUsageLimit: hasPlanUsageLimit,
649
- planUsageLimitMissing: planUsageLimitMissing,
650
- hasTotalUsagePercent: hasTotalUsagePercent,
651
- pu: pu,
652
- }
653
- }
654
-
655
- /**
656
- * Build plan label + progress lines from a Connect-shaped usage object (same JSON as
657
- * GetCurrentPeriodUsage). Returns null when a team account needs the legacy request-based REST path.
658
- */
659
- function buildPlanAndLinesFromConnectStyleUsage(ctx, usage, planName, options) {
660
- options = options || {}
661
- const creditGrants = options.creditGrants
662
- const stripeBalanceCents = options.stripeBalanceCents || 0
663
-
664
- coerceUsageNumbers(usage)
665
-
666
- if (usage.enabled === false || !usage.planUsage) {
667
- throw "No active Cursor subscription."
668
- }
669
-
670
- const normalizedPlanName = typeof planName === "string"
671
- ? planName.toLowerCase()
672
- : ""
673
-
674
- const flags = getConnectUsageMetricFlags(usage)
675
- const hasPlanUsageLimit = flags.hasPlanUsageLimit
676
- const hasTotalUsagePercent = flags.hasTotalUsagePercent
677
- const pu = flags.pu
678
-
679
- if (!hasPlanUsageLimit && !hasTotalUsagePercent) {
680
- throw "Total usage limit missing from API response."
681
- }
682
-
683
- let plan = null
684
- if (planName) {
685
- const planLabel = ctx.fmt.planLabel(planName)
686
- if (planLabel) {
687
- plan = planLabel
688
- }
689
- }
690
-
691
- const lines = []
692
-
693
- const hasCreditGrants = creditGrants && creditGrants.hasCreditGrants === true
694
- const grantTotalCents = hasCreditGrants ? parseInt(creditGrants.totalCents, 10) : 0
695
- const grantUsedCents = hasCreditGrants ? parseInt(creditGrants.usedCents, 10) : 0
696
- const hasValidGrantData = hasCreditGrants &&
697
- grantTotalCents > 0 &&
698
- !isNaN(grantTotalCents) &&
699
- !isNaN(grantUsedCents)
700
- const combinedTotalCents = (hasValidGrantData ? grantTotalCents : 0) + stripeBalanceCents
701
-
702
- if (combinedTotalCents > 0) {
703
- lines.push(ctx.line.progress({
704
- label: "Credits",
705
- used: ctx.fmt.dollars(hasValidGrantData ? grantUsedCents : 0),
706
- limit: ctx.fmt.dollars(combinedTotalCents),
707
- format: { kind: "dollars" },
708
- }))
709
- }
710
-
711
- const planUsed = hasPlanUsageLimit
712
- ? (typeof pu.totalSpend === "number"
713
- ? pu.totalSpend
714
- : pu.limit - (pu.remaining ?? 0))
715
- : 0
716
- const computedPercentUsed = hasPlanUsageLimit && pu.limit > 0
717
- ? (planUsed / pu.limit) * 100
718
- : 0
719
- const totalUsagePercent = hasTotalUsagePercent
720
- ? pu.totalPercentUsed
721
- : computedPercentUsed
722
-
723
- var billingPeriodMs = 30 * 24 * 60 * 60 * 1000
724
- var cycleStart = Number(usage.billingCycleStart)
725
- var cycleEnd = Number(usage.billingCycleEnd)
726
- if (Number.isFinite(cycleStart) && Number.isFinite(cycleEnd) && cycleEnd > cycleStart) {
727
- billingPeriodMs = cycleEnd - cycleStart
728
- }
729
-
730
- const su = usage.spendLimitUsage
731
- const isTeamAccount = (
732
- normalizedPlanName === "team" ||
733
- (su && su.limitType === "team") ||
734
- (su && typeof su.pooledLimit === "number" && su.pooledLimit > 0)
735
- )
736
-
737
- if (isTeamAccount) {
738
- if (!hasPlanUsageLimit) {
739
- return null
740
- }
741
- lines.push(ctx.line.progress({
742
- label: "Total usage",
743
- used: ctx.fmt.dollars(planUsed),
744
- limit: ctx.fmt.dollars(pu.limit),
745
- format: { kind: "dollars" },
746
- resetsAt: ctx.util.toIso(usage.billingCycleEnd),
747
- periodDurationMs: billingPeriodMs,
748
- }))
749
-
750
- if (typeof pu.bonusSpend === "number" && pu.bonusSpend > 0) {
751
- lines.push(ctx.line.text({ label: "Bonus spend", value: "$" + String(ctx.fmt.dollars(pu.bonusSpend)) }))
752
- }
753
- } else {
754
- lines.push(ctx.line.progress({
755
- label: "Total usage",
756
- used: totalUsagePercent,
757
- limit: 100,
758
- format: { kind: "percent" },
759
- resetsAt: ctx.util.toIso(usage.billingCycleEnd),
760
- periodDurationMs: billingPeriodMs,
761
- }))
762
- }
763
-
764
- if (typeof pu.autoPercentUsed === "number" && Number.isFinite(pu.autoPercentUsed)) {
765
- lines.push(ctx.line.progress({
766
- label: "Cursor Models",
767
- used: pu.autoPercentUsed,
768
- limit: 100,
769
- format: { kind: "percent" },
770
- resetsAt: ctx.util.toIso(usage.billingCycleEnd),
771
- periodDurationMs: billingPeriodMs,
772
- }))
773
- }
774
-
775
- if (typeof pu.apiPercentUsed === "number" && Number.isFinite(pu.apiPercentUsed)) {
776
- lines.push(ctx.line.progress({
777
- label: "Other Models",
778
- used: pu.apiPercentUsed,
779
- limit: 100,
780
- format: { kind: "percent" },
781
- resetsAt: ctx.util.toIso(usage.billingCycleEnd),
782
- periodDurationMs: billingPeriodMs,
783
- }))
784
- }
785
-
786
- if (su) {
787
- const limit = su.individualLimit ?? su.pooledLimit ?? 0
788
- const remaining = su.individualRemaining ?? su.pooledRemaining ?? 0
789
- if (limit > 0) {
790
- const used = limit - remaining
791
- lines.push(ctx.line.progress({
792
- label: "On-demand",
793
- used: ctx.fmt.dollars(used),
794
- limit: ctx.fmt.dollars(limit),
795
- format: { kind: "dollars" },
796
- }))
797
- }
798
- }
799
-
800
- return { plan: plan, lines: lines }
801
- }
802
-
803
- function finalizePlanResult(ctx, planName, lines) {
804
- var plan = null
805
- if (planName) {
806
- var planLabel = ctx.fmt.planLabel(planName)
807
- if (planLabel) plan = planLabel
808
- }
809
- return { plan: plan, lines: lines }
810
- }
811
-
812
- /** True when REST /api/usage JSON looks like GetCurrentPeriodUsage (not gpt-4-only enterprise payload). */
813
- function isConnectUsageRestShape(u) {
814
- if (!u || typeof u !== "object") return false
815
- return (
816
- u.planUsage != null ||
817
- typeof u.enabled === "boolean" ||
818
- u.billingCycleStart != null ||
819
- u.billingCycleEnd != null ||
820
- u.spendLimitUsage != null ||
821
- typeof u.displayMessage === "string" ||
822
- typeof u.displayThreshold === "number"
823
- )
824
- }
825
-
826
- function buildRequestBasedResult(ctx, accessToken, planName, unavailableMessage) {
827
- var requestUsage = fetchRequestBasedUsage(ctx, accessToken)
828
- var lines = []
829
-
830
- if (requestUsage) {
831
- var gpt4 = requestUsage["gpt-4"]
832
- if (gpt4 && typeof gpt4.maxRequestUsage === "number" && gpt4.maxRequestUsage > 0) {
833
- var used = gpt4.numRequests || 0
834
- var limit = gpt4.maxRequestUsage
835
-
836
- var billingPeriodMs = 30 * 24 * 60 * 60 * 1000
837
- var cycleStart = requestUsage.startOfMonth
838
- ? ctx.util.parseDateMs(requestUsage.startOfMonth)
839
- : null
840
- var cycleEndMs = cycleStart ? cycleStart + billingPeriodMs : null
841
-
842
- lines.push(ctx.line.progress({
843
- label: "Requests",
844
- used: used,
845
- limit: limit,
846
- format: { kind: "count", suffix: "requests" },
847
- resetsAt: ctx.util.toIso(cycleEndMs),
848
- periodDurationMs: billingPeriodMs,
849
- }))
850
- }
851
- }
852
-
853
- if (lines.length > 0) {
854
- return finalizePlanResult(ctx, planName, lines)
855
- }
856
-
857
- if (requestUsage && isConnectUsageRestShape(requestUsage)) {
858
- var stripeBalanceCents = fetchStripeBalance(ctx, accessToken) || 0
859
- var creditGrants = null
860
- try {
861
- const creditsResp = connectPost(ctx, CREDITS_URL, accessToken)
862
- if (creditsResp.status >= 200 && creditsResp.status < 300) {
863
- creditGrants = ctx.util.tryParseJson(creditsResp.bodyText)
864
- }
865
- } catch (e) {
866
- ctx.host.log.warn("request-based: credit grants fetch failed: " + String(e))
867
- }
868
- try {
869
- var connectStyle = buildPlanAndLinesFromConnectStyleUsage(ctx, requestUsage, planName, {
870
- creditGrants: creditGrants,
871
- stripeBalanceCents: stripeBalanceCents,
872
- })
873
- if (connectStyle) {
874
- return connectStyle
875
- }
876
- } catch (e) {
877
- if (typeof e === "string") {
878
- if (e === "No active Cursor subscription." || e === "Total usage limit missing from API response.") {
879
- throw e
880
- }
881
- ctx.host.log.warn("request-based: connect-style parse failed: " + e)
882
- } else {
883
- ctx.host.log.warn("request-based: connect-style parse failed: " + String(e))
884
- }
885
- }
886
- }
887
-
888
- ctx.host.log.warn("request-based: no usage data available")
889
- throw unavailableMessage
890
- }
891
-
892
- function buildEnterpriseResult(ctx, accessToken, planName) {
893
- return buildRequestBasedResult(
894
- ctx,
895
- accessToken,
896
- planName,
897
- "Enterprise usage data unavailable. Try again later."
898
- )
899
- }
900
-
901
- function buildTeamRequestBasedResult(ctx, accessToken, planName) {
902
- return buildRequestBasedResult(
903
- ctx,
904
- accessToken,
905
- planName,
906
- "Team request-based usage data unavailable. Try again later."
907
- )
908
- }
909
-
910
- function buildUnknownRequestBasedResult(ctx, accessToken, planName) {
911
- return buildRequestBasedResult(
912
- ctx,
913
- accessToken,
914
- planName,
915
- "Cursor request-based usage data unavailable. Try again later."
916
- )
917
- }
918
-
919
- function cursorYyyymmdd(d) {
920
- var y = d.getFullYear()
921
- var m = String(d.getMonth() + 1).padStart(2, "0")
922
- var day = String(d.getDate()).padStart(2, "0")
923
- return String(y) + m + day
924
- }
925
-
926
- function cursorSince31DaysAgo() {
927
- var d = new Date()
928
- d.setDate(d.getDate() - 31)
929
- return cursorYyyymmdd(d)
930
- }
931
-
932
- function cursorUntilToday() {
933
- return cursorYyyymmdd(new Date())
934
- }
935
-
936
- function cursorAccountDisplayName(ctx) {
937
- var base = cursorBaseProviderId(ctx)
938
- var name = base === "cursor-nightly" ? "Cursor Nightly" : "Cursor"
939
- var label = ctx.account && ctx.account.label ? String(ctx.account.label).trim() : ""
940
- if (label) return name + " (" + label + ")"
941
- return name
942
- }
943
-
944
- function dayKeyFromCursorDate(rawDate) {
945
- if (!rawDate) return null
946
- var s = String(rawDate).trim()
947
- if (s.length >= 10 && s.charAt(4) === "-") return s.slice(0, 10)
948
- var digits = s.replace(/\D/g, "")
949
- if (digits.length >= 8) {
950
- return digits.slice(0, 4) + "-" + digits.slice(4, 6) + "-" + digits.slice(6, 8)
951
- }
952
- return null
953
- }
954
-
955
- function cursorActivityDayLabel(rawDate) {
956
- var key = dayKeyFromCursorDate(rawDate)
957
- if (!key) return String(rawDate || "").slice(0, 10) || "Activity"
958
- return Number(key.slice(5, 7)) + "/" + Number(key.slice(8, 10))
959
- }
960
-
961
- function fmtCursorTokens(n) {
962
- var v = Number(n)
963
- if (!Number.isFinite(v) || v < 0) return "0"
964
- if (v >= 1_000_000) return (v / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M"
965
- if (v >= 1_000) return (v / 1_000).toFixed(1).replace(/\.0$/, "") + "k"
966
- return String(Math.round(v))
967
- }
968
-
969
- function queryCursorTranscriptDaily(ctx) {
970
- if (!sharedCredentialsAllowed(ctx)) return null
971
- if (!ctx.host.cursorLogs || typeof ctx.host.cursorLogs.queryDaily !== "function") return null
972
- try {
973
- var resp = ctx.host.cursorLogs.queryDaily({ since: cursorSince31DaysAgo() })
974
- if (!resp || resp.status !== "ok" || !resp.data || !Array.isArray(resp.data.daily)) return null
975
- return resp.data.daily
976
- } catch (e) {
977
- ctx.host.log.warn("cursor transcript daily query failed: " + String(e))
978
- return null
979
- }
980
- }
981
-
982
- function collectCursorActivityChartPoints(daily) {
983
- var points = []
984
- for (var i = 0; i < daily.length; i++) {
985
- var day = daily[i]
986
- var tokens = Number(day && (day.totalTokens != null ? day.totalTokens : day.total_tokens))
987
- if (!Number.isFinite(tokens) || tokens <= 0) continue
988
- var key = dayKeyFromCursorDate(day.date)
989
- if (!key) continue
990
- points.push({
991
- key: key,
992
- label: cursorActivityDayLabel(day.date),
993
- value: tokens,
994
- valueLabel: fmtCursorTokens(tokens) + " tok (est.)",
995
- })
996
- }
997
- return points
998
- .sort(function (a, b) { return a.key.localeCompare(b.key) })
999
- .slice(-31)
1000
- .map(function (point) {
1001
- return {
1002
- label: point.label,
1003
- value: point.value,
1004
- valueLabel: point.valueLabel,
1005
- }
1006
- })
1007
- }
1008
-
1009
- function pushCursorActivityChartLine(lines, ctx, daily) {
1010
- var points = collectCursorActivityChartPoints(daily)
1011
- if (points.length === 0) return
1012
- lines.push(ctx.line.barChart({
1013
- label: "Activity trend",
1014
- points: points,
1015
- note: "Estimated from local Cursor agent transcripts (not billing usage).",
1016
- color: "#6B7280",
1017
- }))
1018
- }
1019
-
1020
- function persistCursorUsageDaily(ctx, daily, displayName) {
1021
- if (!ctx.host.usageDaily || typeof ctx.host.usageDaily.ingest !== "function") return
1022
- if (!daily || !daily.length) return
1023
- try {
1024
- ctx.host.usageDaily.ingest({
1025
- displayName: displayName,
1026
- source: "cursor_transcripts",
1027
- daily: daily,
1028
- })
1029
- } catch (e) { /* ignore */ }
1030
- }
1031
-
1032
- function formatCompactTokens(n) {
1033
- var num = Number(n) || 0
1034
- if (num >= 1000000) return (num / 1000000).toFixed(1) + "M tokens"
1035
- if (num >= 1000) return (num / 1000).toFixed(1) + "K tokens"
1036
- return String(num) + " tokens"
1037
- }
1038
-
1039
- function attachCursorMtdUsage(ctx, result) {
1040
- if (!result || !Array.isArray(result.lines)) return result
1041
- if (!ctx.host.cursorUsageExport || typeof ctx.host.cursorUsageExport.queryMtd !== "function") return result
1042
- try {
1043
- var resp = ctx.host.cursorUsageExport.queryMtd({})
1044
- if (!resp || resp.status !== "ok" || !resp.data) return result
1045
- var d = resp.data
1046
- var inputTok = Number(d.inputTokens) || 0
1047
- var outputTok = Number(d.outputTokens) || 0
1048
- var total = d.totalTokens != null ? d.totalTokens : inputTok + outputTok
1049
- var value = formatCompactTokens(total)
1050
- if (inputTok > 0 || outputTok > 0) {
1051
- value = formatCompactTokens(inputTok) + " in · " + formatCompactTokens(outputTok) + " out"
1052
- if (d.costUsd != null && Number(d.costUsd) > 0) value += " · $" + Number(d.costUsd).toFixed(2)
1053
- } else if (d.costUsd != null && Number(d.costUsd) > 0) {
1054
- value += " · $" + Number(d.costUsd).toFixed(2)
1055
- }
1056
- result.lines.push(ctx.line.text({
1057
- label: "MTD usage",
1058
- value: value,
1059
- subtitle: "From Cursor dashboard export (billing data)",
1060
- }))
1061
- } catch (e) { /* ignore */ }
1062
- return result
1063
- }
1064
-
1065
- function persistCursorBillingDaily(ctx, displayName) {
1066
- if (!ctx.host.cursorUsageExport || typeof ctx.host.cursorUsageExport.queryDaily !== "function") return
1067
- if (!ctx.host.usageDaily || typeof ctx.host.usageDaily.ingest !== "function") return
1068
- try {
1069
- var resp = ctx.host.cursorUsageExport.queryDaily({
1070
- since: cursorSince31DaysAgo(),
1071
- until: cursorUntilToday(),
1072
- })
1073
- if (!resp || resp.status !== "ok" || !resp.data || !Array.isArray(resp.data.daily)) return
1074
- if (!resp.data.daily.length) return
1075
- var daily = resp.data.daily.map(function (row) {
1076
- return {
1077
- date: row.date,
1078
- totalTokens: row.totalTokens,
1079
- inputTokens: row.inputTokens,
1080
- outputTokens: row.outputTokens,
1081
- costUsd: row.costUsd,
1082
- }
1083
- })
1084
- ctx.host.usageDaily.ingest({
1085
- displayName: displayName,
1086
- source: "cursor_billing",
1087
- daily: daily,
1088
- })
1089
- } catch (e) {
1090
- ctx.host.log.warn("cursor billing daily ingest failed: " + String(e))
1091
- }
1092
- }
1093
-
1094
- function attachCursorTranscriptActivity(ctx, result) {
1095
- if (!result || !Array.isArray(result.lines)) return result
1096
- var daily = queryCursorTranscriptDaily(ctx)
1097
- if (!daily || !daily.length) return result
1098
- var displayName = cursorAccountDisplayName(ctx)
1099
- persistCursorUsageDaily(ctx, daily, displayName)
1100
- pushCursorActivityChartLine(result.lines, ctx, daily)
1101
- return result
1102
- }
1103
-
1104
- function attachCursorBillingDaily(ctx, result) {
1105
- if (!result) return result
1106
- persistCursorBillingDaily(ctx, cursorAccountDisplayName(ctx))
1107
- return result
1108
- }
1109
-
1110
- function attachGrokBotUsage(ctx, result, accessToken) {
1111
- if (!result || !Array.isArray(result.lines) || !accessToken) return result
1112
- try {
1113
- const resp = connectPost(ctx, GROK_BOT_USAGE_URL, accessToken, 3000)
1114
- if (resp.status < 200 || resp.status >= 300) return result
1115
- const usage = ctx.util.tryParseJson(resp.bodyText)
1116
- if (!usage || usage.usesPooledEnterpriseAllowance === true ||
1117
- usage.hasNonZeroIncludedLimit === false || usage.includedLimitZero === true) return result
1118
- const percent = readFiniteNumber(usage.usagePercent)
1119
- if (!Number.isFinite(percent) || percent < 0) return result
1120
- const resetMs = ctx.util.parseDateMs(usage.nextResetTimestampUtc)
1121
- const startMs = ctx.util.parseDateMs(usage.currentPeriodStart)
1122
- const line = ctx.line.progress({
1123
- label: "Grok Bot usage",
1124
- used: Math.min(100, percent),
1125
- limit: 100,
1126
- format: { kind: "percent" },
1127
- resetsAt: Number.isFinite(resetMs) ? ctx.util.toIso(resetMs) : undefined,
1128
- periodDurationMs: Number.isFinite(startMs) && Number.isFinite(resetMs) && resetMs > startMs
1129
- ? resetMs - startMs : 7 * 24 * 60 * 60 * 1000,
1130
- })
1131
- let insertAt = result.lines.length
1132
- for (const label of ["Other Models", "Cursor Models", "Total usage"]) {
1133
- const index = result.lines.findIndex((item) => item.label === label)
1134
- if (index >= 0) { insertAt = index + 1; break }
1135
- }
1136
- result.lines.splice(insertAt, 0, line)
1137
- } catch (_) {
1138
- ctx.host.log.warn("Cursor Grok Bot quota unavailable")
1139
- }
1140
- return result
1141
- }
1142
-
1143
- function probeImpl(ctx, state) {
1144
- const authState = loadAuthState(ctx)
1145
- let accessToken = authState.accessToken
1146
- const refreshTokenValue = authState.refreshToken
1147
- const authSource = authState.source
1148
-
1149
- if (
1150
- authSource === "provider-account" &&
1151
- isCrossusageDevMockCredential(accessToken, refreshTokenValue)
1152
- ) {
1153
- return buildCrossusageDevMockProbeOutput(ctx)
1154
- }
1155
-
1156
- if (!accessToken && !refreshTokenValue) {
1157
- ctx.host.log.error("probe failed: no access or refresh token in sqlite/keychain")
1158
- throw "Not logged in. " + LOGIN_HINT
1159
- }
1160
-
1161
- ctx.host.log.info("tokens loaded from " + authSource + ": accessToken=" + (accessToken ? "yes" : "no") + " refreshToken=" + (refreshTokenValue ? "yes" : "no"))
1162
-
1163
- const nowMs = Date.now()
1164
-
1165
- // Proactively refresh if token is expired or about to expire
1166
- if (needsRefresh(ctx, accessToken, nowMs)) {
1167
- ctx.host.log.info("token needs refresh (expired or expiring soon)")
1168
- let refreshed = null
1169
- try {
1170
- refreshed = refreshToken(ctx, refreshTokenValue, authSource)
1171
- } catch (e) {
1172
- // If refresh fails but we have an access token, try it anyway
1173
- ctx.host.log.warn("refresh failed but have access token, will try: " + String(e))
1174
- if (!accessToken) throw e
1175
- }
1176
- if (refreshed) {
1177
- accessToken = refreshed
1178
- } else if (!accessToken) {
1179
- ctx.host.log.error("refresh failed and no access token available")
1180
- throw "Not logged in. " + LOGIN_HINT
1181
- }
1182
- }
1183
-
1184
- let usageResp
1185
- let didRefresh = false
1186
- try {
1187
- usageResp = ctx.util.retryOnceOnAuth({
1188
- request: (token) => {
1189
- try {
1190
- return connectPost(ctx, USAGE_URL, token || accessToken)
1191
- } catch (e) {
1192
- ctx.host.log.error("usage request exception: " + String(e))
1193
- if (didRefresh) {
1194
- throw "Usage request failed after refresh. Try again."
1195
- }
1196
- throw "Usage request failed. Check your connection."
1197
- }
1198
- },
1199
- refresh: () => {
1200
- ctx.host.log.info("usage returned 401, attempting refresh")
1201
- didRefresh = true
1202
- const refreshed = refreshToken(ctx, refreshTokenValue, authSource)
1203
- if (refreshed) accessToken = refreshed
1204
- return refreshed
1205
- },
1206
- })
1207
- } catch (e) {
1208
- if (typeof e === "string") throw e
1209
- ctx.host.log.error("usage request failed: " + String(e))
1210
- throw "Usage request failed. Check your connection."
1211
- }
1212
-
1213
- state.accessToken = accessToken
1214
- if (ctx.util.isAuthStatus(usageResp.status)) {
1215
- ctx.host.log.error("usage returned auth error after all retries: status=" + usageResp.status)
1216
- throw "Token expired. " + LOGIN_HINT
1217
- }
1218
-
1219
- if (usageResp.status < 200 || usageResp.status >= 300) {
1220
- const bodySnippet =
1221
- typeof usageResp.bodyText === "string" ? usageResp.bodyText.slice(0, 240) : ""
1222
- ctx.host.log.error(
1223
- "usage returned error: status=" + usageResp.status + " body=" + bodySnippet
1224
- )
1225
-
1226
- var connectDetail = null
1227
- // Connect/gRPC-Web sometimes returns 400/403 while other endpoints or cursor.com still work.
1228
- if (usageResp.status === 400 || usageResp.status === 403) {
1229
- connectDetail = extractConnectUsageErrorDetail(ctx, usageResp.bodyText)
1230
- ctx.host.log.warn(
1231
- "GetCurrentPeriodUsage returned " +
1232
- String(usageResp.status) +
1233
- "; detail=" +
1234
- String(connectDetail) +
1235
- "; trying GetUsageLimitStatusAndActiveGrants, then REST, then Stripe summary"
1236
- )
1237
-
1238
- try {
1239
- var lgResp = connectPost(ctx, USAGE_LIMIT_GRANTS_URL, accessToken)
1240
- if (lgResp.status >= 200 && lgResp.status < 300) {
1241
- var lgRaw = ctx.util.tryParseJson(lgResp.bodyText)
1242
- var usageShape = normalizeLimitGrantsToUsageShape(lgRaw)
1243
- if (usageShape) {
1244
- var planNameLg = ""
1245
- try {
1246
- var planRespLg = connectPost(ctx, PLAN_URL, accessToken)
1247
- if (planRespLg.status >= 200 && planRespLg.status < 300) {
1248
- var planLg = ctx.util.tryParseJson(planRespLg.bodyText)
1249
- if (planLg && planLg.planInfo && planLg.planInfo.planName) {
1250
- planNameLg = planLg.planInfo.planName
1251
- }
1252
- }
1253
- } catch (ePlan) {
1254
- ctx.host.log.warn("plan info during limit-grants fallback failed: " + String(ePlan))
1255
- }
1256
- var creditGrantsLg = null
1257
- try {
1258
- var creditsRespLg = connectPost(ctx, CREDITS_URL, accessToken)
1259
- if (creditsRespLg.status >= 200 && creditsRespLg.status < 300) {
1260
- creditGrantsLg = ctx.util.tryParseJson(creditsRespLg.bodyText)
1261
- }
1262
- } catch (eCr) {
1263
- ctx.host.log.warn("credit grants during limit-grants fallback failed: " + String(eCr))
1264
- }
1265
- var stripeBalanceLg = fetchStripeBalance(ctx, accessToken) || 0
1266
- try {
1267
- var builtLg = buildPlanAndLinesFromConnectStyleUsage(ctx, usageShape, planNameLg, {
1268
- creditGrants: creditGrantsLg,
1269
- stripeBalanceCents: stripeBalanceLg,
1270
- })
1271
- if (builtLg) return builtLg
1272
- } catch (eBuild) {
1273
- ctx.host.log.warn("limit-grants connect-style build failed: " + String(eBuild))
1274
- }
1275
- }
1276
- } else {
1277
- ctx.host.log.warn(
1278
- "GetUsageLimitStatusAndActiveGrants returned status=" + lgResp.status
1279
- )
1280
- }
1281
- } catch (eLg) {
1282
- ctx.host.log.warn("GetUsageLimitStatusAndActiveGrants request failed: " + String(eLg))
1283
- }
1284
-
1285
- try {
1286
- return buildUnknownRequestBasedResult(ctx, accessToken, "")
1287
- } catch (e) {
1288
- ctx.host.log.warn("REST usage fallback after Connect error failed: " + String(e))
1289
- }
1290
-
1291
- try {
1292
- var partialStripe = buildPartialStripeSubscriptionResult(ctx, accessToken, connectDetail)
1293
- if (partialStripe) return partialStripe
1294
- } catch (eP) {
1295
- ctx.host.log.warn("partial stripe fallback failed: " + String(eP))
1296
- }
1297
-
1298
- throw buildConnectFallbackFailedMessage(connectDetail)
1299
- }
1300
-
1301
- throw "Usage request failed (HTTP " + String(usageResp.status) + "). Try again later."
1302
- }
1303
-
1304
- ctx.host.log.info("usage fetch succeeded")
1305
-
1306
- const usage = ctx.util.tryParseJson(usageResp.bodyText)
1307
- if (usage === null) {
1308
- throw "Usage response invalid. Try again later."
1309
- }
1310
-
1311
- // Fetch plan info early (needed for request-based fallback detection)
1312
- let planName = ""
1313
- let planInfoUnavailable = false
1314
- try {
1315
- const planResp = connectPost(ctx, PLAN_URL, accessToken)
1316
- if (planResp.status >= 200 && planResp.status < 300) {
1317
- const plan = ctx.util.tryParseJson(planResp.bodyText)
1318
- if (plan && plan.planInfo && plan.planInfo.planName) {
1319
- planName = plan.planInfo.planName
1320
- }
1321
- } else {
1322
- planInfoUnavailable = true
1323
- ctx.host.log.warn("plan info returned error: status=" + planResp.status)
1324
- }
1325
- } catch (e) {
1326
- planInfoUnavailable = true
1327
- ctx.host.log.warn("plan info fetch failed: " + String(e))
1328
- }
1329
-
1330
- const normalizedPlanName = typeof planName === "string"
1331
- ? planName.toLowerCase()
1332
- : ""
1333
-
1334
- const hasPlanUsage = !!usage.planUsage
1335
- const hasPlanUsageLimit = hasPlanUsage &&
1336
- typeof usage.planUsage.limit === "number" &&
1337
- Number.isFinite(usage.planUsage.limit)
1338
- const planUsageLimitMissing = hasPlanUsage && !hasPlanUsageLimit
1339
- const hasTotalUsagePercent = hasPlanUsage &&
1340
- typeof usage.planUsage.totalPercentUsed === "number" &&
1341
- Number.isFinite(usage.planUsage.totalPercentUsed)
1342
-
1343
- // Enterprise and some Team request-based accounts can return no planUsage
1344
- // or a planUsage object without limit from the Connect API.
1345
- const needsRequestBasedFallback = usage.enabled !== false && (!hasPlanUsage || planUsageLimitMissing) && (
1346
- normalizedPlanName === "enterprise" ||
1347
- normalizedPlanName === "team"
1348
- )
1349
- if (needsRequestBasedFallback) {
1350
- if (normalizedPlanName === "enterprise") {
1351
- ctx.host.log.info("detected enterprise account, using REST usage API")
1352
- return buildEnterpriseResult(ctx, accessToken, planName)
1353
- }
1354
- ctx.host.log.info("detected team request-based account, using REST usage API")
1355
- return buildTeamRequestBasedResult(ctx, accessToken, planName)
1356
- }
1357
-
1358
- const needsFallbackWithoutPlanInfo = usage.enabled !== false &&
1359
- (!hasPlanUsage || planUsageLimitMissing) &&
1360
- !hasTotalUsagePercent &&
1361
- !normalizedPlanName &&
1362
- planInfoUnavailable
1363
- if (needsFallbackWithoutPlanInfo) {
1364
- ctx.host.log.info("plan info unavailable with missing planUsage, attempting REST usage API fallback")
1365
- return buildUnknownRequestBasedResult(ctx, accessToken, planName)
1366
- }
1367
-
1368
- if (usage.enabled !== false && planUsageLimitMissing && !hasTotalUsagePercent) {
1369
- ctx.host.log.warn("planUsage.limit missing, attempting REST usage API fallback")
1370
- try {
1371
- return buildUnknownRequestBasedResult(ctx, accessToken, planName)
1372
- } catch (e) {
1373
- ctx.host.log.warn("REST usage fallback unavailable: " + String(e))
1374
- }
1375
- }
1376
-
1377
- // Team plans may omit `enabled` even with valid plan usage data.
1378
- if (usage.enabled === false || !usage.planUsage) {
1379
- throw "No active Cursor subscription."
1380
- }
1381
-
1382
- let creditGrants = null
1383
- try {
1384
- const creditsResp = connectPost(ctx, CREDITS_URL, accessToken)
1385
- if (creditsResp.status >= 200 && creditsResp.status < 300) {
1386
- creditGrants = ctx.util.tryParseJson(creditsResp.bodyText)
1387
- }
1388
- } catch (e) {
1389
- ctx.host.log.warn("credit grants fetch failed: " + String(e))
1390
- }
1391
-
1392
- const stripeBalanceCents = fetchStripeBalance(ctx, accessToken) || 0
1393
-
1394
- const connectResult = buildPlanAndLinesFromConnectStyleUsage(ctx, usage, planName, {
1395
- creditGrants: creditGrants,
1396
- stripeBalanceCents: stripeBalanceCents,
1397
- })
1398
- if (connectResult === null) {
1399
- ctx.host.log.warn("team-inferred account missing planUsage.limit, attempting REST usage API fallback")
1400
- return buildUnknownRequestBasedResult(ctx, accessToken, planName)
1401
- }
1402
- return connectResult
1403
- }
1404
-
1405
- function probe(ctx) {
1406
- var state = {}
1407
- var result = probeImpl(ctx, state)
1408
- result = attachGrokBotUsage(ctx, result, state.accessToken)
1409
- result = attachCursorTranscriptActivity(ctx, result)
1410
- result = attachCursorBillingDaily(ctx, result)
1411
- return attachCursorMtdUsage(ctx, result)
1412
- }
1413
-
1414
- var pluginId = globalThis.__OPENUSAGE_PLUGIN_REGISTRATION_ID__ || "cursor"
1415
- globalThis.__openusage_plugin = { id: pluginId, probe }
1416
- })()