cursor-usage-analyzer 0.2.1 → 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 (122) hide show
  1. package/.claude/settings.local.json +12 -6
  2. package/README.md +14 -0
  3. package/analyze.js +277 -49
  4. package/cursor-logs-export/chats/2026-02-05_2026-02-10_08-35-54_uu_app_aicoding_conv55.txt +49 -0
  5. package/cursor-logs-export/chats/2026-02-05_2026-02-10_08-36-35_uu_app_aicoding_conv54.txt +241 -0
  6. package/cursor-logs-export/chats/2026-02-05_2026-02-10_08-47-45_uu_app_aicoding_conv56.txt +122 -0
  7. package/cursor-logs-export/chats/2026-02-05_2026-02-10_08-56-31_uu_app_aicoding_conv40.txt +80 -0
  8. package/cursor-logs-export/chats/2026-02-05_2026-02-10_08-58-09__unmatched__conv108.txt +26 -0
  9. package/cursor-logs-export/chats/2026-02-05_2026-02-10_08-59-08_uu_app_aicoding_conv57.txt +306 -0
  10. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-00-49_uu_app_aicoding_conv41.txt +149 -0
  11. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-04-15_uu_app_aicoding_conv58.txt +143 -0
  12. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-06-29_uu_app_aicoding_conv59.txt +119 -0
  13. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-17-49_uu_app_aicoding_conv60.txt +227 -0
  14. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-18-36_uu_app_aicoding_conv70.txt +193 -0
  15. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-26-21_uu_app_aicoding_conv42.txt +111 -0
  16. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-31-34_uu_app_aicoding_conv71.txt +232 -0
  17. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-40-01_uu_app_aicoding_conv72.txt +125 -0
  18. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-49-58_uu_app_aicoding_conv73.txt +64 -0
  19. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-57-27_uu_entitymanage_conv43.txt +157 -0
  20. package/cursor-logs-export/chats/2026-02-05_2026-02-10_10-02-36_uu_app_aicoding_conv44.txt +294 -0
  21. package/cursor-logs-export/chats/2026-02-05_2026-02-10_10-48-21_uu_app_aicoding_conv79.txt +181 -0
  22. package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-13-29_uu_app_aicoding_conv45.txt +160 -0
  23. package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-19-00_uu_app_aicoding_conv46.txt +82 -0
  24. package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-21-15_uu_app_aicoding_conv74.txt +103 -0
  25. package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-25-21_uu_app_aicoding_conv75.txt +119 -0
  26. package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-26-01_uu_app_aicoding_conv47.txt +266 -0
  27. package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-31-42_uu_entitymanage_conv48.txt +130 -0
  28. package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-33-00_uu_app_aicoding_conv1.txt +260 -0
  29. package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-51-10_uu_app_aicoding_conv80.txt +68 -0
  30. package/cursor-logs-export/chats/2026-02-05_2026-02-10_12-24-42_cursor_usage_an_conv106.txt +769 -0
  31. package/cursor-logs-export/chats/2026-02-05_2026-02-10_12-37-27_uu_app_aicoding_conv2.txt +897 -0
  32. package/cursor-logs-export/chats/2026-02-05_2026-02-10_12-48-53__unmatched__conv109.txt +26 -0
  33. package/cursor-logs-export/chats/2026-02-05_2026-02-10_12-51-19_uu_app_aicoding_conv3.txt +72 -0
  34. package/cursor-logs-export/chats/2026-02-05_2026-02-10_13-01-28_uu_app_aicoding_conv4.txt +112 -0
  35. package/cursor-logs-export/chats/2026-02-05_2026-02-10_13-21-29_uu_app_aicoding_conv5.txt +286 -0
  36. package/cursor-logs-export/chats/2026-02-05_2026-02-10_14-14-37_uu_app_aicoding_conv76.txt +765 -0
  37. package/cursor-logs-export/chats/2026-02-05_2026-02-10_14-25-53_uu_app_aicoding_conv7.txt +134 -0
  38. package/cursor-logs-export/chats/2026-02-05_2026-02-10_14-31-19_uu_app_aicoding_conv8.txt +118 -0
  39. package/cursor-logs-export/chats/2026-02-05_2026-02-10_15-15-16_uu_app_aicoding_conv9.txt +4644 -0
  40. package/cursor-logs-export/chats/2026-02-05_2026-02-10_15-20-50_uu_app_aicoding_conv6.txt +945 -0
  41. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-00-41_cursor_usage_an_conv107.txt +85 -0
  42. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-25-01_uu_app_aicoding_conv11.txt +274 -0
  43. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-29-52_uu_app_aicoding_conv10.txt +1603 -0
  44. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-38-00_uu_app_aicoding_conv12.txt +96 -0
  45. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-43-55_uu_app_aicoding_conv13.txt +74 -0
  46. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-47-13_uu_app_aicoding_conv14.txt +172 -0
  47. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-48-38_uu_cloud_univer_conv82.txt +253 -0
  48. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-51-54_uu_app_aicoding_conv16.txt +189 -0
  49. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-51-54_uu_app_aicoding_conv17.txt +57 -0
  50. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-59-13_uu_app_aicoding_conv15.txt +36 -0
  51. package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-03-28_uu_app_aicoding_conv18.txt +212 -0
  52. package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-05-14_uu_app_aicoding_conv19.txt +87 -0
  53. package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-13-17_uu_app_aicoding_conv20.txt +77 -0
  54. package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-25-15_uu_app_aicoding_conv21.txt +131 -0
  55. package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-31-30_uu_app_aicoding_conv23.txt +108 -0
  56. package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-38-46_uu_app_aicoding_conv81.txt +428 -0
  57. package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-43-08_uu_app_aicoding_conv24.txt +15297 -0
  58. package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-51-39_uu_app_aicoding_conv22.txt +60 -0
  59. package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-59-43_uu_app_aicoding_conv25.txt +189 -0
  60. package/cursor-logs-export/chats/2026-02-05_2026-02-10_18-03-50_uu_app_aicoding_conv26.txt +120 -0
  61. package/cursor-logs-export/chats/2026-02-05_2026-02-10_18-30-45_uu_app_aicoding_conv83.txt +523 -0
  62. package/cursor-logs-export/chats/2026-02-05_2026-02-10_18-32-40_uu_app_aicoding_conv27.txt +3941 -0
  63. package/cursor-logs-export/chats/2026-02-05_2026-02-10_18-39-32_uu_app_aicoding_conv84.txt +133 -0
  64. package/cursor-logs-export/chats/2026-02-05_2026-02-10_18-41-01_uu_app_aicoding_conv28.txt +136 -0
  65. package/cursor-logs-export/chats/2026-02-05_2026-02-10_18-56-27_uu_app_aicoding_conv85.txt +211 -0
  66. package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-10-56_uu_app_aicoding_conv86.txt +319 -0
  67. package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-22-42_uu_app_aicoding_conv87.txt +193 -0
  68. package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-27-57_uu_app_aicoding_conv88.txt +272 -0
  69. package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-32-27_uu_app_aicoding_conv89.txt +50 -0
  70. package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-42-59_uu_app_aicoding_conv90.txt +125 -0
  71. package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-47-01_uu_app_aicoding_conv91.txt +102 -0
  72. package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-58-26_uu_app_aicoding_conv92.txt +145 -0
  73. package/cursor-logs-export/chats/2026-02-05_2026-02-10_20-43-25_uu_app_aicoding_conv93.txt +553 -0
  74. package/cursor-logs-export/chats/2026-02-05_2026-02-10_20-56-36_uu_app_aicoding_conv95.txt +195 -0
  75. package/cursor-logs-export/chats/2026-02-05_2026-02-10_20-58-23_uu_app_aicoding_conv96.txt +86 -0
  76. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-01-26_uu_app_aicoding_conv94.txt +116 -0
  77. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-03-46_uu_app_aicoding_conv61.txt +1743 -0
  78. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-06-54_uu_app_aicoding_conv97.txt +102 -0
  79. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-07-32_uu_app_aicoding_conv29.txt +9930 -0
  80. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-09-02_uu_app_aicoding_conv98.txt +111 -0
  81. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-11-07_uu_app_aicoding_conv49.txt +170 -0
  82. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-16-16_uu_app_aicoding_conv62.txt +200 -0
  83. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-17-18_uu_app_aicoding_conv31.txt +351 -0
  84. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-26-32_uu_app_aicoding_conv99.txt +219 -0
  85. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-29-18_uu_app_aicoding_conv100.txt +121 -0
  86. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-33-35_uu_app_aicoding_conv30.txt +204 -0
  87. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-38-37_uu_app_aicoding_conv63.txt +251 -0
  88. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-42-10_uu_entitymanage_conv33.txt +163 -0
  89. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-43-41_uu_app_aicoding_conv64.txt +139 -0
  90. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-43-53_uu_app_aicoding_conv101.txt +221 -0
  91. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-44-55_uu_app_aicoding_conv50.txt +156 -0
  92. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-47-10_uu_app_aicoding_conv65.txt +136 -0
  93. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-48-40_uu_app_aicoding_conv51.txt +130 -0
  94. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-49-31_uu_app_aicoding_conv102.txt +153 -0
  95. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-49-44_uu_app_aicoding_conv66.txt +54 -0
  96. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-51-05_uu_app_aicoding_conv67.txt +55 -0
  97. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-51-26_uu_app_aicoding_conv32.txt +6172 -0
  98. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-56-08_uu_app_aicoding_conv103.txt +102 -0
  99. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-59-00_uu_app_aicoding_conv52.txt +244 -0
  100. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-10-16_uu_app_aicoding_conv77.txt +61 -0
  101. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-11-24_uu_app_aicoding_conv68.txt +142 -0
  102. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-12-31_uu_app_aicoding_conv104.txt +66 -0
  103. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-16-03_uu_app_aicoding_conv53.txt +439 -0
  104. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-23-41_uu_entitymanage_conv34.txt +2251 -0
  105. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-25-56_uu_app_aicoding_conv69.txt +169 -0
  106. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-26-54_uu_app_aicoding_conv105.txt +70 -0
  107. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-33-45_uu_entitymanage_conv35.txt +144 -0
  108. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-39-23_uu_app_aicoding_conv37.txt +104 -0
  109. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-45-30_uu_app_aicoding_conv78.txt +187 -0
  110. package/cursor-logs-export/chats/2026-02-05_2026-02-10_23-04-38_uu_app_aicoding_conv36.txt +2292 -0
  111. package/cursor-logs-export/chats/2026-02-05_2026-02-10_23-08-50_uu_entitymanage_conv38.txt +109 -0
  112. package/cursor-logs-export/chats/2026-02-05_2026-02-10_23-14-01_uu_entitymanage_conv39.txt +112 -0
  113. package/cursor-logs-export/report.html +3071 -0
  114. package/html-template.js +610 -18
  115. package/package.json +19 -6
  116. package/.idea/cursor-usage-analyzer.iml +0 -12
  117. package/.idea/modules.xml +0 -8
  118. package/.idea/vcs.xml +0 -11
  119. package/cursor-usage-analyzer-0.1.0.tgz +0 -0
  120. package/cursor-usage-analyzer-0.2.0.tgz +0 -0
  121. package/cursor-usage-analyzer-0.2.1.tgz +0 -0
  122. package/team-usage-events-10287858-2025-12-18.csv +0 -600
@@ -1,13 +1,19 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Bash(npm install)",
5
- "Bash(sqlite3:*)",
6
- "Bash(python3:*)",
4
+ "mcp__plus4u-mcp__document_read",
5
+ "mcp__plus4u-mcp__book_list_structure",
6
+ "Bash(npm install:*)",
7
7
  "Bash(node -e:*)",
8
- "Bash(for file in cursor-logs-export/chats/2025-12-17*.txt)",
9
- "Bash(grep:*)",
10
- "Bash(node analyze.js:*)"
8
+ "Bash(node analyze.js:*)",
9
+ "mcp__skilled-plus4u-mcp__login",
10
+ "Bash(npm rebuild:*)",
11
+ "mcp__skilled-plus4u-mcp__executeSkill",
12
+ "Bash(node-gyp rebuild:*)",
13
+ "Bash(npx node-gyp rebuild:*)",
14
+ "Bash(npm run install:*)",
15
+ "Bash(npm run build:*)",
16
+ "Bash(node validate.js:*)"
11
17
  ]
12
18
  }
13
19
  }
package/README.md CHANGED
@@ -34,8 +34,13 @@ cd cursor-usage-analyzer
34
34
 
35
35
  # Install dependencies
36
36
  npm install
37
+
38
+ # Build native bindings (runs automatically via postinstall, but run manually if needed)
39
+ npm run build
37
40
  ```
38
41
 
42
+ **Note**: The `postinstall` script automatically builds `better-sqlite3` native bindings. If you see SQLite errors, run `npm run build` manually.
43
+
39
44
  ## Quick Start
40
45
 
41
46
  ### Using npx
@@ -307,6 +312,15 @@ Ensure Cursor has been used and conversations exist. Check the database path for
307
312
  - Verify you have conversations in Cursor from that period
308
313
  - Ensure Cursor isn't currently running (may lock database)
309
314
 
315
+ ### SQLite errors or "Database exceeds 2GB limit"
316
+ This means `better-sqlite3` native bindings weren't built properly. Fix it by:
317
+
318
+ ```bash
319
+ npm run build
320
+ ```
321
+
322
+ This rebuilds the native SQLite bindings. The tool will fall back to `sql.js` (pure JavaScript) if native bindings fail, but `sql.js` has a 2GB database size limit.
323
+
310
324
  ### Project shows as "unknown"
311
325
  This happens when the analyzer can't determine the workspace from file paths. The conversation is still exported with all content intact.
312
326
 
package/analyze.js CHANGED
@@ -3,10 +3,83 @@
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
5
  import os from 'os';
6
- import Database from 'better-sqlite3';
7
6
  import { generateHTMLReport } from './html-template.js';
8
7
  import { parse } from 'csv-parse/sync';
9
8
 
9
+ // Database abstraction - try better-sqlite3 first, fall back to sql.js
10
+ let dbEngine = null;
11
+
12
+ async function loadDatabaseEngine() {
13
+ // Try better-sqlite3 first (faster, handles large files)
14
+ try {
15
+ const { default: Database } = await import('better-sqlite3');
16
+ // Test that it actually works (native bindings compiled)
17
+ const testDb = new Database(':memory:');
18
+ testDb.close();
19
+ dbEngine = { type: 'better-sqlite3', Database };
20
+ return;
21
+ } catch (e) {
22
+ console.log('Note: better-sqlite3 unavailable, trying sql.js fallback...');
23
+ }
24
+
25
+ // Fall back to sql.js (pure JS, works everywhere, but 2GB limit)
26
+ try {
27
+ const { default: initSqlJs } = await import('sql.js');
28
+ const SQL = await initSqlJs();
29
+ dbEngine = { type: 'sql.js', SQL };
30
+ } catch (e) {
31
+ throw new Error('No SQLite library available. Please run: npm install -g cursor-usage-analyzer');
32
+ }
33
+ }
34
+
35
+ function openDatabase(dbPath) {
36
+ if (dbEngine.type === 'better-sqlite3') {
37
+ return new dbEngine.Database(dbPath, { readonly: true });
38
+ } else {
39
+ const fileSize = fs.statSync(dbPath).size;
40
+ if (fileSize > 2 * 1024 * 1024 * 1024) {
41
+ throw new Error(
42
+ `Database file (${(fileSize / 1024 / 1024 / 1024).toFixed(2)} GB) exceeds sql.js 2GB limit.\n` +
43
+ `To fix this, install globally with native compilation:\n` +
44
+ ` npm install -g cursor-usage-analyzer\n` +
45
+ `Or clone and build locally:\n` +
46
+ ` git clone https://github.com/xaverric/cursor-usage-analyzer.git\n` +
47
+ ` cd cursor-usage-analyzer && npm install && npm rebuild better-sqlite3`
48
+ );
49
+ }
50
+ const dbBuffer = fs.readFileSync(dbPath);
51
+ return new dbEngine.SQL.Database(dbBuffer);
52
+ }
53
+ }
54
+
55
+ function dbQueryAll(db, sql, params = []) {
56
+ if (dbEngine.type === 'better-sqlite3') {
57
+ return db.prepare(sql).all(...params);
58
+ } else {
59
+ const stmt = db.prepare(sql);
60
+ if (params.length > 0) stmt.bind(params);
61
+ const results = [];
62
+ while (stmt.step()) {
63
+ results.push(stmt.getAsObject());
64
+ }
65
+ stmt.free();
66
+ return results;
67
+ }
68
+ }
69
+
70
+ function dbQueryOne(db, sql, params = []) {
71
+ if (dbEngine.type === 'better-sqlite3') {
72
+ return db.prepare(sql).get(...params);
73
+ } else {
74
+ const results = dbQueryAll(db, sql, params);
75
+ return results.length > 0 ? results[0] : null;
76
+ }
77
+ }
78
+
79
+ function dbClose(db) {
80
+ db.close();
81
+ }
82
+
10
83
  // Get Cursor storage paths based on OS
11
84
  function getCursorPaths() {
12
85
  const platform = os.platform();
@@ -135,13 +208,14 @@ function parseCSVUsage(csvPath) {
135
208
  }
136
209
 
137
210
  // Match API calls to a conversation based on timestamp and model
138
- function matchAPICallsToConversation(conv, apiCalls, timeWindow = 5 * 60 * 1000) {
211
+ function matchAPICallsToConversation(conv, apiCalls, timeWindow = 10 * 60 * 1000) {
139
212
  if (!apiCalls || apiCalls.length === 0) return [];
140
213
 
141
- const convStart = conv.timestamp;
142
- const convEnd = conv.messages.length > 0
143
- ? Math.max(...conv.messages.map(m => m.timestamp))
144
- : convStart;
214
+ const convStart = conv.createdAt || conv.timestamp;
215
+ const convEnd = Math.max(
216
+ conv.timestamp,
217
+ conv.messages.length > 0 ? Math.max(...conv.messages.map(m => m.timestamp)) : 0
218
+ );
145
219
 
146
220
  const normalizeModel = (model) => {
147
221
  if (!model) return 'unknown';
@@ -149,20 +223,21 @@ function matchAPICallsToConversation(conv, apiCalls, timeWindow = 5 * 60 * 1000)
149
223
  if (m.includes('sonnet')) return 'sonnet';
150
224
  if (m.includes('opus')) return 'opus';
151
225
  if (m.includes('composer')) return 'composer';
152
- if (m === 'auto' || m === 'default' || m === 'unknown') return 'any';
153
- return model;
226
+ if (m.includes('gpt')) return 'gpt';
227
+ if (m.includes('gemini')) return 'gemini';
228
+ return m;
154
229
  };
155
230
 
156
231
  const convModel = normalizeModel(conv.model);
232
+ // For 'auto', 'default', 'unknown' models - match by time only
233
+ const isFlexibleModel = ['auto', 'default', 'unknown'].includes(convModel);
157
234
 
158
235
  return apiCalls.filter(call => {
159
236
  const callModel = normalizeModel(call.model);
160
237
  const timeMatch = call.timestamp >= (convStart - timeWindow) &&
161
238
  call.timestamp <= (convEnd + timeWindow);
162
239
 
163
- const modelMatch = convModel === 'any' ||
164
- callModel === 'any' ||
165
- convModel === callModel;
240
+ const modelMatch = isFlexibleModel || convModel === callModel;
166
241
 
167
242
  return timeMatch && modelMatch;
168
243
  });
@@ -215,6 +290,37 @@ function readWorkspaceJson(workspaceId) {
215
290
  return null;
216
291
  }
217
292
 
293
+ // Build composerId -> workspaceName map from workspace databases
294
+ function buildComposerWorkspaceMap() {
295
+ const map = {};
296
+ try {
297
+ const entries = fs.readdirSync(CURSOR_WORKSPACE_STORAGE);
298
+ for (const entry of entries) {
299
+ const wsName = readWorkspaceJson(entry);
300
+ if (!wsName) continue;
301
+
302
+ const dbFile = path.join(CURSOR_WORKSPACE_STORAGE, entry, 'state.vscdb');
303
+ if (!fs.existsSync(dbFile)) continue;
304
+
305
+ try {
306
+ const db = openDatabase(dbFile);
307
+ const row = dbQueryOne(db, "SELECT value FROM ItemTable WHERE key = 'composer.composerData'");
308
+ if (row) {
309
+ const data = JSON.parse(row.value);
310
+ const composers = data.allComposers || [];
311
+ for (const c of composers) {
312
+ if (c.composerId) {
313
+ map[c.composerId] = wsName;
314
+ }
315
+ }
316
+ }
317
+ dbClose(db);
318
+ } catch (e) {}
319
+ }
320
+ } catch (e) {}
321
+ return map;
322
+ }
323
+
218
324
  // Find workspace name from file path
219
325
  function findWorkspaceFromPath(filePath) {
220
326
  try {
@@ -245,34 +351,89 @@ function findWorkspaceFromPath(filePath) {
245
351
  return parts[parts.length - 2] || 'unknown';
246
352
  }
247
353
 
354
+ // Extract file paths from various composer data sources
355
+ function extractFilePathsFromComposer(composerData) {
356
+ const paths = [];
357
+
358
+ // newlyCreatedFiles - array of objects with uri.path
359
+ if (Array.isArray(composerData.newlyCreatedFiles)) {
360
+ for (const f of composerData.newlyCreatedFiles) {
361
+ const p = f?.uri?.path || f?.uri?.fsPath;
362
+ if (p) paths.push(p);
363
+ }
364
+ }
365
+
366
+ // codeBlockData - can be array of file URI strings or object with file URI keys
367
+ if (Array.isArray(composerData.codeBlockData)) {
368
+ for (const item of composerData.codeBlockData) {
369
+ if (typeof item === 'string') paths.push(item.replace('file://', ''));
370
+ }
371
+ } else if (composerData.codeBlockData && typeof composerData.codeBlockData === 'object') {
372
+ for (const key of Object.keys(composerData.codeBlockData)) {
373
+ paths.push(key.replace('file://', ''));
374
+ }
375
+ }
376
+
377
+ // originalFileStates - array of file URI strings
378
+ if (Array.isArray(composerData.originalFileStates)) {
379
+ for (const item of composerData.originalFileStates) {
380
+ if (typeof item === 'string') paths.push(item.replace('file://', ''));
381
+ }
382
+ }
383
+
384
+ // allAttachedFileCodeChunksUris - array of file URI strings
385
+ if (Array.isArray(composerData.allAttachedFileCodeChunksUris)) {
386
+ for (const item of composerData.allAttachedFileCodeChunksUris) {
387
+ if (typeof item === 'string') paths.push(item.replace('file://', ''));
388
+ }
389
+ }
390
+
391
+ // addedFiles - can be array of file URI strings (sometimes it's a number)
392
+ if (Array.isArray(composerData.addedFiles)) {
393
+ for (const item of composerData.addedFiles) {
394
+ if (typeof item === 'string') paths.push(item.replace('file://', ''));
395
+ }
396
+ }
397
+
398
+ return paths;
399
+ }
400
+
248
401
  // Determine workspace name from multiple sources
249
- function resolveWorkspace(composerData, headers, db, composerId) {
250
- // Try workspaceId first
402
+ function resolveWorkspace(composerData, headers, composerId, composerWorkspaceMap) {
403
+ // Try composer->workspace map first (most reliable)
404
+ if (composerWorkspaceMap && composerId && composerWorkspaceMap[composerId]) {
405
+ return composerWorkspaceMap[composerId];
406
+ }
407
+
408
+ // Try workspaceId field
251
409
  if (composerData.workspaceId) {
252
410
  const name = readWorkspaceJson(composerData.workspaceId);
253
411
  if (name) return name;
254
412
  }
255
413
 
256
- // Try file paths from various sources
257
- const fileSources = [
258
- composerData.newlyCreatedFiles?.[0]?.uri?.path,
259
- Object.keys(composerData.codeBlockData || {})[0]?.replace('file://', ''),
260
- composerData.addedFiles?.[0]?.replace('file://', ''),
261
- composerData.allAttachedFileCodeChunksUris?.[0]?.replace('file://', '')
262
- ];
263
-
264
- for (const filePath of fileSources) {
414
+ // Try file paths from all available sources
415
+ const filePaths = extractFilePathsFromComposer(composerData);
416
+ for (const filePath of filePaths) {
265
417
  if (filePath) {
266
418
  const name = findWorkspaceFromPath(filePath);
267
419
  if (name !== 'unknown') return name;
268
420
  }
269
421
  }
270
422
 
423
+ return 'unknown';
424
+ }
425
+
426
+ // Determine workspace name with database access for messageRequestContext
427
+ function resolveWorkspaceWithDb(composerData, headers, db, composerId, composerWorkspaceMap) {
428
+ // Try basic resolution first
429
+ const basicResult = resolveWorkspace(composerData, headers, composerId, composerWorkspaceMap);
430
+ if (basicResult !== 'unknown') return basicResult;
431
+
271
432
  // Last resort: messageRequestContext
272
433
  if (headers?.length > 0) {
273
434
  try {
274
435
  const contextKey = `messageRequestContext:${composerId}:${headers[0].bubbleId}`;
275
- const row = db.prepare("SELECT value FROM cursorDiskKV WHERE key = ?").get(contextKey);
436
+ const row = dbQueryOne(db, "SELECT value FROM cursorDiskKV WHERE key = ?", [contextKey]);
276
437
  if (row) {
277
438
  const context = JSON.parse(row.value);
278
439
  if (context.projectLayouts?.length > 0) {
@@ -288,7 +449,7 @@ function resolveWorkspace(composerData, headers, db, composerId) {
288
449
  }
289
450
 
290
451
  // Extract conversations from database
291
- function extractConversations(startTime, endTime, apiCalls = []) {
452
+ async function extractConversations(startTime, endTime, apiCalls = []) {
292
453
  const dbPath = path.join(CURSOR_GLOBAL_STORAGE, 'state.vscdb');
293
454
 
294
455
  if (!fs.existsSync(dbPath)) {
@@ -297,11 +458,15 @@ function extractConversations(startTime, endTime, apiCalls = []) {
297
458
  }
298
459
 
299
460
  try {
300
- const db = new Database(dbPath, { readonly: true });
461
+ console.log('Building workspace map...');
462
+ const composerWorkspaceMap = buildComposerWorkspaceMap();
463
+ console.log(`Mapped ${Object.keys(composerWorkspaceMap).length} composer IDs to workspaces`);
464
+
465
+ const db = openDatabase(dbPath);
301
466
 
302
467
  // Load all bubbles
303
468
  const bubbleMap = {};
304
- const bubbleRows = db.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'bubbleId:%'").all();
469
+ const bubbleRows = dbQueryAll(db, "SELECT key, value FROM cursorDiskKV WHERE key LIKE 'bubbleId:%'");
305
470
 
306
471
  for (const row of bubbleRows) {
307
472
  try {
@@ -312,9 +477,9 @@ function extractConversations(startTime, endTime, apiCalls = []) {
312
477
  }
313
478
 
314
479
  // Load composers
315
- const composerRows = db.prepare(
480
+ const composerRows = dbQueryAll(db,
316
481
  "SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%' AND value LIKE '%fullConversationHeadersOnly%'"
317
- ).all();
482
+ );
318
483
 
319
484
  const conversations = [];
320
485
 
@@ -322,9 +487,12 @@ function extractConversations(startTime, endTime, apiCalls = []) {
322
487
  try {
323
488
  const composerId = row.key.split(':')[1];
324
489
  const composer = JSON.parse(row.value);
325
- const timestamp = composer.lastUpdatedAt || composer.createdAt || Date.now();
490
+ const createdAt = composer.createdAt || 0;
491
+ const lastUpdatedAt = composer.lastUpdatedAt || createdAt;
492
+ const timestamp = lastUpdatedAt || Date.now();
326
493
 
327
- if (timestamp < startTime || timestamp > endTime) continue;
494
+ // Include conversation if its time range overlaps with the query range
495
+ if (lastUpdatedAt < startTime || createdAt > endTime) continue;
328
496
 
329
497
  const headers = composer.fullConversationHeadersOnly || [];
330
498
  if (headers.length === 0) continue;
@@ -341,7 +509,7 @@ function extractConversations(startTime, endTime, apiCalls = []) {
341
509
  return {
342
510
  role: h.type === 1 ? 'user' : 'assistant',
343
511
  text: text.trim(),
344
- timestamp: bubble.timestamp || Date.now()
512
+ timestamp: bubble.timestamp || timestamp
345
513
  };
346
514
  })
347
515
  .filter(Boolean);
@@ -352,9 +520,10 @@ function extractConversations(startTime, endTime, apiCalls = []) {
352
520
  composerId,
353
521
  name: composer.name || 'Untitled Chat',
354
522
  timestamp,
523
+ createdAt,
355
524
  messages,
356
525
  messageCount: messages.length,
357
- workspace: resolveWorkspace(composer, headers, db, composerId),
526
+ workspace: resolveWorkspaceWithDb(composer, headers, db, composerId, composerWorkspaceMap),
358
527
  model: composer.modelConfig?.modelName || 'unknown',
359
528
  contextTokensUsed: composer.contextTokensUsed || 0,
360
529
  contextTokenLimit: composer.contextTokenLimit || 0,
@@ -372,22 +541,20 @@ function extractConversations(startTime, endTime, apiCalls = []) {
372
541
  // Match API calls to conversations WITHOUT double-counting
373
542
  // Each API call should only be matched to ONE conversation
374
543
  if (apiCalls.length > 0) {
544
+ // Tag each API call with its original index for tracking
545
+ const indexedApiCalls = apiCalls.map((call, idx) => ({ ...call, _origIdx: idx }));
375
546
  const usedApiCallIndices = new Set();
376
547
 
377
- for (const conv of conversations) {
378
- const availableApiCalls = apiCalls.filter((_, idx) => !usedApiCallIndices.has(idx));
548
+ // Sort conversations by timestamp so matching is deterministic
549
+ const sortedConvs = [...conversations].sort((a, b) => a.timestamp - b.timestamp);
550
+
551
+ for (const conv of sortedConvs) {
552
+ const availableApiCalls = indexedApiCalls.filter(c => !usedApiCallIndices.has(c._origIdx));
379
553
  const matchedCalls = matchAPICallsToConversation(conv, availableApiCalls);
380
554
 
381
- // Mark these API calls as used
555
+ // Mark these API calls as used by their original index
382
556
  matchedCalls.forEach(call => {
383
- const originalIndex = apiCalls.findIndex(c =>
384
- c.timestamp === call.timestamp &&
385
- c.model === call.model &&
386
- c.totalTokens === call.totalTokens
387
- );
388
- if (originalIndex !== -1) {
389
- usedApiCallIndices.add(originalIndex);
390
- }
557
+ usedApiCallIndices.add(call._origIdx);
391
558
  });
392
559
 
393
560
  conv.apiCalls = matchedCalls;
@@ -403,6 +570,48 @@ function extractConversations(startTime, endTime, apiCalls = []) {
403
570
  cost: matchedCalls.reduce((sum, c) => sum + c.cost, 0)
404
571
  };
405
572
  }
573
+
574
+ // Group unmatched API calls by day and add as synthetic entries
575
+ const unmatchedCalls = indexedApiCalls.filter(c => !usedApiCallIndices.has(c._origIdx));
576
+ if (unmatchedCalls.length > 0) {
577
+ const byDay = {};
578
+ for (const call of unmatchedCalls) {
579
+ const day = new Date(call.timestamp).toLocaleDateString('en-US');
580
+ if (!byDay[day]) byDay[day] = [];
581
+ byDay[day].push(call);
582
+ }
583
+
584
+ for (const [day, calls] of Object.entries(byDay)) {
585
+ const firstTs = Math.min(...calls.map(c => c.timestamp));
586
+ const models = [...new Set(calls.map(c => c.model))];
587
+ const modelStr = models.length <= 3 ? models.join(', ') : `${models.slice(0, 2).join(', ')} +${models.length - 2}`;
588
+ conversations.push({
589
+ composerId: `unmatched-${day}`,
590
+ name: `Unmatched API calls (${day})`,
591
+ timestamp: firstTs,
592
+ messages: [],
593
+ messageCount: 0,
594
+ workspace: '(unmatched)',
595
+ model: modelStr,
596
+ contextTokensUsed: 0,
597
+ contextTokenLimit: 0,
598
+ totalLinesAdded: 0,
599
+ totalLinesRemoved: 0,
600
+ filesChangedCount: 0,
601
+ isUnmatched: true,
602
+ apiCalls: calls,
603
+ apiCallCount: calls.length,
604
+ apiTokens: {
605
+ inputWithCache: calls.reduce((s, c) => s + c.inputWithCache, 0),
606
+ inputWithoutCache: calls.reduce((s, c) => s + c.inputWithoutCache, 0),
607
+ cacheRead: calls.reduce((s, c) => s + c.cacheRead, 0),
608
+ outputTokens: calls.reduce((s, c) => s + c.outputTokens, 0),
609
+ totalTokens: calls.reduce((s, c) => s + c.totalTokens, 0),
610
+ cost: calls.reduce((s, c) => s + c.cost, 0)
611
+ }
612
+ });
613
+ }
614
+ }
406
615
  } else {
407
616
  // No API calls to match
408
617
  conversations.forEach(conv => {
@@ -419,7 +628,7 @@ function extractConversations(startTime, endTime, apiCalls = []) {
419
628
  });
420
629
  }
421
630
 
422
- db.close();
631
+ dbClose(db);
423
632
  return conversations;
424
633
  } catch (error) {
425
634
  console.error('Database error:', error.message);
@@ -487,7 +696,7 @@ function exportConversation(conv, index, dateStr) {
487
696
  }
488
697
 
489
698
  // Generate statistics
490
- function generateStats(conversations, startOfDay, endOfDay) {
699
+ function generateStats(conversations, startOfDay, endOfDay, apiCalls = []) {
491
700
  const daysDiff = Math.ceil((endOfDay - startOfDay) / (1000 * 60 * 60 * 24));
492
701
 
493
702
  const stats = {
@@ -560,12 +769,30 @@ function generateStats(conversations, startOfDay, endOfDay) {
560
769
  apiTokens: conv.apiTokens,
561
770
  linesChanged: `+${conv.totalLinesAdded}/-${conv.totalLinesRemoved}`,
562
771
  files: conv.filesChangedCount,
563
- preview
772
+ preview,
773
+ exportedFile: conv.exportedFile || null,
774
+ isUnmatched: conv.isUnmatched || false
564
775
  });
565
776
  }
566
777
 
567
778
  stats.conversations.sort((a, b) => a.timestamp - b.timestamp);
568
779
 
780
+ // Compute CSV totals (all API calls in range, regardless of matching)
781
+ if (apiCalls.length > 0) {
782
+ const csvTotals = { total: 0, onDemand: 0, included: 0, errored: 0, callCount: apiCalls.length };
783
+ for (const call of apiCalls) {
784
+ csvTotals.total += call.cost;
785
+ const kind = (call.kind || '').toLowerCase();
786
+ if (kind.includes('on-demand')) csvTotals.onDemand += call.cost;
787
+ else if (kind.includes('included')) csvTotals.included += call.cost;
788
+ else if (kind.includes('error')) csvTotals.errored += call.cost;
789
+ else csvTotals.total += 0; // already counted
790
+ }
791
+ stats.csvTotals = csvTotals;
792
+ stats.unmatchedApiCalls = csvTotals.callCount - stats.totalApiCalls;
793
+ stats.unmatchedCost = csvTotals.total - stats.totalApiTokens.cost;
794
+ }
795
+
569
796
  return stats;
570
797
  }
571
798
 
@@ -612,7 +839,8 @@ async function main() {
612
839
 
613
840
  console.log('Extracting conversations...');
614
841
 
615
- const conversations = extractConversations(startOfDay, endOfDay, apiCalls);
842
+ await loadDatabaseEngine();
843
+ const conversations = await extractConversations(startOfDay, endOfDay, apiCalls);
616
844
 
617
845
  console.log(`Found ${conversations.length} conversations\n`);
618
846
 
@@ -623,11 +851,11 @@ async function main() {
623
851
 
624
852
  console.log('Exporting conversations...');
625
853
  conversations.forEach((conv, i) => {
626
- exportConversation(conv, i + 1, dateStr);
854
+ conv.exportedFile = exportConversation(conv, i + 1, dateStr);
627
855
  });
628
856
 
629
857
  console.log('Generating statistics...');
630
- const stats = generateStats(conversations, startOfDay, endOfDay);
858
+ const stats = generateStats(conversations, startOfDay, endOfDay, apiCalls);
631
859
 
632
860
  console.log('Generating HTML report...');
633
861
  try {
@@ -0,0 +1,49 @@
1
+ ================================================================================
2
+ CONVERSATION #55
3
+ Name: Skills reload
4
+ Workspace: uu_app_aicodingg01
5
+ Time: 2/7/2026, 8:35:54 AM
6
+ Model: claude-4.5-opus-high-thinking
7
+ Context Tokens: 25,471 / 164,000 (15.5%)
8
+ Changes: +0 -0 lines in 0 files
9
+ Messages: 2
10
+ Composer ID: 21f2687c-5206-41ff-8d02-bcfe24037482
11
+
12
+ API TOKEN USAGE (from dashboard export):
13
+ API Calls: 4
14
+ Input (w/ Cache Write): 105,264
15
+ Input (w/o Cache Write): 429
16
+ Cache Read: 1,429,552
17
+ Output Tokens: 8,366
18
+ Total API Tokens: 1,543,611
19
+ Cost: $1.97
20
+
21
+ ================================================================================
22
+
23
+
24
+ --------------------------------------------------------------------------------
25
+ [USER] 8:35:54 AM
26
+ --------------------------------------------------------------------------------
27
+ reload skills
28
+
29
+ --------------------------------------------------------------------------------
30
+ [ASSISTANT] 8:35:54 AM
31
+ --------------------------------------------------------------------------------
32
+ Skills reloaded successfully. 12 skills are now available:
33
+
34
+ - `bookkit-add-page`
35
+ - `bookkit-list-pages`
36
+ - `bookkit-read`
37
+ - `bookkit-update`
38
+ - `business-chat`
39
+ - `mngkit-read`
40
+ - `mngkit-update`
41
+ - `sls-read`
42
+ - `validate`
43
+ - `convert`
44
+ - `wtm-worklog`
45
+ - `skill`
46
+
47
+ ================================================================================
48
+ End of conversation
49
+ ================================================================================