555e282cc4
pi-agent-plugin checks / lint (push) Has been cancelled
pi-agent-plugin checks / test (20) (push) Has been cancelled
pi-agent-plugin checks / test (22) (push) Has been cancelled
pi-agent-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / check_changes (push) Has been cancelled
TypeScript SDK CI / changelog_check (push) Has been cancelled
ci / changelog_check (push) Has been cancelled
ci / check_changes (push) Has been cancelled
ci / build_mem0 (3.10) (push) Has been cancelled
ci / build_mem0 (3.11) (push) Has been cancelled
ci / build_mem0 (3.12) (push) Has been cancelled
CLI Node CI / lint (push) Has been cancelled
CLI Node CI / test (20) (push) Has been cancelled
CLI Node CI / test (22) (push) Has been cancelled
CLI Node CI / build (push) Has been cancelled
CLI Python CI / lint (push) Has been cancelled
CLI Python CI / test (3.10) (push) Has been cancelled
CLI Python CI / test (3.11) (push) Has been cancelled
CLI Python CI / test (3.12) (push) Has been cancelled
CLI Python CI / build (push) Has been cancelled
openclaw checks / lint (push) Has been cancelled
openclaw checks / test (20) (push) Has been cancelled
openclaw checks / test (22) (push) Has been cancelled
openclaw checks / build (push) Has been cancelled
opencode-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (22) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (22) (push) Has been cancelled
163 lines
4.9 KiB
Python
Executable File
163 lines
4.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Install Mem0 lifecycle hooks into ~/.codex/hooks.json.
|
|
|
|
Codex discovers hooks only at ~/.codex/hooks.json or <repo>/.codex/hooks.json,
|
|
and has no plugin-host mechanism for auto-wiring hooks from an installed
|
|
plugin. This installer reads the template at hooks/codex-hooks.json, rewrites
|
|
the ${PLUGIN_ROOT} placeholder to the absolute install path of this
|
|
plugin, then merges the entries into ~/.codex/hooks.json.
|
|
|
|
Re-running is idempotent: existing Mem0 entries (identified by the plugin
|
|
directory name in the command string) are removed before fresh entries are
|
|
added, so upgrades don't leave duplicates.
|
|
|
|
Usage:
|
|
python3 install_codex_hooks.py # install or update
|
|
python3 install_codex_hooks.py --uninstall # remove Mem0 entries
|
|
|
|
After installing, Codex requires the hooks feature flag in ~/.codex/config.toml:
|
|
|
|
[features]
|
|
codex_hooks = true
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import platform
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
PLUGIN_ROOT = SCRIPT_DIR.parent
|
|
|
|
CODEX_DIR = Path.home() / ".codex"
|
|
HOOKS_FILE = CODEX_DIR / "hooks.json"
|
|
CONFIG_FILE = CODEX_DIR / "config.toml"
|
|
|
|
TEMPLATE_FILE = PLUGIN_ROOT / "hooks" / "codex-hooks.json"
|
|
|
|
# Substring we look for when identifying entries this installer owns.
|
|
# Matches the plugin directory name, which stays stable across install paths.
|
|
OWNER_MARKER = "mem0-plugin"
|
|
|
|
|
|
def load_template() -> dict:
|
|
raw = TEMPLATE_FILE.read_text()
|
|
raw = raw.replace("${PLUGIN_ROOT}", str(PLUGIN_ROOT))
|
|
return json.loads(raw)
|
|
|
|
|
|
def load_existing() -> dict:
|
|
if not HOOKS_FILE.exists():
|
|
return {"hooks": {}}
|
|
try:
|
|
return json.loads(HOOKS_FILE.read_text())
|
|
except (json.JSONDecodeError, OSError) as e:
|
|
print(f"error: failed to read {HOOKS_FILE}: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def is_owned_entry(entry: dict) -> bool:
|
|
for hook in entry.get("hooks", []):
|
|
if OWNER_MARKER in hook.get("command", ""):
|
|
return True
|
|
return False
|
|
|
|
|
|
def strip_owned_entries(config: dict) -> dict:
|
|
hooks = config.get("hooks", {}) or {}
|
|
for event in list(hooks.keys()):
|
|
hooks[event] = [e for e in hooks[event] if not is_owned_entry(e)]
|
|
if not hooks[event]:
|
|
del hooks[event]
|
|
config["hooks"] = hooks
|
|
return config
|
|
|
|
|
|
def merge_template(config: dict, template: dict) -> dict:
|
|
hooks = config.setdefault("hooks", {})
|
|
for event, entries in template.get("hooks", {}).items():
|
|
hooks.setdefault(event, []).extend(entries)
|
|
return config
|
|
|
|
|
|
def write_config(config: dict) -> None:
|
|
CODEX_DIR.mkdir(parents=True, exist_ok=True)
|
|
HOOKS_FILE.write_text(json.dumps(config, indent=2) + "\n")
|
|
|
|
|
|
def feature_flag_enabled() -> bool:
|
|
if not CONFIG_FILE.exists():
|
|
return False
|
|
content = CONFIG_FILE.read_text()
|
|
for line in content.splitlines():
|
|
stripped = line.split("#", 1)[0].strip().replace(" ", "")
|
|
if stripped == "codex_hooks=true":
|
|
return True
|
|
return False
|
|
|
|
|
|
def print_feature_flag_hint() -> None:
|
|
print()
|
|
print("Codex hooks feature flag is not enabled.")
|
|
print(f"Add this to {CONFIG_FILE}:")
|
|
print()
|
|
print(" [features]")
|
|
print(" codex_hooks = true")
|
|
print()
|
|
print("Then restart Codex.")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Install or remove Mem0 Codex hooks.")
|
|
parser.add_argument(
|
|
"--uninstall",
|
|
action="store_true",
|
|
help="Remove Mem0 entries from ~/.codex/hooks.json and exit.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
config = load_existing()
|
|
|
|
if args.uninstall:
|
|
config = strip_owned_entries(config)
|
|
write_config(config)
|
|
print(f"Removed Mem0 hooks from {HOOKS_FILE}")
|
|
return 0
|
|
|
|
# Codex lifecycle hooks register .sh paths directly in ~/.codex/hooks.json.
|
|
# On native Windows .sh has no default handler, so Codex spawning a hook
|
|
# triggers "Open With" dialogs (one OpenWith.exe per event). See #5243.
|
|
if platform.system() == "Windows":
|
|
print(
|
|
"Codex lifecycle hooks register .sh scripts directly, which Windows\n"
|
|
"cannot execute without a bash interpreter on PATH. Re-run this\n"
|
|
"installer from WSL or Git Bash, or use Mem0 via MCP / Direct tools\n",
|
|
file=sys.stderr,
|
|
)
|
|
return 2
|
|
|
|
if not TEMPLATE_FILE.exists():
|
|
print(f"error: template not found at {TEMPLATE_FILE}", file=sys.stderr)
|
|
return 1
|
|
|
|
template = load_template()
|
|
config = strip_owned_entries(config)
|
|
config = merge_template(config, template)
|
|
write_config(config)
|
|
|
|
print(f"Installed Mem0 hooks into {HOOKS_FILE}")
|
|
print(f"Plugin path: {PLUGIN_ROOT}")
|
|
print("Events: PreToolUse, SessionStart, UserPromptSubmit, PostToolUse")
|
|
|
|
if not feature_flag_enabled():
|
|
print_feature_flag_hint()
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|