codemolt-mcp 0.6.2 → 0.7.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.
- package/dist/index.js +2 -2
- package/dist/scanners/cursor.js +207 -41
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ const SETUP_GUIDE = `CodeBlog is not set up yet. To get started, run the codemol
|
|
|
42
42
|
`No browser needed — everything happens right here.`;
|
|
43
43
|
const server = new McpServer({
|
|
44
44
|
name: "codemolt",
|
|
45
|
-
version: "0.
|
|
45
|
+
version: "0.7.0",
|
|
46
46
|
});
|
|
47
47
|
// ═══════════════════════════════════════════════════════════════════
|
|
48
48
|
// SETUP & STATUS TOOLS
|
|
@@ -152,7 +152,7 @@ server.registerTool("codemolt_status", {
|
|
|
152
152
|
agentInfo = `\n\n⚠️ Not set up. Run codemolt_setup to get started.`;
|
|
153
153
|
}
|
|
154
154
|
return {
|
|
155
|
-
content: [text(`CodeBlog MCP Server v0.
|
|
155
|
+
content: [text(`CodeBlog MCP Server v0.7.0\n` +
|
|
156
156
|
`Platform: ${platform}\n` +
|
|
157
157
|
`Server: ${serverUrl}\n\n` +
|
|
158
158
|
`📡 IDE Scanners:\n${scannerInfo}` +
|
package/dist/scanners/cursor.js
CHANGED
|
@@ -1,29 +1,72 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
2
|
import * as fs from "fs";
|
|
3
|
+
import BetterSqlite3 from "better-sqlite3";
|
|
3
4
|
import { getHome, getPlatform } from "../lib/platform.js";
|
|
4
5
|
import { listFiles, listDirs, safeReadFile, safeReadJson, safeStats, extractProjectDescription } from "../lib/fs-utils.js";
|
|
5
|
-
// Cursor stores conversations in
|
|
6
|
+
// Cursor stores conversations in THREE places (all supported for version compatibility):
|
|
6
7
|
//
|
|
7
|
-
// 1
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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>
|
|
10
11
|
//
|
|
11
|
-
// 2
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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
|
+
// Safe SQLite query helper — returns empty array on any error
|
|
22
|
+
function safeQuery(dbPath, sql) {
|
|
23
|
+
try {
|
|
24
|
+
const db = new BetterSqlite3(dbPath, { readonly: true, fileMustExist: true });
|
|
25
|
+
try {
|
|
26
|
+
const rows = db.prepare(sql).all();
|
|
27
|
+
return rows;
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
db.close();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
console.error(`[codemolt] Cursor safeQuery error:`, err instanceof Error ? err.message : err);
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function getGlobalStoragePath() {
|
|
39
|
+
const home = getHome();
|
|
40
|
+
const platform = getPlatform();
|
|
41
|
+
let p;
|
|
42
|
+
if (platform === "macos") {
|
|
43
|
+
p = path.join(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
44
|
+
}
|
|
45
|
+
else if (platform === "windows") {
|
|
46
|
+
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
47
|
+
p = path.join(appData, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
p = path.join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return fs.existsSync(p) ? p : null;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
16
59
|
export const cursorScanner = {
|
|
17
60
|
name: "Cursor",
|
|
18
61
|
sourceType: "cursor",
|
|
19
|
-
description: "Cursor AI IDE sessions (agent transcripts + chat sessions)",
|
|
62
|
+
description: "Cursor AI IDE sessions (agent transcripts + chat sessions + composer)",
|
|
20
63
|
getSessionDirs() {
|
|
21
64
|
const home = getHome();
|
|
22
65
|
const platform = getPlatform();
|
|
23
66
|
const candidates = [];
|
|
24
|
-
// Agent transcripts
|
|
67
|
+
// Format 1: Agent transcripts
|
|
25
68
|
candidates.push(path.join(home, ".cursor", "projects"));
|
|
26
|
-
//
|
|
69
|
+
// Format 2 & workspace-level Format 3: workspaceStorage
|
|
27
70
|
if (platform === "macos") {
|
|
28
71
|
candidates.push(path.join(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage"));
|
|
29
72
|
}
|
|
@@ -34,6 +77,11 @@ export const cursorScanner = {
|
|
|
34
77
|
else {
|
|
35
78
|
candidates.push(path.join(home, ".config", "Cursor", "User", "workspaceStorage"));
|
|
36
79
|
}
|
|
80
|
+
// Format 3: globalStorage (just check existence for status reporting)
|
|
81
|
+
const globalDb = getGlobalStoragePath();
|
|
82
|
+
if (globalDb) {
|
|
83
|
+
candidates.push(path.dirname(globalDb));
|
|
84
|
+
}
|
|
37
85
|
return candidates.filter((d) => {
|
|
38
86
|
try {
|
|
39
87
|
return fs.existsSync(d);
|
|
@@ -46,34 +94,30 @@ export const cursorScanner = {
|
|
|
46
94
|
scan(limit) {
|
|
47
95
|
const sessions = [];
|
|
48
96
|
const dirs = this.getSessionDirs();
|
|
97
|
+
const seenIds = new Set();
|
|
49
98
|
for (const baseDir of dirs) {
|
|
99
|
+
// Skip globalStorage dir — handled separately via Format 3
|
|
100
|
+
if (baseDir.endsWith("globalStorage"))
|
|
101
|
+
continue;
|
|
50
102
|
const projectDirs = listDirs(baseDir);
|
|
51
103
|
for (const projectDir of projectDirs) {
|
|
52
104
|
const dirName = path.basename(projectDir);
|
|
53
|
-
// Resolve project path:
|
|
54
|
-
// - agent-transcripts dirs: "Users-zhaoyifei-SimenDevelop-Simen" → "/Users/zhaoyifei/SimenDevelop/Simen"
|
|
55
|
-
// - workspaceStorage dirs: read workspace.json for folder URI
|
|
56
105
|
let projectPath;
|
|
57
|
-
const
|
|
58
|
-
const workspaceJson = safeReadJson(workspaceJsonPath);
|
|
106
|
+
const workspaceJson = safeReadJson(path.join(projectDir, "workspace.json"));
|
|
59
107
|
if (workspaceJson?.folder) {
|
|
60
108
|
try {
|
|
61
109
|
projectPath = decodeURIComponent(new URL(workspaceJson.folder).pathname);
|
|
62
110
|
}
|
|
63
|
-
catch { /*
|
|
111
|
+
catch { /* */ }
|
|
64
112
|
}
|
|
65
113
|
if (!projectPath && dirName.startsWith("Users-")) {
|
|
66
|
-
// Decode hyphenated path: "Users-zhaoyifei-Foo" → "/Users/zhaoyifei/Foo"
|
|
67
114
|
projectPath = "/" + dirName.replace(/-/g, "/");
|
|
68
115
|
}
|
|
69
116
|
const project = projectPath ? path.basename(projectPath) : dirName;
|
|
70
|
-
const projectDescription = projectPath
|
|
71
|
-
|
|
72
|
-
: undefined;
|
|
73
|
-
// --- Path 1: agent-transcripts/*.txt ---
|
|
117
|
+
const projectDescription = projectPath ? extractProjectDescription(projectPath) || undefined : undefined;
|
|
118
|
+
// --- FORMAT 1: agent-transcripts/*.txt ---
|
|
74
119
|
const transcriptsDir = path.join(projectDir, "agent-transcripts");
|
|
75
|
-
const
|
|
76
|
-
for (const filePath of txtFiles) {
|
|
120
|
+
for (const filePath of listFiles(transcriptsDir, [".txt"])) {
|
|
77
121
|
const stats = safeStats(filePath);
|
|
78
122
|
if (!stats)
|
|
79
123
|
continue;
|
|
@@ -81,31 +125,30 @@ export const cursorScanner = {
|
|
|
81
125
|
if (!content || content.length < 100)
|
|
82
126
|
continue;
|
|
83
127
|
const userQueries = content.match(/<user_query>\n?([\s\S]*?)\n?<\/user_query>/g) || [];
|
|
84
|
-
|
|
85
|
-
if (humanCount === 0)
|
|
128
|
+
if (userQueries.length === 0)
|
|
86
129
|
continue;
|
|
87
130
|
const firstQuery = content.match(/<user_query>\n?([\s\S]*?)\n?<\/user_query>/);
|
|
88
131
|
const preview = firstQuery ? firstQuery[1].trim().slice(0, 200) : content.slice(0, 200);
|
|
132
|
+
const id = path.basename(filePath, ".txt");
|
|
133
|
+
seenIds.add(id);
|
|
89
134
|
sessions.push({
|
|
90
|
-
id
|
|
135
|
+
id,
|
|
91
136
|
source: "cursor",
|
|
92
137
|
project,
|
|
93
138
|
projectPath,
|
|
94
139
|
projectDescription,
|
|
95
140
|
title: preview.slice(0, 80) || `Cursor session in ${project}`,
|
|
96
|
-
messageCount:
|
|
97
|
-
humanMessages:
|
|
98
|
-
aiMessages:
|
|
141
|
+
messageCount: userQueries.length * 2,
|
|
142
|
+
humanMessages: userQueries.length,
|
|
143
|
+
aiMessages: userQueries.length,
|
|
99
144
|
preview,
|
|
100
145
|
filePath,
|
|
101
146
|
modifiedAt: stats.mtime,
|
|
102
147
|
sizeBytes: stats.size,
|
|
103
148
|
});
|
|
104
149
|
}
|
|
105
|
-
// ---
|
|
106
|
-
const
|
|
107
|
-
const jsonFiles = listFiles(chatSessionsDir, [".json"]);
|
|
108
|
-
for (const filePath of jsonFiles) {
|
|
150
|
+
// --- FORMAT 2: chatSessions/*.json ---
|
|
151
|
+
for (const filePath of listFiles(path.join(projectDir, "chatSessions"), [".json"])) {
|
|
109
152
|
const stats = safeStats(filePath);
|
|
110
153
|
if (!stats || stats.size < 100)
|
|
111
154
|
continue;
|
|
@@ -115,8 +158,10 @@ export const cursorScanner = {
|
|
|
115
158
|
const humanCount = data.requests.length;
|
|
116
159
|
const firstMsg = data.requests[0]?.message || "";
|
|
117
160
|
const preview = (typeof firstMsg === "string" ? firstMsg : "").slice(0, 200);
|
|
161
|
+
const id = data.sessionId || path.basename(filePath, ".json");
|
|
162
|
+
seenIds.add(id);
|
|
118
163
|
sessions.push({
|
|
119
|
-
id
|
|
164
|
+
id,
|
|
120
165
|
source: "cursor",
|
|
121
166
|
project,
|
|
122
167
|
projectPath,
|
|
@@ -133,15 +178,74 @@ export const cursorScanner = {
|
|
|
133
178
|
}
|
|
134
179
|
}
|
|
135
180
|
}
|
|
181
|
+
// --- FORMAT 3: globalStorage state.vscdb (newer Cursor) ---
|
|
182
|
+
// This supplements Formats 1 & 2 — adds any sessions not already found
|
|
183
|
+
try {
|
|
184
|
+
const globalDb = getGlobalStoragePath();
|
|
185
|
+
if (globalDb) {
|
|
186
|
+
const rows = safeQuery(globalDb, "SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'");
|
|
187
|
+
for (const row of rows) {
|
|
188
|
+
try {
|
|
189
|
+
const data = JSON.parse(row.value);
|
|
190
|
+
const composerId = data.composerId || row.key.replace("composerData:", "");
|
|
191
|
+
if (seenIds.has(composerId))
|
|
192
|
+
continue; // skip duplicates
|
|
193
|
+
const bubbleHeaders = data.fullConversationHeadersOnly || [];
|
|
194
|
+
if (bubbleHeaders.length === 0)
|
|
195
|
+
continue;
|
|
196
|
+
const humanCount = bubbleHeaders.filter((b) => b.type === 1).length;
|
|
197
|
+
const aiCount = bubbleHeaders.filter((b) => b.type === 2).length;
|
|
198
|
+
const name = data.name || "";
|
|
199
|
+
// Get first user message as preview
|
|
200
|
+
let preview = name;
|
|
201
|
+
if (!preview) {
|
|
202
|
+
const firstUserBubble = bubbleHeaders.find((b) => b.type === 1);
|
|
203
|
+
if (firstUserBubble) {
|
|
204
|
+
const bubbleRow = safeQuery(globalDb, `SELECT value FROM cursorDiskKV WHERE key='bubbleId:${composerId}:${firstUserBubble.bubbleId}'`);
|
|
205
|
+
if (bubbleRow.length > 0) {
|
|
206
|
+
try {
|
|
207
|
+
const bubble = JSON.parse(bubbleRow[0].value);
|
|
208
|
+
preview = (bubble.text || bubble.message || "").slice(0, 200);
|
|
209
|
+
}
|
|
210
|
+
catch { /* */ }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const createdAt = data.createdAt ? new Date(data.createdAt) : new Date();
|
|
215
|
+
const updatedAt = data.lastUpdatedAt ? new Date(data.lastUpdatedAt) : createdAt;
|
|
216
|
+
sessions.push({
|
|
217
|
+
id: composerId,
|
|
218
|
+
source: "cursor",
|
|
219
|
+
project: "Cursor Composer",
|
|
220
|
+
title: (name || preview || "Cursor composer session").slice(0, 80),
|
|
221
|
+
messageCount: humanCount + aiCount,
|
|
222
|
+
humanMessages: humanCount,
|
|
223
|
+
aiMessages: aiCount,
|
|
224
|
+
preview: preview || "(composer session)",
|
|
225
|
+
filePath: `vscdb:${globalDb}:${composerId}`,
|
|
226
|
+
modifiedAt: updatedAt,
|
|
227
|
+
sizeBytes: row.value.length,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
catch { /* skip malformed entries */ }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
console.error(`[codemolt] Cursor Format 3 error:`, err instanceof Error ? err.message : err);
|
|
236
|
+
}
|
|
136
237
|
sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
|
|
137
238
|
return sessions.slice(0, limit);
|
|
138
239
|
},
|
|
139
240
|
parse(filePath, maxTurns) {
|
|
241
|
+
// FORMAT 3: vscdb virtual path
|
|
242
|
+
if (filePath.startsWith("vscdb:")) {
|
|
243
|
+
return parseVscdbSession(filePath, maxTurns);
|
|
244
|
+
}
|
|
140
245
|
const stats = safeStats(filePath);
|
|
141
246
|
const turns = [];
|
|
142
247
|
if (filePath.endsWith(".txt")) {
|
|
143
|
-
//
|
|
144
|
-
// user:\n<user_query>\n...\n</user_query>\n\nA:\n...
|
|
248
|
+
// FORMAT 1: agent transcript
|
|
145
249
|
const content = safeReadFile(filePath);
|
|
146
250
|
if (!content)
|
|
147
251
|
return null;
|
|
@@ -155,7 +259,6 @@ export const cursorScanner = {
|
|
|
155
259
|
if (queryMatch) {
|
|
156
260
|
turns.push({ role: "human", content: queryMatch[1].trim() });
|
|
157
261
|
}
|
|
158
|
-
// Everything after </user_query> and after "A:" is the assistant response
|
|
159
262
|
const afterQuery = block.split(/<\/user_query>/)[1];
|
|
160
263
|
if (afterQuery) {
|
|
161
264
|
const aiContent = afterQuery.replace(/^\s*\n\s*A:\s*\n?/, "").trim();
|
|
@@ -166,7 +269,7 @@ export const cursorScanner = {
|
|
|
166
269
|
}
|
|
167
270
|
}
|
|
168
271
|
else {
|
|
169
|
-
//
|
|
272
|
+
// FORMAT 2: chatSessions JSON
|
|
170
273
|
const data = safeReadJson(filePath);
|
|
171
274
|
if (!data || !Array.isArray(data.requests))
|
|
172
275
|
return null;
|
|
@@ -181,7 +284,6 @@ export const cursorScanner = {
|
|
|
181
284
|
}
|
|
182
285
|
if (maxTurns && turns.length >= maxTurns)
|
|
183
286
|
break;
|
|
184
|
-
// Response can be array of text chunks or a string
|
|
185
287
|
if (req.response) {
|
|
186
288
|
let respText = "";
|
|
187
289
|
if (typeof req.response === "string") {
|
|
@@ -218,3 +320,67 @@ export const cursorScanner = {
|
|
|
218
320
|
};
|
|
219
321
|
},
|
|
220
322
|
};
|
|
323
|
+
// Parse a session stored in globalStorage state.vscdb (Format 3)
|
|
324
|
+
function parseVscdbSession(virtualPath, maxTurns) {
|
|
325
|
+
// virtualPath = "vscdb:/path/to/state.vscdb:composerId"
|
|
326
|
+
const parts = virtualPath.split(":");
|
|
327
|
+
if (parts.length < 3)
|
|
328
|
+
return null;
|
|
329
|
+
const dbPath = parts[1];
|
|
330
|
+
const composerId = parts.slice(2).join(":");
|
|
331
|
+
// Get composer metadata
|
|
332
|
+
const metaRows = safeQuery(dbPath, `SELECT value FROM cursorDiskKV WHERE key='composerData:${composerId}'`);
|
|
333
|
+
if (metaRows.length === 0)
|
|
334
|
+
return null;
|
|
335
|
+
let composerData;
|
|
336
|
+
try {
|
|
337
|
+
composerData = JSON.parse(metaRows[0].value);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
const bubbleHeaders = composerData.fullConversationHeadersOnly || [];
|
|
343
|
+
if (bubbleHeaders.length === 0)
|
|
344
|
+
return null;
|
|
345
|
+
// Fetch bubble contents
|
|
346
|
+
const turns = [];
|
|
347
|
+
for (const header of bubbleHeaders) {
|
|
348
|
+
if (maxTurns && turns.length >= maxTurns)
|
|
349
|
+
break;
|
|
350
|
+
const bubbleRows = safeQuery(dbPath, `SELECT value FROM cursorDiskKV WHERE key='bubbleId:${composerId}:${header.bubbleId}'`);
|
|
351
|
+
if (bubbleRows.length === 0)
|
|
352
|
+
continue;
|
|
353
|
+
try {
|
|
354
|
+
const bubble = JSON.parse(bubbleRows[0].value);
|
|
355
|
+
const text = bubble.text || bubble.message || bubble.rawText || "";
|
|
356
|
+
if (!text && header.type === 2) {
|
|
357
|
+
// AI response text might be empty in newer versions — skip
|
|
358
|
+
turns.push({ role: "assistant", content: "(AI response)" });
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
turns.push({
|
|
362
|
+
role: header.type === 1 ? "human" : "assistant",
|
|
363
|
+
content: text || "(empty)",
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
catch { /* skip */ }
|
|
367
|
+
}
|
|
368
|
+
if (turns.length === 0)
|
|
369
|
+
return null;
|
|
370
|
+
const humanMsgs = turns.filter((t) => t.role === "human");
|
|
371
|
+
const aiMsgs = turns.filter((t) => t.role === "assistant");
|
|
372
|
+
return {
|
|
373
|
+
id: composerId,
|
|
374
|
+
source: "cursor",
|
|
375
|
+
project: "Cursor Composer",
|
|
376
|
+
title: composerData.name || humanMsgs[0]?.content.slice(0, 80) || "Cursor session",
|
|
377
|
+
messageCount: turns.length,
|
|
378
|
+
humanMessages: humanMsgs.length,
|
|
379
|
+
aiMessages: aiMsgs.length,
|
|
380
|
+
preview: humanMsgs[0]?.content.slice(0, 200) || "",
|
|
381
|
+
filePath: virtualPath,
|
|
382
|
+
modifiedAt: composerData.lastUpdatedAt ? new Date(composerData.lastUpdatedAt) : new Date(),
|
|
383
|
+
sizeBytes: 0,
|
|
384
|
+
turns,
|
|
385
|
+
};
|
|
386
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codemolt-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "CodeBlog MCP server — 14 tools for AI agents to fully participate in a coding forum. Scan 9 IDEs, auto-post insights, comment, vote, debate, and engage with the community",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|