@hashimkarim/usagestat-linux-x64-gnu 2.0.0 → 2.0.2

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 (39) hide show
  1. package/native-manifest.json +112 -40
  2. package/package.json +13 -1
  3. package/plugins/aixy/plugin.json +1 -1
  4. package/plugins/antigravity/plugin.js +16 -12
  5. package/plugins/antigravity-cli/plugin.js +7 -2
  6. package/plugins/atlascloud/plugin.json +1 -1
  7. package/plugins/bifrost/plugin.json +1 -1
  8. package/plugins/cursor/plugin.js +24 -6
  9. package/plugins/devpass/plugin.json +1 -1
  10. package/plugins/gitkraken/plugin.json +1 -1
  11. package/plugins/helmcode/plugin.js +1 -1
  12. package/plugins/helmcode/plugin.json +2 -2
  13. package/plugins/huggingface/plugin.json +1 -1
  14. package/plugins/hyper/plugin.json +1 -1
  15. package/plugins/lithosai/LICENSE +21 -0
  16. package/plugins/lithosai/icon.svg +1 -0
  17. package/plugins/lithosai/plugin.js +125 -0
  18. package/plugins/lithosai/plugin.json +25 -0
  19. package/plugins/llmman/plugin.json +1 -1
  20. package/plugins/muse/plugin.js +93 -129
  21. package/plugins/muse/plugin.json +2 -2
  22. package/plugins/museai/LICENSE +21 -0
  23. package/plugins/museai/icon.svg +3 -0
  24. package/plugins/museai/plugin.js +154 -0
  25. package/plugins/museai/plugin.json +25 -0
  26. package/plugins/nous/plugin.json +1 -1
  27. package/plugins/opencode-go/plugin.js +59 -22
  28. package/plugins/raycast/plugin.json +1 -1
  29. package/plugins/replicate/plugin.json +1 -1
  30. package/plugins/typesafe/plugin.json +1 -1
  31. package/plugins/v0/plugin.json +1 -1
  32. package/plugins/vercel/plugin.json +1 -1
  33. package/plugins/workbuddy/LICENSE +21 -0
  34. package/plugins/workbuddy/icon.svg +1 -0
  35. package/plugins/workbuddy/plugin.js +227 -0
  36. package/plugins/workbuddy/plugin.json +33 -0
  37. package/plugins/xkiro/plugin.json +1 -1
  38. package/usagestat +0 -0
  39. package/usagestatd +0 -0
@@ -0,0 +1,154 @@
1
+ // Generated by tools/vendor-codexbar-providers.cjs; see LICENSE.
2
+ globalThis.__usagestat_bundled_options = {"labels":["Weekly","Additional tokens","Additional"],"defaults":{},"autoMode":"web","maxRequests":160};
3
+ defineProvider({
4
+ id: "museai",
5
+ name: "Muse (muse.ai)",
6
+ endpoints: ["https://muse.ai"],
7
+ settings: [],
8
+ capabilities: ["browser-cookies", "http-status", "persistent-storage"],
9
+ cookieDomains: ["muse.ai"],
10
+ async fetchUsage(ctx) {
11
+ const origin = "https://muse.ai";
12
+ const userAgent =
13
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36";
14
+ const policy = ctx.browser.availability("muse.ai");
15
+ if (policy === "off") throw ctx.fail.missingCredential("muse.ai cookies are disabled.");
16
+ const signedOut = (r) =>
17
+ r.status === 401 || r.status === 403 || (r.headers["location"] ?? "").startsWith("https://auth.muse.ai/");
18
+ const staleAction = (r) => r.status === 404 && /server action not found/i.test(r.bodyText);
19
+ const discoveryFailure = () =>
20
+ ctx.fail.parseFailure(
21
+ "Could not find a working muse.ai subscription action. muse.ai may have changed. Refresh to retry.",
22
+ );
23
+ const post = (cookie, actionID) =>
24
+ ctx.http.post(`${origin}/`, {
25
+ body: [{ includeAgreement: true }],
26
+ headers: {
27
+ Cookie: cookie,
28
+ Accept: "text/x-component",
29
+ "Next-Action": actionID,
30
+ Origin: origin,
31
+ // muse.ai answers 403 to server actions that do not look like same-origin browser fetches.
32
+ "Sec-Fetch-Site": "same-origin",
33
+ "Sec-Fetch-Mode": "cors",
34
+ "Sec-Fetch-Dest": "empty",
35
+ "User-Agent": userAgent,
36
+ },
37
+ });
38
+ const chunkPaths = (text) => [
39
+ ...new Set((text.match(/static\/chunks\/[\w~.-]+\.js/g) ?? []).map((p) => `/_next/${p}`)),
40
+ ];
41
+ const fetchChunks = (paths) =>
42
+ Promise.all(
43
+ paths.map((path) =>
44
+ ctx.http
45
+ .get(`${origin}${path}`, { headers: { Accept: "*/*", "User-Agent": userAgent } })
46
+ .then((r) => (r.status === 200 ? r.bodyText : ""))
47
+ .catch(() => ""),
48
+ ),
49
+ );
50
+ // The action ID changes on every deploy. The app chunk preloads settings through
51
+ // `e.A(<module>).then(({preloadHatchSettingsData…` and the loader chunk lists that module's files, one of which
52
+ // holds `createServerReference("<id>", …, "fetchSubscriptionAction")`.
53
+ // Returns undefined when muse.ai treats the session as signed out.
54
+ const discover = async (cookie) => {
55
+ const page = await ctx.http.get(`${origin}/`, {
56
+ headers: { Cookie: cookie, Accept: "text/html", "User-Agent": userAgent },
57
+ });
58
+ if (signedOut(page)) return undefined;
59
+ // Fetch page chunks a few at a time and stop once the settings loader entry turns up.
60
+ if (page.status !== 200) throw ctx.fail.apiFailure(`muse.ai returned HTTP ${page.status}.`);
61
+ const paths = chunkPaths(page.bodyText).slice(0, 96);
62
+ let text = "";
63
+ for (let i = 0; i < paths.length; i += 8) {
64
+ text += "\n" + (await fetchChunks(paths.slice(i, i + 8))).join("\n");
65
+ if (text.length > 8 * 1024 * 1024) throw discoveryFailure();
66
+ const module = /\.A\((\d+)\)\.then\(\(\{[^}]*Settings/.exec(text)?.[1];
67
+ const loader =
68
+ module && new RegExp(`[,{\\[]${module},\\w+=>\\{\\w+\\.v\\(\\w+=>Promise\\.all\\(\\[([^\\]]*)`).exec(text);
69
+ if (!loader) continue;
70
+ const settingsPaths = chunkPaths(loader[1]).slice(0, 32);
71
+ for (let j = 0; j < settingsPaths.length; j += 8) {
72
+ for (const body of await fetchChunks(settingsPaths.slice(j, j + 8))) {
73
+ const id = /"([0-9a-f]{40,})",[^"]{0,200}"fetchSubscriptionAction"/.exec(body)?.[1];
74
+ if (id) return id;
75
+ }
76
+ }
77
+ break;
78
+ }
79
+ throw discoveryFailure();
80
+ };
81
+
82
+ let actionID = ctx.storage.get("actionID");
83
+ const subscription = async (cookie) => {
84
+ const response = actionID ? await post(cookie, actionID) : undefined;
85
+ if (response && !staleAction(response)) {
86
+ return signedOut(response) ? undefined : response;
87
+ }
88
+ ctx.storage.remove("actionID");
89
+ actionID = await discover(cookie);
90
+ if (!actionID) return undefined;
91
+ const fresh = await post(cookie, actionID);
92
+ if (staleAction(fresh)) throw discoveryFailure();
93
+ return signedOut(fresh) ? undefined : fresh;
94
+ };
95
+
96
+ // Try each browser session, rejecting expired ones so a signed-in browser later in the order still works.
97
+ let response;
98
+ let rejected = false;
99
+ for await (const session of ctx.browser.sessions("muse.ai")) {
100
+ response = await subscription(session.header);
101
+ if (response) break;
102
+ rejected = true;
103
+ ctx.browser.rejectCookie("muse.ai", session);
104
+ if (policy === "manual") break;
105
+ }
106
+ if (!response) {
107
+ throw rejected
108
+ ? ctx.fail.authenticationExpired("muse.ai session expired. Sign in at muse.ai and refresh.")
109
+ : ctx.fail.missingCredential(
110
+ `No muse.ai session found. Sign in at muse.ai. Supported browsers: ${ctx.browser.supportedBrowsers}. Or set a manual Cookie header.`,
111
+ );
112
+ }
113
+ if (response.status !== 200) throw ctx.fail.apiFailure(`muse.ai returned HTTP ${response.status}.`);
114
+
115
+ // React flight rows look like `1:{...}`; the subscription is the row carrying `success`.
116
+ let sub;
117
+ for (const line of response.bodyText.split("\n")) {
118
+ try {
119
+ const row = JSON.parse(/^[0-9a-f]+:(\{.*\})\s*$/.exec(line)?.[1] ?? "null");
120
+ if (row?.success === true) sub = row.subscription;
121
+ } catch {}
122
+ }
123
+ if (typeof sub?.usage?.percentUsed !== "number") throw ctx.fail.parseFailure("Unexpected muse.ai response.");
124
+
125
+ ctx.storage.set("actionID", actionID);
126
+ const seconds = (value) => (typeof value === "number" ? ctx.date.unixSeconds(value) : undefined);
127
+ const tokensLeft = (label) => /\(([^()]+ tokens left)\)/.exec(label ?? "")?.[1];
128
+ // Top-ups (purchased or from referrals) never expire and sit outside the weekly allowance. muse.ai reports
129
+ // the balance in micro-dollars, so the fallback without its label is a dollar amount.
130
+ const { topupBalance: balance, topupTotal: total } = sub;
131
+ const topup =
132
+ typeof balance === "number" && typeof total === "number" && total > 0
133
+ ? {
134
+ label: sub.topupRowLabel ?? "Additional tokens",
135
+ value:
136
+ tokensLeft(sub.topupRowValueLabel) ?? sub.topupRowValueLabel ?? `${ctx.format.usd(balance / 1e6)} left`,
137
+ progress: Math.min(1, Math.max(0, (total - balance) / total)),
138
+ }
139
+ : undefined;
140
+ return {
141
+ primary: {
142
+ usedPercent: Math.min(100, Math.max(0, sub.usage.percentUsed)),
143
+ windowMinutes: 7 * 24 * 60,
144
+ resetsAt: seconds(sub.usage.resetsAt),
145
+ // "3% used (2.8B tokens left)" is the only absolute amount muse.ai reports. Free plans omit it.
146
+ resetDescription: tokensLeft(sub.usageRowValueLabel),
147
+ },
148
+ details: topup ? [{ rows: [topup] }] : undefined,
149
+ subscriptionRenewsAt: seconds(sub.agreement?.currentPeriodEndTime),
150
+ identity: { loginMethod: sub.tier?.name ?? sub.usageRowLabel },
151
+ dataConfidence: "percentOnly",
152
+ };
153
+ },
154
+ });
@@ -0,0 +1,25 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "museai",
4
+ "name": "Muse (muse.ai)",
5
+ "version": "0.1.0",
6
+ "entry": "plugin.js",
7
+ "enabledByDefault": false,
8
+ "supportedModes": [
9
+ "web"
10
+ ],
11
+ "autoMode": "web",
12
+ "usageDashboardUrl": "https://muse.ai",
13
+ "webUrl": "https://muse.ai",
14
+ "envVars": [
15
+ "MUSEAI_COOKIE"
16
+ ],
17
+ "setupHelp": "Use a session Cookie header from muse.ai. This provider is separate from Muse Code and does not use its CLI login.",
18
+ "upstream": {
19
+ "repository": "https://github.com/steipete/CodexBar",
20
+ "revision": "6ba06fe9ec1522dd5c59e16810115bce28aa148f",
21
+ "path": "Sources/CodexBarCore/Resources/Plugins/museai.js",
22
+ "sha256": "ab325a1b6cff46bbe347d98340d9212cab732f6b48c0d2ae5c6d955824ac5019",
23
+ "license": "MIT"
24
+ }
25
+ }
@@ -16,7 +16,7 @@
16
16
  ],
17
17
  "upstream": {
18
18
  "repository": "https://github.com/steipete/CodexBar",
19
- "revision": "25bba9b7fd9ce83c33053958f7366e23b2dc8a82",
19
+ "revision": "6ba06fe9ec1522dd5c59e16810115bce28aa148f",
20
20
  "path": "Sources/CodexBarCore/Resources/Plugins/nous.js",
21
21
  "sha256": "160f5329bf4895a061d3bed74d0476283d3abad47e071fb868d667c0c5cb485e",
22
22
  "license": "MIT"
@@ -12,17 +12,20 @@
12
12
  monthly: 60,
13
13
  };
14
14
 
15
- const HISTORY_EXISTS_SQL = `
16
- SELECT 1 AS present
17
- FROM message
18
- WHERE json_valid(data)
19
- AND json_extract(data, '$.providerID') = 'opencode-go'
20
- AND json_extract(data, '$.role') = 'assistant'
21
- LIMIT 1
22
- `;
23
-
24
- const HISTORY_ROWS_SQL = `
15
+ const TABLES_SQL = "SELECT name FROM sqlite_master WHERE type = 'table' AND name IN ('message','session_message','credential')";
16
+ const CREDENTIAL_SQL = `SELECT value FROM credential
17
+ WHERE integration_id = 'opencode-go' AND (active IS NULL OR active = 1)
18
+ ORDER BY active DESC, time_updated DESC, id DESC LIMIT 1`;
19
+ function historySQL(tables) {
20
+ const sources = [];
21
+ if (tables.includes('session_message')) sources.push(`SELECT id, time_created, data FROM session_message
22
+ WHERE json_valid(data) AND (type = 'assistant' OR (type = 'compaction' AND json_extract(data,'$.status') = 'completed'))`);
23
+ if (tables.includes('message')) sources.push(`SELECT id, time_created, data FROM message
24
+ WHERE json_valid(data) AND json_extract(data,'$.role') = 'assistant'`);
25
+ if (!sources.length) return null;
26
+ return `
25
27
  SELECT
28
+ id,
26
29
  CAST(COALESCE(json_extract(data, '$.time.created'), time_created) AS INTEGER) AS createdMs,
27
30
  json_extract(data, '$.cost') AS cost,
28
31
  json_extract(data, '$.tokens.input') AS inputTokens,
@@ -30,11 +33,10 @@
30
33
  json_extract(data, '$.tokens.cache.read') AS cacheReadTokens,
31
34
  json_extract(data, '$.tokens.cache.write') AS cacheCreationTokens,
32
35
  json_extract(data, '$.tokens.reasoning') AS reasoningOutputTokens
33
- FROM message
34
- WHERE json_valid(data)
35
- AND json_extract(data, '$.providerID') = 'opencode-go'
36
- AND json_extract(data, '$.role') = 'assistant'
36
+ FROM (${sources.join(' UNION ALL ')})
37
+ WHERE COALESCE(json_extract(data, '$.model.providerID'), json_extract(data, '$.providerID')) = 'opencode-go'
37
38
  `;
39
+ }
38
40
 
39
41
  function readNumber(value) {
40
42
  if (typeof value !== "number" && typeof value !== "string") return null;
@@ -317,7 +319,7 @@
317
319
  }
318
320
  return { ok: true, rows };
319
321
  } catch (e) {
320
- ctx.host.log.warn("sqlite query failed: " + String(e));
322
+ ctx.host.log.warn("OpenCode history could not be read");
321
323
  return { ok: false, rows: [] };
322
324
  }
323
325
  }
@@ -326,6 +328,30 @@
326
328
  const configured = trim(ctx.provider && ctx.provider.apiKey) || env(ctx, "OPENCODE_GO_API_KEY");
327
329
  if (configured) return configured;
328
330
  const inline = ctx.host.env.get("OPENCODE_AUTH_CONTENT");
331
+ // A v2 credential table is authoritative even after logout. The one-time
332
+ // imported auth.json must never revive a deleted or inactive account.
333
+ if (!inline) {
334
+ const database = databasePath(ctx);
335
+ if (ctx.host.fs.exists(database)) {
336
+ try {
337
+ const tables = ctx.util.tryParseJson(ctx.host.sqlite.query(database, TABLES_SQL));
338
+ if (!Array.isArray(tables)) throw new Error('Invalid schema');
339
+ if (tables.some(table => table.name === 'credential')) {
340
+ const rows = ctx.util.tryParseJson(ctx.host.sqlite.query(database, CREDENTIAL_SQL));
341
+ if (!Array.isArray(rows)) throw new Error('Invalid credential result');
342
+ if (!rows.length) return null;
343
+ const entry = ctx.util.tryParseJson(rows[0].value);
344
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry) ||
345
+ (entry.key !== undefined && typeof entry.key !== 'string'))
346
+ throw {code:'credential-malformed', message:'OpenCode credential row is malformed.'};
347
+ return trim(entry.key);
348
+ }
349
+ } catch (error) {
350
+ if (error && error.code) throw error;
351
+ throw {code:'credential-unavailable',message:'OpenCode credential database cannot be read. Its imported auth file was not used.'};
352
+ }
353
+ }
354
+ }
329
355
  const path = inline ? null : dataPath(ctx, "auth.json");
330
356
  if (!inline && !ctx.host.fs.exists(path)) return null;
331
357
 
@@ -334,8 +360,7 @@
334
360
  const parsed = ctx.util.tryParseJson(text);
335
361
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
336
362
  if (inline) throw {code: "credential-malformed", message: "OPENCODE_AUTH_CONTENT must contain an auth JSON object."};
337
- ctx.host.log.warn("opencode auth file is not valid json");
338
- return null;
363
+ throw {code:'credential-malformed',message:'OpenCode auth file is not valid JSON.'};
339
364
  }
340
365
  const entry = parsed[PROVIDER_ID];
341
366
  if (!entry || typeof entry !== "object") return null;
@@ -343,25 +368,37 @@
343
368
  return key || null;
344
369
  } catch (e) {
345
370
  if (e && e.code) throw e;
346
- ctx.host.log.warn("opencode auth read failed: " + String(e));
347
- return null;
371
+ throw {code:'credential-unavailable',message:'OpenCode auth file cannot be read.'};
348
372
  }
349
373
  }
350
374
 
351
375
  function hasHistory(ctx) {
352
- const result = queryRows(ctx, HISTORY_EXISTS_SQL);
376
+ const result = queryHistory(ctx, true);
353
377
  if (!result.ok) return { ok: false, present: false };
354
378
  return { ok: true, present: result.rows.length > 0 };
355
379
  }
356
380
 
381
+ function queryHistory(ctx, probeOnly) {
382
+ const schema = queryRows(ctx, TABLES_SQL);
383
+ if (!schema.ok) return schema;
384
+ const sql = historySQL(schema.rows.map(table => table.name));
385
+ if (!sql) return {ok:true,rows:[]};
386
+ return queryRows(ctx, probeOnly ? 'SELECT 1 AS present FROM (' + sql + ') LIMIT 1' : sql);
387
+ }
388
+
357
389
  function loadHistory(ctx) {
358
- const result = queryRows(ctx, HISTORY_ROWS_SQL);
390
+ const result = queryHistory(ctx, false);
359
391
  if (!result.ok) return result;
360
392
 
361
393
  const rows = [];
394
+ const seen = new Set();
362
395
  for (let i = 0; i < result.rows.length; i += 1) {
363
396
  const row = result.rows[i];
364
397
  if (!row || typeof row !== "object") continue;
398
+ if (trim(row.id)) {
399
+ if (seen.has(row.id)) continue;
400
+ seen.add(row.id);
401
+ }
365
402
  const createdMs = readNumber(row.createdMs);
366
403
  const cost = readNumber(row.cost);
367
404
  if (createdMs === null || createdMs <= 0 || createdMs > Date.parse(ctx.nowIso)) continue;
@@ -608,7 +645,7 @@
608
645
  if (!cookie) throw "OpenCode Go session cookie is missing.";
609
646
  return fetchWebResult(ctx, cookie);
610
647
  }
611
- const authKey = loadAuthKey(ctx);
648
+ const authKey = source === 'local' && !ctx.host.env.get('OPENCODE_AUTH_CONTENT') ? null : loadAuthKey(ctx);
612
649
  if (source === "api" && !authKey) throw "OpenCode Go API key missing. Set OPENCODE_GO_API_KEY.";
613
650
  if (authKey && source !== "local") {
614
651
  const result = fetchApiResult(ctx, authKey);
@@ -16,7 +16,7 @@
16
16
  ],
17
17
  "upstream": {
18
18
  "repository": "https://github.com/steipete/CodexBar",
19
- "revision": "25bba9b7fd9ce83c33053958f7366e23b2dc8a82",
19
+ "revision": "6ba06fe9ec1522dd5c59e16810115bce28aa148f",
20
20
  "path": "Sources/CodexBarCore/Resources/Plugins/raycast.js",
21
21
  "sha256": "df8f6d185cb67fd9ec9b622d2321a6d4e5395576b9ab54b1725aac272c95ecb7",
22
22
  "license": "MIT"
@@ -16,7 +16,7 @@
16
16
  ],
17
17
  "upstream": {
18
18
  "repository": "https://github.com/steipete/CodexBar",
19
- "revision": "25bba9b7fd9ce83c33053958f7366e23b2dc8a82",
19
+ "revision": "6ba06fe9ec1522dd5c59e16810115bce28aa148f",
20
20
  "path": "Sources/CodexBarCore/Resources/Plugins/replicate.js",
21
21
  "sha256": "d240c36fa142ee91ce90ae52a2c6814613d2d5533a00c8a4c7ecec2f9386006c",
22
22
  "license": "MIT"
@@ -16,7 +16,7 @@
16
16
  ],
17
17
  "upstream": {
18
18
  "repository": "https://github.com/steipete/CodexBar",
19
- "revision": "25bba9b7fd9ce83c33053958f7366e23b2dc8a82",
19
+ "revision": "6ba06fe9ec1522dd5c59e16810115bce28aa148f",
20
20
  "path": "Sources/CodexBarCore/Resources/Plugins/typesafe.js",
21
21
  "sha256": "eb3941346a966b3f5f9e0010f17edaeed7dfb6c8b0331319a86f26da10d51980",
22
22
  "license": "MIT"
@@ -16,7 +16,7 @@
16
16
  ],
17
17
  "upstream": {
18
18
  "repository": "https://github.com/steipete/CodexBar",
19
- "revision": "25bba9b7fd9ce83c33053958f7366e23b2dc8a82",
19
+ "revision": "6ba06fe9ec1522dd5c59e16810115bce28aa148f",
20
20
  "path": "Sources/CodexBarCore/Resources/Plugins/v0.js",
21
21
  "sha256": "5f54a9329dca35d59d76e427c34d4d9db8d3400d0b38c5fcee8899928a900019",
22
22
  "license": "MIT"
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "upstream": {
17
17
  "repository": "https://github.com/steipete/CodexBar",
18
- "revision": "25bba9b7fd9ce83c33053958f7366e23b2dc8a82",
18
+ "revision": "6ba06fe9ec1522dd5c59e16810115bce28aa148f",
19
19
  "path": "Sources/CodexBarCore/Resources/Plugins/vercel.js",
20
20
  "sha256": "27b39f42709e93a2cfbf850171f198069553c99967f9b7ba6d16614862cb084a",
21
21
  "license": "MIT"
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Peter Steinberger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M2 5h3.2l2.3 9.2L10.2 5h3.6l2.7 9.2L18.8 5H22l-4 14h-3.2L12 9.6 9.2 19H6z"/></svg>
@@ -0,0 +1,227 @@
1
+ // Generated by tools/vendor-codexbar-providers.cjs; see LICENSE.
2
+ globalThis.__usagestat_bundled_options = {"labels":["Credits","Credits","Additional"],"defaults":{},"autoMode":"web"};
3
+ function _nullishCoalesce(lhs, rhsFn) {
4
+ if (lhs != null) {
5
+ return lhs;
6
+ } else {
7
+ return rhsFn();
8
+ }
9
+ }
10
+ defineProvider({
11
+ id: "workbuddy",
12
+ name: "WorkBuddy",
13
+ endpoints: ["https://www.workbuddy.cn"],
14
+ settings: [
15
+ { key: "webTimeoutSeconds", title: "Web timeout", type: "plain" },
16
+ { key: "resetDeadlineMillis", title: "Reset lookup deadline", type: "plain" },
17
+ { key: "chromeMajorVersion", title: "Chrome major version", type: "plain" },
18
+ ],
19
+ capabilities: ["browser-cookies", "http-status"],
20
+ cookieDomains: ["workbuddy.cn", "www.workbuddy.cn"],
21
+ cookiePolicy: {
22
+ selection: "request-url",
23
+ cache: "validated-single-entry",
24
+ imports: "access-gated",
25
+ },
26
+ async fetchUsage(ctx) {
27
+ const origin = "https://www.workbuddy.cn";
28
+ const domain = "www.workbuddy.cn";
29
+ const policy = ctx.browser.availability(domain);
30
+ if (policy === "off") throw ctx.fail.missingCredential("WorkBuddy cookies are disabled.");
31
+ const timeoutRaw = Number(_nullishCoalesce(ctx.settings.get("webTimeoutSeconds"), () => "15"));
32
+ const timeoutSeconds = Number.isFinite(timeoutRaw) ? Math.min(30, Math.max(1, Math.round(timeoutRaw))) : 15;
33
+ // WorkBuddy binds the session to the browser User-Agent, and Chrome's reduced UA varies only by major version.
34
+ // The previous major covers an installed update that Chrome has not relaunched into yet.
35
+ const major = Number(_nullishCoalesce(ctx.settings.get("chromeMajorVersion"), () => ""));
36
+ const userAgent = (version) =>
37
+ `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version}.0.0.0 Safari/537.36`;
38
+ const userAgents =
39
+ Number.isSafeInteger(major) && major > 1 ? [userAgent(major), userAgent(major - 1)] : [undefined];
40
+
41
+ const fail = (field) => {
42
+ throw ctx.fail.parseFailure(`Invalid WorkBuddy billing response: ${field}`);
43
+ };
44
+ const object = (value, field) =>
45
+ value !== null && typeof value === "object" && !Array.isArray(value) ? value : fail(field);
46
+ // Billing amounts arrive as decimal strings ("500"); numbers are accepted for robustness.
47
+ const amount = (value, field) => {
48
+ if (typeof value === "number") return Number.isFinite(value) && value >= 0 ? value : fail(field);
49
+ if (typeof value !== "string" || !/^\d+(?:\.\d+)?$/.test(value.trim())) return fail(field);
50
+ const parsed = Number(value.trim());
51
+ return Number.isFinite(parsed) ? parsed : fail(field);
52
+ };
53
+ const text = (value) => (typeof value === "string" ? value.trim() || undefined : undefined);
54
+ const format = (value) => ctx.format.number(value, { maximumFractionDigits: 2 });
55
+ const decode = (bodyText, field) => {
56
+ let value;
57
+ try {
58
+ value = JSON.parse(bodyText);
59
+ } catch (error) {
60
+ void error;
61
+ return fail(field);
62
+ }
63
+ return object(value, field);
64
+ };
65
+
66
+ const sessionExpired = ctx.fail.authenticationExpired(
67
+ "WorkBuddy session expired. Sign in at www.workbuddy.cn or paste a fresh Cookie header.",
68
+ );
69
+ const post = async (session, agent, path, body, requestTimeout = timeoutSeconds) => {
70
+ const headers = {
71
+ Accept: "application/json",
72
+ Origin: origin,
73
+ Referer: `${origin}/profile/plans-usage`,
74
+ };
75
+ if (agent) headers["User-Agent"] = agent;
76
+ const response = await ctx.http.post(`${origin}${path}`, {
77
+ body,
78
+ timeoutSeconds: requestTimeout,
79
+ cookieSession: session.id,
80
+ headers,
81
+ });
82
+ if (response.status === 401) throw sessionExpired;
83
+ if (response.status === 403) throw ctx.fail.permissionDenied("WorkBuddy denied access to billing data.");
84
+ if (response.status === 429) throw ctx.fail.rateLimited("WorkBuddy billing requests are rate limited.");
85
+ if (response.status >= 500) {
86
+ throw ctx.fail.providerUnavailable(`WorkBuddy billing API returned HTTP ${response.status}.`);
87
+ }
88
+ if (response.status !== 200) throw ctx.fail.apiFailure(`WorkBuddy billing API returned HTTP ${response.status}.`);
89
+ const root = decode(response.bodyText, "expected a JSON object");
90
+ if (root.code !== 0) {
91
+ throw ctx.fail.apiFailure(`WorkBuddy billing API returned code ${String(root.code)}.`);
92
+ }
93
+ return object(root.data, "data");
94
+ };
95
+
96
+ // Cycle timestamps carry no zone. The billing cycle follows China Standard Time calendar months.
97
+ const cycleEnd = (value) => {
98
+ const raw = text(value);
99
+ if (!raw) return undefined;
100
+ const match = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/.exec(raw);
101
+ if (!match) return undefined;
102
+ const end = ctx.date.iso(`${match[1]}T${match[2]}+08:00`);
103
+ // The dashboard refreshes one second after the inclusive end time.
104
+ return new Date(end.getTime() + 1000);
105
+ };
106
+ // Package codes the WorkBuddy client sends for its paid and free listings (WorkBuddy 5.6.2).
107
+ const paidPackageCodes = [
108
+ "TCACA_code_002_AkiJS3ZHF5",
109
+ "TCACA_code_005_maRGyrHhw1",
110
+ "TCACA_code_003_FAnt7lcmRT",
111
+ "TCACA_code_023_4xbGhMrE6q",
112
+ "TCACA_code_026_BaESVICNoi",
113
+ "TCACA_code_027_0FCGVA6vSa",
114
+ "TCACA_code_009_0XmEQc2xOf",
115
+ "TCACA_code_038_OhvqZtiPKr",
116
+ "TCACA_code_036_lupO5WgNdG",
117
+ ];
118
+ const freePackageCodes = [
119
+ "TCACA_code_001_PqouKr6QWV",
120
+ "TCACA_code_008_cfWoLwvjU4",
121
+ "TCACA_code_035_ArVxJcGDsm",
122
+ "TCACA_code_006_DbXS0lrypC",
123
+ "TCACA_code_039_KRcQj7wUat",
124
+ "TCACA_code_040_mi9rCYg46x",
125
+ "TCACA_code_007_nzdH5h4Nl0",
126
+ "TCACA_code_028_NtpWi0jzXs",
127
+ "TCACA_code_037_WxOD3MpI2o",
128
+ "TCACA_code_029_6wCGEWquYy",
129
+ "TCACA_code_030_BjSt89qTvr",
130
+ ];
131
+ // Reset time is optional: a failed or unexpected package listing must not discard the balance.
132
+ const nextReset = async (session, agent) => {
133
+ const now = ctx.date.now().getTime();
134
+ // Share five seconds across listings, retaining the host's margin for publishing the balance.
135
+ const hostDeadline = Number(ctx.settings.get("resetDeadlineMillis"));
136
+ const deadline = Math.min(Date.now() + 5000, hostDeadline > 0 ? hostDeadline : Infinity);
137
+ let earliest;
138
+ for (const [path, codes] of [
139
+ ["/billing/meter/get-user-resource-paid-packages", paidPackageCodes],
140
+ ["/billing/meter/get-user-resource-free-packages", freePackageCodes],
141
+ ]) {
142
+ const budget = (deadline - Date.now()) / 1000;
143
+ if (budget < 1) break;
144
+ try {
145
+ // The listings reject requests without package codes (HTTP 400, code 10001). Status 0 is valid, 3 used up.
146
+ const page = await post(
147
+ session,
148
+ agent,
149
+ path,
150
+ { PageNumber: 1, PageSize: 100, PackageCodes: codes, Status: [0, 3] },
151
+ Math.min(timeoutSeconds, budget),
152
+ );
153
+ if (!Array.isArray(page.Accounts)) continue;
154
+ for (const raw of page.Accounts) {
155
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
156
+ const account = raw;
157
+ if (account.CapacityUnit !== "credits") continue;
158
+ const end = cycleEnd(account.CycleEndTime);
159
+ if (end && end.getTime() > now && (!earliest || end.getTime() < earliest.getTime())) earliest = end;
160
+ }
161
+ } catch (error) {
162
+ const failure = error;
163
+ if (failure.transportClass === "cancelled") throw error;
164
+ }
165
+ }
166
+ return earliest;
167
+ };
168
+
169
+ let rejected = false;
170
+ for await (const session of ctx.browser.sessions(domain)) {
171
+ let summary;
172
+ let agent;
173
+ for (const candidate of userAgents) {
174
+ try {
175
+ summary = await post(session, candidate, "/billing/meter/get-user-resource-summary", {});
176
+ agent = candidate;
177
+ break;
178
+ } catch (error) {
179
+ if (error !== sessionExpired) throw error;
180
+ }
181
+ }
182
+ if (!summary) {
183
+ rejected = true;
184
+ ctx.browser.rejectCookie(domain, session);
185
+ if (policy === "manual") break;
186
+ continue;
187
+ }
188
+
189
+ if (!Array.isArray(summary.Packages)) return fail("Packages");
190
+ // The host caches an imported session only after the billing API accepts it.
191
+ if (policy !== "manual") ctx.browser.acceptCookie(domain, session);
192
+ let total = 0;
193
+ let remaining = 0;
194
+ let frozen = 0;
195
+ for (const raw of summary.Packages) {
196
+ const item = object(raw, "package");
197
+ if (item.CapacityUnit !== "credits") continue;
198
+ total += amount(item.CycleTotalCapacity, "CycleTotalCapacity");
199
+ remaining += amount(item.CycleRemainCapacity, "CycleRemainCapacity");
200
+ if (item.CycleFrozenCapacity !== undefined) frozen += amount(item.CycleFrozenCapacity, "CycleFrozenCapacity");
201
+ }
202
+ const plan = text(summary.SubscriptionPackageName);
203
+ // Frozen credits are listed as reported; the meter uses the remaining amount the dashboard shows.
204
+ const rows = frozen > 0 ? [{ label: "Reserved", value: format(frozen) }] : [];
205
+ if (total <= 0) {
206
+ rows.unshift({ label: "Left", value: format(remaining) }, { label: "Total", value: format(total) });
207
+ }
208
+ return {
209
+ primary:
210
+ total > 0
211
+ ? {
212
+ usedPercent: ctx.pct(Math.max(0, total - remaining), total),
213
+ resetsAt: await nextReset(session, agent),
214
+ resetDescription: `${format(remaining)} / ${format(total)} credits left`,
215
+ }
216
+ : undefined,
217
+ details: rows.length ? [{ title: "Credits", rows }] : undefined,
218
+ identity: plan ? { loginMethod: plan } : undefined,
219
+ dataConfidence: "exact",
220
+ };
221
+ }
222
+ if (rejected) throw sessionExpired;
223
+ throw ctx.fail.missingCredential(
224
+ `No WorkBuddy session cookies found. Sign in at www.workbuddy.cn (${ctx.browser.supportedBrowsers}) or paste a Cookie header.`,
225
+ );
226
+ },
227
+ });