@contextmirror/claude-memory 0.2.0 ā 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +168 -0
- package/dist/license/index.d.ts +39 -0
- package/dist/license/index.js +153 -0
- package/dist/license/types.d.ts +37 -0
- package/dist/license/types.js +4 -0
- package/dist/mcp/server.js +217 -12
- package/dist/scanner/contextGenerator.d.ts +7 -0
- package/dist/scanner/contextGenerator.js +97 -10
- package/dist/scanner/projectScanner.js +59 -15
- package/dist/setup/setupWizard.js +81 -4
- package/dist/types/index.d.ts +36 -1
- package/dist/types/index.js +41 -2
- package/landing/index.html +478 -169
- package/package.json +2 -1
- package/.claude/settings.local.json +0 -10
package/dist/mcp/server.js
CHANGED
|
@@ -13,6 +13,8 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextpro
|
|
|
13
13
|
import { readFileSync, existsSync, readdirSync, statSync, writeFileSync } from 'fs';
|
|
14
14
|
import { join } from 'path';
|
|
15
15
|
import { homedir } from 'os';
|
|
16
|
+
import { DEFAULT_MEMORY_CONFIG } from '../types/index.js';
|
|
17
|
+
import { isPro, getProFeatureMessage } from '../license/index.js';
|
|
16
18
|
const MEMORY_DIR = join(homedir(), '.claude-memory');
|
|
17
19
|
// Tool definitions
|
|
18
20
|
const tools = [
|
|
@@ -21,7 +23,12 @@ const tools = [
|
|
|
21
23
|
description: 'Get an overview of all projects the user is working on. Returns project names, descriptions, tech stacks, and recent activity. Use this at the start of conversations to understand the user\'s development environment.',
|
|
22
24
|
inputSchema: {
|
|
23
25
|
type: 'object',
|
|
24
|
-
properties: {
|
|
26
|
+
properties: {
|
|
27
|
+
cwd: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'Current working directory. If provided, will check if this is a known project or a new project that needs setup.',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
25
32
|
required: [],
|
|
26
33
|
},
|
|
27
34
|
},
|
|
@@ -86,6 +93,25 @@ const tools = [
|
|
|
86
93
|
required: ['project'],
|
|
87
94
|
},
|
|
88
95
|
},
|
|
96
|
+
// Pro tools
|
|
97
|
+
{
|
|
98
|
+
name: 'search_code',
|
|
99
|
+
description: '[PRO] Search for code patterns across all your projects. Find implementations, discover how you solved similar problems before. Requires Pro license.',
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
query: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: 'Code pattern or text to search for',
|
|
106
|
+
},
|
|
107
|
+
filePattern: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
description: 'File pattern to search (e.g., "*.ts", "*.py"). Optional.',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
required: ['query'],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
89
115
|
];
|
|
90
116
|
function loadContext() {
|
|
91
117
|
const contextPath = join(MEMORY_DIR, 'context.json');
|
|
@@ -99,19 +125,156 @@ function loadContext() {
|
|
|
99
125
|
return null;
|
|
100
126
|
}
|
|
101
127
|
}
|
|
102
|
-
function
|
|
128
|
+
function loadConfig() {
|
|
129
|
+
const configPath = join(MEMORY_DIR, 'config.json');
|
|
130
|
+
if (!existsSync(configPath)) {
|
|
131
|
+
return DEFAULT_MEMORY_CONFIG;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
return JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return DEFAULT_MEMORY_CONFIG;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function saveConfig(config) {
|
|
141
|
+
const configPath = join(MEMORY_DIR, 'config.json');
|
|
142
|
+
config.lastUpdated = new Date().toISOString();
|
|
143
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check if a directory looks like a project (has project markers)
|
|
147
|
+
*/
|
|
148
|
+
function looksLikeProject(dir) {
|
|
149
|
+
try {
|
|
150
|
+
const entries = readdirSync(dir);
|
|
151
|
+
return (entries.includes('package.json') ||
|
|
152
|
+
entries.includes('Cargo.toml') ||
|
|
153
|
+
entries.includes('pyproject.toml') ||
|
|
154
|
+
entries.includes('go.mod') ||
|
|
155
|
+
entries.includes('.git') ||
|
|
156
|
+
entries.includes('requirements.txt'));
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Check if a path is under any of the watched directories
|
|
164
|
+
*/
|
|
165
|
+
function isUnderWatchedDir(path, watchedDirs) {
|
|
166
|
+
const normalizedPath = path.replace(/\/$/, '');
|
|
167
|
+
return watchedDirs.some(watchedDir => {
|
|
168
|
+
const normalizedWatched = watchedDir.replace(/\/$/, '');
|
|
169
|
+
return normalizedPath.startsWith(normalizedWatched);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Detect the status of the current working directory
|
|
174
|
+
*/
|
|
175
|
+
function detectCurrentDirectoryStatus(cwd, context, config) {
|
|
176
|
+
const normalizedCwd = cwd.replace(/\/$/, '');
|
|
177
|
+
// Check if it's a known project
|
|
178
|
+
if (context) {
|
|
179
|
+
const knownProject = context.projects.find(p => normalizedCwd === p.path.replace(/\/$/, '') ||
|
|
180
|
+
normalizedCwd.startsWith(p.path.replace(/\/$/, '') + '/'));
|
|
181
|
+
if (knownProject) {
|
|
182
|
+
return {
|
|
183
|
+
path: cwd,
|
|
184
|
+
status: 'known_project',
|
|
185
|
+
projectName: knownProject.name,
|
|
186
|
+
needsSetup: !knownProject.hasFiles.claudeMd,
|
|
187
|
+
suggestedAction: knownProject.hasFiles.claudeMd ? 'none' : 'offer_setup',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Check if it's under a watched directory
|
|
192
|
+
if (config.autoDetectNewProjects && isUnderWatchedDir(cwd, config.watchedDirs)) {
|
|
193
|
+
// It's under a watched dir but not a known project
|
|
194
|
+
if (looksLikeProject(cwd)) {
|
|
195
|
+
return {
|
|
196
|
+
path: cwd,
|
|
197
|
+
status: 'new_project',
|
|
198
|
+
needsSetup: true,
|
|
199
|
+
suggestedAction: 'offer_setup',
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// Empty or non-project directory under watched area
|
|
204
|
+
return {
|
|
205
|
+
path: cwd,
|
|
206
|
+
status: 'new_project',
|
|
207
|
+
needsSetup: true,
|
|
208
|
+
suggestedAction: 'offer_setup',
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Not under watched directories
|
|
213
|
+
if (looksLikeProject(cwd)) {
|
|
214
|
+
return {
|
|
215
|
+
path: cwd,
|
|
216
|
+
status: 'outside_workspace',
|
|
217
|
+
needsSetup: false,
|
|
218
|
+
suggestedAction: 'offer_scan',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
path: cwd,
|
|
223
|
+
status: 'not_a_project',
|
|
224
|
+
needsSetup: false,
|
|
225
|
+
suggestedAction: 'none',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function handleGetGlobalContext(cwd) {
|
|
103
229
|
const context = loadContext();
|
|
230
|
+
const config = loadConfig();
|
|
231
|
+
const lines = [];
|
|
232
|
+
// If cwd is provided, check the current directory status first
|
|
233
|
+
if (cwd) {
|
|
234
|
+
const cwdStatus = detectCurrentDirectoryStatus(cwd, context, config);
|
|
235
|
+
lines.push('# Current Directory Status');
|
|
236
|
+
lines.push('');
|
|
237
|
+
lines.push(`**Path:** ${cwdStatus.path}`);
|
|
238
|
+
lines.push(`**Status:** ${cwdStatus.status}`);
|
|
239
|
+
if (cwdStatus.projectName) {
|
|
240
|
+
lines.push(`**Project:** ${cwdStatus.projectName}`);
|
|
241
|
+
}
|
|
242
|
+
if (cwdStatus.needsSetup) {
|
|
243
|
+
lines.push(`**Needs Setup:** Yes`);
|
|
244
|
+
}
|
|
245
|
+
// Add actionable guidance for Claude
|
|
246
|
+
if (cwdStatus.suggestedAction === 'offer_setup') {
|
|
247
|
+
lines.push('');
|
|
248
|
+
lines.push('> š **New Project Detected!** This directory is not yet registered.');
|
|
249
|
+
lines.push('> Ask the user if they would like you to:');
|
|
250
|
+
lines.push('> 1. Generate a CLAUDE.md file for this project');
|
|
251
|
+
lines.push('> 2. Add this project to the memory index');
|
|
252
|
+
}
|
|
253
|
+
else if (cwdStatus.suggestedAction === 'offer_scan') {
|
|
254
|
+
lines.push('');
|
|
255
|
+
lines.push('> ā¹ļø This looks like a project but is outside your watched directories.');
|
|
256
|
+
lines.push('> You may want to run `claude-memory scan` to add it.');
|
|
257
|
+
}
|
|
258
|
+
lines.push('');
|
|
259
|
+
lines.push('---');
|
|
260
|
+
lines.push('');
|
|
261
|
+
}
|
|
104
262
|
if (!context) {
|
|
105
|
-
|
|
263
|
+
lines.push('No projects scanned yet. Run `claude-memory scan` first.');
|
|
264
|
+
return lines.join('\n');
|
|
106
265
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
''
|
|
114
|
-
|
|
266
|
+
// Check for stale data
|
|
267
|
+
const hoursSinceUpdate = (Date.now() - new Date(context.lastUpdated).getTime()) / (1000 * 60 * 60);
|
|
268
|
+
lines.push('# Your Development Environment');
|
|
269
|
+
lines.push('');
|
|
270
|
+
lines.push(`Last scanned: ${new Date(context.lastUpdated).toLocaleString()}`);
|
|
271
|
+
if (hoursSinceUpdate > 24) {
|
|
272
|
+
lines.push('');
|
|
273
|
+
lines.push(`> ā ļø **Data may be stale** (${Math.floor(hoursSinceUpdate)} hours old). Run \`claude-memory scan\` to refresh.`);
|
|
274
|
+
}
|
|
275
|
+
lines.push('');
|
|
276
|
+
lines.push('## Active Projects');
|
|
277
|
+
lines.push('');
|
|
115
278
|
// Sort by activity
|
|
116
279
|
const sorted = [...context.projects].sort((a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime());
|
|
117
280
|
for (const p of sorted) {
|
|
@@ -123,6 +286,16 @@ function handleGetGlobalContext() {
|
|
|
123
286
|
lines.push(`- **Description:** ${p.description}`);
|
|
124
287
|
lines.push('');
|
|
125
288
|
}
|
|
289
|
+
// Add config info
|
|
290
|
+
if (config.watchedDirs.length > 0) {
|
|
291
|
+
lines.push('## Watched Directories');
|
|
292
|
+
lines.push('');
|
|
293
|
+
for (const dir of config.watchedDirs) {
|
|
294
|
+
lines.push(`- ${dir}`);
|
|
295
|
+
}
|
|
296
|
+
lines.push('');
|
|
297
|
+
lines.push(`Auto-detect new projects: ${config.autoDetectNewProjects ? 'Yes' : 'No'}`);
|
|
298
|
+
}
|
|
126
299
|
return lines.join('\n');
|
|
127
300
|
}
|
|
128
301
|
function handleGetProjectSummary(projectQuery) {
|
|
@@ -197,6 +370,35 @@ function handleRecordInsight(content, relatedProjects) {
|
|
|
197
370
|
return `Failed to record insight: ${err instanceof Error ? err.message : 'Unknown error'}`;
|
|
198
371
|
}
|
|
199
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* Pro feature: Search code across all projects
|
|
375
|
+
*/
|
|
376
|
+
function handleSearchCode(query, filePattern) {
|
|
377
|
+
// Check Pro license
|
|
378
|
+
if (!isPro()) {
|
|
379
|
+
return getProFeatureMessage('Cross-project code search');
|
|
380
|
+
}
|
|
381
|
+
const context = loadContext();
|
|
382
|
+
if (!context) {
|
|
383
|
+
return 'No projects scanned yet. Run `claude-memory scan` first.';
|
|
384
|
+
}
|
|
385
|
+
const lines = [
|
|
386
|
+
`# Code Search Results for "${query}"`,
|
|
387
|
+
'',
|
|
388
|
+
];
|
|
389
|
+
// TODO: Implement actual code search
|
|
390
|
+
// For now, return a placeholder that shows the feature is gated
|
|
391
|
+
lines.push('Searching across projects:');
|
|
392
|
+
for (const p of context.projects) {
|
|
393
|
+
lines.push(`- ${p.name} (${p.path})`);
|
|
394
|
+
}
|
|
395
|
+
lines.push('');
|
|
396
|
+
lines.push(`Pattern: ${filePattern || '*'}`);
|
|
397
|
+
lines.push('');
|
|
398
|
+
lines.push('> Note: Full code search implementation coming soon.');
|
|
399
|
+
lines.push('> This will index and search actual file contents across all your projects.');
|
|
400
|
+
return lines.join('\n');
|
|
401
|
+
}
|
|
200
402
|
function handleGetProjectAnalysis(projectQuery) {
|
|
201
403
|
const context = loadContext();
|
|
202
404
|
if (!context) {
|
|
@@ -351,7 +553,7 @@ async function main() {
|
|
|
351
553
|
let result;
|
|
352
554
|
switch (name) {
|
|
353
555
|
case 'get_global_context':
|
|
354
|
-
result = handleGetGlobalContext();
|
|
556
|
+
result = handleGetGlobalContext(args.cwd);
|
|
355
557
|
break;
|
|
356
558
|
case 'get_project_summary':
|
|
357
559
|
result = handleGetProjectSummary(args.project);
|
|
@@ -365,6 +567,9 @@ async function main() {
|
|
|
365
567
|
case 'get_project_analysis':
|
|
366
568
|
result = handleGetProjectAnalysis(args.project);
|
|
367
569
|
break;
|
|
570
|
+
case 'search_code':
|
|
571
|
+
result = handleSearchCode(args.query, args.filePattern);
|
|
572
|
+
break;
|
|
368
573
|
default:
|
|
369
574
|
throw new Error(`Unknown tool: ${name}`);
|
|
370
575
|
}
|
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
* Context Generator - Produces markdown context files
|
|
3
3
|
*/
|
|
4
4
|
import { ProjectInfo, GlobalContext } from '../types/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Load existing context from disk (if any)
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadExistingContext(): GlobalContext | null;
|
|
5
9
|
/**
|
|
6
10
|
* Generate global context from scanned projects
|
|
11
|
+
* Preserves existing insights and user patterns
|
|
7
12
|
*/
|
|
8
13
|
export declare function generateGlobalContext(projects: ProjectInfo[]): GlobalContext;
|
|
9
14
|
/**
|
|
10
15
|
* Write global context to ~/.claude-memory/
|
|
16
|
+
* Uses atomic write (write to temp, then rename) to prevent corruption
|
|
17
|
+
* Creates backup before overwriting
|
|
11
18
|
*/
|
|
12
19
|
export declare function writeGlobalContext(context: GlobalContext): void;
|
|
13
20
|
/**
|
|
@@ -1,38 +1,125 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Context Generator - Produces markdown context files
|
|
3
3
|
*/
|
|
4
|
-
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
4
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync, renameSync, unlinkSync, copyFileSync } from 'fs';
|
|
5
5
|
import { join } from 'path';
|
|
6
6
|
import { homedir } from 'os';
|
|
7
|
+
import { SCHEMA_VERSION } from '../types/index.js';
|
|
7
8
|
const MEMORY_DIR = join(homedir(), '.claude-memory');
|
|
9
|
+
/**
|
|
10
|
+
* Load existing context from disk (if any)
|
|
11
|
+
*/
|
|
12
|
+
export function loadExistingContext() {
|
|
13
|
+
const jsonPath = join(MEMORY_DIR, 'context.json');
|
|
14
|
+
if (!existsSync(jsonPath)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const data = JSON.parse(readFileSync(jsonPath, 'utf-8'));
|
|
19
|
+
return migrateContext(data);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Migrate old context format to current schema
|
|
27
|
+
*/
|
|
28
|
+
function migrateContext(data) {
|
|
29
|
+
const version = data.schemaVersion || 0;
|
|
30
|
+
// Migration from v0 (no version) to v1
|
|
31
|
+
if (version < 1) {
|
|
32
|
+
// v0 didn't have schemaVersion, excludedProjects
|
|
33
|
+
return {
|
|
34
|
+
schemaVersion: SCHEMA_VERSION,
|
|
35
|
+
lastUpdated: data.lastUpdated || new Date().toISOString(),
|
|
36
|
+
projects: data.projects || [],
|
|
37
|
+
insights: data.insights || [],
|
|
38
|
+
userPatterns: data.userPatterns || [],
|
|
39
|
+
excludedProjects: [],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// Already current version
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
8
45
|
/**
|
|
9
46
|
* Generate global context from scanned projects
|
|
47
|
+
* Preserves existing insights and user patterns
|
|
10
48
|
*/
|
|
11
49
|
export function generateGlobalContext(projects) {
|
|
50
|
+
// Load existing context to preserve insights
|
|
51
|
+
const existing = loadExistingContext();
|
|
12
52
|
return {
|
|
53
|
+
schemaVersion: SCHEMA_VERSION,
|
|
13
54
|
lastUpdated: new Date().toISOString(),
|
|
14
55
|
projects,
|
|
15
|
-
|
|
16
|
-
|
|
56
|
+
// Preserve existing insights and patterns!
|
|
57
|
+
insights: existing?.insights || [],
|
|
58
|
+
userPatterns: existing?.userPatterns || [],
|
|
59
|
+
excludedProjects: existing?.excludedProjects || [],
|
|
17
60
|
};
|
|
18
61
|
}
|
|
19
62
|
/**
|
|
20
63
|
* Write global context to ~/.claude-memory/
|
|
64
|
+
* Uses atomic write (write to temp, then rename) to prevent corruption
|
|
65
|
+
* Creates backup before overwriting
|
|
21
66
|
*/
|
|
22
67
|
export function writeGlobalContext(context) {
|
|
23
68
|
// Ensure directory exists
|
|
24
69
|
if (!existsSync(MEMORY_DIR)) {
|
|
25
70
|
mkdirSync(MEMORY_DIR, { recursive: true });
|
|
26
71
|
}
|
|
27
|
-
// Write JSON for MCP server
|
|
28
72
|
const jsonPath = join(MEMORY_DIR, 'context.json');
|
|
29
|
-
writeFileSync(jsonPath, JSON.stringify(context, null, 2));
|
|
30
|
-
// Write markdown for human readability
|
|
31
73
|
const mdPath = join(MEMORY_DIR, 'global-context.md');
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
74
|
+
const jsonTempPath = join(MEMORY_DIR, 'context.json.tmp');
|
|
75
|
+
const mdTempPath = join(MEMORY_DIR, 'global-context.md.tmp');
|
|
76
|
+
const jsonBackupPath = join(MEMORY_DIR, 'context.json.bak');
|
|
77
|
+
try {
|
|
78
|
+
// Create backup of existing context before overwriting
|
|
79
|
+
if (existsSync(jsonPath)) {
|
|
80
|
+
try {
|
|
81
|
+
copyFileSync(jsonPath, jsonBackupPath);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Backup failed, but continue anyway
|
|
85
|
+
console.warn(' ā ļø Could not create backup');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Write to temp files first (atomic write pattern)
|
|
89
|
+
const jsonContent = JSON.stringify(context, null, 2);
|
|
90
|
+
writeFileSync(jsonTempPath, jsonContent, 'utf-8');
|
|
91
|
+
const mdContent = formatGlobalContextMarkdown(context);
|
|
92
|
+
writeFileSync(mdTempPath, mdContent, 'utf-8');
|
|
93
|
+
// Verify JSON can be parsed back
|
|
94
|
+
const verification = JSON.parse(readFileSync(jsonTempPath, 'utf-8'));
|
|
95
|
+
if (verification.projects.length !== context.projects.length) {
|
|
96
|
+
throw new Error(`Write verification failed: expected ${context.projects.length} projects, got ${verification.projects.length}`);
|
|
97
|
+
}
|
|
98
|
+
// Atomic rename (much safer than direct write)
|
|
99
|
+
renameSync(jsonTempPath, jsonPath);
|
|
100
|
+
renameSync(mdTempPath, mdPath);
|
|
101
|
+
console.log(`\nš Written to:`);
|
|
102
|
+
console.log(` ${jsonPath} (${context.projects.length} projects)`);
|
|
103
|
+
console.log(` ${mdPath}`);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
// Clean up temp files if they exist
|
|
107
|
+
try {
|
|
108
|
+
if (existsSync(jsonTempPath))
|
|
109
|
+
unlinkSync(jsonTempPath);
|
|
110
|
+
if (existsSync(mdTempPath))
|
|
111
|
+
unlinkSync(mdTempPath);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Ignore cleanup errors
|
|
115
|
+
}
|
|
116
|
+
console.error(`\nā Failed to write context:`);
|
|
117
|
+
console.error(` ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
118
|
+
if (existsSync(jsonBackupPath)) {
|
|
119
|
+
console.error(` š¾ Backup available at: ${jsonBackupPath}`);
|
|
120
|
+
}
|
|
121
|
+
throw err; // Re-throw so caller knows it failed
|
|
122
|
+
}
|
|
36
123
|
}
|
|
37
124
|
/**
|
|
38
125
|
* Format global context as markdown
|
|
@@ -5,27 +5,61 @@ import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
|
5
5
|
import { join, basename } from 'path';
|
|
6
6
|
import { simpleGit } from 'simple-git';
|
|
7
7
|
import { DEFAULT_SCAN_OPTIONS } from '../types/index.js';
|
|
8
|
+
import { loadExistingContext } from './contextGenerator.js';
|
|
8
9
|
/**
|
|
9
10
|
* Scan a directory for projects and extract information
|
|
10
11
|
*/
|
|
11
12
|
export async function scanProjects(options = {}) {
|
|
12
13
|
const opts = { ...DEFAULT_SCAN_OPTIONS, ...options };
|
|
13
14
|
const projects = [];
|
|
15
|
+
const errors = [];
|
|
14
16
|
console.log(`š Scanning ${opts.rootDir} for projects...`);
|
|
17
|
+
// Verify root directory exists
|
|
18
|
+
if (!existsSync(opts.rootDir)) {
|
|
19
|
+
console.error(`ā Directory does not exist: ${opts.rootDir}`);
|
|
20
|
+
console.error(` Check the path and try again.`);
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
15
23
|
const candidates = findProjectRoots(opts.rootDir, opts.maxDepth, opts.ignore);
|
|
16
|
-
|
|
24
|
+
if (candidates.length === 0) {
|
|
25
|
+
console.log(` No project directories found in ${opts.rootDir}`);
|
|
26
|
+
console.log(` Looking for: package.json, Cargo.toml, pyproject.toml, go.mod, or .git`);
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
// Load excluded projects from existing context
|
|
30
|
+
const existingContext = loadExistingContext();
|
|
31
|
+
const excludedProjects = new Set(existingContext?.excludedProjects || []);
|
|
32
|
+
// Filter out excluded projects
|
|
33
|
+
const filteredCandidates = candidates.filter(path => {
|
|
34
|
+
if (excludedProjects.has(path)) {
|
|
35
|
+
console.log(` ā Skipping excluded: ${basename(path)}`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
});
|
|
40
|
+
console.log(` Found ${filteredCandidates.length} candidate directories\n`);
|
|
41
|
+
if (excludedProjects.size > 0) {
|
|
42
|
+
console.log(` (${excludedProjects.size} excluded)\n`);
|
|
43
|
+
}
|
|
44
|
+
for (const projectPath of filteredCandidates) {
|
|
17
45
|
try {
|
|
18
46
|
const info = await analyzeProject(projectPath);
|
|
19
47
|
if (info) {
|
|
20
48
|
projects.push(info);
|
|
21
|
-
console.log(` ā
|
|
49
|
+
console.log(` ā ${info.name} (${info.language}) - ${info.techStack.join(', ') || 'no stack detected'}`);
|
|
22
50
|
}
|
|
23
51
|
}
|
|
24
52
|
catch (err) {
|
|
25
|
-
|
|
53
|
+
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
|
|
54
|
+
errors.push({ path: projectPath, error: errorMsg });
|
|
55
|
+
console.warn(` ā Failed: ${basename(projectPath)} - ${errorMsg}`);
|
|
26
56
|
}
|
|
27
57
|
}
|
|
28
|
-
console.log(`\nš
|
|
58
|
+
console.log(`\nš Results:`);
|
|
59
|
+
console.log(` ā
Successfully scanned: ${projects.length}`);
|
|
60
|
+
if (errors.length > 0) {
|
|
61
|
+
console.log(` ā ļø Failed to scan: ${errors.length}`);
|
|
62
|
+
}
|
|
29
63
|
return projects;
|
|
30
64
|
}
|
|
31
65
|
/**
|
|
@@ -77,23 +111,33 @@ async function analyzeProject(projectPath) {
|
|
|
77
111
|
const name = detectProjectName(projectPath);
|
|
78
112
|
const description = detectDescription(projectPath);
|
|
79
113
|
const { language, techStack } = detectTechStack(projectPath);
|
|
80
|
-
// Git info
|
|
114
|
+
// Git info with timeout to prevent hanging on large/slow repos
|
|
81
115
|
let lastActivity = new Date().toISOString();
|
|
82
116
|
let currentBranch = 'unknown';
|
|
83
117
|
let isDirty = false;
|
|
84
118
|
if (existsSync(join(projectPath, '.git'))) {
|
|
85
119
|
try {
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
120
|
+
// 10 second timeout for git operations
|
|
121
|
+
const git = simpleGit(projectPath, { timeout: { block: 10000 } });
|
|
122
|
+
// Wrap in Promise.race for extra safety
|
|
123
|
+
const gitOps = async () => {
|
|
124
|
+
const log = await git.log({ maxCount: 1 });
|
|
125
|
+
if (log.latest) {
|
|
126
|
+
lastActivity = log.latest.date;
|
|
127
|
+
}
|
|
128
|
+
const status = await git.status();
|
|
129
|
+
currentBranch = status.current || 'unknown';
|
|
130
|
+
isDirty = !status.isClean();
|
|
131
|
+
};
|
|
132
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Git timeout')), 10000));
|
|
133
|
+
await Promise.race([gitOps(), timeout]);
|
|
94
134
|
}
|
|
95
|
-
catch {
|
|
96
|
-
// Git operations failed, use defaults
|
|
135
|
+
catch (err) {
|
|
136
|
+
// Git operations failed or timed out, use defaults
|
|
137
|
+
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
|
|
138
|
+
if (errorMsg.includes('timeout') || errorMsg.includes('Timeout')) {
|
|
139
|
+
console.warn(` ā ļø Git timeout for ${basename(projectPath)} - using defaults`);
|
|
140
|
+
}
|
|
97
141
|
}
|
|
98
142
|
}
|
|
99
143
|
// Extract insights from CLAUDE.md if it exists
|