@contextmirror/claude-memory 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +168 -0
- package/dist/license/index.d.ts +39 -0
- package/dist/license/index.js +153 -0
- package/dist/license/types.d.ts +37 -0
- package/dist/license/types.js +4 -0
- package/dist/mcp/server.js +58 -0
- package/dist/scanner/contextGenerator.d.ts +7 -0
- package/dist/scanner/contextGenerator.js +97 -10
- package/dist/scanner/projectScanner.js +59 -15
- package/dist/setup/setupWizard.js +28 -3
- package/dist/types/index.d.ts +7 -1
- package/dist/types/index.js +35 -2
- package/landing/index.html +478 -169
- package/package.json +2 -1
- package/.claude/settings.local.json +0 -10
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ import { scanProjects } from './scanner/projectScanner.js';
|
|
|
16
16
|
import { generateGlobalContext, writeGlobalContext, getMemoryDir } from './scanner/contextGenerator.js';
|
|
17
17
|
import { generateBriefing, briefingToClaudeMd } from './briefing/briefingGenerator.js';
|
|
18
18
|
import { runSetupWizard } from './setup/setupWizard.js';
|
|
19
|
+
import { activateLicense, deactivateLicense, getLicenseStatus } from './license/index.js';
|
|
19
20
|
const program = new Command();
|
|
20
21
|
program
|
|
21
22
|
.name('claude-memory')
|
|
@@ -35,6 +36,11 @@ program
|
|
|
35
36
|
maxDepth: parseInt(options.depth, 10),
|
|
36
37
|
generateClaudeMd: options.generateClaudeMd,
|
|
37
38
|
});
|
|
39
|
+
if (projects.length === 0) {
|
|
40
|
+
console.log('\n⚠️ No projects found. Context not updated.');
|
|
41
|
+
console.log(' Check the directory path and try again.');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
38
44
|
const context = generateGlobalContext(projects);
|
|
39
45
|
writeGlobalContext(context);
|
|
40
46
|
console.log('\n✅ Done! Global context updated.');
|
|
@@ -89,6 +95,106 @@ program
|
|
|
89
95
|
console.log(` CLAUDE.md: ${project.hasFiles.claudeMd ? '✓' : '✗'}`);
|
|
90
96
|
console.log(` README.md: ${project.hasFiles.readme ? '✓' : '✗'}`);
|
|
91
97
|
});
|
|
98
|
+
// Exclude command - exclude a project from scanning
|
|
99
|
+
program
|
|
100
|
+
.command('exclude')
|
|
101
|
+
.description('Exclude a project from future scans')
|
|
102
|
+
.argument('<project>', 'Project path or name')
|
|
103
|
+
.action((projectQuery) => {
|
|
104
|
+
const context = loadContext();
|
|
105
|
+
if (!context) {
|
|
106
|
+
console.log('No projects scanned yet. Run: claude-memory scan');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// Find the project
|
|
110
|
+
const project = context.projects.find((p) => p.name.toLowerCase() === projectQuery.toLowerCase() || p.path.includes(projectQuery));
|
|
111
|
+
if (!project) {
|
|
112
|
+
// Maybe it's a direct path
|
|
113
|
+
if (existsSync(projectQuery)) {
|
|
114
|
+
addExclusion(projectQuery);
|
|
115
|
+
console.log(`✅ Excluded: ${projectQuery}`);
|
|
116
|
+
console.log(' This project will be skipped in future scans.');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
console.log(`Project not found: ${projectQuery}`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
addExclusion(project.path);
|
|
123
|
+
console.log(`✅ Excluded: ${project.name}`);
|
|
124
|
+
console.log(` Path: ${project.path}`);
|
|
125
|
+
console.log(' This project will be skipped in future scans.');
|
|
126
|
+
console.log(' Run `claude-memory include` to re-add it.');
|
|
127
|
+
});
|
|
128
|
+
// Include command - remove a project from exclusion list
|
|
129
|
+
program
|
|
130
|
+
.command('include')
|
|
131
|
+
.description('Remove a project from the exclusion list')
|
|
132
|
+
.argument('<project>', 'Project path or name')
|
|
133
|
+
.action((projectQuery) => {
|
|
134
|
+
const context = loadContext();
|
|
135
|
+
if (!context) {
|
|
136
|
+
console.log('No projects scanned yet. Run: claude-memory scan');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const excluded = context.excludedProjects || [];
|
|
140
|
+
const match = excluded.find(p => p.toLowerCase().includes(projectQuery.toLowerCase()));
|
|
141
|
+
if (!match) {
|
|
142
|
+
console.log(`Project not in exclusion list: ${projectQuery}`);
|
|
143
|
+
if (excluded.length > 0) {
|
|
144
|
+
console.log('\nCurrently excluded:');
|
|
145
|
+
excluded.forEach(p => console.log(` - ${p}`));
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
removeExclusion(match);
|
|
150
|
+
console.log(`✅ Removed from exclusion list: ${match}`);
|
|
151
|
+
console.log(' Run `claude-memory scan` to add it back to your projects.');
|
|
152
|
+
});
|
|
153
|
+
// Excluded command - list excluded projects
|
|
154
|
+
program
|
|
155
|
+
.command('excluded')
|
|
156
|
+
.description('List excluded projects')
|
|
157
|
+
.action(() => {
|
|
158
|
+
const context = loadContext();
|
|
159
|
+
const excluded = context?.excludedProjects || [];
|
|
160
|
+
if (excluded.length === 0) {
|
|
161
|
+
console.log('No excluded projects.');
|
|
162
|
+
console.log('Use `claude-memory exclude <project>` to exclude a project.');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
console.log('🚫 Excluded Projects\n');
|
|
166
|
+
excluded.forEach(p => console.log(` ${p}`));
|
|
167
|
+
console.log('\nUse `claude-memory include <project>` to re-add.');
|
|
168
|
+
});
|
|
169
|
+
function addExclusion(projectPath) {
|
|
170
|
+
const contextPath = join(getMemoryDir(), 'context.json');
|
|
171
|
+
if (!existsSync(contextPath))
|
|
172
|
+
return;
|
|
173
|
+
try {
|
|
174
|
+
const context = JSON.parse(readFileSync(contextPath, 'utf-8'));
|
|
175
|
+
context.excludedProjects = context.excludedProjects || [];
|
|
176
|
+
if (!context.excludedProjects.includes(projectPath)) {
|
|
177
|
+
context.excludedProjects.push(projectPath);
|
|
178
|
+
writeFileSync(contextPath, JSON.stringify(context, null, 2), 'utf-8');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
console.error('Failed to update exclusion list');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function removeExclusion(projectPath) {
|
|
186
|
+
const contextPath = join(getMemoryDir(), 'context.json');
|
|
187
|
+
if (!existsSync(contextPath))
|
|
188
|
+
return;
|
|
189
|
+
try {
|
|
190
|
+
const context = JSON.parse(readFileSync(contextPath, 'utf-8'));
|
|
191
|
+
context.excludedProjects = (context.excludedProjects || []).filter((p) => p !== projectPath);
|
|
192
|
+
writeFileSync(contextPath, JSON.stringify(context, null, 2), 'utf-8');
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
console.error('Failed to update exclusion list');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
92
198
|
// Briefing command - deep project analysis and CLAUDE.md generation
|
|
93
199
|
program
|
|
94
200
|
.command('briefing')
|
|
@@ -193,6 +299,68 @@ program
|
|
|
193
299
|
interactive: !options.nonInteractive,
|
|
194
300
|
});
|
|
195
301
|
});
|
|
302
|
+
// Activate command - activate a Pro license
|
|
303
|
+
program
|
|
304
|
+
.command('activate')
|
|
305
|
+
.description('Activate a Pro license key')
|
|
306
|
+
.argument('<key>', 'License key (format: CM-XXXX-XXXX-XXXX)')
|
|
307
|
+
.action(async (key) => {
|
|
308
|
+
console.log('Activating license...\n');
|
|
309
|
+
const result = await activateLicense(key);
|
|
310
|
+
if (!result.valid) {
|
|
311
|
+
console.log(`❌ ${result.error}`);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
console.log('✅ License activated successfully!');
|
|
315
|
+
console.log(` Plan: Pro`);
|
|
316
|
+
if (result.license?.email) {
|
|
317
|
+
console.log(` Email: ${result.license.email}`);
|
|
318
|
+
}
|
|
319
|
+
console.log('\nPro features are now unlocked.');
|
|
320
|
+
});
|
|
321
|
+
// Status command - show license status
|
|
322
|
+
program
|
|
323
|
+
.command('status')
|
|
324
|
+
.description('Show license status')
|
|
325
|
+
.action(() => {
|
|
326
|
+
const status = getLicenseStatus();
|
|
327
|
+
console.log('🧠 Claude Memory License Status\n');
|
|
328
|
+
console.log(` Plan: ${status.plan === 'pro' ? 'Pro' : 'Free'}`);
|
|
329
|
+
if (status.isPro) {
|
|
330
|
+
if (status.email) {
|
|
331
|
+
console.log(` Email: ${status.email}`);
|
|
332
|
+
}
|
|
333
|
+
if (status.activatedAt) {
|
|
334
|
+
console.log(` Activated: ${new Date(status.activatedAt).toLocaleDateString()}`);
|
|
335
|
+
}
|
|
336
|
+
if (status.expiresAt) {
|
|
337
|
+
console.log(` Expires: ${new Date(status.expiresAt).toLocaleDateString()}`);
|
|
338
|
+
if (status.daysRemaining) {
|
|
339
|
+
console.log(` Days remaining: ${status.daysRemaining}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
console.log(` Status: Active`);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
console.log(` Status: ${status.expiresAt ? 'Expired' : 'Not activated'}`);
|
|
346
|
+
console.log('\n Upgrade at https://claude-memory.dev');
|
|
347
|
+
console.log(' Or run: claude-memory activate <your-key>');
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
// Deactivate command - remove license
|
|
351
|
+
program
|
|
352
|
+
.command('deactivate')
|
|
353
|
+
.description('Remove Pro license')
|
|
354
|
+
.action(() => {
|
|
355
|
+
const removed = deactivateLicense();
|
|
356
|
+
if (removed) {
|
|
357
|
+
console.log('✅ License removed.');
|
|
358
|
+
console.log(' You are now on the Free plan.');
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
console.log('No license to remove.');
|
|
362
|
+
}
|
|
363
|
+
});
|
|
196
364
|
function loadContext() {
|
|
197
365
|
const contextPath = join(getMemoryDir(), 'context.json');
|
|
198
366
|
if (!existsSync(contextPath)) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* License management for Claude Memory Pro
|
|
3
|
+
*/
|
|
4
|
+
import { License, LicenseStatus, LicenseValidationResult } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Load license from disk
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadLicense(): License | null;
|
|
9
|
+
/**
|
|
10
|
+
* Save license to disk
|
|
11
|
+
*/
|
|
12
|
+
export declare function saveLicense(license: License): void;
|
|
13
|
+
/**
|
|
14
|
+
* Check if user has Pro license
|
|
15
|
+
*/
|
|
16
|
+
export declare function isPro(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Validate a license key format
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateKeyFormat(key: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Activate a license key
|
|
23
|
+
*
|
|
24
|
+
* Phase 1: Offline validation (format check only)
|
|
25
|
+
* Phase 2: Will call LemonSqueezy API for server validation
|
|
26
|
+
*/
|
|
27
|
+
export declare function activateLicense(key: string): Promise<LicenseValidationResult>;
|
|
28
|
+
/**
|
|
29
|
+
* Deactivate (remove) the current license
|
|
30
|
+
*/
|
|
31
|
+
export declare function deactivateLicense(): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Get current license status
|
|
34
|
+
*/
|
|
35
|
+
export declare function getLicenseStatus(): LicenseStatus;
|
|
36
|
+
/**
|
|
37
|
+
* Get a user-friendly error message for Pro features
|
|
38
|
+
*/
|
|
39
|
+
export declare function getProFeatureMessage(featureName: string): string;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* License management for Claude Memory Pro
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
const MEMORY_DIR = join(homedir(), '.claude-memory');
|
|
8
|
+
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;
|
|
11
|
+
/**
|
|
12
|
+
* Ensure the memory directory exists
|
|
13
|
+
*/
|
|
14
|
+
function ensureMemoryDir() {
|
|
15
|
+
if (!existsSync(MEMORY_DIR)) {
|
|
16
|
+
mkdirSync(MEMORY_DIR, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Load license from disk
|
|
21
|
+
*/
|
|
22
|
+
export function loadLicense() {
|
|
23
|
+
if (!existsSync(LICENSE_PATH)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const data = readFileSync(LICENSE_PATH, 'utf-8');
|
|
28
|
+
return JSON.parse(data);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Save license to disk
|
|
36
|
+
*/
|
|
37
|
+
export function saveLicense(license) {
|
|
38
|
+
ensureMemoryDir();
|
|
39
|
+
writeFileSync(LICENSE_PATH, JSON.stringify(license, null, 2), 'utf-8');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check if user has Pro license
|
|
43
|
+
*/
|
|
44
|
+
export function isPro() {
|
|
45
|
+
const license = loadLicense();
|
|
46
|
+
if (!license) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (license.plan !== 'pro') {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
// Check expiration
|
|
53
|
+
if (license.expiresAt) {
|
|
54
|
+
const expiresAt = new Date(license.expiresAt);
|
|
55
|
+
if (expiresAt < new Date()) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Validate a license key format
|
|
63
|
+
*/
|
|
64
|
+
export function validateKeyFormat(key) {
|
|
65
|
+
return LICENSE_KEY_PATTERN.test(key);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Activate a license key
|
|
69
|
+
*
|
|
70
|
+
* Phase 1: Offline validation (format check only)
|
|
71
|
+
* Phase 2: Will call LemonSqueezy API for server validation
|
|
72
|
+
*/
|
|
73
|
+
export async function activateLicense(key) {
|
|
74
|
+
// Normalize key to uppercase
|
|
75
|
+
const normalizedKey = key.toUpperCase();
|
|
76
|
+
// Validate format
|
|
77
|
+
if (!validateKeyFormat(normalizedKey)) {
|
|
78
|
+
return {
|
|
79
|
+
valid: false,
|
|
80
|
+
error: 'Invalid license key format. Expected: CM-XXXX-XXXX-XXXX',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
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)
|
|
89
|
+
const license = {
|
|
90
|
+
key: normalizedKey,
|
|
91
|
+
plan: 'pro',
|
|
92
|
+
activatedAt: new Date().toISOString(),
|
|
93
|
+
// No expiration for now
|
|
94
|
+
};
|
|
95
|
+
saveLicense(license);
|
|
96
|
+
return {
|
|
97
|
+
valid: true,
|
|
98
|
+
license,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Deactivate (remove) the current license
|
|
103
|
+
*/
|
|
104
|
+
export function deactivateLicense() {
|
|
105
|
+
if (!existsSync(LICENSE_PATH)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
unlinkSync(LICENSE_PATH);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get current license status
|
|
118
|
+
*/
|
|
119
|
+
export function getLicenseStatus() {
|
|
120
|
+
const license = loadLicense();
|
|
121
|
+
if (!license) {
|
|
122
|
+
return {
|
|
123
|
+
isPro: false,
|
|
124
|
+
plan: 'free',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
let daysRemaining;
|
|
128
|
+
let isExpired = false;
|
|
129
|
+
if (license.expiresAt) {
|
|
130
|
+
const expiresAt = new Date(license.expiresAt);
|
|
131
|
+
const now = new Date();
|
|
132
|
+
const diffMs = expiresAt.getTime() - now.getTime();
|
|
133
|
+
daysRemaining = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
|
134
|
+
isExpired = daysRemaining < 0;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
isPro: license.plan === 'pro' && !isExpired,
|
|
138
|
+
plan: isExpired ? 'free' : license.plan,
|
|
139
|
+
email: license.email,
|
|
140
|
+
expiresAt: license.expiresAt,
|
|
141
|
+
daysRemaining: daysRemaining && daysRemaining > 0 ? daysRemaining : undefined,
|
|
142
|
+
activatedAt: license.activatedAt,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Get a user-friendly error message for Pro features
|
|
147
|
+
*/
|
|
148
|
+
export function getProFeatureMessage(featureName) {
|
|
149
|
+
return `${featureName} is a Pro feature.
|
|
150
|
+
|
|
151
|
+
Upgrade at https://claude-memory.dev or activate your license:
|
|
152
|
+
claude-memory activate <your-key>`;
|
|
153
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* License types for Claude Memory Pro
|
|
3
|
+
*/
|
|
4
|
+
export interface License {
|
|
5
|
+
/** License key (format: CM-XXXX-XXXX-XXXX) */
|
|
6
|
+
key: string;
|
|
7
|
+
/** Email associated with the license */
|
|
8
|
+
email?: string;
|
|
9
|
+
/** License plan */
|
|
10
|
+
plan: 'free' | 'pro';
|
|
11
|
+
/** When the license was activated */
|
|
12
|
+
activatedAt: string;
|
|
13
|
+
/** When the license expires (if applicable) */
|
|
14
|
+
expiresAt?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface LicenseStatus {
|
|
17
|
+
/** Whether the user has Pro features */
|
|
18
|
+
isPro: boolean;
|
|
19
|
+
/** Current plan */
|
|
20
|
+
plan: 'free' | 'pro';
|
|
21
|
+
/** Email associated with license */
|
|
22
|
+
email?: string;
|
|
23
|
+
/** Expiration date */
|
|
24
|
+
expiresAt?: string;
|
|
25
|
+
/** Days remaining until expiration */
|
|
26
|
+
daysRemaining?: number;
|
|
27
|
+
/** When activated */
|
|
28
|
+
activatedAt?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface LicenseValidationResult {
|
|
31
|
+
/** Whether the key is valid */
|
|
32
|
+
valid: boolean;
|
|
33
|
+
/** Error message if invalid */
|
|
34
|
+
error?: string;
|
|
35
|
+
/** License details if valid */
|
|
36
|
+
license?: License;
|
|
37
|
+
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -14,6 +14,7 @@ import { readFileSync, existsSync, readdirSync, statSync, writeFileSync } from '
|
|
|
14
14
|
import { join } from 'path';
|
|
15
15
|
import { homedir } from 'os';
|
|
16
16
|
import { DEFAULT_MEMORY_CONFIG } from '../types/index.js';
|
|
17
|
+
import { isPro, getProFeatureMessage } from '../license/index.js';
|
|
17
18
|
const MEMORY_DIR = join(homedir(), '.claude-memory');
|
|
18
19
|
// Tool definitions
|
|
19
20
|
const tools = [
|
|
@@ -92,6 +93,25 @@ const tools = [
|
|
|
92
93
|
required: ['project'],
|
|
93
94
|
},
|
|
94
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
|
+
},
|
|
95
115
|
];
|
|
96
116
|
function loadContext() {
|
|
97
117
|
const contextPath = join(MEMORY_DIR, 'context.json');
|
|
@@ -243,9 +263,15 @@ function handleGetGlobalContext(cwd) {
|
|
|
243
263
|
lines.push('No projects scanned yet. Run `claude-memory scan` first.');
|
|
244
264
|
return lines.join('\n');
|
|
245
265
|
}
|
|
266
|
+
// Check for stale data
|
|
267
|
+
const hoursSinceUpdate = (Date.now() - new Date(context.lastUpdated).getTime()) / (1000 * 60 * 60);
|
|
246
268
|
lines.push('# Your Development Environment');
|
|
247
269
|
lines.push('');
|
|
248
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
|
+
}
|
|
249
275
|
lines.push('');
|
|
250
276
|
lines.push('## Active Projects');
|
|
251
277
|
lines.push('');
|
|
@@ -344,6 +370,35 @@ function handleRecordInsight(content, relatedProjects) {
|
|
|
344
370
|
return `Failed to record insight: ${err instanceof Error ? err.message : 'Unknown error'}`;
|
|
345
371
|
}
|
|
346
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
|
+
}
|
|
347
402
|
function handleGetProjectAnalysis(projectQuery) {
|
|
348
403
|
const context = loadContext();
|
|
349
404
|
if (!context) {
|
|
@@ -512,6 +567,9 @@ async function main() {
|
|
|
512
567
|
case 'get_project_analysis':
|
|
513
568
|
result = handleGetProjectAnalysis(args.project);
|
|
514
569
|
break;
|
|
570
|
+
case 'search_code':
|
|
571
|
+
result = handleSearchCode(args.query, args.filePattern);
|
|
572
|
+
break;
|
|
515
573
|
default:
|
|
516
574
|
throw new Error(`Unknown tool: ${name}`);
|
|
517
575
|
}
|
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
* Context Generator - Produces markdown context files
|
|
3
3
|
*/
|
|
4
4
|
import { ProjectInfo, GlobalContext } from '../types/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Load existing context from disk (if any)
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadExistingContext(): GlobalContext | null;
|
|
5
9
|
/**
|
|
6
10
|
* Generate global context from scanned projects
|
|
11
|
+
* Preserves existing insights and user patterns
|
|
7
12
|
*/
|
|
8
13
|
export declare function generateGlobalContext(projects: ProjectInfo[]): GlobalContext;
|
|
9
14
|
/**
|
|
10
15
|
* Write global context to ~/.claude-memory/
|
|
16
|
+
* Uses atomic write (write to temp, then rename) to prevent corruption
|
|
17
|
+
* Creates backup before overwriting
|
|
11
18
|
*/
|
|
12
19
|
export declare function writeGlobalContext(context: GlobalContext): void;
|
|
13
20
|
/**
|
|
@@ -1,38 +1,125 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Context Generator - Produces markdown context files
|
|
3
3
|
*/
|
|
4
|
-
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
4
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync, renameSync, unlinkSync, copyFileSync } from 'fs';
|
|
5
5
|
import { join } from 'path';
|
|
6
6
|
import { homedir } from 'os';
|
|
7
|
+
import { SCHEMA_VERSION } from '../types/index.js';
|
|
7
8
|
const MEMORY_DIR = join(homedir(), '.claude-memory');
|
|
9
|
+
/**
|
|
10
|
+
* Load existing context from disk (if any)
|
|
11
|
+
*/
|
|
12
|
+
export function loadExistingContext() {
|
|
13
|
+
const jsonPath = join(MEMORY_DIR, 'context.json');
|
|
14
|
+
if (!existsSync(jsonPath)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const data = JSON.parse(readFileSync(jsonPath, 'utf-8'));
|
|
19
|
+
return migrateContext(data);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Migrate old context format to current schema
|
|
27
|
+
*/
|
|
28
|
+
function migrateContext(data) {
|
|
29
|
+
const version = data.schemaVersion || 0;
|
|
30
|
+
// Migration from v0 (no version) to v1
|
|
31
|
+
if (version < 1) {
|
|
32
|
+
// v0 didn't have schemaVersion, excludedProjects
|
|
33
|
+
return {
|
|
34
|
+
schemaVersion: SCHEMA_VERSION,
|
|
35
|
+
lastUpdated: data.lastUpdated || new Date().toISOString(),
|
|
36
|
+
projects: data.projects || [],
|
|
37
|
+
insights: data.insights || [],
|
|
38
|
+
userPatterns: data.userPatterns || [],
|
|
39
|
+
excludedProjects: [],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// Already current version
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
8
45
|
/**
|
|
9
46
|
* Generate global context from scanned projects
|
|
47
|
+
* Preserves existing insights and user patterns
|
|
10
48
|
*/
|
|
11
49
|
export function generateGlobalContext(projects) {
|
|
50
|
+
// Load existing context to preserve insights
|
|
51
|
+
const existing = loadExistingContext();
|
|
12
52
|
return {
|
|
53
|
+
schemaVersion: SCHEMA_VERSION,
|
|
13
54
|
lastUpdated: new Date().toISOString(),
|
|
14
55
|
projects,
|
|
15
|
-
|
|
16
|
-
|
|
56
|
+
// Preserve existing insights and patterns!
|
|
57
|
+
insights: existing?.insights || [],
|
|
58
|
+
userPatterns: existing?.userPatterns || [],
|
|
59
|
+
excludedProjects: existing?.excludedProjects || [],
|
|
17
60
|
};
|
|
18
61
|
}
|
|
19
62
|
/**
|
|
20
63
|
* Write global context to ~/.claude-memory/
|
|
64
|
+
* Uses atomic write (write to temp, then rename) to prevent corruption
|
|
65
|
+
* Creates backup before overwriting
|
|
21
66
|
*/
|
|
22
67
|
export function writeGlobalContext(context) {
|
|
23
68
|
// Ensure directory exists
|
|
24
69
|
if (!existsSync(MEMORY_DIR)) {
|
|
25
70
|
mkdirSync(MEMORY_DIR, { recursive: true });
|
|
26
71
|
}
|
|
27
|
-
// Write JSON for MCP server
|
|
28
72
|
const jsonPath = join(MEMORY_DIR, 'context.json');
|
|
29
|
-
writeFileSync(jsonPath, JSON.stringify(context, null, 2));
|
|
30
|
-
// Write markdown for human readability
|
|
31
73
|
const mdPath = join(MEMORY_DIR, 'global-context.md');
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
74
|
+
const jsonTempPath = join(MEMORY_DIR, 'context.json.tmp');
|
|
75
|
+
const mdTempPath = join(MEMORY_DIR, 'global-context.md.tmp');
|
|
76
|
+
const jsonBackupPath = join(MEMORY_DIR, 'context.json.bak');
|
|
77
|
+
try {
|
|
78
|
+
// Create backup of existing context before overwriting
|
|
79
|
+
if (existsSync(jsonPath)) {
|
|
80
|
+
try {
|
|
81
|
+
copyFileSync(jsonPath, jsonBackupPath);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Backup failed, but continue anyway
|
|
85
|
+
console.warn(' ⚠️ Could not create backup');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Write to temp files first (atomic write pattern)
|
|
89
|
+
const jsonContent = JSON.stringify(context, null, 2);
|
|
90
|
+
writeFileSync(jsonTempPath, jsonContent, 'utf-8');
|
|
91
|
+
const mdContent = formatGlobalContextMarkdown(context);
|
|
92
|
+
writeFileSync(mdTempPath, mdContent, 'utf-8');
|
|
93
|
+
// Verify JSON can be parsed back
|
|
94
|
+
const verification = JSON.parse(readFileSync(jsonTempPath, 'utf-8'));
|
|
95
|
+
if (verification.projects.length !== context.projects.length) {
|
|
96
|
+
throw new Error(`Write verification failed: expected ${context.projects.length} projects, got ${verification.projects.length}`);
|
|
97
|
+
}
|
|
98
|
+
// Atomic rename (much safer than direct write)
|
|
99
|
+
renameSync(jsonTempPath, jsonPath);
|
|
100
|
+
renameSync(mdTempPath, mdPath);
|
|
101
|
+
console.log(`\n📝 Written to:`);
|
|
102
|
+
console.log(` ${jsonPath} (${context.projects.length} projects)`);
|
|
103
|
+
console.log(` ${mdPath}`);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
// Clean up temp files if they exist
|
|
107
|
+
try {
|
|
108
|
+
if (existsSync(jsonTempPath))
|
|
109
|
+
unlinkSync(jsonTempPath);
|
|
110
|
+
if (existsSync(mdTempPath))
|
|
111
|
+
unlinkSync(mdTempPath);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Ignore cleanup errors
|
|
115
|
+
}
|
|
116
|
+
console.error(`\n❌ Failed to write context:`);
|
|
117
|
+
console.error(` ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
118
|
+
if (existsSync(jsonBackupPath)) {
|
|
119
|
+
console.error(` 💾 Backup available at: ${jsonBackupPath}`);
|
|
120
|
+
}
|
|
121
|
+
throw err; // Re-throw so caller knows it failed
|
|
122
|
+
}
|
|
36
123
|
}
|
|
37
124
|
/**
|
|
38
125
|
* Format global context as markdown
|