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.
- package/bin/openusage +91 -0
- package/package.json +33 -0
- package/plugins/amp/icon.svg +6 -0
- package/plugins/amp/plugin.js +175 -0
- package/plugins/amp/plugin.json +20 -0
- package/plugins/amp/plugin.test.js +365 -0
- package/plugins/antigravity/icon.svg +3 -0
- package/plugins/antigravity/plugin.js +484 -0
- package/plugins/antigravity/plugin.json +17 -0
- package/plugins/antigravity/plugin.test.js +1356 -0
- package/plugins/claude/icon.svg +3 -0
- package/plugins/claude/plugin.js +565 -0
- package/plugins/claude/plugin.json +28 -0
- package/plugins/claude/plugin.test.js +1012 -0
- package/plugins/codex/icon.svg +3 -0
- package/plugins/codex/plugin.js +673 -0
- package/plugins/codex/plugin.json +30 -0
- package/plugins/codex/plugin.test.js +1071 -0
- package/plugins/copilot/icon.svg +3 -0
- package/plugins/copilot/plugin.js +264 -0
- package/plugins/copilot/plugin.json +20 -0
- package/plugins/copilot/plugin.test.js +529 -0
- package/plugins/cursor/icon.svg +3 -0
- package/plugins/cursor/plugin.js +526 -0
- package/plugins/cursor/plugin.json +24 -0
- package/plugins/cursor/plugin.test.js +1168 -0
- package/plugins/factory/icon.svg +1 -0
- package/plugins/factory/plugin.js +407 -0
- package/plugins/factory/plugin.json +19 -0
- package/plugins/factory/plugin.test.js +833 -0
- package/plugins/gemini/icon.svg +4 -0
- package/plugins/gemini/plugin.js +413 -0
- package/plugins/gemini/plugin.json +20 -0
- package/plugins/gemini/plugin.test.js +735 -0
- package/plugins/jetbrains-ai-assistant/icon.svg +3 -0
- package/plugins/jetbrains-ai-assistant/plugin.js +357 -0
- package/plugins/jetbrains-ai-assistant/plugin.json +17 -0
- package/plugins/jetbrains-ai-assistant/plugin.test.js +338 -0
- package/plugins/kimi/icon.svg +3 -0
- package/plugins/kimi/plugin.js +358 -0
- package/plugins/kimi/plugin.json +19 -0
- package/plugins/kimi/plugin.test.js +619 -0
- package/plugins/minimax/icon.svg +4 -0
- package/plugins/minimax/plugin.js +388 -0
- package/plugins/minimax/plugin.json +17 -0
- package/plugins/minimax/plugin.test.js +943 -0
- package/plugins/perplexity/icon.svg +1 -0
- package/plugins/perplexity/plugin.js +378 -0
- package/plugins/perplexity/plugin.json +15 -0
- package/plugins/perplexity/plugin.test.js +602 -0
- package/plugins/windsurf/icon.svg +3 -0
- package/plugins/windsurf/plugin.js +218 -0
- package/plugins/windsurf/plugin.json +16 -0
- package/plugins/windsurf/plugin.test.js +455 -0
- package/plugins/zai/icon.svg +5 -0
- package/plugins/zai/plugin.js +156 -0
- package/plugins/zai/plugin.json +18 -0
- 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
|
+
}
|