@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.
@@ -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 handleGetGlobalContext() {
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
- return 'No projects scanned yet. Run `claude-memory scan` first.';
263
+ lines.push('No projects scanned yet. Run `claude-memory scan` first.');
264
+ return lines.join('\n');
106
265
  }
107
- const lines = [
108
- `# Your Development Environment`,
109
- '',
110
- `Last scanned: ${new Date(context.lastUpdated).toLocaleString()}`,
111
- '',
112
- '## Active Projects',
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
- 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
@@ -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
- for (const projectPath of candidates) {
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(` āœ“ Found: ${info.name} (${info.language})`);
49
+ console.log(` āœ“ ${info.name} (${info.language}) - ${info.techStack.join(', ') || 'no stack detected'}`);
22
50
  }
23
51
  }
24
52
  catch (err) {
25
- console.warn(` ⚠ Failed to analyze: ${projectPath}`);
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šŸ“Š Found ${projects.length} projects`);
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
- 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();
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