Skip to content
Merged
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
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
- Run the E2E suite with `bash scripts/e2e_test.sh` or `make test`.
- Validate harness module manifests with `make harness-validate` when changing
harness module assets.
- Treat `harness/` as an experimental, not-yet-released harness layer. Do not
use it as an implementation dependency for release-path commands such as
`mnemon setup`; formal integrations belong under `cmd/` and `internal/`.
- Treat `.claude/`, `.codex/`, `.openclaw/`, and similar host directories as
local projection surfaces, not canonical project state.

Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ mnemon setup

`mnemon setup` auto-detects Claude Code, then interactively deploys skill, hooks, and behavioral guide. Start a new session — memory just works.

### [Codex](https://github.com/openai/codex)

```bash
mnemon setup --target codex --yes
```

One command deploys the mnemon skill, prompt files, and Codex lifecycle hooks
(`SessionStart`, `UserPromptSubmit`, `Stop`) in `.codex/hooks.json`.

### [OpenClaw](https://github.com/openclaw/openclaw)

```bash
Expand Down
104 changes: 96 additions & 8 deletions cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ var setupCmd = &cobra.Command{
Short: "Deploy mnemon into LLM CLI environments",
Long: `Detect installed LLM CLIs and deploy mnemon integration.

By default, installs to project-local config (.claude/, .openclaw/, .nanobot/).
Use --global to install to user-wide config (~/.claude/, ~/.openclaw/, ~/.nanobot/workspace/).
By default, installs to project-local config (.claude/, .codex/, .openclaw/, .nanobot/).
Use --global to install to user-wide config (~/.claude/, ~/.codex/, ~/.openclaw/, ~/.nanobot/workspace/).

Supported environments: Claude Code, OpenClaw, Nanobot.
Supported environments: Claude Code, Codex, OpenClaw, Nanobot.

Examples:
mnemon setup # Interactive: project-local install
Expand All @@ -38,16 +38,16 @@ Examples:
}

func init() {
setupCmd.Flags().StringVar(&setupTarget, "target", "", "target environment (claude-code, openclaw, nanobot)")
setupCmd.Flags().StringVar(&setupTarget, "target", "", "target environment (claude-code, codex, openclaw, nanobot)")
setupCmd.Flags().BoolVar(&setupEject, "eject", false, "remove mnemon integrations")
setupCmd.Flags().BoolVar(&setupYes, "yes", false, "auto-confirm all prompts (CI-friendly)")
setupCmd.Flags().BoolVar(&setupGlobal, "global", false, "install to user-wide config (~/.claude/) instead of project-local (.claude/)")
setupCmd.Flags().BoolVar(&setupGlobal, "global", false, "install to user-wide config instead of project-local config")
rootCmd.AddCommand(setupCmd)
}

func runSetup(cmd *cobra.Command, args []string) error {
if setupTarget != "" && setupTarget != "claude-code" && setupTarget != "openclaw" && setupTarget != "nanobot" {
return fmt.Errorf("invalid target %q (must be claude-code, openclaw, or nanobot)", setupTarget)
if setupTarget != "" && setupTarget != "claude-code" && setupTarget != "codex" && setupTarget != "openclaw" && setupTarget != "nanobot" {
return fmt.Errorf("invalid target %q (must be claude-code, codex, openclaw, or nanobot)", setupTarget)
}

envs := setup.DetectEnvironments(setupGlobal)
Expand Down Expand Up @@ -83,7 +83,7 @@ func runInstallFlow(envs []setup.Environment) error {

if len(detected) == 0 {
fmt.Println("\nNo supported LLM CLI environments detected.")
fmt.Println("Install Claude Code, OpenClaw, or Nanobot, then run 'mnemon setup' again.")
fmt.Println("Install Claude Code, Codex, OpenClaw, or Nanobot, then run 'mnemon setup' again.")
return nil
}

Expand Down Expand Up @@ -125,6 +125,8 @@ func installEnv(env *setup.Environment) error {
switch env.Name {
case "claude-code":
err = installClaudeCode(env)
case "codex":
err = installCodex(env)
case "openclaw":
err = installOpenClaw(env)
case "nanobot":
Expand Down Expand Up @@ -282,6 +284,85 @@ func selectOptionalHooks() setup.HookSelection {
return sel
}

// ─── Codex ──────────────────────────────────────────────────────────

func installCodex(env *setup.Environment) error {
configDir := env.ConfigDir

if !setupGlobal && !setupYes && setup.IsInteractive() {
home := setup.HomeDir()
localDir := ".codex"
globalDir := home + "/.codex"
idx := setup.SelectOne("Install scope",
[]string{
fmt.Sprintf("Local — this project only (%s/)", localDir),
fmt.Sprintf("Global — all projects (%s/)", globalDir),
}, 0)
if idx == 1 {
configDir = globalDir
} else {
configDir = localDir
}
}

fmt.Printf("\nSetting up Codex (%s)...\n", configDir)

fmt.Println("\n[1/4] Skill")
if path, err := setup.CodexWriteSkill(configDir); err != nil {
setup.StatusError(0, 0, "Skill", err)
return err
} else {
setup.StatusOK(0, 0, "Skill", path)
}

fmt.Println("\n[2/4] Prompts")
if path, err := setup.WritePromptFiles(); err != nil {
setup.StatusError(0, 0, "Prompts", err)
return err
} else {
setup.StatusOK(0, 0, "Prompts", path)
}

fmt.Println("\n[3/4] Hooks")
if path, err := setup.CodexWriteHook(configDir, "prime.sh", assets.CodexPrimeHook); err != nil {
setup.StatusError(0, 0, "Hook: prime", err)
return err
} else {
setup.StatusOK(0, 0, "Hook: prime", path)
}
if path, err := setup.CodexWriteHook(configDir, "user_prompt.sh", assets.CodexUserPromptHook); err != nil {
setup.StatusError(0, 0, "Hook: remind", err)
return err
} else {
setup.StatusOK(0, 0, "Hook: remind", path)
}
if path, err := setup.CodexWriteHook(configDir, "stop.sh", assets.CodexStopHook); err != nil {
setup.StatusError(0, 0, "Hook: stop", err)
return err
} else {
setup.StatusOK(0, 0, "Hook: stop", path)
}

fmt.Println("\n[4/4] Config")
if path, err := setup.CodexRegisterHooks(configDir); err != nil {
setup.StatusError(0, 0, "Hooks config", err)
return err
} else {
setup.StatusUpdated(0, 0, "Hooks config", path)
}

fmt.Println()
fmt.Println("Setup complete!")
fmt.Printf(" Skill %s/skills/mnemon/SKILL.md\n", configDir)
fmt.Printf(" Hooks %s/hooks.json (SessionStart, UserPromptSubmit, Stop)\n", configDir)
fmt.Printf(" Prompts ~/.mnemon/prompt/ (guide.md, skill.md)\n")
fmt.Println()
fmt.Println("Start a new Codex session to activate.")
fmt.Println("Run 'mnemon setup --eject --target codex' to remove.")

return nil
}

// ─── OpenClaw ───────────────────────────────────────────────────────

func installOpenClaw(env *setup.Environment) error {
Expand Down Expand Up @@ -526,6 +607,13 @@ func ejectEnv(env *setup.Environment) error {
return errs[0]
}

case "codex":
errs := setup.CodexEject(env.ConfigDir)
ejectMarkdown("AGENTS.md", "Remove memory guidance from ./AGENTS.md?")
if len(errs) > 0 {
return errs[0]
}

case "openclaw":
errs := setup.OpenClawEject(env.ConfigDir)
ejectMarkdown("AGENTS.md", "Remove memory guidance from ./AGENTS.md?")
Expand Down
14 changes: 13 additions & 1 deletion internal/setup/assets/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ var ClaudeSkill []byte
//go:embed claude/guide.md
var ClaudeGuide []byte

//go:embed codex/SKILL.md
var CodexSkill []byte

//go:embed codex/prime.sh
var CodexPrimeHook []byte

//go:embed codex/user_prompt.sh
var CodexUserPromptHook []byte

//go:embed codex/stop.sh
var CodexStopHook []byte

//go:embed openclaw/SKILL.md
var OpenClawSkill []byte

Expand Down Expand Up @@ -49,5 +61,5 @@ var NanobotSkill []byte

// All returns the embedded filesystem for inspection.
//
//go:embed claude openclaw nanoclaw nanobot
//go:embed claude codex openclaw nanoclaw nanobot
var All embed.FS
44 changes: 44 additions & 0 deletions internal/setup/assets/codex/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
name: mnemon
description: Persistent memory CLI for LLM agents. Store facts, recall past knowledge, link related memories, manage lifecycle.
---

# mnemon

## Workflow

1. **Remember**: `mnemon remember "<fact>" --cat <cat> --imp <1-5> --entities "e1,e2" --source agent`
- Diff is built in: duplicates are skipped, conflicts are auto-replaced.
- Output includes `action` (added/updated/skipped), `semantic_candidates`, and `causal_candidates`.
2. **Link** (evaluate candidates from step 1 using judgment):
- Review `causal_candidates`: link only when the memories are genuinely causally related.
- Review `semantic_candidates`: high `similarity` alone is not enough; skip unrelated keyword matches.
- Syntax: `mnemon link <id> <candidate> --type <causal|semantic> --weight <0-1> [--meta '<json>']`
3. **Recall**: `mnemon recall "<query>" --limit 10`

## Commands

```bash
mnemon remember "<fact>" --cat <cat> --imp <1-5> --entities "e1,e2" --source agent
mnemon link <id1> <id2> --type <type> --weight <0-1> [--meta '<json>']
mnemon recall "<query>" --limit 10
mnemon search "<query>" --limit 10
mnemon forget <id>
mnemon related <id> --edge causal
mnemon gc --threshold 0.4
mnemon gc --keep <id>
mnemon status
mnemon log
mnemon store list
mnemon store create <name>
mnemon store set <name>
mnemon store remove <name>
```

## Guardrails

- Use memory only when it can materially improve continuity or task quality.
- Do not store secrets, passwords, tokens, private keys, or short-lived operational noise.
- Categories: `preference` · `decision` · `insight` · `fact` · `context`
- Edge types: `temporal` · `semantic` · `causal` · `entity`
- Max 8,000 chars per insight.
19 changes: 19 additions & 0 deletions internal/setup/assets/codex/prime.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash
PROMPT_DIR="${HOME}/.mnemon/prompt"

if ! command -v mnemon >/dev/null 2>&1; then
echo "[mnemon] Warning: mnemon not found in PATH."
[ -f "${PROMPT_DIR}/guide.md" ] && cat "${PROMPT_DIR}/guide.md"
exit 0
fi

STATS=$(mnemon status 2>/dev/null)
if [ -n "$STATS" ]; then
INSIGHTS=$(echo "$STATS" | sed -n 's/.*"total_insights": *\([0-9]*\).*/\1/p' | head -1)
EDGES=$(echo "$STATS" | sed -n 's/.*"edge_count": *\([0-9]*\).*/\1/p' | head -1)
echo "[mnemon] Memory active (${INSIGHTS:-0} insights, ${EDGES:-0} edges)."
else
echo "[mnemon] Memory active."
fi

[ -f "${PROMPT_DIR}/guide.md" ] && cat "${PROMPT_DIR}/guide.md"
25 changes: 25 additions & 0 deletions internal/setup/assets/codex/stop.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/bash
# mnemon Codex Stop hook
# Stop requires JSON on stdout. Keep this non-blocking; do not force continuation.
INPUT=$(cat)
python3 - "$INPUT" <<'PY'
import json
import sys

try:
payload = json.loads(sys.argv[1])
except Exception:
payload = {}

if payload.get("stop_hook_active"):
sys.exit(0)

last_message = (payload.get("last_assistant_message") or "").lower()
if "mnemon" in last_message or "durable memory" in last_message:
sys.exit(0)

print(json.dumps({
"continue": True,
"systemMessage": "[mnemon] Consider: does this exchange warrant durable memory?",
}))
PY
5 changes: 5 additions & 0 deletions internal/setup/assets/codex/user_prompt.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash
# mnemon Codex UserPromptSubmit hook
# Plain stdout is added as extra developer context by Codex.
cat >/dev/null || true
echo "[mnemon] Evaluate: recall needed? After responding, evaluate: remember needed?"
Loading
Loading