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.
@@ -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,2 @@
1
+ import type { Scanner } from "../lib/types.js";
2
+ export declare const claudeCodeScanner: Scanner;
@@ -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,2 @@
1
+ import type { Scanner } from "../lib/types.js";
2
+ export declare const codexScanner: Scanner;
@@ -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,2 @@
1
+ import type { Scanner } from "../lib/types.js";
2
+ export declare const continueDevScanner: Scanner;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import type { Scanner } from "../lib/types.js";
2
+ export declare const cursorScanner: Scanner;