tokentracker-cli 0.14.5 → 0.15.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.
@@ -210,7 +210,7 @@
210
210
  ]
211
211
  }
212
212
  </script>
213
- <script type="module" crossorigin src="/assets/main-BZ54G0n1.js"></script>
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-BZ54G0n1.js"></script>
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.14.5",
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.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, 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",
@@ -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,
@@ -506,6 +514,23 @@ async function applyIntegrationSetup({ home, trackerDir, notifyPath, notifyOrigi
506
514
  }
507
515
  }
508
516
 
517
+ // Grok Build (xAI): SessionEnd hook in ~/.grok/hooks/ + handler in ~/.tokentracker/bin/
518
+ {
519
+ try {
520
+ const grokState = await probeGrokHookState({ home, trackerDir, env: process.env });
521
+ if (grokState.hasGrokInstall) {
522
+ const grokRes = await upsertGrokHook({ home, trackerDir, env: process.env });
523
+ summary.push({
524
+ label: "Grok Build",
525
+ status: grokRes.configured ? "installed" : "detected",
526
+ detail: grokRes.configured ? "SessionEnd hook installed (99-tokentracker-usage.json)" : "Grok detected"
527
+ });
528
+ }
529
+ } catch (err) {
530
+ summary.push({ label: "Grok Build", status: "error", detail: String(err?.message || err) });
531
+ }
532
+ }
533
+
509
534
  // Kilo CLI (kilo.ai @kilocode/plugin): passive reader — no hook installation
510
535
  // needed. Reuses OpenCode-fork SQLite schema at ~/.local/share/kilo/kilo.db
511
536
  // (override via KILO_HOME).
@@ -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
  "",
@@ -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"),
@@ -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 },