tokentracker-cli 0.84.9 → 0.85.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-BgBFpxmU.js → AchievementBadge-DEqbHlmn.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-DHPD3Fnu.js → AchievementsPage-BOaEu1AQ.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DouWVD-3.js → AchievementsSection-C370ngO3.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-Df9eQiA3.js → BadgeDetailModal-D7Wkef0K.js} +1 -1
- package/dashboard/dist/assets/{Card-CQ9wBnz6.js → Card-mDEVjNdM.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-XIO-I-5v.js → ClawdAnimated-DdsX-z8n.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-Cy1cJ91B.js → CommandPalette-DhxRIuHC.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-CwKG4LQM.js → CommunityStatsModal-CG-9JPS6.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-BNcWqick.js → ConfirmModal-COhxErLm.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-tX0DKgFE.js +1 -0
- package/dashboard/dist/assets/{DevicePage-pJ2jNkZV.js → DevicePage-BsSeHx84.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-C2SOtkJN.js → DialogClose-eyOW2cbx.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-BwtE4lKl.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-CGSbB8G7.js → DialogTitle-H8sGiH7c.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CZSiw7me.js → FadeIn-pq2ja50M.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-ZmQhX_kR.js → HeaderGithubStar-DWgcpDHr.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-D_YdG369.js → HoverTooltip-CcJ_VR65.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-du886dF0.js → IpCheckPage-CNUN346Q.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-BuAak8lU.js → LandingPage-yaoRGsNI.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-nU0ARYnN.js → LeaderboardAvatar-Ddai4I12.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-CngwCvC7.js → LeaderboardPage-Bajkxf63.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-CXZJ0e-T.js → LeaderboardProfileModal-BGYKDgIi.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CZve6s6b.js → LeaderboardProfilePage-Ck5vPaeN.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-CWvoJPk8.js → LimitsPage-Bqobtsw2.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-DyUAAyyE.js → LocalOnlyNotice-e9KSE3wj.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-eEiRfbdv.js → LoginCard-BhTSm2tb.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-C16WdIQm.js → LoginModal-CRwuThde.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-B2WqpE1t.js → LoginPage-j1i74lcc.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DtQWrGmI.js → PetPage-r62ViU1X.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-CRhqtFLB.js → PopoverPopup-DopNTyD4.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-DWNH_wYu.js → ProviderIcon-c1LkPhYM.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DzCczcWZ.js → ResetPasswordPage-DJdDNvtq.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-CV9ccq6R.js → SegmentedControl-DhWVOTPN.js} +1 -1
- package/dashboard/dist/assets/{Select-BrogNNk0.js → Select-DeUmTmsJ.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-Bpu3swt0.js → SelectItemText-pX0B5f7h.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-D_FIrMJP.js → ServiceStatusPage-ejIPz6oF.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-C8TvTy1o.js → SessionsPage-WEbn90GW.js} +2 -2
- package/dashboard/dist/assets/{SettingsPage-Bd-tzJAR.js → SettingsPage-B8t5gCeI.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-CKGU9wQ4.js → SkillsPage-BWMlzeAp.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-DhHW6L-U.js → TokenGalaxy-DIBRBvMj.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-DQo-4pPB.js → ToolbarRootContext-C6KxLvnI.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-Ejl8d45g.js → TrendMonitor-BaMNh67w.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CY7f4wCD.js → WidgetsPage-DDsy4M3x.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-B4-B2v-v.js → WrappedPage-CiNnUFRI.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-BNUFXCfp.js → __vite-browser-external-CM-H8-LW.js} +23 -16
- package/dashboard/dist/assets/{agent-logos-BRzutmeR.js → agent-logos-BjVa6oe7.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-BcrbPk8c.js → arrow-up-right-CCum1mqX.js} +1 -1
- package/dashboard/dist/assets/{check-BMxeFPfQ.js → check-ySRQc1YP.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-DOstgiRf.js → chevron-right-5vFloHew.js} +1 -1
- package/dashboard/dist/assets/{copy-BsKO524t.js → copy-mhfKVMKo.js} +1 -1
- package/dashboard/dist/assets/{download-dadqxej7.js → download-GdrXglSx.js} +1 -1
- package/dashboard/dist/assets/{external-link-CTu3BMeU.js → external-link-KGFaKc8s.js} +1 -1
- package/dashboard/dist/assets/{flame-B_IvWcMn.js → flame-BmNtNoS1.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DIT6hLSb.js → format-tokens-CCpCNw3d.js} +1 -1
- package/dashboard/dist/assets/{icons-mWku76bu.js → icons-XLKZavTf.js} +1 -1
- package/dashboard/dist/assets/{index-D-gQV-hF.js → index-CsywTeCQ.js} +1 -1
- package/dashboard/dist/assets/{index-CboN9E82.js → index-IKpmeZte.js} +1 -1
- package/dashboard/dist/assets/{index-D5-w-aGy.js → index-_xU5U5Ct.js} +1 -1
- package/dashboard/dist/assets/{info-x6l352SW.js → info-DUV5UpmT.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-Bo12VlLJ.js → limits-providers-DFkCZCZj.js} +1 -1
- package/dashboard/dist/assets/{link-2-eo_xPxH1.js → link-2-D7c64yLY.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-C4n1iGBv.js → loader-circle-sMONRK2M.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-pR8xwsI_.js → maximize-2-CVWRgrk9.js} +1 -1
- package/dashboard/dist/assets/{provider-display-ErggQWu4.js → provider-display-DSKqVjgm.js} +1 -1
- package/dashboard/dist/assets/{react-DYKg02Aq.js → react-c1q4mGkS.js} +1 -1
- package/dashboard/dist/assets/{search-B_jbUwPS.js → search-CwD3s-_C.js} +1 -1
- package/dashboard/dist/assets/{skills-api-DY5ruGmw.js → skills-api-BRXj8kjT.js} +1 -1
- package/dashboard/dist/assets/{store-Cu8i1y26.js → store-lqTW2gfK.js} +1 -1
- package/dashboard/dist/assets/{terminal-D3j6X68s.js → terminal-DU1_KbKo.js} +1 -1
- package/dashboard/dist/assets/{trash-2-CFpwgJw5.js → trash-2-B8Y5Xc-K.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-C0fKq66G.js → use-community-stats-DYwwimvp.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-TOp4UAnI.js → use-limits-display-prefs-Bst6ybK3.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-BZie_7z1.js → use-native-settings-Cqle4lgw.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-Bq92YyUp.js → use-ordered-list-Dtze0ahb.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-CbZDR54N.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-BcpytkES.js → use-session-efficiency-pref-gwbDMHXa.js} +1 -1
- package/dashboard/dist/assets/{use-transform-Dmmrne2z.js → use-transform-CU0yxXpX.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-Cw0Z9Tew.js → use-usage-limits-5y1I1T5_.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-DyyonINj.js → useCurrency-BfVNrywe.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-DvWw86lJ.js → useScrollLock-CF1m7AMJ.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-CBkvGfJ1.js → useTokenFormat-DiRwVvRr.js} +1 -1
- package/dashboard/dist/assets/{zap-CnYA39Ug.js → zap-BZtlWtuN.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/status.js +114 -9
- package/src/commands/sync.js +151 -46
- package/src/lib/claude-categorizer.js +36 -9
- package/src/lib/diagnostics.js +86 -13
- package/src/lib/doctor.js +17 -0
- package/src/lib/grok-context-breakdown.js +794 -0
- package/src/lib/install-resolver.js +10 -2
- package/src/lib/local-api.js +21 -3
- package/src/lib/multi-install-parser.js +9 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +337 -44
- package/src/lib/session-analytics.js +332 -9
- package/dashboard/dist/assets/DashboardPage-Dns_1yXL.js +0 -1
- package/dashboard/dist/assets/DialogDescription-Bhfqqmll.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-ZiVeOkvb.js +0 -1
|
@@ -48,12 +48,27 @@ const { computeRowCost } = require("./pricing");
|
|
|
48
48
|
// - session_id is only set when the log actually contained a sessionId
|
|
49
49
|
// record. It used to fall back to the file's basename, which produced
|
|
50
50
|
// resume commands for non-session files (`claude --resume journal`).
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
// v10 adds Grok Build sessions (~/.grok/sessions/**/updates.jsonl), scanned
|
|
52
|
+
// from turn_completed.usage + tool_call metadata. Bump so Claude/Codex rows
|
|
53
|
+
// stay valid while Grok entries appear on the next full rebuild.
|
|
54
|
+
const SIDECAR_VERSION = 10;
|
|
55
|
+
const EDIT_TOOLS = new Set([
|
|
56
|
+
"apply_patch",
|
|
57
|
+
"edit",
|
|
58
|
+
"write",
|
|
59
|
+
"multiedit",
|
|
60
|
+
"notebookedit",
|
|
61
|
+
// Grok Build write tools (ACP tool titles / x.ai/tool.name).
|
|
62
|
+
"search_replace",
|
|
63
|
+
"str_replace",
|
|
64
|
+
"create_file",
|
|
65
|
+
"write_file",
|
|
66
|
+
]);
|
|
53
67
|
const PLACEHOLDER_MODELS = new Set(["<synthetic>", "synthetic", "<unknown>", "unknown"]);
|
|
54
68
|
const CLAUDE_MEM_OBSERVER_PROJECT_SUFFIX = "--claude-mem-observer-sessions";
|
|
55
69
|
const CODEX_SUBAGENT_TOOLS = new Set(["spawn_agent", "multi_agent_v1__spawn_agent"]);
|
|
56
70
|
const CODEX_SIGNAL_TOOLS = new Set([...EDIT_TOOLS, ...CODEX_SUBAGENT_TOOLS]);
|
|
71
|
+
const GROK_SUBAGENT_TOOLS = new Set(["spawn_subagent", "spawn_agent"]);
|
|
57
72
|
|
|
58
73
|
function normalizeSessionModel(value) {
|
|
59
74
|
if (typeof value !== "string") return null;
|
|
@@ -457,11 +472,302 @@ async function scanCodexSession(filePath) {
|
|
|
457
472
|
});
|
|
458
473
|
}
|
|
459
474
|
|
|
475
|
+
// Grok Build sessions live at:
|
|
476
|
+
// ~/.grok/sessions/<url-encoded-cwd>/<session-uuid>/updates.jsonl
|
|
477
|
+
// with sibling summary.json (id/cwd/title) and signals.json (aggregate
|
|
478
|
+
// counters). Billable tokens come only from turn_completed.usage — the same
|
|
479
|
+
// authority as the Grok usage parser — not from context-window totalTokens.
|
|
480
|
+
function resolveGrokHome(home = os.homedir(), env = process.env) {
|
|
481
|
+
if (typeof env.TOKENTRACKER_GROK_HOME === "string" && env.TOKENTRACKER_GROK_HOME.trim()) {
|
|
482
|
+
return path.resolve(env.TOKENTRACKER_GROK_HOME.trim());
|
|
483
|
+
}
|
|
484
|
+
if (typeof env.GROK_HOME === "string" && env.GROK_HOME.trim()) {
|
|
485
|
+
return path.resolve(env.GROK_HOME.trim());
|
|
486
|
+
}
|
|
487
|
+
return path.join(home, ".grok");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function grokSummaryPathFor(updatesPath) {
|
|
491
|
+
return path.join(path.dirname(updatesPath), "summary.json");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function grokSignalsPathFor(updatesPath) {
|
|
495
|
+
return path.join(path.dirname(updatesPath), "signals.json");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function readJsonFileSync(filePath) {
|
|
499
|
+
try {
|
|
500
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
501
|
+
} catch {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function grokTimestampIso(obj) {
|
|
507
|
+
const metaMs = Number(obj?.params?._meta?.agentTimestampMs);
|
|
508
|
+
if (Number.isFinite(metaMs) && metaMs > 0) return new Date(metaMs).toISOString();
|
|
509
|
+
const top = Number(obj?.timestamp);
|
|
510
|
+
if (!Number.isFinite(top) || top <= 0) return null;
|
|
511
|
+
// Grok writes unix seconds on the envelope; ms values are already > 1e12.
|
|
512
|
+
const ms = top > 1e12 ? top : top * 1000;
|
|
513
|
+
return new Date(ms).toISOString();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Grok reports inputTokens as the full prompt (including cache hits). Split so
|
|
517
|
+
// pricing can apply cache_read rates correctly — same authority as the usage
|
|
518
|
+
// parser and grok-context-breakdown.readUsageTotals. Prefer the reported
|
|
519
|
+
// totalTokens; do not re-sum input+cached (that double-counts cache hits).
|
|
520
|
+
function grokUsageTotals(usage) {
|
|
521
|
+
if (!usage || typeof usage !== "object") return emptyTotals();
|
|
522
|
+
const inputRaw = finite(usage.inputTokens ?? usage.input_tokens);
|
|
523
|
+
const cached_input_tokens = finite(
|
|
524
|
+
usage.cachedReadTokens ?? usage.cache_read_input_tokens ?? usage.cached_input_tokens,
|
|
525
|
+
);
|
|
526
|
+
const cache_creation_input_tokens = finite(
|
|
527
|
+
usage.cachedWriteTokens ?? usage.cache_creation_input_tokens,
|
|
528
|
+
);
|
|
529
|
+
const output_tokens = finite(usage.outputTokens ?? usage.output_tokens);
|
|
530
|
+
const reasoning_output_tokens = finite(
|
|
531
|
+
usage.reasoningTokens ?? usage.reasoning_output_tokens,
|
|
532
|
+
);
|
|
533
|
+
const input_tokens = Math.max(0, inputRaw - cached_input_tokens);
|
|
534
|
+
let total_tokens = finite(usage.totalTokens ?? usage.total_tokens);
|
|
535
|
+
if (total_tokens <= 0) {
|
|
536
|
+
total_tokens = input_tokens
|
|
537
|
+
+ cached_input_tokens
|
|
538
|
+
+ cache_creation_input_tokens
|
|
539
|
+
+ output_tokens
|
|
540
|
+
+ reasoning_output_tokens;
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
input_tokens,
|
|
544
|
+
cached_input_tokens,
|
|
545
|
+
cache_creation_input_tokens,
|
|
546
|
+
output_tokens,
|
|
547
|
+
reasoning_output_tokens,
|
|
548
|
+
total_tokens,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function pickGrokModel(usage, fallback) {
|
|
553
|
+
const modelUsage = usage?.modelUsage;
|
|
554
|
+
if (modelUsage && typeof modelUsage === "object") {
|
|
555
|
+
let bestName = null;
|
|
556
|
+
let bestTokens = -1;
|
|
557
|
+
for (const [name, entry] of Object.entries(modelUsage)) {
|
|
558
|
+
const normalized = normalizeSessionModel(name);
|
|
559
|
+
if (!normalized) continue;
|
|
560
|
+
const tokens = finite(entry?.totalTokens ?? entry?.total_tokens)
|
|
561
|
+
+ finite(entry?.inputTokens ?? entry?.input_tokens)
|
|
562
|
+
+ finite(entry?.outputTokens ?? entry?.output_tokens);
|
|
563
|
+
if (tokens >= bestTokens) {
|
|
564
|
+
bestTokens = tokens;
|
|
565
|
+
bestName = normalized;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (bestName) return bestName;
|
|
569
|
+
}
|
|
570
|
+
return normalizeSessionModel(fallback) || "unknown";
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function extractGrokUserPrompt(update) {
|
|
574
|
+
if (!update || update.sessionUpdate !== "user_message_chunk") return null;
|
|
575
|
+
const content = update.content;
|
|
576
|
+
if (typeof content === "string") {
|
|
577
|
+
const text = content.trim();
|
|
578
|
+
return text || null;
|
|
579
|
+
}
|
|
580
|
+
if (content && typeof content === "object") {
|
|
581
|
+
if (typeof content.text === "string") {
|
|
582
|
+
const text = content.text.trim();
|
|
583
|
+
return text || null;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function extractGrokToolName(update) {
|
|
590
|
+
if (!update || update.sessionUpdate !== "tool_call") return "";
|
|
591
|
+
const metaName = update._meta?.["x.ai/tool"]?.name;
|
|
592
|
+
return canonicalToolName(metaName || update.title || "");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function listGrokSessionFiles(sessionsRoot) {
|
|
596
|
+
if (!fs.existsSync(sessionsRoot)) return [];
|
|
597
|
+
const found = [];
|
|
598
|
+
async function walk(dir, depth) {
|
|
599
|
+
if (depth > 6) return;
|
|
600
|
+
let entries;
|
|
601
|
+
try {
|
|
602
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
603
|
+
} catch {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
for (const entry of entries) {
|
|
607
|
+
if (!entry.isDirectory()) continue;
|
|
608
|
+
if (entry.name.startsWith(".")) continue;
|
|
609
|
+
const full = path.join(dir, entry.name);
|
|
610
|
+
const updatesPath = path.join(full, "updates.jsonl");
|
|
611
|
+
try {
|
|
612
|
+
const stat = await fsp.stat(updatesPath);
|
|
613
|
+
if (stat.isFile() && stat.size > 0) {
|
|
614
|
+
found.push(updatesPath);
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
} catch { /* not a session leaf */ }
|
|
618
|
+
await walk(full, depth + 1);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
await walk(sessionsRoot, 0);
|
|
622
|
+
// Deterministic order so filesSignature is stable across readdir() shuffles.
|
|
623
|
+
found.sort((a, b) => a.localeCompare(b));
|
|
624
|
+
return found;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async function scanGrokSession(filePath) {
|
|
628
|
+
const summary = readJsonFileSync(grokSummaryPathFor(filePath)) || {};
|
|
629
|
+
const signals = readJsonFileSync(grokSignalsPathFor(filePath)) || {};
|
|
630
|
+
const tokens = emptyTotals();
|
|
631
|
+
const bounds = emptyBounds();
|
|
632
|
+
const dirName = path.basename(path.dirname(filePath));
|
|
633
|
+
let observedSessionId = typeof summary?.info?.id === "string" && summary.info.id
|
|
634
|
+
? summary.info.id
|
|
635
|
+
: (/^[0-9a-f]{8}-[0-9a-f-]{27,}$/i.test(dirName) ? dirName : null);
|
|
636
|
+
let cwd = typeof summary?.info?.cwd === "string" && summary.info.cwd
|
|
637
|
+
? summary.info.cwd
|
|
638
|
+
: null;
|
|
639
|
+
// Prefer agent-authored generated_title over free-form session_summary when
|
|
640
|
+
// both exist; both are Grok metadata, never the raw user prompt body.
|
|
641
|
+
const title = cleanSessionTitle(summary.generated_title)
|
|
642
|
+
|| cleanSessionTitle(summary.session_summary)
|
|
643
|
+
|| null;
|
|
644
|
+
let model = pickGrokModel(null, signals.primaryModelId || summary.current_model_id);
|
|
645
|
+
let turns = 0;
|
|
646
|
+
let editTurns = 0;
|
|
647
|
+
let retryTurns = 0;
|
|
648
|
+
let currentHadEdit = false;
|
|
649
|
+
let subagentCalls = 0;
|
|
650
|
+
const subagentTypes = new Map();
|
|
651
|
+
let lastPromptFingerprint = null;
|
|
652
|
+
// Grok streams user_message_chunk pieces for one prompt. Accumulate until
|
|
653
|
+
// turn_completed, then fingerprint the full prompt for retry detection.
|
|
654
|
+
let openUserTurn = false;
|
|
655
|
+
let userChunkBuffer = "";
|
|
656
|
+
|
|
657
|
+
function closeTurn() {
|
|
658
|
+
if (currentHadEdit) editTurns += 1;
|
|
659
|
+
currentHadEdit = false;
|
|
660
|
+
if (userChunkBuffer) {
|
|
661
|
+
const fingerprint = promptFingerprint(userChunkBuffer);
|
|
662
|
+
if (lastPromptFingerprint && fingerprint === lastPromptFingerprint) retryTurns += 1;
|
|
663
|
+
lastPromptFingerprint = fingerprint;
|
|
664
|
+
}
|
|
665
|
+
openUserTurn = false;
|
|
666
|
+
userChunkBuffer = "";
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const input = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
670
|
+
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
671
|
+
for await (const line of lines) {
|
|
672
|
+
let obj;
|
|
673
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
674
|
+
updateBounds(bounds, grokTimestampIso(obj));
|
|
675
|
+
const params = obj.params && typeof obj.params === "object" ? obj.params : {};
|
|
676
|
+
if (typeof params.sessionId === "string" && params.sessionId) {
|
|
677
|
+
observedSessionId = params.sessionId;
|
|
678
|
+
}
|
|
679
|
+
const update = params.update && typeof params.update === "object" ? params.update : null;
|
|
680
|
+
if (!update) continue;
|
|
681
|
+
const sessionUpdate = update.sessionUpdate;
|
|
682
|
+
|
|
683
|
+
if (sessionUpdate === "user_message_chunk") {
|
|
684
|
+
const piece = extractGrokUserPrompt(update);
|
|
685
|
+
if (!piece) continue;
|
|
686
|
+
// First non-empty chunk after a closed turn opens a new user turn;
|
|
687
|
+
// later chunks only extend the fingerprint buffer.
|
|
688
|
+
if (!openUserTurn) {
|
|
689
|
+
turns += 1;
|
|
690
|
+
openUserTurn = true;
|
|
691
|
+
userChunkBuffer = piece;
|
|
692
|
+
} else {
|
|
693
|
+
userChunkBuffer += piece;
|
|
694
|
+
}
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (sessionUpdate === "tool_call") {
|
|
699
|
+
// Tools belong to the current user turn. After turn_completed closes a
|
|
700
|
+
// turn, a tool-only follow-up must open a new anonymous turn — otherwise
|
|
701
|
+
// edit_turns can exceed turns.
|
|
702
|
+
if (!openUserTurn) {
|
|
703
|
+
turns += 1;
|
|
704
|
+
openUserTurn = true;
|
|
705
|
+
}
|
|
706
|
+
const name = extractGrokToolName(update);
|
|
707
|
+
if (EDIT_TOOLS.has(name)) currentHadEdit = true;
|
|
708
|
+
if (GROK_SUBAGENT_TOOLS.has(name)) {
|
|
709
|
+
subagentCalls += 1;
|
|
710
|
+
const display = name === "spawn_agent" ? "spawn_subagent" : name;
|
|
711
|
+
subagentTypes.set(display, (subagentTypes.get(display) || 0) + 1);
|
|
712
|
+
}
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (sessionUpdate === "turn_completed") {
|
|
717
|
+
closeTurn();
|
|
718
|
+
const usage = update.usage;
|
|
719
|
+
addTotals(tokens, grokUsageTotals(usage));
|
|
720
|
+
const candidate = pickGrokModel(usage, model);
|
|
721
|
+
if (candidate && candidate !== "unknown") model = candidate;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
closeTurn();
|
|
726
|
+
|
|
727
|
+
// Prefer observed turn_completed totals. Never invent billable totals from
|
|
728
|
+
// signals.contextTokensUsed (context-window occupancy is not API usage);
|
|
729
|
+
// leave zeros so listSessionsForBrowser filters empty sessions out.
|
|
730
|
+
if (turns === 0 && finite(signals.turnCount) > 0) {
|
|
731
|
+
turns = finite(signals.turnCount);
|
|
732
|
+
}
|
|
733
|
+
if (!Number.isFinite(Date.parse(bounds.started_at || "")) && summary.created_at) {
|
|
734
|
+
updateBounds(bounds, summary.created_at);
|
|
735
|
+
}
|
|
736
|
+
if (!Number.isFinite(Date.parse(bounds.ended_at || "")) && (summary.last_active_at || summary.updated_at)) {
|
|
737
|
+
updateBounds(bounds, summary.last_active_at || summary.updated_at);
|
|
738
|
+
}
|
|
739
|
+
if ((!model || model === "unknown") && signals.primaryModelId) {
|
|
740
|
+
model = normalizeSessionModel(signals.primaryModelId) || model;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return finalizeRecord({
|
|
744
|
+
version: SIDECAR_VERSION,
|
|
745
|
+
session_hash: sessionHash("grok", observedSessionId || filePath),
|
|
746
|
+
session_id: observedSessionId || null,
|
|
747
|
+
// Local-only: stripped in summarizeSessions before any cloud/CSV export.
|
|
748
|
+
title,
|
|
749
|
+
source: "grok",
|
|
750
|
+
project_key: projectKey(cwd, filePath),
|
|
751
|
+
project_ref: cwd || null,
|
|
752
|
+
model: model || "unknown",
|
|
753
|
+
...bounds,
|
|
754
|
+
turns,
|
|
755
|
+
edit_turns: editTurns,
|
|
756
|
+
retry_turns: retryTurns,
|
|
757
|
+
subagent_calls: subagentCalls,
|
|
758
|
+
subagent_types: Object.fromEntries([...subagentTypes.entries()].sort()),
|
|
759
|
+
tokens,
|
|
760
|
+
provenance: { source: "local-session-log", confidence: "observed", retry_confidence: "inferred", content_retained: false },
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
460
764
|
async function discoverSessionFiles(home) {
|
|
461
|
-
const
|
|
765
|
+
const grokHome = resolveGrokHome(home);
|
|
766
|
+
const [allClaude, codex, archived, grok] = await Promise.all([
|
|
462
767
|
listClaudeProjectFiles(path.join(home, ".claude", "projects")),
|
|
463
768
|
listRolloutFilesDeep(path.join(home, ".codex", "sessions")),
|
|
464
769
|
listRolloutFilesDeep(path.join(home, ".codex", "archived_sessions")),
|
|
770
|
+
listGrokSessionFiles(path.join(grokHome, "sessions")),
|
|
465
771
|
]);
|
|
466
772
|
// Claude Memory stores thousands of background observer transcripts beside
|
|
467
773
|
// real Claude Code sessions. They contain <synthetic>/haiku bookkeeping and
|
|
@@ -479,7 +785,7 @@ async function discoverSessionFiles(home) {
|
|
|
479
785
|
const previous = codexBySession.get(id);
|
|
480
786
|
if (!previous || mtimeOf(filePath) > mtimeOf(previous)) codexBySession.set(id, filePath);
|
|
481
787
|
}
|
|
482
|
-
return { claude, codex: [...codexBySession.values()] };
|
|
788
|
+
return { claude, codex: [...codexBySession.values()], grok };
|
|
483
789
|
}
|
|
484
790
|
|
|
485
791
|
function filesSignature(files) {
|
|
@@ -548,10 +854,18 @@ function loadCodexTitleIndex(filePath) {
|
|
|
548
854
|
// A Codex row also depends on session_index.jsonl, not just its rollout file.
|
|
549
855
|
// Include that dependency in the incremental cache key so renaming a thread is
|
|
550
856
|
// visible after the next refresh instead of leaving a stale title indefinitely.
|
|
857
|
+
// Grok titles live in sibling summary.json (and signals can change without
|
|
858
|
+
// touching updates.jsonl on some partial flushes).
|
|
551
859
|
function analyticsEntryStatKey(source, filePath) {
|
|
552
860
|
const sessionStat = sessionFileStatKey(filePath);
|
|
553
|
-
if (!sessionStat
|
|
554
|
-
|
|
861
|
+
if (!sessionStat) return sessionStat;
|
|
862
|
+
if (source === "codex") {
|
|
863
|
+
return `${sessionStat}|title-index:${sessionFileStatKey(codexTitleIndexPathFor(filePath)) || "missing"}`;
|
|
864
|
+
}
|
|
865
|
+
if (source === "grok") {
|
|
866
|
+
return `${sessionStat}|summary:${sessionFileStatKey(grokSummaryPathFor(filePath)) || "missing"}|signals:${sessionFileStatKey(grokSignalsPathFor(filePath)) || "missing"}`;
|
|
867
|
+
}
|
|
868
|
+
return sessionStat;
|
|
555
869
|
}
|
|
556
870
|
|
|
557
871
|
function readSidecar(sidecarPath) {
|
|
@@ -587,11 +901,15 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
587
901
|
const discovered = await discoverSessionFiles(home);
|
|
588
902
|
// Codex thread titles are stored separately from rollout files. Include the
|
|
589
903
|
// index in the overall signature so an index-only rename reaches the
|
|
590
|
-
// per-file dependency check below on the next refresh.
|
|
904
|
+
// per-file dependency check below on the next refresh. Grok titles/metadata
|
|
905
|
+
// live in sibling summary.json / signals.json next to updates.jsonl.
|
|
591
906
|
const signature = filesSignature([
|
|
592
907
|
...discovered.claude,
|
|
593
908
|
...discovered.codex,
|
|
594
909
|
...discovered.codex.map(codexTitleIndexPathFor).filter(Boolean),
|
|
910
|
+
...discovered.grok,
|
|
911
|
+
...discovered.grok.map(grokSummaryPathFor),
|
|
912
|
+
...discovered.grok.map(grokSignalsPathFor),
|
|
595
913
|
]);
|
|
596
914
|
if (!force && previousMeta?.version === SIDECAR_VERSION && previousMeta.signature === signature) {
|
|
597
915
|
await writeAtomic(metaPath, `${JSON.stringify({ ...previousMeta, checked_at: new Date().toISOString() })}\n`);
|
|
@@ -612,6 +930,7 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
612
930
|
const entries = [
|
|
613
931
|
...discovered.claude.map((filePath) => ({ source: "claude", filePath, scan: scanClaudeSession })),
|
|
614
932
|
...discovered.codex.map((filePath) => ({ source: "codex", filePath, scan: scanCodexSession })),
|
|
933
|
+
...discovered.grok.map((filePath) => ({ source: "grok", filePath, scan: scanGrokSession })),
|
|
615
934
|
];
|
|
616
935
|
// Files we could not turn into a row (permission denied, half-written line,
|
|
617
936
|
// vanished mid-scan). Swallowing these silently made sessions disappear with
|
|
@@ -664,8 +983,8 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
664
983
|
return sessions;
|
|
665
984
|
}
|
|
666
985
|
|
|
667
|
-
// A cold scan walks every local Claude/Codex session file. Period switches
|
|
668
|
-
// issue overlapping requests while that scan is still running; share the
|
|
986
|
+
// A cold scan walks every local Claude/Codex/Grok session file. Period switches
|
|
987
|
+
// can issue overlapping requests while that scan is still running; share the
|
|
669
988
|
// promise per home so those requests wait for one scan instead of multiplying
|
|
670
989
|
// the disk work and racing the atomic sidecar write.
|
|
671
990
|
const sessionAnalyticsBuilds = new Map();
|
|
@@ -808,6 +1127,9 @@ function resumeCommandFor(source, sessionId) {
|
|
|
808
1127
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(id)) return null;
|
|
809
1128
|
if (source === "claude") return `claude --resume ${id}`;
|
|
810
1129
|
if (source === "codex") return `codex resume ${id}`;
|
|
1130
|
+
// Grok Build: `grok --resume <session-id>` (also accepts a title). The
|
|
1131
|
+
// command must still be run from the session's project_ref cwd.
|
|
1132
|
+
if (source === "grok") return `grok --resume ${id}`;
|
|
811
1133
|
return null;
|
|
812
1134
|
}
|
|
813
1135
|
|
|
@@ -934,6 +1256,7 @@ module.exports = {
|
|
|
934
1256
|
sessionHash,
|
|
935
1257
|
scanClaudeSession,
|
|
936
1258
|
scanCodexSession,
|
|
1259
|
+
scanGrokSession,
|
|
937
1260
|
buildSessionAnalytics,
|
|
938
1261
|
summarizeSessions,
|
|
939
1262
|
listSessionsForBrowser,
|