tokentracker-cli 0.89.0 → 0.90.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tracker.js +9 -1
- package/dashboard/dist/assets/{AchievementBadge-BpDy2XSt.js → AchievementBadge-CCm2bRPL.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-Z3rNbZAL.js → AchievementsPage-5WDOJGj4.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-Cawmrzgp.js → AchievementsSection-BSanOprB.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-D8IPOyzs.js → BadgeDetailModal-Dt-j-8Dp.js} +1 -1
- package/dashboard/dist/assets/{Card-Bco5i8Fn.js → Card-WBsRpfP2.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-RQvg50Wr.js → ClawdAnimated-BRsbkMjg.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-DWLyd7P_.js → CommandPalette-iqRVfuRt.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-DfjsjuRG.js → CommunityStatsModal-B0UD6Hna.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-DNuSWkt3.js → ConfirmModal-BbQwXQDL.js} +1 -1
- package/dashboard/dist/assets/{Controls-DdLHdkEz.js → Controls-CDUMgZN2.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-DN54xqEE.js → DashboardPage-zaMMBWF4.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-ydAtX5oZ.js → DevicePage-CnWWrfz1.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-C40Az_dd.js → DialogClose-D30L50dn.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-LhmGzR2P.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-CCg6RvL8.js → DialogTitle-Cvy1243B.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-a7dOxD0I.js → FadeIn-OtBQOtmZ.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-DmH6JYQF.js → HeaderGithubStar--yBjV0Ft.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-D3yTRqfx.js → HoverTooltip-CUCz1CCv.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-DH4av7RV.js → IpCheckPage-COJIgfHY.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-B1fuvkPh.js → LandingPage-BH_E3eu9.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-C-tKTfIs.js → LeaderboardAvatar-D2FZA-zp.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-CL7Cre5j.js → LeaderboardPage-wmrvEc7Z.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-CqRvLdEB.js → LeaderboardProfileModal-B6dNUb1h.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-BDjW5z87.js → LeaderboardProfilePage-B35bTvlZ.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-B0zXrzFS.js +5 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-CQDKfXwB.js → LocalOnlyNotice-rZ6i6lZc.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-CptsVdtq.js → LoginCard-jMzbbTEd.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-8V4Vsgpr.js → LoginModal-M3ceaKMM.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-DIwzC_95.js → LoginPage-C3jvMlt3.js} +1 -1
- package/dashboard/dist/assets/{PetPage-CXSq_C6b.js → PetPage-Do6WdobX.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-C_6kkB4_.js → PopoverPopup-BtEnnSN0.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-BKGMzdEg.js → ProviderIcon-BIK7rc0D.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-BThSQVj7.js → ResetPasswordPage-C5qXJp8o.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-DdIw-7OY.js → SegmentedControl-DnqPXqkf.js} +1 -1
- package/dashboard/dist/assets/{Select-BuxVjVTV.js → Select-jVeLCMMw.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-B7AP9a2t.js → SelectItemText-CEBAM4fW.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-Db6XpKGJ.js → ServiceStatusPage-BFvwtaEh.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-CIvvinQm.js → SessionsPage-rgT2wqMv.js} +1 -1
- package/dashboard/dist/assets/SettingsPage-eP01vowc.js +2 -0
- package/dashboard/dist/assets/{SkillsPage-2rr2nzvc.js → SkillsPage-Db-yUmt4.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-WS2JBmzs.js → TokenGalaxy-xN2XwViv.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-B5s-ucZR.js → ToolbarRootContext-7dRFaSLz.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-DfgZi0b3.js → TrendMonitor-DBAfvlqu.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-WWaK3S0S.js → WidgetsPage-D06jYB-3.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-C3C85NLm.js → WrappedPage-BruJzhg_.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-B-CAZ5HB.js → __vite-browser-external-DLAMIXvH.js} +63 -18
- package/dashboard/dist/assets/{agent-logos-Do1kFgsi.js → agent-logos-BU-fwHaZ.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-DbomORWZ.js → arrow-up-right-CsqXjuYH.js} +1 -1
- package/dashboard/dist/assets/{check-DzEvx9pJ.js → check-BYwwPV7c.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-DZTnF2NM.js → chevron-right-DFTO4K-h.js} +1 -1
- package/dashboard/dist/assets/{copy-BS7rQ_jR.js → copy-CYH1tqVs.js} +1 -1
- package/dashboard/dist/assets/{download-DlWVR14_.js → download-CYTW_7DA.js} +1 -1
- package/dashboard/dist/assets/{external-link-CtVBRLbf.js → external-link-h_4oGsgX.js} +1 -1
- package/dashboard/dist/assets/{flame-Bd3FUTdZ.js → flame-ditVJv52.js} +1 -1
- package/dashboard/dist/assets/{format-tokens--QLNTkZB.js → format-tokens-DmNhMo51.js} +1 -1
- package/dashboard/dist/assets/{icons-Cs8SXqmU.js → icons-DUHBy5Pm.js} +1 -1
- package/dashboard/dist/assets/{index-DoTYGePn.js → index-BDHyJHaM.js} +1 -1
- package/dashboard/dist/assets/{index-Cj7qHXa4.js → index-Bs9Z1FMs.js} +1 -1
- package/dashboard/dist/assets/{index-CfS3tuU1.js → index-Cf4CvN9c.js} +1 -1
- package/dashboard/dist/assets/{info-CsAt6S8y.js → info-D0Q3iN9G.js} +1 -1
- package/dashboard/dist/assets/limits-providers-G_ZNj5yH.js +1 -0
- package/dashboard/dist/assets/{link-2-CjC85wrH.js → link-2-DUcAmJaa.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-Cf0h8jN7.js → loader-circle-DAzlJG_O.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-DSLJns7Q.js → maximize-2-DQKB3UV2.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-2DRBUt-i.js → modifiers.esm-dcF3BKjk.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-DitUxFt7.js → monitor-smartphone-CkUERJcs.js} +1 -1
- package/dashboard/dist/assets/{provider-display-Dm_uiVV1.js → provider-display-DfwC5xE5.js} +1 -1
- package/dashboard/dist/assets/{react-CExpcnBO.js → react-D7oNu68q.js} +1 -1
- package/dashboard/dist/assets/{search-S2Q8oj2Y.js → search-Vzmb_9cW.js} +1 -1
- package/dashboard/dist/assets/{skills-api-C3mlKc8x.js → skills-api-Dm8maVV9.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-22qRLWIc.js → sortable.esm-MZdKGRyf.js} +1 -1
- package/dashboard/dist/assets/{store-DktMCl4T.js → store-C17h7vNV.js} +1 -1
- package/dashboard/dist/assets/{terminal-DS6Tqgok.js → terminal-DwroZX9a.js} +1 -1
- package/dashboard/dist/assets/{trash-2-BHgzn2Bd.js → trash-2-DUXMc-rb.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DqQQKHPK.js → use-community-stats-Ceqx8392.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-C2fbOm_s.js → use-limits-display-prefs-Cw7CqToT.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BnIpnofG.js → use-ordered-list-_ata-KnQ.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-ui885aKl.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-B78vQ2pp.js → use-session-efficiency-pref-BLwuzenN.js} +1 -1
- package/dashboard/dist/assets/{use-transform-DJeSBZmb.js → use-transform-BZD5G1Vo.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-BkSQGzCV.js → use-usage-limits-SU1KMiWn.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-BY7ZJ-XD.js → useCurrency-C1XiHwOT.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-CHsj-WN5.js → useScrollLock-BCWru-KW.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-CVJJAs3R.js → useTokenFormat-B17dq2qN.js} +1 -1
- package/dashboard/dist/assets/{zap-Dtudxu92.js → zap-BEIl7X9I.js} +1 -1
- package/dashboard/dist/brand-logos/volcano-ark.svg +14 -0
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/status.js +3 -3
- package/src/lib/ark-coding-plan-limits.js +462 -0
- package/src/lib/command-runner.js +286 -0
- package/src/lib/cursor-config.js +84 -86
- package/src/lib/fs.js +11 -2
- package/src/lib/local-api.js +137 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/proxy-env.js +397 -44
- package/src/lib/proxy-settings.js +163 -0
- package/src/lib/rollout.js +80 -39
- package/src/lib/usage-limits.js +40 -142
- package/dashboard/dist/assets/DialogDescription-CNfc42U-.js +0 -1
- package/dashboard/dist/assets/LimitsPage-D533J_we.js +0 -4
- package/dashboard/dist/assets/SettingsPage-DZo5FGJE.js +0 -1
- package/dashboard/dist/assets/limits-providers-Czh_yu22.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-oDsUC_PJ.js +0 -1
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Shared command runner for local-CLI limit providers.
|
|
4
|
+
//
|
|
5
|
+
// Extracted from usage-limits.js (which keeps a require of it) so
|
|
6
|
+
// ark-coding-plan-limits.js and future providers don't have to copy it.
|
|
7
|
+
// This is a superset of both former copies: the original's `completeWhen`
|
|
8
|
+
// early-settlement hook, plus the hardening that previously only lived in
|
|
9
|
+
// the ark copy — an abort `signal` wired into the spawn lifecycle, a
|
|
10
|
+
// `platform` override with `where.exe` discovery on native Windows, and a
|
|
11
|
+
// byte-capped maxBuffer for piped spawn output.
|
|
12
|
+
|
|
13
|
+
const cp = require("node:child_process");
|
|
14
|
+
const fs = require("node:fs");
|
|
15
|
+
const os = require("node:os");
|
|
16
|
+
const path = require("node:path");
|
|
17
|
+
|
|
18
|
+
function runCommand(commandRunner, command, args, options = {}) {
|
|
19
|
+
const merged = {
|
|
20
|
+
encoding: "utf8",
|
|
21
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
22
|
+
useShell: false,
|
|
23
|
+
...options,
|
|
24
|
+
};
|
|
25
|
+
if (typeof commandRunner === "function") {
|
|
26
|
+
return Promise.resolve(commandRunner(command, args, merged));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
timeout,
|
|
31
|
+
maxBuffer,
|
|
32
|
+
completeWhen,
|
|
33
|
+
completionGraceMs = 250,
|
|
34
|
+
killProcessGroup = false,
|
|
35
|
+
platform = process.platform,
|
|
36
|
+
signal,
|
|
37
|
+
useShell = false,
|
|
38
|
+
...spawnOptions
|
|
39
|
+
} = merged;
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
if (signal?.aborted) {
|
|
42
|
+
const error = new Error(`spawn ${command} aborted`);
|
|
43
|
+
error.name = "AbortError";
|
|
44
|
+
resolve({ status: null, stdout: "", stderr: "", error });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const useProcessGroup = killProcessGroup && platform !== "win32";
|
|
49
|
+
// `useShell` is opt-in per call site. It exists for npm's Windows .cmd
|
|
50
|
+
// shims, which Node's spawn cannot execute directly — but shell
|
|
51
|
+
// execution means cmd.exe re-parses the joined command line, so any
|
|
52
|
+
// argument carrying shell metacharacters (e.g. a powershell -Command
|
|
53
|
+
// script with `|`) would be split. Default false keeps direct spawns,
|
|
54
|
+
// which is what every pre-existing usage-limits call site expects.
|
|
55
|
+
// Under shell execution quote the command unconditionally: cmd.exe
|
|
56
|
+
// metacharacters are not limited to whitespace (`C:\Users\a&b\...`
|
|
57
|
+
// has none yet splits at `&`), and Windows account names allow them.
|
|
58
|
+
const shellCommand = useShell && !command.startsWith('"')
|
|
59
|
+
? `"${command}"`
|
|
60
|
+
: command;
|
|
61
|
+
let child;
|
|
62
|
+
try {
|
|
63
|
+
child = cp.spawn(shellCommand, args, {
|
|
64
|
+
...spawnOptions,
|
|
65
|
+
detached: useProcessGroup || spawnOptions.detached,
|
|
66
|
+
shell: useShell,
|
|
67
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
resolve({ status: null, stdout: "", stderr: "", error });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let stdout = "";
|
|
75
|
+
let stderr = "";
|
|
76
|
+
let outputBytes = 0;
|
|
77
|
+
let settled = false;
|
|
78
|
+
let timedOut = false;
|
|
79
|
+
let timer = null;
|
|
80
|
+
let hardTimer = null;
|
|
81
|
+
let completionTimer = null;
|
|
82
|
+
let abortListener = null;
|
|
83
|
+
|
|
84
|
+
const settle = ({ status = null, error = null } = {}) => {
|
|
85
|
+
if (settled) return;
|
|
86
|
+
settled = true;
|
|
87
|
+
if (timer) clearTimeout(timer);
|
|
88
|
+
if (hardTimer) clearTimeout(hardTimer);
|
|
89
|
+
if (completionTimer) clearTimeout(completionTimer);
|
|
90
|
+
if (abortListener) signal?.removeEventListener("abort", abortListener);
|
|
91
|
+
let finalError = error;
|
|
92
|
+
if (!finalError && timedOut) {
|
|
93
|
+
finalError = new Error(`spawn ${command} ETIMEDOUT`);
|
|
94
|
+
finalError.code = "ETIMEDOUT";
|
|
95
|
+
}
|
|
96
|
+
const result = { status, stdout, stderr };
|
|
97
|
+
if (finalError) result.error = finalError;
|
|
98
|
+
resolve(result);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const signalChild = (killSignal) => {
|
|
102
|
+
try {
|
|
103
|
+
if (useProcessGroup && Number.isInteger(child.pid)) {
|
|
104
|
+
process.kill(-child.pid, killSignal);
|
|
105
|
+
} else {
|
|
106
|
+
child.kill(killSignal);
|
|
107
|
+
}
|
|
108
|
+
} catch (_error) {}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const stopChild = ({ timeoutExpired = false } = {}) => {
|
|
112
|
+
if (settled) return;
|
|
113
|
+
if (timeoutExpired) timedOut = true;
|
|
114
|
+
signalChild("SIGTERM");
|
|
115
|
+
// A CLI may leave descendants or inherited stdio alive after SIGTERM.
|
|
116
|
+
// Escalate after a short grace period and settle even if close never fires.
|
|
117
|
+
hardTimer = setTimeout(() => {
|
|
118
|
+
signalChild("SIGKILL");
|
|
119
|
+
settle({ status: null });
|
|
120
|
+
}, 1000);
|
|
121
|
+
if (typeof hardTimer.unref === "function") hardTimer.unref();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const scheduleCompletion = () => {
|
|
125
|
+
if (typeof completeWhen !== "function" || settled) return;
|
|
126
|
+
let complete = false;
|
|
127
|
+
try {
|
|
128
|
+
complete = Boolean(completeWhen(stdout, stderr));
|
|
129
|
+
} catch (_error) {}
|
|
130
|
+
if (!complete) return;
|
|
131
|
+
if (completionTimer) clearTimeout(completionTimer);
|
|
132
|
+
completionTimer = setTimeout(
|
|
133
|
+
() => stopChild(),
|
|
134
|
+
Math.max(0, Number(completionGraceMs) || 0),
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const appendOutput = (key, chunk) => {
|
|
139
|
+
if (settled) return;
|
|
140
|
+
if (key === "stdout") stdout += chunk;
|
|
141
|
+
else stderr += chunk;
|
|
142
|
+
outputBytes += Buffer.byteLength(chunk, "utf8");
|
|
143
|
+
// Unlike exec/execFile, spawn does not apply a maxBuffer guard to piped
|
|
144
|
+
// streams. Enforce the combined byte cap here so a verbose CLI cannot
|
|
145
|
+
// grow this process without bound.
|
|
146
|
+
if (outputBytes > maxBuffer) {
|
|
147
|
+
const error = new Error(`spawn ${command} maxBuffer length exceeded`);
|
|
148
|
+
error.code = "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
|
|
149
|
+
signalChild("SIGKILL");
|
|
150
|
+
settle({ status: null, error });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
scheduleCompletion();
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
child.stdout?.setEncoding("utf8");
|
|
157
|
+
child.stderr?.setEncoding("utf8");
|
|
158
|
+
child.stdout?.on("data", (chunk) => appendOutput("stdout", chunk));
|
|
159
|
+
child.stderr?.on("data", (chunk) => appendOutput("stderr", chunk));
|
|
160
|
+
child.on("error", (error) => settle({ status: null, error }));
|
|
161
|
+
child.on("close", (code) => settle({ status: timedOut ? null : code }));
|
|
162
|
+
|
|
163
|
+
if (signal) {
|
|
164
|
+
abortListener = () => stopChild();
|
|
165
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
166
|
+
if (signal.aborted) abortListener();
|
|
167
|
+
}
|
|
168
|
+
if (Number.isFinite(timeout) && timeout > 0) {
|
|
169
|
+
timer = setTimeout(() => stopChild({ timeoutExpired: true }), timeout);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Locate a binary on PATH. Unix uses `which`; native Windows ships no `which`
|
|
175
|
+
// (it has `where.exe` instead), so blindly spawning `which` there returns
|
|
176
|
+
// ENOENT and every provider would report itself unconfigured even when the
|
|
177
|
+
// binary is installed and signed in.
|
|
178
|
+
async function whichBinary(binary, { commandRunner, platform = process.platform, signal } = {}) {
|
|
179
|
+
const probe = platform === "win32" ? "where" : "which";
|
|
180
|
+
const result = await runCommand(commandRunner, probe, [binary], {
|
|
181
|
+
timeout: 2000,
|
|
182
|
+
signal,
|
|
183
|
+
platform,
|
|
184
|
+
killProcessGroup: true,
|
|
185
|
+
});
|
|
186
|
+
if (result?.error || result?.status !== 0) return null;
|
|
187
|
+
const stdout = typeof result?.stdout === "string" ? result.stdout.trim() : "";
|
|
188
|
+
if (!stdout) return null;
|
|
189
|
+
// `where` on Windows emits CRLF and may list several matches across PATH
|
|
190
|
+
// entries; take the first line without the trailing `\r`, or the polluted
|
|
191
|
+
// path would fail at spawn time.
|
|
192
|
+
return stdout
|
|
193
|
+
.split(/\r?\n/)
|
|
194
|
+
.map((line) => line.trim())
|
|
195
|
+
.find(Boolean) || null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function isBinaryAvailable(binary, { commandRunner, platform, signal } = {}) {
|
|
199
|
+
return (await whichBinary(binary, { commandRunner, platform, signal })) !== null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Expand a versioned install root (e.g. ~/.nvm/versions/node/<ver>/ or
|
|
203
|
+
// fnm's node-versions/<ver>/installation/) into its per-version bin
|
|
204
|
+
// directories. Returns [] when the root does not exist.
|
|
205
|
+
function versionedBinDirs(root, inner) {
|
|
206
|
+
try {
|
|
207
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
208
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
|
209
|
+
.sort((a, b) => b.name.localeCompare(a.name, "en", { numeric: true }))
|
|
210
|
+
.map((entry) => path.join(root, entry.name, ...inner));
|
|
211
|
+
} catch (_error) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Directories where globally installed CLIs commonly live even when the
|
|
217
|
+
// current process has a minimal PATH (e.g. a Finder-launched macOS app does
|
|
218
|
+
// not inherit the login-shell PATH, so Homebrew and npm global binaries are
|
|
219
|
+
// unreachable through `which`). Probed with statSync — no spawn.
|
|
220
|
+
function commonGlobalBinDirectories({ home = os.homedir(), platform = process.platform } = {}) {
|
|
221
|
+
if (platform === "win32") {
|
|
222
|
+
return [
|
|
223
|
+
path.join(home, "AppData", "Roaming", "npm"),
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
return [
|
|
227
|
+
"/opt/homebrew/bin",
|
|
228
|
+
"/usr/local/bin",
|
|
229
|
+
path.join(home, ".npm-global", "bin"),
|
|
230
|
+
// volta keeps shims for every global install here
|
|
231
|
+
path.join(home, ".volta", "bin"),
|
|
232
|
+
// nvm / fnm install npm globals under a per-version prefix that a
|
|
233
|
+
// minimal PATH never sees; newest version first.
|
|
234
|
+
...versionedBinDirs(path.join(home, ".nvm", "versions", "node"), ["bin"]),
|
|
235
|
+
...versionedBinDirs(path.join(home, ".local", "share", "fnm", "node-versions"), ["installation", "bin"]),
|
|
236
|
+
...(platform === "darwin"
|
|
237
|
+
? versionedBinDirs(path.join(home, "Library", "Application Support", "fnm", "node-versions"), ["installation", "bin"])
|
|
238
|
+
: []),
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Probe `binary` inside the given directories with statSync — no spawn.
|
|
243
|
+
// Exported so providers can treat a hit as install evidence without
|
|
244
|
+
// paying for a `which` process.
|
|
245
|
+
function statBinaryInDirs(binary, searchDirs, platform = process.platform) {
|
|
246
|
+
for (const dir of searchDirs) {
|
|
247
|
+
const candidate = path.join(dir, binary);
|
|
248
|
+
for (const suffix of platform === "win32" ? ["", ".cmd", ".exe"] : [""]) {
|
|
249
|
+
try {
|
|
250
|
+
if (fs.statSync(candidate + suffix).isFile()) return candidate + suffix;
|
|
251
|
+
} catch (_error) {}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Resolve a binary to an absolute path: `which`/`where` first, then a
|
|
259
|
+
* spawn-free probe of the common global-install directories above. Returns
|
|
260
|
+
* null when the binary cannot be found. Callers should spawn the returned
|
|
261
|
+
* path (not the bare name): a bare name goes through PATH search again —
|
|
262
|
+
* and on Windows cmd.exe searches the current directory first, which would
|
|
263
|
+
* let an `arkcli.bat` dropped in the server cwd hijack the spawn.
|
|
264
|
+
*/
|
|
265
|
+
async function resolveBinaryPath(binary, { commandRunner, home, platform = process.platform, signal, globalBinDirs } = {}) {
|
|
266
|
+
let resolved = null;
|
|
267
|
+
try {
|
|
268
|
+
resolved = await whichBinary(binary, { commandRunner, platform, signal });
|
|
269
|
+
} catch (_error) {
|
|
270
|
+
resolved = null;
|
|
271
|
+
}
|
|
272
|
+
if (resolved) return resolved;
|
|
273
|
+
const searchDirs = Array.isArray(globalBinDirs)
|
|
274
|
+
? globalBinDirs
|
|
275
|
+
: commonGlobalBinDirectories({ home, platform });
|
|
276
|
+
return statBinaryInDirs(binary, searchDirs, platform);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
module.exports = {
|
|
280
|
+
runCommand,
|
|
281
|
+
whichBinary,
|
|
282
|
+
isBinaryAvailable,
|
|
283
|
+
commonGlobalBinDirectories,
|
|
284
|
+
statBinaryInDirs,
|
|
285
|
+
resolveBinaryPath,
|
|
286
|
+
};
|
package/src/lib/cursor-config.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const os = require("node:os");
|
|
2
2
|
const path = require("node:path");
|
|
3
3
|
const fs = require("node:fs");
|
|
4
|
-
const https = require("node:https");
|
|
5
4
|
|
|
6
5
|
const { readJson } = require("./fs");
|
|
7
6
|
const { readSqliteFirstValue } = require("./sqlite-reader");
|
|
@@ -139,58 +138,73 @@ const CURSOR_CSV_URL = "https://cursor.com/api/dashboard/export-usage-events-csv
|
|
|
139
138
|
const CURSOR_SUMMARY_URL = "https://cursor.com/api/usage-summary";
|
|
140
139
|
const CURSOR_SOURCE_SCOPE = "account";
|
|
141
140
|
|
|
141
|
+
function isCursorFetchTimeout(err) {
|
|
142
|
+
return Boolean(err && (err.name === "TimeoutError" || err.name === "AbortError"));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function remapCursorFetchTimeout(err) {
|
|
146
|
+
if (isCursorFetchTimeout(err)) {
|
|
147
|
+
throw new Error("Cursor API request timed out");
|
|
148
|
+
}
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const CURSOR_REDIRECT_HOSTS = new Set(["cursor.com", "www.cursor.com"]);
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Resolve a redirect `Location` before the session cookie is forwarded to it.
|
|
156
|
+
*
|
|
157
|
+
* The header is attacker-influenced in principle, and the next hop carries the
|
|
158
|
+
* Cursor session cookie, so an off-origin redirect would disclose it. Resolving
|
|
159
|
+
* against the request URL also handles a relative Location, which bare
|
|
160
|
+
* `fetch(location)` would reject.
|
|
161
|
+
*/
|
|
162
|
+
function resolveCursorRedirect(location, baseUrl) {
|
|
163
|
+
let url;
|
|
164
|
+
try {
|
|
165
|
+
url = new URL(location, baseUrl);
|
|
166
|
+
} catch {
|
|
167
|
+
throw new Error("Cursor API redirect to an untrusted origin");
|
|
168
|
+
}
|
|
169
|
+
if (url.protocol !== "https:" || !CURSOR_REDIRECT_HOSTS.has(url.hostname)) {
|
|
170
|
+
throw new Error("Cursor API redirect to an untrusted origin");
|
|
171
|
+
}
|
|
172
|
+
return url.toString();
|
|
173
|
+
}
|
|
174
|
+
|
|
142
175
|
/**
|
|
143
176
|
* Fetch full usage CSV from Cursor API.
|
|
144
177
|
* Returns raw CSV string or throws on error.
|
|
145
178
|
*/
|
|
146
|
-
function fetchCursorUsageCsv({ cookie, timeoutMs = 30000 }) {
|
|
147
|
-
return
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (res.statusCode !== 200) {
|
|
176
|
-
res.resume();
|
|
177
|
-
return reject(new Error(`Cursor API returned ${res.statusCode}`));
|
|
178
|
-
}
|
|
179
|
-
let data = "";
|
|
180
|
-
res.on("data", (chunk) => {
|
|
181
|
-
data += chunk;
|
|
182
|
-
});
|
|
183
|
-
res.on("end", () => resolve(data));
|
|
184
|
-
res.on("error", reject);
|
|
185
|
-
},
|
|
186
|
-
);
|
|
187
|
-
req.on("error", reject);
|
|
188
|
-
req.on("timeout", () => {
|
|
189
|
-
req.destroy();
|
|
190
|
-
reject(new Error("Cursor API request timed out"));
|
|
191
|
-
});
|
|
192
|
-
req.end();
|
|
193
|
-
});
|
|
179
|
+
function fetchCursorUsageCsv({ cookie, timeoutMs = 30000, fetchImpl = fetch }) {
|
|
180
|
+
return fetchImpl(CURSOR_CSV_URL, {
|
|
181
|
+
method: "GET",
|
|
182
|
+
headers: {
|
|
183
|
+
Accept: "*/*",
|
|
184
|
+
Cookie: cookie,
|
|
185
|
+
Referer: "https://www.cursor.com/settings",
|
|
186
|
+
"User-Agent":
|
|
187
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
188
|
+
},
|
|
189
|
+
redirect: "manual",
|
|
190
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
191
|
+
}).then(async (res) => {
|
|
192
|
+
if (res.status === 401 || res.status === 403) {
|
|
193
|
+
throw new Error("Cursor session expired — re-login in Cursor to refresh");
|
|
194
|
+
}
|
|
195
|
+
if (res.status === 308 || res.status === 301 || res.status === 302) {
|
|
196
|
+
// Follow redirect once. Keep this hop on redirect:"manual" so the
|
|
197
|
+
// Location + Cookie are forwarded explicitly (cursor.com → www.cursor.com).
|
|
198
|
+
const location = res.headers.get("location");
|
|
199
|
+
if (!location) throw new Error("Cursor API redirect without Location header");
|
|
200
|
+
const target = resolveCursorRedirect(location, CURSOR_CSV_URL);
|
|
201
|
+
return fetchUrlRaw({ urlStr: target, cookie, timeoutMs, fetchImpl });
|
|
202
|
+
}
|
|
203
|
+
if (res.status !== 200) {
|
|
204
|
+
throw new Error(`Cursor API returned ${res.status}`);
|
|
205
|
+
}
|
|
206
|
+
return res.text();
|
|
207
|
+
}).catch(remapCursorFetchTimeout);
|
|
194
208
|
}
|
|
195
209
|
|
|
196
210
|
/**
|
|
@@ -220,43 +234,27 @@ function fetchCursorUsageSummary({ cookie, timeoutMs = 30000, fetchImpl = fetch
|
|
|
220
234
|
});
|
|
221
235
|
}
|
|
222
236
|
|
|
223
|
-
function fetchUrlRaw({ urlStr, cookie, timeoutMs }) {
|
|
224
|
-
return
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
(res
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
let data = "";
|
|
246
|
-
res.on("data", (chunk) => {
|
|
247
|
-
data += chunk;
|
|
248
|
-
});
|
|
249
|
-
res.on("end", () => resolve(data));
|
|
250
|
-
res.on("error", reject);
|
|
251
|
-
},
|
|
252
|
-
);
|
|
253
|
-
req.on("error", reject);
|
|
254
|
-
req.on("timeout", () => {
|
|
255
|
-
req.destroy();
|
|
256
|
-
reject(new Error("Cursor API request timed out"));
|
|
257
|
-
});
|
|
258
|
-
req.end();
|
|
259
|
-
});
|
|
237
|
+
function fetchUrlRaw({ urlStr, cookie, timeoutMs = 30000, fetchImpl = fetch }) {
|
|
238
|
+
return fetchImpl(urlStr, {
|
|
239
|
+
method: "GET",
|
|
240
|
+
headers: {
|
|
241
|
+
Accept: "*/*",
|
|
242
|
+
Cookie: cookie,
|
|
243
|
+
Referer: "https://www.cursor.com/settings",
|
|
244
|
+
"User-Agent":
|
|
245
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
246
|
+
},
|
|
247
|
+
// Keep manual: the previous hop did not follow a second redirect.
|
|
248
|
+
// redirect:"follow" would turn a second 301/302/308 into a silent
|
|
249
|
+
// success and may drop the explicit Cookie on a cross-host hop.
|
|
250
|
+
redirect: "manual",
|
|
251
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
252
|
+
}).then(async (res) => {
|
|
253
|
+
if (res.status !== 200) {
|
|
254
|
+
throw new Error(`Cursor API returned ${res.status} from ${urlStr}`);
|
|
255
|
+
}
|
|
256
|
+
return res.text();
|
|
257
|
+
}).catch(remapCursorFetchTimeout);
|
|
260
258
|
}
|
|
261
259
|
|
|
262
260
|
// ── CSV parsing ──
|
package/src/lib/fs.js
CHANGED
|
@@ -10,8 +10,17 @@ async function writeFileAtomic(filePath, content) {
|
|
|
10
10
|
const dir = path.dirname(filePath);
|
|
11
11
|
await ensureDir(dir);
|
|
12
12
|
const tmp = `${filePath}.tmp.${Date.now()}`;
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
try {
|
|
14
|
+
await fs.writeFile(tmp, content, { encoding: "utf8" });
|
|
15
|
+
await fs.rename(tmp, filePath);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
try {
|
|
18
|
+
await fs.unlink(tmp);
|
|
19
|
+
} catch {
|
|
20
|
+
/* tmp may not exist if writeFile failed before creating it */
|
|
21
|
+
}
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
async function readJson(filePath) {
|
package/src/lib/local-api.js
CHANGED
|
@@ -2621,6 +2621,143 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
2621
2621
|
return true;
|
|
2622
2622
|
}
|
|
2623
2623
|
|
|
2624
|
+
// --- outbound proxy preference (manual / system / off) ---
|
|
2625
|
+
// Persisted on config.json next to the queue. GET is unauthenticated so the
|
|
2626
|
+
// settings page can probe availability; POST requires local-auth.
|
|
2627
|
+
if (p === "/functions/tokentracker-proxy-config") {
|
|
2628
|
+
const {
|
|
2629
|
+
normalizeProxyConfig,
|
|
2630
|
+
parseProxyPayload,
|
|
2631
|
+
} = require("./proxy-settings");
|
|
2632
|
+
const {
|
|
2633
|
+
applyUndiciProxyIfNeeded,
|
|
2634
|
+
getLastProxyApplyError,
|
|
2635
|
+
resolveEffectiveProxySource,
|
|
2636
|
+
invalidateSystemProxyCache,
|
|
2637
|
+
} = require("./proxy-env");
|
|
2638
|
+
const { writeFileAtomic, chmod600IfPossible } = require("./fs");
|
|
2639
|
+
const configPath = path.join(path.dirname(qp), "config.json");
|
|
2640
|
+
const readConfig = () => {
|
|
2641
|
+
try {
|
|
2642
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
2643
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
2644
|
+
} catch {
|
|
2645
|
+
return {};
|
|
2646
|
+
}
|
|
2647
|
+
};
|
|
2648
|
+
const writeConfig = async (next) => {
|
|
2649
|
+
await writeFileAtomic(configPath, JSON.stringify(next, null, 2));
|
|
2650
|
+
await chmod600IfPossible(configPath);
|
|
2651
|
+
};
|
|
2652
|
+
const toResponse = (configObj) => {
|
|
2653
|
+
const normalized = normalizeProxyConfig(configObj?.proxy);
|
|
2654
|
+
const effective = resolveEffectiveProxySource({
|
|
2655
|
+
env: process.env,
|
|
2656
|
+
proxyConfig: configObj?.proxy,
|
|
2657
|
+
});
|
|
2658
|
+
return {
|
|
2659
|
+
mode: normalized.mode,
|
|
2660
|
+
protocol: normalized.protocol,
|
|
2661
|
+
host: normalized.host,
|
|
2662
|
+
port: normalized.port,
|
|
2663
|
+
effective: effective.source,
|
|
2664
|
+
applyError: getLastProxyApplyError(),
|
|
2665
|
+
};
|
|
2666
|
+
};
|
|
2667
|
+
const method = String(req.method || "GET").toUpperCase();
|
|
2668
|
+
if (method === "GET") {
|
|
2669
|
+
json(res, toResponse(readConfig()));
|
|
2670
|
+
return true;
|
|
2671
|
+
}
|
|
2672
|
+
if (method === "POST" || method === "PUT") {
|
|
2673
|
+
if (!isAuthorizedLocalMutation(req)) {
|
|
2674
|
+
json(res, { ok: false, error: "Unauthorized" }, 401);
|
|
2675
|
+
return true;
|
|
2676
|
+
}
|
|
2677
|
+
let body = {};
|
|
2678
|
+
try {
|
|
2679
|
+
body = await readJsonBody(req);
|
|
2680
|
+
} catch {
|
|
2681
|
+
json(res, { ok: false, error: "invalid JSON" }, 400);
|
|
2682
|
+
return true;
|
|
2683
|
+
}
|
|
2684
|
+
const parsed = parseProxyPayload(body);
|
|
2685
|
+
if (!parsed.ok) {
|
|
2686
|
+
json(res, { ok: false, error: parsed.error }, 400);
|
|
2687
|
+
return true;
|
|
2688
|
+
}
|
|
2689
|
+
const current = readConfig();
|
|
2690
|
+
const prevProxy = current.proxy && typeof current.proxy === "object" && !Array.isArray(current.proxy)
|
|
2691
|
+
? current.proxy
|
|
2692
|
+
: {};
|
|
2693
|
+
current.proxy = {
|
|
2694
|
+
...prevProxy,
|
|
2695
|
+
mode: parsed.value.mode,
|
|
2696
|
+
protocol: parsed.value.protocol,
|
|
2697
|
+
host: parsed.value.host,
|
|
2698
|
+
port: parsed.value.port,
|
|
2699
|
+
};
|
|
2700
|
+
try {
|
|
2701
|
+
await writeConfig(current);
|
|
2702
|
+
} catch (error) {
|
|
2703
|
+
json(res, { ok: false, error: error?.message || "failed to save proxy config" }, 500);
|
|
2704
|
+
return true;
|
|
2705
|
+
}
|
|
2706
|
+
invalidateSystemProxyCache();
|
|
2707
|
+
const applyResult = applyUndiciProxyIfNeeded({ proxyConfig: current.proxy });
|
|
2708
|
+
json(res, {
|
|
2709
|
+
ok: applyResult?.ok !== false,
|
|
2710
|
+
unprotected: applyResult?.unprotected === true,
|
|
2711
|
+
...toResponse(current),
|
|
2712
|
+
});
|
|
2713
|
+
return true;
|
|
2714
|
+
}
|
|
2715
|
+
json(res, { error: "Method Not Allowed" }, 405);
|
|
2716
|
+
return true;
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
if (p === "/functions/tokentracker-proxy-test") {
|
|
2720
|
+
const { parseProxyPayload, buildProxyUrl } = require("./proxy-settings");
|
|
2721
|
+
const { runProxyConnectivityTest } = require("./proxy-env");
|
|
2722
|
+
if (String(req.method || "GET").toUpperCase() !== "POST") {
|
|
2723
|
+
json(res, { error: "Method Not Allowed" }, 405);
|
|
2724
|
+
return true;
|
|
2725
|
+
}
|
|
2726
|
+
if (!isAuthorizedLocalMutation(req)) {
|
|
2727
|
+
json(res, { ok: false, error: "Unauthorized" }, 401);
|
|
2728
|
+
return true;
|
|
2729
|
+
}
|
|
2730
|
+
let body = {};
|
|
2731
|
+
try {
|
|
2732
|
+
body = await readJsonBody(req);
|
|
2733
|
+
} catch {
|
|
2734
|
+
json(res, { ok: false, error: "invalid JSON" }, 400);
|
|
2735
|
+
return true;
|
|
2736
|
+
}
|
|
2737
|
+
const parsed = parseProxyPayload({ ...body, mode: "manual" });
|
|
2738
|
+
if (!parsed.ok) {
|
|
2739
|
+
json(res, { ok: false, error: parsed.error }, 400);
|
|
2740
|
+
return true;
|
|
2741
|
+
}
|
|
2742
|
+
const proxyUrl = buildProxyUrl({ ...parsed.value, mode: "manual" });
|
|
2743
|
+
const configPath = path.join(path.dirname(qp), "config.json");
|
|
2744
|
+
let config = {};
|
|
2745
|
+
try {
|
|
2746
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf8")) || {};
|
|
2747
|
+
} catch {
|
|
2748
|
+
config = {};
|
|
2749
|
+
}
|
|
2750
|
+
const runtime = resolveRuntimeConfig({ config, env: process.env });
|
|
2751
|
+
const targetUrl = runtime.dashboardUrl || runtime.baseUrl;
|
|
2752
|
+
const result = await runProxyConnectivityTest({
|
|
2753
|
+
proxyUrl,
|
|
2754
|
+
targetUrl,
|
|
2755
|
+
timeoutMs: 5000,
|
|
2756
|
+
});
|
|
2757
|
+
json(res, result);
|
|
2758
|
+
return true;
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2624
2761
|
// --- telemetry preference (anonymous analytics opt-out mirror) ---
|
|
2625
2762
|
// The dashboard's analytics init on localhost / native-app surfaces asks
|
|
2626
2763
|
// this before sending anything, so TOKENTRACKER_NO_TELEMETRY /
|