@hashimkarim/usagestat-linux-x64-gnu 2.0.0-alpha.1

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