@contextmirror/claude-memory 0.4.2 → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Nathan / Context Mirror
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # claude-memory
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@contextmirror/claude-memory.svg)](https://www.npmjs.com/package/@contextmirror/claude-memory)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org/)
6
+ [![Discord](https://img.shields.io/discord/1454173469242822669?color=5865F2&label=Discord&logo=discord&logoColor=white)](https://discord.gg/JBpsSFB7EQ)
7
+
3
8
  Cross-project memory for Claude Code. Give Claude awareness of ALL your projects, not just the one you're in.
4
9
 
5
10
  ## The Problem
@@ -298,6 +303,12 @@ Run `claude-memory scan --quick` when you've made changes to multiple projects.
298
303
  **Is my code uploaded anywhere?**
299
304
  No. All data stays in `~/.claude-memory/` on your machine. Nothing is sent to any server.
300
305
 
306
+ ## Community
307
+
308
+ Join our Discord for feedback, feature requests, and support:
309
+
310
+ [![Discord](https://img.shields.io/discord/1454173469242822669?color=5865F2&label=Discord&logo=discord&logoColor=white)](https://discord.gg/JBpsSFB7EQ)
311
+
301
312
  ## License
302
313
 
303
314
  MIT
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,65 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { formatSearchResults } from '../utils/codeSearch.js';
3
+ describe('Code Search', () => {
4
+ describe('formatSearchResults', () => {
5
+ it('should format empty results correctly', () => {
6
+ const result = formatSearchResults([], 'test');
7
+ expect(result).toBe('No results found for "test".');
8
+ });
9
+ it('should format single result correctly', () => {
10
+ const results = [{
11
+ project: 'my-project',
12
+ projectPath: '/home/user/my-project',
13
+ file: '/home/user/my-project/src/index.ts',
14
+ relativePath: 'src/index.ts',
15
+ line: 10,
16
+ content: 'const test = "hello";',
17
+ context: {
18
+ before: 'import { something } from "./lib";',
19
+ after: 'export default test;',
20
+ },
21
+ }];
22
+ const result = formatSearchResults(results, 'test');
23
+ expect(result).toContain('# Code Search Results for "test"');
24
+ expect(result).toContain('Found 1 result');
25
+ expect(result).toContain('## my-project');
26
+ expect(result).toContain('src/index.ts:10');
27
+ expect(result).toContain('const test = "hello";');
28
+ });
29
+ it('should group results by project', () => {
30
+ const results = [
31
+ {
32
+ project: 'project-a',
33
+ projectPath: '/path/a',
34
+ file: '/path/a/file1.ts',
35
+ relativePath: 'file1.ts',
36
+ line: 1,
37
+ content: 'line 1',
38
+ context: { before: '', after: '' },
39
+ },
40
+ {
41
+ project: 'project-b',
42
+ projectPath: '/path/b',
43
+ file: '/path/b/file2.ts',
44
+ relativePath: 'file2.ts',
45
+ line: 2,
46
+ content: 'line 2',
47
+ context: { before: '', after: '' },
48
+ },
49
+ {
50
+ project: 'project-a',
51
+ projectPath: '/path/a',
52
+ file: '/path/a/file3.ts',
53
+ relativePath: 'file3.ts',
54
+ line: 3,
55
+ content: 'line 3',
56
+ context: { before: '', after: '' },
57
+ },
58
+ ];
59
+ const result = formatSearchResults(results, 'query');
60
+ expect(result).toContain('Found 3 results');
61
+ expect(result).toContain('## project-a');
62
+ expect(result).toContain('## project-b');
63
+ });
64
+ });
65
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { validateKeyFormat, isLemonSqueezyKey } from '../license/index.js';
3
+ describe('License Key Validation', () => {
4
+ describe('validateKeyFormat', () => {
5
+ it('should accept valid LemonSqueezy UUID keys', () => {
6
+ expect(validateKeyFormat('A1B2C3D4-E5F6-7890-ABCD-EF1234567890')).toBe(true);
7
+ expect(validateKeyFormat('a1b2c3d4-e5f6-7890-abcd-ef1234567890')).toBe(true);
8
+ });
9
+ it('should accept valid legacy CM- keys', () => {
10
+ expect(validateKeyFormat('CM-ABCD-1234-WXYZ')).toBe(true);
11
+ expect(validateKeyFormat('cm-abcd-1234-wxyz')).toBe(true);
12
+ });
13
+ it('should reject invalid key formats', () => {
14
+ expect(validateKeyFormat('')).toBe(false);
15
+ expect(validateKeyFormat('invalid-key')).toBe(false);
16
+ expect(validateKeyFormat('CM-ABC-1234-WXYZ')).toBe(false); // Wrong segment length
17
+ expect(validateKeyFormat('12345678-1234-1234-1234-123456789')).toBe(false); // Too short
18
+ });
19
+ });
20
+ describe('isLemonSqueezyKey', () => {
21
+ it('should identify LemonSqueezy UUID keys', () => {
22
+ expect(isLemonSqueezyKey('A1B2C3D4-E5F6-7890-ABCD-EF1234567890')).toBe(true);
23
+ });
24
+ it('should not identify legacy keys as LemonSqueezy', () => {
25
+ expect(isLemonSqueezyKey('CM-ABCD-1234-WXYZ')).toBe(false);
26
+ });
27
+ });
28
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { detectStaleProjects } from '../scanner/stalenessDetector.js';
3
+ function createMockProject(overrides = {}) {
4
+ return {
5
+ name: 'test-project',
6
+ path: '/home/user/projects/test-project',
7
+ description: 'A test project',
8
+ language: 'typescript',
9
+ techStack: ['Node.js', 'TypeScript'],
10
+ currentBranch: 'main',
11
+ isDirty: false,
12
+ lastActivity: new Date().toISOString(),
13
+ hasFiles: {
14
+ readme: true,
15
+ claudeMd: false,
16
+ packageJson: true,
17
+ gitignore: true,
18
+ },
19
+ insights: [],
20
+ lastScanned: new Date().toISOString(),
21
+ ...overrides,
22
+ };
23
+ }
24
+ function createMockContext(projects = [], hoursAgo = 0) {
25
+ const lastUpdated = new Date(Date.now() - hoursAgo * 60 * 60 * 1000).toISOString();
26
+ return {
27
+ schemaVersion: 1,
28
+ lastUpdated,
29
+ projects,
30
+ insights: [],
31
+ userPatterns: [],
32
+ };
33
+ }
34
+ describe('Staleness Detection', () => {
35
+ describe('detectStaleProjects', () => {
36
+ it('should return no stale projects for fresh data', () => {
37
+ const context = createMockContext([createMockProject()], 0);
38
+ const report = detectStaleProjects(context);
39
+ expect(report.staleProjects).toHaveLength(0);
40
+ expect(report.dataAgeHours).toBeLessThan(1);
41
+ });
42
+ it('should mark data as stale after 24 hours', () => {
43
+ const context = createMockContext([createMockProject()], 25);
44
+ const report = detectStaleProjects(context);
45
+ expect(report.dataAgeHours).toBeGreaterThan(24);
46
+ });
47
+ it('should detect projects scanned more than 7 days ago', () => {
48
+ const oldScanDate = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000).toISOString();
49
+ const project = createMockProject({ lastScanned: oldScanDate });
50
+ const context = createMockContext([project], 0);
51
+ const report = detectStaleProjects(context);
52
+ expect(report.staleProjects.length).toBeGreaterThanOrEqual(0);
53
+ });
54
+ it('should handle empty projects list', () => {
55
+ const context = createMockContext([], 0);
56
+ const report = detectStaleProjects(context);
57
+ expect(report.staleProjects).toHaveLength(0);
58
+ });
59
+ });
60
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ // Test version comparison logic
3
+ function isNewerVersion(current, latest) {
4
+ const currentParts = current.split('.').map(Number);
5
+ const latestParts = latest.split('.').map(Number);
6
+ for (let i = 0; i < 3; i++) {
7
+ const curr = currentParts[i] || 0;
8
+ const lat = latestParts[i] || 0;
9
+ if (lat > curr)
10
+ return true;
11
+ if (lat < curr)
12
+ return false;
13
+ }
14
+ return false;
15
+ }
16
+ describe('Update Checker', () => {
17
+ describe('version comparison', () => {
18
+ it('should detect newer major versions', () => {
19
+ expect(isNewerVersion('0.4.2', '1.0.0')).toBe(true);
20
+ expect(isNewerVersion('1.0.0', '2.0.0')).toBe(true);
21
+ });
22
+ it('should detect newer minor versions', () => {
23
+ expect(isNewerVersion('0.4.2', '0.5.0')).toBe(true);
24
+ expect(isNewerVersion('0.4.2', '0.5.1')).toBe(true);
25
+ });
26
+ it('should detect newer patch versions', () => {
27
+ expect(isNewerVersion('0.4.2', '0.4.3')).toBe(true);
28
+ expect(isNewerVersion('0.4.2', '0.4.10')).toBe(true);
29
+ });
30
+ it('should return false for same version', () => {
31
+ expect(isNewerVersion('0.4.2', '0.4.2')).toBe(false);
32
+ });
33
+ it('should return false for older versions', () => {
34
+ expect(isNewerVersion('0.4.2', '0.4.1')).toBe(false);
35
+ expect(isNewerVersion('0.4.2', '0.3.9')).toBe(false);
36
+ expect(isNewerVersion('1.0.0', '0.9.9')).toBe(false);
37
+ });
38
+ it('should handle missing patch version', () => {
39
+ expect(isNewerVersion('0.4', '0.4.1')).toBe(true);
40
+ expect(isNewerVersion('0.4.1', '0.5')).toBe(true);
41
+ });
42
+ });
43
+ });
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.4.2');
32
+ .version(VERSION);
26
33
  // Scan command
27
34
  program
28
35
  .command('scan')
@@ -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;