token-studio 4.8.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 (139) hide show
  1. package/.nvmrc +1 -0
  2. package/CHANGELOG.md +89 -0
  3. package/Dockerfile +17 -0
  4. package/LICENSE +22 -0
  5. package/NOTICE.md +21 -0
  6. package/PRIVACY.md +68 -0
  7. package/README.en.md +220 -0
  8. package/README.md +220 -0
  9. package/config/collectors.json +54 -0
  10. package/data/.gitkeep +1 -0
  11. package/docker-compose.yml +17 -0
  12. package/docs/assets/.gitkeep +1 -0
  13. package/docs/assets/token-studio-v44-dashboard.png +0 -0
  14. package/docs/assets/token-studio-v44-live.png +0 -0
  15. package/docs/assets/token-studio-v44-review-mobile.png +0 -0
  16. package/docs/assets/token-studio-v44-review.png +0 -0
  17. package/docs/assets/token-studio-v45-dashboard.png +0 -0
  18. package/docs/assets/token-studio-v45-live.png +0 -0
  19. package/docs/assets/token-studio-v45-review-mobile.png +0 -0
  20. package/docs/assets/token-studio-v45-review.png +0 -0
  21. package/docs/blog-case-study.md +34 -0
  22. package/docs/collector-support-matrix.md +65 -0
  23. package/docs/competitive-notes.md +87 -0
  24. package/docs/demo-data/README.md +12 -0
  25. package/docs/demo-data/token-studio-v2-demo.json +146 -0
  26. package/docs/demo-flow.md +39 -0
  27. package/docs/first-run.md +95 -0
  28. package/docs/local-collectors.md +49 -0
  29. package/docs/public-launch-checklist.md +45 -0
  30. package/docs/resume-bullets.md +7 -0
  31. package/docs/statusline.md +52 -0
  32. package/index.html +16 -0
  33. package/package.json +36 -0
  34. package/render.yaml +17 -0
  35. package/src/auto-attribution.mjs +396 -0
  36. package/src/ccusage-bridge.mjs +74 -0
  37. package/src/ccusage-import.mjs +415 -0
  38. package/src/cli.mjs +643 -0
  39. package/src/client/dashboard/App.jsx +1734 -0
  40. package/src/client/dashboard/annotation-presets.js +138 -0
  41. package/src/client/dashboard/attribution.js +328 -0
  42. package/src/client/dashboard/components-charts.jsx +622 -0
  43. package/src/client/dashboard/components-tables.jsx +1531 -0
  44. package/src/client/dashboard/components-top.jsx +307 -0
  45. package/src/client/dashboard/import-budget.js +41 -0
  46. package/src/client/dashboard/model-usage.js +108 -0
  47. package/src/client/dashboard/onboarding.js +80 -0
  48. package/src/client/dashboard/styles.css +2606 -0
  49. package/src/client/live/LiveApp.jsx +226 -0
  50. package/src/client/live/styles.css +446 -0
  51. package/src/client/main.jsx +20 -0
  52. package/src/client/review/ReviewApp.jsx +507 -0
  53. package/src/client/review/closure-progress.js +165 -0
  54. package/src/client/review/markdown-report.js +401 -0
  55. package/src/client/review/model-strategy.js +273 -0
  56. package/src/client/review/roi-advisor.js +255 -0
  57. package/src/client/review/roi-evidence.js +78 -0
  58. package/src/client/review/savings-simulator.js +252 -0
  59. package/src/client/review/sections-1.jsx +277 -0
  60. package/src/client/review/sections-2.jsx +927 -0
  61. package/src/client/review/styles.css +2321 -0
  62. package/src/client/review/utils.js +345 -0
  63. package/src/client/shared/utils.js +236 -0
  64. package/src/closure-check.mjs +537 -0
  65. package/src/closure-import.mjs +646 -0
  66. package/src/collect.mjs +247 -0
  67. package/src/collector-config.mjs +82 -0
  68. package/src/collector-registry.mjs +333 -0
  69. package/src/collectors/claude-code.mjs +355 -0
  70. package/src/collectors/codex.mjs +418 -0
  71. package/src/collectors/copilot.mjs +19 -0
  72. package/src/collectors/cursor.mjs +23 -0
  73. package/src/collectors/gemini.mjs +530 -0
  74. package/src/collectors/goose.mjs +15 -0
  75. package/src/collectors/hermes.mjs +206 -0
  76. package/src/collectors/kimi.mjs +15 -0
  77. package/src/collectors/openclaw.mjs +400 -0
  78. package/src/collectors/opencode.mjs +349 -0
  79. package/src/collectors/qwen.mjs +15 -0
  80. package/src/collectors/structured-usage.mjs +437 -0
  81. package/src/collectors/utils.mjs +93 -0
  82. package/src/db.mjs +1397 -0
  83. package/src/demo-seed.mjs +39 -0
  84. package/src/dev.mjs +43 -0
  85. package/src/live.mjs +428 -0
  86. package/src/model-policy.mjs +147 -0
  87. package/src/pricing.mjs +434 -0
  88. package/src/privacy-check.mjs +126 -0
  89. package/src/server.mjs +1240 -0
  90. package/src/source-health.mjs +195 -0
  91. package/src/statusline.mjs +156 -0
  92. package/src/terminal-report.mjs +245 -0
  93. package/src/update-pricing.mjs +8 -0
  94. package/test/annotation-presets.test.mjs +137 -0
  95. package/test/api-annotations.test.mjs +202 -0
  96. package/test/api-auto-attribution.test.mjs +169 -0
  97. package/test/api-source-health.test.mjs +109 -0
  98. package/test/api-v2.test.mjs +278 -0
  99. package/test/api-v43.test.mjs +151 -0
  100. package/test/api-v44.test.mjs +128 -0
  101. package/test/attribution-summary.test.mjs +164 -0
  102. package/test/auto-attribution.test.mjs +116 -0
  103. package/test/ccusage-bridge.test.mjs +36 -0
  104. package/test/ccusage-import.test.mjs +93 -0
  105. package/test/cli-v43.test.mjs +64 -0
  106. package/test/cli-v45.test.mjs +34 -0
  107. package/test/cli-v46.test.mjs +129 -0
  108. package/test/cli-v47.test.mjs +98 -0
  109. package/test/closure-check.test.mjs +202 -0
  110. package/test/closure-import.test.mjs +263 -0
  111. package/test/collector-config.test.mjs +25 -0
  112. package/test/collector-registry.test.mjs +56 -0
  113. package/test/csv.test.mjs +19 -0
  114. package/test/db-annotations.test.mjs +186 -0
  115. package/test/db-v2.test.mjs +200 -0
  116. package/test/db-v4.test.mjs +178 -0
  117. package/test/experimental-collectors.test.mjs +103 -0
  118. package/test/fixtures/collectors/copilot/usage.jsonl +2 -0
  119. package/test/fixtures/collectors/cursor/usage.jsonl +2 -0
  120. package/test/fixtures/collectors/goose/usage.jsonl +2 -0
  121. package/test/fixtures/collectors/kimi/usage.jsonl +2 -0
  122. package/test/fixtures/collectors/qwen/usage.jsonl +2 -0
  123. package/test/import-budget.test.mjs +40 -0
  124. package/test/live.test.mjs +256 -0
  125. package/test/markdown-report.test.mjs +193 -0
  126. package/test/model-policy.test.mjs +34 -0
  127. package/test/model-strategy.test.mjs +116 -0
  128. package/test/model-usage.test.mjs +99 -0
  129. package/test/official-pricing.test.mjs +70 -0
  130. package/test/onboarding.test.mjs +55 -0
  131. package/test/privacy-check.test.mjs +33 -0
  132. package/test/review-closure-progress.test.mjs +99 -0
  133. package/test/roi-advisor.test.mjs +188 -0
  134. package/test/roi-evidence.test.mjs +48 -0
  135. package/test/roi-summary.test.mjs +101 -0
  136. package/test/savings-simulator.test.mjs +141 -0
  137. package/test/source-health.test.mjs +62 -0
  138. package/test/statusline.test.mjs +148 -0
  139. package/vite.config.js +23 -0
@@ -0,0 +1,418 @@
1
+ /**
2
+ * Codex CLI data collector (pure JS).
3
+ *
4
+ * Scans two roots:
5
+ * ~/.codex/sessions/ — active sessions (recursive JSONL)
6
+ * ~/.codex/archived_sessions/ — archived sessions (recursive JSONL)
7
+ * (CODEX_HOME env var overrides ~/.codex)
8
+ *
9
+ * The Codex JSONL format has three relevant event types:
10
+ * session_meta – workspace (cwd), session ID, provider, agent nickname
11
+ * turn_context – current model for the upcoming turn
12
+ * event_msg – when payload.type === "token_count", carries token usage
13
+ *
14
+ * Token counting strategy:
15
+ * • Primary source: last_token_usage (per-request increment)
16
+ * • Fallback: delta of total_token_usage between consecutive events
17
+ * • Dedup: skip events where total_token_usage unchanged
18
+ */
19
+
20
+ import { readdir, readFile } from 'node:fs/promises';
21
+ import { basename, join } from 'node:path';
22
+ import { configuredPaths, configuredStrings, envPathList } from '../collector-config.mjs';
23
+ import { calculateCost } from '../pricing.mjs';
24
+ import { localDateFromTimestamp, normalizeModelForGrouping } from './utils.mjs';
25
+
26
+ /** Recursively collect all .jsonl file paths under a directory. */
27
+ async function collectJsonlFiles(dir) {
28
+ const results = [];
29
+ let entries;
30
+ try {
31
+ entries = await readdir(dir, { withFileTypes: true });
32
+ } catch {
33
+ return results;
34
+ }
35
+ for (const entry of entries) {
36
+ const full = join(dir, entry.name);
37
+ if (entry.isDirectory()) {
38
+ results.push(...await collectJsonlFiles(full));
39
+ } else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
40
+ results.push(full);
41
+ }
42
+ }
43
+ return results;
44
+ }
45
+
46
+ export const CLIENT_KEY = 'codex';
47
+ export const SOURCE_LABEL = 'Codex CLI';
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Path resolution
51
+ // ---------------------------------------------------------------------------
52
+
53
+ function getCodexHomes() {
54
+ return envPathList(process.env.CODEX_HOME, configuredPaths('codex', 'homes'));
55
+ }
56
+
57
+ function getSessionRoots() {
58
+ const subdirs = configuredStrings('codex', 'sessionSubdirs', ['sessions', 'archived_sessions']);
59
+ return getCodexHomes().flatMap((home) => subdirs.map((subdir) => join(home, subdir)));
60
+ }
61
+
62
+ function getHeadlessRoots() {
63
+ const roots = envPathList(
64
+ process.env.TOKEN_STUDIO_HEADLESS_DIR || process.env.AI_TOKEN_DASHBOARD_HEADLESS_DIR,
65
+ configuredPaths('codex', 'headlessRoots')
66
+ );
67
+ return roots.map((root) => join(root, 'codex'));
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Helpers
72
+ // ---------------------------------------------------------------------------
73
+
74
+ async function safeReaddir(dir) {
75
+ try {
76
+ return await readdir(dir, { withFileTypes: true });
77
+ } catch {
78
+ return [];
79
+ }
80
+ }
81
+
82
+ function pos(v) {
83
+ const n = Number(v ?? 0);
84
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
85
+ }
86
+
87
+ function zero() {
88
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 };
89
+ }
90
+
91
+ function addInto(agg, t) {
92
+ agg.input += t.input;
93
+ agg.output += t.output;
94
+ agg.cacheRead += t.cacheRead;
95
+ agg.cacheWrite += t.cacheWrite;
96
+ agg.reasoning += t.reasoning;
97
+ }
98
+
99
+ /** Extract a { input, output, cached, reasoning } summary from a token-usage object. */
100
+ function usageSummary(u) {
101
+ return {
102
+ input: pos(u.input_tokens),
103
+ output: pos(u.output_tokens),
104
+ // Codex uses cached_input_tokens OR cache_read_input_tokens interchangeably
105
+ cached: Math.max(pos(u.cached_input_tokens), pos(u.cache_read_input_tokens)),
106
+ reasoning: pos(u.reasoning_output_tokens)
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Convert a Codex cumulative summary to our token breakdown.
112
+ * cached is clamped to <= input to avoid inflated totals.
113
+ */
114
+ function summaryToTokens(s) {
115
+ const clamped = Math.min(s.cached, s.input);
116
+ return {
117
+ input: Math.max(0, s.input - clamped),
118
+ output: s.output,
119
+ cacheRead: clamped,
120
+ cacheWrite: 0,
121
+ reasoning: s.reasoning
122
+ };
123
+ }
124
+
125
+ function summaryIsZero(s) {
126
+ return s.input === 0 && s.output === 0 && s.cached === 0 && s.reasoning === 0;
127
+ }
128
+
129
+ function summaryEqual(a, b) {
130
+ return a.input === b.input && a.output === b.output &&
131
+ a.cached === b.cached && a.reasoning === b.reasoning;
132
+ }
133
+
134
+ function summaryDelta(current, previous) {
135
+ if (
136
+ current.input < previous.input ||
137
+ current.output < previous.output ||
138
+ current.cached < previous.cached ||
139
+ current.reasoning < previous.reasoning
140
+ ) {
141
+ return null;
142
+ }
143
+
144
+ return {
145
+ input: current.input - previous.input,
146
+ output: current.output - previous.output,
147
+ cached: current.cached - previous.cached,
148
+ reasoning: current.reasoning - previous.reasoning
149
+ };
150
+ }
151
+
152
+ function summaryTotal(s) {
153
+ return s.input + s.output + s.cached + s.reasoning;
154
+ }
155
+
156
+ function looksLikeStaleRegression(current, previous, last) {
157
+ const previousTotal = summaryTotal(previous);
158
+ const currentTotal = summaryTotal(current);
159
+ const lastTotal = summaryTotal(last);
160
+
161
+ if (previousTotal <= 0 || currentTotal <= 0 || lastTotal <= 0) return false;
162
+
163
+ return currentTotal * 100 >= previousTotal * 98 ||
164
+ currentTotal + lastTotal * 2 >= previousTotal;
165
+ }
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // JSONL session parser
169
+ // ---------------------------------------------------------------------------
170
+
171
+ /**
172
+ * Parse a single Codex JSONL session file.
173
+ * Returns an array of { timestamp, date, model, workspace, tokens }.
174
+ */
175
+ async function parseSessionFile(filePath, sessionId) {
176
+ let text;
177
+ try {
178
+ text = await readFile(filePath, 'utf8');
179
+ } catch {
180
+ return [];
181
+ }
182
+
183
+ // Per-file state
184
+ let currentModel = null;
185
+ let previousTotal = null; // last seen total_token_usage summary
186
+ let workspace = null;
187
+
188
+ const events = [];
189
+
190
+ for (const raw of text.split('\n')) {
191
+ const line = raw.trim();
192
+ if (!line) continue;
193
+
194
+ let entry;
195
+ try { entry = JSON.parse(line); } catch { continue; }
196
+
197
+ const type = entry.type;
198
+
199
+ // ── session_meta ──────────────────────────────────────────────────
200
+ if (type === 'session_meta') {
201
+ const payload = entry.payload || {};
202
+ if (payload.cwd) {
203
+ workspace = payload.cwd;
204
+ }
205
+ continue;
206
+ }
207
+
208
+ // ── turn_context ──────────────────────────────────────────────────
209
+ if (type === 'turn_context') {
210
+ const payload = entry.payload || {};
211
+ currentModel = extractModel(payload) || currentModel;
212
+ continue;
213
+ }
214
+
215
+ // ── event_msg / token_count ────────────────────────────────────────
216
+ if (type === 'event_msg') {
217
+ const payload = entry.payload || {};
218
+ if (payload.type !== 'token_count') continue;
219
+
220
+ const info = payload.info || {};
221
+
222
+ // Model resolution: payload.model → info.model → state.currentModel
223
+ const model = normalizeModelForGrouping(
224
+ extractModel(payload) ||
225
+ extractModel(info) ||
226
+ currentModel ||
227
+ 'unknown'
228
+ );
229
+
230
+ currentModel = model;
231
+
232
+ const lastUsage = info.last_token_usage ? usageSummary(info.last_token_usage) : null;
233
+ const totalUsage = info.total_token_usage ? usageSummary(info.total_token_usage) : null;
234
+
235
+ // Dedup: skip if total hasn't changed since last event
236
+ if (totalUsage && previousTotal && summaryEqual(totalUsage, previousTotal)) continue;
237
+
238
+ // Choose token increment
239
+ let increment;
240
+ if (lastUsage && totalUsage && previousTotal) {
241
+ if (!summaryDelta(totalUsage, previousTotal) &&
242
+ looksLikeStaleRegression(totalUsage, previousTotal, lastUsage)) {
243
+ continue;
244
+ }
245
+ // Standard path: use last_token_usage as increment
246
+ increment = lastUsage;
247
+ } else if (lastUsage && totalUsage && !previousTotal) {
248
+ // First event in session: use last to avoid overcounting resumed session context
249
+ increment = lastUsage;
250
+ } else if (!lastUsage && totalUsage && previousTotal) {
251
+ // Fallback: delta of cumulative totals
252
+ increment = summaryDelta(totalUsage, previousTotal);
253
+ if (!increment) {
254
+ // Total went backwards (session context reset or stale event).
255
+ // Accept the new total as the new baseline to avoid future overcounting.
256
+ previousTotal = totalUsage;
257
+ continue;
258
+ }
259
+ } else if (!lastUsage && totalUsage) {
260
+ // Very first event, no last — use full total (legacy/degraded)
261
+ increment = totalUsage;
262
+ } else if (lastUsage) {
263
+ increment = lastUsage;
264
+ } else {
265
+ continue;
266
+ }
267
+
268
+ if (totalUsage) previousTotal = totalUsage;
269
+
270
+ if (summaryIsZero(increment)) continue;
271
+
272
+ const tokens = summaryToTokens(increment);
273
+
274
+ // Date from event timestamp
275
+ const timestamp = typeof entry.timestamp === 'string' ? entry.timestamp : '';
276
+ let date = 'unknown';
277
+ if (timestamp) {
278
+ date = localDateFromTimestamp(timestamp);
279
+ }
280
+
281
+ events.push({ timestamp, date, model, workspace, tokens });
282
+ }
283
+ }
284
+
285
+ return events;
286
+ }
287
+
288
+ function extractModel(obj) {
289
+ if (!obj) return null;
290
+ const v =
291
+ obj.model ||
292
+ obj.model_name ||
293
+ obj.model_info?.slug ||
294
+ null;
295
+ return typeof v === 'string' && v.trim() ? v.trim() : null;
296
+ }
297
+
298
+ // ---------------------------------------------------------------------------
299
+ // Main collector
300
+ // ---------------------------------------------------------------------------
301
+
302
+ export async function collect(pricingData = null) {
303
+ // Scan active, archived, and optional headless Codex outputs.
304
+ const roots = [...getSessionRoots(), ...getHeadlessRoots()];
305
+ const nestedPaths = await Promise.all(roots.map((root) => collectJsonlFiles(root)));
306
+ const filePaths = [...new Set(nestedPaths.flat())];
307
+
308
+ const dailyMap = new Map(); // "date::model" → aggregated
309
+ const wmMap = new Map(); // "workspace::model" → aggregated
310
+ const seenEventKeys = new Set();
311
+
312
+ for (const filePath of filePaths) {
313
+ const sessionId = basename(filePath).replace(/\.jsonl$/, '');
314
+ const events = await parseSessionFile(filePath, sessionId);
315
+
316
+ for (const { timestamp, date, model, workspace, tokens } of events) {
317
+ const eventKey = codexEventDedupKey({ timestamp, model, tokens });
318
+ if (eventKey && seenEventKeys.has(eventKey)) continue;
319
+ if (eventKey) seenEventKeys.add(eventKey);
320
+
321
+ const workspaceKey = workspace || sessionId;
322
+
323
+ // Daily
324
+ const dk = `${date}::${model}`;
325
+ if (!dailyMap.has(dk)) dailyMap.set(dk, { date, model, ...zero(), cost: 0 });
326
+ addInto(dailyMap.get(dk), tokens);
327
+
328
+ // Workspace+model
329
+ const wmk = `${workspaceKey}::${model}`;
330
+ if (!wmMap.has(wmk)) {
331
+ wmMap.set(wmk, {
332
+ workspace: workspaceKey,
333
+ workspaceLabel: decodeWorkspace(workspaceKey),
334
+ model,
335
+ ...zero(),
336
+ cost: 0
337
+ });
338
+ }
339
+ addInto(wmMap.get(wmk), tokens);
340
+ }
341
+ }
342
+
343
+ return buildOutput(dailyMap, wmMap, pricingData);
344
+ }
345
+
346
+ function codexEventDedupKey({ timestamp, model, tokens }) {
347
+ if (!timestamp) return null;
348
+ return [
349
+ timestamp,
350
+ model,
351
+ tokens.input,
352
+ tokens.output,
353
+ tokens.cacheRead,
354
+ tokens.cacheWrite,
355
+ tokens.reasoning
356
+ ].join('::');
357
+ }
358
+
359
+ /**
360
+ * Attempt to produce a human-readable label from a raw workspace path.
361
+ * Codex cwd values are already absolute paths, so just return as-is.
362
+ */
363
+ function decodeWorkspace(raw) {
364
+ return raw;
365
+ }
366
+
367
+ // ---------------------------------------------------------------------------
368
+ // Convert to common collector JSON
369
+ // ---------------------------------------------------------------------------
370
+
371
+ function buildOutput(dailyMap, wmMap, pricingData) {
372
+ const byDate = new Map();
373
+ for (const row of dailyMap.values()) {
374
+ if (!byDate.has(row.date)) byDate.set(row.date, []);
375
+ byDate.get(row.date).push(row);
376
+ }
377
+
378
+ const contributions = [...byDate.entries()]
379
+ .sort(([a], [b]) => a.localeCompare(b))
380
+ .map(([date, rows]) => ({
381
+ date,
382
+ clients: rows.map(row => {
383
+ const tokens = {
384
+ input: row.input,
385
+ output: row.output,
386
+ cacheRead: row.cacheRead,
387
+ cacheWrite: row.cacheWrite,
388
+ reasoning: row.reasoning,
389
+ };
390
+ return {
391
+ client: CLIENT_KEY,
392
+ modelId: row.model,
393
+ tokens,
394
+ cost: calculateCost(row.model, tokens, pricingData),
395
+ };
396
+ })
397
+ }));
398
+
399
+ const entries = [...wmMap.values()].map(wm => {
400
+ const tokens = {
401
+ input: wm.input,
402
+ output: wm.output,
403
+ cacheRead: wm.cacheRead,
404
+ cacheWrite: wm.cacheWrite,
405
+ reasoning: wm.reasoning,
406
+ };
407
+ return {
408
+ client: CLIENT_KEY,
409
+ workspaceKey: wm.workspace,
410
+ workspaceLabel: wm.workspaceLabel,
411
+ model: wm.model,
412
+ ...tokens,
413
+ cost: calculateCost(wm.model, tokens, pricingData),
414
+ };
415
+ });
416
+
417
+ return { graphJson: { contributions }, modelsJson: { entries } };
418
+ }
@@ -0,0 +1,19 @@
1
+ import { join } from 'node:path';
2
+ import { configuredPaths, expandPath } from '../collector-config.mjs';
3
+ import { collectStructuredUsage } from './structured-usage.mjs';
4
+
5
+ export const CLIENT_KEY = 'copilot';
6
+ export const SOURCE_LABEL = 'GitHub Copilot CLI';
7
+
8
+ export function roots() {
9
+ return configuredPaths('copilot', 'roots', [
10
+ '~/.config/github-copilot',
11
+ '~/.copilot',
12
+ '~/Library/Application Support/github-copilot',
13
+ process.env.APPDATA ? join(process.env.APPDATA, 'GitHub Copilot') : null
14
+ ]).map(expandPath).filter(Boolean);
15
+ }
16
+
17
+ export async function collect(pricingData = null) {
18
+ return collectStructuredUsage({ clientKey: CLIENT_KEY, roots: roots(), pricingData });
19
+ }
@@ -0,0 +1,23 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { configuredPaths, expandPath } from '../collector-config.mjs';
4
+ import { collectStructuredUsage } from './structured-usage.mjs';
5
+
6
+ export const CLIENT_KEY = 'cursor';
7
+ export const SOURCE_LABEL = 'Cursor';
8
+
9
+ export function roots() {
10
+ const appData = process.env.APPDATA;
11
+ const localAppData = process.env.LOCALAPPDATA;
12
+ return configuredPaths('cursor', 'roots', [
13
+ appData ? join(appData, 'Cursor') : null,
14
+ localAppData ? join(localAppData, 'Cursor') : null,
15
+ join(homedir(), '.cursor'),
16
+ '~/.config/Cursor',
17
+ '~/Library/Application Support/Cursor'
18
+ ]).map(expandPath).filter(Boolean);
19
+ }
20
+
21
+ export async function collect(pricingData = null) {
22
+ return collectStructuredUsage({ clientKey: CLIENT_KEY, roots: roots(), pricingData });
23
+ }