Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugin/skills/agentmemory-agents/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Generated from `src/cli/connect/index.ts`. Do not edit the block below by hand;
| Antigravity | `antigravity` | Using MCP via mcp_config.json. Antigravity replaces Gemini CLI (sunset 2026-06-18). |
| Claude Code | `claude-code` | Using MCP. Hooks are also available, see https://github.com/rohitg00/agentmemory#claude-code-one-block-paste-it. |
| Cline | `cline` | Using MCP via ~/.cline/mcp.json (CLI). VS Code users: add the same block via Cline Settings → MCP Servers → Edit JSON. |
| Codex CLI | `codex` | Using MCP. Hooks ship via the Codex plugin; on Codex Desktop, also pass --with-hooks to install the global hooks.json workaround for openai/codex#16430. |
| Codex CLI | `codex` | Using MCP. Hooks ship via the Codex plugin; on Codex Desktop, also pass --with-hooks to install (or refresh) the global ~/.codex/hooks.json workaround for openai/codex#16430, works even when MCP is already wired. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Regenerate this documentation block.

Line 3 requires generated output. This row differs from src/cli/connect/codex.ts Line 69. Update the source text, run npm run skills:gen, and commit its output.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/skills/agentmemory-agents/REFERENCE.md` at line 13, Update the Codex
CLI source text corresponding to the documentation row so it matches the
expected behavior in src/cli/connect/codex.ts, then run npm run skills:gen to
regenerate the documentation block and include the generated output.

| Continue | `continue` | Using MCP via ~/.continue/config.yaml (preferred) or config.json (legacy, only when no yaml). |
| GitHub Copilot CLI | `copilot-cli` | Using MCP. Install the plugin too for full hooks/skills coverage. |
| Cursor | `cursor` | Using MCP (the only protocol Cursor speaks). Memory bridge runs at :3111 underneath. |
Expand Down
2 changes: 1 addition & 1 deletion plugin/skills/agentmemory-rest-api/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Generated from `src/triggers/api.ts`. Do not edit the block below by hand; run `
<!-- AUTOGEN:rest START - generated by scripts/skills/generate.ts, do not edit by hand -->
The REST API is the primary surface. All paths are under `http://localhost:3111` (override with `--port`). When `AGENTMEMORY_SECRET` is set, send `Authorization: Bearer $AGENTMEMORY_SECRET`; localhost is otherwise open.

118 registered endpoints:
119 registered endpoints:

| Method | Path |
| --- | --- |
Expand Down
15 changes: 13 additions & 2 deletions src/cli/connect/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const adapter: ConnectAdapter = {
category: "native",
docs: "https://github.com/rohitg00/agentmemory#codex-cli-codex-plugin-platform",
protocolNote:
"→ Using MCP. Hooks ship via the Codex plugin; on Codex Desktop, also pass --with-hooks to install the global hooks.json workaround for openai/codex#16430.",
"→ Using MCP. Hooks ship via the Codex plugin; on Codex Desktop, also pass --with-hooks to install (or refresh) the global ~/.codex/hooks.json workaround for openai/codex#16430 — works even when MCP is already wired.",

detect(): boolean {
return existsSync(CODEX_DIR);
Expand All @@ -79,14 +79,25 @@ export const adapter: ConnectAdapter = {

if (wired && !opts.force) {
logAlreadyWired("Codex CLI", CODEX_TOML);
if (opts.withHooks) {
const hookResult = installCodexHooks(opts);
if (hookResult.kind === "skipped") {
p.log.warn(`Codex hooks fallback skipped: ${hookResult.reason}.`);
}
}
return { kind: "already-wired", mutatedPath: CODEX_TOML };
}

if (opts.dryRun) {
p.log.info(
`[dry-run] Would ${wired ? "rewrite" : "append"} [mcp_servers.agentmemory] in ${CODEX_TOML}`,
);
if (opts.withHooks) installCodexHooks(opts);
if (opts.withHooks) {
const hookResult = installCodexHooks(opts);
if (hookResult.kind === "skipped") {
p.log.warn(`Codex hooks fallback skipped: ${hookResult.reason}.`);
}
}
return { kind: "installed", mutatedPath: CODEX_TOML };
}

Expand Down
150 changes: 148 additions & 2 deletions test/codex-connect-hooks.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { describe, it, expect } from "vitest";
import { writeFileSync, readFileSync, mkdirSync, rmSync } from "node:fs";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import {
writeFileSync,
readFileSync,
mkdirSync,
rmSync,
mkdtempSync,
existsSync,
} from "node:fs";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";
import {
Expand All @@ -10,6 +17,27 @@ import {

const PLUGIN_ROOT = resolve(__dirname, "..", "plugin");

const CODEX_TOML_BLOCK = `[mcp_servers.agentmemory]
command = "npx"
args = ["-y", "@agentmemory/mcp"]

[mcp_servers.agentmemory.env]
AGENTMEMORY_URL = "http://localhost:3111"
`;

const CODEX_EVENTS = [
"SessionStart",
"UserPromptSubmit",
"PreToolUse",
"PostToolUse",
"PreCompact",
"Stop",
] as const;

function freshHome(): string {
return mkdtempSync(join(tmpdir(), "am-codex-connect-"));
}

describe("findPluginRoot", () => {
it("locates the bundled plugin/ directory from src/cli/connect/", () => {
const root = findPluginRoot();
Expand Down Expand Up @@ -135,3 +163,121 @@ describe("buildMergedHooks file round-trip", () => {
}
});
});

describe("connect: Codex --with-hooks", () => {
let home: string;
const ORIG = process.env["HOME"];
const ORIG_USERPROFILE = process.env["USERPROFILE"];

beforeEach(() => {
home = freshHome();
vi.resetModules();
process.env["HOME"] = home;
// os.homedir() on win32 reads USERPROFILE, not HOME — without this,
// adapter.detect()/install() resolve the real user's home directory
// instead of the isolated temp one.
process.env["USERPROFILE"] = home;
});

afterEach(() => {
if (ORIG === undefined) delete process.env["HOME"];
else process.env["HOME"] = ORIG;
if (ORIG_USERPROFILE === undefined) delete process.env["USERPROFILE"];
else process.env["USERPROFILE"] = ORIG_USERPROFILE;
rmSync(home, { recursive: true, force: true });
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it("--with-hooks writes ~/.codex/hooks.json with Codex's six lifecycle events", async () => {
mkdirSync(join(home, ".codex"), { recursive: true });
const { adapter } = await import("../src/cli/connect/codex.js");
const result = await adapter.install({
dryRun: false,
force: false,
withHooks: true,
});
expect(result.kind).toBe("installed");
const hooksPath = join(home, ".codex", "hooks.json");
expect(existsSync(hooksPath)).toBe(true);
const hooks = JSON.parse(readFileSync(hooksPath, "utf-8")) as HookManifest;
expect(Object.keys(hooks.hooks).sort()).toEqual([...CODEX_EVENTS].sort());
});

it("without --with-hooks, does not write ~/.codex/hooks.json", async () => {
mkdirSync(join(home, ".codex"), { recursive: true });
const { adapter } = await import("../src/cli/connect/codex.js");
await adapter.install({ dryRun: false, force: false });
expect(existsSync(join(home, ".codex", "hooks.json"))).toBe(false);
});

it("re-running install with --with-hooks on an already-wired MCP config still refreshes hooks.json", async () => {
const codexDir = join(home, ".codex");
mkdirSync(codexDir, { recursive: true });
writeFileSync(join(codexDir, "config.toml"), CODEX_TOML_BLOCK, "utf-8");

const { adapter } = await import("../src/cli/connect/codex.js");
const result = await adapter.install({
dryRun: false,
force: false,
withHooks: true,
});
expect(result.kind).toBe("already-wired");

const hooksPath = join(codexDir, "hooks.json");
expect(existsSync(hooksPath)).toBe(true);
const hooks = JSON.parse(readFileSync(hooksPath, "utf-8")) as HookManifest;
expect(Object.keys(hooks.hooks).sort()).toEqual([...CODEX_EVENTS].sort());
});

it("already-wired without --with-hooks does not write hooks.json", async () => {
const codexDir = join(home, ".codex");
mkdirSync(codexDir, { recursive: true });
writeFileSync(join(codexDir, "config.toml"), CODEX_TOML_BLOCK, "utf-8");

const { adapter } = await import("../src/cli/connect/codex.js");
const result = await adapter.install({ dryRun: false, force: false });
expect(result.kind).toBe("already-wired");
expect(existsSync(join(codexDir, "hooks.json"))).toBe(false);
});

it("re-install with --with-hooks is idempotent and preserves user hook entries", async () => {
const codexDir = join(home, ".codex");
mkdirSync(codexDir, { recursive: true });
writeFileSync(join(codexDir, "config.toml"), CODEX_TOML_BLOCK, "utf-8");
writeFileSync(
join(codexDir, "hooks.json"),
`${JSON.stringify(
{
hooks: {
SessionStart: [
{ hooks: [{ type: "command", command: "echo user-custom" }] },
],
},
},
null,
2,
)}\n`,
"utf-8",
);

const { adapter } = await import("../src/cli/connect/codex.js");
await adapter.install({ dryRun: false, force: false, withHooks: true });
await adapter.install({ dryRun: false, force: false, withHooks: true });

const hooks = JSON.parse(
readFileSync(join(codexDir, "hooks.json"), "utf-8"),
) as HookManifest;
const sessionStart = hooks.hooks["SessionStart"]!;
expect(
sessionStart.some((e) =>
e.hooks.some((h) => h.command === "echo user-custom"),
),
).toBe(true);
expect(
sessionStart.filter((e) =>
e.hooks.some((h) =>
h.command.replace(/\\/g, "/").includes("/scripts/session-start.mjs"),
),
),
).toHaveLength(1);
});
});
Loading