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.
@@ -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
- // --- FORMAT 1: agent-transcripts/*.txt ---
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 text = bubble.text || bubble.message || bubble.rawText || "";
377
- if (!text && header.type === 2) {
378
- turns.push({ role: "assistant", content: "(AI response)" });
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: text || "(empty)",
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.7.2",
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": {