hacklab 0.0.2 → 0.3.1

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 (154) hide show
  1. package/README.md +44 -3
  2. package/bin/ddd +19 -0
  3. package/dist/commands/brag.d.ts +3 -6
  4. package/dist/commands/brag.d.ts.map +1 -1
  5. package/dist/commands/brag.js +68 -299
  6. package/dist/commands/brag.js.map +1 -1
  7. package/dist/commands/config.d.ts +2 -0
  8. package/dist/commands/config.d.ts.map +1 -0
  9. package/dist/commands/config.js +48 -0
  10. package/dist/commands/config.js.map +1 -0
  11. package/dist/commands/drop.d.ts +2 -0
  12. package/dist/commands/drop.d.ts.map +1 -0
  13. package/dist/commands/drop.js +29 -0
  14. package/dist/commands/drop.js.map +1 -0
  15. package/dist/commands/exam.d.ts +5 -0
  16. package/dist/commands/exam.d.ts.map +1 -0
  17. package/dist/commands/exam.js +224 -0
  18. package/dist/commands/exam.js.map +1 -0
  19. package/dist/commands/join.d.ts +10 -9
  20. package/dist/commands/join.d.ts.map +1 -1
  21. package/dist/commands/join.js +213 -285
  22. package/dist/commands/join.js.map +1 -1
  23. package/dist/commands/login.d.ts +1 -15
  24. package/dist/commands/login.d.ts.map +1 -1
  25. package/dist/commands/login.js +69 -133
  26. package/dist/commands/login.js.map +1 -1
  27. package/dist/commands/scan-profile.d.ts +33 -0
  28. package/dist/commands/scan-profile.d.ts.map +1 -0
  29. package/dist/commands/scan-profile.js +279 -0
  30. package/dist/commands/scan-profile.js.map +1 -0
  31. package/dist/commands/sync.d.ts +2 -0
  32. package/dist/commands/sync.d.ts.map +1 -0
  33. package/dist/commands/sync.js +55 -0
  34. package/dist/commands/sync.js.map +1 -0
  35. package/dist/commands/whoami.d.ts +1 -3
  36. package/dist/commands/whoami.d.ts.map +1 -1
  37. package/dist/commands/whoami.js +10 -37
  38. package/dist/commands/whoami.js.map +1 -1
  39. package/dist/config.d.ts +7 -0
  40. package/dist/config.d.ts.map +1 -0
  41. package/dist/config.js +18 -0
  42. package/dist/config.js.map +1 -0
  43. package/dist/dd.d.ts +3 -0
  44. package/dist/dd.d.ts.map +1 -0
  45. package/dist/dd.js +28 -0
  46. package/dist/dd.js.map +1 -0
  47. package/dist/index.js +117 -112
  48. package/dist/index.js.map +1 -1
  49. package/dist/project-brag.d.ts +58 -0
  50. package/dist/project-brag.d.ts.map +1 -0
  51. package/dist/project-brag.js +270 -0
  52. package/dist/project-brag.js.map +1 -0
  53. package/dist/scanners/index.d.ts +24 -0
  54. package/dist/scanners/index.d.ts.map +1 -0
  55. package/dist/scanners/index.js +543 -0
  56. package/dist/scanners/index.js.map +1 -0
  57. package/dist/scanners/util.d.ts +83 -0
  58. package/dist/scanners/util.d.ts.map +1 -0
  59. package/dist/scanners/util.js +129 -0
  60. package/dist/scanners/util.js.map +1 -0
  61. package/dist/session.d.ts +17 -13
  62. package/dist/session.d.ts.map +1 -1
  63. package/dist/session.js +46 -155
  64. package/dist/session.js.map +1 -1
  65. package/dist/share-card.d.ts +29 -0
  66. package/dist/share-card.d.ts.map +1 -0
  67. package/dist/share-card.js +401 -0
  68. package/dist/share-card.js.map +1 -0
  69. package/dist/sync.d.ts +64 -0
  70. package/dist/sync.d.ts.map +1 -0
  71. package/dist/sync.js +805 -0
  72. package/dist/sync.js.map +1 -0
  73. package/dist/ui.d.ts +10 -0
  74. package/dist/ui.d.ts.map +1 -0
  75. package/dist/ui.js +21 -0
  76. package/dist/ui.js.map +1 -0
  77. package/dist/utils/openBrowser.js.map +1 -1
  78. package/package.json +36 -29
  79. package/dist/api/client.d.ts +0 -150
  80. package/dist/api/client.d.ts.map +0 -1
  81. package/dist/api/client.js +0 -246
  82. package/dist/api/client.js.map +0 -1
  83. package/dist/commands/admin-waitlist.d.ts +0 -12
  84. package/dist/commands/admin-waitlist.d.ts.map +0 -1
  85. package/dist/commands/admin-waitlist.js +0 -96
  86. package/dist/commands/admin-waitlist.js.map +0 -1
  87. package/dist/commands/brag.utils.d.ts +0 -12
  88. package/dist/commands/brag.utils.d.ts.map +0 -1
  89. package/dist/commands/brag.utils.js +0 -43
  90. package/dist/commands/brag.utils.js.map +0 -1
  91. package/dist/commands/essay.d.ts +0 -18
  92. package/dist/commands/essay.d.ts.map +0 -1
  93. package/dist/commands/essay.js +0 -117
  94. package/dist/commands/essay.js.map +0 -1
  95. package/dist/commands/explore.d.ts +0 -9
  96. package/dist/commands/explore.d.ts.map +0 -1
  97. package/dist/commands/explore.js +0 -32
  98. package/dist/commands/explore.js.map +0 -1
  99. package/dist/commands/init.d.ts +0 -16
  100. package/dist/commands/init.d.ts.map +0 -1
  101. package/dist/commands/init.js +0 -237
  102. package/dist/commands/init.js.map +0 -1
  103. package/dist/commands/link.d.ts +0 -17
  104. package/dist/commands/link.d.ts.map +0 -1
  105. package/dist/commands/link.js +0 -36
  106. package/dist/commands/link.js.map +0 -1
  107. package/dist/commands/login.utils.d.ts +0 -3
  108. package/dist/commands/login.utils.d.ts.map +0 -1
  109. package/dist/commands/login.utils.js +0 -13
  110. package/dist/commands/login.utils.js.map +0 -1
  111. package/dist/commands/new.d.ts +0 -15
  112. package/dist/commands/new.d.ts.map +0 -1
  113. package/dist/commands/new.js +0 -172
  114. package/dist/commands/new.js.map +0 -1
  115. package/dist/commands/new.utils.d.ts +0 -7
  116. package/dist/commands/new.utils.d.ts.map +0 -1
  117. package/dist/commands/new.utils.js +0 -50
  118. package/dist/commands/new.utils.js.map +0 -1
  119. package/dist/commands/onboard.d.ts +0 -10
  120. package/dist/commands/onboard.d.ts.map +0 -1
  121. package/dist/commands/onboard.js +0 -93
  122. package/dist/commands/onboard.js.map +0 -1
  123. package/dist/commands/publish.d.ts +0 -8
  124. package/dist/commands/publish.d.ts.map +0 -1
  125. package/dist/commands/publish.js +0 -47
  126. package/dist/commands/publish.js.map +0 -1
  127. package/dist/commands/tui.d.ts +0 -12
  128. package/dist/commands/tui.d.ts.map +0 -1
  129. package/dist/commands/tui.js +0 -112
  130. package/dist/commands/tui.js.map +0 -1
  131. package/dist/help-format.d.ts +0 -4
  132. package/dist/help-format.d.ts.map +0 -1
  133. package/dist/help-format.js +0 -30
  134. package/dist/help-format.js.map +0 -1
  135. package/dist/lab/config.d.ts +0 -32
  136. package/dist/lab/config.d.ts.map +0 -1
  137. package/dist/lab/config.js +0 -148
  138. package/dist/lab/config.js.map +0 -1
  139. package/dist/lab/contentStatus.d.ts +0 -6
  140. package/dist/lab/contentStatus.d.ts.map +0 -1
  141. package/dist/lab/contentStatus.js +0 -19
  142. package/dist/lab/contentStatus.js.map +0 -1
  143. package/dist/ui/banner.d.ts +0 -2
  144. package/dist/ui/banner.d.ts.map +0 -1
  145. package/dist/ui/banner.js +0 -22
  146. package/dist/ui/banner.js.map +0 -1
  147. package/dist/utils/findRepoRoot.d.ts +0 -2
  148. package/dist/utils/findRepoRoot.d.ts.map +0 -1
  149. package/dist/utils/findRepoRoot.js +0 -17
  150. package/dist/utils/findRepoRoot.js.map +0 -1
  151. package/dist/utils/pathExists.d.ts +0 -2
  152. package/dist/utils/pathExists.d.ts.map +0 -1
  153. package/dist/utils/pathExists.js +0 -11
  154. package/dist/utils/pathExists.js.map +0 -1
package/dist/sync.js ADDED
@@ -0,0 +1,805 @@
1
+ import { readdir, readFile, stat } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { loadConfig } from './config.js';
5
+ import { dim } from './ui.js';
6
+ export const LOGIN_EXPIRED_MESSAGE = 'login expired. run hacklab login again';
7
+ const HOURLY_WINDOW_DAYS = 90;
8
+ const modelAccumulator = new Map();
9
+ const hourlyAccumulator = new Map();
10
+ const _cursorScanStatus = {
11
+ value: { source: 'none' },
12
+ };
13
+ function buildApiUrl(session, path) {
14
+ return `${session.appUrl.replace(/\/$/, '')}${path}`;
15
+ }
16
+ function isLoopbackAppUrl(value) {
17
+ try {
18
+ const url = new URL(value);
19
+ return (url.hostname === 'localhost' ||
20
+ url.hostname === '127.0.0.1' ||
21
+ url.hostname === '::1');
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ function getCauseMessage(err) {
28
+ if (!(err instanceof Error))
29
+ return null;
30
+ const cause = err.cause;
31
+ if (cause instanceof Error)
32
+ return cause.message;
33
+ if (typeof cause === 'object' && cause !== null && 'message' in cause) {
34
+ const message = cause.message;
35
+ return typeof message === 'string' ? message : null;
36
+ }
37
+ return err.message === 'fetch failed' ? null : err.message;
38
+ }
39
+ function formatFetchFailure(session, err) {
40
+ const app = session.appUrl.replace(/\/$/, '');
41
+ const cause = getCauseMessage(err);
42
+ const details = cause ? ` (${cause})` : '';
43
+ if (isLoopbackAppUrl(app)) {
44
+ return `could not reach ${app}${details}. start the web app or run ${dim(`HACKLAB_APP_URL=https://staging.hacklab.so hacklab login`)}`;
45
+ }
46
+ return `could not reach ${app}${details}. check ${dim('HACKLAB_APP_URL')} or run ${dim('hacklab login')} again`;
47
+ }
48
+ export async function fetchApi(session, path, init) {
49
+ try {
50
+ return await fetch(buildApiUrl(session, path), init);
51
+ }
52
+ catch (err) {
53
+ throw new Error(formatFetchFailure(session, err));
54
+ }
55
+ }
56
+ export async function checkSession(session) {
57
+ let res;
58
+ try {
59
+ res = await fetchApi(session, '/api/xp', {
60
+ headers: { Authorization: `Bearer ${session.token}` },
61
+ });
62
+ }
63
+ catch (err) {
64
+ return {
65
+ status: 'failed',
66
+ message: err instanceof Error ? err.message : String(err),
67
+ };
68
+ }
69
+ if (res.status === 401)
70
+ return { status: 'unauthorized' };
71
+ if (res.status === 404) {
72
+ const contentType = res.headers.get('content-type') ?? '';
73
+ if (contentType.includes('application/json'))
74
+ return { status: 'ok' };
75
+ return {
76
+ status: 'failed',
77
+ message: `Hacklab API not found at ${session.appUrl.replace(/\/$/, '')}. check ${dim('HACKLAB_APP_URL')} or run ${dim('hacklab login')} again`,
78
+ };
79
+ }
80
+ if (res.status >= 500) {
81
+ return {
82
+ status: 'failed',
83
+ message: `Hacklab API failed at ${session.appUrl.replace(/\/$/, '')} (${res.status}). try again later`,
84
+ };
85
+ }
86
+ return { status: 'ok' };
87
+ }
88
+ function hourlyCutoff() {
89
+ const d = new Date();
90
+ d.setUTCDate(d.getUTCDate() - HOURLY_WINDOW_DAYS);
91
+ return d.toISOString().slice(0, 10);
92
+ }
93
+ function addHourly(date, hour, tool, model, tokens, cutoff, messages = 1) {
94
+ if (date < cutoff)
95
+ return;
96
+ const key = `${date}|${hour}|${tool}|${model ?? ''}`;
97
+ const existing = hourlyAccumulator.get(key);
98
+ if (existing) {
99
+ existing.tokens += tokens;
100
+ existing.messages += messages;
101
+ }
102
+ else {
103
+ hourlyAccumulator.set(key, { tokens, messages });
104
+ }
105
+ }
106
+ function flushHourly() {
107
+ const out = [];
108
+ for (const [key, { tokens, messages }] of hourlyAccumulator.entries()) {
109
+ const [date, hourStr, tool, model] = key.split('|');
110
+ if (!date || hourStr === undefined || !tool)
111
+ continue;
112
+ out.push({
113
+ date,
114
+ hour: Number(hourStr),
115
+ tool,
116
+ model: model || undefined,
117
+ tokens,
118
+ messages,
119
+ });
120
+ }
121
+ return out;
122
+ }
123
+ function addToDaily(map, date, model, tokens, messages) {
124
+ const key = `${date}|${model}`;
125
+ const existing = map.get(key);
126
+ if (existing) {
127
+ existing.tokens += tokens;
128
+ existing.messages += messages;
129
+ }
130
+ else {
131
+ map.set(key, { tokens, messages });
132
+ }
133
+ }
134
+ function dailyMapToEntries(map, tool) {
135
+ return Array.from(map.entries()).map(([key, { tokens, messages }]) => {
136
+ const [date, model] = key.split('|');
137
+ return {
138
+ date: date ?? '',
139
+ tool,
140
+ tokens,
141
+ messages,
142
+ model: model || undefined,
143
+ };
144
+ });
145
+ }
146
+ function toDateStr(ts) {
147
+ const v = typeof ts === 'string' && /^\d+$/.test(ts) ? Number(ts) : ts;
148
+ const d = new Date(v);
149
+ return d.toISOString().slice(0, 10);
150
+ }
151
+ export function formatTokens(n) {
152
+ if (n >= 1_000_000_000)
153
+ return `${(n / 1_000_000_000).toFixed(1)}B`;
154
+ if (n >= 1_000_000)
155
+ return `${(n / 1_000_000).toFixed(1)}M`;
156
+ if (n >= 1_000)
157
+ return `${(n / 1_000).toFixed(0)}K`;
158
+ return String(n);
159
+ }
160
+ export function formatBytes(n) {
161
+ if (n >= 1_073_741_824)
162
+ return `${(n / 1_073_741_824).toFixed(1)}GB`;
163
+ if (n >= 1_048_576)
164
+ return `${(n / 1_048_576).toFixed(1)}MB`;
165
+ if (n >= 1_024)
166
+ return `${(n / 1_024).toFixed(0)}KB`;
167
+ return `${n}B`;
168
+ }
169
+ async function findFiles(dir, ext) {
170
+ const results = [];
171
+ async function walk(current) {
172
+ try {
173
+ const entries = await readdir(current, { withFileTypes: true });
174
+ for (const entry of entries) {
175
+ const fullPath = join(current, entry.name);
176
+ if (entry.isDirectory()) {
177
+ await walk(fullPath);
178
+ }
179
+ else if (entry.name.endsWith(ext)) {
180
+ results.push(fullPath);
181
+ }
182
+ }
183
+ }
184
+ catch {
185
+ // skip unreadable directories
186
+ }
187
+ }
188
+ await walk(dir);
189
+ return results;
190
+ }
191
+ async function scanClaudeCode() {
192
+ const claudeDir = join(homedir(), '.claude', 'projects');
193
+ try {
194
+ await stat(claudeDir);
195
+ }
196
+ catch {
197
+ return [];
198
+ }
199
+ const files = await findFiles(claudeDir, '.jsonl');
200
+ const dailyByModel = new Map();
201
+ const cutoff = hourlyCutoff();
202
+ for (const filePath of files) {
203
+ try {
204
+ const content = await readFile(filePath, 'utf8');
205
+ for (const line of content.split('\n')) {
206
+ if (!line.trim())
207
+ continue;
208
+ try {
209
+ const parsed = JSON.parse(line);
210
+ const usage = parsed.message?.usage ?? parsed.usage ?? null;
211
+ if (!usage)
212
+ continue;
213
+ const tokens = (usage.input_tokens ?? 0) +
214
+ (usage.output_tokens ?? 0) +
215
+ (usage.cache_creation_input_tokens ?? 0) +
216
+ (usage.cache_read_input_tokens ?? 0);
217
+ if (tokens <= 0)
218
+ continue;
219
+ let date;
220
+ let hour = null;
221
+ if (parsed.timestamp) {
222
+ const d = new Date(typeof parsed.timestamp === 'string' &&
223
+ /^\d+$/.test(parsed.timestamp)
224
+ ? Number(parsed.timestamp)
225
+ : parsed.timestamp);
226
+ date = toDateStr(d);
227
+ hour = d.getHours();
228
+ }
229
+ else {
230
+ const s = await stat(filePath);
231
+ date = toDateStr(s.mtime);
232
+ }
233
+ const model = parsed.message?.model ?? '';
234
+ addToDaily(dailyByModel, date, model, tokens, 1);
235
+ if (model) {
236
+ modelAccumulator.set(model, (modelAccumulator.get(model) ?? 0) + tokens);
237
+ }
238
+ if (hour !== null) {
239
+ addHourly(date, hour, 'claude_code', model, tokens, cutoff, 1);
240
+ }
241
+ }
242
+ catch {
243
+ // skip malformed lines
244
+ }
245
+ }
246
+ }
247
+ catch {
248
+ // skip unreadable files
249
+ }
250
+ }
251
+ return dailyMapToEntries(dailyByModel, 'claude_code');
252
+ }
253
+ async function scanCodex() {
254
+ const codexDir = join(homedir(), '.codex', 'sessions');
255
+ try {
256
+ await stat(codexDir);
257
+ }
258
+ catch {
259
+ return [];
260
+ }
261
+ const files = await findFiles(codexDir, '.jsonl');
262
+ const dailyByModel = new Map();
263
+ for (const filePath of files) {
264
+ try {
265
+ const content = await readFile(filePath, 'utf8');
266
+ const relPath = filePath.slice(codexDir.length + 1);
267
+ const parts = relPath.split('/');
268
+ let date;
269
+ if (parts.length >= 3) {
270
+ date = `${parts[0]}-${parts[1]?.padStart(2, '0')}-${parts[2]?.padStart(2, '0')}`;
271
+ }
272
+ else {
273
+ const s = await stat(filePath);
274
+ date = toDateStr(s.mtime);
275
+ }
276
+ let maxTotal = 0;
277
+ let fileModel = '';
278
+ for (const line of content.split('\n')) {
279
+ if (!line.trim())
280
+ continue;
281
+ try {
282
+ const parsed = JSON.parse(line);
283
+ if (parsed.payload?.model)
284
+ fileModel = parsed.payload.model;
285
+ const usage = parsed.payload?.info?.total_token_usage;
286
+ if (usage) {
287
+ const t = (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
288
+ if (t > maxTotal)
289
+ maxTotal = t;
290
+ }
291
+ }
292
+ catch {
293
+ // skip
294
+ }
295
+ }
296
+ if (maxTotal > 0) {
297
+ addToDaily(dailyByModel, date, fileModel, maxTotal, 1);
298
+ if (fileModel) {
299
+ modelAccumulator.set(fileModel, (modelAccumulator.get(fileModel) ?? 0) + maxTotal);
300
+ }
301
+ }
302
+ }
303
+ catch {
304
+ // skip
305
+ }
306
+ }
307
+ return dailyMapToEntries(dailyByModel, 'codex');
308
+ }
309
+ async function scanHermes() {
310
+ const dbPath = join(homedir(), '.hermes', 'state.db');
311
+ try {
312
+ await stat(dbPath);
313
+ }
314
+ catch {
315
+ return [];
316
+ }
317
+ const { execSync } = await import('node:child_process');
318
+ const dailyByModel = new Map();
319
+ const cutoff = hourlyCutoff();
320
+ try {
321
+ const raw = execSync(`sqlite3 -readonly "${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"`, { encoding: 'utf8', timeout: 10000 }).trim();
322
+ if (!raw)
323
+ return [];
324
+ for (const row of raw.split('\n')) {
325
+ const [_id, model, startedAt, inputTokens, outputTokens, cacheRead, cacheWrite, reasoning, msgCount,] = row.split('|');
326
+ const tokens = (Number.parseInt(inputTokens ?? '0', 10) || 0) +
327
+ (Number.parseInt(outputTokens ?? '0', 10) || 0) +
328
+ (Number.parseInt(cacheRead ?? '0', 10) || 0) +
329
+ (Number.parseInt(cacheWrite ?? '0', 10) || 0) +
330
+ (Number.parseInt(reasoning ?? '0', 10) || 0);
331
+ if (tokens <= 0)
332
+ continue;
333
+ const tsSec = Number.parseFloat(startedAt ?? '0');
334
+ if (!Number.isFinite(tsSec) || tsSec <= 0)
335
+ continue;
336
+ const tsMs = Math.round(tsSec * 1000);
337
+ const d = new Date(tsMs);
338
+ const date = toDateStr(d);
339
+ const hour = d.getHours();
340
+ const m = (model ?? '').trim();
341
+ const messages = Number.parseInt(msgCount ?? '0', 10) || 1;
342
+ addToDaily(dailyByModel, date, m, tokens, messages);
343
+ if (m) {
344
+ modelAccumulator.set(m, (modelAccumulator.get(m) ?? 0) + tokens);
345
+ }
346
+ addHourly(date, hour, 'hermes', m, tokens, cutoff, messages);
347
+ }
348
+ }
349
+ catch {
350
+ return [];
351
+ }
352
+ return dailyMapToEntries(dailyByModel, 'hermes');
353
+ }
354
+ async function scanOpenclaw() {
355
+ const agentsDir = join(homedir(), '.openclaw', 'agents');
356
+ try {
357
+ await stat(agentsDir);
358
+ }
359
+ catch {
360
+ return [];
361
+ }
362
+ const sessionFiles = [];
363
+ try {
364
+ const agentIds = await readdir(agentsDir, { withFileTypes: true });
365
+ for (const entry of agentIds) {
366
+ if (!entry.isDirectory())
367
+ continue;
368
+ const sessionsSubdir = join(agentsDir, entry.name, 'sessions');
369
+ const discovered = await findFiles(sessionsSubdir, '.jsonl');
370
+ sessionFiles.push(...discovered);
371
+ }
372
+ }
373
+ catch {
374
+ return [];
375
+ }
376
+ if (sessionFiles.length === 0)
377
+ return [];
378
+ const dailyByModel = new Map();
379
+ const cutoff = hourlyCutoff();
380
+ for (const filePath of sessionFiles) {
381
+ try {
382
+ const content = await readFile(filePath, 'utf8');
383
+ let fileFallbackDate = null;
384
+ for (const line of content.split('\n')) {
385
+ if (!line.trim())
386
+ continue;
387
+ try {
388
+ const parsed = JSON.parse(line);
389
+ const usage = parsed.usage ??
390
+ parsed.tokenUsage ??
391
+ parsed.message?.usage ??
392
+ parsed.response?.usage ??
393
+ null;
394
+ if (!usage || typeof usage !== 'object')
395
+ continue;
396
+ const tokens = (usage.total ??
397
+ (usage.input ?? usage.inputTokens ?? 0) +
398
+ (usage.output ?? usage.outputTokens ?? 0) +
399
+ (usage.cacheRead ?? usage.cacheReadTokens ?? 0) +
400
+ (usage.cacheWrite ?? usage.cacheWriteTokens ?? 0));
401
+ if (!tokens || tokens <= 0)
402
+ continue;
403
+ let date;
404
+ let hour = null;
405
+ const ts = parsed.timestamp ?? parsed.t ?? parsed.time ?? null;
406
+ if (ts) {
407
+ const tsMs = typeof ts === 'string' && /^\d+$/.test(ts) ? Number(ts) : ts;
408
+ const d = new Date(tsMs);
409
+ if (!Number.isNaN(d.getTime())) {
410
+ date = toDateStr(d);
411
+ hour = d.getHours();
412
+ }
413
+ else {
414
+ if (!fileFallbackDate) {
415
+ const s = await stat(filePath);
416
+ fileFallbackDate = toDateStr(s.mtime);
417
+ }
418
+ date = fileFallbackDate;
419
+ }
420
+ }
421
+ else {
422
+ if (!fileFallbackDate) {
423
+ const s = await stat(filePath);
424
+ fileFallbackDate = toDateStr(s.mtime);
425
+ }
426
+ date = fileFallbackDate;
427
+ }
428
+ const model = parsed.model ?? parsed.response?.model ?? '';
429
+ addToDaily(dailyByModel, date, model, tokens, 1);
430
+ if (model) {
431
+ modelAccumulator.set(model, (modelAccumulator.get(model) ?? 0) + tokens);
432
+ }
433
+ if (hour !== null) {
434
+ addHourly(date, hour, 'openclaw', model, tokens, cutoff, 1);
435
+ }
436
+ }
437
+ catch {
438
+ // skip malformed lines
439
+ }
440
+ }
441
+ }
442
+ catch {
443
+ // skip unreadable files
444
+ }
445
+ }
446
+ return dailyMapToEntries(dailyByModel, 'openclaw');
447
+ }
448
+ async function scanCursorLocal() {
449
+ const dbPath = join(homedir(), '.cursor', 'ai-tracking', 'ai-code-tracking.db');
450
+ try {
451
+ await stat(dbPath);
452
+ }
453
+ catch {
454
+ return { entries: [], stats: null };
455
+ }
456
+ const { execSync } = await import('node:child_process');
457
+ try {
458
+ const raw = execSync(`sqlite3 "${dbPath}" "select commitHash, linesAdded, linesDeleted, composerLinesAdded, composerLinesDeleted, humanLinesAdded, humanLinesDeleted, v2AiPercentage from scored_commits;"`, { encoding: 'utf8', timeout: 10000 }).trim();
459
+ if (!raw)
460
+ return { entries: [], stats: null };
461
+ const rows = raw.split('\n');
462
+ let totalTokens = 0;
463
+ let totalAiLines = 0;
464
+ let totalHumanLines = 0;
465
+ let aiPctSum = 0;
466
+ for (const row of rows) {
467
+ const [, , , composerAdded, composerDeleted, humanAdded, , aiPct] = row.split('|');
468
+ const aiLines = (parseInt(composerAdded ?? '0', 10) || 0) +
469
+ (parseInt(composerDeleted ?? '0', 10) || 0);
470
+ totalAiLines += parseInt(composerAdded ?? '0', 10) || 0;
471
+ totalHumanLines += parseInt(humanAdded ?? '0', 10) || 0;
472
+ aiPctSum += parseFloat(aiPct ?? '0') || 0;
473
+ totalTokens += aiLines * 30;
474
+ }
475
+ let models = [];
476
+ try {
477
+ const modelRaw = execSync(`sqlite3 "${dbPath}" "select model, count(*) from ai_code_hashes where model is not null and model != '' group by model order by count(*) desc;"`, { encoding: 'utf8', timeout: 5000 }).trim();
478
+ if (modelRaw) {
479
+ models = modelRaw.split('\n').map((line) => {
480
+ const [model, count] = line.split('|');
481
+ return { model: model ?? '', uses: parseInt(count ?? '0', 10) || 0 };
482
+ });
483
+ }
484
+ }
485
+ catch {
486
+ // optional
487
+ }
488
+ const entries = totalTokens > 0
489
+ ? [{ date: toDateStr(new Date()), tool: 'cursor', tokens: totalTokens }]
490
+ : [];
491
+ return {
492
+ entries,
493
+ stats: {
494
+ totalCommits: rows.length,
495
+ aiLinesAdded: totalAiLines,
496
+ humanLinesAdded: totalHumanLines,
497
+ avgAiPercent: rows.length > 0 ? Math.round((aiPctSum / rows.length) * 10) / 10 : 0,
498
+ models,
499
+ },
500
+ };
501
+ }
502
+ catch {
503
+ return { entries: [], stats: null };
504
+ }
505
+ }
506
+ async function scanCursorApi() {
507
+ const config = await loadConfig();
508
+ if (!config.cursorApiKey) {
509
+ _cursorScanStatus.value = { source: 'none' };
510
+ return [];
511
+ }
512
+ const email = config.cursorEmail;
513
+ const now = Date.now();
514
+ const fiveYearsMs = 5 * 365 * 24 * 60 * 60 * 1000;
515
+ const startDate = now - fiveYearsMs;
516
+ const endDate = now;
517
+ const dailyByModel = new Map();
518
+ const cursorCutoff = hourlyCutoff();
519
+ let page = 1;
520
+ let hasMore = true;
521
+ let retries = 0;
522
+ const MAX_RETRIES = 3;
523
+ let eventsProcessed = 0;
524
+ let partialReason = null;
525
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
526
+ while (hasMore) {
527
+ const body = {
528
+ startDate,
529
+ endDate,
530
+ page,
531
+ pageSize: 100,
532
+ };
533
+ if (email)
534
+ body.email = email;
535
+ let res;
536
+ try {
537
+ res = await fetch('https://api.cursor.com/teams/filtered-usage-events', {
538
+ method: 'POST',
539
+ headers: {
540
+ 'Content-Type': 'application/json',
541
+ Authorization: `Bearer ${config.cursorApiKey}`,
542
+ },
543
+ body: JSON.stringify(body),
544
+ });
545
+ }
546
+ catch (err) {
547
+ partialReason = `network: ${err instanceof Error ? err.message : 'unknown'}`;
548
+ break;
549
+ }
550
+ if (!res.ok) {
551
+ if (res.status === 429 && retries < MAX_RETRIES) {
552
+ const waitMs = 2000 * 2 ** retries;
553
+ await sleep(waitMs);
554
+ retries++;
555
+ continue;
556
+ }
557
+ if (res.status === 401 || res.status === 403) {
558
+ if (eventsProcessed === 0) {
559
+ _cursorScanStatus.value = {
560
+ source: 'api-failed',
561
+ reason: `auth ${res.status} — check CURSOR_API_KEY`,
562
+ };
563
+ return [];
564
+ }
565
+ partialReason = `auth ${res.status} mid-scan`;
566
+ break;
567
+ }
568
+ partialReason = `HTTP ${res.status}`;
569
+ break;
570
+ }
571
+ retries = 0;
572
+ let data;
573
+ try {
574
+ data = await res.json();
575
+ }
576
+ catch {
577
+ partialReason = 'invalid json';
578
+ break;
579
+ }
580
+ const events = data.usageEvents ?? [];
581
+ for (const event of events) {
582
+ if (!event.isTokenBasedCall)
583
+ continue;
584
+ const tu = event.tokenUsage ??
585
+ event;
586
+ const tokens = (tu.inputTokens ?? 0) +
587
+ (tu.outputTokens ?? 0) +
588
+ (tu.cacheWriteTokens ?? 0) +
589
+ (tu.cacheReadTokens ?? 0);
590
+ if (tokens > 0 && event.timestamp) {
591
+ const rawTs = event.timestamp;
592
+ const tsMs = typeof rawTs === 'string' && /^\d+$/.test(rawTs)
593
+ ? Number(rawTs)
594
+ : rawTs;
595
+ const eventDate = new Date(tsMs);
596
+ const date = toDateStr(eventDate);
597
+ const hour = eventDate.getHours();
598
+ const model = event.model ?? '';
599
+ addToDaily(dailyByModel, date, model, tokens, 1);
600
+ if (model) {
601
+ modelAccumulator.set(model, (modelAccumulator.get(model) ?? 0) + tokens);
602
+ }
603
+ addHourly(date, hour, 'cursor', model, tokens, cursorCutoff, 1);
604
+ eventsProcessed++;
605
+ }
606
+ }
607
+ hasMore = data.pagination?.hasNextPage ?? false;
608
+ page++;
609
+ if (page > 500) {
610
+ partialReason = 'pagination cap (500 pages)';
611
+ break;
612
+ }
613
+ }
614
+ _cursorScanStatus.value = partialReason
615
+ ? { source: 'api-partial', events: eventsProcessed, reason: partialReason }
616
+ : { source: 'api', events: eventsProcessed };
617
+ return dailyMapToEntries(dailyByModel, 'cursor');
618
+ }
619
+ async function scanOpenCode() {
620
+ const openCodeDir = join(homedir(), '.local', 'share', 'opencode');
621
+ try {
622
+ await stat(openCodeDir);
623
+ }
624
+ catch {
625
+ return [];
626
+ }
627
+ const cutoff = hourlyCutoff();
628
+ const dailyByModel = new Map();
629
+ // Schema: data column is JSON with shape:
630
+ // { role, modelID, time: { created: ms }, tokens: { input, output, reasoning, cache: { read, write } } }
631
+ const dbPath = join(openCodeDir, 'opencode.db');
632
+ try {
633
+ await stat(dbPath);
634
+ const { execSync } = await import('node:child_process');
635
+ const raw = execSync(`sqlite3 -readonly "${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'"`, { encoding: 'utf8', timeout: 15000, stdio: 'pipe' }).trim();
636
+ if (raw) {
637
+ for (const row of raw.split('\n')) {
638
+ const [timeStr, model, inputStr, outputStr, cacheReadStr, cacheWriteStr, reasoningStr,] = row.split('|');
639
+ const tokens = (Number.parseInt(inputStr ?? '0', 10) || 0) +
640
+ (Number.parseInt(outputStr ?? '0', 10) || 0) +
641
+ (Number.parseInt(cacheReadStr ?? '0', 10) || 0) +
642
+ (Number.parseInt(cacheWriteStr ?? '0', 10) || 0) +
643
+ (Number.parseInt(reasoningStr ?? '0', 10) || 0);
644
+ if (tokens <= 0)
645
+ continue;
646
+ const tsMs = Number(timeStr ?? '0'); // already milliseconds
647
+ if (!Number.isFinite(tsMs) || tsMs <= 0)
648
+ continue;
649
+ const d = new Date(tsMs);
650
+ const date = toDateStr(d);
651
+ const hour = d.getHours();
652
+ const m = (model ?? '').trim();
653
+ addToDaily(dailyByModel, date, m, tokens, 1);
654
+ if (m)
655
+ modelAccumulator.set(m, (modelAccumulator.get(m) ?? 0) + tokens);
656
+ addHourly(date, hour, 'opencode', m, tokens, cutoff, 1);
657
+ }
658
+ if (dailyByModel.size > 0) {
659
+ return dailyMapToEntries(dailyByModel, 'opencode');
660
+ }
661
+ }
662
+ }
663
+ catch {
664
+ // DB unavailable; fall through to JSON files
665
+ }
666
+ // Fallback: scan ~/.local/share/opencode/storage/message/{sessionID}/msg_*.json
667
+ const messageDir = join(openCodeDir, 'storage', 'message');
668
+ try {
669
+ await stat(messageDir);
670
+ }
671
+ catch {
672
+ return [];
673
+ }
674
+ try {
675
+ const sessionDirs = await readdir(messageDir, { withFileTypes: true });
676
+ for (const sessionEntry of sessionDirs) {
677
+ if (!sessionEntry.isDirectory())
678
+ continue;
679
+ const msgFiles = await findFiles(join(messageDir, sessionEntry.name), '.json');
680
+ for (const filePath of msgFiles) {
681
+ try {
682
+ const parsed = JSON.parse(await readFile(filePath, 'utf8'));
683
+ if (parsed.role !== 'assistant')
684
+ continue;
685
+ const t = parsed.tokens;
686
+ if (!t || typeof t !== 'object')
687
+ continue;
688
+ const tokens = (t.input ?? 0) +
689
+ (t.output ?? 0) +
690
+ (t.cache?.read ?? 0) +
691
+ (t.cache?.write ?? 0) +
692
+ (t.reasoning ?? 0);
693
+ if (tokens <= 0)
694
+ continue;
695
+ const tsMs = parsed.time?.created ?? 0;
696
+ let date;
697
+ let hour = null;
698
+ if (tsMs > 0) {
699
+ const d = new Date(tsMs);
700
+ date = toDateStr(d);
701
+ hour = d.getHours();
702
+ }
703
+ else {
704
+ date = toDateStr((await stat(filePath)).mtime);
705
+ }
706
+ const model = String(parsed.modelID ?? '');
707
+ addToDaily(dailyByModel, date, model, tokens, 1);
708
+ if (model)
709
+ modelAccumulator.set(model, (modelAccumulator.get(model) ?? 0) + tokens);
710
+ if (hour !== null)
711
+ addHourly(date, hour, 'opencode', model, tokens, cutoff, 1);
712
+ }
713
+ catch {
714
+ // skip malformed files
715
+ }
716
+ }
717
+ }
718
+ }
719
+ catch {
720
+ // skip
721
+ }
722
+ return dailyMapToEntries(dailyByModel, 'opencode');
723
+ }
724
+ export async function runSync(session) {
725
+ modelAccumulator.clear();
726
+ hourlyAccumulator.clear();
727
+ _cursorScanStatus.value = { source: 'none' };
728
+ const [claudeEntries, codexEntries, cursorApiEntries, cursorLocal, openclawEntries, hermesEntries, opencodeEntries,] = await Promise.all([
729
+ scanClaudeCode(),
730
+ scanCodex(),
731
+ scanCursorApi(),
732
+ scanCursorLocal(),
733
+ scanOpenclaw(),
734
+ scanHermes(),
735
+ scanOpenCode(),
736
+ ]);
737
+ const cursorEntries = cursorApiEntries.length > 0 ? cursorApiEntries : cursorLocal.entries;
738
+ const claudeTotal = claudeEntries.reduce((s, e) => s + e.tokens, 0);
739
+ const codexTotal = codexEntries.reduce((s, e) => s + e.tokens, 0);
740
+ const cursorTotal = cursorEntries.reduce((s, e) => s + e.tokens, 0);
741
+ const openclawTotal = openclawEntries.reduce((s, e) => s + e.tokens, 0);
742
+ const hermesTotal = hermesEntries.reduce((s, e) => s + e.tokens, 0);
743
+ const opencodeTotal = opencodeEntries.reduce((s, e) => s + e.tokens, 0);
744
+ const allEntries = [
745
+ ...claudeEntries,
746
+ ...codexEntries,
747
+ ...cursorEntries,
748
+ ...openclawEntries,
749
+ ...hermesEntries,
750
+ ...opencodeEntries,
751
+ ];
752
+ const messagesTotal = allEntries.reduce((s, e) => s + (e.messages ?? 0), 0);
753
+ const modelTotals = {};
754
+ for (const [name, tokens] of modelAccumulator.entries()) {
755
+ if (tokens > 0)
756
+ modelTotals[name] = tokens;
757
+ }
758
+ const hourlyTotals = flushHourly();
759
+ const res = await fetchApi(session, '/api/claim/sync', {
760
+ method: 'POST',
761
+ headers: {
762
+ 'Content-Type': 'application/json',
763
+ Authorization: `Bearer ${session.token}`,
764
+ },
765
+ body: JSON.stringify({
766
+ dailyTotals: allEntries,
767
+ toolTotals: {
768
+ claude_code: claudeTotal,
769
+ codex: codexTotal,
770
+ cursor: cursorTotal,
771
+ openclaw: openclawTotal,
772
+ hermes: hermesTotal,
773
+ opencode: opencodeTotal,
774
+ },
775
+ modelTotals,
776
+ hourlyTotals,
777
+ }),
778
+ });
779
+ if (res.status === 401) {
780
+ throw new Error(LOGIN_EXPIRED_MESSAGE);
781
+ }
782
+ if (!res.ok) {
783
+ const data = await res.json().catch(() => null);
784
+ throw new Error(data?.error ??
785
+ `sync failed (${res.status})`);
786
+ }
787
+ const result = await res.json();
788
+ return {
789
+ claudeTotal,
790
+ codexTotal,
791
+ cursorTotal,
792
+ openclawTotal,
793
+ hermesTotal,
794
+ opencodeTotal,
795
+ messagesTotal,
796
+ allEntries,
797
+ cursorStats: cursorLocal.stats,
798
+ result,
799
+ models: Array.from(modelAccumulator.entries())
800
+ .map(([name, tokens]) => ({ name, tokens }))
801
+ .sort((a, b) => b.tokens - a.tokens),
802
+ cursorScanStatus: _cursorScanStatus.value,
803
+ };
804
+ }
805
+ //# sourceMappingURL=sync.js.map