@contextmirror/claude-memory 0.4.1 → 0.4.3

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 CHANGED
@@ -10,19 +10,26 @@
10
10
  */
11
11
  import { Command } from 'commander';
12
12
  import { readFileSync, existsSync, writeFileSync } from 'fs';
13
- import { join } from 'path';
13
+ import { join, dirname } from 'path';
14
14
  import { homedir } from 'os';
15
+ import { fileURLToPath } from 'url';
15
16
  import { scanProjects } from './scanner/projectScanner.js';
16
17
  import { generateGlobalContext, writeGlobalContext, getMemoryDir } from './scanner/contextGenerator.js';
17
18
  import { generateBriefing, briefingToClaudeMd } from './briefing/briefingGenerator.js';
18
19
  import { runSetupWizard } from './setup/setupWizard.js';
19
20
  import { activateLicense, deactivateLicense, getLicenseStatus } from './license/index.js';
20
21
  import { detectStaleProjects } from './scanner/stalenessDetector.js';
22
+ // Get version from package.json
23
+ const __filename = fileURLToPath(import.meta.url);
24
+ const __dirname = dirname(__filename);
25
+ const packageJsonPath = join(__dirname, '../package.json');
26
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
27
+ const VERSION = packageJson.version;
21
28
  const program = new Command();
22
29
  program
23
30
  .name('claude-memory')
24
31
  .description('Cross-project memory for Claude Code')
25
- .version('0.1.0');
32
+ .version(VERSION);
26
33
  // Scan command
27
34
  program
28
35
  .command('scan')
@@ -37,7 +44,10 @@ program
37
44
  if (options.check) {
38
45
  const context = loadContext();
39
46
  if (!context) {
40
- console.log('No projects scanned yet. Run: claude-memory scan');
47
+ console.log('No projects scanned yet.\n');
48
+ console.log('Get started:');
49
+ console.log(' claude-memory setup # Interactive wizard (recommended)');
50
+ console.log(' claude-memory scan ~/Projects # Scan a specific directory');
41
51
  return;
42
52
  }
43
53
  const report = detectStaleProjects(context);
@@ -119,7 +129,10 @@ program
119
129
  .action(() => {
120
130
  const context = loadContext();
121
131
  if (!context) {
122
- console.log('No projects scanned yet. Run: claude-memory scan');
132
+ console.log('No projects scanned yet.\n');
133
+ console.log('Get started:');
134
+ console.log(' claude-memory setup # Interactive wizard (recommended)');
135
+ console.log(' claude-memory scan ~/Projects # Scan a specific directory');
123
136
  return;
124
137
  }
125
138
  console.log('🧠 Known Projects\n');
@@ -139,14 +152,27 @@ program
139
152
  .action((projectName) => {
140
153
  const context = loadContext();
141
154
  if (!context) {
142
- console.log('No projects scanned yet. Run: claude-memory scan');
155
+ console.log('No projects scanned yet.\n');
156
+ console.log('Get started:');
157
+ console.log(' claude-memory setup # Interactive wizard (recommended)');
158
+ console.log(' claude-memory scan ~/Projects # Scan a specific directory');
143
159
  return;
144
160
  }
145
161
  const project = context.projects.find((p) => p.name.toLowerCase() === projectName.toLowerCase() || p.path.includes(projectName));
146
162
  if (!project) {
147
- console.log(`Project not found: ${projectName}`);
148
- console.log('\nKnown projects:');
149
- context.projects.forEach((p) => console.log(` - ${p.name}`));
163
+ console.log(`❌ Project not found: "${projectName}"\n`);
164
+ console.log('Did you mean one of these?\n');
165
+ // Show projects sorted by similarity to the query
166
+ const lowerQuery = projectName.toLowerCase();
167
+ const sorted = [...context.projects].sort((a, b) => {
168
+ const aScore = a.name.toLowerCase().includes(lowerQuery) ? 1 : 0;
169
+ const bScore = b.name.toLowerCase().includes(lowerQuery) ? 1 : 0;
170
+ return bScore - aScore;
171
+ });
172
+ sorted.slice(0, 5).forEach((p) => console.log(` ${p.name} (${p.path})`));
173
+ if (context.projects.length > 5) {
174
+ console.log(`\n ... and ${context.projects.length - 5} more. Run 'claude-memory list' to see all.`);
175
+ }
150
176
  return;
151
177
  }
152
178
  console.log(`\n📁 ${project.name}\n`);
@@ -168,7 +194,10 @@ program
168
194
  .action((projectQuery) => {
169
195
  const context = loadContext();
170
196
  if (!context) {
171
- console.log('No projects scanned yet. Run: claude-memory scan');
197
+ console.log('No projects scanned yet.\n');
198
+ console.log('Get started:');
199
+ console.log(' claude-memory setup # Interactive wizard (recommended)');
200
+ console.log(' claude-memory scan ~/Projects # Scan a specific directory');
172
201
  return;
173
202
  }
174
203
  // Find the project
@@ -181,7 +210,8 @@ program
181
210
  console.log(' This project will be skipped in future scans.');
182
211
  return;
183
212
  }
184
- console.log(`Project not found: ${projectQuery}`);
213
+ console.log(`❌ Project not found: "${projectQuery}"\n`);
214
+ console.log('Tip: Use the project name or path. Run "claude-memory list" to see all projects.');
185
215
  return;
186
216
  }
187
217
  addExclusion(project.path);
@@ -198,16 +228,23 @@ program
198
228
  .action((projectQuery) => {
199
229
  const context = loadContext();
200
230
  if (!context) {
201
- console.log('No projects scanned yet. Run: claude-memory scan');
231
+ console.log('No projects scanned yet.\n');
232
+ console.log('Get started:');
233
+ console.log(' claude-memory setup # Interactive wizard (recommended)');
234
+ console.log(' claude-memory scan ~/Projects # Scan a specific directory');
202
235
  return;
203
236
  }
204
237
  const excluded = context.excludedProjects || [];
205
238
  const match = excluded.find(p => p.toLowerCase().includes(projectQuery.toLowerCase()));
206
239
  if (!match) {
207
- console.log(`Project not in exclusion list: ${projectQuery}`);
240
+ console.log(`❌ Project not in exclusion list: "${projectQuery}"\n`);
208
241
  if (excluded.length > 0) {
209
- console.log('\nCurrently excluded:');
210
- excluded.forEach(p => console.log(` - ${p}`));
242
+ console.log('Currently excluded:');
243
+ excluded.forEach(p => console.log(` ${p}`));
244
+ console.log('\nRun "claude-memory excluded" to see all excluded projects.');
245
+ }
246
+ else {
247
+ console.log('No projects are currently excluded.');
211
248
  }
212
249
  return;
213
250
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Centralized constants for Claude Memory
3
+ */
4
+ export declare const GIT_TIMEOUT_MS = 10000;
5
+ export declare const UPDATE_CHECK_INTERVAL_MS: number;
6
+ export declare const STALE_DATA_HOURS = 24;
7
+ export declare const STALE_PROJECT_DAYS = 7;
8
+ export declare const MAX_README_CHARS = 3000;
9
+ export declare const MAX_DESCRIPTION_CHARS = 200;
10
+ export declare const PRODUCT_URL = "https://claude-memory.dev";
11
+ export declare const DISCORD_URL = "https://discord.gg/JBpsSFB7EQ";
12
+ export declare const NPM_REGISTRY_URL = "https://registry.npmjs.org/@contextmirror/claude-memory";
13
+ export declare const LEMONSQUEEZY_VALIDATE_URL = "https://api.lemonsqueezy.com/v1/licenses/validate";
14
+ export declare const MEMORY_DIR_NAME = ".claude-memory";
15
+ export declare const CONTEXT_FILE = "context.json";
16
+ export declare const CONTEXT_BACKUP_FILE = "context.json.bak";
17
+ export declare const CONFIG_FILE = "config.json";
18
+ export declare const LICENSE_FILE = "license.json";
19
+ export declare const UPDATE_CACHE_FILE = "update-check.json";
20
+ export declare const GLOBAL_CONTEXT_MD = "global-context.md";
21
+ export declare const PROJECT_MARKERS: string[];
22
+ export declare const IGNORE_DIRS: string[];
23
+ export declare const EXIT_SUCCESS = 0;
24
+ export declare const EXIT_ERROR = 1;
25
+ export declare const EXIT_INVALID_ARGS = 2;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Centralized constants for Claude Memory
3
+ */
4
+ // Timeouts
5
+ export const GIT_TIMEOUT_MS = 10000;
6
+ export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
7
+ // Staleness thresholds
8
+ export const STALE_DATA_HOURS = 24;
9
+ export const STALE_PROJECT_DAYS = 7;
10
+ // File limits
11
+ export const MAX_README_CHARS = 3000;
12
+ export const MAX_DESCRIPTION_CHARS = 200;
13
+ // URLs
14
+ export const PRODUCT_URL = 'https://claude-memory.dev';
15
+ export const DISCORD_URL = 'https://discord.gg/JBpsSFB7EQ';
16
+ export const NPM_REGISTRY_URL = 'https://registry.npmjs.org/@contextmirror/claude-memory';
17
+ export const LEMONSQUEEZY_VALIDATE_URL = 'https://api.lemonsqueezy.com/v1/licenses/validate';
18
+ // Paths
19
+ export const MEMORY_DIR_NAME = '.claude-memory';
20
+ export const CONTEXT_FILE = 'context.json';
21
+ export const CONTEXT_BACKUP_FILE = 'context.json.bak';
22
+ export const CONFIG_FILE = 'config.json';
23
+ export const LICENSE_FILE = 'license.json';
24
+ export const UPDATE_CACHE_FILE = 'update-check.json';
25
+ export const GLOBAL_CONTEXT_MD = 'global-context.md';
26
+ // Project detection patterns
27
+ export const PROJECT_MARKERS = [
28
+ 'package.json',
29
+ 'Cargo.toml',
30
+ 'pyproject.toml',
31
+ 'go.mod',
32
+ '.git',
33
+ 'requirements.txt',
34
+ ];
35
+ // Directories to ignore during scanning
36
+ export const IGNORE_DIRS = [
37
+ 'node_modules',
38
+ '.git',
39
+ 'dist',
40
+ 'build',
41
+ '__pycache__',
42
+ 'target',
43
+ '.venv',
44
+ 'venv',
45
+ 'coverage',
46
+ '.next',
47
+ '.nuxt',
48
+ '.cache',
49
+ ];
50
+ // Exit codes
51
+ export const EXIT_SUCCESS = 0;
52
+ export const EXIT_ERROR = 1;
53
+ export const EXIT_INVALID_ARGS = 2;
@@ -15,14 +15,18 @@ export declare function saveLicense(license: License): void;
15
15
  */
16
16
  export declare function isPro(): boolean;
17
17
  /**
18
- * Validate a license key format
18
+ * Check if key is LemonSqueezy format
19
+ */
20
+ export declare function isLemonSqueezyKey(key: string): boolean;
21
+ /**
22
+ * Validate a license key format (accepts both LemonSqueezy and legacy formats)
19
23
  */
20
24
  export declare function validateKeyFormat(key: string): boolean;
21
25
  /**
22
26
  * Activate a license key
23
27
  *
24
- * Phase 1: Offline validation (format check only)
25
- * Phase 2: Will call LemonSqueezy API for server validation
28
+ * For LemonSqueezy keys: Validates against their API
29
+ * For legacy keys: Format check only (backwards compatibility)
26
30
  */
27
31
  export declare function activateLicense(key: string): Promise<LicenseValidationResult>;
28
32
  /**
@@ -4,10 +4,14 @@
4
4
  import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
5
5
  import { join } from 'path';
6
6
  import { homedir } from 'os';
7
+ import { LEMONSQUEEZY_VALIDATE_URL } from '../constants.js';
7
8
  const MEMORY_DIR = join(homedir(), '.claude-memory');
8
9
  const LICENSE_PATH = join(MEMORY_DIR, 'license.json');
9
- // License key format: CM-XXXX-XXXX-XXXX (alphanumeric)
10
- const LICENSE_KEY_PATTERN = /^CM-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/i;
10
+ // License key formats:
11
+ // - LemonSqueezy: UUID format (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
12
+ // - Legacy: CM-XXXX-XXXX-XXXX (alphanumeric)
13
+ const LEMONSQUEEZY_KEY_PATTERN = /^[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}$/i;
14
+ const LEGACY_KEY_PATTERN = /^CM-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/i;
11
15
  /**
12
16
  * Ensure the memory directory exists
13
17
  */
@@ -59,38 +63,92 @@ export function isPro() {
59
63
  return true;
60
64
  }
61
65
  /**
62
- * Validate a license key format
66
+ * Check if key is LemonSqueezy format
67
+ */
68
+ export function isLemonSqueezyKey(key) {
69
+ return LEMONSQUEEZY_KEY_PATTERN.test(key);
70
+ }
71
+ /**
72
+ * Validate a license key format (accepts both LemonSqueezy and legacy formats)
63
73
  */
64
74
  export function validateKeyFormat(key) {
65
- return LICENSE_KEY_PATTERN.test(key);
75
+ return LEMONSQUEEZY_KEY_PATTERN.test(key) || LEGACY_KEY_PATTERN.test(key);
76
+ }
77
+ /**
78
+ * Validate license key with LemonSqueezy API
79
+ */
80
+ async function validateWithLemonSqueezy(key) {
81
+ try {
82
+ const response = await fetch(LEMONSQUEEZY_VALIDATE_URL, {
83
+ method: 'POST',
84
+ headers: {
85
+ 'Accept': 'application/json',
86
+ 'Content-Type': 'application/json',
87
+ },
88
+ body: JSON.stringify({
89
+ license_key: key,
90
+ }),
91
+ });
92
+ const data = await response.json();
93
+ if (data.valid) {
94
+ return {
95
+ valid: true,
96
+ email: data.meta?.customer_email,
97
+ };
98
+ }
99
+ else {
100
+ return {
101
+ valid: false,
102
+ error: data.error || 'License key is not valid',
103
+ };
104
+ }
105
+ }
106
+ catch (err) {
107
+ // Network error - allow offline activation with warning
108
+ return {
109
+ valid: true, // Allow offline - will revalidate later
110
+ error: 'Could not reach license server. Activating offline.',
111
+ };
112
+ }
66
113
  }
67
114
  /**
68
115
  * Activate a license key
69
116
  *
70
- * Phase 1: Offline validation (format check only)
71
- * Phase 2: Will call LemonSqueezy API for server validation
117
+ * For LemonSqueezy keys: Validates against their API
118
+ * For legacy keys: Format check only (backwards compatibility)
72
119
  */
73
120
  export async function activateLicense(key) {
74
- // Normalize key to uppercase
121
+ // Normalize key (LemonSqueezy keys are uppercase hex)
75
122
  const normalizedKey = key.toUpperCase();
76
123
  // Validate format
77
124
  if (!validateKeyFormat(normalizedKey)) {
78
125
  return {
79
126
  valid: false,
80
- error: 'Invalid license key format. Expected: CM-XXXX-XXXX-XXXX',
127
+ error: 'Invalid license key format. Get your key from https://claude-memory.dev',
81
128
  };
82
129
  }
83
- // TODO: Phase 2 - Call LemonSqueezy API to validate
84
- // const response = await fetch('https://api.claude-memory.dev/validate', {
85
- // method: 'POST',
86
- // body: JSON.stringify({ key: normalizedKey }),
87
- // });
88
- // For now, accept any valid format (Phase 1 - offline)
130
+ let email;
131
+ // Validate LemonSqueezy keys with their API
132
+ if (isLemonSqueezyKey(normalizedKey)) {
133
+ const result = await validateWithLemonSqueezy(normalizedKey);
134
+ if (!result.valid) {
135
+ return {
136
+ valid: false,
137
+ error: result.error || 'License validation failed',
138
+ };
139
+ }
140
+ email = result.email;
141
+ // Show offline warning if applicable
142
+ if (result.error) {
143
+ console.log(`⚠️ ${result.error}`);
144
+ }
145
+ }
89
146
  const license = {
90
147
  key: normalizedKey,
91
148
  plan: 'pro',
92
149
  activatedAt: new Date().toISOString(),
93
- // No expiration for now
150
+ email,
151
+ // Subscription licenses don't expire locally - LemonSqueezy handles this
94
152
  };
95
153
  saveLicense(license);
96
154
  return {
@@ -11,14 +11,21 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
11
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
12
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
13
13
  import { readFileSync, existsSync, readdirSync, statSync, writeFileSync } from 'fs';
14
- import { join } from 'path';
14
+ import { join, dirname } from 'path';
15
15
  import { homedir } from 'os';
16
+ import { fileURLToPath } from 'url';
16
17
  import { DEFAULT_MEMORY_CONFIG } from '../types/index.js';
17
18
  import { isPro, getProFeatureMessage } from '../license/index.js';
18
19
  import { detectStaleProjects, checkCurrentProjectStaleness, formatStalenessForMcp } from '../scanner/stalenessDetector.js';
19
20
  import { checkForUpdate, formatUpdateMessage } from '../utils/updateChecker.js';
21
+ import { searchCode, formatSearchResults } from '../utils/codeSearch.js';
20
22
  const MEMORY_DIR = join(homedir(), '.claude-memory');
21
- const CURRENT_VERSION = '0.4.0'; // Update this with each release
23
+ // Read version from package.json to ensure single source of truth
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = dirname(__filename);
26
+ const packageJsonPath = join(__dirname, '../../package.json');
27
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
28
+ const CURRENT_VERSION = packageJson.version;
22
29
  // Tool definitions
23
30
  const tools = [
24
31
  {
@@ -405,22 +412,22 @@ function handleSearchCode(query, filePattern) {
405
412
  if (!context) {
406
413
  return 'No projects scanned yet. Run `claude-memory scan` first.';
407
414
  }
408
- const lines = [
409
- `# Code Search Results for "${query}"`,
410
- '',
411
- ];
412
- // TODO: Implement actual code search
413
- // For now, return a placeholder that shows the feature is gated
414
- lines.push('Searching across projects:');
415
- for (const p of context.projects) {
416
- lines.push(`- ${p.name} (${p.path})`);
415
+ if (!query || query.trim().length === 0) {
416
+ return 'Please provide a search query.';
417
417
  }
418
- lines.push('');
419
- lines.push(`Pattern: ${filePattern || '*'}`);
420
- lines.push('');
421
- lines.push('> Note: Full code search implementation coming soon.');
422
- lines.push('> This will index and search actual file contents across all your projects.');
423
- return lines.join('\n');
418
+ // Map projects to the format expected by searchCode
419
+ const projects = context.projects.map(p => ({
420
+ name: p.name,
421
+ path: p.path,
422
+ }));
423
+ // Perform the search
424
+ const results = searchCode(projects, query, {
425
+ filePattern,
426
+ maxResults: 50,
427
+ caseSensitive: false,
428
+ contextLines: 1,
429
+ });
430
+ return formatSearchResults(results, query);
424
431
  }
425
432
  function handleGetProjectAnalysis(projectQuery) {
426
433
  const context = loadContext();
@@ -559,7 +566,7 @@ function getDirectoryStructure(dir, maxDepth, prefix = '', depth = 0) {
559
566
  async function main() {
560
567
  const server = new Server({
561
568
  name: 'claude-memory',
562
- version: '0.1.0',
569
+ version: CURRENT_VERSION,
563
570
  }, {
564
571
  capabilities: {
565
572
  tools: {},
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Code search utility for Pro feature
3
+ */
4
+ export interface SearchResult {
5
+ project: string;
6
+ projectPath: string;
7
+ file: string;
8
+ relativePath: string;
9
+ line: number;
10
+ content: string;
11
+ context: {
12
+ before: string;
13
+ after: string;
14
+ };
15
+ }
16
+ export interface SearchOptions {
17
+ filePattern?: string;
18
+ maxResults?: number;
19
+ caseSensitive?: boolean;
20
+ contextLines?: number;
21
+ }
22
+ /**
23
+ * Search for code across multiple projects
24
+ */
25
+ export declare function searchCode(projects: Array<{
26
+ name: string;
27
+ path: string;
28
+ }>, query: string, options?: SearchOptions): SearchResult[];
29
+ /**
30
+ * Format search results for display
31
+ */
32
+ export declare function formatSearchResults(results: SearchResult[], query: string): string;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Code search utility for Pro feature
3
+ */
4
+ import { readdirSync, readFileSync, statSync } from 'fs';
5
+ import { join, extname, relative } from 'path';
6
+ import { IGNORE_DIRS } from '../constants.js';
7
+ const DEFAULT_OPTIONS = {
8
+ maxResults: 50,
9
+ caseSensitive: false,
10
+ contextLines: 1,
11
+ };
12
+ // File extensions to search
13
+ const SEARCHABLE_EXTENSIONS = new Set([
14
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
15
+ '.py', '.pyw',
16
+ '.rs',
17
+ '.go',
18
+ '.java', '.kt', '.scala',
19
+ '.c', '.cpp', '.h', '.hpp',
20
+ '.rb',
21
+ '.php',
22
+ '.swift',
23
+ '.cs',
24
+ '.vue', '.svelte',
25
+ '.json', '.yaml', '.yml', '.toml',
26
+ '.md', '.txt',
27
+ '.sql',
28
+ '.sh', '.bash', '.zsh',
29
+ '.css', '.scss', '.sass', '.less',
30
+ '.html', '.htm',
31
+ '.xml',
32
+ ]);
33
+ // Max file size to search (1MB)
34
+ const MAX_FILE_SIZE = 1024 * 1024;
35
+ /**
36
+ * Check if a file should be searched based on extension
37
+ */
38
+ function shouldSearchFile(filePath, filePattern) {
39
+ const ext = extname(filePath).toLowerCase();
40
+ // If a pattern is specified, use glob-like matching
41
+ if (filePattern) {
42
+ if (filePattern.startsWith('*.')) {
43
+ const patternExt = filePattern.slice(1);
44
+ return ext === patternExt;
45
+ }
46
+ return filePath.includes(filePattern);
47
+ }
48
+ return SEARCHABLE_EXTENSIONS.has(ext);
49
+ }
50
+ /**
51
+ * Search for a pattern in a file
52
+ */
53
+ function searchFile(filePath, query, options) {
54
+ const results = [];
55
+ try {
56
+ const stat = statSync(filePath);
57
+ if (stat.size > MAX_FILE_SIZE) {
58
+ return results;
59
+ }
60
+ const content = readFileSync(filePath, 'utf-8');
61
+ const lines = content.split('\n');
62
+ const searchQuery = options.caseSensitive ? query : query.toLowerCase();
63
+ for (let i = 0; i < lines.length; i++) {
64
+ const line = lines[i];
65
+ const searchLine = options.caseSensitive ? line : line.toLowerCase();
66
+ if (searchLine.includes(searchQuery)) {
67
+ const contextLines = options.contextLines || 1;
68
+ const beforeLines = lines.slice(Math.max(0, i - contextLines), i);
69
+ const afterLines = lines.slice(i + 1, Math.min(lines.length, i + 1 + contextLines));
70
+ results.push({
71
+ line: i + 1,
72
+ content: line.trim(),
73
+ context: {
74
+ before: beforeLines.join('\n'),
75
+ after: afterLines.join('\n'),
76
+ },
77
+ });
78
+ }
79
+ }
80
+ }
81
+ catch {
82
+ // Skip files that can't be read
83
+ }
84
+ return results;
85
+ }
86
+ /**
87
+ * Recursively search a directory
88
+ */
89
+ function searchDirectory(dir, query, projectName, projectPath, options, results) {
90
+ if (results.length >= (options.maxResults || DEFAULT_OPTIONS.maxResults)) {
91
+ return;
92
+ }
93
+ try {
94
+ const entries = readdirSync(dir);
95
+ for (const entry of entries) {
96
+ if (results.length >= (options.maxResults || DEFAULT_OPTIONS.maxResults)) {
97
+ return;
98
+ }
99
+ if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) {
100
+ continue;
101
+ }
102
+ const fullPath = join(dir, entry);
103
+ try {
104
+ const stat = statSync(fullPath);
105
+ if (stat.isDirectory()) {
106
+ searchDirectory(fullPath, query, projectName, projectPath, options, results);
107
+ }
108
+ else if (stat.isFile() && shouldSearchFile(fullPath, options.filePattern)) {
109
+ const fileResults = searchFile(fullPath, query, options);
110
+ for (const result of fileResults) {
111
+ if (results.length >= (options.maxResults || DEFAULT_OPTIONS.maxResults)) {
112
+ return;
113
+ }
114
+ results.push({
115
+ project: projectName,
116
+ projectPath,
117
+ file: fullPath,
118
+ relativePath: relative(projectPath, fullPath),
119
+ line: result.line,
120
+ content: result.content,
121
+ context: result.context,
122
+ });
123
+ }
124
+ }
125
+ }
126
+ catch {
127
+ // Skip inaccessible files
128
+ }
129
+ }
130
+ }
131
+ catch {
132
+ // Skip inaccessible directories
133
+ }
134
+ }
135
+ /**
136
+ * Search for code across multiple projects
137
+ */
138
+ export function searchCode(projects, query, options = {}) {
139
+ const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
140
+ const results = [];
141
+ for (const project of projects) {
142
+ if (results.length >= mergedOptions.maxResults) {
143
+ break;
144
+ }
145
+ searchDirectory(project.path, query, project.name, project.path, mergedOptions, results);
146
+ }
147
+ return results;
148
+ }
149
+ /**
150
+ * Format search results for display
151
+ */
152
+ export function formatSearchResults(results, query) {
153
+ if (results.length === 0) {
154
+ return `No results found for "${query}".`;
155
+ }
156
+ const lines = [
157
+ `# Code Search Results for "${query}"`,
158
+ '',
159
+ `Found ${results.length} result${results.length === 1 ? '' : 's'}:`,
160
+ '',
161
+ ];
162
+ // Group by project
163
+ const byProject = new Map();
164
+ for (const result of results) {
165
+ const existing = byProject.get(result.project) || [];
166
+ existing.push(result);
167
+ byProject.set(result.project, existing);
168
+ }
169
+ for (const [project, projectResults] of byProject) {
170
+ lines.push(`## ${project}`);
171
+ lines.push('');
172
+ for (const result of projectResults) {
173
+ lines.push(`### ${result.relativePath}:${result.line}`);
174
+ lines.push('```');
175
+ if (result.context.before) {
176
+ lines.push(result.context.before);
177
+ }
178
+ lines.push(`> ${result.content}`);
179
+ if (result.context.after) {
180
+ lines.push(result.context.after);
181
+ }
182
+ lines.push('```');
183
+ lines.push('');
184
+ }
185
+ }
186
+ return lines.join('\n');
187
+ }