codeblog-mcp 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +178 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +29 -0
  4. package/dist/lib/analyzer.d.ts +2 -0
  5. package/dist/lib/analyzer.js +225 -0
  6. package/dist/lib/config.d.ts +15 -0
  7. package/dist/lib/config.js +32 -0
  8. package/dist/lib/fs-utils.d.ts +9 -0
  9. package/dist/lib/fs-utils.js +147 -0
  10. package/dist/lib/platform.d.ts +6 -0
  11. package/dist/lib/platform.js +50 -0
  12. package/dist/lib/registry.d.ts +14 -0
  13. package/dist/lib/registry.js +69 -0
  14. package/dist/lib/types.d.ts +47 -0
  15. package/dist/lib/types.js +1 -0
  16. package/dist/scanners/aider.d.ts +2 -0
  17. package/dist/scanners/aider.js +132 -0
  18. package/dist/scanners/claude-code.d.ts +2 -0
  19. package/dist/scanners/claude-code.js +193 -0
  20. package/dist/scanners/codex.d.ts +2 -0
  21. package/dist/scanners/codex.js +143 -0
  22. package/dist/scanners/continue-dev.d.ts +2 -0
  23. package/dist/scanners/continue-dev.js +136 -0
  24. package/dist/scanners/cursor.d.ts +2 -0
  25. package/dist/scanners/cursor.js +447 -0
  26. package/dist/scanners/index.d.ts +1 -0
  27. package/dist/scanners/index.js +22 -0
  28. package/dist/scanners/vscode-copilot.d.ts +2 -0
  29. package/dist/scanners/vscode-copilot.js +179 -0
  30. package/dist/scanners/warp.d.ts +2 -0
  31. package/dist/scanners/warp.js +20 -0
  32. package/dist/scanners/windsurf.d.ts +2 -0
  33. package/dist/scanners/windsurf.js +197 -0
  34. package/dist/scanners/zed.d.ts +2 -0
  35. package/dist/scanners/zed.js +121 -0
  36. package/dist/tools/forum.d.ts +2 -0
  37. package/dist/tools/forum.js +292 -0
  38. package/dist/tools/posting.d.ts +2 -0
  39. package/dist/tools/posting.js +195 -0
  40. package/dist/tools/sessions.d.ts +2 -0
  41. package/dist/tools/sessions.js +95 -0
  42. package/dist/tools/setup.d.ts +2 -0
  43. package/dist/tools/setup.js +118 -0
  44. package/package.json +48 -0
@@ -0,0 +1,136 @@
1
+ import * as path from "path";
2
+ import * as fs from "fs";
3
+ import { getHome, getPlatform } from "../lib/platform.js";
4
+ import { listFiles, safeReadJson, safeStats } from "../lib/fs-utils.js";
5
+ // Continue.dev stores sessions in:
6
+ // ~/.continue/sessions/*.json
7
+ // macOS: ~/Library/Application Support/Continue/sessions/
8
+ // Windows: %APPDATA%/Continue/sessions/
9
+ // Linux: ~/.config/continue/sessions/
10
+ export const continueDevScanner = {
11
+ name: "Continue.dev",
12
+ sourceType: "continue",
13
+ description: "Continue.dev AI coding assistant sessions",
14
+ getSessionDirs() {
15
+ const home = getHome();
16
+ const platform = getPlatform();
17
+ const candidates = [];
18
+ candidates.push(path.join(home, ".continue", "sessions"));
19
+ if (platform === "macos") {
20
+ candidates.push(path.join(home, "Library", "Application Support", "Continue", "sessions"));
21
+ }
22
+ else if (platform === "windows") {
23
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
24
+ candidates.push(path.join(appData, "Continue", "sessions"));
25
+ }
26
+ else {
27
+ candidates.push(path.join(home, ".config", "continue", "sessions"));
28
+ }
29
+ return candidates.filter((d) => {
30
+ try {
31
+ return fs.existsSync(d);
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ });
37
+ },
38
+ scan(limit) {
39
+ const sessions = [];
40
+ const dirs = this.getSessionDirs();
41
+ for (const dir of dirs) {
42
+ const jsonFiles = listFiles(dir, [".json"]);
43
+ for (const filePath of jsonFiles) {
44
+ const stats = safeStats(filePath);
45
+ if (!stats || stats.size < 100)
46
+ continue;
47
+ const data = safeReadJson(filePath);
48
+ if (!data)
49
+ continue;
50
+ const turns = extractContinueTurns(data);
51
+ if (turns.length < 2)
52
+ continue;
53
+ const humanMsgs = turns.filter((t) => t.role === "human");
54
+ const preview = humanMsgs[0]?.content.slice(0, 200) || "(continue session)";
55
+ sessions.push({
56
+ id: path.basename(filePath, ".json"),
57
+ source: "continue",
58
+ project: data.workspacePath || path.basename(path.dirname(filePath)),
59
+ title: data.title || preview.slice(0, 80),
60
+ messageCount: turns.length,
61
+ humanMessages: humanMsgs.length,
62
+ aiMessages: turns.length - humanMsgs.length,
63
+ preview,
64
+ filePath,
65
+ modifiedAt: stats.mtime,
66
+ sizeBytes: stats.size,
67
+ });
68
+ }
69
+ }
70
+ sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
71
+ return sessions.slice(0, limit);
72
+ },
73
+ parse(filePath, maxTurns) {
74
+ const data = safeReadJson(filePath);
75
+ if (!data)
76
+ return null;
77
+ const stats = safeStats(filePath);
78
+ const turns = extractContinueTurns(data, maxTurns);
79
+ if (turns.length === 0)
80
+ return null;
81
+ const humanMsgs = turns.filter((t) => t.role === "human");
82
+ const aiMsgs = turns.filter((t) => t.role === "assistant");
83
+ return {
84
+ id: path.basename(filePath, ".json"),
85
+ source: "continue",
86
+ project: data.workspacePath || path.basename(path.dirname(filePath)),
87
+ title: data.title || humanMsgs[0]?.content.slice(0, 80) || "Continue session",
88
+ messageCount: turns.length,
89
+ humanMessages: humanMsgs.length,
90
+ aiMessages: aiMsgs.length,
91
+ preview: humanMsgs[0]?.content.slice(0, 200) || "",
92
+ filePath,
93
+ modifiedAt: stats?.mtime || new Date(),
94
+ sizeBytes: stats?.size || 0,
95
+ turns,
96
+ };
97
+ },
98
+ };
99
+ function extractContinueTurns(data, maxTurns) {
100
+ const turns = [];
101
+ // Continue format: { history: [{ role: "user"|"assistant", content: "..." }] }
102
+ const msgArrays = [data.history, data.messages, data.steps];
103
+ for (const arr of msgArrays) {
104
+ if (!Array.isArray(arr))
105
+ continue;
106
+ for (const msg of arr) {
107
+ if (maxTurns && turns.length >= maxTurns)
108
+ break;
109
+ if (!msg || typeof msg !== "object")
110
+ continue;
111
+ const m = msg;
112
+ // Steps format: { name: "UserInput", description: "..." }
113
+ if (m.name === "UserInput" && typeof m.description === "string") {
114
+ turns.push({ role: "human", content: m.description });
115
+ continue;
116
+ }
117
+ if (m.name === "DefaultModelEditCodeStep" || m.name === "ChatModelResponse") {
118
+ if (typeof m.description === "string") {
119
+ turns.push({ role: "assistant", content: m.description });
120
+ }
121
+ continue;
122
+ }
123
+ // Standard message format
124
+ const content = (m.content || m.text || m.message);
125
+ if (typeof content !== "string")
126
+ continue;
127
+ turns.push({
128
+ role: m.role === "user" ? "human" : "assistant",
129
+ content,
130
+ });
131
+ }
132
+ if (turns.length > 0)
133
+ return turns;
134
+ }
135
+ return turns;
136
+ }
@@ -0,0 +1,2 @@
1
+ import type { Scanner } from "../lib/types.js";
2
+ export declare const cursorScanner: Scanner;
@@ -0,0 +1,447 @@
1
+ import * as path from "path";
2
+ import * as fs from "fs";
3
+ import BetterSqlite3 from "better-sqlite3";
4
+ import { getHome, getPlatform } from "../lib/platform.js";
5
+ import { listFiles, listDirs, safeReadFile, safeReadJson, safeStats, extractProjectDescription } from "../lib/fs-utils.js";
6
+ // Cursor stores conversations in THREE places (all supported for version compatibility):
7
+ //
8
+ // FORMAT 1 — Agent transcripts (plain text, XML-like tags):
9
+ // ~/.cursor/projects/<project>/agent-transcripts/*.txt
10
+ // Format: user: <user_query>...</user_query> \n A: <response>
11
+ //
12
+ // FORMAT 2 — Chat sessions (JSON, older Cursor versions):
13
+ // ~/Library/Application Support/Cursor/User/workspaceStorage/<hash>/chatSessions/*.json
14
+ // Format: { requests: [{ message: "...", response: [...] }], sessionId, creationDate }
15
+ //
16
+ // FORMAT 3 — Global SQLite (newer Cursor versions, 2025+):
17
+ // ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb
18
+ // Table: cursorDiskKV
19
+ // Keys: composerData:<composerId> — session metadata (name, timestamps, bubble headers)
20
+ // bubbleId:<composerId>:<bubbleId> — individual message content (type 1=user, 2=ai)
21
+ // Run a callback with a shared DB connection, safely closing on completion
22
+ function withDb(dbPath, fn, fallback) {
23
+ try {
24
+ const db = new BetterSqlite3(dbPath, { readonly: true, fileMustExist: true });
25
+ try {
26
+ return fn(db);
27
+ }
28
+ finally {
29
+ db.close();
30
+ }
31
+ }
32
+ catch (err) {
33
+ console.error(`[codeblog] Cursor DB error:`, err instanceof Error ? err.message : err);
34
+ return fallback;
35
+ }
36
+ }
37
+ // Safe parameterized query helper
38
+ function safeQueryDb(db, sql, params = []) {
39
+ try {
40
+ return db.prepare(sql).all(...params);
41
+ }
42
+ catch (err) {
43
+ console.error(`[codeblog] Cursor query error:`, err instanceof Error ? err.message : err);
44
+ return [];
45
+ }
46
+ }
47
+ // Parse vscdb virtual path: "vscdb:<dbPath>|<composerId>"
48
+ // Uses '|' as separator to avoid conflicts with ':' in Windows paths (C:\...)
49
+ const VSCDB_SEP = "|";
50
+ function makeVscdbPath(dbPath, composerId) {
51
+ return `vscdb:${dbPath}${VSCDB_SEP}${composerId}`;
52
+ }
53
+ function parseVscdbVirtualPath(virtualPath) {
54
+ const prefix = "vscdb:";
55
+ if (!virtualPath.startsWith(prefix))
56
+ return null;
57
+ const rest = virtualPath.slice(prefix.length);
58
+ const sepIdx = rest.lastIndexOf(VSCDB_SEP);
59
+ if (sepIdx <= 0)
60
+ return null;
61
+ return { dbPath: rest.slice(0, sepIdx), composerId: rest.slice(sepIdx + 1) };
62
+ }
63
+ function getGlobalStoragePath() {
64
+ const home = getHome();
65
+ const platform = getPlatform();
66
+ let p;
67
+ if (platform === "macos") {
68
+ p = path.join(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
69
+ }
70
+ else if (platform === "windows") {
71
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
72
+ p = path.join(appData, "Cursor", "User", "globalStorage", "state.vscdb");
73
+ }
74
+ else {
75
+ p = path.join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb");
76
+ }
77
+ try {
78
+ return fs.existsSync(p) ? p : null;
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ export const cursorScanner = {
85
+ name: "Cursor",
86
+ sourceType: "cursor",
87
+ description: "Cursor AI IDE sessions (agent transcripts + chat sessions + composer)",
88
+ getSessionDirs() {
89
+ const home = getHome();
90
+ const platform = getPlatform();
91
+ const candidates = [];
92
+ // Format 1: Agent transcripts
93
+ candidates.push(path.join(home, ".cursor", "projects"));
94
+ // Format 2 & workspace-level Format 3: workspaceStorage
95
+ if (platform === "macos") {
96
+ candidates.push(path.join(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage"));
97
+ }
98
+ else if (platform === "windows") {
99
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
100
+ candidates.push(path.join(appData, "Cursor", "User", "workspaceStorage"));
101
+ }
102
+ else {
103
+ candidates.push(path.join(home, ".config", "Cursor", "User", "workspaceStorage"));
104
+ }
105
+ // Format 3: globalStorage (just check existence for status reporting)
106
+ const globalDb = getGlobalStoragePath();
107
+ if (globalDb) {
108
+ candidates.push(path.dirname(globalDb));
109
+ }
110
+ return candidates.filter((d) => {
111
+ try {
112
+ return fs.existsSync(d);
113
+ }
114
+ catch {
115
+ return false;
116
+ }
117
+ });
118
+ },
119
+ scan(limit) {
120
+ const sessions = [];
121
+ const dirs = this.getSessionDirs();
122
+ const seenIds = new Set();
123
+ for (const baseDir of dirs) {
124
+ // Skip globalStorage dir — handled separately via Format 3
125
+ if (baseDir.endsWith("globalStorage"))
126
+ continue;
127
+ const projectDirs = listDirs(baseDir);
128
+ for (const projectDir of projectDirs) {
129
+ const dirName = path.basename(projectDir);
130
+ let projectPath;
131
+ const workspaceJson = safeReadJson(path.join(projectDir, "workspace.json"));
132
+ if (workspaceJson?.folder) {
133
+ try {
134
+ projectPath = decodeURIComponent(new URL(workspaceJson.folder).pathname);
135
+ }
136
+ catch { /* */ }
137
+ }
138
+ if (!projectPath && dirName.startsWith("Users-")) {
139
+ projectPath = decodeDirNameToPath(dirName) || undefined;
140
+ }
141
+ const project = projectPath ? path.basename(projectPath) : dirName;
142
+ const projectDescription = projectPath ? extractProjectDescription(projectPath) || undefined : undefined;
143
+ // --- FORMAT 1: agent-transcripts/*.txt ---
144
+ const transcriptsDir = path.join(projectDir, "agent-transcripts");
145
+ for (const filePath of listFiles(transcriptsDir, [".txt"])) {
146
+ const stats = safeStats(filePath);
147
+ if (!stats)
148
+ continue;
149
+ const content = safeReadFile(filePath);
150
+ if (!content || content.length < 100)
151
+ continue;
152
+ const userQueries = content.match(/<user_query>\n?([\s\S]*?)\n?<\/user_query>/g) || [];
153
+ if (userQueries.length === 0)
154
+ continue;
155
+ const firstQuery = content.match(/<user_query>\n?([\s\S]*?)\n?<\/user_query>/);
156
+ const preview = firstQuery ? firstQuery[1].trim().slice(0, 200) : content.slice(0, 200);
157
+ const id = path.basename(filePath, ".txt");
158
+ seenIds.add(id);
159
+ sessions.push({
160
+ id,
161
+ source: "cursor",
162
+ project,
163
+ projectPath,
164
+ projectDescription,
165
+ title: preview.slice(0, 80) || `Cursor session in ${project}`,
166
+ messageCount: userQueries.length * 2,
167
+ humanMessages: userQueries.length,
168
+ aiMessages: userQueries.length,
169
+ preview,
170
+ filePath,
171
+ modifiedAt: stats.mtime,
172
+ sizeBytes: stats.size,
173
+ });
174
+ }
175
+ // --- FORMAT 2: chatSessions/*.json ---
176
+ for (const filePath of listFiles(path.join(projectDir, "chatSessions"), [".json"])) {
177
+ const stats = safeStats(filePath);
178
+ if (!stats || stats.size < 100)
179
+ continue;
180
+ const data = safeReadJson(filePath);
181
+ if (!data || !Array.isArray(data.requests) || data.requests.length === 0)
182
+ continue;
183
+ const humanCount = data.requests.length;
184
+ const firstMsg = data.requests[0]?.message || "";
185
+ const preview = (typeof firstMsg === "string" ? firstMsg : "").slice(0, 200);
186
+ const id = data.sessionId || path.basename(filePath, ".json");
187
+ seenIds.add(id);
188
+ sessions.push({
189
+ id,
190
+ source: "cursor",
191
+ project,
192
+ projectPath,
193
+ projectDescription,
194
+ title: preview.slice(0, 80) || `Cursor chat in ${project}`,
195
+ messageCount: humanCount * 2,
196
+ humanMessages: humanCount,
197
+ aiMessages: humanCount,
198
+ preview: preview || "(cursor chat session)",
199
+ filePath,
200
+ modifiedAt: stats.mtime,
201
+ sizeBytes: stats.size,
202
+ });
203
+ }
204
+ }
205
+ }
206
+ // --- FORMAT 3: globalStorage state.vscdb (newer Cursor) ---
207
+ // This supplements Formats 1 & 2 — adds any sessions not already found
208
+ const globalDb = getGlobalStoragePath();
209
+ if (globalDb) {
210
+ withDb(globalDb, (db) => {
211
+ const rows = safeQueryDb(db, "SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'");
212
+ for (const row of rows) {
213
+ try {
214
+ const data = JSON.parse(row.value);
215
+ const composerId = data.composerId || row.key.replace("composerData:", "");
216
+ if (seenIds.has(composerId))
217
+ continue;
218
+ const bubbleHeaders = data.fullConversationHeadersOnly || [];
219
+ if (bubbleHeaders.length === 0)
220
+ continue;
221
+ const humanCount = bubbleHeaders.filter((b) => b.type === 1).length;
222
+ const aiCount = bubbleHeaders.filter((b) => b.type === 2).length;
223
+ const name = data.name || "";
224
+ // Get first user message as preview
225
+ let preview = name;
226
+ if (!preview) {
227
+ const firstUserBubble = bubbleHeaders.find((b) => b.type === 1);
228
+ if (firstUserBubble) {
229
+ const bubbleRow = safeQueryDb(db, "SELECT value FROM cursorDiskKV WHERE key = ?", [`bubbleId:${composerId}:${firstUserBubble.bubbleId}`]);
230
+ if (bubbleRow.length > 0) {
231
+ try {
232
+ const bubble = JSON.parse(bubbleRow[0].value);
233
+ preview = (bubble.text || bubble.message || "").slice(0, 200);
234
+ }
235
+ catch { /* */ }
236
+ }
237
+ }
238
+ }
239
+ const createdAt = data.createdAt ? new Date(data.createdAt) : new Date();
240
+ const updatedAt = data.lastUpdatedAt ? new Date(data.lastUpdatedAt) : createdAt;
241
+ sessions.push({
242
+ id: composerId,
243
+ source: "cursor",
244
+ project: "Cursor Composer",
245
+ title: (name || preview || "Cursor composer session").slice(0, 80),
246
+ messageCount: humanCount + aiCount,
247
+ humanMessages: humanCount,
248
+ aiMessages: aiCount,
249
+ preview: preview || "(composer session)",
250
+ filePath: makeVscdbPath(globalDb, composerId),
251
+ modifiedAt: updatedAt,
252
+ sizeBytes: row.value.length,
253
+ });
254
+ }
255
+ catch { /* skip malformed entries */ }
256
+ }
257
+ }, undefined);
258
+ }
259
+ sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
260
+ return sessions.slice(0, limit);
261
+ },
262
+ parse(filePath, maxTurns) {
263
+ // FORMAT 3: vscdb virtual path
264
+ if (filePath.startsWith("vscdb:")) {
265
+ return parseVscdbSession(filePath, maxTurns);
266
+ }
267
+ const stats = safeStats(filePath);
268
+ const turns = [];
269
+ if (filePath.endsWith(".txt")) {
270
+ // FORMAT 1: agent transcript
271
+ const content = safeReadFile(filePath);
272
+ if (!content)
273
+ return null;
274
+ const blocks = content.split(/^user:\s*$/m);
275
+ for (const block of blocks) {
276
+ if (!block.trim())
277
+ continue;
278
+ if (maxTurns && turns.length >= maxTurns)
279
+ break;
280
+ const queryMatch = block.match(/<user_query>\n?([\s\S]*?)\n?<\/user_query>/);
281
+ if (queryMatch) {
282
+ turns.push({ role: "human", content: queryMatch[1].trim() });
283
+ }
284
+ const afterQuery = block.split(/<\/user_query>/)[1];
285
+ if (afterQuery) {
286
+ const aiContent = afterQuery.replace(/^\s*\n\s*A:\s*\n?/, "").trim();
287
+ if (aiContent && (!maxTurns || turns.length < maxTurns)) {
288
+ turns.push({ role: "assistant", content: aiContent });
289
+ }
290
+ }
291
+ }
292
+ }
293
+ else {
294
+ // FORMAT 2: chatSessions JSON
295
+ const data = safeReadJson(filePath);
296
+ if (!data || !Array.isArray(data.requests))
297
+ return null;
298
+ for (const req of data.requests) {
299
+ if (maxTurns && turns.length >= maxTurns)
300
+ break;
301
+ if (req.message) {
302
+ turns.push({
303
+ role: "human",
304
+ content: typeof req.message === "string" ? req.message : JSON.stringify(req.message),
305
+ });
306
+ }
307
+ if (maxTurns && turns.length >= maxTurns)
308
+ break;
309
+ if (req.response) {
310
+ let respText = "";
311
+ if (typeof req.response === "string") {
312
+ respText = req.response;
313
+ }
314
+ else if (Array.isArray(req.response)) {
315
+ respText = req.response
316
+ .map((r) => (typeof r === "string" ? r : r?.text || ""))
317
+ .join("");
318
+ }
319
+ if (respText.trim()) {
320
+ turns.push({ role: "assistant", content: respText.trim() });
321
+ }
322
+ }
323
+ }
324
+ }
325
+ if (turns.length === 0)
326
+ return null;
327
+ const humanMsgs = turns.filter((t) => t.role === "human");
328
+ const aiMsgs = turns.filter((t) => t.role === "assistant");
329
+ return {
330
+ id: path.basename(filePath).replace(/\.\w+$/, ""),
331
+ source: "cursor",
332
+ project: path.basename(path.dirname(filePath)),
333
+ title: humanMsgs[0]?.content.slice(0, 80) || "Cursor session",
334
+ messageCount: turns.length,
335
+ humanMessages: humanMsgs.length,
336
+ aiMessages: aiMsgs.length,
337
+ preview: humanMsgs[0]?.content.slice(0, 200) || "",
338
+ filePath,
339
+ modifiedAt: stats?.mtime || new Date(),
340
+ sizeBytes: stats?.size || 0,
341
+ turns,
342
+ };
343
+ },
344
+ };
345
+ // Parse a session stored in globalStorage state.vscdb (Format 3)
346
+ function parseVscdbSession(virtualPath, maxTurns) {
347
+ const parsed = parseVscdbVirtualPath(virtualPath);
348
+ if (!parsed)
349
+ return null;
350
+ const { dbPath, composerId } = parsed;
351
+ return withDb(dbPath, (db) => {
352
+ // Get composer metadata
353
+ const metaRows = safeQueryDb(db, "SELECT value FROM cursorDiskKV WHERE key = ?", [`composerData:${composerId}`]);
354
+ if (metaRows.length === 0)
355
+ return null;
356
+ let composerData;
357
+ try {
358
+ composerData = JSON.parse(metaRows[0].value);
359
+ }
360
+ catch {
361
+ return null;
362
+ }
363
+ const bubbleHeaders = composerData.fullConversationHeadersOnly || [];
364
+ if (bubbleHeaders.length === 0)
365
+ return null;
366
+ // Fetch bubble contents — single DB connection reused for all queries
367
+ const turns = [];
368
+ for (const header of bubbleHeaders) {
369
+ if (maxTurns && turns.length >= maxTurns)
370
+ break;
371
+ const bubbleRows = safeQueryDb(db, "SELECT value FROM cursorDiskKV WHERE key = ?", [`bubbleId:${composerId}:${header.bubbleId}`]);
372
+ if (bubbleRows.length === 0)
373
+ continue;
374
+ try {
375
+ const bubble = JSON.parse(bubbleRows[0].value);
376
+ const text = bubble.text || bubble.message || bubble.rawText || "";
377
+ if (!text && header.type === 2) {
378
+ turns.push({ role: "assistant", content: "(AI response)" });
379
+ continue;
380
+ }
381
+ turns.push({
382
+ role: header.type === 1 ? "human" : "assistant",
383
+ content: text || "(empty)",
384
+ });
385
+ }
386
+ catch { /* skip */ }
387
+ }
388
+ if (turns.length === 0)
389
+ return null;
390
+ const humanMsgs = turns.filter((t) => t.role === "human");
391
+ const aiMsgs = turns.filter((t) => t.role === "assistant");
392
+ return {
393
+ id: composerId,
394
+ source: "cursor",
395
+ project: "Cursor Composer",
396
+ title: composerData.name || humanMsgs[0]?.content.slice(0, 80) || "Cursor session",
397
+ messageCount: turns.length,
398
+ humanMessages: humanMsgs.length,
399
+ aiMessages: aiMsgs.length,
400
+ preview: humanMsgs[0]?.content.slice(0, 200) || "",
401
+ filePath: virtualPath,
402
+ modifiedAt: composerData.lastUpdatedAt ? new Date(composerData.lastUpdatedAt) : new Date(),
403
+ sizeBytes: 0,
404
+ turns,
405
+ };
406
+ }, null);
407
+ }
408
+ // Decode a directory name like "Users-zhaoyifei-my-cool-project" back to a real path.
409
+ // On Windows, names look like "c-Users-PC-project" → "C:\Users\PC\project".
410
+ // Greedy strategy: try longest segments first, check if path exists on disk.
411
+ function decodeDirNameToPath(dirName) {
412
+ const platform = getPlatform();
413
+ const stripped = dirName.startsWith("-") ? dirName.slice(1) : dirName;
414
+ const parts = stripped.split("-");
415
+ let currentPath = "";
416
+ let i = 0;
417
+ // On Windows, the first part may be a drive letter (e.g. "c" → "C:")
418
+ if (platform === "windows" && parts.length > 0 && /^[a-zA-Z]$/.test(parts[0])) {
419
+ currentPath = parts[0].toUpperCase() + ":";
420
+ i = 1;
421
+ }
422
+ while (i < parts.length) {
423
+ let bestMatch = "";
424
+ let bestLen = 0;
425
+ for (let end = parts.length; end > i; end--) {
426
+ const segment = parts.slice(i, end).join("-");
427
+ const candidate = currentPath + path.sep + segment;
428
+ try {
429
+ if (fs.existsSync(candidate)) {
430
+ bestMatch = candidate;
431
+ bestLen = end - i;
432
+ break;
433
+ }
434
+ }
435
+ catch { /* ignore */ }
436
+ }
437
+ if (bestLen > 0) {
438
+ currentPath = bestMatch;
439
+ i += bestLen;
440
+ }
441
+ else {
442
+ currentPath += path.sep + parts[i];
443
+ i++;
444
+ }
445
+ }
446
+ return currentPath || null;
447
+ }
@@ -0,0 +1 @@
1
+ export declare function registerAllScanners(): void;
@@ -0,0 +1,22 @@
1
+ import { registerScanner } from "../lib/registry.js";
2
+ import { claudeCodeScanner } from "./claude-code.js";
3
+ import { cursorScanner } from "./cursor.js";
4
+ import { windsurfScanner } from "./windsurf.js";
5
+ import { codexScanner } from "./codex.js";
6
+ import { warpScanner } from "./warp.js";
7
+ import { vscodeCopilotScanner } from "./vscode-copilot.js";
8
+ import { aiderScanner } from "./aider.js";
9
+ import { continueDevScanner } from "./continue-dev.js";
10
+ import { zedScanner } from "./zed.js";
11
+ // Register all scanners
12
+ export function registerAllScanners() {
13
+ registerScanner(claudeCodeScanner);
14
+ registerScanner(cursorScanner);
15
+ registerScanner(windsurfScanner);
16
+ registerScanner(codexScanner);
17
+ registerScanner(warpScanner);
18
+ registerScanner(vscodeCopilotScanner);
19
+ registerScanner(aiderScanner);
20
+ registerScanner(continueDevScanner);
21
+ registerScanner(zedScanner);
22
+ }
@@ -0,0 +1,2 @@
1
+ import type { Scanner } from "../lib/types.js";
2
+ export declare const vscodeCopilotScanner: Scanner;