fix(codex): make mem-search MCP startup self-locating

This commit is contained in:
Alex Newman
2026-05-06 14:20:55 -07:00
parent bb3dbfdb5a
commit 938c608507
7 changed files with 98 additions and 11 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
"name": "claude-mem", "name": "claude-mem",
"source": { "source": {
"source": "local", "source": "local",
"path": "./" "path": "./plugin"
}, },
"policy": { "policy": {
"installation": "AVAILABLE", "installation": "AVAILABLE",
+1 -1
View File
@@ -5,7 +5,7 @@
"command": "sh", "command": "sh",
"args": [ "args": [
"-c", "-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\""
] ]
} }
} }
+1 -1
View File
@@ -5,7 +5,7 @@
"command": "sh", "command": "sh",
"args": [ "args": [
"-c", "-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\""
] ]
} }
} }
+10
View File
@@ -384,6 +384,16 @@ async function buildHooks() {
throw new Error(`plugin/hooks/codex-hooks.json contains unknown Codex hook event: ${eventName}`); 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('✓ All required distribution files present');
console.log('\n✅ All build targets compiled successfully!'); console.log('\n✅ All build targets compiled successfully!');
+31 -6
View File
@@ -11,8 +11,10 @@ const MARKETPLACE_NAME = 'claude-mem-local';
const MIN_CODEX_MARKETPLACE_VERSION = '0.128.0'; const MIN_CODEX_MARKETPLACE_VERSION = '0.128.0';
const REQUIRED_MARKETPLACE_FILES = [ const REQUIRED_MARKETPLACE_FILES = [
path.join('.agents', 'plugins', 'marketplace.json'), path.join('.agents', 'plugins', 'marketplace.json'),
path.join('.codex-plugin', 'plugin.json'), path.join('plugin', '.codex-plugin', 'plugin.json'),
'.mcp.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 { 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); let current = path.resolve(start);
while (true) { while (true) {
if (existsSync(path.join(current, '.codex-plugin', 'plugin.json'))) { if (existsSync(path.join(current, '.agents', 'plugins', 'marketplace.json'))) {
return current; return current;
} }
const parent = path.dirname(current); const parent = path.dirname(current);
@@ -66,11 +68,11 @@ function resolvePluginMarketplaceRoot(preferredRoot?: string): string {
].filter((value): value is string => Boolean(value)); ].filter((value): value is string => Boolean(value));
for (const candidate of candidates) { for (const candidate of candidates) {
const resolved = findAncestorWithCodexPlugin(candidate); const resolved = findAncestorWithCodexMarketplace(candidate);
if (resolved && missingMarketplaceFiles(resolved).length === 0) return resolved; 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 { 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 { function parseSemver(value: string): [number, number, number] | null {
const match = value.match(/(\d+)\.(\d+)\.(\d+)/); const match = value.match(/(\d+)\.(\d+)\.(\d+)/);
if (!match) return null; if (!match) return null;
@@ -187,6 +201,16 @@ export async function installCodexCli(marketplaceRootOverride?: string): Promise
console.log(` Registering Codex plugin marketplace: ${marketplaceRoot}`); console.log(` Registering Codex plugin marketplace: ${marketplaceRoot}`);
runCodex(['plugin', 'marketplace', 'add', 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()) { if (!cleanupLegacyCodexAgentsMdContext()) {
console.warn(` Native Codex hooks registered, but failed to remove legacy AGENTS.md context from ${CODEX_AGENTS_MD_PATH}.`); 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 1. Open Codex CLI in your project
2. Run /plugins 2. Run /plugins
3. Install claude-mem from the claude-mem (local) marketplace 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: For a fresh setup, the supported entry point is:
npx claude-mem@latest install npx claude-mem@latest install
@@ -37,8 +37,12 @@ describe('Plugin Distribution - Skills', () => {
describe('Plugin Distribution - Required Files', () => { describe('Plugin Distribution - Required Files', () => {
const requiredFiles = [ const requiredFiles = [
'plugin/hooks/hooks.json', 'plugin/hooks/hooks.json',
'plugin/hooks/codex-hooks.json',
'plugin/.claude-plugin/plugin.json', 'plugin/.claude-plugin/plugin.json',
'plugin/.codex-plugin/plugin.json',
'plugin/.mcp.json',
'plugin/skills/mem-search/SKILL.md', 'plugin/skills/mem-search/SKILL.md',
'.agents/plugins/marketplace.json',
]; ];
for (const filePath of requiredFiles) { 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', () => { describe('Plugin Distribution - hooks.json Integrity', () => {
it('should have valid JSON in hooks.json', () => { it('should have valid JSON in hooks.json', () => {
const hooksPath = path.join(projectRoot, 'plugin/hooks/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', () => { 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 packageJsonPath = path.join(projectRoot, 'package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
expect(packageJson.files).toBeDefined(); 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');
}); });
}); });
+25
View File
@@ -103,6 +103,13 @@ describe('Install Non-TTY Support', () => {
expect(copyRegion).toContain("'.mcp.json'"); 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', () => { it('does not exclude MCP manifests during local marketplace sync', () => {
const gitignoreExcludeRegion = syncMarketplaceSource.slice( const gitignoreExcludeRegion = syncMarketplaceSource.slice(
syncMarketplaceSource.indexOf('function getGitignoreExcludes'), syncMarketplaceSource.indexOf('function getGitignoreExcludes'),
@@ -116,6 +123,24 @@ describe('Install Non-TTY Support', () => {
expect(installSource).toContain('installCodexCli(marketplaceDirectory())'); 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', () => { it('captures Codex CLI output for install failure reporting', () => {
const runCodexRegion = codexInstallerSource.slice( const runCodexRegion = codexInstallerSource.slice(
codexInstallerSource.indexOf('function runCodex'), codexInstallerSource.indexOf('function runCodex'),