diff --git a/CHANGELOG.md b/CHANGELOG.md index 09b07861..e3ffc4f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Added + +- `mnemon setup --target zcode` now installs a ZCode-compatible Mnemon skill. + With `--global`, setup also registers user-level `SessionStart`, + `UserPromptSubmit`, and `Stop` process hooks in + `~/.zcode/cli/config.json`; project-local setup intentionally skips hooks + because ZCode does not execute project-level hook configuration. +- `mnemon setup --eject --target zcode` removes Mnemon-owned ZCode files and + hook entries while preserving unrelated skills, hook events, and settings. + +### Tests + +- Added ZCode coverage for embedded artifacts, hook registration, unrelated + configuration preservation, and scoped eject cleanup. + ## [0.1.15] - 2026-06-18 ### Added diff --git a/README.md b/README.md index bf83b768..21698d69 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,18 @@ One command deploys the mnemon skill, prompt files, and Cursor lifecycle hooks to `.cursor/`. The integration primes new agent sessions with Mnemon guidance and memory status, then nudges for durable-memory writeback after responses. +### [ZCode](https://zcode.z.ai/) + +```bash +mnemon setup --target zcode --global --yes +``` + +ZCode installs the Mnemon skill under `~/.zcode/skills/` and registers +user-level lifecycle hooks in `~/.zcode/cli/config.json`. The hooks prime new +sessions, add recall guidance before model calls, and prompt for durable-memory +writeback at stop. Without `--global`, setup installs only the project skill; +ZCode currently ignores project-level hook configuration. + ### [TRAE](https://www.trae.ai/) (TRAE Work) ```bash @@ -319,7 +331,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, CodeBuddy, WorkBuddy, Kimi Code, OpenCode, and Hermes Agent (hooks/plugins), OpenClaw (plugins), Pi (extensions), Nanobot (skills), DeepSeek Harness (via the dsh-mnemon plugin), and more +- **Multi-framework support** — Claude Code, Codex, Cursor, ZCode, TRAE/TRAE Work, Qoder/QoderWork, CodeBuddy, WorkBuddy, Kimi Code, OpenCode, and Hermes Agent (hooks/plugins), OpenClaw (plugins), Pi (extensions), Nanobot (skills), DeepSeek Harness (via the dsh-mnemon plugin), and more - **Runtime-native integration** — runtime-specific `SKILL.md`, shared `guide.md`, and supported hooks or extensions - **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 @@ -370,7 +382,7 @@ 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, +read and write. Claude Code, Codex, Cursor, ZCode, TRAE/TRAE Work, Qoder/QoderWork, CodeBuddy, WorkBuddy, Kimi Code, OpenCode, and Hermes Agent setup automate hook/plugin installation; OpenClaw can use plugin hooks; Pi integrates via native skills and TypeScript lifecycle extensions; Nanobot integrates via skill files; NanoClaw integrates diff --git a/cmd/memory/setup.go b/cmd/memory/setup.go index 4a6a8f4e..6bf19646 100644 --- a/cmd/memory/setup.go +++ b/cmd/memory/setup.go @@ -22,17 +22,18 @@ 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/, .codebuddy/, .workbuddy/, .opencode/, .openclaw/, .nanobot/, .pi/). -Use --global to install to user-wide config (~/.claude/, ~/.codex/, ~/.cursor/, ~/.trae/, ~/.qoder/, ~/.codebuddy/, ~/.workbuddy/, ~/.config/opencode/, ~/.openclaw/, ~/.nanobot/workspace/, ~/.pi/agent/). +By default, installs to project-local config (.claude/, .codex/, .cursor/, .zcode/, .trae/, .qoder/, .codebuddy/, .workbuddy/, .opencode/, .openclaw/, .nanobot/, .pi/). +Use --global to install to user-wide config (~/.claude/, ~/.codex/, ~/.cursor/, ~/.zcode/, ~/.trae/, ~/.qoder/, ~/.codebuddy/, ~/.workbuddy/, ~/.config/opencode/, ~/.openclaw/, ~/.nanobot/workspace/, ~/.pi/agent/). Hermes Agent, QoderWork, and Kimi Code use native user config at ~/.hermes/, ~/.qoderwork/, and ~/.kimi-code/. -Supported environments: Claude Code, Codex, Cursor, Trae, Qoder, QoderWork, CodeBuddy, WorkBuddy, Kimi Code, OpenCode, OpenClaw, Nanobot, Pi, Hermes Agent. +Supported environments: Claude Code, Codex, Cursor, ZCode, Trae, Qoder, QoderWork, CodeBuddy, WorkBuddy, Kimi Code, OpenCode, OpenClaw, Nanobot, Pi, Hermes Agent. Examples: mnemon setup # Interactive: project-local install mnemon setup --global # Interactive: user-wide install mnemon setup --target claude-code # Non-interactive: Claude Code only mnemon setup --target cursor # Non-interactive: Cursor skill only + mnemon setup --target zcode --global # Non-interactive: ZCode skill and user hooks 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 @@ -48,7 +49,7 @@ Examples: } func init() { - setupCmd.Flags().StringVar(&setupTarget, "target", "", "target environment (claude-code, codex, cursor, trae, qoder, qoderwork, codebuddy, workbuddy, kimi, opencode, openclaw, nanobot, pi, hermes)") + setupCmd.Flags().StringVar(&setupTarget, "target", "", "target environment (claude-code, codex, cursor, zcode, trae, qoder, qoderwork, codebuddy, workbuddy, kimi, opencode, 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") @@ -56,8 +57,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 != "codebuddy" && setupTarget != "workbuddy" && setupTarget != "kimi" && setupTarget != "opencode" && setupTarget != "openclaw" && setupTarget != "nanobot" && setupTarget != "pi" && setupTarget != "hermes" { - return fmt.Errorf("invalid target %q (must be claude-code, codex, cursor, trae, qoder, qoderwork, codebuddy, workbuddy, kimi, opencode, openclaw, nanobot, pi, or hermes)", setupTarget) + if setupTarget != "" && setupTarget != "claude-code" && setupTarget != "codex" && setupTarget != "cursor" && setupTarget != "zcode" && setupTarget != "trae" && setupTarget != "qoder" && setupTarget != "qoderwork" && setupTarget != "codebuddy" && setupTarget != "workbuddy" && setupTarget != "kimi" && setupTarget != "opencode" && setupTarget != "openclaw" && setupTarget != "nanobot" && setupTarget != "pi" && setupTarget != "hermes" { + return fmt.Errorf("invalid target %q (must be claude-code, codex, cursor, zcode, trae, qoder, qoderwork, codebuddy, workbuddy, kimi, opencode, openclaw, nanobot, pi, or hermes)", setupTarget) } envs := setup.DetectEnvironments(setupGlobal) @@ -93,7 +94,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, CodeBuddy, WorkBuddy, Kimi Code, OpenCode, OpenClaw, Nanobot, Pi, or Hermes Agent, then run 'mnemon setup' again.") + fmt.Println("Install Claude Code, Codex, Cursor, ZCode, Trae, Qoder, QoderWork, CodeBuddy, WorkBuddy, Kimi Code, OpenCode, OpenClaw, Nanobot, Pi, or Hermes Agent, then run 'mnemon setup' again.") return nil } @@ -139,6 +140,8 @@ func installEnv(env *setup.Environment) error { err = installCodex(env) case "cursor": err = installCursor(env) + case "zcode": + err = installZCode(env) case "trae": err = installTrae(env) case "qoder": @@ -510,6 +513,103 @@ func selectCursorOptionalHooks() setup.HookSelection { return sel } +// ─── ZCode ───────────────────────────────────────────────────────── + +func installZCode(env *setup.Environment) error { + configDir := env.ConfigDir + globalInstall := setupGlobal + + if !setupGlobal && !setupYes && setup.IsInteractive() { + home := setup.HomeDir() + localDir := ".zcode" + globalDir := home + "/.zcode" + idx := setup.SelectOne("Install scope", + []string{ + fmt.Sprintf("Local — skill for this project only (%s/)", localDir), + fmt.Sprintf("Global — skill and lifecycle hooks (%s/)", globalDir), + }, 0) + if idx == 1 { + configDir = globalDir + globalInstall = true + } else { + configDir = localDir + } + } + + totalSteps := 2 + if globalInstall { + totalSteps = 4 + } + fmt.Printf("\nSetting up ZCode (%s)...\n", configDir) + + fmt.Printf("\n[1/%d] Skill\n", totalSteps) + if path, err := setup.ZCodeWriteSkill(configDir); err != nil { + setup.StatusError(0, 0, "Skill", err) + return err + } else { + setup.StatusOK(0, 0, "Skill", path) + } + + fmt.Printf("\n[2/%d] Prompts\n", totalSteps) + 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 + } + + if !globalInstall { + fmt.Println() + fmt.Println("Setup complete!") + fmt.Printf(" Skill %s/skills/mnemon/SKILL.md\n", configDir) + fmt.Printf(" Prompts %s/ (guide.md, skill.md)\n", promptPath) + fmt.Println() + fmt.Println("ZCode currently ignores project-level hooks; use '--global' to install lifecycle hooks.") + fmt.Println("Refresh Settings → Skills or start a new ZCode session to activate.") + fmt.Println("Run 'mnemon setup --eject --target zcode' to remove.") + return nil + } + + fmt.Printf("\n[3/%d] Hooks\n", totalSteps) + hooks := []struct { + label, filename string + content []byte + }{ + {label: "Hook: prime", filename: "prime.sh", content: assets.ZCodePrimeHook}, + {label: "Hook: remind", filename: "user_prompt.sh", content: assets.ZCodeUserPromptHook}, + {label: "Hook: nudge", filename: "stop.sh", content: assets.ZCodeStopHook}, + } + for _, hook := range hooks { + if path, err := setup.ZCodeWriteHook(configDir, hook.filename, hook.content); err != nil { + setup.StatusError(0, 0, hook.label, err) + return err + } else { + setup.StatusOK(0, 0, hook.label, path) + } + } + + fmt.Printf("\n[4/%d] Config\n", totalSteps) + if path, err := setup.ZCodeRegisterHooks(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/cli/config.json (SessionStart, UserPromptSubmit, Stop)\n", configDir) + fmt.Printf(" Prompts %s/ (guide.md, skill.md)\n", promptPath) + fmt.Println() + fmt.Println("Start a new ZCode session to activate the mnemon skill and hooks.") + fmt.Println("Run 'mnemon setup --eject --target zcode --global' to remove.") + + return nil +} + // ─── Trae ─────────────────────────────────────────────────────────── func installTrae(env *setup.Environment) error { @@ -1407,6 +1507,12 @@ func ejectEnv(env *setup.Environment) error { return errs[0] } + case "zcode": + errs := setup.ZCodeEject(env.ConfigDir) + if len(errs) > 0 { + return errs[0] + } + case "trae": errs := setup.TraeEject(env.ConfigDir) ejectMarkdown("AGENTS.md", "Remove memory guidance from ./AGENTS.md?") diff --git a/docs/USAGE.md b/docs/USAGE.md index a1d894a0..46d8de4d 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -33,6 +33,7 @@ mnemon setup --global mnemon setup --target claude-code mnemon setup --target codex mnemon setup --target cursor +mnemon setup --target zcode --global mnemon setup --target trae mnemon setup --target qoder mnemon setup --target qoderwork @@ -55,8 +56,8 @@ 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/`; Kimi Code installs to `~/.kimi-code/` or `$KIMI_CODE_HOME/`; OpenCode installs to `~/.config/opencode/`) | -| `--target ` | (auto-detect) | Target environment: `claude-code`, `codex`, `cursor`, `trae`, `qoder`, `qoderwork`, `codebuddy`, `workbuddy`, `kimi`, `opencode`, `openclaw`, `nanobot`, `pi`, or `hermes` | +| `--global` | `false` | Install to user-wide config instead of project-local (required for ZCode lifecycle hooks; recommended for Nanobot: installs to `~/.nanobot/workspace/`; Pi installs to `~/.pi/agent/`; Hermes installs to `~/.hermes/`; QoderWork installs to `~/.qoderwork/`; Kimi Code installs to `~/.kimi-code/` or `$KIMI_CODE_HOME/`; OpenCode installs to `~/.config/opencode/`) | +| `--target ` | (auto-detect) | Target environment: `claude-code`, `codex`, `cursor`, `zcode`, `trae`, `qoder`, `qoderwork`, `codebuddy`, `workbuddy`, `kimi`, `opencode`, `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 f333861d..a8d09a06 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -107,6 +107,17 @@ mnemon setup `mnemon setup` 自动检测 Claude Code,交互式部署技能文件、钩子和行为引导。启动新会话 — 记忆自动运作。 +### [ZCode](https://zcode.z.ai/) + +```bash +mnemon setup --target zcode --global --yes +``` + +ZCode 会将 Mnemon skill 安装到 `~/.zcode/skills/`,并在 +`~/.zcode/cli/config.json` 中注册用户级生命周期 hooks。新会话会注入记忆状态, +模型调用前会收到 recall 指引,停止时会评估 durable-memory 回写。不加 +`--global` 时只安装项目 skill,因为 ZCode 当前不会执行项目级 hooks 配置。 + ### [TRAE](https://www.trae.ai/) (TRAE Work) ```bash @@ -283,7 +294,7 @@ store 可见。**Remind** 触发 recall 判断。**Nudge** 触发 writeback 判 - **零用户操作** — 安装一次;支持 hook 的 runtime 可用 hook,minimal runtime 可用持久规则 - **LLM 监督式** — 宿主 LLM 主动决定记什么、更新什么、遗忘什么;无内嵌 LLM,无 API 密钥 -- **多框架支持** — Claude Code、Codex、Cursor、TRAE/TRAE Work、Qoder/QoderWork、CodeBuddy、WorkBuddy、Kimi Code、OpenCode 和 Hermes Agent(hooks/plugins)、OpenClaw(plugins)、Pi(extensions)、Nanobot(skills)、DeepSeek Harness(通过 dsh-mnemon 插件)等 +- **多框架支持** — Claude Code、Codex、Cursor、ZCode、TRAE/TRAE Work、Qoder/QoderWork、CodeBuddy、WorkBuddy、Kimi Code、OpenCode 和 Hermes Agent(hooks/plugins)、OpenClaw(plugins)、Pi(extensions)、Nanobot(skills)、DeepSeek Harness(通过 dsh-mnemon 插件)等 - **Runtime 原生集成** — 各 runtime 的 `SKILL.md`、共享 `guide.md`,以及受支持的 hook 或 extension - **四图架构** — 时序、实体、因果、语义四种边,不仅仅是向量相似度 - **意图原生协议** — 三个原语(`remember`、`link`、`recall`)映射到 LLM 的认知词汇而非数据库语法;结构化 JSON 输出,带信号透明度 diff --git a/docs/zh/USAGE.md b/docs/zh/USAGE.md index 9078b8f9..ed9ced77 100644 --- a/docs/zh/USAGE.md +++ b/docs/zh/USAGE.md @@ -33,6 +33,7 @@ mnemon setup --global mnemon setup --target claude-code mnemon setup --target codex mnemon setup --target cursor +mnemon setup --target zcode --global mnemon setup --target trae mnemon setup --target qoder mnemon setup --target qoderwork @@ -55,8 +56,8 @@ mnemon setup --eject --target claude-code | 标志 | 默认值 | 说明 | |---|---|---| -| `--global` | `false` | 安装到用户级配置而非项目本地(Nanobot 推荐安装到 `~/.nanobot/workspace/`;Pi 安装到 `~/.pi/agent/`;Hermes 安装到 `~/.hermes/`;QoderWork 安装到 `~/.qoderwork/`;Kimi Code 安装到 `~/.kimi-code/` 或 `$KIMI_CODE_HOME/`;OpenCode 安装到 `~/.config/opencode/`) | -| `--target ` | (自动检测) | 目标环境:`claude-code`、`codex`、`cursor`、`trae`、`qoder`、`qoderwork`、`codebuddy`、`workbuddy`、`kimi`、`opencode`、`openclaw`、`nanobot`、`pi` 或 `hermes` | +| `--global` | `false` | 安装到用户级配置而非项目本地(ZCode 生命周期 hooks 必须使用;Nanobot 推荐安装到 `~/.nanobot/workspace/`;Pi 安装到 `~/.pi/agent/`;Hermes 安装到 `~/.hermes/`;QoderWork 安装到 `~/.qoderwork/`;Kimi Code 安装到 `~/.kimi-code/` 或 `$KIMI_CODE_HOME/`;OpenCode 安装到 `~/.config/opencode/`) | +| `--target ` | (自动检测) | 目标环境:`claude-code`、`codex`、`cursor`、`zcode`、`trae`、`qoder`、`qoderwork`、`codebuddy`、`workbuddy`、`kimi`、`opencode`、`openclaw`、`nanobot`、`pi` 或 `hermes` | | `--eject` | `false` | 移除 mnemon 集成 | | `--yes` | `false` | 自动确认所有提示 | diff --git a/internal/memory/setup/assets/assets.go b/internal/memory/setup/assets/assets.go index 2aa6d2ad..a3fe0159 100644 --- a/internal/memory/setup/assets/assets.go +++ b/internal/memory/setup/assets/assets.go @@ -44,6 +44,18 @@ var CursorStopHook []byte //go:embed cursor/compact.sh var CursorCompactHook []byte +//go:embed zcode/SKILL.md +var ZCodeSkill []byte + +//go:embed zcode/prime.sh +var ZCodePrimeHook []byte + +//go:embed zcode/user_prompt.sh +var ZCodeUserPromptHook []byte + +//go:embed zcode/stop.sh +var ZCodeStopHook []byte + //go:embed trae/SKILL.md var TraeSkill []byte @@ -163,5 +175,5 @@ var HermesCompactHook []byte // All returns the embedded filesystem for inspection. // -//go:embed claude codex cursor trae qoder qoderwork codebuddy workbuddy kimi opencode openclaw nanoclaw nanobot pi hermes +//go:embed claude codex cursor zcode trae qoder qoderwork codebuddy workbuddy kimi opencode openclaw nanoclaw nanobot pi hermes var All embed.FS diff --git a/internal/memory/setup/assets/zcode/SKILL.md b/internal/memory/setup/assets/zcode/SKILL.md new file mode 100644 index 00000000..c6a0d03c --- /dev/null +++ b/internal/memory/setup/assets/zcode/SKILL.md @@ -0,0 +1,57 @@ +--- +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 "" --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 +``` + +## Import Historical Chats + +When the user asks to import old chats, notes, or exported context, create a +`memory_draft.json` with `schema_version: "1"`, `insights` entries containing +`content`, `category`, `importance`, `tags`, `entities`, and optional +`created_at`, plus optional `edges` using `source_index`, `target_index`, +`edge_type`, `weight`, and `reason`. Run `mnemon import --dry-run `, +then run `mnemon import ` only after validation passes. After import, +verify with `mnemon status` and a focused `mnemon search` or `mnemon recall`. +Check the output `errors` field because imports can partially succeed. + +## 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/memory/setup/assets/zcode/prime.sh b/internal/memory/setup/assets/zcode/prime.sh new file mode 100644 index 00000000..0b9f59da --- /dev/null +++ b/internal/memory/setup/assets/zcode/prime.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# mnemon ZCode SessionStart hook. + +cat >/dev/null || true + +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 + +resolve_mnemon() { + if [ -n "${MNEMON_BIN:-}" ] && [ -x "${MNEMON_BIN}" ]; then + printf "%s" "${MNEMON_BIN}" + return 0 + fi + if command -v mnemon >/dev/null 2>&1; then + command -v mnemon + return 0 + fi + if [ -x "${HOME}/go/bin/mnemon" ]; then + printf "%s" "${HOME}/go/bin/mnemon" + return 0 + fi + return 1 +} + +{ + if MNEMON="$(resolve_mnemon)"; then + STATS="$("${MNEMON}" status 2>/dev/null || true)" + 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 + else + echo "[mnemon] Warning: mnemon not found in PATH. Set MNEMON_BIN or add mnemon to PATH." + fi + + if [ -f "${PROMPT_DIR}/guide.md" ]; then + cat "${PROMPT_DIR}/guide.md" + fi +} | python3 -c 'import json, sys; text = sys.stdin.read().strip(); print(json.dumps({"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": text}} if text else {}))' diff --git a/internal/memory/setup/assets/zcode/stop.sh b/internal/memory/setup/assets/zcode/stop.sh new file mode 100644 index 00000000..8786ff3a --- /dev/null +++ b/internal/memory/setup/assets/zcode/stop.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# mnemon ZCode Stop hook. Continue at most once so the agent can evaluate writeback. + +python3 -c ' +import json +import sys + +try: + payload = json.load(sys.stdin) +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({ + "decision": "block", + "reason": ( + "[mnemon] Briefly evaluate whether this exchange warrants durable memory. " + "If yes, use the mnemon skill/CLI to remember only durable, non-secret facts; " + "otherwise say no durable memory is needed." + ), +})) +' diff --git a/internal/memory/setup/assets/zcode/user_prompt.sh b/internal/memory/setup/assets/zcode/user_prompt.sh new file mode 100644 index 00000000..bc267e7e --- /dev/null +++ b/internal/memory/setup/assets/zcode/user_prompt.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# mnemon ZCode UserPromptSubmit hook. + +python3 -c 'import json, sys; sys.stdin.read(); print(json.dumps({"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": "[mnemon] Evaluate: recall needed? After responding, evaluate: remember needed?"}}))' diff --git a/internal/memory/setup/detect.go b/internal/memory/setup/detect.go index abfe1876..27a8e96b 100644 --- a/internal/memory/setup/detect.go +++ b/internal/memory/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", "codebuddy", "workbuddy", "kimi", "opencode", "openclaw", "nanobot", "pi", "hermes" - Display string // "Claude Code", "Codex", "Cursor", "Trae", "Qoder", "QoderWork", "CodeBuddy", "WorkBuddy", "Kimi Code", "OpenCode", "OpenClaw", "Nanobot", "Pi", "Hermes Agent" + Name string // "claude-code", "codex", "cursor", "zcode", "trae", "qoder", "qoderwork", "codebuddy", "workbuddy", "kimi", "opencode", "openclaw", "nanobot", "pi", "hermes" + Display string // "Claude Code", "Codex", "Cursor", "ZCode", "Trae", "Qoder", "QoderWork", "CodeBuddy", "WorkBuddy", "Kimi Code", "OpenCode", "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 @@ -32,6 +32,7 @@ func DetectEnvironments(global bool) []Environment { detectClaude(global), detectCodex(global), detectCursor(global), + detectZCode(global), detectTrae(global), detectQoder(global), detectQoderWork(), @@ -46,6 +47,47 @@ func DetectEnvironments(global bool) []Environment { } } +func detectZCode(global bool) Environment { + home := HomeDir() + globalDir := filepath.Join(home, ".zcode") + localDir := ".zcode" + + configDir := localDir + if global { + configDir = globalDir + } + + env := Environment{ + Name: "zcode", + Display: "ZCode", + ConfigDir: configDir, + } + + if binPath, err := exec.LookPath("zcode"); 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") + hooksPath := filepath.Join(configDir, "cli", "config.json") + if _, err := os.Stat(skillPath); err == nil { + env.Installed = true + } else if data, err := ReadJSONFile(hooksPath); 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 detectClaude(global bool) Environment { home := HomeDir() globalDir := filepath.Join(home, ".claude") diff --git a/internal/memory/setup/host_artifacts_test.go b/internal/memory/setup/host_artifacts_test.go index 260d7374..fe1ee5e3 100644 --- a/internal/memory/setup/host_artifacts_test.go +++ b/internal/memory/setup/host_artifacts_test.go @@ -24,6 +24,7 @@ func TestHostSkillAndHookArtifacts(t *testing.T) { {name: "Kimi", skill: assets.KimiSkill, writeSkill: KimiWriteSkill, writeHook: KimiWriteHook}, {name: "Trae", skill: assets.TraeSkill, writeSkill: TraeWriteSkill, writeHook: TraeWriteHook}, {name: "WorkBuddy", skill: assets.WorkBuddySkill, writeSkill: WorkBuddyWriteSkill, writeHook: WorkBuddyWriteHook}, + {name: "ZCode", skill: assets.ZCodeSkill, writeSkill: ZCodeWriteSkill, writeHook: ZCodeWriteHook}, } for _, test := range tests { diff --git a/internal/memory/setup/zcode.go b/internal/memory/setup/zcode.go new file mode 100644 index 00000000..d0800d32 --- /dev/null +++ b/internal/memory/setup/zcode.go @@ -0,0 +1,172 @@ +package setup + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/mnemon-dev/mnemon/internal/memory/setup/assets" +) + +// ZCodeWriteSkill writes the mnemon skill to the ZCode skills directory. +func ZCodeWriteSkill(configDir string) (string, error) { + skillDir := filepath.Join(configDir, "skills", "mnemon") + if err := os.MkdirAll(skillDir, 0o755); err != nil { + return "", err + } + skillPath := filepath.Join(skillDir, "SKILL.md") + if err := os.WriteFile(skillPath, assets.ZCodeSkill, 0o644); err != nil { + return "", err + } + return skillPath, nil +} + +// ZCodeWriteHook writes a hook script to the ZCode hooks directory. +func ZCodeWriteHook(configDir, filename string, content []byte) (string, error) { + hooksDir := filepath.Join(configDir, "hooks", "mnemon") + if err := os.MkdirAll(hooksDir, 0o755); err != nil { + return "", err + } + hookPath := filepath.Join(hooksDir, filename) + if err := writeExecutableFile(hookPath, content); err != nil { + return "", err + } + return hookPath, nil +} + +// ZCodeRegisterHooks registers user-level Mnemon hooks in cli/config.json. +func ZCodeRegisterHooks(configDir string) (string, error) { + hooksDir := filepath.Join(configDir, "hooks", "mnemon") + absHooksDir, err := filepath.Abs(hooksDir) + if err != nil { + return "", err + } + configPath := filepath.Join(configDir, "cli", "config.json") + data, err := ReadJSONFile(configPath) + if err != nil { + return "", err + } + addZCodeHooks(data, absHooksDir) + if err := WriteJSONFile(configPath, data); err != nil { + return "", err + } + return configPath, nil +} + +// ZCodeEject removes the Mnemon skill and user-level hooks from ZCode. +func ZCodeEject(configDir string) []error { + var errs []error + + fmt.Printf("\nRemoving ZCode 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")) + + configPath := filepath.Join(configDir, "cli", "config.json") + data, err := ReadJSONFile(configPath) + if err != nil { + StatusError(2, 3, "Hooks config", err) + errs = append(errs, err) + } else { + removeZCodeHooks(data) + if err := WriteOrRemoveJSONFile(configPath, data); err != nil { + StatusError(2, 3, "Hooks config", err) + errs = append(errs, err) + } else { + StatusOK(2, 3, "Hooks config", configPath+" cleaned") + } + } + removeIfEmpty(filepath.Join(configDir, "cli")) + + 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 addZCodeHooks(data map[string]interface{}, hooksDir string) { + removeZCodeHooks(data) + hooks := ensureHooksMap(data) + hooks["enabled"] = true + + events, ok := hooks["events"].(map[string]interface{}) + if !ok { + events = make(map[string]interface{}) + hooks["events"] = events + } + + events["SessionStart"] = appendZCodeHook(events["SessionStart"], map[string]interface{}{ + "matcher": "startup|clear|compact", + "hooks": []interface{}{ + zcodeProcessHook(filepath.Join(hooksDir, "prime.sh"), "Loading Mnemon context"), + }, + }) + events["UserPromptSubmit"] = appendZCodeHook(events["UserPromptSubmit"], map[string]interface{}{ + "hooks": []interface{}{ + zcodeProcessHook(filepath.Join(hooksDir, "user_prompt.sh"), "Checking Mnemon recall guidance"), + }, + }) + events["Stop"] = appendZCodeHook(events["Stop"], map[string]interface{}{ + "hooks": []interface{}{ + zcodeProcessHook(filepath.Join(hooksDir, "stop.sh"), "Checking Mnemon writeback guidance"), + }, + }) +} + +func appendZCodeHook(current interface{}, entry map[string]interface{}) []interface{} { + entries, _ := current.([]interface{}) + return append(entries, entry) +} + +func zcodeProcessHook(scriptPath, status string) map[string]interface{} { + return map[string]interface{}{ + "type": "process", + "command": "bash", + "args": []interface{}{scriptPath}, + "enabled": true, + "timeoutMs": 30000, + "statusMessage": status, + } +} + +func removeZCodeHooks(data map[string]interface{}) { + hooks, ok := data["hooks"].(map[string]interface{}) + if !ok { + return + } + events, ok := hooks["events"].(map[string]interface{}) + if !ok { + return + } + for _, key := range []string{"SessionStart", "UserPromptSubmit", "Stop"} { + entries, ok := events[key].([]interface{}) + if !ok { + continue + } + filtered := filterHookArray(entries) + if len(filtered) == 0 { + delete(events, key) + } else { + events[key] = filtered + } + } + if len(events) == 0 { + delete(hooks, "events") + } + if len(hooks) == 1 && hooks["enabled"] == true { + delete(data, "hooks") + } +} diff --git a/internal/memory/setup/zcode_test.go b/internal/memory/setup/zcode_test.go new file mode 100644 index 00000000..b1d41b64 --- /dev/null +++ b/internal/memory/setup/zcode_test.go @@ -0,0 +1,131 @@ +package setup + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestZCodeRegisterHooksPreservesUnrelatedConfig(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, "cli", "config.json") + if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil { + t.Fatalf("mkdir config dir: %v", err) + } + if err := os.WriteFile(configPath, []byte(`{ + "theme": "dark", + "hooks": { + "enabled": false, + "timeoutMs": 15000, + "events": { + "SessionStart": [ + {"hooks": [{"type": "process", "command": "bash", "args": ["/old/mnemon/prime.sh"]}]}, + {"hooks": [{"type": "process", "command": "node", "args": ["/keep/custom.mjs"]}]} + ], + "PreToolUse": [ + {"matcher": "Write", "hooks": [{"type": "process", "command": "node", "args": ["/keep/check.mjs"]}]} + ] + } + } +}`), 0o644); err != nil { + t.Fatalf("write config: %v", err) + } + + if _, err := ZCodeRegisterHooks(dir); err != nil { + t.Fatalf("register hooks: %v", err) + } + + data, err := ReadJSONFile(configPath) + if err != nil { + t.Fatalf("read config: %v", err) + } + if data["theme"] != "dark" { + t.Fatalf("unrelated root config should be preserved: %#v", data) + } + hooks := data["hooks"].(map[string]interface{}) + if hooks["enabled"] != true || hooks["timeoutMs"].(float64) != 15000 { + t.Fatalf("hooks settings not preserved/enabled: %#v", hooks) + } + events := hooks["events"].(map[string]interface{}) + if _, ok := events["PreToolUse"]; !ok { + t.Fatalf("custom event should be preserved: %#v", events) + } + sessionStart := events["SessionStart"].([]interface{}) + if len(sessionStart) != 2 { + t.Fatalf("expected custom hook plus new prime hook: %#v", sessionStart) + } + newHook := sessionStart[1].(map[string]interface{})["hooks"].([]interface{})[0].(map[string]interface{}) + args := newHook["args"].([]interface{}) + if newHook["type"] != "process" || newHook["command"] != "bash" || len(args) != 1 || !strings.Contains(args[0].(string), "hooks/mnemon/prime.sh") { + t.Fatalf("unexpected ZCode process hook: %#v", newHook) + } + for _, event := range []string{"UserPromptSubmit", "Stop"} { + if _, ok := events[event]; !ok { + t.Fatalf("missing %s hook: %#v", event, events) + } + } +} + +func TestZCodeEjectRemovesOnlyMnemonFilesAndHooks(t *testing.T) { + dir := t.TempDir() + if _, err := ZCodeWriteSkill(dir); err != nil { + t.Fatalf("write skill: %v", err) + } + if _, err := ZCodeWriteHook(dir, "prime.sh", []byte("#!/bin/bash\n")); err != nil { + t.Fatalf("write hook: %v", err) + } + if _, err := ZCodeRegisterHooks(dir); err != nil { + t.Fatalf("register hooks: %v", err) + } + + customSkillDir := filepath.Join(dir, "skills", "custom") + if err := os.MkdirAll(customSkillDir, 0o755); err != nil { + t.Fatalf("create custom skill: %v", err) + } + configPath := filepath.Join(dir, "cli", "config.json") + data, err := ReadJSONFile(configPath) + if err != nil { + t.Fatalf("read config: %v", err) + } + hooks := data["hooks"].(map[string]interface{}) + events := hooks["events"].(map[string]interface{}) + events["SessionStart"] = append(events["SessionStart"].([]interface{}), map[string]interface{}{ + "hooks": []interface{}{map[string]interface{}{ + "type": "process", "command": "node", "args": []interface{}{`/keep/custom.mjs`}, + }}, + }) + if err := WriteJSONFile(configPath, data); err != nil { + t.Fatalf("write config: %v", err) + } + + if errs := ZCodeEject(dir); 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(configPath) + if err != nil { + t.Fatalf("read config after eject: %v", err) + } + hooks = data["hooks"].(map[string]interface{}) + events = hooks["events"].(map[string]interface{}) + sessionStart := events["SessionStart"].([]interface{}) + if len(sessionStart) != 1 || containsMnemon(sessionStart[0]) { + t.Fatalf("custom hook should be preserved and mnemon removed: %#v", sessionStart) + } + if _, ok := events["UserPromptSubmit"]; ok { + t.Fatalf("user prompt hook should be removed: %#v", events) + } + if _, ok := events["Stop"]; ok { + t.Fatalf("stop hook should be removed: %#v", events) + } +}