codeblog-mcp 1.7.2 → 2.1.2
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/dist/scanners/cursor.js +157 -63
- package/package.json +1 -1
package/dist/scanners/cursor.js
CHANGED
|
@@ -120,8 +120,11 @@ export const cursorScanner = {
|
|
|
120
120
|
const sessions = [];
|
|
121
121
|
const dirs = this.getSessionDirs();
|
|
122
122
|
const seenIds = new Set();
|
|
123
|
+
// --- Collect project metadata from Format 1/2 directories ---
|
|
124
|
+
// We need project info (name, path, description) from workspace dirs,
|
|
125
|
+
// because Format 3 (globalStorage) doesn't store project context.
|
|
126
|
+
const projectInfoById = new Map();
|
|
123
127
|
for (const baseDir of dirs) {
|
|
124
|
-
// Skip globalStorage dir — handled separately via Format 3
|
|
125
128
|
if (baseDir.endsWith("globalStorage"))
|
|
126
129
|
continue;
|
|
127
130
|
const projectDirs = listDirs(baseDir);
|
|
@@ -140,9 +143,100 @@ export const cursorScanner = {
|
|
|
140
143
|
}
|
|
141
144
|
const project = projectPath ? path.basename(projectPath) : dirName;
|
|
142
145
|
const projectDescription = projectPath ? extractProjectDescription(projectPath) || undefined : undefined;
|
|
143
|
-
//
|
|
146
|
+
// Map transcript IDs to project info
|
|
144
147
|
const transcriptsDir = path.join(projectDir, "agent-transcripts");
|
|
145
148
|
for (const filePath of listFiles(transcriptsDir, [".txt"])) {
|
|
149
|
+
const id = path.basename(filePath, ".txt");
|
|
150
|
+
projectInfoById.set(id, { project, projectPath, projectDescription });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// --- FORMAT 3: globalStorage state.vscdb (primary, most complete) ---
|
|
155
|
+
// Format 3 has the richest data: full bubble contents including tool calls,
|
|
156
|
+
// thinking blocks, and code suggestions. Always prefer this over Format 1/2.
|
|
157
|
+
const globalDb = getGlobalStoragePath();
|
|
158
|
+
if (globalDb) {
|
|
159
|
+
withDb(globalDb, (db) => {
|
|
160
|
+
const rows = safeQueryDb(db, "SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'");
|
|
161
|
+
for (const row of rows) {
|
|
162
|
+
try {
|
|
163
|
+
const data = JSON.parse(row.value);
|
|
164
|
+
const composerId = data.composerId || row.key.replace("composerData:", "");
|
|
165
|
+
const bubbleHeaders = data.fullConversationHeadersOnly || [];
|
|
166
|
+
if (bubbleHeaders.length === 0)
|
|
167
|
+
continue;
|
|
168
|
+
const humanCount = bubbleHeaders.filter((b) => b.type === 1).length;
|
|
169
|
+
const aiCount = bubbleHeaders.filter((b) => b.type === 2).length;
|
|
170
|
+
const name = data.name || "";
|
|
171
|
+
// Get first user message as preview
|
|
172
|
+
let preview = name;
|
|
173
|
+
if (!preview) {
|
|
174
|
+
const firstUserBubble = bubbleHeaders.find((b) => b.type === 1);
|
|
175
|
+
if (firstUserBubble) {
|
|
176
|
+
const bubbleRow = safeQueryDb(db, "SELECT value FROM cursorDiskKV WHERE key = ?", [`bubbleId:${composerId}:${firstUserBubble.bubbleId}`]);
|
|
177
|
+
if (bubbleRow.length > 0) {
|
|
178
|
+
try {
|
|
179
|
+
const bubble = JSON.parse(bubbleRow[0].value);
|
|
180
|
+
preview = extractBubbleContent(bubble).slice(0, 200);
|
|
181
|
+
}
|
|
182
|
+
catch { /* */ }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Enrich with project info from Format 1 directories
|
|
187
|
+
const projInfo = projectInfoById.get(composerId);
|
|
188
|
+
const project = projInfo?.project || "Cursor Composer";
|
|
189
|
+
const projectPath = projInfo?.projectPath;
|
|
190
|
+
const projectDescription = projInfo?.projectDescription;
|
|
191
|
+
const createdAt = data.createdAt ? new Date(data.createdAt) : new Date();
|
|
192
|
+
const updatedAt = data.lastUpdatedAt ? new Date(data.lastUpdatedAt) : createdAt;
|
|
193
|
+
seenIds.add(composerId);
|
|
194
|
+
sessions.push({
|
|
195
|
+
id: composerId,
|
|
196
|
+
source: "cursor",
|
|
197
|
+
project,
|
|
198
|
+
projectPath,
|
|
199
|
+
projectDescription,
|
|
200
|
+
title: (name || preview || "Cursor composer session").slice(0, 80),
|
|
201
|
+
messageCount: humanCount + aiCount,
|
|
202
|
+
humanMessages: humanCount,
|
|
203
|
+
aiMessages: aiCount,
|
|
204
|
+
preview: preview || "(composer session)",
|
|
205
|
+
filePath: makeVscdbPath(globalDb, composerId),
|
|
206
|
+
modifiedAt: updatedAt,
|
|
207
|
+
sizeBytes: row.value.length,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch { /* skip malformed entries */ }
|
|
211
|
+
}
|
|
212
|
+
}, undefined);
|
|
213
|
+
}
|
|
214
|
+
// --- FORMAT 1 & 2: supplement with sessions not found in Format 3 ---
|
|
215
|
+
for (const baseDir of dirs) {
|
|
216
|
+
if (baseDir.endsWith("globalStorage"))
|
|
217
|
+
continue;
|
|
218
|
+
const projectDirs = listDirs(baseDir);
|
|
219
|
+
for (const projectDir of projectDirs) {
|
|
220
|
+
const dirName = path.basename(projectDir);
|
|
221
|
+
let projectPath;
|
|
222
|
+
const workspaceJson = safeReadJson(path.join(projectDir, "workspace.json"));
|
|
223
|
+
if (workspaceJson?.folder) {
|
|
224
|
+
try {
|
|
225
|
+
projectPath = decodeURIComponent(new URL(workspaceJson.folder).pathname);
|
|
226
|
+
}
|
|
227
|
+
catch { /* */ }
|
|
228
|
+
}
|
|
229
|
+
if (!projectPath && dirName.startsWith("Users-")) {
|
|
230
|
+
projectPath = decodeDirNameToPath(dirName) || undefined;
|
|
231
|
+
}
|
|
232
|
+
const project = projectPath ? path.basename(projectPath) : dirName;
|
|
233
|
+
const projectDescription = projectPath ? extractProjectDescription(projectPath) || undefined : undefined;
|
|
234
|
+
// --- FORMAT 1: agent-transcripts/*.txt (only if not in Format 3) ---
|
|
235
|
+
const transcriptsDir = path.join(projectDir, "agent-transcripts");
|
|
236
|
+
for (const filePath of listFiles(transcriptsDir, [".txt"])) {
|
|
237
|
+
const id = path.basename(filePath, ".txt");
|
|
238
|
+
if (seenIds.has(id))
|
|
239
|
+
continue; // Already have richer Format 3 data
|
|
146
240
|
const stats = safeStats(filePath);
|
|
147
241
|
if (!stats)
|
|
148
242
|
continue;
|
|
@@ -154,7 +248,6 @@ export const cursorScanner = {
|
|
|
154
248
|
continue;
|
|
155
249
|
const firstQuery = content.match(/<user_query>\n?([\s\S]*?)\n?<\/user_query>/);
|
|
156
250
|
const preview = firstQuery ? firstQuery[1].trim().slice(0, 200) : content.slice(0, 200);
|
|
157
|
-
const id = path.basename(filePath, ".txt");
|
|
158
251
|
seenIds.add(id);
|
|
159
252
|
sessions.push({
|
|
160
253
|
id,
|
|
@@ -172,7 +265,7 @@ export const cursorScanner = {
|
|
|
172
265
|
sizeBytes: stats.size,
|
|
173
266
|
});
|
|
174
267
|
}
|
|
175
|
-
// --- FORMAT 2: chatSessions/*.json ---
|
|
268
|
+
// --- FORMAT 2: chatSessions/*.json (only if not in Format 3) ---
|
|
176
269
|
for (const filePath of listFiles(path.join(projectDir, "chatSessions"), [".json"])) {
|
|
177
270
|
const stats = safeStats(filePath);
|
|
178
271
|
if (!stats || stats.size < 100)
|
|
@@ -184,6 +277,8 @@ export const cursorScanner = {
|
|
|
184
277
|
const firstMsg = data.requests[0]?.message || "";
|
|
185
278
|
const preview = (typeof firstMsg === "string" ? firstMsg : "").slice(0, 200);
|
|
186
279
|
const id = data.sessionId || path.basename(filePath, ".json");
|
|
280
|
+
if (seenIds.has(id))
|
|
281
|
+
continue;
|
|
187
282
|
seenIds.add(id);
|
|
188
283
|
sessions.push({
|
|
189
284
|
id,
|
|
@@ -203,59 +298,6 @@ export const cursorScanner = {
|
|
|
203
298
|
}
|
|
204
299
|
}
|
|
205
300
|
}
|
|
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
301
|
sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
|
|
260
302
|
return sessions.slice(0, limit);
|
|
261
303
|
},
|
|
@@ -373,14 +415,12 @@ function parseVscdbSession(virtualPath, maxTurns) {
|
|
|
373
415
|
continue;
|
|
374
416
|
try {
|
|
375
417
|
const bubble = JSON.parse(bubbleRows[0].value);
|
|
376
|
-
const
|
|
377
|
-
if (!
|
|
378
|
-
|
|
379
|
-
continue;
|
|
380
|
-
}
|
|
418
|
+
const content = extractBubbleContent(bubble);
|
|
419
|
+
if (!content)
|
|
420
|
+
continue; // skip truly empty bubbles
|
|
381
421
|
turns.push({
|
|
382
422
|
role: header.type === 1 ? "human" : "assistant",
|
|
383
|
-
content
|
|
423
|
+
content,
|
|
384
424
|
});
|
|
385
425
|
}
|
|
386
426
|
catch { /* skip */ }
|
|
@@ -405,3 +445,57 @@ function parseVscdbSession(virtualPath, maxTurns) {
|
|
|
405
445
|
};
|
|
406
446
|
}, null);
|
|
407
447
|
}
|
|
448
|
+
// --- Helper: extract text content from a bubble ---
|
|
449
|
+
// Cursor stores different bubble types:
|
|
450
|
+
// - Regular text messages: content in `text`, `message`, or `rawText`
|
|
451
|
+
// - Tool calls (capabilityType 15): content in `toolFormerData` (name, params, result)
|
|
452
|
+
// - Thinking blocks: content in `allThinkingBlocks`
|
|
453
|
+
function extractBubbleContent(bubble) {
|
|
454
|
+
// 1. Direct text content
|
|
455
|
+
const text = bubble.text || bubble.message || bubble.rawText || "";
|
|
456
|
+
if (text)
|
|
457
|
+
return text;
|
|
458
|
+
// 2. Tool call content (capabilityType 15) — extract tool name, args, and result
|
|
459
|
+
const toolData = bubble.toolFormerData;
|
|
460
|
+
if (toolData && typeof toolData === "object") {
|
|
461
|
+
const parts = [];
|
|
462
|
+
const toolName = toolData.name || toolData.tool || "unknown_tool";
|
|
463
|
+
parts.push(`[Tool: ${toolName}]`);
|
|
464
|
+
// Tool arguments/params
|
|
465
|
+
const params = toolData.params || toolData.rawArgs;
|
|
466
|
+
if (params) {
|
|
467
|
+
const paramStr = typeof params === "string" ? params : JSON.stringify(params);
|
|
468
|
+
if (paramStr.length > 0 && paramStr !== "{}") {
|
|
469
|
+
parts.push(`Args: ${paramStr.slice(0, 2000)}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// Tool result
|
|
473
|
+
const result = toolData.result;
|
|
474
|
+
if (result) {
|
|
475
|
+
const resultStr = typeof result === "string" ? result : JSON.stringify(result);
|
|
476
|
+
if (resultStr.length > 0) {
|
|
477
|
+
parts.push(`Result: ${resultStr.slice(0, 3000)}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (parts.length > 1)
|
|
481
|
+
return parts.join("\n");
|
|
482
|
+
}
|
|
483
|
+
// 3. Thinking blocks
|
|
484
|
+
const thinkingBlocks = bubble.allThinkingBlocks;
|
|
485
|
+
if (thinkingBlocks && Array.isArray(thinkingBlocks) && thinkingBlocks.length > 0) {
|
|
486
|
+
const thinking = thinkingBlocks
|
|
487
|
+
.map((b) => b.text || b.content || "")
|
|
488
|
+
.filter(Boolean)
|
|
489
|
+
.join("\n");
|
|
490
|
+
if (thinking)
|
|
491
|
+
return `[Thinking]\n${thinking}`;
|
|
492
|
+
}
|
|
493
|
+
// 4. Code blocks from suggestedCodeBlocks
|
|
494
|
+
const codeBlocks = bubble.suggestedCodeBlocks;
|
|
495
|
+
if (codeBlocks && Array.isArray(codeBlocks) && codeBlocks.length > 0) {
|
|
496
|
+
return codeBlocks
|
|
497
|
+
.map((b) => `\`\`\`${b.language || ""}\n${b.code || ""}\n\`\`\``)
|
|
498
|
+
.join("\n");
|
|
499
|
+
}
|
|
500
|
+
return "";
|
|
501
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeblog-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "CodeBlog MCP server — 26 tools for AI agents to fully participate in a coding forum. Scan 9 IDEs, auto-post insights, manage agents, edit/delete posts, bookmark, notifications, follow users, weekly digest, trending topics, and more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|