codemolt-mcp 0.4.1 → 0.6.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/README.md +1 -1
- package/dist/index.js +626 -346
- package/dist/lib/analyzer.d.ts +2 -0
- package/dist/lib/analyzer.js +225 -0
- package/dist/lib/fs-utils.d.ts +9 -0
- package/dist/lib/fs-utils.js +147 -0
- package/dist/lib/platform.d.ts +6 -0
- package/dist/lib/platform.js +50 -0
- package/dist/lib/registry.d.ts +13 -0
- package/dist/lib/registry.js +48 -0
- package/dist/lib/types.d.ts +47 -0
- package/dist/lib/types.js +1 -0
- package/dist/scanners/aider.d.ts +2 -0
- package/dist/scanners/aider.js +130 -0
- package/dist/scanners/claude-code.d.ts +2 -0
- package/dist/scanners/claude-code.js +187 -0
- package/dist/scanners/codex.d.ts +2 -0
- package/dist/scanners/codex.js +142 -0
- package/dist/scanners/continue-dev.d.ts +2 -0
- package/dist/scanners/continue-dev.js +134 -0
- package/dist/scanners/cursor.d.ts +2 -0
- package/dist/scanners/cursor.js +219 -0
- package/dist/scanners/index.d.ts +1 -0
- package/dist/scanners/index.js +22 -0
- package/dist/scanners/vscode-copilot.d.ts +2 -0
- package/dist/scanners/vscode-copilot.js +177 -0
- package/dist/scanners/warp.d.ts +2 -0
- package/dist/scanners/warp.js +20 -0
- package/dist/scanners/windsurf.d.ts +2 -0
- package/dist/scanners/windsurf.js +171 -0
- package/dist/scanners/zed.d.ts +2 -0
- package/dist/scanners/zed.js +119 -0
- package/package.json +6 -4
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { getHome, getPlatform } from "../lib/platform.js";
|
|
3
|
+
import { listFiles, listDirs, safeReadFile, safeReadJson, safeStats, extractProjectDescription } from "../lib/fs-utils.js";
|
|
4
|
+
// Cursor stores conversations in two places:
|
|
5
|
+
//
|
|
6
|
+
// 1. Agent transcripts (plain text, XML-like tags):
|
|
7
|
+
// ~/.cursor/projects/<project>/agent-transcripts/*.txt
|
|
8
|
+
// Format: user: <user_query>...</user_query> \n A: <response>
|
|
9
|
+
//
|
|
10
|
+
// 2. Chat sessions (JSON):
|
|
11
|
+
// macOS: ~/Library/Application Support/Cursor/User/workspaceStorage/<hash>/chatSessions/*.json
|
|
12
|
+
// Windows: %APPDATA%/Cursor/User/workspaceStorage/<hash>/chatSessions/*.json
|
|
13
|
+
// Linux: ~/.config/Cursor/User/workspaceStorage/<hash>/chatSessions/*.json
|
|
14
|
+
// Format: { requests: [{ message: "...", response: [...] }], sessionId, creationDate }
|
|
15
|
+
export const cursorScanner = {
|
|
16
|
+
name: "Cursor",
|
|
17
|
+
sourceType: "cursor",
|
|
18
|
+
description: "Cursor AI IDE sessions (agent transcripts + chat sessions)",
|
|
19
|
+
getSessionDirs() {
|
|
20
|
+
const home = getHome();
|
|
21
|
+
const platform = getPlatform();
|
|
22
|
+
const candidates = [];
|
|
23
|
+
// Agent transcripts (all platforms)
|
|
24
|
+
candidates.push(path.join(home, ".cursor", "projects"));
|
|
25
|
+
// Chat sessions in workspaceStorage
|
|
26
|
+
if (platform === "macos") {
|
|
27
|
+
candidates.push(path.join(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage"));
|
|
28
|
+
}
|
|
29
|
+
else if (platform === "windows") {
|
|
30
|
+
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
31
|
+
candidates.push(path.join(appData, "Cursor", "User", "workspaceStorage"));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
candidates.push(path.join(home, ".config", "Cursor", "User", "workspaceStorage"));
|
|
35
|
+
}
|
|
36
|
+
return candidates.filter((d) => {
|
|
37
|
+
try {
|
|
38
|
+
return require("fs").existsSync(d);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
scan(limit) {
|
|
46
|
+
const sessions = [];
|
|
47
|
+
const dirs = this.getSessionDirs();
|
|
48
|
+
for (const baseDir of dirs) {
|
|
49
|
+
const projectDirs = listDirs(baseDir);
|
|
50
|
+
for (const projectDir of projectDirs) {
|
|
51
|
+
const dirName = path.basename(projectDir);
|
|
52
|
+
// Resolve project path:
|
|
53
|
+
// - agent-transcripts dirs: "Users-zhaoyifei-SimenDevelop-Simen" → "/Users/zhaoyifei/SimenDevelop/Simen"
|
|
54
|
+
// - workspaceStorage dirs: read workspace.json for folder URI
|
|
55
|
+
let projectPath;
|
|
56
|
+
const workspaceJsonPath = path.join(projectDir, "workspace.json");
|
|
57
|
+
const workspaceJson = safeReadJson(workspaceJsonPath);
|
|
58
|
+
if (workspaceJson?.folder) {
|
|
59
|
+
try {
|
|
60
|
+
projectPath = decodeURIComponent(new URL(workspaceJson.folder).pathname);
|
|
61
|
+
}
|
|
62
|
+
catch { /* ignore */ }
|
|
63
|
+
}
|
|
64
|
+
if (!projectPath && dirName.startsWith("Users-")) {
|
|
65
|
+
// Decode hyphenated path: "Users-zhaoyifei-Foo" → "/Users/zhaoyifei/Foo"
|
|
66
|
+
projectPath = "/" + dirName.replace(/-/g, "/");
|
|
67
|
+
}
|
|
68
|
+
const project = projectPath ? path.basename(projectPath) : dirName;
|
|
69
|
+
const projectDescription = projectPath
|
|
70
|
+
? extractProjectDescription(projectPath) || undefined
|
|
71
|
+
: undefined;
|
|
72
|
+
// --- Path 1: agent-transcripts/*.txt ---
|
|
73
|
+
const transcriptsDir = path.join(projectDir, "agent-transcripts");
|
|
74
|
+
const txtFiles = listFiles(transcriptsDir, [".txt"]);
|
|
75
|
+
for (const filePath of txtFiles) {
|
|
76
|
+
const stats = safeStats(filePath);
|
|
77
|
+
if (!stats)
|
|
78
|
+
continue;
|
|
79
|
+
const content = safeReadFile(filePath);
|
|
80
|
+
if (!content || content.length < 100)
|
|
81
|
+
continue;
|
|
82
|
+
const userQueries = content.match(/<user_query>\n?([\s\S]*?)\n?<\/user_query>/g) || [];
|
|
83
|
+
const humanCount = userQueries.length;
|
|
84
|
+
if (humanCount === 0)
|
|
85
|
+
continue;
|
|
86
|
+
const firstQuery = content.match(/<user_query>\n?([\s\S]*?)\n?<\/user_query>/);
|
|
87
|
+
const preview = firstQuery ? firstQuery[1].trim().slice(0, 200) : content.slice(0, 200);
|
|
88
|
+
sessions.push({
|
|
89
|
+
id: path.basename(filePath, ".txt"),
|
|
90
|
+
source: "cursor",
|
|
91
|
+
project,
|
|
92
|
+
projectPath,
|
|
93
|
+
projectDescription,
|
|
94
|
+
title: preview.slice(0, 80) || `Cursor session in ${project}`,
|
|
95
|
+
messageCount: humanCount * 2,
|
|
96
|
+
humanMessages: humanCount,
|
|
97
|
+
aiMessages: humanCount,
|
|
98
|
+
preview,
|
|
99
|
+
filePath,
|
|
100
|
+
modifiedAt: stats.mtime,
|
|
101
|
+
sizeBytes: stats.size,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// --- Path 2: chatSessions/*.json (inside workspaceStorage/<hash>/) ---
|
|
105
|
+
const chatSessionsDir = path.join(projectDir, "chatSessions");
|
|
106
|
+
const jsonFiles = listFiles(chatSessionsDir, [".json"]);
|
|
107
|
+
for (const filePath of jsonFiles) {
|
|
108
|
+
const stats = safeStats(filePath);
|
|
109
|
+
if (!stats || stats.size < 100)
|
|
110
|
+
continue;
|
|
111
|
+
const data = safeReadJson(filePath);
|
|
112
|
+
if (!data || !Array.isArray(data.requests) || data.requests.length === 0)
|
|
113
|
+
continue;
|
|
114
|
+
const humanCount = data.requests.length;
|
|
115
|
+
const firstMsg = data.requests[0]?.message || "";
|
|
116
|
+
const preview = (typeof firstMsg === "string" ? firstMsg : "").slice(0, 200);
|
|
117
|
+
sessions.push({
|
|
118
|
+
id: data.sessionId || path.basename(filePath, ".json"),
|
|
119
|
+
source: "cursor",
|
|
120
|
+
project,
|
|
121
|
+
projectPath,
|
|
122
|
+
projectDescription,
|
|
123
|
+
title: preview.slice(0, 80) || `Cursor chat in ${project}`,
|
|
124
|
+
messageCount: humanCount * 2,
|
|
125
|
+
humanMessages: humanCount,
|
|
126
|
+
aiMessages: humanCount,
|
|
127
|
+
preview: preview || "(cursor chat session)",
|
|
128
|
+
filePath,
|
|
129
|
+
modifiedAt: stats.mtime,
|
|
130
|
+
sizeBytes: stats.size,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
|
|
136
|
+
return sessions.slice(0, limit);
|
|
137
|
+
},
|
|
138
|
+
parse(filePath, maxTurns) {
|
|
139
|
+
const stats = safeStats(filePath);
|
|
140
|
+
const turns = [];
|
|
141
|
+
if (filePath.endsWith(".txt")) {
|
|
142
|
+
// Parse agent transcript format:
|
|
143
|
+
// user:\n<user_query>\n...\n</user_query>\n\nA:\n...
|
|
144
|
+
const content = safeReadFile(filePath);
|
|
145
|
+
if (!content)
|
|
146
|
+
return null;
|
|
147
|
+
const blocks = content.split(/^user:\s*$/m);
|
|
148
|
+
for (const block of blocks) {
|
|
149
|
+
if (!block.trim())
|
|
150
|
+
continue;
|
|
151
|
+
if (maxTurns && turns.length >= maxTurns)
|
|
152
|
+
break;
|
|
153
|
+
const queryMatch = block.match(/<user_query>\n?([\s\S]*?)\n?<\/user_query>/);
|
|
154
|
+
if (queryMatch) {
|
|
155
|
+
turns.push({ role: "human", content: queryMatch[1].trim() });
|
|
156
|
+
}
|
|
157
|
+
// Everything after </user_query> and after "A:" is the assistant response
|
|
158
|
+
const afterQuery = block.split(/<\/user_query>/)[1];
|
|
159
|
+
if (afterQuery) {
|
|
160
|
+
const aiContent = afterQuery.replace(/^\s*\n\s*A:\s*\n?/, "").trim();
|
|
161
|
+
if (aiContent && (!maxTurns || turns.length < maxTurns)) {
|
|
162
|
+
turns.push({ role: "assistant", content: aiContent });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// Parse chatSessions JSON: { requests: [{ message, response }] }
|
|
169
|
+
const data = safeReadJson(filePath);
|
|
170
|
+
if (!data || !Array.isArray(data.requests))
|
|
171
|
+
return null;
|
|
172
|
+
for (const req of data.requests) {
|
|
173
|
+
if (maxTurns && turns.length >= maxTurns)
|
|
174
|
+
break;
|
|
175
|
+
if (req.message) {
|
|
176
|
+
turns.push({
|
|
177
|
+
role: "human",
|
|
178
|
+
content: typeof req.message === "string" ? req.message : JSON.stringify(req.message),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (maxTurns && turns.length >= maxTurns)
|
|
182
|
+
break;
|
|
183
|
+
// Response can be array of text chunks or a string
|
|
184
|
+
if (req.response) {
|
|
185
|
+
let respText = "";
|
|
186
|
+
if (typeof req.response === "string") {
|
|
187
|
+
respText = req.response;
|
|
188
|
+
}
|
|
189
|
+
else if (Array.isArray(req.response)) {
|
|
190
|
+
respText = req.response
|
|
191
|
+
.map((r) => (typeof r === "string" ? r : r?.text || ""))
|
|
192
|
+
.join("");
|
|
193
|
+
}
|
|
194
|
+
if (respText.trim()) {
|
|
195
|
+
turns.push({ role: "assistant", content: respText.trim() });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (turns.length === 0)
|
|
201
|
+
return null;
|
|
202
|
+
const humanMsgs = turns.filter((t) => t.role === "human");
|
|
203
|
+
const aiMsgs = turns.filter((t) => t.role === "assistant");
|
|
204
|
+
return {
|
|
205
|
+
id: path.basename(filePath).replace(/\.\w+$/, ""),
|
|
206
|
+
source: "cursor",
|
|
207
|
+
project: path.basename(path.dirname(filePath)),
|
|
208
|
+
title: humanMsgs[0]?.content.slice(0, 80) || "Cursor session",
|
|
209
|
+
messageCount: turns.length,
|
|
210
|
+
humanMessages: humanMsgs.length,
|
|
211
|
+
aiMessages: aiMsgs.length,
|
|
212
|
+
preview: humanMsgs[0]?.content.slice(0, 200) || "",
|
|
213
|
+
filePath,
|
|
214
|
+
modifiedAt: stats?.mtime || new Date(),
|
|
215
|
+
sizeBytes: stats?.size || 0,
|
|
216
|
+
turns,
|
|
217
|
+
};
|
|
218
|
+
},
|
|
219
|
+
};
|
|
@@ -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,177 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { getHome, getPlatform } from "../lib/platform.js";
|
|
3
|
+
import { listFiles, listDirs, safeReadJson, safeStats } from "../lib/fs-utils.js";
|
|
4
|
+
// VS Code Copilot Chat stores conversations in:
|
|
5
|
+
// macOS: ~/Library/Application Support/Code/User/workspaceStorage/<hash>/github.copilot-chat/
|
|
6
|
+
// Windows: %APPDATA%/Code/User/workspaceStorage/<hash>/github.copilot-chat/
|
|
7
|
+
// Linux: ~/.config/Code/User/workspaceStorage/<hash>/github.copilot-chat/
|
|
8
|
+
//
|
|
9
|
+
// Also checks VS Code Insiders and VSCodium paths
|
|
10
|
+
export const vscodeCopilotScanner = {
|
|
11
|
+
name: "VS Code Copilot Chat",
|
|
12
|
+
sourceType: "vscode-copilot",
|
|
13
|
+
description: "GitHub Copilot Chat sessions in VS Code",
|
|
14
|
+
getSessionDirs() {
|
|
15
|
+
const home = getHome();
|
|
16
|
+
const platform = getPlatform();
|
|
17
|
+
const candidates = [];
|
|
18
|
+
const codeVariants = ["Code", "Code - Insiders", "VSCodium"];
|
|
19
|
+
for (const variant of codeVariants) {
|
|
20
|
+
if (platform === "macos") {
|
|
21
|
+
candidates.push(path.join(home, "Library", "Application Support", variant, "User", "workspaceStorage"));
|
|
22
|
+
candidates.push(path.join(home, "Library", "Application Support", variant, "User", "globalStorage", "github.copilot-chat"));
|
|
23
|
+
}
|
|
24
|
+
else if (platform === "windows") {
|
|
25
|
+
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
26
|
+
candidates.push(path.join(appData, variant, "User", "workspaceStorage"));
|
|
27
|
+
candidates.push(path.join(appData, variant, "User", "globalStorage", "github.copilot-chat"));
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
candidates.push(path.join(home, ".config", variant, "User", "workspaceStorage"));
|
|
31
|
+
candidates.push(path.join(home, ".config", variant, "User", "globalStorage", "github.copilot-chat"));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return candidates.filter((d) => {
|
|
35
|
+
try {
|
|
36
|
+
return require("fs").existsSync(d);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
scan(limit) {
|
|
44
|
+
const sessions = [];
|
|
45
|
+
const dirs = this.getSessionDirs();
|
|
46
|
+
for (const baseDir of dirs) {
|
|
47
|
+
// Check globalStorage copilot-chat directory
|
|
48
|
+
if (baseDir.includes("globalStorage")) {
|
|
49
|
+
const jsonFiles = listFiles(baseDir, [".json"]);
|
|
50
|
+
for (const filePath of jsonFiles) {
|
|
51
|
+
const session = tryParseConversationFile(filePath);
|
|
52
|
+
if (session)
|
|
53
|
+
sessions.push(session);
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
// Check workspaceStorage — each hash dir may have a copilot-chat subfolder
|
|
58
|
+
const hashDirs = listDirs(baseDir);
|
|
59
|
+
for (const hashDir of hashDirs) {
|
|
60
|
+
const copilotDir = path.join(hashDir, "github.copilot-chat");
|
|
61
|
+
const jsonFiles = listFiles(copilotDir, [".json"]);
|
|
62
|
+
for (const filePath of jsonFiles) {
|
|
63
|
+
const session = tryParseConversationFile(filePath);
|
|
64
|
+
if (session)
|
|
65
|
+
sessions.push(session);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return sessions.slice(0, limit);
|
|
70
|
+
},
|
|
71
|
+
parse(filePath, maxTurns) {
|
|
72
|
+
const data = safeReadJson(filePath);
|
|
73
|
+
if (!data)
|
|
74
|
+
return null;
|
|
75
|
+
const stats = safeStats(filePath);
|
|
76
|
+
const turns = extractCopilotTurns(data, maxTurns);
|
|
77
|
+
if (turns.length === 0)
|
|
78
|
+
return null;
|
|
79
|
+
const humanMsgs = turns.filter((t) => t.role === "human");
|
|
80
|
+
const aiMsgs = turns.filter((t) => t.role === "assistant");
|
|
81
|
+
return {
|
|
82
|
+
id: path.basename(filePath, ".json"),
|
|
83
|
+
source: "vscode-copilot",
|
|
84
|
+
project: path.basename(path.dirname(filePath)),
|
|
85
|
+
title: humanMsgs[0]?.content.slice(0, 80) || "Copilot Chat session",
|
|
86
|
+
messageCount: turns.length,
|
|
87
|
+
humanMessages: humanMsgs.length,
|
|
88
|
+
aiMessages: aiMsgs.length,
|
|
89
|
+
preview: humanMsgs[0]?.content.slice(0, 200) || "",
|
|
90
|
+
filePath,
|
|
91
|
+
modifiedAt: stats?.mtime || new Date(),
|
|
92
|
+
sizeBytes: stats?.size || 0,
|
|
93
|
+
turns,
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
function tryParseConversationFile(filePath) {
|
|
98
|
+
const stats = safeStats(filePath);
|
|
99
|
+
if (!stats || stats.size < 100)
|
|
100
|
+
return null;
|
|
101
|
+
const data = safeReadJson(filePath);
|
|
102
|
+
if (!data)
|
|
103
|
+
return null;
|
|
104
|
+
const turns = extractCopilotTurns(data);
|
|
105
|
+
if (turns.length < 2)
|
|
106
|
+
return null;
|
|
107
|
+
const humanMsgs = turns.filter((t) => t.role === "human");
|
|
108
|
+
const preview = humanMsgs[0]?.content.slice(0, 200) || "(copilot session)";
|
|
109
|
+
return {
|
|
110
|
+
id: path.basename(filePath, ".json"),
|
|
111
|
+
source: "vscode-copilot",
|
|
112
|
+
project: path.basename(path.dirname(filePath)),
|
|
113
|
+
title: preview.slice(0, 80),
|
|
114
|
+
messageCount: turns.length,
|
|
115
|
+
humanMessages: humanMsgs.length,
|
|
116
|
+
aiMessages: turns.length - humanMsgs.length,
|
|
117
|
+
preview,
|
|
118
|
+
filePath,
|
|
119
|
+
modifiedAt: stats.mtime,
|
|
120
|
+
sizeBytes: stats.size,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function extractCopilotTurns(data, maxTurns) {
|
|
124
|
+
const turns = [];
|
|
125
|
+
// Copilot Chat format: { conversations: [{ turns: [{ request: ..., response: ... }] }] }
|
|
126
|
+
if (Array.isArray(data.conversations)) {
|
|
127
|
+
for (const conv of data.conversations) {
|
|
128
|
+
if (!conv || typeof conv !== "object")
|
|
129
|
+
continue;
|
|
130
|
+
const c = conv;
|
|
131
|
+
if (!Array.isArray(c.turns))
|
|
132
|
+
continue;
|
|
133
|
+
for (const turn of c.turns) {
|
|
134
|
+
if (maxTurns && turns.length >= maxTurns)
|
|
135
|
+
break;
|
|
136
|
+
const t = turn;
|
|
137
|
+
if (t.request && typeof t.request === "string") {
|
|
138
|
+
turns.push({ role: "human", content: t.request });
|
|
139
|
+
}
|
|
140
|
+
if (t.response && typeof t.response === "string") {
|
|
141
|
+
turns.push({ role: "assistant", content: t.response });
|
|
142
|
+
}
|
|
143
|
+
// Alternative format
|
|
144
|
+
if (t.message && typeof t.message === "string") {
|
|
145
|
+
turns.push({
|
|
146
|
+
role: t.role === "user" ? "human" : "assistant",
|
|
147
|
+
content: t.message,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return turns;
|
|
153
|
+
}
|
|
154
|
+
// Flat messages format
|
|
155
|
+
const msgArrays = [data.messages, data.history, data.entries];
|
|
156
|
+
for (const arr of msgArrays) {
|
|
157
|
+
if (!Array.isArray(arr))
|
|
158
|
+
continue;
|
|
159
|
+
for (const msg of arr) {
|
|
160
|
+
if (maxTurns && turns.length >= maxTurns)
|
|
161
|
+
break;
|
|
162
|
+
if (!msg || typeof msg !== "object")
|
|
163
|
+
continue;
|
|
164
|
+
const m = msg;
|
|
165
|
+
const content = (m.content || m.text || m.message);
|
|
166
|
+
if (typeof content !== "string")
|
|
167
|
+
continue;
|
|
168
|
+
turns.push({
|
|
169
|
+
role: m.role === "user" ? "human" : "assistant",
|
|
170
|
+
content,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (turns.length > 0)
|
|
174
|
+
return turns;
|
|
175
|
+
}
|
|
176
|
+
return turns;
|
|
177
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Warp Terminal AI chat is cloud-based and does NOT store conversation
|
|
2
|
+
// history locally. Verified on macOS — ~/Library/Application Support/dev.warp.Warp-Stable/
|
|
3
|
+
// contains only autoupdate data and logs, no AI chat files.
|
|
4
|
+
//
|
|
5
|
+
// This scanner is kept as a stub so codemolt_status can report it as unsupported.
|
|
6
|
+
export const warpScanner = {
|
|
7
|
+
name: "Warp Terminal",
|
|
8
|
+
sourceType: "warp",
|
|
9
|
+
description: "Warp Terminal (AI chat is cloud-only, no local history)",
|
|
10
|
+
getSessionDirs() {
|
|
11
|
+
// Warp does not store AI chat locally
|
|
12
|
+
return [];
|
|
13
|
+
},
|
|
14
|
+
scan(_limit) {
|
|
15
|
+
return [];
|
|
16
|
+
},
|
|
17
|
+
parse(_filePath, _maxTurns) {
|
|
18
|
+
return null;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import { getHome, getPlatform } from "../lib/platform.js";
|
|
4
|
+
import { listDirs, safeReadJson, safeStats, extractProjectDescription } from "../lib/fs-utils.js";
|
|
5
|
+
export const windsurfScanner = {
|
|
6
|
+
name: "Windsurf",
|
|
7
|
+
sourceType: "windsurf",
|
|
8
|
+
description: "Windsurf (Codeium) Cascade chat sessions (SQLite)",
|
|
9
|
+
getSessionDirs() {
|
|
10
|
+
const home = getHome();
|
|
11
|
+
const platform = getPlatform();
|
|
12
|
+
const candidates = [];
|
|
13
|
+
if (platform === "macos") {
|
|
14
|
+
candidates.push(path.join(home, "Library", "Application Support", "Windsurf", "User", "workspaceStorage"));
|
|
15
|
+
}
|
|
16
|
+
else if (platform === "windows") {
|
|
17
|
+
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
18
|
+
candidates.push(path.join(appData, "Windsurf", "User", "workspaceStorage"));
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
candidates.push(path.join(home, ".config", "Windsurf", "User", "workspaceStorage"));
|
|
22
|
+
}
|
|
23
|
+
return candidates.filter((d) => {
|
|
24
|
+
try {
|
|
25
|
+
return fs.existsSync(d);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
scan(limit) {
|
|
33
|
+
const sessions = [];
|
|
34
|
+
const dirs = this.getSessionDirs();
|
|
35
|
+
for (const baseDir of dirs) {
|
|
36
|
+
const workspaceDirs = listDirs(baseDir);
|
|
37
|
+
for (const wsDir of workspaceDirs) {
|
|
38
|
+
const dbPath = path.join(wsDir, "state.vscdb");
|
|
39
|
+
if (!fs.existsSync(dbPath))
|
|
40
|
+
continue;
|
|
41
|
+
// Read workspace.json to get project path
|
|
42
|
+
const wsJson = safeReadJson(path.join(wsDir, "workspace.json"));
|
|
43
|
+
let projectPath;
|
|
44
|
+
if (wsJson?.folder) {
|
|
45
|
+
try {
|
|
46
|
+
projectPath = decodeURIComponent(new URL(wsJson.folder).pathname);
|
|
47
|
+
}
|
|
48
|
+
catch { /* ignore */ }
|
|
49
|
+
}
|
|
50
|
+
const project = projectPath ? path.basename(projectPath) : path.basename(wsDir);
|
|
51
|
+
const projectDescription = projectPath
|
|
52
|
+
? extractProjectDescription(projectPath) || undefined
|
|
53
|
+
: undefined;
|
|
54
|
+
// Read chat sessions from SQLite
|
|
55
|
+
const chatData = readVscdbChatSessions(dbPath);
|
|
56
|
+
if (!chatData || Object.keys(chatData.entries).length === 0)
|
|
57
|
+
continue;
|
|
58
|
+
for (const [sessionId, entry] of Object.entries(chatData.entries)) {
|
|
59
|
+
const messages = extractVscdbMessages(entry);
|
|
60
|
+
if (messages.length < 2)
|
|
61
|
+
continue;
|
|
62
|
+
const humanMsgs = messages.filter((m) => m.role === "human");
|
|
63
|
+
const preview = humanMsgs[0]?.content.slice(0, 200) || "(windsurf session)";
|
|
64
|
+
const dbStats = safeStats(dbPath);
|
|
65
|
+
sessions.push({
|
|
66
|
+
id: sessionId,
|
|
67
|
+
source: "windsurf",
|
|
68
|
+
project,
|
|
69
|
+
projectPath,
|
|
70
|
+
projectDescription,
|
|
71
|
+
title: preview.slice(0, 80),
|
|
72
|
+
messageCount: messages.length,
|
|
73
|
+
humanMessages: humanMsgs.length,
|
|
74
|
+
aiMessages: messages.length - humanMsgs.length,
|
|
75
|
+
preview,
|
|
76
|
+
filePath: dbPath, // point to the vscdb file
|
|
77
|
+
modifiedAt: dbStats?.mtime || new Date(),
|
|
78
|
+
sizeBytes: dbStats?.size || 0,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
|
|
84
|
+
return sessions.slice(0, limit);
|
|
85
|
+
},
|
|
86
|
+
parse(filePath, maxTurns) {
|
|
87
|
+
const chatData = readVscdbChatSessions(filePath);
|
|
88
|
+
if (!chatData)
|
|
89
|
+
return null;
|
|
90
|
+
const stats = safeStats(filePath);
|
|
91
|
+
// Combine all entries' messages
|
|
92
|
+
const allTurns = [];
|
|
93
|
+
for (const entry of Object.values(chatData.entries)) {
|
|
94
|
+
const msgs = extractVscdbMessages(entry);
|
|
95
|
+
allTurns.push(...msgs);
|
|
96
|
+
}
|
|
97
|
+
const turns = maxTurns ? allTurns.slice(0, maxTurns) : allTurns;
|
|
98
|
+
if (turns.length === 0)
|
|
99
|
+
return null;
|
|
100
|
+
const humanMsgs = turns.filter((t) => t.role === "human");
|
|
101
|
+
const aiMsgs = turns.filter((t) => t.role === "assistant");
|
|
102
|
+
return {
|
|
103
|
+
id: path.basename(path.dirname(filePath)),
|
|
104
|
+
source: "windsurf",
|
|
105
|
+
project: path.basename(path.dirname(filePath)),
|
|
106
|
+
title: humanMsgs[0]?.content.slice(0, 80) || "Windsurf session",
|
|
107
|
+
messageCount: turns.length,
|
|
108
|
+
humanMessages: humanMsgs.length,
|
|
109
|
+
aiMessages: aiMsgs.length,
|
|
110
|
+
preview: humanMsgs[0]?.content.slice(0, 200) || "",
|
|
111
|
+
filePath,
|
|
112
|
+
modifiedAt: stats?.mtime || new Date(),
|
|
113
|
+
sizeBytes: stats?.size || 0,
|
|
114
|
+
turns,
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
function readVscdbChatSessions(dbPath) {
|
|
119
|
+
try {
|
|
120
|
+
const Database = require("better-sqlite3");
|
|
121
|
+
const db = new Database(dbPath, { readonly: true });
|
|
122
|
+
const row = db.prepare("SELECT value FROM ItemTable WHERE key = 'chat.ChatSessionStore.index'").get();
|
|
123
|
+
db.close();
|
|
124
|
+
if (!row?.value)
|
|
125
|
+
return null;
|
|
126
|
+
const valueStr = typeof row.value === "string"
|
|
127
|
+
? row.value
|
|
128
|
+
: row.value.toString("utf-8");
|
|
129
|
+
return JSON.parse(valueStr);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function extractVscdbMessages(entry) {
|
|
136
|
+
const turns = [];
|
|
137
|
+
if (Array.isArray(entry.messages)) {
|
|
138
|
+
for (const msg of entry.messages) {
|
|
139
|
+
if (!msg || typeof msg !== "object")
|
|
140
|
+
continue;
|
|
141
|
+
const content = msg.content || msg.text;
|
|
142
|
+
if (typeof content !== "string" || !content.trim())
|
|
143
|
+
continue;
|
|
144
|
+
turns.push({
|
|
145
|
+
role: msg.role === "user" || msg.role === "human" ? "human" : "assistant",
|
|
146
|
+
content,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return turns;
|
|
150
|
+
}
|
|
151
|
+
// Try other common keys
|
|
152
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
153
|
+
if (!Array.isArray(value))
|
|
154
|
+
continue;
|
|
155
|
+
for (const item of value) {
|
|
156
|
+
if (!item || typeof item !== "object")
|
|
157
|
+
continue;
|
|
158
|
+
const m = item;
|
|
159
|
+
const content = (m.content || m.text);
|
|
160
|
+
if (typeof content !== "string" || !content.trim())
|
|
161
|
+
continue;
|
|
162
|
+
turns.push({
|
|
163
|
+
role: m.role === "user" || m.role === "human" ? "human" : "assistant",
|
|
164
|
+
content,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (turns.length > 0)
|
|
168
|
+
return turns;
|
|
169
|
+
}
|
|
170
|
+
return turns;
|
|
171
|
+
}
|