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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/LICENSE +57744 -0
  2. package/native-manifest.json +1414 -0
  3. package/package.json +254 -0
  4. package/plugins/abacus-ai/icon.svg +18 -0
  5. package/plugins/abacus-ai/plugin.js +121 -0
  6. package/plugins/abacus-ai/plugin.json +17 -0
  7. package/plugins/alibaba/icon-color.svg +1 -0
  8. package/plugins/alibaba/icon.svg +1 -0
  9. package/plugins/alibaba/plugin.js +322 -0
  10. package/plugins/alibaba/plugin.json +20 -0
  11. package/plugins/alibaba-token-plan/icon-color.svg +1 -0
  12. package/plugins/alibaba-token-plan/icon.svg +1 -0
  13. package/plugins/alibaba-token-plan/plugin.js +544 -0
  14. package/plugins/alibaba-token-plan/plugin.json +19 -0
  15. package/plugins/amp/icon-color.svg +1 -0
  16. package/plugins/amp/icon-conflict.svg +6 -0
  17. package/plugins/amp/icon.svg +1 -0
  18. package/plugins/amp/plugin.js +175 -0
  19. package/plugins/amp/plugin.json +16 -0
  20. package/plugins/antigravity/icon-color.svg +1 -0
  21. package/plugins/antigravity/icon-conflict.svg +3 -0
  22. package/plugins/antigravity/icon.svg +1 -0
  23. package/plugins/antigravity/plugin.js +739 -0
  24. package/plugins/antigravity/plugin.json +31 -0
  25. package/plugins/antigravity-cli/icon.svg +3 -0
  26. package/plugins/antigravity-cli/plugin.js +570 -0
  27. package/plugins/antigravity-cli/plugin.json +16 -0
  28. package/plugins/antigravity-ide/icon.svg +3 -0
  29. package/plugins/antigravity-ide/plugin.js +257 -0
  30. package/plugins/antigravity-ide/plugin.json +29 -0
  31. package/plugins/augment/icon.svg +14 -0
  32. package/plugins/augment/plugin.js +59 -0
  33. package/plugins/augment/plugin.json +18 -0
  34. package/plugins/aws-bedrock/icon-color.svg +1 -0
  35. package/plugins/aws-bedrock/icon.svg +1 -0
  36. package/plugins/aws-bedrock/plugin.js +206 -0
  37. package/plugins/aws-bedrock/plugin.json +18 -0
  38. package/plugins/azure-openai/icon-color.svg +1 -0
  39. package/plugins/azure-openai/icon.svg +1 -0
  40. package/plugins/azure-openai/plugin.js +145 -0
  41. package/plugins/azure-openai/plugin.json +17 -0
  42. package/plugins/chutes/icon.svg +7 -0
  43. package/plugins/chutes/plugin.js +236 -0
  44. package/plugins/chutes/plugin.json +19 -0
  45. package/plugins/claude/icon-color.svg +1 -0
  46. package/plugins/claude/icon.svg +1 -0
  47. package/plugins/claude/plugin.js +1723 -0
  48. package/plugins/claude/plugin.json +30 -0
  49. package/plugins/codebuff/icon.svg +92 -0
  50. package/plugins/codebuff/plugin.js +113 -0
  51. package/plugins/codebuff/plugin.json +17 -0
  52. package/plugins/codex/icon-color.svg +5 -0
  53. package/plugins/codex/icon.svg +1 -0
  54. package/plugins/codex/plugin.js +1009 -0
  55. package/plugins/codex/plugin.json +82 -0
  56. package/plugins/command-code/icon.svg +3 -0
  57. package/plugins/command-code/plugin.js +184 -0
  58. package/plugins/command-code/plugin.json +20 -0
  59. package/plugins/copilot/icon-color.svg +1 -0
  60. package/plugins/copilot/icon.svg +1 -0
  61. package/plugins/copilot/plugin.js +684 -0
  62. package/plugins/copilot/plugin.json +21 -0
  63. package/plugins/crof/icon.svg +3 -0
  64. package/plugins/crof/plugin.js +53 -0
  65. package/plugins/crof/plugin.json +17 -0
  66. package/plugins/crofai/icon.svg +16 -0
  67. package/plugins/crofai/plugin.js +81 -0
  68. package/plugins/crofai/plugin.json +18 -0
  69. package/plugins/cursor/icon-conflict.svg +3 -0
  70. package/plugins/cursor/icon.svg +1 -0
  71. package/plugins/cursor/plugin.js +1379 -0
  72. package/plugins/cursor/plugin.json +23 -0
  73. package/plugins/cursor-nightly/icon.png +0 -0
  74. package/plugins/cursor-nightly/plugin.js +1379 -0
  75. package/plugins/cursor-nightly/plugin.json +23 -0
  76. package/plugins/deepgram/icon.svg +1 -0
  77. package/plugins/deepgram/plugin.js +20 -0
  78. package/plugins/deepgram/plugin.json +17 -0
  79. package/plugins/deepseek/icon-color.svg +1 -0
  80. package/plugins/deepseek/icon.svg +1 -0
  81. package/plugins/deepseek/plugin.js +71 -0
  82. package/plugins/deepseek/plugin.json +17 -0
  83. package/plugins/devin/icon.svg +5 -0
  84. package/plugins/devin/plugin.js +341 -0
  85. package/plugins/devin/plugin.json +16 -0
  86. package/plugins/doubao/icon-color.svg +1 -0
  87. package/plugins/doubao/icon.svg +1 -0
  88. package/plugins/doubao/plugin.js +92 -0
  89. package/plugins/doubao/plugin.json +17 -0
  90. package/plugins/droid/icon.svg +1 -0
  91. package/plugins/droid/plugin.json +20 -0
  92. package/plugins/elevenlabs/icon.svg +4 -0
  93. package/plugins/elevenlabs/plugin.js +48 -0
  94. package/plugins/elevenlabs/plugin.json +18 -0
  95. package/plugins/factory/icon-conflict.svg +1 -0
  96. package/plugins/factory/icon.svg +1 -0
  97. package/plugins/factory/plugin.js +487 -0
  98. package/plugins/factory/plugin.json +15 -0
  99. package/plugins/fireworks-ai/icon.svg +6 -0
  100. package/plugins/fireworks-ai/plugin.js +370 -0
  101. package/plugins/fireworks-ai/plugin.json +19 -0
  102. package/plugins/gemini/icon-color.svg +1 -0
  103. package/plugins/gemini/icon.svg +1 -0
  104. package/plugins/gemini/plugin.js +460 -0
  105. package/plugins/gemini/plugin.json +16 -0
  106. package/plugins/grok/icon.svg +1 -0
  107. package/plugins/grok/plugin.js +297 -0
  108. package/plugins/grok/plugin.json +19 -0
  109. package/plugins/groqcloud/icon.svg +1 -0
  110. package/plugins/groqcloud/plugin.js +117 -0
  111. package/plugins/groqcloud/plugin.json +19 -0
  112. package/plugins/jetbrains-ai-assistant/icon-color.svg +14 -0
  113. package/plugins/jetbrains-ai-assistant/icon-conflict.svg +3 -0
  114. package/plugins/jetbrains-ai-assistant/icon.svg +14 -0
  115. package/plugins/jetbrains-ai-assistant/plugin.js +313 -0
  116. package/plugins/jetbrains-ai-assistant/plugin.json +16 -0
  117. package/plugins/kilo/icon.svg +1 -0
  118. package/plugins/kilo/plugin.js +111 -0
  119. package/plugins/kilo/plugin.json +19 -0
  120. package/plugins/kimi/icon-color.svg +1 -0
  121. package/plugins/kimi/icon-conflict.svg +3 -0
  122. package/plugins/kimi/icon.svg +1 -0
  123. package/plugins/kimi/plugin.js +358 -0
  124. package/plugins/kimi/plugin.json +15 -0
  125. package/plugins/kimi-k2/icon-color.svg +1 -0
  126. package/plugins/kimi-k2/icon.svg +1 -0
  127. package/plugins/kimi-k2/plugin.js +58 -0
  128. package/plugins/kimi-k2/plugin.json +18 -0
  129. package/plugins/kiro/icon-color.svg +5 -0
  130. package/plugins/kiro/icon-conflict.svg +1 -0
  131. package/plugins/kiro/icon.svg +4 -0
  132. package/plugins/kiro/plugin.js +390 -0
  133. package/plugins/kiro/plugin.json +16 -0
  134. package/plugins/litellm/icon.svg +16 -0
  135. package/plugins/litellm/plugin.js +199 -0
  136. package/plugins/litellm/plugin.json +16 -0
  137. package/plugins/llm-proxy/icon.svg +1 -0
  138. package/plugins/llm-proxy/plugin.js +40 -0
  139. package/plugins/llm-proxy/plugin.json +17 -0
  140. package/plugins/manus/icon.svg +6 -0
  141. package/plugins/manus/plugin.js +50 -0
  142. package/plugins/manus/plugin.json +17 -0
  143. package/plugins/mimo/icon.svg +1 -0
  144. package/plugins/mimo/plugin.js +177 -0
  145. package/plugins/mimo/plugin.json +19 -0
  146. package/plugins/minimax/icon-color.svg +1 -0
  147. package/plugins/minimax/icon-conflict.svg +4 -0
  148. package/plugins/minimax/icon.svg +1 -0
  149. package/plugins/minimax/plugin.js +453 -0
  150. package/plugins/minimax/plugin.json +14 -0
  151. package/plugins/mistral/icon-color.svg +1 -0
  152. package/plugins/mistral/icon.svg +1 -0
  153. package/plugins/mistral/plugin.js +152 -0
  154. package/plugins/mistral/plugin.json +18 -0
  155. package/plugins/moonshot/icon.svg +1 -0
  156. package/plugins/moonshot/plugin.js +43 -0
  157. package/plugins/moonshot/plugin.json +16 -0
  158. package/plugins/nanogpt/icon-color.svg +17 -0
  159. package/plugins/nanogpt/icon.svg +10 -0
  160. package/plugins/nanogpt/plugin.js +80 -0
  161. package/plugins/nanogpt/plugin.json +17 -0
  162. package/plugins/neuralwatt/icon.svg +3 -0
  163. package/plugins/neuralwatt/plugin.js +152 -0
  164. package/plugins/neuralwatt/plugin.json +17 -0
  165. package/plugins/ollama/icon.svg +1 -0
  166. package/plugins/ollama/plugin.js +92 -0
  167. package/plugins/ollama/plugin.json +20 -0
  168. package/plugins/openai-api/icon.svg +1 -0
  169. package/plugins/openai-api/plugin.js +533 -0
  170. package/plugins/openai-api/plugin.json +26 -0
  171. package/plugins/opencode/icon.svg +1 -0
  172. package/plugins/opencode/plugin.js +342 -0
  173. package/plugins/opencode/plugin.json +19 -0
  174. package/plugins/opencode-go/icon-conflict.svg +1 -0
  175. package/plugins/opencode-go/icon.svg +1 -0
  176. package/plugins/opencode-go/plugin.js +617 -0
  177. package/plugins/opencode-go/plugin.json +50 -0
  178. package/plugins/openrouter/icon.svg +1 -0
  179. package/plugins/openrouter/plugin.js +156 -0
  180. package/plugins/openrouter/plugin.json +8 -0
  181. package/plugins/perplexity/icon-color.svg +1 -0
  182. package/plugins/perplexity/icon-conflict.svg +1 -0
  183. package/plugins/perplexity/icon.svg +1 -0
  184. package/plugins/perplexity/plugin.js +425 -0
  185. package/plugins/perplexity/plugin.json +19 -0
  186. package/plugins/poe/icon.svg +1 -0
  187. package/plugins/poe/plugin.js +190 -0
  188. package/plugins/poe/plugin.json +19 -0
  189. package/plugins/stepfun/icon-color.svg +1 -0
  190. package/plugins/stepfun/icon.svg +1 -0
  191. package/plugins/stepfun/plugin.js +155 -0
  192. package/plugins/stepfun/plugin.json +18 -0
  193. package/plugins/synthetic/icon-conflict.svg +12 -0
  194. package/plugins/synthetic/icon.svg +14 -0
  195. package/plugins/synthetic/plugin.js +331 -0
  196. package/plugins/synthetic/plugin.json +23 -0
  197. package/plugins/t3chat/icon-color.svg +28 -0
  198. package/plugins/t3chat/icon.svg +4 -0
  199. package/plugins/t3chat/plugin.js +243 -0
  200. package/plugins/t3chat/plugin.json +20 -0
  201. package/plugins/venice/icon-color.svg +1 -0
  202. package/plugins/venice/icon.svg +1 -0
  203. package/plugins/venice/plugin.js +69 -0
  204. package/plugins/venice/plugin.json +16 -0
  205. package/plugins/vertex-ai/icon-color.svg +1 -0
  206. package/plugins/vertex-ai/icon.svg +1 -0
  207. package/plugins/vertex-ai/plugin.js +172 -0
  208. package/plugins/vertex-ai/plugin.json +18 -0
  209. package/plugins/warp/icon-color.svg +10 -0
  210. package/plugins/warp/icon.svg +6 -0
  211. package/plugins/warp/plugin.js +108 -0
  212. package/plugins/warp/plugin.json +18 -0
  213. package/plugins/windsurf/icon-conflict.svg +3 -0
  214. package/plugins/windsurf/icon.svg +1 -0
  215. package/plugins/windsurf/plugin.js +208 -0
  216. package/plugins/windsurf/plugin.json +16 -0
  217. package/plugins/zai/icon-conflict.svg +5 -0
  218. package/plugins/zai/icon.svg +1 -0
  219. package/plugins/zai/plugin.js +188 -0
  220. package/plugins/zai/plugin.json +16 -0
  221. package/plugins/zed/icon.svg +4 -0
  222. package/plugins/zed/plugin.js +270 -0
  223. package/plugins/zed/plugin.json +19 -0
  224. package/usagestat +0 -0
  225. package/usagestatd +0 -0
@@ -0,0 +1,156 @@
1
+ globalThis.__usagestat_plugin = {
2
+ probe(ctx) {
3
+ const apiKey = readApiKey(ctx);
4
+ const apiBase = readApiBase(ctx);
5
+
6
+ const credits = requestJson(ctx, {
7
+ method: "GET",
8
+ url: apiBase + "/credits",
9
+ headers: {
10
+ "Authorization": "Bearer " + apiKey,
11
+ "Accept": "application/json"
12
+ },
13
+ timeoutMs: 15000
14
+ });
15
+
16
+ if (credits.resp.status === 401 || credits.resp.status === 403) {
17
+ throw "OpenRouter API key is invalid.";
18
+ }
19
+ if (credits.resp.status < 200 || credits.resp.status >= 300) {
20
+ throw "OpenRouter credits request failed (HTTP " + credits.resp.status + ").";
21
+ }
22
+
23
+ const data = credits.json && credits.json.data ? credits.json.data : {};
24
+ const totalCredits = readNumber(data.total_credits);
25
+ const totalUsage = readNumber(data.total_usage);
26
+
27
+ if (totalCredits === null || totalUsage === null) {
28
+ throw "OpenRouter credits response did not include usage totals.";
29
+ }
30
+
31
+ const remaining = Math.max(0, totalCredits - totalUsage);
32
+ const usedPercent = totalCredits > 0 ? clamp((totalUsage / totalCredits) * 100, 0, 100) : 0;
33
+ const metrics = [
34
+ {
35
+ type: "progress",
36
+ label: "Credits",
37
+ used: round2(totalUsage),
38
+ limit: round2(totalCredits),
39
+ format: { kind: "dollars" }
40
+ },
41
+ {
42
+ type: "text",
43
+ label: "Balance",
44
+ value: "$" + round2(remaining).toFixed(2)
45
+ },
46
+ {
47
+ type: "text",
48
+ label: "Used",
49
+ value: "$" + round2(totalUsage).toFixed(2) + " (" + round1(usedPercent) + "%)"
50
+ }
51
+ ];
52
+
53
+ try {
54
+ const key = requestJson(ctx, {
55
+ method: "GET",
56
+ url: apiBase + "/key",
57
+ headers: {
58
+ "Authorization": "Bearer " + apiKey,
59
+ "Accept": "application/json"
60
+ },
61
+ timeoutMs: 15000
62
+ });
63
+ if (key.resp.status >= 200 && key.resp.status < 300 && key.json && key.json.data) {
64
+ appendKeyMetrics(metrics, key.json.data);
65
+ }
66
+ } catch (error) {
67
+ ctx.host.log.warn("OpenRouter key details request failed: " + String(error));
68
+ }
69
+
70
+ return {
71
+ displayName: "OpenRouter",
72
+ source: "api",
73
+ metrics
74
+ };
75
+ }
76
+ };
77
+
78
+ function readApiKey(ctx) {
79
+ const value = ctx.host.env.get("OPENROUTER_API_KEY");
80
+ if (typeof value === "string" && value.trim()) {
81
+ return value.trim();
82
+ }
83
+ throw "No OPENROUTER_API_KEY found.";
84
+ }
85
+
86
+ function readApiBase(ctx) {
87
+ const value = ctx.host.env.get("OPENROUTER_API_BASE");
88
+ if (typeof value === "string" && value.trim()) {
89
+ return value.trim().replace(/\/+$/, "");
90
+ }
91
+ return "https://openrouter.ai/api/v1/auth";
92
+ }
93
+
94
+ function requestJson(ctx, request) {
95
+ let resp;
96
+ try {
97
+ resp = ctx.host.http.request(request);
98
+ } catch (error) {
99
+ ctx.host.log.error("HTTP request failed: " + String(error));
100
+ throw "OpenRouter request failed. Check your connection.";
101
+ }
102
+
103
+ let json = null;
104
+ try {
105
+ json = resp.bodyText ? JSON.parse(resp.bodyText) : null;
106
+ } catch {
107
+ throw "OpenRouter response was not valid JSON.";
108
+ }
109
+
110
+ return { resp, json };
111
+ }
112
+
113
+ function appendKeyMetrics(metrics, data) {
114
+ const limit = readNumber(data.limit);
115
+ const usage = readNumber(data.usage);
116
+ if (limit !== null && usage !== null && limit > 0) {
117
+ metrics.push({
118
+ type: "progress",
119
+ label: "Key Limit",
120
+ used: round2(usage),
121
+ limit: round2(limit),
122
+ format: { kind: "dollars" }
123
+ });
124
+ }
125
+
126
+ const rateLimit = data.rate_limit || data.rateLimit;
127
+ if (rateLimit && rateLimit.requests !== undefined) {
128
+ const interval = typeof rateLimit.interval === "string" ? " / " + rateLimit.interval : "";
129
+ metrics.push({
130
+ type: "text",
131
+ label: "Rate Limit",
132
+ value: String(rateLimit.requests) + " requests" + interval
133
+ });
134
+ }
135
+ }
136
+
137
+ function readNumber(value) {
138
+ if (typeof value === "number" && Number.isFinite(value)) return value;
139
+ if (typeof value === "string" && value.trim()) {
140
+ const parsed = Number(value);
141
+ if (Number.isFinite(parsed)) return parsed;
142
+ }
143
+ return null;
144
+ }
145
+
146
+ function clamp(value, min, max) {
147
+ return Math.max(min, Math.min(max, value));
148
+ }
149
+
150
+ function round1(value) {
151
+ return Math.round(value * 10) / 10;
152
+ }
153
+
154
+ function round2(value) {
155
+ return Math.round(value * 100) / 100;
156
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "id": "openrouter",
3
+ "name": "OpenRouter",
4
+ "entry": "plugin.js",
5
+ "enabledByDefault": false,
6
+ "supportedModes": ["api"],
7
+ "autoMode": "api"
8
+ }
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" 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="#22B8CD" fill-rule="nonzero"></path></svg>
@@ -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 @@
1
+ <svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" 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"></path></svg>
@@ -0,0 +1,425 @@
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
+ const RATE_LIMIT_ENDPOINT = "https://www.perplexity.ai/rest/rate-limit/all"
6
+
7
+ const LOCAL_CACHE_DB_PATHS = [
8
+ "~/Library/Containers/ai.perplexity.mac/Data/Library/Caches/ai.perplexity.mac/Cache.db",
9
+ "~/Library/Caches/ai.perplexity.mac/Cache.db",
10
+ ]
11
+
12
+ // Only need request_object; receiver body is optional and can be malformed.
13
+ const LOCAL_SESSION_SQL =
14
+ "SELECT hex(b.request_object) AS requestHex " +
15
+ "FROM cfurl_cache_response r " +
16
+ "JOIN cfurl_cache_blob_data b ON b.entry_ID = r.entry_ID " +
17
+ "WHERE r.request_key = '" + LOCAL_USER_ENDPOINT + "' " +
18
+ "ORDER BY r.entry_ID DESC LIMIT 1;"
19
+
20
+ const BEARER_HEX_PREFIX = "42656172657220" // "Bearer "
21
+ const ASK_UA_HEX_PREFIX = "41736B2F" // Ask/
22
+ const MACOS_DEVICE_ID_HEX_PREFIX = "6D61636F733A" // macos:
23
+ const MAX_REQUEST_FIELD_LENGTH = 220
24
+ const RATE_LIMIT_CATEGORIES = [
25
+ { key: "remaining_pro", label: "Queries" },
26
+ { key: "remaining_research", label: "Deep Research" },
27
+ { key: "remaining_labs", label: "Labs" },
28
+ { key: "remaining_agentic_research", label: "Agentic Research" },
29
+ ]
30
+
31
+ function readNumberField(obj, keys) {
32
+ if (!obj || typeof obj !== "object") return null
33
+ for (let i = 0; i < keys.length; i += 1) {
34
+ const n = Number(obj[keys[i]])
35
+ if (Number.isFinite(n)) return n
36
+ }
37
+ return null
38
+ }
39
+
40
+ function parseMoneyNumber(value) {
41
+ if (value === null || value === undefined) return null
42
+ if (typeof value === "number") return Number.isFinite(value) ? value : null
43
+ if (typeof value !== "string") return null
44
+ const trimmed = value.trim()
45
+ if (!trimmed) return null
46
+ const cleaned = trimmed.replace(/[$,]/g, "")
47
+ const n = Number(cleaned)
48
+ return Number.isFinite(n) ? n : null
49
+ }
50
+
51
+ function readMoneyLike(value) {
52
+ const direct = parseMoneyNumber(value)
53
+ if (direct !== null) return direct
54
+ if (!value || typeof value !== "object") return null
55
+
56
+ const cents = readNumberField(value, ["cents", "amount_cents", "amountCents", "value_cents", "valueCents"])
57
+ if (cents !== null) {
58
+ const dollars = cents / 100
59
+ return Number.isFinite(dollars) ? dollars : null
60
+ }
61
+
62
+ return (
63
+ readNumberField(value, ["usd", "amount_usd", "amountUsd", "value_usd", "valueUsd"]) ??
64
+ readNumberField(value, ["amount", "value", "balance", "remaining", "available"])
65
+ )
66
+ }
67
+
68
+ function isAllowedAuthByte(byte) {
69
+ return (
70
+ (byte >= 0x30 && byte <= 0x39) ||
71
+ (byte >= 0x41 && byte <= 0x5a) ||
72
+ (byte >= 0x61 && byte <= 0x7a) ||
73
+ byte === 0x2e ||
74
+ byte === 0x2d ||
75
+ byte === 0x5f
76
+ )
77
+ }
78
+
79
+ function extractAuthToken(requestHex) {
80
+ if (typeof requestHex !== "string") return null
81
+ const upper = requestHex.trim().toUpperCase()
82
+ if (!upper) return null
83
+ const idx = upper.indexOf(BEARER_HEX_PREFIX)
84
+ if (idx === -1) return null
85
+ const start = idx + BEARER_HEX_PREFIX.length
86
+ let token = ""
87
+ for (let i = start; i + 1 < upper.length; i += 2) {
88
+ const byte = parseInt(upper.slice(i, i + 2), 16)
89
+ if (!Number.isFinite(byte)) break
90
+ if (!isAllowedAuthByte(byte)) break
91
+ if (byte === 0x5f) {
92
+ // Stop before bplist marker bytes; avoids capturing '_' that precedes plist int markers.
93
+ const next = i + 3 < upper.length ? parseInt(upper.slice(i + 2, i + 4), 16) : NaN
94
+ if (next === 0x10 || next === 0x11 || next === 0x12 || next === 0x13 || next === 0x14) break
95
+ }
96
+ token += String.fromCharCode(byte)
97
+ }
98
+ const dots = (token.match(/\./g) || []).length
99
+ return dots >= 2 ? token : null
100
+ }
101
+
102
+ function isPrintableAscii(byte) {
103
+ return byte >= 0x20 && byte <= 0x7e
104
+ }
105
+
106
+ function extractPrintableField(requestHex, prefixHex) {
107
+ if (typeof requestHex !== "string") return null
108
+ const upper = requestHex.trim().toUpperCase()
109
+ if (!upper) return null
110
+ const idx = upper.indexOf(prefixHex)
111
+ if (idx === -1) return null
112
+ let out = ""
113
+ for (let i = idx; i + 1 < upper.length && out.length < MAX_REQUEST_FIELD_LENGTH; i += 2) {
114
+ const byte = parseInt(upper.slice(i, i + 2), 16)
115
+ if (!Number.isFinite(byte) || !isPrintableAscii(byte)) break
116
+ out += String.fromCharCode(byte)
117
+ }
118
+ return out ? out : null
119
+ }
120
+
121
+ function askAppVersionFromUserAgent(userAgent) {
122
+ if (typeof userAgent !== "string") return null
123
+ const m = /^Ask\/([^/]+)/.exec(userAgent.trim())
124
+ return m && m[1] ? m[1] : null
125
+ }
126
+
127
+ function makeRestHeaderOverrides(session) {
128
+ const headers = {
129
+ Accept: "*/*",
130
+ "User-Agent": (session && session.userAgent) || "Ask/0 (macOS) isiOSOnMac/false",
131
+ "X-Client-Name": "Perplexity-Mac",
132
+ "X-App-ApiVersion": "2.17",
133
+ "X-App-ApiClient": "macos",
134
+ "X-Client-Env": "production",
135
+ }
136
+ if (session && session.appVersion) headers["X-App-Version"] = session.appVersion
137
+ if (session && session.deviceId) headers["X-Device-ID"] = session.deviceId
138
+ return headers
139
+ }
140
+
141
+ function fetchJsonOptional(ctx, url, authToken, extraHeaders) {
142
+ if (!authToken) return null
143
+ let resp
144
+ try {
145
+ const headers = {
146
+ Authorization: "Bearer " + authToken,
147
+ Accept: "application/json",
148
+ "User-Agent": "OpenUsage",
149
+ }
150
+ if (extraHeaders && typeof extraHeaders === "object") for (const k in extraHeaders) headers[k] = extraHeaders[k]
151
+ resp = ctx.util.request({ method: "GET", url: url, headers: headers, timeoutMs: 10000 })
152
+ } catch (e) {
153
+ ctx.host.log.warn("request failed (" + url + "): " + String(e))
154
+ return null
155
+ }
156
+ if (ctx.util.isAuthStatus(resp.status)) {
157
+ if (resp.status === 403 && typeof resp.bodyText === "string" && resp.bodyText.indexOf("Just a moment") !== -1) {
158
+ ctx.host.log.warn("cloudflare challenge (try opening perplexity.ai in a browser once)")
159
+ }
160
+ ctx.host.log.warn("request unauthorized (" + url + "): status=" + String(resp.status))
161
+ return null
162
+ }
163
+ if (resp.status < 200 || resp.status >= 300) {
164
+ ctx.host.log.warn("request returned status " + String(resp.status) + " (" + url + ")")
165
+ return null
166
+ }
167
+ const parsed = ctx.util.tryParseJson(resp.bodyText)
168
+ if (!parsed || typeof parsed !== "object") {
169
+ ctx.host.log.warn("request returned invalid JSON (" + url + ")")
170
+ return null
171
+ }
172
+ return parsed
173
+ }
174
+
175
+ function readGroupId(value) {
176
+ if (value === null || value === undefined) return null
177
+ if (typeof value === "string") {
178
+ const trimmed = value.trim()
179
+ return trimmed ? trimmed : null
180
+ }
181
+ const n = Number(value)
182
+ if (Number.isFinite(n)) return String(Math.floor(n))
183
+ return null
184
+ }
185
+
186
+ function pickGroupId(groupsJson) {
187
+ const tryFromObj = (obj) => {
188
+ if (!obj || typeof obj !== "object") return null
189
+ return (
190
+ readGroupId(obj.api_org_id) ||
191
+ readGroupId(obj.apiOrgId) ||
192
+ readGroupId(obj.org_id) ||
193
+ readGroupId(obj.orgId) ||
194
+ readGroupId(obj.id) ||
195
+ readGroupId(obj.group_id) ||
196
+ readGroupId(obj.groupId)
197
+ )
198
+ }
199
+
200
+ const tryFromArray = (arr) => {
201
+ if (!Array.isArray(arr)) return null
202
+ let first = null
203
+ for (let i = 0; i < arr.length; i += 1) {
204
+ const item = arr[i]
205
+ const id = tryFromObj(item)
206
+ if (!id) continue
207
+ if (!first) first = id
208
+ if (item && (item.is_default_org === true || item.isDefaultOrg === true)) return id
209
+ }
210
+ return first
211
+ }
212
+
213
+ if (Array.isArray(groupsJson)) return tryFromArray(groupsJson)
214
+ if (!groupsJson || typeof groupsJson !== "object") return null
215
+
216
+ const direct = tryFromObj(groupsJson)
217
+ if (direct) return direct
218
+
219
+ const keys = ["orgs", "groups", "results", "items", "data"]
220
+ for (let i = 0; i < keys.length; i += 1) {
221
+ const id = tryFromArray(groupsJson[keys[i]])
222
+ if (id) return id
223
+ }
224
+ return null
225
+ }
226
+
227
+ function readBalanceUsd(group) {
228
+ const wrappers = ["apiOrganization", "api_organization", "group", "org", "organization", "data", "result", "item"]
229
+ const keys = ["balance_usd", "balanceUsd", "balance", "pending_balance", "pendingBalance"]
230
+
231
+ const tryNode = (node) => {
232
+ if (!node) return null
233
+ if (Array.isArray(node)) {
234
+ for (let i = 0; i < node.length; i += 1) {
235
+ const n = tryNode(node[i])
236
+ if (n !== null) return n
237
+ }
238
+ return null
239
+ }
240
+ if (typeof node !== "object") return null
241
+
242
+ for (let i = 0; i < wrappers.length; i += 1) {
243
+ const n = tryNode(node[wrappers[i]])
244
+ if (n !== null) return n
245
+ }
246
+
247
+ let n = readNumberField(node, keys)
248
+ if (n !== null) return n
249
+
250
+ for (let i = 0; i < keys.length; i += 1) {
251
+ n = readMoneyLike(node[keys[i]])
252
+ if (n !== null) return n
253
+ }
254
+
255
+ const nested = [node.customerInfo, node.wallet, node.billing, node.usage, node.account, node.balances]
256
+ for (let i = 0; i < nested.length; i += 1) {
257
+ n = tryNode(nested[i])
258
+ if (n !== null) return n
259
+ }
260
+
261
+ for (const k in node) if (/(balance|credit|wallet|prepaid|available)/i.test(k)) {
262
+ n = readMoneyLike(node[k])
263
+ if (n !== null) return n
264
+ }
265
+ return null
266
+ }
267
+
268
+ return tryNode(group)
269
+ }
270
+
271
+ function sumUsageCostUsd(usageAnalytics) {
272
+ if (!Array.isArray(usageAnalytics)) return null
273
+ let costSum = 0
274
+ let hasCost = false
275
+ let hasValidMeter = usageAnalytics.length === 0
276
+ for (let i = 0; i < usageAnalytics.length; i += 1) {
277
+ const meter = usageAnalytics[i]
278
+ if (!meter || typeof meter !== "object") continue
279
+ const summaries = meter.meter_event_summaries || meter.meterEventSummaries
280
+ if (!Array.isArray(summaries)) continue
281
+ if (summaries.length === 0) hasValidMeter = true
282
+ for (let j = 0; j < summaries.length; j += 1) {
283
+ const s = summaries[j]
284
+ if (!s || typeof s !== "object") continue
285
+ const c = Number(s.cost)
286
+ if (Number.isFinite(c)) {
287
+ costSum += c
288
+ hasCost = true
289
+ }
290
+ }
291
+ }
292
+ return (hasCost || hasValidMeter) ? costSum : null
293
+ }
294
+
295
+ function detectPlanLabel(customerInfo) {
296
+ if (!customerInfo || typeof customerInfo !== "object") return null
297
+ if (customerInfo.is_max === true || customerInfo.subscription_tier === "max") return "Max"
298
+ if (customerInfo.is_pro === true) return "Pro"
299
+ return null
300
+ }
301
+
302
+ function buildRateLimitLines(ctx, rateLimits) {
303
+ if (!rateLimits || typeof rateLimits !== "object") return []
304
+ const lines = []
305
+ for (let i = 0; i < RATE_LIMIT_CATEGORIES.length; i += 1) {
306
+ const category = RATE_LIMIT_CATEGORIES[i]
307
+ const val = rateLimits[category.key]
308
+ if (val === undefined || val === null) continue
309
+ const n = Number(val)
310
+ if (!Number.isFinite(n)) continue
311
+ lines.push(ctx.line.text({
312
+ label: category.label,
313
+ value: String(Math.max(0, Math.floor(n))) + " remaining",
314
+ }))
315
+ }
316
+ return lines
317
+ }
318
+
319
+ function queryLocalSessionFromCache(ctx, dbPath) {
320
+ let rows
321
+ try {
322
+ const json = ctx.host.sqlite.query(dbPath, LOCAL_SESSION_SQL)
323
+ rows = ctx.util.tryParseJson(json)
324
+ } catch (e) {
325
+ ctx.host.log.warn("local sqlite read failed (" + dbPath + "): " + String(e))
326
+ return null
327
+ }
328
+ if (!Array.isArray(rows) || rows.length === 0) return null
329
+
330
+ const row = rows[0] || {}
331
+ const requestHex = typeof row.requestHex === "string" ? row.requestHex : null
332
+ if (!requestHex) return null
333
+
334
+ const authToken = extractAuthToken(requestHex)
335
+ if (!authToken) return null
336
+
337
+ const userAgent = extractPrintableField(requestHex, ASK_UA_HEX_PREFIX)
338
+ const appVersion = askAppVersionFromUserAgent(userAgent)
339
+ const deviceId = extractPrintableField(requestHex, MACOS_DEVICE_ID_HEX_PREFIX)
340
+
341
+ return { authToken: authToken, userAgent: userAgent, appVersion: appVersion, deviceId: deviceId, sourcePath: dbPath }
342
+ }
343
+
344
+ function loadLocalSession(ctx) {
345
+ if (ctx.app.platform !== "macos") throw {code: "unsupported", message: "Perplexity app-cache authentication requires the legacy macOS CFNetwork cache. A separate supported web/API method is required; this cache reader cannot import browser cookies."}
346
+ var explicit = ctx.provider && ctx.provider.settings && ctx.provider.settings.cacheDbPath
347
+ var paths = explicit ? [explicit] : LOCAL_CACHE_DB_PATHS
348
+ for (let i = 0; i < paths.length; i += 1) {
349
+ const dbPath = paths[i]
350
+ try {
351
+ if (!ctx.host.fs.exists(dbPath)) continue
352
+ } catch (e) {
353
+ ctx.host.log.warn("local cache exists check failed (" + dbPath + "): " + String(e))
354
+ continue
355
+ }
356
+ const found = queryLocalSessionFromCache(ctx, dbPath)
357
+ if (found) return found
358
+ }
359
+ return null
360
+ }
361
+
362
+ function fetchRestState(ctx, session) {
363
+ const authToken = session && session.authToken
364
+ if (!authToken) return null
365
+ const restHeaders = makeRestHeaderOverrides(session)
366
+ const groups =
367
+ fetchJsonOptional(ctx, REST_GROUPS_ENDPOINT, authToken, restHeaders) ||
368
+ fetchJsonOptional(ctx, REST_GROUPS_ENDPOINT + "/", authToken, restHeaders)
369
+ const groupId = pickGroupId(groups)
370
+ if (!groupId) return null
371
+ const base = REST_API_BASE + "/groups/" + encodeURIComponent(groupId)
372
+ const group =
373
+ fetchJsonOptional(ctx, base, authToken, restHeaders) ||
374
+ fetchJsonOptional(ctx, base + "/", authToken, restHeaders)
375
+ const usageUrl = base + "/usage-analytics"
376
+ const usageAnalytics =
377
+ fetchJsonOptional(ctx, usageUrl, authToken, restHeaders) ||
378
+ fetchJsonOptional(ctx, usageUrl + "/", authToken, restHeaders)
379
+ const rateLimits = fetchJsonOptional(ctx, RATE_LIMIT_ENDPOINT, authToken, restHeaders)
380
+ return { groupId: groupId, group: group, usageAnalytics: usageAnalytics, rateLimits: rateLimits }
381
+ }
382
+
383
+ function probe(ctx) {
384
+ const session = loadLocalSession(ctx)
385
+ if (!session) throw "Not logged in. Sign in via Perplexity app."
386
+ if (session.sourcePath) ctx.host.log.info("using cache db: " + session.sourcePath)
387
+
388
+ const restState = fetchRestState(ctx, session)
389
+ if (!restState || !restState.group) throw "Unable to connect. Try again later."
390
+
391
+ const customerInfo = restState.group && restState.group.customerInfo
392
+ const plan = detectPlanLabel(customerInfo)
393
+
394
+ const dollarLines = []
395
+ const balanceUsd = readBalanceUsd(restState.group)
396
+ if (balanceUsd !== null && balanceUsd > 0) {
397
+ const usedUsd = sumUsageCostUsd(restState.usageAnalytics)
398
+ if (usedUsd !== null) {
399
+ const usedCents = Math.max(0, Math.round(usedUsd * 100))
400
+ const limitCents = Math.max(0, Math.round(balanceUsd * 100))
401
+ if (Number.isFinite(limitCents) && limitCents > 0) {
402
+ dollarLines.push(ctx.line.progress({
403
+ label: "API credits",
404
+ used: usedCents / 100,
405
+ limit: limitCents / 100,
406
+ format: { kind: "dollars" },
407
+ }))
408
+ }
409
+ }
410
+ }
411
+
412
+ const rateLines = buildRateLimitLines(ctx, restState.rateLimits)
413
+
414
+ const lines = dollarLines.concat(rateLines)
415
+ if (lines.length === 0) {
416
+ if (balanceUsd !== null && balanceUsd > 0) throw "Usage data unavailable. Try again later."
417
+ if (plan) throw "Rate limits unavailable. Try again later."
418
+ throw "Balance unavailable. Try again later."
419
+ }
420
+
421
+ return plan ? { plan: plan, lines: lines } : { lines: lines }
422
+ }
423
+
424
+ globalThis.__openusage_plugin = { id: "perplexity", probe: probe }
425
+ })()
@@ -0,0 +1,19 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "perplexity",
4
+ "name": "Perplexity",
5
+ "version": "0.0.2",
6
+ "entry": "plugin.js",
7
+ "icon": "icon.svg",
8
+ "brandColor": "#20808D",
9
+ "supportedModes": ["oauth", "local", "web"],
10
+ "autoMode": "web",
11
+ "webUrl": "https://www.perplexity.ai",
12
+ "lines": [
13
+ { "type": "progress", "label": "API credits", "scope": "overview", "primaryOrder": 1 },
14
+ { "type": "text", "label": "Queries", "scope": "overview" },
15
+ { "type": "text", "label": "Deep Research", "scope": "overview" },
16
+ { "type": "text", "label": "Labs", "scope": "detail" },
17
+ { "type": "text", "label": "Agentic Research", "scope": "detail" }
18
+ ]
19
+ }
@@ -0,0 +1 @@
1
+ <svg fill="#26AD61" fill-rule="evenodd" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Poe</title><path d="M12.003 2C6.48 2 2 6.48 2 12s4.48 10 10.003 10 10.003-4.48 10.003-10S17.526 2 12.003 2zm4.45 13.897c-.397.6-.968.963-1.71.963-.742 0-1.313-.363-1.71-.963-.445-.668-.58-1.457-.41-2.347.172-.892.628-1.605 1.25-2.07.624-.462 1.34-.65 2.124-.65.79 0 1.53.216 2.13.65.64.494 1.11 1.232 1.29 2.14.18.91.04 1.72-.43 2.43-.42.64-1.05 1.03-1.88 1.03-.74 0-1.34-.36-1.75-1.03v2.85H12.003v-3.01l1.67.002c.59.002 1.01.27 1.25.8.26.57.21 1.23-.18 1.95-.4.71-1.07 1.07-2.06 1.07-1 0-1.69-.36-2.1-1.07-.4-.72-.51-1.52-.31-2.39.19-.87.64-1.54 1.34-2.01.7-.47 1.55-.7 2.52-.7.84 0 1.63.18 2.32.52.7.34 1.24.8 1.61 1.34l-1.24.8c-.56-.8-1.5-1.2-2.82-1.2-1.1 0-1.91.35-2.4 1.05-.49.7-.58 1.55-.27 2.57.32 1 .95 1.5 1.88 1.5.93 0 1.61-.4 2.05-1.2.27-.47.37-.97.3-1.5h-3.14v-1.2h4.45z"/></svg>