hacklab 0.22.3 → 26.921.2

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 (259) hide show
  1. package/README.md +39 -415
  2. package/dist/index.js +317 -23
  3. package/package.json +22 -40
  4. package/LICENSE +0 -21
  5. package/dist/agent-handoff.d.ts +0 -51
  6. package/dist/agent-handoff.d.ts.map +0 -1
  7. package/dist/agent-handoff.js +0 -91
  8. package/dist/agent-handoff.js.map +0 -1
  9. package/dist/api-client.d.ts +0 -18
  10. package/dist/api-client.d.ts.map +0 -1
  11. package/dist/api-client.js +0 -40
  12. package/dist/api-client.js.map +0 -1
  13. package/dist/belt.d.ts +0 -25
  14. package/dist/belt.d.ts.map +0 -1
  15. package/dist/belt.js +0 -113
  16. package/dist/belt.js.map +0 -1
  17. package/dist/card.d.ts +0 -66
  18. package/dist/card.d.ts.map +0 -1
  19. package/dist/card.js +0 -151
  20. package/dist/card.js.map +0 -1
  21. package/dist/cli.d.ts +0 -2
  22. package/dist/cli.d.ts.map +0 -1
  23. package/dist/cli.js +0 -238
  24. package/dist/cli.js.map +0 -1
  25. package/dist/commands/book.d.ts +0 -14
  26. package/dist/commands/book.d.ts.map +0 -1
  27. package/dist/commands/book.js +0 -90
  28. package/dist/commands/book.js.map +0 -1
  29. package/dist/commands/chat.d.ts +0 -36
  30. package/dist/commands/chat.d.ts.map +0 -1
  31. package/dist/commands/chat.js +0 -642
  32. package/dist/commands/chat.js.map +0 -1
  33. package/dist/commands/config.d.ts +0 -2
  34. package/dist/commands/config.d.ts.map +0 -1
  35. package/dist/commands/config.js +0 -84
  36. package/dist/commands/config.js.map +0 -1
  37. package/dist/commands/daemon.d.ts +0 -19
  38. package/dist/commands/daemon.d.ts.map +0 -1
  39. package/dist/commands/daemon.js +0 -72
  40. package/dist/commands/daemon.js.map +0 -1
  41. package/dist/commands/drop.d.ts +0 -12
  42. package/dist/commands/drop.d.ts.map +0 -1
  43. package/dist/commands/drop.js +0 -103
  44. package/dist/commands/drop.js.map +0 -1
  45. package/dist/commands/drops.d.ts +0 -9
  46. package/dist/commands/drops.d.ts.map +0 -1
  47. package/dist/commands/drops.js +0 -96
  48. package/dist/commands/drops.js.map +0 -1
  49. package/dist/commands/essay.d.ts +0 -40
  50. package/dist/commands/essay.d.ts.map +0 -1
  51. package/dist/commands/essay.js +0 -651
  52. package/dist/commands/essay.js.map +0 -1
  53. package/dist/commands/hackathon.d.ts +0 -2
  54. package/dist/commands/hackathon.d.ts.map +0 -1
  55. package/dist/commands/hackathon.js +0 -893
  56. package/dist/commands/hackathon.js.map +0 -1
  57. package/dist/commands/hacker.d.ts +0 -2
  58. package/dist/commands/hacker.d.ts.map +0 -1
  59. package/dist/commands/hacker.js +0 -87
  60. package/dist/commands/hacker.js.map +0 -1
  61. package/dist/commands/jobs.d.ts +0 -18
  62. package/dist/commands/jobs.d.ts.map +0 -1
  63. package/dist/commands/jobs.js +0 -138
  64. package/dist/commands/jobs.js.map +0 -1
  65. package/dist/commands/keys.d.ts +0 -2
  66. package/dist/commands/keys.d.ts.map +0 -1
  67. package/dist/commands/keys.js +0 -138
  68. package/dist/commands/keys.js.map +0 -1
  69. package/dist/commands/login.d.ts +0 -83
  70. package/dist/commands/login.d.ts.map +0 -1
  71. package/dist/commands/login.js +0 -236
  72. package/dist/commands/login.js.map +0 -1
  73. package/dist/commands/logout.d.ts +0 -2
  74. package/dist/commands/logout.d.ts.map +0 -1
  75. package/dist/commands/logout.js +0 -22
  76. package/dist/commands/logout.js.map +0 -1
  77. package/dist/commands/org-jobs.d.ts +0 -50
  78. package/dist/commands/org-jobs.d.ts.map +0 -1
  79. package/dist/commands/org-jobs.js +0 -383
  80. package/dist/commands/org-jobs.js.map +0 -1
  81. package/dist/commands/org.d.ts +0 -67
  82. package/dist/commands/org.d.ts.map +0 -1
  83. package/dist/commands/org.js +0 -1244
  84. package/dist/commands/org.js.map +0 -1
  85. package/dist/commands/ping.d.ts +0 -2
  86. package/dist/commands/ping.d.ts.map +0 -1
  87. package/dist/commands/ping.js +0 -50
  88. package/dist/commands/ping.js.map +0 -1
  89. package/dist/commands/profile.d.ts +0 -72
  90. package/dist/commands/profile.d.ts.map +0 -1
  91. package/dist/commands/profile.js +0 -672
  92. package/dist/commands/profile.js.map +0 -1
  93. package/dist/commands/project.d.ts +0 -2
  94. package/dist/commands/project.d.ts.map +0 -1
  95. package/dist/commands/project.js +0 -583
  96. package/dist/commands/project.js.map +0 -1
  97. package/dist/commands/referral.d.ts +0 -6
  98. package/dist/commands/referral.d.ts.map +0 -1
  99. package/dist/commands/referral.js +0 -93
  100. package/dist/commands/referral.js.map +0 -1
  101. package/dist/commands/rtfm.d.ts +0 -9
  102. package/dist/commands/rtfm.d.ts.map +0 -1
  103. package/dist/commands/rtfm.js +0 -114
  104. package/dist/commands/rtfm.js.map +0 -1
  105. package/dist/commands/scan.d.ts +0 -13
  106. package/dist/commands/scan.d.ts.map +0 -1
  107. package/dist/commands/scan.js +0 -286
  108. package/dist/commands/scan.js.map +0 -1
  109. package/dist/commands/scout.d.ts +0 -18
  110. package/dist/commands/scout.d.ts.map +0 -1
  111. package/dist/commands/scout.js +0 -232
  112. package/dist/commands/scout.js.map +0 -1
  113. package/dist/commands/setup-rail.d.ts +0 -58
  114. package/dist/commands/setup-rail.d.ts.map +0 -1
  115. package/dist/commands/setup-rail.js +0 -135
  116. package/dist/commands/setup-rail.js.map +0 -1
  117. package/dist/commands/setup.d.ts +0 -29
  118. package/dist/commands/setup.d.ts.map +0 -1
  119. package/dist/commands/setup.js +0 -442
  120. package/dist/commands/setup.js.map +0 -1
  121. package/dist/commands/sync.d.ts +0 -9
  122. package/dist/commands/sync.d.ts.map +0 -1
  123. package/dist/commands/sync.js +0 -345
  124. package/dist/commands/sync.js.map +0 -1
  125. package/dist/commands/update.d.ts +0 -2
  126. package/dist/commands/update.d.ts.map +0 -1
  127. package/dist/commands/update.js +0 -92
  128. package/dist/commands/update.js.map +0 -1
  129. package/dist/commands/whoami.d.ts +0 -2
  130. package/dist/commands/whoami.d.ts.map +0 -1
  131. package/dist/commands/whoami.js +0 -41
  132. package/dist/commands/whoami.js.map +0 -1
  133. package/dist/config.d.ts +0 -78
  134. package/dist/config.d.ts.map +0 -1
  135. package/dist/config.js +0 -93
  136. package/dist/config.js.map +0 -1
  137. package/dist/daily-sync.d.ts +0 -150
  138. package/dist/daily-sync.d.ts.map +0 -1
  139. package/dist/daily-sync.js +0 -665
  140. package/dist/daily-sync.js.map +0 -1
  141. package/dist/dossier.d.ts +0 -73
  142. package/dist/dossier.d.ts.map +0 -1
  143. package/dist/dossier.js +0 -141
  144. package/dist/dossier.js.map +0 -1
  145. package/dist/field-normalize.d.ts +0 -4
  146. package/dist/field-normalize.d.ts.map +0 -1
  147. package/dist/field-normalize.js +0 -8
  148. package/dist/field-normalize.js.map +0 -1
  149. package/dist/index.d.ts +0 -3
  150. package/dist/index.d.ts.map +0 -1
  151. package/dist/index.js.map +0 -1
  152. package/dist/job-fields.d.ts +0 -47
  153. package/dist/job-fields.d.ts.map +0 -1
  154. package/dist/job-fields.js +0 -148
  155. package/dist/job-fields.js.map +0 -1
  156. package/dist/machine.d.ts +0 -15
  157. package/dist/machine.d.ts.map +0 -1
  158. package/dist/machine.js +0 -64
  159. package/dist/machine.js.map +0 -1
  160. package/dist/org-fields.d.ts +0 -57
  161. package/dist/org-fields.d.ts.map +0 -1
  162. package/dist/org-fields.js +0 -167
  163. package/dist/org-fields.js.map +0 -1
  164. package/dist/posthog.d.ts +0 -38
  165. package/dist/posthog.d.ts.map +0 -1
  166. package/dist/posthog.js +0 -224
  167. package/dist/posthog.js.map +0 -1
  168. package/dist/project-fields.d.ts +0 -40
  169. package/dist/project-fields.d.ts.map +0 -1
  170. package/dist/project-fields.js +0 -79
  171. package/dist/project-fields.js.map +0 -1
  172. package/dist/prompt-consent.d.ts +0 -73
  173. package/dist/prompt-consent.d.ts.map +0 -1
  174. package/dist/prompt-consent.js +0 -191
  175. package/dist/prompt-consent.js.map +0 -1
  176. package/dist/prompt-stats.d.ts +0 -226
  177. package/dist/prompt-stats.d.ts.map +0 -1
  178. package/dist/prompt-stats.js +0 -561
  179. package/dist/prompt-stats.js.map +0 -1
  180. package/dist/referral.d.ts +0 -14
  181. package/dist/referral.d.ts.map +0 -1
  182. package/dist/referral.js +0 -28
  183. package/dist/referral.js.map +0 -1
  184. package/dist/registry.d.ts +0 -15
  185. package/dist/registry.d.ts.map +0 -1
  186. package/dist/registry.js +0 -180
  187. package/dist/registry.js.map +0 -1
  188. package/dist/resolve-command.d.ts +0 -20
  189. package/dist/resolve-command.d.ts.map +0 -1
  190. package/dist/resolve-command.js +0 -21
  191. package/dist/resolve-command.js.map +0 -1
  192. package/dist/scanners/antigravity.d.ts +0 -41
  193. package/dist/scanners/antigravity.d.ts.map +0 -1
  194. package/dist/scanners/antigravity.js +0 -210
  195. package/dist/scanners/antigravity.js.map +0 -1
  196. package/dist/scanners/github-copilot-telemetry.d.ts +0 -23
  197. package/dist/scanners/github-copilot-telemetry.d.ts.map +0 -1
  198. package/dist/scanners/github-copilot-telemetry.js +0 -220
  199. package/dist/scanners/github-copilot-telemetry.js.map +0 -1
  200. package/dist/scanners/github-copilot.d.ts +0 -68
  201. package/dist/scanners/github-copilot.d.ts.map +0 -1
  202. package/dist/scanners/github-copilot.js +0 -262
  203. package/dist/scanners/github-copilot.js.map +0 -1
  204. package/dist/scanners/incremental.d.ts +0 -282
  205. package/dist/scanners/incremental.d.ts.map +0 -1
  206. package/dist/scanners/incremental.js +0 -788
  207. package/dist/scanners/incremental.js.map +0 -1
  208. package/dist/scanners/index.d.ts +0 -116
  209. package/dist/scanners/index.d.ts.map +0 -1
  210. package/dist/scanners/index.js +0 -726
  211. package/dist/scanners/index.js.map +0 -1
  212. package/dist/scanners/sqlite.d.ts +0 -18
  213. package/dist/scanners/sqlite.d.ts.map +0 -1
  214. package/dist/scanners/sqlite.js +0 -63
  215. package/dist/scanners/sqlite.js.map +0 -1
  216. package/dist/scanners/util.d.ts +0 -112
  217. package/dist/scanners/util.d.ts.map +0 -1
  218. package/dist/scanners/util.js +0 -167
  219. package/dist/scanners/util.js.map +0 -1
  220. package/dist/session.d.ts +0 -106
  221. package/dist/session.d.ts.map +0 -1
  222. package/dist/session.js +0 -191
  223. package/dist/session.js.map +0 -1
  224. package/dist/share-card-colors.d.ts +0 -19
  225. package/dist/share-card-colors.d.ts.map +0 -1
  226. package/dist/share-card-colors.js +0 -36
  227. package/dist/share-card-colors.js.map +0 -1
  228. package/dist/share-card.d.ts +0 -31
  229. package/dist/share-card.d.ts.map +0 -1
  230. package/dist/share-card.js +0 -380
  231. package/dist/share-card.js.map +0 -1
  232. package/dist/share.d.ts +0 -17
  233. package/dist/share.d.ts.map +0 -1
  234. package/dist/share.js +0 -75
  235. package/dist/share.js.map +0 -1
  236. package/dist/sync.d.ts +0 -100
  237. package/dist/sync.d.ts.map +0 -1
  238. package/dist/sync.js +0 -304
  239. package/dist/sync.js.map +0 -1
  240. package/dist/ui.d.ts +0 -33
  241. package/dist/ui.d.ts.map +0 -1
  242. package/dist/ui.js +0 -127
  243. package/dist/ui.js.map +0 -1
  244. package/dist/utils/npmGlobal.d.ts +0 -30
  245. package/dist/utils/npmGlobal.d.ts.map +0 -1
  246. package/dist/utils/npmGlobal.js +0 -69
  247. package/dist/utils/npmGlobal.js.map +0 -1
  248. package/dist/utils/openBrowser.d.ts +0 -14
  249. package/dist/utils/openBrowser.d.ts.map +0 -1
  250. package/dist/utils/openBrowser.js +0 -47
  251. package/dist/utils/openBrowser.js.map +0 -1
  252. package/dist/utils/updateCheck.d.ts +0 -16
  253. package/dist/utils/updateCheck.d.ts.map +0 -1
  254. package/dist/utils/updateCheck.js +0 -111
  255. package/dist/utils/updateCheck.js.map +0 -1
  256. package/dist/utils/waitForEnter.d.ts +0 -12
  257. package/dist/utils/waitForEnter.d.ts.map +0 -1
  258. package/dist/utils/waitForEnter.js +0 -43
  259. package/dist/utils/waitForEnter.js.map +0 -1
@@ -1,726 +0,0 @@
1
- import { readdir, readFile, stat } from 'node:fs/promises';
2
- import { homedir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { resolveCursorAuth } from '../config.js';
5
- import { scanAntigravity } from './antigravity.js';
6
- import { scanGitHubCopilot } from './github-copilot.js';
7
- import { num, queryDb, str } from './sqlite.js';
8
- import { emptyResult, findFiles, TokenCollector, toDateStr, } from './util.js';
9
- export * from './util.js';
10
- // One self-contained scanner per tool. Each returns a uniform ScanResult and
11
- // keeps its own state (via TokenCollector), so they're pure and run in parallel.
12
- // Where each harness keeps its logs. Resolved per call rather than at import
13
- // time so a test (or a run with a different HOME) sees the right home dir.
14
- export function claudeProjectsDir() {
15
- return join(homedir(), '.claude', 'projects');
16
- }
17
- export function codexSessionsDir() {
18
- return join(homedir(), '.codex', 'sessions');
19
- }
20
- export function openclawAgentsDir() {
21
- return join(homedir(), '.openclaw', 'agents');
22
- }
23
- export function hermesDbPath() {
24
- return join(homedir(), '.hermes', 'state.db');
25
- }
26
- export function opencodeDir() {
27
- return join(homedir(), '.local', 'share', 'opencode');
28
- }
29
- export function opencodeDbPath() {
30
- return join(opencodeDir(), 'opencode.db');
31
- }
32
- /** Grok Build home. `GROK_HOME` wins so tests (and unusual installs) can point elsewhere. */
33
- export function grokHome() {
34
- const override = process.env.GROK_HOME?.trim();
35
- return override ? override : join(homedir(), '.grok');
36
- }
37
- export function grokLogsDir() {
38
- return join(grokHome(), 'logs');
39
- }
40
- export function parseClaudeCodeLine(line) {
41
- if (!line.trim())
42
- return null;
43
- try {
44
- const parsed = JSON.parse(line);
45
- const usage = parsed.message?.usage ?? parsed.usage ?? null;
46
- if (!usage)
47
- return null;
48
- const tokens = (usage.input_tokens ?? 0) +
49
- (usage.output_tokens ?? 0) +
50
- (usage.cache_creation_input_tokens ?? 0) +
51
- (usage.cache_read_input_tokens ?? 0);
52
- if (tokens <= 0)
53
- return null;
54
- let date = null;
55
- if (parsed.timestamp) {
56
- const d = new Date(typeof parsed.timestamp === 'string' && /^\d+$/.test(parsed.timestamp)
57
- ? Number(parsed.timestamp)
58
- : parsed.timestamp);
59
- date = toDateStr(d);
60
- }
61
- return { tokens, model: parsed.message?.model ?? '', date };
62
- }
63
- catch {
64
- // malformed line (or an unparseable timestamp) — skip it
65
- return null;
66
- }
67
- }
68
- /** Every Claude Code transcript on this machine. */
69
- export async function claudeCodeFiles() {
70
- return findFiles(claudeProjectsDir(), '.jsonl');
71
- }
72
- export async function scanClaudeCode() {
73
- const collector = new TokenCollector('claude_code');
74
- for (const filePath of await claudeCodeFiles()) {
75
- try {
76
- const content = await readFile(filePath, 'utf8');
77
- let fallbackDate = null;
78
- for (const line of content.split('\n')) {
79
- const usage = parseClaudeCodeLine(line);
80
- if (!usage)
81
- continue;
82
- let date = usage.date;
83
- if (!date) {
84
- fallbackDate ??= toDateStr((await stat(filePath)).mtime);
85
- date = fallbackDate;
86
- }
87
- collector.addDaily(date, usage.model, usage.tokens, 1);
88
- }
89
- }
90
- catch {
91
- // skip unreadable files
92
- }
93
- }
94
- return collector.result();
95
- }
96
- /**
97
- * Extract the session date from a Codex session path laid out as
98
- * `YYYY/MM/DD/file.jsonl`, or null if the path doesn't carry one. Splits on
99
- * either separator so it works on Windows (backslashes) too.
100
- */
101
- export function codexDateFromRelPath(relPath) {
102
- const parts = relPath.split(/[\\/]/);
103
- if (parts.length >= 3) {
104
- return `${parts[0]}-${parts[1]?.padStart(2, '0')}-${parts[2]?.padStart(2, '0')}`;
105
- }
106
- return null;
107
- }
108
- /** Session date for a Codex log path (.codex/sessions/YYYY/MM/DD/file.jsonl). */
109
- export function codexDateForFile(filePath) {
110
- return codexDateFromRelPath(filePath.slice(codexSessionsDir().length + 1));
111
- }
112
- /** Every Codex session log on this machine. */
113
- export async function codexFiles() {
114
- return findFiles(codexSessionsDir(), '.jsonl');
115
- }
116
- /**
117
- * The running token total (and last model) a chunk of a Codex session log
118
- * reports. Codex writes cumulative totals rather than per-message deltas, so a
119
- * file's contribution is the max it ever reports — which also means an appended
120
- * chunk can only ever raise that max, and the incremental tick can take the
121
- * difference without re-reading the whole file.
122
- */
123
- export function codexFileTotals(content) {
124
- let maxTotal = 0;
125
- let model = '';
126
- for (const line of content.split('\n')) {
127
- if (!line.trim())
128
- continue;
129
- try {
130
- const parsed = JSON.parse(line);
131
- if (parsed.payload?.model)
132
- model = parsed.payload.model;
133
- const usage = parsed.payload?.info?.total_token_usage;
134
- if (usage) {
135
- const t = (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
136
- if (t > maxTotal)
137
- maxTotal = t;
138
- }
139
- }
140
- catch {
141
- // skip malformed lines
142
- }
143
- }
144
- return { maxTotal, model };
145
- }
146
- export async function scanCodex() {
147
- const collector = new TokenCollector('codex');
148
- for (const filePath of await codexFiles()) {
149
- try {
150
- const { maxTotal, model } = codexFileTotals(await readFile(filePath, 'utf8'));
151
- if (maxTotal <= 0)
152
- continue;
153
- const date = codexDateForFile(filePath) ?? toDateStr((await stat(filePath)).mtime);
154
- collector.addDaily(date, model, maxTotal, 1);
155
- }
156
- catch {
157
- // skip unreadable files
158
- }
159
- }
160
- return collector.result();
161
- }
162
- export async function scanHermes() {
163
- const dbPath = hermesDbPath();
164
- try {
165
- await stat(dbPath);
166
- }
167
- catch {
168
- return emptyResult('hermes');
169
- }
170
- const collector = new TokenCollector('hermes');
171
- try {
172
- const rows = await queryDb(dbPath, `select s.id, coalesce(s.model,'') as model, s.started_at, s.input_tokens, s.output_tokens, s.cache_read_tokens, s.cache_write_tokens, s.reasoning_tokens, coalesce((select count(*) from messages m where m.session_id = s.id and m.role = 'assistant'), 0) as msg_count from sessions s`);
173
- if (rows.length === 0)
174
- return emptyResult('hermes');
175
- for (const row of rows) {
176
- const [, model, startedAt, inputTokens, outputTokens, cacheRead, cacheWrite, reasoning, msgCount,] = row;
177
- const tokens = num(inputTokens) +
178
- num(outputTokens) +
179
- num(cacheRead) +
180
- num(cacheWrite) +
181
- num(reasoning);
182
- if (tokens <= 0)
183
- continue;
184
- const tsSec = num(startedAt);
185
- if (!Number.isFinite(tsSec) || tsSec <= 0)
186
- continue;
187
- const d = new Date(Math.round(tsSec * 1000));
188
- const date = toDateStr(d);
189
- const m = str(model).trim();
190
- const messages = num(msgCount) || 1;
191
- collector.addDaily(date, m, tokens, messages);
192
- }
193
- }
194
- catch {
195
- return emptyResult('hermes');
196
- }
197
- return collector.result();
198
- }
199
- export function parseOpenclawLine(line) {
200
- if (!line.trim())
201
- return null;
202
- try {
203
- const parsed = JSON.parse(line);
204
- const usage = parsed.usage ??
205
- parsed.tokenUsage ??
206
- parsed.message?.usage ??
207
- parsed.response?.usage ??
208
- null;
209
- if (!usage || typeof usage !== 'object')
210
- return null;
211
- const tokens = (usage.total ??
212
- (usage.input ?? usage.inputTokens ?? 0) +
213
- (usage.output ?? usage.outputTokens ?? 0) +
214
- (usage.cacheRead ?? usage.cacheReadTokens ?? 0) +
215
- (usage.cacheWrite ?? usage.cacheWriteTokens ?? 0));
216
- if (!tokens || tokens <= 0)
217
- return null;
218
- let date = null;
219
- const ts = parsed.timestamp ?? parsed.t ?? parsed.time ?? null;
220
- if (ts) {
221
- const tsMs = typeof ts === 'string' && /^\d+$/.test(ts) ? Number(ts) : ts;
222
- const d = new Date(tsMs);
223
- if (!Number.isNaN(d.getTime()))
224
- date = toDateStr(d);
225
- }
226
- const model = parsed.model ?? parsed.response?.model ?? '';
227
- return { tokens, model, date };
228
- }
229
- catch {
230
- // skip malformed lines
231
- return null;
232
- }
233
- }
234
- /** Every OpenClaw session log, across all of its agents. */
235
- export async function openclawFiles() {
236
- const agentsDir = openclawAgentsDir();
237
- const sessionFiles = [];
238
- try {
239
- const agentIds = await readdir(agentsDir, { withFileTypes: true });
240
- for (const entry of agentIds) {
241
- if (!entry.isDirectory())
242
- continue;
243
- const discovered = await findFiles(join(agentsDir, entry.name, 'sessions'), '.jsonl');
244
- sessionFiles.push(...discovered);
245
- }
246
- }
247
- catch {
248
- // no OpenClaw on this machine
249
- }
250
- return sessionFiles;
251
- }
252
- export async function scanOpenclaw() {
253
- const collector = new TokenCollector('openclaw');
254
- for (const filePath of await openclawFiles()) {
255
- try {
256
- const content = await readFile(filePath, 'utf8');
257
- let fileFallbackDate = null;
258
- for (const line of content.split('\n')) {
259
- const usage = parseOpenclawLine(line);
260
- if (!usage)
261
- continue;
262
- let date = usage.date;
263
- if (!date) {
264
- fileFallbackDate ??= toDateStr((await stat(filePath)).mtime);
265
- date = fileFallbackDate;
266
- }
267
- collector.addDaily(date, usage.model, usage.tokens, 1);
268
- }
269
- }
270
- catch {
271
- // skip unreadable files
272
- }
273
- }
274
- return collector.result();
275
- }
276
- export async function scanOpenCode() {
277
- const baseDir = opencodeDir();
278
- try {
279
- await stat(baseDir);
280
- }
281
- catch {
282
- return emptyResult('opencode');
283
- }
284
- const collector = new TokenCollector('opencode');
285
- // Preferred: SQLite. Schema: message.data JSON with
286
- // { role, modelID, time: { created: ms }, tokens: { input, output, reasoning, cache: { read, write } } }
287
- const dbPath = opencodeDbPath();
288
- try {
289
- await stat(dbPath);
290
- const rows = await queryDb(dbPath, `SELECT json_extract(data,'$.time.created'), coalesce(json_extract(data,'$.modelID'),''), coalesce(json_extract(data,'$.tokens.input'),0), coalesce(json_extract(data,'$.tokens.output'),0), coalesce(json_extract(data,'$.tokens.cache.read'),0), coalesce(json_extract(data,'$.tokens.cache.write'),0), coalesce(json_extract(data,'$.tokens.reasoning'),0) FROM message WHERE json_extract(data,'$.role')='assistant'`);
291
- if (rows.length > 0) {
292
- for (const row of rows) {
293
- const [time, model, inp, out, cr, cw, reason] = row;
294
- const tokens = num(inp) + num(out) + num(cr) + num(cw) + num(reason);
295
- if (tokens <= 0)
296
- continue;
297
- const tsMs = num(time);
298
- if (!Number.isFinite(tsMs) || tsMs <= 0)
299
- continue;
300
- const d = new Date(tsMs);
301
- const m = str(model).trim();
302
- collector.addDaily(toDateStr(d), m, tokens, 1);
303
- }
304
- const dbResult = collector.result();
305
- if (dbResult.daily.length > 0)
306
- return dbResult;
307
- }
308
- }
309
- catch {
310
- // DB unavailable; fall through to JSON files
311
- }
312
- // Fallback: storage/message/{sessionID}/msg_*.json
313
- const messageDir = join(baseDir, 'storage', 'message');
314
- try {
315
- await stat(messageDir);
316
- }
317
- catch {
318
- return collector.result();
319
- }
320
- try {
321
- const sessionDirs = await readdir(messageDir, { withFileTypes: true });
322
- for (const sessionEntry of sessionDirs) {
323
- if (!sessionEntry.isDirectory())
324
- continue;
325
- const msgFiles = await findFiles(join(messageDir, sessionEntry.name), '.json');
326
- for (const filePath of msgFiles) {
327
- try {
328
- const parsed = JSON.parse(await readFile(filePath, 'utf8'));
329
- if (parsed.role !== 'assistant')
330
- continue;
331
- const t = parsed.tokens;
332
- if (!t || typeof t !== 'object')
333
- continue;
334
- const tokens = (t.input ?? 0) +
335
- (t.output ?? 0) +
336
- (t.cache?.read ?? 0) +
337
- (t.cache?.write ?? 0) +
338
- (t.reasoning ?? 0);
339
- if (tokens <= 0)
340
- continue;
341
- const tsMs = parsed.time?.created ?? 0;
342
- const date = tsMs > 0
343
- ? toDateStr(new Date(tsMs))
344
- : toDateStr((await stat(filePath)).mtime);
345
- const model = String(parsed.modelID ?? '');
346
- collector.addDaily(date, model, tokens, 1);
347
- }
348
- catch {
349
- // skip malformed files
350
- }
351
- }
352
- }
353
- }
354
- catch {
355
- // skip
356
- }
357
- return collector.result();
358
- }
359
- /**
360
- * Grok Build writes one `shell.turn.inference_done` line per model round to
361
- * `~/.grok/logs/unified.jsonl`. `prompt_tokens` is the full prompt (cache hits
362
- * included); `cached_prompt_tokens` and `reasoning_tokens` are subsets, so the
363
- * billed total is prompt + completion — same shape as counting Claude's
364
- * input+output+cache as one number.
365
- */
366
- export function parseGrokLine(line) {
367
- if (!line.includes('shell.turn.inference_done'))
368
- return null;
369
- try {
370
- const parsed = JSON.parse(line);
371
- if (parsed.msg !== 'shell.turn.inference_done')
372
- return null;
373
- const ctx = parsed.ctx ?? {};
374
- const tokens = Number(ctx.prompt_tokens ?? 0) + Number(ctx.completion_tokens ?? 0);
375
- if (!Number.isFinite(tokens) || tokens <= 0)
376
- return null;
377
- let date = null;
378
- if (parsed.ts) {
379
- const d = new Date(parsed.ts);
380
- if (!Number.isNaN(d.getTime()))
381
- date = toDateStr(d);
382
- }
383
- const model = String(ctx.model_id ?? ctx.model ?? parsed.model ?? '').trim() || 'grok';
384
- return { tokens, model, date };
385
- }
386
- catch {
387
- return null;
388
- }
389
- }
390
- /** Grok Build's local usage log (and any rotated `unified*.jsonl` siblings). */
391
- export async function grokLogFiles() {
392
- return findFiles(grokLogsDir(), '.jsonl');
393
- }
394
- export async function scanGrok() {
395
- const collector = new TokenCollector('grok');
396
- for (const filePath of await grokLogFiles()) {
397
- try {
398
- const content = await readFile(filePath, 'utf8');
399
- let fallbackDate = null;
400
- for (const line of content.split('\n')) {
401
- const usage = parseGrokLine(line);
402
- if (!usage)
403
- continue;
404
- let date = usage.date;
405
- if (!date) {
406
- fallbackDate ??= toDateStr((await stat(filePath)).mtime);
407
- date = fallbackDate;
408
- }
409
- collector.addDaily(date, usage.model, usage.tokens, 1);
410
- }
411
- }
412
- catch {
413
- // skip unreadable files
414
- }
415
- }
416
- return collector.result();
417
- }
418
- const CURSOR_TRACKING_DB = join(homedir(), '.cursor', 'ai-tracking', 'ai-code-tracking.db');
419
- /** Cursor's Electron state dir, per platform. Not where the tracking db lives. */
420
- function cursorStateDirs() {
421
- const home = homedir();
422
- if (process.platform === 'darwin') {
423
- return [join(home, 'Library', 'Application Support', 'Cursor')];
424
- }
425
- if (process.platform === 'win32') {
426
- return process.env.APPDATA ? [join(process.env.APPDATA, 'Cursor')] : [];
427
- }
428
- return [join(home, '.config', 'Cursor')];
429
- }
430
- async function pathExists(p) {
431
- try {
432
- await stat(p);
433
- return true;
434
- }
435
- catch {
436
- return false;
437
- }
438
- }
439
- async function dirHasEntries(p) {
440
- try {
441
- return (await readdir(p)).length > 0;
442
- }
443
- catch {
444
- return false;
445
- }
446
- }
447
- /**
448
- * Does this machine look like a Cursor machine? `scan` uses this to decide
449
- * whether asking for a Cursor API key is worth a prompt.
450
- *
451
- * A false positive costs a question the user has to skip, so this looks for
452
- * evidence Cursor has been *used* — the AI-tracking db, editor session state,
453
- * per-workspace storage, or local file history — rather than merely installed.
454
- * Every probe is a filesystem existence check, so it stays cheap enough to run
455
- * inline in the scan flow.
456
- */
457
- export async function detectCursorUsage() {
458
- if (await pathExists(CURSOR_TRACKING_DB))
459
- return true;
460
- for (const dir of cursorStateDirs()) {
461
- if (await pathExists(join(dir, 'User', 'globalStorage', 'state.vscdb'))) {
462
- return true;
463
- }
464
- if (await dirHasEntries(join(dir, 'User', 'workspaceStorage')))
465
- return true;
466
- if (await dirHasEntries(join(dir, 'User', 'History')))
467
- return true;
468
- }
469
- return false;
470
- }
471
- export async function scanCursorLocal() {
472
- const dbPath = CURSOR_TRACKING_DB;
473
- try {
474
- await stat(dbPath);
475
- }
476
- catch {
477
- return { ...emptyResult('cursor'), cursorStats: null };
478
- }
479
- const collector = new TokenCollector('cursor');
480
- try {
481
- const rows = await queryDb(dbPath, `select commitHash, linesAdded, linesDeleted, composerLinesAdded, composerLinesDeleted, humanLinesAdded, humanLinesDeleted, v2AiPercentage from scored_commits;`);
482
- if (rows.length === 0)
483
- return { ...emptyResult('cursor'), cursorStats: null };
484
- let totalTokens = 0;
485
- let totalAiLines = 0;
486
- let totalHumanLines = 0;
487
- let aiPctSum = 0;
488
- for (const row of rows) {
489
- const composerAdded = num(row[3]);
490
- const composerDeleted = num(row[4]);
491
- const humanAdded = num(row[5]);
492
- const aiLines = composerAdded + composerDeleted;
493
- totalAiLines += composerAdded;
494
- totalHumanLines += humanAdded;
495
- aiPctSum += num(row[7]);
496
- totalTokens += aiLines * 30;
497
- }
498
- let models = [];
499
- try {
500
- const modelRows = await queryDb(dbPath, `select model, count(*) from ai_code_hashes where model is not null and model != '' group by model order by count(*) desc;`);
501
- models = modelRows.map((row) => ({
502
- model: str(row[0]),
503
- uses: num(row[1]),
504
- }));
505
- }
506
- catch {
507
- // optional
508
- }
509
- if (totalTokens > 0)
510
- collector.addDaily(toDateStr(new Date()), '', totalTokens, 1);
511
- const stats = {
512
- totalCommits: rows.length,
513
- aiLinesAdded: totalAiLines,
514
- humanLinesAdded: totalHumanLines,
515
- avgAiPercent: rows.length > 0 ? Math.round((aiPctSum / rows.length) * 10) / 10 : 0,
516
- models,
517
- };
518
- return collector.result({ cursorStats: stats });
519
- }
520
- catch {
521
- return { ...emptyResult('cursor'), cursorStats: null };
522
- }
523
- }
524
- export async function scanCursorApi() {
525
- const { apiKey, email } = await resolveCursorAuth();
526
- if (!apiKey) {
527
- return { ...emptyResult('cursor'), cursorScanStatus: { source: 'none' } };
528
- }
529
- const collector = new TokenCollector('cursor');
530
- const now = Date.now();
531
- const startDate = now - 5 * 365 * 24 * 60 * 60 * 1000;
532
- const endDate = now;
533
- let page = 1;
534
- let hasMore = true;
535
- let retries = 0;
536
- const MAX_RETRIES = 3;
537
- let eventsProcessed = 0;
538
- let partialReason = null;
539
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
540
- while (hasMore) {
541
- const body = {
542
- startDate,
543
- endDate,
544
- page,
545
- pageSize: 100,
546
- };
547
- if (email)
548
- body.email = email;
549
- let res;
550
- try {
551
- res = await fetch('https://api.cursor.com/teams/filtered-usage-events', {
552
- method: 'POST',
553
- headers: {
554
- 'Content-Type': 'application/json',
555
- Authorization: `Bearer ${apiKey}`,
556
- },
557
- body: JSON.stringify(body),
558
- });
559
- }
560
- catch (err) {
561
- partialReason = `network: ${err instanceof Error ? err.message : 'unknown'}`;
562
- break;
563
- }
564
- if (!res.ok) {
565
- if (res.status === 429 && retries < MAX_RETRIES) {
566
- await sleep(2000 * 2 ** retries);
567
- retries++;
568
- continue;
569
- }
570
- if (res.status === 401 || res.status === 403) {
571
- if (eventsProcessed === 0) {
572
- return {
573
- ...emptyResult('cursor'),
574
- cursorScanStatus: {
575
- source: 'api-failed',
576
- reason: `Cursor rejected the API key (auth ${res.status})`,
577
- },
578
- };
579
- }
580
- partialReason = `auth ${res.status} mid-scan`;
581
- break;
582
- }
583
- partialReason = `HTTP ${res.status}`;
584
- break;
585
- }
586
- retries = 0;
587
- let data;
588
- try {
589
- data = await res.json();
590
- }
591
- catch {
592
- partialReason = 'invalid json';
593
- break;
594
- }
595
- for (const event of data.usageEvents ?? []) {
596
- if (!event.isTokenBasedCall)
597
- continue;
598
- const tu = event.tokenUsage ??
599
- event;
600
- const tokens = (tu.inputTokens ?? 0) +
601
- (tu.outputTokens ?? 0) +
602
- (tu.cacheWriteTokens ?? 0) +
603
- (tu.cacheReadTokens ?? 0);
604
- if (tokens > 0 && event.timestamp) {
605
- const rawTs = event.timestamp;
606
- const tsMs = typeof rawTs === 'string' && /^\d+$/.test(rawTs)
607
- ? Number(rawTs)
608
- : rawTs;
609
- const d = new Date(tsMs);
610
- const model = event.model ?? '';
611
- collector.addDaily(toDateStr(d), model, tokens, 1);
612
- eventsProcessed++;
613
- }
614
- }
615
- hasMore = data.pagination?.hasNextPage ?? false;
616
- page++;
617
- if (page > 500) {
618
- partialReason = 'pagination cap (500 pages)';
619
- break;
620
- }
621
- }
622
- const cursorScanStatus = partialReason
623
- ? { source: 'api-partial', events: eventsProcessed, reason: partialReason }
624
- : { source: 'api', events: eventsProcessed };
625
- return collector.result({ cursorScanStatus });
626
- }
627
- /**
628
- * Choose between the two Cursor sources: the API scan when it produced data,
629
- * else the local `lines × 30` estimate.
630
- *
631
- * Either way the winner carries the local commit stats (the API has no notion of
632
- * commits, and the share card renders them) and the API's status. Propagating
633
- * the status onto the local fallback is what lets a rejected key surface as
634
- * `api-failed` — otherwise a bad key is indistinguishable from no key at all,
635
- * and the user silently gets the estimate while believing they get exact counts.
636
- */
637
- function resolveCursor(api, local) {
638
- if (api.daily.length > 0) {
639
- return { ...api, cursorStats: local.cursorStats ?? null };
640
- }
641
- return {
642
- ...local,
643
- cursorScanStatus: api.cursorScanStatus ?? { source: 'none' },
644
- };
645
- }
646
- /** Run every scanner in parallel, returning one resolved result per tool. */
647
- export async function collectToolScans() {
648
- const [claude, codex, cursorApi, cursorLocal, openclaw, hermes, opencode, grok, copilot, antigravity,] = await Promise.all([
649
- scanClaudeCode(),
650
- scanCodex(),
651
- scanCursorApi(),
652
- scanCursorLocal(),
653
- scanOpenclaw(),
654
- scanHermes(),
655
- scanOpenCode(),
656
- scanGrok(),
657
- scanGitHubCopilot(),
658
- scanAntigravity(),
659
- ]);
660
- const cursor = resolveCursor(cursorApi, cursorLocal);
661
- return [
662
- claude,
663
- codex,
664
- cursor,
665
- openclaw,
666
- hermes,
667
- opencode,
668
- grok,
669
- copilot,
670
- antigravity,
671
- ];
672
- }
673
- /**
674
- * Re-run only the Cursor API scanner and swap the result into an existing set of
675
- * scans. `scan` calls this after the user supplies a key mid-flow, so the exact
676
- * API numbers replace the local estimate without re-walking every other tool's
677
- * logs (which for a heavy Claude Code user means re-parsing thousands of files).
678
- */
679
- export async function rescanCursorWithApi(results) {
680
- const api = await scanCursorApi();
681
- return results.map((r) => (r.tool === 'cursor' ? resolveCursor(api, r) : r));
682
- }
683
- /** Merge per-tool results into a single submit-ready payload. */
684
- export function mergeToolScans(results) {
685
- const toolTotals = {};
686
- const dailyTotals = [];
687
- const modelTotals = {};
688
- const modelsByTool = {};
689
- let grandTotal = 0;
690
- for (const r of results) {
691
- let toolSum = 0;
692
- for (const d of r.daily) {
693
- dailyTotals.push(d);
694
- toolSum += d.tokens;
695
- }
696
- toolTotals[r.tool] = (toolTotals[r.tool] ?? 0) + toolSum;
697
- grandTotal += toolSum;
698
- let byTool = modelsByTool[r.tool];
699
- if (!byTool) {
700
- byTool = {};
701
- modelsByTool[r.tool] = byTool;
702
- }
703
- for (const [model, tokens] of Object.entries(r.models)) {
704
- modelTotals[model] = (modelTotals[model] ?? 0) + tokens;
705
- byTool[model] = (byTool[model] ?? 0) + tokens;
706
- }
707
- }
708
- const cursor = results.find((r) => r.tool === 'cursor');
709
- return {
710
- toolTotals,
711
- dailyTotals,
712
- modelTotals,
713
- modelsByTool,
714
- grandTotal,
715
- cursorStats: cursor?.cursorStats ?? null,
716
- cursorScanStatus: cursor?.cursorScanStatus ?? { source: 'none' },
717
- };
718
- }
719
- /**
720
- * Run every scanner and merge the results — the one-shot entry point for callers
721
- * that don't need to re-scan a single tool afterwards (`sync`, the daily job).
722
- */
723
- export async function scanAllTools() {
724
- return mergeToolScans(await collectToolScans());
725
- }
726
- //# sourceMappingURL=index.js.map