tokentracker-cli 0.14.6 → 0.15.1
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.md +6 -4
- package/README.zh-CN.md +7 -5
- package/dashboard/dist/assets/{main-DZknCFji.js → main-DAAixEqh.js} +168 -168
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +2 -2
- package/src/commands/init.js +26 -0
- package/src/commands/status.js +12 -0
- package/src/commands/sync.js +68 -0
- package/src/commands/uninstall.js +5 -0
- package/src/lib/diagnostics.js +13 -0
- package/src/lib/grok-hook.js +272 -0
- package/src/lib/pricing/curated-overrides.json +8 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +250 -0
|
@@ -210,7 +210,7 @@
|
|
|
210
210
|
]
|
|
211
211
|
}
|
|
212
212
|
</script>
|
|
213
|
-
<script type="module" crossorigin src="/assets/main-
|
|
213
|
+
<script type="module" crossorigin src="/assets/main-DAAixEqh.js"></script>
|
|
214
214
|
<link rel="stylesheet" crossorigin href="/assets/main-B-qohcBn.css">
|
|
215
215
|
</head>
|
|
216
216
|
<body>
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"description": "Shareable Token Tracker dashboard snapshot."
|
|
52
52
|
}
|
|
53
53
|
</script>
|
|
54
|
-
<script type="module" crossorigin src="/assets/main-
|
|
54
|
+
<script type="module" crossorigin src="/assets/main-DAAixEqh.js"></script>
|
|
55
55
|
<link rel="stylesheet" crossorigin href="/assets/main-B-qohcBn.css">
|
|
56
56
|
</head>
|
|
57
57
|
<body>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokentracker-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor, Gemini, Kiro, OpenCode, OpenClaw, Every Code, Hermes, GitHub Copilot, Kimi Code, CodeBuddy, oh-my-pi, pi, Craft Agents, Kilo CLI, Kilo Code)",
|
|
3
|
+
"version": "0.15.1",
|
|
4
|
+
"description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor, Gemini, Kiro, OpenCode, OpenClaw, Every Code, Hermes, GitHub Copilot, Kimi Code, CodeBuddy, Grok Build, oh-my-pi, pi, Craft Agents, Kilo CLI, Kilo Code)",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"tokentracker-cli": "bin/tracker.js",
|
package/src/commands/init.js
CHANGED
|
@@ -43,6 +43,14 @@ const {
|
|
|
43
43
|
installOpenclawSessionPlugin,
|
|
44
44
|
probeOpenclawSessionPluginState,
|
|
45
45
|
} = require("../lib/openclaw-session-plugin");
|
|
46
|
+
const {
|
|
47
|
+
resolveGrokHome,
|
|
48
|
+
resolveGrokHooksDir,
|
|
49
|
+
upsertGrokHook,
|
|
50
|
+
probeGrokHookState,
|
|
51
|
+
removeGrokHook,
|
|
52
|
+
GROK_HOOK_FILENAME
|
|
53
|
+
} = require("../lib/grok-hook");
|
|
46
54
|
const { resolveTrackerPaths } = require("../lib/tracker-paths");
|
|
47
55
|
const {
|
|
48
56
|
resolveOmpAgentDir,
|
|
@@ -96,6 +104,7 @@ const SUPPORTED_PROVIDERS = [
|
|
|
96
104
|
"Kimi Code",
|
|
97
105
|
"oh-my-pi",
|
|
98
106
|
"CodeBuddy",
|
|
107
|
+
"Grok Build",
|
|
99
108
|
"Kilo CLI",
|
|
100
109
|
"Kilo Code",
|
|
101
110
|
];
|
|
@@ -506,6 +515,23 @@ async function applyIntegrationSetup({ home, trackerDir, notifyPath, notifyOrigi
|
|
|
506
515
|
}
|
|
507
516
|
}
|
|
508
517
|
|
|
518
|
+
// Grok Build (xAI): SessionEnd hook in ~/.grok/hooks/ + handler in ~/.tokentracker/bin/
|
|
519
|
+
{
|
|
520
|
+
try {
|
|
521
|
+
const grokState = await probeGrokHookState({ home, trackerDir, env: process.env });
|
|
522
|
+
if (grokState.hasGrokInstall) {
|
|
523
|
+
const grokRes = await upsertGrokHook({ home, trackerDir, env: process.env });
|
|
524
|
+
summary.push({
|
|
525
|
+
label: "Grok Build",
|
|
526
|
+
status: grokRes.configured ? "installed" : "detected",
|
|
527
|
+
detail: grokRes.configured ? "SessionEnd hook installed (99-tokentracker-usage.json)" : "Grok detected"
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
} catch (err) {
|
|
531
|
+
summary.push({ label: "Grok Build", status: "error", detail: String(err?.message || err) });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
509
535
|
// Kilo CLI (kilo.ai @kilocode/plugin): passive reader — no hook installation
|
|
510
536
|
// needed. Reuses OpenCode-fork SQLite schema at ~/.local/share/kilo/kilo.db
|
|
511
537
|
// (override via KILO_HOME).
|
package/src/commands/status.js
CHANGED
|
@@ -47,7 +47,9 @@ const {
|
|
|
47
47
|
resolveCraftSessionFiles,
|
|
48
48
|
resolveCraftConfigDir,
|
|
49
49
|
resolveKilocodeTaskFiles,
|
|
50
|
+
resolveGrokBuildSessions,
|
|
50
51
|
} = require("../lib/rollout");
|
|
52
|
+
const { probeGrokHookState, resolveGrokHome } = require("../lib/grok-hook");
|
|
51
53
|
|
|
52
54
|
async function cmdStatus(argv = []) {
|
|
53
55
|
const opts = parseArgs(argv);
|
|
@@ -212,6 +214,13 @@ async function cmdStatus(argv = []) {
|
|
|
212
214
|
const kilocodeTaskFiles = resolveKilocodeTaskFiles(process.env);
|
|
213
215
|
const kilocodeInstalled = kilocodeTaskFiles.length > 0;
|
|
214
216
|
|
|
217
|
+
// Grok Build (xAI TUI)
|
|
218
|
+
const grokHookState = await probeGrokHookState({ home, trackerDir, env: process.env });
|
|
219
|
+
const grokSessions = grokHookState.hasGrokInstall || grokHookState.sessionsDir
|
|
220
|
+
? resolveGrokBuildSessions(process.env)
|
|
221
|
+
: [];
|
|
222
|
+
const grokInstalled = grokHookState.hasGrokInstall || grokSessions.length > 0;
|
|
223
|
+
|
|
215
224
|
const copilotToken = readCopilotOauthToken({ home });
|
|
216
225
|
const copilotOtel = describeCopilotOtelStatus({ home, env: process.env });
|
|
217
226
|
const copilotLines = formatCopilotLines({
|
|
@@ -265,6 +274,9 @@ async function cmdStatus(argv = []) {
|
|
|
265
274
|
kilocodeInstalled
|
|
266
275
|
? `- Kilo Code (VS Code extension): passive reader (${kilocodeTaskFiles.length} task${kilocodeTaskFiles.length !== 1 ? "s" : ""} across ${new Set(kilocodeTaskFiles.map((t) => t.ide)).size} IDE${new Set(kilocodeTaskFiles.map((t) => t.ide)).size !== 1 ? "s" : ""})`
|
|
267
276
|
: null,
|
|
277
|
+
grokInstalled
|
|
278
|
+
? `- Grok Build (xAI): ${grokHookState.configured ? "hook installed" : "detected"} (${grokSessions.length} session${grokSessions.length !== 1 ? "s" : ""} found, hook: ${grokHookState.configured ? "yes" : "no"})`
|
|
279
|
+
: null,
|
|
268
280
|
...copilotLines,
|
|
269
281
|
...subscriptionLines,
|
|
270
282
|
"",
|
package/src/commands/sync.js
CHANGED
|
@@ -35,6 +35,8 @@ const {
|
|
|
35
35
|
piAgentDirCollidesWithOmp,
|
|
36
36
|
resolveCraftSessionFiles,
|
|
37
37
|
parseCraftIncremental,
|
|
38
|
+
resolveGrokBuildSessions,
|
|
39
|
+
parseGrokBuildIncremental,
|
|
38
40
|
resolveCodebuddyProjectFiles,
|
|
39
41
|
parseCodebuddyIncremental,
|
|
40
42
|
resolveKiroCliSessionFiles,
|
|
@@ -121,11 +123,24 @@ async function cmdSync(argv) {
|
|
|
121
123
|
const projectQueuePath = path.join(trackerDir, "project.queue.jsonl");
|
|
122
124
|
const projectQueueStatePath = path.join(trackerDir, "project.queue.state.json");
|
|
123
125
|
const uploadThrottlePath = path.join(trackerDir, "upload.throttle.json");
|
|
126
|
+
const grokSignalPath = path.join(trackerDir, "grok-last-session.json");
|
|
127
|
+
const legacyGrokSignalPath = path.join(trackerDir, "tracker", "grok-last-session.json");
|
|
124
128
|
|
|
125
129
|
const config = await readJson(configPath);
|
|
126
130
|
const cursors = (await readJson(cursorsPath)) || { version: 1, files: {}, updatedAt: null };
|
|
127
131
|
const uploadThrottle = normalizeUploadState(await readJson(uploadThrottlePath));
|
|
128
132
|
let uploadThrottleState = uploadThrottle;
|
|
133
|
+
let grokHookSignal = null;
|
|
134
|
+
let grokHookSignalPath = null;
|
|
135
|
+
for (const candidate of [grokSignalPath, legacyGrokSignalPath]) {
|
|
136
|
+
const signal = await readJson(candidate);
|
|
137
|
+
if (signal && typeof signal === "object") {
|
|
138
|
+
grokHookSignal = signal;
|
|
139
|
+
grokHookSignalPath = candidate;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
let grokHookSignalConsumed = false;
|
|
129
144
|
|
|
130
145
|
const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
|
|
131
146
|
const codeHome = process.env.CODE_HOME || path.join(home, ".code");
|
|
@@ -627,6 +642,54 @@ async function cmdSync(argv) {
|
|
|
627
642
|
});
|
|
628
643
|
}
|
|
629
644
|
|
|
645
|
+
// ── Grok Build (xAI) ──
|
|
646
|
+
let grokResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
647
|
+
// Full passive scan of all Grok sessions (historical + any not covered by hook)
|
|
648
|
+
const grokSessions = resolveGrokBuildSessions(process.env);
|
|
649
|
+
const grokSessionInputs = [...grokSessions];
|
|
650
|
+
if (grokHookSignal && typeof grokHookSignal === "object") {
|
|
651
|
+
const hookSessionId =
|
|
652
|
+
typeof grokHookSignal.sessionId === "string" && grokHookSignal.sessionId.trim()
|
|
653
|
+
? grokHookSignal.sessionId.trim()
|
|
654
|
+
: null;
|
|
655
|
+
if (hookSessionId) {
|
|
656
|
+
grokSessionInputs.unshift({
|
|
657
|
+
sessionId: hookSessionId,
|
|
658
|
+
signals: {
|
|
659
|
+
contextTokensUsed: grokHookSignal.totalTokens,
|
|
660
|
+
assistantMessageCount: grokHookSignal.messageCount,
|
|
661
|
+
primaryModelId: grokHookSignal.model,
|
|
662
|
+
lastActiveAt: grokHookSignal.lastActive,
|
|
663
|
+
},
|
|
664
|
+
summary: { updated_at: grokHookSignal.lastActive },
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
grokHookSignalConsumed = true;
|
|
668
|
+
}
|
|
669
|
+
if (grokSessionInputs.length > 0) {
|
|
670
|
+
if (progress?.enabled) {
|
|
671
|
+
progress.start(`Parsing Grok Build ${renderBar(0)} | buckets 0`);
|
|
672
|
+
}
|
|
673
|
+
const grokScanResult = await parseGrokBuildIncremental({
|
|
674
|
+
sessions: grokSessionInputs,
|
|
675
|
+
cursors,
|
|
676
|
+
queuePath,
|
|
677
|
+
env: process.env,
|
|
678
|
+
onProgress: (p) => {
|
|
679
|
+
if (!progress?.enabled) return;
|
|
680
|
+
const pct = p.total > 0 ? p.index / p.total : 1;
|
|
681
|
+
progress.update(
|
|
682
|
+
`Parsing Grok Build ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} sessions | buckets ${formatNumber(p.bucketsQueued)}`,
|
|
683
|
+
);
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
grokResult = {
|
|
687
|
+
recordsProcessed: grokResult.recordsProcessed + grokScanResult.recordsProcessed,
|
|
688
|
+
eventsAggregated: grokResult.eventsAggregated + grokScanResult.eventsAggregated,
|
|
689
|
+
bucketsQueued: grokResult.bucketsQueued + grokScanResult.bucketsQueued,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
630
693
|
// ── GitHub Copilot CLI (OTEL JSONL files) ──
|
|
631
694
|
let copilotResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
632
695
|
const copilotPaths = resolveCopilotOtelPaths(process.env);
|
|
@@ -666,6 +729,9 @@ async function cmdSync(argv) {
|
|
|
666
729
|
|
|
667
730
|
cursors.updatedAt = new Date().toISOString();
|
|
668
731
|
await writeJson(cursorsPath, cursors);
|
|
732
|
+
if (grokHookSignalConsumed && grokHookSignalPath) {
|
|
733
|
+
await fs.unlink(grokHookSignalPath).catch(() => {});
|
|
734
|
+
}
|
|
669
735
|
|
|
670
736
|
progress?.stop();
|
|
671
737
|
|
|
@@ -749,6 +815,7 @@ async function cmdSync(argv) {
|
|
|
749
815
|
ompResult.recordsProcessed +
|
|
750
816
|
piResult.recordsProcessed +
|
|
751
817
|
craftResult.recordsProcessed +
|
|
818
|
+
grokResult.recordsProcessed +
|
|
752
819
|
copilotResult.recordsProcessed +
|
|
753
820
|
kiloResult.messagesProcessed +
|
|
754
821
|
kilocodeResult.recordsProcessed;
|
|
@@ -767,6 +834,7 @@ async function cmdSync(argv) {
|
|
|
767
834
|
ompResult.bucketsQueued +
|
|
768
835
|
piResult.bucketsQueued +
|
|
769
836
|
craftResult.bucketsQueued +
|
|
837
|
+
grokResult.bucketsQueued +
|
|
770
838
|
copilotResult.bucketsQueued +
|
|
771
839
|
kiloResult.bucketsQueued +
|
|
772
840
|
kilocodeResult.bucketsQueued;
|
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
const { resolveOpencodeConfigDir, removeOpencodePlugin } = require("../lib/opencode-config");
|
|
14
14
|
const { removeOpenclawHookConfig } = require("../lib/openclaw-hook");
|
|
15
15
|
const { removeOpenclawSessionPluginConfig } = require("../lib/openclaw-session-plugin");
|
|
16
|
+
const { removeGrokHook } = require("../lib/grok-hook");
|
|
16
17
|
const { resolveTrackerPaths } = require("../lib/tracker-paths");
|
|
17
18
|
|
|
18
19
|
async function cmdUninstall(argv) {
|
|
@@ -79,6 +80,7 @@ async function cmdUninstall(argv) {
|
|
|
79
80
|
env: process.env,
|
|
80
81
|
});
|
|
81
82
|
const openclawHookRemove = await removeOpenclawHookConfig({ home, trackerDir, env: process.env });
|
|
83
|
+
const grokHookRemove = await removeGrokHook({ home, trackerDir, env: process.env });
|
|
82
84
|
|
|
83
85
|
// Remove installed notify handler.
|
|
84
86
|
await fs.unlink(notifyPath).catch(() => {});
|
|
@@ -147,6 +149,9 @@ async function cmdUninstall(argv) {
|
|
|
147
149
|
: openclawHookRemove?.skippedReason === "openclaw-config-missing"
|
|
148
150
|
? "- OpenClaw hook (legacy): skipped (openclaw config not found)"
|
|
149
151
|
: "- OpenClaw hook (legacy): no change",
|
|
152
|
+
grokHookRemove?.removed
|
|
153
|
+
? `- Grok Build hook removed: ${grokHookRemove.hookPath}`
|
|
154
|
+
: "- Grok Build hook: no change",
|
|
150
155
|
opts.purge ? `- Purged: ${path.join(home, ".tokentracker")}` : "- Purge: skipped (use --purge)",
|
|
151
156
|
"",
|
|
152
157
|
].join("\n"),
|
package/src/lib/diagnostics.js
CHANGED
|
@@ -15,6 +15,7 @@ const { resolveOpencodeConfigDir, isOpencodePluginInstalled } = require("./openc
|
|
|
15
15
|
const { normalizeState: normalizeUploadState } = require("./upload-throttle");
|
|
16
16
|
const { probeOpenclawHookState } = require("./openclaw-hook");
|
|
17
17
|
const { probeOpenclawSessionPluginState } = require("./openclaw-session-plugin");
|
|
18
|
+
const { probeGrokHookState } = require("./grok-hook");
|
|
18
19
|
const { resolveTrackerPaths } = require("./tracker-paths");
|
|
19
20
|
// TASK-011: Kiro CLI DB path inlined here to avoid pulling the ~4000-line
|
|
20
21
|
// rollout module on every `tokentracker status` / `diagnostics` call.
|
|
@@ -52,6 +53,10 @@ async function collectTrackerDiagnostics({
|
|
|
52
53
|
const geminiConfigDir = resolveGeminiConfigDir({ home, env: process.env });
|
|
53
54
|
const geminiSettingsPath = resolveGeminiSettingsPath({ configDir: geminiConfigDir });
|
|
54
55
|
const opencodeConfigDir = resolveOpencodeConfigDir({ home, env: process.env });
|
|
56
|
+
const grokHome =
|
|
57
|
+
process.env.TOKENTRACKER_GROK_HOME ||
|
|
58
|
+
process.env.GROK_HOME ||
|
|
59
|
+
path.join(home, ".grok");
|
|
55
60
|
|
|
56
61
|
const config = await readJson(configPath);
|
|
57
62
|
const cursors = await readJson(cursorsPath);
|
|
@@ -94,6 +99,7 @@ async function collectTrackerDiagnostics({
|
|
|
94
99
|
env: process.env,
|
|
95
100
|
});
|
|
96
101
|
const openclawHookState = await probeOpenclawHookState({ home, trackerDir, env: process.env });
|
|
102
|
+
const grokHookState = await probeGrokHookState({ home, trackerDir, env: process.env });
|
|
97
103
|
|
|
98
104
|
// Kiro IDE and Kiro CLI sub-path presence — merged under one "kiro" source
|
|
99
105
|
// at token/cost aggregation level; operators need visibility of both
|
|
@@ -137,6 +143,9 @@ async function collectTrackerDiagnostics({
|
|
|
137
143
|
claude_config: redactValue(claudeConfigPath, home),
|
|
138
144
|
gemini_config: redactValue(geminiSettingsPath, home),
|
|
139
145
|
opencode_config: redactValue(opencodeConfigDir, home),
|
|
146
|
+
grok_home: redactValue(grokHome, home),
|
|
147
|
+
grok_hooks: redactValue(grokHookState?.grokHooksDir, home),
|
|
148
|
+
grok_handler: redactValue(grokHookState?.handlerPath, home),
|
|
140
149
|
kiro_ide_dev_data: redactValue(kiroIdeDevDataDir, home),
|
|
141
150
|
kiro_cli_db: redactValue(kiroCliDbPath, home),
|
|
142
151
|
},
|
|
@@ -184,6 +193,10 @@ async function collectTrackerDiagnostics({
|
|
|
184
193
|
openclaw_hook_configured: Boolean(openclawHookState?.configured),
|
|
185
194
|
openclaw_hook_linked: Boolean(openclawHookState?.linked),
|
|
186
195
|
openclaw_hook_enabled: Boolean(openclawHookState?.enabled),
|
|
196
|
+
grok_hook_configured: Boolean(grokHookState?.configured),
|
|
197
|
+
grok_hook_exists: Boolean(grokHookState?.hookExists),
|
|
198
|
+
grok_hook_handler_exists: Boolean(grokHookState?.handlerExists),
|
|
199
|
+
grok_sessions_dir: redactValue(grokHookState?.sessionsDir, home),
|
|
187
200
|
},
|
|
188
201
|
upload: {
|
|
189
202
|
last_success_at: lastSuccessAt,
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
const os = require("node:os");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const fs = require("node:fs/promises");
|
|
4
|
+
const fssync = require("node:fs");
|
|
5
|
+
|
|
6
|
+
const GROK_HOOK_FILENAME = "99-tokentracker-usage.json";
|
|
7
|
+
|
|
8
|
+
function resolveGrokHome(env = process.env) {
|
|
9
|
+
if (env.TOKENTRACKER_GROK_HOME && env.TOKENTRACKER_GROK_HOME.length > 0) {
|
|
10
|
+
return env.TOKENTRACKER_GROK_HOME;
|
|
11
|
+
}
|
|
12
|
+
if (env.GROK_HOME && env.GROK_HOME.length > 0) {
|
|
13
|
+
return env.GROK_HOME;
|
|
14
|
+
}
|
|
15
|
+
return path.join(os.homedir(), ".grok");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveGrokHooksDir(env = process.env) {
|
|
19
|
+
return path.join(resolveGrokHome(env), "hooks");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveTrackerBinDir(trackerDir) {
|
|
23
|
+
if (!trackerDir) throw new Error("trackerDir is required");
|
|
24
|
+
return path.basename(trackerDir) === "tracker"
|
|
25
|
+
? path.join(path.dirname(trackerDir), "bin")
|
|
26
|
+
: path.join(trackerDir, "bin");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveLegacyTrackerBinDir(trackerDir) {
|
|
30
|
+
if (!trackerDir) throw new Error("trackerDir is required");
|
|
31
|
+
return path.join(trackerDir, "bin");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildGrokSessionEndHookJson({ notifyGrokHandlerPath }) {
|
|
35
|
+
// The command runs our dedicated handler.
|
|
36
|
+
// We pass the session id and cwd via environment variables that Grok already sets.
|
|
37
|
+
const cmd = `/usr/bin/env node ${shellQuote(notifyGrokHandlerPath)}`;
|
|
38
|
+
return {
|
|
39
|
+
hooks: {
|
|
40
|
+
SessionEnd: [
|
|
41
|
+
{
|
|
42
|
+
hooks: [
|
|
43
|
+
{
|
|
44
|
+
type: "command",
|
|
45
|
+
command: cmd,
|
|
46
|
+
timeout: 60
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function shellQuote(value) {
|
|
56
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function ensureGrokHookFiles({
|
|
60
|
+
grokHooksDir,
|
|
61
|
+
trackerDir,
|
|
62
|
+
handlerSource // path to the .cjs template we will write
|
|
63
|
+
} = {}) {
|
|
64
|
+
if (!grokHooksDir || !trackerDir) {
|
|
65
|
+
throw new Error("grokHooksDir and trackerDir are required");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await fs.mkdir(grokHooksDir, { recursive: true });
|
|
69
|
+
|
|
70
|
+
const hookPath = path.join(grokHooksDir, GROK_HOOK_FILENAME);
|
|
71
|
+
const binDir = resolveTrackerBinDir(trackerDir);
|
|
72
|
+
await fs.mkdir(binDir, { recursive: true });
|
|
73
|
+
|
|
74
|
+
const handlerPath = path.join(binDir, "grok-session-end-hook.cjs");
|
|
75
|
+
|
|
76
|
+
// Write the hook JSON (always overwrite to keep command up-to-date if bin path changes)
|
|
77
|
+
const hookJson = buildGrokSessionEndHookJson({ notifyGrokHandlerPath: handlerPath });
|
|
78
|
+
await fs.writeFile(hookPath, JSON.stringify(hookJson, null, 2) + "\n", "utf8");
|
|
79
|
+
|
|
80
|
+
// Write (or update) the handler script
|
|
81
|
+
const handlerSourceCode = buildGrokSessionEndHandler({ trackerDir });
|
|
82
|
+
await fs.writeFile(handlerPath, handlerSourceCode, "utf8");
|
|
83
|
+
|
|
84
|
+
// Make executable
|
|
85
|
+
try {
|
|
86
|
+
fssync.chmodSync(handlerPath, 0o755);
|
|
87
|
+
} catch {}
|
|
88
|
+
|
|
89
|
+
return { hookPath, handlerPath };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildGrokSessionEndHandler({ trackerDir }) {
|
|
93
|
+
// This is the source of the .cjs that will be executed by the Grok hook.
|
|
94
|
+
// It must be self-contained enough or rely on the copied runtime.
|
|
95
|
+
// For simplicity and reliability we write a small script that:
|
|
96
|
+
// 1. Reads GROK_SESSION_ID + GROK_WORKSPACE_ROOT from env
|
|
97
|
+
// 2. Locates the signals.json
|
|
98
|
+
// 3. Extracts usage
|
|
99
|
+
// 4. Writes a signal file under trackerDir that sync.js will pick up on next run
|
|
100
|
+
|
|
101
|
+
return `#!/usr/bin/env node
|
|
102
|
+
const fs = require('node:fs');
|
|
103
|
+
const path = require('node:path');
|
|
104
|
+
const os = require('node:os');
|
|
105
|
+
|
|
106
|
+
const GROK_HOME = process.env.TOKENTRACKER_GROK_HOME || process.env.GROK_HOME || path.join(os.homedir(), '.grok');
|
|
107
|
+
const SESSION_ID = process.env.GROK_SESSION_ID;
|
|
108
|
+
const WORKSPACE_ROOT = process.env.GROK_WORKSPACE_ROOT || process.cwd();
|
|
109
|
+
|
|
110
|
+
if (!SESSION_ID) {
|
|
111
|
+
process.exit(0); // nothing to do
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function encodeGrokCwd(cwd) {
|
|
115
|
+
// Grok uses encodeURIComponent on the full path, replacing / with %2F etc.
|
|
116
|
+
return encodeURIComponent(cwd);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const encodedCwd = encodeGrokCwd(WORKSPACE_ROOT);
|
|
120
|
+
const sessionDir = path.join(GROK_HOME, 'sessions', encodedCwd, SESSION_ID);
|
|
121
|
+
const signalsPath = path.join(sessionDir, 'signals.json');
|
|
122
|
+
const summaryPath = path.join(sessionDir, 'summary.json');
|
|
123
|
+
|
|
124
|
+
let signals = null;
|
|
125
|
+
try {
|
|
126
|
+
const raw = fs.readFileSync(signalsPath, 'utf8');
|
|
127
|
+
signals = JSON.parse(raw);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
// Session may still be active or signals not written yet; exit quietly
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const summary = (() => {
|
|
134
|
+
try {
|
|
135
|
+
return JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
|
|
136
|
+
} catch {
|
|
137
|
+
return {};
|
|
138
|
+
}
|
|
139
|
+
})();
|
|
140
|
+
|
|
141
|
+
function toNonNegativeFiniteNumber(value) {
|
|
142
|
+
const number = Number(value || 0);
|
|
143
|
+
return Number.isFinite(number) && number > 0 ? number : 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const totalTokens = toNonNegativeFiniteNumber(signals.contextTokensUsed);
|
|
147
|
+
const messageCount = toNonNegativeFiniteNumber(signals.assistantMessageCount || signals.num_chat_messages);
|
|
148
|
+
const model = signals.primaryModelId || (Array.isArray(signals.modelsUsed) ? signals.modelsUsed[0] : 'grok-build');
|
|
149
|
+
const lastActive = signals.lastActiveAt || summary.updated_at || new Date().toISOString();
|
|
150
|
+
|
|
151
|
+
if (totalTokens <= 0) {
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Write a signal that the TokenTracker sync / local API can pick up
|
|
156
|
+
const trackerDir = ${JSON.stringify(trackerDir)};
|
|
157
|
+
const signalDir = trackerDir;
|
|
158
|
+
try { fs.mkdirSync(signalDir, { recursive: true }); } catch {}
|
|
159
|
+
|
|
160
|
+
const signal = {
|
|
161
|
+
source: 'grok',
|
|
162
|
+
sessionId: SESSION_ID,
|
|
163
|
+
cwd: WORKSPACE_ROOT,
|
|
164
|
+
model,
|
|
165
|
+
totalTokens,
|
|
166
|
+
messageCount,
|
|
167
|
+
lastActive,
|
|
168
|
+
capturedAt: new Date().toISOString()
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const signalPath = path.join(signalDir, 'grok-last-session.json');
|
|
172
|
+
fs.writeFileSync(signalPath, JSON.stringify(signal, null, 2) + '\\n');
|
|
173
|
+
|
|
174
|
+
// Also touch a file so any running serve instance can react (optional)
|
|
175
|
+
try {
|
|
176
|
+
fs.writeFileSync(path.join(signalDir, 'grok-session-end.trigger'), Date.now().toString());
|
|
177
|
+
} catch {}
|
|
178
|
+
|
|
179
|
+
process.exit(0);
|
|
180
|
+
`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function probeGrokHookState({ home = os.homedir(), trackerDir, env = process.env } = {}) {
|
|
184
|
+
const grokHooksDir = resolveGrokHooksDir(env);
|
|
185
|
+
const hookPath = path.join(grokHooksDir, GROK_HOOK_FILENAME);
|
|
186
|
+
const binDir = resolveTrackerBinDir(trackerDir);
|
|
187
|
+
const handlerPath = path.join(binDir, "grok-session-end-hook.cjs");
|
|
188
|
+
const legacyHandlerPath = path.join(resolveLegacyTrackerBinDir(trackerDir), "grok-session-end-hook.cjs");
|
|
189
|
+
|
|
190
|
+
const hookExists = fssync.existsSync(hookPath);
|
|
191
|
+
const handlerExists = fssync.existsSync(handlerPath) || fssync.existsSync(legacyHandlerPath);
|
|
192
|
+
|
|
193
|
+
let configured = false;
|
|
194
|
+
if (hookExists) {
|
|
195
|
+
try {
|
|
196
|
+
const content = fssync.readFileSync(hookPath, "utf8");
|
|
197
|
+
const json = JSON.parse(content);
|
|
198
|
+
configured = Boolean(json?.hooks?.SessionEnd?.[0]?.hooks?.[0]?.command?.includes("grok-session-end-hook"));
|
|
199
|
+
} catch {}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const sessionsDir = path.join(resolveGrokHome(env), "sessions");
|
|
203
|
+
const hasSessions = fssync.existsSync(sessionsDir);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
configured,
|
|
207
|
+
hookExists,
|
|
208
|
+
handlerExists,
|
|
209
|
+
grokHome: resolveGrokHome(env),
|
|
210
|
+
grokHooksDir,
|
|
211
|
+
hookPath,
|
|
212
|
+
handlerPath,
|
|
213
|
+
legacyHandlerPath,
|
|
214
|
+
hasGrokInstall: fssync.existsSync(path.join(resolveGrokHome(env), "bin", "grok")) || hasSessions,
|
|
215
|
+
sessionsDir
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function upsertGrokHook({ home = os.homedir(), trackerDir, env = process.env } = {}) {
|
|
220
|
+
const grokHooksDir = resolveGrokHooksDir(env);
|
|
221
|
+
const result = await ensureGrokHookFiles({ grokHooksDir, trackerDir });
|
|
222
|
+
|
|
223
|
+
const state = await probeGrokHookState({ home, trackerDir, env });
|
|
224
|
+
return {
|
|
225
|
+
...state,
|
|
226
|
+
changed: true,
|
|
227
|
+
...result
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function removeGrokHook({ home = os.homedir(), trackerDir, env = process.env } = {}) {
|
|
232
|
+
const grokHooksDir = resolveGrokHooksDir(env);
|
|
233
|
+
const hookPath = path.join(grokHooksDir, GROK_HOOK_FILENAME);
|
|
234
|
+
const binDir = resolveTrackerBinDir(trackerDir);
|
|
235
|
+
const handlerPath = path.join(binDir, "grok-session-end-hook.cjs");
|
|
236
|
+
const legacyHandlerPath = path.join(resolveLegacyTrackerBinDir(trackerDir), "grok-session-end-hook.cjs");
|
|
237
|
+
|
|
238
|
+
let removed = false;
|
|
239
|
+
try {
|
|
240
|
+
if (fssync.existsSync(hookPath)) {
|
|
241
|
+
fssync.unlinkSync(hookPath);
|
|
242
|
+
removed = true;
|
|
243
|
+
}
|
|
244
|
+
} catch {}
|
|
245
|
+
try {
|
|
246
|
+
if (fssync.existsSync(handlerPath)) {
|
|
247
|
+
fssync.unlinkSync(handlerPath);
|
|
248
|
+
removed = true;
|
|
249
|
+
}
|
|
250
|
+
} catch {}
|
|
251
|
+
if (legacyHandlerPath !== handlerPath) {
|
|
252
|
+
try {
|
|
253
|
+
if (fssync.existsSync(legacyHandlerPath)) {
|
|
254
|
+
fssync.unlinkSync(legacyHandlerPath);
|
|
255
|
+
removed = true;
|
|
256
|
+
}
|
|
257
|
+
} catch {}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { removed, hookPath, handlerPath, legacyHandlerPath };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = {
|
|
264
|
+
resolveGrokHome,
|
|
265
|
+
resolveGrokHooksDir,
|
|
266
|
+
buildGrokSessionEndHookJson,
|
|
267
|
+
upsertGrokHook,
|
|
268
|
+
probeGrokHookState,
|
|
269
|
+
removeGrokHook,
|
|
270
|
+
GROK_HOOK_FILENAME,
|
|
271
|
+
buildGrokSessionEndHandler
|
|
272
|
+
};
|
|
@@ -18,6 +18,14 @@
|
|
|
18
18
|
"deepseek-v4-flash":{ "input": 0.14, "output": 0.28, "cache_read": 0.0028, "cache_write": 0.14 },
|
|
19
19
|
"deepseek-v4-pro": { "input": 0.435,"output": 0.87, "cache_read": 0.003625, "cache_write": 0.435 },
|
|
20
20
|
"deepseek-chat": { "input": 0.14, "output": 0.28, "cache_read": 0.0028, "cache_write": 0.14 },
|
|
21
|
+
"grok-build": { "input": 1.25, "output": 2.50, "cache_read": 0.20, "note": "Grok Build TUI estimate. signals.json currently exposes contextTokensUsed snapshots, so TokenTracker estimates input/output split until Grok exposes per-call telemetry." },
|
|
22
|
+
"grok-4-0709": { "input": 3.00, "output": 15.00, "cache_read": 0.75 },
|
|
23
|
+
"grok-4": { "input": 3.00, "output": 15.00, "cache_read": 0.75 },
|
|
24
|
+
"grok-4-latest": { "input": 3.00, "output": 15.00, "cache_read": 0.75 },
|
|
25
|
+
"grok-4-fast": { "input": 0.20, "output": 0.50, "cache_read": 0.05 },
|
|
26
|
+
"grok-4-fast-reasoning": { "input": 0.20, "output": 0.50, "cache_read": 0.05 },
|
|
27
|
+
"grok-4-fast-non-reasoning": { "input": 0.20, "output": 0.50, "cache_read": 0.05 },
|
|
28
|
+
"grok-4-1-fast-non-reasoning": { "input": 0.20, "output": 0.50, "cache_read": 0.05 },
|
|
21
29
|
"deepseek-reasoner":{ "input": 0.14, "output": 0.28, "cache_read": 0.0028, "cache_write": 0.14 },
|
|
22
30
|
"kimi-for-coding": { "input": 0.6, "output": 2, "cache_read": 0.15 },
|
|
23
31
|
"kimi-k2.5": { "input": 0.6, "output": 2, "cache_read": 0.15 },
|