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,130 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { getHome } from "../lib/platform.js";
|
|
3
|
+
import { listFiles, safeReadFile, safeStats } from "../lib/fs-utils.js";
|
|
4
|
+
// Aider stores chat history in:
|
|
5
|
+
// <project>/.aider.chat.history.md (markdown format)
|
|
6
|
+
// <project>/.aider.input.history (readline history)
|
|
7
|
+
// ~/.aider/history/ (global history)
|
|
8
|
+
// Same paths on all platforms
|
|
9
|
+
export const aiderScanner = {
|
|
10
|
+
name: "Aider",
|
|
11
|
+
sourceType: "aider",
|
|
12
|
+
description: "Aider AI pair programming sessions",
|
|
13
|
+
getSessionDirs() {
|
|
14
|
+
const home = getHome();
|
|
15
|
+
const candidates = [
|
|
16
|
+
path.join(home, ".aider", "history"),
|
|
17
|
+
path.join(home, ".aider"),
|
|
18
|
+
];
|
|
19
|
+
return candidates.filter((d) => {
|
|
20
|
+
try {
|
|
21
|
+
return require("fs").existsSync(d);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
scan(limit) {
|
|
29
|
+
const sessions = [];
|
|
30
|
+
const dirs = this.getSessionDirs();
|
|
31
|
+
for (const dir of dirs) {
|
|
32
|
+
const mdFiles = listFiles(dir, [".md"], true);
|
|
33
|
+
for (const filePath of mdFiles) {
|
|
34
|
+
if (!path.basename(filePath).includes("aider"))
|
|
35
|
+
continue;
|
|
36
|
+
const stats = safeStats(filePath);
|
|
37
|
+
if (!stats || stats.size < 100)
|
|
38
|
+
continue;
|
|
39
|
+
const content = safeReadFile(filePath);
|
|
40
|
+
if (!content)
|
|
41
|
+
continue;
|
|
42
|
+
const { humanCount, aiCount, preview } = parseAiderMarkdown(content);
|
|
43
|
+
if (humanCount === 0)
|
|
44
|
+
continue;
|
|
45
|
+
sessions.push({
|
|
46
|
+
id: path.basename(filePath, ".md"),
|
|
47
|
+
source: "aider",
|
|
48
|
+
project: path.basename(path.dirname(filePath)),
|
|
49
|
+
title: preview.slice(0, 80) || "Aider session",
|
|
50
|
+
messageCount: humanCount + aiCount,
|
|
51
|
+
humanMessages: humanCount,
|
|
52
|
+
aiMessages: aiCount,
|
|
53
|
+
preview,
|
|
54
|
+
filePath,
|
|
55
|
+
modifiedAt: stats.mtime,
|
|
56
|
+
sizeBytes: stats.size,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return sessions.slice(0, limit);
|
|
61
|
+
},
|
|
62
|
+
parse(filePath, maxTurns) {
|
|
63
|
+
const content = safeReadFile(filePath);
|
|
64
|
+
if (!content)
|
|
65
|
+
return null;
|
|
66
|
+
const stats = safeStats(filePath);
|
|
67
|
+
const turns = parseAiderTurns(content, maxTurns);
|
|
68
|
+
if (turns.length === 0)
|
|
69
|
+
return null;
|
|
70
|
+
const humanMsgs = turns.filter((t) => t.role === "human");
|
|
71
|
+
const aiMsgs = turns.filter((t) => t.role === "assistant");
|
|
72
|
+
return {
|
|
73
|
+
id: path.basename(filePath, ".md"),
|
|
74
|
+
source: "aider",
|
|
75
|
+
project: path.basename(path.dirname(filePath)),
|
|
76
|
+
title: humanMsgs[0]?.content.slice(0, 80) || "Aider session",
|
|
77
|
+
messageCount: turns.length,
|
|
78
|
+
humanMessages: humanMsgs.length,
|
|
79
|
+
aiMessages: aiMsgs.length,
|
|
80
|
+
preview: humanMsgs[0]?.content.slice(0, 200) || "",
|
|
81
|
+
filePath,
|
|
82
|
+
modifiedAt: stats?.mtime || new Date(),
|
|
83
|
+
sizeBytes: stats?.size || 0,
|
|
84
|
+
turns,
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
function parseAiderMarkdown(content) {
|
|
89
|
+
// Aider chat history format:
|
|
90
|
+
// #### <user message>
|
|
91
|
+
// <assistant response>
|
|
92
|
+
const userBlocks = content.split(/^####\s+/m).filter(Boolean);
|
|
93
|
+
let humanCount = 0;
|
|
94
|
+
let aiCount = 0;
|
|
95
|
+
let preview = "";
|
|
96
|
+
for (const block of userBlocks) {
|
|
97
|
+
const lines = block.split("\n");
|
|
98
|
+
const firstLine = lines[0]?.trim();
|
|
99
|
+
if (firstLine) {
|
|
100
|
+
humanCount++;
|
|
101
|
+
if (!preview)
|
|
102
|
+
preview = firstLine.slice(0, 200);
|
|
103
|
+
// Everything after the first line is AI response
|
|
104
|
+
const rest = lines.slice(1).join("\n").trim();
|
|
105
|
+
if (rest)
|
|
106
|
+
aiCount++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { humanCount, aiCount, preview };
|
|
110
|
+
}
|
|
111
|
+
function parseAiderTurns(content, maxTurns) {
|
|
112
|
+
const turns = [];
|
|
113
|
+
const blocks = content.split(/^####\s+/m).filter(Boolean);
|
|
114
|
+
for (const block of blocks) {
|
|
115
|
+
if (maxTurns && turns.length >= maxTurns)
|
|
116
|
+
break;
|
|
117
|
+
const lines = block.split("\n");
|
|
118
|
+
const userMsg = lines[0]?.trim();
|
|
119
|
+
if (userMsg) {
|
|
120
|
+
turns.push({ role: "human", content: userMsg });
|
|
121
|
+
const aiResponse = lines.slice(1).join("\n").trim();
|
|
122
|
+
if (aiResponse) {
|
|
123
|
+
if (maxTurns && turns.length >= maxTurns)
|
|
124
|
+
break;
|
|
125
|
+
turns.push({ role: "assistant", content: aiResponse });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return turns;
|
|
130
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { getHome } from "../lib/platform.js";
|
|
3
|
+
import { listFiles, listDirs, safeStats, readJsonl, extractProjectDescription } from "../lib/fs-utils.js";
|
|
4
|
+
export const claudeCodeScanner = {
|
|
5
|
+
name: "Claude Code",
|
|
6
|
+
sourceType: "claude-code",
|
|
7
|
+
description: "Claude Code CLI sessions (~/.claude/projects/)",
|
|
8
|
+
getSessionDirs() {
|
|
9
|
+
const home = getHome();
|
|
10
|
+
const candidates = [path.join(home, ".claude", "projects")];
|
|
11
|
+
return candidates.filter((d) => {
|
|
12
|
+
try {
|
|
13
|
+
return require("fs").existsSync(d);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
scan(limit) {
|
|
21
|
+
const sessions = [];
|
|
22
|
+
const dirs = this.getSessionDirs();
|
|
23
|
+
for (const baseDir of dirs) {
|
|
24
|
+
const projectDirs = listDirs(baseDir);
|
|
25
|
+
for (const projectDir of projectDirs) {
|
|
26
|
+
const project = path.basename(projectDir);
|
|
27
|
+
const files = listFiles(projectDir, [".jsonl"]);
|
|
28
|
+
for (const filePath of files) {
|
|
29
|
+
const stats = safeStats(filePath);
|
|
30
|
+
if (!stats)
|
|
31
|
+
continue;
|
|
32
|
+
const lines = readJsonl(filePath);
|
|
33
|
+
if (lines.length < 3)
|
|
34
|
+
continue;
|
|
35
|
+
const humanMsgs = lines.filter((l) => l.type === "user");
|
|
36
|
+
const aiMsgs = lines.filter((l) => l.type === "assistant");
|
|
37
|
+
// Extract cwd (project path) from first message that has it
|
|
38
|
+
const cwdLine = lines.find((l) => l.cwd);
|
|
39
|
+
let projectPath = cwdLine?.cwd || null;
|
|
40
|
+
// Fallback: derive from directory name (e.g. "-Users-zhaoyifei-Foo" → "/Users/zhaoyifei/Foo")
|
|
41
|
+
if (!projectPath && project.startsWith("-")) {
|
|
42
|
+
projectPath = decodeClaudeProjectDir(project);
|
|
43
|
+
}
|
|
44
|
+
const projectName = projectPath ? path.basename(projectPath) : project;
|
|
45
|
+
// Get project description from README/package.json
|
|
46
|
+
const projectDescription = projectPath
|
|
47
|
+
? extractProjectDescription(projectPath)
|
|
48
|
+
: null;
|
|
49
|
+
let preview = "";
|
|
50
|
+
for (const msg of humanMsgs.slice(0, 8)) {
|
|
51
|
+
const content = extractContent(msg);
|
|
52
|
+
if (!content || content.length < 10)
|
|
53
|
+
continue;
|
|
54
|
+
// Skip system-like messages and slash commands
|
|
55
|
+
if (content.startsWith("<local-command-caveat>"))
|
|
56
|
+
continue;
|
|
57
|
+
if (content.startsWith("<environment_context>"))
|
|
58
|
+
continue;
|
|
59
|
+
if (content.startsWith("<command-name>"))
|
|
60
|
+
continue;
|
|
61
|
+
preview = content.slice(0, 200);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
sessions.push({
|
|
65
|
+
id: path.basename(filePath, ".jsonl"),
|
|
66
|
+
source: "claude-code",
|
|
67
|
+
project: projectName,
|
|
68
|
+
projectPath: projectPath || undefined,
|
|
69
|
+
projectDescription: projectDescription || undefined,
|
|
70
|
+
title: preview.slice(0, 80) || `Claude session in ${projectName}`,
|
|
71
|
+
messageCount: humanMsgs.length + aiMsgs.length,
|
|
72
|
+
humanMessages: humanMsgs.length,
|
|
73
|
+
aiMessages: aiMsgs.length,
|
|
74
|
+
preview: preview || "(no preview)",
|
|
75
|
+
filePath,
|
|
76
|
+
modifiedAt: stats.mtime,
|
|
77
|
+
sizeBytes: stats.size,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Sort by modification time (newest first), then apply limit
|
|
83
|
+
sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
|
|
84
|
+
return sessions.slice(0, limit);
|
|
85
|
+
},
|
|
86
|
+
parse(filePath, maxTurns) {
|
|
87
|
+
const lines = readJsonl(filePath);
|
|
88
|
+
if (lines.length === 0)
|
|
89
|
+
return null;
|
|
90
|
+
const stats = safeStats(filePath);
|
|
91
|
+
const turns = [];
|
|
92
|
+
// Extract project info
|
|
93
|
+
const cwdLine = lines.find((l) => l.cwd);
|
|
94
|
+
const projectPath = cwdLine?.cwd || undefined;
|
|
95
|
+
const projectName = projectPath
|
|
96
|
+
? path.basename(projectPath)
|
|
97
|
+
: path.basename(path.dirname(filePath));
|
|
98
|
+
const projectDescription = projectPath
|
|
99
|
+
? extractProjectDescription(projectPath) || undefined
|
|
100
|
+
: undefined;
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
if (maxTurns && turns.length >= maxTurns)
|
|
103
|
+
break;
|
|
104
|
+
if (line.type !== "user" && line.type !== "assistant")
|
|
105
|
+
continue;
|
|
106
|
+
const content = extractContent(line);
|
|
107
|
+
if (!content)
|
|
108
|
+
continue;
|
|
109
|
+
const role = line.type === "user" ? "human" : "assistant";
|
|
110
|
+
turns.push({
|
|
111
|
+
role,
|
|
112
|
+
content,
|
|
113
|
+
timestamp: line.timestamp ? new Date(line.timestamp) : undefined,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const humanMsgs = turns.filter((t) => t.role === "human");
|
|
117
|
+
const aiMsgs = turns.filter((t) => t.role === "assistant");
|
|
118
|
+
return {
|
|
119
|
+
id: path.basename(filePath, ".jsonl"),
|
|
120
|
+
source: "claude-code",
|
|
121
|
+
project: projectName,
|
|
122
|
+
projectPath,
|
|
123
|
+
projectDescription,
|
|
124
|
+
title: humanMsgs[0]?.content.slice(0, 80) || "Claude session",
|
|
125
|
+
messageCount: turns.length,
|
|
126
|
+
humanMessages: humanMsgs.length,
|
|
127
|
+
aiMessages: aiMsgs.length,
|
|
128
|
+
preview: humanMsgs[0]?.content.slice(0, 200) || "",
|
|
129
|
+
filePath,
|
|
130
|
+
modifiedAt: stats?.mtime || new Date(),
|
|
131
|
+
sizeBytes: stats?.size || 0,
|
|
132
|
+
turns,
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
// Decode Claude Code project directory name back to a real path.
|
|
137
|
+
// e.g. "-Users-zhaoyifei-VibeCodingWork-ai-code-forum" → "/Users/zhaoyifei/VibeCodingWork/ai-code-forum"
|
|
138
|
+
// The challenge: hyphens in the dir name could be path separators OR part of a folder name.
|
|
139
|
+
// Strategy: greedily build path segments, checking which paths actually exist on disk.
|
|
140
|
+
function decodeClaudeProjectDir(dirName) {
|
|
141
|
+
const fs = require("fs");
|
|
142
|
+
// Remove leading dash
|
|
143
|
+
const stripped = dirName.startsWith("-") ? dirName.slice(1) : dirName;
|
|
144
|
+
const parts = stripped.split("-");
|
|
145
|
+
let currentPath = "";
|
|
146
|
+
let i = 0;
|
|
147
|
+
while (i < parts.length) {
|
|
148
|
+
// Try progressively longer segments (greedy: longest existing path wins)
|
|
149
|
+
let bestMatch = "";
|
|
150
|
+
let bestLen = 0;
|
|
151
|
+
for (let end = parts.length; end > i; end--) {
|
|
152
|
+
const segment = parts.slice(i, end).join("-");
|
|
153
|
+
const candidate = currentPath + "/" + segment;
|
|
154
|
+
try {
|
|
155
|
+
if (fs.existsSync(candidate)) {
|
|
156
|
+
bestMatch = candidate;
|
|
157
|
+
bestLen = end - i;
|
|
158
|
+
break; // Found longest match
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch { /* ignore */ }
|
|
162
|
+
}
|
|
163
|
+
if (bestLen > 0) {
|
|
164
|
+
currentPath = bestMatch;
|
|
165
|
+
i += bestLen;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// No existing path found, just use single segment
|
|
169
|
+
currentPath += "/" + parts[i];
|
|
170
|
+
i++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return currentPath || null;
|
|
174
|
+
}
|
|
175
|
+
function extractContent(msg) {
|
|
176
|
+
if (!msg.message?.content)
|
|
177
|
+
return "";
|
|
178
|
+
if (typeof msg.message.content === "string")
|
|
179
|
+
return msg.message.content;
|
|
180
|
+
if (Array.isArray(msg.message.content)) {
|
|
181
|
+
return msg.message.content
|
|
182
|
+
.filter((c) => c.type === "text" && c.text)
|
|
183
|
+
.map((c) => c.text)
|
|
184
|
+
.join("\n");
|
|
185
|
+
}
|
|
186
|
+
return "";
|
|
187
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { getHome } from "../lib/platform.js";
|
|
3
|
+
import { listFiles, safeStats, readJsonl, extractProjectDescription } from "../lib/fs-utils.js";
|
|
4
|
+
export const codexScanner = {
|
|
5
|
+
name: "Codex (OpenAI CLI)",
|
|
6
|
+
sourceType: "codex",
|
|
7
|
+
description: "OpenAI Codex CLI sessions (~/.codex/)",
|
|
8
|
+
getSessionDirs() {
|
|
9
|
+
const home = getHome();
|
|
10
|
+
const candidates = [
|
|
11
|
+
path.join(home, ".codex", "sessions"),
|
|
12
|
+
path.join(home, ".codex", "archived_sessions"),
|
|
13
|
+
];
|
|
14
|
+
return candidates.filter((d) => {
|
|
15
|
+
try {
|
|
16
|
+
return require("fs").existsSync(d);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
scan(limit) {
|
|
24
|
+
const sessions = [];
|
|
25
|
+
const dirs = this.getSessionDirs();
|
|
26
|
+
for (const dir of dirs) {
|
|
27
|
+
// Recursive scan — sessions are in nested YYYY/MM/DD/ subdirectories
|
|
28
|
+
const files = listFiles(dir, [".jsonl"], true);
|
|
29
|
+
for (const filePath of files) {
|
|
30
|
+
const stats = safeStats(filePath);
|
|
31
|
+
if (!stats)
|
|
32
|
+
continue;
|
|
33
|
+
const lines = readJsonl(filePath);
|
|
34
|
+
if (lines.length < 3)
|
|
35
|
+
continue;
|
|
36
|
+
const messageTurns = extractCodexTurns(lines);
|
|
37
|
+
const humanMsgs = messageTurns.filter((t) => t.role === "human");
|
|
38
|
+
const aiMsgs = messageTurns.filter((t) => t.role === "assistant");
|
|
39
|
+
// Extract project path (cwd) from session metadata
|
|
40
|
+
const startLine = lines.find((l) => l.payload?.cwd);
|
|
41
|
+
const projectPath = startLine?.payload?.cwd || null;
|
|
42
|
+
const project = projectPath
|
|
43
|
+
? path.basename(projectPath)
|
|
44
|
+
: path.basename(dir);
|
|
45
|
+
const projectDescription = projectPath
|
|
46
|
+
? extractProjectDescription(projectPath)
|
|
47
|
+
: null;
|
|
48
|
+
const preview = humanMsgs[0]?.content.slice(0, 200) || "(codex session)";
|
|
49
|
+
sessions.push({
|
|
50
|
+
id: path.basename(filePath, ".jsonl"),
|
|
51
|
+
source: "codex",
|
|
52
|
+
project,
|
|
53
|
+
projectPath: projectPath || undefined,
|
|
54
|
+
projectDescription: projectDescription || undefined,
|
|
55
|
+
title: preview.slice(0, 80) || "Codex session",
|
|
56
|
+
messageCount: messageTurns.length,
|
|
57
|
+
humanMessages: humanMsgs.length,
|
|
58
|
+
aiMessages: aiMsgs.length,
|
|
59
|
+
preview,
|
|
60
|
+
filePath,
|
|
61
|
+
modifiedAt: stats.mtime,
|
|
62
|
+
sizeBytes: stats.size,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
|
|
67
|
+
return sessions.slice(0, limit);
|
|
68
|
+
},
|
|
69
|
+
parse(filePath, maxTurns) {
|
|
70
|
+
const lines = readJsonl(filePath);
|
|
71
|
+
if (lines.length === 0)
|
|
72
|
+
return null;
|
|
73
|
+
const stats = safeStats(filePath);
|
|
74
|
+
const allTurns = extractCodexTurns(lines);
|
|
75
|
+
const turns = maxTurns ? allTurns.slice(0, maxTurns) : allTurns;
|
|
76
|
+
if (turns.length === 0)
|
|
77
|
+
return null;
|
|
78
|
+
const humanMsgs = turns.filter((t) => t.role === "human");
|
|
79
|
+
const aiMsgs = turns.filter((t) => t.role === "assistant");
|
|
80
|
+
const startLine = lines.find((l) => l.payload?.cwd);
|
|
81
|
+
const projectPath = startLine?.payload?.cwd || undefined;
|
|
82
|
+
const project = projectPath
|
|
83
|
+
? path.basename(projectPath)
|
|
84
|
+
: path.basename(path.dirname(filePath));
|
|
85
|
+
const projectDescription = projectPath
|
|
86
|
+
? extractProjectDescription(projectPath) || undefined
|
|
87
|
+
: undefined;
|
|
88
|
+
return {
|
|
89
|
+
id: path.basename(filePath, ".jsonl"),
|
|
90
|
+
source: "codex",
|
|
91
|
+
project,
|
|
92
|
+
projectPath,
|
|
93
|
+
projectDescription,
|
|
94
|
+
title: humanMsgs[0]?.content.slice(0, 80) || "Codex session",
|
|
95
|
+
messageCount: turns.length,
|
|
96
|
+
humanMessages: humanMsgs.length,
|
|
97
|
+
aiMessages: aiMsgs.length,
|
|
98
|
+
preview: humanMsgs[0]?.content.slice(0, 200) || "",
|
|
99
|
+
filePath,
|
|
100
|
+
modifiedAt: stats?.mtime || new Date(),
|
|
101
|
+
sizeBytes: stats?.size || 0,
|
|
102
|
+
turns,
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
// Extract conversation turns from Codex JSONL format
|
|
107
|
+
function extractCodexTurns(lines) {
|
|
108
|
+
const turns = [];
|
|
109
|
+
for (const line of lines) {
|
|
110
|
+
if (!line.payload)
|
|
111
|
+
continue;
|
|
112
|
+
const p = line.payload;
|
|
113
|
+
// Only process message-type payloads
|
|
114
|
+
if (p.type !== "message")
|
|
115
|
+
continue;
|
|
116
|
+
// Extract text from content array
|
|
117
|
+
const textParts = (p.content || [])
|
|
118
|
+
.filter((c) => c.text && c.type !== "input_text" || c.type === "input_text")
|
|
119
|
+
.map((c) => c.text || "")
|
|
120
|
+
.filter(Boolean);
|
|
121
|
+
const content = textParts.join("\n").trim();
|
|
122
|
+
if (!content)
|
|
123
|
+
continue;
|
|
124
|
+
// Skip developer/system messages — they are AGENTS.md instructions, not conversation
|
|
125
|
+
if (p.role === "developer" || p.role === "system")
|
|
126
|
+
continue;
|
|
127
|
+
// Skip system-like user messages (AGENTS.md, environment context, etc.)
|
|
128
|
+
if (p.role === "user" && (content.startsWith("# AGENTS.md") ||
|
|
129
|
+
content.startsWith("<environment_context>") ||
|
|
130
|
+
content.startsWith("<permissions") ||
|
|
131
|
+
content.startsWith("<app-context>") ||
|
|
132
|
+
content.startsWith("<collaboration_mode>")))
|
|
133
|
+
continue;
|
|
134
|
+
const role = p.role === "user" ? "human" : "assistant";
|
|
135
|
+
turns.push({
|
|
136
|
+
role,
|
|
137
|
+
content,
|
|
138
|
+
timestamp: line.timestamp ? new Date(line.timestamp) : undefined,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return turns;
|
|
142
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { getHome, getPlatform } from "../lib/platform.js";
|
|
3
|
+
import { listFiles, safeReadJson, safeStats } from "../lib/fs-utils.js";
|
|
4
|
+
// Continue.dev stores sessions in:
|
|
5
|
+
// ~/.continue/sessions/*.json
|
|
6
|
+
// macOS: ~/Library/Application Support/Continue/sessions/
|
|
7
|
+
// Windows: %APPDATA%/Continue/sessions/
|
|
8
|
+
// Linux: ~/.config/continue/sessions/
|
|
9
|
+
export const continueDevScanner = {
|
|
10
|
+
name: "Continue.dev",
|
|
11
|
+
sourceType: "continue",
|
|
12
|
+
description: "Continue.dev AI coding assistant sessions",
|
|
13
|
+
getSessionDirs() {
|
|
14
|
+
const home = getHome();
|
|
15
|
+
const platform = getPlatform();
|
|
16
|
+
const candidates = [];
|
|
17
|
+
candidates.push(path.join(home, ".continue", "sessions"));
|
|
18
|
+
if (platform === "macos") {
|
|
19
|
+
candidates.push(path.join(home, "Library", "Application Support", "Continue", "sessions"));
|
|
20
|
+
}
|
|
21
|
+
else if (platform === "windows") {
|
|
22
|
+
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
23
|
+
candidates.push(path.join(appData, "Continue", "sessions"));
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
candidates.push(path.join(home, ".config", "continue", "sessions"));
|
|
27
|
+
}
|
|
28
|
+
return candidates.filter((d) => {
|
|
29
|
+
try {
|
|
30
|
+
return require("fs").existsSync(d);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
scan(limit) {
|
|
38
|
+
const sessions = [];
|
|
39
|
+
const dirs = this.getSessionDirs();
|
|
40
|
+
for (const dir of dirs) {
|
|
41
|
+
const jsonFiles = listFiles(dir, [".json"]);
|
|
42
|
+
for (const filePath of jsonFiles) {
|
|
43
|
+
const stats = safeStats(filePath);
|
|
44
|
+
if (!stats || stats.size < 100)
|
|
45
|
+
continue;
|
|
46
|
+
const data = safeReadJson(filePath);
|
|
47
|
+
if (!data)
|
|
48
|
+
continue;
|
|
49
|
+
const turns = extractContinueTurns(data);
|
|
50
|
+
if (turns.length < 2)
|
|
51
|
+
continue;
|
|
52
|
+
const humanMsgs = turns.filter((t) => t.role === "human");
|
|
53
|
+
const preview = humanMsgs[0]?.content.slice(0, 200) || "(continue session)";
|
|
54
|
+
sessions.push({
|
|
55
|
+
id: path.basename(filePath, ".json"),
|
|
56
|
+
source: "continue",
|
|
57
|
+
project: data.workspacePath || path.basename(path.dirname(filePath)),
|
|
58
|
+
title: data.title || preview.slice(0, 80),
|
|
59
|
+
messageCount: turns.length,
|
|
60
|
+
humanMessages: humanMsgs.length,
|
|
61
|
+
aiMessages: turns.length - humanMsgs.length,
|
|
62
|
+
preview,
|
|
63
|
+
filePath,
|
|
64
|
+
modifiedAt: stats.mtime,
|
|
65
|
+
sizeBytes: stats.size,
|
|
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 = extractContinueTurns(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: "continue",
|
|
84
|
+
project: data.workspacePath || path.basename(path.dirname(filePath)),
|
|
85
|
+
title: data.title || humanMsgs[0]?.content.slice(0, 80) || "Continue 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 extractContinueTurns(data, maxTurns) {
|
|
98
|
+
const turns = [];
|
|
99
|
+
// Continue format: { history: [{ role: "user"|"assistant", content: "..." }] }
|
|
100
|
+
const msgArrays = [data.history, data.messages, data.steps];
|
|
101
|
+
for (const arr of msgArrays) {
|
|
102
|
+
if (!Array.isArray(arr))
|
|
103
|
+
continue;
|
|
104
|
+
for (const msg of arr) {
|
|
105
|
+
if (maxTurns && turns.length >= maxTurns)
|
|
106
|
+
break;
|
|
107
|
+
if (!msg || typeof msg !== "object")
|
|
108
|
+
continue;
|
|
109
|
+
const m = msg;
|
|
110
|
+
// Steps format: { name: "UserInput", description: "..." }
|
|
111
|
+
if (m.name === "UserInput" && typeof m.description === "string") {
|
|
112
|
+
turns.push({ role: "human", content: m.description });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (m.name === "DefaultModelEditCodeStep" || m.name === "ChatModelResponse") {
|
|
116
|
+
if (typeof m.description === "string") {
|
|
117
|
+
turns.push({ role: "assistant", content: m.description });
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// Standard message format
|
|
122
|
+
const content = (m.content || m.text || m.message);
|
|
123
|
+
if (typeof content !== "string")
|
|
124
|
+
continue;
|
|
125
|
+
turns.push({
|
|
126
|
+
role: m.role === "user" ? "human" : "assistant",
|
|
127
|
+
content,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (turns.length > 0)
|
|
131
|
+
return turns;
|
|
132
|
+
}
|
|
133
|
+
return turns;
|
|
134
|
+
}
|