@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 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
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * License types for Claude Memory Pro
3
+ */
4
+ export {};
@@ -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
- 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