Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,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).
Expand Down
2 changes: 1 addition & 1 deletion cmd/engram/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3627,7 +3627,7 @@ Commands:
--paths-only Limit pruning to project names containing / or \
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
Expand Down
51 changes: 50 additions & 1 deletion cmd/engram/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ func TestPrintUsage(t *testing.T) {
if !strings.Contains(stdout, "search <query>") || !strings.Contains(stdout, "[--match all|any]") || !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)
}
Expand Down Expand Up @@ -563,6 +563,8 @@ func TestPrintPostInstall(t *testing.T) {
result: &setup.Result{Agent: "kilocode"},
expects: []string{"Restart Kilo Code", "~/.config/kilo/opencode.json"},
},
// 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"},
Expand Down Expand Up @@ -592,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
Expand Down
15 changes: 15 additions & 0 deletions docs/AGENT-SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <agent>` configures the
Expand Down Expand Up @@ -724,6 +725,20 @@ 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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

**`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

The pattern is always the same — point your agent's MCP config to `engram mcp` via stdio transport.
Expand Down
1 change: 1 addition & 0 deletions docs/INSTALLATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,4 +227,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\` |
2 changes: 1 addition & 1 deletion docs/codebase/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions internal/setup/agents.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package setup

import (
"fmt"
"os"
"path/filepath"
)
Expand Down Expand Up @@ -160,6 +161,23 @@ 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},
},
// 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",
fmt.Sprintf("Verify %s includes mcpServers.engram", kimiMCPPath()),
fmt.Sprintf("Verify %s has the Memory Protocol block", kimiAgentsPath()),
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
}
}

Expand Down Expand Up @@ -291,3 +309,28 @@ 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. 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) {
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")
}
70 changes: 69 additions & 1 deletion internal/setup/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
}
}

Expand All @@ -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
}

Expand All @@ -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] {
Expand Down Expand Up @@ -450,4 +452,70 @@ 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, 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) {
home := stubRegistryEnv(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)
}
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 !strings.Contains(steps, agentsPath) {
t.Errorf("post-install steps missing %q: %q", agentsPath, steps)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Loading