Merge pull request #2342 from thedotmack/fix-and-ship-codex-mem-search-access

Fix Codex mem-search MCP startup
This commit is contained in:
Alex Newman
2026-05-06 19:12:40 -07:00
committed by GitHub
13 changed files with 748 additions and 278 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",
"_C=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"; _E=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; _P=$({ [ -n \"$_E\" ] && printf '%s\\n' \"$_E\"; ls -dt \"$_C/plugins/cache/thedotmack/claude-mem\"/[0-9]*/ 2>/dev/null; printf '%s\\n' \"$_C/plugins/marketplaces/thedotmack/plugin\"; } | while IFS= read -r _R; do _R=\"${_R%/}\"; [ -d \"$_R/plugin/scripts\" ] && _Q=\"$_R/plugin\" || _Q=\"$_R\"; [ -f \"$_Q/scripts/mcp-server.cjs\" ] && { printf '%s\\n' \"$_Q\"; break; }; done); [ -n \"$_P\" ] || { echo \"claude-mem: mcp server not found\" >&2; exit 1; }; exec node \"$_P/scripts/mcp-server.cjs\"" "_C=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"; _E=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; _P=$({ [ -n \"$_E\" ] && printf '%s\\n' \"$_E\"; printf '%s\\n' \"$PWD/plugin\" \"$PWD\"; ls -dt \"$HOME/.codex/plugins/cache/claude-mem-local/claude-mem\"/[0-9]*/ \"$HOME/.codex/plugins/cache/thedotmack/claude-mem\"/[0-9]*/ \"$_C/plugins/cache/thedotmack/claude-mem\"/[0-9]*/ 2>/dev/null; printf '%s\\n' \"$_C/plugins/marketplaces/thedotmack/plugin\"; } | while IFS= read -r _R; do _R=\"${_R%/}\"; [ -d \"$_R/plugin/scripts\" ] && _Q=\"$_R/plugin\" || _Q=\"$_R\"; [ -f \"$_Q/scripts/mcp-server.cjs\" ] && { printf '%s\\n' \"$_Q\"; break; }; done); [ -n \"$_P\" ] || { echo \"claude-mem: mcp server not found\" >&2; exit 1; }; exec node \"$_P/scripts/mcp-server.cjs\""
] ]
} }
} }
@@ -0,0 +1,249 @@
# Codex Plugin Version Mismatch Investigation Plan
Date: 2026-05-06
## Summary
Codex is still exposing `claude-mem` from:
```text
/Users/alexnewman/.codex/plugins/cache/thedotmack/claude-mem/12.3.1
```
That cache entry is the source of the `claude-mem:...` skills loaded in this Codex session. The working tree and the Codex marketplace clone both advertise `12.7.2`, but the enabled Codex plugin points at the old installed cache. This is not a model-memory issue.
The likely root cause is an incomplete migration from marketplace registration to a first-class Codex plugin install. The current installer registers the marketplace, but it does not verify that the actual enabled plugin cache was installed or upgraded to the current `.codex-plugin` bundle.
## Evidence
- Current repository metadata is `12.7.2`:
- `package.json`
- `.codex-plugin/plugin.json`
- `plugin/.codex-plugin/plugin.json`
- `plugin/package.json`
- Codex marketplace source is current:
- `/Users/alexnewman/.codex/config.toml` contains `[marketplaces.claude-mem-local]`
- `last_updated = "2026-05-06T23:13:59Z"`
- `last_revision = "bb3dbfdb5ae92b55b7e4686e4904995184261232"`
- `/Users/alexnewman/.codex/.tmp/marketplaces/claude-mem-local/package.json` is `12.7.2`
- `/Users/alexnewman/.codex/.tmp/marketplaces/claude-mem-local/.codex-plugin/plugin.json` is `12.7.2`
- Active enabled plugin state is still old:
- `/Users/alexnewman/.codex/config.toml` contains `[plugins."claude-mem@thedotmack"] enabled = true`
- The only `claude-mem` plugin cache under `~/.codex/plugins/cache/thedotmack/claude-mem` is `12.3.1`
- `/Users/alexnewman/.codex/plugins/cache/thedotmack/claude-mem/12.3.1/package.json` is `12.3.1`
- `/Users/alexnewman/.codex/plugins/cache/thedotmack/claude-mem/12.3.1/.install-version` records `{"version":"12.3.1", ...}`
- The active cache is not shaped like the new first-class Codex bundle:
- It has `.claude-plugin/plugin.json`
- It does not have `.codex-plugin/plugin.json`
- It does not have `hooks/codex-hooks.json`
- Its `.mcp.json` still uses the old `bun` command with `"${CLAUDE_PLUGIN_ROOT}/scripts/mcp-server.cjs"`
- Current Codex CLI capability is limited:
- `codex-cli 0.128.0`
- `codex plugin marketplace` exposes `add`, `upgrade`, and `remove`
- There is no CLI `plugin list` or `plugin install` subcommand in this build
- Current installer code only registers a marketplace:
- `src/services/integrations/CodexCliInstaller.ts:188` prints the marketplace root
- `src/services/integrations/CodexCliInstaller.ts:189` runs `codex plugin marketplace add <root>`
- `src/services/integrations/CodexCliInstaller.ts:200` through `src/services/integrations/CodexCliInstaller.ts:203` tells the user to open `/plugins` and install manually
- `src/npx-cli/commands/install.ts:271` through `src/npx-cli/commands/install.ts:281` reports success as "hooks marketplace registered", not "plugin installed"
## Working Theory
There are two independent states:
1. Marketplace source state: current and registered as `claude-mem-local`.
2. Installed plugin cache state: stale and enabled as `claude-mem@thedotmack`.
Codex loads skills, hooks, and MCP metadata from the installed plugin cache, not directly from the marketplace source. Since the installed cache is still `12.3.1`, every new Codex session sees `claude-mem` as `12.3.1`, even though the marketplace clone is already at `12.7.2`.
The `claude-mem@thedotmack` plugin ID also suggests this cache came from an older GitHub marketplace install path, while the current installer registers `claude-mem-local`. That mismatch needs to be handled explicitly during repair and install.
## Phase 0: Reproduce And Baseline
What to do:
- Capture a clean before-state snapshot:
- `codex --version`
- `sed -n '1,220p' ~/.codex/config.toml`
- `find ~/.codex/plugins/cache -maxdepth 5 -type f \( -name 'plugin.json' -o -name 'package.json' -o -name '.mcp.json' -o -name 'codex-hooks.json' \) -print`
- `find ~/.codex/plugins/cache/thedotmack/claude-mem -maxdepth 2 -type d -print`
- Confirm which paths Codex injects into the session skill list:
- Start a fresh Codex session.
- Inspect the available skills list for `claude-mem:` paths.
- Expected current bad path: `~/.codex/plugins/cache/thedotmack/claude-mem/12.3.1/skills`.
Verification:
- The before-state snapshot shows the stale cache and current marketplace clone side by side.
- The fresh session still reports `12.3.1` before remediation.
Anti-pattern guards:
- Do not delete `~/.codex/plugins/cache` blindly.
- Do not edit unrelated `~/.codex/config.toml` project trust settings.
- Do not assume `codex plugin marketplace upgrade` upgrades the installed plugin cache until verified.
## Phase 1: Local Recovery Procedure
What to do:
- Back up the current Codex plugin state:
- `cp ~/.codex/config.toml ~/.codex/config.toml.bak-$(date +%Y%m%d-%H%M%S)`
- Archive or copy `~/.codex/plugins/cache/thedotmack/claude-mem/12.3.1`
- Remove the stale enabled plugin state through supported UI where possible:
- Open Codex.
- Run `/plugins`.
- Disable or uninstall `claude-mem@thedotmack` if it appears.
- Register or refresh the current marketplace:
- `codex plugin marketplace upgrade claude-mem-local`
- If needed, re-add from the durable local marketplace root produced by the installer.
- Install `claude-mem` from the `claude-mem (local)` marketplace in `/plugins`.
- Restart Codex.
Verification:
- `~/.codex/plugins/cache` contains a `claude-mem` cache with `.codex-plugin/plugin.json`.
- The active plugin cache has `version: 12.7.2`.
- The active plugin cache has `hooks/codex-hooks.json`.
- The active plugin cache `.mcp.json` uses the portable `sh -c` wrapper from the current repo.
- A fresh Codex session lists `claude-mem:` skills from the new cache, not `12.3.1`.
Anti-pattern guards:
- Do not manually copy the repository into `~/.codex/plugins/cache` as the primary fix. Use it only as a diagnostic fallback.
- Do not leave both `claude-mem@thedotmack` and a new local `claude-mem` enabled if Codex treats them as distinct plugins.
- Do not accept "marketplace upgraded" as proof. The cache path and loaded skill path are the source of truth.
## Phase 2: Installer Fix
What to implement:
- Change the Codex installer outcome from "registered marketplace" to "registered marketplace and verified installability".
- Add a post-registration diagnostic that checks whether an enabled stale `claude-mem` plugin is already present.
- If a stale cache is detected, print a direct remediation message that names the exact stale cache path and exact `/plugins` action required.
- If Codex exposes an install/enable CLI in a future version, use it. In `0.128.0`, keep the `/plugins` step but verify and report the gap.
Code references:
- `src/services/integrations/CodexCliInstaller.ts:10` for `MARKETPLACE_NAME`.
- `src/services/integrations/CodexCliInstaller.ts:12` through `src/services/integrations/CodexCliInstaller.ts:16` for required marketplace files.
- `src/services/integrations/CodexCliInstaller.ts:175` through `src/services/integrations/CodexCliInstaller.ts:214` for install flow.
- `src/npx-cli/commands/install.ts:269` through `src/npx-cli/commands/install.ts:282` for task status text.
- `tests/install-non-tty.test.ts` for existing installer behavior assertions.
Suggested implementation details:
- Add a `diagnoseCodexPluginState()` helper that reads:
- `~/.codex/config.toml`
- `~/.codex/plugins/cache/**/claude-mem/**/.codex-plugin/plugin.json`
- `~/.codex/plugins/cache/**/claude-mem/**/.claude-plugin/plugin.json`
- `~/.codex/plugins/cache/**/claude-mem/**/.install-version`
- Classify state as:
- `not_installed`
- `installed_current_codex`
- `installed_stale_codex`
- `installed_legacy_claude_shape`
- `duplicate_installs`
- Include current repo/package version in the expected state.
- Treat `installed_legacy_claude_shape` as a warning or failure for Codex integration, because it is the exact observed bad state.
Verification:
- Unit tests cover stale `12.3.1` legacy cache with `.claude-plugin` only.
- Unit tests cover current `12.7.2` first-class cache with `.codex-plugin`.
- Unit tests cover duplicate stale plus current installs.
- Installer output no longer says only "hooks marketplace registered" when the installed plugin cache is stale.
Anti-pattern guards:
- Do not parse TOML with regex if a TOML parser is already available in the dependency set.
- Do not bake in `12.7.2`; read expected version from package metadata.
- Do not rely on `~/.codex/.tmp/marketplaces/...` as proof of plugin installation.
## Phase 3: Repair Command Fix
What to implement:
- Extend `npx claude-mem repair --ide codex-cli` or equivalent repair flow to handle Codex first-class plugin state.
- The repair should:
- Register or upgrade the local marketplace.
- Detect stale enabled `claude-mem@thedotmack`.
- Tell the user whether manual `/plugins` installation is still required.
- Verify the active cache after restart or after the user completes `/plugins`.
Code references:
- `src/npx-cli/commands/install.ts` for marketplace copy and IDE task orchestration.
- `src/services/integrations/CodexCliInstaller.ts` for Codex-specific registration.
- `src/npx-cli/commands/uninstall.ts` for uninstall symmetry.
Verification:
- Repair from a synthetic `12.3.1` legacy cache reports the correct stale-cache diagnosis.
- Repair from a current cache is idempotent.
- Repair does not remove unrelated Codex settings or non-claude-mem plugins.
Anti-pattern guards:
- Do not silently delete old caches without a backup or explicit command mode.
- Do not make repair depend on an interactive TUI if the install command supports non-TTY mode.
## Phase 4: Documentation Fix
What to update:
- Document that Codex currently has two steps:
- Marketplace registration via `npx claude-mem install`.
- Plugin install/enable via `/plugins`.
- Add troubleshooting for this exact mismatch:
- Symptom: Codex skill list shows `~/.codex/plugins/cache/thedotmack/claude-mem/12.3.1`.
- Cause: stale installed plugin cache, despite current marketplace source.
- Fix: uninstall old `claude-mem@thedotmack`, install from `claude-mem (local)`, restart Codex.
Code/doc references:
- `docs/public/installation.mdx`
- `docs/public/troubleshooting.mdx`
- `README.md`
Verification:
- Docs mention the cache path as a diagnostic check.
- Docs do not imply that `codex plugin marketplace add` alone installs the plugin.
## Phase 5: End-To-End Verification
Manual verification checklist:
- Fresh install on a clean Codex profile.
- Upgrade from old `12.3.1` cache.
- Upgrade from current marketplace but stale installed cache.
- Duplicate install case with both `claude-mem@thedotmack` and local `claude-mem`.
Acceptance criteria:
- Fresh Codex session loads `claude-mem:` skills from a current cache path.
- Loaded plugin cache contains `.codex-plugin/plugin.json`.
- Loaded plugin cache contains Codex hooks at the path declared by `.codex-plugin/plugin.json`.
- MCP server starts through the current `.mcp.json` wrapper.
- Installer and repair output make stale-cache state explicit.
## Open Questions
- Does the Codex `/plugins` UI use marketplace name, repository owner, or plugin author to derive the installed plugin cache namespace?
- Does `codex plugin marketplace upgrade claude-mem-local` intentionally avoid updating already-installed plugin caches?
- Is there a hidden or upcoming non-interactive plugin install command that can replace the manual `/plugins` step?
- Should the installer remove or disable `claude-mem@thedotmack` when installing `claude-mem-local`, or should it only warn?
+1 -1
View File
@@ -5,7 +5,7 @@
"command": "sh", "command": "sh",
"args": [ "args": [
"-c", "-c",
"_C=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"; _E=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; _P=$({ [ -n \"$_E\" ] && printf '%s\\n' \"$_E\"; ls -dt \"$_C/plugins/cache/thedotmack/claude-mem\"/[0-9]*/ 2>/dev/null; printf '%s\\n' \"$_C/plugins/marketplaces/thedotmack/plugin\"; } | while IFS= read -r _R; do _R=\"${_R%/}\"; [ -d \"$_R/plugin/scripts\" ] && _Q=\"$_R/plugin\" || _Q=\"$_R\"; [ -f \"$_Q/scripts/mcp-server.cjs\" ] && { printf '%s\\n' \"$_Q\"; break; }; done); [ -n \"$_P\" ] || { echo \"claude-mem: mcp server not found\" >&2; exit 1; }; exec node \"$_P/scripts/mcp-server.cjs\"" "_C=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"; _E=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; _P=$({ [ -n \"$_E\" ] && printf '%s\\n' \"$_E\"; printf '%s\\n' \"$PWD/plugin\" \"$PWD\"; ls -dt \"$HOME/.codex/plugins/cache/claude-mem-local/claude-mem\"/[0-9]*/ \"$HOME/.codex/plugins/cache/thedotmack/claude-mem\"/[0-9]*/ \"$_C/plugins/cache/thedotmack/claude-mem\"/[0-9]*/ 2>/dev/null; printf '%s\\n' \"$_C/plugins/marketplaces/thedotmack/plugin\"; } | while IFS= read -r _R; do _R=\"${_R%/}\"; [ -d \"$_R/plugin/scripts\" ] && _Q=\"$_R/plugin\" || _Q=\"$_R\"; [ -f \"$_Q/scripts/mcp-server.cjs\" ] && { printf '%s\\n' \"$_Q\"; break; }; done); [ -n \"$_P\" ] || { echo \"claude-mem: mcp server not found\" >&2; exit 1; }; exec node \"$_P/scripts/mcp-server.cjs\""
] ]
} }
} }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+17
View File
@@ -384,6 +384,23 @@ 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 rootMcp = JSON.parse(fs.readFileSync('.mcp.json', 'utf-8'));
const bundledMcp = JSON.parse(fs.readFileSync('plugin/.mcp.json', 'utf-8'));
if (JSON.stringify(rootMcp.mcpServers?.['mcp-search']) !== JSON.stringify(bundledMcp.mcpServers?.['mcp-search'])) {
throw new Error('.mcp.json and plugin/.mcp.json mcp-search launchers must stay in sync');
}
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');
}
if (!mcpSearchCommand.includes('plugins/cache/thedotmack/claude-mem')) {
throw new Error('plugin/.mcp.json mcp-search launcher must include Claude 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!');
+101 -7
View File
@@ -4,15 +4,19 @@ import { execFileSync, spawnSync } from 'child_process';
import { existsSync, readFileSync, writeFileSync } from 'fs'; import { existsSync, readFileSync, writeFileSync } from 'fs';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { logger } from '../../utils/logger.js'; import { logger } from '../../utils/logger.js';
import { paths } from '../../shared/paths.js';
const CODEX_DIR = path.join(homedir(), '.codex'); const CODEX_DIR = path.join(homedir(), '.codex');
const CODEX_AGENTS_MD_PATH = path.join(CODEX_DIR, 'AGENTS.md'); const CODEX_AGENTS_MD_PATH = path.join(CODEX_DIR, 'AGENTS.md');
const CODEX_TRANSCRIPT_WATCH_CONFIG_PATH = paths.transcriptsConfig();
const MARKETPLACE_NAME = 'claude-mem-local'; 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 +32,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 +70,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 +98,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;
@@ -172,6 +188,66 @@ function readAndStripContextTags(startTag: string, endTag: string): void {
const cleanupLegacyCodexAgentsMdContext = removeCodexAgentsMdContext; const cleanupLegacyCodexAgentsMdContext = removeCodexAgentsMdContext;
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function isCodexTranscriptWatch(watch: Record<string, unknown>): boolean {
return watch.name === 'codex' || watch.schema === 'codex';
}
function expandHome(inputPath: string): string {
if (inputPath === '~') return homedir();
if (inputPath.startsWith('~/') || inputPath.startsWith('~\\')) {
return path.join(homedir(), inputPath.slice(2));
}
return inputPath;
}
function isLegacyCodexAgentsContext(context: Record<string, unknown>): boolean {
if (context.mode !== 'agents') return false;
const updateOn = context.updateOn;
const hasLegacyUpdateOn = Array.isArray(updateOn)
&& updateOn.length === 2
&& updateOn.includes('session_start')
&& updateOn.includes('session_end');
if (!hasLegacyUpdateOn) return false;
if (context.path === undefined) return true;
return typeof context.path === 'string'
&& path.resolve(expandHome(context.path)) === CODEX_AGENTS_MD_PATH;
}
function disableCodexTranscriptAgentsContext(): boolean {
if (!existsSync(CODEX_TRANSCRIPT_WATCH_CONFIG_PATH)) return true;
try {
const parsed = JSON.parse(readFileSync(CODEX_TRANSCRIPT_WATCH_CONFIG_PATH, 'utf-8')) as unknown;
if (!isRecord(parsed) || !Array.isArray(parsed.watches)) return true;
let changed = false;
for (const watch of parsed.watches) {
if (!isRecord(watch) || !isCodexTranscriptWatch(watch)) continue;
if (!isRecord(watch.context) || !isLegacyCodexAgentsContext(watch.context)) continue;
delete watch.context;
changed = true;
}
if (changed) {
writeFileSync(CODEX_TRANSCRIPT_WATCH_CONFIG_PATH, `${JSON.stringify(parsed, null, 2)}\n`);
console.log(` Disabled legacy Codex transcript AGENTS.md context in ${CODEX_TRANSCRIPT_WATCH_CONFIG_PATH}`);
}
return true;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.warn('WORKER', 'Failed to disable Codex transcript AGENTS.md context', { error: message });
return false;
}
}
const cleanupLegacyCodexTranscriptAgentsContext = disableCodexTranscriptAgentsContext;
export async function installCodexCli(marketplaceRootOverride?: string): Promise<number> { export async function installCodexCli(marketplaceRootOverride?: string): Promise<number> {
console.log('\nInstalling Claude-Mem for Codex CLI (native hooks)...\n'); console.log('\nInstalling Claude-Mem for Codex CLI (native hooks)...\n');
@@ -187,9 +263,22 @@ 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}.`);
} }
if (!cleanupLegacyCodexTranscriptAgentsContext()) {
console.warn(` Native Codex hooks registered, but failed to disable legacy transcript AGENTS.md context in ${CODEX_TRANSCRIPT_WATCH_CONFIG_PATH}.`);
}
console.log(` console.log(`
Installation complete! Installation complete!
@@ -201,6 +290,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
@@ -235,9 +325,13 @@ export function uninstallCodexCli(): number {
console.error(`\nFailed to remove legacy AGENTS.md context from ${CODEX_AGENTS_MD_PATH}.`); console.error(`\nFailed to remove legacy AGENTS.md context from ${CODEX_AGENTS_MD_PATH}.`);
failed = true; failed = true;
} }
if (!cleanupLegacyCodexTranscriptAgentsContext()) {
console.error(`\nFailed to disable legacy transcript AGENTS.md context in ${CODEX_TRANSCRIPT_WATCH_CONFIG_PATH}.`);
failed = true;
}
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(error); const message = error instanceof Error ? error.message : String(error);
console.error(`\nLegacy AGENTS.md cleanup failed: ${message}`); console.error(`\nLegacy context cleanup failed: ${message}`);
failed = true; failed = true;
} }
+27 -10
View File
@@ -47,15 +47,14 @@ const CODEX_SAMPLE_SCHEMA: TranscriptSchema = {
}, },
{ {
name: 'tool-use', name: 'tool-use',
match: { path: 'payload.type', in: ['function_call', 'custom_tool_call', 'web_search_call', 'exec_command'] }, match: { path: 'payload.type', in: ['function_call', 'custom_tool_call', 'web_search_call'] },
action: 'tool_use', action: 'tool_use',
fields: { fields: {
toolId: 'payload.call_id', toolId: 'payload.call_id',
toolName: { toolName: {
coalesce: [ coalesce: [
'payload.name', 'payload.name',
'payload.type', 'payload.type'
{ value: 'web_search' }
] ]
}, },
toolInput: { toolInput: {
@@ -70,16 +69,38 @@ const CODEX_SAMPLE_SCHEMA: TranscriptSchema = {
}, },
{ {
name: 'tool-result', name: 'tool-result',
match: { path: 'payload.type', in: ['function_call_output', 'custom_tool_call_output', 'exec_command_output'] }, match: { path: 'payload.type', in: ['function_call_output', 'custom_tool_call_output'] },
action: 'tool_result', action: 'tool_result',
fields: { fields: {
toolId: 'payload.call_id', toolId: 'payload.call_id',
toolResponse: 'payload.output' toolResponse: 'payload.output'
} }
}, },
{
name: 'exec-command-end',
match: { path: 'payload.type', in: ['exec_command_end', 'exec_command_output'] },
action: 'observation',
fields: {
toolUseId: 'payload.call_id',
toolName: { value: 'exec_command' },
toolInput: {
coalesce: [
'payload.command',
'payload.input'
]
},
toolResponse: {
coalesce: [
'payload.aggregated_output',
'payload.output',
'payload.stdout',
'payload.stderr'
]
}
}
},
{ {
name: 'session-end', name: 'session-end',
// TODO(#2249): delete watcher when Codex hook lifecycle migration ships
match: { path: 'payload.type', in: ['turn_aborted', 'turn_completed', 'task_complete'] }, match: { path: 'payload.type', in: ['turn_aborted', 'turn_completed', 'task_complete'] },
action: 'session_end' action: 'session_end'
} }
@@ -96,11 +117,7 @@ export const SAMPLE_CONFIG: TranscriptWatchConfig = {
name: 'codex', name: 'codex',
path: '~/.codex/sessions/**/*.jsonl', path: '~/.codex/sessions/**/*.jsonl',
schema: 'codex', schema: 'codex',
startAtEnd: true, startAtEnd: true
context: {
mode: 'agents',
updateOn: ['session_start', 'session_end']
}
} }
], ],
stateFile: DEFAULT_STATE_PATH stateFile: DEFAULT_STATE_PATH
@@ -57,8 +57,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) {
@@ -69,6 +73,32 @@ 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('plugins/cache/thedotmack/claude-mem');
expect(command).toContain('claude-mem: mcp server not found');
});
it('keeps root and bundled MCP launchers in sync', () => {
const rootMcp = JSON.parse(readFileSync(path.join(projectRoot, '.mcp.json'), 'utf-8'));
const bundledMcp = JSON.parse(readFileSync(path.join(projectRoot, 'plugin/.mcp.json'), 'utf-8'));
expect(rootMcp.mcpServers['mcp-search']).toEqual(bundledMcp.mcpServers['mcp-search']);
});
});
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');
@@ -148,13 +178,15 @@ describe('Plugin Distribution - Startup Root Resolution', () => {
}); });
describe('Plugin Distribution - package.json Files Field', () => { describe('Plugin Distribution - package.json Files Field', () => {
it('should include plugin distribution files 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/hooks'); expect(packageJson.files).toContain('plugin/.codex-plugin');
expect(packageJson.files).toContain('plugin/.mcp.json'); expect(packageJson.files).toContain('plugin/.mcp.json');
expect(packageJson.files).toContain('plugin/hooks');
expect(packageJson.files).toContain('plugin/skills'); expect(packageJson.files).toContain('plugin/skills');
expect(packageJson.files).toContain('plugin/scripts/*.cjs');
}); });
}); });
+47
View File
@@ -27,6 +27,15 @@ const syncMarketplaceSourcePath = join(
'sync-marketplace.cjs', 'sync-marketplace.cjs',
); );
const syncMarketplaceSource = readFileSync(syncMarketplaceSourcePath, 'utf-8'); const syncMarketplaceSource = readFileSync(syncMarketplaceSourcePath, 'utf-8');
const transcriptConfigSourcePath = join(
__dirname,
'..',
'src',
'services',
'transcripts',
'config.ts',
);
const transcriptConfigSource = readFileSync(transcriptConfigSourcePath, 'utf-8');
describe('Install Non-TTY Support', () => { describe('Install Non-TTY Support', () => {
describe('isInteractive flag', () => { describe('isInteractive flag', () => {
@@ -103,6 +112,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 +132,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'),
@@ -147,7 +181,9 @@ describe('Install Non-TTY Support', () => {
it('reports legacy Codex AGENTS cleanup failures to callers', () => { it('reports legacy Codex AGENTS cleanup failures to callers', () => {
expect(codexInstallerSource).toContain('function removeCodexAgentsMdContext(): boolean'); expect(codexInstallerSource).toContain('function removeCodexAgentsMdContext(): boolean');
expect(codexInstallerSource).toContain('function disableCodexTranscriptAgentsContext(): boolean');
expect(codexInstallerSource).toContain('if (!cleanupLegacyCodexAgentsMdContext())'); expect(codexInstallerSource).toContain('if (!cleanupLegacyCodexAgentsMdContext())');
expect(codexInstallerSource).toContain('if (!cleanupLegacyCodexTranscriptAgentsContext())');
}); });
it('does not fail Codex install after marketplace registration when only AGENTS cleanup fails', () => { it('does not fail Codex install after marketplace registration when only AGENTS cleanup fails', () => {
@@ -162,6 +198,17 @@ describe('Install Non-TTY Support', () => {
expect(cleanupFailureRegion).toContain('console.warn'); expect(cleanupFailureRegion).toContain('console.warn');
expect(cleanupFailureRegion).not.toContain('return 1'); expect(cleanupFailureRegion).not.toContain('return 1');
}); });
it('does not seed new Codex transcript watcher configs with AGENTS context injection', () => {
expect(transcriptConfigSource).toContain("name: 'codex'");
const codexWatchRegion = transcriptConfigSource.slice(
transcriptConfigSource.indexOf("name: 'codex'"),
transcriptConfigSource.indexOf('stateFile: DEFAULT_STATE_PATH'),
);
expect(codexWatchRegion).toContain("path: '~/.codex/sessions/**/*.jsonl'");
expect(codexWatchRegion).not.toContain("mode: 'agents'");
expect(codexWatchRegion).not.toContain('updateOn');
});
}); });
describe('TaskDescriptor interface', () => { describe('TaskDescriptor interface', () => {
+28 -9
View File
@@ -3,7 +3,7 @@
"schemas": { "schemas": {
"codex": { "codex": {
"name": "codex", "name": "codex",
"version": "0.2", "version": "0.3",
"description": "Schema for Codex session JSONL files under ~/.codex/sessions.", "description": "Schema for Codex session JSONL files under ~/.codex/sessions.",
"events": [ "events": [
{ {
@@ -48,18 +48,42 @@
"toolName": { "toolName": {
"coalesce": [ "coalesce": [
"payload.name", "payload.name",
{ "value": "web_search" } "payload.type"
] ]
}, },
"toolInput": { "toolInput": {
"coalesce": [ "coalesce": [
"payload.arguments", "payload.arguments",
"payload.input", "payload.input",
"payload.command",
"payload.action" "payload.action"
] ]
} }
} }
}, },
{
"name": "exec-command-end",
"match": { "path": "payload.type", "in": ["exec_command_end", "exec_command_output"] },
"action": "observation",
"fields": {
"toolUseId": "payload.call_id",
"toolName": { "value": "exec_command" },
"toolInput": {
"coalesce": [
"payload.command",
"payload.input"
]
},
"toolResponse": {
"coalesce": [
"payload.aggregated_output",
"payload.output",
"payload.stdout",
"payload.stderr"
]
}
}
},
{ {
"name": "tool-result", "name": "tool-result",
"match": { "path": "payload.type", "in": ["function_call_output", "custom_tool_call_output"] }, "match": { "path": "payload.type", "in": ["function_call_output", "custom_tool_call_output"] },
@@ -71,7 +95,7 @@
}, },
{ {
"name": "session-end", "name": "session-end",
"match": { "path": "payload.type", "equals": "turn_aborted" }, "match": { "path": "payload.type", "in": ["turn_aborted", "turn_completed", "task_complete"] },
"action": "session_end" "action": "session_end"
} }
] ]
@@ -82,12 +106,7 @@
"name": "codex", "name": "codex",
"path": "~/.codex/sessions/**/*.jsonl", "path": "~/.codex/sessions/**/*.jsonl",
"schema": "codex", "schema": "codex",
"startAtEnd": true, "startAtEnd": true
"context": {
"mode": "agents",
"path": "~/.codex/AGENTS.md",
"updateOn": ["session_start", "session_end"]
}
} }
], ],
"stateFile": "~/.claude-mem/transcript-watch-state.json" "stateFile": "~/.claude-mem/transcript-watch-state.json"