tokentracker-cli 0.72.1 → 0.74.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.
Files changed (64) hide show
  1. package/README.de.md +5 -4
  2. package/README.ja.md +5 -4
  3. package/README.ko.md +5 -4
  4. package/README.md +5 -4
  5. package/README.zh-CN.md +5 -4
  6. package/dashboard/dist/assets/{ActivityHeatmap-CtDL5OPO.js → ActivityHeatmap-DkoSqxrd.js} +1 -1
  7. package/dashboard/dist/assets/{Card-C5lKqKNT.js → Card-DleS-r54.js} +1 -1
  8. package/dashboard/dist/assets/{CommunityStatsModal-XnEef7Ra.js → CommunityStatsModal-DV41-F9D.js} +1 -1
  9. package/dashboard/dist/assets/{ConfirmModal-CkZvn-sV.js → ConfirmModal-Dgp07Uka.js} +1 -1
  10. package/dashboard/dist/assets/{DashboardPage-TmZBLaB9.js → DashboardPage-EKy2BzUx.js} +1 -1
  11. package/dashboard/dist/assets/{DevicePage-BhDh3VIP.js → DevicePage-eJInQ2UX.js} +1 -1
  12. package/dashboard/dist/assets/{DialogClose-CddI1gcT.js → DialogClose-DEfesqFq.js} +1 -1
  13. package/dashboard/dist/assets/DialogDescription-D6txiEc4.js +1 -0
  14. package/dashboard/dist/assets/{DialogTitle-Cvpx9Lfv.js → DialogTitle-v1JrTvr4.js} +1 -1
  15. package/dashboard/dist/assets/{FadeIn-BojLJcFD.js → FadeIn-hqIBbKaJ.js} +1 -1
  16. package/dashboard/dist/assets/{HeaderGithubStar-BrSt1RIj.js → HeaderGithubStar-C4zauuP2.js} +1 -1
  17. package/dashboard/dist/assets/{IpCheckPage-DxuF-1tF.js → IpCheckPage-CiRcccks.js} +1 -1
  18. package/dashboard/dist/assets/{LandingPage-DmCjJVwh.js → LandingPage-CbOHycnd.js} +1 -1
  19. package/dashboard/dist/assets/{LeaderboardAvatar-B8SGUidW.js → LeaderboardAvatar-CQL1iOTS.js} +1 -1
  20. package/dashboard/dist/assets/{LeaderboardPage-D7CQCdDr.js → LeaderboardPage-BuEp247d.js} +2 -2
  21. package/dashboard/dist/assets/{LeaderboardProfileModal-Dv7CQVqB.js → LeaderboardProfileModal-DT8hrPkI.js} +1 -1
  22. package/dashboard/dist/assets/{LeaderboardProfilePage-BtGLbQnT.js → LeaderboardProfilePage-B-M0AXMb.js} +1 -1
  23. package/dashboard/dist/assets/{LimitsPage-DRf6h5_2.js → LimitsPage-Df2es3Xs.js} +1 -1
  24. package/dashboard/dist/assets/{LocalOnlyNotice-BnlODmxA.js → LocalOnlyNotice-DJIfKTKo.js} +1 -1
  25. package/dashboard/dist/assets/{LoginPage-CYXdV0X0.js → LoginPage-DtaaIy_8.js} +1 -1
  26. package/dashboard/dist/assets/{PopoverPopup-x5vYN3uF.js → PopoverPopup-CqWH6aB9.js} +1 -1
  27. package/dashboard/dist/assets/{ResetPasswordPage-D8ifLVwK.js → ResetPasswordPage-DCH5GU1c.js} +1 -1
  28. package/dashboard/dist/assets/{Select-CaaIuo5M.js → Select-BwyliVKJ.js} +1 -1
  29. package/dashboard/dist/assets/{SelectItemText-CdJ-E-d1.js → SelectItemText-B5_DKKEv.js} +1 -1
  30. package/dashboard/dist/assets/{SettingsPage-Dc7v5PEC.js → SettingsPage-BoAFdOUa.js} +1 -1
  31. package/dashboard/dist/assets/{SkillsPage-CRbEMatQ.js → SkillsPage-BVdre-kF.js} +1 -1
  32. package/dashboard/dist/assets/{WidgetsPage-xgy8mLMV.js → WidgetsPage-CtjV405P.js} +1 -1
  33. package/dashboard/dist/assets/{WrappedPage-BdpfOWJu.js → WrappedPage-DGmPdtHt.js} +1 -1
  34. package/dashboard/dist/assets/{agent-logos-CrxAAMko.js → agent-logos-DqevdWvx.js} +1 -1
  35. package/dashboard/dist/assets/{arrow-up-right-D23bZC75.js → arrow-up-right-DA5Pybq1.js} +1 -1
  36. package/dashboard/dist/assets/check-BWzAT1oX.js +1 -0
  37. package/dashboard/dist/assets/{copy-hiVk9JEl.js → copy-CsQRTd89.js} +1 -1
  38. package/dashboard/dist/assets/{download-CqLiBDG-.js → download-TEHkAKT0.js} +1 -1
  39. package/dashboard/dist/assets/{format-tokens-Ck5lRlFd.js → format-tokens-CtFO_jkz.js} +1 -1
  40. package/dashboard/dist/assets/{info-CxDHBJXB.js → info-Dxn3fiaX.js} +1 -1
  41. package/dashboard/dist/assets/main-Dxi4sSXf.js +1128 -0
  42. package/dashboard/dist/assets/{use-community-stats-CeaLHlCk.js → use-community-stats-BLNxr-bQ.js} +1 -1
  43. package/dashboard/dist/assets/{use-limits-display-prefs-CvDWYfjp.js → use-limits-display-prefs-CC7gb303.js} +1 -1
  44. package/dashboard/dist/assets/{use-native-settings-HuxDwmn6.js → use-native-settings-SYtjLlxZ.js} +1 -1
  45. package/dashboard/dist/assets/{use-ordered-list-KIV2M5tf.js → use-ordered-list-_O8onJV7.js} +1 -1
  46. package/dashboard/dist/assets/{use-quality-per-dollar-pref-BbsMhRJt.js → use-quality-per-dollar-pref-tuspH_6n.js} +1 -1
  47. package/dashboard/dist/assets/{use-usage-limits-COBOepfU.js → use-usage-limits-whCVzQbf.js} +1 -1
  48. package/dashboard/dist/assets/{useCurrency-DbBq0Fal.js → useCurrency-BiDjYxfm.js} +1 -1
  49. package/dashboard/dist/assets/{useScrollLock-C4eBSnib.js → useScrollLock-DK-ndHcw.js} +1 -1
  50. package/dashboard/dist/index.html +1 -1
  51. package/dashboard/dist/share.html +1 -1
  52. package/package.json +1 -1
  53. package/src/commands/status.js +47 -4
  54. package/src/commands/sync.js +72 -18
  55. package/src/lib/local-api.js +24 -2
  56. package/src/lib/pricing/curated-overrides.json +7 -2
  57. package/src/lib/pricing/seed-snapshot.json +1 -1
  58. package/src/lib/rollout.js +385 -0
  59. package/src/lib/sqlite-reader.js +24 -6
  60. package/src/lib/usage-limits.js +200 -29
  61. package/src/lib/zcode-limits.js +375 -40
  62. package/dashboard/dist/assets/DialogDescription-DeEet5bz.js +0 -1
  63. package/dashboard/dist/assets/check-fkZxgcVQ.js +0 -1
  64. package/dashboard/dist/assets/main-B0LYKUIY.js +0 -1127
@@ -9448,6 +9448,387 @@ async function parseCopilotIncremental({ otelPaths, cursors, queuePath, onProgre
9448
9448
  return { recordsProcessed, eventsAggregated, bucketsQueued };
9449
9449
  }
9450
9450
 
9451
+ // ─────────────────────────────────────────────────────────────────────────────
9452
+ // GitHub Copilot App — passive reader for ~/.copilot/data.db
9453
+ //
9454
+ // The Copilot App persists one row per local App session in `sessions`, with
9455
+ // cumulative `total_*` token summary columns. We intentionally do NOT read
9456
+ // session-state/<id>/events.jsonl: forked App sessions inherit parent events
9457
+ // there before the child has produced any tokens, so parsing event history would
9458
+ // double-count parent usage. The only accounting source here is positive deltas
9459
+ // from the `sessions.total_*` counters. Those deltas feed the same aggregate
9460
+ // `source="copilot"` pipeline as existing OTEL data (dashboard totals, trends,
9461
+ // model breakdown, cost), but the cursor is separate (`cursors.copilotApp`) so
9462
+ // Copilot CLI / Chat extension OTEL support keeps its existing behavior.
9463
+ // Project attribution and per-conversation detail are intentionally absent:
9464
+ // data.db's summary columns do not expose privacy-safe project/message metadata.
9465
+ // ─────────────────────────────────────────────────────────────────────────────
9466
+
9467
+ const COPILOT_APP_CURSOR_VERSION = 1;
9468
+ const COPILOT_APP_DEFAULT_MODEL = "github-copilot";
9469
+
9470
+ function normalizeCopilotAppDbPath(dbPath, env = process.env) {
9471
+ if (typeof dbPath !== "string" || !dbPath.trim()) return null;
9472
+ const expanded = expandHomePath(dbPath.trim(), env);
9473
+ return path.isAbsolute(expanded) ? path.normalize(expanded) : path.resolve(expanded);
9474
+ }
9475
+
9476
+ function resolveCopilotAppDbPaths(env = process.env) {
9477
+ const home = env.HOME || require("node:os").homedir();
9478
+ const paths = new Set();
9479
+ const addDbPath = (dbPath) => {
9480
+ const normalized = normalizeCopilotAppDbPath(dbPath, env);
9481
+ if (normalized) paths.add(normalized);
9482
+ };
9483
+ const addHome = (homePath) => {
9484
+ const normalizedHome = normalizeCopilotAppDbPath(homePath, env);
9485
+ if (normalizedHome) addDbPath(path.join(normalizedHome, "data.db"));
9486
+ };
9487
+
9488
+ if (typeof env.TOKENTRACKER_COPILOT_APP_DB === "string" && env.TOKENTRACKER_COPILOT_APP_DB.trim()) {
9489
+ addDbPath(env.TOKENTRACKER_COPILOT_APP_DB);
9490
+ }
9491
+ if (typeof env.COPILOT_HOME === "string" && env.COPILOT_HOME.trim()) {
9492
+ addHome(env.COPILOT_HOME);
9493
+ }
9494
+
9495
+ const defaultDb = path.join(home, ".copilot", "data.db");
9496
+ if (process.platform === "win32") {
9497
+ const wslHome = wsl.shouldProbeWsl(env) ? wsl.discoverWslHome(".copilot", { env }) : null;
9498
+ const wslDb = wslHome ? path.join(wslHome, "data.db") : null;
9499
+ const resolved = resolveInstallPaths({ nativeValue: defaultDb, wslValue: wslDb }, env);
9500
+ if (resolved.native) addDbPath(resolved.native);
9501
+ if (resolved.wsl) addDbPath(resolved.wsl);
9502
+ if (paths.size === 0 && wsl.shouldProbeNative(env)) addDbPath(defaultDb);
9503
+ } else {
9504
+ addDbPath(defaultDb);
9505
+ }
9506
+
9507
+ return Array.from(paths).sort();
9508
+ }
9509
+
9510
+ function resolveCopilotAppDbPath(env = process.env) {
9511
+ const paths = resolveCopilotAppDbPaths(env);
9512
+ for (const candidate of paths) {
9513
+ try {
9514
+ if (fssync.existsSync(candidate)) return candidate;
9515
+ } catch (_e) {}
9516
+ }
9517
+ return paths[0] || null;
9518
+ }
9519
+
9520
+ function shouldCountCopilotAppSession(row) {
9521
+ const sessionType = typeof row?.session_type === "string" ? row.session_type.trim().toLowerCase() : "";
9522
+ const providerId = typeof row?.provider_id === "string" ? row.provider_id.trim().toLowerCase() : "";
9523
+ // OTEL remains the owner for recognizable Copilot CLI / VS Code extension
9524
+ // sessions. The App DB path is separately cursored, but skipping known
9525
+ // OTEL-backed surfaces avoids duplicate source="copilot" buckets if GitHub
9526
+ // mirrors those sessions into data.db in a future release.
9527
+ const markers = [sessionType, providerId];
9528
+ return !markers.some((value) =>
9529
+ value === "cli" ||
9530
+ value === "copilot-cli" ||
9531
+ value.includes("vscode") ||
9532
+ value.includes("extension") ||
9533
+ value.includes("otel")
9534
+ );
9535
+ }
9536
+
9537
+ function parseCopilotAppTimestamp(value) {
9538
+ if (value === null || value === undefined || value === "") return null;
9539
+ if (typeof value === "number") {
9540
+ const ms = value > 10_000_000_000 ? value : value * 1000;
9541
+ const d = new Date(ms);
9542
+ return Number.isNaN(d.getTime()) ? null : d.toISOString();
9543
+ }
9544
+ if (typeof value !== "string") return null;
9545
+ const trimmed = value.trim();
9546
+ if (!trimmed) return null;
9547
+ if (/^\d+$/.test(trimmed)) {
9548
+ return parseCopilotAppTimestamp(Number(trimmed));
9549
+ }
9550
+ const parsed = new Date(trimmed);
9551
+ return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
9552
+ }
9553
+
9554
+ function normalizeCopilotAppModel(model) {
9555
+ if (typeof model !== "string") return "";
9556
+ const normalized = normalizeModelInput(model);
9557
+ if (!normalized) return "";
9558
+ if (/^claude-(sonnet|opus|haiku)-\d+\.\d+/.test(normalized)) {
9559
+ return normalized.replace(/^(claude-(?:sonnet|opus|haiku)-\d+)\.(\d+)/, "$1-$2");
9560
+ }
9561
+ return normalized;
9562
+ }
9563
+
9564
+ function readCopilotAppSessionsFromSqlite(dbPath, sqliteOptions = {}) {
9565
+ const pragmaRows = readSqliteJsonRows(dbPath, "PRAGMA table_info(sessions)", {
9566
+ label: "GitHub Copilot App",
9567
+ maxBuffer: 4 * 1024 * 1024,
9568
+ timeout: 10_000,
9569
+ readOnly: true,
9570
+ throwOnReadFailure: true,
9571
+ ...sqliteOptions,
9572
+ });
9573
+ const columns = new Set(pragmaRows.map((row) => row?.name).filter(Boolean));
9574
+ if (!columns.has("id")) return [];
9575
+ const optional = (col) => (columns.has(col) ? col : `NULL AS ${col}`);
9576
+ const orderTerms = ["updated_at", "created_at"].filter((col) => columns.has(col));
9577
+ const orderBy = orderTerms.length > 0
9578
+ ? `ORDER BY COALESCE(${orderTerms.join(", ")}, ''), id`
9579
+ : "ORDER BY id";
9580
+ // Select only the columns the parser consumes. Content-ish columns like
9581
+ // `title` must stay out of the query: token counts only, never user content.
9582
+ const sql = `
9583
+ SELECT
9584
+ id,
9585
+ ${optional("session_type")},
9586
+ ${optional("model")},
9587
+ ${optional("provider_id")},
9588
+ ${optional("created_at")},
9589
+ ${optional("updated_at")},
9590
+ ${optional("total_input_tokens")},
9591
+ ${optional("total_output_tokens")},
9592
+ ${optional("total_cached_tokens")},
9593
+ ${optional("total_reasoning_tokens")}
9594
+ FROM sessions
9595
+ ${orderBy}
9596
+ `.trim();
9597
+ return readSqliteJsonRows(dbPath, sql, {
9598
+ label: "GitHub Copilot App",
9599
+ maxBuffer: 64 * 1024 * 1024,
9600
+ timeout: 60_000,
9601
+ readOnly: true,
9602
+ throwOnReadFailure: true,
9603
+ ...sqliteOptions,
9604
+ });
9605
+ }
9606
+
9607
+ function sqliteSidecarFingerprint(dbPath) {
9608
+ const out = {};
9609
+ for (const suffix of ["", "-wal", "-shm", "-journal"]) {
9610
+ const filePath = `${dbPath}${suffix}`;
9611
+ try {
9612
+ const stat = fssync.statSync(filePath);
9613
+ out[suffix || "db"] = {
9614
+ size: stat.size,
9615
+ mtimeMs: stat.mtimeMs,
9616
+ ino: stat.ino,
9617
+ };
9618
+ } catch (e) {
9619
+ if (!e || e.code !== "ENOENT") throw e;
9620
+ }
9621
+ }
9622
+ return out;
9623
+ }
9624
+
9625
+ function sameSqliteFingerprint(a, b) {
9626
+ return JSON.stringify(a || {}) === JSON.stringify(b || {});
9627
+ }
9628
+
9629
+ function copilotAppBaselineTotal(value) {
9630
+ if (!value || typeof value !== "object") return 0;
9631
+ return (
9632
+ Number(value.input || 0) +
9633
+ Number(value.output || 0) +
9634
+ Number(value.cached || 0) +
9635
+ Number(value.reasoning || 0)
9636
+ );
9637
+ }
9638
+
9639
+ function capCopilotAppSessionTotals(sessionTotals, maxEntries = 10_000) {
9640
+ const entries = Object.entries(sessionTotals);
9641
+ if (entries.length <= maxEntries) return sessionTotals;
9642
+
9643
+ const nonzero = [];
9644
+ const zero = [];
9645
+ for (const entry of entries) {
9646
+ if (copilotAppBaselineTotal(entry[1]) > 0) nonzero.push(entry);
9647
+ else zero.push(entry);
9648
+ }
9649
+
9650
+ zero.sort((a, b) => {
9651
+ const ta = Date.parse(a[1]?.updatedAt || "") || 0;
9652
+ const tb = Date.parse(b[1]?.updatedAt || "") || 0;
9653
+ return tb - ta;
9654
+ });
9655
+ const zeroBudget = Math.max(0, maxEntries - nonzero.length);
9656
+ const capped = Object.fromEntries([...nonzero, ...zero.slice(0, zeroBudget)]);
9657
+ for (const key of Object.keys(sessionTotals)) delete sessionTotals[key];
9658
+ Object.assign(sessionTotals, capped);
9659
+ return sessionTotals;
9660
+ }
9661
+
9662
+ function normalizeCopilotAppTokenDelta(curr, prev) {
9663
+ const inputDelta = Math.max(0, Number(curr?.input || 0) - Number(prev?.input || 0));
9664
+ const cachedDelta = Math.max(0, Number(curr?.cached || 0) - Number(prev?.cached || 0));
9665
+ const cachedInputTokens = Math.min(cachedDelta, inputDelta);
9666
+ const inputTokens = Math.max(0, inputDelta - cachedInputTokens);
9667
+ const outputTokens = Math.max(0, Number(curr?.output || 0) - Number(prev?.output || 0));
9668
+ const reasoningTokens = Math.max(0, Number(curr?.reasoning || 0) - Number(prev?.reasoning || 0));
9669
+ const totalTokens = inputTokens + cachedInputTokens + outputTokens + reasoningTokens;
9670
+ return {
9671
+ input_tokens: inputTokens,
9672
+ cached_input_tokens: cachedInputTokens,
9673
+ cache_creation_input_tokens: 0,
9674
+ output_tokens: outputTokens,
9675
+ reasoning_output_tokens: reasoningTokens,
9676
+ total_tokens: totalTokens,
9677
+ conversation_count: copilotAppBaselineTotal(prev) === 0 && totalTokens > 0 ? 1 : 0,
9678
+ };
9679
+ }
9680
+
9681
+ async function parseCopilotAppDbIncremental({
9682
+ dbPath,
9683
+ dbPaths,
9684
+ cursors,
9685
+ queuePath,
9686
+ onProgress,
9687
+ env,
9688
+ sqliteOptions,
9689
+ } = {}) {
9690
+ await ensureDir(path.dirname(queuePath));
9691
+ const paths = Array.isArray(dbPaths) && dbPaths.length > 0
9692
+ ? dbPaths
9693
+ : dbPath
9694
+ ? [dbPath]
9695
+ : resolveCopilotAppDbPaths(env || process.env);
9696
+ const uniquePaths = [...new Set(paths.map((p) => normalizeCopilotAppDbPath(p, env || process.env)).filter(Boolean))];
9697
+ const appState =
9698
+ cursors.copilotApp && typeof cursors.copilotApp === "object" ? cursors.copilotApp : {};
9699
+ const dbStates =
9700
+ appState.dbs && typeof appState.dbs === "object" ? { ...appState.dbs } : {};
9701
+ const hourlyState = normalizeHourlyState(cursors?.hourly);
9702
+ const touchedBuckets = new Set();
9703
+ const cb = typeof onProgress === "function" ? onProgress : null;
9704
+ let recordsProcessed = 0;
9705
+ let eventsAggregated = 0;
9706
+ let totalRows = 0;
9707
+ let dbErrors = 0;
9708
+ const updatedAt = new Date().toISOString();
9709
+
9710
+ for (const resolvedDb of uniquePaths) {
9711
+ const dbState = dbStates[resolvedDb] && typeof dbStates[resolvedDb] === "object"
9712
+ ? dbStates[resolvedDb]
9713
+ : {};
9714
+ const sessionTotals =
9715
+ dbState.sessionTotals && typeof dbState.sessionTotals === "object"
9716
+ ? { ...dbState.sessionTotals }
9717
+ : {};
9718
+ let currentFingerprint = null;
9719
+ try {
9720
+ fssync.statSync(resolvedDb);
9721
+ currentFingerprint = sqliteSidecarFingerprint(resolvedDb);
9722
+ } catch (e) {
9723
+ if (e && e.code === "ENOENT") continue;
9724
+ dbErrors++;
9725
+ dbStates[resolvedDb] = {
9726
+ ...dbState,
9727
+ sessionTotals,
9728
+ lastError: e && e.message ? e.message : String(e),
9729
+ lastErrorAt: updatedAt,
9730
+ };
9731
+ continue;
9732
+ }
9733
+
9734
+ if (dbState.lastDbFingerprint && sameSqliteFingerprint(currentFingerprint, dbState.lastDbFingerprint)) {
9735
+ dbStates[resolvedDb] = { ...dbState, sessionTotals, updatedAt };
9736
+ continue;
9737
+ }
9738
+
9739
+ let snap = null;
9740
+ let rows = [];
9741
+ try {
9742
+ snap = snapshotSqliteDb(resolvedDb);
9743
+ rows = readCopilotAppSessionsFromSqlite(snap.path, sqliteOptions);
9744
+ } catch (err) {
9745
+ dbErrors++;
9746
+ dbStates[resolvedDb] = {
9747
+ ...dbState,
9748
+ sessionTotals,
9749
+ lastError: err && err.message ? err.message : String(err),
9750
+ lastErrorAt: updatedAt,
9751
+ };
9752
+ continue;
9753
+ } finally {
9754
+ if (snap) snap.cleanup();
9755
+ }
9756
+ totalRows += rows.length;
9757
+
9758
+ for (const row of rows) {
9759
+ recordsProcessed++;
9760
+ if (!row || typeof row.id !== "string" || !row.id.trim()) continue;
9761
+ if (!shouldCountCopilotAppSession(row)) continue;
9762
+
9763
+ const sessionId = row.id.trim();
9764
+ const curr = {
9765
+ input: toNonNegativeInt(row.total_input_tokens),
9766
+ output: toNonNegativeInt(row.total_output_tokens),
9767
+ cached: toNonNegativeInt(row.total_cached_tokens),
9768
+ reasoning: toNonNegativeInt(row.total_reasoning_tokens),
9769
+ };
9770
+ const prev = sessionTotals[sessionId] || { input: 0, output: 0, cached: 0, reasoning: 0 };
9771
+ const bucketDelta = normalizeCopilotAppTokenDelta(curr, prev);
9772
+ const totalDelta = bucketDelta.total_tokens;
9773
+ const rowUpdatedAt =
9774
+ parseCopilotAppTimestamp(row.updated_at) ||
9775
+ parseCopilotAppTimestamp(row.created_at) ||
9776
+ updatedAt;
9777
+ const model = normalizeCopilotAppModel(row.model) || COPILOT_APP_DEFAULT_MODEL;
9778
+
9779
+ if (totalDelta <= 0) {
9780
+ sessionTotals[sessionId] = { ...curr, model, updatedAt: rowUpdatedAt };
9781
+ continue;
9782
+ }
9783
+
9784
+ const bucketStart = toUtcHalfHourStart(rowUpdatedAt);
9785
+ if (!bucketStart) {
9786
+ sessionTotals[sessionId] = { ...curr, model, updatedAt: rowUpdatedAt };
9787
+ continue;
9788
+ }
9789
+
9790
+ const bucket = getHourlyBucket(hourlyState, "copilot", model, bucketStart);
9791
+ addTotals(bucket.totals, bucketDelta);
9792
+ touchedBuckets.add(bucketKey("copilot", model, bucketStart));
9793
+ sessionTotals[sessionId] = { ...curr, model, updatedAt: rowUpdatedAt };
9794
+ eventsAggregated++;
9795
+
9796
+ if (cb) {
9797
+ cb({
9798
+ index: recordsProcessed,
9799
+ total: Math.max(totalRows, recordsProcessed),
9800
+ recordsProcessed,
9801
+ eventsAggregated,
9802
+ bucketsQueued: touchedBuckets.size,
9803
+ });
9804
+ }
9805
+ }
9806
+
9807
+ capCopilotAppSessionTotals(sessionTotals);
9808
+
9809
+ dbStates[resolvedDb] = {
9810
+ ...dbState,
9811
+ sessionTotals,
9812
+ lastDbFingerprint: currentFingerprint,
9813
+ lastError: null,
9814
+ lastErrorAt: null,
9815
+ updatedAt,
9816
+ };
9817
+ }
9818
+
9819
+ const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
9820
+ hourlyState.updatedAt = updatedAt;
9821
+ cursors.hourly = hourlyState;
9822
+ cursors.copilotApp = {
9823
+ ...appState,
9824
+ version: COPILOT_APP_CURSOR_VERSION,
9825
+ dbs: dbStates,
9826
+ updatedAt,
9827
+ };
9828
+
9829
+ return { recordsProcessed, eventsAggregated, bucketsQueued, dbErrors };
9830
+ }
9831
+
9451
9832
  // ─────────────────────────────────────────────────────────────────────────────
9452
9833
  // Grok Build (xAI) — passive reader for ~/.grok/sessions/**/updates.jsonl + signals.json
9453
9834
  // Triggered either by full scan in sync or by the SessionEnd hook writing a signal.
@@ -10363,6 +10744,9 @@ module.exports = {
10363
10744
  probeWslDistros,
10364
10745
  discoverWslHermesHome,
10365
10746
  resolveCopilotOtelPaths,
10747
+ resolveCopilotAppDbPath,
10748
+ resolveCopilotAppDbPaths,
10749
+ readCopilotAppSessionsFromSqlite,
10366
10750
  parseRolloutIncremental,
10367
10751
  parseClaudeIncremental,
10368
10752
  parseGeminiIncremental,
@@ -10376,6 +10760,7 @@ module.exports = {
10376
10760
  zedInstallOwnsCursor,
10377
10761
  hermesInstallOwnsCursor,
10378
10762
  parseCopilotIncremental,
10763
+ parseCopilotAppDbIncremental,
10379
10764
  resolveKimiWireFiles,
10380
10765
  resolveKimiDefaultModel,
10381
10766
  parseKimiIncremental,
@@ -30,8 +30,9 @@ function warnSqliteUnavailable({ dbPath, label, cliError, nodeSqliteError, env,
30
30
  }
31
31
  }
32
32
 
33
- function readSqliteRowsWithCli(dbPath, sql, { execFileSync, timeout, maxBuffer }) {
34
- const raw = execFileSync("sqlite3", ["-json", dbPath, sql], {
33
+ function readSqliteRowsWithCli(dbPath, sql, { execFileSync, timeout, maxBuffer, readOnly }) {
34
+ const args = readOnly ? ["-readonly", "-json", dbPath, sql] : ["-json", dbPath, sql];
35
+ const raw = execFileSync("sqlite3", args, {
35
36
  encoding: "utf8",
36
37
  maxBuffer,
37
38
  timeout,
@@ -92,7 +93,7 @@ function readSqliteJsonRows(dbPath, sql, options = {}) {
92
93
 
93
94
  let cliError = null;
94
95
  try {
95
- return readSqliteRowsWithCli(dbPath, sql, { execFileSync, timeout, maxBuffer });
96
+ return readSqliteRowsWithCli(dbPath, sql, { execFileSync, timeout, maxBuffer, readOnly: Boolean(options.readOnly) });
96
97
  } catch (err) {
97
98
  cliError = err;
98
99
  }
@@ -114,11 +115,20 @@ function readSqliteJsonRows(dbPath, sql, options = {}) {
114
115
  stderr: options.stderr,
115
116
  });
116
117
  }
118
+ if (options.throwOnReadFailure) {
119
+ const err = new Error(
120
+ `Cannot read ${label} SQLite database at ${dbPath}: ${formatError(cliError)}; ${formatError(nodeSqliteError)}`,
121
+ );
122
+ err.cliError = cliError;
123
+ err.nodeSqliteError = nodeSqliteError;
124
+ throw err;
125
+ }
117
126
  return [];
118
127
  }
119
128
 
120
- async function readSqliteRowsWithCliAsync(dbPath, sql, { execFile, timeout, maxBuffer }) {
121
- const { stdout } = await execFile("sqlite3", ["-json", dbPath, sql], {
129
+ async function readSqliteRowsWithCliAsync(dbPath, sql, { execFile, timeout, maxBuffer, readOnly }) {
130
+ const args = readOnly ? ["-readonly", "-json", dbPath, sql] : ["-json", dbPath, sql];
131
+ const { stdout } = await execFile("sqlite3", args, {
122
132
  encoding: "utf8",
123
133
  maxBuffer,
124
134
  timeout,
@@ -149,7 +159,7 @@ async function readSqliteJsonRowsAsync(dbPath, sql, options = {}) {
149
159
 
150
160
  let cliError = null;
151
161
  try {
152
- return await readSqliteRowsWithCliAsync(dbPath, sql, { execFile, timeout, maxBuffer });
162
+ return await readSqliteRowsWithCliAsync(dbPath, sql, { execFile, timeout, maxBuffer, readOnly: Boolean(options.readOnly) });
153
163
  } catch (err) {
154
164
  cliError = err;
155
165
  }
@@ -171,6 +181,14 @@ async function readSqliteJsonRowsAsync(dbPath, sql, options = {}) {
171
181
  stderr: options.stderr,
172
182
  });
173
183
  }
184
+ if (options.throwOnReadFailure) {
185
+ const err = new Error(
186
+ `Cannot read ${label} SQLite database at ${dbPath}: ${formatError(cliError)}; ${formatError(nodeSqliteError)}`,
187
+ );
188
+ err.cliError = cliError;
189
+ err.nodeSqliteError = nodeSqliteError;
190
+ throw err;
191
+ }
174
192
  return [];
175
193
  }
176
194