From 938c608507be6c73c3bee50762892f5e25ae8980 Mon Sep 17 00:00:00 2001 From: Alex Newman Date: Wed, 6 May 2026 14:20:55 -0700 Subject: [PATCH] fix(codex): make mem-search MCP startup self-locating --- .agents/plugins/marketplace.json | 2 +- .mcp.json | 2 +- plugin/.mcp.json | 2 +- scripts/build-hooks.js | 10 +++++ .../integrations/CodexCliInstaller.ts | 37 ++++++++++++++++--- .../plugin-distribution.test.ts | 31 +++++++++++++++- tests/install-non-tty.test.ts | 25 +++++++++++++ 7 files changed, 98 insertions(+), 11 deletions(-) diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index dfe7d8bd..7e62d637 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -8,7 +8,7 @@ "name": "claude-mem", "source": { "source": "local", - "path": "./" + "path": "./plugin" }, "policy": { "installation": "AVAILABLE", diff --git a/.mcp.json b/.mcp.json index c4f03da4..e7e60187 100644 --- a/.mcp.json +++ b/.mcp.json @@ -5,7 +5,7 @@ "command": "sh", "args": [ "-c", - "_R=\"${CLAUDE_PLUGIN_ROOT:-$PLUGIN_ROOT}\"; [ -d \"$_R/plugin/scripts\" ] && _P=\"$_R/plugin\" || _P=\"$_R\"; exec node \"$_P/scripts/mcp-server.cjs\"" + "_R=\"${CLAUDE_PLUGIN_ROOT:-$PLUGIN_ROOT}\"; if [ -n \"$_R\" ]; then [ -d \"$_R/plugin/scripts\" ] && _P=\"$_R/plugin\" || _P=\"$_R\"; else _S=$(ls -dt \"$PWD\"/plugin/scripts/mcp-server.cjs \"$PWD\"/scripts/mcp-server.cjs \"$HOME\"/.codex/plugins/cache/claude-mem-local/claude-mem/*/scripts/mcp-server.cjs \"$HOME\"/.codex/plugins/cache/thedotmack/claude-mem/*/scripts/mcp-server.cjs \"$HOME\"/.claude/plugins/cache/thedotmack/claude-mem/*/scripts/mcp-server.cjs \"$HOME\"/.claude/plugins/marketplaces/thedotmack/plugin/scripts/mcp-server.cjs 2>/dev/null | head -n 1); _P=\"${_S%/scripts/mcp-server.cjs}\"; fi; [ -n \"$_P\" ] || { echo \"claude-mem MCP server not found\" >&2; exit 1; }; exec node \"$_P/scripts/mcp-server.cjs\"" ] } } diff --git a/plugin/.mcp.json b/plugin/.mcp.json index c4f03da4..e7e60187 100644 --- a/plugin/.mcp.json +++ b/plugin/.mcp.json @@ -5,7 +5,7 @@ "command": "sh", "args": [ "-c", - "_R=\"${CLAUDE_PLUGIN_ROOT:-$PLUGIN_ROOT}\"; [ -d \"$_R/plugin/scripts\" ] && _P=\"$_R/plugin\" || _P=\"$_R\"; exec node \"$_P/scripts/mcp-server.cjs\"" + "_R=\"${CLAUDE_PLUGIN_ROOT:-$PLUGIN_ROOT}\"; if [ -n \"$_R\" ]; then [ -d \"$_R/plugin/scripts\" ] && _P=\"$_R/plugin\" || _P=\"$_R\"; else _S=$(ls -dt \"$PWD\"/plugin/scripts/mcp-server.cjs \"$PWD\"/scripts/mcp-server.cjs \"$HOME\"/.codex/plugins/cache/claude-mem-local/claude-mem/*/scripts/mcp-server.cjs \"$HOME\"/.codex/plugins/cache/thedotmack/claude-mem/*/scripts/mcp-server.cjs \"$HOME\"/.claude/plugins/cache/thedotmack/claude-mem/*/scripts/mcp-server.cjs \"$HOME\"/.claude/plugins/marketplaces/thedotmack/plugin/scripts/mcp-server.cjs 2>/dev/null | head -n 1); _P=\"${_S%/scripts/mcp-server.cjs}\"; fi; [ -n \"$_P\" ] || { echo \"claude-mem MCP server not found\" >&2; exit 1; }; exec node \"$_P/scripts/mcp-server.cjs\"" ] } } diff --git a/scripts/build-hooks.js b/scripts/build-hooks.js index a4377f05..2df00280 100644 --- a/scripts/build-hooks.js +++ b/scripts/build-hooks.js @@ -384,6 +384,16 @@ async function buildHooks() { throw new Error(`plugin/hooks/codex-hooks.json contains unknown Codex hook event: ${eventName}`); } } + const codexMarketplace = JSON.parse(fs.readFileSync('.agents/plugins/marketplace.json', 'utf-8')); + const claudeMemMarketplaceEntry = (codexMarketplace.plugins ?? []).find((plugin) => plugin.name === 'claude-mem'); + if (claudeMemMarketplaceEntry?.source?.path !== './plugin') { + throw new Error('.agents/plugins/marketplace.json must point claude-mem source.path at ./plugin so Codex loads the bundled plugin root'); + } + const bundledMcp = JSON.parse(fs.readFileSync('plugin/.mcp.json', 'utf-8')); + const mcpSearchCommand = bundledMcp.mcpServers?.['mcp-search']?.args?.join(' ') ?? ''; + if (!mcpSearchCommand.includes('.codex/plugins/cache/claude-mem-local/claude-mem')) { + throw new Error('plugin/.mcp.json mcp-search launcher must include Codex cache fallback for hosts that do not inject PLUGIN_ROOT'); + } console.log('āœ“ All required distribution files present'); console.log('\nāœ… All build targets compiled successfully!'); diff --git a/src/services/integrations/CodexCliInstaller.ts b/src/services/integrations/CodexCliInstaller.ts index 3f91bce9..1959bb3d 100644 --- a/src/services/integrations/CodexCliInstaller.ts +++ b/src/services/integrations/CodexCliInstaller.ts @@ -11,8 +11,10 @@ const MARKETPLACE_NAME = 'claude-mem-local'; const MIN_CODEX_MARKETPLACE_VERSION = '0.128.0'; const REQUIRED_MARKETPLACE_FILES = [ path.join('.agents', 'plugins', 'marketplace.json'), - path.join('.codex-plugin', 'plugin.json'), - '.mcp.json', + path.join('plugin', '.codex-plugin', 'plugin.json'), + path.join('plugin', '.mcp.json'), + path.join('plugin', 'hooks', 'codex-hooks.json'), + path.join('plugin', 'skills', 'mem-search', 'SKILL.md'), ]; function commandExists(command: string): boolean { @@ -28,10 +30,10 @@ function commandExists(command: string): boolean { } } -function findAncestorWithCodexPlugin(start: string): string | null { +function findAncestorWithCodexMarketplace(start: string): string | null { let current = path.resolve(start); while (true) { - if (existsSync(path.join(current, '.codex-plugin', 'plugin.json'))) { + if (existsSync(path.join(current, '.agents', 'plugins', 'marketplace.json'))) { return current; } const parent = path.dirname(current); @@ -66,11 +68,11 @@ function resolvePluginMarketplaceRoot(preferredRoot?: string): string { ].filter((value): value is string => Boolean(value)); for (const candidate of candidates) { - const resolved = findAncestorWithCodexPlugin(candidate); + const resolved = findAncestorWithCodexMarketplace(candidate); if (resolved && missingMarketplaceFiles(resolved).length === 0) return resolved; } - throw new Error('Could not locate a Codex marketplace root with .agents/plugins/marketplace.json, .codex-plugin/plugin.json, and .mcp.json. Run npx claude-mem@latest install from the package or repo root.'); + throw new Error('Could not locate a Codex marketplace root with .agents/plugins/marketplace.json and plugin/.codex-plugin/plugin.json. Run npx claude-mem@latest install from the package or repo root.'); } function runCodex(args: string[]): void { @@ -94,6 +96,18 @@ function runCodex(args: string[]): void { } } +function runCodexBestEffort(args: string[], successMessage: string, failureMessage: string): boolean { + try { + runCodex(args); + console.log(` ${successMessage}`); + return true; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.warn(` ${failureMessage}: ${message}`); + return false; + } +} + function parseSemver(value: string): [number, number, number] | null { const match = value.match(/(\d+)\.(\d+)\.(\d+)/); if (!match) return null; @@ -187,6 +201,16 @@ export async function installCodexCli(marketplaceRootOverride?: string): Promise console.log(` Registering Codex plugin marketplace: ${marketplaceRoot}`); runCodex(['plugin', 'marketplace', 'add', marketplaceRoot]); + runCodexBestEffort( + ['plugin', 'marketplace', 'upgrade', MARKETPLACE_NAME], + 'Refreshed Codex marketplace and installed plugin cache.', + 'Could not refresh Codex marketplace cache; reinstall or upgrade claude-mem from /plugins if Codex still uses old MCP config', + ); + runCodexBestEffort( + ['features', 'enable', 'plugin_hooks'], + 'Enabled Codex plugin_hooks so claude-mem hooks can run.', + 'Could not enable Codex plugin_hooks; run `codex features enable plugin_hooks` if context hooks do not appear', + ); if (!cleanupLegacyCodexAgentsMdContext()) { console.warn(` Native Codex hooks registered, but failed to remove legacy AGENTS.md context from ${CODEX_AGENTS_MD_PATH}.`); } @@ -201,6 +225,7 @@ Next steps: 1. Open Codex CLI in your project 2. Run /plugins 3. Install claude-mem from the claude-mem (local) marketplace + 4. Restart Codex CLI after install so MCP tools and plugin hooks reload For a fresh setup, the supported entry point is: npx claude-mem@latest install diff --git a/tests/infrastructure/plugin-distribution.test.ts b/tests/infrastructure/plugin-distribution.test.ts index c1fa4fac..dd415d83 100644 --- a/tests/infrastructure/plugin-distribution.test.ts +++ b/tests/infrastructure/plugin-distribution.test.ts @@ -37,8 +37,12 @@ describe('Plugin Distribution - Skills', () => { describe('Plugin Distribution - Required Files', () => { const requiredFiles = [ 'plugin/hooks/hooks.json', + 'plugin/hooks/codex-hooks.json', 'plugin/.claude-plugin/plugin.json', + 'plugin/.codex-plugin/plugin.json', + 'plugin/.mcp.json', 'plugin/skills/mem-search/SKILL.md', + '.agents/plugins/marketplace.json', ]; for (const filePath of requiredFiles) { @@ -49,6 +53,25 @@ describe('Plugin Distribution - Required Files', () => { } }); +describe('Plugin Distribution - Codex Marketplace', () => { + it('points Codex at the bundled plugin root', () => { + const marketplacePath = path.join(projectRoot, '.agents/plugins/marketplace.json'); + const marketplace = JSON.parse(readFileSync(marketplacePath, 'utf-8')); + + expect(marketplace.plugins[0].source.path).toBe('./plugin'); + }); + + it('MCP launcher can recover without plugin root environment variables', () => { + const mcpPath = path.join(projectRoot, 'plugin/.mcp.json'); + const mcp = JSON.parse(readFileSync(mcpPath, 'utf-8')); + const command = mcp.mcpServers['mcp-search'].args.join(' '); + + expect(command).toContain('.codex/plugins/cache/claude-mem-local/claude-mem'); + expect(command).toContain('.claude/plugins/cache/thedotmack/claude-mem'); + expect(command).toContain('claude-mem MCP server not found'); + }); +}); + describe('Plugin Distribution - hooks.json Integrity', () => { it('should have valid JSON in hooks.json', () => { const hooksPath = path.join(projectRoot, 'plugin/hooks/hooks.json'); @@ -108,11 +131,15 @@ describe('Plugin Distribution - hooks.json Integrity', () => { }); describe('Plugin Distribution - package.json Files Field', () => { - it('should include "plugin" in root package.json files field', () => { + it('should include bundled plugin entries in root package.json files field', () => { const packageJsonPath = path.join(projectRoot, 'package.json'); const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); expect(packageJson.files).toBeDefined(); - expect(packageJson.files).toContain('plugin'); + expect(packageJson.files).toContain('plugin/.codex-plugin'); + expect(packageJson.files).toContain('plugin/.mcp.json'); + expect(packageJson.files).toContain('plugin/hooks'); + expect(packageJson.files).toContain('plugin/skills'); + expect(packageJson.files).toContain('plugin/scripts/*.cjs'); }); }); diff --git a/tests/install-non-tty.test.ts b/tests/install-non-tty.test.ts index c755353c..69940904 100644 --- a/tests/install-non-tty.test.ts +++ b/tests/install-non-tty.test.ts @@ -103,6 +103,13 @@ describe('Install Non-TTY Support', () => { expect(copyRegion).toContain("'.mcp.json'"); }); + it('validates the bundled plugin as the Codex marketplace source', () => { + expect(codexInstallerSource).toContain("path.join('plugin', '.codex-plugin', 'plugin.json')"); + expect(codexInstallerSource).toContain("path.join('plugin', '.mcp.json')"); + expect(codexInstallerSource).toContain("path.join('plugin', 'hooks', 'codex-hooks.json')"); + expect(codexInstallerSource).toContain("path.join('plugin', 'skills', 'mem-search', 'SKILL.md')"); + }); + it('does not exclude MCP manifests during local marketplace sync', () => { const gitignoreExcludeRegion = syncMarketplaceSource.slice( syncMarketplaceSource.indexOf('function getGitignoreExcludes'), @@ -116,6 +123,24 @@ describe('Install Non-TTY Support', () => { expect(installSource).toContain('installCodexCli(marketplaceDirectory())'); }); + it('refreshes Codex marketplace cache after registration', () => { + const installRegion = codexInstallerSource.slice( + codexInstallerSource.indexOf('export async function installCodexCli'), + codexInstallerSource.indexOf('export function uninstallCodexCli'), + ); + expect(installRegion).toContain("['plugin', 'marketplace', 'upgrade', MARKETPLACE_NAME]"); + expect(installRegion).toContain('installed plugin cache'); + }); + + it('enables Codex plugin hooks during install', () => { + const installRegion = codexInstallerSource.slice( + codexInstallerSource.indexOf('export async function installCodexCli'), + codexInstallerSource.indexOf('export function uninstallCodexCli'), + ); + expect(installRegion).toContain("['features', 'enable', 'plugin_hooks']"); + expect(installRegion).toContain('codex features enable plugin_hooks'); + }); + it('captures Codex CLI output for install failure reporting', () => { const runCodexRegion = codexInstallerSource.slice( codexInstallerSource.indexOf('function runCodex'),