feat(plugin): ship Codex plugin manifest + marketplace (#311)
* feat(plugin): ship Codex plugin manifest + marketplace (#codex-plugins)
OpenAI Codex shipped a plugin platform
(developers.openai.com/codex/plugins) with the same shape as Anthropic
Claude Code plugins: .codex-plugin/plugin.json manifest, optional
.mcp.json, hooks/hooks.json, and a skills/ directory.
Verified against openai/codex source
(codex-rs/hooks/src/engine/discovery.rs): Codex's hook engine
explicitly injects CLAUDE_PLUGIN_ROOT into hook subprocesses for
OOTB compat with existing Claude Code plugins. The wire-format
input schemas
(codex-rs/hooks/schema/generated/session-start.command.input.schema.json
and siblings) use the same field names as Claude Code
(session_id, cwd, hook_event_name, source, transcript_path, model,
permission_mode).
Net result: our existing plugin/ directory already works as a Codex
plugin with two small additions:
1. plugin/.codex-plugin/plugin.json — Codex manifest pointing at the
shared ./.mcp.json, ./skills/, and a Codex-specific ./hooks/
hooks.codex.json. Name (kebab-case), version, description match
the Claude Code manifest.
2. plugin/hooks/hooks.codex.json — Codex-compatible hook subset.
Drops SubagentStart, SubagentStop, SessionEnd, Notification,
TaskCompleted, PostToolUseFailure (Claude-Code-only). Keeps
SessionStart, UserPromptSubmit, PreToolUse, PostToolUse,
PreCompact, Stop. Adds Codex-specific `statusMessage` decorations
on the two hooks the user sees most (SessionStart + UserPromptSubmit).
The script commands themselves still reference ${CLAUDE_PLUGIN_ROOT}
because Codex's engine injects it.
3. .codex-plugin/marketplace.json at repo root — git-subdir source
pointing at ./plugin so `codex plugin marketplace add rohitg00/
agentmemory` works the same way `claude plugin marketplace add`
does. Mirrors the existing .claude-plugin/marketplace.json shape
but in Codex's marketplace schema (name + interface.displayName
+ plugins[].source.{source, url, path, ref} + plugins[].policy).
README updates:
- Codex CLI tile in the Works-with-every-agent grid now reads
"6 hooks + MCP + skills" instead of "MCP server" to surface the
upgraded surface.
- "Other agents" install table now distinguishes "Codex CLI (MCP
only)" (existing codex mcp add path) from "Codex CLI (full
plugin)" (new marketplace install).
- New Codex section in the "paste this prompt" block with the
two-command install (server + marketplace add + plugin install),
the list of registered surfaces, and the env-var-injection note
citing the Codex source line.
6 new tests in test/codex-plugin.test.ts cover:
1. .codex-plugin/plugin.json present with kebab-case name and
required references.
2. Manifest version matches main package.json (so future bumps
don't drift the Codex side).
3. Every path referenced in the manifest resolves to a real file or
directory on disk.
4. hooks.codex.json contains only events Codex supports — any
future addition has to be added to a Codex allowlist or the
test fails loudly.
5. Every ${CLAUDE_PLUGIN_ROOT}/scripts/* command references an
existing script file (catches typos before users hit them).
6. .codex-plugin/marketplace.json declares git-subdir source with
path: "./plugin" pointing at this repo.
874 / 874 tests pass.
* fix(plugin/codex): align manifest description + assert PreCompact hook
Two reviewer findings addressed:
1. plugin/.codex-plugin/plugin.json's description claimed "12 hooks"
but Codex only registers 6 (the Codex hook input schemas don't
define SubagentStart, SubagentStop, SessionEnd, Notification,
TaskCompleted, PostToolUseFailure). Updated the description to
"6 hooks, 51 MCP tools, 4 skills, real-time viewer." so the
marketplace listing matches the registered surface.
2. test/codex-plugin.test.ts asserted five lifecycle events
(SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop)
but omitted PreCompact, leaving a regression hole: dropping
PreCompact from hooks.codex.json would still pass the test
because the allowlist loop only blocks UNKNOWN events. Added
expect(events).toContain("PreCompact") so the suite enforces
the full six-hook contract.
Skipped (with reason):
3. marketplace.json source.ref currently "main" — reviewer asked
for a fixed tag/SHA. Skipping because pinning to a tag forces
updating marketplace.json on every release; "main" matches our
convention where releases tag from main HEAD and pre-release
work is on feature branches. Users wanting an immutable cut
can pass --ref vX.Y.Z to `codex plugin marketplace add` per
Codex docs.
Validation: 6/6 codex tests pass; 874/874 full suite unchanged.
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "agentmemory",
|
||||
"interface": {
|
||||
"displayName": "agentmemory"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "agentmemory",
|
||||
"source": {
|
||||
"source": "git-subdir",
|
||||
"url": "https://github.com/rohitg00/agentmemory.git",
|
||||
"path": "./plugin",
|
||||
"ref": "main"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Memory"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -107,7 +107,7 @@ agentmemory works with any agent that supports hooks, MCP, or REST API. All agen
|
||||
<td align="center" width="12.5%">
|
||||
<a href="https://github.com/openai/codex"><img src="https://github.com/openai.png?size=120" alt="Codex CLI" width="48" height="48" /></a><br/>
|
||||
<strong>Codex CLI</strong><br/>
|
||||
<sub>MCP server</sub>
|
||||
<sub>6 hooks + MCP + skills</sub>
|
||||
</td>
|
||||
<td align="center" width="12.5%">
|
||||
<a href="https://github.com/cline/cline"><img src="https://github.com/cline.png?size=120" alt="Cline" width="48" height="48" /></a><br/>
|
||||
@@ -355,6 +355,25 @@ Implementation details live in `src/cli.ts` (see `runUpgrade` around the `src/cl
|
||||
Install agentmemory: run `npx @agentmemory/agentmemory` in a separate terminal to start the memory server. Then run `/plugin marketplace add rohitg00/agentmemory` and `/plugin install agentmemory` — the plugin registers all 12 hooks, 4 skills, AND auto-wires the `@agentmemory/mcp` stdio server via its `.mcp.json`, so you get 51 MCP tools (memory_smart_search, memory_save, memory_sessions, memory_governance_delete, etc.) without any extra config step. Verify with `curl http://localhost:3111/agentmemory/health`. The real-time viewer is at http://localhost:3113.
|
||||
```
|
||||
|
||||
### Codex CLI (Codex plugin platform)
|
||||
|
||||
```bash
|
||||
# 1. start the memory server in a separate terminal
|
||||
npx @agentmemory/agentmemory
|
||||
|
||||
# 2. register the agentmemory marketplace and install the plugin
|
||||
codex plugin marketplace add rohitg00/agentmemory
|
||||
codex plugin install agentmemory
|
||||
```
|
||||
|
||||
The Codex plugin ships from the same `plugin/` directory as the Claude Code plugin. It registers:
|
||||
|
||||
- `@agentmemory/mcp` as an MCP server (all 51 tools when `AGENTMEMORY_TOOLS=all`)
|
||||
- 6 lifecycle hooks: `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PreCompact`, `Stop`
|
||||
- 4 skills: `/recall`, `/remember`, `/session-history`, `/forget`
|
||||
|
||||
Codex's hook engine injects `CLAUDE_PLUGIN_ROOT` into hook subprocesses (per [`codex-rs/hooks/src/engine/discovery.rs`](https://github.com/openai/codex/blob/main/codex-rs/hooks/src/engine/discovery.rs)), so the same hook scripts work across both hosts without duplication. Subagent / SessionEnd / Notification / TaskCompleted / PostToolUseFailure events are Claude-Code-only and are not registered for Codex.
|
||||
|
||||
<details>
|
||||
<summary><b>OpenClaw (paste this prompt)</b></summary>
|
||||
|
||||
@@ -427,7 +446,8 @@ The agentmemory entry is the **same MCP server block** across every host that us
|
||||
| **Windsurf** | `~/.codeium/windsurf/mcp_config.json` | Same `mcpServers` block. |
|
||||
| **Gemini CLI** | `~/.gemini/settings.json` | `gemini mcp add agentmemory npx -y @agentmemory/mcp --scope user` (auto-merges). |
|
||||
| **OpenClaw** | OpenClaw MCP config | Same `mcpServers` block, or use the deeper [memory plugin](integrations/openclaw/). |
|
||||
| **Codex CLI** | `.codex/config.toml` | TOML shape: `codex mcp add agentmemory -- npx -y @agentmemory/mcp`, or add `[mcp_servers.agentmemory]` manually. |
|
||||
| **Codex CLI (MCP only)** | `.codex/config.toml` | TOML shape: `codex mcp add agentmemory -- npx -y @agentmemory/mcp`, or add `[mcp_servers.agentmemory]` manually. |
|
||||
| **Codex CLI (full plugin)** | Codex plugin marketplace | `codex plugin marketplace add rohitg00/agentmemory` then `codex plugin install agentmemory`. Registers MCP + 6 lifecycle hooks (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PreCompact, Stop) + 4 skills. |
|
||||
| **OpenCode** | `opencode.json` | Different shape — top-level `mcp` key, command as array: `{"mcp": {"agentmemory": {"type": "local", "command": ["npx", "-y", "@agentmemory/mcp"], "enabled": true}}}`. |
|
||||
| **pi** | `~/.pi/agent/extensions/agentmemory` | Copy [`integrations/pi`](integrations/pi/) and restart pi. |
|
||||
| **Hermes Agent** | `~/.hermes/config.yaml` | Use the deeper [memory provider plugin](integrations/hermes/) with `memory.provider: agentmemory`. |
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "agentmemory",
|
||||
"version": "0.9.10",
|
||||
"description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 6 hooks, 51 MCP tools, 4 skills, real-time viewer.",
|
||||
"author": {
|
||||
"name": "Rohit Ghumare",
|
||||
"url": "https://github.com/rohitg00"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://github.com/rohitg00/agentmemory",
|
||||
"repository": "https://github.com/rohitg00/agentmemory",
|
||||
"skills": "./skills/",
|
||||
"mcpServers": "./.mcp.json",
|
||||
"hooks": "./hooks/hooks.codex.json"
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/session-start.mjs",
|
||||
"statusMessage": "agentmemory: loading session context"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/prompt-submit.mjs",
|
||||
"statusMessage": "agentmemory: recalling relevant memories"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write|Read|Glob|Grep",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/pre-tool-use.mjs"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/post-tool-use.mjs"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreCompact": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/pre-compact.mjs"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/stop.mjs"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
const repoRoot = resolve(__dirname, "..");
|
||||
const pluginRoot = join(repoRoot, "plugin");
|
||||
|
||||
function readJson<T = unknown>(path: string): T {
|
||||
return JSON.parse(readFileSync(path, "utf-8")) as T;
|
||||
}
|
||||
|
||||
describe("Codex plugin manifest (developers.openai.com/codex/plugins)", () => {
|
||||
it("ships .codex-plugin/plugin.json with kebab-case name + version + references", () => {
|
||||
const manifestPath = join(pluginRoot, ".codex-plugin/plugin.json");
|
||||
expect(existsSync(manifestPath)).toBe(true);
|
||||
const manifest = readJson<{
|
||||
name: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
skills?: string;
|
||||
mcpServers?: string;
|
||||
hooks?: string;
|
||||
}>(manifestPath);
|
||||
expect(manifest.name).toBe("agentmemory");
|
||||
expect(manifest.name).toMatch(/^[a-z][a-z0-9-]*$/);
|
||||
expect(manifest.version).toMatch(/^\d+\.\d+\.\d+/);
|
||||
expect(manifest.skills).toBeDefined();
|
||||
expect(manifest.mcpServers).toBeDefined();
|
||||
expect(manifest.hooks).toBeDefined();
|
||||
});
|
||||
|
||||
it("manifest version matches main package.json", () => {
|
||||
const pkgVer = readJson<{ version: string }>(join(repoRoot, "package.json")).version;
|
||||
const codexVer = readJson<{ version: string }>(
|
||||
join(pluginRoot, ".codex-plugin/plugin.json"),
|
||||
).version;
|
||||
expect(codexVer).toBe(pkgVer);
|
||||
});
|
||||
|
||||
it("all referenced manifest paths resolve to existing files / directories", () => {
|
||||
const manifest = readJson<{ skills: string; mcpServers: string; hooks: string }>(
|
||||
join(pluginRoot, ".codex-plugin/plugin.json"),
|
||||
);
|
||||
expect(existsSync(join(pluginRoot, manifest.skills))).toBe(true);
|
||||
expect(existsSync(join(pluginRoot, manifest.mcpServers))).toBe(true);
|
||||
expect(existsSync(join(pluginRoot, manifest.hooks))).toBe(true);
|
||||
});
|
||||
|
||||
it("hooks.codex.json contains only events Codex supports (no Subagent / SessionEnd / Notification / TaskCompleted / PostToolUseFailure)", () => {
|
||||
const hooksPath = join(pluginRoot, "hooks/hooks.codex.json");
|
||||
const hooks = readJson<{ hooks: Record<string, unknown> }>(hooksPath);
|
||||
const events = Object.keys(hooks.hooks);
|
||||
const codexSupported = new Set([
|
||||
"SessionStart",
|
||||
"UserPromptSubmit",
|
||||
"PreToolUse",
|
||||
"PostToolUse",
|
||||
"PermissionRequest",
|
||||
"PreCompact",
|
||||
"PostCompact",
|
||||
"Stop",
|
||||
]);
|
||||
for (const event of events) {
|
||||
expect(codexSupported.has(event), `unexpected event "${event}" in hooks.codex.json`).toBe(true);
|
||||
}
|
||||
expect(events).toContain("SessionStart");
|
||||
expect(events).toContain("UserPromptSubmit");
|
||||
expect(events).toContain("PreToolUse");
|
||||
expect(events).toContain("PostToolUse");
|
||||
expect(events).toContain("PreCompact");
|
||||
expect(events).toContain("Stop");
|
||||
});
|
||||
|
||||
it("hook command scripts referenced in hooks.codex.json exist on disk", () => {
|
||||
type HookHandler = { type: string; command: string };
|
||||
type HookEntry = { hooks: HookHandler[] };
|
||||
const hooks = readJson<{ hooks: Record<string, HookEntry[]> }>(
|
||||
join(pluginRoot, "hooks/hooks.codex.json"),
|
||||
);
|
||||
const scriptRefs = new Set<string>();
|
||||
for (const entries of Object.values(hooks.hooks)) {
|
||||
for (const entry of entries) {
|
||||
for (const handler of entry.hooks) {
|
||||
const match = handler.command.match(/\$\{CLAUDE_PLUGIN_ROOT\}\/(scripts\/[^\s]+)/);
|
||||
if (match) scriptRefs.add(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(scriptRefs.size).toBeGreaterThan(0);
|
||||
for (const rel of scriptRefs) {
|
||||
expect(existsSync(join(pluginRoot, rel)), `missing hook script: ${rel}`).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Codex marketplace.json (.codex-plugin/marketplace.json at repo root)", () => {
|
||||
it("ships a marketplace manifest pointing at the plugin/ subdirectory", () => {
|
||||
const marketplacePath = join(repoRoot, ".codex-plugin/marketplace.json");
|
||||
expect(existsSync(marketplacePath)).toBe(true);
|
||||
const marketplace = readJson<{
|
||||
name: string;
|
||||
plugins: Array<{
|
||||
name: string;
|
||||
source: { source: string; url: string; path: string; ref?: string };
|
||||
}>;
|
||||
}>(marketplacePath);
|
||||
expect(marketplace.name).toBe("agentmemory");
|
||||
expect(marketplace.plugins).toHaveLength(1);
|
||||
const entry = marketplace.plugins[0];
|
||||
expect(entry.name).toBe("agentmemory");
|
||||
expect(entry.source.source).toBe("git-subdir");
|
||||
expect(entry.source.path).toBe("./plugin");
|
||||
expect(entry.source.url).toMatch(/rohitg00\/agentmemory/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user