@contextmirror/claude-memory 0.2.2 ā 0.3.0
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
|
-
|
|
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:');
|
package/dist/mcp/server.js
CHANGED
|
@@ -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.2.3'; // 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
|
-
|
|
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?:
|
|
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
|
+
}
|