diff --git a/README.md b/README.md index 1e493797..4d753ffb 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,18 @@ One command deploys the mnemon skill, prompt files, and TRAE native hooks for both TRAE IDE and TRAE Work to `.trae/`. The integration uses `SessionStart`, `UserPromptSubmit`, and `Stop` hooks in `.trae/hooks.json`. +### [Qoder](https://qoder.com/) (QoderWork) + +```bash +mnemon setup --target qoder --yes +mnemon setup --target qoderwork --yes +``` + +Qoder deploys the mnemon skill, prompt files, and native hooks to `.qoder/` +or `~/.qoder/`. QoderWork uses its native user config at `~/.qoderwork/`. +Both integrations register `SessionStart`, `UserPromptSubmit`, and `Stop` +hooks in `settings.json`. + ### [OpenClaw](https://github.com/openclaw/openclaw) ```bash @@ -221,7 +233,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, and Cursor (hooks), OpenClaw (plugins), Pi (extensions), Nanobot (skills), and more +- **Multi-framework support** — Claude Code, Codex, Cursor, Qoder, and QoderWork (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 @@ -238,8 +250,14 @@ All your local agentic AIs — across sessions and frameworks — sharing one po ``` Claude Code ──┐ │ + Codex ────────┤ + │ Cursor ───────┤ │ + Qoder ────────┤ + │ + QoderWork ────┤ + │ OpenClaw ─────┤ │ Pi ───────────┤ @@ -254,7 +272,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, and Cursor setup automate hook +read and write. Claude Code, Codex, Cursor, Qoder, and QoderWork 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 diff --git a/cmd/setup.go b/cmd/setup.go index a2fd204a..e348e75b 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/, .openclaw/, .nanobot/, .pi/). -Use --global to install to user-wide config (~/.claude/, ~/.codex/, ~/.cursor/, ~/.trae/, ~/.openclaw/, ~/.nanobot/workspace/, ~/.pi/agent/). -Hermes Agent uses its native user config at ~/.hermes/. +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/). +Hermes Agent and QoderWork use their native user config at ~/.hermes/ and ~/.qoderwork/. -Supported environments: Claude Code, Codex, Cursor, Trae, OpenClaw, Nanobot, Pi, Hermes Agent. +Supported environments: Claude Code, Codex, Cursor, Trae, Qoder, QoderWork, OpenClaw, Nanobot, Pi, Hermes Agent. Examples: mnemon setup # Interactive: project-local install @@ -34,6 +34,8 @@ Examples: mnemon setup --target claude-code # Non-interactive: Claude Code only mnemon setup --target cursor # Non-interactive: Cursor skill only 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 hermes # Non-interactive: Hermes Agent only mnemon setup --eject # Interactive: remove integrations mnemon setup --eject --target claude-code # Non-interactive: remove Claude Code only @@ -42,7 +44,7 @@ Examples: } func init() { - setupCmd.Flags().StringVar(&setupTarget, "target", "", "target environment (claude-code, codex, cursor, trae, openclaw, nanobot, pi, hermes)") + setupCmd.Flags().StringVar(&setupTarget, "target", "", "target environment (claude-code, codex, cursor, trae, qoder, qoderwork, 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") @@ -50,8 +52,8 @@ func init() { } func runSetup(cmd *cobra.Command, args []string) error { - if setupTarget != "" && setupTarget != "claude-code" && setupTarget != "codex" && setupTarget != "cursor" && setupTarget != "trae" && setupTarget != "openclaw" && setupTarget != "nanobot" && setupTarget != "pi" && setupTarget != "hermes" { - return fmt.Errorf("invalid target %q (must be claude-code, codex, cursor, trae, openclaw, nanobot, pi, or hermes)", setupTarget) + 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) } envs := setup.DetectEnvironments(setupGlobal) @@ -87,7 +89,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, OpenClaw, Nanobot, Pi, or Hermes Agent, then run 'mnemon setup' again.") + fmt.Println("Install Claude Code, Codex, Cursor, Trae, Qoder, QoderWork, OpenClaw, Nanobot, Pi, or Hermes Agent, then run 'mnemon setup' again.") return nil } @@ -135,6 +137,10 @@ func installEnv(env *setup.Environment) error { err = installCursor(env) case "trae": err = installTrae(env) + case "qoder": + err = installQoder(env) + case "qoderwork": + err = installQoderWork(env) case "openclaw": err = installOpenClaw(env) case "nanobot": @@ -569,6 +575,109 @@ func installTrae(env *setup.Environment) error { return nil } +// ─── Qoder ────────────────────────────────────────────────────────── + +func installQoder(env *setup.Environment) error { + configDir := env.ConfigDir + + if !setupGlobal && !setupYes && setup.IsInteractive() { + home := setup.HomeDir() + localDir := ".qoder" + globalDir := home + "/.qoder" + 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 Qoder (%s)...\n", configDir) + + return installQoderLike( + configDir, + setup.QoderWriteSkill, + setup.QoderRegisterHooks, + "Restart Qoder IDE/CLI to activate the mnemon skill and hooks.", + "Run 'mnemon setup --eject --target qoder' to remove.", + ) +} + +// ─── QoderWork ────────────────────────────────────────────────────── + +func installQoderWork(env *setup.Environment) error { + configDir := env.ConfigDir + + fmt.Printf("\nSetting up QoderWork (%s)...\n", configDir) + + return installQoderLike( + configDir, + setup.QoderWorkWriteSkill, + setup.QoderWorkRegisterHooks, + "Restart QoderWork to activate the mnemon skill and hooks.", + "Run 'mnemon setup --eject --target qoderwork' to remove.", + ) +} + +func installQoderLike(configDir string, writeSkill func(string) (string, error), registerHooks func(string) (string, error), activation, ejectHint string) error { + fmt.Println("\n[1/3] Skill") + if path, err := writeSkill(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.QoderPrimeHook}, + {"Hook: remind", "user_prompt.sh", assets.QoderUserPromptHook}, + {"Hook: nudge", "stop.sh", assets.QoderStopHook}, + } { + if path, err := setup.QoderWriteHook(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 := registerHooks(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(activation) + fmt.Println(ejectHint) + + return nil +} + // ─── OpenClaw ─────────────────────────────────────────────────────── func installOpenClaw(env *setup.Environment) error { @@ -1010,6 +1119,20 @@ func ejectEnv(env *setup.Environment) error { return errs[0] } + case "qoder": + errs := setup.QoderEject(env.ConfigDir) + ejectMarkdown("AGENTS.md", "Remove memory guidance from ./AGENTS.md?") + if len(errs) > 0 { + return errs[0] + } + + case "qoderwork": + errs := setup.QoderWorkEject(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 9f62741c..aa94c625 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -32,6 +32,8 @@ mnemon setup --target claude-code mnemon setup --target codex mnemon setup --target cursor mnemon setup --target trae +mnemon setup --target qoder +mnemon setup --target qoderwork mnemon setup --target openclaw mnemon setup --target pi mnemon setup --target nanobot --global @@ -47,8 +49,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/`) | -| `--target ` | (auto-detect) | Target environment: `claude-code`, `codex`, `cursor`, `trae`, `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/`) | +| `--target ` | (auto-detect) | Target environment: `claude-code`, `codex`, `cursor`, `trae`, `qoder`, `qoderwork`, `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 56ee0c70..098388ea 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -92,15 +92,26 @@ mnemon setup `mnemon setup` 自动检测 Claude Code,交互式部署技能文件、钩子和行为引导。启动新会话 — 记忆自动运作。 -### [Trae](https://www.trae.ai/) +### [TRAE](https://www.trae.ai/) (TRAE Work) ```bash mnemon setup --target trae --yes ``` -一条命令将 mnemon skill、prompt 文件和 Trae 原生 hooks 部署到 `.trae/`。 -该集成使用 `.trae/hooks.json` 中的 `SessionStart`、`UserPromptSubmit` 和 -`Stop` hooks。 +一条命令将 mnemon skill、prompt 文件和 TRAE 原生 hooks 部署到 `.trae/`, +同时覆盖 TRAE IDE 和 TRAE Work。该集成使用 `.trae/hooks.json` 中的 +`SessionStart`、`UserPromptSubmit` 和 `Stop` hooks。 + +### [Qoder](https://qoder.com/) (QoderWork) + +```bash +mnemon setup --target qoder --yes +mnemon setup --target qoderwork --yes +``` + +Qoder 会将 mnemon skill、prompt 文件和原生 hooks 部署到 `.qoder/` 或 +`~/.qoder/`。QoderWork 使用原生用户级配置 `~/.qoderwork/`。两者都会在 +`settings.json` 中注册 `SessionStart`、`UserPromptSubmit` 和 `Stop` hooks。 ### [OpenClaw](https://github.com/openclaw/openclaw) @@ -185,7 +196,7 @@ Agent 工作,并且只在有用时调用 Mnemon - **零用户操作** — 安装一次;支持 hook 的 runtime 可用 hook,minimal runtime 可用持久规则 - **LLM 监督式** — 宿主 LLM 主动决定记什么、更新什么、遗忘什么;无内嵌 LLM,无 API 密钥 -- **多框架支持** — Claude Code 和 Codex(hooks)、OpenClaw(plugins)、Pi(extensions)、Nanobot(skills)等 +- **多框架支持** — Claude Code、Codex、Cursor、Qoder 和 QoderWork(hooks)、OpenClaw(plugins)、Pi(extensions)、Nanobot(skills)等 - **Markdown 可安装 harness** — `SKILL.md`、`INSTALL.md`、`GUIDELINE.md` 和四个生命周期提醒 - **四图架构** — 时序、实体、因果、语义四种边,不仅仅是向量相似度 - **意图原生协议** — 三个原语(`remember`、`link`、`recall`)映射到 LLM 的认知词汇而非数据库语法;结构化 JSON 输出,带信号透明度 diff --git a/docs/zh/USAGE.md b/docs/zh/USAGE.md index d5ac2f4e..9053f906 100644 --- a/docs/zh/USAGE.md +++ b/docs/zh/USAGE.md @@ -32,6 +32,8 @@ mnemon setup --target claude-code mnemon setup --target codex mnemon setup --target cursor mnemon setup --target trae +mnemon setup --target qoder +mnemon setup --target qoderwork mnemon setup --target openclaw mnemon setup --target pi mnemon setup --target nanobot --global @@ -47,8 +49,8 @@ mnemon setup --eject --target claude-code | 标志 | 默认值 | 说明 | |---|---|---| -| `--global` | `false` | 安装到用户级配置而非项目本地(Nanobot 推荐安装到 `~/.nanobot/workspace/`;Pi 安装到 `~/.pi/agent/`;Hermes 安装到 `~/.hermes/`) | -| `--target ` | (自动检测) | 目标环境:`claude-code`、`codex`、`cursor`、`trae`、`openclaw`、`nanobot`、`pi` 或 `hermes` | +| `--global` | `false` | 安装到用户级配置而非项目本地(Nanobot 推荐安装到 `~/.nanobot/workspace/`;Pi 安装到 `~/.pi/agent/`;Hermes 安装到 `~/.hermes/`;QoderWork 安装到 `~/.qoderwork/`) | +| `--target ` | (自动检测) | 目标环境:`claude-code`、`codex`、`cursor`、`trae`、`qoder`、`qoderwork`、`openclaw`、`nanobot`、`pi` 或 `hermes` | | `--eject` | `false` | 移除 mnemon 集成 | | `--yes` | `false` | 自动确认所有提示 | diff --git a/internal/setup/assets/assets.go b/internal/setup/assets/assets.go index a967b2fd..66dd96b4 100644 --- a/internal/setup/assets/assets.go +++ b/internal/setup/assets/assets.go @@ -56,6 +56,21 @@ var TraeUserPromptHook []byte //go:embed trae/stop.sh var TraeStopHook []byte +//go:embed qoder/SKILL.md +var QoderSkill []byte + +//go:embed qoderwork/SKILL.md +var QoderWorkSkill []byte + +//go:embed qoder/prime.sh +var QoderPrimeHook []byte + +//go:embed qoder/user_prompt.sh +var QoderUserPromptHook []byte + +//go:embed qoder/stop.sh +var QoderStopHook []byte + //go:embed openclaw/SKILL.md var OpenClawSkill []byte @@ -106,5 +121,5 @@ var HermesCompactHook []byte // All returns the embedded filesystem for inspection. // -//go:embed claude codex cursor trae openclaw nanoclaw nanobot pi hermes +//go:embed claude codex cursor trae qoder qoderwork openclaw nanoclaw nanobot pi hermes var All embed.FS diff --git a/internal/setup/assets/qoder/SKILL.md b/internal/setup/assets/qoder/SKILL.md new file mode 100644 index 00000000..2dba308d --- /dev/null +++ b/internal/setup/assets/qoder/SKILL.md @@ -0,0 +1,46 @@ +--- +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 +``` + +## 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/qoder/prime.sh b/internal/setup/assets/qoder/prime.sh new file mode 100644 index 00000000..694a2d0a --- /dev/null +++ b/internal/setup/assets/qoder/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/qoder/stop.sh b/internal/setup/assets/qoder/stop.sh new file mode 100644 index 00000000..73b72c03 --- /dev/null +++ b/internal/setup/assets/qoder/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}/qoder-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/qoder/user_prompt.sh b/internal/setup/assets/qoder/user_prompt.sh new file mode 100644 index 00000000..6e5d6d2e --- /dev/null +++ b/internal/setup/assets/qoder/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/assets/qoderwork/SKILL.md b/internal/setup/assets/qoderwork/SKILL.md new file mode 100644 index 00000000..d283a4c8 --- /dev/null +++ b/internal/setup/assets/qoderwork/SKILL.md @@ -0,0 +1,46 @@ +--- +name: mnemon +description: Persistent memory CLI for QoderWork. 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/detect.go b/internal/setup/detect.go index d1fd787b..a39d8b28 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", "openclaw", "nanobot", "pi", "hermes" - Display string // "Claude Code", "Codex", "Cursor", "Trae", "OpenClaw", "Nanobot", "Pi", "Hermes Agent" + 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" Detected bool // CLI binary or global config dir found BinPath string // exec.LookPath result Installed bool // mnemon integration present at ConfigDir @@ -33,6 +33,8 @@ func DetectEnvironments(global bool) []Environment { detectCodex(global), detectCursor(global), detectTrae(global), + detectQoder(global), + detectQoderWork(), detectOpenClaw(global), detectNanobot(global), detectPi(global), @@ -199,6 +201,82 @@ func detectTrae(global bool) Environment { return env } +func detectQoder(global bool) Environment { + home := HomeDir() + globalDir := filepath.Join(home, ".qoder") + localDir := ".qoder" + + configDir := localDir + if global { + configDir = globalDir + } + + env := Environment{ + Name: "qoder", + Display: "Qoder", + ConfigDir: configDir, + } + + if binPath, err := exec.LookPath("qoder"); 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 detectQoderWork() Environment { + home := HomeDir() + configDir := filepath.Join(home, ".qoderwork") + + env := Environment{ + Name: "qoderwork", + Display: "QoderWork", + ConfigDir: configDir, + } + + if binPath, err := exec.LookPath("qoderwork"); 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") + 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") diff --git a/internal/setup/qoder.go b/internal/setup/qoder.go new file mode 100644 index 00000000..52d7035d --- /dev/null +++ b/internal/setup/qoder.go @@ -0,0 +1,168 @@ +package setup + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/mnemon-dev/mnemon/internal/setup/assets" +) + +// QoderWriteSkill writes the mnemon skill to the Qoder skills directory. +func QoderWriteSkill(configDir string) (string, error) { + return writeQoderSkill(configDir, assets.QoderSkill) +} + +// QoderWorkWriteSkill writes the mnemon skill to the QoderWork skills directory. +func QoderWorkWriteSkill(configDir string) (string, error) { + return writeQoderSkill(configDir, assets.QoderWorkSkill) +} + +func writeQoderSkill(configDir string, content []byte) (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, content, 0644); err != nil { + return "", err + } + return skillPath, nil +} + +// QoderWriteHook writes a hook script to the Qoder hooks directory. +func QoderWriteHook(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 +} + +// QoderRegisterHooks registers Mnemon lifecycle hooks in Qoder settings.json. +func QoderRegisterHooks(configDir string) (string, error) { + return registerQoderHooks(configDir) +} + +// QoderWorkRegisterHooks registers Mnemon lifecycle hooks in QoderWork settings.json. +func QoderWorkRegisterHooks(configDir string) (string, error) { + return registerQoderHooks(configDir) +} + +func registerQoderHooks(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 + } + addQoderHooks(data, absHooksDir) + if err := WriteJSONFile(settingsPath, data); err != nil { + return "", err + } + return settingsPath, nil +} + +// QoderEject removes mnemon skill and hooks from the given Qoder config dir. +func QoderEject(configDir string) []error { + return ejectQoder("Qoder", configDir) +} + +// QoderWorkEject removes mnemon skill and hooks from the given QoderWork config dir. +func QoderWorkEject(configDir string) []error { + return ejectQoder("QoderWork", configDir) +} + +func ejectQoder(display, configDir string) []error { + var errs []error + + fmt.Printf("\nRemoving %s integration (%s)...\n", display, 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 { + removeQoderHooks(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 addQoderHooks(data map[string]interface{}, hooksDir string) { + removeQoderHooks(data) + hooks := ensureHooksMap(data) + + addQoderHook(hooks, "SessionStart", filepath.Join(hooksDir, "prime.sh")) + addQoderHook(hooks, "UserPromptSubmit", filepath.Join(hooksDir, "user_prompt.sh")) + addQoderHook(hooks, "Stop", filepath.Join(hooksDir, "stop.sh")) +} + +func addQoderHook(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 removeQoderHooks(data map[string]interface{}) { + hooks, ok := data["hooks"].(map[string]interface{}) + if !ok { + return + } + for _, key := range []string{"SessionStart", "UserPromptSubmit", "Stop", "PreToolUse", "PostToolUse", "PostToolUseFailure", "Notification", "PermissionRequest", "PreCompact", "SessionEnd", "SubagentStart", "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/qoder_test.go b/internal/setup/qoder_test.go new file mode 100644 index 00000000..9433d06b --- /dev/null +++ b/internal/setup/qoder_test.go @@ -0,0 +1,194 @@ +package setup + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestQoderWriteSkill(t *testing.T) { + dir := t.TempDir() + + skillPath, err := QoderWriteSkill(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) + } + if _, err := os.Stat(skillPath); err != nil { + t.Fatalf("stat skill: %v", err) + } +} + +func TestQoderWorkWriteSkill(t *testing.T) { + dir := t.TempDir() + + skillPath, err := QoderWorkWriteSkill(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), "QoderWork") { + t.Fatalf("qoderwork skill should mention QoderWork: %s", string(data)) + } +} + +func TestQoderWriteHook(t *testing.T) { + dir := t.TempDir() + + hookPath, err := QoderWriteHook(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 TestQoderRegisterHooksPreservesUnrelatedConfig(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 := QoderRegisterHooks(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("qoder hook schema should not include loop_limit: %#v", stop[0]) + } +} + +func TestQoderWorkRegisterHooksUsesSettingsJSON(t *testing.T) { + dir := t.TempDir() + + settingsPath, err := QoderWorkRegisterHooks(dir) + if err != nil { + t.Fatalf("register hooks: %v", err) + } + if settingsPath != filepath.Join(dir, "settings.json") { + t.Fatalf("settings path = %q", settingsPath) + } + data, err := ReadJSONFile(settingsPath) + if err != nil { + t.Fatalf("read settings: %v", err) + } + hooks := data["hooks"].(map[string]any) + if _, ok := hooks["SessionStart"]; !ok { + t.Fatalf("session hook should be registered: %#v", hooks) + } + if _, ok := hooks["UserPromptSubmit"]; !ok { + t.Fatalf("user prompt hook should be registered: %#v", hooks) + } + if _, ok := hooks["Stop"]; !ok { + t.Fatalf("stop hook should be registered: %#v", hooks) + } +} + +func TestQoderEjectRemovesOnlyMnemonFilesAndHooks(t *testing.T) { + dir := t.TempDir() + if _, err := QoderWriteSkill(dir); err != nil { + t.Fatalf("write skill: %v", err) + } + if _, err := QoderWriteHook(dir, "prime.sh", []byte("#!/bin/bash\n")); err != nil { + t.Fatalf("write hook: %v", err) + } + if _, err := QoderRegisterHooks(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 := QoderEject(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) + } +}