tokentracker-cli 0.79.8 → 0.80.0
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/dashboard/dist/assets/{AchievementBadge-Ctu3c_8t.js → AchievementBadge-D9jOMhlD.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-B3kBTHDB.js → AchievementsPage-DuswZa4g.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-CAnTXGpC.js → AchievementsSection-BJPeE1Uq.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-oTUXEQk9.js → BadgeDetailModal-nNPIsy6i.js} +1 -1
- package/dashboard/dist/assets/{Card-BHt5f5cD.js → Card-VENq8eEp.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-ChxXY3l0.js → ClawdAnimated-Cq7TW0vv.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-CVLlItP1.js → CommunityStatsModal-y2VftQuq.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-ClrILpb3.js → ConfirmModal-CWgnJ4hm.js} +1 -1
- package/dashboard/dist/assets/{Controls-D_XBBpIh.js → Controls-BUQ_iZZq.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-WkiygMmq.js +19 -0
- package/dashboard/dist/assets/{DevicePage-J7DW7yeX.js → DevicePage-Dh3XDtNx.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-B1r9gzUF.js → DialogClose-KTFU_B3F.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-DIra288n.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-BL2puDA7.js → DialogTitle-bza2pIek.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Cd-YT4oT.js → FadeIn-Dm3FVNfr.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-BAtl4a-K.js → HeaderGithubStar-CbSElzLt.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-BXK_cYN2.js → IpCheckPage-BDON7VCU.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-C_16AbpF.js → LandingPage-Ck3CwZrt.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardAvatar-B0vUuCYU.js → LeaderboardAvatar-GVUy0Ynq.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-Duhdaq09.js → LeaderboardPage-C19Rkt44.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-CFFyHRTB.js → LeaderboardProfileModal-QcEq_o6E.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CjTelV-4.js → LeaderboardProfilePage-pFhI6292.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-DIcXRgSv.js → LimitsPage-ChV3Qezd.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-DC7wXWds.js → LocalOnlyNotice-ALvpjznF.js} +1 -1
- package/dashboard/dist/assets/{LoginPage--8hYT3aq.js → LoginPage-CL-QrJxU.js} +1 -1
- package/dashboard/dist/assets/{PetPage-ByWUW7-V.js → PetPage-DSuvdGgd.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-g_v1fmhH.js → PopoverPopup-jSDQ87Bi.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-BfunstIi.js → ResetPasswordPage-Dk2IDAJq.js} +1 -1
- package/dashboard/dist/assets/{Select-C4ShPt1t.js → Select-Cz6WTj7D.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-gw_TdvY6.js → SelectItemText-BZyRjWCM.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-BFCFrDuA.js → SettingsPage-CANz8HMM.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-BNQDySeK.js → SkillsPage-WZkQG_83.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-B3bHP3Vk.js → WidgetsPage-BEyJ62NV.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-6amxWnWT.js → WrappedPage-Ck_dOx_w.js} +1 -1
- package/dashboard/dist/assets/{agent-logos-DPIQzYms.js → agent-logos-DpjHiAbZ.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CKcE2mif.js → arrow-up-right-C4iYPBtK.js} +1 -1
- package/dashboard/dist/assets/check-BkPUxPZV.js +1 -0
- package/dashboard/dist/assets/{copy-vi-V2M_I.js → copy-CHJ-38K6.js} +1 -1
- package/dashboard/dist/assets/{download-BH0Spg15.js → download-BSeG8u17.js} +1 -1
- package/dashboard/dist/assets/{flame-C3jUAnqA.js → flame-DeHwKcE-.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-tnfJT09G.js → format-tokens-B-MJYJyp.js} +1 -1
- package/dashboard/dist/assets/{info-CGCZ5JvP.js → info-DA_0xr_o.js} +1 -1
- package/dashboard/dist/assets/{link-2-CXULWwZY.js → link-2-noUH83Lc.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-B324hSa3.js → loader-circle-wL7KZtm0.js} +1 -1
- package/dashboard/dist/assets/{main-vGUuyRq-.js → main-BHfJDFcu.js} +20 -20
- package/dashboard/dist/assets/{provider-display-CeoAhoCb.js → provider-display-DxIClOGM.js} +1 -1
- package/dashboard/dist/assets/{trash-2-L9vxbDeP.js → trash-2-B8TPUmxZ.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DLE30Lh0.js → use-community-stats-CXD3MRRE.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-BxN--PEW.js → use-limits-display-prefs-C2pJVYTn.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-Dn5gWQkJ.js → use-native-settings-BeOBLEZa.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-DYoz2NJ9.js → use-ordered-list-kAtW8pJm.js} +1 -1
- package/dashboard/dist/assets/{use-quality-per-dollar-pref-JXorBQsT.js → use-quality-per-dollar-pref-CFZUZM3S.js} +1 -1
- package/dashboard/dist/assets/{use-transform-BTTF5vPC.js → use-transform-B9YwpyKi.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-BUemSWQ9.js → use-usage-limits-DSX5iKPE.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-Cxv4RR3U.js → useCurrency-CKg5K4Nj.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-DTPnHKfF.js → useScrollLock-DyyY_Vyt.js} +1 -1
- package/dashboard/dist/assets/useTokenFormat-FfTjEH_m.js +1 -0
- package/dashboard/dist/assets/{zap-CG8zuY9S.js → zap-B69x9NCT.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/init.js +89 -12
- package/src/commands/serve.js +65 -4
- package/src/commands/status.js +2 -1
- package/src/commands/sync.js +170 -7
- package/src/commands/uninstall.js +10 -2
- package/src/lib/claude-config.js +69 -28
- package/src/lib/diagnostics.js +2 -2
- package/src/lib/local-api.js +19 -6
- package/src/lib/openclaw-session-plugin.js +13 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +63 -2
- package/src/lib/usage-limits.js +9 -5
- package/dashboard/dist/assets/DashboardPage-CizTJox2.js +0 -19
- package/dashboard/dist/assets/DialogDescription-Bd9-kaCS.js +0 -1
- package/dashboard/dist/assets/check-B5V4_ezt.js +0 -1
- package/dashboard/dist/assets/useTokenFormat-DMAb2ec3.js +0 -1
package/src/commands/init.js
CHANGED
|
@@ -21,9 +21,11 @@ const {
|
|
|
21
21
|
} = require("../lib/codex-config");
|
|
22
22
|
const {
|
|
23
23
|
upsertClaudeHook,
|
|
24
|
+
upsertClaudeUsageHooks,
|
|
24
25
|
buildClaudeHookCommand,
|
|
25
26
|
buildHookCommand,
|
|
26
27
|
isClaudeHookConfigured,
|
|
28
|
+
areClaudeUsageHooksConfigured,
|
|
27
29
|
} = require("../lib/claude-config");
|
|
28
30
|
const {
|
|
29
31
|
resolveGeminiConfigDir,
|
|
@@ -418,6 +420,78 @@ async function repairCodexNotifyIntegration({ home = os.homedir(), trackerDir, b
|
|
|
418
420
|
return { ...result, skippedReason: null, notifyPath };
|
|
419
421
|
}
|
|
420
422
|
|
|
423
|
+
async function repairRuntimeIntegrations({
|
|
424
|
+
home = os.homedir(),
|
|
425
|
+
trackerDir,
|
|
426
|
+
binDir,
|
|
427
|
+
safeMode = true,
|
|
428
|
+
} = {}) {
|
|
429
|
+
const paths = trackerDir && binDir ? { trackerDir, binDir } : await resolveTrackerPaths({ home });
|
|
430
|
+
const resolvedTrackerDir = trackerDir || paths.trackerDir;
|
|
431
|
+
const resolvedBinDir = binDir || paths.binDir;
|
|
432
|
+
const notifyPath = await writeNotifyHandler({
|
|
433
|
+
trackerDir: resolvedTrackerDir,
|
|
434
|
+
binDir: resolvedBinDir,
|
|
435
|
+
});
|
|
436
|
+
const context = buildIntegrationTargets({
|
|
437
|
+
home,
|
|
438
|
+
trackerDir: resolvedTrackerDir,
|
|
439
|
+
notifyPath,
|
|
440
|
+
});
|
|
441
|
+
const integrations = {};
|
|
442
|
+
const warnings = [];
|
|
443
|
+
|
|
444
|
+
const attempt = async (key, work) => {
|
|
445
|
+
try {
|
|
446
|
+
integrations[key] = await work();
|
|
447
|
+
} catch (error) {
|
|
448
|
+
integrations[key] = { changed: false, skippedReason: "repair-failed" };
|
|
449
|
+
warnings.push({ integration: key, error: error?.message || String(error) });
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
await attempt("codex", () => repairCodexNotifyIntegration({
|
|
454
|
+
home,
|
|
455
|
+
trackerDir: resolvedTrackerDir,
|
|
456
|
+
binDir: resolvedBinDir,
|
|
457
|
+
safeMode,
|
|
458
|
+
}));
|
|
459
|
+
|
|
460
|
+
const hookRepairs = [
|
|
461
|
+
["claude", context.claudeDir, () => upsertClaudeUsageHooks({
|
|
462
|
+
settingsPath: context.claudeSettingsPath,
|
|
463
|
+
hookCommand: context.claudeHookCommand,
|
|
464
|
+
})],
|
|
465
|
+
["gemini", context.geminiConfigDir, () => upsertGeminiHook({
|
|
466
|
+
settingsPath: context.geminiSettingsPath,
|
|
467
|
+
hookCommand: context.geminiHookCommand,
|
|
468
|
+
})],
|
|
469
|
+
["codebuddy", context.codebuddyDir, () => upsertClaudeHook({
|
|
470
|
+
settingsPath: context.codebuddySettingsPath,
|
|
471
|
+
hookCommand: context.codebuddyHookCommand,
|
|
472
|
+
})],
|
|
473
|
+
["workbuddy", context.workbuddyDir, () => upsertClaudeHook({
|
|
474
|
+
settingsPath: context.workbuddySettingsPath,
|
|
475
|
+
hookCommand: context.workbuddyHookCommand,
|
|
476
|
+
})],
|
|
477
|
+
];
|
|
478
|
+
for (const [key, configDir, repair] of hookRepairs) {
|
|
479
|
+
if (await isDir(configDir)) await attempt(key, repair);
|
|
480
|
+
else integrations[key] = { changed: false, skippedReason: "config-missing" };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (await isDir(context.opencodeConfigDir)) {
|
|
484
|
+
await attempt("opencode", () => upsertOpencodePlugin({
|
|
485
|
+
configDir: context.opencodeConfigDir,
|
|
486
|
+
notifyPath,
|
|
487
|
+
}));
|
|
488
|
+
} else {
|
|
489
|
+
integrations.opencode = { changed: false, skippedReason: "config-missing" };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return { notifyPath, integrations, warnings };
|
|
493
|
+
}
|
|
494
|
+
|
|
421
495
|
function buildIntegrationTargets({ home, trackerDir, notifyPath }) {
|
|
422
496
|
const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
|
|
423
497
|
const codexConfigPath = path.join(codexHome, "config.toml");
|
|
@@ -495,7 +569,7 @@ async function applyIntegrationSetup({ home, trackerDir, notifyPath, notifyOrigi
|
|
|
495
569
|
|
|
496
570
|
const claudeDirExists = await isDir(context.claudeDir);
|
|
497
571
|
if (claudeDirExists) {
|
|
498
|
-
await
|
|
572
|
+
await upsertClaudeUsageHooks({
|
|
499
573
|
settingsPath: context.claudeSettingsPath,
|
|
500
574
|
hookCommand: context.claudeHookCommand,
|
|
501
575
|
});
|
|
@@ -787,7 +861,7 @@ async function previewIntegrations({ context }) {
|
|
|
787
861
|
|
|
788
862
|
const claudeDirExists = await isDir(context.claudeDir);
|
|
789
863
|
if (claudeDirExists) {
|
|
790
|
-
const configured = await
|
|
864
|
+
const configured = await areClaudeUsageHooksConfigured({
|
|
791
865
|
settingsPath: context.claudeSettingsPath,
|
|
792
866
|
hookCommand: context.claudeHookCommand,
|
|
793
867
|
});
|
|
@@ -1071,7 +1145,6 @@ const codexOriginalPath = ${JSON.stringify(originalPath)};
|
|
|
1071
1145
|
const codeOriginalPath = ${JSON.stringify(path.join(trackerDir, "code_notify_original.json"))};
|
|
1072
1146
|
const trackerBinPath = ${JSON.stringify(trackerBinPath)};
|
|
1073
1147
|
const depsMarkerPath = path.join(trackerDir, 'app', 'bin', 'tracker.js');
|
|
1074
|
-
const configPath = path.join(trackerDir, 'config.json');
|
|
1075
1148
|
const fallbackPkg = ${JSON.stringify(fallbackPkg)};
|
|
1076
1149
|
const selfPath = path.resolve(__filename);
|
|
1077
1150
|
const home = os.homedir();
|
|
@@ -1109,18 +1182,10 @@ if (debugEnabled) {
|
|
|
1109
1182
|
// Throttle spawn: at most once per 20 seconds.
|
|
1110
1183
|
try {
|
|
1111
1184
|
const throttlePath = path.join(trackerDir, 'sync.throttle');
|
|
1112
|
-
let deviceToken = process.env.TOKENTRACKER_DEVICE_TOKEN || null;
|
|
1113
|
-
if (!deviceToken) {
|
|
1114
|
-
try {
|
|
1115
|
-
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
1116
|
-
if (cfg && typeof cfg.deviceToken === 'string') deviceToken = cfg.deviceToken;
|
|
1117
|
-
} catch (_) {}
|
|
1118
|
-
}
|
|
1119
|
-
const canSync = Boolean(deviceToken && deviceToken.length > 0);
|
|
1120
1185
|
const now = Date.now();
|
|
1121
1186
|
let last = 0;
|
|
1122
1187
|
try { last = Number(fs.readFileSync(throttlePath, 'utf8')) || 0; } catch (_) {}
|
|
1123
|
-
if (
|
|
1188
|
+
if (now - last > 20_000) {
|
|
1124
1189
|
try { fs.writeFileSync(throttlePath, String(now), 'utf8'); } catch (_) {}
|
|
1125
1190
|
const hasLocalRuntime = fs.existsSync(trackerBinPath);
|
|
1126
1191
|
const hasLocalDeps = fs.existsSync(depsMarkerPath);
|
|
@@ -1241,6 +1306,7 @@ module.exports = {
|
|
|
1241
1306
|
buildNotifyHandler,
|
|
1242
1307
|
installLocalTrackerApp,
|
|
1243
1308
|
repairCodexNotifyIntegration,
|
|
1309
|
+
repairRuntimeIntegrations,
|
|
1244
1310
|
};
|
|
1245
1311
|
|
|
1246
1312
|
async function probeFile(p) {
|
|
@@ -1266,6 +1332,11 @@ async function isDir(p) {
|
|
|
1266
1332
|
}
|
|
1267
1333
|
|
|
1268
1334
|
async function installLocalTrackerApp({ appDir }) {
|
|
1335
|
+
// Test-only escape hatch: skip the ~10 MB runtime copy (src + dashboard/dist +
|
|
1336
|
+
// node_modules) when a test only exercises hook/config/notify wiring. Copying
|
|
1337
|
+
// the bundle per test case dominates the init test suite's wall time. Tests
|
|
1338
|
+
// that assert on the copied runtime simply leave this unset.
|
|
1339
|
+
if (process.env.TOKENTRACKER_SKIP_LOCAL_RUNTIME_COPY === "1") return;
|
|
1269
1340
|
// Copy the current package's runtime (bin + src) into ~/.tokentracker so notify can run sync without npx.
|
|
1270
1341
|
const packageRoot = path.resolve(__dirname, "../..");
|
|
1271
1342
|
const srcFrom = path.join(packageRoot, "src");
|
|
@@ -1321,6 +1392,12 @@ async function safeRealpath(p) {
|
|
|
1321
1392
|
const FIRST_SYNC_TIMEOUT_MS = 15_000;
|
|
1322
1393
|
|
|
1323
1394
|
async function runFirstSyncAndRead({ trackerBinPath, trackerDir, packageName }) {
|
|
1395
|
+
// Test-only escape hatch: skip spawning a real `sync --drain` subprocess when
|
|
1396
|
+
// a test only exercises init wiring. Returns the same shape derived from an
|
|
1397
|
+
// (empty) queue so callers behave identically without the ~1s spawn per case.
|
|
1398
|
+
if (process.env.TOKENTRACKER_SKIP_FIRST_SYNC === "1") {
|
|
1399
|
+
return readFirstSyncTotals(trackerDir);
|
|
1400
|
+
}
|
|
1324
1401
|
const fallbackPkg = packageName || "tokentracker-cli";
|
|
1325
1402
|
const argv = ["sync", "--drain"];
|
|
1326
1403
|
const hasLocalRuntime = typeof trackerBinPath === "string" && fssync.existsSync(trackerBinPath);
|
package/src/commands/serve.js
CHANGED
|
@@ -22,6 +22,7 @@ const WSL_DEFAULT_PORT = 7681;
|
|
|
22
22
|
const DEFAULT_MAX_PORT_ATTEMPTS = 20;
|
|
23
23
|
const NPM_PACKAGE_NAME = "tokentracker-cli";
|
|
24
24
|
const LOCAL_BIND_HOST = "127.0.0.1";
|
|
25
|
+
const NATIVE_BACKGROUND_SYNC_INTERVAL_MS = 60_000;
|
|
25
26
|
const STATIC_ASSET_EXTENSIONS = new Set([
|
|
26
27
|
".css",
|
|
27
28
|
".gif",
|
|
@@ -71,11 +72,13 @@ async function cmdServe(argv) {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
try {
|
|
74
|
-
const { installLocalTrackerApp,
|
|
75
|
+
const { installLocalTrackerApp, repairRuntimeIntegrations } = require("./init");
|
|
75
76
|
await installLocalTrackerApp({ appDir: path.join(trackerDir, "app") });
|
|
76
|
-
const repairResult = await
|
|
77
|
-
|
|
78
|
-
process.stdout.write(
|
|
77
|
+
const repairResult = await repairRuntimeIntegrations({ trackerDir, binDir, safeMode: true });
|
|
78
|
+
for (const warning of repairResult?.warnings || []) {
|
|
79
|
+
process.stdout.write(
|
|
80
|
+
`Runtime integration repair warning (${warning.integration}): ${warning.error}\n`,
|
|
81
|
+
);
|
|
79
82
|
}
|
|
80
83
|
} catch (e) {
|
|
81
84
|
process.stdout.write(`Runtime refresh warning: ${e?.message || e}\n`);
|
|
@@ -217,6 +220,18 @@ async function cmdServe(argv) {
|
|
|
217
220
|
|
|
218
221
|
await maybeShowStarCta({ trackerDir });
|
|
219
222
|
|
|
223
|
+
// Native desktop apps keep this server alive even when the dashboard window
|
|
224
|
+
// is closed. Provider hooks are the fast path, but they can be removed by a
|
|
225
|
+
// tool update or skipped by a provider. Scan every local source once a
|
|
226
|
+
// minute as a bounded fallback so the queue cannot remain stale indefinitely.
|
|
227
|
+
// `--no-sync` still skips the blocking startup sync; this periodic pass is
|
|
228
|
+
// native-shell-only and uses the lightweight local mode (no cloud upload,
|
|
229
|
+
// Cursor network request, or deep Codex archive scan).
|
|
230
|
+
const nativeBackgroundSync = startNativeBackgroundSync({
|
|
231
|
+
onError: (e) =>
|
|
232
|
+
process.stdout.write(`Background local sync warning: ${e?.message || e}\n`),
|
|
233
|
+
});
|
|
234
|
+
|
|
220
235
|
// Anonymous daily heartbeat (see src/lib/telemetry.js for the privacy
|
|
221
236
|
// contract). Fire-and-forget at startup, then re-checked every 6 hours so
|
|
222
237
|
// long-lived embedded-app servers still count on later days; the shared
|
|
@@ -238,6 +253,7 @@ async function cmdServe(argv) {
|
|
|
238
253
|
// 5. Graceful shutdown
|
|
239
254
|
const shutdown = () => {
|
|
240
255
|
process.stdout.write("\nShutting down...\n");
|
|
256
|
+
nativeBackgroundSync?.stop();
|
|
241
257
|
server.close(() => process.exit(0));
|
|
242
258
|
setTimeout(() => process.exit(0), 3000);
|
|
243
259
|
};
|
|
@@ -248,6 +264,49 @@ async function cmdServe(argv) {
|
|
|
248
264
|
await new Promise(() => {});
|
|
249
265
|
}
|
|
250
266
|
|
|
267
|
+
function startNativeBackgroundSync({
|
|
268
|
+
appShell = process.env.TOKENTRACKER_APP_SHELL,
|
|
269
|
+
intervalMs = NATIVE_BACKGROUND_SYNC_INTERVAL_MS,
|
|
270
|
+
runSync,
|
|
271
|
+
setIntervalFn = setInterval,
|
|
272
|
+
clearIntervalFn = clearInterval,
|
|
273
|
+
onError = () => {},
|
|
274
|
+
} = {}) {
|
|
275
|
+
const normalizedShell = String(appShell || "").trim().toLowerCase();
|
|
276
|
+
if (normalizedShell !== "macos" && normalizedShell !== "windows") return null;
|
|
277
|
+
|
|
278
|
+
const sync = runSync || (async (args) => {
|
|
279
|
+
const { cmdSync } = require("./sync");
|
|
280
|
+
await cmdSync(args);
|
|
281
|
+
});
|
|
282
|
+
let inFlight = null;
|
|
283
|
+
const run = () => {
|
|
284
|
+
if (inFlight) return inFlight;
|
|
285
|
+
const pending = Promise.resolve()
|
|
286
|
+
.then(() => sync(["--auto", "--background", "--all-local-sources"]))
|
|
287
|
+
.catch((error) => {
|
|
288
|
+
try { onError(error); } catch (_e) {}
|
|
289
|
+
})
|
|
290
|
+
.finally(() => {
|
|
291
|
+
if (inFlight === pending) inFlight = null;
|
|
292
|
+
});
|
|
293
|
+
inFlight = pending;
|
|
294
|
+
return pending;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const timer = setIntervalFn(() => {
|
|
298
|
+
void run();
|
|
299
|
+
}, intervalMs);
|
|
300
|
+
timer?.unref?.();
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
run,
|
|
304
|
+
stop() {
|
|
305
|
+
clearIntervalFn(timer);
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
251
310
|
function findPidOnPort(port) {
|
|
252
311
|
try {
|
|
253
312
|
const out = cp.execFileSync("lsof", ["-ti", `tcp:${port}`], { encoding: "utf8", timeout: 5000 });
|
|
@@ -455,4 +514,6 @@ module.exports = {
|
|
|
455
514
|
isRunningUnderWsl,
|
|
456
515
|
resolveDefaultPort,
|
|
457
516
|
shouldServeSpaFallback,
|
|
517
|
+
startNativeBackgroundSync,
|
|
518
|
+
NATIVE_BACKGROUND_SYNC_INTERVAL_MS,
|
|
458
519
|
};
|
package/src/commands/status.js
CHANGED
|
@@ -8,6 +8,7 @@ const { readJson } = require("../lib/fs");
|
|
|
8
8
|
const { readCodexNotify, readEveryCodeNotify } = require("../lib/codex-config");
|
|
9
9
|
const {
|
|
10
10
|
isClaudeHookConfigured,
|
|
11
|
+
areClaudeUsageHooksConfigured,
|
|
11
12
|
buildClaudeHookCommand,
|
|
12
13
|
buildHookCommand,
|
|
13
14
|
} = require("../lib/claude-config");
|
|
@@ -155,7 +156,7 @@ async function cmdStatus(argv = []) {
|
|
|
155
156
|
const everyCodeNotify = await readEveryCodeNotify(codeConfigPath);
|
|
156
157
|
const everyCodeConfigured =
|
|
157
158
|
Array.isArray(everyCodeNotify) && everyCodeNotify.length > 0;
|
|
158
|
-
const claudeHookConfigured = await
|
|
159
|
+
const claudeHookConfigured = await areClaudeUsageHooksConfigured({
|
|
159
160
|
settingsPath: claudeSettingsPath,
|
|
160
161
|
hookCommand: claudeHookCommand,
|
|
161
162
|
});
|
package/src/commands/sync.js
CHANGED
|
@@ -172,6 +172,18 @@ const CLOUD_CONVERSATIONS_BACKFILL_KEY = "cloudConversationsBackfill_2026_06";
|
|
|
172
172
|
// ~/.codex/archived_sessions/ which sync does not scan) — clearing its bucket
|
|
173
173
|
// would lose that history (ref the v6 ground-truth-repair data-loss incident).
|
|
174
174
|
const CODEX_RESCAN_DEDUP_REPAIR_KEY = "codexRescanDedupRepair_2026_06";
|
|
175
|
+
// One-time repair (#169 follow-up): forked Codex rollouts replay the parent
|
|
176
|
+
// session's token history into the child file, and until the same-day burst
|
|
177
|
+
// detector landed in rollout.js the parser counted those replayed rows as live
|
|
178
|
+
// usage (PR #169's date guard only caught cross-day forks). Cursors sit at EOF
|
|
179
|
+
// on every already-parsed file, so the forward fix alone never corrects the
|
|
180
|
+
// inflated history. This re-runs the exact #187 guarded rebuild under a new key
|
|
181
|
+
// — the rebuild re-parses every codex file with the CURRENT (fork-aware)
|
|
182
|
+
// parser, so replay rows are excluded — and inherits all its safety properties
|
|
183
|
+
// (unreproducible-session skip, atomic throwaway rebuild, queue strip, upload
|
|
184
|
+
// offset reset for the cloud overwrite). Pre-gated: installs with no forked
|
|
185
|
+
// rollout on disk carry no fork phantom and mark done without the rebuild.
|
|
186
|
+
const CODEX_FORK_REPLAY_REPAIR_KEY = "codexForkReplayRepair_2026_07";
|
|
175
187
|
const DROID_DUP_SESSION_REPAIR_KEY = "droidDupSessionInflationRepair_2026_06";
|
|
176
188
|
const CODEX_COLD_SCAN_AUDIT_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
177
189
|
const CODEX_COLD_SCAN_AUDIT_MAX_SYNCS = 288;
|
|
@@ -330,11 +342,18 @@ async function cmdSync(argv) {
|
|
|
330
342
|
|
|
331
343
|
const autoSourceScope = resolveAutoSourceScope(opts);
|
|
332
344
|
const isBackgroundLightweightSync = opts.auto && opts.background && !opts.drain;
|
|
345
|
+
const isBackgroundAllLocalSync = isBackgroundLightweightSync && opts.allLocalSources;
|
|
333
346
|
const isFullSourceScan = !autoSourceScope && !isBackgroundLightweightSync;
|
|
334
347
|
const sourceAllowed = (...sources) => {
|
|
335
348
|
if (isBackgroundLightweightSync) {
|
|
336
349
|
if (autoSourceScope) {
|
|
337
|
-
return
|
|
350
|
+
return (
|
|
351
|
+
(isBackgroundAllLocalSync || BACKGROUND_AUTO_SYNC_SOURCES.has(autoSourceScope)) &&
|
|
352
|
+
sources.includes(autoSourceScope)
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
if (isBackgroundAllLocalSync) {
|
|
356
|
+
return sources.some((source) => AUTO_SYNC_SOURCES.has(source));
|
|
338
357
|
}
|
|
339
358
|
return sources.some((source) => BACKGROUND_AUTO_SYNC_SOURCES.has(source));
|
|
340
359
|
}
|
|
@@ -398,13 +417,22 @@ async function cmdSync(argv) {
|
|
|
398
417
|
|
|
399
418
|
if (isFullSourceScan) {
|
|
400
419
|
await migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rolloutFiles });
|
|
401
|
-
await repairCodexRescanInflation({
|
|
420
|
+
const codexRescanRepairRan = await repairCodexRescanInflation({
|
|
421
|
+
cursors,
|
|
422
|
+
queuePath,
|
|
423
|
+
queueStatePath,
|
|
424
|
+
projectQueuePath,
|
|
425
|
+
projectQueueStatePath,
|
|
426
|
+
rolloutFiles,
|
|
427
|
+
});
|
|
428
|
+
await repairCodexForkReplayInflation({
|
|
402
429
|
cursors,
|
|
403
430
|
queuePath,
|
|
404
431
|
queueStatePath,
|
|
405
432
|
projectQueuePath,
|
|
406
433
|
projectQueueStatePath,
|
|
407
434
|
rolloutFiles,
|
|
435
|
+
legacyRepairRan: codexRescanRepairRan,
|
|
408
436
|
});
|
|
409
437
|
await repairDroidDuplicateSessionInflation({ cursors, queuePath, queueStatePath });
|
|
410
438
|
await repairMimoClaudeMislabel({
|
|
@@ -1998,6 +2026,7 @@ function parseArgs(argv) {
|
|
|
1998
2026
|
source: null,
|
|
1999
2027
|
drain: false,
|
|
2000
2028
|
background: false,
|
|
2029
|
+
allLocalSources: false,
|
|
2001
2030
|
repairGrok: false,
|
|
2002
2031
|
};
|
|
2003
2032
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -2013,6 +2042,7 @@ function parseArgs(argv) {
|
|
|
2013
2042
|
else if (a.startsWith("--source=")) out.source = normalizeSyncSource(a.slice("--source=".length));
|
|
2014
2043
|
else if (a === "--drain") out.drain = true;
|
|
2015
2044
|
else if (a === "--background" || a === "--lightweight") out.background = true;
|
|
2045
|
+
else if (a === "--all-local-sources") out.allLocalSources = true;
|
|
2016
2046
|
else if (a === "--repair-grok") out.repairGrok = true;
|
|
2017
2047
|
else throw new Error(`Unknown option: ${a}`);
|
|
2018
2048
|
}
|
|
@@ -2088,6 +2118,7 @@ module.exports = {
|
|
|
2088
2118
|
migrateCursorUnknownBuckets,
|
|
2089
2119
|
migrateRolloutCumulativeDeltaBuckets,
|
|
2090
2120
|
repairCodexRescanInflation,
|
|
2121
|
+
repairCodexForkReplayInflation,
|
|
2091
2122
|
repairDroidDuplicateSessionInflation,
|
|
2092
2123
|
repairMimoClaudeMislabel,
|
|
2093
2124
|
reincludeClaudeMemObserverFiles,
|
|
@@ -2100,6 +2131,7 @@ module.exports = {
|
|
|
2100
2131
|
CURSOR_UNKNOWN_MIGRATION_KEY,
|
|
2101
2132
|
ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY,
|
|
2102
2133
|
CODEX_RESCAN_DEDUP_REPAIR_KEY,
|
|
2134
|
+
CODEX_FORK_REPLAY_REPAIR_KEY,
|
|
2103
2135
|
DROID_DUP_SESSION_REPAIR_KEY,
|
|
2104
2136
|
CLAUDE_MEM_OBSERVER_REINCLUDE_KEY,
|
|
2105
2137
|
GROK_APPEND_ONLY_REPAIR_MIGRATION_KEY,
|
|
@@ -3148,6 +3180,11 @@ async function repairCodexRescanInflation({
|
|
|
3148
3180
|
projectQueuePath,
|
|
3149
3181
|
projectQueueStatePath,
|
|
3150
3182
|
rolloutFiles,
|
|
3183
|
+
// The fork-replay repair (#169 follow-up) re-runs this exact rebuild under
|
|
3184
|
+
// its own key: the rebuild always uses the CURRENT parser, so any parser fix
|
|
3185
|
+
// shipped since the last run is applied to the rebuilt history.
|
|
3186
|
+
migrationKey = CODEX_RESCAN_DEDUP_REPAIR_KEY,
|
|
3187
|
+
uploadNote = "reset_after_codex_rescan_dedup_2026_06",
|
|
3151
3188
|
}) {
|
|
3152
3189
|
if (!cursors || typeof cursors !== "object") return false;
|
|
3153
3190
|
const migrations = (cursors.migrations ||= {});
|
|
@@ -3158,7 +3195,7 @@ async function repairCodexRescanInflation({
|
|
|
3158
3195
|
// found). Treating the skip sentinel as "done" is what left users like #187
|
|
3159
3196
|
// permanently stuck on the inflated value after upgrading (the key was truthy
|
|
3160
3197
|
// so the guard never got a second chance).
|
|
3161
|
-
const priorRepair = migrations[
|
|
3198
|
+
const priorRepair = migrations[migrationKey];
|
|
3162
3199
|
if (priorRepair && !(typeof priorRepair === "object" && priorRepair.skipped)) return false;
|
|
3163
3200
|
|
|
3164
3201
|
// Codex session files THIS sync discovered (source === "codex").
|
|
@@ -3194,7 +3231,7 @@ async function repairCodexRescanInflation({
|
|
|
3194
3231
|
if (codexFileSet.has(fp)) continue; // exact file re-scanned this run
|
|
3195
3232
|
const id = codexSessionIdFromPath(fp);
|
|
3196
3233
|
if (id && scannedSessionIds.has(id)) continue; // same session scanned elsewhere (moved)
|
|
3197
|
-
migrations[
|
|
3234
|
+
migrations[migrationKey] = {
|
|
3198
3235
|
skipped: true,
|
|
3199
3236
|
reason: "codex_session_unreproducible",
|
|
3200
3237
|
at: new Date().toISOString(),
|
|
@@ -3453,7 +3490,7 @@ async function repairCodexRescanInflation({
|
|
|
3453
3490
|
}
|
|
3454
3491
|
uploadState.offset = 0;
|
|
3455
3492
|
uploadState.updatedAt = new Date().toISOString();
|
|
3456
|
-
uploadState.note =
|
|
3493
|
+
uploadState.note = uploadNote;
|
|
3457
3494
|
await fs.writeFile(queueStatePath, JSON.stringify(uploadState));
|
|
3458
3495
|
}
|
|
3459
3496
|
if (projectRepairEnabled && typeof projectQueueStatePath === "string" && projectQueueStatePath) {
|
|
@@ -3465,14 +3502,140 @@ async function repairCodexRescanInflation({
|
|
|
3465
3502
|
}
|
|
3466
3503
|
uploadState.offset = 0;
|
|
3467
3504
|
uploadState.updatedAt = new Date().toISOString();
|
|
3468
|
-
uploadState.note =
|
|
3505
|
+
uploadState.note = uploadNote;
|
|
3469
3506
|
await fs.writeFile(projectQueueStatePath, JSON.stringify(uploadState));
|
|
3470
3507
|
}
|
|
3471
3508
|
|
|
3472
|
-
migrations[
|
|
3509
|
+
migrations[migrationKey] = new Date().toISOString();
|
|
3473
3510
|
return true;
|
|
3474
3511
|
}
|
|
3475
3512
|
|
|
3513
|
+
// #169 follow-up: repair the historical inflation left by same-day forked
|
|
3514
|
+
// Codex rollout replays (see CODEX_FORK_REPLAY_REPAIR_KEY). Delegates to the
|
|
3515
|
+
// #187 guarded rebuild with the fork-repair key.
|
|
3516
|
+
async function repairCodexForkReplayInflation({
|
|
3517
|
+
cursors,
|
|
3518
|
+
queuePath,
|
|
3519
|
+
queueStatePath,
|
|
3520
|
+
projectQueuePath,
|
|
3521
|
+
projectQueueStatePath,
|
|
3522
|
+
rolloutFiles,
|
|
3523
|
+
// True when the #187 repair ran its rebuild earlier in THIS sync: that
|
|
3524
|
+
// rebuild already used the fork-aware parser, so the history is clean and a
|
|
3525
|
+
// second rebuild would be pure waste.
|
|
3526
|
+
legacyRepairRan = false,
|
|
3527
|
+
}) {
|
|
3528
|
+
if (!cursors || typeof cursors !== "object") return false;
|
|
3529
|
+
const migrations = (cursors.migrations ||= {});
|
|
3530
|
+
// Same completed-vs-skipped semantics as the #187 repair: an ISO string is
|
|
3531
|
+
// final, a {skipped:true} sentinel retries (the skip condition can clear).
|
|
3532
|
+
const prior = migrations[CODEX_FORK_REPLAY_REPAIR_KEY];
|
|
3533
|
+
if (prior && !(typeof prior === "object" && prior.skipped)) return false;
|
|
3534
|
+
|
|
3535
|
+
if (legacyRepairRan) {
|
|
3536
|
+
migrations[CODEX_FORK_REPLAY_REPAIR_KEY] = new Date().toISOString();
|
|
3537
|
+
return false;
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
// Pre-gate: fork phantom can only exist on installs that have (or had) a
|
|
3541
|
+
// forked rollout. Scanning file heads is cheap; the rebuild re-parses every
|
|
3542
|
+
// codex file. Files forked AFTER the parser fix shipped never accrue
|
|
3543
|
+
// phantom, so a definitive "no forks" verdict may finalize — but ONLY when
|
|
3544
|
+
// this scan can actually stand for the install's whole codex history:
|
|
3545
|
+
// 1. every candidate head was readable (an unreadable candidate could be
|
|
3546
|
+
// the one forked rollout), AND
|
|
3547
|
+
// 2. every codex session recorded in cursors is covered by THIS scan's
|
|
3548
|
+
// file list (same UUID-keyed reproducibility rule as the #187 guard).
|
|
3549
|
+
// A forked rollout can be real yet absent from one run's rolloutFiles —
|
|
3550
|
+
// WSL/native split probing, a temporarily missing ~/.codex, a deleted
|
|
3551
|
+
// file whose phantom is already in history — and finalizing off such a
|
|
3552
|
+
// partial view would permanently foreclose the repair this migration
|
|
3553
|
+
// exists to perform, while the guarded repair itself would only have
|
|
3554
|
+
// deferred retryably.
|
|
3555
|
+
// Either failure defers with the retryable {skipped:true} sentinel.
|
|
3556
|
+
const forkScan = await scanForForkedCodexRollout(rolloutFiles);
|
|
3557
|
+
if (!forkScan.forked) {
|
|
3558
|
+
if (forkScan.indeterminate) {
|
|
3559
|
+
migrations[CODEX_FORK_REPLAY_REPAIR_KEY] = {
|
|
3560
|
+
skipped: true,
|
|
3561
|
+
reason: "fork_scan_indeterminate",
|
|
3562
|
+
at: new Date().toISOString(),
|
|
3563
|
+
};
|
|
3564
|
+
return false;
|
|
3565
|
+
}
|
|
3566
|
+
const scannedSessionIds = new Set();
|
|
3567
|
+
const scannedPaths = new Set();
|
|
3568
|
+
for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
|
|
3569
|
+
const fp = typeof entry === "string" ? entry : entry?.path;
|
|
3570
|
+
const src = typeof entry === "string" ? "codex" : String(entry?.source || "codex");
|
|
3571
|
+
if (!fp || src !== "codex") continue;
|
|
3572
|
+
scannedPaths.add(fp);
|
|
3573
|
+
const id = codexSessionIdFromPath(fp);
|
|
3574
|
+
if (id) scannedSessionIds.add(id);
|
|
3575
|
+
}
|
|
3576
|
+
if (cursors.files && typeof cursors.files === "object") {
|
|
3577
|
+
for (const fp of Object.keys(cursors.files)) {
|
|
3578
|
+
if (!isCodexSessionCursorPath(fp)) continue;
|
|
3579
|
+
if (scannedPaths.has(fp)) continue;
|
|
3580
|
+
const id = codexSessionIdFromPath(fp);
|
|
3581
|
+
if (id && scannedSessionIds.has(id)) continue;
|
|
3582
|
+
migrations[CODEX_FORK_REPLAY_REPAIR_KEY] = {
|
|
3583
|
+
skipped: true,
|
|
3584
|
+
reason: "codex_history_not_covered",
|
|
3585
|
+
at: new Date().toISOString(),
|
|
3586
|
+
};
|
|
3587
|
+
return false;
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
migrations[CODEX_FORK_REPLAY_REPAIR_KEY] = new Date().toISOString();
|
|
3591
|
+
return false;
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
return repairCodexRescanInflation({
|
|
3595
|
+
cursors,
|
|
3596
|
+
queuePath,
|
|
3597
|
+
queueStatePath,
|
|
3598
|
+
projectQueuePath,
|
|
3599
|
+
projectQueueStatePath,
|
|
3600
|
+
rolloutFiles,
|
|
3601
|
+
migrationKey: CODEX_FORK_REPLAY_REPAIR_KEY,
|
|
3602
|
+
uploadNote: "reset_after_codex_fork_replay_2026_07",
|
|
3603
|
+
});
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
// Scans codex rollout heads for the fork marker. The child session_meta (with
|
|
3607
|
+
// forked_from_id) is the FIRST line of a forked rollout — observed at byte
|
|
3608
|
+
// offset ≤169 across every local sample — so a bounded head read is
|
|
3609
|
+
// sufficient. 64KB leaves ample margin for long base_instructions preceding
|
|
3610
|
+
// it; a substring false positive (the marker quoted in unrelated head content)
|
|
3611
|
+
// only costs one redundant guarded rebuild, never data. Tri-state result:
|
|
3612
|
+
// {forked:true} on a hit, {forked:false, indeterminate:false} only when every
|
|
3613
|
+
// candidate was inspected, {forked:false, indeterminate:true} when any head
|
|
3614
|
+
// read failed — the caller must not finalize "no forks" off a failed read.
|
|
3615
|
+
async function scanForForkedCodexRollout(rolloutFiles) {
|
|
3616
|
+
const HEAD_BYTES = 65536;
|
|
3617
|
+
let indeterminate = false;
|
|
3618
|
+
for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
|
|
3619
|
+
const fp = typeof entry === "string" ? entry : entry?.path;
|
|
3620
|
+
const src = typeof entry === "string" ? "codex" : String(entry?.source || "codex");
|
|
3621
|
+
if (!fp || src !== "codex") continue;
|
|
3622
|
+
let fh = null;
|
|
3623
|
+
try {
|
|
3624
|
+
fh = await fs.open(fp, "r");
|
|
3625
|
+
const buf = Buffer.alloc(HEAD_BYTES);
|
|
3626
|
+
const { bytesRead } = await fh.read(buf, 0, HEAD_BYTES, 0);
|
|
3627
|
+
if (buf.toString("utf8", 0, bytesRead).includes('"forked_from_id"')) {
|
|
3628
|
+
return { forked: true, indeterminate: false };
|
|
3629
|
+
}
|
|
3630
|
+
} catch (_e) {
|
|
3631
|
+
indeterminate = true;
|
|
3632
|
+
} finally {
|
|
3633
|
+
if (fh) await fh.close().catch(() => {});
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
return { forked: false, indeterminate };
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3476
3639
|
function isCodexSessionCursorPath(filePath) {
|
|
3477
3640
|
if (typeof filePath !== "string") return false;
|
|
3478
3641
|
const normalized = filePath.replace(/\\/g, "/");
|
|
@@ -3,7 +3,12 @@ const path = require("node:path");
|
|
|
3
3
|
const fs = require("node:fs/promises");
|
|
4
4
|
|
|
5
5
|
const { restoreCodexNotify, restoreEveryCodeNotify } = require("../lib/codex-config");
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
removeClaudeHook,
|
|
8
|
+
removeClaudeUsageHooks,
|
|
9
|
+
buildClaudeHookCommand,
|
|
10
|
+
buildHookCommand,
|
|
11
|
+
} = require("../lib/claude-config");
|
|
7
12
|
const {
|
|
8
13
|
resolveGeminiConfigDir,
|
|
9
14
|
resolveGeminiSettingsPath,
|
|
@@ -64,7 +69,10 @@ async function cmdUninstall(argv) {
|
|
|
64
69
|
})
|
|
65
70
|
: { restored: false, skippedReason: "config-missing" };
|
|
66
71
|
const claudeRemove = claudeConfigExists
|
|
67
|
-
? await
|
|
72
|
+
? await removeClaudeUsageHooks({
|
|
73
|
+
settingsPath: claudeSettingsPath,
|
|
74
|
+
hookCommand: claudeHookCommand,
|
|
75
|
+
})
|
|
68
76
|
: { removed: false, skippedReason: "config-missing" };
|
|
69
77
|
const codebuddyRemove = codebuddyConfigExists
|
|
70
78
|
? await removeClaudeHook({
|