@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,189 @@
1
+ const os = require("node:os");
2
+ const path = require("node:path");
3
+ const fs = require("node:fs/promises");
4
+
5
+ const { restoreCodexNotify, restoreEveryCodeNotify } = require("../lib/codex-config");
6
+ const { removeClaudeHook, buildClaudeHookCommand, buildHookCommand } = require("../lib/claude-config");
7
+ const {
8
+ resolveGeminiConfigDir,
9
+ resolveGeminiSettingsPath,
10
+ buildGeminiHookCommand,
11
+ removeGeminiHook,
12
+ } = require("../lib/gemini-config");
13
+ const { resolveOpencodeConfigDir, removeOpencodePlugin } = require("../lib/opencode-config");
14
+ const { removeOpenclawHookConfig } = require("../lib/openclaw-hook");
15
+ const { removeOpenclawSessionPluginConfig } = require("../lib/openclaw-session-plugin");
16
+ const { removeGrokHook } = require("../lib/grok-hook");
17
+ const { resolveTrackerPaths } = require("../lib/tracker-paths");
18
+
19
+ async function cmdUninstall(argv) {
20
+ const opts = parseArgs(argv);
21
+ const home = os.homedir();
22
+ const { trackerDir, binDir } = await resolveTrackerPaths({ home });
23
+ const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
24
+ const codexConfigPath = path.join(codexHome, "config.toml");
25
+ const codeHome = process.env.CODE_HOME || path.join(home, ".code");
26
+ const codeConfigPath = path.join(codeHome, "config.toml");
27
+ const claudeSettingsPath = path.join(home, ".claude", "settings.json");
28
+ const codebuddyDir = process.env.CODEBUDDY_HOME || path.join(home, ".codebuddy");
29
+ const codebuddySettingsPath = path.join(codebuddyDir, "settings.json");
30
+ const geminiConfigDir = resolveGeminiConfigDir({ home, env: process.env });
31
+ const geminiSettingsPath = resolveGeminiSettingsPath({ configDir: geminiConfigDir });
32
+ const opencodeConfigDir = resolveOpencodeConfigDir({ home, env: process.env });
33
+ const notifyPath = path.join(binDir, "notify.cjs");
34
+ const notifyOriginalPath = path.join(trackerDir, "codex_notify_original.json");
35
+ const codeNotifyOriginalPath = path.join(trackerDir, "code_notify_original.json");
36
+ const codexNotifyCmd = ["/usr/bin/env", "node", notifyPath];
37
+ const codeNotifyCmd = ["/usr/bin/env", "node", notifyPath, "--source=every-code"];
38
+ const claudeHookCommand = buildClaudeHookCommand(notifyPath);
39
+ const codebuddyHookCommand = buildHookCommand(notifyPath, "codebuddy");
40
+ const geminiHookCommand = buildGeminiHookCommand(notifyPath);
41
+
42
+ const codexConfigExists = await isFile(codexConfigPath);
43
+ const codeConfigExists = await isFile(codeConfigPath);
44
+ const claudeConfigExists = await isFile(claudeSettingsPath);
45
+ const codebuddyConfigExists = await isFile(codebuddySettingsPath);
46
+ const geminiConfigExists = await isDir(geminiConfigDir);
47
+ const opencodeConfigExists = await isDir(opencodeConfigDir);
48
+ const codexRestore = codexConfigExists
49
+ ? await restoreCodexNotify({
50
+ codexConfigPath,
51
+ notifyOriginalPath,
52
+ notifyCmd: codexNotifyCmd,
53
+ })
54
+ : { restored: false, skippedReason: "config-missing" };
55
+ const codeRestore = codeConfigExists
56
+ ? await restoreEveryCodeNotify({
57
+ codeConfigPath,
58
+ notifyOriginalPath: codeNotifyOriginalPath,
59
+ notifyCmd: codeNotifyCmd,
60
+ })
61
+ : { restored: false, skippedReason: "config-missing" };
62
+ const claudeRemove = claudeConfigExists
63
+ ? await removeClaudeHook({ settingsPath: claudeSettingsPath, hookCommand: claudeHookCommand })
64
+ : { removed: false, skippedReason: "config-missing" };
65
+ const codebuddyRemove = codebuddyConfigExists
66
+ ? await removeClaudeHook({
67
+ settingsPath: codebuddySettingsPath,
68
+ hookCommand: codebuddyHookCommand,
69
+ })
70
+ : { removed: false, skippedReason: "config-missing" };
71
+ const geminiRemove = geminiConfigExists
72
+ ? await removeGeminiHook({ settingsPath: geminiSettingsPath, hookCommand: geminiHookCommand })
73
+ : { removed: false, skippedReason: "config-missing" };
74
+ const opencodeRemove = opencodeConfigExists
75
+ ? await removeOpencodePlugin({ configDir: opencodeConfigDir })
76
+ : { removed: false, skippedReason: "config-missing" };
77
+ const openclawSessionPluginRemove = await removeOpenclawSessionPluginConfig({
78
+ home,
79
+ trackerDir,
80
+ env: process.env,
81
+ });
82
+ const openclawHookRemove = await removeOpenclawHookConfig({ home, trackerDir, env: process.env });
83
+ const grokHookRemove = await removeGrokHook({ home, trackerDir, env: process.env });
84
+
85
+ // Remove installed notify handler.
86
+ await fs.unlink(notifyPath).catch(() => {});
87
+
88
+ // Remove local app runtime (installed by init for notify-driven sync).
89
+ await fs.rm(path.join(trackerDir, "app"), { recursive: true, force: true }).catch(() => {});
90
+
91
+ if (opts.purge) {
92
+ await fs.rm(path.join(home, ".tokentracker"), { recursive: true, force: true }).catch(() => {});
93
+ }
94
+
95
+ process.stdout.write(
96
+ [
97
+ "Uninstalled:",
98
+ codexConfigExists
99
+ ? codexRestore?.restored
100
+ ? `- Codex notify restored: ${codexConfigPath}`
101
+ : codexRestore?.skippedReason === "no-backup-not-installed"
102
+ ? "- Codex notify: skipped (no backup; not installed)"
103
+ : "- Codex notify: no change"
104
+ : "- Codex notify: skipped (config.toml not found)",
105
+ codeConfigExists
106
+ ? codeRestore?.restored
107
+ ? `- Every Code notify restored: ${codeConfigPath}`
108
+ : codeRestore?.skippedReason === "no-backup-not-installed"
109
+ ? "- Every Code notify: skipped (no backup; not installed)"
110
+ : "- Every Code notify: no change"
111
+ : "- Every Code notify: skipped (config.toml not found)",
112
+ claudeConfigExists
113
+ ? claudeRemove?.removed
114
+ ? `- Claude hooks removed: ${claudeSettingsPath}`
115
+ : claudeRemove?.skippedReason === "hook-missing"
116
+ ? "- Claude hooks: no change"
117
+ : "- Claude hooks: skipped"
118
+ : "- Claude hooks: skipped (settings.json not found)",
119
+ codebuddyConfigExists
120
+ ? codebuddyRemove?.removed
121
+ ? `- CodeBuddy hooks removed: ${codebuddySettingsPath}`
122
+ : codebuddyRemove?.skippedReason === "hook-missing"
123
+ ? "- CodeBuddy hooks: no change"
124
+ : "- CodeBuddy hooks: skipped"
125
+ : "- CodeBuddy hooks: skipped (settings.json not found)",
126
+ geminiConfigExists
127
+ ? geminiRemove?.removed
128
+ ? `- Gemini hooks removed: ${geminiSettingsPath}`
129
+ : geminiRemove?.skippedReason === "hook-missing"
130
+ ? "- Gemini hooks: no change"
131
+ : "- Gemini hooks: skipped"
132
+ : `- Gemini hooks: skipped (${geminiConfigDir} not found)`,
133
+ opencodeConfigExists
134
+ ? opencodeRemove?.removed
135
+ ? `- Opencode plugin removed: ${opencodeConfigDir}`
136
+ : opencodeRemove?.skippedReason === "plugin-missing"
137
+ ? "- Opencode plugin: no change"
138
+ : opencodeRemove?.skippedReason === "unexpected-content"
139
+ ? "- Opencode plugin: skipped (unexpected content)"
140
+ : "- Opencode plugin: skipped"
141
+ : `- Opencode plugin: skipped (${opencodeConfigDir} not found)`,
142
+ openclawSessionPluginRemove?.removed
143
+ ? `- OpenClaw session plugin removed: ${openclawSessionPluginRemove.openclawConfigPath}`
144
+ : openclawSessionPluginRemove?.skippedReason === "openclaw-config-missing"
145
+ ? "- OpenClaw session plugin: skipped (openclaw config not found)"
146
+ : "- OpenClaw session plugin: no change",
147
+ openclawHookRemove?.removed
148
+ ? `- OpenClaw hook (legacy) removed: ${openclawHookRemove.openclawConfigPath}`
149
+ : openclawHookRemove?.skippedReason === "openclaw-config-missing"
150
+ ? "- OpenClaw hook (legacy): skipped (openclaw config not found)"
151
+ : "- OpenClaw hook (legacy): no change",
152
+ grokHookRemove?.removed
153
+ ? `- Grok Build hook removed: ${grokHookRemove.hookPath}`
154
+ : "- Grok Build hook: no change",
155
+ opts.purge ? `- Purged: ${path.join(home, ".tokentracker")}` : "- Purge: skipped (use --purge)",
156
+ "",
157
+ ].join("\n"),
158
+ );
159
+ }
160
+
161
+ function parseArgs(argv) {
162
+ const out = { purge: false };
163
+ for (let i = 0; i < argv.length; i++) {
164
+ const a = argv[i];
165
+ if (a === "--purge") out.purge = true;
166
+ else throw new Error(`Unknown option: ${a}`);
167
+ }
168
+ return out;
169
+ }
170
+
171
+ module.exports = { cmdUninstall };
172
+
173
+ async function isFile(p) {
174
+ try {
175
+ const st = await fs.stat(p);
176
+ return st.isFile();
177
+ } catch (_e) {
178
+ return false;
179
+ }
180
+ }
181
+
182
+ async function isDir(p) {
183
+ try {
184
+ const st = await fs.stat(p);
185
+ return st.isDirectory();
186
+ } catch (_e) {
187
+ return false;
188
+ }
189
+ }
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+
3
+ const os = require("node:os");
4
+ const path = require("node:path");
5
+ const fs = require("node:fs");
6
+
7
+ const { resolveTrackerPaths } = require("../lib/tracker-paths");
8
+ const { aggregateWrapped, formatCompact } = require("../lib/wrapped-aggregator");
9
+
10
+ async function readQueueRows(queuePath) {
11
+ if (!fs.existsSync(queuePath)) return [];
12
+ const raw = await fs.promises.readFile(queuePath, "utf8");
13
+ const rows = [];
14
+ for (const line of raw.split("\n")) {
15
+ if (!line.trim()) continue;
16
+ try {
17
+ rows.push(JSON.parse(line));
18
+ } catch (_e) {
19
+ // skip malformed line — production queues sometimes hold a partial
20
+ // tail row mid-write that becomes valid on the next read.
21
+ }
22
+ }
23
+ return rows;
24
+ }
25
+
26
+ function parseArgs(argv) {
27
+ const out = { year: null, json: false };
28
+ for (let i = 0; i < argv.length; i++) {
29
+ const a = argv[i];
30
+ if (a === "--json") out.json = true;
31
+ else if (a === "--year") {
32
+ const v = argv[++i];
33
+ const y = Number(v);
34
+ if (!Number.isInteger(y) || y < 2000 || y > 2100) {
35
+ throw new Error(`--year expects a 4-digit year, got: ${v}`);
36
+ }
37
+ out.year = y;
38
+ } else throw new Error(`Unknown option: ${a}`);
39
+ }
40
+ return out;
41
+ }
42
+
43
+ function renderAscii(wrapped) {
44
+ const lines = [];
45
+ const w = 60;
46
+ const border = "═".repeat(w - 2);
47
+ const center = (s) => {
48
+ const text = String(s);
49
+ const padTotal = Math.max(0, w - 2 - text.length);
50
+ const left = Math.floor(padTotal / 2);
51
+ const right = padTotal - left;
52
+ return `║${" ".repeat(left)}${text}${" ".repeat(right)}║`;
53
+ };
54
+ const row = (label, value) => {
55
+ const text = ` ${label}: ${value}`;
56
+ const right = Math.max(0, w - 2 - text.length);
57
+ return `║${text}${" ".repeat(right)}║`;
58
+ };
59
+
60
+ lines.push(`╔${border}╗`);
61
+ lines.push(center(""));
62
+ lines.push(center(`TokenTracker Wrapped · ${wrapped.year}`));
63
+ lines.push(center(""));
64
+ lines.push(`╠${border}╣`);
65
+ lines.push(row("Total tokens", formatCompact(wrapped.totals.tokens)));
66
+ lines.push(row("Conversations", wrapped.totals.conversations.toLocaleString("en-US")));
67
+ lines.push(row("Active days", `${wrapped.totals.active_days} / 365`));
68
+ lines.push(row("Tools used", String(wrapped.totals.sources)));
69
+ lines.push(row("Models used", String(wrapped.totals.models)));
70
+ if (wrapped.peak_hour) {
71
+ lines.push(
72
+ row(
73
+ "Peak hour",
74
+ `${String(wrapped.peak_hour.hour).padStart(2, "0")}:00 UTC (${formatCompact(wrapped.peak_hour.tokens)})`,
75
+ ),
76
+ );
77
+ }
78
+ if (wrapped.longest_streak.days > 0) {
79
+ lines.push(
80
+ row("Longest streak", `${wrapped.longest_streak.days} days (${wrapped.longest_streak.from} → ${wrapped.longest_streak.to})`),
81
+ );
82
+ }
83
+ lines.push(`╠${border}╣`);
84
+
85
+ if (wrapped.top.sources.length > 0) {
86
+ lines.push(row("Top tools", ""));
87
+ for (const s of wrapped.top.sources.slice(0, 3)) {
88
+ lines.push(row(` ${s.source}`, `${formatCompact(s.tokens)} (${(s.share * 100).toFixed(0)}%)`));
89
+ }
90
+ }
91
+ if (wrapped.top.models.length > 0) {
92
+ lines.push(`╠${border}╣`);
93
+ lines.push(row("Top models", ""));
94
+ for (const m of wrapped.top.models.slice(0, 3)) {
95
+ lines.push(row(` ${m.model}`, `${formatCompact(m.tokens)} (${(m.share * 100).toFixed(0)}%)`));
96
+ }
97
+ }
98
+ if (wrapped.top.days.length > 0) {
99
+ lines.push(`╠${border}╣`);
100
+ lines.push(row("Top days", ""));
101
+ for (const d of wrapped.top.days.slice(0, 3)) {
102
+ lines.push(row(` ${d.day}`, formatCompact(d.tokens)));
103
+ }
104
+ }
105
+ if (wrapped.highlights.length > 0) {
106
+ lines.push(`╠${border}╣`);
107
+ const max = w - 4; // 2 borders + 2 padding
108
+ for (const h of wrapped.highlights) {
109
+ // Wrap long highlights into multiple rows.
110
+ for (let start = 0; start < h.length; start += max) {
111
+ const segment = h.slice(start, start + max);
112
+ const padded = ` ${segment}`;
113
+ const right = Math.max(0, w - 2 - padded.length);
114
+ lines.push(`║${padded}${" ".repeat(right)}║`);
115
+ }
116
+ }
117
+ }
118
+ lines.push(`╚${border}╝`);
119
+ return lines.join("\n");
120
+ }
121
+
122
+ async function cmdWrapped(argv = []) {
123
+ const opts = parseArgs(argv);
124
+ const home = os.homedir();
125
+ const { trackerDir } = await resolveTrackerPaths({ home });
126
+ const queuePath = path.join(trackerDir, "queue.jsonl");
127
+
128
+ const rows = await readQueueRows(queuePath);
129
+ if (rows.length === 0) {
130
+ if (opts.json) {
131
+ process.stdout.write(JSON.stringify({ error: "no data" }, null, 2) + "\n");
132
+ } else {
133
+ process.stdout.write(
134
+ "No queue data found yet. Run `tracker sync` first to ingest some history.\n",
135
+ );
136
+ }
137
+ return;
138
+ }
139
+
140
+ const wrapped = aggregateWrapped(rows, opts.year ? { year: opts.year } : {});
141
+
142
+ if (opts.json) {
143
+ process.stdout.write(JSON.stringify(wrapped, null, 2) + "\n");
144
+ return;
145
+ }
146
+
147
+ process.stdout.write(renderAscii(wrapped) + "\n");
148
+ }
149
+
150
+ module.exports = { cmdWrapped };
@@ -0,0 +1,48 @@
1
+ const fs = require("node:fs");
2
+ const os = require("node:os");
3
+ const path = require("node:path");
4
+
5
+ const MAIN_APP_SUBDIR = "antigravity";
6
+ const IDE_SUBDIR = "antigravity-ide";
7
+
8
+ function geminiHome(env) {
9
+ return path.join(env.HOME || env.USERPROFILE || os.homedir(), ".gemini");
10
+ }
11
+
12
+ function trimmed(value) {
13
+ return typeof value === "string" ? value.trim() : "";
14
+ }
15
+
16
+ function existsDir(dirPath) {
17
+ try {
18
+ return fs.statSync(dirPath).isDirectory();
19
+ } catch (_e) {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ function resolveAntigravitySkillDirs(env = process.env) {
25
+ const override = trimmed(env.TOKENTRACKER_ANTIGRAVITY_HOME);
26
+ if (override) return [path.join(override, "skills")];
27
+
28
+ const home = geminiHome(env);
29
+ const mainSkills = path.join(home, MAIN_APP_SUBDIR, "skills");
30
+ const ideSkills = path.join(home, IDE_SUBDIR, "skills");
31
+ const mainParent = path.join(home, MAIN_APP_SUBDIR);
32
+ const ideParent = path.join(home, IDE_SUBDIR);
33
+
34
+ const dirs = [];
35
+ if (existsDir(mainParent)) dirs.push(mainSkills);
36
+ if (existsDir(ideParent)) dirs.push(ideSkills);
37
+
38
+ // Neither Antigravity install present: fall back to the main-app path so
39
+ // targetList still surfaces a stable path string and the user can install
40
+ // skills now and run Antigravity later. The empty parent will be created on
41
+ // demand by ensureDir; cleanup happens naturally via uninstallSkill.
42
+ if (dirs.length > 0) return dirs;
43
+ return [mainSkills];
44
+ }
45
+
46
+ module.exports = {
47
+ resolveAntigravitySkillDirs,
48
+ };
@@ -0,0 +1,281 @@
1
+ const http = require("node:http");
2
+ const crypto = require("node:crypto");
3
+ const cp = require("node:child_process");
4
+
5
+ const { DEFAULT_BASE_URL } = require("./runtime-config");
6
+
7
+ async function beginBrowserAuth({ baseUrl, dashboardUrl, timeoutMs, open }) {
8
+ const nonce = crypto.randomBytes(16).toString("hex");
9
+ const callbackPath = `/tokentracker/callback/${nonce}`;
10
+ const authUrl = dashboardUrl ? new URL("/", dashboardUrl) : new URL("/auth/sign-up", baseUrl);
11
+ const postAuthRedirect = resolvePostAuthRedirect({ dashboardUrl, authUrl });
12
+ const { callbackUrl, waitForCallback } = await startLocalCallbackServer({
13
+ callbackPath,
14
+ timeoutMs,
15
+ redirectUrl: postAuthRedirect,
16
+ });
17
+ authUrl.searchParams.set("redirect", callbackUrl);
18
+ if (dashboardUrl && baseUrl && baseUrl !== DEFAULT_BASE_URL) {
19
+ authUrl.searchParams.set("base_url", baseUrl);
20
+ }
21
+
22
+ if (open !== false) openInBrowser(authUrl.toString());
23
+
24
+ return { authUrl: authUrl.toString(), waitForCallback };
25
+ }
26
+
27
+ async function startLocalCallbackServer({ callbackPath, timeoutMs, redirectUrl }) {
28
+ let resolved = false;
29
+ let resolveResult;
30
+ let rejectResult;
31
+
32
+ const resultPromise = new Promise((resolve, reject) => {
33
+ resolveResult = resolve;
34
+ rejectResult = reject;
35
+ });
36
+
37
+ const server = http.createServer((req, res) => {
38
+ if (resolved) {
39
+ res.writeHead(409, { "Content-Type": "text/plain; charset=utf-8" });
40
+ res.end("Already authenticated.\n");
41
+ return;
42
+ }
43
+
44
+ const method = req.method || "GET";
45
+ if (method !== "GET") {
46
+ res.writeHead(405, { "Content-Type": "text/plain; charset=utf-8" });
47
+ res.end("Method not allowed.\n");
48
+ return;
49
+ }
50
+
51
+ const url = new URL(req.url || "/", "http://127.0.0.1");
52
+ if (url.pathname !== callbackPath) {
53
+ res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
54
+ res.end("Not found.\n");
55
+ return;
56
+ }
57
+
58
+ const accessToken = url.searchParams.get("access_token") || "";
59
+ if (!accessToken) {
60
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
61
+ res.end("Missing access_token.\n");
62
+ return;
63
+ }
64
+
65
+ resolved = true;
66
+ if (redirectUrl) {
67
+ res.writeHead(302, {
68
+ Location: redirectUrl,
69
+ "Content-Type": "text/html; charset=utf-8",
70
+ });
71
+ res.end(
72
+ [
73
+ "<!doctype html>",
74
+ '<html><head><meta charset="utf-8"><title>TokenTracker</title></head>',
75
+ "<body>",
76
+ "<h2>Login succeeded</h2>",
77
+ `<p>Redirecting to <a href="${redirectUrl}">dashboard</a>...</p>`,
78
+ "</body></html>",
79
+ ].join(""),
80
+ );
81
+ } else {
82
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
83
+ res.end(
84
+ [
85
+ "<!doctype html>",
86
+ '<html><head><meta charset="utf-8"><title>TokenTracker</title></head>',
87
+ "<body>",
88
+ "<h2>Login succeeded</h2>",
89
+ "<p>You can close this tab and return to the CLI.</p>",
90
+ "</body></html>",
91
+ ].join(""),
92
+ );
93
+ }
94
+
95
+ resolveResult({
96
+ accessToken,
97
+ userId: url.searchParams.get("user_id") || null,
98
+ email: url.searchParams.get("email") || null,
99
+ name: url.searchParams.get("name") || null,
100
+ });
101
+ });
102
+
103
+ await new Promise((resolve, reject) => {
104
+ server.once("error", reject);
105
+ server.listen(0, "127.0.0.1", resolve);
106
+ });
107
+
108
+ const addr = server.address();
109
+ const port = typeof addr === "object" && addr ? addr.port : null;
110
+ if (!port) {
111
+ server.close();
112
+ throw new Error("Failed to bind local callback server");
113
+ }
114
+
115
+ const callbackUrl = `http://127.0.0.1:${port}${callbackPath}`;
116
+
117
+ const timer = setTimeout(() => {
118
+ if (resolved) return;
119
+ resolved = true;
120
+ rejectResult(new Error("Authentication timed out"));
121
+ server.close();
122
+ }, timeoutMs);
123
+
124
+ async function waitForCallback() {
125
+ try {
126
+ return await resultPromise;
127
+ } finally {
128
+ clearTimeout(timer);
129
+ server.close();
130
+ }
131
+ }
132
+
133
+ return { callbackUrl, waitForCallback };
134
+ }
135
+
136
+ function detectDefaultBrowser() {
137
+ try {
138
+ const raw = cp.execFileSync("defaults", [
139
+ "read", "com.apple.LaunchServices/com.apple.launchservices.secure", "LSHandlers",
140
+ ], { encoding: "utf8", timeout: 3000 });
141
+ const match = raw.match(/https[\s\S]*?LSHandlerRoleAll\s*=\s*"([^"]+)"/);
142
+ if (!match) return null;
143
+ const bundleId = match[1].toLowerCase();
144
+ if (bundleId.includes("chrome")) return "Google Chrome";
145
+ if (bundleId.includes("safari")) return "Safari";
146
+ if (bundleId.includes("edgemac")) return "Microsoft Edge";
147
+ if (bundleId.includes("thebrowser") || bundleId.includes("arc")) return "Arc";
148
+ return null;
149
+ } catch (_e) {
150
+ return null;
151
+ }
152
+ }
153
+
154
+ function buildBrowserList() {
155
+ const all = ["Google Chrome", "Safari", "Microsoft Edge", "Arc"];
156
+ const def = detectDefaultBrowser();
157
+ if (!def) return all;
158
+ return [def, ...all.filter((b) => b !== def)];
159
+ }
160
+
161
+ function openInBrowser(url) {
162
+ const platform = process.platform;
163
+
164
+ if (platform === "darwin") {
165
+ // On macOS, prefer reusing a matching tab in a supported running browser.
166
+ // Supported browsers are checked with the user's default browser first.
167
+ const browsers = buildBrowserList();
168
+ const listLiteral = browsers.map((b) => `"${b}"`).join(", ");
169
+ const script = `
170
+ tell application "System Events"
171
+ set browserList to {${listLiteral}}
172
+ set runningBrowser to ""
173
+ repeat with b in browserList
174
+ if (exists process (b as text)) then
175
+ set runningBrowser to (b as text)
176
+ exit repeat
177
+ end if
178
+ end repeat
179
+ end tell
180
+
181
+ if runningBrowser is "" then
182
+ open location "${url}"
183
+ else if runningBrowser is "Google Chrome" then
184
+ tell application "Google Chrome"
185
+ set found to false
186
+ repeat with w in windows
187
+ set tabIndex to 0
188
+ repeat with t in tabs of w
189
+ set tabIndex to tabIndex + 1
190
+ if URL of t starts with "${url}" then
191
+ set active tab index of w to tabIndex
192
+ set index of w to 1
193
+ reload t
194
+ activate
195
+ set found to true
196
+ exit repeat
197
+ end if
198
+ end repeat
199
+ if found then exit repeat
200
+ end repeat
201
+ if not found then
202
+ open location "${url}"
203
+ activate
204
+ end if
205
+ end tell
206
+ else if runningBrowser is "Safari" then
207
+ tell application "Safari"
208
+ set found to false
209
+ repeat with w in windows
210
+ set tabIndex to 0
211
+ repeat with t in tabs of w
212
+ set tabIndex to tabIndex + 1
213
+ if URL of t starts with "${url}" then
214
+ set current tab of w to t
215
+ set index of w to 1
216
+ do JavaScript "location.reload()" in t
217
+ activate
218
+ set found to true
219
+ exit repeat
220
+ end if
221
+ end repeat
222
+ if found then exit repeat
223
+ end repeat
224
+ if not found then
225
+ open location "${url}"
226
+ activate
227
+ end if
228
+ end tell
229
+ else
230
+ open location "${url}"
231
+ end if
232
+ `;
233
+ try {
234
+ const child = cp.spawn("osascript", ["-e", script], { stdio: "ignore", detached: true });
235
+ child.unref();
236
+ } catch (_e) {
237
+ // Fallback to plain open
238
+ try {
239
+ const child = cp.spawn("open", [url], { stdio: "ignore", detached: true });
240
+ child.unref();
241
+ } catch (_e2) {}
242
+ }
243
+ return;
244
+ }
245
+
246
+ let cmd = null;
247
+ let args = [];
248
+
249
+ if (platform === "win32") {
250
+ cmd = "cmd";
251
+ args = ["/c", "start", "", url];
252
+ } else {
253
+ cmd = "xdg-open";
254
+ args = [url];
255
+ }
256
+
257
+ try {
258
+ const child = cp.spawn(cmd, args, { stdio: "ignore", detached: true });
259
+ child.unref();
260
+ } catch (_e) {}
261
+ }
262
+
263
+ function resolvePostAuthRedirect({ dashboardUrl, authUrl }) {
264
+ try {
265
+ if (dashboardUrl) {
266
+ const target = new URL("/", dashboardUrl);
267
+ if (target.protocol === "http:" || target.protocol === "https:") {
268
+ return target.toString();
269
+ }
270
+ return null;
271
+ }
272
+ } catch (_e) {
273
+ return null;
274
+ }
275
+ return null;
276
+ }
277
+
278
+ module.exports = {
279
+ beginBrowserAuth,
280
+ openInBrowser,
281
+ };