claude-recall 0.7.4 → 0.7.5

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/README.md CHANGED
@@ -94,7 +94,22 @@ npm uninstall -g claude-recall
94
94
 
95
95
  Claude Recall works on **Windows, Linux, and macOS**. Native binaries (SQLite) compile automatically for your platform during `npm install`.
96
96
 
97
- **WSL Users:** Use local installation only. Global installation on Windows + WSL usage causes binary conflicts ("invalid ELF header" errors).
97
+ **WSL Users - Special Case:**
98
+
99
+ If you use **both Windows and WSL** for the same project (e.g., Electron app runs on Windows, but Claude Code runs in WSL):
100
+
101
+ **Problem**: Installing locally creates Windows binaries, but WSL needs Linux binaries → "invalid ELF header" errors.
102
+
103
+ **Solution**: Install globally in WSL only:
104
+ ```bash
105
+ # From WSL:
106
+ npm install -g claude-recall
107
+
108
+ # Verify:
109
+ claude-recall --version
110
+ ```
111
+
112
+ **Important**: Global installation does NOT affect project-specific memory scoping! See [How Project Scoping Works](#how-project-scoping-works-installation-location) below.
98
113
 
99
114
  **Everyone else:** Both local and global work, but local is still recommended for the benefits above.
100
115
 
@@ -526,6 +541,98 @@ All commands support:
526
541
  - `--config <path>` - Use custom config file
527
542
  - `-h, --help` - Show help for any command
528
543
 
544
+ ## Project Management (v0.7.5+)
545
+
546
+ Claude Recall maintains a **project registry** to track all projects using it, enabling better organization and visibility across multiple projects.
547
+
548
+ ### Auto-Registration
549
+
550
+ Projects are automatically registered when:
551
+ - The MCP server starts (every time Claude Code connects)
552
+ - You run `npm install claude-recall` locally
553
+ - You manually run `npx claude-recall project register`
554
+
555
+ The registry stores:
556
+ - Project path
557
+ - Claude Recall version
558
+ - Registration date
559
+ - Last activity timestamp
560
+
561
+ ### Project Commands
562
+
563
+ **List all registered projects:**
564
+ ```bash
565
+ npx claude-recall project list # Human-friendly output
566
+ npx claude-recall project list --json # JSON format
567
+ ```
568
+
569
+ Shows:
570
+ - Project name and path
571
+ - Claude Recall version
572
+ - Last activity
573
+ - MCP server status (active/inactive)
574
+
575
+ **Show project details:**
576
+ ```bash
577
+ npx claude-recall project show # Current project
578
+ npx claude-recall project show my-project # Specific project
579
+ ```
580
+
581
+ Displays:
582
+ - Registry information (path, version, timestamps)
583
+ - MCP server status (running/stopped, PID)
584
+
585
+ **Register a project:**
586
+ ```bash
587
+ npx claude-recall project register # Current directory
588
+ npx claude-recall project register --path /path/to/project
589
+ ```
590
+
591
+ **Unregister a project:**
592
+ ```bash
593
+ npx claude-recall project unregister # Current project
594
+ npx claude-recall project unregister my-project
595
+ ```
596
+
597
+ **Note**: Unregistering does NOT remove memories or MCP configuration - only the registry entry.
598
+
599
+ **Clean up stale entries:**
600
+ ```bash
601
+ npx claude-recall project clean # Remove projects not seen in 30 days
602
+ npx claude-recall project clean --days 60 # Custom threshold
603
+ npx claude-recall project clean --dry-run # Preview without changes
604
+ ```
605
+
606
+ ### Enhanced MCP Commands
607
+
608
+ MCP commands now show registry information:
609
+
610
+ ```bash
611
+ npx claude-recall mcp status # Shows registry info + MCP status
612
+ npx claude-recall mcp ps # Lists all servers with version info
613
+ ```
614
+
615
+ ### Registry Storage
616
+
617
+ Registry stored at: `~/.claude-recall/projects.json`
618
+
619
+ Format:
620
+ ```json
621
+ {
622
+ "version": 1,
623
+ "projects": {
624
+ "my-project": {
625
+ "path": "/full/path/to/my-project",
626
+ "registeredAt": "2025-11-09T...",
627
+ "version": "0.7.5",
628
+ "lastSeen": "2025-11-09T..."
629
+ }
630
+ }
631
+ }
632
+ ```
633
+
634
+ **Note**: The registry is separate from your memory database. Cleaning the registry does NOT affect your stored memories.
635
+
529
636
  ## Project Scoping (v0.7.2+)
530
637
 
531
638
  Claude Recall now supports **project-specific memory isolation** while keeping universal preferences available everywhere.
@@ -610,6 +717,71 @@ npx claude-recall stats --global
610
717
  - Works like v0.7.1 and earlier
611
718
  - Available everywhere unless you explicitly scope them
612
719
 
720
+ ### How Project Scoping Works (Installation Location)
721
+
722
+ **Important**: Where you install claude-recall (global vs local) does NOT affect project-specific memory scoping!
723
+
724
+ **What Determines Project Scope:**
725
+
726
+ 1. āœ… **Claude Code's working directory** - Where you run Claude Code
727
+ 2. āœ… **Database scoping logic** - Filters memories by project_id
728
+ 3. āŒ **Installation location** - Doesn't matter for scoping
729
+
730
+ **How It Works:**
731
+
732
+ ```
733
+ # Claude Code runs in this directory:
734
+ /home/user/projects/my-app
735
+
736
+ # Project ID detected from directory name:
737
+ project_id = "my-app"
738
+
739
+ # Memories stored/searched with that project_id:
740
+ - "For this project, use PostgreSQL" → stored with project_id="my-app"
741
+ - "Remember everywhere: I prefer TypeScript" → stored with scope="universal"
742
+
743
+ # Same behavior whether claude-recall is:
744
+ # - Installed globally: /usr/local/lib/node_modules/claude-recall
745
+ # - Installed locally: ./node_modules/claude-recall
746
+ ```
747
+
748
+ **Database Location (Always Global):**
749
+
750
+ Whether you install globally or locally, the database is **always** at:
751
+ ```
752
+ ~/.claude-recall/claude-recall.db
753
+ ```
754
+
755
+ This single database contains:
756
+ - Project-specific memories for each project (isolated by project_id)
757
+ - Universal memories (available in all projects)
758
+ - Unscoped memories (backward compatible)
759
+
760
+ **Example: Two Projects, One Installation**
761
+
762
+ ```bash
763
+ # Project A:
764
+ cd ~/projects/project-a
765
+ # Claude Code detects: project_id = "project-a"
766
+ # Memories: isolated to project-a + universal + unscoped
767
+
768
+ # Project B:
769
+ cd ~/projects/project-b
770
+ # Claude Code detects: project_id = "project-b"
771
+ # Memories: isolated to project-b + universal + unscoped
772
+
773
+ # Same global installation, proper isolation!
774
+ ```
775
+
776
+ **WSL Users:**
777
+
778
+ This is why **global installation in WSL** works perfectly for project scoping:
779
+ - Code location: `/home/user/.nvm/.../bin/claude-recall` (global)
780
+ - Project detection: Based on Claude Code's `cwd`, NOT installation path
781
+ - Memory isolation: Each project gets its own memories + universal preferences
782
+
783
+ Global installation actually **helps** WSL users by avoiding binary conflicts while maintaining perfect project isolation!
784
+
613
785
  ## Memory Management
614
786
 
615
787
  Claude Recall automatically manages memory to prevent unlimited database growth, with user notifications:
@@ -48,6 +48,7 @@ const live_test_1 = require("./commands/live-test");
48
48
  const queue_integration_1 = require("../services/queue-integration");
49
49
  const memory_evolution_1 = require("../services/memory-evolution");
50
50
  const mcp_commands_1 = require("./commands/mcp-commands");
51
+ const project_commands_1 = require("./commands/project-commands");
51
52
  const program = new commander_1.Command();
52
53
  class ClaudeRecallCLI {
53
54
  constructor(options) {
@@ -612,6 +613,8 @@ async function main() {
612
613
  });
613
614
  // Register MCP process management commands
614
615
  mcp_commands_1.MCPCommands.register(mcpCmd);
616
+ // Project management commands
617
+ project_commands_1.ProjectCommands.register(program);
615
618
  // Migration commands
616
619
  migrate_1.MigrateCommand.register(program);
617
620
  // Register live test command
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.MCPCommands = void 0;
7
7
  const process_manager_1 = require("../../services/process-manager");
8
8
  const config_1 = require("../../services/config");
9
+ const project_registry_1 = require("../../services/project-registry");
9
10
  const chalk_1 = __importDefault(require("chalk"));
10
11
  /**
11
12
  * MCP Process Management Commands
@@ -21,6 +22,7 @@ class MCPCommands {
21
22
  constructor() {
22
23
  this.processManager = process_manager_1.ProcessManager.getInstance();
23
24
  this.config = config_1.ConfigService.getInstance();
25
+ this.projectRegistry = project_registry_1.ProjectRegistry.getInstance();
24
26
  }
25
27
  static register(mcpCmd) {
26
28
  const commands = new MCPCommands();
@@ -72,6 +74,7 @@ class MCPCommands {
72
74
  async showStatus() {
73
75
  const projectId = this.config.getProjectId();
74
76
  const status = this.processManager.getServerStatus(projectId);
77
+ const registryEntry = this.projectRegistry.get(projectId);
75
78
  console.log(chalk_1.default.cyan('\nšŸ“Š MCP Server Status\n'));
76
79
  console.log(`Project: ${chalk_1.default.yellow(projectId)}`);
77
80
  console.log(`Status: ${status.isRunning ? chalk_1.default.green('āœ“ Running') : chalk_1.default.gray('āœ— Stopped')}`);
@@ -82,6 +85,20 @@ class MCPCommands {
82
85
  }
83
86
  }
84
87
  console.log(`PID File: ${chalk_1.default.gray(status.pidFile)}`);
88
+ // Show registry information if available
89
+ if (registryEntry) {
90
+ console.log();
91
+ console.log(chalk_1.default.bold('Registry Info:'));
92
+ console.log(` Version: ${chalk_1.default.gray(registryEntry.version)}`);
93
+ console.log(` Path: ${chalk_1.default.gray(registryEntry.path)}`);
94
+ console.log(` Registered: ${chalk_1.default.gray(new Date(registryEntry.registeredAt).toLocaleString())}`);
95
+ console.log(` Last Seen: ${chalk_1.default.gray(new Date(registryEntry.lastSeen).toLocaleString())}`);
96
+ }
97
+ else {
98
+ console.log();
99
+ console.log(chalk_1.default.yellow('⚠ Project not registered in registry'));
100
+ console.log(chalk_1.default.gray(' Run `npx claude-recall project register` to register'));
101
+ }
85
102
  console.log();
86
103
  }
87
104
  /**
@@ -89,9 +106,10 @@ class MCPCommands {
89
106
  */
90
107
  async listServers() {
91
108
  const servers = this.processManager.getAllRunningServers();
92
- console.log(chalk_1.default.cyan('\nšŸ“‹ Running MCP Servers\n'));
93
- if (servers.length === 0) {
94
- console.log(chalk_1.default.gray('No MCP servers found.'));
109
+ const registeredProjects = this.projectRegistry.list();
110
+ console.log(chalk_1.default.cyan('\nšŸ“‹ MCP Servers & Registered Projects\n'));
111
+ if (servers.length === 0 && registeredProjects.length === 0) {
112
+ console.log(chalk_1.default.gray('No MCP servers or registered projects found.'));
95
113
  console.log();
96
114
  return;
97
115
  }
@@ -100,7 +118,9 @@ class MCPCommands {
100
118
  if (runningServers.length > 0) {
101
119
  console.log(chalk_1.default.green(`āœ“ Running (${runningServers.length}):`));
102
120
  for (const server of runningServers) {
103
- console.log(` ${chalk_1.default.yellow(server.projectId.padEnd(40))} PID: ${chalk_1.default.cyan(server.pid)}`);
121
+ const registryEntry = this.projectRegistry.get(server.projectId);
122
+ const versionInfo = registryEntry ? ` [v${registryEntry.version}]` : '';
123
+ console.log(` ${chalk_1.default.yellow(server.projectId.padEnd(40))} PID: ${chalk_1.default.cyan(server.pid)}${chalk_1.default.gray(versionInfo)}`);
104
124
  }
105
125
  console.log();
106
126
  }
@@ -113,6 +133,18 @@ class MCPCommands {
113
133
  console.log(chalk_1.default.yellow(`šŸ’” Run 'npx claude-recall mcp cleanup' to remove stale PID files`));
114
134
  console.log();
115
135
  }
136
+ // Show registered projects that don't have running servers
137
+ const runningProjectIds = new Set(runningServers.map(s => s.projectId));
138
+ const inactiveProjects = registeredProjects.filter(({ projectId }) => !runningProjectIds.has(projectId));
139
+ if (inactiveProjects.length > 0) {
140
+ console.log(chalk_1.default.gray(`šŸ“‚ Registered but Inactive (${inactiveProjects.length}):`));
141
+ for (const { projectId, entry } of inactiveProjects) {
142
+ console.log(` ${chalk_1.default.gray(projectId.padEnd(40))} v${entry.version}`);
143
+ }
144
+ console.log();
145
+ console.log(chalk_1.default.gray(`šŸ’” Run 'npx claude-recall project list' for detailed registry info`));
146
+ console.log();
147
+ }
116
148
  }
117
149
  /**
118
150
  * Stop MCP server
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.ProjectCommands = void 0;
40
+ const project_registry_1 = require("../../services/project-registry");
41
+ const config_1 = require("../../services/config");
42
+ const process_manager_1 = require("../../services/process-manager");
43
+ const chalk_1 = __importDefault(require("chalk"));
44
+ const path = __importStar(require("path"));
45
+ const fs = __importStar(require("fs"));
46
+ /**
47
+ * Project Management Commands
48
+ *
49
+ * Provides CLI commands for managing project registration:
50
+ * - register: Register current or specific project
51
+ * - unregister: Remove project from registry
52
+ * - list: Show all registered projects
53
+ * - show: Show details for specific project
54
+ * - clean: Remove stale registry entries
55
+ */
56
+ class ProjectCommands {
57
+ constructor() {
58
+ this.projectRegistry = project_registry_1.ProjectRegistry.getInstance();
59
+ this.config = config_1.ConfigService.getInstance();
60
+ this.processManager = process_manager_1.ProcessManager.getInstance();
61
+ }
62
+ static register(program) {
63
+ const commands = new ProjectCommands();
64
+ const projectCmd = program
65
+ .command('project')
66
+ .description('Project registration and management');
67
+ // project register
68
+ projectCmd
69
+ .command('register')
70
+ .description('Register current or specified project')
71
+ .option('--path <path>', 'Project path (default: current directory)')
72
+ .action(async (options) => {
73
+ await commands.registerProject(options);
74
+ });
75
+ // project unregister
76
+ projectCmd
77
+ .command('unregister [project-id]')
78
+ .description('Unregister a project')
79
+ .action(async (projectId) => {
80
+ await commands.unregisterProject(projectId);
81
+ });
82
+ // project list
83
+ projectCmd
84
+ .command('list')
85
+ .description('List all registered projects')
86
+ .option('--json', 'Output as JSON')
87
+ .action(async (options) => {
88
+ await commands.listProjects(options);
89
+ });
90
+ // project show
91
+ projectCmd
92
+ .command('show [project-id]')
93
+ .description('Show details for a project (default: current)')
94
+ .action(async (projectId) => {
95
+ await commands.showProject(projectId);
96
+ });
97
+ // project clean
98
+ projectCmd
99
+ .command('clean')
100
+ .description('Remove stale registry entries')
101
+ .option('--dry-run', 'Show what would be removed without removing')
102
+ .option('--days <number>', 'Days without activity to consider stale', '30')
103
+ .action(async (options) => {
104
+ await commands.cleanRegistry(options);
105
+ });
106
+ }
107
+ /**
108
+ * Register a project
109
+ */
110
+ async registerProject(options) {
111
+ console.log(chalk_1.default.cyan('\nšŸ“ Registering Project\n'));
112
+ try {
113
+ // Determine project path
114
+ const projectPath = options.path
115
+ ? path.resolve(options.path)
116
+ : process.cwd();
117
+ // Validate path exists
118
+ if (!fs.existsSync(projectPath)) {
119
+ console.log(chalk_1.default.red(`āœ— Path does not exist: ${projectPath}`));
120
+ console.log();
121
+ return;
122
+ }
123
+ // Get project ID
124
+ const projectId = path.basename(projectPath);
125
+ // Get version
126
+ const version = this.getVersion();
127
+ // Register
128
+ this.projectRegistry.register(projectId, projectPath, version);
129
+ console.log(`Project: ${chalk_1.default.yellow(projectId)}`);
130
+ console.log(`Path: ${chalk_1.default.gray(projectPath)}`);
131
+ console.log(`Version: ${chalk_1.default.gray(version)}`);
132
+ console.log();
133
+ console.log(chalk_1.default.green('āœ“ Project registered successfully'));
134
+ console.log();
135
+ }
136
+ catch (error) {
137
+ console.log(chalk_1.default.red(`āœ— Failed to register project: ${error}`));
138
+ console.log();
139
+ }
140
+ }
141
+ /**
142
+ * Unregister a project
143
+ */
144
+ async unregisterProject(projectId) {
145
+ console.log(chalk_1.default.cyan('\nšŸ—‘ļø Unregistering Project\n'));
146
+ try {
147
+ // If no projectId provided, use current directory
148
+ const targetProjectId = projectId || this.config.getProjectId();
149
+ // Get project info before removing
150
+ const entry = this.projectRegistry.get(targetProjectId);
151
+ if (!entry) {
152
+ console.log(chalk_1.default.yellow(`⚠ Project not found in registry: ${targetProjectId}`));
153
+ console.log();
154
+ return;
155
+ }
156
+ console.log(`Project: ${chalk_1.default.yellow(targetProjectId)}`);
157
+ console.log(`Path: ${chalk_1.default.gray(entry.path)}`);
158
+ console.log();
159
+ // Unregister
160
+ const success = this.projectRegistry.unregister(targetProjectId);
161
+ if (success) {
162
+ console.log(chalk_1.default.green('āœ“ Project unregistered successfully'));
163
+ console.log();
164
+ console.log(chalk_1.default.gray('Note: This does not remove the MCP server configuration from ~/.claude.json'));
165
+ console.log(chalk_1.default.gray(' Use `claude mcp` commands to manage MCP servers.'));
166
+ console.log();
167
+ }
168
+ else {
169
+ console.log(chalk_1.default.red('āœ— Failed to unregister project'));
170
+ console.log();
171
+ }
172
+ }
173
+ catch (error) {
174
+ console.log(chalk_1.default.red(`āœ— Error: ${error}`));
175
+ console.log();
176
+ }
177
+ }
178
+ /**
179
+ * List all registered projects
180
+ */
181
+ async listProjects(options) {
182
+ const projects = this.projectRegistry.list();
183
+ if (options.json) {
184
+ console.log(JSON.stringify(projects, null, 2));
185
+ return;
186
+ }
187
+ console.log(chalk_1.default.cyan('\nšŸ“‹ Registered Projects\n'));
188
+ if (projects.length === 0) {
189
+ console.log(chalk_1.default.gray('No projects registered.'));
190
+ console.log();
191
+ console.log(chalk_1.default.yellow('šŸ’” Run `npx claude-recall project register` to register the current project'));
192
+ console.log();
193
+ return;
194
+ }
195
+ // Get running servers for status
196
+ const runningServers = this.processManager.getAllRunningServers();
197
+ const runningProjectIds = new Set(runningServers.filter(s => s.isRunning).map(s => s.projectId));
198
+ // Sort by lastSeen (most recent first)
199
+ projects.sort((a, b) => {
200
+ return new Date(b.entry.lastSeen).getTime() - new Date(a.entry.lastSeen).getTime();
201
+ });
202
+ for (const { projectId, entry } of projects) {
203
+ const isRunning = runningProjectIds.has(projectId);
204
+ const lastSeenTime = this.formatRelativeTime(entry.lastSeen);
205
+ console.log(chalk_1.default.bold(projectId));
206
+ console.log(` Path: ${chalk_1.default.gray(entry.path)}`);
207
+ console.log(` Version: ${chalk_1.default.gray(entry.version)}`);
208
+ console.log(` Last seen: ${chalk_1.default.gray(lastSeenTime)}`);
209
+ console.log(` Status: ${isRunning ? chalk_1.default.green('āœ“ Active (MCP running)') : chalk_1.default.gray('āœ— Inactive')}`);
210
+ console.log();
211
+ }
212
+ console.log(chalk_1.default.gray(`Total: ${projects.length} project(s)`));
213
+ console.log();
214
+ }
215
+ /**
216
+ * Show details for a specific project
217
+ */
218
+ async showProject(projectId) {
219
+ const targetProjectId = projectId || this.config.getProjectId();
220
+ console.log(chalk_1.default.cyan('\nšŸ“Š Project Details\n'));
221
+ const entry = this.projectRegistry.get(targetProjectId);
222
+ if (!entry) {
223
+ console.log(chalk_1.default.yellow(`⚠ Project not found: ${targetProjectId}`));
224
+ console.log();
225
+ console.log(chalk_1.default.gray('Run `npx claude-recall project list` to see registered projects'));
226
+ console.log();
227
+ return;
228
+ }
229
+ // Check if MCP server is running
230
+ const status = this.processManager.getServerStatus(targetProjectId);
231
+ console.log(`${chalk_1.default.bold('Project:')} ${chalk_1.default.yellow(targetProjectId)}`);
232
+ console.log();
233
+ console.log(chalk_1.default.bold('Registry Information:'));
234
+ console.log(` Path: ${chalk_1.default.gray(entry.path)}`);
235
+ console.log(` Version: ${chalk_1.default.gray(entry.version)}`);
236
+ console.log(` Registered: ${chalk_1.default.gray(new Date(entry.registeredAt).toLocaleString())}`);
237
+ console.log(` Last Seen: ${chalk_1.default.gray(new Date(entry.lastSeen).toLocaleString())} (${this.formatRelativeTime(entry.lastSeen)})`);
238
+ console.log();
239
+ console.log(chalk_1.default.bold('MCP Server Status:'));
240
+ console.log(` Running: ${status.isRunning ? chalk_1.default.green('āœ“ Yes') : chalk_1.default.gray('āœ— No')}`);
241
+ if (status.pid) {
242
+ console.log(` PID: ${chalk_1.default.gray(status.pid)}`);
243
+ }
244
+ console.log(` PID File: ${chalk_1.default.gray(status.pidFile)}`);
245
+ console.log();
246
+ }
247
+ /**
248
+ * Clean stale registry entries
249
+ */
250
+ async cleanRegistry(options) {
251
+ const daysOld = parseInt(options.days || '30', 10);
252
+ console.log(chalk_1.default.cyan('\n🧹 Cleaning Registry\n'));
253
+ if (options.dryRun) {
254
+ console.log(chalk_1.default.yellow('DRY RUN MODE - No changes will be made\n'));
255
+ }
256
+ console.log(`Removing projects not seen in ${daysOld} days...\n`);
257
+ const removed = this.projectRegistry.clean(daysOld, options.dryRun || false);
258
+ if (removed === 0) {
259
+ console.log(chalk_1.default.green('āœ“ No stale entries found'));
260
+ }
261
+ else {
262
+ if (options.dryRun) {
263
+ console.log(chalk_1.default.gray(`\nWould remove ${removed} stale project(s)`));
264
+ }
265
+ else {
266
+ console.log(chalk_1.default.green(`\nāœ“ Removed ${removed} stale project(s)`));
267
+ }
268
+ }
269
+ console.log();
270
+ }
271
+ /**
272
+ * Format relative time (e.g., "2 minutes ago", "5 days ago")
273
+ */
274
+ formatRelativeTime(timestamp) {
275
+ const now = Date.now();
276
+ const then = new Date(timestamp).getTime();
277
+ const diffMs = now - then;
278
+ const seconds = Math.floor(diffMs / 1000);
279
+ const minutes = Math.floor(seconds / 60);
280
+ const hours = Math.floor(minutes / 60);
281
+ const days = Math.floor(hours / 24);
282
+ if (days > 0) {
283
+ return `${days} day${days === 1 ? '' : 's'} ago`;
284
+ }
285
+ else if (hours > 0) {
286
+ return `${hours} hour${hours === 1 ? '' : 's'} ago`;
287
+ }
288
+ else if (minutes > 0) {
289
+ return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;
290
+ }
291
+ else {
292
+ return 'just now';
293
+ }
294
+ }
295
+ /**
296
+ * Get current version
297
+ */
298
+ getVersion() {
299
+ try {
300
+ const packageJsonPath = path.join(__dirname, '../../../package.json');
301
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
302
+ return packageJson.version;
303
+ }
304
+ catch (error) {
305
+ return 'unknown';
306
+ }
307
+ }
308
+ }
309
+ exports.ProjectCommands = ProjectCommands;
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.MCPServer = void 0;
4
37
  const stdio_1 = require("./transports/stdio");
@@ -18,6 +51,9 @@ const context_enhancer_1 = require("../services/context-enhancer");
18
51
  const conversation_context_manager_1 = require("../services/conversation-context-manager");
19
52
  const process_manager_1 = require("../services/process-manager");
20
53
  const config_1 = require("../services/config");
54
+ const project_registry_1 = require("../services/project-registry");
55
+ const path = __importStar(require("path"));
56
+ const fs = __importStar(require("fs"));
21
57
  class MCPServer {
22
58
  constructor() {
23
59
  this.tools = new Map();
@@ -384,6 +420,13 @@ class MCPServer {
384
420
  this.logger.info('MCPServer', 'Starting Claude Recall MCP server...');
385
421
  // Get project ID for PID tracking
386
422
  const projectId = this.config.getProjectId();
423
+ // Auto-register project in registry
424
+ const rootDir = this.config.getConfig().project.rootDir;
425
+ const version = this.getVersion();
426
+ const projectRegistry = project_registry_1.ProjectRegistry.getInstance();
427
+ projectRegistry.register(projectId, rootDir, version);
428
+ projectRegistry.updateLastSeen(projectId);
429
+ this.logger.debug('MCPServer', `Project registered: ${projectId} at ${rootDir} (v${version})`);
387
430
  // Check for existing MCP server process
388
431
  const existingPid = this.processManager.readPidFile(projectId);
389
432
  if (existingPid) {
@@ -460,5 +503,18 @@ class MCPServer {
460
503
  process.exit(0);
461
504
  });
462
505
  }
506
+ /**
507
+ * Get current version from package.json
508
+ */
509
+ getVersion() {
510
+ try {
511
+ const packageJsonPath = path.join(__dirname, '../../package.json');
512
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
513
+ return packageJson.version;
514
+ }
515
+ catch (error) {
516
+ return 'unknown';
517
+ }
518
+ }
463
519
  }
464
520
  exports.MCPServer = MCPServer;
@@ -0,0 +1,241 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ProjectRegistry = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const logging_1 = require("./logging");
40
+ const config_1 = require("./config");
41
+ /**
42
+ * ProjectRegistry
43
+ *
44
+ * Manages a registry of projects that have claude-recall installed.
45
+ * Stored in ~/.claude-recall/projects.json
46
+ *
47
+ * Features:
48
+ * - Auto-registration on MCP server start
49
+ * - Manual registration via CLI commands
50
+ * - Discovery of registered projects
51
+ * - Cleanup of stale entries
52
+ */
53
+ class ProjectRegistry {
54
+ constructor() {
55
+ this.logger = logging_1.LoggingService.getInstance();
56
+ const config = config_1.ConfigService.getInstance();
57
+ const baseDir = path.dirname(config.getDatabasePath());
58
+ this.registryPath = path.join(baseDir, 'projects.json');
59
+ }
60
+ static getInstance() {
61
+ if (!ProjectRegistry.instance) {
62
+ ProjectRegistry.instance = new ProjectRegistry();
63
+ }
64
+ return ProjectRegistry.instance;
65
+ }
66
+ /**
67
+ * Read the project registry from disk
68
+ */
69
+ readRegistry() {
70
+ if (!fs.existsSync(this.registryPath)) {
71
+ return { version: 1, projects: {} };
72
+ }
73
+ try {
74
+ const content = fs.readFileSync(this.registryPath, 'utf-8');
75
+ const data = JSON.parse(content);
76
+ // Validate structure
77
+ if (typeof data.version !== 'number' || typeof data.projects !== 'object') {
78
+ this.logger.warn('ProjectRegistry', 'Invalid registry format, resetting');
79
+ return { version: 1, projects: {} };
80
+ }
81
+ return data;
82
+ }
83
+ catch (error) {
84
+ this.logger.error('ProjectRegistry', `Failed to read registry: ${error}`);
85
+ return { version: 1, projects: {} };
86
+ }
87
+ }
88
+ /**
89
+ * Write the project registry to disk atomically
90
+ */
91
+ writeRegistry(data) {
92
+ try {
93
+ // Ensure directory exists
94
+ const dir = path.dirname(this.registryPath);
95
+ if (!fs.existsSync(dir)) {
96
+ fs.mkdirSync(dir, { recursive: true });
97
+ }
98
+ // Atomic write: temp file + rename
99
+ const tempPath = `${this.registryPath}.tmp`;
100
+ fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf-8');
101
+ fs.renameSync(tempPath, this.registryPath);
102
+ this.logger.debug('ProjectRegistry', `Wrote registry with ${Object.keys(data.projects).length} projects`);
103
+ }
104
+ catch (error) {
105
+ this.logger.error('ProjectRegistry', `Failed to write registry: ${error}`);
106
+ throw error;
107
+ }
108
+ }
109
+ /**
110
+ * Register a project in the registry
111
+ * @param projectId Project identifier (usually directory basename)
112
+ * @param projectPath Absolute path to project directory
113
+ * @param version Claude Recall version
114
+ */
115
+ register(projectId, projectPath, version) {
116
+ try {
117
+ const registry = this.readRegistry();
118
+ const now = new Date().toISOString();
119
+ // Check if already registered
120
+ const existing = registry.projects[projectId];
121
+ if (existing && existing.path === projectPath) {
122
+ // Already registered, just update version and lastSeen
123
+ registry.projects[projectId].version = version;
124
+ registry.projects[projectId].lastSeen = now;
125
+ this.logger.debug('ProjectRegistry', `Updated existing project: ${projectId}`);
126
+ }
127
+ else {
128
+ // New registration
129
+ registry.projects[projectId] = {
130
+ path: projectPath,
131
+ registeredAt: now,
132
+ version: version,
133
+ lastSeen: now
134
+ };
135
+ this.logger.info('ProjectRegistry', `Registered new project: ${projectId} at ${projectPath}`);
136
+ }
137
+ this.writeRegistry(registry);
138
+ }
139
+ catch (error) {
140
+ this.logger.error('ProjectRegistry', `Failed to register project ${projectId}: ${error}`);
141
+ // Don't throw - registration failure shouldn't break MCP server startup
142
+ }
143
+ }
144
+ /**
145
+ * Unregister a project from the registry
146
+ */
147
+ unregister(projectId) {
148
+ try {
149
+ const registry = this.readRegistry();
150
+ if (!registry.projects[projectId]) {
151
+ this.logger.warn('ProjectRegistry', `Project not found: ${projectId}`);
152
+ return false;
153
+ }
154
+ delete registry.projects[projectId];
155
+ this.writeRegistry(registry);
156
+ this.logger.info('ProjectRegistry', `Unregistered project: ${projectId}`);
157
+ return true;
158
+ }
159
+ catch (error) {
160
+ this.logger.error('ProjectRegistry', `Failed to unregister project ${projectId}: ${error}`);
161
+ return false;
162
+ }
163
+ }
164
+ /**
165
+ * Update lastSeen timestamp for a project
166
+ */
167
+ updateLastSeen(projectId) {
168
+ try {
169
+ const registry = this.readRegistry();
170
+ if (!registry.projects[projectId]) {
171
+ this.logger.debug('ProjectRegistry', `Cannot update lastSeen for unknown project: ${projectId}`);
172
+ return;
173
+ }
174
+ registry.projects[projectId].lastSeen = new Date().toISOString();
175
+ this.writeRegistry(registry);
176
+ }
177
+ catch (error) {
178
+ this.logger.error('ProjectRegistry', `Failed to update lastSeen for ${projectId}: ${error}`);
179
+ // Don't throw - this is non-critical
180
+ }
181
+ }
182
+ /**
183
+ * Get all registered projects
184
+ */
185
+ list() {
186
+ const registry = this.readRegistry();
187
+ return Object.entries(registry.projects).map(([projectId, entry]) => ({
188
+ projectId,
189
+ entry
190
+ }));
191
+ }
192
+ /**
193
+ * Get a specific project entry
194
+ */
195
+ get(projectId) {
196
+ const registry = this.readRegistry();
197
+ return registry.projects[projectId] || null;
198
+ }
199
+ /**
200
+ * Clean stale registry entries (not seen in X days)
201
+ * @param daysOld Number of days without activity to consider stale
202
+ * @param dryRun If true, don't actually remove entries
203
+ * @returns Number of entries that would be/were removed
204
+ */
205
+ clean(daysOld = 30, dryRun = false) {
206
+ try {
207
+ const registry = this.readRegistry();
208
+ const now = Date.now();
209
+ const cutoff = now - (daysOld * 24 * 60 * 60 * 1000);
210
+ let removed = 0;
211
+ for (const [projectId, entry] of Object.entries(registry.projects)) {
212
+ const lastSeenTime = new Date(entry.lastSeen).getTime();
213
+ if (lastSeenTime < cutoff) {
214
+ if (dryRun) {
215
+ this.logger.info('ProjectRegistry', `[DRY RUN] Would remove stale project: ${projectId} (last seen: ${entry.lastSeen})`);
216
+ }
217
+ else {
218
+ this.logger.info('ProjectRegistry', `Removing stale project: ${projectId} (last seen: ${entry.lastSeen})`);
219
+ delete registry.projects[projectId];
220
+ }
221
+ removed++;
222
+ }
223
+ }
224
+ if (!dryRun && removed > 0) {
225
+ this.writeRegistry(registry);
226
+ }
227
+ return removed;
228
+ }
229
+ catch (error) {
230
+ this.logger.error('ProjectRegistry', `Failed to clean registry: ${error}`);
231
+ return 0;
232
+ }
233
+ }
234
+ /**
235
+ * Get the registry file path (for debugging/inspection)
236
+ */
237
+ getRegistryPath() {
238
+ return this.registryPath;
239
+ }
240
+ }
241
+ exports.ProjectRegistry = ProjectRegistry;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claude-recall",
3
- "version": "0.7.4",
4
- "description": "Persistent memory for Claude Code with automatic capture, failure learning, sophistication tracking, and project scoping via MCP server",
3
+ "version": "0.7.5",
4
+ "description": "Persistent memory for Claude Code with automatic capture, failure learning, sophistication tracking, project scoping, and project registry via MCP server",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "claude-recall": "dist/cli/claude-recall-cli.js"
@@ -11,6 +11,7 @@
11
11
  ".claude/",
12
12
  "scripts/postinstall.js",
13
13
  "scripts/postinstall-claude-md.js",
14
+ "scripts/uninstall.js",
14
15
  "README.md",
15
16
  "LICENSE",
16
17
  "docs/"
@@ -89,7 +90,6 @@
89
90
  "dependencies": {
90
91
  "better-sqlite3": "^12.2.0",
91
92
  "chalk": "^5.5.0",
92
- "claude-recall": "^0.7.3",
93
93
  "commander": "^12.1.0"
94
94
  }
95
95
  }
@@ -56,6 +56,53 @@ try {
56
56
  // Don't fail installation if CLAUDE.md update fails
57
57
  }
58
58
 
59
+ // Auto-register project if this is a local install
60
+ try {
61
+ // Detect if we're in a project (not global install, not claude-recall itself)
62
+ const cwd = process.cwd();
63
+ const projectName = path.basename(cwd);
64
+
65
+ // Skip registration if:
66
+ // 1. We're inside claude-recall itself
67
+ // 2. We're in node_modules (global install)
68
+ if (projectName !== 'claude-recall' && !cwd.includes('node_modules/.pnpm') && !cwd.includes('node_modules/claude-recall')) {
69
+ const registryPath = path.join(dbDir, 'projects.json');
70
+
71
+ // Read or create registry
72
+ let registry = { version: 1, projects: {} };
73
+ if (fs.existsSync(registryPath)) {
74
+ const registryContent = fs.readFileSync(registryPath, 'utf8');
75
+ registry = JSON.parse(registryContent);
76
+ }
77
+
78
+ // Get version from package.json
79
+ const packageJsonPath = path.join(__dirname, '../package.json');
80
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
81
+ const version = packageJson.version;
82
+
83
+ // Register project
84
+ const now = new Date().toISOString();
85
+ const existing = registry.projects[projectName];
86
+
87
+ registry.projects[projectName] = {
88
+ path: cwd,
89
+ registeredAt: existing ? existing.registeredAt : now,
90
+ version: version,
91
+ lastSeen: now
92
+ };
93
+
94
+ // Write registry atomically
95
+ const tempPath = registryPath + '.tmp';
96
+ fs.writeFileSync(tempPath, JSON.stringify(registry, null, 2));
97
+ fs.renameSync(tempPath, registryPath);
98
+
99
+ console.log(`šŸ“‹ Registered project: ${projectName}`);
100
+ }
101
+ } catch (error) {
102
+ // Don't fail installation if registration fails
103
+ console.log('āš ļø Failed to register project (non-fatal):', error.message);
104
+ }
105
+
59
106
  console.log('\nšŸ“ Installation complete!');
60
107
  console.log(' Claude Recall MCP server is now configured.');
61
108
  console.log(' Restart your terminal to activate the memory system.');
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ console.log('\nšŸ‘‹ Uninstalling Claude Recall...\n');
8
+
9
+ try {
10
+ const dbDir = path.join(os.homedir(), '.claude-recall');
11
+ const registryPath = path.join(dbDir, 'projects.json');
12
+ const cwd = process.cwd();
13
+ const projectName = path.basename(cwd);
14
+
15
+ // Remove project from registry
16
+ if (fs.existsSync(registryPath)) {
17
+ try {
18
+ const registryContent = fs.readFileSync(registryPath, 'utf8');
19
+ const registry = JSON.parse(registryContent);
20
+
21
+ if (registry.projects && registry.projects[projectName]) {
22
+ delete registry.projects[projectName];
23
+
24
+ // Write registry atomically
25
+ const tempPath = registryPath + '.tmp';
26
+ fs.writeFileSync(tempPath, JSON.stringify(registry, null, 2));
27
+ fs.renameSync(tempPath, registryPath);
28
+
29
+ console.log(`šŸ“‹ Removed project from registry: ${projectName}`);
30
+ } else {
31
+ console.log(`āš ļø Project not found in registry: ${projectName}`);
32
+ }
33
+ } catch (error) {
34
+ console.log('āš ļø Failed to update registry (non-fatal):', error.message);
35
+ }
36
+ }
37
+
38
+ // Stop MCP server if running
39
+ const pidDir = path.join(dbDir, 'pids');
40
+ const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, '_');
41
+ const pidFile = path.join(pidDir, `mcp-${safeName}.pid`);
42
+
43
+ if (fs.existsSync(pidFile)) {
44
+ try {
45
+ const pidContent = fs.readFileSync(pidFile, 'utf8').trim();
46
+ const pid = parseInt(pidContent, 10);
47
+
48
+ if (!isNaN(pid) && pid > 0) {
49
+ try {
50
+ // Check if process exists
51
+ process.kill(pid, 0);
52
+ // If we get here, process exists - kill it
53
+ process.kill(pid, 'SIGTERM');
54
+ console.log(`šŸ›‘ Stopped MCP server (PID: ${pid})`);
55
+ } catch (error) {
56
+ // Process doesn't exist or no permission
57
+ if (error.code === 'ESRCH') {
58
+ console.log('āš ļø MCP server was not running');
59
+ }
60
+ }
61
+ }
62
+
63
+ // Remove PID file
64
+ fs.unlinkSync(pidFile);
65
+ console.log('šŸ“„ Removed PID file');
66
+ } catch (error) {
67
+ console.log('āš ļø Failed to stop MCP server (non-fatal):', error.message);
68
+ }
69
+ }
70
+
71
+ console.log('\nāœ… Uninstallation cleanup complete!\n');
72
+
73
+ console.log('šŸ“ Note:');
74
+ console.log(' - Your memory database is still intact at ~/.claude-recall/');
75
+ console.log(' - MCP server configuration in ~/.claude.json is still present');
76
+ console.log(' - Other projects using Claude Recall are unaffected\n');
77
+
78
+ console.log('šŸ—‘ļø To completely remove Claude Recall:');
79
+ console.log(' 1. Remove MCP server from ~/.claude.json');
80
+ console.log(' 2. Delete ~/.claude-recall/ directory');
81
+ console.log(' 3. Uninstall globally: npm uninstall -g claude-recall\n');
82
+
83
+ } catch (error) {
84
+ console.error('āŒ Error during uninstall:', error.message);
85
+ console.log('\nšŸ’” You may need to manually clean up:');
86
+ console.log(' - Remove project from ~/.claude-recall/projects.json');
87
+ console.log(' - Stop MCP server: npx claude-recall mcp stop');
88
+ console.log(' - Remove PID file from ~/.claude-recall/pids/\n');
89
+ }