tokentracker-cli 1.3.8 → 1.3.10
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 +7 -6
- package/README.ja.md +6 -5
- package/README.ko.md +6 -5
- package/README.md +7 -6
- package/README.zh-CN.md +6 -5
- package/dashboard/dist/assets/{AchievementBadge-BqLeP5y3.js → AchievementBadge-vU4Dwu_p.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-BwVkM45R.js → AchievementsPage-C8LspyPK.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-CglgOcQ1.js → AchievementsSection-CP8J07fV.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-IurF0cmo.js → BadgeDetailModal-Ci1egHvR.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-C4_NxPuy.js → BotAnimated-CMXILM_h.js} +1 -1
- package/dashboard/dist/assets/{Card-C-UuIN42.js → Card-BHHCCnqR.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-CrMDquLi.js → CloudCheckoutPage-BY9Hsbix.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-D6xTPnMs.js → CloudPage-Y6JgOXcJ.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-8LFLY-LQ.js → CommandPalette-BRWQFJwy.js} +1 -1
- package/dashboard/dist/assets/CommunityStatsModal-krT4ZZX9.js +1 -0
- package/dashboard/dist/assets/{ConfirmModal-St89kP59.js → ConfirmModal-ZvPOJ0he.js} +1 -1
- package/dashboard/dist/assets/Controls-CQlxw03i.js +1 -0
- package/dashboard/dist/assets/{DashboardPage-BvGqFvP7.js → DashboardPage-Dbi0d2JY.js} +1 -1
- package/dashboard/dist/assets/{DateRangePopover-Cbd1idiF.js → DateRangePopover-DJ8RpQfV.js} +1 -1
- package/dashboard/dist/assets/DevicePage-C4_cbw3M.js +1 -0
- package/dashboard/dist/assets/{DialogClose-Cb1-Hd8P.js → DialogClose-Dx5AqmUG.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-LvhfT1Ox.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-VXrHY7H0.js → DialogTitle-Cro2S8s8.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-DmQqYnSD.js → ElectricLogo-gqnBCtSG.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-BEieTXMD.js → FadeIn-B9oATcDZ.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-BlYC_PSb.js → HeaderGithubStar-CYa94wna.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-C5Pe7iBz.js → HoverTooltip-BJ4qLyuS.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Diln3EdG.js → IpCheckPage-Clpq3jdY.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-BzGYuAO7.js → LandingPage-CuazuOAo.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-Bn-ehU5S.js → LeaderboardPage-BSF1Lbtq.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-SovdGwYs.js → LeaderboardProBadge-K32gY52s.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-BhxmTcHh.js → LeaderboardProfileModal-D5gpn7pP.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-3iAYvoTb.js → LeaderboardProfilePage-BmO9nIi-.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-DHxFAdqK.js → LimitsPage-D9ZJ1TeK.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-HlGfiiI3.js → LocalOnlyNotice-BkzysTS7.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-CamUYmiF.js → LoginCard-Do3SNJFU.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-CcmYUJ45.js → LoginModal-hph1i_YK.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-DMNNmZwr.js → LoginPage-DXYAaFjJ.js} +1 -1
- package/dashboard/dist/assets/{PetPage-D6xFX8QA.js → PetPage-D1Pk4OKI.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-CKKgItAT.js → PopoverPopup-CAIuP_a5.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-B-uS3pJ3.js → ProviderIcon-R6Bsj81X.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DX4RJjX0.js → ResetPasswordPage-BR3GTF-3.js} +1 -1
- package/dashboard/dist/assets/{Select-Dcg7FYAt.js → Select-38Dh-OWv.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-4vuj6xMs.js → SelectItemText-90n7jKbI.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-ButVBgE7.js → SelfHostPage-BKVYkHI6.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-DJDDO6lV.js → ServiceStatusPage-PBmBn29J.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-CiUirFMD.js → SessionsPage-58MfDHhe.js} +3 -3
- package/dashboard/dist/assets/SettingsPage-BNLHbZUQ.js +5 -0
- package/dashboard/dist/assets/{SkillsPage-D2tfFFY8.js → SkillsPage-DMMTk4jD.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-TQ-eWK8P.js → TokenGalaxy-BdPETjru.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-BhSCJqFR.js → TooltipPopup-BXl-vM4L.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-CP6ZZ53c.js → TrendMonitor-wPfTlX-7.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-BdeR7P8D.js → WidgetsPage-CuwRMWka.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-OqcqupRE.js → WrappedPage-DOpA9kkg.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-BXwSXUpN.js → __vite-browser-external-Go6WKt6Z.js} +3 -3
- package/dashboard/dist/assets/__vite-browser-external-kAUz3kFs.css +1 -0
- package/dashboard/dist/assets/{agent-logos-E6GfkcgD.js → agent-logos-D7eMvAlk.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-D_ufthLH.js → arrow-left--eOVP_Dk.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-CgV_OrFz.js → arrow-right-DJYhwBMz.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CzeEeXRz.js → arrow-up-right-DhDvpd-S.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-BQdTF7ec.js → calendar-days-BuOFlNcJ.js} +1 -1
- package/dashboard/dist/assets/{check-C--D8Lze.js → check-UztXSVYi.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-BzkceD4z.js → chevron-down-CbJ2QGDU.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-m1Yllbij.js → cloud-checkout-CoRQjKEu.js} +1 -1
- package/dashboard/dist/assets/{composite-7DhFi6TA.js → composite-PlKvZfdM.js} +1 -1
- package/dashboard/dist/assets/{download-BSyGLtfW.js → download-McE9gFto.js} +1 -1
- package/dashboard/dist/assets/{external-link-KzFbueBT.js → external-link-DZWnR9gS.js} +1 -1
- package/dashboard/dist/assets/{flame-DlPM_oaj.js → flame-0xjueXt4.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-BHBaWyA3.js → format-tokens-CZoG68uJ.js} +1 -1
- package/dashboard/dist/assets/{gift-BhWBQnun.js → gift-pKc80Nqb.js} +1 -1
- package/dashboard/dist/assets/{host-mode--iIPGrkN.js → host-mode-99_v1j5J.js} +1 -1
- package/dashboard/dist/assets/{icons-COEaSBYi.js → icons-BtfYKSGC.js} +1 -1
- package/dashboard/dist/assets/{index-Cf4HcmLp.js → index-Cgthm7BO.js} +1 -1
- package/dashboard/dist/assets/{index-Bo8CSxCq.js → index-CyZxN1O0.js} +1 -1
- package/dashboard/dist/assets/{index-CW6A_Ff_.js → index-DuaQmNZo.js} +1 -1
- package/dashboard/dist/assets/{info-D1gOdsM_.js → info-C8vkxwsO.js} +1 -1
- package/dashboard/dist/assets/{link-2-CXDnDOK4.js → link-2-CQzIW5b6.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-DWeQPecG.js → loader-circle-B9GqzQyC.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-BljqMT7V.js → modifiers.esm-ChkCUrZp.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-DpkdtOjk.js → monitor-smartphone-DQjBY1W3.js} +1 -1
- package/dashboard/dist/assets/{play-BlGNrekI.js → play-qC6xc6fe.js} +1 -1
- package/dashboard/dist/assets/{plus-CpYVQ_t-.js → plus-CoNHx5Og.js} +1 -1
- package/dashboard/dist/assets/{provider-display-D6Blvktj.js → provider-display-x2EG6zAY.js} +1 -1
- package/dashboard/dist/assets/{react-BMJ2wuz1.js → react-EctmAw3f.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-BFHXICk1.js → safePolygon-DlSbLtZC.js} +1 -1
- package/dashboard/dist/assets/{search-0NxXkytY.js → search-CBcpAj9I.js} +1 -1
- package/dashboard/dist/assets/{server-GHkrUKSI.js → server-DtMUoEq0.js} +1 -1
- package/dashboard/dist/assets/{shield-check-C16cbx1z.js → shield-check-ByUPtUmu.js} +1 -1
- package/dashboard/dist/assets/{skills-api-DAm1fFB4.js → skills-api-BD9VNRej.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-CkOax8h0.js → sortable.esm-B6qEViGJ.js} +1 -1
- package/dashboard/dist/assets/{store-DR444-5P.js → store-BgW2uBRe.js} +1 -1
- package/dashboard/dist/assets/{terminal-BpvNaIz9.js → terminal-DWPNkr2L.js} +1 -1
- package/dashboard/dist/assets/{trash-DX8LwOmN.js → trash-DdbRpgD1.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-BFW4-DYG.js → use-community-stats-43ySv9uM.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-Cpcl08EJ.js → use-latest-request-guard-BuezAv4k.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-BqixyeAa.js → use-limits-display-prefs-Yi0Hznar.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-BSx7lGEK.js → use-native-settings-BaSrhrM8.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-UCjB0_zK.js → use-ordered-list-CBD-EIBf.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-ObUuoXUE.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-BgOJaGEy.js → use-session-efficiency-pref-CwaFIkE5.js} +1 -1
- package/dashboard/dist/assets/{use-transform-Y98QifnQ.js → use-transform-Dea4uz8K.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-DJGO-Kxv.js → use-usage-limits-DxkYcUIR.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-DL458pqS.js → useCurrency-CcZktkbA.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-DnCQwlVT.js → usePositioner-BLGIGihf.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-CYLt9pgs.js → useTokenFormat--dhm3nNf.js} +1 -1
- package/dashboard/dist/assets/{zap-BW6E_Jv3.js → zap-CWTmP6Zw.js} +1 -1
- package/dashboard/dist/index.html +14 -12
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/llms.txt +3 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +2 -2
- package/src/commands/init.js +9 -0
- package/src/commands/status.js +11 -0
- package/src/commands/sync.js +197 -122
- package/src/lib/dumate.js +95 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/qoder-cn-cli-limits.js +117 -0
- package/src/lib/qoder-limits.js +24 -11
- package/src/lib/rollout.js +12 -9
- package/src/lib/session-analytics.js +210 -19
- package/src/lib/upload-queue.js +134 -0
- package/dashboard/dist/assets/CommunityStatsModal-DYTjxABN.js +0 -1
- package/dashboard/dist/assets/Controls-BJAwM2gB.js +0 -1
- package/dashboard/dist/assets/DevicePage-BcSU0xgf.js +0 -1
- package/dashboard/dist/assets/DialogDescription-DQvYzWMp.js +0 -1
- package/dashboard/dist/assets/SettingsPage-CB4CML8u.js +0 -5
- package/dashboard/dist/assets/__vite-browser-external-DvNSoTb8.css +0 -1
- package/dashboard/dist/assets/use-reduced-motion-C6zOHbfR.js +0 -1
package/src/commands/sync.js
CHANGED
|
@@ -8,6 +8,7 @@ const { functionUrlFor, fetchFunctionResponse } = require("../lib/function-url")
|
|
|
8
8
|
|
|
9
9
|
const { resolveInstallPaths, resolveZcodeNativeDbPath, resolveMimoNativeDbPath, ensureFlatCursor } = require("../lib/install-resolver");
|
|
10
10
|
const { multiInstallParse, mergeBothFileSources } = require("../lib/multi-install-parser");
|
|
11
|
+
const { resolveDumateDbPaths, parseDumateIncremental } = require("../lib/dumate");
|
|
11
12
|
const wsl = require("../lib/wsl-probe");
|
|
12
13
|
const {
|
|
13
14
|
ensureDir,
|
|
@@ -155,12 +156,14 @@ const {
|
|
|
155
156
|
recordUploadSuccess,
|
|
156
157
|
parseRetryAfterMs,
|
|
157
158
|
} = require("../lib/upload-throttle");
|
|
159
|
+
const { compactPendingQueue, iterateQueueLines, sessionSnapshotMs } = require("../lib/upload-queue");
|
|
158
160
|
const AUTO_UPLOAD_CONFIG = {
|
|
159
161
|
intervalMs: 15 * 60_000,
|
|
160
162
|
batchSize: 200,
|
|
161
163
|
maxBatchesSmall: 5,
|
|
162
|
-
maxBatchesLarge:
|
|
164
|
+
maxBatchesLarge: 100,
|
|
163
165
|
};
|
|
166
|
+
const UPLOAD_BUDGET_MS = 90_000;
|
|
164
167
|
const { maybeSendHeartbeat } = require("../lib/telemetry");
|
|
165
168
|
const {
|
|
166
169
|
isCursorInstalled,
|
|
@@ -327,6 +330,7 @@ const AUTO_SYNC_SOURCES = new Set([
|
|
|
327
330
|
"cursor",
|
|
328
331
|
"devin",
|
|
329
332
|
"droid",
|
|
333
|
+
"dumate",
|
|
330
334
|
"dsh",
|
|
331
335
|
"every-code",
|
|
332
336
|
"gemini",
|
|
@@ -1686,6 +1690,20 @@ async function cmdSync(argv, context = {}) {
|
|
|
1686
1690
|
}
|
|
1687
1691
|
}
|
|
1688
1692
|
|
|
1693
|
+
let dumateResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
1694
|
+
if (sourceAllowed("dumate")) {
|
|
1695
|
+
const dumateDbPaths = resolveDumateDbPaths({ home });
|
|
1696
|
+
if (dumateDbPaths.length > 0) {
|
|
1697
|
+
if (progress?.enabled) progress.start(`Parsing DuMate ${renderBar(0)} | buckets 0`);
|
|
1698
|
+
try {
|
|
1699
|
+
dumateResult = await parseDumateIncremental({
|
|
1700
|
+
dbPaths: dumateDbPaths, cursors, queuePath, projectQueuePath,
|
|
1701
|
+
onProgress: makeProviderProgress("DuMate"),
|
|
1702
|
+
});
|
|
1703
|
+
} catch (err) { warnProviderParseFailure("DuMate", err, opts); }
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1689
1707
|
// ── Mimo (mimocode — OpenCode-fork SQLite) ──
|
|
1690
1708
|
// readMimoDbMessages filters out mirrored Claude Code rows to avoid
|
|
1691
1709
|
// double-counting usage already counted as source=claude.
|
|
@@ -3319,6 +3337,7 @@ async function cmdSync(argv, context = {}) {
|
|
|
3319
3337
|
anythingllmResult.recordsProcessed +
|
|
3320
3338
|
devinResult.recordsProcessed +
|
|
3321
3339
|
kiloResult.recordsProcessed +
|
|
3340
|
+
dumateResult.recordsProcessed +
|
|
3322
3341
|
mimoResult.recordsProcessed +
|
|
3323
3342
|
zcodeResult.recordsProcessed +
|
|
3324
3343
|
kilocodeResult.recordsProcessed +
|
|
@@ -3364,6 +3383,7 @@ async function cmdSync(argv, context = {}) {
|
|
|
3364
3383
|
anythingllmResult.bucketsQueued +
|
|
3365
3384
|
devinResult.bucketsQueued +
|
|
3366
3385
|
kiloResult.bucketsQueued +
|
|
3386
|
+
dumateResult.bucketsQueued +
|
|
3367
3387
|
mimoResult.bucketsQueued +
|
|
3368
3388
|
zcodeResult.bucketsQueued +
|
|
3369
3389
|
kilocodeResult.bucketsQueued +
|
|
@@ -3490,6 +3510,10 @@ async function cmdSync(argv, context = {}) {
|
|
|
3490
3510
|
if (canUpload() && runtime.deviceToken && runtime.baseUrl &&
|
|
3491
3511
|
(!isBackgroundLightweightSync || opts.publishAccount) &&
|
|
3492
3512
|
(!autoUploadDecision || autoUploadDecision.allowed)) {
|
|
3513
|
+
// Decide the window budget from the original backlog. Compact only once
|
|
3514
|
+
// upload is allowed so a cooldown run cannot shrink away that budget.
|
|
3515
|
+
const state = (await readJson(queueStatePath)) || { offset: 0 };
|
|
3516
|
+
await compactPendingQueue({ queuePath, offset: Number(state.offset || 0) });
|
|
3493
3517
|
uploadAttempted = true;
|
|
3494
3518
|
// Mirror the machine identity into the purge-surviving seed file so a
|
|
3495
3519
|
// future `uninstall --purge` + reinstall recovers the same cloud device
|
|
@@ -3511,6 +3535,7 @@ async function cmdSync(argv, context = {}) {
|
|
|
3511
3535
|
queueStatePath,
|
|
3512
3536
|
maxBatches: opts.drain ? 100 : (autoUploadDecision?.maxBatches || 5),
|
|
3513
3537
|
batchSize: autoUploadDecision?.batchSize || 200,
|
|
3538
|
+
maxDurationMs: UPLOAD_BUDGET_MS,
|
|
3514
3539
|
canUpload,
|
|
3515
3540
|
});
|
|
3516
3541
|
try {
|
|
@@ -3570,6 +3595,14 @@ async function cmdSync(argv, context = {}) {
|
|
|
3570
3595
|
await writeJson(uploadThrottlePath, uploadThrottleState);
|
|
3571
3596
|
}
|
|
3572
3597
|
} catch (e) {
|
|
3598
|
+
if (e.uploadResult?.batches > 0) {
|
|
3599
|
+
uploadResult = e.uploadResult;
|
|
3600
|
+
uploadThrottleState = recordUploadSuccess({
|
|
3601
|
+
nowMs: Date.now(), state: uploadThrottleState, config: AUTO_UPLOAD_CONFIG,
|
|
3602
|
+
cloudAccess: uploadResult.cloudAccess,
|
|
3603
|
+
attemptId: process.env.TOKENTRACKER_LOCAL_SYNC_ATTEMPT_ID,
|
|
3604
|
+
});
|
|
3605
|
+
}
|
|
3573
3606
|
// Persist a backoff on 429 / 5xx so the next auto-sync waits instead
|
|
3574
3607
|
// of retrying immediately and making the rate-limit worse. The
|
|
3575
3608
|
// throttle module already parses Retry-After when we surface it on
|
|
@@ -4252,7 +4285,7 @@ const AUTO_RETRY_MAX_DELAY_MS = 2 * 60 * 60 * 1000;
|
|
|
4252
4285
|
const INGEST_SLUG = "tokentracker-ingest";
|
|
4253
4286
|
const MAX_INGEST_BUCKETS = 500;
|
|
4254
4287
|
|
|
4255
|
-
async function drainQueueToCloud({ baseUrl, anonKey, deviceToken, queuePath, queueStatePath, maxBatches = 5, batchSize = 200, canUpload = () => true }) {
|
|
4288
|
+
async function drainQueueToCloud({ baseUrl, anonKey, deviceToken, queuePath, queueStatePath, maxBatches = 5, batchSize = 200, maxDurationMs = UPLOAD_BUDGET_MS, canUpload = () => true }) {
|
|
4256
4289
|
const state = (await readJson(queueStatePath)) || { offset: 0 };
|
|
4257
4290
|
let offset = Number(state.offset || 0);
|
|
4258
4291
|
let inserted = 0;
|
|
@@ -4260,84 +4293,113 @@ async function drainQueueToCloud({ baseUrl, anonKey, deviceToken, queuePath, que
|
|
|
4260
4293
|
let batches = 0;
|
|
4261
4294
|
let cloudAccess = null;
|
|
4262
4295
|
const uploadId = crypto.randomUUID();
|
|
4296
|
+
const deadline = Date.now() + maxDurationMs;
|
|
4297
|
+
let batchBudget = Math.min(maxBatches, 100);
|
|
4298
|
+
let requestSignal = null;
|
|
4299
|
+
const resultSoFar = () => ({ inserted, skipped, batches, ...(cloudAccess?.next_allowed_at ? { cloudAccess } : {}) });
|
|
4263
4300
|
|
|
4264
4301
|
const queueSize = await safeStatSize(queuePath);
|
|
4265
4302
|
const limit = Math.min(Math.max(1, Math.floor(Number(batchSize || 200))), MAX_INGEST_BUCKETS);
|
|
4266
4303
|
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
headers,
|
|
4287
|
-
body: JSON.stringify({
|
|
4288
|
-
upload_id: uploadId,
|
|
4289
|
-
hourly: result.buckets,
|
|
4290
|
-
...(result.sessionStates.length > 0
|
|
4291
|
-
? { account_session_states: result.sessionStates }
|
|
4292
|
-
: {}),
|
|
4293
|
-
}),
|
|
4294
|
-
});
|
|
4304
|
+
try {
|
|
4305
|
+
for (let batch = 0; batch < batchBudget; batch++) {
|
|
4306
|
+
if (Date.now() >= deadline) break;
|
|
4307
|
+
if (offset >= queueSize) break;
|
|
4308
|
+
const result = await readQueueBatch(queuePath, offset, limit);
|
|
4309
|
+
// A states-only tail (e.g. a retry batch whose bucket rows already
|
|
4310
|
+
// uploaded) is still a meaningful upload: it advances the canonical
|
|
4311
|
+
// session corrections cloud-side.
|
|
4312
|
+
if (result.buckets.length === 0 && result.sessionStates.length === 0) {
|
|
4313
|
+
// Complete ignored control/malformed rows need no cloud request, but
|
|
4314
|
+
// must not leave a permanent pending tail. Incomplete bytes stay put.
|
|
4315
|
+
if (result.nextOffset > offset) {
|
|
4316
|
+
offset = result.nextOffset;
|
|
4317
|
+
state.offset = offset;
|
|
4318
|
+
state.updatedAt = new Date().toISOString();
|
|
4319
|
+
await writeJson(queueStatePath, state);
|
|
4320
|
+
}
|
|
4321
|
+
break;
|
|
4322
|
+
}
|
|
4295
4323
|
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
const
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
}
|
|
4324
|
+
const root = baseUrl.replace(/\/$/, "");
|
|
4325
|
+
const headers = {
|
|
4326
|
+
"Content-Type": "application/json",
|
|
4327
|
+
Accept: "application/json",
|
|
4328
|
+
Authorization: `Bearer ${deviceToken}`,
|
|
4329
|
+
};
|
|
4330
|
+
if (anonKey) headers.apikey = anonKey;
|
|
4331
|
+
// Re-read after parsing/each batch so switching off stops an active drain.
|
|
4332
|
+
if (!canUpload()) break;
|
|
4333
|
+
const remainingMs = deadline - Date.now();
|
|
4334
|
+
if (remainingMs <= 0) break;
|
|
4335
|
+
requestSignal = AbortSignal.timeout(Math.max(1, Math.ceil(remainingMs)));
|
|
4336
|
+
const res = await fetchFunctionResponse(functionUrlFor(root, INGEST_SLUG), {
|
|
4337
|
+
method: "POST",
|
|
4338
|
+
headers,
|
|
4339
|
+
signal: requestSignal,
|
|
4340
|
+
body: JSON.stringify({
|
|
4341
|
+
upload_id: uploadId,
|
|
4342
|
+
hourly: result.buckets,
|
|
4343
|
+
...(result.sessionStates.length > 0
|
|
4344
|
+
? { account_session_states: result.sessionStates }
|
|
4345
|
+
: {}),
|
|
4346
|
+
}),
|
|
4347
|
+
});
|
|
4321
4348
|
|
|
4322
|
-
|
|
4349
|
+
const rawText = await res.text();
|
|
4350
|
+
let data = {};
|
|
4351
|
+
try { data = JSON.parse(rawText); } catch { data = {}; }
|
|
4352
|
+
if (!res.ok) {
|
|
4353
|
+
const err = new Error(`HTTP ${res.status}: ${rawText.substring(0, 500)}`);
|
|
4354
|
+
err.status = res.status;
|
|
4355
|
+
err.code = res.status === 401 ? "CLOUD_DEVICE_TOKEN_REJECTED"
|
|
4356
|
+
: typeof data?.code === "string" ? data.code
|
|
4357
|
+
: res.status === 403 ? "CLOUD_UPLOAD_FORBIDDEN" : "CLOUD_UPLOAD_FAILED";
|
|
4358
|
+
const retryAfter = res.headers?.get?.("Retry-After") ?? null;
|
|
4359
|
+
const retryAfterMs = parseRetryAfterMs(retryAfter);
|
|
4360
|
+
if (retryAfterMs !== null) err.retryAfterMs = retryAfterMs;
|
|
4361
|
+
throw err;
|
|
4362
|
+
}
|
|
4363
|
+
|
|
4364
|
+
cloudAccess = data;
|
|
4365
|
+
if (data.upload_scope === "community") batchBudget = Math.min(batchBudget, 10);
|
|
4366
|
+
inserted += Number(data?.inserted || 0);
|
|
4367
|
+
skipped += Number(data?.skipped || 0);
|
|
4368
|
+
batches += 1;
|
|
4369
|
+
|
|
4370
|
+
offset = result.nextOffset;
|
|
4371
|
+
state.offset = offset;
|
|
4372
|
+
state.updatedAt = new Date().toISOString();
|
|
4373
|
+
await writeJson(queueStatePath, state);
|
|
4374
|
+
}
|
|
4375
|
+
} catch (error) {
|
|
4376
|
+
// A bounded drain can finish partially without discarding successful
|
|
4377
|
+
// offsets or the server cooldown. Other failures retain their retry path.
|
|
4378
|
+
const requestTimedOut = requestSignal?.aborted && requestSignal.reason?.name === "TimeoutError" &&
|
|
4379
|
+
(error === requestSignal.reason || error.name === "TimeoutError" || error.name === "AbortError");
|
|
4380
|
+
if (!requestTimedOut || batches === 0) {
|
|
4381
|
+
error.uploadResult = resultSoFar();
|
|
4382
|
+
throw error;
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
return resultSoFar();
|
|
4323
4386
|
}
|
|
4324
4387
|
|
|
4325
4388
|
async function readQueueBatch(queuePath, startOffset, maxBuckets) {
|
|
4326
4389
|
const st = await fs.stat(queuePath).catch(() => null);
|
|
4327
|
-
if (!st || !st.isFile()) return { buckets: [], nextOffset: startOffset };
|
|
4328
|
-
if (startOffset >= st.size) return { buckets: [], nextOffset: startOffset };
|
|
4390
|
+
if (!st || !st.isFile()) return { buckets: [], sessionStates: [], nextOffset: startOffset };
|
|
4391
|
+
if (startOffset >= st.size) return { buckets: [], sessionStates: [], nextOffset: startOffset };
|
|
4329
4392
|
|
|
4330
|
-
const
|
|
4331
|
-
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
4393
|
+
const handle = await fs.open(queuePath, "r");
|
|
4332
4394
|
|
|
4333
4395
|
const bucketMap = new Map();
|
|
4334
4396
|
// Account session states (kind: "account_session_state") are queue
|
|
4335
4397
|
// control records, not usage rows: collect them separately so they ride
|
|
4336
4398
|
// the same upload (after the bucket rows in file order) without entering
|
|
4337
|
-
// the bucket stream. Keyed by (source, session_id)
|
|
4338
|
-
//
|
|
4339
|
-
//
|
|
4340
|
-
//
|
|
4399
|
+
// the bucket stream. Keyed by (source, session_id), use the strictly newer
|
|
4400
|
+
// snapshot_verified_at and retain the first row on ties, matching account
|
|
4401
|
+
// truth even after a clock rollback. Only one observation per session may
|
|
4402
|
+
// reach the ingest batch (duplicates would affect one ON CONFLICT row twice).
|
|
4341
4403
|
// snapshot_verified_at rides along verbatim from the ORIGINAL fetch, so a
|
|
4342
4404
|
// transport retry never fakes logical freshness.
|
|
4343
4405
|
//
|
|
@@ -4351,68 +4413,81 @@ async function readQueueBatch(queuePath, startOffset, maxBuckets) {
|
|
|
4351
4413
|
// (and the MAX_INGEST_BUCKETS=500 clamp above), every request stays under
|
|
4352
4414
|
// the edge cap while batching follows the existing design.
|
|
4353
4415
|
const sessionStateMap = new Map();
|
|
4416
|
+
const sessionStateStamps = new Map();
|
|
4354
4417
|
let offset = startOffset;
|
|
4355
4418
|
let linesRead = 0;
|
|
4356
|
-
|
|
4357
|
-
const
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
const ssSource = bucket.source.trim().toLowerCase();
|
|
4369
|
-
sessionStateMap.set(ssSource + "|" + bucket.session_id.trim(), {
|
|
4370
|
-
source: ssSource,
|
|
4371
|
-
session_id: bucket.session_id.trim(),
|
|
4372
|
-
model: typeof bucket.model === "string" ? bucket.model : "",
|
|
4373
|
-
bucket_start: typeof bucket.bucket_start === "string" ? bucket.bucket_start : "",
|
|
4374
|
-
input_tokens: Number(bucket.input_tokens) || 0,
|
|
4375
|
-
output_tokens: Number(bucket.output_tokens) || 0,
|
|
4376
|
-
cached_input_tokens: Number(bucket.cached_input_tokens) || 0,
|
|
4377
|
-
cache_creation_input_tokens: Number(bucket.cache_creation_input_tokens) || 0,
|
|
4378
|
-
reasoning_output_tokens: Number(bucket.reasoning_output_tokens) || 0,
|
|
4379
|
-
total_tokens: Number(bucket.total_tokens) || 0,
|
|
4380
|
-
...(typeof bucket.snapshot_verified_at === "string" && bucket.snapshot_verified_at
|
|
4381
|
-
? { snapshot_verified_at: bucket.snapshot_verified_at }
|
|
4382
|
-
: {}),
|
|
4383
|
-
});
|
|
4384
|
-
// Counted like a bucket row: the record rides THIS batch and its
|
|
4385
|
-
// bytes are covered by nextOffset, so the next batch resumes right
|
|
4386
|
-
// after it.
|
|
4387
|
-
linesRead += 1;
|
|
4388
|
-
if (linesRead >= maxBuckets) break;
|
|
4419
|
+
try {
|
|
4420
|
+
for await (const record of iterateQueueLines(handle, startOffset, st.size)) {
|
|
4421
|
+
// A producer may still be writing the last row; retry it once complete.
|
|
4422
|
+
if (!record.complete) break;
|
|
4423
|
+
const line = record.bytes.toString("utf8");
|
|
4424
|
+
offset += record.bytes.length;
|
|
4425
|
+
if (!line.trim()) continue;
|
|
4426
|
+
let bucket;
|
|
4427
|
+
try {
|
|
4428
|
+
bucket = JSON.parse(line);
|
|
4429
|
+
} catch (_e) {
|
|
4430
|
+
continue;
|
|
4389
4431
|
}
|
|
4390
|
-
|
|
4432
|
+
if (bucket?.kind === "account_session_state") {
|
|
4433
|
+
if (typeof bucket.source === "string" && typeof bucket.session_id === "string" && bucket.session_id.trim()) {
|
|
4434
|
+
const ssSource = bucket.source.trim().toLowerCase();
|
|
4435
|
+
const stateKey = ssSource + "|" + bucket.session_id.trim();
|
|
4436
|
+
const sessionState = {
|
|
4437
|
+
source: ssSource,
|
|
4438
|
+
session_id: bucket.session_id.trim(),
|
|
4439
|
+
model: typeof bucket.model === "string" ? bucket.model : "",
|
|
4440
|
+
bucket_start: typeof bucket.bucket_start === "string" ? bucket.bucket_start : "",
|
|
4441
|
+
input_tokens: Number(bucket.input_tokens) || 0,
|
|
4442
|
+
output_tokens: Number(bucket.output_tokens) || 0,
|
|
4443
|
+
cached_input_tokens: Number(bucket.cached_input_tokens) || 0,
|
|
4444
|
+
cache_creation_input_tokens: Number(bucket.cache_creation_input_tokens) || 0,
|
|
4445
|
+
reasoning_output_tokens: Number(bucket.reasoning_output_tokens) || 0,
|
|
4446
|
+
total_tokens: Number(bucket.total_tokens) || 0,
|
|
4447
|
+
...(typeof bucket.snapshot_verified_at === "string" && bucket.snapshot_verified_at
|
|
4448
|
+
? { snapshot_verified_at: bucket.snapshot_verified_at }
|
|
4449
|
+
: {}),
|
|
4450
|
+
};
|
|
4451
|
+
const priorState = sessionStateMap.get(stateKey);
|
|
4452
|
+
const verifiedMs = sessionSnapshotMs(bucket);
|
|
4453
|
+
const priorVerifiedMs = sessionStateStamps.get(stateKey) ?? null;
|
|
4454
|
+
if (!priorState || (verifiedMs !== null && (priorVerifiedMs === null || verifiedMs > priorVerifiedMs))) {
|
|
4455
|
+
sessionStateMap.set(stateKey, sessionState);
|
|
4456
|
+
sessionStateStamps.set(stateKey, verifiedMs);
|
|
4457
|
+
}
|
|
4458
|
+
// Counted like a bucket row: the record rides THIS batch and its
|
|
4459
|
+
// bytes are covered by nextOffset, so the next batch resumes right
|
|
4460
|
+
// after it.
|
|
4461
|
+
linesRead += 1;
|
|
4462
|
+
if (linesRead >= maxBuckets) break;
|
|
4463
|
+
}
|
|
4464
|
+
continue;
|
|
4465
|
+
}
|
|
4466
|
+
// Pre-release watermark records from this feature branch's dev queues
|
|
4467
|
+
// are obsolete control records: drop them instead of letting them enter
|
|
4468
|
+
// the bucket stream (they carry no hour_start and would be skipped
|
|
4469
|
+
// downstream anyway, but be explicit).
|
|
4470
|
+
if (bucket?.kind === "account_sync_watermark") continue;
|
|
4471
|
+
const hourStart = typeof bucket?.hour_start === "string" ? bucket.hour_start : null;
|
|
4472
|
+
if (!hourStart) continue;
|
|
4473
|
+
const source = (typeof bucket?.source === "string" ? bucket.source.trim().toLowerCase() : "") || "codex";
|
|
4474
|
+
const model = (typeof bucket?.model === "string" ? bucket.model.trim() : "") || "unknown";
|
|
4475
|
+
bucket.source = source;
|
|
4476
|
+
bucket.model = model;
|
|
4477
|
+
// Apply the same legacy-row corrections every local reader applies
|
|
4478
|
+
// (local-api readQueueData / project queue / wrapped aggregator). Without
|
|
4479
|
+
// this the cloud permanently kept the RAW legacy values — e.g. old Codex
|
|
4480
|
+
// rows whose input_tokens still include cached tokens (6-7x inflated) —
|
|
4481
|
+
// while the local dashboard showed corrected numbers.
|
|
4482
|
+
bucket = require("../lib/local-api").normalizeQueueRow(bucket);
|
|
4483
|
+
bucketMap.set(`${source}|${model}|${hourStart}`, bucket);
|
|
4484
|
+
linesRead += 1;
|
|
4485
|
+
if (linesRead >= maxBuckets) break;
|
|
4391
4486
|
}
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
if (bucket?.kind === "account_sync_watermark") continue;
|
|
4397
|
-
const hourStart = typeof bucket?.hour_start === "string" ? bucket.hour_start : null;
|
|
4398
|
-
if (!hourStart) continue;
|
|
4399
|
-
const source = (typeof bucket?.source === "string" ? bucket.source.trim().toLowerCase() : "") || "codex";
|
|
4400
|
-
const model = (typeof bucket?.model === "string" ? bucket.model.trim() : "") || "unknown";
|
|
4401
|
-
bucket.source = source;
|
|
4402
|
-
bucket.model = model;
|
|
4403
|
-
// Apply the same legacy-row corrections every local reader applies
|
|
4404
|
-
// (local-api readQueueData / project queue / wrapped aggregator). Without
|
|
4405
|
-
// this the cloud permanently kept the RAW legacy values — e.g. old Codex
|
|
4406
|
-
// rows whose input_tokens still include cached tokens (6-7x inflated) —
|
|
4407
|
-
// while the local dashboard showed corrected numbers.
|
|
4408
|
-
bucket = require("../lib/local-api").normalizeQueueRow(bucket);
|
|
4409
|
-
bucketMap.set(`${source}|${model}|${hourStart}`, bucket);
|
|
4410
|
-
linesRead += 1;
|
|
4411
|
-
if (linesRead >= maxBuckets) break;
|
|
4412
|
-
}
|
|
4413
|
-
|
|
4414
|
-
rl.close();
|
|
4415
|
-
stream.close?.();
|
|
4487
|
+
|
|
4488
|
+
} finally {
|
|
4489
|
+
await handle.close();
|
|
4490
|
+
}
|
|
4416
4491
|
return {
|
|
4417
4492
|
buckets: Array.from(bucketMap.values()),
|
|
4418
4493
|
sessionStates: Array.from(sessionStateMap.values()),
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const os = require("node:os");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const { expandHome } = require("./scan-roots");
|
|
5
|
+
const { readOpencodeDbMessages, parseOpencodeDbIncremental, normalizeOpencodeTokens } = require("./rollout");
|
|
6
|
+
|
|
7
|
+
function resolveDumateDbPaths({ home = os.homedir(), env = process.env, platform = process.platform } = {}) {
|
|
8
|
+
const override = expandHome(env.TOKENTRACKER_DUMATE_HOME, home);
|
|
9
|
+
const appdata = typeof env.APPDATA === "string" && env.APPDATA.trim()
|
|
10
|
+
? env.APPDATA.trim() : path.join(home, "AppData", "Roaming");
|
|
11
|
+
if (!override && platform !== "win32") return [];
|
|
12
|
+
const root = override || path.join(appdata, "qianfan-desktop-app", "qianfan_desk_xdg");
|
|
13
|
+
// The override accepts the data/opencode directory or an install's XDG root.
|
|
14
|
+
const candidates = [path.join(root, "opencode.db"), path.join(root, "data", "opencode", "opencode.db")];
|
|
15
|
+
try {
|
|
16
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
17
|
+
if (entry.isDirectory()) candidates.push(path.join(root, entry.name, "data", "opencode", "opencode.db"));
|
|
18
|
+
}
|
|
19
|
+
} catch (_e) { }
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
return candidates.sort().filter((candidate) => {
|
|
22
|
+
try {
|
|
23
|
+
if (!fs.statSync(candidate).isFile()) return false;
|
|
24
|
+
const realPath = fs.realpathSync(candidate);
|
|
25
|
+
if (seen.has(realPath)) return false;
|
|
26
|
+
seen.add(realPath);
|
|
27
|
+
return true;
|
|
28
|
+
} catch (_e) { return false; }
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function parseDumateIncremental({ dbPaths, ...options }) {
|
|
33
|
+
// Reinstalls can copy the same sessions into a new install-id directory.
|
|
34
|
+
// Share a message cursor across installations and choose the newest copy
|
|
35
|
+
// before parsing, so stale copies cannot roll back a completed turn.
|
|
36
|
+
const messages = new Map();
|
|
37
|
+
const includeZeroMessage = (message) => {
|
|
38
|
+
const provider = message.data?.providerID || message.data?.model?.providerID;
|
|
39
|
+
return String(provider || "").toLowerCase() === "qianfanpersonalquota";
|
|
40
|
+
};
|
|
41
|
+
for (const dbPath of dbPaths) {
|
|
42
|
+
for (const message of readOpencodeDbMessages(dbPath, {}, { propagateReadFailure: true, includeZeroMessage })) {
|
|
43
|
+
// DuMate 1.0.83 bundles OpenCode's import command without an import
|
|
44
|
+
// marker. Its own runtime uses QianfanPersonalQuota; other providers
|
|
45
|
+
// can be imported history already counted under Claude or OpenCode.
|
|
46
|
+
const provider = message.data?.providerID || message.data?.model?.providerID;
|
|
47
|
+
if (String(provider || "").toLowerCase() !== "qianfanpersonalquota") continue;
|
|
48
|
+
const id = message.data?.id || message.id;
|
|
49
|
+
const session = message.data?.sessionID || message.sessionID || "";
|
|
50
|
+
if (!id) continue;
|
|
51
|
+
const key = `${session}|${id}`;
|
|
52
|
+
if (Number(message.timeUpdated || 0) < Number(options.cursors?.dumate?.messages?.[key]?.sourceUpdatedAt || 0)) continue;
|
|
53
|
+
const previous = messages.get(key);
|
|
54
|
+
if (!previous || Number(message.timeUpdated || 0) > Number(previous.message.timeUpdated || 0)) {
|
|
55
|
+
messages.set(key, { message, dbPath });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const byDatabase = new Map();
|
|
60
|
+
for (const [key, { message, dbPath }] of messages) {
|
|
61
|
+
// DuMate's OpenAI-compatible adapter retains reasoning inside output.
|
|
62
|
+
// The shared parser expects separate columns, so subtract that subset once.
|
|
63
|
+
const usage = normalizeOpencodeTokens(message.data?.tokens);
|
|
64
|
+
// Zero copies participate in latest-copy selection so a stale install
|
|
65
|
+
// cannot resurrect usage. Only a previously counted message needs a
|
|
66
|
+
// queued retraction; a new empty message contributes nothing.
|
|
67
|
+
const previous = options.cursors?.dumate?.messages?.[key];
|
|
68
|
+
if (usage?.total_tokens === 0 && (!previous?.lastTotals || previous.dedupedForkCopy === true)) continue;
|
|
69
|
+
if (!byDatabase.has(dbPath)) byDatabase.set(dbPath, []);
|
|
70
|
+
const normalized = usage ? {
|
|
71
|
+
...message,
|
|
72
|
+
data: { ...message.data, tokens: { ...message.data.tokens,
|
|
73
|
+
output: Math.max(0, usage.output_tokens - usage.reasoning_output_tokens),
|
|
74
|
+
} },
|
|
75
|
+
} : message;
|
|
76
|
+
byDatabase.get(dbPath).push(normalized);
|
|
77
|
+
}
|
|
78
|
+
const result = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
79
|
+
for (const [dbPath, dbMessages] of byDatabase) {
|
|
80
|
+
const parsed = await parseOpencodeDbIncremental({
|
|
81
|
+
...options, dbPath, dbMessages, source: "dumate", cursorKey: "dumate",
|
|
82
|
+
});
|
|
83
|
+
for (const message of dbMessages) {
|
|
84
|
+
const key = `${message.data?.sessionID || message.sessionID || ""}|${message.data?.id || message.id}`;
|
|
85
|
+
const cursor = options.cursors?.dumate?.messages?.[key];
|
|
86
|
+
if (cursor) cursor.sourceUpdatedAt = Number(message.timeUpdated || 0);
|
|
87
|
+
}
|
|
88
|
+
result.recordsProcessed += parsed.messagesProcessed || 0;
|
|
89
|
+
result.eventsAggregated += parsed.eventsAggregated || 0;
|
|
90
|
+
result.bucketsQueued += parsed.bucketsQueued || 0;
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { resolveDumateDbPaths, parseDumateIncremental };
|