claude-wec 1.0.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 (137) hide show
  1. package/LICENSE +675 -0
  2. package/README.md +371 -0
  3. package/dist/api-docs.html +879 -0
  4. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  5. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  6. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  11. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  12. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  17. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  18. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  19. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  20. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  21. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  22. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  23. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  24. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  26. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  28. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  30. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  31. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  32. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  33. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  34. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  35. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  36. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  46. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  47. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  48. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  49. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  50. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  51. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  52. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  53. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  54. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  55. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  56. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  57. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  58. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  59. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  60. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  61. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  62. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  63. package/dist/assets/index-cIxJ4RXb.js +1226 -0
  64. package/dist/assets/index-oyEz69sP.css +32 -0
  65. package/dist/assets/vendor-codemirror-CJLzwpLB.js +39 -0
  66. package/dist/assets/vendor-react-DcyRfQm3.js +59 -0
  67. package/dist/assets/vendor-xterm-DfaPXD3y.js +66 -0
  68. package/dist/clear-cache.html +85 -0
  69. package/dist/convert-icons.md +53 -0
  70. package/dist/favicon.png +0 -0
  71. package/dist/favicon.svg +9 -0
  72. package/dist/generate-icons.js +49 -0
  73. package/dist/icons/claude-ai-icon.svg +1 -0
  74. package/dist/icons/codex-white.svg +3 -0
  75. package/dist/icons/codex.svg +3 -0
  76. package/dist/icons/cursor-white.svg +12 -0
  77. package/dist/icons/cursor.svg +1 -0
  78. package/dist/icons/generate-icons.md +19 -0
  79. package/dist/icons/icon-128x128.png +0 -0
  80. package/dist/icons/icon-128x128.svg +12 -0
  81. package/dist/icons/icon-144x144.png +0 -0
  82. package/dist/icons/icon-144x144.svg +12 -0
  83. package/dist/icons/icon-152x152.png +0 -0
  84. package/dist/icons/icon-152x152.svg +12 -0
  85. package/dist/icons/icon-192x192.png +0 -0
  86. package/dist/icons/icon-192x192.svg +12 -0
  87. package/dist/icons/icon-384x384.png +0 -0
  88. package/dist/icons/icon-384x384.svg +12 -0
  89. package/dist/icons/icon-512x512.png +0 -0
  90. package/dist/icons/icon-512x512.svg +12 -0
  91. package/dist/icons/icon-72x72.png +0 -0
  92. package/dist/icons/icon-72x72.svg +12 -0
  93. package/dist/icons/icon-96x96.png +0 -0
  94. package/dist/icons/icon-96x96.svg +12 -0
  95. package/dist/icons/icon-template.svg +12 -0
  96. package/dist/index.html +52 -0
  97. package/dist/logo-128.png +0 -0
  98. package/dist/logo-256.png +0 -0
  99. package/dist/logo-32.png +0 -0
  100. package/dist/logo-512.png +0 -0
  101. package/dist/logo-64.png +0 -0
  102. package/dist/logo.svg +17 -0
  103. package/dist/manifest.json +61 -0
  104. package/dist/screenshots/cli-selection.png +0 -0
  105. package/dist/screenshots/desktop-main.png +0 -0
  106. package/dist/screenshots/mobile-chat.png +0 -0
  107. package/dist/screenshots/tools-modal.png +0 -0
  108. package/dist/sw.js +49 -0
  109. package/package.json +109 -0
  110. package/server/claude-sdk.js +721 -0
  111. package/server/cli.js +327 -0
  112. package/server/cursor-cli.js +267 -0
  113. package/server/database/auth.db +0 -0
  114. package/server/database/db.js +361 -0
  115. package/server/database/init.sql +52 -0
  116. package/server/index.js +1747 -0
  117. package/server/middleware/auth.js +111 -0
  118. package/server/openai-codex.js +389 -0
  119. package/server/projects.js +1604 -0
  120. package/server/routes/agent.js +1230 -0
  121. package/server/routes/auth.js +135 -0
  122. package/server/routes/cli-auth.js +263 -0
  123. package/server/routes/codex.js +345 -0
  124. package/server/routes/commands.js +521 -0
  125. package/server/routes/cursor.js +795 -0
  126. package/server/routes/git.js +1128 -0
  127. package/server/routes/mcp-utils.js +48 -0
  128. package/server/routes/mcp.js +552 -0
  129. package/server/routes/projects.js +378 -0
  130. package/server/routes/settings.js +178 -0
  131. package/server/routes/taskmaster.js +1963 -0
  132. package/server/routes/user.js +106 -0
  133. package/server/utils/commandParser.js +303 -0
  134. package/server/utils/gitConfig.js +24 -0
  135. package/server/utils/mcp-detector.js +198 -0
  136. package/server/utils/taskmaster-websocket.js +129 -0
  137. package/shared/modelConstants.js +65 -0
@@ -0,0 +1,795 @@
1
+ import express from 'express';
2
+ import { promises as fs } from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { spawn } from 'child_process';
6
+ import sqlite3 from 'sqlite3';
7
+ import { open } from 'sqlite';
8
+ import crypto from 'crypto';
9
+ import { CURSOR_MODELS } from '../../shared/modelConstants.js';
10
+
11
+ const router = express.Router();
12
+
13
+ // GET /api/cursor/config - Read Cursor CLI configuration
14
+ router.get('/config', async (req, res) => {
15
+ try {
16
+ const configPath = path.join(os.homedir(), '.cursor', 'cli-config.json');
17
+
18
+ try {
19
+ const configContent = await fs.readFile(configPath, 'utf8');
20
+ const config = JSON.parse(configContent);
21
+
22
+ res.json({
23
+ success: true,
24
+ config: config,
25
+ path: configPath
26
+ });
27
+ } catch (error) {
28
+ // Config doesn't exist or is invalid
29
+ console.log('Cursor config not found or invalid:', error.message);
30
+
31
+ // Return default config
32
+ res.json({
33
+ success: true,
34
+ config: {
35
+ version: 1,
36
+ model: {
37
+ modelId: CURSOR_MODELS.DEFAULT,
38
+ displayName: "GPT-5"
39
+ },
40
+ permissions: {
41
+ allow: [],
42
+ deny: []
43
+ }
44
+ },
45
+ isDefault: true
46
+ });
47
+ }
48
+ } catch (error) {
49
+ console.error('Error reading Cursor config:', error);
50
+ res.status(500).json({
51
+ error: 'Failed to read Cursor configuration',
52
+ details: error.message
53
+ });
54
+ }
55
+ });
56
+
57
+ // POST /api/cursor/config - Update Cursor CLI configuration
58
+ router.post('/config', async (req, res) => {
59
+ try {
60
+ const { permissions, model } = req.body;
61
+ const configPath = path.join(os.homedir(), '.cursor', 'cli-config.json');
62
+
63
+ // Read existing config or create default
64
+ let config = {
65
+ version: 1,
66
+ editor: {
67
+ vimMode: false
68
+ },
69
+ hasChangedDefaultModel: false,
70
+ privacyCache: {
71
+ ghostMode: false,
72
+ privacyMode: 3,
73
+ updatedAt: Date.now()
74
+ }
75
+ };
76
+
77
+ try {
78
+ const existing = await fs.readFile(configPath, 'utf8');
79
+ config = JSON.parse(existing);
80
+ } catch (error) {
81
+ // Config doesn't exist, use defaults
82
+ console.log('Creating new Cursor config');
83
+ }
84
+
85
+ // Update permissions if provided
86
+ if (permissions) {
87
+ config.permissions = {
88
+ allow: permissions.allow || [],
89
+ deny: permissions.deny || []
90
+ };
91
+ }
92
+
93
+ // Update model if provided
94
+ if (model) {
95
+ config.model = model;
96
+ config.hasChangedDefaultModel = true;
97
+ }
98
+
99
+ // Ensure directory exists
100
+ const configDir = path.dirname(configPath);
101
+ await fs.mkdir(configDir, { recursive: true });
102
+
103
+ // Write updated config
104
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
105
+
106
+ res.json({
107
+ success: true,
108
+ config: config,
109
+ message: 'Cursor configuration updated successfully'
110
+ });
111
+ } catch (error) {
112
+ console.error('Error updating Cursor config:', error);
113
+ res.status(500).json({
114
+ error: 'Failed to update Cursor configuration',
115
+ details: error.message
116
+ });
117
+ }
118
+ });
119
+
120
+ // GET /api/cursor/mcp - Read Cursor MCP servers configuration
121
+ router.get('/mcp', async (req, res) => {
122
+ try {
123
+ const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
124
+
125
+ try {
126
+ const mcpContent = await fs.readFile(mcpPath, 'utf8');
127
+ const mcpConfig = JSON.parse(mcpContent);
128
+
129
+ // Convert to UI-friendly format
130
+ const servers = [];
131
+ if (mcpConfig.mcpServers && typeof mcpConfig.mcpServers === 'object') {
132
+ for (const [name, config] of Object.entries(mcpConfig.mcpServers)) {
133
+ const server = {
134
+ id: name,
135
+ name: name,
136
+ type: 'stdio',
137
+ scope: 'cursor',
138
+ config: {},
139
+ raw: config
140
+ };
141
+
142
+ // Determine transport type and extract config
143
+ if (config.command) {
144
+ server.type = 'stdio';
145
+ server.config.command = config.command;
146
+ server.config.args = config.args || [];
147
+ server.config.env = config.env || {};
148
+ } else if (config.url) {
149
+ server.type = config.transport || 'http';
150
+ server.config.url = config.url;
151
+ server.config.headers = config.headers || {};
152
+ }
153
+
154
+ servers.push(server);
155
+ }
156
+ }
157
+
158
+ res.json({
159
+ success: true,
160
+ servers: servers,
161
+ path: mcpPath
162
+ });
163
+ } catch (error) {
164
+ // MCP config doesn't exist
165
+ console.log('Cursor MCP config not found:', error.message);
166
+ res.json({
167
+ success: true,
168
+ servers: [],
169
+ isDefault: true
170
+ });
171
+ }
172
+ } catch (error) {
173
+ console.error('Error reading Cursor MCP config:', error);
174
+ res.status(500).json({
175
+ error: 'Failed to read Cursor MCP configuration',
176
+ details: error.message
177
+ });
178
+ }
179
+ });
180
+
181
+ // POST /api/cursor/mcp/add - Add MCP server to Cursor configuration
182
+ router.post('/mcp/add', async (req, res) => {
183
+ try {
184
+ const { name, type = 'stdio', command, args = [], url, headers = {}, env = {} } = req.body;
185
+ const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
186
+
187
+ console.log(`➕ Adding MCP server to Cursor config: ${name}`);
188
+
189
+ // Read existing config or create new
190
+ let mcpConfig = { mcpServers: {} };
191
+
192
+ try {
193
+ const existing = await fs.readFile(mcpPath, 'utf8');
194
+ mcpConfig = JSON.parse(existing);
195
+ if (!mcpConfig.mcpServers) {
196
+ mcpConfig.mcpServers = {};
197
+ }
198
+ } catch (error) {
199
+ console.log('Creating new Cursor MCP config');
200
+ }
201
+
202
+ // Build server config based on type
203
+ let serverConfig = {};
204
+
205
+ if (type === 'stdio') {
206
+ serverConfig = {
207
+ command: command,
208
+ args: args,
209
+ env: env
210
+ };
211
+ } else if (type === 'http' || type === 'sse') {
212
+ serverConfig = {
213
+ url: url,
214
+ transport: type,
215
+ headers: headers
216
+ };
217
+ }
218
+
219
+ // Add server to config
220
+ mcpConfig.mcpServers[name] = serverConfig;
221
+
222
+ // Ensure directory exists
223
+ const mcpDir = path.dirname(mcpPath);
224
+ await fs.mkdir(mcpDir, { recursive: true });
225
+
226
+ // Write updated config
227
+ await fs.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2));
228
+
229
+ res.json({
230
+ success: true,
231
+ message: `MCP server "${name}" added to Cursor configuration`,
232
+ config: mcpConfig
233
+ });
234
+ } catch (error) {
235
+ console.error('Error adding MCP server to Cursor:', error);
236
+ res.status(500).json({
237
+ error: 'Failed to add MCP server',
238
+ details: error.message
239
+ });
240
+ }
241
+ });
242
+
243
+ // DELETE /api/cursor/mcp/:name - Remove MCP server from Cursor configuration
244
+ router.delete('/mcp/:name', async (req, res) => {
245
+ try {
246
+ const { name } = req.params;
247
+ const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
248
+
249
+ console.log(`🗑️ Removing MCP server from Cursor config: ${name}`);
250
+
251
+ // Read existing config
252
+ let mcpConfig = { mcpServers: {} };
253
+
254
+ try {
255
+ const existing = await fs.readFile(mcpPath, 'utf8');
256
+ mcpConfig = JSON.parse(existing);
257
+ } catch (error) {
258
+ return res.status(404).json({
259
+ error: 'Cursor MCP configuration not found'
260
+ });
261
+ }
262
+
263
+ // Check if server exists
264
+ if (!mcpConfig.mcpServers || !mcpConfig.mcpServers[name]) {
265
+ return res.status(404).json({
266
+ error: `MCP server "${name}" not found in Cursor configuration`
267
+ });
268
+ }
269
+
270
+ // Remove server from config
271
+ delete mcpConfig.mcpServers[name];
272
+
273
+ // Write updated config
274
+ await fs.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2));
275
+
276
+ res.json({
277
+ success: true,
278
+ message: `MCP server "${name}" removed from Cursor configuration`,
279
+ config: mcpConfig
280
+ });
281
+ } catch (error) {
282
+ console.error('Error removing MCP server from Cursor:', error);
283
+ res.status(500).json({
284
+ error: 'Failed to remove MCP server',
285
+ details: error.message
286
+ });
287
+ }
288
+ });
289
+
290
+ // POST /api/cursor/mcp/add-json - Add MCP server using JSON format
291
+ router.post('/mcp/add-json', async (req, res) => {
292
+ try {
293
+ const { name, jsonConfig } = req.body;
294
+ const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
295
+
296
+ console.log(`➕ Adding MCP server to Cursor config via JSON: ${name}`);
297
+
298
+ // Validate and parse JSON config
299
+ let parsedConfig;
300
+ try {
301
+ parsedConfig = typeof jsonConfig === 'string' ? JSON.parse(jsonConfig) : jsonConfig;
302
+ } catch (parseError) {
303
+ return res.status(400).json({
304
+ error: 'Invalid JSON configuration',
305
+ details: parseError.message
306
+ });
307
+ }
308
+
309
+ // Read existing config or create new
310
+ let mcpConfig = { mcpServers: {} };
311
+
312
+ try {
313
+ const existing = await fs.readFile(mcpPath, 'utf8');
314
+ mcpConfig = JSON.parse(existing);
315
+ if (!mcpConfig.mcpServers) {
316
+ mcpConfig.mcpServers = {};
317
+ }
318
+ } catch (error) {
319
+ console.log('Creating new Cursor MCP config');
320
+ }
321
+
322
+ // Add server to config
323
+ mcpConfig.mcpServers[name] = parsedConfig;
324
+
325
+ // Ensure directory exists
326
+ const mcpDir = path.dirname(mcpPath);
327
+ await fs.mkdir(mcpDir, { recursive: true });
328
+
329
+ // Write updated config
330
+ await fs.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2));
331
+
332
+ res.json({
333
+ success: true,
334
+ message: `MCP server "${name}" added to Cursor configuration via JSON`,
335
+ config: mcpConfig
336
+ });
337
+ } catch (error) {
338
+ console.error('Error adding MCP server to Cursor via JSON:', error);
339
+ res.status(500).json({
340
+ error: 'Failed to add MCP server',
341
+ details: error.message
342
+ });
343
+ }
344
+ });
345
+
346
+ // GET /api/cursor/sessions - Get Cursor sessions from SQLite database
347
+ router.get('/sessions', async (req, res) => {
348
+ try {
349
+ const { projectPath } = req.query;
350
+
351
+ // Calculate cwdID hash for the project path (Cursor uses MD5 hash)
352
+ const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex');
353
+ const cursorChatsPath = path.join(os.homedir(), '.cursor', 'chats', cwdId);
354
+
355
+
356
+ // Check if the directory exists
357
+ try {
358
+ await fs.access(cursorChatsPath);
359
+ } catch (error) {
360
+ // No sessions for this project
361
+ return res.json({
362
+ success: true,
363
+ sessions: [],
364
+ cwdId: cwdId,
365
+ path: cursorChatsPath
366
+ });
367
+ }
368
+
369
+ // List all session directories
370
+ const sessionDirs = await fs.readdir(cursorChatsPath);
371
+ const sessions = [];
372
+
373
+ for (const sessionId of sessionDirs) {
374
+ const sessionPath = path.join(cursorChatsPath, sessionId);
375
+ const storeDbPath = path.join(sessionPath, 'store.db');
376
+ let dbStatMtimeMs = null;
377
+
378
+ try {
379
+ // Check if store.db exists
380
+ await fs.access(storeDbPath);
381
+
382
+ // Capture store.db mtime as a reliable fallback timestamp (last activity)
383
+ try {
384
+ const stat = await fs.stat(storeDbPath);
385
+ dbStatMtimeMs = stat.mtimeMs;
386
+ } catch (_) {}
387
+
388
+ // Open SQLite database
389
+ const db = await open({
390
+ filename: storeDbPath,
391
+ driver: sqlite3.Database,
392
+ mode: sqlite3.OPEN_READONLY
393
+ });
394
+
395
+ // Get metadata from meta table
396
+ const metaRows = await db.all(`
397
+ SELECT key, value FROM meta
398
+ `);
399
+
400
+ let sessionData = {
401
+ id: sessionId,
402
+ name: 'Untitled Session',
403
+ createdAt: null,
404
+ mode: null,
405
+ projectPath: projectPath,
406
+ lastMessage: null,
407
+ messageCount: 0
408
+ };
409
+
410
+ // Parse meta table entries
411
+ for (const row of metaRows) {
412
+ if (row.value) {
413
+ try {
414
+ // Try to decode as hex-encoded JSON
415
+ const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/);
416
+ if (hexMatch) {
417
+ const jsonStr = Buffer.from(row.value, 'hex').toString('utf8');
418
+ const data = JSON.parse(jsonStr);
419
+
420
+ if (row.key === 'agent') {
421
+ sessionData.name = data.name || sessionData.name;
422
+ // Normalize createdAt to ISO string in milliseconds
423
+ let createdAt = data.createdAt;
424
+ if (typeof createdAt === 'number') {
425
+ if (createdAt < 1e12) {
426
+ createdAt = createdAt * 1000; // seconds -> ms
427
+ }
428
+ sessionData.createdAt = new Date(createdAt).toISOString();
429
+ } else if (typeof createdAt === 'string') {
430
+ const n = Number(createdAt);
431
+ if (!Number.isNaN(n)) {
432
+ const ms = n < 1e12 ? n * 1000 : n;
433
+ sessionData.createdAt = new Date(ms).toISOString();
434
+ } else {
435
+ // Assume it's already an ISO/date string
436
+ const d = new Date(createdAt);
437
+ sessionData.createdAt = isNaN(d.getTime()) ? null : d.toISOString();
438
+ }
439
+ } else {
440
+ sessionData.createdAt = sessionData.createdAt || null;
441
+ }
442
+ sessionData.mode = data.mode;
443
+ sessionData.agentId = data.agentId;
444
+ sessionData.latestRootBlobId = data.latestRootBlobId;
445
+ }
446
+ } else {
447
+ // If not hex, use raw value for simple keys
448
+ if (row.key === 'name') {
449
+ sessionData.name = row.value.toString();
450
+ }
451
+ }
452
+ } catch (e) {
453
+ console.log(`Could not parse meta value for key ${row.key}:`, e.message);
454
+ }
455
+ }
456
+ }
457
+
458
+ // Get message count from JSON blobs only (actual messages, not DAG structure)
459
+ try {
460
+ const blobCount = await db.get(`
461
+ SELECT COUNT(*) as count
462
+ FROM blobs
463
+ WHERE substr(data, 1, 1) = X'7B'
464
+ `);
465
+ sessionData.messageCount = blobCount.count;
466
+
467
+ // Get the most recent JSON blob for preview (actual message, not DAG structure)
468
+ const lastBlob = await db.get(`
469
+ SELECT data FROM blobs
470
+ WHERE substr(data, 1, 1) = X'7B'
471
+ ORDER BY rowid DESC
472
+ LIMIT 1
473
+ `);
474
+
475
+ if (lastBlob && lastBlob.data) {
476
+ try {
477
+ // Try to extract readable preview from blob (may contain binary with embedded JSON)
478
+ const raw = lastBlob.data.toString('utf8');
479
+ let preview = '';
480
+ // Attempt direct JSON parse
481
+ try {
482
+ const parsed = JSON.parse(raw);
483
+ if (parsed?.content) {
484
+ if (Array.isArray(parsed.content)) {
485
+ const firstText = parsed.content.find(p => p?.type === 'text' && p.text)?.text || '';
486
+ preview = firstText;
487
+ } else if (typeof parsed.content === 'string') {
488
+ preview = parsed.content;
489
+ }
490
+ }
491
+ } catch (_) {}
492
+ if (!preview) {
493
+ // Strip non-printable and try to find JSON chunk
494
+ const cleaned = raw.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, '');
495
+ const s = cleaned;
496
+ const start = s.indexOf('{');
497
+ const end = s.lastIndexOf('}');
498
+ if (start !== -1 && end > start) {
499
+ const jsonStr = s.slice(start, end + 1);
500
+ try {
501
+ const parsed = JSON.parse(jsonStr);
502
+ if (parsed?.content) {
503
+ if (Array.isArray(parsed.content)) {
504
+ const firstText = parsed.content.find(p => p?.type === 'text' && p.text)?.text || '';
505
+ preview = firstText;
506
+ } else if (typeof parsed.content === 'string') {
507
+ preview = parsed.content;
508
+ }
509
+ }
510
+ } catch (_) {
511
+ preview = s;
512
+ }
513
+ } else {
514
+ preview = s;
515
+ }
516
+ }
517
+ if (preview && preview.length > 0) {
518
+ sessionData.lastMessage = preview.substring(0, 100) + (preview.length > 100 ? '...' : '');
519
+ }
520
+ } catch (e) {
521
+ console.log('Could not parse blob data:', e.message);
522
+ }
523
+ }
524
+ } catch (e) {
525
+ console.log('Could not read blobs:', e.message);
526
+ }
527
+
528
+ await db.close();
529
+
530
+ // Finalize createdAt: use parsed meta value when valid, else fall back to store.db mtime
531
+ if (!sessionData.createdAt) {
532
+ if (dbStatMtimeMs && Number.isFinite(dbStatMtimeMs)) {
533
+ sessionData.createdAt = new Date(dbStatMtimeMs).toISOString();
534
+ }
535
+ }
536
+
537
+ sessions.push(sessionData);
538
+
539
+ } catch (error) {
540
+ console.log(`Could not read session ${sessionId}:`, error.message);
541
+ }
542
+ }
543
+
544
+ // Fallback: ensure createdAt is a valid ISO string (use session directory mtime as last resort)
545
+ for (const s of sessions) {
546
+ if (!s.createdAt) {
547
+ try {
548
+ const sessionDir = path.join(cursorChatsPath, s.id);
549
+ const st = await fs.stat(sessionDir);
550
+ s.createdAt = new Date(st.mtimeMs).toISOString();
551
+ } catch {
552
+ s.createdAt = new Date().toISOString();
553
+ }
554
+ }
555
+ }
556
+ // Sort sessions by creation date (newest first)
557
+ sessions.sort((a, b) => {
558
+ if (!a.createdAt) return 1;
559
+ if (!b.createdAt) return -1;
560
+ return new Date(b.createdAt) - new Date(a.createdAt);
561
+ });
562
+
563
+ res.json({
564
+ success: true,
565
+ sessions: sessions,
566
+ cwdId: cwdId,
567
+ path: cursorChatsPath
568
+ });
569
+
570
+ } catch (error) {
571
+ console.error('Error reading Cursor sessions:', error);
572
+ res.status(500).json({
573
+ error: 'Failed to read Cursor sessions',
574
+ details: error.message
575
+ });
576
+ }
577
+ });
578
+
579
+ // GET /api/cursor/sessions/:sessionId - Get specific Cursor session from SQLite
580
+ router.get('/sessions/:sessionId', async (req, res) => {
581
+ try {
582
+ const { sessionId } = req.params;
583
+ const { projectPath } = req.query;
584
+
585
+ // Calculate cwdID hash for the project path
586
+ const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex');
587
+ const storeDbPath = path.join(os.homedir(), '.cursor', 'chats', cwdId, sessionId, 'store.db');
588
+
589
+
590
+ // Open SQLite database
591
+ const db = await open({
592
+ filename: storeDbPath,
593
+ driver: sqlite3.Database,
594
+ mode: sqlite3.OPEN_READONLY
595
+ });
596
+
597
+ // Get all blobs to build the DAG structure
598
+ const allBlobs = await db.all(`
599
+ SELECT rowid, id, data FROM blobs
600
+ `);
601
+
602
+ // Build the DAG structure from parent-child relationships
603
+ const blobMap = new Map(); // id -> blob data
604
+ const parentRefs = new Map(); // blob id -> [parent blob ids]
605
+ const childRefs = new Map(); // blob id -> [child blob ids]
606
+ const jsonBlobs = []; // Clean JSON messages
607
+
608
+ for (const blob of allBlobs) {
609
+ blobMap.set(blob.id, blob);
610
+
611
+ // Check if this is a JSON blob (actual message) or protobuf (DAG structure)
612
+ if (blob.data && blob.data[0] === 0x7B) { // Starts with '{' - JSON blob
613
+ try {
614
+ const parsed = JSON.parse(blob.data.toString('utf8'));
615
+ jsonBlobs.push({ ...blob, parsed });
616
+ } catch (e) {
617
+ console.log('Failed to parse JSON blob:', blob.rowid);
618
+ }
619
+ } else if (blob.data) { // Protobuf blob - extract parent references
620
+ const parents = [];
621
+ let i = 0;
622
+
623
+ // Scan for parent references (0x0A 0x20 followed by 32-byte hash)
624
+ while (i < blob.data.length - 33) {
625
+ if (blob.data[i] === 0x0A && blob.data[i+1] === 0x20) {
626
+ const parentHash = blob.data.slice(i+2, i+34).toString('hex');
627
+ if (blobMap.has(parentHash)) {
628
+ parents.push(parentHash);
629
+ }
630
+ i += 34;
631
+ } else {
632
+ i++;
633
+ }
634
+ }
635
+
636
+ if (parents.length > 0) {
637
+ parentRefs.set(blob.id, parents);
638
+ // Update child references
639
+ for (const parentId of parents) {
640
+ if (!childRefs.has(parentId)) {
641
+ childRefs.set(parentId, []);
642
+ }
643
+ childRefs.get(parentId).push(blob.id);
644
+ }
645
+ }
646
+ }
647
+ }
648
+
649
+ // Perform topological sort to get chronological order
650
+ const visited = new Set();
651
+ const sorted = [];
652
+
653
+ // DFS-based topological sort
654
+ function visit(nodeId) {
655
+ if (visited.has(nodeId)) return;
656
+ visited.add(nodeId);
657
+
658
+ // Visit all parents first (dependencies)
659
+ const parents = parentRefs.get(nodeId) || [];
660
+ for (const parentId of parents) {
661
+ visit(parentId);
662
+ }
663
+
664
+ // Add this node after all its parents
665
+ const blob = blobMap.get(nodeId);
666
+ if (blob) {
667
+ sorted.push(blob);
668
+ }
669
+ }
670
+
671
+ // Start with nodes that have no parents (roots)
672
+ for (const blob of allBlobs) {
673
+ if (!parentRefs.has(blob.id)) {
674
+ visit(blob.id);
675
+ }
676
+ }
677
+
678
+ // Visit any remaining nodes (disconnected components)
679
+ for (const blob of allBlobs) {
680
+ visit(blob.id);
681
+ }
682
+
683
+ // Now extract JSON messages in the order they appear in the sorted DAG
684
+ const messageOrder = new Map(); // JSON blob id -> order index
685
+ let orderIndex = 0;
686
+
687
+ for (const blob of sorted) {
688
+ // Check if this blob references any JSON messages
689
+ if (blob.data && blob.data[0] !== 0x7B) { // Protobuf blob
690
+ // Look for JSON blob references
691
+ for (const jsonBlob of jsonBlobs) {
692
+ try {
693
+ const jsonIdBytes = Buffer.from(jsonBlob.id, 'hex');
694
+ if (blob.data.includes(jsonIdBytes)) {
695
+ if (!messageOrder.has(jsonBlob.id)) {
696
+ messageOrder.set(jsonBlob.id, orderIndex++);
697
+ }
698
+ }
699
+ } catch (e) {
700
+ // Skip if can't convert ID
701
+ }
702
+ }
703
+ }
704
+ }
705
+
706
+ // Sort JSON blobs by their appearance order in the DAG
707
+ const sortedJsonBlobs = jsonBlobs.sort((a, b) => {
708
+ const orderA = messageOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER;
709
+ const orderB = messageOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER;
710
+ if (orderA !== orderB) return orderA - orderB;
711
+ // Fallback to rowid if not in order map
712
+ return a.rowid - b.rowid;
713
+ });
714
+
715
+ // Use sorted JSON blobs
716
+ const blobs = sortedJsonBlobs.map((blob, idx) => ({
717
+ ...blob,
718
+ sequence_num: idx + 1,
719
+ original_rowid: blob.rowid
720
+ }));
721
+
722
+ // Get metadata from meta table
723
+ const metaRows = await db.all(`
724
+ SELECT key, value FROM meta
725
+ `);
726
+
727
+ // Parse metadata
728
+ let metadata = {};
729
+ for (const row of metaRows) {
730
+ if (row.value) {
731
+ try {
732
+ // Try to decode as hex-encoded JSON
733
+ const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/);
734
+ if (hexMatch) {
735
+ const jsonStr = Buffer.from(row.value, 'hex').toString('utf8');
736
+ metadata[row.key] = JSON.parse(jsonStr);
737
+ } else {
738
+ metadata[row.key] = row.value.toString();
739
+ }
740
+ } catch (e) {
741
+ metadata[row.key] = row.value.toString();
742
+ }
743
+ }
744
+ }
745
+
746
+ // Extract messages from sorted JSON blobs
747
+ const messages = [];
748
+ for (const blob of blobs) {
749
+ try {
750
+ // We already parsed JSON blobs earlier
751
+ const parsed = blob.parsed;
752
+
753
+ if (parsed) {
754
+ // Filter out ONLY system messages at the server level
755
+ // Check both direct role and nested message.role
756
+ const role = parsed?.role || parsed?.message?.role;
757
+ if (role === 'system') {
758
+ continue; // Skip only system messages
759
+ }
760
+ messages.push({
761
+ id: blob.id,
762
+ sequence: blob.sequence_num,
763
+ rowid: blob.original_rowid,
764
+ content: parsed
765
+ });
766
+ }
767
+ } catch (e) {
768
+ // Skip blobs that cause errors
769
+ console.log(`Skipping blob ${blob.id}: ${e.message}`);
770
+ }
771
+ }
772
+
773
+ await db.close();
774
+
775
+ res.json({
776
+ success: true,
777
+ session: {
778
+ id: sessionId,
779
+ projectPath: projectPath,
780
+ messages: messages,
781
+ metadata: metadata,
782
+ cwdId: cwdId
783
+ }
784
+ });
785
+
786
+ } catch (error) {
787
+ console.error('Error reading Cursor session:', error);
788
+ res.status(500).json({
789
+ error: 'Failed to read Cursor session',
790
+ details: error.message
791
+ });
792
+ }
793
+ });
794
+
795
+ export default router;