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
26 changes: 25 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,31 @@ Two readers exist: `claude_code` and `codex`. Adding a third means adding a `Rea

- **`plugins/engram/` is the single source of truth.** `install.rs` embeds the command bodies with `include_str!`, so the plugin directory and the installed files cannot drift and the compiler enforces the files exist. Exactly two substitutions, via `str::replace`, no template engine: `{{DB}}` and `{{HARNESS}}`.
- **`{{DB}}` is load-bearing.** The path is discovered from the harness's *own* MCP registration (`harness::registered_db`) — on a typical host all writable harnesses point at one shared store (here `~/.local/share/engram/engram.db`) — but see the drift note below: what they registered *yesterday* is not necessarily what a previously-generated command still pins. A generated command that omitted `--db` would fall back to clap's relative `engram.db` default and quietly write to a different store than the agents read. Config formats are scanned narrowly rather than deserialized: JSON (`mcpServers`, or Opencode's `mcp`), **JSONC** (comment-stripped by a string-aware pass — a `//` inside `"https://…"` must survive), and TOML (line-scanned, so engram needs no TOML dependency). Engram **reads** JSONC and never rewrites it; a serde round-trip would delete the user's comments.
- **7 of 9 harnesses can host something; only 3 host a *command*.** Claude Code, **OpenClaude**, and Opencode have writable command dirs. **Codex 0.149 removed `~/.codex/prompts/`** — the binary contains no such string — and moved to skills at `~/.codex/skills/<name>/SKILL.md`, discovered automatically with nothing to register; engram writes there now. It wrote prompt files nobody read for a release, which is exactly what a harness table drifting from reality looks like. **Antigravity has no slash-command directory at all** — its extension surface is skills, packaged in plugins, and `agy plugin validate` reports a plugin's `commands/` as "2 processed (converted to skills)", so a command there is a skill either way. Engram writes it a plugin (`~/.gemini/config/plugins/engram/`: `plugin.json` + one `skills/engram-<name>/SKILL.md` per command). **Kimi** and **Qwen** take skills in their own config roots (`~/.kimi-code/skills`, `~/.qwen/skills`) — Qwen's was verified against its own bundled `docs/features/skills.md`, having previously been dismissed as "format unverified". Goose and Copilot CLI have nothing engram can write and each says so **in its own words** — one shared sentence described none of them accurately.
- **9 of 11 harnesses can host something.** Claude Code, **OpenClaude**, and Opencode have writable command dirs. **Codex 0.149 removed `~/.codex/prompts/`** — the binary contains no such string — and moved to skills at `~/.codex/skills/<name>/SKILL.md`, discovered automatically with nothing to register; engram writes there now. It wrote prompt files nobody read for a release, which is exactly what a harness table drifting from reality looks like. **Antigravity has no slash-command directory at all** — its extension surface is skills, packaged in plugins, and `agy plugin validate` reports a plugin's `commands/` as "2 processed (converted to skills)", so a command there is a skill either way. Engram writes it a plugin (`~/.gemini/config/plugins/engram/`: `plugin.json` + one `skills/engram-<name>/SKILL.md` per command). **Kimi** and **Qwen** take skills in their own config roots (`~/.kimi-code/skills`, `~/.qwen/skills`) — Qwen's was verified against its own bundled `docs/features/skills.md`, having previously been dismissed as "format unverified". Goose and Copilot CLI have nothing engram can write and each says so **in its own words** — one shared sentence described none of them accurately.
- **A skill description is YAML, so engram quotes it.** `description: Save this
conversation: capture the transcript ...` emitted bare is invalid YAML
(`mapping values are not allowed in this context`) and the **whole skill
silently fails to load** — Antigravity offered two of engram's three commands
for exactly this reason, with no error anywhere. `yaml_quote` emits a
double-quoted scalar, the form that survives colons, `#`, and the apostrophe
in "engram's" alike.
- **VS Code and Cursor, both first-party-verified.** VS Code takes a reusable
prompt file, `~/.config/Code/User/prompts/engram-<name>.prompt.md` — Microsoft
documents the `.prompt.md` extension and VS Code itself creates the profile
folder — and its `mcp.json` keys servers under `servers`, not `mcpServers`,
which `json_engram_args` now accepts. Cursor takes a skill at
`~/.cursor/skills/<name>/SKILL.md`: both that path and `SKILL.md` appear
inside the cursor-agent binary. `~/.cursor/skills-cursor` is the vendor's own
bundle and is *not* a user surface — Grok's compatibility scanner filters
those same vendor defaults out.
- **A shared skills directory duplicates commands, and engram says so.**
`also_scans` records the directories a harness *loads* from but engram never
writes. On a machine where `~/.claude/skills` and `~/.codex/skills` both
resolve to one library (a normal way to keep a single skill set), the
commands engram writes for Codex are also loaded by Claude Code, which then
offers every engram command twice. Engram cannot fix this by writing
differently — both targets are correct for their own harness — so `install`
reports the overlap and names the harness whose commands are being doubled.
- **Grok needs no harness entry, and adding one would duplicate its commands.**
Grok reads other vendors' directories on purpose: `[compat.claude]` in
`~/.grok/config.toml` defaults every cell to `true`, so `~/.claude/commands/`
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ follows [Keep a Changelog](https://keepachangelog.com/); versions follow

### Fixed

- **A skill description containing a colon silently broke the whole skill.**
`description: Save this conversation: capture ...` is invalid YAML, so
Antigravity loaded two of engram's three commands and reported nothing.
Descriptions are now emitted as quoted scalars.

- **An unwritable target no longer aborts the whole install.** A read-only
command or skills directory is now reported per file with its reason and the
run continues; previously the `EROFS` propagated and every harness after the
Expand All @@ -32,6 +37,13 @@ follows [Keep a Changelog](https://keepachangelog.com/); versions follow

### Added

- **VS Code** and **Cursor** are supported harnesses (the tenth and eleventh).
VS Code gets `~/.config/Code/User/prompts/engram-<name>.prompt.md`; Cursor
gets `~/.cursor/skills/engram-<name>/SKILL.md`. VS Code's `mcp.json` keys its
servers under `servers`, which the MCP scanner now reads.
- `install` warns when two harnesses share a command or skills directory, so a
duplicated slash command is explained rather than left to be noticed.

- **Kimi** is a supported harness (the ninth). Skills go to
`~/.kimi-code/skills/engram-<name>/SKILL.md`; the MCP registration is read
from `~/.kimi-code/mcp.json`. Its transcripts (`~/.kimi/sessions/<project>/
Expand Down
78 changes: 76 additions & 2 deletions src/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ pub enum Harness {
Codex,
Opencode,
Kimi,
/// Renamed on both derives for the same reason as `OpenClaude`:
/// kebab-casing gives `vs-code`, but the editor is written `vscode` and
/// that is what [`HarnessSpec::name`] carries.
#[serde(rename = "vscode")]
#[clap(name = "vscode")]
VsCode,
Cursor,
Antigravity,
Goose,
CopilotCli,
Expand Down Expand Up @@ -124,6 +131,16 @@ pub struct HarnessSpec {
/// can host one, and claiming otherwise would make `install` look broken
/// on the rest.
pub command_surface: CommandSurface,
/// Extra home-relative directories this harness *loads* commands or skills
/// from, which engram never writes to.
///
/// Engram writes one target per harness, but several harnesses read more
/// than one directory, and on a machine where those directories are
/// symlinked together a command engram wrote for harness A shows up a
/// second time in harness B. Recording what a harness reads is what lets
/// `install` say so instead of leaving the user to notice the duplicate in
/// their own slash-command list.
pub also_scans: &'static [&'static str],
/// Where this harness registers MCP servers, when engram knows. Read to
/// discover which database the user already shares between harnesses.
pub mcp_config: Option<McpConfigSource>,
Expand All @@ -138,7 +155,8 @@ pub struct HarnessSpec {
/// database the user actually shares between harnesses rather than guessing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum McpConfigSource {
/// A JSON file with an `mcpServers` (or `mcp`) object at the top level.
/// A JSON file with an `mcpServers` (or `mcp`, or VS Code's `servers`)
/// object at the top level.
Json(&'static str),
/// The same, but JSON **with comments** — Opencode's `opencode.jsonc`.
/// Comments are stripped before parsing; the file itself is never
Expand All @@ -162,11 +180,13 @@ pub const ALL: &[HarnessSpec] = &[
probe: &[".claude", ".claude.json"],
sessions_dir: Some(".claude/projects"),
transcript: TranscriptSupport::Reader(ReaderKind::ClaudeCode),
// Claude Code loads skills as well as commands.
command_surface: CommandSurface::Markdown {
dir: ".claude/commands",
file: "engram-{name}.md",
frontmatter: true,
},
also_scans: &[".claude/skills"],
mcp_config: Some(McpConfigSource::Json(".claude.json")),
hooks_config: Some(".claude/settings.json"),
},
Expand All @@ -188,6 +208,7 @@ pub const ALL: &[HarnessSpec] = &[
file: "engram-{name}.md",
frontmatter: true,
},
also_scans: &[".openclaude/skills"],
mcp_config: Some(McpConfigSource::Json(".openclaude.json")),
// The fork has a `hooks` key, but its shape is unverified against a
// real run; `install --hooks` stays Claude-Code-only until it is.
Expand All @@ -205,6 +226,7 @@ pub const ALL: &[HarnessSpec] = &[
command_surface: CommandSurface::Skill {
dir: ".codex/skills",
},
also_scans: &[],
mcp_config: Some(McpConfigSource::Toml(".codex/config.toml")),
hooks_config: None,
},
Expand All @@ -221,6 +243,7 @@ pub const ALL: &[HarnessSpec] = &[
file: "engram-{name}.md",
frontmatter: true,
},
also_scans: &[],
mcp_config: Some(McpConfigSource::Jsonc(".config/opencode/opencode.jsonc")),
hooks_config: None,
},
Expand All @@ -241,9 +264,54 @@ pub const ALL: &[HarnessSpec] = &[
command_surface: CommandSurface::Skill {
dir: ".kimi-code/skills",
},
also_scans: &[],
mcp_config: Some(McpConfigSource::Json(".kimi-code/mcp.json")),
hooks_config: None,
},
HarnessSpec {
id: Harness::VsCode,
name: "vscode",
probe: &[".config/Code"],
// Chat history lives in workspaceStorage as editor state, not as a
// transcript engram can read turn by turn.
sessions_dir: None,
transcript: TranscriptSupport::Unsupported {
detail: "vs code keeps chat in workspaceStorage as editor state, not a \
per-session transcript file",
},
// Reusable prompt files: `<name>.prompt.md` in the profile's `prompts`
// folder, invoked with `/<name>`. The extension and the `description`
// frontmatter field are Microsoft-documented; the directory is created
// by VS Code itself.
command_surface: CommandSurface::Markdown {
dir: ".config/Code/User/prompts",
file: "engram-{name}.prompt.md",
frontmatter: true,
},
also_scans: &[],
mcp_config: Some(McpConfigSource::Json(".config/Code/User/mcp.json")),
hooks_config: None,
},
HarnessSpec {
id: Harness::Cursor,
name: "cursor",
probe: &[".cursor"],
sessions_dir: Some(".cursor/chats"),
transcript: TranscriptSupport::NotImplemented {
detail: "cursor stores chats under ~/.cursor/chats in a format engram has not \
surveyed",
},
// `.cursor/skills` and `SKILL.md` both appear in the cursor-agent
// binary. `.cursor/skills-cursor` is the vendor's own bundle and is
// not a user surface --- Grok's compatibility scanner filters those
// same vendor defaults out.
command_surface: CommandSurface::Skill {
dir: ".cursor/skills",
},
also_scans: &[".cursor/commands"],
mcp_config: Some(McpConfigSource::Json(".cursor/mcp.json")),
hooks_config: None,
},
HarnessSpec {
id: Harness::Antigravity,
name: "antigravity",
Expand All @@ -255,6 +323,7 @@ pub const ALL: &[HarnessSpec] = &[
command_surface: CommandSurface::Plugin {
dir: ".gemini/config/plugins",
},
also_scans: &[],
mcp_config: Some(McpConfigSource::Json(".gemini/antigravity/mcp_config.json")),
hooks_config: None,
},
Expand All @@ -269,6 +338,7 @@ pub const ALL: &[HarnessSpec] = &[
command_surface: CommandSurface::None {
detail: "goose has no user command directory engram has surveyed",
},
also_scans: &[],
mcp_config: None,
hooks_config: None,
},
Expand All @@ -285,6 +355,7 @@ pub const ALL: &[HarnessSpec] = &[
them from a marketplace, a GitHub repository, or a git URL --- \
there is no user-writable directory engram can drop a command into",
},
also_scans: &[],
mcp_config: Some(McpConfigSource::Json(".copilot/mcp-config.json")),
hooks_config: None,
},
Expand All @@ -302,6 +373,7 @@ pub const ALL: &[HarnessSpec] = &[
command_surface: CommandSurface::Skill {
dir: ".qwen/skills",
},
also_scans: &[],
mcp_config: Some(McpConfigSource::Json(".qwen/settings.json")),
hooks_config: None,
},
Expand Down Expand Up @@ -500,7 +572,9 @@ fn json_engram_args(text: &str) -> Option<Vec<String>> {
// `mcpServers` is the common key; Opencode uses `mcp`.
let servers = value
.get("mcpServers")
.or_else(|| value.get("mcp"))?
.or_else(|| value.get("mcp"))
// VS Code's `~/.config/Code/User/mcp.json` uses `servers`.
.or_else(|| value.get("servers"))?
.as_object()?;
let entry = servers.get("engram")?;
// `command` may be a bare string with `args` alongside, or (Opencode) an
Expand Down
Loading
Loading