@contextmirror/claude-memory 0.2.1 ā 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 +234 -1
- 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 +75 -1
- package/dist/scanner/contextGenerator.d.ts +7 -0
- package/dist/scanner/contextGenerator.js +97 -10
- package/dist/scanner/projectScanner.d.ts +9 -1
- package/dist/scanner/projectScanner.js +84 -15
- package/dist/scanner/stalenessDetector.d.ts +40 -0
- package/dist/scanner/stalenessDetector.js +198 -0
- 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
|
@@ -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
|
|
@@ -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 {};
|
|
@@ -5,27 +5,86 @@ import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
|
5
5
|
import { join, basename } from 'path';
|
|
6
6
|
import { simpleGit } from 'simple-git';
|
|
7
7
|
import { DEFAULT_SCAN_OPTIONS } from '../types/index.js';
|
|
8
|
+
import { loadExistingContext } from './contextGenerator.js';
|
|
8
9
|
/**
|
|
9
10
|
* Scan a directory for projects and extract information
|
|
10
11
|
*/
|
|
11
12
|
export async function scanProjects(options = {}) {
|
|
12
13
|
const opts = { ...DEFAULT_SCAN_OPTIONS, ...options };
|
|
13
14
|
const projects = [];
|
|
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
|
|
14
41
|
console.log(`š Scanning ${opts.rootDir} for projects...`);
|
|
42
|
+
// Verify root directory exists
|
|
43
|
+
if (!existsSync(opts.rootDir)) {
|
|
44
|
+
console.error(`ā Directory does not exist: ${opts.rootDir}`);
|
|
45
|
+
console.error(` Check the path and try again.`);
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
15
48
|
const candidates = findProjectRoots(opts.rootDir, opts.maxDepth, opts.ignore);
|
|
16
|
-
|
|
49
|
+
if (candidates.length === 0) {
|
|
50
|
+
console.log(` No project directories found in ${opts.rootDir}`);
|
|
51
|
+
console.log(` Looking for: package.json, Cargo.toml, pyproject.toml, go.mod, or .git`);
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
// Load excluded projects from existing context
|
|
55
|
+
const existingContext = loadExistingContext();
|
|
56
|
+
const excludedProjects = new Set(existingContext?.excludedProjects || []);
|
|
57
|
+
// Filter out excluded projects
|
|
58
|
+
const filteredCandidates = candidates.filter(path => {
|
|
59
|
+
if (excludedProjects.has(path)) {
|
|
60
|
+
console.log(` ā Skipping excluded: ${basename(path)}`);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
});
|
|
65
|
+
console.log(` Found ${filteredCandidates.length} candidate directories\n`);
|
|
66
|
+
if (excludedProjects.size > 0) {
|
|
67
|
+
console.log(` (${excludedProjects.size} excluded)\n`);
|
|
68
|
+
}
|
|
69
|
+
for (const projectPath of filteredCandidates) {
|
|
17
70
|
try {
|
|
18
71
|
const info = await analyzeProject(projectPath);
|
|
19
72
|
if (info) {
|
|
20
73
|
projects.push(info);
|
|
21
|
-
console.log(` ā
|
|
74
|
+
console.log(` ā ${info.name} (${info.language}) - ${info.techStack.join(', ') || 'no stack detected'}`);
|
|
22
75
|
}
|
|
23
76
|
}
|
|
24
77
|
catch (err) {
|
|
25
|
-
|
|
78
|
+
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
|
|
79
|
+
errors.push({ path: projectPath, error: errorMsg });
|
|
80
|
+
console.warn(` ā Failed: ${basename(projectPath)} - ${errorMsg}`);
|
|
26
81
|
}
|
|
27
82
|
}
|
|
28
|
-
console.log(`\nš
|
|
83
|
+
console.log(`\nš Results:`);
|
|
84
|
+
console.log(` ā
Successfully scanned: ${projects.length}`);
|
|
85
|
+
if (errors.length > 0) {
|
|
86
|
+
console.log(` ā ļø Failed to scan: ${errors.length}`);
|
|
87
|
+
}
|
|
29
88
|
return projects;
|
|
30
89
|
}
|
|
31
90
|
/**
|
|
@@ -77,23 +136,33 @@ async function analyzeProject(projectPath) {
|
|
|
77
136
|
const name = detectProjectName(projectPath);
|
|
78
137
|
const description = detectDescription(projectPath);
|
|
79
138
|
const { language, techStack } = detectTechStack(projectPath);
|
|
80
|
-
// Git info
|
|
139
|
+
// Git info with timeout to prevent hanging on large/slow repos
|
|
81
140
|
let lastActivity = new Date().toISOString();
|
|
82
141
|
let currentBranch = 'unknown';
|
|
83
142
|
let isDirty = false;
|
|
84
143
|
if (existsSync(join(projectPath, '.git'))) {
|
|
85
144
|
try {
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
145
|
+
// 10 second timeout for git operations
|
|
146
|
+
const git = simpleGit(projectPath, { timeout: { block: 10000 } });
|
|
147
|
+
// Wrap in Promise.race for extra safety
|
|
148
|
+
const gitOps = async () => {
|
|
149
|
+
const log = await git.log({ maxCount: 1 });
|
|
150
|
+
if (log.latest) {
|
|
151
|
+
lastActivity = log.latest.date;
|
|
152
|
+
}
|
|
153
|
+
const status = await git.status();
|
|
154
|
+
currentBranch = status.current || 'unknown';
|
|
155
|
+
isDirty = !status.isClean();
|
|
156
|
+
};
|
|
157
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Git timeout')), 10000));
|
|
158
|
+
await Promise.race([gitOps(), timeout]);
|
|
94
159
|
}
|
|
95
|
-
catch {
|
|
96
|
-
// Git operations failed, use defaults
|
|
160
|
+
catch (err) {
|
|
161
|
+
// Git operations failed or timed out, use defaults
|
|
162
|
+
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
|
|
163
|
+
if (errorMsg.includes('timeout') || errorMsg.includes('Timeout')) {
|
|
164
|
+
console.warn(` ā ļø Git timeout for ${basename(projectPath)} - using defaults`);
|
|
165
|
+
}
|
|
97
166
|
}
|
|
98
167
|
}
|
|
99
168
|
// Extract insights from CLAUDE.md if it exists
|
|
@@ -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
|
+
}
|
|
@@ -25,12 +25,33 @@ const MCP_SERVER_CONFIG = {
|
|
|
25
25
|
* Run the setup wizard
|
|
26
26
|
*/
|
|
27
27
|
export async function runSetupWizard(options = {}) {
|
|
28
|
-
const { projectsDir
|
|
28
|
+
const { projectsDir: providedDir, skipMcp = false, skipBriefing = false, interactive = true, } = options;
|
|
29
29
|
console.log('');
|
|
30
30
|
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
31
31
|
console.log('ā š§ Claude Memory - Setup Wizard ā');
|
|
32
32
|
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
33
33
|
console.log('');
|
|
34
|
+
// Determine projects directory
|
|
35
|
+
let projectsDir = providedDir || guessProjectsDir();
|
|
36
|
+
if (!projectsDir) {
|
|
37
|
+
console.log('š Step 0: Locate your projects\n');
|
|
38
|
+
console.log(' Could not auto-detect your projects directory.');
|
|
39
|
+
console.log(' Common locations like ~/Projects, ~/Code, ~/dev were not found.\n');
|
|
40
|
+
if (interactive) {
|
|
41
|
+
const answer = await ask(' Enter your projects directory path: ');
|
|
42
|
+
if (!answer || !existsSync(answer)) {
|
|
43
|
+
console.log('\n ā Invalid directory. Please run:');
|
|
44
|
+
console.log(' claude-memory setup -d /path/to/your/projects');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
projectsDir = answer;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log(' ā Cannot continue without a projects directory.');
|
|
51
|
+
console.log(' Run: claude-memory setup -d /path/to/your/projects');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
34
55
|
// Step 1: Find projects
|
|
35
56
|
console.log('š Step 1: Scanning for projects\n');
|
|
36
57
|
console.log(` Looking in: ${projectsDir}`);
|
|
@@ -164,6 +185,7 @@ export async function runSetupWizard(options = {}) {
|
|
|
164
185
|
}
|
|
165
186
|
/**
|
|
166
187
|
* Guess the user's projects directory
|
|
188
|
+
* Returns null if we can't find a reasonable guess (better than scanning home!)
|
|
167
189
|
*/
|
|
168
190
|
function guessProjectsDir() {
|
|
169
191
|
const home = homedir();
|
|
@@ -178,14 +200,17 @@ function guessProjectsDir() {
|
|
|
178
200
|
join(home, 'src'),
|
|
179
201
|
join(home, 'repos'),
|
|
180
202
|
join(home, 'git'),
|
|
203
|
+
join(home, 'workspace'),
|
|
204
|
+
join(home, 'Workspace'),
|
|
181
205
|
];
|
|
182
206
|
for (const dir of candidates) {
|
|
183
207
|
if (existsSync(dir)) {
|
|
184
208
|
return dir;
|
|
185
209
|
}
|
|
186
210
|
}
|
|
187
|
-
//
|
|
188
|
-
|
|
211
|
+
// Don't fall back to home - that scans way too much and causes issues
|
|
212
|
+
// Return null and let the caller handle it
|
|
213
|
+
return null;
|
|
189
214
|
}
|
|
190
215
|
/**
|
|
191
216
|
* Save memory config to ~/.claude-memory/config.json
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core types for Claude Memory
|
|
3
3
|
*/
|
|
4
|
+
/** Current schema version - increment when making breaking changes */
|
|
5
|
+
export declare const SCHEMA_VERSION = 1;
|
|
4
6
|
export interface ProjectInfo {
|
|
5
7
|
/** Absolute path to project root */
|
|
6
8
|
path: string;
|
|
@@ -43,14 +45,18 @@ export interface ProjectInsight {
|
|
|
43
45
|
discoveredAt: string;
|
|
44
46
|
}
|
|
45
47
|
export interface GlobalContext {
|
|
48
|
+
/** Schema version for migration support */
|
|
49
|
+
schemaVersion: number;
|
|
46
50
|
/** When the global context was last updated */
|
|
47
51
|
lastUpdated: string;
|
|
48
52
|
/** All scanned projects */
|
|
49
53
|
projects: ProjectInfo[];
|
|
50
|
-
/** Cross-project insights */
|
|
54
|
+
/** Cross-project insights (preserved across rescans) */
|
|
51
55
|
insights: GlobalInsight[];
|
|
52
56
|
/** User preferences/patterns observed */
|
|
53
57
|
userPatterns: UserPattern[];
|
|
58
|
+
/** Projects to exclude from scanning (by path) */
|
|
59
|
+
excludedProjects?: string[];
|
|
54
60
|
}
|
|
55
61
|
export interface GlobalInsight {
|
|
56
62
|
/** Insight content */
|
package/dist/types/index.js
CHANGED
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core types for Claude Memory
|
|
3
3
|
*/
|
|
4
|
+
/** Current schema version - increment when making breaking changes */
|
|
5
|
+
export const SCHEMA_VERSION = 1;
|
|
6
|
+
/**
|
|
7
|
+
* Get a sensible default projects directory
|
|
8
|
+
* Checks common locations, doesn't fall back to home
|
|
9
|
+
*/
|
|
10
|
+
function getDefaultProjectsDir() {
|
|
11
|
+
const home = process.env.HOME || '';
|
|
12
|
+
if (!home)
|
|
13
|
+
return './projects';
|
|
14
|
+
const candidates = [
|
|
15
|
+
`${home}/Projects`,
|
|
16
|
+
`${home}/Project`,
|
|
17
|
+
`${home}/projects`,
|
|
18
|
+
`${home}/Code`,
|
|
19
|
+
`${home}/code`,
|
|
20
|
+
`${home}/dev`,
|
|
21
|
+
];
|
|
22
|
+
// Import fs dynamically to avoid issues
|
|
23
|
+
try {
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
for (const dir of candidates) {
|
|
26
|
+
if (fs.existsSync(dir)) {
|
|
27
|
+
return dir;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// fs not available, use first candidate
|
|
33
|
+
}
|
|
34
|
+
// Default to ~/Projects (most common convention)
|
|
35
|
+
return `${home}/Projects`;
|
|
36
|
+
}
|
|
4
37
|
export const DEFAULT_SCAN_OPTIONS = {
|
|
5
|
-
rootDir:
|
|
38
|
+
rootDir: getDefaultProjectsDir(),
|
|
6
39
|
maxDepth: 2,
|
|
7
|
-
ignore: ['node_modules', '.git', 'dist', 'build', '__pycache__', 'target'],
|
|
40
|
+
ignore: ['node_modules', '.git', 'dist', 'build', '__pycache__', 'target', '.venv', 'venv', 'coverage', '.next'],
|
|
8
41
|
generateClaudeMd: false,
|
|
9
42
|
overwriteClaudeMd: false,
|
|
10
43
|
};
|