@contextmirror/claude-memory 0.1.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.
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Project Scanner - Discovers and analyzes projects
3
+ */
4
+ import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
5
+ import { join, basename } from 'path';
6
+ import { simpleGit } from 'simple-git';
7
+ import { DEFAULT_SCAN_OPTIONS } from '../types/index.js';
8
+ /**
9
+ * Scan a directory for projects and extract information
10
+ */
11
+ export async function scanProjects(options = {}) {
12
+ const opts = { ...DEFAULT_SCAN_OPTIONS, ...options };
13
+ const projects = [];
14
+ console.log(`šŸ” Scanning ${opts.rootDir} for projects...`);
15
+ const candidates = findProjectRoots(opts.rootDir, opts.maxDepth, opts.ignore);
16
+ for (const projectPath of candidates) {
17
+ try {
18
+ const info = await analyzeProject(projectPath);
19
+ if (info) {
20
+ projects.push(info);
21
+ console.log(` āœ“ Found: ${info.name} (${info.language})`);
22
+ }
23
+ }
24
+ catch (err) {
25
+ console.warn(` ⚠ Failed to analyze: ${projectPath}`);
26
+ }
27
+ }
28
+ console.log(`\nšŸ“Š Found ${projects.length} projects`);
29
+ return projects;
30
+ }
31
+ /**
32
+ * Find directories that look like project roots
33
+ */
34
+ function findProjectRoots(rootDir, maxDepth, ignore) {
35
+ const roots = [];
36
+ function walk(dir, depth) {
37
+ if (depth > maxDepth)
38
+ return;
39
+ try {
40
+ const entries = readdirSync(dir);
41
+ // Check if this is a project root
42
+ const isProject = entries.includes('package.json') ||
43
+ entries.includes('Cargo.toml') ||
44
+ entries.includes('pyproject.toml') ||
45
+ entries.includes('go.mod') ||
46
+ entries.includes('.git');
47
+ if (isProject) {
48
+ roots.push(dir);
49
+ return; // Don't recurse into project subdirectories
50
+ }
51
+ // Recurse into subdirectories
52
+ for (const entry of entries) {
53
+ if (ignore.includes(entry))
54
+ continue;
55
+ const fullPath = join(dir, entry);
56
+ try {
57
+ if (statSync(fullPath).isDirectory()) {
58
+ walk(fullPath, depth + 1);
59
+ }
60
+ }
61
+ catch {
62
+ // Skip inaccessible directories
63
+ }
64
+ }
65
+ }
66
+ catch {
67
+ // Skip inaccessible directories
68
+ }
69
+ }
70
+ walk(rootDir, 0);
71
+ return roots;
72
+ }
73
+ /**
74
+ * Analyze a single project
75
+ */
76
+ async function analyzeProject(projectPath) {
77
+ const name = detectProjectName(projectPath);
78
+ const description = detectDescription(projectPath);
79
+ const { language, techStack } = detectTechStack(projectPath);
80
+ // Git info
81
+ let lastActivity = new Date().toISOString();
82
+ let currentBranch = 'unknown';
83
+ let isDirty = false;
84
+ if (existsSync(join(projectPath, '.git'))) {
85
+ try {
86
+ const git = simpleGit(projectPath);
87
+ const log = await git.log({ maxCount: 1 });
88
+ if (log.latest) {
89
+ lastActivity = log.latest.date;
90
+ }
91
+ const status = await git.status();
92
+ currentBranch = status.current || 'unknown';
93
+ isDirty = !status.isClean();
94
+ }
95
+ catch {
96
+ // Git operations failed, use defaults
97
+ }
98
+ }
99
+ // Extract insights from CLAUDE.md if it exists
100
+ const insights = extractInsightsFromClaudeMd(projectPath);
101
+ return {
102
+ path: projectPath,
103
+ name,
104
+ description,
105
+ techStack,
106
+ language,
107
+ lastActivity,
108
+ currentBranch,
109
+ isDirty,
110
+ hasFiles: {
111
+ claudeMd: existsSync(join(projectPath, 'CLAUDE.md')),
112
+ readme: existsSync(join(projectPath, 'README.md')),
113
+ packageJson: existsSync(join(projectPath, 'package.json')),
114
+ gitignore: existsSync(join(projectPath, '.gitignore')),
115
+ },
116
+ insights,
117
+ lastScanned: new Date().toISOString(),
118
+ };
119
+ }
120
+ /**
121
+ * Detect project name from various sources
122
+ */
123
+ function detectProjectName(projectPath) {
124
+ // Try package.json
125
+ const pkgPath = join(projectPath, 'package.json');
126
+ if (existsSync(pkgPath)) {
127
+ try {
128
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
129
+ if (pkg.name)
130
+ return pkg.name;
131
+ }
132
+ catch {
133
+ // Invalid JSON
134
+ }
135
+ }
136
+ // Try Cargo.toml
137
+ const cargoPath = join(projectPath, 'Cargo.toml');
138
+ if (existsSync(cargoPath)) {
139
+ try {
140
+ const cargo = readFileSync(cargoPath, 'utf-8');
141
+ const match = cargo.match(/name\s*=\s*"([^"]+)"/);
142
+ if (match)
143
+ return match[1];
144
+ }
145
+ catch {
146
+ // Read failed
147
+ }
148
+ }
149
+ // Fall back to folder name
150
+ return basename(projectPath);
151
+ }
152
+ /**
153
+ * Detect project description
154
+ */
155
+ function detectDescription(projectPath) {
156
+ // Try package.json
157
+ const pkgPath = join(projectPath, 'package.json');
158
+ if (existsSync(pkgPath)) {
159
+ try {
160
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
161
+ if (pkg.description)
162
+ return pkg.description;
163
+ }
164
+ catch {
165
+ // Invalid JSON
166
+ }
167
+ }
168
+ // Try README first line
169
+ const readmePath = join(projectPath, 'README.md');
170
+ if (existsSync(readmePath)) {
171
+ try {
172
+ const readme = readFileSync(readmePath, 'utf-8');
173
+ const lines = readme.split('\n').filter((l) => l.trim() && !l.startsWith('#'));
174
+ if (lines[0])
175
+ return lines[0].slice(0, 200);
176
+ }
177
+ catch {
178
+ // Read failed
179
+ }
180
+ }
181
+ // Try CLAUDE.md first paragraph
182
+ const claudePath = join(projectPath, 'CLAUDE.md');
183
+ if (existsSync(claudePath)) {
184
+ try {
185
+ const claude = readFileSync(claudePath, 'utf-8');
186
+ const match = claude.match(/##\s*Project Overview\s*\n+([^\n]+)/i);
187
+ if (match)
188
+ return match[1].slice(0, 200);
189
+ }
190
+ catch {
191
+ // Read failed
192
+ }
193
+ }
194
+ return 'No description available';
195
+ }
196
+ /**
197
+ * Detect tech stack and primary language
198
+ */
199
+ function detectTechStack(projectPath) {
200
+ const techStack = [];
201
+ let language = 'other';
202
+ // Check package.json
203
+ const pkgPath = join(projectPath, 'package.json');
204
+ if (existsSync(pkgPath)) {
205
+ try {
206
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
207
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
208
+ // Detect TypeScript
209
+ if (deps.typescript || existsSync(join(projectPath, 'tsconfig.json'))) {
210
+ language = 'typescript';
211
+ techStack.push('TypeScript');
212
+ }
213
+ else {
214
+ language = 'javascript';
215
+ techStack.push('JavaScript');
216
+ }
217
+ // Detect frameworks
218
+ if (deps.react)
219
+ techStack.push('React');
220
+ if (deps.vue)
221
+ techStack.push('Vue');
222
+ if (deps.next)
223
+ techStack.push('Next.js');
224
+ if (deps.express)
225
+ techStack.push('Express');
226
+ if (deps.fastify)
227
+ techStack.push('Fastify');
228
+ if (deps['@anthropic-ai/sdk'])
229
+ techStack.push('Claude SDK');
230
+ if (deps['@modelcontextprotocol/sdk'])
231
+ techStack.push('MCP');
232
+ }
233
+ catch {
234
+ // Invalid JSON
235
+ }
236
+ }
237
+ // Check Cargo.toml
238
+ if (existsSync(join(projectPath, 'Cargo.toml'))) {
239
+ language = 'rust';
240
+ techStack.push('Rust');
241
+ }
242
+ // Check pyproject.toml or requirements.txt
243
+ if (existsSync(join(projectPath, 'pyproject.toml')) ||
244
+ existsSync(join(projectPath, 'requirements.txt'))) {
245
+ language = 'python';
246
+ techStack.push('Python');
247
+ }
248
+ // Check go.mod
249
+ if (existsSync(join(projectPath, 'go.mod'))) {
250
+ language = 'go';
251
+ techStack.push('Go');
252
+ }
253
+ return { language, techStack };
254
+ }
255
+ /**
256
+ * Extract insights from CLAUDE.md file
257
+ * Looks for patterns like:
258
+ * - ## Session Context / ## Current State sections
259
+ * - Key architectural decisions
260
+ * - TODOs and next steps
261
+ */
262
+ function extractInsightsFromClaudeMd(projectPath) {
263
+ const claudePath = join(projectPath, 'CLAUDE.md');
264
+ if (!existsSync(claudePath)) {
265
+ return [];
266
+ }
267
+ const insights = [];
268
+ try {
269
+ const content = readFileSync(claudePath, 'utf-8');
270
+ // Extract "Session Context" or "Current State" sections
271
+ const sessionMatch = content.match(/##\s*(Session Context|Current State|Where we left off)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
272
+ if (sessionMatch) {
273
+ insights.push({
274
+ type: 'notable',
275
+ title: 'Session Context',
276
+ description: sessionMatch[2].trim().slice(0, 500),
277
+ source: 'CLAUDE.md',
278
+ discoveredAt: new Date().toISOString(),
279
+ });
280
+ }
281
+ // Extract "What's Been Built" or similar sections
282
+ const builtMatch = content.match(/##\s*(What's Been Built|Features|Implemented)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
283
+ if (builtMatch) {
284
+ insights.push({
285
+ type: 'architecture',
286
+ title: 'What\'s Built',
287
+ description: builtMatch[2].trim().slice(0, 500),
288
+ source: 'CLAUDE.md',
289
+ discoveredAt: new Date().toISOString(),
290
+ });
291
+ }
292
+ // Extract "Next Steps" or "TODO" sections
293
+ const todoMatch = content.match(/##\s*(Next Steps|TODO|Roadmap|Immediate)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
294
+ if (todoMatch) {
295
+ insights.push({
296
+ type: 'todo',
297
+ title: 'Next Steps',
298
+ description: todoMatch[2].trim().slice(0, 500),
299
+ source: 'CLAUDE.md',
300
+ discoveredAt: new Date().toISOString(),
301
+ });
302
+ }
303
+ // Extract architecture/tech decisions
304
+ const archMatch = content.match(/##\s*(Architecture|Tech Stack|Design)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
305
+ if (archMatch) {
306
+ insights.push({
307
+ type: 'architecture',
308
+ title: 'Architecture',
309
+ description: archMatch[2].trim().slice(0, 500),
310
+ source: 'CLAUDE.md',
311
+ discoveredAt: new Date().toISOString(),
312
+ });
313
+ }
314
+ }
315
+ catch {
316
+ // Failed to read or parse
317
+ }
318
+ return insights;
319
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Core types for Claude Memory
3
+ */
4
+ export interface ProjectInfo {
5
+ /** Absolute path to project root */
6
+ path: string;
7
+ /** Project name (from package.json, Cargo.toml, or folder name) */
8
+ name: string;
9
+ /** Short description (from package.json, README first line, etc.) */
10
+ description: string;
11
+ /** Detected tech stack */
12
+ techStack: string[];
13
+ /** Primary language */
14
+ language: 'typescript' | 'javascript' | 'python' | 'rust' | 'go' | 'other';
15
+ /** Last git commit date */
16
+ lastActivity: string;
17
+ /** Current git branch */
18
+ currentBranch: string;
19
+ /** Has uncommitted changes */
20
+ isDirty: boolean;
21
+ /** Key files that exist */
22
+ hasFiles: {
23
+ claudeMd: boolean;
24
+ readme: boolean;
25
+ packageJson: boolean;
26
+ gitignore: boolean;
27
+ };
28
+ /** Extracted insights (patterns, conventions, notable things) */
29
+ insights: ProjectInsight[];
30
+ /** When this project was last scanned */
31
+ lastScanned: string;
32
+ }
33
+ export interface ProjectInsight {
34
+ /** What kind of insight */
35
+ type: 'pattern' | 'convention' | 'architecture' | 'notable' | 'todo';
36
+ /** Short title */
37
+ title: string;
38
+ /** Description */
39
+ description: string;
40
+ /** Source file (if applicable) */
41
+ source?: string;
42
+ /** When discovered */
43
+ discoveredAt: string;
44
+ }
45
+ export interface GlobalContext {
46
+ /** When the global context was last updated */
47
+ lastUpdated: string;
48
+ /** All scanned projects */
49
+ projects: ProjectInfo[];
50
+ /** Cross-project insights */
51
+ insights: GlobalInsight[];
52
+ /** User preferences/patterns observed */
53
+ userPatterns: UserPattern[];
54
+ }
55
+ export interface GlobalInsight {
56
+ /** Insight content */
57
+ content: string;
58
+ /** Which projects this relates to */
59
+ relatedProjects: string[];
60
+ /** When discovered */
61
+ discoveredAt: string;
62
+ }
63
+ export interface UserPattern {
64
+ /** Pattern name */
65
+ name: string;
66
+ /** Description */
67
+ description: string;
68
+ /** Examples from projects */
69
+ examples: Array<{
70
+ project: string;
71
+ file: string;
72
+ snippet?: string;
73
+ }>;
74
+ }
75
+ export interface ScanOptions {
76
+ /** Root directory to scan (default: ~/Projects) */
77
+ rootDir: string;
78
+ /** Max depth to search for projects */
79
+ maxDepth: number;
80
+ /** Patterns to ignore */
81
+ ignore: string[];
82
+ /** Whether to generate CLAUDE.md files */
83
+ generateClaudeMd: boolean;
84
+ /** Whether to overwrite existing CLAUDE.md */
85
+ overwriteClaudeMd: boolean;
86
+ }
87
+ export declare const DEFAULT_SCAN_OPTIONS: ScanOptions;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Core types for Claude Memory
3
+ */
4
+ export const DEFAULT_SCAN_OPTIONS = {
5
+ rootDir: process.env.HOME ? `${process.env.HOME}/Project` : './projects',
6
+ maxDepth: 2,
7
+ ignore: ['node_modules', '.git', 'dist', 'build', '__pycache__', 'target'],
8
+ generateClaudeMd: false,
9
+ overwriteClaudeMd: false,
10
+ };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@contextmirror/claude-memory",
3
+ "version": "0.1.0",
4
+ "description": "Cross-project memory for Claude Code - know about all your projects",
5
+ "type": "module",
6
+ "main": "dist/cli.js",
7
+ "bin": {
8
+ "claude-memory": "dist/cli.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "scan": "node dist/cli.js scan",
14
+ "mcp": "node dist/mcp/server.js",
15
+ "test": "vitest"
16
+ },
17
+ "keywords": [
18
+ "claude",
19
+ "mcp",
20
+ "memory",
21
+ "context",
22
+ "ai"
23
+ ],
24
+ "author": "Nathan",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.0.0",
28
+ "commander": "^12.1.0",
29
+ "glob": "^11.0.0",
30
+ "simple-git": "^3.27.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^22.10.2",
34
+ "typescript": "^5.7.2",
35
+ "vitest": "^2.1.8"
36
+ }
37
+ }