@contextmirror/claude-memory 0.2.2 → 0.3.1

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
@@ -17,6 +17,7 @@ import { generateGlobalContext, writeGlobalContext, getMemoryDir } from './scann
17
17
  import { generateBriefing, briefingToClaudeMd } from './briefing/briefingGenerator.js';
18
18
  import { runSetupWizard } from './setup/setupWizard.js';
19
19
  import { activateLicense, deactivateLicense, getLicenseStatus } from './license/index.js';
20
+ import { detectStaleProjects } from './scanner/stalenessDetector.js';
20
21
  const program = new Command();
21
22
  program
22
23
  .name('claude-memory')
@@ -29,19 +30,83 @@ program
29
30
  .argument('[directory]', 'Root directory to scan', `${homedir()}/Project`)
30
31
  .option('-d, --depth <number>', 'Max depth to search', '2')
31
32
  .option('--generate-claude-md', 'Generate CLAUDE.md for projects without one')
33
+ .option('-q, --quick', 'Only rescan stale projects (faster)')
34
+ .option('--check', 'Only check for stale projects, don\'t scan')
32
35
  .action(async (directory, options) => {
36
+ // Check-only mode: just report staleness
37
+ if (options.check) {
38
+ const context = loadContext();
39
+ if (!context) {
40
+ console.log('No projects scanned yet. Run: claude-memory scan');
41
+ return;
42
+ }
43
+ const report = detectStaleProjects(context);
44
+ console.log('🧠 Claude Memory - Staleness Check\n');
45
+ console.log(`Last full scan: ${new Date(context.lastUpdated).toLocaleString()}`);
46
+ console.log(`Data age: ${report.dataAgeHours} hours\n`);
47
+ if (report.staleProjects.length === 0) {
48
+ console.log('āœ… All projects are up to date!');
49
+ }
50
+ else {
51
+ console.log(`āš ļø ${report.staleProjects.length} project(s) need updating:\n`);
52
+ for (const p of report.staleProjects) {
53
+ const icon = p.reason === 'git_activity' ? 'šŸ“' :
54
+ p.reason === 'file_changed' ? 'šŸ“„' : 'ā°';
55
+ console.log(` ${icon} ${p.name}`);
56
+ console.log(` ${p.details}`);
57
+ }
58
+ console.log(`\n${report.freshCount} project(s) are fresh.`);
59
+ console.log('\nRun `claude-memory scan --quick` to refresh only stale projects.');
60
+ }
61
+ return;
62
+ }
33
63
  console.log('🧠 Claude Memory Scanner\n');
64
+ // Quick mode: only scan stale projects
65
+ let projectsToScan;
66
+ if (options.quick) {
67
+ const existingContext = loadContext();
68
+ if (existingContext) {
69
+ const report = detectStaleProjects(existingContext);
70
+ if (report.staleProjects.length === 0) {
71
+ console.log('āœ… All projects are up to date! Nothing to scan.');
72
+ return;
73
+ }
74
+ projectsToScan = report.staleProjects.map(p => p.path);
75
+ console.log(`šŸ”„ Quick mode: Rescanning ${projectsToScan.length} stale project(s)...\n`);
76
+ }
77
+ }
34
78
  const projects = await scanProjects({
35
79
  rootDir: directory,
36
80
  maxDepth: parseInt(options.depth, 10),
37
81
  generateClaudeMd: options.generateClaudeMd,
82
+ onlyPaths: projectsToScan,
38
83
  });
39
84
  if (projects.length === 0) {
40
85
  console.log('\nāš ļø No projects found. Context not updated.');
41
86
  console.log(' Check the directory path and try again.');
42
87
  return;
43
88
  }
44
- const context = generateGlobalContext(projects);
89
+ // For quick mode, merge with existing context
90
+ let context;
91
+ if (options.quick && projectsToScan) {
92
+ const existingContext = loadContext();
93
+ if (existingContext) {
94
+ // Replace only the rescanned projects
95
+ const rescannedPaths = new Set(projects.map(p => p.path));
96
+ const unchangedProjects = existingContext.projects.filter(p => !rescannedPaths.has(p.path));
97
+ context = {
98
+ ...existingContext,
99
+ lastUpdated: new Date().toISOString(),
100
+ projects: [...unchangedProjects, ...projects],
101
+ };
102
+ }
103
+ else {
104
+ context = generateGlobalContext(projects);
105
+ }
106
+ }
107
+ else {
108
+ context = generateGlobalContext(projects);
109
+ }
45
110
  writeGlobalContext(context);
46
111
  console.log('\nāœ… Done! Global context updated.');
47
112
  console.log('\nTo use with Claude Code, add the MCP server to your config:');
@@ -15,7 +15,9 @@ import { join } from 'path';
15
15
  import { homedir } from 'os';
16
16
  import { DEFAULT_MEMORY_CONFIG } from '../types/index.js';
17
17
  import { isPro, getProFeatureMessage } from '../license/index.js';
18
+ import { detectStaleProjects, checkCurrentProjectStaleness, formatStalenessForMcp } from '../scanner/stalenessDetector.js';
18
19
  const MEMORY_DIR = join(homedir(), '.claude-memory');
20
+ const CURRENT_VERSION = '0.3.0'; // Update this with each release
19
21
  // Tool definitions
20
22
  const tools = [
21
23
  {
@@ -238,6 +240,13 @@ function handleGetGlobalContext(cwd) {
238
240
  lines.push(`**Status:** ${cwdStatus.status}`);
239
241
  if (cwdStatus.projectName) {
240
242
  lines.push(`**Project:** ${cwdStatus.projectName}`);
243
+ // Check if current project is stale
244
+ if (context) {
245
+ const currentStaleness = checkCurrentProjectStaleness(context, cwd);
246
+ if (currentStaleness.isStale && currentStaleness.reason) {
247
+ lines.push(`**āš ļø Stale:** ${currentStaleness.reason}`);
248
+ }
249
+ }
241
250
  }
242
251
  if (cwdStatus.needsSetup) {
243
252
  lines.push(`**Needs Setup:** Yes`);
@@ -267,8 +276,15 @@ function handleGetGlobalContext(cwd) {
267
276
  const hoursSinceUpdate = (Date.now() - new Date(context.lastUpdated).getTime()) / (1000 * 60 * 60);
268
277
  lines.push('# Your Development Environment');
269
278
  lines.push('');
270
- lines.push(`Last scanned: ${new Date(context.lastUpdated).toLocaleString()}`);
271
- if (hoursSinceUpdate > 24) {
279
+ lines.push(`Last scanned: ${new Date(context.lastUpdated).toLocaleString()} | claude-memory v${CURRENT_VERSION}`);
280
+ // Run detailed staleness check
281
+ const stalenessReport = detectStaleProjects(context);
282
+ // Add staleness warnings if needed
283
+ if (stalenessReport.staleProjects.length > 0) {
284
+ lines.push('');
285
+ lines.push(formatStalenessForMcp(stalenessReport));
286
+ }
287
+ else if (hoursSinceUpdate > 24) {
272
288
  lines.push('');
273
289
  lines.push(`> āš ļø **Data may be stale** (${Math.floor(hoursSinceUpdate)} hours old). Run \`claude-memory scan\` to refresh.`);
274
290
  }
@@ -2,7 +2,15 @@
2
2
  * Project Scanner - Discovers and analyzes projects
3
3
  */
4
4
  import { ProjectInfo, ScanOptions } from '../types/index.js';
5
+ /**
6
+ * Extended scan options with onlyPaths support
7
+ */
8
+ interface ExtendedScanOptions extends Partial<ScanOptions> {
9
+ /** If provided, only scan these specific paths (for quick/incremental scan) */
10
+ onlyPaths?: string[];
11
+ }
5
12
  /**
6
13
  * Scan a directory for projects and extract information
7
14
  */
8
- export declare function scanProjects(options?: Partial<ScanOptions>): Promise<ProjectInfo[]>;
15
+ export declare function scanProjects(options?: ExtendedScanOptions): Promise<ProjectInfo[]>;
16
+ export {};
@@ -13,6 +13,31 @@ export async function scanProjects(options = {}) {
13
13
  const opts = { ...DEFAULT_SCAN_OPTIONS, ...options };
14
14
  const projects = [];
15
15
  const errors = [];
16
+ // Quick mode: only scan specific paths
17
+ if (options.onlyPaths && options.onlyPaths.length > 0) {
18
+ console.log(`šŸ” Quick scan: ${options.onlyPaths.length} project(s)...`);
19
+ for (const projectPath of options.onlyPaths) {
20
+ if (!existsSync(projectPath)) {
21
+ console.warn(` ⚠ Path not found: ${projectPath}`);
22
+ continue;
23
+ }
24
+ try {
25
+ const info = await analyzeProject(projectPath);
26
+ if (info) {
27
+ projects.push(info);
28
+ console.log(` āœ“ ${info.name} (${info.language})`);
29
+ }
30
+ }
31
+ catch (err) {
32
+ const errorMsg = err instanceof Error ? err.message : 'Unknown error';
33
+ errors.push({ path: projectPath, error: errorMsg });
34
+ console.warn(` ⚠ Failed: ${basename(projectPath)} - ${errorMsg}`);
35
+ }
36
+ }
37
+ console.log(`\nšŸ“Š Quick scan results: ${projects.length} project(s) updated`);
38
+ return projects;
39
+ }
40
+ // Full scan mode
16
41
  console.log(`šŸ” Scanning ${opts.rootDir} for projects...`);
17
42
  // Verify root directory exists
18
43
  if (!existsSync(opts.rootDir)) {
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Staleness detection for claude-memory
3
+ * Detects which projects need rescanning based on age and git activity
4
+ */
5
+ import { GlobalContext, ProjectInfo } from '../types/index.js';
6
+ export interface StaleProject {
7
+ name: string;
8
+ path: string;
9
+ daysSinceScanned: number;
10
+ reason: 'age' | 'git_activity' | 'file_changed' | 'never_scanned';
11
+ details?: string;
12
+ }
13
+ export interface StalenessReport {
14
+ /** Overall data age in hours */
15
+ dataAgeHours: number;
16
+ /** Projects that need rescanning */
17
+ staleProjects: StaleProject[];
18
+ /** Projects that are fresh */
19
+ freshCount: number;
20
+ /** Human-readable summary */
21
+ summary: string;
22
+ /** Suggested CLI command */
23
+ suggestion?: string;
24
+ }
25
+ /**
26
+ * Detect all stale projects in the global context
27
+ */
28
+ export declare function detectStaleProjects(context: GlobalContext): StalenessReport;
29
+ /**
30
+ * Get a quick staleness check for the current working directory
31
+ */
32
+ export declare function checkCurrentProjectStaleness(context: GlobalContext, cwd: string): {
33
+ isStale: boolean;
34
+ project?: ProjectInfo;
35
+ reason?: string;
36
+ };
37
+ /**
38
+ * Format staleness report for MCP output
39
+ */
40
+ export declare function formatStalenessForMcp(report: StalenessReport): string;
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Staleness detection for claude-memory
3
+ * Detects which projects need rescanning based on age and git activity
4
+ */
5
+ import { execSync } from 'child_process';
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ /** Thresholds for staleness (in days) */
9
+ const STALENESS_THRESHOLDS = {
10
+ WARNING: 3, // Yellow warning
11
+ STALE: 7, // Orange warning
12
+ CRITICAL: 14, // Red warning
13
+ };
14
+ /**
15
+ * Check if a project has new git commits since last scan
16
+ */
17
+ function hasNewCommits(projectPath, lastScanned) {
18
+ try {
19
+ const gitDir = path.join(projectPath, '.git');
20
+ if (!fs.existsSync(gitDir))
21
+ return false;
22
+ // Get latest commit date
23
+ const result = execSync('git log -1 --format=%cI 2>/dev/null || echo ""', { cwd: projectPath, timeout: 5000, encoding: 'utf-8' }).trim();
24
+ if (!result)
25
+ return false;
26
+ const lastCommitDate = new Date(result);
27
+ const lastScannedDate = new Date(lastScanned);
28
+ return lastCommitDate > lastScannedDate;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ /**
35
+ * Check if key project files have been modified since last scan
36
+ */
37
+ function hasModifiedFiles(projectPath, lastScanned) {
38
+ const keyFiles = [
39
+ 'package.json',
40
+ 'CLAUDE.md',
41
+ 'README.md',
42
+ 'Cargo.toml',
43
+ 'pyproject.toml',
44
+ 'go.mod',
45
+ ];
46
+ const lastScannedTime = new Date(lastScanned).getTime();
47
+ for (const file of keyFiles) {
48
+ try {
49
+ const filePath = path.join(projectPath, file);
50
+ if (fs.existsSync(filePath)) {
51
+ const stat = fs.statSync(filePath);
52
+ if (stat.mtimeMs > lastScannedTime) {
53
+ return true;
54
+ }
55
+ }
56
+ }
57
+ catch {
58
+ // Ignore errors
59
+ }
60
+ }
61
+ return false;
62
+ }
63
+ /**
64
+ * Analyze a single project for staleness
65
+ */
66
+ function analyzeProjectStaleness(project) {
67
+ const now = new Date();
68
+ const lastScanned = new Date(project.lastScanned);
69
+ const daysSinceScanned = Math.floor((now.getTime() - lastScanned.getTime()) / (1000 * 60 * 60 * 24));
70
+ // Check for git activity first (most reliable signal)
71
+ if (hasNewCommits(project.path, project.lastScanned)) {
72
+ return {
73
+ name: project.name,
74
+ path: project.path,
75
+ daysSinceScanned,
76
+ reason: 'git_activity',
77
+ details: 'New commits since last scan',
78
+ };
79
+ }
80
+ // Check for modified key files
81
+ if (hasModifiedFiles(project.path, project.lastScanned)) {
82
+ return {
83
+ name: project.name,
84
+ path: project.path,
85
+ daysSinceScanned,
86
+ reason: 'file_changed',
87
+ details: 'Key files modified since last scan',
88
+ };
89
+ }
90
+ // Check age threshold
91
+ if (daysSinceScanned >= STALENESS_THRESHOLDS.STALE) {
92
+ return {
93
+ name: project.name,
94
+ path: project.path,
95
+ daysSinceScanned,
96
+ reason: 'age',
97
+ details: `Not scanned in ${daysSinceScanned} days`,
98
+ };
99
+ }
100
+ return null;
101
+ }
102
+ /**
103
+ * Detect all stale projects in the global context
104
+ */
105
+ export function detectStaleProjects(context) {
106
+ const now = new Date();
107
+ const lastUpdated = new Date(context.lastUpdated);
108
+ const dataAgeHours = Math.floor((now.getTime() - lastUpdated.getTime()) / (1000 * 60 * 60));
109
+ const staleProjects = [];
110
+ for (const project of context.projects) {
111
+ const staleness = analyzeProjectStaleness(project);
112
+ if (staleness) {
113
+ staleProjects.push(staleness);
114
+ }
115
+ }
116
+ // Sort by days since scanned (most stale first)
117
+ staleProjects.sort((a, b) => b.daysSinceScanned - a.daysSinceScanned);
118
+ const freshCount = context.projects.length - staleProjects.length;
119
+ // Generate summary
120
+ let summary;
121
+ let suggestion;
122
+ if (staleProjects.length === 0) {
123
+ summary = `All ${context.projects.length} projects are up to date.`;
124
+ }
125
+ else if (staleProjects.length === 1) {
126
+ const p = staleProjects[0];
127
+ summary = `1 project needs updating: ${p.name} (${p.details})`;
128
+ suggestion = `claude-memory scan --quick`;
129
+ }
130
+ else {
131
+ const critical = staleProjects.filter(p => p.daysSinceScanned >= STALENESS_THRESHOLDS.CRITICAL);
132
+ if (critical.length > 0) {
133
+ summary = `āš ļø ${staleProjects.length} projects need updating (${critical.length} critical)`;
134
+ }
135
+ else {
136
+ summary = `${staleProjects.length} projects could use a refresh`;
137
+ }
138
+ suggestion = `claude-memory scan --quick`;
139
+ }
140
+ return {
141
+ dataAgeHours,
142
+ staleProjects,
143
+ freshCount,
144
+ summary,
145
+ suggestion,
146
+ };
147
+ }
148
+ /**
149
+ * Get a quick staleness check for the current working directory
150
+ */
151
+ export function checkCurrentProjectStaleness(context, cwd) {
152
+ // Find the project that matches the cwd
153
+ const project = context.projects.find(p => cwd.startsWith(p.path) || p.path.startsWith(cwd));
154
+ if (!project) {
155
+ return { isStale: false };
156
+ }
157
+ const staleness = analyzeProjectStaleness(project);
158
+ if (staleness) {
159
+ return {
160
+ isStale: true,
161
+ project,
162
+ reason: staleness.details,
163
+ };
164
+ }
165
+ return { isStale: false, project };
166
+ }
167
+ /**
168
+ * Format staleness report for MCP output
169
+ */
170
+ export function formatStalenessForMcp(report) {
171
+ const lines = [];
172
+ if (report.staleProjects.length === 0) {
173
+ return ''; // Don't add noise if everything is fresh
174
+ }
175
+ lines.push('');
176
+ lines.push('---');
177
+ lines.push('');
178
+ lines.push('### šŸ”„ Data Freshness');
179
+ lines.push('');
180
+ lines.push(report.summary);
181
+ if (report.staleProjects.length > 0 && report.staleProjects.length <= 5) {
182
+ lines.push('');
183
+ for (const p of report.staleProjects) {
184
+ const icon = p.reason === 'git_activity' ? 'šŸ“' :
185
+ p.reason === 'file_changed' ? 'šŸ“„' : 'ā°';
186
+ lines.push(`- ${icon} **${p.name}**: ${p.details}`);
187
+ }
188
+ }
189
+ else if (report.staleProjects.length > 5) {
190
+ lines.push('');
191
+ lines.push(`Stale projects: ${report.staleProjects.map(p => p.name).join(', ')}`);
192
+ }
193
+ if (report.suggestion) {
194
+ lines.push('');
195
+ lines.push(`> Run \`${report.suggestion}\` to refresh`);
196
+ }
197
+ return lines.join('\n');
198
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contextmirror/claude-memory",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "Cross-project memory for Claude Code - know about all your projects",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",