From b11ff9dfb715dd37bc5f0b7a68c7339dff5950c2 Mon Sep 17 00:00:00 2001 From: Grivn Date: Sat, 20 Jun 2026 17:14:42 +0000 Subject: [PATCH] feat(setup): add CodeBuddy integration Add a CodeBuddy setup target that installs the mnemon skill, prompt files, and native command hooks into .codebuddy/ or ~/.codebuddy/. The integration registers SessionStart, UserPromptSubmit, and Stop hooks in settings.json and uses CodeBuddy's current continue:false Stop hook response shape. This intentionally scopes the change to CodeBuddy only; WorkBuddy uses a separate config surface and can be handled independently. Validated with go test ./internal/setup and go build -o mnemon . --- README.md | 24 ++- cmd/setup.go | 101 +++++++++++- docs/USAGE.md | 3 +- docs/zh/README.md | 16 +- docs/zh/USAGE.md | 3 +- internal/setup/assets/assets.go | 14 +- internal/setup/assets/codebuddy/SKILL.md | 46 ++++++ internal/setup/assets/codebuddy/prime.sh | 22 +++ internal/setup/assets/codebuddy/stop.sh | 31 ++++ .../setup/assets/codebuddy/user_prompt.sh | 3 + internal/setup/codebuddy.go | 141 ++++++++++++++++ internal/setup/codebuddy_test.go | 153 ++++++++++++++++++ internal/setup/detect.go | 46 +++++- 13 files changed, 583 insertions(+), 20 deletions(-) create mode 100644 internal/setup/assets/codebuddy/SKILL.md create mode 100644 internal/setup/assets/codebuddy/prime.sh create mode 100644 internal/setup/assets/codebuddy/stop.sh create mode 100644 internal/setup/assets/codebuddy/user_prompt.sh create mode 100644 internal/setup/codebuddy.go create mode 100644 internal/setup/codebuddy_test.go diff --git a/README.md b/README.md index e821b054..86630a40 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,16 @@ or `~/.qoder/`. QoderWork uses its native user config at `~/.qoderwork/`. Both integrations register `SessionStart`, `UserPromptSubmit`, and `Stop` hooks in `settings.json`. +### [CodeBuddy](https://www.codebuddy.cn/) + +```bash +mnemon setup --target codebuddy --yes +``` + +CodeBuddy deploys the mnemon skill, prompt files, and native hooks to +`.codebuddy/` or `~/.codebuddy/`. The integration registers `SessionStart`, +`UserPromptSubmit`, and `Stop` hooks in `settings.json`. + ### [OpenClaw](https://github.com/openclaw/openclaw) ```bash @@ -233,7 +243,7 @@ memory is useful. - **Zero user-side operation** — install once; supported runtimes can use hooks, minimal runtimes can use persistent rules - **LLM-supervised** — the host LLM decides what to remember, update, and forget; no embedded LLM, no API keys -- **Multi-framework support** — Claude Code, Codex, Cursor, TRAE/TRAE Work, Qoder/QoderWork, and Hermes Agent (hooks), OpenClaw (plugins), Pi (extensions), Nanobot (skills), and more +- **Multi-framework support** — Claude Code, Codex, Cursor, TRAE/TRAE Work, Qoder/QoderWork, CodeBuddy, and Hermes Agent (hooks), OpenClaw (plugins), Pi (extensions), Nanobot (skills), and more - **Markdown-installable harness** — `SKILL.md`, `INSTALL.md`, `GUIDELINE.md`, and four lifecycle reminders - **Four-graph architecture** — temporal, entity, causal, and semantic edges, not just vector similarity - **Intent-native protocol** — three primitives (`remember`, `link`, `recall`) map to the LLM's cognitive vocabulary, not database syntax; structured JSON output with signal transparency @@ -262,6 +272,8 @@ All your local agentic AIs — across sessions and frameworks — sharing one po │ QoderWork ────┤ │ + CodeBuddy ────┤ + │ Hermes Agent ─┤ │ OpenClaw ─────┤ @@ -279,11 +291,11 @@ All your local agentic AIs — across sessions and frameworks — sharing one po The foundation is in place: a single `~/.mnemon` database that any agent can read and write. Claude Code, Codex, Cursor, TRAE/TRAE Work, Qoder/QoderWork, -and Hermes Agent setup automate hook installation; OpenClaw can use plugin -hooks; Pi integrates via native skills and TypeScript lifecycle extensions; -Nanobot integrates via skill files; NanoClaw integrates via container skills and -volume mounts. The same harness can be installed in any LLM CLI that supports -skills, rules, system prompts, or event hooks. +CodeBuddy, and Hermes Agent setup automate hook installation; OpenClaw can use +plugin hooks; Pi integrates via native skills and TypeScript lifecycle +extensions; Nanobot integrates via skill files; NanoClaw integrates via +container skills and volume mounts. The same harness can be installed in any LLM +CLI that supports skills, rules, system prompts, or event hooks. The longer-term direction is a **memory gateway**: protocol decoupled from storage engine. The current SQLite backend is the first adapter; the protocol surface (`remember / link / recall`) can sit on top of PostgreSQL, Neo4j, or any graph database. Agent-side optimization (when to recall, what to remember) and storage-side optimization (indexing, graph algorithms) evolve independently. See [Future Direction](docs/design/08-decisions.md#82-future-direction) for details. diff --git a/cmd/setup.go b/cmd/setup.go index e348e75b..df3128b1 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -22,11 +22,11 @@ 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/, .codex/, .cursor/, .trae/, .qoder/, .openclaw/, .nanobot/, .pi/). -Use --global to install to user-wide config (~/.claude/, ~/.codex/, ~/.cursor/, ~/.trae/, ~/.qoder/, ~/.openclaw/, ~/.nanobot/workspace/, ~/.pi/agent/). +By default, installs to project-local config (.claude/, .codex/, .cursor/, .trae/, .qoder/, .codebuddy/, .openclaw/, .nanobot/, .pi/). +Use --global to install to user-wide config (~/.claude/, ~/.codex/, ~/.cursor/, ~/.trae/, ~/.qoder/, ~/.codebuddy/, ~/.openclaw/, ~/.nanobot/workspace/, ~/.pi/agent/). Hermes Agent and QoderWork use their native user config at ~/.hermes/ and ~/.qoderwork/. -Supported environments: Claude Code, Codex, Cursor, Trae, Qoder, QoderWork, OpenClaw, Nanobot, Pi, Hermes Agent. +Supported environments: Claude Code, Codex, Cursor, Trae, Qoder, QoderWork, CodeBuddy, OpenClaw, Nanobot, Pi, Hermes Agent. Examples: mnemon setup # Interactive: project-local install @@ -36,6 +36,7 @@ Examples: mnemon setup --target trae # Non-interactive: Trae skill and hooks mnemon setup --target qoder # Non-interactive: Qoder skill and hooks mnemon setup --target qoderwork # Non-interactive: QoderWork skill and hooks + mnemon setup --target codebuddy # Non-interactive: CodeBuddy skill and hooks mnemon setup --target hermes # Non-interactive: Hermes Agent only mnemon setup --eject # Interactive: remove integrations mnemon setup --eject --target claude-code # Non-interactive: remove Claude Code only @@ -44,7 +45,7 @@ Examples: } func init() { - setupCmd.Flags().StringVar(&setupTarget, "target", "", "target environment (claude-code, codex, cursor, trae, qoder, qoderwork, openclaw, nanobot, pi, hermes)") + setupCmd.Flags().StringVar(&setupTarget, "target", "", "target environment (claude-code, codex, cursor, trae, qoder, qoderwork, codebuddy, openclaw, nanobot, pi, hermes)") 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 instead of project-local config") @@ -52,8 +53,8 @@ func init() { } func runSetup(cmd *cobra.Command, args []string) error { - if setupTarget != "" && setupTarget != "claude-code" && setupTarget != "codex" && setupTarget != "cursor" && setupTarget != "trae" && setupTarget != "qoder" && setupTarget != "qoderwork" && setupTarget != "openclaw" && setupTarget != "nanobot" && setupTarget != "pi" && setupTarget != "hermes" { - return fmt.Errorf("invalid target %q (must be claude-code, codex, cursor, trae, qoder, qoderwork, openclaw, nanobot, pi, or hermes)", setupTarget) + if setupTarget != "" && setupTarget != "claude-code" && setupTarget != "codex" && setupTarget != "cursor" && setupTarget != "trae" && setupTarget != "qoder" && setupTarget != "qoderwork" && setupTarget != "codebuddy" && setupTarget != "openclaw" && setupTarget != "nanobot" && setupTarget != "pi" && setupTarget != "hermes" { + return fmt.Errorf("invalid target %q (must be claude-code, codex, cursor, trae, qoder, qoderwork, codebuddy, openclaw, nanobot, pi, or hermes)", setupTarget) } envs := setup.DetectEnvironments(setupGlobal) @@ -89,7 +90,7 @@ func runInstallFlow(envs []setup.Environment) error { if len(detected) == 0 { fmt.Println("\nNo supported LLM CLI environments detected.") - fmt.Println("Install Claude Code, Codex, Cursor, Trae, Qoder, QoderWork, OpenClaw, Nanobot, Pi, or Hermes Agent, then run 'mnemon setup' again.") + fmt.Println("Install Claude Code, Codex, Cursor, Trae, Qoder, QoderWork, CodeBuddy, OpenClaw, Nanobot, Pi, or Hermes Agent, then run 'mnemon setup' again.") return nil } @@ -141,6 +142,8 @@ func installEnv(env *setup.Environment) error { err = installQoder(env) case "qoderwork": err = installQoderWork(env) + case "codebuddy": + err = installCodeBuddy(env) case "openclaw": err = installOpenClaw(env) case "nanobot": @@ -678,6 +681,83 @@ func installQoderLike(configDir string, writeSkill func(string) (string, error), return nil } +// ─── CodeBuddy ────────────────────────────────────────────────────── + +func installCodeBuddy(env *setup.Environment) error { + configDir := env.ConfigDir + + if !setupGlobal && !setupYes && setup.IsInteractive() { + home := setup.HomeDir() + localDir := ".codebuddy" + globalDir := home + "/.codebuddy" + 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 CodeBuddy (%s)...\n", configDir) + + fmt.Println("\n[1/3] Skill") + if path, err := setup.CodeBuddyWriteSkill(configDir); err != nil { + setup.StatusError(0, 0, "Skill", err) + return err + } else { + setup.StatusOK(0, 0, "Skill", path) + } + + fmt.Println("\n[2/3] Prompts") + var promptPath string + if path, err := setup.WritePromptFiles(); err != nil { + setup.StatusError(0, 0, "Prompts", err) + return err + } else { + setup.StatusOK(0, 0, "Prompts", path) + promptPath = path + } + + fmt.Println("\n[3/3] Hooks") + for _, hook := range []struct { + label string + filename string + content []byte + }{ + {"Hook: prime", "prime.sh", assets.CodeBuddyPrimeHook}, + {"Hook: remind", "user_prompt.sh", assets.CodeBuddyUserPromptHook}, + {"Hook: nudge", "stop.sh", assets.CodeBuddyStopHook}, + } { + if path, err := setup.CodeBuddyWriteHook(configDir, hook.filename, hook.content); err != nil { + setup.StatusError(0, 0, hook.label, err) + return err + } else { + setup.StatusOK(0, 0, hook.label, path) + } + } + if path, err := setup.CodeBuddyRegisterHooks(configDir); err != nil { + setup.StatusError(0, 0, "Settings", err) + return err + } else { + setup.StatusUpdated(0, 0, "Settings", path) + } + + fmt.Println() + fmt.Println("Setup complete!") + fmt.Printf(" Skill %s/skills/mnemon/SKILL.md\n", configDir) + fmt.Printf(" Hooks %s/settings.json (SessionStart, UserPromptSubmit, Stop)\n", configDir) + fmt.Printf(" Prompts %s/ (guide.md, skill.md)\n", promptPath) + fmt.Println() + fmt.Println("Restart CodeBuddy Code to activate the mnemon skill and hooks.") + fmt.Println("Run 'mnemon setup --eject --target codebuddy' to remove.") + + return nil +} + // ─── OpenClaw ─────────────────────────────────────────────────────── func installOpenClaw(env *setup.Environment) error { @@ -1133,6 +1213,13 @@ func ejectEnv(env *setup.Environment) error { return errs[0] } + case "codebuddy": + errs := setup.CodeBuddyEject(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?") diff --git a/docs/USAGE.md b/docs/USAGE.md index aa94c625..8f30a41d 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -34,6 +34,7 @@ mnemon setup --target cursor mnemon setup --target trae mnemon setup --target qoder mnemon setup --target qoderwork +mnemon setup --target codebuddy mnemon setup --target openclaw mnemon setup --target pi mnemon setup --target nanobot --global @@ -50,7 +51,7 @@ mnemon setup --eject --target claude-code | Flag | Default | Description | |---|---|---| | `--global` | `false` | Install to user-wide config instead of project-local (recommended for Nanobot: installs to `~/.nanobot/workspace/`; Pi installs to `~/.pi/agent/`; Hermes installs to `~/.hermes/`; QoderWork installs to `~/.qoderwork/`) | -| `--target ` | (auto-detect) | Target environment: `claude-code`, `codex`, `cursor`, `trae`, `qoder`, `qoderwork`, `openclaw`, `nanobot`, `pi`, or `hermes` | +| `--target ` | (auto-detect) | Target environment: `claude-code`, `codex`, `cursor`, `trae`, `qoder`, `qoderwork`, `codebuddy`, `openclaw`, `nanobot`, `pi`, or `hermes` | | `--eject` | `false` | Remove mnemon integrations | | `--yes` | `false` | Auto-confirm all prompts | diff --git a/docs/zh/README.md b/docs/zh/README.md index a13ad468..9b8075c9 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -113,6 +113,16 @@ Qoder 会将 mnemon skill、prompt 文件和原生 hooks 部署到 `.qoder/` 或 `~/.qoder/`。QoderWork 使用原生用户级配置 `~/.qoderwork/`。两者都会在 `settings.json` 中注册 `SessionStart`、`UserPromptSubmit` 和 `Stop` hooks。 +### [CodeBuddy](https://www.codebuddy.cn/) + +```bash +mnemon setup --target codebuddy --yes +``` + +CodeBuddy 会将 mnemon skill、prompt 文件和原生 hooks 部署到 `.codebuddy/` +或 `~/.codebuddy/`。该集成会在 `settings.json` 中注册 `SessionStart`、 +`UserPromptSubmit` 和 `Stop` hooks。 + ### [OpenClaw](https://github.com/openclaw/openclaw) ```bash @@ -196,7 +206,7 @@ Agent 工作,并且只在有用时调用 Mnemon - **零用户操作** — 安装一次;支持 hook 的 runtime 可用 hook,minimal runtime 可用持久规则 - **LLM 监督式** — 宿主 LLM 主动决定记什么、更新什么、遗忘什么;无内嵌 LLM,无 API 密钥 -- **多框架支持** — Claude Code、Codex、Cursor、TRAE/TRAE Work、Qoder/QoderWork 和 Hermes Agent(hooks)、OpenClaw(plugins)、Pi(extensions)、Nanobot(skills)等 +- **多框架支持** — Claude Code、Codex、Cursor、TRAE/TRAE Work、Qoder/QoderWork、CodeBuddy 和 Hermes Agent(hooks)、OpenClaw(plugins)、Pi(extensions)、Nanobot(skills)等 - **Markdown 可安装 harness** — `SKILL.md`、`INSTALL.md`、`GUIDELINE.md` 和四个生命周期提醒 - **四图架构** — 时序、实体、因果、语义四种边,不仅仅是向量相似度 - **意图原生协议** — 三个原语(`remember`、`link`、`recall`)映射到 LLM 的认知词汇而非数据库语法;结构化 JSON 输出,带信号透明度 @@ -224,6 +234,8 @@ Agent 工作,并且只在有用时调用 Mnemon │ QoderWork ────┤ │ + CodeBuddy ────┤ + │ Hermes Agent ─┤ │ OpenClaw ─────┤ @@ -239,7 +251,7 @@ Agent 工作,并且只在有用时调用 Mnemon Gemini CLI ───┘ ``` -基础已就绪:一个 `~/.mnemon` 数据库,任何 agent 都可以读写。Claude Code、Codex、Cursor、TRAE/TRAE Work、Qoder/QoderWork 和 Hermes Agent setup 可自动安装 hook;OpenClaw 可以使用 plugin hooks;Pi 通过原生 skill 和 TypeScript lifecycle extension 集成;Nanobot 通过 skill 文件集成;NanoClaw 通过容器技能和卷挂载集成。同一个 harness 可以安装到任何支持 skill、rule、system prompt 或 event hook 的 LLM CLI。 +基础已就绪:一个 `~/.mnemon` 数据库,任何 agent 都可以读写。Claude Code、Codex、Cursor、TRAE/TRAE Work、Qoder/QoderWork、CodeBuddy 和 Hermes Agent setup 可自动安装 hook;OpenClaw 可以使用 plugin hooks;Pi 通过原生 skill 和 TypeScript lifecycle extension 集成;Nanobot 通过 skill 文件集成;NanoClaw 通过容器技能和卷挂载集成。同一个 harness 可以安装到任何支持 skill、rule、system prompt 或 event hook 的 LLM CLI。 更长远的方向是**记忆网关**:协议层与存储引擎解耦。当前 SQLite 后端是第一个适配器;协议面(`remember / link / recall`)可运行在 PostgreSQL、Neo4j 或任何图数据库之上。Agent 侧优化(何时召回、记什么)与存储侧优化(索引、图算法)独立演进。详见[未来方向](design/08-decisions.md#82-未来方向)。 diff --git a/docs/zh/USAGE.md b/docs/zh/USAGE.md index 9053f906..2e13887f 100644 --- a/docs/zh/USAGE.md +++ b/docs/zh/USAGE.md @@ -34,6 +34,7 @@ mnemon setup --target cursor mnemon setup --target trae mnemon setup --target qoder mnemon setup --target qoderwork +mnemon setup --target codebuddy mnemon setup --target openclaw mnemon setup --target pi mnemon setup --target nanobot --global @@ -50,7 +51,7 @@ mnemon setup --eject --target claude-code | 标志 | 默认值 | 说明 | |---|---|---| | `--global` | `false` | 安装到用户级配置而非项目本地(Nanobot 推荐安装到 `~/.nanobot/workspace/`;Pi 安装到 `~/.pi/agent/`;Hermes 安装到 `~/.hermes/`;QoderWork 安装到 `~/.qoderwork/`) | -| `--target ` | (自动检测) | 目标环境:`claude-code`、`codex`、`cursor`、`trae`、`qoder`、`qoderwork`、`openclaw`、`nanobot`、`pi` 或 `hermes` | +| `--target ` | (自动检测) | 目标环境:`claude-code`、`codex`、`cursor`、`trae`、`qoder`、`qoderwork`、`codebuddy`、`openclaw`、`nanobot`、`pi` 或 `hermes` | | `--eject` | `false` | 移除 mnemon 集成 | | `--yes` | `false` | 自动确认所有提示 | diff --git a/internal/setup/assets/assets.go b/internal/setup/assets/assets.go index 66dd96b4..10336569 100644 --- a/internal/setup/assets/assets.go +++ b/internal/setup/assets/assets.go @@ -71,6 +71,18 @@ var QoderUserPromptHook []byte //go:embed qoder/stop.sh var QoderStopHook []byte +//go:embed codebuddy/SKILL.md +var CodeBuddySkill []byte + +//go:embed codebuddy/prime.sh +var CodeBuddyPrimeHook []byte + +//go:embed codebuddy/user_prompt.sh +var CodeBuddyUserPromptHook []byte + +//go:embed codebuddy/stop.sh +var CodeBuddyStopHook []byte + //go:embed openclaw/SKILL.md var OpenClawSkill []byte @@ -121,5 +133,5 @@ var HermesCompactHook []byte // All returns the embedded filesystem for inspection. // -//go:embed claude codex cursor trae qoder qoderwork openclaw nanoclaw nanobot pi hermes +//go:embed claude codex cursor trae qoder qoderwork codebuddy openclaw nanoclaw nanobot pi hermes var All embed.FS diff --git a/internal/setup/assets/codebuddy/SKILL.md b/internal/setup/assets/codebuddy/SKILL.md new file mode 100644 index 00000000..def7eb51 --- /dev/null +++ b/internal/setup/assets/codebuddy/SKILL.md @@ -0,0 +1,46 @@ +--- +name: mnemon +description: Persistent memory CLI for CodeBuddy. Store facts, recall past knowledge, link related memories, manage lifecycle. +--- + +# mnemon + +## Workflow + +1. **Remember**: `mnemon remember "" --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 --type --weight <0-1> [--meta '']` +3. **Recall**: `mnemon recall "" --limit 10` + +## Commands + +```bash +mnemon remember "" --cat --imp <1-5> --entities "e1,e2" --source agent +mnemon link --type --weight <0-1> [--meta ''] +mnemon recall "" --limit 10 +mnemon search "" --limit 10 +mnemon import --dry-run +mnemon import +mnemon forget +mnemon related --edge causal +mnemon gc --threshold 0.4 +mnemon gc --keep +mnemon status +mnemon log +mnemon store list +mnemon store create +mnemon store set +mnemon store remove +``` + +## 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. diff --git a/internal/setup/assets/codebuddy/prime.sh b/internal/setup/assets/codebuddy/prime.sh new file mode 100644 index 00000000..694a2d0a --- /dev/null +++ b/internal/setup/assets/codebuddy/prime.sh @@ -0,0 +1,22 @@ +#!/bin/bash +PROMPT_DIR="${MNEMON_DATA_DIR:-$HOME/.mnemon}/prompt" +if [ ! -f "${PROMPT_DIR}/guide.md" ] && [ -f "${HOME}/.mnemon/prompt/guide.md" ]; then + PROMPT_DIR="${HOME}/.mnemon/prompt" +fi + +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" diff --git a/internal/setup/assets/codebuddy/stop.sh b/internal/setup/assets/codebuddy/stop.sh new file mode 100644 index 00000000..263abf68 --- /dev/null +++ b/internal/setup/assets/codebuddy/stop.sh @@ -0,0 +1,31 @@ +#!/bin/bash +INPUT=$(cat) + +if echo "$INPUT" | grep -q '"stop_hook_active"[[:space:]]*:[[:space:]]*true'; then + exit 0 +fi + +SESSION_ID=$(echo "$INPUT" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) +if [ -z "$SESSION_ID" ]; then + SESSION_ID=$(echo "$INPUT" | sed -n 's/.*"cwd"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1 | sed 's/[^A-Za-z0-9_.-]/_/g') +fi +if [ -z "$SESSION_ID" ]; then + SESSION_ID="unknown" +fi + +STATE_DIR="${MNEMON_DATA_DIR:-$HOME/.mnemon}/hooks" +STATE_FILE="${STATE_DIR}/codebuddy-stop-${SESSION_ID}.seen" +mkdir -p "$STATE_DIR" 2>/dev/null || true + +if [ -f "$STATE_FILE" ]; then + rm -f "$STATE_FILE" 2>/dev/null || true + exit 0 +fi + +touch "$STATE_FILE" 2>/dev/null || true +cat <<'JSON' +{ + "continue": false, + "reason": "[mnemon] Before stopping, evaluate whether this exchange contains durable preferences, decisions, insights, facts, or context worth remembering. If yes, run mnemon remember/link; if no, state that no memory update is needed, then finish." +} +JSON diff --git a/internal/setup/assets/codebuddy/user_prompt.sh b/internal/setup/assets/codebuddy/user_prompt.sh new file mode 100644 index 00000000..6e5d6d2e --- /dev/null +++ b/internal/setup/assets/codebuddy/user_prompt.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cat >/dev/null || true +echo "[mnemon] Evaluate: recall needed? After responding, evaluate: remember needed?" diff --git a/internal/setup/codebuddy.go b/internal/setup/codebuddy.go new file mode 100644 index 00000000..b8e0f869 --- /dev/null +++ b/internal/setup/codebuddy.go @@ -0,0 +1,141 @@ +package setup + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/mnemon-dev/mnemon/internal/setup/assets" +) + +// CodeBuddyWriteSkill writes the mnemon skill to the CodeBuddy skills directory. +func CodeBuddyWriteSkill(configDir string) (string, error) { + skillDir := filepath.Join(configDir, "skills", "mnemon") + if err := os.MkdirAll(skillDir, 0755); err != nil { + return "", err + } + skillPath := filepath.Join(skillDir, "SKILL.md") + if err := os.WriteFile(skillPath, assets.CodeBuddySkill, 0644); err != nil { + return "", err + } + return skillPath, nil +} + +// CodeBuddyWriteHook writes a hook script to the CodeBuddy hooks directory. +func CodeBuddyWriteHook(configDir, filename string, content []byte) (string, error) { + hooksDir := filepath.Join(configDir, "hooks", "mnemon") + if err := os.MkdirAll(hooksDir, 0755); err != nil { + return "", err + } + hookPath := filepath.Join(hooksDir, filename) + if err := os.WriteFile(hookPath, content, 0755); err != nil { + return "", err + } + return hookPath, nil +} + +// CodeBuddyRegisterHooks registers Mnemon lifecycle hooks in settings.json. +func CodeBuddyRegisterHooks(configDir string) (string, error) { + hooksDir := filepath.Join(configDir, "hooks", "mnemon") + absHooksDir, err := filepath.Abs(hooksDir) + if err != nil { + return "", err + } + settingsPath := filepath.Join(configDir, "settings.json") + data, err := ReadJSONFile(settingsPath) + if err != nil { + return "", err + } + addCodeBuddyHooks(data, absHooksDir) + if err := WriteJSONFile(settingsPath, data); err != nil { + return "", err + } + return settingsPath, nil +} + +// CodeBuddyEject removes mnemon skill and hooks from the given CodeBuddy config dir. +func CodeBuddyEject(configDir string) []error { + var errs []error + + fmt.Printf("\nRemoving CodeBuddy integration (%s)...\n", configDir) + + hooksDir := filepath.Join(configDir, "hooks", "mnemon") + if err := os.RemoveAll(hooksDir); err != nil { + StatusError(1, 3, "Hooks", err) + errs = append(errs, err) + } else { + StatusOK(1, 3, "Hooks", hooksDir+" removed") + } + removeIfEmpty(filepath.Join(configDir, "hooks")) + + settingsPath := filepath.Join(configDir, "settings.json") + data, err := ReadJSONFile(settingsPath) + if err != nil { + StatusError(2, 3, "Settings", err) + errs = append(errs, err) + } else { + removeCodeBuddyHooks(data) + if err := WriteOrRemoveJSONFile(settingsPath, data); err != nil { + StatusError(2, 3, "Settings", err) + errs = append(errs, err) + } else { + StatusOK(2, 3, "Settings", settingsPath+" cleaned") + } + } + + skillDir := filepath.Join(configDir, "skills", "mnemon") + if err := os.RemoveAll(skillDir); err != nil { + StatusError(3, 3, "Skill", err) + errs = append(errs, err) + } else { + StatusOK(3, 3, "Skill", skillDir+" removed") + } + removeIfEmpty(filepath.Join(configDir, "skills")) + removeIfEmpty(configDir) + + return errs +} + +func addCodeBuddyHooks(data map[string]interface{}, hooksDir string) { + removeCodeBuddyHooks(data) + hooks := ensureHooksMap(data) + + addCodeBuddyHook(hooks, "SessionStart", filepath.Join(hooksDir, "prime.sh")) + addCodeBuddyHook(hooks, "UserPromptSubmit", filepath.Join(hooksDir, "user_prompt.sh")) + addCodeBuddyHook(hooks, "Stop", filepath.Join(hooksDir, "stop.sh")) +} + +func addCodeBuddyHook(hooks map[string]interface{}, event, command string) { + entry := map[string]interface{}{ + "hooks": []interface{}{ + map[string]interface{}{ + "type": "command", + "command": command, + }, + }, + } + arr, _ := hooks[event].([]interface{}) + hooks[event] = append(arr, entry) +} + +func removeCodeBuddyHooks(data map[string]interface{}) { + hooks, ok := data["hooks"].(map[string]interface{}) + if !ok { + return + } + for _, key := range []string{"SessionStart", "UserPromptSubmit", "Stop", "PreToolUse", "PostToolUse", "Notification", "PreCompact", "SessionEnd", "SubagentStop"} { + arr, ok := hooks[key].([]interface{}) + if !ok { + continue + } + filtered := filterHookArray(arr) + if len(filtered) == 0 { + delete(hooks, key) + } else { + hooks[key] = filtered + } + } + if len(hooks) == 0 { + delete(data, "hooks") + } +} diff --git a/internal/setup/codebuddy_test.go b/internal/setup/codebuddy_test.go new file mode 100644 index 00000000..0209250c --- /dev/null +++ b/internal/setup/codebuddy_test.go @@ -0,0 +1,153 @@ +package setup + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestCodeBuddyWriteSkill(t *testing.T) { + dir := t.TempDir() + + skillPath, err := CodeBuddyWriteSkill(dir) + if err != nil { + t.Fatalf("write skill: %v", err) + } + if skillPath != filepath.Join(dir, "skills", "mnemon", "SKILL.md") { + t.Fatalf("skill path = %q", skillPath) + } + data, err := os.ReadFile(skillPath) + if err != nil { + t.Fatalf("read skill: %v", err) + } + if !strings.Contains(string(data), "CodeBuddy") { + t.Fatalf("codebuddy skill should mention CodeBuddy: %s", string(data)) + } +} + +func TestCodeBuddyWriteHook(t *testing.T) { + dir := t.TempDir() + + hookPath, err := CodeBuddyWriteHook(dir, "prime.sh", []byte("#!/bin/bash\n")) + if err != nil { + t.Fatalf("write hook: %v", err) + } + if hookPath != filepath.Join(dir, "hooks", "mnemon", "prime.sh") { + t.Fatalf("hook path = %q", hookPath) + } + info, err := os.Stat(hookPath) + if err != nil { + t.Fatalf("stat hook: %v", err) + } + if info.Mode().Perm() != 0755 { + t.Fatalf("hook permissions = %v, want 0755", info.Mode().Perm()) + } +} + +func TestCodeBuddyRegisterHooksPreservesUnrelatedConfig(t *testing.T) { + dir := t.TempDir() + settingsPath := filepath.Join(dir, "settings.json") + if err := os.WriteFile(settingsPath, []byte(`{ + "hooks": { + "SessionStart": [ + {"hooks": [{"type": "command", "command": "/old/mnemon/prime.sh"}]}, + {"hooks": [{"type": "command", "command": "/keep/custom.sh"}]} + ], + "Stop": [ + {"hooks": [{"type": "command", "command": "/old/mnemon/stop.sh"}]} + ] + }, + "other": true +}`), 0644); err != nil { + t.Fatalf("write settings: %v", err) + } + + if _, err := CodeBuddyRegisterHooks(dir); err != nil { + t.Fatalf("register hooks: %v", err) + } + + data, err := ReadJSONFile(settingsPath) + if err != nil { + t.Fatalf("read settings: %v", err) + } + if data["other"] != true { + t.Fatalf("unrelated setting should be preserved: %#v", data) + } + hooks := data["hooks"].(map[string]any) + sessionStart := hooks["SessionStart"].([]any) + if len(sessionStart) != 2 { + t.Fatalf("expected custom hook plus new prime hook: %#v", sessionStart) + } + if !strings.Contains(sessionStart[1].(map[string]any)["hooks"].([]any)[0].(map[string]any)["command"].(string), "hooks/mnemon/prime.sh") { + t.Fatalf("expected new prime hook, got %#v", sessionStart[1]) + } + if _, ok := hooks["UserPromptSubmit"]; !ok { + t.Fatalf("user prompt hook should be registered: %#v", hooks) + } + stop := hooks["Stop"].([]any) + if len(stop) != 1 { + t.Fatalf("expected one stop hook: %#v", stop) + } + if _, ok := stop[0].(map[string]any)["loop_limit"]; ok { + t.Fatalf("codebuddy hook schema should not include loop_limit: %#v", stop[0]) + } +} + +func TestCodeBuddyEjectRemovesOnlyMnemonFilesAndHooks(t *testing.T) { + dir := t.TempDir() + if _, err := CodeBuddyWriteSkill(dir); err != nil { + t.Fatalf("write skill: %v", err) + } + if _, err := CodeBuddyWriteHook(dir, "prime.sh", []byte("#!/bin/bash\n")); err != nil { + t.Fatalf("write hook: %v", err) + } + if _, err := CodeBuddyRegisterHooks(dir); err != nil { + t.Fatalf("register hooks: %v", err) + } + customSkillDir := filepath.Join(dir, "skills", "custom") + if err := os.MkdirAll(customSkillDir, 0755); err != nil { + t.Fatalf("create custom skill: %v", err) + } + settingsPath := filepath.Join(dir, "settings.json") + data, err := ReadJSONFile(settingsPath) + if err != nil { + t.Fatalf("read settings: %v", err) + } + hooks := data["hooks"].(map[string]any) + hooks["SessionStart"] = append(hooks["SessionStart"].([]any), map[string]any{ + "hooks": []any{map[string]any{"type": "command", "command": "/keep/custom.sh"}}, + }) + if err := WriteJSONFile(settingsPath, data); err != nil { + t.Fatalf("write settings: %v", err) + } + + errs := CodeBuddyEject(dir) + if len(errs) > 0 { + t.Fatalf("eject errors: %v", errs) + } + if _, err := os.Stat(filepath.Join(dir, "skills", "mnemon")); !os.IsNotExist(err) { + t.Fatalf("mnemon skill should be removed, err=%v", err) + } + if _, err := os.Stat(customSkillDir); err != nil { + t.Fatalf("custom skill should be preserved: %v", err) + } + if _, err := os.Stat(filepath.Join(dir, "hooks", "mnemon")); !os.IsNotExist(err) { + t.Fatalf("mnemon hooks should be removed, err=%v", err) + } + data, err = ReadJSONFile(settingsPath) + if err != nil { + t.Fatalf("read settings after eject: %v", err) + } + hooks = data["hooks"].(map[string]any) + sessionStart := hooks["SessionStart"].([]any) + if len(sessionStart) != 1 || containsMnemon(sessionStart[0]) { + t.Fatalf("custom hook should be preserved and mnemon removed: %#v", sessionStart) + } + if _, ok := hooks["UserPromptSubmit"]; ok { + t.Fatalf("user prompt hooks should be removed: %#v", hooks) + } + if _, ok := hooks["Stop"]; ok { + t.Fatalf("stop hooks should be removed: %#v", hooks) + } +} diff --git a/internal/setup/detect.go b/internal/setup/detect.go index a39d8b28..2fc131c7 100644 --- a/internal/setup/detect.go +++ b/internal/setup/detect.go @@ -9,8 +9,8 @@ import ( // Environment describes a detected LLM CLI environment. type Environment struct { - Name string // "claude-code", "codex", "cursor", "trae", "qoder", "qoderwork", "openclaw", "nanobot", "pi", "hermes" - Display string // "Claude Code", "Codex", "Cursor", "Trae", "Qoder", "QoderWork", "OpenClaw", "Nanobot", "Pi", "Hermes Agent" + Name string // "claude-code", "codex", "cursor", "trae", "qoder", "qoderwork", "codebuddy", "openclaw", "nanobot", "pi", "hermes" + Display string // "Claude Code", "Codex", "Cursor", "Trae", "Qoder", "QoderWork", "CodeBuddy", "OpenClaw", "Nanobot", "Pi", "Hermes Agent" Detected bool // CLI binary or global config dir found BinPath string // exec.LookPath result Installed bool // mnemon integration present at ConfigDir @@ -35,6 +35,7 @@ func DetectEnvironments(global bool) []Environment { detectTrae(global), detectQoder(global), detectQoderWork(), + detectCodeBuddy(global), detectOpenClaw(global), detectNanobot(global), detectPi(global), @@ -277,6 +278,47 @@ func detectQoderWork() Environment { return env } +func detectCodeBuddy(global bool) Environment { + home := HomeDir() + globalDir := filepath.Join(home, ".codebuddy") + localDir := ".codebuddy" + + configDir := localDir + if global { + configDir = globalDir + } + + env := Environment{ + Name: "codebuddy", + Display: "CodeBuddy", + ConfigDir: configDir, + } + + if binPath, err := exec.LookPath("codebuddy"); err == nil { + env.Detected = true + env.BinPath = binPath + } + if _, err := os.Stat(globalDir); err == nil { + env.Detected = true + } + + skillPath := filepath.Join(configDir, "skills", "mnemon", "SKILL.md") + settingsPath := filepath.Join(configDir, "settings.json") + if _, err := os.Stat(skillPath); err == nil { + env.Installed = true + } else if data, err := ReadJSONFile(settingsPath); err == nil && containsMnemon(data) { + env.Installed = true + } + + if env.BinPath != "" { + if out, err := exec.Command(env.BinPath, "--version").Output(); err == nil { + env.Version = cleanVersion(strings.TrimSpace(string(out))) + } + } + + return env +} + func detectOpenClaw(global bool) Environment { home := HomeDir() globalDir := filepath.Join(home, ".openclaw")