tokentracker-cli 0.84.6 → 0.84.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +10 -8
- package/README.ja.md +9 -7
- package/README.ko.md +9 -7
- package/README.md +12 -10
- package/README.zh-CN.md +9 -7
- package/dashboard/dist/assets/{AchievementBadge-CKIvD3lG.js → AchievementBadge-DAAenS6A.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-CZWCUiS5.js → AchievementsPage-DJ9GMKxL.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-BSk_fR8P.js → AchievementsSection-DtpLrh2Q.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-CjHkcmuq.js → BadgeDetailModal-CkE1DwdN.js} +1 -1
- package/dashboard/dist/assets/Card-Cs2vVMHG.js +1 -0
- package/dashboard/dist/assets/{ClawdAnimated-Cl6gmF7u.js → ClawdAnimated-BCSNeob2.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-B0rcodcF.js → CommandPalette-CFDTjp76.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-BtlTJbf4.js → CommunityStatsModal-D2oJpI9i.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-tJCWVZQH.js → ConfirmModal-D0a_xdwF.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-Dtxq8dzI.js → DashboardPage-CMCqvpkA.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-9USuEsRu.js → DevicePage-c9Fk13gJ.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-DxHdqMcl.js → DialogClose-jsfDvF2n.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-Dwxj5h5w.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-C1ZeQgf2.js → DialogTitle-Dt835L7T.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Cx4jLl9G.js → FadeIn-DJssGrj8.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-BOyuGkoA.js → HeaderGithubStar-B9MwwA-P.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-BDhqMR3L.js → HoverTooltip-DPLMJkai.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-DTvA9Wmx.js → IpCheckPage-BXKjxEHe.js} +1 -1
- package/dashboard/dist/assets/LandingPage-BJNMQTec.js +2 -0
- package/dashboard/dist/assets/{LeaderboardAvatar-3ibNg6If.js → LeaderboardAvatar-BNwbnOa3.js} +1 -1
- package/dashboard/dist/assets/LeaderboardPage-BOxejyMy.js +2 -0
- package/dashboard/dist/assets/{LeaderboardProfileModal-DeGbJtvi.js → LeaderboardProfileModal-BEqUZCOD.js} +4 -4
- package/dashboard/dist/assets/{LeaderboardProfilePage-DEPzO6hW.js → LeaderboardProfilePage-CUDV5iyh.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-ejlJN1vi.js +4 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-C76LAT_3.js → LocalOnlyNotice-B3BSeCHd.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-DEyCJT67.js → LoginCard-2mAb696j.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-CueMQgAZ.js → LoginModal-cacKhc6U.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-CwqBb5Fd.js → LoginPage-rnBlie8p.js} +1 -1
- package/dashboard/dist/assets/PetPage-D3DhuS6O.js +1 -0
- package/dashboard/dist/assets/{PopoverPopup-Dt9VY2VQ.js → PopoverPopup-CxATX7M9.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-CHx4ohfx.js → ProviderIcon-B5b9LkmM.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DB6R7fjf.js → ResetPasswordPage-BGKmQ_JW.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-BINOpXIY.js → SegmentedControl-BMeYCm2b.js} +1 -1
- package/dashboard/dist/assets/{Select-DcYXcsKv.js → Select-CYHXq8im.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-BDv2Kb3x.js → SelectItemText-DvI4i06h.js} +1 -1
- package/dashboard/dist/assets/ServiceStatusPage-dv26-VvI.js +1 -0
- package/dashboard/dist/assets/{SessionsPage-cL_w0ytB.js → SessionsPage-DBQruqkO.js} +2 -2
- package/dashboard/dist/assets/SettingsPage-BWRbQ3Iq.js +1 -0
- package/dashboard/dist/assets/SkillsPage-BonhKgAt.js +1 -0
- package/dashboard/dist/assets/{TokenGalaxy-BHq4YSv2.js → TokenGalaxy-BBUD1kpU.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-akXaaKb6.js → ToolbarRootContext-DPMm8uYz.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-DPhFXzQc.js → TrendMonitor-CZVE6gID.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-Nf0UEz7s.js → WidgetsPage-Bz9cieeZ.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-D4zOSsqD.js → WrappedPage-CruMolZd.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-C3VH6nS4.js +1483 -0
- package/dashboard/dist/assets/__vite-browser-external-hXV_ZYTG.css +1 -0
- package/dashboard/dist/assets/{agent-logos-Bja3VCAa.js → agent-logos-HIeYwIqN.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CzcCNJJ2.js → arrow-up-right-n7Bi4Tvd.js} +1 -1
- package/dashboard/dist/assets/{check-DUHSZCYn.js → check-C5Q44G5k.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-jLvhaNUy.js → chevron-right-BsnptUU0.js} +1 -1
- package/dashboard/dist/assets/{copy-ChyH-ynx.js → copy-BGuL9uFa.js} +1 -1
- package/dashboard/dist/assets/{download-CgwvXrUe.js → download-BljgPwR2.js} +1 -1
- package/dashboard/dist/assets/external-link-B4Oz8hUe.js +1 -0
- package/dashboard/dist/assets/{flame-ysf1F8uB.js → flame-ChLG_NZE.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DAa2cubl.js → format-tokens-DjuNk13Z.js} +1 -1
- package/dashboard/dist/assets/{icons-DccoWQq9.js → icons-CkO42M-u.js} +1 -1
- package/dashboard/dist/assets/{index-CAycNPQf.js → index-CEsvYBWY.js} +1 -1
- package/dashboard/dist/assets/{index-CxExExXh.js → index-CGxawuWx.js} +1 -1
- package/dashboard/dist/assets/{index-kkQrDRat.js → index-D87oE_rV.js} +1 -1
- package/dashboard/dist/assets/{info-DWQQIq9M.js → info-et8xtJ4v.js} +1 -1
- package/dashboard/dist/assets/limits-providers-vO_86yRy.js +1 -0
- package/dashboard/dist/assets/{link-2-CKeVVq4P.js → link-2-Dge2rBkr.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-Dw0tu6IN.js → loader-circle-Cs3wpqJe.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-BMU0YzEF.js → maximize-2-zn71uZX3.js} +1 -1
- package/dashboard/dist/assets/{provider-display-Cf9I-VLS.js → provider-display-CTt6mzLe.js} +1 -1
- package/dashboard/dist/assets/{react-BhbcISKy.js → react-BbHDQe6Q.js} +1 -1
- package/dashboard/dist/assets/{search-BoY8EW1z.js → search-3CMhatWb.js} +1 -1
- package/dashboard/dist/assets/{skills-api-KVH21GJw.js → skills-api-Cm1aiqWL.js} +1 -1
- package/dashboard/dist/assets/{store-BL7rSajG.js → store-DclXV77t.js} +1 -1
- package/dashboard/dist/assets/{terminal-CeMuITdm.js → terminal-BC7uFcA7.js} +1 -1
- package/dashboard/dist/assets/{trash-2-jVUCKqrR.js → trash-2-BT8V531F.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-uWK0fE6l.js → use-community-stats-DJr59SGu.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-DWsF4ShM.js → use-limits-display-prefs-DU8OOFVb.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-SEbqWDvc.js → use-native-settings-BvSwI4BL.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-D4UhrPvw.js → use-ordered-list-Cq5AyV2x.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-D1_q3aRK.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-DbysyUTJ.js → use-session-efficiency-pref-WS8NhXuW.js} +1 -1
- package/dashboard/dist/assets/{use-transform-DaEssGaZ.js → use-transform-CLYsi8xc.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-CmmQ34Ov.js → use-usage-limits-CGoQQDG-.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-DvBEn4rx.js → useCurrency-D0GtEoJD.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-QozsdFng.js → useScrollLock-TIUBSeIM.js} +1 -1
- package/dashboard/dist/assets/useTokenFormat-B0TBL9La.js +1 -0
- package/dashboard/dist/assets/{zap-Dp_aJ1q6.js → zap-D89769CL.js} +1 -1
- package/dashboard/dist/brand-logos/qoder.svg +5 -0
- package/dashboard/dist/index.html +18 -15
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/llms.txt +3 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +2 -2
- package/src/commands/init.js +1 -0
- package/src/commands/status.js +17 -0
- package/src/commands/sync.js +146 -126
- package/src/lib/local-api.js +4 -1
- package/src/lib/pet-packages.js +44 -2
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/provider-status.js +104 -0
- package/src/lib/qoder-limits.js +782 -0
- package/src/lib/rollout.js +385 -0
- package/src/lib/usage-limits.js +31 -2
- package/dashboard/dist/assets/Card-D6hdzwvI.js +0 -1
- package/dashboard/dist/assets/DialogDescription-DgUSe6A1.js +0 -1
- package/dashboard/dist/assets/LandingPage-k1271lWV.js +0 -2
- package/dashboard/dist/assets/LeaderboardPage-Byww5UpT.js +0 -2
- package/dashboard/dist/assets/LimitsPage-DPtqL0PS.js +0 -4
- package/dashboard/dist/assets/PetPage--xQAjW4_.js +0 -1
- package/dashboard/dist/assets/SettingsPage-CHGA0bQS.js +0 -1
- package/dashboard/dist/assets/SkillsPage-I29SqkXO.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-UqlSqI-1.js +0 -1465
- package/dashboard/dist/assets/__vite-browser-external-WKilYZnq.css +0 -1
- package/dashboard/dist/assets/limits-providers-B_18X9Rp.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-C3UrOh55.js +0 -1
- package/dashboard/dist/assets/useTokenFormat-BTLrL7Mi.js +0 -1
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const net = require("node:net");
|
|
6
|
+
const os = require("node:os");
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
|
|
9
|
+
const QODER_SITES = {
|
|
10
|
+
international: {
|
|
11
|
+
id: "international",
|
|
12
|
+
origin: "https://qoder.com",
|
|
13
|
+
usageUrl: "https://qoder.com/api/v2/me/usages/big_model_credits",
|
|
14
|
+
domains: new Set(["qoder.com", "www.qoder.com"]),
|
|
15
|
+
},
|
|
16
|
+
china: {
|
|
17
|
+
id: "china",
|
|
18
|
+
origin: "https://qoder.com.cn",
|
|
19
|
+
usageUrl: "https://qoder.com.cn/api/v2/me/usages/big_model_credits",
|
|
20
|
+
domains: new Set(["qoder.com.cn", "www.qoder.com.cn"]),
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const QODER_USER_AGENT =
|
|
25
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
|
|
26
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36";
|
|
27
|
+
|
|
28
|
+
// Qoder 1.18 serves promotional call buckets (including Ultimate Free Calls)
|
|
29
|
+
// from its OpenAPI activity endpoint. The request uses the same COSY envelope
|
|
30
|
+
// as Qoder CLI 1.0.22. Authentication comes from Qoder's already-running local
|
|
31
|
+
// service over JSON-RPC; TokenTracker never persists or logs those credentials.
|
|
32
|
+
const QODER_ACTIVITY_URL = "https://openapi.qoder.sh/algo/api/v2/activity";
|
|
33
|
+
const QODER_ULTIMATE_ACTIVITY_ID = "ultimate_200_free_invoke";
|
|
34
|
+
const QODER_COSY_VERSION = "1.0.22";
|
|
35
|
+
const QODER_ACTIVITY_CACHE_FILE = "qoder-activity-cache.json";
|
|
36
|
+
const QODER_ACTIVITY_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
37
|
+
const QODER_LIMITS_CACHE_FILE = "qoder-usage-limits-cache.json";
|
|
38
|
+
const QODER_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
39
|
+
const QODER_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
|
|
40
|
+
const QODER_SERVER_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
41
|
+
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
|
|
42
|
+
4k6Pz1EaDicBMpdpxKduSZu5OANqUq8er4GM95omAGIOPOh+Nx0spthYA2BqGz+l
|
|
43
|
+
6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
|
|
44
|
+
XcW+ML9FoCI6AOvOzwIDAQAB
|
|
45
|
+
-----END PUBLIC KEY-----`;
|
|
46
|
+
|
|
47
|
+
function finiteNumber(value, field) {
|
|
48
|
+
const number = Number(value);
|
|
49
|
+
if (!Number.isFinite(number)) {
|
|
50
|
+
throw new Error(`Qoder usage response is missing ${field}.`);
|
|
51
|
+
}
|
|
52
|
+
return number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function firstDefined(object, camel, snake) {
|
|
56
|
+
return object?.[camel] ?? object?.[snake];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeResetAt(value) {
|
|
60
|
+
if (value === null || value === undefined || value === "") return null;
|
|
61
|
+
if (typeof value === "number" || /^\d+(?:\.\d+)?$/.test(String(value).trim())) {
|
|
62
|
+
const raw = Number(value);
|
|
63
|
+
if (!Number.isFinite(raw)) return null;
|
|
64
|
+
const milliseconds = raw > 10_000_000_000 ? raw : raw * 1000;
|
|
65
|
+
const date = new Date(milliseconds);
|
|
66
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : null;
|
|
67
|
+
}
|
|
68
|
+
const timestamp = Date.parse(String(value));
|
|
69
|
+
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function quotaSummary(container) {
|
|
73
|
+
return firstDefined(container, "quotaSummary", "quota_summary") || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeQuotaSummary(summary) {
|
|
77
|
+
if (!summary || typeof summary !== "object") {
|
|
78
|
+
throw new Error("Qoder usage response is missing totalQuota.quotaSummary.");
|
|
79
|
+
}
|
|
80
|
+
const used = finiteNumber(firstDefined(summary, "usedValue", "used_value"), "usedValue");
|
|
81
|
+
const total = finiteNumber(firstDefined(summary, "limitValue", "limit_value"), "limitValue");
|
|
82
|
+
const rawRemaining = firstDefined(summary, "remainingValue", "remaining_value");
|
|
83
|
+
const remaining = rawRemaining === null || rawRemaining === undefined
|
|
84
|
+
? Math.max(0, total - used)
|
|
85
|
+
: finiteNumber(rawRemaining, "remainingValue");
|
|
86
|
+
const rawPercentage = firstDefined(summary, "usagePercentage", "usage_percentage");
|
|
87
|
+
const providedPercentage = rawPercentage === null || rawPercentage === undefined
|
|
88
|
+
? null
|
|
89
|
+
: finiteNumber(rawPercentage, "usagePercentage");
|
|
90
|
+
|
|
91
|
+
if (used < 0 || total < 0 || remaining < 0) {
|
|
92
|
+
throw new Error("Qoder quota values must be nonnegative.");
|
|
93
|
+
}
|
|
94
|
+
if (total === 0 && (used !== 0 || remaining !== 0)) {
|
|
95
|
+
throw new Error("Qoder zero total quota must have zero usage and remaining.");
|
|
96
|
+
}
|
|
97
|
+
const usagePercentage = providedPercentage ?? (total > 0 ? (used / total) * 100 : 100);
|
|
98
|
+
return {
|
|
99
|
+
used,
|
|
100
|
+
total,
|
|
101
|
+
remaining,
|
|
102
|
+
usagePercentage: Math.max(0, Math.min(100, usagePercentage)),
|
|
103
|
+
unit: typeof summary.unit === "string" && summary.unit.trim() ? summary.unit.trim() : null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeQoderUsageResponse(response) {
|
|
108
|
+
if (!response || typeof response !== "object") {
|
|
109
|
+
throw new Error("Qoder usage response is not an object.");
|
|
110
|
+
}
|
|
111
|
+
const totalContainer = firstDefined(response, "totalQuota", "total_quota");
|
|
112
|
+
const sharedContainer = firstDefined(response, "sharedQuota", "shared_quota");
|
|
113
|
+
const base = normalizeQuotaSummary(quotaSummary(totalContainer));
|
|
114
|
+
const sharedSummary = quotaSummary(sharedContainer);
|
|
115
|
+
const shared = sharedSummary ? normalizeQuotaSummary(sharedSummary) : null;
|
|
116
|
+
|
|
117
|
+
const used = base.used + (shared?.used || 0);
|
|
118
|
+
const total = base.total + (shared?.total || 0);
|
|
119
|
+
const remaining = base.remaining + (shared?.remaining || 0);
|
|
120
|
+
const usedPercent = shared
|
|
121
|
+
? total > 0
|
|
122
|
+
? (used / total) * 100
|
|
123
|
+
: 100
|
|
124
|
+
: base.usagePercentage;
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
configured: true,
|
|
128
|
+
error: null,
|
|
129
|
+
primary_window: {
|
|
130
|
+
used_percent: Math.max(0, Math.min(100, usedPercent)),
|
|
131
|
+
reset_at: normalizeResetAt(firstDefined(response, "nextResetAt", "next_reset_at")),
|
|
132
|
+
used_credits: used,
|
|
133
|
+
limit_credits: total,
|
|
134
|
+
remaining_credits: remaining,
|
|
135
|
+
unit: base.unit || shared?.unit || null,
|
|
136
|
+
},
|
|
137
|
+
source: "provider-api",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function qoderDataRoot({
|
|
142
|
+
home = os.homedir(),
|
|
143
|
+
env = process.env,
|
|
144
|
+
platform = process.platform,
|
|
145
|
+
} = {}) {
|
|
146
|
+
if (typeof env.QODER_HOME === "string" && env.QODER_HOME.trim()) {
|
|
147
|
+
return path.resolve(env.QODER_HOME.trim());
|
|
148
|
+
}
|
|
149
|
+
if (platform === "darwin") {
|
|
150
|
+
return path.join(home, "Library", "Application Support", "Qoder");
|
|
151
|
+
}
|
|
152
|
+
if (platform === "win32") {
|
|
153
|
+
return path.join(env.APPDATA || path.join(home, "AppData", "Roaming"), "Qoder");
|
|
154
|
+
}
|
|
155
|
+
return path.join(home, ".config", "Qoder");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function qoderRpcRequest(method, params = {}, {
|
|
159
|
+
home = os.homedir(),
|
|
160
|
+
env = process.env,
|
|
161
|
+
platform = process.platform,
|
|
162
|
+
timeoutMs = 1500,
|
|
163
|
+
netModule = net,
|
|
164
|
+
} = {}) {
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
const infoPath = path.join(qoderDataRoot({ home, env, platform }), "SharedClientCache", ".info.json");
|
|
167
|
+
let info;
|
|
168
|
+
try {
|
|
169
|
+
info = JSON.parse(fs.readFileSync(infoPath, "utf8"));
|
|
170
|
+
} catch (_error) {
|
|
171
|
+
reject(new Error("Qoder local service is not running."));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const socketPath = typeof info?.ipcServerPath === "string" ? info.ipcServerPath.trim() : "";
|
|
175
|
+
if (!socketPath) {
|
|
176
|
+
reject(new Error("Qoder local service endpoint is unavailable."));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let settled = false;
|
|
181
|
+
let buffer = Buffer.alloc(0);
|
|
182
|
+
const socket = netModule.createConnection(socketPath);
|
|
183
|
+
const timer = setTimeout(() => finish(new Error("Qoder local service request timed out.")), timeoutMs);
|
|
184
|
+
|
|
185
|
+
function finish(error, value) {
|
|
186
|
+
if (settled) return;
|
|
187
|
+
settled = true;
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
socket.destroy();
|
|
190
|
+
if (error) reject(error);
|
|
191
|
+
else resolve(value);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
socket.on("connect", () => {
|
|
195
|
+
const body = Buffer.from(JSON.stringify({
|
|
196
|
+
jsonrpc: "2.0",
|
|
197
|
+
id: 1,
|
|
198
|
+
method,
|
|
199
|
+
params,
|
|
200
|
+
}));
|
|
201
|
+
socket.write(`Content-Length: ${body.length}\r\n\r\n`);
|
|
202
|
+
socket.write(body);
|
|
203
|
+
});
|
|
204
|
+
socket.on("error", (error) => finish(error));
|
|
205
|
+
socket.on("data", (chunk) => {
|
|
206
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
207
|
+
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
208
|
+
if (headerEnd < 0) return;
|
|
209
|
+
const match = /Content-Length:\s*(\d+)/i.exec(buffer.subarray(0, headerEnd).toString("ascii"));
|
|
210
|
+
if (!match) {
|
|
211
|
+
finish(new Error("Qoder local service returned an invalid response."));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const bodyLength = Number(match[1]);
|
|
215
|
+
const bodyStart = headerEnd + 4;
|
|
216
|
+
if (!Number.isFinite(bodyLength) || bodyLength < 0 || bodyLength > 4 * 1024 * 1024) {
|
|
217
|
+
finish(new Error("Qoder local service response is too large."));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (buffer.length < bodyStart + bodyLength) return;
|
|
221
|
+
try {
|
|
222
|
+
const message = JSON.parse(buffer.subarray(bodyStart, bodyStart + bodyLength).toString("utf8"));
|
|
223
|
+
if (message.error) {
|
|
224
|
+
finish(new Error(message.error.message || "Qoder local service request failed."));
|
|
225
|
+
} else {
|
|
226
|
+
finish(null, message.result);
|
|
227
|
+
}
|
|
228
|
+
} catch (_error) {
|
|
229
|
+
finish(new Error("Qoder local service returned invalid JSON."));
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function normalizeQoderRpcUsage(response) {
|
|
236
|
+
if (!response || typeof response !== "object") return null;
|
|
237
|
+
const quota = response.userQuota;
|
|
238
|
+
if (!quota || typeof quota !== "object") return null;
|
|
239
|
+
const used = finiteNumber(quota.used, "userQuota.used");
|
|
240
|
+
const total = finiteNumber(quota.total, "userQuota.total");
|
|
241
|
+
const remaining = finiteNumber(quota.remaining, "userQuota.remaining");
|
|
242
|
+
const reportedPercent = Number(response.totalUsagePercentage ?? quota.percentage);
|
|
243
|
+
const usedPercent = total === 0
|
|
244
|
+
? 0
|
|
245
|
+
: response.isQuotaExceeded === true
|
|
246
|
+
? 100
|
|
247
|
+
: Number.isFinite(reportedPercent)
|
|
248
|
+
? reportedPercent
|
|
249
|
+
: (used / total) * 100;
|
|
250
|
+
const normalizedExpiry = normalizeResetAt(response.expiresAt);
|
|
251
|
+
const expiryMs = normalizedExpiry ? Date.parse(normalizedExpiry) : NaN;
|
|
252
|
+
return {
|
|
253
|
+
configured: true,
|
|
254
|
+
error: null,
|
|
255
|
+
plan_label: typeof response.userType === "string" ? response.userType : null,
|
|
256
|
+
quota_exceeded: response.isQuotaExceeded === true,
|
|
257
|
+
primary_window: {
|
|
258
|
+
used_percent: Math.max(0, Math.min(100, usedPercent)),
|
|
259
|
+
// Qoder uses 9999-12-31 as a no-expiry sentinel for Free accounts.
|
|
260
|
+
reset_at: Number.isFinite(expiryMs) && expiryMs < Date.UTC(2100, 0, 1)
|
|
261
|
+
? normalizedExpiry
|
|
262
|
+
: null,
|
|
263
|
+
used_credits: used,
|
|
264
|
+
limit_credits: total,
|
|
265
|
+
remaining_credits: remaining,
|
|
266
|
+
unit: typeof quota.unit === "string" && quota.unit.trim() ? quota.unit.trim() : "credits",
|
|
267
|
+
},
|
|
268
|
+
source: "local-ipc",
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function normalizeQoderActivityResponse(response, { nowMs = Date.now() } = {}) {
|
|
273
|
+
if (!response || typeof response !== "object") {
|
|
274
|
+
throw new Error("Qoder activity response is not an object.");
|
|
275
|
+
}
|
|
276
|
+
if (response.code !== undefined && Number(response.code) !== 0) {
|
|
277
|
+
throw new Error(`Qoder activity API returned ${response.msg || `code ${response.code}`}.`);
|
|
278
|
+
}
|
|
279
|
+
const activities = Array.isArray(response.data?.activities) ? response.data.activities : [];
|
|
280
|
+
const activity = activities.find((entry) => {
|
|
281
|
+
if (!entry || entry.activityId !== QODER_ULTIMATE_ACTIVITY_ID || entry.eligible === false) return false;
|
|
282
|
+
if (String(entry.tagStyle || "").toUpperCase() === "EXPIRED") return false;
|
|
283
|
+
const endAt = Number(entry.activityEndAt);
|
|
284
|
+
return !Number.isFinite(endAt) || endAt <= 0 || endAt > nowMs;
|
|
285
|
+
});
|
|
286
|
+
if (!activity) return null;
|
|
287
|
+
const used = finiteNumber(activity.used, "activity.used");
|
|
288
|
+
const total = finiteNumber(activity.limit, "activity.limit");
|
|
289
|
+
const rawRemaining = Number(activity.remaining);
|
|
290
|
+
const remaining = Number.isFinite(rawRemaining) ? rawRemaining : Math.max(0, total - used);
|
|
291
|
+
if (used < 0 || total <= 0 || remaining < 0) {
|
|
292
|
+
throw new Error("Qoder activity quota values are invalid.");
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
used_percent: Math.max(0, Math.min(100, (used / total) * 100)),
|
|
296
|
+
reset_at: normalizeResetAt(activity.activityEndAt),
|
|
297
|
+
used_credits: used,
|
|
298
|
+
limit_credits: total,
|
|
299
|
+
remaining_credits: remaining,
|
|
300
|
+
unit: "calls",
|
|
301
|
+
activity_id: QODER_ULTIMATE_ACTIVITY_ID,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function qoderActivityCachePath({ home = os.homedir() } = {}) {
|
|
306
|
+
return path.join(home, ".tokentracker", "tracker", QODER_ACTIVITY_CACHE_FILE);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function readQoderActivityCache({
|
|
310
|
+
home = os.homedir(),
|
|
311
|
+
nowMs = Date.now(),
|
|
312
|
+
maxAgeMs = QODER_ACTIVITY_CACHE_MAX_AGE_MS,
|
|
313
|
+
} = {}) {
|
|
314
|
+
try {
|
|
315
|
+
const parsed = JSON.parse(fs.readFileSync(qoderActivityCachePath({ home }), "utf8"));
|
|
316
|
+
const window = parsed?.activity_window;
|
|
317
|
+
const cachedAtMs = Date.parse(parsed?.cached_at || "");
|
|
318
|
+
if (!window || !Number.isFinite(cachedAtMs) || cachedAtMs > nowMs + 60_000) return null;
|
|
319
|
+
const resetAtMs = Date.parse(window.reset_at || "");
|
|
320
|
+
if (Number.isFinite(resetAtMs)) {
|
|
321
|
+
if (resetAtMs <= nowMs) return null;
|
|
322
|
+
} else if (nowMs - cachedAtMs > maxAgeMs) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
const used = Number(window.used_credits);
|
|
326
|
+
const total = Number(window.limit_credits);
|
|
327
|
+
const remaining = Number(window.remaining_credits);
|
|
328
|
+
if (!Number.isFinite(used) || !Number.isFinite(total) || !Number.isFinite(remaining)
|
|
329
|
+
|| used < 0 || total <= 0 || remaining < 0) return null;
|
|
330
|
+
return window;
|
|
331
|
+
} catch (_error) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function writeQoderActivityCache(window, {
|
|
337
|
+
home = os.homedir(),
|
|
338
|
+
nowMs = Date.now(),
|
|
339
|
+
} = {}) {
|
|
340
|
+
if (!window) return;
|
|
341
|
+
const cachePath = qoderActivityCachePath({ home });
|
|
342
|
+
const payload = {
|
|
343
|
+
activity_window: window,
|
|
344
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
345
|
+
};
|
|
346
|
+
try {
|
|
347
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
348
|
+
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
349
|
+
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
350
|
+
fs.renameSync(tmpPath, cachePath);
|
|
351
|
+
} catch (_error) {}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function qoderLimitsCachePath({ home = os.homedir() } = {}) {
|
|
355
|
+
return path.join(home, ".tokentracker", "tracker", QODER_LIMITS_CACHE_FILE);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function qoderCachedWindow(window, { cachedAtMs, nowMs } = {}) {
|
|
359
|
+
if (!window || typeof window !== "object") return null;
|
|
360
|
+
const resetAtMs = Date.parse(window.reset_at || "");
|
|
361
|
+
if (Number.isFinite(resetAtMs)) return resetAtMs > nowMs ? window : null;
|
|
362
|
+
return nowMs - cachedAtMs <= QODER_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS ? window : null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function readQoderLimitsCache({
|
|
366
|
+
home = os.homedir(),
|
|
367
|
+
nowMs = Date.now(),
|
|
368
|
+
} = {}) {
|
|
369
|
+
try {
|
|
370
|
+
const raw = JSON.parse(fs.readFileSync(qoderLimitsCachePath({ home }), "utf8"))?.qoder;
|
|
371
|
+
const cachedAtMs = Date.parse(raw?.cached_at || "");
|
|
372
|
+
if (!Number.isFinite(cachedAtMs) || cachedAtMs > nowMs + 60_000) return null;
|
|
373
|
+
const primaryWindow = qoderCachedWindow(raw.primary_window, { cachedAtMs, nowMs });
|
|
374
|
+
const secondaryWindow = qoderCachedWindow(raw.secondary_window, { cachedAtMs, nowMs });
|
|
375
|
+
if (!primaryWindow && !secondaryWindow) return null;
|
|
376
|
+
const hasUnexpiredDatedWindow = [primaryWindow, secondaryWindow]
|
|
377
|
+
.some((window) => Number.isFinite(Date.parse(window?.reset_at || "")));
|
|
378
|
+
if (nowMs - cachedAtMs > QODER_LIMITS_CACHE_MAX_AGE_MS && !hasUnexpiredDatedWindow) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
configured: true,
|
|
383
|
+
error: null,
|
|
384
|
+
plan_label: typeof raw.plan_label === "string" ? raw.plan_label : null,
|
|
385
|
+
quota_exceeded: raw.quota_exceeded === true,
|
|
386
|
+
primary_window: primaryWindow,
|
|
387
|
+
secondary_window: secondaryWindow,
|
|
388
|
+
cached: true,
|
|
389
|
+
stale: true,
|
|
390
|
+
cached_at: raw.cached_at,
|
|
391
|
+
source: "disk-cache",
|
|
392
|
+
};
|
|
393
|
+
} catch (_error) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function writeQoderLimitsCache(limits, {
|
|
399
|
+
home = os.homedir(),
|
|
400
|
+
nowMs = Date.now(),
|
|
401
|
+
} = {}) {
|
|
402
|
+
if (!limits?.configured || limits.error || (!limits.primary_window && !limits.secondary_window)) return;
|
|
403
|
+
const cachePath = qoderLimitsCachePath({ home });
|
|
404
|
+
const payload = {
|
|
405
|
+
qoder: {
|
|
406
|
+
plan_label: limits.plan_label || null,
|
|
407
|
+
quota_exceeded: limits.quota_exceeded === true,
|
|
408
|
+
primary_window: limits.primary_window || null,
|
|
409
|
+
secondary_window: limits.secondary_window || null,
|
|
410
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
try {
|
|
414
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
415
|
+
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
416
|
+
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
417
|
+
fs.renameSync(tmpPath, cachePath);
|
|
418
|
+
} catch (_error) {}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function buildQoderActivityHeaders(authStatus, {
|
|
422
|
+
nowMs = Date.now(),
|
|
423
|
+
randomUUID = crypto.randomUUID,
|
|
424
|
+
} = {}) {
|
|
425
|
+
const userId = String(authStatus?.id || authStatus?.accountId || "").trim();
|
|
426
|
+
const accessToken = String(authStatus?.token || "").trim();
|
|
427
|
+
if (!userId || !accessToken) {
|
|
428
|
+
throw new Error("Qoder local session is missing activity credentials.");
|
|
429
|
+
}
|
|
430
|
+
const temporaryKey = Buffer.from(randomUUID().replace(/-/g, "").slice(0, 16), "ascii");
|
|
431
|
+
const cosyKey = crypto.publicEncrypt(
|
|
432
|
+
{ key: QODER_SERVER_PUBLIC_KEY, padding: crypto.constants.RSA_PKCS1_PADDING },
|
|
433
|
+
temporaryKey,
|
|
434
|
+
).toString("base64");
|
|
435
|
+
const identity = {
|
|
436
|
+
name: authStatus.name || "",
|
|
437
|
+
aid: userId,
|
|
438
|
+
uid: userId,
|
|
439
|
+
yx_uid: authStatus.yxUid || "",
|
|
440
|
+
organization_id: authStatus.orgId || "",
|
|
441
|
+
organization_name: authStatus.orgName || "",
|
|
442
|
+
user_type: authStatus.userType || "personal_standard",
|
|
443
|
+
security_oauth_token: accessToken,
|
|
444
|
+
refresh_token: authStatus.refreshToken || "",
|
|
445
|
+
};
|
|
446
|
+
const cipher = crypto.createCipheriv("aes-128-cbc", temporaryKey, temporaryKey);
|
|
447
|
+
const info = Buffer.concat([
|
|
448
|
+
cipher.update(Buffer.from(JSON.stringify(identity), "utf8")),
|
|
449
|
+
cipher.final(),
|
|
450
|
+
]).toString("base64");
|
|
451
|
+
const payload = Buffer.from(JSON.stringify({
|
|
452
|
+
version: "v1",
|
|
453
|
+
requestId: randomUUID(),
|
|
454
|
+
info,
|
|
455
|
+
cosyVersion: QODER_COSY_VERSION,
|
|
456
|
+
ideVersion: "",
|
|
457
|
+
}), "utf8").toString("base64");
|
|
458
|
+
const cosyDate = String(Math.floor(nowMs / 1000));
|
|
459
|
+
const signature = crypto
|
|
460
|
+
.createHash("md5")
|
|
461
|
+
.update(`${payload}\n${cosyKey}\n${cosyDate}\n\n/api/v2/activity`, "utf8")
|
|
462
|
+
.digest("hex");
|
|
463
|
+
const machineId = randomUUID();
|
|
464
|
+
return {
|
|
465
|
+
"cosy-data-policy": "agree",
|
|
466
|
+
"cosy-machinetype": "5",
|
|
467
|
+
"cosy-clienttype": "5",
|
|
468
|
+
"cosy-date": cosyDate,
|
|
469
|
+
"cosy-user": userId,
|
|
470
|
+
"cosy-key": cosyKey,
|
|
471
|
+
"cache-control": "no-cache",
|
|
472
|
+
"cosy-business-product": "cli",
|
|
473
|
+
"cosy-business-type": "agent",
|
|
474
|
+
"cosy-scene": "assistant",
|
|
475
|
+
accept: "application/json",
|
|
476
|
+
authorization: `Bearer COSY.${payload}.${signature}`,
|
|
477
|
+
"accept-encoding": "identity",
|
|
478
|
+
"cosy-version": QODER_COSY_VERSION,
|
|
479
|
+
"cosy-machineid": machineId,
|
|
480
|
+
"cosy-machinetoken": machineId,
|
|
481
|
+
"login-version": "v2",
|
|
482
|
+
"user-agent": "Go-http-client/2.0",
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function fetchQoderActivity(authStatus, fetchImpl = fetch, options = {}) {
|
|
487
|
+
const response = await fetchImpl(QODER_ACTIVITY_URL, {
|
|
488
|
+
method: "GET",
|
|
489
|
+
headers: buildQoderActivityHeaders(authStatus, options),
|
|
490
|
+
});
|
|
491
|
+
if (response.status === 401 || response.status === 403) {
|
|
492
|
+
throw new Error("Qoder activity session expired.");
|
|
493
|
+
}
|
|
494
|
+
if (!response.ok) {
|
|
495
|
+
throw new Error(`Qoder activity API returned HTTP ${response.status}.`);
|
|
496
|
+
}
|
|
497
|
+
return normalizeQoderActivityResponse(await response.json(), options);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function qoderRequestHeaders(cookieHeader, site) {
|
|
501
|
+
return {
|
|
502
|
+
Cookie: cookieHeader,
|
|
503
|
+
Accept: "application/json, text/plain, */*",
|
|
504
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
505
|
+
"User-Agent": QODER_USER_AGENT,
|
|
506
|
+
Origin: site.origin,
|
|
507
|
+
Referer: `${site.origin}/account/usage`,
|
|
508
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
509
|
+
"Bx-V": "2.5.35",
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function fetchQoderUsage(cookieHeader, site, fetchImpl) {
|
|
514
|
+
const response = await fetchImpl(site.usageUrl, {
|
|
515
|
+
method: "GET",
|
|
516
|
+
headers: qoderRequestHeaders(cookieHeader, site),
|
|
517
|
+
});
|
|
518
|
+
if (response.status === 401 || response.status === 403) {
|
|
519
|
+
const error = new Error("Qoder login expired. Sign in at qoder.com again.");
|
|
520
|
+
error.code = "AUTH_EXPIRED";
|
|
521
|
+
throw error;
|
|
522
|
+
}
|
|
523
|
+
if (!response.ok) {
|
|
524
|
+
throw new Error(`Qoder usage API returned HTTP ${response.status}.`);
|
|
525
|
+
}
|
|
526
|
+
return normalizeQoderUsageResponse(await response.json());
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function listRecentQoderRendererLogs(logRoot, maxFiles = 12) {
|
|
530
|
+
if (!fs.existsSync(logRoot)) return [];
|
|
531
|
+
const files = [];
|
|
532
|
+
const stack = [{ dir: logRoot, depth: 0 }];
|
|
533
|
+
while (stack.length > 0 && files.length < 2000) {
|
|
534
|
+
const current = stack.pop();
|
|
535
|
+
let entries;
|
|
536
|
+
try {
|
|
537
|
+
entries = fs.readdirSync(current.dir, { withFileTypes: true });
|
|
538
|
+
} catch (_error) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
for (const entry of entries) {
|
|
542
|
+
const full = path.join(current.dir, entry.name);
|
|
543
|
+
if (entry.isDirectory() && current.depth < 4) {
|
|
544
|
+
stack.push({ dir: full, depth: current.depth + 1 });
|
|
545
|
+
} else if (entry.isFile() && entry.name === "renderer.log") {
|
|
546
|
+
let mtimeMs = 0;
|
|
547
|
+
try {
|
|
548
|
+
mtimeMs = fs.statSync(full).mtimeMs;
|
|
549
|
+
} catch (_error) {}
|
|
550
|
+
files.push({ full, mtimeMs });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return files
|
|
555
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
556
|
+
.slice(0, maxFiles)
|
|
557
|
+
.map((entry) => entry.full);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function parseQoderQuotaLog(text) {
|
|
561
|
+
if (typeof text !== "string" || !text) return null;
|
|
562
|
+
const matches = Array.from(text.matchAll(
|
|
563
|
+
/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,
|
|
564
|
+
));
|
|
565
|
+
const match = matches.at(-1);
|
|
566
|
+
if (!match) return null;
|
|
567
|
+
const quotaExceeded = match[2] === "true";
|
|
568
|
+
const used = Number(match[3]);
|
|
569
|
+
const total = Number(match[4]);
|
|
570
|
+
const remaining = Number(match[5]);
|
|
571
|
+
const percentage = Number(match[6]);
|
|
572
|
+
if (![used, total, remaining, percentage].every(Number.isFinite)) return null;
|
|
573
|
+
return {
|
|
574
|
+
configured: true,
|
|
575
|
+
error: null,
|
|
576
|
+
plan_label: match[1],
|
|
577
|
+
quota_exceeded: quotaExceeded,
|
|
578
|
+
primary_window: {
|
|
579
|
+
used_percent: total === 0
|
|
580
|
+
? 0
|
|
581
|
+
: quotaExceeded
|
|
582
|
+
? 100
|
|
583
|
+
: Math.max(0, Math.min(100, percentage)),
|
|
584
|
+
reset_at: null,
|
|
585
|
+
used_credits: used,
|
|
586
|
+
limit_credits: total,
|
|
587
|
+
remaining_credits: remaining,
|
|
588
|
+
unit: match[7],
|
|
589
|
+
},
|
|
590
|
+
source: "local-log",
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function readQoderLocalQuota({
|
|
595
|
+
home = os.homedir(),
|
|
596
|
+
platform = process.platform,
|
|
597
|
+
env = process.env,
|
|
598
|
+
} = {}) {
|
|
599
|
+
const explicitLogRoot =
|
|
600
|
+
typeof env.QODER_LOG_ROOT === "string" && env.QODER_LOG_ROOT.trim()
|
|
601
|
+
? path.resolve(env.QODER_LOG_ROOT.trim())
|
|
602
|
+
: null;
|
|
603
|
+
const configuredRoot =
|
|
604
|
+
typeof env.QODER_HOME === "string" && env.QODER_HOME.trim()
|
|
605
|
+
? path.resolve(env.QODER_HOME.trim())
|
|
606
|
+
: null;
|
|
607
|
+
const logRoot = explicitLogRoot || (configuredRoot
|
|
608
|
+
? path.join(configuredRoot, "logs")
|
|
609
|
+
: platform === "darwin"
|
|
610
|
+
? path.join(home, "Library", "Application Support", "Qoder", "logs")
|
|
611
|
+
: platform === "win32"
|
|
612
|
+
? path.join(env.APPDATA || path.join(home, "AppData", "Roaming"), "Qoder", "logs")
|
|
613
|
+
: path.join(home, ".config", "Qoder", "logs"));
|
|
614
|
+
for (const logPath of listRecentQoderRendererLogs(logRoot)) {
|
|
615
|
+
try {
|
|
616
|
+
const fd = fs.openSync(logPath, "r");
|
|
617
|
+
try {
|
|
618
|
+
const stat = fs.fstatSync(fd);
|
|
619
|
+
const length = Math.min(stat.size, 512 * 1024);
|
|
620
|
+
const buffer = Buffer.alloc(length);
|
|
621
|
+
fs.readSync(fd, buffer, 0, length, Math.max(0, stat.size - length));
|
|
622
|
+
const parsed = parseQoderQuotaLog(buffer.toString("utf8"));
|
|
623
|
+
if (parsed) return parsed;
|
|
624
|
+
} finally {
|
|
625
|
+
fs.closeSync(fd);
|
|
626
|
+
}
|
|
627
|
+
} catch (_error) {}
|
|
628
|
+
}
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function siteFromEnv(value) {
|
|
633
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
634
|
+
return normalized === "cn" || normalized === "china" || normalized.includes(".cn")
|
|
635
|
+
? QODER_SITES.china
|
|
636
|
+
: QODER_SITES.international;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function fetchQoderLimits({
|
|
640
|
+
home = os.homedir(),
|
|
641
|
+
env = process.env,
|
|
642
|
+
platform = process.platform,
|
|
643
|
+
fetchImpl = fetch,
|
|
644
|
+
rpcRequest = qoderRpcRequest,
|
|
645
|
+
nowMs = Date.now(),
|
|
646
|
+
} = {}) {
|
|
647
|
+
let rpcUsage = null;
|
|
648
|
+
let rpcAuth = null;
|
|
649
|
+
let activityWindow = null;
|
|
650
|
+
let activitySource = null;
|
|
651
|
+
const rpcOptions = { home, env, platform };
|
|
652
|
+
// Qoder's shared client accepts only one reliable request at a time. Parallel
|
|
653
|
+
// connections intermittently time out even while the desktop app is running.
|
|
654
|
+
try {
|
|
655
|
+
rpcUsage = normalizeQoderRpcUsage(await rpcRequest("credit/usage", {}, rpcOptions));
|
|
656
|
+
} catch (_error) {}
|
|
657
|
+
try {
|
|
658
|
+
const authResult = await rpcRequest("auth/status", {}, rpcOptions);
|
|
659
|
+
if (authResult && typeof authResult === "object") rpcAuth = authResult;
|
|
660
|
+
} catch (_error) {}
|
|
661
|
+
if (rpcAuth) {
|
|
662
|
+
try {
|
|
663
|
+
activityWindow = await fetchQoderActivity(rpcAuth, fetchImpl, { nowMs });
|
|
664
|
+
if (activityWindow) {
|
|
665
|
+
activitySource = "provider-api";
|
|
666
|
+
writeQoderActivityCache(activityWindow, { home, nowMs });
|
|
667
|
+
}
|
|
668
|
+
} catch (_error) {}
|
|
669
|
+
}
|
|
670
|
+
// Qoder's activity endpoint occasionally succeeds with an empty activities
|
|
671
|
+
// array while the promotion is still active. Treat that as a transient
|
|
672
|
+
// omission, not an authoritative deletion: an activity carries its own
|
|
673
|
+
// expiry, so the last observed window remains valid until reset_at.
|
|
674
|
+
if (!activityWindow) {
|
|
675
|
+
activityWindow = readQoderActivityCache({ home, nowMs });
|
|
676
|
+
if (activityWindow) activitySource = "disk-cache";
|
|
677
|
+
}
|
|
678
|
+
if (rpcUsage) {
|
|
679
|
+
if (!rpcUsage.plan_label && typeof rpcAuth?.userType === "string") {
|
|
680
|
+
rpcUsage.plan_label = rpcAuth.userType;
|
|
681
|
+
}
|
|
682
|
+
if (activityWindow) rpcUsage.secondary_window = activityWindow;
|
|
683
|
+
rpcUsage.source = activityWindow
|
|
684
|
+
? `local-ipc+${activitySource === "disk-cache" ? "cached-activity" : "provider-api"}`
|
|
685
|
+
: "local-ipc";
|
|
686
|
+
writeQoderLimitsCache(rpcUsage, { home, nowMs });
|
|
687
|
+
return rpcUsage;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Match the Antigravity last-good behavior: once the local service is
|
|
691
|
+
// unavailable, prefer a bounded disk snapshot before any provider fallback.
|
|
692
|
+
const cachedLimits = readQoderLimitsCache({ home, nowMs });
|
|
693
|
+
if (cachedLimits) {
|
|
694
|
+
if (activityWindow) cachedLimits.secondary_window = activityWindow;
|
|
695
|
+
return cachedLimits;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const manualCookie = typeof env.QODER_COOKIE === "string" ? env.QODER_COOKIE.trim() : "";
|
|
699
|
+
const sessions = manualCookie
|
|
700
|
+
? [{ cookieHeader: manualCookie, site: siteFromEnv(env.QODER_SITE), sourceLabel: "QODER_COOKIE" }]
|
|
701
|
+
: [];
|
|
702
|
+
|
|
703
|
+
let lastError = null;
|
|
704
|
+
for (const session of sessions) {
|
|
705
|
+
try {
|
|
706
|
+
const result = await fetchQoderUsage(session.cookieHeader, session.site, fetchImpl);
|
|
707
|
+
// Qoder 1.18 can return an all-zero active plan with usage_percentage=0
|
|
708
|
+
// while the desktop client separately marks the account quota-exceeded.
|
|
709
|
+
// Preserve that status as metadata, but match Qoder's Credits UI by
|
|
710
|
+
// rendering an empty 0/0 bucket as 0%, not a full red 100% bar.
|
|
711
|
+
const local = result.primary_window?.limit_credits === 0
|
|
712
|
+
? readQoderLocalQuota({ home, platform, env })
|
|
713
|
+
: null;
|
|
714
|
+
if (local) {
|
|
715
|
+
result.quota_exceeded = local.quota_exceeded === true;
|
|
716
|
+
result.primary_window.reset_at = null;
|
|
717
|
+
result.source = "provider-api+local-log";
|
|
718
|
+
result.plan_label = local.plan_label || null;
|
|
719
|
+
}
|
|
720
|
+
if (activityWindow) {
|
|
721
|
+
result.secondary_window = activityWindow;
|
|
722
|
+
result.source += activitySource === "disk-cache" ? "+cached-activity" : "+activity";
|
|
723
|
+
}
|
|
724
|
+
const liveResult = {
|
|
725
|
+
...result,
|
|
726
|
+
cookie_source: session.sourceLabel,
|
|
727
|
+
site: session.site.id,
|
|
728
|
+
};
|
|
729
|
+
writeQoderLimitsCache(liveResult, { home, nowMs });
|
|
730
|
+
return liveResult;
|
|
731
|
+
} catch (error) {
|
|
732
|
+
lastError = error;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const local = readQoderLocalQuota({ home, platform, env });
|
|
737
|
+
if (local) {
|
|
738
|
+
if (activityWindow) {
|
|
739
|
+
local.secondary_window = activityWindow;
|
|
740
|
+
local.source += activitySource === "disk-cache" ? "+cached-activity" : "+activity";
|
|
741
|
+
}
|
|
742
|
+
writeQoderLimitsCache(local, { home, nowMs });
|
|
743
|
+
return local;
|
|
744
|
+
}
|
|
745
|
+
if (activityWindow) {
|
|
746
|
+
return {
|
|
747
|
+
configured: true,
|
|
748
|
+
error: null,
|
|
749
|
+
plan_label: typeof rpcAuth?.userType === "string" ? rpcAuth.userType : null,
|
|
750
|
+
primary_window: null,
|
|
751
|
+
secondary_window: activityWindow,
|
|
752
|
+
source: activitySource === "disk-cache" ? "cached-activity" : "local-ipc+provider-api",
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
if (lastError) {
|
|
756
|
+
return {
|
|
757
|
+
configured: true,
|
|
758
|
+
error: lastError.message || "Qoder usage fetch failed.",
|
|
759
|
+
auth_action_required: lastError.code === "AUTH_EXPIRED" ? "reauth" : undefined,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
return { configured: false };
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
module.exports = {
|
|
766
|
+
QODER_SITES,
|
|
767
|
+
normalizeQoderUsageResponse,
|
|
768
|
+
normalizeQoderRpcUsage,
|
|
769
|
+
normalizeQoderActivityResponse,
|
|
770
|
+
qoderRequestHeaders,
|
|
771
|
+
fetchQoderUsage,
|
|
772
|
+
qoderRpcRequest,
|
|
773
|
+
buildQoderActivityHeaders,
|
|
774
|
+
fetchQoderActivity,
|
|
775
|
+
readQoderActivityCache,
|
|
776
|
+
writeQoderActivityCache,
|
|
777
|
+
readQoderLimitsCache,
|
|
778
|
+
writeQoderLimitsCache,
|
|
779
|
+
parseQoderQuotaLog,
|
|
780
|
+
readQoderLocalQuota,
|
|
781
|
+
fetchQoderLimits,
|
|
782
|
+
};
|