diff --git a/README.md b/README.md index b4c3430..328149c 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,17 @@ mnemon setup --target openclaw --yes One command deploys skill, hook, plugin, and behavioral guide to `~/.openclaw/`. Restart the OpenClaw gateway to activate. +### [Pi](https://pi.dev) + +```bash +mnemon setup --target pi --yes +``` + +One command deploys the mnemon skill, prompt files, and a Pi TypeScript extension +to `.pi/`. The extension maps Mnemon's lifecycle reminders onto Pi events +(`resources_discover`, `before_agent_start`, `agent_end`, +`session_before_compact`). Start a new Pi session or run `/reload` to activate. + ### [NanoClaw](https://github.com/qwibitai/nanoclaw) NanoClaw runs agents inside Linux containers. Use the `/add-mnemon` skill to integrate: @@ -178,7 +189,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 (hooks), OpenClaw (plugins), Nanobot (skills), and more +- **Multi-framework support** — Claude Code and Codex (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 @@ -197,6 +208,8 @@ All your local agentic AIs — across sessions and frameworks — sharing one po │ OpenClaw ─────┤ │ + Pi ───────────┤ + │ Nanobot ──────┤ │ NanoClaw ─────┤ @@ -208,7 +221,8 @@ 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 setup automates hook installation; OpenClaw can use -plugin hooks; Nanobot integrates via skill files; NanoClaw integrates via +plugin hooks; Pi integrates via native skills and TypeScript lifecycle +extensions; Nanobot integrates via skill files; NanoClaw integrates via container skills and volume mounts. The same harness can be installed in any LLM CLI that supports skills, rules, system prompts, or event hooks. diff --git a/cmd/setup.go b/cmd/setup.go index cd3fe40..bd95342 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -22,10 +22,10 @@ var setupCmd = &cobra.Command{ Short: "Deploy mnemon into LLM CLI environments", Long: `Detect installed LLM CLIs and deploy mnemon integration. -By default, installs to project-local config (.claude/, .codex/, .openclaw/, .nanobot/). -Use --global to install to user-wide config (~/.claude/, ~/.codex/, ~/.openclaw/, ~/.nanobot/workspace/). +By default, installs to project-local config (.claude/, .codex/, .openclaw/, .nanobot/, .pi/). +Use --global to install to user-wide config (~/.claude/, ~/.codex/, ~/.openclaw/, ~/.nanobot/workspace/, ~/.pi/agent/). -Supported environments: Claude Code, Codex, OpenClaw, Nanobot. +Supported environments: Claude Code, Codex, OpenClaw, Nanobot, Pi. Examples: mnemon setup # Interactive: project-local install @@ -38,7 +38,7 @@ Examples: } func init() { - setupCmd.Flags().StringVar(&setupTarget, "target", "", "target environment (claude-code, codex, openclaw, nanobot)") + setupCmd.Flags().StringVar(&setupTarget, "target", "", "target environment (claude-code, codex, openclaw, nanobot, pi)") 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") @@ -46,8 +46,8 @@ func init() { } func runSetup(cmd *cobra.Command, args []string) error { - if setupTarget != "" && setupTarget != "claude-code" && setupTarget != "codex" && setupTarget != "openclaw" && setupTarget != "nanobot" { - return fmt.Errorf("invalid target %q (must be claude-code, codex, openclaw, or nanobot)", setupTarget) + if setupTarget != "" && setupTarget != "claude-code" && setupTarget != "codex" && setupTarget != "openclaw" && setupTarget != "nanobot" && setupTarget != "pi" { + return fmt.Errorf("invalid target %q (must be claude-code, codex, openclaw, nanobot, or pi)", setupTarget) } envs := setup.DetectEnvironments(setupGlobal) @@ -83,7 +83,7 @@ func runInstallFlow(envs []setup.Environment) error { if len(detected) == 0 { fmt.Println("\nNo supported LLM CLI environments detected.") - fmt.Println("Install Claude Code, Codex, OpenClaw, or Nanobot, then run 'mnemon setup' again.") + fmt.Println("Install Claude Code, Codex, OpenClaw, Nanobot, or Pi, then run 'mnemon setup' again.") return nil } @@ -131,6 +131,8 @@ func installEnv(env *setup.Environment) error { err = installOpenClaw(env) case "nanobot": err = installNanobot(env) + case "pi": + err = installPi(env) } if err != nil { return err @@ -543,6 +545,67 @@ func installNanobot(env *setup.Environment) error { return nil } +// ─── Pi ───────────────────────────────────────────────────────────── + +func installPi(env *setup.Environment) error { + configDir := env.ConfigDir + + if !setupGlobal && !setupYes && setup.IsInteractive() { + home := setup.HomeDir() + localDir := ".pi" + globalDir := home + "/.pi/agent" + 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 Pi (%s)...\n", configDir) + + fmt.Println("\n[1/3] Skill") + if path, err := setup.PiWriteSkill(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] Extension") + if path, err := setup.PiWriteExtension(configDir); err != nil { + setup.StatusError(0, 0, "Extension", err) + return err + } else { + setup.StatusOK(0, 0, "Extension", path) + } + + fmt.Println() + fmt.Println("Setup complete!") + fmt.Printf(" Skill %s/skills/mnemon/SKILL.md\n", configDir) + fmt.Printf(" Extension %s/extensions/mnemon.ts (resources_discover, before_agent_start, agent_end, session_before_compact)\n", configDir) + fmt.Printf(" Prompts %s/ (guide.md, skill.md)\n", promptPath) + fmt.Println() + fmt.Println("Start a new Pi session or run /reload to activate.") + fmt.Println("Run 'mnemon setup --eject --target pi' to remove.") + + return nil +} + // ─── Eject ────────────────────────────────────────────────────────── func runEjectFlow(envs []setup.Environment) error { @@ -635,6 +698,13 @@ func ejectEnv(env *setup.Environment) error { if len(errs) > 0 { return errs[0] } + + case "pi": + errs := setup.PiEject(env.ConfigDir) + ejectMarkdown("AGENTS.md", "Remove memory guidance from ./AGENTS.md?") + if len(errs) > 0 { + return errs[0] + } } return nil } diff --git a/docs/USAGE.md b/docs/USAGE.md index 306092d..f52fb1c 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -30,6 +30,7 @@ mnemon setup --global # Non-interactive: specific target only mnemon setup --target claude-code mnemon setup --target openclaw +mnemon setup --target pi mnemon setup --target nanobot --global # Auto-confirm all prompts (CI-friendly) @@ -42,8 +43,8 @@ mnemon setup --eject --target claude-code | Flag | Default | Description | |---|---|---| -| `--global` | `false` | Install to user-wide config instead of project-local (required for Nanobot: installs to `~/.nanobot/workspace/`) | -| `--target ` | (auto-detect) | Target environment: `claude-code`, `openclaw`, or `nanobot` | +| `--global` | `false` | Install to user-wide config instead of project-local (recommended for Nanobot: installs to `~/.nanobot/workspace/`; Pi installs to `~/.pi/agent/`) | +| `--target ` | (auto-detect) | Target environment: `claude-code`, `codex`, `openclaw`, `nanobot`, or `pi` | | `--eject` | `false` | Remove mnemon integrations | | `--yes` | `false` | Auto-confirm all prompts | diff --git a/docs/design/07-integration.md b/docs/design/07-integration.md index aaf530b..17fc8a3 100644 --- a/docs/design/07-integration.md +++ b/docs/design/07-integration.md @@ -88,6 +88,7 @@ The same harness maps differently across runtimes: | Codex | `AGENTS.md`, skills, local instructions, and hooks when enabled | | Claude Code | `CLAUDE.md`, skills, slash commands, settings hooks, and project/user memory files | | OpenClaw | Plugin hooks and skills, without requiring a Mnemon-specific memory engine | +| Pi | `AGENTS.md`, native skills, and TypeScript extension lifecycle events | | Skill-first agents | Skills, memory guidance, and lightweight reminders | | Minimal CLIs | A rules file or system instruction that references `SKILL.md` and `GUIDELINE.md` | diff --git a/docs/zh/README.md b/docs/zh/README.md index a9364c2..f70626b 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -100,6 +100,17 @@ mnemon setup --target openclaw --yes 一条命令将技能文件、钩子、插件和行为引导部署到 `~/.openclaw/`。重启 OpenClaw 网关即可激活。 +### [Pi](https://pi.dev) + +```bash +mnemon setup --target pi --yes +``` + +一条命令将 mnemon skill、prompt 文件和 Pi TypeScript extension 部署到 +`.pi/`。这个 extension 会把 Mnemon 的 lifecycle reminder 映射到 Pi 事件 +(`resources_discover`、`before_agent_start`、`agent_end`、 +`session_before_compact`)。启动新的 Pi session 或运行 `/reload` 即可激活。 + ### [NanoClaw](https://github.com/qwibitai/nanoclaw) NanoClaw 在 Linux 容器内运行智能体。使用 `/add-mnemon` 技能集成: @@ -153,6 +164,7 @@ Agent 工作,并且只在有用时调用 Mnemon - **零用户操作** — 安装一次;支持 hook 的 runtime 可用 hook,minimal runtime 可用持久规则 - **LLM 监督式** — 宿主 LLM 主动决定记什么、更新什么、遗忘什么;无内嵌 LLM,无 API 密钥 +- **多框架支持** — Claude Code 和 Codex(hooks)、OpenClaw(plugins)、Pi(extensions)、Nanobot(skills)等 - **Markdown 可安装 harness** — `SKILL.md`、`INSTALL.md`、`GUIDELINE.md` 和四个生命周期提醒 - **四图架构** — 时序、实体、因果、语义四种边,不仅仅是向量相似度 - **意图原生协议** — 三个原语(`remember`、`link`、`recall`)映射到 LLM 的认知词汇而非数据库语法;结构化 JSON 输出,带信号透明度 @@ -170,6 +182,8 @@ Agent 工作,并且只在有用时调用 Mnemon │ OpenClaw ─────┤ │ + Pi ───────────┤ + │ NanoClaw ─────┤ ├──▶ ~/.mnemon ◀── 共享记忆 OpenCode ─────┤ @@ -177,7 +191,7 @@ Agent 工作,并且只在有用时调用 Mnemon Gemini CLI ───┘ ``` -基础已就绪:一个 `~/.mnemon` 数据库,任何 agent 都可以读写。Claude Code setup 可自动安装 hook;OpenClaw 可以使用 plugin hooks;NanoClaw 通过容器技能和卷挂载集成。同一个 harness 可以安装到任何支持 skill、rule、system prompt 或 event hook 的 LLM CLI。 +基础已就绪:一个 `~/.mnemon` 数据库,任何 agent 都可以读写。Claude Code setup 可自动安装 hook;OpenClaw 可以使用 plugin hooks;Pi 通过原生 skill 和 TypeScript lifecycle extension 集成;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 5648018..77b442f 100644 --- a/docs/zh/USAGE.md +++ b/docs/zh/USAGE.md @@ -29,7 +29,10 @@ mnemon setup --global # 非交互式:仅指定目标 mnemon setup --target claude-code +mnemon setup --target codex mnemon setup --target openclaw +mnemon setup --target pi +mnemon setup --target nanobot --global # 自动确认所有提示(CI 友好) mnemon setup --yes @@ -41,8 +44,8 @@ mnemon setup --eject --target claude-code | 标志 | 默认值 | 说明 | |---|---|---| -| `--global` | `false` | 安装到用户级配置(`~/.claude/`)而非项目本地(`.claude/`) | -| `--target ` | (自动检测) | 目标环境:`claude-code` 或 `openclaw` | +| `--global` | `false` | 安装到用户级配置而非项目本地(Nanobot 推荐安装到 `~/.nanobot/workspace/`;Pi 安装到 `~/.pi/agent/`) | +| `--target ` | (自动检测) | 目标环境:`claude-code`、`codex`、`openclaw`、`nanobot` 或 `pi` | | `--eject` | `false` | 移除 mnemon 集成 | | `--yes` | `false` | 自动确认所有提示 | diff --git a/docs/zh/design/07-integration.md b/docs/zh/design/07-integration.md index 6a6d7ec..3b09faa 100644 --- a/docs/zh/design/07-integration.md +++ b/docs/zh/design/07-integration.md @@ -74,6 +74,7 @@ Hook 契约是行为契约。脚本正文是 runtime-specific implementation det | Codex | `AGENTS.md`、skill、本地指令,以及启用后的 hooks | | Claude Code | `CLAUDE.md`、skill、slash command、settings hooks、project/user memory 文件 | | OpenClaw | Plugin hooks 和 skill,但不要求 Mnemon-specific memory engine | +| Pi | `AGENTS.md`、原生 skill,以及 TypeScript extension lifecycle events | | Skill-first agents | Skill、memory guidance 和轻量提醒 | | Minimal CLIs | 引用 `SKILL.md` 和 `GUIDELINE.md` 的 rules 文件或 system instruction | diff --git a/internal/setup/assets/assets.go b/internal/setup/assets/assets.go index 3c245ae..5227451 100644 --- a/internal/setup/assets/assets.go +++ b/internal/setup/assets/assets.go @@ -59,7 +59,13 @@ var NanoClawContainerSkill []byte //go:embed nanobot/SKILL.md var NanobotSkill []byte +//go:embed pi/SKILL.md +var PiSkill []byte + +//go:embed pi/mnemon.ts +var PiExtension []byte + // All returns the embedded filesystem for inspection. // -//go:embed claude codex openclaw nanoclaw nanobot +//go:embed claude codex openclaw nanoclaw nanobot pi var All embed.FS diff --git a/internal/setup/assets/pi/SKILL.md b/internal/setup/assets/pi/SKILL.md new file mode 100644 index 0000000..c6a0d03 --- /dev/null +++ b/internal/setup/assets/pi/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/setup/assets/pi/mnemon.ts b/internal/setup/assets/pi/mnemon.ts new file mode 100644 index 0000000..72ff87a --- /dev/null +++ b/internal/setup/assets/pi/mnemon.ts @@ -0,0 +1,76 @@ +import { execFileSync } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; + +function promptDir(): string { + return join(process.env.MNEMON_DATA_DIR ?? join(process.env.HOME ?? "", ".mnemon"), "prompt"); +} + +function guidePath(): string | undefined { + const scoped = join(promptDir(), "guide.md"); + if (existsSync(scoped)) return scoped; + + const legacy = join(process.env.HOME ?? "", ".mnemon", "prompt", "guide.md"); + if (existsSync(legacy)) return legacy; + + return undefined; +} + +function readGuide(): string { + const path = guidePath(); + return path ? readFileSync(path, "utf8") : ""; +} + +function memoryStatus(): string { + try { + const raw = execFileSync("mnemon", ["status"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + timeout: 5000, + }); + const stats = JSON.parse(raw); + return `[mnemon] Memory active (${stats.total_insights ?? 0} insights, ${stats.edge_count ?? 0} edges).`; + } catch { + return "[mnemon] Memory active."; + } +} + +function visibleMessage(content: string) { + return { + customType: "mnemon", + content, + display: true, + }; +} + +export default function (pi: ExtensionAPI) { + pi.on("resources_discover", async () => { + return { + skillPaths: [join(process.env.PI_CODING_AGENT_DIR ?? join(process.env.HOME ?? "", ".pi", "agent"), "skills")], + }; + }); + + pi.on("session_start", async (_event, ctx) => { + ctx.ui.setStatus("mnemon", "mnemon"); + }); + + pi.on("before_agent_start", async () => { + const guide = readGuide(); + const content = [memoryStatus(), guide, "[mnemon] Evaluate: recall needed? After responding, evaluate: remember needed?"] + .filter(Boolean) + .join("\n\n"); + + return { message: visibleMessage(content) }; + }); + + pi.on("agent_end", async (_event, ctx) => { + ctx.ui.notify("[mnemon] Consider whether this exchange warrants durable memory.", "info"); + }); + + pi.on("session_before_compact", async () => { + return { + customInstructions: "[mnemon] Before compacting, preserve only critical continuity with mnemon remember when justified. Do not store the full transcript.", + }; + }); +} diff --git a/internal/setup/detect.go b/internal/setup/detect.go index c385bf1..9a0838c 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", "openclaw" - Display string // "Claude Code", "Codex", "OpenClaw" + Name string // "claude-code", "codex", "openclaw", "nanobot", "pi" + Display string // "Claude Code", "Codex", "OpenClaw", "Nanobot", "Pi" Detected bool // CLI binary or global config dir found BinPath string // exec.LookPath result Installed bool // mnemon integration present at ConfigDir @@ -33,6 +33,7 @@ func DetectEnvironments(global bool) []Environment { detectCodex(global), detectOpenClaw(global), detectNanobot(global), + detectPi(global), } } @@ -205,3 +206,41 @@ func detectNanobot(global bool) Environment { return env } + +func detectPi(global bool) Environment { + home := HomeDir() + globalDir := filepath.Join(home, ".pi", "agent") + localDir := ".pi" + + configDir := localDir + if global { + configDir = globalDir + } + + env := Environment{ + Name: "pi", + Display: "Pi", + ConfigDir: configDir, + } + + if binPath, err := exec.LookPath("pi"); 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") + if _, err := os.Stat(skillPath); err == nil { + 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 +} diff --git a/internal/setup/pi.go b/internal/setup/pi.go new file mode 100644 index 0000000..6ec291e --- /dev/null +++ b/internal/setup/pi.go @@ -0,0 +1,65 @@ +package setup + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/mnemon-dev/mnemon/internal/setup/assets" +) + +// PiWriteSkill writes the mnemon skill to the Pi skills directory. +func PiWriteSkill(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.PiSkill, 0644); err != nil { + return "", err + } + return skillPath, nil +} + +// PiWriteExtension writes the Mnemon lifecycle extension to Pi. +func PiWriteExtension(configDir string) (string, error) { + extDir := filepath.Join(configDir, "extensions") + if err := os.MkdirAll(extDir, 0755); err != nil { + return "", err + } + extPath := filepath.Join(extDir, "mnemon.ts") + if err := os.WriteFile(extPath, assets.PiExtension, 0644); err != nil { + return "", err + } + return extPath, nil +} + +// PiEject removes mnemon skill and extension from the given Pi config dir. +func PiEject(configDir string) []error { + var errs []error + + fmt.Printf("\nRemoving Pi integration (%s)...\n", configDir) + + targets := []struct { + label string + path string + }{ + {"Extension", filepath.Join(configDir, "extensions", "mnemon.ts")}, + {"Skill", filepath.Join(configDir, "skills", "mnemon")}, + } + + for i, target := range targets { + if err := os.RemoveAll(target.path); err != nil { + StatusError(i+1, len(targets), target.label, err) + errs = append(errs, err) + } else { + StatusOK(i+1, len(targets), target.label, target.path+" removed") + } + } + + removeIfEmpty(filepath.Join(configDir, "extensions")) + removeIfEmpty(filepath.Join(configDir, "skills")) + removeIfEmpty(configDir) + + return errs +} diff --git a/internal/setup/pi_test.go b/internal/setup/pi_test.go new file mode 100644 index 0000000..f7445ff --- /dev/null +++ b/internal/setup/pi_test.go @@ -0,0 +1,85 @@ +package setup + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/mnemon-dev/mnemon/internal/setup/assets" +) + +func TestPiWriteSkillAndExtension(t *testing.T) { + dir := t.TempDir() + + skillPath, err := PiWriteSkill(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 data, err := os.ReadFile(skillPath); err != nil { + t.Fatalf("read skill: %v", err) + } else if !strings.Contains(string(data), "name: mnemon") { + t.Fatalf("unexpected skill content: %q", string(data)) + } + + extPath, err := PiWriteExtension(dir) + if err != nil { + t.Fatalf("write extension: %v", err) + } + if extPath != filepath.Join(dir, "extensions", "mnemon.ts") { + t.Fatalf("extension path = %q", extPath) + } + if data, err := os.ReadFile(extPath); err != nil { + t.Fatalf("read extension: %v", err) + } else if !strings.Contains(string(data), `pi.on("before_agent_start"`) { + t.Fatalf("unexpected extension content: %q", string(data)) + } +} + +func TestPiExtensionMapsLifecycleEvents(t *testing.T) { + extension := string(assets.PiExtension) + for _, want := range []string{ + `pi.on("resources_discover"`, + `pi.on("session_start"`, + `pi.on("before_agent_start"`, + `pi.on("agent_end"`, + `pi.on("session_before_compact"`, + "process.env.MNEMON_DATA_DIR", + "process.env.PI_CODING_AGENT_DIR", + } { + if !strings.Contains(extension, want) { + t.Fatalf("Pi extension missing %q", want) + } + } +} + +func TestPiEjectRemovesOnlyMnemonFiles(t *testing.T) { + dir := t.TempDir() + if _, err := PiWriteSkill(dir); err != nil { + t.Fatalf("write skill: %v", err) + } + if _, err := PiWriteExtension(dir); err != nil { + t.Fatalf("write extension: %v", err) + } + other := filepath.Join(dir, "extensions", "custom.ts") + if err := os.WriteFile(other, []byte("export default function () {}\n"), 0644); err != nil { + t.Fatalf("write custom extension: %v", err) + } + + errs := PiEject(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(filepath.Join(dir, "extensions", "mnemon.ts")); !os.IsNotExist(err) { + t.Fatalf("mnemon extension should be removed, err=%v", err) + } + if _, err := os.Stat(other); err != nil { + t.Fatalf("custom extension should be preserved: %v", err) + } +}