From 63b57e9c132282400885418156a795308da8e605 Mon Sep 17 00:00:00 2001 From: JAAvila Date: Sat, 18 Jul 2026 11:10:35 -0500 Subject: [PATCH 1/3] feat(setup): add kimi agent adapter for Kimi Code CLI Kimi Code CLI had no one-liner setup: users had to hand-edit mcp.json to register the engram MCP server and paste the Memory Protocol into AGENTS.md themselves. This adds a declarative "kimi" adapter to the setup registry so "engram setup kimi" registers mcpServers.engram in $KIMI_CODE_HOME/mcp.json and writes the Memory Protocol as a marker-delimited block in $KIMI_CODE_HOME/AGENTS.md, both idempotently and preserving existing content. The adapter is declarative (same shape as qwen/windsurf/kiro) rather than a kimi.plugin.json plugin because Kimi Code plugins only install interactively from the /plugins TUI, while mcp.json and AGENTS.md are documented stable surfaces a CLI installer can write safely. - honor KIMI_CODE_HOME when set, ignoring relative values like the existing XDG helpers - cover the registry entry, slug list, env handling, CLI usage and post-install output with tests - document the new agent in README, AGENT-SETUP, INSTALLATION and codebase/integrations Refs #650 --- README.md | 1 + cmd/engram/main.go | 2 +- cmd/engram/main_test.go | 7 ++++++- docs/AGENT-SETUP.md | 13 ++++++++++++ docs/INSTALLATION.md | 1 + docs/codebase/integrations.md | 2 +- internal/setup/agents.go | 37 +++++++++++++++++++++++++++++++++ internal/setup/registry_test.go | 31 ++++++++++++++++++++++++++- 8 files changed, 90 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6d0c04107..dba58ddf6 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ Windows, Linux, and other install methods → [docs/INSTALLATION.md](docs/INSTAL | Cursor | `engram setup cursor` | | VS Code (Copilot) | `engram setup vscode-copilot` | | Kilo Code | `engram setup kilocode` | +| Kimi Code | `engram setup kimi` | | Any other MCP client | See [docs/AGENT-SETUP.md](docs/AGENT-SETUP.md) | Full per-agent config, Memory Protocol, and compaction survival → [docs/AGENT-SETUP.md](docs/AGENT-SETUP.md) diff --git a/cmd/engram/main.go b/cmd/engram/main.go index 730d783f9..844ccda6d 100644 --- a/cmd/engram/main.go +++ b/cmd/engram/main.go @@ -2681,7 +2681,7 @@ Commands: --dry-run Preview what would be merged (no changes) setup [agent] Install/setup agent integration (opencode, pi, claude-code, gemini-cli, codex, antigravity-cli, windsurf, qwen, kiro, - cursor, vscode-copilot, kilocode) + cursor, vscode-copilot, kilocode, kimi) sync Export new memories as compressed chunk to .engram/ --import Import new chunks from .engram/ into local DB --status Show sync status diff --git a/cmd/engram/main_test.go b/cmd/engram/main_test.go index 76fbc7c06..ade3c7ae2 100644 --- a/cmd/engram/main_test.go +++ b/cmd/engram/main_test.go @@ -166,7 +166,7 @@ func TestPrintUsage(t *testing.T) { if !strings.Contains(stdout, "search ") || !strings.Contains(stdout, "setup [agent]") { t.Fatalf("usage missing expected commands: %q", stdout) } - for _, agent := range []string{"opencode", "pi", "claude-code", "gemini-cli", "codex", "antigravity-cli", "windsurf", "qwen", "kiro", "cursor", "vscode-copilot", "kilocode"} { + for _, agent := range []string{"opencode", "pi", "claude-code", "gemini-cli", "codex", "antigravity-cli", "windsurf", "qwen", "kiro", "cursor", "vscode-copilot", "kilocode", "kimi"} { if !strings.Contains(stdout, agent) { t.Fatalf("usage missing setup agent %q: %q", agent, stdout) } @@ -266,6 +266,11 @@ func TestPrintPostInstall(t *testing.T) { result: &setup.Result{Agent: "kilocode"}, expects: []string{"Restart Kilo Code", "~/.config/kilo/opencode.json"}, }, + { + name: "kimi", + result: &setup.Result{Agent: "kimi"}, + expects: []string{"Restart Kimi Code", "~/.kimi-code/mcp.json", "~/.kimi-code/AGENTS.md"}, + }, { name: "unknown", result: &setup.Result{Agent: "unknown"}, diff --git a/docs/AGENT-SETUP.md b/docs/AGENT-SETUP.md index c57f0a8cf..3abcbf65a 100644 --- a/docs/AGENT-SETUP.md +++ b/docs/AGENT-SETUP.md @@ -28,6 +28,7 @@ Engram works with **any MCP-compatible agent**. Pick your agent below. | Cursor | `engram setup cursor` | [Details](#cursor) | | VS Code Copilot | `engram setup vscode-copilot` | [Details](#vs-code-copilot--claude-code-extension) | | Kilo Code | `engram setup kilocode` | [Details](#kilo-code) | +| Kimi Code | `engram setup kimi` | [Details](#kimi-code) | | Any MCP agent | `engram mcp` (stdio) | [Details](#any-other-mcp-agent) | > **Native setup for all agents above.** `engram setup ` writes the right @@ -691,6 +692,18 @@ Registers the engram server under the OpenCode-style `mcp` object in `~/.config/ --- +## Kimi Code + +**Automated:** + +```bash +engram setup kimi +``` + +Registers `mcpServers.engram` in `~/.kimi-code/mcp.json` and writes the Memory Protocol as a marker block in `~/.kimi-code/AGENTS.md`. Both files live under the Kimi Code data root, so when `KIMI_CODE_HOME` is set the setup honors it and writes there instead. + +--- + ## Any other MCP agent The pattern is always the same — point your agent's MCP config to `engram mcp` via stdio transport. diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 3923197dc..16bfe00ba 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -197,4 +197,5 @@ When using `engram setup`, config files are written to platform-appropriate loca | Cursor | `~/.cursor/mcp.json` + `~/.cursor/rules/engram.mdc` | `%USERPROFILE%\.cursor\...` | | VS Code Copilot | `~/.config/Code/User/mcp.json` + `.../prompts/engram.instructions.md` (macOS: `~/Library/Application Support/Code/User/`) | `%APPDATA%\Code\User\...` | | Kilo Code | `~/.config/kilo/opencode.json` + `~/.config/kilo/AGENTS.md` | `%USERPROFILE%\.config\kilo\...` | +| Kimi Code | `~/.kimi-code/mcp.json` + `~/.kimi-code/AGENTS.md` | `%USERPROFILE%\.kimi-code\...` | | Data directory | `~/.engram/` | `%USERPROFILE%\.engram\` | diff --git a/docs/codebase/integrations.md b/docs/codebase/integrations.md index ba3a4c23e..3898c15a6 100644 --- a/docs/codebase/integrations.md +++ b/docs/codebase/integrations.md @@ -39,7 +39,7 @@ is either: - **declarative** — just an MCP path + format (`mcpServers` / `servers` / OpenCode's `mcp` object) and instruction surfaces; the generic `injectMCP` / `writeInstruction` driver in `registry.go` does the writes. Antigravity CLI, Windsurf, Qwen, Kiro, - Cursor, VS Code Copilot, and Kilo Code are all declarative. + Cursor, VS Code Copilot, Kilo Code, and Kimi Code are all declarative. Adding a declarative agent is normally just a new entry in `agentAdapters()` plus its path helpers — no new install code path. Agents not in the registry remain diff --git a/internal/setup/agents.go b/internal/setup/agents.go index 51e619a7f..4aa46b51c 100644 --- a/internal/setup/agents.go +++ b/internal/setup/agents.go @@ -160,6 +160,20 @@ func agentAdapters() []agentAdapter { "Verify ~/.config/kilo/AGENTS.md has the Memory Protocol block", }, }, + { + slug: "kimi", + description: "Kimi Code CLI — MCP registration in ~/.kimi-code/mcp.json plus AGENTS.md Memory Protocol", + mcpPath: kimiMCPPath, + mcpFormat: mcpServersObject, + instructions: []instrSurface{ + {path: kimiAgentsPath, style: markerBlock, body: memoryProtocolMarkdown}, + }, + postInstall: []string{ + "Restart Kimi Code so MCP config is reloaded", + "Verify ~/.kimi-code/mcp.json includes mcpServers.engram", + "Verify ~/.kimi-code/AGENTS.md has the Memory Protocol block", + }, + }, } } @@ -291,3 +305,26 @@ func kilocodeConfigPath() string { func kilocodeAgentsPath() string { return filepath.Join(kilocodeConfigDir(), "AGENTS.md") } + +// ─── Kimi Code paths ───────────────────────────────────────────────────────── +// +// Kimi Code keeps all user-level data under KIMI_CODE_HOME (default +// ~/.kimi-code on every platform, including Windows). MCP servers are declared +// in mcp.json (top-level "mcpServers") and global agent instructions in +// AGENTS.md; both live directly under the data root. + +func kimiCodeHome() string { + if dir := os.Getenv("KIMI_CODE_HOME"); dir != "" && filepath.IsAbs(dir) { + return dir + } + home, _ := userHome() + return filepath.Join(home, ".kimi-code") +} + +func kimiMCPPath() string { + return filepath.Join(kimiCodeHome(), "mcp.json") +} + +func kimiAgentsPath() string { + return filepath.Join(kimiCodeHome(), "AGENTS.md") +} diff --git a/internal/setup/registry_test.go b/internal/setup/registry_test.go index ad23d7552..247b0979b 100644 --- a/internal/setup/registry_test.go +++ b/internal/setup/registry_test.go @@ -31,6 +31,7 @@ func declarativeAgents() []declarativeAgent { {"cursor", cursorMCPPath, "mcpServers", mcpServersObject, cursorMemoryProtocolPath, wholeFile}, {"vscode-copilot", vscodeMCPPath, "servers", serversObject, vscodePromptPath, wholeFile}, {"kilocode", kilocodeConfigPath, "mcp", opencodeObject, kilocodeAgentsPath, markerBlock}, + {"kimi", kimiMCPPath, "mcpServers", mcpServersObject, kimiAgentsPath, markerBlock}, } } @@ -44,6 +45,7 @@ func stubRegistryEnv(t *testing.T) string { osExecutable = func() (string, error) { return testEngramBin, nil } t.Setenv("XDG_CONFIG_HOME", "") t.Setenv("APPDATA", "") + t.Setenv("KIMI_CODE_HOME", "") return home } @@ -58,7 +60,7 @@ func TestSupportedAgentsIncludesAllRegistryAgents(t *testing.T) { want := []string{ "opencode", "pi", "claude-code", "gemini-cli", "codex", "antigravity-cli", "windsurf", "qwen", "kiro", "cursor", - "vscode-copilot", "kilocode", + "vscode-copilot", "kilocode", "kimi", } for _, slug := range want { if !got[slug] { @@ -436,4 +438,31 @@ func TestConfigDirsIgnoreRelativeConfigHome(t *testing.T) { t.Errorf("vscodeUserDir with relative APPDATA = %q, want %q", got, want) } }) + + t.Run("relative KIMI_CODE_HOME ignored", func(t *testing.T) { + t.Setenv("KIMI_CODE_HOME", "relative/kimi") + if got, want := kimiCodeHome(), filepath.Join(home, ".kimi-code"); got != want { + t.Errorf("kimiCodeHome with relative KIMI_CODE_HOME = %q, want %q", got, want) + } + }) +} + +// TestKimiCodeHomeHonorsAbsoluteEnv verifies an absolute KIMI_CODE_HOME +// relocates both the MCP config and the AGENTS.md instruction surface. +func TestKimiCodeHomeHonorsAbsoluteEnv(t *testing.T) { + resetSetupSeams(t) + useTestHome(t) + + custom := filepath.Join(t.TempDir(), "kimi-home") + t.Setenv("KIMI_CODE_HOME", custom) + + if got := kimiCodeHome(); got != custom { + t.Errorf("kimiCodeHome = %q, want %q", got, custom) + } + if got, want := kimiMCPPath(), filepath.Join(custom, "mcp.json"); got != want { + t.Errorf("kimiMCPPath = %q, want %q", got, want) + } + if got, want := kimiAgentsPath(), filepath.Join(custom, "AGENTS.md"); got != want { + t.Errorf("kimiAgentsPath = %q, want %q", got, want) + } } From 5bc305c6f02301817d3a64071f9b0874ed38d6f6 Mon Sep 17 00:00:00 2001 From: JAAvila Date: Mon, 21 Sep 2026 08:44:37 -0500 Subject: [PATCH 2/3] fix(setup): name the kimi paths setup actually wrote engram setup kimi honors an absolute KIMI_CODE_HOME, but the post-install steps always pointed at ~/.kimi-code, sending anyone with an override to files setup never touched. Those lines now come from kimiMCPPath() and kimiAgentsPath(), so they follow the resolved data root. AGENT-SETUP states the absolute-only contract: a relative KIMI_CODE_HOME is ignored and falls back to the default root, so config never lands in whatever directory engram happened to run from. The custom-home test installs for real instead of only checking the path helpers: it asserts mcp.json and AGENTS.md land under the override, that the default root is never created, and that the next steps name those files. The CLI case moves out of the post-install table because it needs explicit KIMI_CODE_HOME control. Refs #650 --- cmd/engram/main_test.go | 54 ++++++++++++++++++++++++++++++--- docs/AGENT-SETUP.md | 2 ++ internal/setup/agents.go | 12 ++++++-- internal/setup/registry_test.go | 53 +++++++++++++++++++++++++++----- 4 files changed, 106 insertions(+), 15 deletions(-) diff --git a/cmd/engram/main_test.go b/cmd/engram/main_test.go index 61709962e..125abf15a 100644 --- a/cmd/engram/main_test.go +++ b/cmd/engram/main_test.go @@ -563,11 +563,8 @@ func TestPrintPostInstall(t *testing.T) { result: &setup.Result{Agent: "kilocode"}, expects: []string{"Restart Kilo Code", "~/.config/kilo/opencode.json"}, }, - { - name: "kimi", - result: &setup.Result{Agent: "kimi"}, - expects: []string{"Restart Kimi Code", "~/.kimi-code/mcp.json", "~/.kimi-code/AGENTS.md"}, - }, + // kimi lives in TestPrintPostInstallKimiUsesEffectivePaths: its steps + // depend on KIMI_CODE_HOME, so the expectations need explicit env control. { name: "unknown", result: &setup.Result{Agent: "unknown"}, @@ -597,6 +594,53 @@ func TestPrintPostInstall(t *testing.T) { } } +// TestPrintPostInstallKimiUsesEffectivePaths verifies the Kimi Code next steps +// name the files setup actually wrote: the default data root when +// KIMI_CODE_HOME is unset, and the override when it holds an absolute path. +func TestPrintPostInstallKimiUsesEffectivePaths(t *testing.T) { + kimiSteps := func(t *testing.T) string { + t.Helper() + stdout, stderr := captureOutput(t, func() { printPostInstall(&setup.Result{Agent: "kimi"}) }) + if stderr != "" { + t.Fatalf("expected no stderr, got: %q", stderr) + } + return stdout + } + + t.Run("default data root", func(t *testing.T) { + t.Setenv("KIMI_CODE_HOME", "") + + stdout := kimiSteps(t) + for _, expected := range []string{ + "Restart Kimi Code", + filepath.Join(".kimi-code", "mcp.json"), + filepath.Join(".kimi-code", "AGENTS.md"), + } { + if !strings.Contains(stdout, expected) { + t.Fatalf("output missing %q: %q", expected, stdout) + } + } + }) + + t.Run("absolute KIMI_CODE_HOME", func(t *testing.T) { + custom := t.TempDir() + t.Setenv("KIMI_CODE_HOME", custom) + + stdout := kimiSteps(t) + for _, expected := range []string{ + filepath.Join(custom, "mcp.json"), + filepath.Join(custom, "AGENTS.md"), + } { + if !strings.Contains(stdout, expected) { + t.Fatalf("output missing %q: %q", expected, stdout) + } + } + if strings.Contains(stdout, filepath.Join(".kimi-code", "mcp.json")) { + t.Fatalf("output still points at the default root: %q", stdout) + } + }) +} + func TestPrintPostInstallClaudeCodeAllowlist(t *testing.T) { t.Run("user accepts allowlist", func(t *testing.T) { oldScan := scanInputLine diff --git a/docs/AGENT-SETUP.md b/docs/AGENT-SETUP.md index 3575bbfdc..860b99130 100644 --- a/docs/AGENT-SETUP.md +++ b/docs/AGENT-SETUP.md @@ -735,6 +735,8 @@ engram setup kimi Registers `mcpServers.engram` in `~/.kimi-code/mcp.json` and writes the Memory Protocol as a marker block in `~/.kimi-code/AGENTS.md`. Both files live under the Kimi Code data root, so when `KIMI_CODE_HOME` is set the setup honors it and writes there instead. +**`KIMI_CODE_HOME` is only honored when it is an absolute path.** A relative value (for example `KIMI_CODE_HOME=.kimi-code`) is ignored and setup falls back to the default `~/.kimi-code` root, so config never lands in whatever directory you happened to run `engram` from. The `Next steps` printed after setup name the files that were actually written, so they follow the override. + --- ## Any other MCP agent diff --git a/internal/setup/agents.go b/internal/setup/agents.go index e34ebfa60..36ed0f664 100644 --- a/internal/setup/agents.go +++ b/internal/setup/agents.go @@ -1,6 +1,7 @@ package setup import ( + "fmt" "os" "path/filepath" ) @@ -168,10 +169,13 @@ func agentAdapters() []agentAdapter { instructions: []instrSurface{ {path: kimiAgentsPath, style: markerBlock, body: memoryProtocolMarkdown}, }, + // Built from the resolved paths instead of the default root: an + // absolute KIMI_CODE_HOME relocates both files, so a hardcoded + // ~/.kimi-code would send the user to files setup never wrote. postInstall: []string{ "Restart Kimi Code so MCP config is reloaded", - "Verify ~/.kimi-code/mcp.json includes mcpServers.engram", - "Verify ~/.kimi-code/AGENTS.md has the Memory Protocol block", + fmt.Sprintf("Verify %s includes mcpServers.engram", kimiMCPPath()), + fmt.Sprintf("Verify %s has the Memory Protocol block", kimiAgentsPath()), }, }, } @@ -311,7 +315,9 @@ func kilocodeAgentsPath() string { // Kimi Code keeps all user-level data under KIMI_CODE_HOME (default // ~/.kimi-code on every platform, including Windows). MCP servers are declared // in mcp.json (top-level "mcpServers") and global agent instructions in -// AGENTS.md; both live directly under the data root. +// AGENTS.md; both live directly under the data root. Like the XDG/APPDATA +// helpers above, only an absolute KIMI_CODE_HOME is honored — a relative value +// falls back to the default root instead of writing under the working directory. func kimiCodeHome() string { if dir := os.Getenv("KIMI_CODE_HOME"); dir != "" && filepath.IsAbs(dir) { diff --git a/internal/setup/registry_test.go b/internal/setup/registry_test.go index 162ae834b..6210ca62b 100644 --- a/internal/setup/registry_test.go +++ b/internal/setup/registry_test.go @@ -462,10 +462,11 @@ func TestConfigDirsIgnoreRelativeConfigHome(t *testing.T) { } // TestKimiCodeHomeHonorsAbsoluteEnv verifies an absolute KIMI_CODE_HOME -// relocates both the MCP config and the AGENTS.md instruction surface. +// relocates both the MCP config and the AGENTS.md instruction surface, that a +// real Install writes both files under the override (and nothing under the +// default root), and that the post-install steps name those same files. func TestKimiCodeHomeHonorsAbsoluteEnv(t *testing.T) { - resetSetupSeams(t) - useTestHome(t) + home := stubRegistryEnv(t) custom := filepath.Join(t.TempDir(), "kimi-home") t.Setenv("KIMI_CODE_HOME", custom) @@ -473,10 +474,48 @@ func TestKimiCodeHomeHonorsAbsoluteEnv(t *testing.T) { if got := kimiCodeHome(); got != custom { t.Errorf("kimiCodeHome = %q, want %q", got, custom) } - if got, want := kimiMCPPath(), filepath.Join(custom, "mcp.json"); got != want { - t.Errorf("kimiMCPPath = %q, want %q", got, want) + mcpPath := filepath.Join(custom, "mcp.json") + agentsPath := filepath.Join(custom, "AGENTS.md") + if got := kimiMCPPath(); got != mcpPath { + t.Errorf("kimiMCPPath = %q, want %q", got, mcpPath) + } + if got := kimiAgentsPath(); got != agentsPath { + t.Errorf("kimiAgentsPath = %q, want %q", got, agentsPath) + } + + result, err := Install("kimi") + if err != nil { + t.Fatalf("Install(kimi): %v", err) + } + if result.Files != 2 { + t.Fatalf("expected 2 files, got %d", result.Files) + } + + entry := readEngramEntry(t, mcpPath, "mcpServers") + if entry["command"] != testEngramBin { + t.Errorf("expected command %q under the override, got %#v", testEngramBin, entry["command"]) + } + + instr, err := os.ReadFile(agentsPath) + if err != nil { + t.Fatalf("read %s: %v", agentsPath, err) + } + if !strings.Contains(string(instr), engramMarkerBegin) { + t.Errorf("AGENTS.md under the override is missing the Memory Protocol marker block") + } + + // The default root stays untouched: an honored override must not also seed + // ~/.kimi-code with a half-written config the user would never read. + if _, err := os.Stat(filepath.Join(home, ".kimi-code")); !errors.Is(err, os.ErrNotExist) { + t.Errorf("default root ~/.kimi-code should not exist when KIMI_CODE_HOME is honored (stat error = %v)", err) + } + + // Post-install guidance points at the files that were actually written. + steps := strings.Join(PostInstallSteps("kimi"), "\n") + if !strings.Contains(steps, mcpPath) { + t.Errorf("post-install steps missing %q: %q", mcpPath, steps) } - if got, want := kimiAgentsPath(), filepath.Join(custom, "AGENTS.md"); got != want { - t.Errorf("kimiAgentsPath = %q, want %q", got, want) + if !strings.Contains(steps, agentsPath) { + t.Errorf("post-install steps missing %q: %q", agentsPath, steps) } } From 5f994cffacb0007f66146071e6b4aa72baeb4693 Mon Sep 17 00:00:00 2001 From: Daniel Rosales <111561081+dnlrsls@users.noreply.github.com> Date: Tue, 22 Sep 2026 16:08:33 -0500 Subject: [PATCH 3/3] docs(setup): avoid duplicate agent table --- README.md | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/README.md b/README.md index fca007f71..8f0f59641 100644 --- a/README.md +++ b/README.md @@ -85,22 +85,6 @@ Treat Engram as a curated project memory, not a transcript sink. Use this operat **Learned**: Reuse the request id as the idempotency key. ``` -| Agent | One-liner | -| --------------------------- | -------------------------------------------------------------------------------------------- | -| Claude Code | `claude plugin marketplace add Gentleman-Programming/engram && claude plugin install engram` | -| Pi | `engram setup pi` | -| OpenCode | `engram setup opencode` | -| Gemini CLI | `engram setup gemini-cli` | -| Codex | `engram setup codex` | -| Antigravity CLI | `engram setup antigravity-cli` | -| Windsurf | `engram setup windsurf` | -| Qwen Code | `engram setup qwen` | -| Kiro | `engram setup kiro` | -| Cursor | `engram setup cursor` | -| VS Code (Copilot) | `engram setup vscode-copilot` | -| Kilo Code | `engram setup kilocode` | -| Kimi Code | `engram setup kimi` | -| Any other MCP client | See [docs/AGENT-SETUP.md](docs/AGENT-SETUP.md) | Use a short, searchable title and a fitting type with that content. The full [Memory Protocol](DOCS.md#memory-protocol) defines the durable-save rules and session-summary shape. ### Choose MCP tools by intent @@ -152,6 +136,7 @@ Run the setup command for the agent you use, then restart that agent. `engram se | Cursor | `engram setup cursor` | | VS Code (Copilot) | `engram setup vscode-copilot` | | Kilo Code | `engram setup kilocode` | +| Kimi Code | `engram setup kimi` | | Another MCP-compatible agent | [Manual MCP setup](docs/AGENT-SETUP.md#any-other-mcp-agent) | See [Agent Setup](docs/AGENT-SETUP.md) for per-agent configuration, plugin behavior, manual MCP setup, compaction resilience, and troubleshooting. Pi users can also find the package at [`gentle-engram`](plugin/pi/README.md).