@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
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@ipv9/tokentracker-cli",
3
+ "version": "0.39.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, Roo Code, Zed Agent, Goose)",
5
+ "main": "src/cli.js",
6
+ "bin": {
7
+ "tokentracker-cli": "bin/tracker.js",
8
+ "tracker": "bin/tracker.js",
9
+ "tokentracker": "bin/tracker.js",
10
+ "tokentracker-tracker": "bin/tracker.js"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "src/",
15
+ "dashboard/dist/",
16
+ "scripts/install-local-service.sh",
17
+ "scripts/uninstall-local-service.sh",
18
+ "LICENSE",
19
+ "README.md"
20
+ ],
21
+ "type": "commonjs",
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "ci:local": "npm test && npm run validate:copy && npm run validate:ui-hardcode && npm run validate:guardrails && node --test test/architecture-guardrails.test.js && npm --prefix dashboard run build",
27
+ "copy:pull": "node scripts/copy-sync.cjs pull",
28
+ "copy:push": "node scripts/copy-sync.cjs push",
29
+ "dashboard:build": "npm --prefix dashboard run build",
30
+ "dashboard:dev": "npm --prefix dashboard run dev",
31
+ "dashboard:open": "bash scripts/open-dashboard.sh",
32
+ "dashboard:preview": "npm --prefix dashboard run preview",
33
+ "dev:shim": "node scripts/dev-bin-shim.cjs",
34
+ "audit:tokens": "node scripts/audit-token-correctness.cjs",
35
+ "graph:auto-index": "node scripts/graph/auto-index.cjs",
36
+ "graph:scip": "node scripts/graph/generate-scip.cjs",
37
+ "pricing:build-seed": "node scripts/build-pricing-seed.cjs",
38
+ "prepublishOnly": "node scripts/build-pricing-seed.cjs",
39
+ "test": "node --test test/*.test.js",
40
+ "validate:copy": "node scripts/validate-copy-registry.cjs",
41
+ "validate:guardrails": "node scripts/validate-architecture-guardrails.cjs",
42
+ "validate:retros": "node scripts/validate-retros.cjs",
43
+ "validate:ui-hardcode": "node scripts/ops/validate-ui-hardcode.cjs"
44
+ },
45
+ "dependencies": {
46
+ "@mongodb-js/zstd": "^2.0.1",
47
+ "undici": "^8.2.0"
48
+ },
49
+ "devDependencies": {
50
+ "@sourcegraph/scip-typescript": "^0.3.6",
51
+ "esbuild": "0.27.2"
52
+ },
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/pitimon/TokenTracker.git"
56
+ },
57
+ "homepage": "https://github.com/pitimon/TokenTracker#readme",
58
+ "bugs": {
59
+ "url": "https://github.com/pitimon/TokenTracker/issues"
60
+ },
61
+ "keywords": [
62
+ "token",
63
+ "tracker",
64
+ "ai",
65
+ "claude",
66
+ "codex",
67
+ "cursor",
68
+ "gemini",
69
+ "kiro",
70
+ "opencode",
71
+ "usage",
72
+ "cost"
73
+ ],
74
+ "license": "MIT",
75
+ "engines": {
76
+ "node": ">=20"
77
+ }
78
+ }
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ PORT="${TOKENTRACKER_DASHBOARD_PORT:-7680}"
5
+ PACKAGE_NAME="${TOKENTRACKER_NPM_PACKAGE:-@ipv9/tokentracker-cli}"
6
+ TRACKER_HOME="${TOKENTRACKER_HOME:-$HOME/.tokentracker}"
7
+ SERVICE_PATH="${TOKENTRACKER_SERVICE_PATH:-/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin}"
8
+ LOG_DIR="$TRACKER_HOME/tracker/logs"
9
+ BIN_DIR="$TRACKER_HOME/bin"
10
+ AGENTS_DIR="$HOME/Library/LaunchAgents"
11
+ DASHBOARD_LABEL="com.pitimon.tokentracker.dashboard"
12
+ LOCAL_SYNC_LABEL="com.pitimon.tokentracker.local-sync"
13
+ DASHBOARD_PLIST="$AGENTS_DIR/$DASHBOARD_LABEL.plist"
14
+ LOCAL_SYNC_PLIST="$AGENTS_DIR/$LOCAL_SYNC_LABEL.plist"
15
+ LOCAL_SYNC_WRAPPER="$BIN_DIR/tokentracker-local-sync-service.sh"
16
+
17
+ if [[ "$(uname -s)" != "Darwin" ]]; then
18
+ echo "TokenTracker local service installer currently supports macOS launchd only." >&2
19
+ exit 1
20
+ fi
21
+
22
+ mkdir -p "$LOG_DIR" "$BIN_DIR" "$AGENTS_DIR"
23
+
24
+ cat > "$LOCAL_SYNC_WRAPPER" <<'WRAPPER'
25
+ #!/usr/bin/env bash
26
+ set -euo pipefail
27
+
28
+ CONFIG_PATH="${TOKENTRACKER_CONFIG_PATH:-$HOME/.tokentracker/tracker/config.json}"
29
+ PACKAGE_NAME="${TOKENTRACKER_NPM_PACKAGE:-@ipv9/tokentracker-cli}"
30
+ export PATH="${TOKENTRACKER_SERVICE_PATH:-/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin}"
31
+
32
+ if [[ -n "${TOKENTRACKER_DEVICE_TOKEN:-}" ]]; then
33
+ echo "TokenTracker local-sync skipped: TOKENTRACKER_DEVICE_TOKEN is configured."
34
+ exit 0
35
+ fi
36
+
37
+ if [[ -f "$CONFIG_PATH" ]]; then
38
+ HAS_DEVICE_TOKEN="$(node -e '
39
+ const fs = require("node:fs");
40
+ const p = process.argv[1];
41
+ try {
42
+ const cfg = JSON.parse(fs.readFileSync(p, "utf8"));
43
+ process.stdout.write(typeof cfg.deviceToken === "string" && cfg.deviceToken.trim() ? "1" : "0");
44
+ } catch {
45
+ process.stdout.write("0");
46
+ }
47
+ ' "$CONFIG_PATH")"
48
+ if [[ "$HAS_DEVICE_TOKEN" == "1" ]]; then
49
+ echo "TokenTracker local-sync skipped: deviceToken is configured."
50
+ exit 0
51
+ fi
52
+ fi
53
+
54
+ exec npx --yes "$PACKAGE_NAME" sync --auto
55
+ WRAPPER
56
+ chmod 0755 "$LOCAL_SYNC_WRAPPER"
57
+
58
+ launchctl bootout "gui/$(id -u)" "$DASHBOARD_PLIST" >/dev/null 2>&1 || true
59
+ launchctl bootout "gui/$(id -u)" "$LOCAL_SYNC_PLIST" >/dev/null 2>&1 || true
60
+
61
+ cat > "$DASHBOARD_PLIST" <<PLIST
62
+ <?xml version="1.0" encoding="UTF-8"?>
63
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
64
+ <plist version="1.0">
65
+ <dict>
66
+ <key>Label</key>
67
+ <string>$DASHBOARD_LABEL</string>
68
+ <key>ProgramArguments</key>
69
+ <array>
70
+ <string>/usr/bin/env</string>
71
+ <string>npx</string>
72
+ <string>--yes</string>
73
+ <string>$PACKAGE_NAME</string>
74
+ <string>serve</string>
75
+ <string>--no-open</string>
76
+ <string>--port</string>
77
+ <string>$PORT</string>
78
+ </array>
79
+ <key>RunAtLoad</key>
80
+ <true/>
81
+ <key>KeepAlive</key>
82
+ <true/>
83
+ <key>EnvironmentVariables</key>
84
+ <dict>
85
+ <key>PATH</key>
86
+ <string>$SERVICE_PATH</string>
87
+ <key>TOKENTRACKER_NPM_PACKAGE</key>
88
+ <string>$PACKAGE_NAME</string>
89
+ </dict>
90
+ <key>StandardOutPath</key>
91
+ <string>$LOG_DIR/dashboard.log</string>
92
+ <key>StandardErrorPath</key>
93
+ <string>$LOG_DIR/dashboard.err.log</string>
94
+ <key>WorkingDirectory</key>
95
+ <string>$HOME</string>
96
+ </dict>
97
+ </plist>
98
+ PLIST
99
+
100
+ cat > "$LOCAL_SYNC_PLIST" <<PLIST
101
+ <?xml version="1.0" encoding="UTF-8"?>
102
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
103
+ <plist version="1.0">
104
+ <dict>
105
+ <key>Label</key>
106
+ <string>$LOCAL_SYNC_LABEL</string>
107
+ <key>ProgramArguments</key>
108
+ <array>
109
+ <string>$LOCAL_SYNC_WRAPPER</string>
110
+ </array>
111
+ <key>RunAtLoad</key>
112
+ <true/>
113
+ <key>StartInterval</key>
114
+ <integer>300</integer>
115
+ <key>EnvironmentVariables</key>
116
+ <dict>
117
+ <key>PATH</key>
118
+ <string>$SERVICE_PATH</string>
119
+ <key>TOKENTRACKER_NPM_PACKAGE</key>
120
+ <string>$PACKAGE_NAME</string>
121
+ <key>TOKENTRACKER_SERVICE_PATH</key>
122
+ <string>$SERVICE_PATH</string>
123
+ </dict>
124
+ <key>StandardOutPath</key>
125
+ <string>$LOG_DIR/local-sync.log</string>
126
+ <key>StandardErrorPath</key>
127
+ <string>$LOG_DIR/local-sync.err.log</string>
128
+ <key>WorkingDirectory</key>
129
+ <string>$HOME</string>
130
+ </dict>
131
+ </plist>
132
+ PLIST
133
+
134
+ launchctl bootstrap "gui/$(id -u)" "$DASHBOARD_PLIST"
135
+ launchctl bootstrap "gui/$(id -u)" "$LOCAL_SYNC_PLIST"
136
+
137
+ echo "Installed TokenTracker local services:"
138
+ echo " $DASHBOARD_LABEL -> http://127.0.0.1:$PORT/dashboard"
139
+ echo " $LOCAL_SYNC_LABEL -> local sync every 5 minutes, skips when deviceToken is configured"
140
+ echo "Logs: $LOG_DIR"
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ AGENTS_DIR="$HOME/Library/LaunchAgents"
5
+ DASHBOARD_LABEL="com.pitimon.tokentracker.dashboard"
6
+ LOCAL_SYNC_LABEL="com.pitimon.tokentracker.local-sync"
7
+ DASHBOARD_PLIST="$AGENTS_DIR/$DASHBOARD_LABEL.plist"
8
+ LOCAL_SYNC_PLIST="$AGENTS_DIR/$LOCAL_SYNC_LABEL.plist"
9
+ LOCAL_SYNC_WRAPPER="${TOKENTRACKER_HOME:-$HOME/.tokentracker}/bin/tokentracker-local-sync-service.sh"
10
+
11
+ if [[ "$(uname -s)" != "Darwin" ]]; then
12
+ echo "TokenTracker local service uninstaller currently supports macOS launchd only." >&2
13
+ exit 1
14
+ fi
15
+
16
+ launchctl bootout "gui/$(id -u)" "$DASHBOARD_PLIST" >/dev/null 2>&1 || true
17
+ launchctl bootout "gui/$(id -u)" "$LOCAL_SYNC_PLIST" >/dev/null 2>&1 || true
18
+
19
+ rm -f "$DASHBOARD_PLIST" "$LOCAL_SYNC_PLIST" "$LOCAL_SYNC_WRAPPER"
20
+
21
+ echo "Uninstalled TokenTracker local services."
package/src/cli.js ADDED
@@ -0,0 +1,95 @@
1
+ const { cmdInit } = require("./commands/init");
2
+ const { cmdSync } = require("./commands/sync");
3
+ const { cmdStatus } = require("./commands/status");
4
+ const { cmdDiagnostics } = require("./commands/diagnostics");
5
+ const { cmdDoctor } = require("./commands/doctor");
6
+ const { cmdUninstall } = require("./commands/uninstall");
7
+ const { cmdServe } = require("./commands/serve");
8
+ const { cmdDeviceLogin } = require("./commands/device-login");
9
+ const { cmdWrapped } = require("./commands/wrapped");
10
+
11
+ async function run(argv) {
12
+ const [command, ...rest] = argv;
13
+
14
+ // No args → launch dashboard
15
+ if (!command) {
16
+ await cmdServe(argv);
17
+ return;
18
+ }
19
+
20
+ if (command === "-h" || command === "--help") {
21
+ printHelp();
22
+ return;
23
+ }
24
+
25
+ switch (command) {
26
+ case "serve":
27
+ await cmdServe(rest);
28
+ return;
29
+ case "init":
30
+ await cmdInit(rest);
31
+ return;
32
+ case "sync":
33
+ await cmdSync(rest);
34
+ return;
35
+ case "status":
36
+ await cmdStatus(rest);
37
+ return;
38
+ case "diagnostics":
39
+ await cmdDiagnostics(rest);
40
+ return;
41
+ case "doctor":
42
+ await cmdDoctor(rest);
43
+ return;
44
+ case "uninstall":
45
+ await cmdUninstall(rest);
46
+ return;
47
+ case "device-login":
48
+ await cmdDeviceLogin(rest);
49
+ return;
50
+ case "wrapped":
51
+ await cmdWrapped(rest);
52
+ return;
53
+ default:
54
+ throw new Error(`Unknown command: ${command}`);
55
+ }
56
+ }
57
+
58
+ function printHelp() {
59
+ // Keep this short; npx users want quick guidance.
60
+ process.stdout.write(
61
+ [
62
+ "tokentracker",
63
+ "",
64
+ "Usage:",
65
+ " npx --yes @ipv9/tokentracker-cli Open local dashboard",
66
+ " npx --yes @ipv9/tokentracker-cli [--debug] serve [--port 7680] [--no-open] [--sync]",
67
+ " npx --yes @ipv9/tokentracker-cli [--debug] init [--yes] [--dry-run] [--no-open] [--link-code <code>]",
68
+ " npx --yes @ipv9/tokentracker-cli [--debug] sync [--auto] [--drain] [--from-openclaw]",
69
+ " npx --yes @ipv9/tokentracker-cli [--debug] status [--probe-keychain] [--probe-keychain-details]",
70
+ " npx --yes @ipv9/tokentracker-cli [--debug] diagnostics [--out diagnostics.json]",
71
+ " npx --yes @ipv9/tokentracker-cli [--debug] doctor [--json] [--out doctor.json] [--base-url <url>]",
72
+ " npx --yes @ipv9/tokentracker-cli [--debug] uninstall [--purge]",
73
+ " npx --yes @ipv9/tokentracker-cli [--debug] device-login [--json] [--base-url <url>]",
74
+ " npx --yes @ipv9/tokentracker-cli [--debug] wrapped [--year 2026] [--json]",
75
+ "",
76
+ "Notes:",
77
+ " - init: consent first, local setup next, browser sign-in last.",
78
+ " - --yes skips the consent menu (non-interactive safe).",
79
+ " - --dry-run previews changes without writing files.",
80
+ " - optional: --link-code <code> skips browser login when provided by Dashboard.",
81
+ " - Every Code notify installs when ~/.code/config.toml exists.",
82
+ " - OpenClaw hook auto-links when OpenClaw is installed (requires gateway restart).",
83
+ " - auto sync waits for a device token.",
84
+ " - optional: --dashboard-url for hosted landing.",
85
+ " - serve opens the local dashboard without syncing; pass --sync to parse local logs first.",
86
+ " - sync parses ~/.codex/sessions/**/rollout-*.jsonl and ~/.code/sessions/**/rollout-*.jsonl, then uploads token deltas only when cloud credentials are configured.",
87
+ " - --from-openclaw marks sync runs triggered by OpenClaw hooks.",
88
+ " - --debug shows original backend errors.",
89
+ " - device-login pairs a headless CLI / SSH session with a browser sign-in (15-min code).",
90
+ "",
91
+ ].join("\n"),
92
+ );
93
+ }
94
+
95
+ module.exports = { run };
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+
3
+ const os = require("node:os");
4
+ const path = require("node:path");
5
+ const fs = require("node:fs/promises");
6
+
7
+ const { readJson, writeJson } = require("../lib/fs");
8
+ const { resolveTrackerPaths } = require("../lib/tracker-paths");
9
+
10
+ const DEFAULT_BASE_URL = "https://srctyff5.us-east.insforge.app";
11
+ const POLL_INTERVAL_MS = 5_000;
12
+ const ABSOLUTE_TIMEOUT_MS = 16 * 60 * 1000; // matches the 15-min server window with a small buffer
13
+
14
+ function readBaseUrl(config) {
15
+ return (
16
+ process.env.TOKENTRACKER_BASE_URL ||
17
+ process.env.TOKENTRACKER_API_URL ||
18
+ config?.baseUrl ||
19
+ DEFAULT_BASE_URL
20
+ );
21
+ }
22
+
23
+ async function authorize({ baseUrl, clientInfo }) {
24
+ const res = await fetch(`${baseUrl}/functions/tokentracker-device-flow-authorize`, {
25
+ method: "POST",
26
+ headers: { "Content-Type": "application/json" },
27
+ body: JSON.stringify({ client_info: clientInfo }),
28
+ });
29
+ if (!res.ok) {
30
+ const text = await res.text();
31
+ throw new Error(`authorize failed (HTTP ${res.status}): ${text.slice(0, 200)}`);
32
+ }
33
+ return res.json();
34
+ }
35
+
36
+ async function pollOnce({ baseUrl, deviceCode }) {
37
+ const res = await fetch(`${baseUrl}/functions/tokentracker-device-flow-poll`, {
38
+ method: "POST",
39
+ headers: { "Content-Type": "application/json" },
40
+ body: JSON.stringify({ device_code: deviceCode }),
41
+ });
42
+ const data = await res.json().catch(() => ({}));
43
+ // 404 = unknown, 410 = expired, 200 = {status, user_id?, device_token?}.
44
+ // Anything else (502 from a misconfigured edge, 5xx during deploy, …) must
45
+ // bubble up as a network-style error — masking it as "unknown" would tell
46
+ // the user their device_code was evicted when it wasn't.
47
+ if (!res.ok && res.status !== 404 && res.status !== 410) {
48
+ throw new Error(`poll HTTP ${res.status}: ${(data?.error ?? "").toString().slice(0, 200)}`);
49
+ }
50
+ return {
51
+ status: data.status ?? "unknown",
52
+ user_id: data.user_id ?? null,
53
+ deviceToken: data.device_token ?? data.deviceToken ?? null,
54
+ deviceId: data.device_id ?? data.deviceId ?? null,
55
+ httpStatus: res.status,
56
+ };
57
+ }
58
+
59
+ function sleep(ms) {
60
+ return new Promise((resolve) => setTimeout(resolve, ms));
61
+ }
62
+
63
+ async function cmdDeviceLogin(argv = [], options = {}) {
64
+ const opts = parseArgs(argv);
65
+ const home = options.home || os.homedir();
66
+ const sleepFn = options.sleep || sleep;
67
+ const { trackerDir } = await resolveTrackerPaths({ home });
68
+ const configPath = path.join(trackerDir, "config.json");
69
+ const config = (await readJson(configPath)) || {};
70
+ const baseUrl = opts.baseUrl || readBaseUrl(config);
71
+
72
+ const clientInfo = `${os.platform()}-${os.arch()} ${os.hostname()}`;
73
+ process.stdout.write(`Requesting device code from ${baseUrl}...\n`);
74
+ const authResp = await authorize({ baseUrl, clientInfo });
75
+
76
+ if (opts.json) {
77
+ process.stdout.write(JSON.stringify(authResp, null, 2) + "\n");
78
+ } else {
79
+ process.stdout.write(
80
+ [
81
+ "",
82
+ " Sign in from a browser:",
83
+ ` ${authResp.verification_uri_complete || authResp.verification_uri}`,
84
+ "",
85
+ ` Or visit ${authResp.verification_uri} and enter the code:`,
86
+ "",
87
+ ` ${authResp.user_code}`,
88
+ "",
89
+ ` This code expires in ${Math.round(authResp.expires_in / 60)} minutes.`,
90
+ " Polling every 5 seconds until you approve…",
91
+ "",
92
+ ].join("\n"),
93
+ );
94
+ }
95
+
96
+ const startedAt = Date.now();
97
+ let consecutiveErrors = 0;
98
+ const MAX_BACKOFF_MS = 30_000;
99
+ while (Date.now() - startedAt < ABSOLUTE_TIMEOUT_MS) {
100
+ // Exponential backoff on consecutive network failures (capped at 30s) so
101
+ // a flaky network doesn't hammer the server at the full 5s cadence for
102
+ // the entire 15-minute window. ±20% jitter on retries prevents
103
+ // thundering-herd reconnects when many CLIs lose connectivity at once.
104
+ let wait =
105
+ consecutiveErrors === 0
106
+ ? POLL_INTERVAL_MS
107
+ : Math.min(POLL_INTERVAL_MS * Math.pow(2, consecutiveErrors - 1), MAX_BACKOFF_MS);
108
+ if (consecutiveErrors > 0) {
109
+ const jitter = wait * 0.2 * (Math.random() * 2 - 1);
110
+ wait = Math.max(POLL_INTERVAL_MS, wait + jitter);
111
+ }
112
+ await sleepFn(wait);
113
+ let result;
114
+ try {
115
+ result = await pollOnce({ baseUrl, deviceCode: authResp.device_code });
116
+ consecutiveErrors = 0;
117
+ } catch (e) {
118
+ consecutiveErrors++;
119
+ process.stderr.write(`poll error (retry ${consecutiveErrors}): ${e?.message || e}\n`);
120
+ continue;
121
+ }
122
+ if (result.status === "approved" && result.user_id) {
123
+ if (!result.deviceToken) {
124
+ throw new Error("device login approved but server did not return a device token");
125
+ }
126
+ const next = {
127
+ ...config,
128
+ baseUrl,
129
+ user_id: result.user_id,
130
+ deviceToken: result.deviceToken,
131
+ deviceId: result.deviceId || config.deviceId,
132
+ device_login_at: new Date().toISOString(),
133
+ };
134
+ await writeJson(configPath, next);
135
+ process.stdout.write(`\n✓ Approved. device token written to ${configPath}\n`);
136
+ return;
137
+ }
138
+ if (result.status === "expired") {
139
+ throw new Error("device_code expired — re-run `tracker device-login`");
140
+ }
141
+ if (result.status === "unknown") {
142
+ throw new Error("device_code is unknown — server may have evicted it");
143
+ }
144
+ // status === "pending" → just keep polling silently
145
+ }
146
+ throw new Error("device-login timed out without approval");
147
+ }
148
+
149
+ function parseArgs(argv) {
150
+ const out = { json: false, baseUrl: null };
151
+ for (let i = 0; i < argv.length; i++) {
152
+ const a = argv[i];
153
+ if (a === "--json") out.json = true;
154
+ else if (a === "--base-url") {
155
+ out.baseUrl = argv[++i] || null;
156
+ } else throw new Error(`Unknown option: ${a}`);
157
+ }
158
+ return out;
159
+ }
160
+
161
+ module.exports = { cmdDeviceLogin, authorize, pollOnce };
@@ -0,0 +1,38 @@
1
+ const path = require("node:path");
2
+
3
+ const { writeFileAtomic, chmod600IfPossible } = require("../lib/fs");
4
+ const { collectTrackerDiagnostics } = require("../lib/diagnostics");
5
+
6
+ async function cmdDiagnostics(argv = []) {
7
+ const opts = parseArgs(argv);
8
+ const diagnostics = await collectTrackerDiagnostics();
9
+ const json = JSON.stringify(diagnostics, null, opts.compact ? 0 : 2) + "\n";
10
+
11
+ if (opts.out) {
12
+ const outPath = path.resolve(process.cwd(), opts.out);
13
+ await writeFileAtomic(outPath, json);
14
+ await chmod600IfPossible(outPath);
15
+ process.stderr.write(`Wrote diagnostics to: ${outPath}\n`);
16
+ }
17
+
18
+ process.stdout.write(json);
19
+ }
20
+
21
+ function parseArgs(argv) {
22
+ const out = {
23
+ out: null,
24
+ compact: false,
25
+ };
26
+
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const a = argv[i];
29
+ if (a === "--out") out.out = argv[++i] || null;
30
+ else if (a === "--compact") out.compact = true;
31
+ else if (a === "--pretty") out.compact = false;
32
+ else throw new Error(`Unknown option: ${a}`);
33
+ }
34
+
35
+ return out;
36
+ }
37
+
38
+ module.exports = { cmdDiagnostics };
@@ -0,0 +1,96 @@
1
+ const os = require("node:os");
2
+ const path = require("node:path");
3
+
4
+ const { readJsonStrict, writeFileAtomic, chmod600IfPossible } = require("../lib/fs");
5
+ const { resolveTrackerPaths } = require("../lib/tracker-paths");
6
+ const { collectTrackerDiagnostics } = require("../lib/diagnostics");
7
+ const { resolveRuntimeConfig } = require("../lib/runtime-config");
8
+ const { buildDoctorReport } = require("../lib/doctor");
9
+
10
+ async function cmdDoctor(argv = []) {
11
+ const opts = parseArgs(argv);
12
+ const home = os.homedir();
13
+ const { trackerDir } = await resolveTrackerPaths({ home });
14
+ const configPath = path.join(trackerDir, "config.json");
15
+
16
+ const configStatus = await readJsonStrict(configPath);
17
+ const config =
18
+ configStatus.status === "ok" && isPlainObject(configStatus.value) ? configStatus.value : {};
19
+ const runtime = resolveRuntimeConfig({
20
+ cli: { baseUrl: opts.baseUrl },
21
+ config,
22
+ env: process.env,
23
+ });
24
+ const diagnostics = await collectTrackerDiagnostics({ home });
25
+ const cliPath = process.argv[1] ? path.resolve(process.argv[1]) : null;
26
+
27
+ const report = await buildDoctorReport({
28
+ runtime,
29
+ diagnostics,
30
+ fetch: globalThis.fetch,
31
+ paths: {
32
+ trackerDir,
33
+ configPath,
34
+ cliPath,
35
+ },
36
+ });
37
+
38
+ const jsonOutput = opts.json || Boolean(opts.out);
39
+ const payload = JSON.stringify(report, null, jsonOutput ? 2 : 0) + "\n";
40
+
41
+ if (opts.out) {
42
+ const outPath = path.resolve(process.cwd(), opts.out);
43
+ await writeFileAtomic(outPath, payload);
44
+ await chmod600IfPossible(outPath);
45
+ process.stderr.write(`Wrote doctor report to: ${outPath}\n`);
46
+ }
47
+
48
+ if (jsonOutput) {
49
+ process.stdout.write(payload);
50
+ } else {
51
+ process.stdout.write(renderHumanReport(report));
52
+ }
53
+
54
+ if (report.summary.critical > 0) {
55
+ process.exitCode = 1;
56
+ }
57
+ }
58
+
59
+ function parseArgs(argv) {
60
+ const out = { json: false, out: null, baseUrl: null };
61
+ for (let i = 0; i < argv.length; i += 1) {
62
+ const arg = argv[i];
63
+ if (arg === "--json") out.json = true;
64
+ else if (arg === "--out") out.out = argv[++i] || null;
65
+ else if (arg === "--base-url") out.baseUrl = argv[++i] || null;
66
+ else throw new Error(`Unknown option: ${arg}`);
67
+ }
68
+ return out;
69
+ }
70
+
71
+ function renderHumanReport(report) {
72
+ const lines = [];
73
+ lines.push("Doctor report");
74
+ lines.push("");
75
+ for (const check of report.checks || []) {
76
+ lines.push(formatCheckLine(check));
77
+ }
78
+ lines.push("");
79
+ lines.push(
80
+ `Summary: ok ${report.summary.ok} | warn ${report.summary.warn} | fail ${report.summary.fail} | critical ${report.summary.critical}`,
81
+ );
82
+ lines.push("");
83
+ return lines.join("\n");
84
+ }
85
+
86
+ function formatCheckLine(check = {}) {
87
+ const status = String(check.status || "unknown").toUpperCase();
88
+ const detail = check.detail ? ` - ${check.detail}` : "";
89
+ return `- [${status}] ${check.id || "unknown"}${detail}`;
90
+ }
91
+
92
+ function isPlainObject(value) {
93
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
94
+ }
95
+
96
+ module.exports = { cmdDoctor };