From 86b67e3ebecde5f2dac7f6fcbc07fd04c262ca6a Mon Sep 17 00:00:00 2001 From: Grivn Date: Sun, 31 May 2026 12:47:15 +0000 Subject: [PATCH] feat(setup): add Pi integration Add Pi as a supported setup target with detection, local/global install paths, skill deployment, and a TypeScript lifecycle extension that maps Mnemon reminders onto Pi events. Eject removes only the Mnemon skill and extension while preserving unrelated Pi files. Document the Pi setup flow in the English and Chinese README/usage docs and update the runtime mapping. Validation: go test ./..., go build -o mnemon ., and a temporary mnemon setup --target pi --yes install check. --- README.md | 18 ++++++- cmd/setup.go | 84 ++++++++++++++++++++++++++--- docs/USAGE.md | 5 +- docs/design/07-integration.md | 1 + docs/zh/README.md | 16 +++++- docs/zh/USAGE.md | 7 ++- docs/zh/design/07-integration.md | 1 + internal/setup/assets/assets.go | 8 ++- internal/setup/assets/pi/SKILL.md | 57 ++++++++++++++++++++ internal/setup/assets/pi/mnemon.ts | 76 ++++++++++++++++++++++++++ internal/setup/detect.go | 43 ++++++++++++++- internal/setup/pi.go | 65 +++++++++++++++++++++++ internal/setup/pi_test.go | 85 ++++++++++++++++++++++++++++++ 13 files changed, 449 insertions(+), 17 deletions(-) create mode 100644 internal/setup/assets/pi/SKILL.md create mode 100644 internal/setup/assets/pi/mnemon.ts create mode 100644 internal/setup/pi.go create mode 100644 internal/setup/pi_test.go 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) + } +}