codemolt-mcp 0.4.1 → 0.5.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 +278 -355
- 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,225 @@
|
|
|
1
|
+
// Analyze a parsed session and extract structured insights
|
|
2
|
+
export function analyzeSession(session) {
|
|
3
|
+
const allContent = session.turns.map((t) => t.content).join("\n");
|
|
4
|
+
const humanContent = session.turns
|
|
5
|
+
.filter((t) => t.role === "human")
|
|
6
|
+
.map((t) => t.content)
|
|
7
|
+
.join("\n");
|
|
8
|
+
const aiContent = session.turns
|
|
9
|
+
.filter((t) => t.role === "assistant")
|
|
10
|
+
.map((t) => t.content)
|
|
11
|
+
.join("\n");
|
|
12
|
+
return {
|
|
13
|
+
summary: generateSummary(session),
|
|
14
|
+
topics: extractTopics(allContent),
|
|
15
|
+
languages: detectLanguages(allContent),
|
|
16
|
+
keyInsights: extractInsights(session.turns),
|
|
17
|
+
codeSnippets: extractCodeSnippets(allContent),
|
|
18
|
+
problems: extractProblems(humanContent),
|
|
19
|
+
solutions: extractSolutions(aiContent),
|
|
20
|
+
suggestedTitle: suggestTitle(session),
|
|
21
|
+
suggestedTags: suggestTags(allContent),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function generateSummary(session) {
|
|
25
|
+
const humanMsgs = session.turns.filter((t) => t.role === "human");
|
|
26
|
+
const topics = humanMsgs
|
|
27
|
+
.slice(0, 5)
|
|
28
|
+
.map((m) => m.content.slice(0, 100))
|
|
29
|
+
.join("; ");
|
|
30
|
+
return (`${session.source} session in project "${session.project}" with ` +
|
|
31
|
+
`${session.humanMessages} user messages and ${session.aiMessages} AI responses. ` +
|
|
32
|
+
`Topics discussed: ${topics}`);
|
|
33
|
+
}
|
|
34
|
+
function extractTopics(content) {
|
|
35
|
+
const topics = new Set();
|
|
36
|
+
// Common programming topics
|
|
37
|
+
const topicPatterns = [
|
|
38
|
+
[/\b(react|vue|angular|svelte|nextjs|next\.js|nuxt)\b/i, "frontend"],
|
|
39
|
+
[/\b(express|fastify|koa|nest\.?js|django|flask|rails)\b/i, "backend"],
|
|
40
|
+
[/\b(typescript|javascript|python|rust|go|java|c\+\+|ruby|swift|kotlin)\b/i, "programming-language"],
|
|
41
|
+
[/\b(docker|kubernetes|k8s|ci\/cd|deploy|devops)\b/i, "devops"],
|
|
42
|
+
[/\b(sql|postgres|mysql|mongodb|redis|database|prisma|drizzle)\b/i, "database"],
|
|
43
|
+
[/\b(test|jest|vitest|pytest|testing|spec|unit test)\b/i, "testing"],
|
|
44
|
+
[/\b(api|rest|graphql|grpc|websocket)\b/i, "api"],
|
|
45
|
+
[/\b(auth|jwt|oauth|session|login|password)\b/i, "authentication"],
|
|
46
|
+
[/\b(css|tailwind|styled|sass|scss|styling)\b/i, "styling"],
|
|
47
|
+
[/\b(git|merge|rebase|branch|commit)\b/i, "git"],
|
|
48
|
+
[/\b(performance|optimize|cache|lazy|memo)\b/i, "performance"],
|
|
49
|
+
[/\b(debug|error|bug|fix|issue|crash)\b/i, "debugging"],
|
|
50
|
+
[/\b(refactor|clean|architecture|pattern|design)\b/i, "architecture"],
|
|
51
|
+
[/\b(security|vulnerability|xss|csrf|injection)\b/i, "security"],
|
|
52
|
+
[/\b(ai|ml|llm|gpt|claude|model|prompt)\b/i, "ai-ml"],
|
|
53
|
+
];
|
|
54
|
+
for (const [pattern, topic] of topicPatterns) {
|
|
55
|
+
if (pattern.test(content)) {
|
|
56
|
+
topics.add(topic);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return Array.from(topics);
|
|
60
|
+
}
|
|
61
|
+
function detectLanguages(content) {
|
|
62
|
+
const langs = new Set();
|
|
63
|
+
const langPatterns = [
|
|
64
|
+
[/```(?:typescript|tsx?)\b/i, "TypeScript"],
|
|
65
|
+
[/```(?:javascript|jsx?)\b/i, "JavaScript"],
|
|
66
|
+
[/```python\b/i, "Python"],
|
|
67
|
+
[/```rust\b/i, "Rust"],
|
|
68
|
+
[/```go\b/i, "Go"],
|
|
69
|
+
[/```java\b/i, "Java"],
|
|
70
|
+
[/```(?:c\+\+|cpp)\b/i, "C++"],
|
|
71
|
+
[/```c\b/i, "C"],
|
|
72
|
+
[/```ruby\b/i, "Ruby"],
|
|
73
|
+
[/```swift\b/i, "Swift"],
|
|
74
|
+
[/```kotlin\b/i, "Kotlin"],
|
|
75
|
+
[/```(?:bash|sh|shell|zsh)\b/i, "Shell"],
|
|
76
|
+
[/```sql\b/i, "SQL"],
|
|
77
|
+
[/```html\b/i, "HTML"],
|
|
78
|
+
[/```css\b/i, "CSS"],
|
|
79
|
+
[/```yaml\b/i, "YAML"],
|
|
80
|
+
[/```json\b/i, "JSON"],
|
|
81
|
+
[/```(?:dockerfile|docker)\b/i, "Docker"],
|
|
82
|
+
];
|
|
83
|
+
for (const [pattern, lang] of langPatterns) {
|
|
84
|
+
if (pattern.test(content)) {
|
|
85
|
+
langs.add(lang);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Also detect from imports/keywords if no code blocks
|
|
89
|
+
if (langs.size === 0) {
|
|
90
|
+
if (/\bimport\s+.*\s+from\s+['"]/.test(content))
|
|
91
|
+
langs.add("JavaScript/TypeScript");
|
|
92
|
+
if (/\bdef\s+\w+\s*\(/.test(content))
|
|
93
|
+
langs.add("Python");
|
|
94
|
+
if (/\bfn\s+\w+\s*\(/.test(content))
|
|
95
|
+
langs.add("Rust");
|
|
96
|
+
if (/\bfunc\s+\w+\s*\(/.test(content))
|
|
97
|
+
langs.add("Go");
|
|
98
|
+
}
|
|
99
|
+
return Array.from(langs);
|
|
100
|
+
}
|
|
101
|
+
function extractInsights(turns) {
|
|
102
|
+
const insights = [];
|
|
103
|
+
for (let i = 0; i < turns.length; i++) {
|
|
104
|
+
const turn = turns[i];
|
|
105
|
+
if (turn.role !== "assistant")
|
|
106
|
+
continue;
|
|
107
|
+
const content = turn.content;
|
|
108
|
+
// Look for key insight patterns in AI responses
|
|
109
|
+
const patterns = [
|
|
110
|
+
/(?:the (?:issue|problem|bug|root cause) (?:is|was))\s+(.{20,150})/i,
|
|
111
|
+
/(?:the (?:solution|fix|answer) (?:is|was))\s+(.{20,150})/i,
|
|
112
|
+
/(?:you (?:should|need to|can))\s+(.{20,150})/i,
|
|
113
|
+
/(?:this (?:happens|occurs) because)\s+(.{20,150})/i,
|
|
114
|
+
/(?:(?:key|important) (?:thing|point|takeaway))\s+(.{20,150})/i,
|
|
115
|
+
/(?:TIL|Today I learned|Learned that)\s+(.{20,150})/i,
|
|
116
|
+
];
|
|
117
|
+
for (const pattern of patterns) {
|
|
118
|
+
const match = content.match(pattern);
|
|
119
|
+
if (match && match[1]) {
|
|
120
|
+
insights.push(match[1].trim().replace(/\.$/, ""));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Deduplicate and limit
|
|
125
|
+
return [...new Set(insights)].slice(0, 10);
|
|
126
|
+
}
|
|
127
|
+
function extractCodeSnippets(content) {
|
|
128
|
+
const snippets = [];
|
|
129
|
+
const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
|
|
130
|
+
let match;
|
|
131
|
+
while ((match = codeBlockRegex.exec(content)) !== null) {
|
|
132
|
+
const language = match[1] || "unknown";
|
|
133
|
+
const code = match[2].trim();
|
|
134
|
+
if (code.length < 10 || code.length > 2000)
|
|
135
|
+
continue;
|
|
136
|
+
// Get surrounding context (text before the code block)
|
|
137
|
+
const beforeIdx = Math.max(0, match.index - 200);
|
|
138
|
+
const context = content.slice(beforeIdx, match.index).trim().split("\n").pop() || "";
|
|
139
|
+
snippets.push({ language, code, context });
|
|
140
|
+
}
|
|
141
|
+
return snippets.slice(0, 10);
|
|
142
|
+
}
|
|
143
|
+
function extractProblems(humanContent) {
|
|
144
|
+
const problems = [];
|
|
145
|
+
const lines = humanContent.split("\n");
|
|
146
|
+
for (const line of lines) {
|
|
147
|
+
const trimmed = line.trim();
|
|
148
|
+
if (trimmed.length < 15 || trimmed.length > 300)
|
|
149
|
+
continue;
|
|
150
|
+
// Look for problem indicators
|
|
151
|
+
if (/\b(error|bug|issue|problem|broken|doesn't work|not working|failing|crash|wrong)\b/i.test(trimmed) &&
|
|
152
|
+
!trimmed.startsWith("//") &&
|
|
153
|
+
!trimmed.startsWith("#")) {
|
|
154
|
+
problems.push(trimmed);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return [...new Set(problems)].slice(0, 5);
|
|
158
|
+
}
|
|
159
|
+
function extractSolutions(aiContent) {
|
|
160
|
+
const solutions = [];
|
|
161
|
+
const sentences = aiContent.split(/[.!]\s+/);
|
|
162
|
+
for (const sentence of sentences) {
|
|
163
|
+
const trimmed = sentence.trim();
|
|
164
|
+
if (trimmed.length < 20 || trimmed.length > 300)
|
|
165
|
+
continue;
|
|
166
|
+
if (/\b(fix|solve|solution|resolve|instead|should|try|change|update|replace|use)\b/i.test(trimmed) &&
|
|
167
|
+
!/\b(error|bug|issue|problem)\b/i.test(trimmed)) {
|
|
168
|
+
solutions.push(trimmed);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return [...new Set(solutions)].slice(0, 5);
|
|
172
|
+
}
|
|
173
|
+
function suggestTitle(session) {
|
|
174
|
+
const firstHuman = session.turns.find((t) => t.role === "human");
|
|
175
|
+
if (!firstHuman)
|
|
176
|
+
return `${session.source} coding session`;
|
|
177
|
+
const content = firstHuman.content.slice(0, 100);
|
|
178
|
+
// Clean up for title
|
|
179
|
+
return content
|
|
180
|
+
.replace(/\n/g, " ")
|
|
181
|
+
.replace(/\s+/g, " ")
|
|
182
|
+
.trim();
|
|
183
|
+
}
|
|
184
|
+
function suggestTags(content) {
|
|
185
|
+
const tags = new Set();
|
|
186
|
+
// Detect specific technologies
|
|
187
|
+
const techPatterns = [
|
|
188
|
+
[/\breact\b/i, "react"],
|
|
189
|
+
[/\bnext\.?js\b/i, "nextjs"],
|
|
190
|
+
[/\btypescript\b/i, "typescript"],
|
|
191
|
+
[/\bpython\b/i, "python"],
|
|
192
|
+
[/\brust\b/i, "rust"],
|
|
193
|
+
[/\bdocker\b/i, "docker"],
|
|
194
|
+
[/\bprisma\b/i, "prisma"],
|
|
195
|
+
[/\btailwind\b/i, "tailwindcss"],
|
|
196
|
+
[/\bnode\.?js\b/i, "nodejs"],
|
|
197
|
+
[/\bgit\b/i, "git"],
|
|
198
|
+
[/\bpostgres\b/i, "postgresql"],
|
|
199
|
+
[/\bmongodb\b/i, "mongodb"],
|
|
200
|
+
[/\bredis\b/i, "redis"],
|
|
201
|
+
[/\baws\b/i, "aws"],
|
|
202
|
+
[/\bvue\b/i, "vue"],
|
|
203
|
+
[/\bangular\b/i, "angular"],
|
|
204
|
+
[/\bsvelte\b/i, "svelte"],
|
|
205
|
+
[/\bgraphql\b/i, "graphql"],
|
|
206
|
+
[/\bwebsocket\b/i, "websocket"],
|
|
207
|
+
];
|
|
208
|
+
for (const [pattern, tag] of techPatterns) {
|
|
209
|
+
if (pattern.test(content)) {
|
|
210
|
+
tags.add(tag);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Detect activity type
|
|
214
|
+
if (/\b(bug|fix|error|debug)\b/i.test(content))
|
|
215
|
+
tags.add("bug-fix");
|
|
216
|
+
if (/\b(refactor|clean|restructure)\b/i.test(content))
|
|
217
|
+
tags.add("refactoring");
|
|
218
|
+
if (/\b(performance|optimize|speed|cache)\b/i.test(content))
|
|
219
|
+
tags.add("performance");
|
|
220
|
+
if (/\b(test|spec|coverage)\b/i.test(content))
|
|
221
|
+
tags.add("testing");
|
|
222
|
+
if (/\b(deploy|ci|cd|pipeline)\b/i.test(content))
|
|
223
|
+
tags.add("devops");
|
|
224
|
+
return Array.from(tags).slice(0, 8);
|
|
225
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
export declare function safeReadFile(filePath: string): string | null;
|
|
3
|
+
export declare function safeReadJson<T = unknown>(filePath: string): T | null;
|
|
4
|
+
export declare function safeStats(filePath: string): fs.Stats | null;
|
|
5
|
+
export declare function listFiles(dir: string, extensions?: string[], recursive?: boolean): string[];
|
|
6
|
+
export declare function listDirs(dir: string): string[];
|
|
7
|
+
export declare function exists(p: string): boolean;
|
|
8
|
+
export declare function extractProjectDescription(projectPath: string): string | null;
|
|
9
|
+
export declare function readJsonl<T = unknown>(filePath: string): T[];
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
// Safely read a file, return null on error
|
|
4
|
+
export function safeReadFile(filePath) {
|
|
5
|
+
try {
|
|
6
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
// Safely read JSON file
|
|
13
|
+
export function safeReadJson(filePath) {
|
|
14
|
+
const content = safeReadFile(filePath);
|
|
15
|
+
if (!content)
|
|
16
|
+
return null;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(content);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Get file stats safely
|
|
25
|
+
export function safeStats(filePath) {
|
|
26
|
+
try {
|
|
27
|
+
return fs.statSync(filePath);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// List files in directory with extension filter
|
|
34
|
+
export function listFiles(dir, extensions, recursive = false) {
|
|
35
|
+
if (!fs.existsSync(dir))
|
|
36
|
+
return [];
|
|
37
|
+
const results = [];
|
|
38
|
+
try {
|
|
39
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const fullPath = path.join(dir, entry.name);
|
|
42
|
+
if (entry.isFile()) {
|
|
43
|
+
if (!extensions || extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
44
|
+
results.push(fullPath);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (entry.isDirectory() && recursive) {
|
|
48
|
+
results.push(...listFiles(fullPath, extensions, true));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Permission denied or other errors
|
|
54
|
+
}
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
// List subdirectories
|
|
58
|
+
export function listDirs(dir) {
|
|
59
|
+
if (!fs.existsSync(dir))
|
|
60
|
+
return [];
|
|
61
|
+
try {
|
|
62
|
+
return fs
|
|
63
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
64
|
+
.filter((e) => e.isDirectory())
|
|
65
|
+
.map((e) => path.join(dir, e.name));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Check if path exists
|
|
72
|
+
export function exists(p) {
|
|
73
|
+
try {
|
|
74
|
+
return fs.existsSync(p);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Extract project description from a project directory
|
|
81
|
+
// Reads README.md (first paragraph) and package.json (description field)
|
|
82
|
+
export function extractProjectDescription(projectPath) {
|
|
83
|
+
if (!projectPath || !fs.existsSync(projectPath))
|
|
84
|
+
return null;
|
|
85
|
+
// Try package.json first (most concise)
|
|
86
|
+
const pkgPath = path.join(projectPath, "package.json");
|
|
87
|
+
const pkg = safeReadJson(pkgPath);
|
|
88
|
+
if (pkg?.description) {
|
|
89
|
+
return pkg.description.slice(0, 200);
|
|
90
|
+
}
|
|
91
|
+
// Try README.md — extract first non-heading, non-empty paragraph
|
|
92
|
+
for (const readmeName of ["README.md", "readme.md", "Readme.md", "README.rst"]) {
|
|
93
|
+
const readmePath = path.join(projectPath, readmeName);
|
|
94
|
+
const content = safeReadFile(readmePath);
|
|
95
|
+
if (!content)
|
|
96
|
+
continue;
|
|
97
|
+
const lines = content.split("\n");
|
|
98
|
+
let desc = "";
|
|
99
|
+
for (const line of lines) {
|
|
100
|
+
const trimmed = line.trim();
|
|
101
|
+
if (!trimmed) {
|
|
102
|
+
if (desc)
|
|
103
|
+
break;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (trimmed.startsWith("#") || trimmed.startsWith("=") || trimmed.startsWith("-")) {
|
|
107
|
+
if (desc)
|
|
108
|
+
break;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (trimmed.startsWith("![") || trimmed.startsWith("<img"))
|
|
112
|
+
continue;
|
|
113
|
+
desc += (desc ? " " : "") + trimmed;
|
|
114
|
+
if (desc.length > 200)
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
if (desc.length > 10)
|
|
118
|
+
return desc.slice(0, 300);
|
|
119
|
+
}
|
|
120
|
+
// Try Cargo.toml, pyproject.toml etc.
|
|
121
|
+
const cargoPath = path.join(projectPath, "Cargo.toml");
|
|
122
|
+
const cargo = safeReadFile(cargoPath);
|
|
123
|
+
if (cargo) {
|
|
124
|
+
const match = cargo.match(/description\s*=\s*"([^"]+)"/);
|
|
125
|
+
if (match)
|
|
126
|
+
return match[1].slice(0, 200);
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
// Read JSONL file (one JSON object per line)
|
|
131
|
+
export function readJsonl(filePath) {
|
|
132
|
+
const content = safeReadFile(filePath);
|
|
133
|
+
if (!content)
|
|
134
|
+
return [];
|
|
135
|
+
return content
|
|
136
|
+
.split("\n")
|
|
137
|
+
.filter(Boolean)
|
|
138
|
+
.map((line) => {
|
|
139
|
+
try {
|
|
140
|
+
return JSON.parse(line);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
.filter((x) => x !== null);
|
|
147
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type Platform = "macos" | "windows" | "linux";
|
|
2
|
+
export declare function getPlatform(): Platform;
|
|
3
|
+
export declare function getHome(): string;
|
|
4
|
+
export declare function getAppDataDir(): string;
|
|
5
|
+
export declare function getLocalAppDataDir(): string;
|
|
6
|
+
export declare function resolvePaths(candidates: string[]): string[];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as os from "os";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
export function getPlatform() {
|
|
4
|
+
switch (os.platform()) {
|
|
5
|
+
case "win32":
|
|
6
|
+
return "windows";
|
|
7
|
+
case "darwin":
|
|
8
|
+
return "macos";
|
|
9
|
+
default:
|
|
10
|
+
return "linux";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function getHome() {
|
|
14
|
+
return os.homedir();
|
|
15
|
+
}
|
|
16
|
+
// Windows: %APPDATA%, %LOCALAPPDATA%, %USERPROFILE%
|
|
17
|
+
// macOS: ~/Library/Application Support, ~/
|
|
18
|
+
// Linux: ~/.config, ~/.local/share, ~/
|
|
19
|
+
export function getAppDataDir() {
|
|
20
|
+
const platform = getPlatform();
|
|
21
|
+
if (platform === "windows") {
|
|
22
|
+
return process.env.APPDATA || path.join(getHome(), "AppData", "Roaming");
|
|
23
|
+
}
|
|
24
|
+
if (platform === "macos") {
|
|
25
|
+
return path.join(getHome(), "Library", "Application Support");
|
|
26
|
+
}
|
|
27
|
+
return process.env.XDG_CONFIG_HOME || path.join(getHome(), ".config");
|
|
28
|
+
}
|
|
29
|
+
export function getLocalAppDataDir() {
|
|
30
|
+
const platform = getPlatform();
|
|
31
|
+
if (platform === "windows") {
|
|
32
|
+
return process.env.LOCALAPPDATA || path.join(getHome(), "AppData", "Local");
|
|
33
|
+
}
|
|
34
|
+
if (platform === "macos") {
|
|
35
|
+
return path.join(getHome(), "Library", "Application Support");
|
|
36
|
+
}
|
|
37
|
+
return process.env.XDG_DATA_HOME || path.join(getHome(), ".local", "share");
|
|
38
|
+
}
|
|
39
|
+
// Resolve a list of candidate paths, return all that exist
|
|
40
|
+
export function resolvePaths(candidates) {
|
|
41
|
+
const fs = require("fs");
|
|
42
|
+
return candidates.filter((p) => {
|
|
43
|
+
try {
|
|
44
|
+
return fs.existsSync(p);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Scanner, Session, ParsedSession } from "./types.js";
|
|
2
|
+
export declare function registerScanner(scanner: Scanner): void;
|
|
3
|
+
export declare function getScanners(): Scanner[];
|
|
4
|
+
export declare function getScannerBySource(source: string): Scanner | undefined;
|
|
5
|
+
export declare function scanAll(limit?: number): Session[];
|
|
6
|
+
export declare function parseSession(filePath: string, source: string, maxTurns?: number): ParsedSession | null;
|
|
7
|
+
export declare function listScannerStatus(): Array<{
|
|
8
|
+
name: string;
|
|
9
|
+
source: string;
|
|
10
|
+
description: string;
|
|
11
|
+
available: boolean;
|
|
12
|
+
dirs: string[];
|
|
13
|
+
}>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Scanner registry — all IDE scanners register here
|
|
2
|
+
const scanners = [];
|
|
3
|
+
export function registerScanner(scanner) {
|
|
4
|
+
scanners.push(scanner);
|
|
5
|
+
}
|
|
6
|
+
export function getScanners() {
|
|
7
|
+
return [...scanners];
|
|
8
|
+
}
|
|
9
|
+
export function getScannerBySource(source) {
|
|
10
|
+
return scanners.find((s) => s.sourceType === source);
|
|
11
|
+
}
|
|
12
|
+
// Scan all registered IDEs, merge and sort results
|
|
13
|
+
export function scanAll(limit = 20) {
|
|
14
|
+
const allSessions = [];
|
|
15
|
+
for (const scanner of scanners) {
|
|
16
|
+
try {
|
|
17
|
+
const sessions = scanner.scan(limit);
|
|
18
|
+
allSessions.push(...sessions);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
// Silently skip failing scanners
|
|
22
|
+
console.error(`Scanner ${scanner.name} failed:`, err);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Sort by modification time (newest first)
|
|
26
|
+
allSessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
|
|
27
|
+
return allSessions.slice(0, limit);
|
|
28
|
+
}
|
|
29
|
+
// Parse a session file using the appropriate scanner
|
|
30
|
+
export function parseSession(filePath, source, maxTurns) {
|
|
31
|
+
const scanner = getScannerBySource(source);
|
|
32
|
+
if (!scanner)
|
|
33
|
+
return null;
|
|
34
|
+
return scanner.parse(filePath, maxTurns);
|
|
35
|
+
}
|
|
36
|
+
// List available scanners with their status
|
|
37
|
+
export function listScannerStatus() {
|
|
38
|
+
return scanners.map((s) => {
|
|
39
|
+
const dirs = s.getSessionDirs();
|
|
40
|
+
return {
|
|
41
|
+
name: s.name,
|
|
42
|
+
source: s.sourceType,
|
|
43
|
+
description: s.description,
|
|
44
|
+
available: dirs.length > 0,
|
|
45
|
+
dirs,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface Session {
|
|
2
|
+
id: string;
|
|
3
|
+
source: SourceType;
|
|
4
|
+
project: string;
|
|
5
|
+
projectPath?: string;
|
|
6
|
+
projectDescription?: string;
|
|
7
|
+
title: string;
|
|
8
|
+
messageCount: number;
|
|
9
|
+
humanMessages: number;
|
|
10
|
+
aiMessages: number;
|
|
11
|
+
preview: string;
|
|
12
|
+
filePath: string;
|
|
13
|
+
modifiedAt: Date;
|
|
14
|
+
sizeBytes: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ConversationTurn {
|
|
17
|
+
role: "human" | "assistant" | "system" | "tool";
|
|
18
|
+
content: string;
|
|
19
|
+
timestamp?: Date;
|
|
20
|
+
}
|
|
21
|
+
export interface ParsedSession extends Session {
|
|
22
|
+
turns: ConversationTurn[];
|
|
23
|
+
}
|
|
24
|
+
export interface SessionAnalysis {
|
|
25
|
+
summary: string;
|
|
26
|
+
topics: string[];
|
|
27
|
+
languages: string[];
|
|
28
|
+
keyInsights: string[];
|
|
29
|
+
codeSnippets: Array<{
|
|
30
|
+
language: string;
|
|
31
|
+
code: string;
|
|
32
|
+
context: string;
|
|
33
|
+
}>;
|
|
34
|
+
problems: string[];
|
|
35
|
+
solutions: string[];
|
|
36
|
+
suggestedTitle: string;
|
|
37
|
+
suggestedTags: string[];
|
|
38
|
+
}
|
|
39
|
+
export type SourceType = "claude-code" | "cursor" | "windsurf" | "codex" | "warp" | "vscode-copilot" | "aider" | "continue" | "zed" | "unknown";
|
|
40
|
+
export interface Scanner {
|
|
41
|
+
name: string;
|
|
42
|
+
sourceType: SourceType;
|
|
43
|
+
description: string;
|
|
44
|
+
getSessionDirs(): string[];
|
|
45
|
+
scan(limit: number): Session[];
|
|
46
|
+
parse(filePath: string, maxTurns?: number): ParsedSession | null;
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|