diff --git a/README.md b/README.md index 33e98df4..5098df40 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ npx claude-mem install --ide gemini-cli Or install from the plugin marketplace inside Claude Code: -``` +```bash /plugin marketplace add thedotmack/claude-mem /plugin install claude-mem diff --git a/src/integrations/opencode-plugin/index.ts b/src/integrations/opencode-plugin/index.ts index 2133a22a..b9c40ed3 100644 --- a/src/integrations/opencode-plugin/index.ts +++ b/src/integrations/opencode-plugin/index.ts @@ -165,8 +165,19 @@ async function workerGetText(path: string): Promise { const contentSessionIdsByOpenCodeSessionId = new Map(); +const MAX_SESSION_MAP_ENTRIES = 1000; + function getOrCreateContentSessionId(openCodeSessionId: string): string { if (!contentSessionIdsByOpenCodeSessionId.has(openCodeSessionId)) { + // Evict oldest entries when the map exceeds the cap (Map preserves insertion order) + while (contentSessionIdsByOpenCodeSessionId.size >= MAX_SESSION_MAP_ENTRIES) { + const oldestKey = contentSessionIdsByOpenCodeSessionId.keys().next().value; + if (oldestKey !== undefined) { + contentSessionIdsByOpenCodeSessionId.delete(oldestKey); + } else { + break; + } + } contentSessionIdsByOpenCodeSessionId.set( openCodeSessionId, `opencode-${openCodeSessionId}-${Date.now()}`, diff --git a/src/npx-cli/commands/uninstall.ts b/src/npx-cli/commands/uninstall.ts index 63e3176e..86b0fa98 100644 --- a/src/npx-cli/commands/uninstall.ts +++ b/src/npx-cli/commands/uninstall.ts @@ -105,13 +105,25 @@ export async function runUninstallCommand(): Promise { } } - // Stop the worker first (best-effort) + // Stop the worker and wait for it to exit before deleting files + const workerPort = process.env.CLAUDE_MEM_WORKER_PORT || '37777'; try { - const workerPort = process.env.CLAUDE_MEM_WORKER_PORT || '37777'; await fetch(`http://127.0.0.1:${workerPort}/api/admin/shutdown`, { method: 'POST', signal: AbortSignal.timeout(5000), }); + // Poll health endpoint until worker is gone (max 10s) + for (let attempt = 0; attempt < 20; attempt++) { + await new Promise((resolve) => setTimeout(resolve, 500)); + try { + await fetch(`http://127.0.0.1:${workerPort}/api/health`, { + signal: AbortSignal.timeout(1000), + }); + // Still alive — keep waiting + } catch { + break; // Connection refused = worker is gone + } + } p.log.info('Worker service stopped.'); } catch { // Worker may not be running — that is fine @@ -159,6 +171,41 @@ export async function runUninstallCommand(): Promise { }, ]); + // Remove IDE-specific hooks and config (best-effort, each is independent) + const ideCleanups: Array<{ label: string; fn: () => Promise | number }> = [ + { label: 'Gemini CLI hooks', fn: async () => { + const { uninstallGeminiCliHooks } = await import('../../services/integrations/GeminiCliHooksInstaller.js'); + return uninstallGeminiCliHooks(); + }}, + { label: 'Windsurf hooks', fn: async () => { + const { uninstallWindsurfHooks } = await import('../../services/integrations/WindsurfHooksInstaller.js'); + return uninstallWindsurfHooks(); + }}, + { label: 'OpenCode plugin', fn: async () => { + const { uninstallOpenCodePlugin } = await import('../../services/integrations/OpenCodeInstaller.js'); + return uninstallOpenCodePlugin(); + }}, + { label: 'OpenClaw plugin', fn: async () => { + const { uninstallOpenClawPlugin } = await import('../../services/integrations/OpenClawInstaller.js'); + return uninstallOpenClawPlugin(); + }}, + { label: 'Codex CLI', fn: async () => { + const { uninstallCodexCli } = await import('../../services/integrations/CodexCliInstaller.js'); + return uninstallCodexCli(); + }}, + ]; + + for (const { label, fn } of ideCleanups) { + try { + const result = await fn(); + if (result === 0) { + p.log.info(`${label}: removed.`); + } + } catch { + // IDE not configured or uninstaller errored — skip silently + } + } + p.note( [ `Your data directory at ${pc.cyan('~/.claude-mem')} was preserved.`, diff --git a/src/npx-cli/utils/paths.ts b/src/npx-cli/utils/paths.ts index 2dbf4cbc..1de55255 100644 --- a/src/npx-cli/utils/paths.ts +++ b/src/npx-cli/utils/paths.ts @@ -73,7 +73,14 @@ export function claudeMemDataDirectory(): string { export function npmPackageRootDirectory(): string { const currentFilePath = fileURLToPath(import.meta.url); // /dist/npx-cli/index.js -> up 2 levels -> - return join(dirname(currentFilePath), '..', '..'); + const root = join(dirname(currentFilePath), '..', '..'); + if (!existsSync(join(root, 'package.json'))) { + throw new Error( + `npmPackageRootDirectory: expected package.json at ${root}. ` + + `Bundle structure may have changed — update the path walk.`, + ); + } + return root; } /** diff --git a/src/services/integrations/GeminiCliHooksInstaller.ts b/src/services/integrations/GeminiCliHooksInstaller.ts index f620126c..4ec749ab 100644 --- a/src/services/integrations/GeminiCliHooksInstaller.ts +++ b/src/services/integrations/GeminiCliHooksInstaller.ts @@ -119,7 +119,10 @@ function buildHookCommand( throw new Error(`Unknown Gemini CLI event: ${geminiEventName}`); } - // Escape backslashes for JSON compatibility on Windows + // Double-escape backslashes intentionally: this command string is embedded inside + // a JSON value, so `\\` in the source becomes `\` when the JSON is parsed by the + // IDE. Without double-escaping, Windows paths like C:\Users would lose their + // backslashes and break when the IDE deserializes the hook configuration. const escapedBunPath = bunPath.replace(/\\/g, '\\\\'); const escapedWorkerPath = workerServicePath.replace(/\\/g, '\\\\'); diff --git a/src/services/integrations/OpenCodeInstaller.ts b/src/services/integrations/OpenCodeInstaller.ts index 62d9410c..e16c29a8 100644 --- a/src/services/integrations/OpenCodeInstaller.ts +++ b/src/services/integrations/OpenCodeInstaller.ts @@ -16,6 +16,7 @@ import path from 'path'; import { homedir } from 'os'; +import { fileURLToPath } from 'url'; import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, unlinkSync } from 'fs'; import { logger } from '../../utils/logger.js'; import { CONTEXT_TAG_OPEN, CONTEXT_TAG_CLOSE, injectContextIntoMarkdownFile } from '../../utils/context-injection.js'; @@ -74,8 +75,8 @@ export function findBuiltPluginPath(): string | null { 'plugins', 'marketplaces', 'thedotmack', 'dist', 'opencode-plugin', 'index.js', ), - // Development location (relative to project root) - path.join(process.cwd(), 'dist', 'opencode-plugin', 'index.js'), + // Development location (relative to this module's package root) + path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'dist', 'opencode-plugin', 'index.js'), ]; for (const candidatePath of possiblePaths) { @@ -171,7 +172,10 @@ export async function syncContextToAgentsMd( const contextText = await response.text(); if (contextText && contextText.trim()) { - injectContextIntoAgentsMd(contextText); + const injectResult = injectContextIntoAgentsMd(contextText); + if (injectResult !== 0) { + logger.warn('OPENCODE', 'Failed to inject context into AGENTS.md during sync'); + } } } catch { // Worker not available — non-critical @@ -315,22 +319,41 @@ Use claude-mem search tools for manual memory queries.`; if (contextResponse.ok) { const realContext = await contextResponse.text(); if (realContext && realContext.trim()) { - injectContextIntoAgentsMd(realContext); - console.log(' Context injected from existing memory'); + const injectResult = injectContextIntoAgentsMd(realContext); + if (injectResult !== 0) { + logger.warn('OPENCODE', 'Failed to inject real context into AGENTS.md during install'); + } else { + console.log(' Context injected from existing memory'); + } } else { - injectContextIntoAgentsMd(placeholderContext); - console.log(' Placeholder context created (will populate after first session)'); + const injectResult = injectContextIntoAgentsMd(placeholderContext); + if (injectResult !== 0) { + logger.warn('OPENCODE', 'Failed to inject placeholder context into AGENTS.md during install'); + } else { + console.log(' Placeholder context created (will populate after first session)'); + } } } else { - injectContextIntoAgentsMd(placeholderContext); + const injectResult = injectContextIntoAgentsMd(placeholderContext); + if (injectResult !== 0) { + logger.warn('OPENCODE', 'Failed to inject placeholder context into AGENTS.md during install'); + } } } else { - injectContextIntoAgentsMd(placeholderContext); - console.log(' Placeholder context created (worker not running)'); + const injectResult = injectContextIntoAgentsMd(placeholderContext); + if (injectResult !== 0) { + logger.warn('OPENCODE', 'Failed to inject placeholder context into AGENTS.md during install'); + } else { + console.log(' Placeholder context created (worker not running)'); + } } } catch { - injectContextIntoAgentsMd(placeholderContext); - console.log(' Placeholder context created (worker not running)'); + const injectResult = injectContextIntoAgentsMd(placeholderContext); + if (injectResult !== 0) { + logger.warn('OPENCODE', 'Failed to inject placeholder context into AGENTS.md during install'); + } else { + console.log(' Placeholder context created (worker not running)'); + } } console.log(` diff --git a/src/services/integrations/WindsurfHooksInstaller.ts b/src/services/integrations/WindsurfHooksInstaller.ts index d40b8bfd..6c6b213b 100644 --- a/src/services/integrations/WindsurfHooksInstaller.ts +++ b/src/services/integrations/WindsurfHooksInstaller.ts @@ -46,8 +46,7 @@ interface WindsurfHooksJson { } interface WindsurfProjectRegistry { - [projectName: string]: { - workspacePath: string; + [workspacePath: string]: { installedAt: string; }; } @@ -104,37 +103,37 @@ export function writeWindsurfRegistry(registry: WindsurfProjectRegistry): void { } /** - * Register a project for auto-context updates + * Register a project for auto-context updates. + * Keys by full workspacePath to avoid collisions between directories with the same basename. */ -export function registerWindsurfProject(projectName: string, workspacePath: string): void { +export function registerWindsurfProject(workspacePath: string): void { const registry = readWindsurfRegistry(); - registry[projectName] = { - workspacePath, + registry[workspacePath] = { installedAt: new Date().toISOString(), }; writeWindsurfRegistry(registry); - logger.info('WINDSURF', 'Registered project for auto-context updates', { projectName, workspacePath }); + logger.info('WINDSURF', 'Registered project for auto-context updates', { workspacePath }); } /** * Unregister a project from auto-context updates */ -export function unregisterWindsurfProject(projectName: string): void { +export function unregisterWindsurfProject(workspacePath: string): void { const registry = readWindsurfRegistry(); - if (registry[projectName]) { - delete registry[projectName]; + if (registry[workspacePath]) { + delete registry[workspacePath]; writeWindsurfRegistry(registry); - logger.info('WINDSURF', 'Unregistered project', { projectName }); + logger.info('WINDSURF', 'Unregistered project', { workspacePath }); } } /** - * Update Windsurf context files for all registered projects matching this project name. + * Update Windsurf context files for a registered project. * Called by SDK agents after saving a summary. */ -export async function updateWindsurfContextForProject(projectName: string, port: number): Promise { +export async function updateWindsurfContextForProject(projectName: string, workspacePath: string, port: number): Promise { const registry = readWindsurfRegistry(); - const entry = registry[projectName]; + const entry = registry[workspacePath]; if (!entry) return; // Project doesn't have Windsurf hooks installed @@ -148,11 +147,11 @@ export async function updateWindsurfContextForProject(projectName: string, port: const context = await response.text(); if (!context || !context.trim()) return; - writeWindsurfContextFile(entry.workspacePath, context); - logger.debug('WINDSURF', 'Updated context file', { projectName, workspacePath: entry.workspacePath }); + writeWindsurfContextFile(workspacePath, context); + logger.debug('WINDSURF', 'Updated context file', { projectName, workspacePath }); } catch (error) { // Background context update — failure is non-critical - logger.error('WINDSURF', 'Failed to update context file', { projectName }, error as Error); + logger.error('WINDSURF', 'Failed to update context file', { projectName, workspacePath }, error as Error); } } @@ -371,7 +370,7 @@ Use claude-mem's MCP search tools for manual memory queries. } // Register project for automatic context updates after summaries - registerWindsurfProject(projectName, workspaceRoot); + registerWindsurfProject(workspaceRoot); console.log(` Registered for auto-context updates`); } @@ -423,8 +422,7 @@ export function uninstallWindsurfHooks(): number { } // Unregister project - const projectName = path.basename(workspaceRoot); - unregisterWindsurfProject(projectName); + unregisterWindsurfProject(workspaceRoot); console.log(` Unregistered from auto-context updates`); console.log(`\nUninstallation complete!\n`);