cursor-history 0.5.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 (58) hide show
  1. package/README.md +225 -0
  2. package/dist/cli/commands/export.d.ts +9 -0
  3. package/dist/cli/commands/export.d.ts.map +1 -0
  4. package/dist/cli/commands/export.js +126 -0
  5. package/dist/cli/commands/export.js.map +1 -0
  6. package/dist/cli/commands/list.d.ts +9 -0
  7. package/dist/cli/commands/list.d.ts.map +1 -0
  8. package/dist/cli/commands/list.js +77 -0
  9. package/dist/cli/commands/list.js.map +1 -0
  10. package/dist/cli/commands/search.d.ts +9 -0
  11. package/dist/cli/commands/search.d.ts.map +1 -0
  12. package/dist/cli/commands/search.js +47 -0
  13. package/dist/cli/commands/search.js.map +1 -0
  14. package/dist/cli/commands/show.d.ts +9 -0
  15. package/dist/cli/commands/show.d.ts.map +1 -0
  16. package/dist/cli/commands/show.js +51 -0
  17. package/dist/cli/commands/show.js.map +1 -0
  18. package/dist/cli/formatters/index.d.ts +6 -0
  19. package/dist/cli/formatters/index.d.ts.map +1 -0
  20. package/dist/cli/formatters/index.js +6 -0
  21. package/dist/cli/formatters/index.js.map +1 -0
  22. package/dist/cli/formatters/json.d.ts +28 -0
  23. package/dist/cli/formatters/json.d.ts.map +1 -0
  24. package/dist/cli/formatters/json.js +98 -0
  25. package/dist/cli/formatters/json.js.map +1 -0
  26. package/dist/cli/formatters/table.d.ts +45 -0
  27. package/dist/cli/formatters/table.d.ts.map +1 -0
  28. package/dist/cli/formatters/table.js +439 -0
  29. package/dist/cli/formatters/table.js.map +1 -0
  30. package/dist/cli/index.d.ts +6 -0
  31. package/dist/cli/index.d.ts.map +1 -0
  32. package/dist/cli/index.js +51 -0
  33. package/dist/cli/index.js.map +1 -0
  34. package/dist/core/index.d.ts +7 -0
  35. package/dist/core/index.d.ts.map +1 -0
  36. package/dist/core/index.js +8 -0
  37. package/dist/core/index.js.map +1 -0
  38. package/dist/core/parser.d.ts +38 -0
  39. package/dist/core/parser.d.ts.map +1 -0
  40. package/dist/core/parser.js +324 -0
  41. package/dist/core/parser.js.map +1 -0
  42. package/dist/core/storage.d.ts +45 -0
  43. package/dist/core/storage.d.ts.map +1 -0
  44. package/dist/core/storage.js +869 -0
  45. package/dist/core/storage.js.map +1 -0
  46. package/dist/core/types.d.ts +113 -0
  47. package/dist/core/types.d.ts.map +1 -0
  48. package/dist/core/types.js +6 -0
  49. package/dist/core/types.js.map +1 -0
  50. package/dist/lib/errors.d.ts +56 -0
  51. package/dist/lib/errors.d.ts.map +1 -0
  52. package/dist/lib/errors.js +90 -0
  53. package/dist/lib/errors.js.map +1 -0
  54. package/dist/lib/platform.d.ts +25 -0
  55. package/dist/lib/platform.d.ts.map +1 -0
  56. package/dist/lib/platform.js +66 -0
  57. package/dist/lib/platform.js.map +1 -0
  58. package/package.json +64 -0
@@ -0,0 +1,869 @@
1
+ /**
2
+ * Storage discovery and database access for Cursor chat history
3
+ */
4
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { homedir } from 'node:os';
7
+ import Database from 'better-sqlite3';
8
+ import { getCursorDataPath, contractPath } from '../lib/platform.js';
9
+ import { parseChatData, getSearchSnippets } from './parser.js';
10
+ /**
11
+ * Known SQLite keys for chat data (in priority order)
12
+ */
13
+ const CHAT_DATA_KEYS = [
14
+ 'composer.composerData', // New Cursor format
15
+ 'workbench.panel.aichat.view.aichat.chatdata', // Legacy format
16
+ 'workbench.panel.chat.view.chat.chatdata', // Legacy format
17
+ ];
18
+ /**
19
+ * Keys for prompts and generations (new Cursor format)
20
+ */
21
+ const PROMPTS_KEY = 'aiService.prompts';
22
+ const GENERATIONS_KEY = 'aiService.generations';
23
+ /**
24
+ * Get the global Cursor storage path
25
+ */
26
+ function getGlobalStoragePath() {
27
+ const platform = process.platform;
28
+ const home = homedir();
29
+ if (platform === 'win32') {
30
+ return join(process.env['APPDATA'] ?? join(home, 'AppData', 'Roaming'), 'Cursor', 'User', 'globalStorage');
31
+ }
32
+ else if (platform === 'darwin') {
33
+ return join(home, 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage');
34
+ }
35
+ else {
36
+ return join(home, '.config', 'Cursor', 'User', 'globalStorage');
37
+ }
38
+ }
39
+ /**
40
+ * Open a SQLite database file
41
+ */
42
+ export function openDatabase(dbPath) {
43
+ return new Database(dbPath, { readonly: true });
44
+ }
45
+ /**
46
+ * Read workspace.json to get the original workspace path
47
+ */
48
+ export function readWorkspaceJson(workspaceDir) {
49
+ const jsonPath = join(workspaceDir, 'workspace.json');
50
+ if (!existsSync(jsonPath)) {
51
+ return null;
52
+ }
53
+ try {
54
+ const content = readFileSync(jsonPath, 'utf-8');
55
+ const data = JSON.parse(content);
56
+ if (data.folder) {
57
+ // Convert file:// URL to path
58
+ return data.folder.replace(/^file:\/\//, '').replace(/%20/g, ' ');
59
+ }
60
+ return null;
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ /**
67
+ * Find all workspaces with chat history
68
+ */
69
+ export function findWorkspaces(customDataPath) {
70
+ const basePath = getCursorDataPath(customDataPath);
71
+ if (!existsSync(basePath)) {
72
+ return [];
73
+ }
74
+ const workspaces = [];
75
+ try {
76
+ const entries = readdirSync(basePath, { withFileTypes: true });
77
+ for (const entry of entries) {
78
+ if (!entry.isDirectory())
79
+ continue;
80
+ const workspaceDir = join(basePath, entry.name);
81
+ const dbPath = join(workspaceDir, 'state.vscdb');
82
+ if (!existsSync(dbPath))
83
+ continue;
84
+ const workspacePath = readWorkspaceJson(workspaceDir);
85
+ if (!workspacePath)
86
+ continue;
87
+ // Count sessions in this workspace
88
+ let sessionCount = 0;
89
+ try {
90
+ const db = openDatabase(dbPath);
91
+ const result = getChatDataFromDb(db);
92
+ if (result) {
93
+ const parsed = parseChatData(result.data, result.bundle);
94
+ sessionCount = parsed.length;
95
+ }
96
+ db.close();
97
+ }
98
+ catch {
99
+ // Skip workspaces with unreadable databases
100
+ continue;
101
+ }
102
+ if (sessionCount > 0) {
103
+ workspaces.push({
104
+ id: entry.name,
105
+ path: workspacePath,
106
+ dbPath,
107
+ sessionCount,
108
+ });
109
+ }
110
+ }
111
+ }
112
+ catch {
113
+ return [];
114
+ }
115
+ return workspaces;
116
+ }
117
+ /**
118
+ * Get chat data JSON from database
119
+ * Returns both the main chat data and the bundle for new format
120
+ */
121
+ function getChatDataFromDb(db) {
122
+ let mainData = null;
123
+ const bundle = {};
124
+ // Try to get the main chat data
125
+ for (const key of CHAT_DATA_KEYS) {
126
+ try {
127
+ const row = db.prepare('SELECT value FROM ItemTable WHERE key = ?').get(key);
128
+ if (row?.value) {
129
+ mainData = row.value;
130
+ if (key === 'composer.composerData') {
131
+ bundle.composerData = row.value;
132
+ }
133
+ break;
134
+ }
135
+ }
136
+ catch {
137
+ continue;
138
+ }
139
+ }
140
+ if (!mainData) {
141
+ return null;
142
+ }
143
+ // For new format, also get prompts and generations
144
+ try {
145
+ const promptsRow = db.prepare('SELECT value FROM ItemTable WHERE key = ?').get(PROMPTS_KEY);
146
+ if (promptsRow?.value) {
147
+ bundle.prompts = promptsRow.value;
148
+ }
149
+ }
150
+ catch {
151
+ // Ignore
152
+ }
153
+ try {
154
+ const gensRow = db.prepare('SELECT value FROM ItemTable WHERE key = ?').get(GENERATIONS_KEY);
155
+ if (gensRow?.value) {
156
+ bundle.generations = gensRow.value;
157
+ }
158
+ }
159
+ catch {
160
+ // Ignore
161
+ }
162
+ return { data: mainData, bundle };
163
+ }
164
+ /**
165
+ * List chat sessions with optional filtering
166
+ * Uses workspace storage for listing (has correct paths and complete list)
167
+ */
168
+ export function listSessions(options, customDataPath) {
169
+ const workspaces = findWorkspaces(customDataPath);
170
+ // Filter by workspace if specified
171
+ const filteredWorkspaces = options.workspacePath
172
+ ? workspaces.filter((w) => w.path === options.workspacePath || w.path.endsWith(options.workspacePath ?? ''))
173
+ : workspaces;
174
+ const allSessions = [];
175
+ for (const workspace of filteredWorkspaces) {
176
+ try {
177
+ const db = openDatabase(workspace.dbPath);
178
+ const result = getChatDataFromDb(db);
179
+ db.close();
180
+ if (!result)
181
+ continue;
182
+ const sessions = parseChatData(result.data, result.bundle);
183
+ for (const session of sessions) {
184
+ allSessions.push({
185
+ id: session.id,
186
+ index: 0, // Will be assigned after sorting
187
+ title: session.title,
188
+ createdAt: session.createdAt,
189
+ lastUpdatedAt: session.lastUpdatedAt,
190
+ messageCount: session.messageCount,
191
+ workspaceId: workspace.id,
192
+ workspacePath: contractPath(workspace.path),
193
+ preview: session.messages[0]?.content.slice(0, 100) ?? '(Empty session)',
194
+ });
195
+ }
196
+ }
197
+ catch {
198
+ continue;
199
+ }
200
+ }
201
+ // Sort by most recent first
202
+ allSessions.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
203
+ // Assign indexes
204
+ allSessions.forEach((session, i) => {
205
+ session.index = i + 1;
206
+ });
207
+ // Apply limit
208
+ if (!options.all && options.limit > 0) {
209
+ return allSessions.slice(0, options.limit);
210
+ }
211
+ return allSessions;
212
+ }
213
+ /**
214
+ * List all workspaces with chat history
215
+ */
216
+ export function listWorkspaces(customDataPath) {
217
+ const workspaces = findWorkspaces(customDataPath);
218
+ // Sort by session count descending
219
+ workspaces.sort((a, b) => b.sessionCount - a.sessionCount);
220
+ return workspaces.map((w) => ({
221
+ ...w,
222
+ path: contractPath(w.path),
223
+ }));
224
+ }
225
+ /**
226
+ * Get a specific session by index
227
+ * Tries global storage first for complete AI responses, falls back to workspace storage
228
+ */
229
+ export function getSession(index, customDataPath) {
230
+ const summaries = listSessions({ limit: 0, all: true }, customDataPath);
231
+ const summary = summaries.find((s) => s.index === index);
232
+ if (!summary) {
233
+ return null;
234
+ }
235
+ // Try to get full session from global storage (has AI responses)
236
+ const globalPath = getGlobalStoragePath();
237
+ const globalDbPath = join(globalPath, 'state.vscdb');
238
+ if (existsSync(globalDbPath)) {
239
+ try {
240
+ const db = openDatabase(globalDbPath);
241
+ // Check if cursorDiskKV table exists
242
+ const tableCheck = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='cursorDiskKV'").get();
243
+ if (tableCheck) {
244
+ // Get all bubbles for this composer
245
+ const bubbleRows = db
246
+ .prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE ? ORDER BY rowid ASC")
247
+ .all(`bubbleId:${summary.id}:%`);
248
+ db.close();
249
+ if (bubbleRows.length > 0) {
250
+ const messages = bubbleRows.map((row) => {
251
+ try {
252
+ const data = JSON.parse(row.value);
253
+ const text = extractBubbleText(data);
254
+ const role = data.type === 2 ? 'assistant' : 'user';
255
+ return {
256
+ id: data.bubbleId ?? row.key.split(':').pop() ?? null,
257
+ role: role,
258
+ content: text,
259
+ timestamp: data.createdAt ? new Date(data.createdAt) : new Date(),
260
+ codeBlocks: [],
261
+ };
262
+ }
263
+ catch {
264
+ return null;
265
+ }
266
+ }).filter((m) => m !== null && m.content.length > 0);
267
+ if (messages.length > 0) {
268
+ return {
269
+ id: summary.id,
270
+ index,
271
+ title: summary.title,
272
+ createdAt: summary.createdAt,
273
+ lastUpdatedAt: summary.lastUpdatedAt,
274
+ messageCount: messages.length,
275
+ messages,
276
+ workspaceId: summary.workspaceId,
277
+ workspacePath: summary.workspacePath,
278
+ };
279
+ }
280
+ }
281
+ }
282
+ else {
283
+ db.close();
284
+ }
285
+ }
286
+ catch {
287
+ // Fall through to workspace storage
288
+ }
289
+ }
290
+ // Fall back to workspace storage
291
+ const workspaces = findWorkspaces(customDataPath);
292
+ const workspace = workspaces.find((w) => w.id === summary.workspaceId);
293
+ if (!workspace) {
294
+ return null;
295
+ }
296
+ try {
297
+ const db = openDatabase(workspace.dbPath);
298
+ const result = getChatDataFromDb(db);
299
+ db.close();
300
+ if (!result)
301
+ return null;
302
+ const sessions = parseChatData(result.data, result.bundle);
303
+ const session = sessions.find((s) => s.id === summary.id);
304
+ if (!session)
305
+ return null;
306
+ return {
307
+ ...session,
308
+ index,
309
+ workspaceId: workspace.id,
310
+ workspacePath: summary.workspacePath,
311
+ };
312
+ }
313
+ catch {
314
+ return null;
315
+ }
316
+ }
317
+ /**
318
+ * Search across all chat sessions
319
+ */
320
+ export function searchSessions(query, options, customDataPath) {
321
+ const summaries = listSessions({ limit: 0, all: true, workspacePath: options.workspacePath }, customDataPath);
322
+ const results = [];
323
+ const lowerQuery = query.toLowerCase();
324
+ for (const summary of summaries) {
325
+ const session = getSession(summary.index, customDataPath);
326
+ if (!session)
327
+ continue;
328
+ const snippets = getSearchSnippets(session.messages, lowerQuery, options.contextChars);
329
+ if (snippets.length > 0) {
330
+ const matchCount = snippets.reduce((sum, s) => sum + s.matchPositions.length, 0);
331
+ results.push({
332
+ sessionId: summary.id,
333
+ index: summary.index,
334
+ workspacePath: summary.workspacePath,
335
+ createdAt: summary.createdAt,
336
+ matchCount,
337
+ snippets,
338
+ });
339
+ }
340
+ }
341
+ // Sort by match count descending
342
+ results.sort((a, b) => b.matchCount - a.matchCount);
343
+ // Apply limit
344
+ if (options.limit > 0) {
345
+ return results.slice(0, options.limit);
346
+ }
347
+ return results;
348
+ }
349
+ /**
350
+ * List sessions from global Cursor storage (cursorDiskKV table)
351
+ * This is where Cursor stores full conversation data including AI responses
352
+ */
353
+ export function listGlobalSessions() {
354
+ const globalPath = getGlobalStoragePath();
355
+ const dbPath = join(globalPath, 'state.vscdb');
356
+ if (!existsSync(dbPath)) {
357
+ return [];
358
+ }
359
+ try {
360
+ const db = openDatabase(dbPath);
361
+ // Check if cursorDiskKV table exists
362
+ const tableCheck = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='cursorDiskKV'").get();
363
+ if (!tableCheck) {
364
+ db.close();
365
+ return [];
366
+ }
367
+ // Get all composerData entries
368
+ const composerRows = db
369
+ .prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'")
370
+ .all();
371
+ const sessions = [];
372
+ for (const row of composerRows) {
373
+ const composerId = row.key.replace('composerData:', '');
374
+ try {
375
+ const data = JSON.parse(row.value);
376
+ // Count bubbles for this composer
377
+ const bubbleCount = db
378
+ .prepare("SELECT COUNT(*) as count FROM cursorDiskKV WHERE key LIKE ?")
379
+ .get(`bubbleId:${composerId}:%`);
380
+ if (bubbleCount.count === 0)
381
+ continue;
382
+ // Get first bubble for preview
383
+ const firstBubble = db
384
+ .prepare("SELECT value FROM cursorDiskKV WHERE key LIKE ? ORDER BY rowid ASC LIMIT 1")
385
+ .get(`bubbleId:${composerId}:%`);
386
+ let preview = '';
387
+ if (firstBubble) {
388
+ try {
389
+ const bubbleData = JSON.parse(firstBubble.value);
390
+ preview = extractBubbleText(bubbleData).slice(0, 100);
391
+ }
392
+ catch {
393
+ // Ignore
394
+ }
395
+ }
396
+ const createdAt = data.createdAt ? new Date(data.createdAt) : new Date();
397
+ const workspacePath = data.workspaceUri
398
+ ? data.workspaceUri.replace(/^file:\/\//, '').replace(/%20/g, ' ')
399
+ : 'Global';
400
+ sessions.push({
401
+ id: composerId,
402
+ index: 0,
403
+ title: data.name ?? data.title ?? null,
404
+ createdAt,
405
+ lastUpdatedAt: data.updatedAt ? new Date(data.updatedAt) : createdAt,
406
+ messageCount: bubbleCount.count,
407
+ workspaceId: 'global',
408
+ workspacePath: contractPath(workspacePath),
409
+ preview,
410
+ });
411
+ }
412
+ catch {
413
+ continue;
414
+ }
415
+ }
416
+ db.close();
417
+ // Sort by most recent first
418
+ sessions.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
419
+ // Assign indexes
420
+ sessions.forEach((session, i) => {
421
+ session.index = i + 1;
422
+ });
423
+ return sessions;
424
+ }
425
+ catch {
426
+ return [];
427
+ }
428
+ }
429
+ /**
430
+ * Get a session from global storage by index
431
+ */
432
+ export function getGlobalSession(index) {
433
+ const summaries = listGlobalSessions();
434
+ const summary = summaries.find((s) => s.index === index);
435
+ if (!summary) {
436
+ return null;
437
+ }
438
+ const globalPath = getGlobalStoragePath();
439
+ const dbPath = join(globalPath, 'state.vscdb');
440
+ try {
441
+ const db = openDatabase(dbPath);
442
+ // Get all bubbles for this composer
443
+ const bubbleRows = db
444
+ .prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE ? ORDER BY rowid ASC")
445
+ .all(`bubbleId:${summary.id}:%`);
446
+ db.close();
447
+ const messages = bubbleRows.map((row) => {
448
+ try {
449
+ const data = JSON.parse(row.value);
450
+ const text = extractBubbleText(data);
451
+ const role = data.type === 2 ? 'assistant' : 'user';
452
+ return {
453
+ id: data.bubbleId ?? row.key.split(':').pop() ?? null,
454
+ role: role,
455
+ content: text,
456
+ timestamp: data.createdAt ? new Date(data.createdAt) : new Date(),
457
+ codeBlocks: [],
458
+ };
459
+ }
460
+ catch {
461
+ return null;
462
+ }
463
+ }).filter((m) => m !== null && m.content.length > 0);
464
+ return {
465
+ id: summary.id,
466
+ index,
467
+ title: summary.title,
468
+ createdAt: summary.createdAt,
469
+ lastUpdatedAt: summary.lastUpdatedAt,
470
+ messageCount: messages.length,
471
+ messages,
472
+ workspaceId: 'global',
473
+ };
474
+ }
475
+ catch {
476
+ return null;
477
+ }
478
+ }
479
+ /**
480
+ * Format a tool call for display
481
+ */
482
+ function formatToolCall(toolData) {
483
+ const lines = [];
484
+ const toolName = toolData.name ?? 'unknown';
485
+ // Parse params
486
+ let params = {};
487
+ try {
488
+ params = JSON.parse(toolData.params ?? '{}');
489
+ }
490
+ catch {
491
+ // Ignore parse errors
492
+ }
493
+ // Helper to get string param
494
+ const getParam = (...keys) => {
495
+ for (const key of keys) {
496
+ const val = params[key];
497
+ if (typeof val === 'string' && val.trim())
498
+ return val;
499
+ }
500
+ return '';
501
+ };
502
+ // Format based on tool type
503
+ if (toolName === 'read_file') {
504
+ lines.push(`[Tool: Read File]`);
505
+ const file = getParam('targetFile', 'path', 'file');
506
+ if (file)
507
+ lines.push(`File: ${file}`);
508
+ // Show abbreviated content
509
+ try {
510
+ const result = JSON.parse(toolData.result ?? '{}');
511
+ if (result.contents) {
512
+ const preview = result.contents.slice(0, 300).replace(/\n/g, '\\n');
513
+ lines.push(`Content: ${preview}${result.contents.length > 300 ? '...' : ''}`);
514
+ }
515
+ }
516
+ catch {
517
+ // Ignore
518
+ }
519
+ }
520
+ else if (toolName === 'list_dir') {
521
+ lines.push(`[Tool: List Directory]`);
522
+ const dir = getParam('targetDirectory', 'path', 'directory');
523
+ if (dir)
524
+ lines.push(`Directory: ${dir}`);
525
+ }
526
+ else if (toolName === 'grep' || toolName === 'search' || toolName === 'codebase_search') {
527
+ lines.push(`[Tool: ${toolName === 'grep' ? 'Grep' : 'Search'}]`);
528
+ const pattern = getParam('pattern', 'query', 'searchQuery', 'regex');
529
+ const path = getParam('path', 'directory', 'targetDirectory');
530
+ if (pattern)
531
+ lines.push(`Pattern: ${pattern}`);
532
+ if (path)
533
+ lines.push(`Path: ${path}`);
534
+ }
535
+ else if (toolName === 'run_terminal_command' || toolName === 'execute_command') {
536
+ lines.push(`[Tool: Terminal Command]`);
537
+ const cmd = getParam('command', 'cmd');
538
+ if (cmd)
539
+ lines.push(`Command: ${cmd}`);
540
+ // Show command output from result
541
+ if (toolData.result) {
542
+ try {
543
+ const result = JSON.parse(toolData.result);
544
+ if (result.output && typeof result.output === 'string') {
545
+ const output = result.output.trim();
546
+ if (output) {
547
+ const preview = output.slice(0, 500);
548
+ lines.push(`Output: ${preview}${output.length > 500 ? '...' : ''}`);
549
+ }
550
+ }
551
+ }
552
+ catch {
553
+ // Ignore parse errors
554
+ }
555
+ }
556
+ }
557
+ else if (toolName === 'edit_file' || toolName === 'search_replace') {
558
+ lines.push(`[Tool: ${toolName === 'search_replace' ? 'Search & Replace' : 'Edit File'}]`);
559
+ const file = getParam('targetFile', 'path', 'file', 'filePath', 'relativeWorkspacePath');
560
+ if (file)
561
+ lines.push(`File: ${file}`);
562
+ // Show edit details
563
+ const oldString = getParam('oldString', 'old_string', 'search', 'searchString');
564
+ const newString = getParam('newString', 'new_string', 'replace', 'replaceString');
565
+ if (oldString || newString) {
566
+ if (oldString)
567
+ lines.push(`Old: ${oldString.slice(0, 100)}${oldString.length > 100 ? '...' : ''}`);
568
+ if (newString)
569
+ lines.push(`New: ${newString.slice(0, 100)}${newString.length > 100 ? '...' : ''}`);
570
+ }
571
+ }
572
+ else if (toolName === 'create_file' || toolName === 'write_file' || toolName === 'write') {
573
+ lines.push(`[Tool: ${toolName === 'create_file' ? 'Create File' : 'Write File'}]`);
574
+ const file = getParam('targetFile', 'path', 'file', 'relativeWorkspacePath');
575
+ if (file)
576
+ lines.push(`File: ${file}`);
577
+ // Note: Content is extracted from bubble's codeBlocks field in extractBubbleText(), not from params
578
+ }
579
+ else {
580
+ // Generic tool - show all string params
581
+ lines.push(`[Tool: ${toolName}]`);
582
+ for (const [key, val] of Object.entries(params)) {
583
+ if (typeof val === 'string' && val.trim()) {
584
+ const label = key.charAt(0).toUpperCase() + key.slice(1);
585
+ lines.push(`${label}: ${val.length > 100 ? val.slice(0, 100) + '...' : val}`);
586
+ }
587
+ }
588
+ // Try to extract result for generic tools
589
+ if (toolData.result) {
590
+ try {
591
+ const result = JSON.parse(toolData.result);
592
+ // Check for common result fields
593
+ const resultText = result.output || result.result || result.content || result.text;
594
+ if (resultText && typeof resultText === 'string' && resultText.trim()) {
595
+ const preview = resultText.slice(0, 500);
596
+ lines.push(`Result: ${preview}${resultText.length > 500 ? '...' : ''}`);
597
+ }
598
+ }
599
+ catch {
600
+ // If result is not JSON, show it directly if it's a string
601
+ if (typeof toolData.result === 'string' && toolData.result.length > 0 && toolData.result.length < 1000) {
602
+ lines.push(`Result: ${toolData.result}`);
603
+ }
604
+ }
605
+ }
606
+ }
607
+ // Add status indicator (for all tools)
608
+ if (toolData.status) {
609
+ const statusEmoji = toolData.status === 'completed' ? '✓' : '❌';
610
+ lines.push(`Status: ${statusEmoji} ${toolData.status}`);
611
+ }
612
+ // Add user decision if present (accepted/rejected/pending)
613
+ const userDecision = toolData.additionalData?.userDecision;
614
+ if (userDecision && typeof userDecision === 'string') {
615
+ const decisionEmoji = userDecision === 'accepted' ? '✓' : userDecision === 'rejected' ? '✗' : '⏳';
616
+ lines.push(`User Decision: ${decisionEmoji} ${userDecision}`);
617
+ }
618
+ return lines.join('\n');
619
+ }
620
+ /**
621
+ * Format a diff block for display
622
+ */
623
+ function formatDiffBlock(diffData) {
624
+ if (!diffData.chunks || !Array.isArray(diffData.chunks)) {
625
+ return null;
626
+ }
627
+ const lines = [];
628
+ for (const chunk of diffData.chunks) {
629
+ if (chunk.diffString && typeof chunk.diffString === 'string') {
630
+ // Show the full diff with fences
631
+ lines.push('```diff');
632
+ lines.push(chunk.diffString);
633
+ lines.push('```');
634
+ }
635
+ }
636
+ return lines.length > 0 ? lines.join('\n') : null;
637
+ }
638
+ /**
639
+ * Format tool call data that includes result with diff
640
+ */
641
+ function formatToolCallWithResult(toolData) {
642
+ const lines = [];
643
+ // Parse params to get file path first
644
+ let filePath = '';
645
+ if (toolData.params || toolData.rawArgs) {
646
+ try {
647
+ const params = JSON.parse(toolData.params ?? toolData.rawArgs ?? '{}');
648
+ filePath = params.relativeWorkspacePath ?? params.file_path ?? '';
649
+ }
650
+ catch {
651
+ // Ignore parse errors
652
+ }
653
+ }
654
+ // Parse the result for diff information
655
+ try {
656
+ const result = JSON.parse(toolData.result ?? '{}');
657
+ // Check if result has diff
658
+ if (result.diff && typeof result.diff === 'object') {
659
+ // Format as tool call header
660
+ const toolName = toolData.name ?? 'write';
661
+ lines.push(`[Tool: ${toolName === 'write' || toolName === 'write_file' ? 'Write File' : 'Edit File'}]`);
662
+ if (filePath) {
663
+ lines.push(`File: ${filePath}`);
664
+ }
665
+ // Add the diff blocks
666
+ const diffText = formatDiffBlock(result.diff);
667
+ if (diffText) {
668
+ lines.push('');
669
+ lines.push(diffText);
670
+ }
671
+ // Add result summary if available
672
+ if (result.resultForModel && typeof result.resultForModel === 'string') {
673
+ lines.push('');
674
+ lines.push(`Result: ${result.resultForModel}`);
675
+ }
676
+ }
677
+ }
678
+ catch {
679
+ // Not JSON or no diff
680
+ return null;
681
+ }
682
+ // Add status indicator
683
+ if (toolData.status) {
684
+ const statusEmoji = toolData.status === 'completed' ? '✓' : '❌';
685
+ lines.push('');
686
+ lines.push(`Status: ${statusEmoji} ${toolData.status}`);
687
+ }
688
+ // Add user decision if present
689
+ const userDecision = toolData.additionalData?.userDecision;
690
+ if (userDecision && typeof userDecision === 'string') {
691
+ const decisionEmoji = userDecision === 'accepted' ? '✓' : userDecision === 'rejected' ? '✗' : '⏳';
692
+ lines.push(`User Decision: ${decisionEmoji} ${userDecision}`);
693
+ }
694
+ return lines.length > 0 ? lines.join('\n') : null;
695
+ }
696
+ /**
697
+ * Extract thinking/reasoning text from bubble
698
+ */
699
+ function extractThinkingText(data) {
700
+ const thinking = data['thinking'];
701
+ if (thinking?.text && typeof thinking.text === 'string' && thinking.text.trim()) {
702
+ return thinking.text;
703
+ }
704
+ return null;
705
+ }
706
+ /**
707
+ * Extract text content from a bubble object
708
+ *
709
+ * Key insight from Cursor storage analysis:
710
+ * - `text` field contains the natural language explanation ("Based on my analysis...")
711
+ * - `codeBlocks[].content` contains code/mermaid artifacts
712
+ * - Both should be COMBINED, not one chosen over the other
713
+ *
714
+ * Priority for assistant messages:
715
+ * 1. text (main natural language) + codeBlocks (code artifacts) - COMBINED
716
+ * 2. thinking.text (reasoning)
717
+ * 3. toolFormerData.result (tool output)
718
+ *
719
+ * Priority for user messages:
720
+ * 1. codeBlocks (user-pasted code/content)
721
+ * 2. text, content, etc. (user typed message)
722
+ */
723
+ function extractBubbleText(data) {
724
+ const bubbleType = data['type'];
725
+ const isAssistant = bubbleType === 2;
726
+ // Check for tool call in toolFormerData (with name = tool action)
727
+ const toolFormerData = data['toolFormerData'];
728
+ // Check if it's an error - but don't return yet, mark it and continue extraction
729
+ const isError = toolFormerData?.additionalData?.status === 'error';
730
+ // Priority 1: Check if toolFormerData has result with diff (write/edit operations)
731
+ if (toolFormerData?.result) {
732
+ const toolResult = formatToolCallWithResult(toolFormerData);
733
+ if (toolResult) {
734
+ return toolResult;
735
+ }
736
+ }
737
+ // Priority 2: Check if it's a tool call with name (completed, cancelled, or error)
738
+ if (toolFormerData?.name) {
739
+ const toolInfo = formatToolCall(toolFormerData);
740
+ // Extract content from codeBlocks if available (for ANY tool type)
741
+ const codeBlocks = data['codeBlocks'];
742
+ if (codeBlocks && codeBlocks.length > 0 && codeBlocks[0]?.content) {
743
+ const content = codeBlocks[0].content;
744
+ const preview = content.slice(0, 200).replace(/\n/g, '\\n');
745
+ return toolInfo + `\nContent: ${preview}${content.length > 200 ? '...' : ''}`;
746
+ }
747
+ return toolInfo;
748
+ }
749
+ // Extract codeBlocks content
750
+ const codeBlocks = data['codeBlocks'];
751
+ const codeBlockParts = [];
752
+ if (codeBlocks && Array.isArray(codeBlocks)) {
753
+ for (const cb of codeBlocks) {
754
+ if (typeof cb.content === 'string' && cb.content.trim().length > 0) {
755
+ const lang = cb.languageId ?? '';
756
+ // Wrap code blocks in markdown fences for display
757
+ if (lang) {
758
+ codeBlockParts.push(`\`\`\`${lang}\n${cb.content}\n\`\`\``);
759
+ }
760
+ else {
761
+ codeBlockParts.push(cb.content);
762
+ }
763
+ }
764
+ }
765
+ }
766
+ // For ASSISTANT messages: prioritize `text` field (natural language), combine with codeBlocks
767
+ if (isAssistant) {
768
+ const textField = data['text'];
769
+ if (typeof textField === 'string' && textField.trim().length > 0) {
770
+ // Check if text is a JSON diff block (backup check if toolFormerData didn't catch it)
771
+ if (textField.trim().startsWith('{')) {
772
+ try {
773
+ const parsed = JSON.parse(textField);
774
+ // Check for diff structure
775
+ if (parsed.diff && typeof parsed.diff === 'object') {
776
+ const diffText = formatDiffBlock(parsed.diff);
777
+ if (diffText) {
778
+ // Add result message if available
779
+ if (parsed.resultForModel) {
780
+ return diffText + `\n\nResult: ${parsed.resultForModel}`;
781
+ }
782
+ return diffText;
783
+ }
784
+ }
785
+ }
786
+ catch {
787
+ // Not JSON, treat as regular text
788
+ }
789
+ }
790
+ // Regular text - combine with code artifacts
791
+ if (codeBlockParts.length > 0) {
792
+ return textField + '\n\n' + codeBlockParts.join('\n\n');
793
+ }
794
+ return textField;
795
+ }
796
+ // Fall back to thinking.text
797
+ const thinkingText = extractThinkingText(data);
798
+ if (thinkingText) {
799
+ if (codeBlockParts.length > 0) {
800
+ return `[Thinking]\n${thinkingText}\n\n` + codeBlockParts.join('\n\n');
801
+ }
802
+ return `[Thinking]\n${thinkingText}`;
803
+ }
804
+ // Fall back to toolFormerData.result
805
+ if (toolFormerData?.result) {
806
+ try {
807
+ const result = JSON.parse(toolFormerData.result);
808
+ if (result.contents && typeof result.contents === 'string') {
809
+ return result.contents;
810
+ }
811
+ if (result.content && typeof result.content === 'string') {
812
+ return result.content;
813
+ }
814
+ if (result.text && typeof result.text === 'string') {
815
+ return result.text;
816
+ }
817
+ }
818
+ catch {
819
+ if (toolFormerData.result.length > 50 && !toolFormerData.result.startsWith('{')) {
820
+ return toolFormerData.result;
821
+ }
822
+ }
823
+ }
824
+ // Fall back to codeBlocks alone
825
+ if (codeBlockParts.length > 0) {
826
+ return codeBlockParts.join('\n\n');
827
+ }
828
+ }
829
+ // For USER messages: codeBlocks first (user-pasted content), then text fields
830
+ if (codeBlockParts.length > 0) {
831
+ return codeBlockParts.join('\n\n');
832
+ }
833
+ // Common text fields
834
+ for (const key of ['text', 'content', 'finalText', 'message', 'markdown', 'textDescription']) {
835
+ const value = data[key];
836
+ if (typeof value === 'string' && value.trim().length > 0) {
837
+ return value;
838
+ }
839
+ }
840
+ // Fallback: thinking.text
841
+ const thinkingText = extractThinkingText(data);
842
+ if (thinkingText) {
843
+ return `[Thinking]\n${thinkingText}`;
844
+ }
845
+ // Last resort: find longest string with markdown features
846
+ let best = '';
847
+ const walk = (obj) => {
848
+ if (typeof obj === 'object' && obj !== null) {
849
+ if (Array.isArray(obj)) {
850
+ obj.forEach(walk);
851
+ }
852
+ else {
853
+ Object.values(obj).forEach(walk);
854
+ }
855
+ }
856
+ else if (typeof obj === 'string') {
857
+ if (obj.length > best.length && (obj.includes('\n') || obj.includes('```') || obj.includes('# '))) {
858
+ best = obj;
859
+ }
860
+ }
861
+ };
862
+ walk(data);
863
+ // If this was marked as an error, prefix with [Error] marker
864
+ if (isError && best) {
865
+ return `[Error]\n${best}`;
866
+ }
867
+ return best;
868
+ }
869
+ //# sourceMappingURL=storage.js.map