openusage 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/bin/openusage +91 -0
  2. package/package.json +33 -0
  3. package/plugins/amp/icon.svg +6 -0
  4. package/plugins/amp/plugin.js +175 -0
  5. package/plugins/amp/plugin.json +20 -0
  6. package/plugins/amp/plugin.test.js +365 -0
  7. package/plugins/antigravity/icon.svg +3 -0
  8. package/plugins/antigravity/plugin.js +484 -0
  9. package/plugins/antigravity/plugin.json +17 -0
  10. package/plugins/antigravity/plugin.test.js +1356 -0
  11. package/plugins/claude/icon.svg +3 -0
  12. package/plugins/claude/plugin.js +565 -0
  13. package/plugins/claude/plugin.json +28 -0
  14. package/plugins/claude/plugin.test.js +1012 -0
  15. package/plugins/codex/icon.svg +3 -0
  16. package/plugins/codex/plugin.js +673 -0
  17. package/plugins/codex/plugin.json +30 -0
  18. package/plugins/codex/plugin.test.js +1071 -0
  19. package/plugins/copilot/icon.svg +3 -0
  20. package/plugins/copilot/plugin.js +264 -0
  21. package/plugins/copilot/plugin.json +20 -0
  22. package/plugins/copilot/plugin.test.js +529 -0
  23. package/plugins/cursor/icon.svg +3 -0
  24. package/plugins/cursor/plugin.js +526 -0
  25. package/plugins/cursor/plugin.json +24 -0
  26. package/plugins/cursor/plugin.test.js +1168 -0
  27. package/plugins/factory/icon.svg +1 -0
  28. package/plugins/factory/plugin.js +407 -0
  29. package/plugins/factory/plugin.json +19 -0
  30. package/plugins/factory/plugin.test.js +833 -0
  31. package/plugins/gemini/icon.svg +4 -0
  32. package/plugins/gemini/plugin.js +413 -0
  33. package/plugins/gemini/plugin.json +20 -0
  34. package/plugins/gemini/plugin.test.js +735 -0
  35. package/plugins/jetbrains-ai-assistant/icon.svg +3 -0
  36. package/plugins/jetbrains-ai-assistant/plugin.js +357 -0
  37. package/plugins/jetbrains-ai-assistant/plugin.json +17 -0
  38. package/plugins/jetbrains-ai-assistant/plugin.test.js +338 -0
  39. package/plugins/kimi/icon.svg +3 -0
  40. package/plugins/kimi/plugin.js +358 -0
  41. package/plugins/kimi/plugin.json +19 -0
  42. package/plugins/kimi/plugin.test.js +619 -0
  43. package/plugins/minimax/icon.svg +4 -0
  44. package/plugins/minimax/plugin.js +388 -0
  45. package/plugins/minimax/plugin.json +17 -0
  46. package/plugins/minimax/plugin.test.js +943 -0
  47. package/plugins/perplexity/icon.svg +1 -0
  48. package/plugins/perplexity/plugin.js +378 -0
  49. package/plugins/perplexity/plugin.json +15 -0
  50. package/plugins/perplexity/plugin.test.js +602 -0
  51. package/plugins/windsurf/icon.svg +3 -0
  52. package/plugins/windsurf/plugin.js +218 -0
  53. package/plugins/windsurf/plugin.json +16 -0
  54. package/plugins/windsurf/plugin.test.js +455 -0
  55. package/plugins/zai/icon.svg +5 -0
  56. package/plugins/zai/plugin.js +156 -0
  57. package/plugins/zai/plugin.json +18 -0
  58. package/plugins/zai/plugin.test.js +396 -0
@@ -0,0 +1 @@
1
+ <svg height="100" style="flex:none;line-height:1" viewBox="0 0 24 24" width="100" xmlns="http://www.w3.org/2000/svg"><title>Perplexity</title><path d="M19.785 0v7.272H22.5V17.62h-2.935V24l-7.037-6.194v6.145h-1.091v-6.152L4.392 24v-6.465H1.5V7.188h2.884V0l7.053 6.494V.19h1.09v6.49L19.786 0zm-7.257 9.044v7.319l5.946 5.234V14.44l-5.946-5.397zm-1.099-.08l-5.946 5.398v7.235l5.946-5.234V8.965zm8.136 7.58h1.844V8.349H13.46l6.105 5.54v2.655zm-8.982-8.28H2.59v8.195h1.8v-2.576l6.192-5.62zM5.475 2.476v4.71h5.115l-5.115-4.71zm13.219 0l-5.115 4.71h5.115v-4.71z" fill="currentColor" fill-rule="nonzero"></path></svg>
@@ -0,0 +1,378 @@
1
+ (function () {
2
+ const LOCAL_USER_ENDPOINT = "https://www.perplexity.ai/api/user"
3
+ const REST_API_BASE = "https://www.perplexity.ai/rest/pplx-api/v2"
4
+ const REST_GROUPS_ENDPOINT = REST_API_BASE + "/groups"
5
+
6
+ const LOCAL_CACHE_DB_PATHS = [
7
+ "~/Library/Containers/ai.perplexity.mac/Data/Library/Caches/ai.perplexity.mac/Cache.db",
8
+ "~/Library/Caches/ai.perplexity.mac/Cache.db",
9
+ ]
10
+
11
+ // Only need request_object; receiver body is optional and can be malformed.
12
+ const LOCAL_SESSION_SQL =
13
+ "SELECT hex(b.request_object) AS requestHex " +
14
+ "FROM cfurl_cache_response r " +
15
+ "JOIN cfurl_cache_blob_data b ON b.entry_ID = r.entry_ID " +
16
+ "WHERE r.request_key = '" + LOCAL_USER_ENDPOINT + "' " +
17
+ "ORDER BY r.entry_ID DESC LIMIT 1;"
18
+
19
+ const BEARER_HEX_PREFIX = "42656172657220" // "Bearer "
20
+ const ASK_UA_HEX_PREFIX = "41736B2F" // Ask/
21
+ const MACOS_DEVICE_ID_HEX_PREFIX = "6D61636F733A" // macos:
22
+ const MAX_REQUEST_FIELD_LENGTH = 220
23
+
24
+ function readNumberField(obj, keys) {
25
+ if (!obj || typeof obj !== "object") return null
26
+ for (let i = 0; i < keys.length; i += 1) {
27
+ const n = Number(obj[keys[i]])
28
+ if (Number.isFinite(n)) return n
29
+ }
30
+ return null
31
+ }
32
+
33
+ function parseMoneyNumber(value) {
34
+ if (value === null || value === undefined) return null
35
+ if (typeof value === "number") return Number.isFinite(value) ? value : null
36
+ if (typeof value !== "string") return null
37
+ const trimmed = value.trim()
38
+ if (!trimmed) return null
39
+ const cleaned = trimmed.replace(/[$,]/g, "")
40
+ const n = Number(cleaned)
41
+ return Number.isFinite(n) ? n : null
42
+ }
43
+
44
+ function readMoneyLike(value) {
45
+ const direct = parseMoneyNumber(value)
46
+ if (direct !== null) return direct
47
+ if (!value || typeof value !== "object") return null
48
+
49
+ const cents = readNumberField(value, ["cents", "amount_cents", "amountCents", "value_cents", "valueCents"])
50
+ if (cents !== null) {
51
+ const dollars = cents / 100
52
+ return Number.isFinite(dollars) ? dollars : null
53
+ }
54
+
55
+ return (
56
+ readNumberField(value, ["usd", "amount_usd", "amountUsd", "value_usd", "valueUsd"]) ??
57
+ readNumberField(value, ["amount", "value", "balance", "remaining", "available"])
58
+ )
59
+ }
60
+
61
+ function isAllowedAuthByte(byte) {
62
+ return (
63
+ (byte >= 0x30 && byte <= 0x39) ||
64
+ (byte >= 0x41 && byte <= 0x5a) ||
65
+ (byte >= 0x61 && byte <= 0x7a) ||
66
+ byte === 0x2e ||
67
+ byte === 0x2d ||
68
+ byte === 0x5f
69
+ )
70
+ }
71
+
72
+ function extractAuthToken(requestHex) {
73
+ if (typeof requestHex !== "string") return null
74
+ const upper = requestHex.trim().toUpperCase()
75
+ if (!upper) return null
76
+ const idx = upper.indexOf(BEARER_HEX_PREFIX)
77
+ if (idx === -1) return null
78
+ const start = idx + BEARER_HEX_PREFIX.length
79
+ let token = ""
80
+ for (let i = start; i + 1 < upper.length; i += 2) {
81
+ const byte = parseInt(upper.slice(i, i + 2), 16)
82
+ if (!Number.isFinite(byte)) break
83
+ if (!isAllowedAuthByte(byte)) break
84
+ if (byte === 0x5f) {
85
+ // Stop before bplist marker bytes; avoids capturing '_' that precedes plist int markers.
86
+ const next = i + 3 < upper.length ? parseInt(upper.slice(i + 2, i + 4), 16) : NaN
87
+ if (next === 0x10 || next === 0x11 || next === 0x12 || next === 0x13 || next === 0x14) break
88
+ }
89
+ token += String.fromCharCode(byte)
90
+ }
91
+ const dots = (token.match(/\./g) || []).length
92
+ return dots >= 2 ? token : null
93
+ }
94
+
95
+ function isPrintableAscii(byte) {
96
+ return byte >= 0x20 && byte <= 0x7e
97
+ }
98
+
99
+ function extractPrintableField(requestHex, prefixHex) {
100
+ if (typeof requestHex !== "string") return null
101
+ const upper = requestHex.trim().toUpperCase()
102
+ if (!upper) return null
103
+ const idx = upper.indexOf(prefixHex)
104
+ if (idx === -1) return null
105
+ let out = ""
106
+ for (let i = idx; i + 1 < upper.length && out.length < MAX_REQUEST_FIELD_LENGTH; i += 2) {
107
+ const byte = parseInt(upper.slice(i, i + 2), 16)
108
+ if (!Number.isFinite(byte) || !isPrintableAscii(byte)) break
109
+ out += String.fromCharCode(byte)
110
+ }
111
+ return out ? out : null
112
+ }
113
+
114
+ function askAppVersionFromUserAgent(userAgent) {
115
+ if (typeof userAgent !== "string") return null
116
+ const m = /^Ask\/([^/]+)/.exec(userAgent.trim())
117
+ return m && m[1] ? m[1] : null
118
+ }
119
+
120
+ function makeRestHeaderOverrides(session) {
121
+ const headers = {
122
+ Accept: "*/*",
123
+ "User-Agent": (session && session.userAgent) || "Ask/0 (macOS) isiOSOnMac/false",
124
+ "X-Client-Name": "Perplexity-Mac",
125
+ "X-App-ApiVersion": "2.17",
126
+ "X-App-ApiClient": "macos",
127
+ "X-Client-Env": "production",
128
+ }
129
+ if (session && session.appVersion) headers["X-App-Version"] = session.appVersion
130
+ if (session && session.deviceId) headers["X-Device-ID"] = session.deviceId
131
+ return headers
132
+ }
133
+
134
+ function fetchJsonOptional(ctx, url, authToken, extraHeaders) {
135
+ if (!authToken) return null
136
+ let resp
137
+ try {
138
+ const headers = {
139
+ Authorization: "Bearer " + authToken,
140
+ Accept: "application/json",
141
+ "User-Agent": "OpenUsage",
142
+ }
143
+ if (extraHeaders && typeof extraHeaders === "object") for (const k in extraHeaders) headers[k] = extraHeaders[k]
144
+ resp = ctx.util.request({ method: "GET", url: url, headers: headers, timeoutMs: 10000 })
145
+ } catch (e) {
146
+ ctx.host.log.warn("request failed (" + url + "): " + String(e))
147
+ return null
148
+ }
149
+ if (ctx.util.isAuthStatus(resp.status)) {
150
+ if (resp.status === 403 && typeof resp.bodyText === "string" && resp.bodyText.indexOf("Just a moment") !== -1) {
151
+ ctx.host.log.warn("cloudflare challenge (try opening perplexity.ai in a browser once)")
152
+ }
153
+ ctx.host.log.warn("request unauthorized (" + url + "): status=" + String(resp.status))
154
+ return null
155
+ }
156
+ if (resp.status < 200 || resp.status >= 300) {
157
+ ctx.host.log.warn("request returned status " + String(resp.status) + " (" + url + ")")
158
+ return null
159
+ }
160
+ const parsed = ctx.util.tryParseJson(resp.bodyText)
161
+ if (!parsed || typeof parsed !== "object") {
162
+ ctx.host.log.warn("request returned invalid JSON (" + url + ")")
163
+ return null
164
+ }
165
+ return parsed
166
+ }
167
+
168
+ function readGroupId(value) {
169
+ if (value === null || value === undefined) return null
170
+ if (typeof value === "string") {
171
+ const trimmed = value.trim()
172
+ return trimmed ? trimmed : null
173
+ }
174
+ const n = Number(value)
175
+ if (Number.isFinite(n)) return String(Math.floor(n))
176
+ return null
177
+ }
178
+
179
+ function pickGroupId(groupsJson) {
180
+ const tryFromObj = (obj) => {
181
+ if (!obj || typeof obj !== "object") return null
182
+ return (
183
+ readGroupId(obj.api_org_id) ||
184
+ readGroupId(obj.apiOrgId) ||
185
+ readGroupId(obj.org_id) ||
186
+ readGroupId(obj.orgId) ||
187
+ readGroupId(obj.id) ||
188
+ readGroupId(obj.group_id) ||
189
+ readGroupId(obj.groupId)
190
+ )
191
+ }
192
+
193
+ const tryFromArray = (arr) => {
194
+ if (!Array.isArray(arr)) return null
195
+ let first = null
196
+ for (let i = 0; i < arr.length; i += 1) {
197
+ const item = arr[i]
198
+ const id = tryFromObj(item)
199
+ if (!id) continue
200
+ if (!first) first = id
201
+ if (item && (item.is_default_org === true || item.isDefaultOrg === true)) return id
202
+ }
203
+ return first
204
+ }
205
+
206
+ if (Array.isArray(groupsJson)) return tryFromArray(groupsJson)
207
+ if (!groupsJson || typeof groupsJson !== "object") return null
208
+
209
+ const direct = tryFromObj(groupsJson)
210
+ if (direct) return direct
211
+
212
+ const keys = ["orgs", "groups", "results", "items", "data"]
213
+ for (let i = 0; i < keys.length; i += 1) {
214
+ const id = tryFromArray(groupsJson[keys[i]])
215
+ if (id) return id
216
+ }
217
+ return null
218
+ }
219
+
220
+ function readBalanceUsd(group) {
221
+ const wrappers = ["apiOrganization", "api_organization", "group", "org", "organization", "data", "result", "item"]
222
+ const keys = ["balance_usd", "balanceUsd", "balance", "pending_balance", "pendingBalance"]
223
+
224
+ const tryNode = (node) => {
225
+ if (!node) return null
226
+ if (Array.isArray(node)) {
227
+ for (let i = 0; i < node.length; i += 1) {
228
+ const n = tryNode(node[i])
229
+ if (n !== null) return n
230
+ }
231
+ return null
232
+ }
233
+ if (typeof node !== "object") return null
234
+
235
+ for (let i = 0; i < wrappers.length; i += 1) {
236
+ const n = tryNode(node[wrappers[i]])
237
+ if (n !== null) return n
238
+ }
239
+
240
+ let n = readNumberField(node, keys)
241
+ if (n !== null) return n
242
+
243
+ for (let i = 0; i < keys.length; i += 1) {
244
+ n = readMoneyLike(node[keys[i]])
245
+ if (n !== null) return n
246
+ }
247
+
248
+ const nested = [node.customerInfo, node.wallet, node.billing, node.usage, node.account, node.balances]
249
+ for (let i = 0; i < nested.length; i += 1) {
250
+ n = tryNode(nested[i])
251
+ if (n !== null) return n
252
+ }
253
+
254
+ for (const k in node) if (/(balance|credit|wallet|prepaid|available)/i.test(k)) {
255
+ n = readMoneyLike(node[k])
256
+ if (n !== null) return n
257
+ }
258
+ return null
259
+ }
260
+
261
+ return tryNode(group)
262
+ }
263
+
264
+ function sumUsageCostUsd(usageAnalytics) {
265
+ if (!Array.isArray(usageAnalytics)) return null
266
+ let costSum = 0
267
+ let hasCost = false
268
+ for (let i = 0; i < usageAnalytics.length; i += 1) {
269
+ const meter = usageAnalytics[i]
270
+ if (!meter || typeof meter !== "object") continue
271
+ const summaries = meter.meter_event_summaries || meter.meterEventSummaries
272
+ if (!Array.isArray(summaries)) continue
273
+ for (let j = 0; j < summaries.length; j += 1) {
274
+ const s = summaries[j]
275
+ if (!s || typeof s !== "object") continue
276
+ const c = Number(s.cost)
277
+ if (Number.isFinite(c)) {
278
+ costSum += c
279
+ hasCost = true
280
+ }
281
+ }
282
+ }
283
+ return hasCost ? costSum : null
284
+ }
285
+
286
+ function queryLocalSessionFromCache(ctx, dbPath) {
287
+ let rows
288
+ try {
289
+ const json = ctx.host.sqlite.query(dbPath, LOCAL_SESSION_SQL)
290
+ rows = ctx.util.tryParseJson(json)
291
+ } catch (e) {
292
+ ctx.host.log.warn("local sqlite read failed (" + dbPath + "): " + String(e))
293
+ return null
294
+ }
295
+ if (!Array.isArray(rows) || rows.length === 0) return null
296
+
297
+ const row = rows[0] || {}
298
+ const requestHex = typeof row.requestHex === "string" ? row.requestHex : null
299
+ if (!requestHex) return null
300
+
301
+ const authToken = extractAuthToken(requestHex)
302
+ if (!authToken) return null
303
+
304
+ const userAgent = extractPrintableField(requestHex, ASK_UA_HEX_PREFIX)
305
+ const appVersion = askAppVersionFromUserAgent(userAgent)
306
+ const deviceId = extractPrintableField(requestHex, MACOS_DEVICE_ID_HEX_PREFIX)
307
+
308
+ return { authToken: authToken, userAgent: userAgent, appVersion: appVersion, deviceId: deviceId, sourcePath: dbPath }
309
+ }
310
+
311
+ function loadLocalSession(ctx) {
312
+ for (let i = 0; i < LOCAL_CACHE_DB_PATHS.length; i += 1) {
313
+ const dbPath = LOCAL_CACHE_DB_PATHS[i]
314
+ try {
315
+ if (!ctx.host.fs.exists(dbPath)) continue
316
+ } catch (e) {
317
+ ctx.host.log.warn("local cache exists check failed (" + dbPath + "): " + String(e))
318
+ continue
319
+ }
320
+ const found = queryLocalSessionFromCache(ctx, dbPath)
321
+ if (found) return found
322
+ }
323
+ return null
324
+ }
325
+
326
+ function fetchRestState(ctx, session) {
327
+ const authToken = session && session.authToken
328
+ if (!authToken) return null
329
+ const restHeaders = makeRestHeaderOverrides(session)
330
+ const groups =
331
+ fetchJsonOptional(ctx, REST_GROUPS_ENDPOINT, authToken, restHeaders) ||
332
+ fetchJsonOptional(ctx, REST_GROUPS_ENDPOINT + "/", authToken, restHeaders)
333
+ const groupId = pickGroupId(groups)
334
+ if (!groupId) return null
335
+ const base = REST_API_BASE + "/groups/" + encodeURIComponent(groupId)
336
+ const group =
337
+ fetchJsonOptional(ctx, base, authToken, restHeaders) ||
338
+ fetchJsonOptional(ctx, base + "/", authToken, restHeaders)
339
+ const usageUrl = base + "/usage-analytics"
340
+ const usageAnalytics =
341
+ fetchJsonOptional(ctx, usageUrl, authToken, restHeaders) ||
342
+ fetchJsonOptional(ctx, usageUrl + "/", authToken, restHeaders)
343
+ return { groupId: groupId, group: group, usageAnalytics: usageAnalytics }
344
+ }
345
+
346
+ function probe(ctx) {
347
+ const session = loadLocalSession(ctx)
348
+ if (!session) throw "Not logged in. Sign in via Perplexity app."
349
+ if (session.sourcePath) ctx.host.log.info("using cache db: " + session.sourcePath)
350
+
351
+ const restState = fetchRestState(ctx, session)
352
+ if (!restState || !restState.group) throw "Balance unavailable. Try again later."
353
+
354
+ const balanceUsd = readBalanceUsd(restState.group)
355
+ if (balanceUsd === null) throw "Balance unavailable. Try again later."
356
+
357
+ const usedUsd = sumUsageCostUsd(restState.usageAnalytics)
358
+ if (usedUsd === null) throw "Usage unavailable. Try again later."
359
+ const usedCents = Math.max(0, Math.round(usedUsd * 100))
360
+ const limitCents = Math.max(0, Math.round(balanceUsd * 100))
361
+ if (!Number.isFinite(limitCents) || limitCents <= 0) throw "Balance unavailable. Try again later."
362
+
363
+ const line = ctx.line.progress({
364
+ label: "Usage",
365
+ used: usedCents / 100,
366
+ limit: limitCents / 100,
367
+ format: { kind: "dollars" },
368
+ })
369
+
370
+ let plan = null
371
+ const isPro = restState.group && restState.group.customerInfo && restState.group.customerInfo.is_pro
372
+ if (isPro === true) plan = "Pro"
373
+
374
+ return plan ? { plan: plan, lines: [line] } : { lines: [line] }
375
+ }
376
+
377
+ globalThis.__openusage_plugin = { id: "perplexity", probe: probe }
378
+ })()
@@ -0,0 +1,15 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "perplexity",
4
+ "name": "Perplexity",
5
+ "version": "0.0.1",
6
+ "entry": "plugin.js",
7
+ "icon": "icon.svg",
8
+ "brandColor": "#20808D",
9
+ "cli": {
10
+ "category": "ide"
11
+ },
12
+ "lines": [
13
+ { "type": "progress", "label": "Usage", "scope": "overview", "primaryOrder": 1 }
14
+ ]
15
+ }