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.
- package/README.md +225 -0
- package/dist/cli/commands/export.d.ts +9 -0
- package/dist/cli/commands/export.d.ts.map +1 -0
- package/dist/cli/commands/export.js +126 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/list.d.ts +9 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +77 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/search.d.ts +9 -0
- package/dist/cli/commands/search.d.ts.map +1 -0
- package/dist/cli/commands/search.js +47 -0
- package/dist/cli/commands/search.js.map +1 -0
- package/dist/cli/commands/show.d.ts +9 -0
- package/dist/cli/commands/show.d.ts.map +1 -0
- package/dist/cli/commands/show.js +51 -0
- package/dist/cli/commands/show.js.map +1 -0
- package/dist/cli/formatters/index.d.ts +6 -0
- package/dist/cli/formatters/index.d.ts.map +1 -0
- package/dist/cli/formatters/index.js +6 -0
- package/dist/cli/formatters/index.js.map +1 -0
- package/dist/cli/formatters/json.d.ts +28 -0
- package/dist/cli/formatters/json.d.ts.map +1 -0
- package/dist/cli/formatters/json.js +98 -0
- package/dist/cli/formatters/json.js.map +1 -0
- package/dist/cli/formatters/table.d.ts +45 -0
- package/dist/cli/formatters/table.d.ts.map +1 -0
- package/dist/cli/formatters/table.js +439 -0
- package/dist/cli/formatters/table.js.map +1 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +51 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +8 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/parser.d.ts +38 -0
- package/dist/core/parser.d.ts.map +1 -0
- package/dist/core/parser.js +324 -0
- package/dist/core/parser.js.map +1 -0
- package/dist/core/storage.d.ts +45 -0
- package/dist/core/storage.d.ts.map +1 -0
- package/dist/core/storage.js +869 -0
- package/dist/core/storage.js.map +1 -0
- package/dist/core/types.d.ts +113 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +6 -0
- package/dist/core/types.js.map +1 -0
- package/dist/lib/errors.d.ts +56 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +90 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/platform.d.ts +25 -0
- package/dist/lib/platform.d.ts.map +1 -0
- package/dist/lib/platform.js +66 -0
- package/dist/lib/platform.js.map +1 -0
- 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
|