tokentracker-cli 0.96.2 → 0.96.4
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 +34 -25
- package/README.ja.md +38 -27
- package/README.ko.md +39 -28
- package/README.md +36 -28
- package/README.zh-CN.md +36 -25
- package/dashboard/dist/assets/{AchievementBadge-CgEitoX1.js → AchievementBadge-CCcvmz6R.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-dnnea0JH.js → AchievementsPage-C5jdktOK.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-CJqG0UP7.js → AchievementsSection-BFMtUzn1.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-BLV6SJoI.js → BadgeDetailModal-Be8gM7tM.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-Cm78yEbz.js → BotAnimated-CXTGR71z.js} +1 -1
- package/dashboard/dist/assets/{Card-IPGSYT6O.js → Card-CsUsp3CV.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-04w1U1sm.js → ClawdAnimated-BEjpZjEE.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-gSSa1V_s.js → CommandPalette-CHc_Z1r8.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-CICwUz14.js → CommunityStatsModal-Gs6kZ_Mp.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-DaWC0cY7.js → ConfirmModal-niHkpv2O.js} +1 -1
- package/dashboard/dist/assets/{Controls-BDihA03F.js → Controls-X2Z1UNUx.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-B66s3Exi.js +1 -0
- package/dashboard/dist/assets/{DevicePage-BPpo_eiY.js → DevicePage-DQdLdEI8.js} +1 -1
- package/dashboard/dist/assets/DialogClose-CCUjTxl8.js +1 -0
- package/dashboard/dist/assets/DialogDescription-CjMjYG3N.js +1 -0
- package/dashboard/dist/assets/DialogTitle-CgkUGAag.js +1 -0
- package/dashboard/dist/assets/{FadeIn-DmI9PRLa.js → FadeIn-D4QD1bPo.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-CBO4cY7i.js → HeaderGithubStar-D0my-Z3a.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-CPFGhi9K.js → HoverTooltip-nSWgrKlK.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Do7w_6z6.js → IpCheckPage-4hfqHAgi.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-at_Ti1ZE.js → LandingPage-wIc4tVRI.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-BH1cnKPp.js → LeaderboardAvatar-DGlN8Vy8.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-Cd30aXwL.js → LeaderboardPage-CybuzE87.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-CVsti_HN.js → LeaderboardProfileModal-D9la3tNd.js} +4 -4
- package/dashboard/dist/assets/{LeaderboardProfilePage-BfSpDM9e.js → LeaderboardProfilePage-gxXSeJYb.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-CZH3G54Y.js +8 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-9-vRgsdn.js → LocalOnlyNotice-Ckc7zohC.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-DM_ZdFNl.js → LoginCard-BqeRHqWK.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-BaNMznwh.js → LoginModal-BjH6aIBG.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-BMMthUf4.js → LoginPage-BOyPLNU3.js} +1 -1
- package/dashboard/dist/assets/PetPage-BXRXG40D.js +1 -0
- package/dashboard/dist/assets/PopoverPopup-Dad1ZYVd.js +1 -0
- package/dashboard/dist/assets/ProviderIcon-BDmrNJSl.js +1 -0
- package/dashboard/dist/assets/{ResetPasswordPage-Ca4SULSV.js → ResetPasswordPage-CZpE6qfa.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-Bbsn-qvZ.js → SegmentedControl-CE1hrQFi.js} +1 -1
- package/dashboard/dist/assets/Select-YVj7u6Nv.js +1 -0
- package/dashboard/dist/assets/SelectItemText-BMYGgQ3o.js +1 -0
- package/dashboard/dist/assets/{ServiceStatusPage-BPS8KJ3a.js → ServiceStatusPage-HehjDbZg.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-C1yt3TMZ.js → SessionsPage-Chvu4YqZ.js} +2 -2
- package/dashboard/dist/assets/SettingsPage-BXjS7u9_.js +2 -0
- package/dashboard/dist/assets/SkillsPage-q3Rjlj9h.js +1 -0
- package/dashboard/dist/assets/{TokenGalaxy-DKvbJbnv.js → TokenGalaxy-BqZyj82r.js} +1 -1
- package/dashboard/dist/assets/ToolbarRootContext-Ch8ueI_C.js +1 -0
- package/dashboard/dist/assets/{TrendMonitor-YF5C_-L1.js → TrendMonitor-C2aTL9p1.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-BKV3DgXg.js → WidgetsPage-DYaIai8j.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-s4IRoLf7.js → WrappedPage-51B0v-Sr.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-CvMMIjhi.css → __vite-browser-external-5P7me0Qq.css} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-Cp7dNuCm.js +45 -0
- package/dashboard/dist/assets/{agent-logos-Hbt-wyl2.js → agent-logos-DXrPSAdX.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-KeIpKlw3.js → arrow-up-right-BFkf7l0U.js} +1 -1
- package/dashboard/dist/assets/{check-_QKyY1v_.js → check--N8wmgIJ.js} +1 -1
- package/dashboard/dist/assets/composite-zF5Pj94e.js +1 -0
- package/dashboard/dist/assets/{copy-DpLM3QMD.js → copy-BG0JMr6A.js} +1 -1
- package/dashboard/dist/assets/{download-C4NX-JnW.js → download-CPFJf-Sf.js} +1 -1
- package/dashboard/dist/assets/{external-link-CG-7FcOF.js → external-link-D2yF0kgP.js} +1 -1
- package/dashboard/dist/assets/{flame-CkWid7Ji.js → flame-l460AFx9.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CbwWhioZ.js → format-tokens-DKO4ljuH.js} +1 -1
- package/dashboard/dist/assets/{icons-BZq17rU3.js → icons-RoxRGkz4.js} +1 -1
- package/dashboard/dist/assets/{index-CR9tDiiC.js → index--ouT5L4N.js} +1 -1
- package/dashboard/dist/assets/{index-DDGLE0dz.js → index-DhyLCvvv.js} +1 -1
- package/dashboard/dist/assets/{index-CSwEiUn4.js → index-PTJ4368-.js} +1 -1
- package/dashboard/dist/assets/{info-mkeRjzqc.js → info-DNWMO4Ca.js} +1 -1
- package/dashboard/dist/assets/{link-2-BjMw8csC.js → link-2-DzWXAz1h.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-cGWOJnWn.js → loader-circle-BzGMqXNu.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-Cg_S73tD.js → maximize-2-Dkpyk4x3.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-DNxTWxId.js → modifiers.esm-lggHUQpW.js} +1 -1
- package/dashboard/dist/assets/module-DUSXqJtA.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-BvE9Tt2U.js → monitor-smartphone-Ck0-gz9l.js} +1 -1
- package/dashboard/dist/assets/{plus-BC2wdPRr.js → plus-qTJpnn43.js} +1 -1
- package/dashboard/dist/assets/provider-display-psY26qmp.js +42 -0
- package/dashboard/dist/assets/{react-D0kcslTz.js → react-CYS24KmX.js} +1 -1
- package/dashboard/dist/assets/{search-BuBNF3E3.js → search-CUzE6zxr.js} +1 -1
- package/dashboard/dist/assets/{skills-api-DxAN5pNx.js → skills-api-D9SzxPFG.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-DoqMcEX-.js → sortable.esm-Bl2hAUyR.js} +1 -1
- package/dashboard/dist/assets/store-mHSJAJtE.js +1 -0
- package/dashboard/dist/assets/{terminal-COWFUJdU.js → terminal-BlQNEvn1.js} +1 -1
- package/dashboard/dist/assets/{trash-2-Cqjki3RI.js → trash-CHdgmOyk.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DsrlOPDM.js → use-community-stats-DUiCs91N.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-ryRFL2gx.js → use-latest-request-guard-ehXfkvuU.js} +1 -1
- package/dashboard/dist/assets/use-limits-display-prefs-CcwwQ0Fq.js +1 -0
- package/dashboard/dist/assets/{use-ordered-list-D-Q86R1b.js → use-ordered-list-CCOtI3Wa.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-CDlahPqe.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-CrNz45y7.js → use-session-efficiency-pref-CYRDpcc_.js} +1 -1
- package/dashboard/dist/assets/{use-transform-CoGM21px.js → use-transform-btLlTJpj.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-BGr5sR-c.js +1 -0
- package/dashboard/dist/assets/{useCurrency-CgCfDktb.js → useCurrency-CNutuEKd.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-QKtzCKTL.js → useTokenFormat-C0bAJFMC.js} +1 -1
- package/dashboard/dist/assets/{zap-CTIobzgU.js → zap-B2fbyjbc.js} +1 -1
- package/dashboard/dist/brand-logos/acode-dark.png +0 -0
- package/dashboard/dist/brand-logos/acode.png +0 -0
- package/dashboard/dist/brand-logos/devin.svg +6 -0
- package/dashboard/dist/index.html +20 -16
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/llms.txt +4 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +2 -2
- package/src/commands/init.js +114 -4
- package/src/commands/status.js +37 -0
- package/src/commands/sync.js +92 -10
- package/src/commands/uninstall.js +23 -0
- package/src/lib/codex-config.js +42 -0
- package/src/lib/devin-limits.js +249 -0
- package/src/lib/devin-usage.js +212 -0
- package/src/lib/local-api.js +19 -0
- package/src/lib/passive-mode.js +13 -1
- package/src/lib/pricing/curated-overrides.json +67 -1
- package/src/lib/pricing/index.js +5 -2
- package/src/lib/pricing/matcher.js +28 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +1310 -144
- package/src/lib/skills-manager.js +231 -30
- package/src/lib/subscriptions.js +70 -16
- package/src/lib/usage-limits.js +199 -49
- package/dashboard/dist/assets/DashboardPage-hV7wncKF.js +0 -1
- package/dashboard/dist/assets/DialogClose-C_dF-r3s.js +0 -1
- package/dashboard/dist/assets/DialogDescription-GClOSCaE.js +0 -1
- package/dashboard/dist/assets/DialogTitle-Buh_5688.js +0 -1
- package/dashboard/dist/assets/LimitsPage-_-BadDAj.js +0 -8
- package/dashboard/dist/assets/PetPage-Iq8Hswai.js +0 -1
- package/dashboard/dist/assets/PopoverPopup-DXr3JxVV.js +0 -1
- package/dashboard/dist/assets/ProviderIcon-sb2keSY2.js +0 -1
- package/dashboard/dist/assets/Select-vD-yj-qa.js +0 -1
- package/dashboard/dist/assets/SelectItemText-CRtFEuvg.js +0 -1
- package/dashboard/dist/assets/SettingsPage-CWVHrMer.js +0 -2
- package/dashboard/dist/assets/SkillsPage-DGWEH1xm.js +0 -1
- package/dashboard/dist/assets/ToolbarRootContext-D8yov1dW.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-_LUatzwU.js +0 -45
- package/dashboard/dist/assets/composite-BidfcMeb.js +0 -1
- package/dashboard/dist/assets/limits-providers-DRkcWQup.js +0 -1
- package/dashboard/dist/assets/module-xpSFyNBK.js +0 -3
- package/dashboard/dist/assets/provider-display-B7GggHQy.js +0 -42
- package/dashboard/dist/assets/store-Cg1HeUFM.js +0 -1
- package/dashboard/dist/assets/use-limits-display-prefs-DkSuu12m.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-D7NNqavo.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-C_6L1sO5.js +0 -1
package/src/lib/codex-config.js
CHANGED
|
@@ -19,6 +19,10 @@ function buildEveryCodeNotifyCmd(notifyPath, options) {
|
|
|
19
19
|
return [...buildCodexNotifyCmd(notifyPath, options), "--source=every-code"];
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function buildAcodeNotifyCmd(notifyPath, options) {
|
|
23
|
+
return [...buildCodexNotifyCmd(notifyPath, options), "--source=acode"];
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
// Is this notify command one we wrote? Strict array equality is not a safe
|
|
23
27
|
// test, because the command is not a stable constant across invocations:
|
|
24
28
|
// - argv[0] on Windows is an absolute `process.execPath` (#361). The desktop
|
|
@@ -172,6 +176,39 @@ async function readCodexNotify(codexConfigPath) {
|
|
|
172
176
|
return readNotify(codexConfigPath);
|
|
173
177
|
}
|
|
174
178
|
|
|
179
|
+
async function upsertAcodeNotify({
|
|
180
|
+
acodeConfigPath,
|
|
181
|
+
notifyCmd,
|
|
182
|
+
notifyOriginalPath,
|
|
183
|
+
captureOriginal = true,
|
|
184
|
+
replaceOriginal = false,
|
|
185
|
+
}) {
|
|
186
|
+
return upsertNotify({
|
|
187
|
+
configPath: acodeConfigPath,
|
|
188
|
+
notifyCmd,
|
|
189
|
+
notifyOriginalPath,
|
|
190
|
+
configLabel: "AStudio config",
|
|
191
|
+
captureOriginal,
|
|
192
|
+
replaceOriginal,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function restoreAcodeNotify({ acodeConfigPath, notifyOriginalPath, notifyCmd }) {
|
|
197
|
+
return restoreNotify({
|
|
198
|
+
configPath: acodeConfigPath,
|
|
199
|
+
notifyOriginalPath,
|
|
200
|
+
expectedNotify: notifyCmd,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function loadAcodeNotifyOriginal(notifyOriginalPath) {
|
|
205
|
+
return loadNotifyOriginal(notifyOriginalPath);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function readAcodeNotify(acodeConfigPath) {
|
|
209
|
+
return readNotify(acodeConfigPath);
|
|
210
|
+
}
|
|
211
|
+
|
|
175
212
|
async function upsertEveryCodeNotify({
|
|
176
213
|
codeConfigPath,
|
|
177
214
|
notifyCmd,
|
|
@@ -536,6 +573,7 @@ function arraysEqual(a, b) {
|
|
|
536
573
|
|
|
537
574
|
module.exports = {
|
|
538
575
|
buildCodexNotifyCmd,
|
|
576
|
+
buildAcodeNotifyCmd,
|
|
539
577
|
buildEveryCodeNotifyCmd,
|
|
540
578
|
isManagedNotifyCmd,
|
|
541
579
|
upsertNotify,
|
|
@@ -546,6 +584,10 @@ module.exports = {
|
|
|
546
584
|
restoreCodexNotify,
|
|
547
585
|
loadCodexNotifyOriginal,
|
|
548
586
|
readCodexNotify,
|
|
587
|
+
upsertAcodeNotify,
|
|
588
|
+
restoreAcodeNotify,
|
|
589
|
+
loadAcodeNotifyOriginal,
|
|
590
|
+
readAcodeNotify,
|
|
549
591
|
upsertEveryCodeNotify,
|
|
550
592
|
restoreEveryCodeNotify,
|
|
551
593
|
loadEveryCodeNotifyOriginal,
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Devin subscription usage limits.
|
|
2
|
+
//
|
|
3
|
+
// Source: the official seat-management RPC the Devin web app calls,
|
|
4
|
+
// POST https://server.codeium.com/exa.seat_management_pb.SeatManagementService/GetPlanStatus
|
|
5
|
+
// authenticated by `x-auth-token` — the session token the Devin CLI stores in
|
|
6
|
+
// $XDG_DATA_HOME/devin/credentials.toml (~/.local/share/devin/ fallback).
|
|
7
|
+
//
|
|
8
|
+
// Two non-obvious contract points, both verified against the official
|
|
9
|
+
// generated descriptor (docs/devin-limits.md):
|
|
10
|
+
// - the quota percent/reset fields are proto3 implicit scalars, so the
|
|
11
|
+
// server omits them at 0: absent percent + live reset = exhausted
|
|
12
|
+
// (100% used), both absent = no such window;
|
|
13
|
+
// - a non-default api_server_url is an unsupported configuration, never a
|
|
14
|
+
// different request destination — the token only ever goes to the fixed
|
|
15
|
+
// host above.
|
|
16
|
+
|
|
17
|
+
const fs = require("node:fs");
|
|
18
|
+
const path = require("node:path");
|
|
19
|
+
|
|
20
|
+
const DEVIN_PLAN_STATUS_URL =
|
|
21
|
+
"https://server.codeium.com/exa.seat_management_pb.SeatManagementService/GetPlanStatus";
|
|
22
|
+
const DEVIN_API_ORIGIN = "https://server.codeium.com";
|
|
23
|
+
|
|
24
|
+
const DEVIN_DAILY_WINDOW_SECONDS = 24 * 60 * 60;
|
|
25
|
+
const DEVIN_WEEKLY_WINDOW_SECONDS = 7 * 24 * 60 * 60;
|
|
26
|
+
|
|
27
|
+
function isNonEmptyString(value) {
|
|
28
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Minimal root-level TOML reader for the CLI-written credentials file: bare
|
|
32
|
+
// `key = value` lines before the first `[section]`, nothing else.
|
|
33
|
+
function parseDevinCredentialsToml(raw) {
|
|
34
|
+
const fields = {};
|
|
35
|
+
for (const line of String(raw).split(/\r?\n/)) {
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
38
|
+
if (trimmed.startsWith("[")) break;
|
|
39
|
+
const match = /^([A-Za-z0-9_]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s#]+))\s*(?:#.*)?$/.exec(
|
|
40
|
+
trimmed,
|
|
41
|
+
);
|
|
42
|
+
if (!match) continue;
|
|
43
|
+
const [, key, doubleQuoted, singleQuoted, bare] = match;
|
|
44
|
+
fields[key] = doubleQuoted ?? singleQuoted ?? bare ?? "";
|
|
45
|
+
}
|
|
46
|
+
return fields;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Returns { apiKey } or null when no sign-in exists (no home, or the file is
|
|
50
|
+
// absent — ENOENT only). Any other read failure or a mangled file throws an
|
|
51
|
+
// owned, actionable error with no path or filesystem details, so a broken
|
|
52
|
+
// sign-in can never masquerade as "not configured".
|
|
53
|
+
function readDevinCredentials({ home, env = process.env } = {}) {
|
|
54
|
+
const xdg = env && typeof env === "object" ? env.XDG_DATA_HOME : null;
|
|
55
|
+
const dir = isNonEmptyString(xdg)
|
|
56
|
+
? xdg.trim()
|
|
57
|
+
: isNonEmptyString(home)
|
|
58
|
+
? home.trim()
|
|
59
|
+
: isNonEmptyString(env?.HOME)
|
|
60
|
+
? env.HOME.trim()
|
|
61
|
+
: null;
|
|
62
|
+
if (!dir) return null;
|
|
63
|
+
const base = isNonEmptyString(xdg)
|
|
64
|
+
? path.join(dir, "devin", "credentials.toml")
|
|
65
|
+
: path.join(dir, ".local", "share", "devin", "credentials.toml");
|
|
66
|
+
let raw;
|
|
67
|
+
try {
|
|
68
|
+
raw = fs.readFileSync(base, "utf8");
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error && error.code === "ENOENT") return null;
|
|
71
|
+
throw new Error(
|
|
72
|
+
"Could not read the Devin CLI credentials file — check its permissions or run `devin auth login` to sign in again.",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const fields = parseDevinCredentialsToml(raw);
|
|
76
|
+
const apiKey = isNonEmptyString(fields.windsurf_api_key)
|
|
77
|
+
? fields.windsurf_api_key.trim()
|
|
78
|
+
: null;
|
|
79
|
+
if (!apiKey) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
"Devin credentials file has no sign-in token — run `devin auth login`.",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const apiServerUrl = isNonEmptyString(fields.api_server_url)
|
|
85
|
+
? fields.api_server_url.trim().replace(/\/+$/, "")
|
|
86
|
+
: null;
|
|
87
|
+
if (apiServerUrl && apiServerUrl !== DEVIN_API_ORIGIN) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
"Devin credentials point at a custom api_server_url, which this quota provider does not support.",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return { apiKey };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function malformed(field) {
|
|
96
|
+
return new Error(`Devin quota response has a malformed ${field}.`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// undefined → proto-default semantics handled by the caller; null/non-numeric
|
|
100
|
+
// → malformed. Percentages are clamped defensively to 0..100.
|
|
101
|
+
function normalizeRemainingPercent(value) {
|
|
102
|
+
if (value === undefined) return undefined;
|
|
103
|
+
if (typeof value !== "number" && typeof value !== "string") {
|
|
104
|
+
throw malformed("quota remaining percent");
|
|
105
|
+
}
|
|
106
|
+
if (typeof value === "string" && value.trim() === "") {
|
|
107
|
+
throw malformed("quota remaining percent");
|
|
108
|
+
}
|
|
109
|
+
const n = Number(value);
|
|
110
|
+
if (!Number.isFinite(n)) throw malformed("quota remaining percent");
|
|
111
|
+
if (n <= 0) return 0;
|
|
112
|
+
if (n >= 100) return 100;
|
|
113
|
+
return n;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// int64 unix seconds (JSON string or number). undefined or proto-default 0 →
|
|
117
|
+
// absent; null/non-numeric/non-positive → malformed. Returns epoch ms.
|
|
118
|
+
function normalizeResetMs(value) {
|
|
119
|
+
if (value === undefined || value === 0 || value === "0") return null;
|
|
120
|
+
if (typeof value !== "number" && typeof value !== "string") {
|
|
121
|
+
throw malformed("quota reset timestamp");
|
|
122
|
+
}
|
|
123
|
+
if (typeof value === "string" && value.trim() === "") {
|
|
124
|
+
throw malformed("quota reset timestamp");
|
|
125
|
+
}
|
|
126
|
+
const n = Number(value);
|
|
127
|
+
if (!Number.isFinite(n) || n <= 0) throw malformed("quota reset timestamp");
|
|
128
|
+
const ms = n * 1000;
|
|
129
|
+
if (!Number.isFinite(new Date(ms).getTime())) {
|
|
130
|
+
throw malformed("quota reset timestamp");
|
|
131
|
+
}
|
|
132
|
+
return ms;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeWindow({ remainingPercent, resetUnix, hidden, windowSeconds }) {
|
|
136
|
+
if (hidden === true) return null;
|
|
137
|
+
const resetMs = normalizeResetMs(resetUnix);
|
|
138
|
+
const remaining = normalizeRemainingPercent(remainingPercent);
|
|
139
|
+
if (remaining === undefined && resetMs === null) return null;
|
|
140
|
+
return {
|
|
141
|
+
used_percent: remaining === undefined ? 100 : 100 - remaining,
|
|
142
|
+
reset_at: resetMs === null ? null : new Date(resetMs).toISOString(),
|
|
143
|
+
limit_window_seconds: windowSeconds,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizePlanStatus(body) {
|
|
148
|
+
const planStatus =
|
|
149
|
+
body && typeof body === "object" ? body.planStatus : undefined;
|
|
150
|
+
if (!planStatus || typeof planStatus !== "object") {
|
|
151
|
+
throw new Error("Devin quota response is missing planStatus.");
|
|
152
|
+
}
|
|
153
|
+
const planInfo =
|
|
154
|
+
planStatus.planInfo && typeof planStatus.planInfo === "object"
|
|
155
|
+
? planStatus.planInfo
|
|
156
|
+
: {};
|
|
157
|
+
const planLabel = isNonEmptyString(planInfo.planName)
|
|
158
|
+
? planInfo.planName.trim()
|
|
159
|
+
: null;
|
|
160
|
+
|
|
161
|
+
// Legacy Windsurf billing fills ACU credit fields instead of the daily /
|
|
162
|
+
// weekly quota; those strategies have nothing renderable here.
|
|
163
|
+
const billingStrategy = planInfo.billingStrategy;
|
|
164
|
+
if (
|
|
165
|
+
billingStrategy !== undefined &&
|
|
166
|
+
billingStrategy !== "BILLING_STRATEGY_QUOTA"
|
|
167
|
+
) {
|
|
168
|
+
return { plan_label: planLabel, primary_window: null, secondary_window: null };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
plan_label: planLabel,
|
|
173
|
+
primary_window: normalizeWindow({
|
|
174
|
+
remainingPercent: planStatus.dailyQuotaRemainingPercent,
|
|
175
|
+
resetUnix: planStatus.dailyQuotaResetAtUnix,
|
|
176
|
+
hidden: planInfo.hideDailyQuota === true,
|
|
177
|
+
windowSeconds: DEVIN_DAILY_WINDOW_SECONDS,
|
|
178
|
+
}),
|
|
179
|
+
secondary_window: normalizeWindow({
|
|
180
|
+
remainingPercent: planStatus.weeklyQuotaRemainingPercent,
|
|
181
|
+
resetUnix: planStatus.weeklyQuotaResetAtUnix,
|
|
182
|
+
hidden: planInfo.hideWeeklyQuota === true,
|
|
183
|
+
windowSeconds: DEVIN_WEEKLY_WINDOW_SECONDS,
|
|
184
|
+
}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// `{ configured: false }` when the provider is not enabled or no Devin CLI
|
|
189
|
+
// sign-in exists, otherwise the normalized windows. `enabled` is the user's
|
|
190
|
+
// explicit provider selection forwarded from the local API — without it the
|
|
191
|
+
// credentials file is never opened and no request is made. Throws owned
|
|
192
|
+
// errors only — never raw upstream bodies, filesystem paths or token values.
|
|
193
|
+
// `code: "AUTH_EXPIRED"` flags genuine auth failure for the aggregator's
|
|
194
|
+
// auth_action_required path.
|
|
195
|
+
async function fetchDevinLimits({ home, env = process.env, enabled = false, fetchImpl = fetch } = {}) {
|
|
196
|
+
if (enabled !== true) return { configured: false };
|
|
197
|
+
const credentials = readDevinCredentials({ home, env });
|
|
198
|
+
if (!credentials) return { configured: false };
|
|
199
|
+
|
|
200
|
+
let response;
|
|
201
|
+
try {
|
|
202
|
+
response = await fetchImpl(DEVIN_PLAN_STATUS_URL, {
|
|
203
|
+
method: "POST",
|
|
204
|
+
headers: {
|
|
205
|
+
Accept: "application/json",
|
|
206
|
+
"Content-Type": "application/json",
|
|
207
|
+
"Connect-Protocol-Version": "1",
|
|
208
|
+
"x-auth-token": credentials.apiKey,
|
|
209
|
+
},
|
|
210
|
+
body: "{}",
|
|
211
|
+
// The session token must never reach another origin.
|
|
212
|
+
redirect: "error",
|
|
213
|
+
});
|
|
214
|
+
} catch (error) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
error && (error.name === "AbortError" || error.name === "TimeoutError")
|
|
217
|
+
? "Devin quota request timed out."
|
|
218
|
+
: "Devin quota request failed.",
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (response?.status === 401 || response?.status === 403) {
|
|
222
|
+
const error = new Error(
|
|
223
|
+
"Devin sign-in expired or was rejected — run `devin auth login` to sign in again.",
|
|
224
|
+
);
|
|
225
|
+
error.code = "AUTH_EXPIRED";
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
if (!response?.ok) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`Devin quota request was rejected (HTTP ${response?.status ?? "?"}).`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
let body;
|
|
234
|
+
try {
|
|
235
|
+
body = await response.json();
|
|
236
|
+
} catch {
|
|
237
|
+
throw new Error("Devin quota response was not JSON.");
|
|
238
|
+
}
|
|
239
|
+
const windows = normalizePlanStatus(body);
|
|
240
|
+
return {
|
|
241
|
+
configured: true,
|
|
242
|
+
error: null,
|
|
243
|
+
plan_label: windows.plan_label,
|
|
244
|
+
primary_window: windows.primary_window,
|
|
245
|
+
secondary_window: windows.secondary_window,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = { fetchDevinLimits };
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Devin CLI (Cognition — devin.ai) local history projection.
|
|
4
|
+
//
|
|
5
|
+
// Devin persists chat history in a SQLite database at
|
|
6
|
+
// `$XDG_DATA_HOME/devin/cli/sessions.db` (default
|
|
7
|
+
// `~/.local/share/devin/cli/sessions.db`). Token usage lives on assistant
|
|
8
|
+
// message nodes inside the `chat_message` JSON document. The SQL below is a
|
|
9
|
+
// deliberately narrow projection: request identity, generation model, the
|
|
10
|
+
// original generation timestamps, the numeric usage metrics, and the owning
|
|
11
|
+
// session's `working_directory`/`created_at` for canonical ordering and local
|
|
12
|
+
// project attribution. Message bodies, prompt text, node `metadata`,
|
|
13
|
+
// `sessions.cogs_json`, and any credential-bearing state are never selected.
|
|
14
|
+
//
|
|
15
|
+
// Verified against devin CLI 3000.10.21 (611c1cba): replay/fork/compaction
|
|
16
|
+
// produces multiple `message_nodes` rows that share one
|
|
17
|
+
// `metadata.request_id` with identical metrics and original timestamps, so
|
|
18
|
+
// `request_id` — never `row_id` or `(session_id, request_id)` — is the
|
|
19
|
+
// canonical usage identity across the whole local install.
|
|
20
|
+
//
|
|
21
|
+
// Token components are disjoint: the DB's `metrics.input_tokens` already
|
|
22
|
+
// excludes `cache_read_tokens` and `cache_creation_tokens`, so each reported
|
|
23
|
+
// counter is added once and caches are never subtracted from input.
|
|
24
|
+
|
|
25
|
+
const DEVIN_TABLE_PROBE_SQL =
|
|
26
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name IN ('message_nodes','sessions')";
|
|
27
|
+
|
|
28
|
+
const metricField = (jsonPath, alias) =>
|
|
29
|
+
`CASE WHEN json_valid(n.chat_message) THEN json_extract(n.chat_message, '${jsonPath}') ELSE NULL END AS ${alias}`;
|
|
30
|
+
|
|
31
|
+
// `sessions` is LEFT JOINed only for `working_directory` and `created_at`;
|
|
32
|
+
// older databases may not have the table, in which case the projection runs
|
|
33
|
+
// without it (project attribution and session-age ordering degrade to nulls).
|
|
34
|
+
function devinUsageSql({ hasSessionsTable = true } = {}) {
|
|
35
|
+
const join = hasSessionsTable ? "LEFT JOIN sessions s ON s.id = n.session_id" : "";
|
|
36
|
+
const sessionCreatedAt = hasSessionsTable ? "s.created_at" : "NULL";
|
|
37
|
+
const workingDirectory = hasSessionsTable ? "s.working_directory" : "NULL";
|
|
38
|
+
return `
|
|
39
|
+
SELECT
|
|
40
|
+
n.row_id AS row_id,
|
|
41
|
+
n.session_id AS session_id,
|
|
42
|
+
${sessionCreatedAt} AS session_created_at,
|
|
43
|
+
${workingDirectory} AS working_directory,
|
|
44
|
+
${metricField("$.metadata.request_id", "request_id")},
|
|
45
|
+
${metricField("$.metadata.generation_model", "generation_model")},
|
|
46
|
+
${metricField("$.metadata.started_generation_at", "started_generation_at")},
|
|
47
|
+
${metricField("$.metadata.created_at", "message_created_at")},
|
|
48
|
+
${metricField("$.metadata.metrics.input_tokens", "input_tokens")},
|
|
49
|
+
${metricField("$.metadata.metrics.output_tokens", "output_tokens")},
|
|
50
|
+
${metricField("$.metadata.metrics.cache_read_tokens", "cache_read_tokens")},
|
|
51
|
+
${metricField("$.metadata.metrics.cache_creation_tokens", "cache_creation_tokens")}
|
|
52
|
+
FROM message_nodes n
|
|
53
|
+
${join}
|
|
54
|
+
WHERE json_valid(n.chat_message)
|
|
55
|
+
AND json_extract(n.chat_message, '$.role') = 'assistant'
|
|
56
|
+
AND json_extract(n.chat_message, '$.metadata.request_id') IS NOT NULL
|
|
57
|
+
ORDER BY n.row_id
|
|
58
|
+
`.trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeText(value) {
|
|
62
|
+
if (typeof value !== "string") return null;
|
|
63
|
+
const trimmed = value.trim();
|
|
64
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Strict integer check: usage fields must be finite non-negative safe
|
|
68
|
+
// integers. `null`/`undefined` cache counters are legal (no reported cache
|
|
69
|
+
// contribution) — callers pass them through `optionalCount`; required
|
|
70
|
+
// counters go through `requiredCount`.
|
|
71
|
+
function toSafeCount(value) {
|
|
72
|
+
const n = Number(value);
|
|
73
|
+
return Number.isSafeInteger(n) && n >= 0 ? n : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function requiredCount(value) {
|
|
77
|
+
// A missing/NULL required counter is incomplete usage, not zero — pending
|
|
78
|
+
// assistant records carry no metrics block and must stay uncounted until
|
|
79
|
+
// the writer finalizes them.
|
|
80
|
+
if (value === null || value === undefined) return null;
|
|
81
|
+
return toSafeCount(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function optionalCount(value) {
|
|
85
|
+
if (value === null || value === undefined) return 0;
|
|
86
|
+
return toSafeCount(value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// RFC3339 timestamps arrive with both `Z` and `+00:00` spellings; Date.parse
|
|
90
|
+
// normalizes both to the same instant. Returns epoch ms or 0 when missing or
|
|
91
|
+
// unparsable.
|
|
92
|
+
function parseDevinTimestampMs(value) {
|
|
93
|
+
if (typeof value !== "string" || !value.trim()) return 0;
|
|
94
|
+
const ms = Date.parse(value.trim());
|
|
95
|
+
return Number.isFinite(ms) && ms > 0 ? ms : 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// session.created_at is Unix seconds; node row ordering uses ms.
|
|
99
|
+
function epochSecondsToMs(value) {
|
|
100
|
+
const n = Number(value);
|
|
101
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) * 1000 : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Normalize one projected row into a canonical candidate, or null when the
|
|
105
|
+
// record is pending/malformed: missing request identity, missing/invalid
|
|
106
|
+
// numeric usage, or no usable original timestamp. Such rows are re-evaluated
|
|
107
|
+
// on every scan, so a record that gains metrics or a timestamp later is
|
|
108
|
+
// counted then.
|
|
109
|
+
function normalizeDevinUsageRow(row) {
|
|
110
|
+
const requestId = normalizeText(row?.request_id);
|
|
111
|
+
if (!requestId) return null;
|
|
112
|
+
|
|
113
|
+
const input = requiredCount(row?.input_tokens);
|
|
114
|
+
const output = requiredCount(row?.output_tokens);
|
|
115
|
+
const cacheRead = optionalCount(row?.cache_read_tokens);
|
|
116
|
+
const cacheCreation = optionalCount(row?.cache_creation_tokens);
|
|
117
|
+
if (input === null || output === null || cacheRead === null || cacheCreation === null) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// The original generation time decides the bucket; the node insertion time
|
|
122
|
+
// (n.created_at) is clone/replay insertion time and is never a fallback.
|
|
123
|
+
const tsMs =
|
|
124
|
+
parseDevinTimestampMs(row?.started_generation_at) ||
|
|
125
|
+
parseDevinTimestampMs(row?.message_created_at);
|
|
126
|
+
if (!tsMs) return null;
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
requestId,
|
|
130
|
+
sessionId: normalizeText(row?.session_id) || "",
|
|
131
|
+
rowId: toSafeCount(row?.row_id) ?? 0,
|
|
132
|
+
sessionCreatedAtMs: epochSecondsToMs(row?.session_created_at),
|
|
133
|
+
workingDirectory: normalizeText(row?.working_directory),
|
|
134
|
+
model: normalizeText(row?.generation_model) || "unknown",
|
|
135
|
+
tsMs,
|
|
136
|
+
input,
|
|
137
|
+
output,
|
|
138
|
+
cacheRead,
|
|
139
|
+
cacheCreation,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Pick the canonical retained record for one request_id. Copies created by
|
|
144
|
+
// replay/fork/compaction carry identical original timestamps and metrics, so
|
|
145
|
+
// the order is: earliest original generation time, then the earliest-created
|
|
146
|
+
// owning session (a fork's copy lives in a younger session), then session id,
|
|
147
|
+
// then newest row first so an in-place correction appended as a fresh node
|
|
148
|
+
// inside the same session wins over the stale sibling.
|
|
149
|
+
function compareCandidates(a, b) {
|
|
150
|
+
if (a.tsMs !== b.tsMs) return a.tsMs - b.tsMs;
|
|
151
|
+
const sessionA = a.sessionCreatedAtMs ?? Number.POSITIVE_INFINITY;
|
|
152
|
+
const sessionB = b.sessionCreatedAtMs ?? Number.POSITIVE_INFINITY;
|
|
153
|
+
if (sessionA !== sessionB) return sessionA - sessionB;
|
|
154
|
+
if (a.sessionId !== b.sessionId) return a.sessionId < b.sessionId ? -1 : 1;
|
|
155
|
+
return b.rowId - a.rowId;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Build the deduplicated per-request event list. Conversation ownership and
|
|
159
|
+
// session/project attribution are decided by the persisted request ledger in
|
|
160
|
+
// parseDevinIncremental — the projection only reports each request's
|
|
161
|
+
// best-evidence retained record; `conversation_count` stays 0 here and is
|
|
162
|
+
// filled from ledger state at reconcile time.
|
|
163
|
+
function buildDevinUsageEvents(rows) {
|
|
164
|
+
const byRequest = new Map();
|
|
165
|
+
let rowsSeen = 0;
|
|
166
|
+
let rowsSkipped = 0;
|
|
167
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
168
|
+
rowsSeen += 1;
|
|
169
|
+
const candidate = normalizeDevinUsageRow(row);
|
|
170
|
+
if (!candidate) {
|
|
171
|
+
rowsSkipped += 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const list = byRequest.get(candidate.requestId);
|
|
175
|
+
if (list) list.push(candidate);
|
|
176
|
+
else byRequest.set(candidate.requestId, [candidate]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const events = [];
|
|
180
|
+
for (const [requestId, candidates] of byRequest) {
|
|
181
|
+
candidates.sort(compareCandidates);
|
|
182
|
+
const canonical = candidates[0];
|
|
183
|
+
const total = canonical.input + canonical.output + canonical.cacheRead + canonical.cacheCreation;
|
|
184
|
+
events.push({
|
|
185
|
+
requestId,
|
|
186
|
+
sessionId: canonical.sessionId,
|
|
187
|
+
workingDirectory: canonical.workingDirectory,
|
|
188
|
+
model: canonical.model,
|
|
189
|
+
tsMs: canonical.tsMs,
|
|
190
|
+
totals: {
|
|
191
|
+
input_tokens: canonical.input,
|
|
192
|
+
cached_input_tokens: canonical.cacheRead,
|
|
193
|
+
cache_creation_input_tokens: canonical.cacheCreation,
|
|
194
|
+
output_tokens: canonical.output,
|
|
195
|
+
reasoning_output_tokens: 0,
|
|
196
|
+
total_tokens: total,
|
|
197
|
+
billable_total_tokens: total,
|
|
198
|
+
total_cost_usd: 0,
|
|
199
|
+
conversation_count: 0,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
events.sort((a, b) => a.tsMs - b.tsMs || (a.requestId < b.requestId ? -1 : 1));
|
|
204
|
+
|
|
205
|
+
return { events, rowsSeen, rowsSkipped };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = {
|
|
209
|
+
DEVIN_TABLE_PROBE_SQL,
|
|
210
|
+
devinUsageSql,
|
|
211
|
+
buildDevinUsageEvents,
|
|
212
|
+
};
|
package/src/lib/local-api.js
CHANGED
|
@@ -3160,6 +3160,11 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
3160
3160
|
json(res, { ok: true, skill: await skills.installSkill(body.skill, body.targets || ["claude", "codex"]) });
|
|
3161
3161
|
return true;
|
|
3162
3162
|
}
|
|
3163
|
+
if (action === "update_all") {
|
|
3164
|
+
// Partial success is normal, so this reports per-skill rather than failing.
|
|
3165
|
+
json(res, { ok: true, ...(await skills.updateSkills(Array.isArray(body.ids) ? body.ids : [])) });
|
|
3166
|
+
return true;
|
|
3167
|
+
}
|
|
3163
3168
|
if (action === "uninstall") {
|
|
3164
3169
|
json(res, { ok: true, ...(skills.uninstallSkill(body.id) || {}) });
|
|
3165
3170
|
return true;
|
|
@@ -3215,6 +3220,19 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
3215
3220
|
if (p === "/functions/tokentracker-usage-limits") {
|
|
3216
3221
|
const { getUsageLimits, resetUsageLimitsCache } = require("./usage-limits");
|
|
3217
3222
|
try {
|
|
3223
|
+
// Devin quota is opt-in (Settings > Usage & Limits > Providers). An
|
|
3224
|
+
// explicit devin=1 without local authentication is rejected before any
|
|
3225
|
+
// cache reset or provider work — silently downgrading it to a disabled
|
|
3226
|
+
// response would misreport an enabled client as a disabled provider.
|
|
3227
|
+
// Authorization is evaluated unconditionally so the check never depends
|
|
3228
|
+
// on the user-controlled opt-in flag itself.
|
|
3229
|
+
const localAuthorized = isAuthorizedLocalMutation(req);
|
|
3230
|
+
const devinParam = url.searchParams.get("devin");
|
|
3231
|
+
const devinEnabled = devinParam === "1" || devinParam === "true";
|
|
3232
|
+
if (devinEnabled && !localAuthorized) {
|
|
3233
|
+
json(res, { error: "Unauthorized" }, 401);
|
|
3234
|
+
return true;
|
|
3235
|
+
}
|
|
3218
3236
|
const refreshParam = url.searchParams.get("refresh");
|
|
3219
3237
|
const forceRefresh = refreshParam === "1" || refreshParam === "true";
|
|
3220
3238
|
if (forceRefresh) {
|
|
@@ -3227,6 +3245,7 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
3227
3245
|
// Punches through the Claude disk fresh-cache (but not the 429
|
|
3228
3246
|
// cooldown) — an explicit user refresh should hit upstream.
|
|
3229
3247
|
forceRefresh,
|
|
3248
|
+
devinEnabled,
|
|
3230
3249
|
});
|
|
3231
3250
|
json(res, data);
|
|
3232
3251
|
} catch (e) {
|
package/src/lib/passive-mode.js
CHANGED
|
@@ -89,7 +89,7 @@ function classifyWritableFailure(filePath) {
|
|
|
89
89
|
* every_code, opencode, openclaw, codebuddy, workbuddy, grok }
|
|
90
90
|
* @returns {PassiveProvider[]}
|
|
91
91
|
*/
|
|
92
|
-
function detectPassiveProviders({ home, hookStatus }) {
|
|
92
|
+
function detectPassiveProviders({ home, hookStatus, env = process.env }) {
|
|
93
93
|
const out = [];
|
|
94
94
|
|
|
95
95
|
// Claude Code — logs at ~/.claude/projects/<name>/*.jsonl
|
|
@@ -128,6 +128,18 @@ function detectPassiveProviders({ home, hookStatus }) {
|
|
|
128
128
|
settingsPath: path.join(home, ".codex", "config.toml"),
|
|
129
129
|
}));
|
|
130
130
|
|
|
131
|
+
const acodeHome = env.TOKENTRACKER_ACODE_HOME || path.join(home, ".acode");
|
|
132
|
+
out.push(buildEntry({
|
|
133
|
+
name: "acode",
|
|
134
|
+
hookExpected: true,
|
|
135
|
+
hookInstalled: Boolean(hookStatus?.acode_notify ?? hookStatus?.acode),
|
|
136
|
+
logsDir: acodeHome,
|
|
137
|
+
logsPredicate: (_full, name, isDir) =>
|
|
138
|
+
(isDir && (name === "sessions" || name === "archived_sessions")) ||
|
|
139
|
+
(!isDir && name.endsWith(".jsonl")),
|
|
140
|
+
settingsPath: path.join(acodeHome, "config.toml"),
|
|
141
|
+
}));
|
|
142
|
+
|
|
131
143
|
// Every Code — same shape as Codex, different home
|
|
132
144
|
out.push(buildEntry({
|
|
133
145
|
name: "every_code",
|