tokentracker-cli 0.84.5 → 0.84.7

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 (120) hide show
  1. package/README.de.md +10 -8
  2. package/README.ja.md +9 -7
  3. package/README.ko.md +9 -7
  4. package/README.md +12 -10
  5. package/README.zh-CN.md +9 -7
  6. package/dashboard/dist/assets/{AchievementBadge-DJvRfvu1.js → AchievementBadge-DAPsTMsx.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-Cr8WToxS.js → AchievementsPage-B9oXfXvm.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-BCPYB46e.js → AchievementsSection-DI_LTZb3.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-D-12Ek-H.js → BadgeDetailModal--WCrJWVa.js} +1 -1
  10. package/dashboard/dist/assets/Card-BzbSN3EC.js +1 -0
  11. package/dashboard/dist/assets/{ClawdAnimated-DoGgAppT.js → ClawdAnimated-Dgxae1At.js} +1 -1
  12. package/dashboard/dist/assets/{CommandPalette-DIKocEAD.js → CommandPalette-BiZBYV5A.js} +1 -1
  13. package/dashboard/dist/assets/{CommunityStatsModal-BNuMbEWy.js → CommunityStatsModal-C6jZx10_.js} +1 -1
  14. package/dashboard/dist/assets/{ConfirmModal-F2gPIIS9.js → ConfirmModal-lD-mHMvO.js} +1 -1
  15. package/dashboard/dist/assets/{DashboardPage-YvoCEGG9.js → DashboardPage-DCPNoiRG.js} +1 -1
  16. package/dashboard/dist/assets/{DevicePage-CbC_J22s.js → DevicePage-DiF2IOjJ.js} +1 -1
  17. package/dashboard/dist/assets/{DialogClose-VEynQCYg.js → DialogClose-Crx2EFnI.js} +1 -1
  18. package/dashboard/dist/assets/DialogDescription-CHsHo924.js +1 -0
  19. package/dashboard/dist/assets/{DialogTitle-BbAVHI_v.js → DialogTitle-amHXttsp.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-CZVqSnis.js → FadeIn-CIhF_9PP.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-Cs_h332Y.js → HeaderGithubStar-D6Ph2GuY.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-DvvHTphK.js → HoverTooltip-BS8eO6tj.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-DPVmqSS_.js → IpCheckPage-Cz1Je6WS.js} +1 -1
  24. package/dashboard/dist/assets/LandingPage-DqeXJfwn.js +2 -0
  25. package/dashboard/dist/assets/{LeaderboardAvatar-BAPZ2ROX.js → LeaderboardAvatar-D09yLoVA.js} +1 -1
  26. package/dashboard/dist/assets/LeaderboardPage-BetpFetS.js +2 -0
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-r03akfe2.js → LeaderboardProfileModal-UW3KnANZ.js} +4 -4
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-CU63YW2M.js → LeaderboardProfilePage-Chd_2AVY.js} +1 -1
  29. package/dashboard/dist/assets/LimitsPage-uhpNteXJ.js +4 -0
  30. package/dashboard/dist/assets/{LocalOnlyNotice-BHlcPCZk.js → LocalOnlyNotice-CpFFX8ro.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-C-dqUumR.js → LoginCard-DkZpGnVu.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-BFzRg4Iy.js → LoginModal-CALv6jkD.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-QtlnGF1S.js → LoginPage-BZuXHYAT.js} +1 -1
  34. package/dashboard/dist/assets/PetPage-C1LuDyYr.js +1 -0
  35. package/dashboard/dist/assets/{PopoverPopup-CltmqcrT.js → PopoverPopup-CluVhrMr.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-BqOI9EiD.js → ProviderIcon-D3YKjVKb.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-BsfCBUZD.js → ResetPasswordPage-CzN50nX5.js} +1 -1
  38. package/dashboard/dist/assets/{SegmentedControl-D4biXw_Q.js → SegmentedControl-DFOAcEKU.js} +1 -1
  39. package/dashboard/dist/assets/{Select-DTw-14iE.js → Select-B3W_ihs_.js} +1 -1
  40. package/dashboard/dist/assets/{SelectItemText-3tUrnu3f.js → SelectItemText-FXRtXnFV.js} +1 -1
  41. package/dashboard/dist/assets/ServiceStatusPage-BItZqH7q.js +1 -0
  42. package/dashboard/dist/assets/{SessionsPage-DYYlNqHg.js → SessionsPage-DVQU-hTH.js} +2 -2
  43. package/dashboard/dist/assets/SettingsPage-ByvrwOi6.js +1 -0
  44. package/dashboard/dist/assets/SkillsPage-DrGr5Qij.js +1 -0
  45. package/dashboard/dist/assets/{TokenGalaxy-DU77e8sM.js → TokenGalaxy-ClF_n9od.js} +1 -1
  46. package/dashboard/dist/assets/{ToolbarRootContext-DV3KGf-a.js → ToolbarRootContext-B82hGhAf.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-D6QYKmbl.js → TrendMonitor-DkWm7_1z.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-DSP4jCsH.js → WidgetsPage-DCY4pyR2.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-B0x6O_DU.js → WrappedPage-yGQ4DLqj.js} +1 -1
  50. package/dashboard/dist/assets/__vite-browser-external-BdRrPLk6.js +1483 -0
  51. package/dashboard/dist/assets/__vite-browser-external-hXV_ZYTG.css +1 -0
  52. package/dashboard/dist/assets/{agent-logos-B2S_cdOL.js → agent-logos-D5SHGOxB.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-up-right-DNUVuhx2.js → arrow-up-right-CbMcAcss.js} +1 -1
  54. package/dashboard/dist/assets/{check-C3ARiiE_.js → check-Dyzh7lTz.js} +1 -1
  55. package/dashboard/dist/assets/{chevron-right-3bY7H8jO.js → chevron-right-DT06ZIKC.js} +1 -1
  56. package/dashboard/dist/assets/{copy-C-UIMKoO.js → copy-B3Kt3Wrf.js} +1 -1
  57. package/dashboard/dist/assets/{download-DtRzJfcr.js → download-B3rOytqI.js} +1 -1
  58. package/dashboard/dist/assets/external-link-BamTP0Ks.js +1 -0
  59. package/dashboard/dist/assets/{flame-Bas3ry-g.js → flame-msy2h1ms.js} +1 -1
  60. package/dashboard/dist/assets/{format-tokens-DFtRUIH8.js → format-tokens-8b1OUM_8.js} +1 -1
  61. package/dashboard/dist/assets/{icons-CHBt4Tmo.js → icons-C2NR2_Tf.js} +1 -1
  62. package/dashboard/dist/assets/{index-CZRlrK7w.js → index-C8sDJzUY.js} +1 -1
  63. package/dashboard/dist/assets/{index-C1ADnvzm.js → index-CvTfpiCF.js} +1 -1
  64. package/dashboard/dist/assets/{index-4unZlm2w.js → index-x3vFJQFt.js} +1 -1
  65. package/dashboard/dist/assets/{info-BkrO-fg3.js → info-C9j23aGq.js} +1 -1
  66. package/dashboard/dist/assets/limits-providers-ySB9Ci5l.js +1 -0
  67. package/dashboard/dist/assets/{link-2-DTLFz8uG.js → link-2-rrcyTT6S.js} +1 -1
  68. package/dashboard/dist/assets/{loader-circle-BqQW_Ipq.js → loader-circle-x9x9I8Hv.js} +1 -1
  69. package/dashboard/dist/assets/{maximize-2-YdpeE4dq.js → maximize-2-7TWFb2k7.js} +1 -1
  70. package/dashboard/dist/assets/{provider-display-CgBOpjq0.js → provider-display-JbSwpvxU.js} +1 -1
  71. package/dashboard/dist/assets/{react-DD9jpv--.js → react-DK5clHtY.js} +1 -1
  72. package/dashboard/dist/assets/{search--TvX-fig.js → search-DukXRvEV.js} +1 -1
  73. package/dashboard/dist/assets/{skills-api-C5C8a1at.js → skills-api-BUFJuwiR.js} +1 -1
  74. package/dashboard/dist/assets/{store-CRA8gPnP.js → store-BTh7bv09.js} +1 -1
  75. package/dashboard/dist/assets/{terminal-BXZyl2ek.js → terminal-CcUqLtn1.js} +1 -1
  76. package/dashboard/dist/assets/{trash-2-IFdBQ65v.js → trash-2-Bq7hnvNq.js} +1 -1
  77. package/dashboard/dist/assets/{use-community-stats-LyJ5aMpX.js → use-community-stats-qTpx6ugG.js} +1 -1
  78. package/dashboard/dist/assets/{use-limits-display-prefs-CKvFS1Nn.js → use-limits-display-prefs-Di_J4eVv.js} +1 -1
  79. package/dashboard/dist/assets/{use-native-settings-Cg-4j_0z.js → use-native-settings-CIqpv9Z3.js} +1 -1
  80. package/dashboard/dist/assets/{use-ordered-list-2pX5PXZO.js → use-ordered-list-OwogSXT7.js} +1 -1
  81. package/dashboard/dist/assets/use-reduced-motion-5QGGPoom.js +1 -0
  82. package/dashboard/dist/assets/{use-session-efficiency-pref-DilJOODA.js → use-session-efficiency-pref-CjAvMh7u.js} +1 -1
  83. package/dashboard/dist/assets/{use-transform-DiyYTLpI.js → use-transform-KZLdt-NZ.js} +1 -1
  84. package/dashboard/dist/assets/{use-usage-limits-DkUmp_nW.js → use-usage-limits-Tbifa037.js} +1 -1
  85. package/dashboard/dist/assets/{useCurrency-lm7KjGoD.js → useCurrency-D2td-bkn.js} +1 -1
  86. package/dashboard/dist/assets/{useScrollLock-DVSyJ-GE.js → useScrollLock-Cvyfyezc.js} +1 -1
  87. package/dashboard/dist/assets/useTokenFormat-DYIUS6IH.js +1 -0
  88. package/dashboard/dist/assets/{zap-CKZt1_i5.js → zap-B19mEed5.js} +1 -1
  89. package/dashboard/dist/brand-logos/qoder.svg +5 -0
  90. package/dashboard/dist/index.html +18 -15
  91. package/dashboard/dist/ip-check.html +2 -2
  92. package/dashboard/dist/leaderboard.html +2 -2
  93. package/dashboard/dist/llms.txt +3 -2
  94. package/dashboard/dist/share.html +2 -2
  95. package/package.json +2 -2
  96. package/src/commands/init.js +1 -0
  97. package/src/commands/status.js +34 -7
  98. package/src/commands/sync.js +353 -5
  99. package/src/lib/codex-rollout-parser.js +17 -39
  100. package/src/lib/codex-token-usage.js +195 -0
  101. package/src/lib/local-api.js +4 -1
  102. package/src/lib/pet-packages.js +44 -2
  103. package/src/lib/pricing/seed-snapshot.json +1 -1
  104. package/src/lib/provider-status.js +104 -0
  105. package/src/lib/qoder-limits.js +985 -0
  106. package/src/lib/rollout.js +744 -91
  107. package/src/lib/usage-limits.js +243 -43
  108. package/dashboard/dist/assets/Card-Ha3x9zIj.js +0 -1
  109. package/dashboard/dist/assets/DialogDescription-B9bb9Ar0.js +0 -1
  110. package/dashboard/dist/assets/LandingPage-BCvNh3zE.js +0 -2
  111. package/dashboard/dist/assets/LeaderboardPage-DZSxazHP.js +0 -2
  112. package/dashboard/dist/assets/LimitsPage-JWZDa5_Y.js +0 -4
  113. package/dashboard/dist/assets/PetPage-C6UMY2qD.js +0 -1
  114. package/dashboard/dist/assets/SettingsPage-CYlEboSl.js +0 -1
  115. package/dashboard/dist/assets/SkillsPage-Bzq8V5Pd.js +0 -1
  116. package/dashboard/dist/assets/__vite-browser-external-D3llGq6p.js +0 -1465
  117. package/dashboard/dist/assets/__vite-browser-external-WKilYZnq.css +0 -1
  118. package/dashboard/dist/assets/limits-providers-BtwYmYHA.js +0 -1
  119. package/dashboard/dist/assets/use-reduced-motion-BCR01kRg.js +0 -1
  120. package/dashboard/dist/assets/useTokenFormat-Li8BANKO.js +0 -1
@@ -0,0 +1,985 @@
1
+ "use strict";
2
+
3
+ const cp = require("node:child_process");
4
+ const crypto = require("node:crypto");
5
+ const fs = require("node:fs");
6
+ const net = require("node:net");
7
+ const os = require("node:os");
8
+ const path = require("node:path");
9
+ const { promisify } = require("node:util");
10
+
11
+ const { readSqliteJsonRowsAsync } = require("./sqlite-reader");
12
+
13
+ const execFileAsync = promisify(cp.execFile);
14
+
15
+ const QODER_SITES = {
16
+ international: {
17
+ id: "international",
18
+ origin: "https://qoder.com",
19
+ usageUrl: "https://qoder.com/api/v2/me/usages/big_model_credits",
20
+ domains: new Set(["qoder.com", "www.qoder.com"]),
21
+ },
22
+ china: {
23
+ id: "china",
24
+ origin: "https://qoder.com.cn",
25
+ usageUrl: "https://qoder.com.cn/api/v2/me/usages/big_model_credits",
26
+ domains: new Set(["qoder.com.cn", "www.qoder.com.cn"]),
27
+ },
28
+ };
29
+
30
+ const QODER_USER_AGENT =
31
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
32
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36";
33
+
34
+ // Qoder 1.18 serves promotional call buckets (including Ultimate Free Calls)
35
+ // from its OpenAPI activity endpoint. The request uses the same COSY envelope
36
+ // as Qoder CLI 1.0.22. Authentication comes from Qoder's already-running local
37
+ // service over JSON-RPC; TokenTracker never persists or logs those credentials.
38
+ const QODER_ACTIVITY_URL = "https://openapi.qoder.sh/algo/api/v2/activity";
39
+ const QODER_ULTIMATE_ACTIVITY_ID = "ultimate_200_free_invoke";
40
+ const QODER_COSY_VERSION = "1.0.22";
41
+ const QODER_ACTIVITY_CACHE_FILE = "qoder-activity-cache.json";
42
+ const QODER_ACTIVITY_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
43
+ const QODER_LIMITS_CACHE_FILE = "qoder-usage-limits-cache.json";
44
+ const QODER_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
45
+ const QODER_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
46
+ const QODER_SERVER_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
47
+ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
48
+ 4k6Pz1EaDicBMpdpxKduSZu5OANqUq8er4GM95omAGIOPOh+Nx0spthYA2BqGz+l
49
+ 6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
50
+ XcW+ML9FoCI6AOvOzwIDAQAB
51
+ -----END PUBLIC KEY-----`;
52
+
53
+ const MACOS_CHROMIUM_BROWSERS = [
54
+ {
55
+ name: "Google Chrome",
56
+ root: ["Library", "Application Support", "Google", "Chrome"],
57
+ service: "Chrome Safe Storage",
58
+ account: "Chrome",
59
+ },
60
+ {
61
+ name: "Brave",
62
+ root: ["Library", "Application Support", "BraveSoftware", "Brave-Browser"],
63
+ service: "Brave Safe Storage",
64
+ account: "Brave",
65
+ },
66
+ {
67
+ name: "Microsoft Edge",
68
+ root: ["Library", "Application Support", "Microsoft Edge"],
69
+ service: "Microsoft Edge Safe Storage",
70
+ account: "Microsoft Edge",
71
+ },
72
+ {
73
+ name: "Arc",
74
+ root: ["Library", "Application Support", "Arc", "User Data"],
75
+ service: "Arc Safe Storage",
76
+ account: "Arc",
77
+ },
78
+ {
79
+ name: "Chromium",
80
+ root: ["Library", "Application Support", "Chromium"],
81
+ service: "Chromium Safe Storage",
82
+ account: "Chromium",
83
+ },
84
+ ];
85
+
86
+ const CHROMIUM_COOKIE_SQL = `
87
+ SELECT
88
+ host_key,
89
+ name,
90
+ value,
91
+ hex(encrypted_value) AS encrypted_hex
92
+ FROM cookies
93
+ WHERE lower(host_key) IN (
94
+ '.qoder.com', 'qoder.com', 'www.qoder.com',
95
+ '.qoder.com.cn', 'qoder.com.cn', 'www.qoder.com.cn'
96
+ )
97
+ ORDER BY host_key, name
98
+ `;
99
+
100
+ function finiteNumber(value, field) {
101
+ const number = Number(value);
102
+ if (!Number.isFinite(number)) {
103
+ throw new Error(`Qoder usage response is missing ${field}.`);
104
+ }
105
+ return number;
106
+ }
107
+
108
+ function firstDefined(object, camel, snake) {
109
+ return object?.[camel] ?? object?.[snake];
110
+ }
111
+
112
+ function normalizeResetAt(value) {
113
+ if (value === null || value === undefined || value === "") return null;
114
+ if (typeof value === "number" || /^\d+(?:\.\d+)?$/.test(String(value).trim())) {
115
+ const raw = Number(value);
116
+ if (!Number.isFinite(raw)) return null;
117
+ const milliseconds = raw > 10_000_000_000 ? raw : raw * 1000;
118
+ const date = new Date(milliseconds);
119
+ return Number.isFinite(date.getTime()) ? date.toISOString() : null;
120
+ }
121
+ const timestamp = Date.parse(String(value));
122
+ return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
123
+ }
124
+
125
+ function quotaSummary(container) {
126
+ return firstDefined(container, "quotaSummary", "quota_summary") || null;
127
+ }
128
+
129
+ function normalizeQuotaSummary(summary) {
130
+ if (!summary || typeof summary !== "object") {
131
+ throw new Error("Qoder usage response is missing totalQuota.quotaSummary.");
132
+ }
133
+ const used = finiteNumber(firstDefined(summary, "usedValue", "used_value"), "usedValue");
134
+ const total = finiteNumber(firstDefined(summary, "limitValue", "limit_value"), "limitValue");
135
+ const rawRemaining = firstDefined(summary, "remainingValue", "remaining_value");
136
+ const remaining = rawRemaining === null || rawRemaining === undefined
137
+ ? Math.max(0, total - used)
138
+ : finiteNumber(rawRemaining, "remainingValue");
139
+ const rawPercentage = firstDefined(summary, "usagePercentage", "usage_percentage");
140
+ const providedPercentage = rawPercentage === null || rawPercentage === undefined
141
+ ? null
142
+ : finiteNumber(rawPercentage, "usagePercentage");
143
+
144
+ if (used < 0 || total < 0 || remaining < 0) {
145
+ throw new Error("Qoder quota values must be nonnegative.");
146
+ }
147
+ if (total === 0 && (used !== 0 || remaining !== 0)) {
148
+ throw new Error("Qoder zero total quota must have zero usage and remaining.");
149
+ }
150
+ const usagePercentage = providedPercentage ?? (total > 0 ? (used / total) * 100 : 100);
151
+ return {
152
+ used,
153
+ total,
154
+ remaining,
155
+ usagePercentage: Math.max(0, Math.min(100, usagePercentage)),
156
+ unit: typeof summary.unit === "string" && summary.unit.trim() ? summary.unit.trim() : null,
157
+ };
158
+ }
159
+
160
+ function normalizeQoderUsageResponse(response) {
161
+ if (!response || typeof response !== "object") {
162
+ throw new Error("Qoder usage response is not an object.");
163
+ }
164
+ const totalContainer = firstDefined(response, "totalQuota", "total_quota");
165
+ const sharedContainer = firstDefined(response, "sharedQuota", "shared_quota");
166
+ const base = normalizeQuotaSummary(quotaSummary(totalContainer));
167
+ const sharedSummary = quotaSummary(sharedContainer);
168
+ const shared = sharedSummary ? normalizeQuotaSummary(sharedSummary) : null;
169
+
170
+ const used = base.used + (shared?.used || 0);
171
+ const total = base.total + (shared?.total || 0);
172
+ const remaining = base.remaining + (shared?.remaining || 0);
173
+ const usedPercent = shared
174
+ ? total > 0
175
+ ? (used / total) * 100
176
+ : 100
177
+ : base.usagePercentage;
178
+
179
+ return {
180
+ configured: true,
181
+ error: null,
182
+ primary_window: {
183
+ used_percent: Math.max(0, Math.min(100, usedPercent)),
184
+ reset_at: normalizeResetAt(firstDefined(response, "nextResetAt", "next_reset_at")),
185
+ used_credits: used,
186
+ limit_credits: total,
187
+ remaining_credits: remaining,
188
+ unit: base.unit || shared?.unit || null,
189
+ },
190
+ source: "provider-api",
191
+ };
192
+ }
193
+
194
+ function qoderDataRoot({
195
+ home = os.homedir(),
196
+ env = process.env,
197
+ platform = process.platform,
198
+ } = {}) {
199
+ if (typeof env.QODER_HOME === "string" && env.QODER_HOME.trim()) {
200
+ return path.resolve(env.QODER_HOME.trim());
201
+ }
202
+ if (platform === "darwin") {
203
+ return path.join(home, "Library", "Application Support", "Qoder");
204
+ }
205
+ if (platform === "win32") {
206
+ return path.join(env.APPDATA || path.join(home, "AppData", "Roaming"), "Qoder");
207
+ }
208
+ return path.join(home, ".config", "Qoder");
209
+ }
210
+
211
+ function qoderRpcRequest(method, params = {}, {
212
+ home = os.homedir(),
213
+ env = process.env,
214
+ platform = process.platform,
215
+ timeoutMs = 1500,
216
+ netModule = net,
217
+ } = {}) {
218
+ return new Promise((resolve, reject) => {
219
+ const infoPath = path.join(qoderDataRoot({ home, env, platform }), "SharedClientCache", ".info.json");
220
+ let info;
221
+ try {
222
+ info = JSON.parse(fs.readFileSync(infoPath, "utf8"));
223
+ } catch (_error) {
224
+ reject(new Error("Qoder local service is not running."));
225
+ return;
226
+ }
227
+ const socketPath = typeof info?.ipcServerPath === "string" ? info.ipcServerPath.trim() : "";
228
+ if (!socketPath) {
229
+ reject(new Error("Qoder local service endpoint is unavailable."));
230
+ return;
231
+ }
232
+
233
+ let settled = false;
234
+ let buffer = Buffer.alloc(0);
235
+ const socket = netModule.createConnection(socketPath);
236
+ const timer = setTimeout(() => finish(new Error("Qoder local service request timed out.")), timeoutMs);
237
+
238
+ function finish(error, value) {
239
+ if (settled) return;
240
+ settled = true;
241
+ clearTimeout(timer);
242
+ socket.destroy();
243
+ if (error) reject(error);
244
+ else resolve(value);
245
+ }
246
+
247
+ socket.on("connect", () => {
248
+ const body = Buffer.from(JSON.stringify({
249
+ jsonrpc: "2.0",
250
+ id: 1,
251
+ method,
252
+ params,
253
+ }));
254
+ socket.write(`Content-Length: ${body.length}\r\n\r\n`);
255
+ socket.write(body);
256
+ });
257
+ socket.on("error", (error) => finish(error));
258
+ socket.on("data", (chunk) => {
259
+ buffer = Buffer.concat([buffer, chunk]);
260
+ const headerEnd = buffer.indexOf("\r\n\r\n");
261
+ if (headerEnd < 0) return;
262
+ const match = /Content-Length:\s*(\d+)/i.exec(buffer.subarray(0, headerEnd).toString("ascii"));
263
+ if (!match) {
264
+ finish(new Error("Qoder local service returned an invalid response."));
265
+ return;
266
+ }
267
+ const bodyLength = Number(match[1]);
268
+ const bodyStart = headerEnd + 4;
269
+ if (!Number.isFinite(bodyLength) || bodyLength < 0 || bodyLength > 4 * 1024 * 1024) {
270
+ finish(new Error("Qoder local service response is too large."));
271
+ return;
272
+ }
273
+ if (buffer.length < bodyStart + bodyLength) return;
274
+ try {
275
+ const message = JSON.parse(buffer.subarray(bodyStart, bodyStart + bodyLength).toString("utf8"));
276
+ if (message.error) {
277
+ finish(new Error(message.error.message || "Qoder local service request failed."));
278
+ } else {
279
+ finish(null, message.result);
280
+ }
281
+ } catch (_error) {
282
+ finish(new Error("Qoder local service returned invalid JSON."));
283
+ }
284
+ });
285
+ });
286
+ }
287
+
288
+ function normalizeQoderRpcUsage(response) {
289
+ if (!response || typeof response !== "object") return null;
290
+ const quota = response.userQuota;
291
+ if (!quota || typeof quota !== "object") return null;
292
+ const used = finiteNumber(quota.used, "userQuota.used");
293
+ const total = finiteNumber(quota.total, "userQuota.total");
294
+ const remaining = finiteNumber(quota.remaining, "userQuota.remaining");
295
+ const reportedPercent = Number(response.totalUsagePercentage ?? quota.percentage);
296
+ const usedPercent = total === 0
297
+ ? 0
298
+ : response.isQuotaExceeded === true
299
+ ? 100
300
+ : Number.isFinite(reportedPercent)
301
+ ? reportedPercent
302
+ : (used / total) * 100;
303
+ const normalizedExpiry = normalizeResetAt(response.expiresAt);
304
+ const expiryMs = normalizedExpiry ? Date.parse(normalizedExpiry) : NaN;
305
+ return {
306
+ configured: true,
307
+ error: null,
308
+ plan_label: typeof response.userType === "string" ? response.userType : null,
309
+ quota_exceeded: response.isQuotaExceeded === true,
310
+ primary_window: {
311
+ used_percent: Math.max(0, Math.min(100, usedPercent)),
312
+ // Qoder uses 9999-12-31 as a no-expiry sentinel for Free accounts.
313
+ reset_at: Number.isFinite(expiryMs) && expiryMs < Date.UTC(2100, 0, 1)
314
+ ? normalizedExpiry
315
+ : null,
316
+ used_credits: used,
317
+ limit_credits: total,
318
+ remaining_credits: remaining,
319
+ unit: typeof quota.unit === "string" && quota.unit.trim() ? quota.unit.trim() : "credits",
320
+ },
321
+ source: "local-ipc",
322
+ };
323
+ }
324
+
325
+ function normalizeQoderActivityResponse(response, { nowMs = Date.now() } = {}) {
326
+ if (!response || typeof response !== "object") {
327
+ throw new Error("Qoder activity response is not an object.");
328
+ }
329
+ if (response.code !== undefined && Number(response.code) !== 0) {
330
+ throw new Error(`Qoder activity API returned ${response.msg || `code ${response.code}`}.`);
331
+ }
332
+ const activities = Array.isArray(response.data?.activities) ? response.data.activities : [];
333
+ const activity = activities.find((entry) => {
334
+ if (!entry || entry.activityId !== QODER_ULTIMATE_ACTIVITY_ID || entry.eligible === false) return false;
335
+ if (String(entry.tagStyle || "").toUpperCase() === "EXPIRED") return false;
336
+ const endAt = Number(entry.activityEndAt);
337
+ return !Number.isFinite(endAt) || endAt <= 0 || endAt > nowMs;
338
+ });
339
+ if (!activity) return null;
340
+ const used = finiteNumber(activity.used, "activity.used");
341
+ const total = finiteNumber(activity.limit, "activity.limit");
342
+ const rawRemaining = Number(activity.remaining);
343
+ const remaining = Number.isFinite(rawRemaining) ? rawRemaining : Math.max(0, total - used);
344
+ if (used < 0 || total <= 0 || remaining < 0) {
345
+ throw new Error("Qoder activity quota values are invalid.");
346
+ }
347
+ return {
348
+ used_percent: Math.max(0, Math.min(100, (used / total) * 100)),
349
+ reset_at: normalizeResetAt(activity.activityEndAt),
350
+ used_credits: used,
351
+ limit_credits: total,
352
+ remaining_credits: remaining,
353
+ unit: "calls",
354
+ activity_id: QODER_ULTIMATE_ACTIVITY_ID,
355
+ };
356
+ }
357
+
358
+ function qoderActivityCachePath({ home = os.homedir() } = {}) {
359
+ return path.join(home, ".tokentracker", "tracker", QODER_ACTIVITY_CACHE_FILE);
360
+ }
361
+
362
+ function readQoderActivityCache({
363
+ home = os.homedir(),
364
+ nowMs = Date.now(),
365
+ maxAgeMs = QODER_ACTIVITY_CACHE_MAX_AGE_MS,
366
+ } = {}) {
367
+ try {
368
+ const parsed = JSON.parse(fs.readFileSync(qoderActivityCachePath({ home }), "utf8"));
369
+ const window = parsed?.activity_window;
370
+ const cachedAtMs = Date.parse(parsed?.cached_at || "");
371
+ if (!window || !Number.isFinite(cachedAtMs) || cachedAtMs > nowMs + 60_000) return null;
372
+ const resetAtMs = Date.parse(window.reset_at || "");
373
+ if (Number.isFinite(resetAtMs)) {
374
+ if (resetAtMs <= nowMs) return null;
375
+ } else if (nowMs - cachedAtMs > maxAgeMs) {
376
+ return null;
377
+ }
378
+ const used = Number(window.used_credits);
379
+ const total = Number(window.limit_credits);
380
+ const remaining = Number(window.remaining_credits);
381
+ if (!Number.isFinite(used) || !Number.isFinite(total) || !Number.isFinite(remaining)
382
+ || used < 0 || total <= 0 || remaining < 0) return null;
383
+ return window;
384
+ } catch (_error) {
385
+ return null;
386
+ }
387
+ }
388
+
389
+ function writeQoderActivityCache(window, {
390
+ home = os.homedir(),
391
+ nowMs = Date.now(),
392
+ } = {}) {
393
+ if (!window) return;
394
+ const cachePath = qoderActivityCachePath({ home });
395
+ const payload = {
396
+ activity_window: window,
397
+ cached_at: new Date(nowMs).toISOString(),
398
+ };
399
+ try {
400
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
401
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
402
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
403
+ fs.renameSync(tmpPath, cachePath);
404
+ } catch (_error) {}
405
+ }
406
+
407
+ function qoderLimitsCachePath({ home = os.homedir() } = {}) {
408
+ return path.join(home, ".tokentracker", "tracker", QODER_LIMITS_CACHE_FILE);
409
+ }
410
+
411
+ function qoderCachedWindow(window, { cachedAtMs, nowMs } = {}) {
412
+ if (!window || typeof window !== "object") return null;
413
+ const resetAtMs = Date.parse(window.reset_at || "");
414
+ if (Number.isFinite(resetAtMs)) return resetAtMs > nowMs ? window : null;
415
+ return nowMs - cachedAtMs <= QODER_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS ? window : null;
416
+ }
417
+
418
+ function readQoderLimitsCache({
419
+ home = os.homedir(),
420
+ nowMs = Date.now(),
421
+ } = {}) {
422
+ try {
423
+ const raw = JSON.parse(fs.readFileSync(qoderLimitsCachePath({ home }), "utf8"))?.qoder;
424
+ const cachedAtMs = Date.parse(raw?.cached_at || "");
425
+ if (!Number.isFinite(cachedAtMs) || cachedAtMs > nowMs + 60_000) return null;
426
+ const primaryWindow = qoderCachedWindow(raw.primary_window, { cachedAtMs, nowMs });
427
+ const secondaryWindow = qoderCachedWindow(raw.secondary_window, { cachedAtMs, nowMs });
428
+ if (!primaryWindow && !secondaryWindow) return null;
429
+ const hasUnexpiredDatedWindow = [primaryWindow, secondaryWindow]
430
+ .some((window) => Number.isFinite(Date.parse(window?.reset_at || "")));
431
+ if (nowMs - cachedAtMs > QODER_LIMITS_CACHE_MAX_AGE_MS && !hasUnexpiredDatedWindow) {
432
+ return null;
433
+ }
434
+ return {
435
+ configured: true,
436
+ error: null,
437
+ plan_label: typeof raw.plan_label === "string" ? raw.plan_label : null,
438
+ quota_exceeded: raw.quota_exceeded === true,
439
+ primary_window: primaryWindow,
440
+ secondary_window: secondaryWindow,
441
+ cached: true,
442
+ stale: true,
443
+ cached_at: raw.cached_at,
444
+ source: "disk-cache",
445
+ };
446
+ } catch (_error) {
447
+ return null;
448
+ }
449
+ }
450
+
451
+ function writeQoderLimitsCache(limits, {
452
+ home = os.homedir(),
453
+ nowMs = Date.now(),
454
+ } = {}) {
455
+ if (!limits?.configured || limits.error || (!limits.primary_window && !limits.secondary_window)) return;
456
+ const cachePath = qoderLimitsCachePath({ home });
457
+ const payload = {
458
+ qoder: {
459
+ plan_label: limits.plan_label || null,
460
+ quota_exceeded: limits.quota_exceeded === true,
461
+ primary_window: limits.primary_window || null,
462
+ secondary_window: limits.secondary_window || null,
463
+ cached_at: new Date(nowMs).toISOString(),
464
+ },
465
+ };
466
+ try {
467
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
468
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
469
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
470
+ fs.renameSync(tmpPath, cachePath);
471
+ } catch (_error) {}
472
+ }
473
+
474
+ function buildQoderActivityHeaders(authStatus, {
475
+ nowMs = Date.now(),
476
+ randomUUID = crypto.randomUUID,
477
+ } = {}) {
478
+ const userId = String(authStatus?.id || authStatus?.accountId || "").trim();
479
+ const accessToken = String(authStatus?.token || "").trim();
480
+ if (!userId || !accessToken) {
481
+ throw new Error("Qoder local session is missing activity credentials.");
482
+ }
483
+ const temporaryKey = Buffer.from(randomUUID().replace(/-/g, "").slice(0, 16), "ascii");
484
+ const cosyKey = crypto.publicEncrypt(
485
+ { key: QODER_SERVER_PUBLIC_KEY, padding: crypto.constants.RSA_PKCS1_PADDING },
486
+ temporaryKey,
487
+ ).toString("base64");
488
+ const identity = {
489
+ name: authStatus.name || "",
490
+ aid: userId,
491
+ uid: userId,
492
+ yx_uid: authStatus.yxUid || "",
493
+ organization_id: authStatus.orgId || "",
494
+ organization_name: authStatus.orgName || "",
495
+ user_type: authStatus.userType || "personal_standard",
496
+ security_oauth_token: accessToken,
497
+ refresh_token: authStatus.refreshToken || "",
498
+ };
499
+ const cipher = crypto.createCipheriv("aes-128-cbc", temporaryKey, temporaryKey);
500
+ const info = Buffer.concat([
501
+ cipher.update(Buffer.from(JSON.stringify(identity), "utf8")),
502
+ cipher.final(),
503
+ ]).toString("base64");
504
+ const payload = Buffer.from(JSON.stringify({
505
+ version: "v1",
506
+ requestId: randomUUID(),
507
+ info,
508
+ cosyVersion: QODER_COSY_VERSION,
509
+ ideVersion: "",
510
+ }), "utf8").toString("base64");
511
+ const cosyDate = String(Math.floor(nowMs / 1000));
512
+ const signature = crypto
513
+ .createHash("md5")
514
+ .update(`${payload}\n${cosyKey}\n${cosyDate}\n\n/api/v2/activity`, "utf8")
515
+ .digest("hex");
516
+ const machineId = randomUUID();
517
+ return {
518
+ "cosy-data-policy": "agree",
519
+ "cosy-machinetype": "5",
520
+ "cosy-clienttype": "5",
521
+ "cosy-date": cosyDate,
522
+ "cosy-user": userId,
523
+ "cosy-key": cosyKey,
524
+ "cache-control": "no-cache",
525
+ "cosy-business-product": "cli",
526
+ "cosy-business-type": "agent",
527
+ "cosy-scene": "assistant",
528
+ accept: "application/json",
529
+ authorization: `Bearer COSY.${payload}.${signature}`,
530
+ "accept-encoding": "identity",
531
+ "cosy-version": QODER_COSY_VERSION,
532
+ "cosy-machineid": machineId,
533
+ "cosy-machinetoken": machineId,
534
+ "login-version": "v2",
535
+ "user-agent": "Go-http-client/2.0",
536
+ };
537
+ }
538
+
539
+ async function fetchQoderActivity(authStatus, fetchImpl = fetch, options = {}) {
540
+ const response = await fetchImpl(QODER_ACTIVITY_URL, {
541
+ method: "GET",
542
+ headers: buildQoderActivityHeaders(authStatus, options),
543
+ });
544
+ if (response.status === 401 || response.status === 403) {
545
+ throw new Error("Qoder activity session expired.");
546
+ }
547
+ if (!response.ok) {
548
+ throw new Error(`Qoder activity API returned HTTP ${response.status}.`);
549
+ }
550
+ return normalizeQoderActivityResponse(await response.json(), options);
551
+ }
552
+
553
+ function normalizeCookieDomain(hostKey) {
554
+ return String(hostKey || "").trim().toLowerCase().replace(/^\./, "");
555
+ }
556
+
557
+ function decryptChromiumCookie({ encryptedHex, plaintextValue, hostKey, password }) {
558
+ if (typeof plaintextValue === "string" && plaintextValue) return plaintextValue;
559
+ if (!encryptedHex || !password) return null;
560
+ let encrypted;
561
+ try {
562
+ encrypted = Buffer.from(encryptedHex, "hex");
563
+ } catch (_error) {
564
+ return null;
565
+ }
566
+ if (encrypted.length <= 3) return null;
567
+ const version = encrypted.subarray(0, 3).toString("ascii");
568
+ if (version !== "v10" && version !== "v11") return null;
569
+
570
+ try {
571
+ const key = crypto.pbkdf2Sync(password, "saltysalt", 1003, 16, "sha1");
572
+ const decipher = crypto.createDecipheriv(
573
+ "aes-128-cbc",
574
+ key,
575
+ Buffer.alloc(16, 0x20),
576
+ );
577
+ let decrypted = Buffer.concat([
578
+ decipher.update(encrypted.subarray(3)),
579
+ decipher.final(),
580
+ ]);
581
+ const hostDigest = crypto.createHash("sha256").update(String(hostKey || "")).digest();
582
+ if (decrypted.length >= hostDigest.length && decrypted.subarray(0, 32).equals(hostDigest)) {
583
+ decrypted = decrypted.subarray(32);
584
+ }
585
+ const value = decrypted.toString("utf8");
586
+ return value || null;
587
+ } catch (_error) {
588
+ return null;
589
+ }
590
+ }
591
+
592
+ async function readMacosSafeStoragePassword({
593
+ service,
594
+ account,
595
+ securityRunner,
596
+ } = {}) {
597
+ const runner = securityRunner || (async (args) => {
598
+ const result = await execFileAsync("/usr/bin/security", args, {
599
+ timeout: 5000,
600
+ maxBuffer: 1024 * 1024,
601
+ });
602
+ return result.stdout;
603
+ });
604
+ try {
605
+ const output = await runner([
606
+ "find-generic-password",
607
+ "-w",
608
+ "-s",
609
+ service,
610
+ "-a",
611
+ account,
612
+ ]);
613
+ const password = String(output || "").trim();
614
+ return password || null;
615
+ } catch (_error) {
616
+ return null;
617
+ }
618
+ }
619
+
620
+ function chromiumProfileDirs(browserRoot) {
621
+ if (!fs.existsSync(browserRoot)) return [];
622
+ let entries = [];
623
+ try {
624
+ entries = fs.readdirSync(browserRoot, { withFileTypes: true });
625
+ } catch (_error) {
626
+ return [];
627
+ }
628
+ return entries
629
+ .filter((entry) => entry.isDirectory() && (entry.name === "Default" || /^Profile \d+$/.test(entry.name)))
630
+ .map((entry) => path.join(browserRoot, entry.name))
631
+ .filter((profileDir) => fs.existsSync(path.join(profileDir, "Cookies")));
632
+ }
633
+
634
+ function cookieHeaderFromRows(rows, password, site) {
635
+ const pairs = [];
636
+ for (const row of Array.isArray(rows) ? rows : []) {
637
+ const domain = normalizeCookieDomain(row?.host_key);
638
+ if (!site.domains.has(domain)) continue;
639
+ const name = typeof row?.name === "string" ? row.name.trim() : "";
640
+ if (!name) continue;
641
+ const value = decryptChromiumCookie({
642
+ encryptedHex: row.encrypted_hex,
643
+ plaintextValue: row.value,
644
+ hostKey: row.host_key,
645
+ password,
646
+ });
647
+ if (value) pairs.push(`${name}=${value}`);
648
+ }
649
+ return pairs.length > 0 ? pairs.join("; ") : null;
650
+ }
651
+
652
+ async function importMacosQoderSessions({
653
+ home = os.homedir(),
654
+ platform = process.platform,
655
+ securityRunner,
656
+ sqliteReader = readSqliteJsonRowsAsync,
657
+ } = {}) {
658
+ if (platform !== "darwin") return [];
659
+ const sessions = [];
660
+ for (const browser of MACOS_CHROMIUM_BROWSERS) {
661
+ const browserRoot = path.join(home, ...browser.root);
662
+ const profiles = chromiumProfileDirs(browserRoot);
663
+ if (profiles.length === 0) continue;
664
+ const password = await readMacosSafeStoragePassword({
665
+ service: browser.service,
666
+ account: browser.account,
667
+ securityRunner,
668
+ });
669
+ if (!password) continue;
670
+ for (const profileDir of profiles) {
671
+ let rows;
672
+ try {
673
+ rows = await sqliteReader(path.join(profileDir, "Cookies"), CHROMIUM_COOKIE_SQL, {
674
+ label: `${browser.name} cookies`,
675
+ });
676
+ } catch (_error) {
677
+ continue;
678
+ }
679
+ for (const site of Object.values(QODER_SITES)) {
680
+ const cookieHeader = cookieHeaderFromRows(rows, password, site);
681
+ if (cookieHeader) {
682
+ sessions.push({
683
+ cookieHeader,
684
+ site,
685
+ sourceLabel: `${browser.name} ${path.basename(profileDir)}`,
686
+ });
687
+ }
688
+ }
689
+ }
690
+ }
691
+ return sessions;
692
+ }
693
+
694
+ function qoderRequestHeaders(cookieHeader, site) {
695
+ return {
696
+ Cookie: cookieHeader,
697
+ Accept: "application/json, text/plain, */*",
698
+ "Accept-Language": "en-US,en;q=0.9",
699
+ "User-Agent": QODER_USER_AGENT,
700
+ Origin: site.origin,
701
+ Referer: `${site.origin}/account/usage`,
702
+ "X-Requested-With": "XMLHttpRequest",
703
+ "Bx-V": "2.5.35",
704
+ };
705
+ }
706
+
707
+ async function fetchQoderUsage(cookieHeader, site, fetchImpl) {
708
+ const response = await fetchImpl(site.usageUrl, {
709
+ method: "GET",
710
+ headers: qoderRequestHeaders(cookieHeader, site),
711
+ });
712
+ if (response.status === 401 || response.status === 403) {
713
+ const error = new Error("Qoder login expired. Sign in at qoder.com again.");
714
+ error.code = "AUTH_EXPIRED";
715
+ throw error;
716
+ }
717
+ if (!response.ok) {
718
+ throw new Error(`Qoder usage API returned HTTP ${response.status}.`);
719
+ }
720
+ return normalizeQoderUsageResponse(await response.json());
721
+ }
722
+
723
+ function listRecentQoderRendererLogs(logRoot, maxFiles = 12) {
724
+ if (!fs.existsSync(logRoot)) return [];
725
+ const files = [];
726
+ const stack = [{ dir: logRoot, depth: 0 }];
727
+ while (stack.length > 0 && files.length < 2000) {
728
+ const current = stack.pop();
729
+ let entries;
730
+ try {
731
+ entries = fs.readdirSync(current.dir, { withFileTypes: true });
732
+ } catch (_error) {
733
+ continue;
734
+ }
735
+ for (const entry of entries) {
736
+ const full = path.join(current.dir, entry.name);
737
+ if (entry.isDirectory() && current.depth < 4) {
738
+ stack.push({ dir: full, depth: current.depth + 1 });
739
+ } else if (entry.isFile() && entry.name === "renderer.log") {
740
+ let mtimeMs = 0;
741
+ try {
742
+ mtimeMs = fs.statSync(full).mtimeMs;
743
+ } catch (_error) {}
744
+ files.push({ full, mtimeMs });
745
+ }
746
+ }
747
+ }
748
+ return files
749
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)
750
+ .slice(0, maxFiles)
751
+ .map((entry) => entry.full);
752
+ }
753
+
754
+ function parseQoderQuotaLog(text) {
755
+ if (typeof text !== "string" || !text) return null;
756
+ const matches = Array.from(text.matchAll(
757
+ /userType=([^,\s]+)[\s\S]*?isQuotaExceeded=(true|false)[\s\S]*?userQuota(?:=|\s+)used=([0-9.]+),\s*total=([0-9.]+),\s*remaining=([0-9.]+),\s*percentage=([0-9.]+),\s*unit=([^,\s]+)/g,
758
+ ));
759
+ const match = matches.at(-1);
760
+ if (!match) return null;
761
+ const quotaExceeded = match[2] === "true";
762
+ const used = Number(match[3]);
763
+ const total = Number(match[4]);
764
+ const remaining = Number(match[5]);
765
+ const percentage = Number(match[6]);
766
+ if (![used, total, remaining, percentage].every(Number.isFinite)) return null;
767
+ return {
768
+ configured: true,
769
+ error: null,
770
+ plan_label: match[1],
771
+ quota_exceeded: quotaExceeded,
772
+ primary_window: {
773
+ used_percent: total === 0
774
+ ? 0
775
+ : quotaExceeded
776
+ ? 100
777
+ : Math.max(0, Math.min(100, percentage)),
778
+ reset_at: null,
779
+ used_credits: used,
780
+ limit_credits: total,
781
+ remaining_credits: remaining,
782
+ unit: match[7],
783
+ },
784
+ source: "local-log",
785
+ };
786
+ }
787
+
788
+ function readQoderLocalQuota({
789
+ home = os.homedir(),
790
+ platform = process.platform,
791
+ env = process.env,
792
+ } = {}) {
793
+ const explicitLogRoot =
794
+ typeof env.QODER_LOG_ROOT === "string" && env.QODER_LOG_ROOT.trim()
795
+ ? path.resolve(env.QODER_LOG_ROOT.trim())
796
+ : null;
797
+ const configuredRoot =
798
+ typeof env.QODER_HOME === "string" && env.QODER_HOME.trim()
799
+ ? path.resolve(env.QODER_HOME.trim())
800
+ : null;
801
+ const logRoot = explicitLogRoot || (configuredRoot
802
+ ? path.join(configuredRoot, "logs")
803
+ : platform === "darwin"
804
+ ? path.join(home, "Library", "Application Support", "Qoder", "logs")
805
+ : platform === "win32"
806
+ ? path.join(env.APPDATA || path.join(home, "AppData", "Roaming"), "Qoder", "logs")
807
+ : path.join(home, ".config", "Qoder", "logs"));
808
+ for (const logPath of listRecentQoderRendererLogs(logRoot)) {
809
+ try {
810
+ const fd = fs.openSync(logPath, "r");
811
+ try {
812
+ const stat = fs.fstatSync(fd);
813
+ const length = Math.min(stat.size, 512 * 1024);
814
+ const buffer = Buffer.alloc(length);
815
+ fs.readSync(fd, buffer, 0, length, Math.max(0, stat.size - length));
816
+ const parsed = parseQoderQuotaLog(buffer.toString("utf8"));
817
+ if (parsed) return parsed;
818
+ } finally {
819
+ fs.closeSync(fd);
820
+ }
821
+ } catch (_error) {}
822
+ }
823
+ return null;
824
+ }
825
+
826
+ function siteFromEnv(value) {
827
+ const normalized = String(value || "").trim().toLowerCase();
828
+ return normalized === "cn" || normalized === "china" || normalized.includes(".cn")
829
+ ? QODER_SITES.china
830
+ : QODER_SITES.international;
831
+ }
832
+
833
+ async function fetchQoderLimits({
834
+ home = os.homedir(),
835
+ env = process.env,
836
+ platform = process.platform,
837
+ securityRunner,
838
+ sqliteReader,
839
+ fetchImpl = fetch,
840
+ rpcRequest = qoderRpcRequest,
841
+ nowMs = Date.now(),
842
+ } = {}) {
843
+ let rpcUsage = null;
844
+ let rpcAuth = null;
845
+ let activityWindow = null;
846
+ let activitySource = null;
847
+ const rpcOptions = { home, env, platform };
848
+ // Qoder's shared client accepts only one reliable request at a time. Parallel
849
+ // connections intermittently time out even while the desktop app is running.
850
+ try {
851
+ rpcUsage = normalizeQoderRpcUsage(await rpcRequest("credit/usage", {}, rpcOptions));
852
+ } catch (_error) {}
853
+ try {
854
+ const authResult = await rpcRequest("auth/status", {}, rpcOptions);
855
+ if (authResult && typeof authResult === "object") rpcAuth = authResult;
856
+ } catch (_error) {}
857
+ if (rpcAuth) {
858
+ try {
859
+ activityWindow = await fetchQoderActivity(rpcAuth, fetchImpl, { nowMs });
860
+ if (activityWindow) {
861
+ activitySource = "provider-api";
862
+ writeQoderActivityCache(activityWindow, { home, nowMs });
863
+ }
864
+ } catch (_error) {}
865
+ }
866
+ // Qoder's activity endpoint occasionally succeeds with an empty activities
867
+ // array while the promotion is still active. Treat that as a transient
868
+ // omission, not an authoritative deletion: an activity carries its own
869
+ // expiry, so the last observed window remains valid until reset_at.
870
+ if (!activityWindow) {
871
+ activityWindow = readQoderActivityCache({ home, nowMs });
872
+ if (activityWindow) activitySource = "disk-cache";
873
+ }
874
+ if (rpcUsage) {
875
+ if (!rpcUsage.plan_label && typeof rpcAuth?.userType === "string") {
876
+ rpcUsage.plan_label = rpcAuth.userType;
877
+ }
878
+ if (activityWindow) rpcUsage.secondary_window = activityWindow;
879
+ rpcUsage.source = activityWindow
880
+ ? `local-ipc+${activitySource === "disk-cache" ? "cached-activity" : "provider-api"}`
881
+ : "local-ipc";
882
+ writeQoderLimitsCache(rpcUsage, { home, nowMs });
883
+ return rpcUsage;
884
+ }
885
+
886
+ // Match the Antigravity last-good behavior: once the local service is
887
+ // unavailable, prefer a bounded disk snapshot before attempting the much
888
+ // slower browser-cookie fallback. On macOS, probing Chromium profiles may
889
+ // involve multiple Keychain reads; letting those run first can consume the
890
+ // provider timeout and make a perfectly valid cache look intermittent.
891
+ const cachedLimits = readQoderLimitsCache({ home, nowMs });
892
+ if (cachedLimits) {
893
+ if (activityWindow) cachedLimits.secondary_window = activityWindow;
894
+ return cachedLimits;
895
+ }
896
+
897
+ const manualCookie = typeof env.QODER_COOKIE === "string" ? env.QODER_COOKIE.trim() : "";
898
+ const sessions = manualCookie
899
+ ? [{ cookieHeader: manualCookie, site: siteFromEnv(env.QODER_SITE), sourceLabel: "QODER_COOKIE" }]
900
+ : await importMacosQoderSessions({ home, platform, securityRunner, sqliteReader });
901
+
902
+ let lastError = null;
903
+ for (const session of sessions) {
904
+ try {
905
+ const result = await fetchQoderUsage(session.cookieHeader, session.site, fetchImpl);
906
+ // Qoder 1.18 can return an all-zero active plan with usage_percentage=0
907
+ // while the desktop client separately marks the account quota-exceeded.
908
+ // Preserve that status as metadata, but match Qoder's Credits UI by
909
+ // rendering an empty 0/0 bucket as 0%, not a full red 100% bar.
910
+ const local = result.primary_window?.limit_credits === 0
911
+ ? readQoderLocalQuota({ home, platform, env })
912
+ : null;
913
+ if (local) {
914
+ result.quota_exceeded = local.quota_exceeded === true;
915
+ result.primary_window.reset_at = null;
916
+ result.source = "provider-api+local-log";
917
+ result.plan_label = local.plan_label || null;
918
+ }
919
+ if (activityWindow) {
920
+ result.secondary_window = activityWindow;
921
+ result.source += activitySource === "disk-cache" ? "+cached-activity" : "+activity";
922
+ }
923
+ const liveResult = {
924
+ ...result,
925
+ cookie_source: session.sourceLabel,
926
+ site: session.site.id,
927
+ };
928
+ writeQoderLimitsCache(liveResult, { home, nowMs });
929
+ return liveResult;
930
+ } catch (error) {
931
+ lastError = error;
932
+ }
933
+ }
934
+
935
+ const local = readQoderLocalQuota({ home, platform, env });
936
+ if (local) {
937
+ if (activityWindow) {
938
+ local.secondary_window = activityWindow;
939
+ local.source += activitySource === "disk-cache" ? "+cached-activity" : "+activity";
940
+ }
941
+ writeQoderLimitsCache(local, { home, nowMs });
942
+ return local;
943
+ }
944
+ if (activityWindow) {
945
+ return {
946
+ configured: true,
947
+ error: null,
948
+ plan_label: typeof rpcAuth?.userType === "string" ? rpcAuth.userType : null,
949
+ primary_window: null,
950
+ secondary_window: activityWindow,
951
+ source: activitySource === "disk-cache" ? "cached-activity" : "local-ipc+provider-api",
952
+ };
953
+ }
954
+ if (lastError) {
955
+ return {
956
+ configured: true,
957
+ error: lastError.message || "Qoder usage fetch failed.",
958
+ auth_action_required: lastError.code === "AUTH_EXPIRED" ? "reauth" : undefined,
959
+ };
960
+ }
961
+ return { configured: false };
962
+ }
963
+
964
+ module.exports = {
965
+ QODER_SITES,
966
+ CHROMIUM_COOKIE_SQL,
967
+ normalizeQoderUsageResponse,
968
+ normalizeQoderRpcUsage,
969
+ normalizeQoderActivityResponse,
970
+ decryptChromiumCookie,
971
+ cookieHeaderFromRows,
972
+ importMacosQoderSessions,
973
+ qoderRequestHeaders,
974
+ fetchQoderUsage,
975
+ qoderRpcRequest,
976
+ buildQoderActivityHeaders,
977
+ fetchQoderActivity,
978
+ readQoderActivityCache,
979
+ writeQoderActivityCache,
980
+ readQoderLimitsCache,
981
+ writeQoderLimitsCache,
982
+ parseQoderQuotaLog,
983
+ readQoderLocalQuota,
984
+ fetchQoderLimits,
985
+ };