feat(kimi): Mem0 plugin for Kimi Code (MCP + skills + auto-capture) (#6919)

This commit is contained in:
Himanshu
2026-08-13 16:15:04 +05:30
committed by GitHub
parent 14c431735b
commit ba2fb9f4c3
14 changed files with 624 additions and 3 deletions
@@ -0,0 +1,126 @@
"""Tests for the mem0.md extraction-policy feature.
Covers the parser (`parse_section_text` + the Instructions sections in
`load_full_config`) and the `_instructions.load_instructions` helper that the
hook writers use to attach `custom_instructions` / `agent_custom_instructions`
to a memory write.
"""
from __future__ import annotations
MEM0_MD = """\
# mem0.md
## Categories
- architecture_decisions
## Instructions
Remember architecture decisions and conventions.
Ignore transient debug output and secrets (issue #123 refs are fine).
## Agent Instructions
For agent memories, focus on tools and task outcomes.
"""
# ---------------------------------------------------------------------------
# parse_section_text
# ---------------------------------------------------------------------------
def test_parse_section_text_collapses_prose():
from parse_mem0_config import parse_section_text
text = parse_section_text(MEM0_MD, "Instructions")
assert text == (
"Remember architecture decisions and conventions. "
"Ignore transient debug output and secrets (issue #123 refs are fine)."
)
def test_parse_section_text_preserves_inline_hash():
"""Inline '#' (e.g. issue refs) is kept; only full-line comments are dropped."""
from parse_mem0_config import parse_section_text
assert "#123" in parse_section_text(MEM0_MD, "Instructions")
def test_instructions_and_agent_instructions_are_distinct():
"""'## Instructions' must not swallow '## Agent Instructions'."""
from parse_mem0_config import parse_section_text
assert parse_section_text(MEM0_MD, "Instructions").startswith("Remember architecture")
assert parse_section_text(MEM0_MD, "Agent Instructions") == (
"For agent memories, focus on tools and task outcomes."
)
def test_parse_section_text_missing_returns_empty():
from parse_mem0_config import parse_section_text
assert parse_section_text("## Retention\nx: 1d\n", "Instructions") == ""
def test_load_full_config_includes_instructions(tmp_path):
from parse_mem0_config import load_full_config
(tmp_path / "mem0.md").write_text(MEM0_MD)
config = load_full_config(str(tmp_path))
assert config["instructions"].startswith("Remember architecture")
assert config["agent_instructions"].startswith("For agent memories")
# ---------------------------------------------------------------------------
# load_instructions (the helper the hook writers call)
# ---------------------------------------------------------------------------
def test_load_instructions_maps_to_api_field_names(tmp_path):
from _instructions import load_instructions
(tmp_path / "mem0.md").write_text(MEM0_MD)
out = load_instructions(str(tmp_path))
assert out == {
"custom_instructions": (
"Remember architecture decisions and conventions. "
"Ignore transient debug output and secrets (issue #123 refs are fine)."
),
"agent_custom_instructions": "For agent memories, focus on tools and task outcomes.",
}
def test_load_instructions_no_config_is_empty(tmp_path):
"""No mem0.md -> empty dict, so a write body gains nothing."""
from _instructions import load_instructions
assert load_instructions(str(tmp_path)) == {}
def test_load_instructions_only_custom(tmp_path):
"""A project with only '## Instructions' omits the agent key entirely."""
from _instructions import load_instructions
(tmp_path / "mem0.md").write_text("## Instructions\nRemember decisions.\n")
out = load_instructions(str(tmp_path))
assert out == {"custom_instructions": "Remember decisions."}
def test_load_instructions_defaults_cwd_to_env(tmp_path, monkeypatch):
"""With no arg, cwd falls back to MEM0_CWD."""
from _instructions import load_instructions
(tmp_path / "mem0.md").write_text("## Instructions\nRemember decisions.\n")
monkeypatch.setenv("MEM0_CWD", str(tmp_path))
assert load_instructions() == {"custom_instructions": "Remember decisions."}
def test_load_instructions_body_merge_shape(tmp_path):
"""The result merges cleanly into an add body without clobbering other keys."""
from _instructions import load_instructions
(tmp_path / "mem0.md").write_text("## Instructions\nRemember decisions.\n")
body = {"messages": [], "user_id": "u", "infer": True}
body.update(load_instructions(str(tmp_path)))
assert body["user_id"] == "u" and body["infer"] is True
assert body["custom_instructions"] == "Remember decisions."
assert "agent_custom_instructions" not in body
@@ -0,0 +1,82 @@
"""Validate the Kimi Code plugin manifests and hook wiring.
Kimi Code loads a plugin from `.kimi-plugin/plugin.json`. This test locks down
the structural contract so a stray edit cannot silently break the plugin:
- both manifests parse and carry the required Kimi fields
- the MCP server uses Kimi's supported auth (transport/url/bearerTokenEnvVar)
- every hook command routes through kimi_hook_shim.sh and targets a script
that actually exists in scripts/
- the sessionStart skill and skills/ path resolve on disk
"""
import json
import os
import pytest
PLUGIN_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
REPO_ROOT = os.path.dirname(os.path.dirname(PLUGIN_DIR))
SUBDIR_MANIFEST = os.path.join(PLUGIN_DIR, ".kimi-plugin", "plugin.json")
ROOT_MARKETPLACE = os.path.join(REPO_ROOT, ".kimi-plugin", "marketplace.json")
SHIM = os.path.join(PLUGIN_DIR, "scripts", "kimi_hook_shim.sh")
def _load(path):
with open(path) as fh:
return json.load(fh)
def test_manifest_parses_and_has_required_fields():
m = _load(SUBDIR_MANIFEST)
assert m["name"] == "mem0"
for field in ("version", "description", "skills", "mcpServers", "interface", "hooks"):
assert field in m, f"manifest missing {field}"
def test_mcp_uses_kimi_supported_auth():
srv = _load(SUBDIR_MANIFEST)["mcpServers"]["mem0"]
assert srv["transport"] == "http"
assert srv["url"] == "https://mcp.mem0.ai/mcp/" # trailing slash avoids the 307 redirect
# Kimi reads the named env var and sends Authorization: Bearer <value>.
assert srv["bearerTokenEnvVar"] == "MEM0_API_KEY"
assert "headers" not in srv # Kimi does not interpolate ${env:...} inside headers
def test_sessionstart_skill_and_skills_dir_resolve():
m = _load(SUBDIR_MANIFEST)
assert m["skills"] == "./skills/"
assert os.path.isdir(os.path.join(PLUGIN_DIR, "skills"))
start_skill = m["sessionStart"]["skill"]
assert os.path.isdir(os.path.join(PLUGIN_DIR, "skills", start_skill))
def test_shim_exists_and_is_executable():
assert os.path.isfile(SHIM)
assert os.access(SHIM, os.X_OK), "kimi_hook_shim.sh must be executable"
def test_every_hook_routes_through_shim_to_a_real_script():
hooks = _load(SUBDIR_MANIFEST)["hooks"]
assert hooks, "no hooks declared"
valid_events = {
"SessionStart", "UserPromptSubmit", "PreToolUse",
"PostToolUse", "Stop", "PreCompact",
}
for hook in hooks:
assert hook["event"] in valid_events, f"unknown event {hook['event']}"
cmd = hook["command"]
assert "kimi_hook_shim.sh" in cmd, f"hook must route through the shim: {cmd}"
assert "$KIMI_PLUGIN_ROOT/scripts/kimi_hook_shim.sh" in cmd
target = cmd.rsplit('"', 1)[-1].strip() # the script name after the shim
assert target.endswith(".sh")
assert os.path.isfile(os.path.join(PLUGIN_DIR, "scripts", target)), \
f"hook target script missing: {target}"
def test_root_marketplace_catalog():
cat = _load(ROOT_MARKETPLACE)
entry = cat["plugins"][0]
assert entry["id"] == "mem0" # Kimi marketplace entries key on id
assert entry["source"].startswith("https://github.com/mem0ai/mem0")
@@ -157,6 +157,22 @@ def test_platform_codex(monkeypatch):
assert telemetry.detect_platform() == "codex"
def test_platform_kimi(monkeypatch):
"""Kimi Code usage must be attributed to its own platform so it can be
counted distinctly. The shim also pins MEM0_PLATFORM=kimi; this covers the
KIMI_PLUGIN_ROOT auto-detection fallback."""
import telemetry
monkeypatch.delenv("MEM0_PLATFORM", raising=False)
monkeypatch.delenv("ANTIGRAVITY_PLUGIN_ROOT", raising=False)
monkeypatch.delenv("CLAUDECODE", raising=False)
monkeypatch.delenv("CLAUDE_PLUGIN_ROOT", raising=False)
monkeypatch.delenv("CURSOR_PLUGIN_ROOT", raising=False)
monkeypatch.delenv("PLUGIN_ROOT", raising=False)
monkeypatch.setenv("KIMI_PLUGIN_ROOT", "/path")
assert telemetry.detect_platform() == "kimi"
def test_platform_explicit_override(monkeypatch):
"""MEM0_PLATFORM wins over auto-detection so each editor can label
itself reliably even when host env vars are ambiguous or absent."""
@@ -190,6 +206,7 @@ def test_plugin_version_is_per_editor(monkeypatch):
"claude-code": os.path.join(".claude-plugin", "plugin.json"),
"cursor": os.path.join(".cursor-plugin", "plugin.json"),
"codex": os.path.join(".codex-plugin", "plugin.json"),
"kimi": os.path.join(".kimi-plugin", "plugin.json"),
}
for plat, rel in manifests.items():
monkeypatch.setenv("MEM0_PLATFORM", plat)