@contextmirror/claude-memory 0.2.1 → 0.3.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.
@@ -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
- insights: [], // TODO: Cross-project analysis
16
- userPatterns: [], // TODO: Pattern detection
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
- writeFileSync(mdPath, formatGlobalContextMarkdown(context));
33
- console.log(`\nšŸ“ Written to:`);
34
- console.log(` ${jsonPath}`);
35
- console.log(` ${mdPath}`);
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
@@ -2,7 +2,15 @@
2
2
  * Project Scanner - Discovers and analyzes projects
3
3
  */
4
4
  import { ProjectInfo, ScanOptions } from '../types/index.js';
5
+ /**
6
+ * Extended scan options with onlyPaths support
7
+ */
8
+ interface ExtendedScanOptions extends Partial<ScanOptions> {
9
+ /** If provided, only scan these specific paths (for quick/incremental scan) */
10
+ onlyPaths?: string[];
11
+ }
5
12
  /**
6
13
  * Scan a directory for projects and extract information
7
14
  */
8
- export declare function scanProjects(options?: Partial<ScanOptions>): Promise<ProjectInfo[]>;
15
+ export declare function scanProjects(options?: ExtendedScanOptions): Promise<ProjectInfo[]>;
16
+ export {};
@@ -5,27 +5,86 @@ 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 = [];
16
+ // Quick mode: only scan specific paths
17
+ if (options.onlyPaths && options.onlyPaths.length > 0) {
18
+ console.log(`šŸ” Quick scan: ${options.onlyPaths.length} project(s)...`);
19
+ for (const projectPath of options.onlyPaths) {
20
+ if (!existsSync(projectPath)) {
21
+ console.warn(` ⚠ Path not found: ${projectPath}`);
22
+ continue;
23
+ }
24
+ try {
25
+ const info = await analyzeProject(projectPath);
26
+ if (info) {
27
+ projects.push(info);
28
+ console.log(` āœ“ ${info.name} (${info.language})`);
29
+ }
30
+ }
31
+ catch (err) {
32
+ const errorMsg = err instanceof Error ? err.message : 'Unknown error';
33
+ errors.push({ path: projectPath, error: errorMsg });
34
+ console.warn(` ⚠ Failed: ${basename(projectPath)} - ${errorMsg}`);
35
+ }
36
+ }
37
+ console.log(`\nšŸ“Š Quick scan results: ${projects.length} project(s) updated`);
38
+ return projects;
39
+ }
40
+ // Full scan mode
14
41
  console.log(`šŸ” Scanning ${opts.rootDir} for projects...`);
42
+ // Verify root directory exists
43
+ if (!existsSync(opts.rootDir)) {
44
+ console.error(`āŒ Directory does not exist: ${opts.rootDir}`);
45
+ console.error(` Check the path and try again.`);
46
+ return [];
47
+ }
15
48
  const candidates = findProjectRoots(opts.rootDir, opts.maxDepth, opts.ignore);
16
- for (const projectPath of candidates) {
49
+ if (candidates.length === 0) {
50
+ console.log(` No project directories found in ${opts.rootDir}`);
51
+ console.log(` Looking for: package.json, Cargo.toml, pyproject.toml, go.mod, or .git`);
52
+ return [];
53
+ }
54
+ // Load excluded projects from existing context
55
+ const existingContext = loadExistingContext();
56
+ const excludedProjects = new Set(existingContext?.excludedProjects || []);
57
+ // Filter out excluded projects
58
+ const filteredCandidates = candidates.filter(path => {
59
+ if (excludedProjects.has(path)) {
60
+ console.log(` ⊘ Skipping excluded: ${basename(path)}`);
61
+ return false;
62
+ }
63
+ return true;
64
+ });
65
+ console.log(` Found ${filteredCandidates.length} candidate directories\n`);
66
+ if (excludedProjects.size > 0) {
67
+ console.log(` (${excludedProjects.size} excluded)\n`);
68
+ }
69
+ for (const projectPath of filteredCandidates) {
17
70
  try {
18
71
  const info = await analyzeProject(projectPath);
19
72
  if (info) {
20
73
  projects.push(info);
21
- console.log(` āœ“ Found: ${info.name} (${info.language})`);
74
+ console.log(` āœ“ ${info.name} (${info.language}) - ${info.techStack.join(', ') || 'no stack detected'}`);
22
75
  }
23
76
  }
24
77
  catch (err) {
25
- console.warn(` ⚠ Failed to analyze: ${projectPath}`);
78
+ const errorMsg = err instanceof Error ? err.message : 'Unknown error';
79
+ errors.push({ path: projectPath, error: errorMsg });
80
+ console.warn(` ⚠ Failed: ${basename(projectPath)} - ${errorMsg}`);
26
81
  }
27
82
  }
28
- console.log(`\nšŸ“Š Found ${projects.length} projects`);
83
+ console.log(`\nšŸ“Š Results:`);
84
+ console.log(` āœ… Successfully scanned: ${projects.length}`);
85
+ if (errors.length > 0) {
86
+ console.log(` āš ļø Failed to scan: ${errors.length}`);
87
+ }
29
88
  return projects;
30
89
  }
31
90
  /**
@@ -77,23 +136,33 @@ async function analyzeProject(projectPath) {
77
136
  const name = detectProjectName(projectPath);
78
137
  const description = detectDescription(projectPath);
79
138
  const { language, techStack } = detectTechStack(projectPath);
80
- // Git info
139
+ // Git info with timeout to prevent hanging on large/slow repos
81
140
  let lastActivity = new Date().toISOString();
82
141
  let currentBranch = 'unknown';
83
142
  let isDirty = false;
84
143
  if (existsSync(join(projectPath, '.git'))) {
85
144
  try {
86
- const git = simpleGit(projectPath);
87
- const log = await git.log({ maxCount: 1 });
88
- if (log.latest) {
89
- lastActivity = log.latest.date;
90
- }
91
- const status = await git.status();
92
- currentBranch = status.current || 'unknown';
93
- isDirty = !status.isClean();
145
+ // 10 second timeout for git operations
146
+ const git = simpleGit(projectPath, { timeout: { block: 10000 } });
147
+ // Wrap in Promise.race for extra safety
148
+ const gitOps = async () => {
149
+ const log = await git.log({ maxCount: 1 });
150
+ if (log.latest) {
151
+ lastActivity = log.latest.date;
152
+ }
153
+ const status = await git.status();
154
+ currentBranch = status.current || 'unknown';
155
+ isDirty = !status.isClean();
156
+ };
157
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Git timeout')), 10000));
158
+ await Promise.race([gitOps(), timeout]);
94
159
  }
95
- catch {
96
- // Git operations failed, use defaults
160
+ catch (err) {
161
+ // Git operations failed or timed out, use defaults
162
+ const errorMsg = err instanceof Error ? err.message : 'Unknown error';
163
+ if (errorMsg.includes('timeout') || errorMsg.includes('Timeout')) {
164
+ console.warn(` āš ļø Git timeout for ${basename(projectPath)} - using defaults`);
165
+ }
97
166
  }
98
167
  }
99
168
  // Extract insights from CLAUDE.md if it exists
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Staleness detection for claude-memory
3
+ * Detects which projects need rescanning based on age and git activity
4
+ */
5
+ import { GlobalContext, ProjectInfo } from '../types/index.js';
6
+ export interface StaleProject {
7
+ name: string;
8
+ path: string;
9
+ daysSinceScanned: number;
10
+ reason: 'age' | 'git_activity' | 'file_changed' | 'never_scanned';
11
+ details?: string;
12
+ }
13
+ export interface StalenessReport {
14
+ /** Overall data age in hours */
15
+ dataAgeHours: number;
16
+ /** Projects that need rescanning */
17
+ staleProjects: StaleProject[];
18
+ /** Projects that are fresh */
19
+ freshCount: number;
20
+ /** Human-readable summary */
21
+ summary: string;
22
+ /** Suggested CLI command */
23
+ suggestion?: string;
24
+ }
25
+ /**
26
+ * Detect all stale projects in the global context
27
+ */
28
+ export declare function detectStaleProjects(context: GlobalContext): StalenessReport;
29
+ /**
30
+ * Get a quick staleness check for the current working directory
31
+ */
32
+ export declare function checkCurrentProjectStaleness(context: GlobalContext, cwd: string): {
33
+ isStale: boolean;
34
+ project?: ProjectInfo;
35
+ reason?: string;
36
+ };
37
+ /**
38
+ * Format staleness report for MCP output
39
+ */
40
+ export declare function formatStalenessForMcp(report: StalenessReport): string;
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Staleness detection for claude-memory
3
+ * Detects which projects need rescanning based on age and git activity
4
+ */
5
+ import { execSync } from 'child_process';
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ /** Thresholds for staleness (in days) */
9
+ const STALENESS_THRESHOLDS = {
10
+ WARNING: 3, // Yellow warning
11
+ STALE: 7, // Orange warning
12
+ CRITICAL: 14, // Red warning
13
+ };
14
+ /**
15
+ * Check if a project has new git commits since last scan
16
+ */
17
+ function hasNewCommits(projectPath, lastScanned) {
18
+ try {
19
+ const gitDir = path.join(projectPath, '.git');
20
+ if (!fs.existsSync(gitDir))
21
+ return false;
22
+ // Get latest commit date
23
+ const result = execSync('git log -1 --format=%cI 2>/dev/null || echo ""', { cwd: projectPath, timeout: 5000, encoding: 'utf-8' }).trim();
24
+ if (!result)
25
+ return false;
26
+ const lastCommitDate = new Date(result);
27
+ const lastScannedDate = new Date(lastScanned);
28
+ return lastCommitDate > lastScannedDate;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ /**
35
+ * Check if key project files have been modified since last scan
36
+ */
37
+ function hasModifiedFiles(projectPath, lastScanned) {
38
+ const keyFiles = [
39
+ 'package.json',
40
+ 'CLAUDE.md',
41
+ 'README.md',
42
+ 'Cargo.toml',
43
+ 'pyproject.toml',
44
+ 'go.mod',
45
+ ];
46
+ const lastScannedTime = new Date(lastScanned).getTime();
47
+ for (const file of keyFiles) {
48
+ try {
49
+ const filePath = path.join(projectPath, file);
50
+ if (fs.existsSync(filePath)) {
51
+ const stat = fs.statSync(filePath);
52
+ if (stat.mtimeMs > lastScannedTime) {
53
+ return true;
54
+ }
55
+ }
56
+ }
57
+ catch {
58
+ // Ignore errors
59
+ }
60
+ }
61
+ return false;
62
+ }
63
+ /**
64
+ * Analyze a single project for staleness
65
+ */
66
+ function analyzeProjectStaleness(project) {
67
+ const now = new Date();
68
+ const lastScanned = new Date(project.lastScanned);
69
+ const daysSinceScanned = Math.floor((now.getTime() - lastScanned.getTime()) / (1000 * 60 * 60 * 24));
70
+ // Check for git activity first (most reliable signal)
71
+ if (hasNewCommits(project.path, project.lastScanned)) {
72
+ return {
73
+ name: project.name,
74
+ path: project.path,
75
+ daysSinceScanned,
76
+ reason: 'git_activity',
77
+ details: 'New commits since last scan',
78
+ };
79
+ }
80
+ // Check for modified key files
81
+ if (hasModifiedFiles(project.path, project.lastScanned)) {
82
+ return {
83
+ name: project.name,
84
+ path: project.path,
85
+ daysSinceScanned,
86
+ reason: 'file_changed',
87
+ details: 'Key files modified since last scan',
88
+ };
89
+ }
90
+ // Check age threshold
91
+ if (daysSinceScanned >= STALENESS_THRESHOLDS.STALE) {
92
+ return {
93
+ name: project.name,
94
+ path: project.path,
95
+ daysSinceScanned,
96
+ reason: 'age',
97
+ details: `Not scanned in ${daysSinceScanned} days`,
98
+ };
99
+ }
100
+ return null;
101
+ }
102
+ /**
103
+ * Detect all stale projects in the global context
104
+ */
105
+ export function detectStaleProjects(context) {
106
+ const now = new Date();
107
+ const lastUpdated = new Date(context.lastUpdated);
108
+ const dataAgeHours = Math.floor((now.getTime() - lastUpdated.getTime()) / (1000 * 60 * 60));
109
+ const staleProjects = [];
110
+ for (const project of context.projects) {
111
+ const staleness = analyzeProjectStaleness(project);
112
+ if (staleness) {
113
+ staleProjects.push(staleness);
114
+ }
115
+ }
116
+ // Sort by days since scanned (most stale first)
117
+ staleProjects.sort((a, b) => b.daysSinceScanned - a.daysSinceScanned);
118
+ const freshCount = context.projects.length - staleProjects.length;
119
+ // Generate summary
120
+ let summary;
121
+ let suggestion;
122
+ if (staleProjects.length === 0) {
123
+ summary = `All ${context.projects.length} projects are up to date.`;
124
+ }
125
+ else if (staleProjects.length === 1) {
126
+ const p = staleProjects[0];
127
+ summary = `1 project needs updating: ${p.name} (${p.details})`;
128
+ suggestion = `claude-memory scan --quick`;
129
+ }
130
+ else {
131
+ const critical = staleProjects.filter(p => p.daysSinceScanned >= STALENESS_THRESHOLDS.CRITICAL);
132
+ if (critical.length > 0) {
133
+ summary = `āš ļø ${staleProjects.length} projects need updating (${critical.length} critical)`;
134
+ }
135
+ else {
136
+ summary = `${staleProjects.length} projects could use a refresh`;
137
+ }
138
+ suggestion = `claude-memory scan --quick`;
139
+ }
140
+ return {
141
+ dataAgeHours,
142
+ staleProjects,
143
+ freshCount,
144
+ summary,
145
+ suggestion,
146
+ };
147
+ }
148
+ /**
149
+ * Get a quick staleness check for the current working directory
150
+ */
151
+ export function checkCurrentProjectStaleness(context, cwd) {
152
+ // Find the project that matches the cwd
153
+ const project = context.projects.find(p => cwd.startsWith(p.path) || p.path.startsWith(cwd));
154
+ if (!project) {
155
+ return { isStale: false };
156
+ }
157
+ const staleness = analyzeProjectStaleness(project);
158
+ if (staleness) {
159
+ return {
160
+ isStale: true,
161
+ project,
162
+ reason: staleness.details,
163
+ };
164
+ }
165
+ return { isStale: false, project };
166
+ }
167
+ /**
168
+ * Format staleness report for MCP output
169
+ */
170
+ export function formatStalenessForMcp(report) {
171
+ const lines = [];
172
+ if (report.staleProjects.length === 0) {
173
+ return ''; // Don't add noise if everything is fresh
174
+ }
175
+ lines.push('');
176
+ lines.push('---');
177
+ lines.push('');
178
+ lines.push('### šŸ”„ Data Freshness');
179
+ lines.push('');
180
+ lines.push(report.summary);
181
+ if (report.staleProjects.length > 0 && report.staleProjects.length <= 5) {
182
+ lines.push('');
183
+ for (const p of report.staleProjects) {
184
+ const icon = p.reason === 'git_activity' ? 'šŸ“' :
185
+ p.reason === 'file_changed' ? 'šŸ“„' : 'ā°';
186
+ lines.push(`- ${icon} **${p.name}**: ${p.details}`);
187
+ }
188
+ }
189
+ else if (report.staleProjects.length > 5) {
190
+ lines.push('');
191
+ lines.push(`Stale projects: ${report.staleProjects.map(p => p.name).join(', ')}`);
192
+ }
193
+ if (report.suggestion) {
194
+ lines.push('');
195
+ lines.push(`> Run \`${report.suggestion}\` to refresh`);
196
+ }
197
+ return lines.join('\n');
198
+ }
@@ -25,12 +25,33 @@ const MCP_SERVER_CONFIG = {
25
25
  * Run the setup wizard
26
26
  */
27
27
  export async function runSetupWizard(options = {}) {
28
- const { projectsDir = guessProjectsDir(), skipMcp = false, skipBriefing = false, interactive = true, } = options;
28
+ const { projectsDir: providedDir, skipMcp = false, skipBriefing = false, interactive = true, } = options;
29
29
  console.log('');
30
30
  console.log('╔══════════════════════════════════════════════════════════╗');
31
31
  console.log('ā•‘ 🧠 Claude Memory - Setup Wizard ā•‘');
32
32
  console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•');
33
33
  console.log('');
34
+ // Determine projects directory
35
+ let projectsDir = providedDir || guessProjectsDir();
36
+ if (!projectsDir) {
37
+ console.log('šŸ“ Step 0: Locate your projects\n');
38
+ console.log(' Could not auto-detect your projects directory.');
39
+ console.log(' Common locations like ~/Projects, ~/Code, ~/dev were not found.\n');
40
+ if (interactive) {
41
+ const answer = await ask(' Enter your projects directory path: ');
42
+ if (!answer || !existsSync(answer)) {
43
+ console.log('\n āŒ Invalid directory. Please run:');
44
+ console.log(' claude-memory setup -d /path/to/your/projects');
45
+ return;
46
+ }
47
+ projectsDir = answer;
48
+ }
49
+ else {
50
+ console.log(' āŒ Cannot continue without a projects directory.');
51
+ console.log(' Run: claude-memory setup -d /path/to/your/projects');
52
+ return;
53
+ }
54
+ }
34
55
  // Step 1: Find projects
35
56
  console.log('šŸ“ Step 1: Scanning for projects\n');
36
57
  console.log(` Looking in: ${projectsDir}`);
@@ -164,6 +185,7 @@ export async function runSetupWizard(options = {}) {
164
185
  }
165
186
  /**
166
187
  * Guess the user's projects directory
188
+ * Returns null if we can't find a reasonable guess (better than scanning home!)
167
189
  */
168
190
  function guessProjectsDir() {
169
191
  const home = homedir();
@@ -178,14 +200,17 @@ function guessProjectsDir() {
178
200
  join(home, 'src'),
179
201
  join(home, 'repos'),
180
202
  join(home, 'git'),
203
+ join(home, 'workspace'),
204
+ join(home, 'Workspace'),
181
205
  ];
182
206
  for (const dir of candidates) {
183
207
  if (existsSync(dir)) {
184
208
  return dir;
185
209
  }
186
210
  }
187
- // Fall back to home directory
188
- return home;
211
+ // Don't fall back to home - that scans way too much and causes issues
212
+ // Return null and let the caller handle it
213
+ return null;
189
214
  }
190
215
  /**
191
216
  * Save memory config to ~/.claude-memory/config.json
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Core types for Claude Memory
3
3
  */
4
+ /** Current schema version - increment when making breaking changes */
5
+ export declare const SCHEMA_VERSION = 1;
4
6
  export interface ProjectInfo {
5
7
  /** Absolute path to project root */
6
8
  path: string;
@@ -43,14 +45,18 @@ export interface ProjectInsight {
43
45
  discoveredAt: string;
44
46
  }
45
47
  export interface GlobalContext {
48
+ /** Schema version for migration support */
49
+ schemaVersion: number;
46
50
  /** When the global context was last updated */
47
51
  lastUpdated: string;
48
52
  /** All scanned projects */
49
53
  projects: ProjectInfo[];
50
- /** Cross-project insights */
54
+ /** Cross-project insights (preserved across rescans) */
51
55
  insights: GlobalInsight[];
52
56
  /** User preferences/patterns observed */
53
57
  userPatterns: UserPattern[];
58
+ /** Projects to exclude from scanning (by path) */
59
+ excludedProjects?: string[];
54
60
  }
55
61
  export interface GlobalInsight {
56
62
  /** Insight content */
@@ -1,10 +1,43 @@
1
1
  /**
2
2
  * Core types for Claude Memory
3
3
  */
4
+ /** Current schema version - increment when making breaking changes */
5
+ export const SCHEMA_VERSION = 1;
6
+ /**
7
+ * Get a sensible default projects directory
8
+ * Checks common locations, doesn't fall back to home
9
+ */
10
+ function getDefaultProjectsDir() {
11
+ const home = process.env.HOME || '';
12
+ if (!home)
13
+ return './projects';
14
+ const candidates = [
15
+ `${home}/Projects`,
16
+ `${home}/Project`,
17
+ `${home}/projects`,
18
+ `${home}/Code`,
19
+ `${home}/code`,
20
+ `${home}/dev`,
21
+ ];
22
+ // Import fs dynamically to avoid issues
23
+ try {
24
+ const fs = require('fs');
25
+ for (const dir of candidates) {
26
+ if (fs.existsSync(dir)) {
27
+ return dir;
28
+ }
29
+ }
30
+ }
31
+ catch {
32
+ // fs not available, use first candidate
33
+ }
34
+ // Default to ~/Projects (most common convention)
35
+ return `${home}/Projects`;
36
+ }
4
37
  export const DEFAULT_SCAN_OPTIONS = {
5
- rootDir: process.env.HOME ? `${process.env.HOME}/Project` : './projects',
38
+ rootDir: getDefaultProjectsDir(),
6
39
  maxDepth: 2,
7
- ignore: ['node_modules', '.git', 'dist', 'build', '__pycache__', 'target'],
40
+ ignore: ['node_modules', '.git', 'dist', 'build', '__pycache__', 'target', '.venv', 'venv', 'coverage', '.next'],
8
41
  generateClaudeMd: false,
9
42
  overwriteClaudeMd: false,
10
43
  };