@ipv9/tokentracker-cli 0.39.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 (192) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +464 -0
  3. package/README.ko.md +464 -0
  4. package/README.md +476 -0
  5. package/README.zh-CN.md +469 -0
  6. package/bin/tracker.js +52 -0
  7. package/dashboard/dist/app-icon.png +0 -0
  8. package/dashboard/dist/apple-touch-icon.png +0 -0
  9. package/dashboard/dist/assets/ActivityHeatmap-BxBGHY1z.js +42 -0
  10. package/dashboard/dist/assets/Card-GROxUc_6.js +1 -0
  11. package/dashboard/dist/assets/DashboardPage-BZ5VFoBk.js +19 -0
  12. package/dashboard/dist/assets/DevicePage-CQts7ivB.js +1 -0
  13. package/dashboard/dist/assets/DialogTitle-DqFsk26U.js +1 -0
  14. package/dashboard/dist/assets/FadeIn-HBBXW_3q.js +1 -0
  15. package/dashboard/dist/assets/HeaderGithubStar-COD2HfaF.js +1 -0
  16. package/dashboard/dist/assets/IpCheckPage-Cn0l8-BQ.js +15 -0
  17. package/dashboard/dist/assets/LandingPage-CmV1BMU4.js +4356 -0
  18. package/dashboard/dist/assets/LeaderboardAvatar-CiIpWZwb.js +1 -0
  19. package/dashboard/dist/assets/LeaderboardPage-BU0mlR3g.js +6 -0
  20. package/dashboard/dist/assets/LeaderboardProfileModal-C49_3Xm9.js +72 -0
  21. package/dashboard/dist/assets/LeaderboardProfilePage-Bh1n0BAM.js +1 -0
  22. package/dashboard/dist/assets/LimitsPage-fZAmwLWT.js +2 -0
  23. package/dashboard/dist/assets/LocalOnlyNotice-D3rQdA-P.js +1 -0
  24. package/dashboard/dist/assets/LoginPage-enO4XP-c.js +1 -0
  25. package/dashboard/dist/assets/PopoverPopup-pitJEpN1.js +1 -0
  26. package/dashboard/dist/assets/Select-tT9x4ntD.js +1 -0
  27. package/dashboard/dist/assets/SelectItemText-C7F7eIm0.js +1 -0
  28. package/dashboard/dist/assets/SettingsPage-pDasZNU2.js +1 -0
  29. package/dashboard/dist/assets/SkillsPage-DEu7FY-2.js +1 -0
  30. package/dashboard/dist/assets/WidgetsPage-DGcO7rbp.js +1 -0
  31. package/dashboard/dist/assets/WrappedPage-D_1ID213.js +1 -0
  32. package/dashboard/dist/assets/agent-logos-DXQVg1xy.js +1 -0
  33. package/dashboard/dist/assets/arrow-up-right-DNLo92Ba.js +1 -0
  34. package/dashboard/dist/assets/download-DbBBBpJF.js +1 -0
  35. package/dashboard/dist/assets/geist-mono-cyrillic-400-normal-BPBWmzPh.woff +0 -0
  36. package/dashboard/dist/assets/geist-mono-cyrillic-400-normal-Ce5q_31Z.woff2 +0 -0
  37. package/dashboard/dist/assets/geist-mono-cyrillic-500-normal-CJBLNVQT.woff2 +0 -0
  38. package/dashboard/dist/assets/geist-mono-cyrillic-500-normal-mNhfPmgl.woff +0 -0
  39. package/dashboard/dist/assets/geist-mono-cyrillic-700-normal-DH5Q319x.woff +0 -0
  40. package/dashboard/dist/assets/geist-mono-cyrillic-700-normal-VCNRadI3.woff2 +0 -0
  41. package/dashboard/dist/assets/geist-mono-cyrillic-900-normal-BKC3PZkM.woff +0 -0
  42. package/dashboard/dist/assets/geist-mono-cyrillic-900-normal-DdrQ8KBw.woff2 +0 -0
  43. package/dashboard/dist/assets/geist-mono-latin-400-normal-CoULgQGM.woff +0 -0
  44. package/dashboard/dist/assets/geist-mono-latin-400-normal-LC9RFr9I.woff2 +0 -0
  45. package/dashboard/dist/assets/geist-mono-latin-500-normal-D3o2eNa9.woff2 +0 -0
  46. package/dashboard/dist/assets/geist-mono-latin-500-normal-DOxI7kZ4.woff +0 -0
  47. package/dashboard/dist/assets/geist-mono-latin-700-normal-D6izGJRP.woff2 +0 -0
  48. package/dashboard/dist/assets/geist-mono-latin-700-normal-QGw08Lff.woff +0 -0
  49. package/dashboard/dist/assets/geist-mono-latin-900-normal-CmoKXrdK.woff +0 -0
  50. package/dashboard/dist/assets/geist-mono-latin-900-normal-Cu5MFKsu.woff2 +0 -0
  51. package/dashboard/dist/assets/geist-mono-latin-ext-400-normal-Cgks_Qgx.woff2 +0 -0
  52. package/dashboard/dist/assets/geist-mono-latin-ext-400-normal-CxNRRMGd.woff +0 -0
  53. package/dashboard/dist/assets/geist-mono-latin-ext-500-normal-CQcGuCNt.woff2 +0 -0
  54. package/dashboard/dist/assets/geist-mono-latin-ext-500-normal-diTenJ8L.woff +0 -0
  55. package/dashboard/dist/assets/geist-mono-latin-ext-700-normal-BX9f1BHp.woff +0 -0
  56. package/dashboard/dist/assets/geist-mono-latin-ext-700-normal-YOllDaLV.woff2 +0 -0
  57. package/dashboard/dist/assets/geist-mono-latin-ext-900-normal-Br6WNUGb.woff +0 -0
  58. package/dashboard/dist/assets/geist-mono-latin-ext-900-normal-CrmBTzU2.woff2 +0 -0
  59. package/dashboard/dist/assets/index-BeoRn2gJ.js +2 -0
  60. package/dashboard/dist/assets/info-Bh5qTz6k.js +1 -0
  61. package/dashboard/dist/assets/main-BIY3Nj6I.js +969 -0
  62. package/dashboard/dist/assets/main-G_F9Iu_x.css +1 -0
  63. package/dashboard/dist/assets/use-limits-display-prefs-B4iJ-bBc.js +1 -0
  64. package/dashboard/dist/assets/use-native-settings-DHjPFDv8.js +1 -0
  65. package/dashboard/dist/assets/use-usage-limits-odqe9OY7.js +1 -0
  66. package/dashboard/dist/assets/useCurrency-BsSZiZMP.js +1 -0
  67. package/dashboard/dist/assets/useOpenInteractionType-BQhgq4rv.js +12 -0
  68. package/dashboard/dist/brand-logos/antigravity.svg +1 -0
  69. package/dashboard/dist/brand-logos/claude-code.svg +1 -0
  70. package/dashboard/dist/brand-logos/codebuddy.svg +1 -0
  71. package/dashboard/dist/brand-logos/codex.svg +15 -0
  72. package/dashboard/dist/brand-logos/copilot.svg +1 -0
  73. package/dashboard/dist/brand-logos/cursor.svg +1 -0
  74. package/dashboard/dist/brand-logos/gemini.svg +1 -0
  75. package/dashboard/dist/brand-logos/grok.svg +1 -0
  76. package/dashboard/dist/brand-logos/hermes.svg +1 -0
  77. package/dashboard/dist/brand-logos/kilo.svg +4 -0
  78. package/dashboard/dist/brand-logos/kimi.svg +1 -0
  79. package/dashboard/dist/brand-logos/kiro.svg +1 -0
  80. package/dashboard/dist/brand-logos/openclaw.svg +22 -0
  81. package/dashboard/dist/brand-logos/opencode.svg +3 -0
  82. package/dashboard/dist/clawd/happy.svg +161 -0
  83. package/dashboard/dist/clawd/idle/collapse.svg +101 -0
  84. package/dashboard/dist/clawd/idle/doze.svg +72 -0
  85. package/dashboard/dist/clawd/idle/follow.svg +64 -0
  86. package/dashboard/dist/clawd/idle/living.svg +196 -0
  87. package/dashboard/dist/clawd/idle/look.svg +115 -0
  88. package/dashboard/dist/clawd/idle/yawn.svg +158 -0
  89. package/dashboard/dist/clawd/mini/alert.svg +129 -0
  90. package/dashboard/dist/clawd/mini/crabwalk.svg +76 -0
  91. package/dashboard/dist/clawd/mini/enter-sleep.svg +65 -0
  92. package/dashboard/dist/clawd/mini/enter.svg +115 -0
  93. package/dashboard/dist/clawd/mini/happy.svg +124 -0
  94. package/dashboard/dist/clawd/mini/idle-tight.svg +15 -0
  95. package/dashboard/dist/clawd/mini/idle.svg +83 -0
  96. package/dashboard/dist/clawd/mini/peek.svg +82 -0
  97. package/dashboard/dist/clawd/mini/sleep.svg +112 -0
  98. package/dashboard/dist/clawd/react/double.svg +108 -0
  99. package/dashboard/dist/clawd/react/drag.svg +102 -0
  100. package/dashboard/dist/clawd/react/left.svg +102 -0
  101. package/dashboard/dist/clawd/react/right.svg +102 -0
  102. package/dashboard/dist/clawd/sleep/collapse-sleep.svg +247 -0
  103. package/dashboard/dist/clawd/sleep/sleeping.svg +118 -0
  104. package/dashboard/dist/clawd/sleep/wake.svg +277 -0
  105. package/dashboard/dist/clawd/static-base.svg +21 -0
  106. package/dashboard/dist/clawd/status/disconnected.svg +136 -0
  107. package/dashboard/dist/clawd/status/error.svg +94 -0
  108. package/dashboard/dist/clawd/status/notification.svg +134 -0
  109. package/dashboard/dist/clawd/working/building.svg +231 -0
  110. package/dashboard/dist/clawd/working/carrying.svg +107 -0
  111. package/dashboard/dist/clawd/working/conducting.svg +118 -0
  112. package/dashboard/dist/clawd/working/confused.svg +113 -0
  113. package/dashboard/dist/clawd/working/debugger.svg +91 -0
  114. package/dashboard/dist/clawd/working/juggling.svg +82 -0
  115. package/dashboard/dist/clawd/working/overheated.svg +75 -0
  116. package/dashboard/dist/clawd/working/pushing.svg +125 -0
  117. package/dashboard/dist/clawd/working/sweeping.svg +79 -0
  118. package/dashboard/dist/clawd/working/thinking.svg +119 -0
  119. package/dashboard/dist/clawd/working/typing.svg +153 -0
  120. package/dashboard/dist/clawd/working/ultrathink.svg +166 -0
  121. package/dashboard/dist/clawd/working/wizard.svg +98 -0
  122. package/dashboard/dist/dashboard-dark.png +0 -0
  123. package/dashboard/dist/favicon-16.png +0 -0
  124. package/dashboard/dist/favicon-32.png +0 -0
  125. package/dashboard/dist/favicon.ico +0 -0
  126. package/dashboard/dist/feed.xml +34 -0
  127. package/dashboard/dist/icon-192.png +0 -0
  128. package/dashboard/dist/icon-512.png +0 -0
  129. package/dashboard/dist/index.html +355 -0
  130. package/dashboard/dist/llms.txt +53 -0
  131. package/dashboard/dist/neon_bg.png +0 -0
  132. package/dashboard/dist/robots.txt +7 -0
  133. package/dashboard/dist/share.html +140 -0
  134. package/dashboard/dist/sitemap.xml +27 -0
  135. package/dashboard/dist/widgets-overview.png +0 -0
  136. package/package.json +78 -0
  137. package/scripts/install-local-service.sh +140 -0
  138. package/scripts/uninstall-local-service.sh +21 -0
  139. package/src/cli.js +95 -0
  140. package/src/commands/device-login.js +161 -0
  141. package/src/commands/diagnostics.js +38 -0
  142. package/src/commands/doctor.js +96 -0
  143. package/src/commands/init.js +1195 -0
  144. package/src/commands/serve.js +351 -0
  145. package/src/commands/status.js +609 -0
  146. package/src/commands/sync.js +2285 -0
  147. package/src/commands/uninstall.js +189 -0
  148. package/src/commands/wrapped.js +150 -0
  149. package/src/lib/antigravity-paths.js +48 -0
  150. package/src/lib/browser-auth.js +281 -0
  151. package/src/lib/categorizer-utils.js +232 -0
  152. package/src/lib/claude-categorizer.js +1319 -0
  153. package/src/lib/claude-config.js +204 -0
  154. package/src/lib/cli-ui.js +179 -0
  155. package/src/lib/codex-config.js +350 -0
  156. package/src/lib/codex-context-breakdown.js +400 -0
  157. package/src/lib/codex-rollout-parser.js +623 -0
  158. package/src/lib/codex-token-refresh.js +112 -0
  159. package/src/lib/cursor-config.js +425 -0
  160. package/src/lib/debug-flags.js +7 -0
  161. package/src/lib/diagnostics.js +272 -0
  162. package/src/lib/doctor.js +338 -0
  163. package/src/lib/fs.js +91 -0
  164. package/src/lib/gemini-config.js +288 -0
  165. package/src/lib/grok-hook.js +351 -0
  166. package/src/lib/init-flow.js +42 -0
  167. package/src/lib/local-api.js +1861 -0
  168. package/src/lib/openclaw-hook.js +420 -0
  169. package/src/lib/openclaw-session-plugin.js +520 -0
  170. package/src/lib/opencode-config.js +99 -0
  171. package/src/lib/passive-mode.js +185 -0
  172. package/src/lib/pricing/curated-overrides.json +79 -0
  173. package/src/lib/pricing/index.js +143 -0
  174. package/src/lib/pricing/litellm-fetcher.js +172 -0
  175. package/src/lib/pricing/matcher.js +255 -0
  176. package/src/lib/pricing/seed-snapshot.json +1 -0
  177. package/src/lib/progress.js +76 -0
  178. package/src/lib/project-usage-purge.js +106 -0
  179. package/src/lib/prompt.js +24 -0
  180. package/src/lib/proxy-env.js +131 -0
  181. package/src/lib/rollout.js +8442 -0
  182. package/src/lib/runtime-config.js +122 -0
  183. package/src/lib/skill-usage.js +267 -0
  184. package/src/lib/skills-manager.js +1127 -0
  185. package/src/lib/source-metadata.js +48 -0
  186. package/src/lib/sqlite-reader.js +136 -0
  187. package/src/lib/static-server.js +59 -0
  188. package/src/lib/subscriptions.js +453 -0
  189. package/src/lib/tracker-paths.js +16 -0
  190. package/src/lib/upload-throttle.js +148 -0
  191. package/src/lib/usage-limits.js +1806 -0
  192. package/src/lib/wrapped-aggregator.js +225 -0
@@ -0,0 +1,288 @@
1
+ const os = require("node:os");
2
+ const path = require("node:path");
3
+ const fs = require("node:fs/promises");
4
+
5
+ const { ensureDir, readJson, writeJson } = require("./fs");
6
+
7
+ const DEFAULT_EVENT = "SessionEnd";
8
+ const DEFAULT_HOOK_NAME = "tokentracker";
9
+ const DEFAULT_MATCHER = "exit|clear|logout|prompt_input_exit|other";
10
+
11
+ function resolveGeminiConfigDir({ home = os.homedir(), env = process.env } = {}) {
12
+ const explicit = typeof env.GEMINI_HOME === "string" ? env.GEMINI_HOME.trim() : "";
13
+ if (explicit) return path.resolve(explicit);
14
+ return path.join(home, ".gemini");
15
+ }
16
+
17
+ function resolveGeminiSettingsPath({ configDir }) {
18
+ return path.join(configDir, "settings.json");
19
+ }
20
+
21
+ async function upsertGeminiHook({
22
+ settingsPath,
23
+ hookCommand,
24
+ hookName = DEFAULT_HOOK_NAME,
25
+ matcher = DEFAULT_MATCHER,
26
+ event = DEFAULT_EVENT,
27
+ }) {
28
+ const existing = await readJson(settingsPath);
29
+ const settings = normalizeSettings(existing);
30
+ const enableResult = ensureHooksEnabled(settings);
31
+ const baseSettings = enableResult.settings;
32
+ const hooks = normalizeHooks(settings.hooks);
33
+ const entries = normalizeEntries(hooks[event]);
34
+
35
+ const normalized = normalizeEntriesForHook(entries, { hookCommand, hookName });
36
+ let nextEntries = normalized.entries;
37
+ let changed = normalized.changed || enableResult.changed;
38
+
39
+ if (!hasHook(nextEntries, { hookCommand, hookName })) {
40
+ nextEntries = nextEntries.concat([buildHookEntry({ hookCommand, hookName, matcher })]);
41
+ changed = true;
42
+ }
43
+
44
+ if (!changed) return { changed: false, backupPath: null };
45
+
46
+ const nextHooks = { ...hooks, [event]: nextEntries };
47
+ const nextSettings = { ...baseSettings, hooks: nextHooks };
48
+ const backupPath = await writeGeminiSettings({ settingsPath, settings: nextSettings });
49
+ return { changed: true, backupPath };
50
+ }
51
+
52
+ async function removeGeminiHook({
53
+ settingsPath,
54
+ hookCommand,
55
+ hookName = DEFAULT_HOOK_NAME,
56
+ event = DEFAULT_EVENT,
57
+ }) {
58
+ const existing = await readJson(settingsPath);
59
+ if (!existing) return { removed: false, skippedReason: "settings-missing" };
60
+
61
+ const settings = normalizeSettings(existing);
62
+ const hooks = normalizeHooks(settings.hooks);
63
+ const entries = normalizeEntries(hooks[event]);
64
+ if (entries.length === 0) return { removed: false, skippedReason: "hook-missing" };
65
+
66
+ let removed = false;
67
+ const nextEntries = [];
68
+ for (const entry of entries) {
69
+ const res = stripHookFromEntry(entry, { hookCommand, hookName });
70
+ if (res.removed) removed = true;
71
+ if (res.entry) nextEntries.push(res.entry);
72
+ }
73
+
74
+ if (!removed) return { removed: false, skippedReason: "hook-missing" };
75
+
76
+ const nextHooks = { ...hooks };
77
+ if (nextEntries.length > 0) nextHooks[event] = nextEntries;
78
+ else delete nextHooks[event];
79
+
80
+ const nextSettings = { ...settings };
81
+ if (Object.keys(nextHooks).length > 0) nextSettings.hooks = nextHooks;
82
+ else delete nextSettings.hooks;
83
+
84
+ const backupPath = await writeGeminiSettings({ settingsPath, settings: nextSettings });
85
+ return { removed: true, skippedReason: null, backupPath };
86
+ }
87
+
88
+ async function isGeminiHookConfigured({
89
+ settingsPath,
90
+ hookCommand,
91
+ hookName = DEFAULT_HOOK_NAME,
92
+ event = DEFAULT_EVENT,
93
+ }) {
94
+ const settings = await readJson(settingsPath);
95
+ if (!settings || typeof settings !== "object") return false;
96
+ const hooks = settings.hooks;
97
+ if (!hooks || typeof hooks !== "object") return false;
98
+ const entries = normalizeEntries(hooks[event]);
99
+ return hasHook(entries, { hookCommand, hookName });
100
+ }
101
+
102
+ function buildGeminiHookCommand(notifyPath) {
103
+ const cmd = typeof notifyPath === "string" ? notifyPath : "";
104
+ return `/usr/bin/env node ${quoteArg(cmd)} --source=gemini`;
105
+ }
106
+
107
+ function buildHookEntry({ hookCommand, hookName, matcher }) {
108
+ const hook = {
109
+ name: hookName,
110
+ type: "command",
111
+ command: hookCommand,
112
+ };
113
+ const entry = { hooks: [hook] };
114
+ if (typeof matcher === "string" && matcher.length > 0) entry.matcher = matcher;
115
+ return entry;
116
+ }
117
+
118
+ function normalizeSettings(raw) {
119
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
120
+ }
121
+
122
+ function normalizeHooks(raw) {
123
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
124
+ }
125
+
126
+ function normalizeTools(raw) {
127
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
128
+ }
129
+
130
+ function normalizeEntries(raw) {
131
+ return Array.isArray(raw) ? raw.slice() : [];
132
+ }
133
+
134
+ function normalizeCommand(cmd) {
135
+ if (Array.isArray(cmd)) return cmd.map((v) => String(v)).join("\u0000");
136
+ if (typeof cmd === "string") return cmd.trim();
137
+ return null;
138
+ }
139
+
140
+ function normalizeName(name) {
141
+ if (typeof name !== "string") return null;
142
+ const trimmed = name.trim();
143
+ return trimmed.length > 0 ? trimmed : null;
144
+ }
145
+
146
+ function ensureHooksEnabled(settings) {
147
+ const tools = normalizeTools(settings.tools);
148
+ if (tools.enableHooks === true) return { settings, changed: false };
149
+ const nextTools = { ...tools, enableHooks: true };
150
+ return { settings: { ...settings, tools: nextTools }, changed: true };
151
+ }
152
+
153
+ function hookMatches(hook, { hookCommand, hookName, requireCommand = false }) {
154
+ if (!hook || typeof hook !== "object") return false;
155
+ const name = normalizeName(hook.name);
156
+ const targetName = normalizeName(hookName);
157
+ const cmd = normalizeCommand(hook.command);
158
+ const targetCmd = normalizeCommand(hookCommand);
159
+ const commandMatches = Boolean(cmd && targetCmd && cmd === targetCmd);
160
+ if (requireCommand) return commandMatches;
161
+ const nameMatches = Boolean(name && targetName && name === targetName);
162
+ return Boolean(commandMatches || nameMatches);
163
+ }
164
+
165
+ function entryMatches(entry, { hookCommand, hookName, requireCommand = false }) {
166
+ if (!entry || typeof entry !== "object") return false;
167
+ if (entry.command || entry.name)
168
+ return hookMatches(entry, { hookCommand, hookName, requireCommand });
169
+ if (!Array.isArray(entry.hooks)) return false;
170
+ return entry.hooks.some((hook) => hookMatches(hook, { hookCommand, hookName, requireCommand }));
171
+ }
172
+
173
+ function hasHook(entries, { hookCommand, hookName }) {
174
+ return entries.some((entry) => entryMatches(entry, { hookCommand, hookName }));
175
+ }
176
+
177
+ function normalizeEntriesForHook(entries, { hookCommand, hookName }) {
178
+ let changed = false;
179
+ const nextEntries = entries.map((entry) => {
180
+ if (!entry || typeof entry !== "object") return entry;
181
+
182
+ if (entry.command || entry.name) {
183
+ if (hookMatches(entry, { hookCommand, hookName })) {
184
+ const next = normalizeHookObject(entry, { hookCommand, hookName });
185
+ if (next !== entry) changed = true;
186
+ return next;
187
+ }
188
+ return entry;
189
+ }
190
+
191
+ const hooks = Array.isArray(entry.hooks) ? entry.hooks : null;
192
+ if (!hooks) return entry;
193
+
194
+ let hooksChanged = false;
195
+ const nextHooks = hooks.map((hook) => {
196
+ if (hookMatches(hook, { hookCommand, hookName })) {
197
+ const next = normalizeHookObject(hook, { hookCommand, hookName });
198
+ if (next !== hook) hooksChanged = true;
199
+ return next;
200
+ }
201
+ return hook;
202
+ });
203
+
204
+ if (!hooksChanged) return entry;
205
+ changed = true;
206
+ return { ...entry, hooks: nextHooks };
207
+ });
208
+
209
+ return { entries: nextEntries, changed };
210
+ }
211
+
212
+ function normalizeHookObject(hook, { hookCommand, hookName }) {
213
+ const next = { ...hook };
214
+ let changed = false;
215
+
216
+ if (next.type !== "command") {
217
+ next.type = "command";
218
+ changed = true;
219
+ }
220
+
221
+ if (hookCommand && next.command !== hookCommand) {
222
+ next.command = hookCommand;
223
+ changed = true;
224
+ }
225
+
226
+ if (hookName && next.name !== hookName) {
227
+ next.name = hookName;
228
+ changed = true;
229
+ }
230
+
231
+ return changed ? next : hook;
232
+ }
233
+
234
+ function stripHookFromEntry(entry, { hookCommand, hookName }) {
235
+ if (!entry || typeof entry !== "object") return { entry, removed: false };
236
+
237
+ if (entry.command || entry.name) {
238
+ if (hookMatches(entry, { hookCommand, hookName, requireCommand: true }))
239
+ return { entry: null, removed: true };
240
+ return { entry, removed: false };
241
+ }
242
+
243
+ const hooks = Array.isArray(entry.hooks) ? entry.hooks : null;
244
+ if (!hooks) return { entry, removed: false };
245
+
246
+ const nextHooks = hooks.filter(
247
+ (hook) => !hookMatches(hook, { hookCommand, hookName, requireCommand: true }),
248
+ );
249
+ if (nextHooks.length === hooks.length) return { entry, removed: false };
250
+ if (nextHooks.length === 0) return { entry: null, removed: true };
251
+
252
+ return { entry: { ...entry, hooks: nextHooks }, removed: true };
253
+ }
254
+
255
+ function quoteArg(value) {
256
+ const v = typeof value === "string" ? value : "";
257
+ if (!v) return '""';
258
+ if (/^[A-Za-z0-9_\-./:@]+$/.test(v)) return v;
259
+ return `"${v.replace(/"/g, '\\"')}"`;
260
+ }
261
+
262
+ async function writeGeminiSettings({ settingsPath, settings }) {
263
+ await ensureDir(path.dirname(settingsPath));
264
+ let backupPath = null;
265
+ try {
266
+ const st = await fs.stat(settingsPath);
267
+ if (st && st.isFile()) {
268
+ backupPath = `${settingsPath}.bak.${new Date().toISOString().replace(/[:.]/g, "-")}`;
269
+ await fs.copyFile(settingsPath, backupPath);
270
+ }
271
+ } catch (_e) {
272
+ // Ignore missing file.
273
+ }
274
+ await writeJson(settingsPath, settings);
275
+ return backupPath;
276
+ }
277
+
278
+ module.exports = {
279
+ DEFAULT_EVENT,
280
+ DEFAULT_HOOK_NAME,
281
+ DEFAULT_MATCHER,
282
+ resolveGeminiConfigDir,
283
+ resolveGeminiSettingsPath,
284
+ buildGeminiHookCommand,
285
+ upsertGeminiHook,
286
+ removeGeminiHook,
287
+ isGeminiHookConfigured,
288
+ };
@@ -0,0 +1,351 @@
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 session metadata files
98
+ // 3. Extracts usage metadata only
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
+ const readline = require('node:readline');
106
+
107
+ const GROK_HOME = process.env.TOKENTRACKER_GROK_HOME || process.env.GROK_HOME || path.join(os.homedir(), '.grok');
108
+ const SESSION_ID = process.env.GROK_SESSION_ID;
109
+ const WORKSPACE_ROOT = process.env.GROK_WORKSPACE_ROOT || process.cwd();
110
+
111
+ if (!SESSION_ID) {
112
+ process.exit(0); // nothing to do
113
+ }
114
+
115
+ function encodeGrokCwd(cwd) {
116
+ // Grok uses encodeURIComponent on the full path, replacing / with %2F etc.
117
+ return encodeURIComponent(cwd);
118
+ }
119
+
120
+ const encodedCwd = encodeGrokCwd(WORKSPACE_ROOT);
121
+ const sessionDir = path.join(GROK_HOME, 'sessions', encodedCwd, SESSION_ID);
122
+ const signalsPath = path.join(sessionDir, 'signals.json');
123
+ const summaryPath = path.join(sessionDir, 'summary.json');
124
+ const updatesPath = path.join(sessionDir, 'updates.jsonl');
125
+
126
+ let signals = {};
127
+ try {
128
+ const raw = fs.readFileSync(signalsPath, 'utf8');
129
+ signals = JSON.parse(raw);
130
+ } catch (err) {
131
+ signals = {};
132
+ }
133
+ if (!signals || typeof signals !== 'object') {
134
+ signals = {};
135
+ }
136
+
137
+ const summary = (() => {
138
+ try {
139
+ return JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
140
+ } catch {
141
+ return {};
142
+ }
143
+ })();
144
+
145
+ function toNonNegativeFiniteNumber(value) {
146
+ const number = Number(value || 0);
147
+ return Number.isFinite(number) && number > 0 ? number : 0;
148
+ }
149
+
150
+ function timestampToIso(value) {
151
+ if (value == null) return null;
152
+ if (typeof value === 'number') {
153
+ if (!Number.isFinite(value) || value <= 0) return null;
154
+ const millis = value < 10000000000 ? value * 1000 : value;
155
+ const dt = new Date(millis);
156
+ return Number.isFinite(dt.getTime()) ? dt.toISOString() : null;
157
+ }
158
+ if (typeof value === 'string') {
159
+ const trimmed = value.trim();
160
+ if (!trimmed) return null;
161
+ if (/^[0-9]+(?:\\.[0-9]+)?$/.test(trimmed)) return timestampToIso(Number(trimmed));
162
+ const dt = new Date(trimmed);
163
+ return Number.isFinite(dt.getTime()) ? dt.toISOString() : null;
164
+ }
165
+ return null;
166
+ }
167
+
168
+ async function readUpdateTelemetry(filePath) {
169
+ let totalTokens = 0;
170
+ let lastEventId = null;
171
+ let lastEventTimestamp = null;
172
+ let lineNumber = 0;
173
+
174
+ try {
175
+ const input = fs.createReadStream(filePath, { encoding: 'utf8' });
176
+ const rl = readline.createInterface({ input, crlfDelay: Infinity });
177
+ try {
178
+ for await (const line of rl) {
179
+ lineNumber += 1;
180
+ if (!line || !line.trim()) continue;
181
+ let record = null;
182
+ try {
183
+ record = JSON.parse(line);
184
+ } catch {
185
+ continue;
186
+ }
187
+ const meta = record && record.params && record.params._meta ? record.params._meta : record && record._meta;
188
+ if (!meta || typeof meta !== 'object') continue;
189
+ const nextTotal = toNonNegativeFiniteNumber(meta.totalTokens);
190
+ if (nextTotal <= 0) continue;
191
+ totalTokens = Math.max(totalTokens, nextTotal);
192
+ lastEventId = meta.eventId != null ? String(meta.eventId) : String(lineNumber);
193
+ lastEventTimestamp =
194
+ timestampToIso(meta.agentTimestampMs) ||
195
+ timestampToIso(meta.timestampMs) ||
196
+ timestampToIso(record.timestamp_ms) ||
197
+ timestampToIso(record.timestamp) ||
198
+ lastEventTimestamp;
199
+ }
200
+ } finally {
201
+ rl.close();
202
+ }
203
+ } catch {
204
+ return { totalTokens, lastEventId, lastEventTimestamp };
205
+ }
206
+ return { totalTokens, lastEventId, lastEventTimestamp };
207
+ }
208
+
209
+ async function main() {
210
+ const contextTokensUsed = toNonNegativeFiniteNumber(signals.contextTokensUsed ?? signals.totalTokens);
211
+ const totalTokensBeforeCompaction = toNonNegativeFiniteNumber(signals.totalTokensBeforeCompaction);
212
+ const signalTotalTokens = totalTokensBeforeCompaction + contextTokensUsed;
213
+ const updateTelemetry = await readUpdateTelemetry(updatesPath);
214
+ const totalTokens = Math.max(updateTelemetry.totalTokens, signalTotalTokens);
215
+ const messageCount = toNonNegativeFiniteNumber(signals.assistantMessageCount || signals.num_chat_messages);
216
+ const model = signals.primaryModelId || (Array.isArray(signals.modelsUsed) ? signals.modelsUsed[0] : 'grok-build');
217
+ const lastActive = signals.lastActiveAt || updateTelemetry.lastEventTimestamp || summary.updated_at || new Date().toISOString();
218
+
219
+ if (totalTokens <= 0) {
220
+ process.exit(0);
221
+ }
222
+
223
+ // Write a signal that the TokenTracker sync / local API can pick up
224
+ const trackerDir = ${JSON.stringify(trackerDir)};
225
+ const signalDir = trackerDir;
226
+ try { fs.mkdirSync(signalDir, { recursive: true }); } catch {}
227
+
228
+ const signal = {
229
+ source: 'grok',
230
+ sessionId: SESSION_ID,
231
+ cwd: WORKSPACE_ROOT,
232
+ model,
233
+ totalTokens,
234
+ contextTokensUsed,
235
+ totalTokensBeforeCompaction,
236
+ messageCount,
237
+ lastActive,
238
+ sessionDir,
239
+ updatesPath,
240
+ signalsPath,
241
+ summaryPath,
242
+ lastEventId: updateTelemetry.lastEventId,
243
+ lastEventTimestamp: updateTelemetry.lastEventTimestamp,
244
+ capturedAt: new Date().toISOString()
245
+ };
246
+
247
+ const signalPath = path.join(signalDir, 'grok-last-session.json');
248
+ fs.writeFileSync(signalPath, JSON.stringify(signal, null, 2) + '\\n');
249
+
250
+ // Also touch a file so any running serve instance can react (optional)
251
+ try {
252
+ fs.writeFileSync(path.join(signalDir, 'grok-session-end.trigger'), Date.now().toString());
253
+ } catch {}
254
+
255
+ process.exit(0);
256
+ }
257
+
258
+ main().catch(() => process.exit(0));
259
+ `;
260
+ }
261
+
262
+ async function probeGrokHookState({ home = os.homedir(), trackerDir, env = process.env } = {}) {
263
+ const grokHooksDir = resolveGrokHooksDir(env);
264
+ const hookPath = path.join(grokHooksDir, GROK_HOOK_FILENAME);
265
+ const binDir = resolveTrackerBinDir(trackerDir);
266
+ const handlerPath = path.join(binDir, "grok-session-end-hook.cjs");
267
+ const legacyHandlerPath = path.join(resolveLegacyTrackerBinDir(trackerDir), "grok-session-end-hook.cjs");
268
+
269
+ const hookExists = fssync.existsSync(hookPath);
270
+ const handlerExists = fssync.existsSync(handlerPath) || fssync.existsSync(legacyHandlerPath);
271
+
272
+ let configured = false;
273
+ if (hookExists) {
274
+ try {
275
+ const content = fssync.readFileSync(hookPath, "utf8");
276
+ const json = JSON.parse(content);
277
+ configured = Boolean(json?.hooks?.SessionEnd?.[0]?.hooks?.[0]?.command?.includes("grok-session-end-hook"));
278
+ } catch {}
279
+ }
280
+
281
+ const sessionsDir = path.join(resolveGrokHome(env), "sessions");
282
+ const hasSessions = fssync.existsSync(sessionsDir);
283
+
284
+ return {
285
+ configured,
286
+ hookExists,
287
+ handlerExists,
288
+ grokHome: resolveGrokHome(env),
289
+ grokHooksDir,
290
+ hookPath,
291
+ handlerPath,
292
+ legacyHandlerPath,
293
+ hasGrokInstall: fssync.existsSync(path.join(resolveGrokHome(env), "bin", "grok")) || hasSessions,
294
+ sessionsDir
295
+ };
296
+ }
297
+
298
+ async function upsertGrokHook({ home = os.homedir(), trackerDir, env = process.env } = {}) {
299
+ const grokHooksDir = resolveGrokHooksDir(env);
300
+ const result = await ensureGrokHookFiles({ grokHooksDir, trackerDir });
301
+
302
+ const state = await probeGrokHookState({ home, trackerDir, env });
303
+ return {
304
+ ...state,
305
+ changed: true,
306
+ ...result
307
+ };
308
+ }
309
+
310
+ async function removeGrokHook({ home = os.homedir(), trackerDir, env = process.env } = {}) {
311
+ const grokHooksDir = resolveGrokHooksDir(env);
312
+ const hookPath = path.join(grokHooksDir, GROK_HOOK_FILENAME);
313
+ const binDir = resolveTrackerBinDir(trackerDir);
314
+ const handlerPath = path.join(binDir, "grok-session-end-hook.cjs");
315
+ const legacyHandlerPath = path.join(resolveLegacyTrackerBinDir(trackerDir), "grok-session-end-hook.cjs");
316
+
317
+ let removed = false;
318
+ try {
319
+ if (fssync.existsSync(hookPath)) {
320
+ fssync.unlinkSync(hookPath);
321
+ removed = true;
322
+ }
323
+ } catch {}
324
+ try {
325
+ if (fssync.existsSync(handlerPath)) {
326
+ fssync.unlinkSync(handlerPath);
327
+ removed = true;
328
+ }
329
+ } catch {}
330
+ if (legacyHandlerPath !== handlerPath) {
331
+ try {
332
+ if (fssync.existsSync(legacyHandlerPath)) {
333
+ fssync.unlinkSync(legacyHandlerPath);
334
+ removed = true;
335
+ }
336
+ } catch {}
337
+ }
338
+
339
+ return { removed, hookPath, handlerPath, legacyHandlerPath };
340
+ }
341
+
342
+ module.exports = {
343
+ resolveGrokHome,
344
+ resolveGrokHooksDir,
345
+ buildGrokSessionEndHookJson,
346
+ upsertGrokHook,
347
+ probeGrokHookState,
348
+ removeGrokHook,
349
+ GROK_HOOK_FILENAME,
350
+ buildGrokSessionEndHandler
351
+ };
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+
3
+ const { formatSummaryLine, renderBox, underline } = require("./cli-ui");
4
+
5
+ const DIVIDER = "----------------------------------------------";
6
+
7
+ function renderLocalReport({ summary, isDryRun }) {
8
+ const header = isDryRun
9
+ ? "Dry run complete. Preview only; no changes were applied."
10
+ : "Local configuration complete.";
11
+ const lines = [header, "", "Integration Status:"];
12
+ for (const item of summary || []) lines.push(formatSummaryLine(item));
13
+ process.stdout.write(`${lines.join("\n")}\n`);
14
+ }
15
+
16
+ function renderAuthTransition({ authUrl, canAutoOpen }) {
17
+ const lines = ["", DIVIDER, "", "Next: Registering device..."];
18
+ if (canAutoOpen) {
19
+ lines.push("Opening your browser to link account...");
20
+ if (authUrl) lines.push(`If it does not open, visit: ${underline(authUrl)}`);
21
+ } else {
22
+ lines.push("Open the link below to sign in.");
23
+ if (authUrl) lines.push(`Visit: ${underline(authUrl)}`);
24
+ }
25
+ lines.push("");
26
+ process.stdout.write(`${lines.join("\n")}\n`);
27
+ }
28
+
29
+ function renderSuccessBox({ configPath, dashboardUrl }) {
30
+ const identityLine = "Account linked.";
31
+ const lines = ["You are all set!", "", identityLine, `Token saved to: ${configPath}`, ""];
32
+ if (dashboardUrl) lines.push(`View your stats at: ${dashboardUrl}`);
33
+ lines.push("You can close this terminal window.");
34
+ process.stdout.write(`${renderBox(lines)}\n`);
35
+ }
36
+
37
+ module.exports = {
38
+ DIVIDER,
39
+ renderLocalReport,
40
+ renderAuthTransition,
41
+ renderSuccessBox,
42
+ };