@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.
- package/LICENSE +57744 -0
- package/native-manifest.json +1414 -0
- package/package.json +254 -0
- package/plugins/abacus-ai/icon.svg +18 -0
- package/plugins/abacus-ai/plugin.js +121 -0
- package/plugins/abacus-ai/plugin.json +17 -0
- package/plugins/alibaba/icon-color.svg +1 -0
- package/plugins/alibaba/icon.svg +1 -0
- package/plugins/alibaba/plugin.js +322 -0
- package/plugins/alibaba/plugin.json +20 -0
- package/plugins/alibaba-token-plan/icon-color.svg +1 -0
- package/plugins/alibaba-token-plan/icon.svg +1 -0
- package/plugins/alibaba-token-plan/plugin.js +544 -0
- package/plugins/alibaba-token-plan/plugin.json +19 -0
- package/plugins/amp/icon-color.svg +1 -0
- package/plugins/amp/icon-conflict.svg +6 -0
- package/plugins/amp/icon.svg +1 -0
- package/plugins/amp/plugin.js +175 -0
- package/plugins/amp/plugin.json +16 -0
- package/plugins/antigravity/icon-color.svg +1 -0
- package/plugins/antigravity/icon-conflict.svg +3 -0
- package/plugins/antigravity/icon.svg +1 -0
- package/plugins/antigravity/plugin.js +739 -0
- package/plugins/antigravity/plugin.json +31 -0
- package/plugins/antigravity-cli/icon.svg +3 -0
- package/plugins/antigravity-cli/plugin.js +570 -0
- package/plugins/antigravity-cli/plugin.json +16 -0
- package/plugins/antigravity-ide/icon.svg +3 -0
- package/plugins/antigravity-ide/plugin.js +257 -0
- package/plugins/antigravity-ide/plugin.json +29 -0
- package/plugins/augment/icon.svg +14 -0
- package/plugins/augment/plugin.js +59 -0
- package/plugins/augment/plugin.json +18 -0
- package/plugins/aws-bedrock/icon-color.svg +1 -0
- package/plugins/aws-bedrock/icon.svg +1 -0
- package/plugins/aws-bedrock/plugin.js +206 -0
- package/plugins/aws-bedrock/plugin.json +18 -0
- package/plugins/azure-openai/icon-color.svg +1 -0
- package/plugins/azure-openai/icon.svg +1 -0
- package/plugins/azure-openai/plugin.js +145 -0
- package/plugins/azure-openai/plugin.json +17 -0
- package/plugins/chutes/icon.svg +7 -0
- package/plugins/chutes/plugin.js +236 -0
- package/plugins/chutes/plugin.json +19 -0
- package/plugins/claude/icon-color.svg +1 -0
- package/plugins/claude/icon.svg +1 -0
- package/plugins/claude/plugin.js +1723 -0
- package/plugins/claude/plugin.json +30 -0
- package/plugins/codebuff/icon.svg +92 -0
- package/plugins/codebuff/plugin.js +113 -0
- package/plugins/codebuff/plugin.json +17 -0
- package/plugins/codex/icon-color.svg +5 -0
- package/plugins/codex/icon.svg +1 -0
- package/plugins/codex/plugin.js +1009 -0
- package/plugins/codex/plugin.json +82 -0
- package/plugins/command-code/icon.svg +3 -0
- package/plugins/command-code/plugin.js +184 -0
- package/plugins/command-code/plugin.json +20 -0
- package/plugins/copilot/icon-color.svg +1 -0
- package/plugins/copilot/icon.svg +1 -0
- package/plugins/copilot/plugin.js +684 -0
- package/plugins/copilot/plugin.json +21 -0
- package/plugins/crof/icon.svg +3 -0
- package/plugins/crof/plugin.js +53 -0
- package/plugins/crof/plugin.json +17 -0
- package/plugins/crofai/icon.svg +16 -0
- package/plugins/crofai/plugin.js +81 -0
- package/plugins/crofai/plugin.json +18 -0
- package/plugins/cursor/icon-conflict.svg +3 -0
- package/plugins/cursor/icon.svg +1 -0
- package/plugins/cursor/plugin.js +1379 -0
- package/plugins/cursor/plugin.json +23 -0
- package/plugins/cursor-nightly/icon.png +0 -0
- package/plugins/cursor-nightly/plugin.js +1379 -0
- package/plugins/cursor-nightly/plugin.json +23 -0
- package/plugins/deepgram/icon.svg +1 -0
- package/plugins/deepgram/plugin.js +20 -0
- package/plugins/deepgram/plugin.json +17 -0
- package/plugins/deepseek/icon-color.svg +1 -0
- package/plugins/deepseek/icon.svg +1 -0
- package/plugins/deepseek/plugin.js +71 -0
- package/plugins/deepseek/plugin.json +17 -0
- package/plugins/devin/icon.svg +5 -0
- package/plugins/devin/plugin.js +341 -0
- package/plugins/devin/plugin.json +16 -0
- package/plugins/doubao/icon-color.svg +1 -0
- package/plugins/doubao/icon.svg +1 -0
- package/plugins/doubao/plugin.js +92 -0
- package/plugins/doubao/plugin.json +17 -0
- package/plugins/droid/icon.svg +1 -0
- package/plugins/droid/plugin.json +20 -0
- package/plugins/elevenlabs/icon.svg +4 -0
- package/plugins/elevenlabs/plugin.js +48 -0
- package/plugins/elevenlabs/plugin.json +18 -0
- package/plugins/factory/icon-conflict.svg +1 -0
- package/plugins/factory/icon.svg +1 -0
- package/plugins/factory/plugin.js +487 -0
- package/plugins/factory/plugin.json +15 -0
- package/plugins/fireworks-ai/icon.svg +6 -0
- package/plugins/fireworks-ai/plugin.js +370 -0
- package/plugins/fireworks-ai/plugin.json +19 -0
- package/plugins/gemini/icon-color.svg +1 -0
- package/plugins/gemini/icon.svg +1 -0
- package/plugins/gemini/plugin.js +460 -0
- package/plugins/gemini/plugin.json +16 -0
- package/plugins/grok/icon.svg +1 -0
- package/plugins/grok/plugin.js +297 -0
- package/plugins/grok/plugin.json +19 -0
- package/plugins/groqcloud/icon.svg +1 -0
- package/plugins/groqcloud/plugin.js +117 -0
- package/plugins/groqcloud/plugin.json +19 -0
- package/plugins/jetbrains-ai-assistant/icon-color.svg +14 -0
- package/plugins/jetbrains-ai-assistant/icon-conflict.svg +3 -0
- package/plugins/jetbrains-ai-assistant/icon.svg +14 -0
- package/plugins/jetbrains-ai-assistant/plugin.js +313 -0
- package/plugins/jetbrains-ai-assistant/plugin.json +16 -0
- package/plugins/kilo/icon.svg +1 -0
- package/plugins/kilo/plugin.js +111 -0
- package/plugins/kilo/plugin.json +19 -0
- package/plugins/kimi/icon-color.svg +1 -0
- package/plugins/kimi/icon-conflict.svg +3 -0
- package/plugins/kimi/icon.svg +1 -0
- package/plugins/kimi/plugin.js +358 -0
- package/plugins/kimi/plugin.json +15 -0
- package/plugins/kimi-k2/icon-color.svg +1 -0
- package/plugins/kimi-k2/icon.svg +1 -0
- package/plugins/kimi-k2/plugin.js +58 -0
- package/plugins/kimi-k2/plugin.json +18 -0
- package/plugins/kiro/icon-color.svg +5 -0
- package/plugins/kiro/icon-conflict.svg +1 -0
- package/plugins/kiro/icon.svg +4 -0
- package/plugins/kiro/plugin.js +390 -0
- package/plugins/kiro/plugin.json +16 -0
- package/plugins/litellm/icon.svg +16 -0
- package/plugins/litellm/plugin.js +199 -0
- package/plugins/litellm/plugin.json +16 -0
- package/plugins/llm-proxy/icon.svg +1 -0
- package/plugins/llm-proxy/plugin.js +40 -0
- package/plugins/llm-proxy/plugin.json +17 -0
- package/plugins/manus/icon.svg +6 -0
- package/plugins/manus/plugin.js +50 -0
- package/plugins/manus/plugin.json +17 -0
- package/plugins/mimo/icon.svg +1 -0
- package/plugins/mimo/plugin.js +177 -0
- package/plugins/mimo/plugin.json +19 -0
- package/plugins/minimax/icon-color.svg +1 -0
- package/plugins/minimax/icon-conflict.svg +4 -0
- package/plugins/minimax/icon.svg +1 -0
- package/plugins/minimax/plugin.js +453 -0
- package/plugins/minimax/plugin.json +14 -0
- package/plugins/mistral/icon-color.svg +1 -0
- package/plugins/mistral/icon.svg +1 -0
- package/plugins/mistral/plugin.js +152 -0
- package/plugins/mistral/plugin.json +18 -0
- package/plugins/moonshot/icon.svg +1 -0
- package/plugins/moonshot/plugin.js +43 -0
- package/plugins/moonshot/plugin.json +16 -0
- package/plugins/nanogpt/icon-color.svg +17 -0
- package/plugins/nanogpt/icon.svg +10 -0
- package/plugins/nanogpt/plugin.js +80 -0
- package/plugins/nanogpt/plugin.json +17 -0
- package/plugins/neuralwatt/icon.svg +3 -0
- package/plugins/neuralwatt/plugin.js +152 -0
- package/plugins/neuralwatt/plugin.json +17 -0
- package/plugins/ollama/icon.svg +1 -0
- package/plugins/ollama/plugin.js +92 -0
- package/plugins/ollama/plugin.json +20 -0
- package/plugins/openai-api/icon.svg +1 -0
- package/plugins/openai-api/plugin.js +533 -0
- package/plugins/openai-api/plugin.json +26 -0
- package/plugins/opencode/icon.svg +1 -0
- package/plugins/opencode/plugin.js +342 -0
- package/plugins/opencode/plugin.json +19 -0
- package/plugins/opencode-go/icon-conflict.svg +1 -0
- package/plugins/opencode-go/icon.svg +1 -0
- package/plugins/opencode-go/plugin.js +617 -0
- package/plugins/opencode-go/plugin.json +50 -0
- package/plugins/openrouter/icon.svg +1 -0
- package/plugins/openrouter/plugin.js +156 -0
- package/plugins/openrouter/plugin.json +8 -0
- package/plugins/perplexity/icon-color.svg +1 -0
- package/plugins/perplexity/icon-conflict.svg +1 -0
- package/plugins/perplexity/icon.svg +1 -0
- package/plugins/perplexity/plugin.js +425 -0
- package/plugins/perplexity/plugin.json +19 -0
- package/plugins/poe/icon.svg +1 -0
- package/plugins/poe/plugin.js +190 -0
- package/plugins/poe/plugin.json +19 -0
- package/plugins/stepfun/icon-color.svg +1 -0
- package/plugins/stepfun/icon.svg +1 -0
- package/plugins/stepfun/plugin.js +155 -0
- package/plugins/stepfun/plugin.json +18 -0
- package/plugins/synthetic/icon-conflict.svg +12 -0
- package/plugins/synthetic/icon.svg +14 -0
- package/plugins/synthetic/plugin.js +331 -0
- package/plugins/synthetic/plugin.json +23 -0
- package/plugins/t3chat/icon-color.svg +28 -0
- package/plugins/t3chat/icon.svg +4 -0
- package/plugins/t3chat/plugin.js +243 -0
- package/plugins/t3chat/plugin.json +20 -0
- package/plugins/venice/icon-color.svg +1 -0
- package/plugins/venice/icon.svg +1 -0
- package/plugins/venice/plugin.js +69 -0
- package/plugins/venice/plugin.json +16 -0
- package/plugins/vertex-ai/icon-color.svg +1 -0
- package/plugins/vertex-ai/icon.svg +1 -0
- package/plugins/vertex-ai/plugin.js +172 -0
- package/plugins/vertex-ai/plugin.json +18 -0
- package/plugins/warp/icon-color.svg +10 -0
- package/plugins/warp/icon.svg +6 -0
- package/plugins/warp/plugin.js +108 -0
- package/plugins/warp/plugin.json +18 -0
- package/plugins/windsurf/icon-conflict.svg +3 -0
- package/plugins/windsurf/icon.svg +1 -0
- package/plugins/windsurf/plugin.js +208 -0
- package/plugins/windsurf/plugin.json +16 -0
- package/plugins/zai/icon-conflict.svg +5 -0
- package/plugins/zai/icon.svg +1 -0
- package/plugins/zai/plugin.js +188 -0
- package/plugins/zai/plugin.json +16 -0
- package/plugins/zed/icon.svg +4 -0
- package/plugins/zed/plugin.js +270 -0
- package/plugins/zed/plugin.json +19 -0
- package/usagestat +0 -0
- 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
|
+
})()
|