diff --git a/README.md b/README.md index cde3ac9..0765e61 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,16 @@ WorkBuddy deploys the mnemon skill, prompt files, and native hooks to `.workbuddy/` or `~/.workbuddy/`. The integration registers `SessionStart`, `UserPromptSubmit`, and `Stop` hooks in `settings.json`. +### [Kimi Code](https://github.com/MoonshotAI/kimi-code) + +```bash +mnemon setup --target kimi --yes +``` + +Kimi Code deploys the mnemon skill, prompt files, and native lifecycle hooks to +`~/.kimi-code/` or `$KIMI_CODE_HOME/`. The integration registers +`SessionStart`, `UserPromptSubmit`, and `Stop` hooks in `config.toml`. + ### [OpenClaw](https://github.com/openclaw/openclaw) ```bash @@ -253,7 +263,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, 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, WorkBuddy, Kimi Code, 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 @@ -286,6 +296,8 @@ All your local agentic AIs — across sessions and frameworks — sharing one po │ WorkBuddy ────┤ │ + Kimi Code ────┤ + │ Hermes Agent ─┤ │ OpenClaw ─────┤ @@ -303,7 +315,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, -CodeBuddy, WorkBuddy, and Hermes Agent setup automate hook installation; +CodeBuddy, WorkBuddy, Kimi Code, 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 diff --git a/cmd/setup.go b/cmd/setup.go index 7ca4969..f0c0f05 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -24,9 +24,9 @@ var setupCmd = &cobra.Command{ By default, installs to project-local config (.claude/, .codex/, .cursor/, .trae/, .qoder/, .codebuddy/, .workbuddy/, .openclaw/, .nanobot/, .pi/). Use --global to install to user-wide config (~/.claude/, ~/.codex/, ~/.cursor/, ~/.trae/, ~/.qoder/, ~/.codebuddy/, ~/.workbuddy/, ~/.openclaw/, ~/.nanobot/workspace/, ~/.pi/agent/). -Hermes Agent and QoderWork use their native user config at ~/.hermes/ and ~/.qoderwork/. +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, OpenClaw, Nanobot, Pi, Hermes Agent. +Supported environments: Claude Code, Codex, Cursor, Trae, Qoder, QoderWork, CodeBuddy, WorkBuddy, Kimi Code, OpenClaw, Nanobot, Pi, Hermes Agent. Examples: mnemon setup # Interactive: project-local install @@ -38,6 +38,7 @@ Examples: mnemon setup --target qoderwork # Non-interactive: QoderWork skill and hooks mnemon setup --target codebuddy # Non-interactive: CodeBuddy skill and hooks mnemon setup --target workbuddy # Non-interactive: WorkBuddy skill and hooks + mnemon setup --target kimi # Non-interactive: Kimi Code 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 @@ -46,7 +47,7 @@ Examples: } func init() { - setupCmd.Flags().StringVar(&setupTarget, "target", "", "target environment (claude-code, codex, cursor, trae, qoder, qoderwork, codebuddy, workbuddy, openclaw, nanobot, pi, hermes)") + setupCmd.Flags().StringVar(&setupTarget, "target", "", "target environment (claude-code, codex, cursor, trae, qoder, qoderwork, codebuddy, workbuddy, kimi, 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") @@ -54,8 +55,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 != "openclaw" && setupTarget != "nanobot" && setupTarget != "pi" && setupTarget != "hermes" { - return fmt.Errorf("invalid target %q (must be claude-code, codex, cursor, trae, qoder, qoderwork, codebuddy, workbuddy, openclaw, nanobot, pi, or hermes)", setupTarget) + if setupTarget != "" && setupTarget != "claude-code" && setupTarget != "codex" && setupTarget != "cursor" && setupTarget != "trae" && setupTarget != "qoder" && setupTarget != "qoderwork" && setupTarget != "codebuddy" && setupTarget != "workbuddy" && setupTarget != "kimi" && 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, openclaw, nanobot, pi, or hermes)", setupTarget) } envs := setup.DetectEnvironments(setupGlobal) @@ -91,7 +92,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, OpenClaw, Nanobot, Pi, or Hermes Agent, then run 'mnemon setup' again.") + fmt.Println("Install Claude Code, Codex, Cursor, Trae, Qoder, QoderWork, CodeBuddy, WorkBuddy, Kimi Code, OpenClaw, Nanobot, Pi, or Hermes Agent, then run 'mnemon setup' again.") return nil } @@ -147,6 +148,8 @@ func installEnv(env *setup.Environment) error { err = installCodeBuddy(env) case "workbuddy": err = installWorkBuddy(env) + case "kimi": + err = installKimi(env) case "openclaw": err = installOpenClaw(env) case "nanobot": @@ -838,6 +841,67 @@ func installWorkBuddy(env *setup.Environment) error { return nil } +// ─── Kimi Code ────────────────────────────────────────────────────── + +func installKimi(env *setup.Environment) error { + configDir := env.ConfigDir + + fmt.Printf("\nSetting up Kimi Code (%s)...\n", configDir) + + fmt.Println("\n[1/3] Skill") + if path, err := setup.KimiWriteSkill(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.KimiPrimeHook}, + {"Hook: remind", "user_prompt.sh", assets.KimiUserPromptHook}, + {"Hook: nudge", "stop.sh", assets.KimiStopHook}, + } { + if path, err := setup.KimiWriteHook(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.KimiRegisterHooks(configDir); err != nil { + setup.StatusError(0, 0, "Config", err) + return err + } else { + setup.StatusUpdated(0, 0, "Config", path) + } + + fmt.Println() + fmt.Println("Setup complete!") + fmt.Printf(" Skill %s/skills/mnemon/SKILL.md\n", configDir) + fmt.Printf(" Hooks %s/config.toml (SessionStart, UserPromptSubmit, Stop)\n", configDir) + fmt.Printf(" Prompts %s/ (guide.md, skill.md)\n", promptPath) + fmt.Println() + fmt.Println("Restart Kimi Code to activate the mnemon skill and hooks.") + fmt.Println("Run 'mnemon setup --eject --target kimi' to remove.") + + return nil +} + // ─── OpenClaw ─────────────────────────────────────────────────────── func installOpenClaw(env *setup.Environment) error { @@ -1307,6 +1371,13 @@ func ejectEnv(env *setup.Environment) error { return errs[0] } + case "kimi": + errs := setup.KimiEject(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 315f2a7..dcc80ea 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -36,6 +36,7 @@ mnemon setup --target qoder mnemon setup --target qoderwork mnemon setup --target codebuddy mnemon setup --target workbuddy +mnemon setup --target kimi mnemon setup --target openclaw mnemon setup --target pi mnemon setup --target nanobot --global @@ -51,8 +52,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/`) | -| `--target ` | (auto-detect) | Target environment: `claude-code`, `codex`, `cursor`, `trae`, `qoder`, `qoderwork`, `codebuddy`, `workbuddy`, `openclaw`, `nanobot`, `pi`, or `hermes` | +| `--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/`) | +| `--target ` | (auto-detect) | Target environment: `claude-code`, `codex`, `cursor`, `trae`, `qoder`, `qoderwork`, `codebuddy`, `workbuddy`, `kimi`, `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 a38d0d9..112ceb3 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -133,6 +133,16 @@ WorkBuddy 会将 mnemon skill、prompt 文件和原生 hooks 部署到 `.workbud 或 `~/.workbuddy/`。该集成会在 `settings.json` 中注册 `SessionStart`、 `UserPromptSubmit` 和 `Stop` hooks。 +### [Kimi Code](https://github.com/MoonshotAI/kimi-code) + +```bash +mnemon setup --target kimi --yes +``` + +Kimi Code 会将 mnemon skill、prompt 文件和原生生命周期 hooks 部署到 +`~/.kimi-code/` 或 `$KIMI_CODE_HOME/`。该集成会在 `config.toml` 中注册 +`SessionStart`、`UserPromptSubmit` 和 `Stop` hooks。 + ### [OpenClaw](https://github.com/openclaw/openclaw) ```bash @@ -216,7 +226,7 @@ Agent 工作,并且只在有用时调用 Mnemon - **零用户操作** — 安装一次;支持 hook 的 runtime 可用 hook,minimal runtime 可用持久规则 - **LLM 监督式** — 宿主 LLM 主动决定记什么、更新什么、遗忘什么;无内嵌 LLM,无 API 密钥 -- **多框架支持** — Claude Code、Codex、Cursor、TRAE/TRAE Work、Qoder/QoderWork、CodeBuddy、WorkBuddy 和 Hermes Agent(hooks)、OpenClaw(plugins)、Pi(extensions)、Nanobot(skills)等 +- **多框架支持** — Claude Code、Codex、Cursor、TRAE/TRAE Work、Qoder/QoderWork、CodeBuddy、WorkBuddy、Kimi Code 和 Hermes Agent(hooks)、OpenClaw(plugins)、Pi(extensions)、Nanobot(skills)等 - **Markdown 可安装 harness** — `SKILL.md`、`INSTALL.md`、`GUIDELINE.md` 和四个生命周期提醒 - **四图架构** — 时序、实体、因果、语义四种边,不仅仅是向量相似度 - **意图原生协议** — 三个原语(`remember`、`link`、`recall`)映射到 LLM 的认知词汇而非数据库语法;结构化 JSON 输出,带信号透明度 @@ -248,6 +258,8 @@ Agent 工作,并且只在有用时调用 Mnemon │ WorkBuddy ────┤ │ + Kimi Code ────┤ + │ Hermes Agent ─┤ │ OpenClaw ─────┤ @@ -263,7 +275,7 @@ Agent 工作,并且只在有用时调用 Mnemon Gemini CLI ───┘ ``` -基础已就绪:一个 `~/.mnemon` 数据库,任何 agent 都可以读写。Claude Code、Codex、Cursor、TRAE/TRAE Work、Qoder/QoderWork、CodeBuddy、WorkBuddy 和 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、WorkBuddy、Kimi Code 和 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 d9c8534..9fa4f4d 100644 --- a/docs/zh/USAGE.md +++ b/docs/zh/USAGE.md @@ -36,6 +36,7 @@ mnemon setup --target qoder mnemon setup --target qoderwork mnemon setup --target codebuddy mnemon setup --target workbuddy +mnemon setup --target kimi mnemon setup --target openclaw mnemon setup --target pi mnemon setup --target nanobot --global @@ -51,8 +52,8 @@ 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`、`codebuddy`、`workbuddy`、`openclaw`、`nanobot`、`pi` 或 `hermes` | +| `--global` | `false` | 安装到用户级配置而非项目本地(Nanobot 推荐安装到 `~/.nanobot/workspace/`;Pi 安装到 `~/.pi/agent/`;Hermes 安装到 `~/.hermes/`;QoderWork 安装到 `~/.qoderwork/`;Kimi Code 安装到 `~/.kimi-code/` 或 `$KIMI_CODE_HOME/`) | +| `--target ` | (自动检测) | 目标环境:`claude-code`、`codex`、`cursor`、`trae`、`qoder`、`qoderwork`、`codebuddy`、`workbuddy`、`kimi`、`openclaw`、`nanobot`、`pi` 或 `hermes` | | `--eject` | `false` | 移除 mnemon 集成 | | `--yes` | `false` | 自动确认所有提示 | diff --git a/internal/setup/assets/assets.go b/internal/setup/assets/assets.go index ca8362a..caacd33 100644 --- a/internal/setup/assets/assets.go +++ b/internal/setup/assets/assets.go @@ -95,6 +95,18 @@ var WorkBuddyUserPromptHook []byte //go:embed workbuddy/stop.sh var WorkBuddyStopHook []byte +//go:embed kimi/SKILL.md +var KimiSkill []byte + +//go:embed kimi/prime.sh +var KimiPrimeHook []byte + +//go:embed kimi/user_prompt.sh +var KimiUserPromptHook []byte + +//go:embed kimi/stop.sh +var KimiStopHook []byte + //go:embed openclaw/SKILL.md var OpenClawSkill []byte @@ -145,5 +157,5 @@ var HermesCompactHook []byte // All returns the embedded filesystem for inspection. // -//go:embed claude codex cursor trae qoder qoderwork codebuddy workbuddy openclaw nanoclaw nanobot pi hermes +//go:embed claude codex cursor trae qoder qoderwork codebuddy workbuddy kimi openclaw nanoclaw nanobot pi hermes var All embed.FS diff --git a/internal/setup/assets/kimi/SKILL.md b/internal/setup/assets/kimi/SKILL.md new file mode 100644 index 0000000..00f756c --- /dev/null +++ b/internal/setup/assets/kimi/SKILL.md @@ -0,0 +1,46 @@ +--- +name: mnemon +description: Persistent memory CLI for Kimi Code. 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/kimi/prime.sh b/internal/setup/assets/kimi/prime.sh new file mode 100644 index 0000000..694a2d0 --- /dev/null +++ b/internal/setup/assets/kimi/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/kimi/stop.sh b/internal/setup/assets/kimi/stop.sh new file mode 100644 index 0000000..3b1f022 --- /dev/null +++ b/internal/setup/assets/kimi/stop.sh @@ -0,0 +1,28 @@ +#!/bin/bash +INPUT=$(cat) + +MSG=$(echo "$INPUT" | sed -n 's/.*"last_assistant_message"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) +if echo "$MSG" | grep -qiE "mnemon remember|mnemon recall|mnemon link|Stored.*imp="; 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}/kimi-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 +echo "[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." >&2 +exit 2 diff --git a/internal/setup/assets/kimi/user_prompt.sh b/internal/setup/assets/kimi/user_prompt.sh new file mode 100644 index 0000000..6e5d6d2 --- /dev/null +++ b/internal/setup/assets/kimi/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/detect.go b/internal/setup/detect.go index 95b41ae..05c7380 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", "codebuddy", "workbuddy", "openclaw", "nanobot", "pi", "hermes" - Display string // "Claude Code", "Codex", "Cursor", "Trae", "Qoder", "QoderWork", "CodeBuddy", "WorkBuddy", "OpenClaw", "Nanobot", "Pi", "Hermes Agent" + Name string // "claude-code", "codex", "cursor", "trae", "qoder", "qoderwork", "codebuddy", "workbuddy", "kimi", "openclaw", "nanobot", "pi", "hermes" + Display string // "Claude Code", "Codex", "Cursor", "Trae", "Qoder", "QoderWork", "CodeBuddy", "WorkBuddy", "Kimi Code", "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 @@ -37,6 +37,7 @@ func DetectEnvironments(global bool) []Environment { detectQoderWork(), detectCodeBuddy(global), detectWorkBuddy(global), + detectKimi(), detectOpenClaw(global), detectNanobot(global), detectPi(global), @@ -361,6 +362,44 @@ func detectWorkBuddy(global bool) Environment { return env } +func detectKimi() Environment { + home := HomeDir() + configDir := filepath.Join(home, ".kimi-code") + if envHome := strings.TrimSpace(os.Getenv("KIMI_CODE_HOME")); envHome != "" { + configDir = envHome + } + + env := Environment{ + Name: "kimi", + Display: "Kimi Code", + ConfigDir: configDir, + } + + if binPath, err := exec.LookPath("kimi"); err == nil { + env.Detected = true + env.BinPath = binPath + } + if _, err := os.Stat(configDir); err == nil { + env.Detected = true + } + + skillPath := filepath.Join(configDir, "skills", "mnemon", "SKILL.md") + configPath := filepath.Join(configDir, "config.toml") + if _, err := os.Stat(skillPath); err == nil { + env.Installed = true + } else if data, err := os.ReadFile(configPath); err == nil && strings.Contains(string(data), "mnemon") { + 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") diff --git a/internal/setup/kimi.go b/internal/setup/kimi.go new file mode 100644 index 0000000..07d2b54 --- /dev/null +++ b/internal/setup/kimi.go @@ -0,0 +1,191 @@ +package setup + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/mnemon-dev/mnemon/internal/setup/assets" +) + +// KimiWriteSkill writes the mnemon skill to the Kimi Code skills directory. +func KimiWriteSkill(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.KimiSkill, 0644); err != nil { + return "", err + } + return skillPath, nil +} + +// KimiWriteHook writes a hook script to the Kimi Code hooks directory. +func KimiWriteHook(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 +} + +// KimiRegisterHooks registers Mnemon lifecycle hooks in config.toml. +func KimiRegisterHooks(configDir string) (string, error) { + hooksDir := filepath.Join(configDir, "hooks", "mnemon") + absHooksDir, err := filepath.Abs(hooksDir) + if err != nil { + return "", err + } + configPath := filepath.Join(configDir, "config.toml") + data, err := os.ReadFile(configPath) + if err != nil && !os.IsNotExist(err) { + return "", err + } + + updated := addKimiHooks(string(data), absHooksDir) + if err := os.MkdirAll(configDir, 0755); err != nil { + return "", err + } + tmp := configPath + ".tmp" + if err := os.WriteFile(tmp, []byte(updated), 0644); err != nil { + return "", err + } + if err := os.Rename(tmp, configPath); err != nil { + return "", err + } + return configPath, nil +} + +// KimiEject removes mnemon skill and hooks from the given Kimi Code config dir. +func KimiEject(configDir string) []error { + var errs []error + + fmt.Printf("\nRemoving Kimi Code 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, "config.toml") + data, err := os.ReadFile(configPath) + if err != nil { + if !os.IsNotExist(err) { + StatusError(2, 3, "Config", err) + errs = append(errs, err) + } + } else { + cleaned := removeKimiHooks(string(data)) + if strings.TrimSpace(cleaned) == "" { + if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) { + StatusError(2, 3, "Config", err) + errs = append(errs, err) + } else { + StatusOK(2, 3, "Config", configPath+" removed") + } + } else if err := os.WriteFile(configPath, []byte(cleaned), 0644); err != nil { + StatusError(2, 3, "Config", err) + errs = append(errs, err) + } else { + StatusOK(2, 3, "Config", configPath+" 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 addKimiHooks(config, hooksDir string) string { + cleaned := strings.TrimRight(removeKimiHooks(config), "\n") + blocks := []string{ + kimiHookBlock("SessionStart", "", filepath.Join(hooksDir, "prime.sh")), + kimiHookBlock("UserPromptSubmit", "", filepath.Join(hooksDir, "user_prompt.sh")), + kimiHookBlock("Stop", "", filepath.Join(hooksDir, "stop.sh")), + } + if cleaned == "" { + return strings.Join(blocks, "\n\n") + "\n" + } + return cleaned + "\n\n" + strings.Join(blocks, "\n\n") + "\n" +} + +func kimiHookBlock(event, matcher, command string) string { + lines := []string{ + "[[hooks]]", + fmt.Sprintf("event = %q", event), + fmt.Sprintf("command = %q", command), + "timeout = 10", + } + if matcher != "" { + lines = append(lines[:2], append([]string{fmt.Sprintf("matcher = %q", matcher)}, lines[2:]...)...) + } + return strings.Join(lines, "\n") +} + +func removeKimiHooks(config string) string { + if strings.TrimSpace(config) == "" { + return "" + } + lines := strings.SplitAfter(config, "\n") + var out strings.Builder + for i := 0; i < len(lines); { + trimmed := strings.TrimSpace(strings.TrimRight(lines[i], "\n")) + if trimmed != "[[hooks]]" { + out.WriteString(lines[i]) + i++ + continue + } + + start := i + i++ + for i < len(lines) { + next := strings.TrimSpace(strings.TrimRight(lines[i], "\n")) + if strings.HasPrefix(next, "[") { + break + } + i++ + } + block := strings.Join(lines[start:i], "") + if containsMnemon(block) { + continue + } + out.WriteString(block) + } + return collapseExcessBlankLines(out.String()) +} + +func collapseExcessBlankLines(s string) string { + lines := strings.Split(s, "\n") + var out []string + blank := 0 + for _, line := range lines { + if strings.TrimSpace(line) == "" { + blank++ + if blank > 2 { + continue + } + } else { + blank = 0 + } + out = append(out, line) + } + return strings.TrimRight(strings.Join(out, "\n"), "\n") + "\n" +} diff --git a/internal/setup/kimi_test.go b/internal/setup/kimi_test.go new file mode 100644 index 0000000..b8fd62e --- /dev/null +++ b/internal/setup/kimi_test.go @@ -0,0 +1,152 @@ +package setup + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestKimiWriteSkill(t *testing.T) { + dir := t.TempDir() + + skillPath, err := KimiWriteSkill(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), "Kimi Code") { + t.Fatalf("kimi skill should mention Kimi Code: %s", string(data)) + } +} + +func TestKimiWriteHook(t *testing.T) { + dir := t.TempDir() + + hookPath, err := KimiWriteHook(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 TestKimiRegisterHooksPreservesUnrelatedConfig(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, "config.toml") + if err := os.MkdirAll(dir, 0755); err != nil { + t.Fatalf("mkdir: %v", err) + } + if err := os.WriteFile(configPath, []byte(`model = "kimi" + +[[hooks]] +event = "SessionStart" +command = "/old/mnemon/prime.sh" +timeout = 1 + +[[hooks]] +event = "Notification" +matcher = "task\\.completed" +command = "/keep/custom.sh" +timeout = 3 +`), 0644); err != nil { + t.Fatalf("write config: %v", err) + } + + if _, err := KimiRegisterHooks(dir); err != nil { + t.Fatalf("register hooks: %v", err) + } + + data, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("read config: %v", err) + } + text := string(data) + if !strings.Contains(text, `model = "kimi"`) { + t.Fatalf("unrelated setting should be preserved: %s", text) + } + if !strings.Contains(text, `command = "/keep/custom.sh"`) { + t.Fatalf("custom hook should be preserved: %s", text) + } + if strings.Contains(text, "/old/mnemon/prime.sh") { + t.Fatalf("old mnemon hook should be removed: %s", text) + } + for _, event := range []string{`event = "SessionStart"`, `event = "UserPromptSubmit"`, `event = "Stop"`} { + if !strings.Contains(text, event) { + t.Fatalf("missing %s hook: %s", event, text) + } + } + if strings.Contains(text, "loop_limit") { + t.Fatalf("kimi hook schema should not include loop_limit: %s", text) + } +} + +func TestKimiEjectRemovesOnlyMnemonFilesAndHooks(t *testing.T) { + dir := t.TempDir() + if _, err := KimiWriteSkill(dir); err != nil { + t.Fatalf("write skill: %v", err) + } + if _, err := KimiWriteHook(dir, "prime.sh", []byte("#!/bin/bash\n")); err != nil { + t.Fatalf("write hook: %v", err) + } + if _, err := KimiRegisterHooks(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) + } + configPath := filepath.Join(dir, "config.toml") + existing, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("read config: %v", err) + } + existing = append(existing, []byte(` +[[hooks]] +event = "Notification" +command = "/keep/custom.sh" +timeout = 3 +`)...) + if err := os.WriteFile(configPath, existing, 0644); err != nil { + t.Fatalf("write config: %v", err) + } + + errs := KimiEject(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 := os.ReadFile(configPath) + if err != nil { + t.Fatalf("read config after eject: %v", err) + } + text := string(data) + if strings.Contains(text, "mnemon") { + t.Fatalf("mnemon hooks should be removed: %s", text) + } + if !strings.Contains(text, `command = "/keep/custom.sh"`) { + t.Fatalf("custom hook should be preserved: %s", text) + } +}