feat(agents): add CodeBuddy AI platform support (#423)

Adds CodeBuddy as a first-class agent platform, mirroring the Claude Code / Codex
integration pattern across detection, config, pathjail, hooks, rules injection,
setup and uninstall. `~/.codebuddy` gets the same jail treatment as `.claude`/
`.codex` (pathjail IDE_CONFIG_DIRS, pathutil broad-root guard, server home/agent
dir checks). MCP config path: `~/.codebuddy/mcp.json`.

Co-authored-by: studyzy <studyzy@qq.com>
This commit is contained in:
Devin
2026-06-15 19:05:15 +08:00
committed by GitHub
parent 5d44263668
commit 4639af114b
59 changed files with 1149 additions and 48 deletions
+1
View File
@@ -981,6 +981,7 @@ Runtime client identification (`core/client_capabilities.rs`) detects the connec
|:-------|:-----|:-----------------|
| Cursor | 1 | All features — resources, prompts, elicitation, sampling, dynamic tools |
| Claude Code | 1 | All features |
| CodeBuddy | 1 | All features (same architecture as Claude Code) |
| Windsurf | 2 | Resources, prompts, dynamic tools (100-tool limit) |
| Zed | 2 | Resources, prompts |
| VS Code Copilot | 2 | Resources, dynamic tools |
+1
View File
@@ -298,6 +298,7 @@ Define how LeanCTX communicates with the outside world.
|---|---|---|---|
| Cursor | MCP (stdio) + Shell Hook | MCP manifest v1 + tool schemas + shell patterns | `lean-ctx setup` |
| Claude Code | MCP (stdio) + Shell Hook | MCP manifest v1 + tool schemas + shell patterns | `lean-ctx init --agent claude` |
| CodeBuddy | MCP (stdio) + Shell Hook | MCP manifest v1 + tool schemas + shell patterns | `lean-ctx init --agent codebuddy` |
| GitHub Copilot | MCP (stdio) + Shell Hook | MCP manifest v1 + tool schemas | `lean-ctx init --agent copilot` |
| Remote agents | HTTP | HTTP MCP v1 + typed errors | `lean-ctx serve` |
| Teams | HTTP | Team Server v1 + audit log | `lean-ctx team serve` |
+2 -2
View File
@@ -205,7 +205,7 @@ Tracks wasted tokens from compressed→full re-reads:
Runtime detection of 9 IDE clients:
- Cursor, Claude Code, Windsurf, Zed, VS Code Copilot, Kiro, Codex, Antigravity, Gemini CLI
- Cursor, Claude Code, CodeBuddy, Windsurf, Zed, VS Code Copilot, Kiro, Codex, Antigravity, Gemini CLI
Tier 14 classification determines feature gating for resources, prompts, elicitation, and dynamic tools.
@@ -243,7 +243,7 @@ Previously deprecated aliases have been removed. Use the canonical tools:
### SKILL.md Auto-Installation
- `lean-ctx init` writes `SKILL.md` to agent-specific skill directories
- Auto-detects Cursor, Claude Code, Codex, Gemini CLI, Kiro skill paths
- Auto-detects Cursor, Claude Code, CodeBuddy, Codex, Gemini CLI, Kiro skill paths
### Compressed Output Cache
- `map` and `signatures` read modes cache compressed output
+1
View File
@@ -410,6 +410,7 @@ LeanCTX is a standard **MCP server**, so it works with any MCP-compatible client
|---|:---:|:---:|---|
| Cursor | ● | | `lean-ctx init --agent cursor` |
| Claude Code | ● | | `lean-ctx init --agent claude` |
| CodeBuddy | ● | | `lean-ctx init --agent codebuddy` |
| Augment CLI / VS Code | ● | | `lean-ctx init --agent augment` |
| Codex CLI | ● | | `lean-ctx init --agent codex` |
| Gemini CLI | ● | | `lean-ctx init --agent gemini` |
+1 -1
View File
@@ -119,7 +119,7 @@ Changes to these files receive extra scrutiny:
|------|------|-----|
| `rust/src/shell/` | Shell execution | Wraps your shell, executes commands |
| `rust/src/server/` | MCP protocol | Handles all tool calls from AI editors/agents |
| `rust/src/hooks/` | Editor integration | Installs hooks/config into Claude Code, Cursor, etc. |
| `rust/src/hooks/` | Editor integration | Installs hooks/config into Claude Code, CodeBuddy, Cursor, etc. |
| `rust/src/core/cache.rs` | File caching | Reads and stores file contents |
| `rust/Cargo.toml` | Supply chain | Dependency manifest |
| `.github/workflows/*.yml` | CI/CD | Release pipeline integrity |
+1 -1
View File
@@ -43,7 +43,7 @@ Technical depth: [`docs/cognition-interface.md`](docs/cognition-interface.md) ·
- **Evidence over claims.** Policy decides what an agent may see; signed
evidence proves what it saw. Compliance reports (EU AI Act, ISO/IEC 42001,
SOC 2) are generated from real session data, offline-verifiable.
- **One binary, 30+ tools.** Cursor, Claude Code, Windsurf, Copilot, Codex,
- **One binary, 30+ tools.** Cursor, Claude Code, CodeBuddy, Windsurf, Copilot, Codex,
Gemini, JetBrains and more — the same engine everywhere.
## Direction
+19 -1
View File
@@ -41,6 +41,8 @@ detect_agent() {
if command -v claude &>/dev/null && [[ -d "$HOME/.claude" ]]; then
AGENT="claude"
elif command -v codebuddy &>/dev/null || [[ -d "$HOME/.codebuddy" ]]; then
AGENT="codebuddy"
elif [[ -d "$HOME/.cursor" ]]; then
AGENT="cursor"
elif command -v gemini &>/dev/null || [[ -d "$HOME/.gemini" ]]; then
@@ -80,7 +82,7 @@ while [[ $# -gt 0 ]]; do
echo "Usage: lctx [options] [project_dir] [prompt]"
echo ""
echo "Options:"
echo " --agent, -a NAME Agent to use: claude|cursor|gemini|codex|windsurf|cline"
echo " --agent, -a NAME Agent to use: claude|codebuddy|cursor|gemini|codex|windsurf|cline"
echo " --agents LIST Launch multiple agents in parallel (comma-separated)"
echo " --resume, -r ID Resume Claude Code session"
echo " --scan-only Build project graph only, don't launch agent"
@@ -163,6 +165,22 @@ case "$AGENT" in
cd "$PROJECT_DIR"
exec claude ${CLAUDE_ARGS[@]+"${CLAUDE_ARGS[@]}"} ;;
codebuddy)
echo "Setting up lean-ctx for CodeBuddy..."
"$LEAN_CTX" init --agent codebuddy --global 2>/dev/null || true
CODEBUDDY_ARGS=()
if [[ -n "$RESUME_ID" ]]; then
CODEBUDDY_ARGS+=("--resume" "$RESUME_ID")
elif [[ -n "$PROMPT" ]]; then
CODEBUDDY_ARGS+=("-p" "$PROMPT")
fi
echo "Launching CodeBuddy..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cd "$PROJECT_DIR"
exec codebuddy ${CODEBUDDY_ARGS[@]+"${CODEBUDDY_ARGS[@]}"} ;;
cursor)
echo "Setting up lean-ctx for Cursor..."
"$LEAN_CTX" init --agent cursor --global 2>/dev/null || true
+1 -1
View File
@@ -20,7 +20,7 @@ Then run `lean-ctx setup` and `lean-ctx doctor` to verify.
No. Since v3.2.3 the install script auto-detects if `cargo` is missing and downloads a pre-built binary. Rust is only needed if you want to build from source.
**Q: Which editors/AI tools are supported?**
lean-ctx auto-configures for: **Cursor, Claude Code, GitHub Copilot, Windsurf, VS Code, Zed, Codex CLI, Gemini CLI, OpenCode, Pi, Qwen Code, Trae, Amazon Q, JetBrains, Antigravity, Cline/Roo Code, Aider, Amp, Kiro, Continue, Crush** — run `lean-ctx setup` and it detects everything.
lean-ctx auto-configures for: **Cursor, Claude Code, CodeBuddy, GitHub Copilot, Windsurf, VS Code, Zed, Codex CLI, Gemini CLI, OpenCode, Pi, Qwen Code, Trae, Amazon Q, JetBrains, Antigravity, Cline/Roo Code, Aider, Amp, Kiro, Continue, Crush** — run `lean-ctx setup` and it detects everything.
**Q: How do I update?**
```bash
+1 -1
View File
@@ -18,7 +18,7 @@ Then run `lean-ctx setup` and `lean-ctx doctor` to verify.
No. Since v3.2.3 the install script auto-detects if `cargo` is missing and downloads a pre-built binary. Rust is only needed if you want to build from source.
**Q: Which editors/AI tools are supported?**
lean-ctx auto-configures for: **Cursor, Claude Code, GitHub Copilot, Windsurf, VS Code, Zed, Codex CLI, Gemini CLI, OpenCode, Pi, Qwen Code, Trae, Amazon Q, JetBrains, Antigravity, Cline/Roo Code, Aider, Amp, Kiro, Continue, Crush** — run `lean-ctx setup` and it detects everything.
lean-ctx auto-configures for: **Cursor, Claude Code, CodeBuddy, GitHub Copilot, Windsurf, VS Code, Zed, Codex CLI, Gemini CLI, OpenCode, Pi, Qwen Code, Trae, Amazon Q, JetBrains, Antigravity, Cline/Roo Code, Aider, Amp, Kiro, Continue, Crush** — run `lean-ctx setup` and it detects everything.
**Q: How do I update?**
```bash
+2
View File
@@ -24,6 +24,7 @@ The default per agent comes from `recommend_hook_mode`: agents in the `HYBRID_AG
| `codex` | **Hybrid** | `hooks.json` (SessionStart/PreToolUse) for Bash; MCP for reads (Desktop/Cloud variants have no hooks) |
| `gemini` | **Hybrid** | BeforeTool hooks for shell; MCP for reads/search |
| `claude` / `claude-code` | **Hybrid** | PreToolUse Bash hooks + MCP (hooks don't fire in headless `-p` mode → MCP guarantees reads) |
| `codebuddy` | **Hybrid** | Same architecture as Claude Code — PreToolUse Bash hooks + MCP |
| `windsurf` | **Hybrid** | `~/.codeium/windsurf/hooks.json` for shell + MCP for full Context OS |
| `copilot` | **Hybrid** | `.github/hooks/hooks.json` for shell + MCP |
| `qoder` | **Hybrid** | Bash hook in `settings.json` + MCP for reads |
@@ -40,6 +41,7 @@ Legend:
|------|------------------|-----------|--------------|-------|
| Cursor (`cursor`) | `~/.cursor/mcp.json` (MCP enabled — Hybrid) | `~/.cursor/rules/lean-ctx.mdc` | `~/.cursor/hooks.json` + `~/.cursor/hooks/lean-ctx-*.sh` | `~/.cursor/skills/lean-ctx/SKILL.md` |
| Claude Code (`claude`) | `~/.claude.json` (MCP enabled — Hybrid) | `~/.claude/CLAUDE.md` block (no rules file since 3.8) | `~/.claude/settings.json` hook wiring (Bash rewrite + Read redirect) | `~/.claude/skills/lean-ctx/SKILL.md` |
| CodeBuddy (`codebuddy`) | `~/.codebuddy.json` (MCP enabled — Hybrid) | `~/.codebuddy/CODEBUDDY.md` block | `~/.codebuddy/settings.json` hook wiring (Bash rewrite + Read redirect) | `~/.codebuddy/skills/lean-ctx/SKILL.md` |
| Codex (`codex`) | `~/.codex/config.toml` (MCP enabled — Hybrid) | `~/.codex/LEAN-CTX.md` + `~/.codex/AGENTS.md` | `~/.codex/hooks.json` (SessionStart/PreToolUse) | `~/.codex/skills/lean-ctx/SKILL.md` |
| OpenCode (`opencode`) | `~/.config/opencode/opencode.json` (MCP enabled — Hybrid) | `~/.config/opencode/rules/lean-ctx.md` | `~/.config/opencode/plugins/lean-ctx.ts` | — |
| Windsurf (`windsurf`) | `~/.codeium/windsurf/mcp_config.json` | `~/.codeium/windsurf/rules/lean-ctx.md` | project `.windsurfrules` (when not global) | — |
+1 -1
View File
@@ -28,7 +28,7 @@ Examples: "**LeanCTX** is the Context OS for AI development." · `lean-ctx setup
- **License:** Apache-2.0 · **open source.**
- **76 MCP tools**, **10 read modes**, **95+ shell compression patterns**.
- **Up to 99% token savings**; cached re-reads cost **~13 tokens**.
- **Works with 24+ AI tools** — Cursor, Claude Code, GitHub Copilot, Windsurf, OpenAI
- **Works with 25+ AI tools** — Cursor, Claude Code, CodeBuddy, GitHub Copilot, Windsurf, OpenAI
Codex CLI, Gemini CLI, Cline, JetBrains, VS Code, Zed, and more.
- **Three integration modes:** CLI-Redirect (zero MCP overhead), Hybrid, Full MCP.
+4 -3
View File
@@ -17,8 +17,8 @@
lean-ctx reduces LLM token consumption by **up to 99%** through two complementary strategies in a single binary:
1. **Shell Hook** — Transparently compresses CLI output (95+ patterns) before it reaches the LLM. Works without LLM cooperation.
2. **MCP Server** — 76 tools for cached file reads, adaptive mode selection, incremental deltas, dependency maps, intent detection, cross-file dedup, project graph, cross-session memory (CCP), multi-agent coordination, semantic caching, and session metrics. Works with Cursor, GitHub Copilot, Claude Code, Windsurf, OpenAI Codex, Google Antigravity, OpenCode, and any MCP-compatible editor.
3. **AI Tool Hooks** — One-command integration for Claude Code, Cursor, Gemini CLI, Codex, Crush, Windsurf, and Cline via `lean-ctx init --agent <tool>`.
2. **MCP Server** — 76 tools for cached file reads, adaptive mode selection, incremental deltas, dependency maps, intent detection, cross-file dedup, project graph, cross-session memory (CCP), multi-agent coordination, semantic caching, and session metrics. Works with Cursor, GitHub Copilot, Claude Code, CodeBuddy, Windsurf, OpenAI Codex, Google Antigravity, OpenCode, and any MCP-compatible editor.
3. **AI Tool Hooks** — One-command integration for Claude Code, CodeBuddy, Cursor, Gemini CLI, Codex, Crush, Windsurf, and Cline via `lean-ctx init --agent <tool>`.
## Token Savings (Typical Cursor/Claude Code Session)
@@ -193,7 +193,8 @@ lean-ctx pack --pr # PR context pack (unchanged)
```bash
lean-ctx init --global # Install 23 shell aliases (.zshrc/.bashrc/.config/fish)
lean-ctx init --agent claude # Install Claude Code PreToolUse hook
lean-ctx init --agent claude # Install Claude Code PreToolUse hook
lean-ctx init --agent codebuddy # Install CodeBuddy PreToolUse hook
lean-ctx init --agent cursor # Install Cursor hooks.json
lean-ctx init --agent gemini # Install Gemini CLI BeforeTool hook
lean-ctx init --agent codex # Install Codex AGENTS.md + compatible hooks
+2 -2
View File
@@ -204,7 +204,7 @@ COMMANDS:
dev-install Build release + atomic install + restart (for development)
gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
buddy [show|stats|ascii|json] Token Guardian: your data-driven coding companion
doctor integrations [--json] Integration health checks (Cursor/Claude Code)
doctor integrations [--json] Integration health checks (Cursor/Claude Code/CodeBuddy)
doctor [--fix] [--json] Run diagnostics (and optionally repair)
doctor --migrate-check v1.0 migration readiness audit (config, deprecations, data)
smells [scan|summary|rules|file] [--rule=<r>] [--path=<p>] [--json]
@@ -304,7 +304,7 @@ EXAMPLES:
lean-ctx-status Show whether compression is active
lean-ctx init --agent pi Install Pi Coding Agent extension
lean-ctx doctor Check PATH, config, MCP, and dashboard port
lean-ctx doctor integrations Premium integration checks (Cursor/Claude Code)
lean-ctx doctor integrations Premium integration checks (Cursor/Claude Code/CodeBuddy)
lean-ctx doctor --fix --json Repair + machine-readable report
lean-ctx status --json Machine-readable current status
lean-ctx session task \"implement auth\"
+39 -1
View File
@@ -39,8 +39,9 @@ pub fn run(args: &[String]) {
"mdc" => export_mdc(&high_confidence, &project_root),
"agents-md" => export_agents_md(&high_confidence, &project_root),
"claude-md" => export_claude_md(&high_confidence, &project_root),
"codebuddy-md" => export_codebuddy_md(&high_confidence, &project_root),
_ => {
eprintln!("Unknown format: {format}. Supported: mdc, agents-md, claude-md");
eprintln!("Unknown format: {format}. Supported: mdc, agents-md, claude-md, codebuddy-md");
std::process::exit(1);
}
}
@@ -140,6 +141,43 @@ fn export_claude_md(facts: &[&KnowledgeFact], project_root: &Path) {
}
}
fn export_codebuddy_md(facts: &[&KnowledgeFact], project_root: &Path) {
let output_path = project_root.join("CODEBUDDY.md");
let section = build_agents_section(facts);
if output_path.exists() {
let existing = std::fs::read_to_string(&output_path).unwrap_or_default();
let marker_start = "<!-- lean-ctx-knowledge-start -->";
let marker_end = "<!-- lean-ctx-knowledge-end -->";
let new_content = if existing.contains(marker_start) {
let before = existing.split(marker_start).next().unwrap_or("");
let after = existing.split(marker_end).nth(1).unwrap_or("");
format!("{before}{marker_start}\n{section}\n{marker_end}{after}")
} else {
format!("{existing}\n\n{marker_start}\n{section}\n{marker_end}\n")
};
match std::fs::write(&output_path, &new_content) {
Ok(()) => println!("Updated {} rules in {}", facts.len(), output_path.display()),
Err(e) => eprintln!("Error writing {}: {e}", output_path.display()),
}
} else {
let content = format!(
"# Project Rules (auto-generated by lean-ctx)\n\n\
<!-- lean-ctx-knowledge-start -->\n{section}\n<!-- lean-ctx-knowledge-end -->\n"
);
match std::fs::write(&output_path, &content) {
Ok(()) => println!(
"Created {} with {} rules",
output_path.display(),
facts.len()
),
Err(e) => eprintln!("Error writing {}: {e}", output_path.display()),
}
}
}
fn build_mdc_content(facts: &[&KnowledgeFact]) -> String {
let mut out = String::from(
"---\ndescription: \"Project knowledge (auto-generated by lean-ctx export-rules)\"\n\
+1
View File
@@ -184,6 +184,7 @@ fn discover_mcp_configs() -> Vec<PathBuf> {
let candidates = [
home.join(".cursor").join("mcp.json"),
home.join(".claude.json"),
home.join(".codebuddy.json"),
home.join(".codeium")
.join("windsurf")
.join("mcp_config.json"),
+1
View File
@@ -85,6 +85,7 @@ fn project_owned_candidates(dir: &Path) -> Vec<PathBuf> {
vec![
dir.join(".cursor/rules/lean-ctx.mdc"),
dir.join(".claude/rules/lean-ctx.md"),
dir.join(".codebuddy/rules/lean-ctx.md"),
dir.join(".windsurf/rules/lean-ctx.md"),
dir.join(".cline/rules/lean-ctx.md"),
dir.join(".roo/rules/lean-ctx.md"),
+5 -5
View File
@@ -208,7 +208,7 @@ if (-not $env:LEAN_CTX_ACTIVE -and -not $env:LEAN_CTX_DISABLED -and -not $env:LE
switch ($_leanCtxActivation) {{
{{ $_ -in 'off','none','manual' }} {{ $_leanCtxShouldActivate = $false }}
{{ $_ -in 'agents-only','agents_only','agentsonly' }} {{
$_leanCtxShouldActivate = $env:LEAN_CTX_AGENT -or $env:CLAUDECODE -or $env:CODEX_CLI_SESSION -or $env:GEMINI_SESSION
$_leanCtxShouldActivate = $env:LEAN_CTX_AGENT -or $env:CLAUDECODE -or $env:CODEBUDDY -or $env:CODEX_CLI_SESSION -or $env:GEMINI_SESSION
}}
default {{ $_leanCtxShouldActivate = $true }}
}}
@@ -315,7 +315,7 @@ pub fn generate_hook_fish(binary: &str) -> String {
set -g _lean_ctx_cmds {alias_list}\n\
\n\
function _lc_is_agent\n\
\tset -q LEAN_CTX_AGENT; or set -q CODEX_CLI_SESSION; or set -q CLAUDECODE; or set -q GEMINI_SESSION\n\
\tset -q LEAN_CTX_AGENT; or set -q CODEX_CLI_SESSION; or set -q CLAUDECODE; or set -q CODEBUDDY; or set -q GEMINI_SESSION\n\
end\n\
\n\
function _lc\n\
@@ -417,7 +417,7 @@ pub fn generate_hook_fish(binary: &str) -> String {
\t\tcase off none manual\n\
\t\t\treturn 1\n\
\t\tcase 'agents-only' agents_only agentsonly\n\
\t\t\tif set -q LEAN_CTX_AGENT; or set -q CLAUDECODE; or set -q CODEX_CLI_SESSION; or set -q GEMINI_SESSION\n\
\t\t\tif set -q LEAN_CTX_AGENT; or set -q CLAUDECODE; or set -q CODEBUDDY; or set -q CODEX_CLI_SESSION; or set -q GEMINI_SESSION\n\
\t\t\t\treturn 0\n\
\t\t\tend\n\
\t\t\treturn 1\n\
@@ -461,7 +461,7 @@ pub fn generate_hook_posix(binary: &str) -> String {
_lean_ctx_cmds=({alias_list})
_lc_is_agent() {{
[ -n "${{LEAN_CTX_AGENT:-}}" ] || [ -n "${{CODEX_CLI_SESSION:-}}" ] || [ -n "${{CLAUDECODE:-}}" ] || [ -n "${{GEMINI_SESSION:-}}" ]
[ -n "${{LEAN_CTX_AGENT:-}}" ] || [ -n "${{CODEX_CLI_SESSION:-}}" ] || [ -n "${{CLAUDECODE:-}}" ] || [ -n "${{CODEBUDDY:-}}" ] || [ -n "${{GEMINI_SESSION:-}}" ]
}}
_lc() {{
@@ -574,7 +574,7 @@ _lean_ctx_should_activate() {{
case "${{LEAN_CTX_SHELL_ACTIVATION:-{baked_default}}}" in
off|none|manual) return 1 ;;
agents-only|agents_only|agentsonly)
[ -n "${{LEAN_CTX_AGENT:-}}" ] || [ -n "${{CLAUDECODE:-}}" ] || [ -n "${{CODEX_CLI_SESSION:-}}" ] || [ -n "${{GEMINI_SESSION:-}}" ] ;;
[ -n "${{LEAN_CTX_AGENT:-}}" ] || [ -n "${{CLAUDECODE:-}}" ] || [ -n "${{CODEBUDDY:-}}" ] || [ -n "${{CODEX_CLI_SESSION:-}}" ] || [ -n "${{GEMINI_SESSION:-}}" ] ;;
*) return 0 ;;
esac
}}
+1 -1
View File
@@ -17,7 +17,7 @@ use std::path::{Path, PathBuf};
/// Env var name prefixes identifying agent runtime/session state worth forwarding
/// to `ctx_shell` child processes.
pub const FORWARD_PREFIXES: &[&str] = &["CODEX_", "CLAUDE_", "OPENCODE_", "HERMES_", "GEMINI_"];
pub const FORWARD_PREFIXES: &[&str] = &["CODEX_", "CLAUDE_", "CODEBUDDY_", "OPENCODE_", "HERMES_", "GEMINI_"];
const FILE_NAME: &str = "agent_runtime_env.json";
+2
View File
@@ -160,6 +160,8 @@ impl ClientMcpCapabilities {
fn identify_client(lower: &str) -> String {
if lower.contains("cursor") {
"cursor".to_string()
} else if lower.contains("codebuddy") {
"codebuddy".to_string()
} else if lower.contains("claude") {
"claude-code".to_string()
} else if lower.contains("windsurf") || lower.contains("codeium") {
+6
View File
@@ -22,6 +22,12 @@ pub const ALL_CLIENTS: &[ClientConstraints] = &[
mcp_instructions_max_chars: Some(2048),
supports_auto_approve: false,
},
ClientConstraints {
id: "codebuddy",
display_name: "CodeBuddy",
mcp_instructions_max_chars: Some(2048),
supports_auto_approve: false,
},
ClientConstraints {
id: "vscode-copilot",
display_name: "VS Code / GitHub Copilot",
+2 -2
View File
@@ -264,10 +264,10 @@ pub enum RulesScope {
Project,
}
/// How agent rules are injected for AGENTS.md/CLAUDE.md/GEMINI.md consumers.
/// How agent rules are injected for AGENTS.md/CLAUDE.md/CODEBUDDY.md/GEMINI.md consumers.
///
/// - `Shared` (default): write a marker-delimited block into the user's shared
/// instruction file (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`) — zero-config
/// instruction file (`CLAUDE.md`, `CODEBUDDY.md`, `AGENTS.md`, `GEMINI.md`) — zero-config
/// discoverability, but touches a file the user also authors.
/// - `Dedicated`: never write into those shared files. Instead use each agent's
/// config-driven, fully-removable auto-load path (Claude/Codex `SessionStart`
+4 -4
View File
@@ -145,7 +145,7 @@ pub struct Config {
#[serde(default)]
pub rules_scope: Option<String>,
/// Controls how rules are injected for shared-instruction-file agents.
/// Values: "shared" (default, marker block in CLAUDE.md/AGENTS.md/GEMINI.md),
/// Values: "shared" (default, marker block in CLAUDE.md/CODEBUDDY.md/AGENTS.md/GEMINI.md),
/// "dedicated" (never touch those files; use each agent's config-driven
/// auto-load: SessionStart hook / instructions[] / context.fileName, #343), or
/// "off" (write no rules file at all — for hosts that supply their own
@@ -187,7 +187,7 @@ pub struct Config {
#[serde(default)]
pub allow_paths: Vec<String>,
/// Allow jailed tool access to home-level IDE config dirs (~/.cursor,
/// ~/.claude, …). Default false: those dirs expose other projects'
/// ~/.claude, ~/.codebuddy, …). Default false: those dirs expose other projects'
/// sessions, MCP configs and credentials. `~/.lean-ctx` (own data dir)
/// is always allowed. Override via LEAN_CTX_ALLOW_IDE_DIRS=1.
#[serde(default)]
@@ -550,7 +550,7 @@ impl Config {
}
/// Returns the effective rules injection mode, preferring env var over config.
/// Default is `Shared` (zero-config discovery via a CLAUDE.md/AGENTS.md block).
/// Default is `Shared` (zero-config discovery via a CLAUDE.md/CODEBUDDY.md/AGENTS.md block).
pub fn rules_injection_effective(&self) -> RulesInjection {
let raw = std::env::var("LEAN_CTX_RULES_INJECTION")
.ok()
@@ -582,7 +582,7 @@ impl Config {
/// non-polluting auto-load path *and* global rules are in scope.
///
/// Gates the Claude/Codex `SessionStart` `additionalContext` summary: it
/// stands in for the (now-skipped) shared CLAUDE.md/AGENTS.md block, so it
/// stands in for the (now-skipped) shared CLAUDE.md/CODEBUDDY.md/AGENTS.md block, so it
/// only fires when injection is `Dedicated` and the scope isn't project-only.
#[must_use]
pub fn dedicated_session_context_active(&self) -> bool {
+1
View File
@@ -83,6 +83,7 @@ impl SetupConfig {
let check_paths = [
home.join(".cursor/rules/lean-ctx.mdc"),
crate::core::editor_registry::claude_rules_dir(&home).join("lean-ctx.md"),
crate::core::editor_registry::codebuddy_rules_dir(&home).join("lean-ctx.md"),
home.join(".gemini/GEMINI.md"),
home.join(".codeium/windsurf/rules/lean-ctx.md"),
];
+4 -3
View File
@@ -47,7 +47,7 @@ impl ShellActivation {
r#"if [ -z "${LEAN_CTX_ACTIVE:-}" ] && [ -z "${LEAN_CTX_DISABLED:-}" ] && [ "${LEAN_CTX_ENABLED:-1}" != "0" ]; then"#
}
Self::AgentsOnly => {
r#"if [ -z "${LEAN_CTX_ACTIVE:-}" ] && [ -z "${LEAN_CTX_DISABLED:-}" ] && [ "${LEAN_CTX_ENABLED:-1}" != "0" ] && { [ -n "${LEAN_CTX_AGENT:-}" ] || [ -n "${CLAUDECODE:-}" ] || [ -n "${CODEX_CLI_SESSION:-}" ] || [ -n "${GEMINI_SESSION:-}" ]; }; then"#
r#"if [ -z "${LEAN_CTX_ACTIVE:-}" ] && [ -z "${LEAN_CTX_DISABLED:-}" ] && [ "${LEAN_CTX_ENABLED:-1}" != "0" ] && { [ -n "${LEAN_CTX_AGENT:-}" ] || [ -n "${CLAUDECODE:-}" ] || [ -n "${CODEBUDDY:-}" ] || [ -n "${CODEX_CLI_SESSION:-}" ] || [ -n "${GEMINI_SESSION:-}" ]; }; then"#
}
Self::Off => "",
}
@@ -59,7 +59,7 @@ impl ShellActivation {
"if not set -q LEAN_CTX_ACTIVE; and not set -q LEAN_CTX_DISABLED; and test (set -q LEAN_CTX_ENABLED; and echo $LEAN_CTX_ENABLED; or echo 1) != '0'"
}
Self::AgentsOnly => {
"if not set -q LEAN_CTX_ACTIVE; and not set -q LEAN_CTX_DISABLED; and test (set -q LEAN_CTX_ENABLED; and echo $LEAN_CTX_ENABLED; or echo 1) != '0'; and begin; set -q LEAN_CTX_AGENT; or set -q CLAUDECODE; or set -q CODEX_CLI_SESSION; or set -q GEMINI_SESSION; end"
"if not set -q LEAN_CTX_ACTIVE; and not set -q LEAN_CTX_DISABLED; and test (set -q LEAN_CTX_ENABLED; and echo $LEAN_CTX_ENABLED; or echo 1) != '0'; and begin; set -q LEAN_CTX_AGENT; or set -q CLAUDECODE; or set -q CODEBUDDY; or set -q CODEX_CLI_SESSION; or set -q GEMINI_SESSION; end"
}
Self::Off => "",
}
@@ -71,7 +71,7 @@ impl ShellActivation {
"if (-not $env:LEAN_CTX_ACTIVE -and -not $env:LEAN_CTX_DISABLED -and -not $env:LEAN_CTX_NO_HOOK)"
}
Self::AgentsOnly => {
"if (-not $env:LEAN_CTX_ACTIVE -and -not $env:LEAN_CTX_DISABLED -and -not $env:LEAN_CTX_NO_HOOK -and ($env:LEAN_CTX_AGENT -or $env:CLAUDECODE -or $env:CODEX_CLI_SESSION -or $env:GEMINI_SESSION))"
"if (-not $env:LEAN_CTX_ACTIVE -and -not $env:LEAN_CTX_DISABLED -and -not $env:LEAN_CTX_NO_HOOK -and ($env:LEAN_CTX_AGENT -or $env:CLAUDECODE -or $env:CODEBUDDY -or $env:CODEX_CLI_SESSION -or $env:GEMINI_SESSION))"
}
Self::Off => "",
}
@@ -108,6 +108,7 @@ mod tests {
let guard = ShellActivation::AgentsOnly.posix_guard();
assert!(guard.contains("LEAN_CTX_AGENT"));
assert!(guard.contains("CLAUDECODE"));
assert!(guard.contains("CODEBUDDY"));
assert!(guard.contains("CODEX_CLI_SESSION"));
assert!(guard.contains("GEMINI_SESSION"));
}
+2
View File
@@ -115,6 +115,7 @@ impl ContextRadar {
cwd.join(".cursorrules"),
cwd.join("AGENTS.md"),
cwd.join("CLAUDE.md"),
cwd.join("CODEBUDDY.md"),
cwd.join("LEAN-CTX.md"),
home.join(".cursor").join("rules"),
home.join(".cursorrules"),
@@ -165,6 +166,7 @@ impl ContextRadar {
if name == ".cursorrules"
|| name == "AGENTS.md"
|| name == "CLAUDE.md"
|| name == "CODEBUDDY.md"
|| name == "LEAN-CTX.md"
{
return true;
+34 -2
View File
@@ -2,8 +2,8 @@ use std::path::{Path, PathBuf};
use super::paths::{
augment_cli_settings_path, augment_vscode_mcp_path, claude_mcp_json_path, cline_mcp_path,
qoder_all_mcp_paths, qoderwork_mcp_path, roo_mcp_path, vscode_mcp_path, zed_config_dir,
zed_settings_path,
codebuddy_mcp_json_path, qoder_all_mcp_paths, qoderwork_mcp_path, roo_mcp_path,
vscode_mcp_path, zed_config_dir, zed_settings_path,
};
use super::types::{ConfigType, EditorTarget};
@@ -42,6 +42,13 @@ pub fn build_targets(home: &Path) -> Vec<EditorTarget> {
detect_path: detect_claude_path(),
config_type: ConfigType::McpJson,
},
EditorTarget {
name: "CodeBuddy",
agent_key: "codebuddy".to_string(),
config_path: codebuddy_mcp_json_path(home),
detect_path: detect_codebuddy_path(),
config_type: ConfigType::McpJson,
},
EditorTarget {
name: "Augment CLI",
agent_key: "augment".to_string(),
@@ -374,6 +381,31 @@ pub fn detect_claude_path() -> PathBuf {
PathBuf::from("/nonexistent")
}
pub fn detect_codebuddy_path() -> PathBuf {
let which_cmd = if cfg!(windows) { "where" } else { "which" };
if let Ok(output) = std::process::Command::new(which_cmd).arg("codebuddy").output() {
if output.status.success() {
return PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
}
}
if let Ok(dir) = std::env::var("CODEBUDDY_CONFIG_DIR") {
let dir = dir.trim();
if !dir.is_empty() {
let p = PathBuf::from(dir);
if p.exists() {
return p;
}
}
}
if let Some(home) = dirs::home_dir() {
let codebuddy_json = codebuddy_mcp_json_path(&home);
if codebuddy_json.exists() {
return codebuddy_json;
}
}
PathBuf::from("/nonexistent")
}
pub fn detect_augment_path(home: &Path) -> PathBuf {
let which_cmd = if cfg!(windows) { "where" } else { "which" };
if let Ok(output) = std::process::Command::new(which_cmd).arg("auggie").output() {
+18
View File
@@ -197,6 +197,24 @@ pub fn claude_rules_dir(home: &Path) -> PathBuf {
claude_state_dir(home).join("rules")
}
pub fn codebuddy_mcp_json_path(home: &Path) -> PathBuf {
codebuddy_state_dir(home).join("mcp.json")
}
pub fn codebuddy_state_dir(home: &Path) -> PathBuf {
if let Ok(dir) = std::env::var("CODEBUDDY_CONFIG_DIR") {
let dir = dir.trim();
if !dir.is_empty() {
return PathBuf::from(dir);
}
}
home.join(".codebuddy")
}
pub fn codebuddy_rules_dir(home: &Path) -> PathBuf {
codebuddy_state_dir(home).join("rules")
}
pub fn augment_cli_settings_path(home: &Path) -> PathBuf {
home.join(".augment/settings.json")
}
+1 -1
View File
@@ -73,7 +73,7 @@ pub fn compile(
}
let mut rules_files = Vec::new();
if opts.include_rules_files && client_id == "claude-code" {
if opts.include_rules_files && (client_id == "claude-code" || client_id == "codebuddy") {
let config_dir = crate::instructions::claude_config_dir_display();
rules_files.push(CompiledRuleFile {
path: format!("{config_dir}/rules/lean-ctx.md"),
+1 -1
View File
@@ -45,7 +45,7 @@ impl LitmProfile {
pub fn from_name(name: &str) -> Self {
match name.to_lowercase().as_str() {
"claude" | "cursor" => Self::CLAUDE,
"claude" | "codebuddy" | "cursor" => Self::CLAUDE,
"gemini" => Self::GEMINI,
"gpt" | "openai" | "codex" => Self::GPT,
_ => Self::DEFAULT,
+1
View File
@@ -15,6 +15,7 @@ const IDE_CONFIG_DIRS: &[&str] = &[
".amp",
".aider",
".continue",
".codebuddy",
];
/// Expands `~`, `$VAR` and `${VAR}` in a config-supplied path entry.
+2
View File
@@ -177,8 +177,10 @@ pub fn is_broad_or_unsafe_root(dir: &Path) -> bool {
}
s.ends_with("/.claude")
|| s.ends_with("/.codex")
|| s.ends_with("/.codebuddy")
|| s.contains("/.claude/")
|| s.contains("/.codex/")
|| s.contains("/.codebuddy/")
}
/// Well-known project markers used to identify project roots.
+2
View File
@@ -84,10 +84,12 @@ fn resolve_for_match(path: &std::path::Path) -> std::path::PathBuf {
fn is_agent_or_temp_dir(dir: &std::path::Path) -> bool {
let s = dir.to_string_lossy();
s.contains("/.claude")
|| s.contains("/.codebuddy")
|| s.contains("/.codex")
|| s.contains("/var/folders/")
|| s.contains("/tmp/")
|| s.contains("\\.claude")
|| s.contains("\\.codebuddy")
|| s.contains("\\.codex")
|| s.contains("\\AppData\\Local\\Temp")
|| s.contains("\\Temp\\")
+62
View File
@@ -583,6 +583,7 @@ pub(super) fn skill_files_outcome() -> Outcome {
let candidates = [
("Claude Code", home.join(".claude/skills/lean-ctx/SKILL.md")),
("CodeBuddy", home.join(".codebuddy/skills/lean-ctx/SKILL.md")),
("Cursor", home.join(".cursor/skills/lean-ctx/SKILL.md")),
(
"Codex CLI",
@@ -934,6 +935,25 @@ pub(super) fn claude_truncation_outcome() -> Option<Outcome> {
))
}
pub(super) fn codebuddy_truncation_outcome() -> Option<Outcome> {
let home = dirs::home_dir()?;
let codebuddy_detected =
crate::core::editor_registry::codebuddy_mcp_json_path(&home).exists()
|| crate::core::editor_registry::codebuddy_state_dir(&home).exists()
|| codebuddy_binary_exists();
if !codebuddy_detected {
return None;
}
let cfg = crate::core::config::Config::load();
Some(codebuddy_instructions_check(
&home,
cfg.rules_scope_effective(),
cfg.rules_injection_effective(),
))
}
/// Verify Claude Code receives the full lean-ctx instructions despite the
/// 2048-char MCP instructions cap.
///
@@ -983,6 +1003,48 @@ fn claude_instructions_check(
}
}
/// CodeBuddy instructions check — mirrors `claude_instructions_check` since
/// CodeBuddy uses the same CODEBUDDY.md block + skill pattern as Claude Code.
fn codebuddy_instructions_check(
home: &std::path::Path,
scope: crate::core::config::RulesScope,
injection: crate::core::config::RulesInjection,
) -> Outcome {
use super::common::ClaudeInstructionsState as S;
let state = super::common::codebuddy_instructions_state(home, scope, injection);
let line = match state {
S::ProjectScope => format!(
"{BOLD}CodeBuddy instructions{RST} {GREEN}project scope{RST} {DIM}(global instructions intentionally absent; project files carry them){RST}"
),
S::InjectionOff => format!(
"{BOLD}CodeBuddy instructions{RST} {GREEN}rules injection off{RST} {DIM}(instructions intentionally not installed — config rules_injection=off){RST}"
),
S::DedicatedWithSkill => format!(
"{BOLD}CodeBuddy instructions{RST} {GREEN}dedicated injection + skill installed{RST} {DIM}(SessionStart hook injects instructions){RST}"
),
S::DedicatedMissingSkill => format!(
"{BOLD}CodeBuddy instructions{RST} {YELLOW}lean-ctx skill missing{RST} {DIM}(run: lean-ctx setup){RST}"
),
S::BlockAndSkill => format!(
"{BOLD}CodeBuddy instructions{RST} {GREEN}CODEBUDDY.md block + skill installed{RST} {DIM}(MCP instructions capped at 2048 chars — full content via CODEBUDDY.md){RST}"
),
S::BlockOnly => format!(
"{BOLD}CodeBuddy instructions{RST} {GREEN}CODEBUDDY.md block installed{RST} {DIM}(MCP instructions capped at 2048 chars — full content via CODEBUDDY.md){RST}"
),
S::LegacyRules => format!(
"{BOLD}CodeBuddy instructions{RST} {GREEN}legacy rules file installed{RST} {DIM}(next `lean-ctx setup` migrates it to the CODEBUDDY.md block + skill){RST}"
),
S::Missing => format!(
"{BOLD}CodeBuddy instructions{RST} {YELLOW}no CODEBUDDY.md block or rules file found — MCP instructions truncated at 2048 chars{RST} {DIM}(run: lean-ctx setup){RST}"
),
};
Outcome {
ok: state.ok(),
line,
}
}
pub(super) fn bm25_cache_health_outcome() -> Outcome {
let Ok(data_dir) = crate::core::data_dir::lean_ctx_data_dir() else {
return Outcome {
+73
View File
@@ -203,6 +203,14 @@ pub(super) fn mcp_config_locations(home: &std::path::Path) -> Vec<McpLocation> {
),
path: crate::core::editor_registry::claude_mcp_json_path(home),
},
McpLocation {
name: "CodeBuddy",
display: format!(
"{}",
crate::core::editor_registry::codebuddy_mcp_json_path(home).display()
),
path: crate::core::editor_registry::codebuddy_mcp_json_path(home),
},
McpLocation {
name: "Windsurf",
display: "~/.codeium/windsurf/mcp_config.json".into(),
@@ -540,6 +548,54 @@ pub(super) fn claude_instructions_state(
S::Missing
}
/// CodeBuddy instructions state — mirrors `claude_instructions_state` since
/// CodeBuddy uses the same CODEBUDDY.md block + skill pattern as Claude Code.
pub(super) fn codebuddy_instructions_state(
home: &std::path::Path,
scope: crate::core::config::RulesScope,
injection: crate::core::config::RulesInjection,
) -> ClaudeInstructionsState {
use ClaudeInstructionsState as S;
if scope == crate::core::config::RulesScope::Project {
return S::ProjectScope;
}
if injection == crate::core::config::RulesInjection::Off {
return S::InjectionOff;
}
let has_skill = home.join(".codebuddy/skills/lean-ctx/SKILL.md").exists();
if injection == crate::core::config::RulesInjection::Dedicated {
return if has_skill {
S::DedicatedWithSkill
} else {
S::DedicatedMissingSkill
};
}
let codebuddy_md =
crate::core::editor_registry::codebuddy_state_dir(home).join("CODEBUDDY.md");
let has_block = std::fs::read_to_string(&codebuddy_md)
.is_ok_and(|c| c.contains(crate::hooks::agents::CODEBUDDY_MD_BLOCK_START));
if has_block {
return if has_skill {
S::BlockAndSkill
} else {
S::BlockOnly
};
}
let has_rules = crate::core::editor_registry::codebuddy_rules_dir(home)
.join("lean-ctx.md")
.exists();
if has_rules {
return S::LegacyRules;
}
S::Missing
}
pub(super) fn claude_binary_exists() -> bool {
#[cfg(unix)]
{
@@ -556,3 +612,20 @@ pub(super) fn claude_binary_exists() -> bool {
.is_ok_and(|o| o.status.success())
}
}
pub(super) fn codebuddy_binary_exists() -> bool {
#[cfg(unix)]
{
std::process::Command::new("which")
.arg("codebuddy")
.output()
.is_ok_and(|o| o.status.success())
}
#[cfg(windows)]
{
std::process::Command::new("where")
.arg("codebuddy")
.output()
.is_ok_and(|o| o.status.success())
}
}
+69 -2
View File
@@ -1,7 +1,7 @@
use chrono::Utc;
use serde::Serialize;
use super::{claude_binary_exists, resolve_lean_ctx_binary, BOLD, DIM, GREEN, RST, WHITE, YELLOW};
use super::{claude_binary_exists, codebuddy_binary_exists, resolve_lean_ctx_binary, BOLD, DIM, GREEN, RST, WHITE, YELLOW};
#[derive(Debug, Clone, Copy)]
pub(super) struct IntegrationsOptions {
@@ -49,9 +49,10 @@ pub(super) fn run_integrations(opts: &IntegrationsOptions) -> i32 {
let mut integrations = vec![
integration_cursor(&home, &binary, &data_dir),
integration_claude(&home, &binary, &data_dir),
integration_codebuddy(&home, &binary, &data_dir),
];
for t in crate::core::editor_registry::build_targets(&home) {
if matches!(t.name, "Cursor" | "Claude Code") {
if matches!(t.name, "Cursor" | "Claude Code" | "CodeBuddy") {
continue;
}
integrations.push(integration_generic(&home, &binary, &data_dir, &t));
@@ -287,6 +288,72 @@ fn integration_claude(home: &std::path::Path, binary: &str, data_dir: &str) -> I
}
}
fn integration_codebuddy(
home: &std::path::Path,
binary: &str,
data_dir: &str,
) -> IntegrationStatus {
let target = crate::core::editor_registry::build_targets(home)
.into_iter()
.find(|t| t.agent_key == "codebuddy");
let detected = target.as_ref().is_some_and(|t| t.detect_path.exists())
|| crate::core::editor_registry::codebuddy_state_dir(home).exists()
|| codebuddy_binary_exists();
if !detected {
return IntegrationStatus {
name: "CodeBuddy".to_string(),
detected: false,
checks: Vec::new(),
ok: true,
};
}
let mut checks = Vec::new();
let mcp_path = crate::core::editor_registry::codebuddy_mcp_json_path(home);
checks.push(check_mcp_json(&mcp_path, binary, data_dir));
let settings_path =
crate::core::editor_registry::codebuddy_state_dir(home).join("settings.json");
checks.push(check_claude_hooks(&settings_path, binary));
// CodeBuddy uses the same block + skill pattern as Claude Code.
{
use super::common::ClaudeInstructionsState as S;
let cfg = crate::core::config::Config::load();
let state = super::common::codebuddy_instructions_state(
home,
cfg.rules_scope_effective(),
cfg.rules_injection_effective(),
);
let codebuddy_md =
crate::core::editor_registry::codebuddy_state_dir(home).join("CODEBUDDY.md");
let detail = match state {
S::ProjectScope => "project scope (global instructions intentionally absent)".into(),
S::InjectionOff => "rules injection off (intentionally not installed)".into(),
S::DedicatedWithSkill => "dedicated injection + skill".into(),
S::DedicatedMissingSkill => "skill missing (run: lean-ctx setup)".into(),
S::BlockAndSkill => format!("{} block + skill", codebuddy_md.display()),
S::BlockOnly => format!("{} block", codebuddy_md.display()),
S::LegacyRules => "legacy rules file (migrates on next setup)".into(),
S::Missing => "missing (run: lean-ctx setup)".into(),
};
checks.push(NamedCheck {
name: "Instructions".to_string(),
ok: state.ok(),
detail,
});
}
let ok = checks.iter().all(|c| c.ok);
IntegrationStatus {
name: "CodeBuddy".to_string(),
detected: true,
checks,
ok,
}
}
fn check_mcp_json(path: &std::path::Path, binary: &str, data_dir: &str) -> NamedCheck {
if !path.exists() {
return NamedCheck {
+9
View File
@@ -461,6 +461,15 @@ pub fn run() {
print_check(ct);
}
// 14a) CodeBuddy instruction truncation guard
let codebuddy_truncation = codebuddy_truncation_outcome();
if let Some(ref cbt) = codebuddy_truncation {
if cbt.ok {
passed += 1;
}
print_check(cbt);
}
// 15) BM25 cache health
let bm25_health = bm25_cache_health_outcome();
if bm25_health.ok {
+6
View File
@@ -125,6 +125,11 @@ pub(crate) fn collect_rules_files(home: &Path, project: &Path) -> Vec<RulesFileC
&home.join(".claude/CLAUDE.md"),
vec!["claude"],
);
push_rules_file(
out.as_mut(),
&home.join(".codebuddy/CODEBUDDY.md"),
vec!["codebuddy"],
);
push_rules_file(out.as_mut(), &home.join(".codex/AGENTS.md"), vec!["codex"]);
push_rules_file(
out.as_mut(),
@@ -142,6 +147,7 @@ pub(crate) fn collect_rules_files(home: &Path, project: &Path) -> Vec<RulesFileC
// other agents auto-load it.
push_rules_file(out.as_mut(), &d.join("AGENTS.md"), vec!["cursor", "codex"]);
push_rules_file(out.as_mut(), &d.join("CLAUDE.md"), vec!["claude"]);
push_rules_file(out.as_mut(), &d.join("CODEBUDDY.md"), vec!["codebuddy"]);
push_rules_file(out.as_mut(), &d.join("GEMINI.md"), vec!["gemini"]);
if d == *home {
+1 -1
View File
@@ -108,7 +108,7 @@ fn build_dual_rewrite_output(tool_input: Option<&serde_json::Value>, rewritten:
// Cursor hook output format
"permission": "allow",
"updated_input": updated_input,
// Claude Code hook output format (extra fields are ignored by other hosts)
// Claude Code / CodeBuddy hook output format (extra fields are ignored by other hosts)
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
+3 -3
View File
@@ -19,10 +19,10 @@ pub fn handle_observe() {
let Some(input) = read_stdin_with_timeout(HOOK_STDIN_TIMEOUT) else {
return;
};
// Dedicated rules-injection mode (#343): a Claude/Codex `SessionStart` hook
// Dedicated rules-injection mode (#343): a Claude/Codex/CodeBuddy `SessionStart` hook
// injects the compact lean-ctx summary as `additionalContext` — the
// non-polluting stand-in for the (skipped) CLAUDE.md/AGENTS.md block. Both
// agents register `hook observe` on SessionStart, so this is the single
// non-polluting stand-in for the (skipped) CLAUDE.md/CODEBUDDY.md/AGENTS.md block. All
// three agents register `hook observe` on SessionStart, so this is the single
// emit point (the Codex-specific handler stays silent in dedicated mode).
emit_dedicated_session_context(&input);
let Some(event) = parse_observe_event(&input) else {
+560
View File
@@ -0,0 +1,560 @@
use super::super::{
generate_rewrite_script, make_executable, mcp_server_quiet_mode, resolve_binary_path,
resolve_binary_path_for_bash, write_file, HookMode, REDIRECT_SCRIPT_CLAUDE,
};
pub(crate) fn install_codebuddy_hook_with_mode(global: bool, mode: HookMode) {
let Some(home) = crate::core::home::resolve_home_dir() else {
tracing::error!("Cannot resolve home directory");
return;
};
install_codebuddy_hook_scripts(&home);
install_codebuddy_hook_config(&home);
if matches!(mode, HookMode::Hybrid | HookMode::Mcp) {
install_codebuddy_mcp_server(&home);
}
let scope = crate::core::config::Config::load().rules_scope_effective();
if scope != crate::core::config::RulesScope::Project {
remove_codebuddy_rules_file(&home);
install_codebuddy_global_codebuddy_md_for_mode(&home, mode);
install_codebuddy_skill(&home);
}
let _ = global;
}
fn install_codebuddy_mcp_server(home: &std::path::Path) {
let config_path = crate::core::editor_registry::codebuddy_mcp_json_path(home);
let binary = super::super::resolve_binary_path();
let existing = std::fs::read_to_string(&config_path).unwrap_or_default();
if existing.contains("\"lean-ctx\"") && existing.contains("mcpServers") {
return;
}
let parsed: Result<serde_json::Value, _> = if existing.trim().is_empty() {
Ok(serde_json::json!({}))
} else {
crate::core::jsonc::parse_jsonc(&existing)
};
if let Ok(mut root) = parsed {
if let Some(obj) = root.as_object_mut() {
let servers = obj
.entry("mcpServers")
.or_insert_with(|| serde_json::json!({}));
if let Some(servers_obj) = servers.as_object_mut() {
if !servers_obj.contains_key("lean-ctx") {
servers_obj.insert(
"lean-ctx".to_string(),
serde_json::json!({
"command": binary,
"args": []
}),
);
write_file(
&config_path,
&serde_json::to_string_pretty(&root).unwrap_or_default(),
);
if !super::super::mcp_server_quiet_mode() {
eprintln!("Added lean-ctx MCP server to {}", config_path.display());
}
}
}
}
}
}
/// Shared with `doctor` so the instructions check recognises the same block
/// this installer writes.
pub(crate) const CODEBUDDY_MD_BLOCK_START: &str = "<!-- lean-ctx -->";
const CODEBUDDY_MD_BLOCK_END: &str = "<!-- /lean-ctx -->";
const CODEBUDDY_MD_BLOCK_VERSION: &str = "lean-ctx-codebuddy-v1";
const CODEBUDDY_MD_BLOCK_CONTENT_MCP: &str = "\
<!-- lean-ctx -->
<!-- lean-ctx-codebuddy-v1 -->
## lean-ctx — Context Runtime
Always prefer lean-ctx MCP tools over native equivalents:
- `ctx_read` instead of `Read` / `cat` (cached, 10 modes, re-reads ~13 tokens)
- `ctx_shell` instead of `bash` / `Shell` (95+ compression patterns)
- `ctx_search` instead of `Grep` / `rg` (compact results)
- `ctx_tree` instead of `ls` / `find` (compact directory maps)
- Native Edit/StrReplace stay unchanged. If Edit requires Read and Read is unavailable, use `ctx_edit(path, old_string, new_string)` instead.
- Write, Delete, Glob — use normally.
Read modes: full (edit), map (overview), signatures (API), diff (post-edit), lines:N-M (range), auto.
Details live in the `lean-ctx` skill (loads on demand — keep this file lean).
<!-- /lean-ctx -->";
fn install_codebuddy_global_codebuddy_md_for_mode(home: &std::path::Path, mode: HookMode) {
let codebuddy_dir = crate::core::editor_registry::codebuddy_state_dir(home);
let _ = std::fs::create_dir_all(&codebuddy_dir);
let codebuddy_md_path = codebuddy_dir.join("CODEBUDDY.md");
if crate::core::config::Config::load().rules_injection_effective()
== crate::core::config::RulesInjection::Dedicated
{
strip_codebuddy_md_block(&codebuddy_md_path);
return;
}
let existing = std::fs::read_to_string(&codebuddy_md_path).unwrap_or_default();
let block = match mode {
HookMode::Mcp | HookMode::Hybrid => CODEBUDDY_MD_BLOCK_CONTENT_MCP,
};
let block_version = match mode {
HookMode::Mcp | HookMode::Hybrid => CODEBUDDY_MD_BLOCK_VERSION,
};
if existing.contains(CODEBUDDY_MD_BLOCK_START) {
if existing.contains(block_version) {
return;
}
let cleaned = remove_block(&existing, CODEBUDDY_MD_BLOCK_START, CODEBUDDY_MD_BLOCK_END);
let updated = format!("{}\n\n{}\n", cleaned.trim(), block);
write_file(&codebuddy_md_path, &updated);
return;
}
if existing.trim().is_empty() {
write_file(&codebuddy_md_path, block);
} else {
let updated = format!("{}\n\n{}\n", existing.trim(), block);
write_file(&codebuddy_md_path, &updated);
}
}
fn strip_codebuddy_md_block(codebuddy_md_path: &std::path::Path) {
let Ok(existing) = std::fs::read_to_string(codebuddy_md_path) else {
return;
};
if !existing.contains(CODEBUDDY_MD_BLOCK_START) {
return;
}
let cleaned = remove_block(&existing, CODEBUDDY_MD_BLOCK_START, CODEBUDDY_MD_BLOCK_END);
if cleaned.trim().is_empty() {
let _ = std::fs::remove_file(codebuddy_md_path);
} else {
write_file(codebuddy_md_path, &format!("{}\n", cleaned.trim_end()));
}
}
fn remove_block(content: &str, start: &str, end: &str) -> String {
let s = content.find(start);
let e = content.find(end);
match (s, e) {
(Some(si), Some(ei)) if ei >= si => {
let after_end = ei + end.len();
let before = content[..si].trim_end_matches('\n');
let after = &content[after_end..];
let mut out = before.to_string();
out.push('\n');
if !after.trim().is_empty() {
out.push('\n');
out.push_str(after.trim_start_matches('\n'));
}
out
}
_ => content.to_string(),
}
}
/// Remove the lean-ctx-owned `~/.codebuddy/rules/lean-ctx.md` (GL #555/#558).
///
/// CodeBuddy auto-loads every `~/.codebuddy/rules/*.md` file unconditionally at
/// session start, so this file duplicated the CODEBUDDY.md block in every session.
/// The CODEBUDDY.md block is self-contained and detail docs live in the on-demand
/// skill; only files carrying our rules marker are touched.
fn remove_codebuddy_rules_file(home: &std::path::Path) {
let rules_path =
crate::core::editor_registry::codebuddy_rules_dir(home).join("lean-ctx.md");
let Ok(existing) = std::fs::read_to_string(&rules_path) else {
return;
};
if existing.contains("<!-- lean-ctx-rules-")
&& std::fs::remove_file(&rules_path).is_ok()
&& !super::super::mcp_server_quiet_mode()
{
eprintln!(
"Removed {} (always-loaded duplicate; CODEBUDDY.md block + skill replace it)",
rules_path.display()
);
}
}
fn install_codebuddy_skill(home: &std::path::Path) {
let skill_dir = home.join(".codebuddy/skills/lean-ctx");
let _ = std::fs::create_dir_all(skill_dir.join("scripts"));
let skill_md = include_str!("../../templates/SKILL.md");
let install_sh = include_str!("../../templates/skill_install.sh");
let skill_path = skill_dir.join("SKILL.md");
let script_path = skill_dir.join("scripts/install.sh");
write_file(&skill_path, skill_md);
write_file(&script_path, install_sh);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(mut perms) = std::fs::metadata(&script_path).map(|m| m.permissions()) {
perms.set_mode(0o755);
let _ = std::fs::set_permissions(&script_path, perms);
}
}
}
pub(crate) fn install_codebuddy_hook_scripts(home: &std::path::Path) {
let hooks_dir = crate::core::editor_registry::codebuddy_state_dir(home).join("hooks");
let _ = std::fs::create_dir_all(&hooks_dir);
let binary = resolve_binary_path();
let rewrite_path = hooks_dir.join("lean-ctx-rewrite.sh");
let rewrite_script = generate_rewrite_script(&resolve_binary_path_for_bash());
write_file(&rewrite_path, &rewrite_script);
make_executable(&rewrite_path);
let redirect_path = hooks_dir.join("lean-ctx-redirect.sh");
write_file(&redirect_path, REDIRECT_SCRIPT_CLAUDE);
make_executable(&redirect_path);
let wrapper = |subcommand: &str| -> String {
if cfg!(windows) {
format!("{binary} hook {subcommand}")
} else {
format!("{} hook {subcommand}", resolve_binary_path_for_bash())
}
};
let rewrite_native = hooks_dir.join("lean-ctx-rewrite-native");
write_file(
&rewrite_native,
&format!(
"#!/bin/sh\nexec {} hook rewrite\n",
resolve_binary_path_for_bash()
),
);
make_executable(&rewrite_native);
let redirect_native = hooks_dir.join("lean-ctx-redirect-native");
write_file(
&redirect_native,
&format!(
"#!/bin/sh\nexec {} hook redirect\n",
resolve_binary_path_for_bash()
),
);
make_executable(&redirect_native);
let _ = wrapper;
}
const REDIRECT_MATCHER: &str = "Read|read|ReadFile|read_file|View|view|Grep|grep|Search|search|ListFiles|list_files|ListDirectory|list_directory";
fn lean_ctx_action_token(command: &str) -> &str {
match command.rfind(" hook ") {
Some(i) => command[i + 1..].trim_end(),
None => command.trim_end(),
}
}
fn is_lean_ctx_command_for(hook: &serde_json::Value, action: &str) -> bool {
if hook.get("type").and_then(|t| t.as_str()) != Some("command") {
return false;
}
let Some(cmd) = hook.get("command").and_then(|c| c.as_str()) else {
return false;
};
if !cmd.contains("lean-ctx") {
return false;
}
if cmd.trim_end().ends_with(action) {
return true;
}
let legacy = if action.ends_with("rewrite") {
"lean-ctx-rewrite"
} else if action.ends_with("redirect") {
"lean-ctx-redirect"
} else {
return false;
};
cmd.contains(legacy)
}
fn ensure_command_hook(pre_arr: &mut Vec<serde_json::Value>, matcher: &str, command: &str) {
let action = lean_ctx_action_token(command);
for group in pre_arr.iter_mut() {
if let Some(hooks) = group.get_mut("hooks").and_then(|h| h.as_array_mut()) {
hooks.retain(|h| !is_lean_ctx_command_for(h, action));
}
}
pre_arr.retain(|g| {
g.get("hooks")
.and_then(|h| h.as_array())
.is_none_or(|hooks| !hooks.is_empty())
});
let desired = serde_json::json!({ "type": "command", "command": command });
if let Some(group) = pre_arr
.iter_mut()
.find(|g| g.get("matcher").and_then(|m| m.as_str()) == Some(matcher))
{
if let Some(obj) = group.as_object_mut() {
match obj
.entry("hooks".to_string())
.or_insert_with(|| serde_json::json!([]))
.as_array_mut()
{
Some(hooks) => hooks.push(desired),
None => {
obj.insert("hooks".to_string(), serde_json::json!([desired]));
}
}
}
return;
}
pre_arr.push(serde_json::json!({ "matcher": matcher, "hooks": [desired] }));
}
fn ensure_codebuddy_observe_hooks(
hooks_obj: &mut serde_json::Map<String, serde_json::Value>,
observe_cmd: &str,
) {
let observe_events = [
"PostToolUse",
"UserPromptSubmit",
"Stop",
"PreCompact",
"SessionStart",
"SessionEnd",
];
for event in observe_events {
let entry = hooks_obj
.entry(event.to_string())
.or_insert_with(|| serde_json::json!([]));
if let Some(arr) = entry.as_array() {
let already = arr.iter().any(|group| {
group
.get("hooks")
.and_then(|h| h.as_array())
.is_some_and(|hooks| {
hooks.iter().any(|hook| {
hook.get("command")
.and_then(|c| c.as_str())
.is_some_and(|c| c.contains("hook observe"))
})
})
});
if already {
continue;
}
}
if let Some(arr) = entry.as_array_mut() {
arr.push(serde_json::json!({
"matcher": ".*",
"hooks": [{ "type": "command", "command": observe_cmd }]
}));
} else {
*entry = serde_json::json!([{
"matcher": ".*",
"hooks": [{ "type": "command", "command": observe_cmd }]
}]);
}
}
}
pub(crate) fn install_codebuddy_hook_config(home: &std::path::Path) {
let hooks_dir = crate::core::editor_registry::codebuddy_state_dir(home).join("hooks");
let binary = resolve_binary_path();
let rewrite_cmd = format!("{binary} hook rewrite");
let redirect_cmd = format!("{binary} hook redirect");
let observe_cmd = format!("{binary} hook observe");
let settings_path = crate::core::editor_registry::codebuddy_state_dir(home).join("settings.json");
let settings_content = if settings_path.exists() {
std::fs::read_to_string(&settings_path).unwrap_or_default()
} else {
String::new()
};
let bash_matcher = if cfg!(windows) {
"Bash|bash|PowerShell|powershell"
} else {
"Bash|bash"
};
let desired_pretooluse = serde_json::json!([
{
"matcher": bash_matcher,
"hooks": [{
"type": "command",
"command": rewrite_cmd
}]
},
{
"matcher": REDIRECT_MATCHER,
"hooks": [{
"type": "command",
"command": redirect_cmd
}]
}
]);
if settings_content.is_empty() {
let mut hook_map = serde_json::Map::new();
hook_map.insert("PreToolUse".to_string(), desired_pretooluse);
ensure_codebuddy_observe_hooks(&mut hook_map, &observe_cmd);
let hook_entry = serde_json::json!({ "hooks": serde_json::Value::Object(hook_map) });
write_file(
&settings_path,
&serde_json::to_string_pretty(&hook_entry).unwrap_or_default(),
);
} else if let Ok(mut existing) = crate::core::jsonc::parse_jsonc(&settings_content) {
let before = serde_json::to_string_pretty(&existing).unwrap_or_default();
if let Some(root) = existing.as_object_mut() {
let hooks = root
.entry("hooks".to_string())
.or_insert_with(|| serde_json::json!({}));
if let Some(hooks_obj) = hooks.as_object_mut() {
let pre = hooks_obj
.entry("PreToolUse".to_string())
.or_insert_with(|| serde_json::json!([]));
if let Some(pre_arr) = pre.as_array_mut() {
ensure_command_hook(pre_arr, bash_matcher, &rewrite_cmd);
ensure_command_hook(pre_arr, REDIRECT_MATCHER, &redirect_cmd);
}
ensure_codebuddy_observe_hooks(hooks_obj, &observe_cmd);
}
}
let after = serde_json::to_string_pretty(&existing).unwrap_or_default();
if after != before {
write_file(&settings_path, &after);
}
}
if !mcp_server_quiet_mode() {
eprintln!("Installed CodeBuddy hooks at {}", hooks_dir.display());
}
}
pub(crate) fn install_codebuddy_project_hooks(cwd: &std::path::Path) {
let binary = resolve_binary_path();
let rewrite_cmd = format!("{binary} hook rewrite");
let redirect_cmd = format!("{binary} hook redirect");
let observe_cmd = format!("{binary} hook observe");
let settings_path = cwd.join(".codebuddy").join("settings.local.json");
let _ = std::fs::create_dir_all(cwd.join(".codebuddy"));
let existing = std::fs::read_to_string(&settings_path).unwrap_or_default();
let bash_matcher = if cfg!(windows) {
"Bash|bash|PowerShell|powershell"
} else {
"Bash|bash"
};
let desired_pretooluse = serde_json::json!([
{
"matcher": bash_matcher,
"hooks": [{
"type": "command",
"command": rewrite_cmd
}]
},
{
"matcher": REDIRECT_MATCHER,
"hooks": [{
"type": "command",
"command": redirect_cmd
}]
}
]);
if existing.is_empty() {
let mut hook_map = serde_json::Map::new();
hook_map.insert("PreToolUse".to_string(), desired_pretooluse);
ensure_codebuddy_project_observe_hooks(&mut hook_map, &observe_cmd);
let hook_entry = serde_json::json!({ "hooks": serde_json::Value::Object(hook_map) });
write_file(
&settings_path,
&serde_json::to_string_pretty(&hook_entry).unwrap_or_default(),
);
} else if let Ok(mut json) = crate::core::jsonc::parse_jsonc(&existing) {
let before = serde_json::to_string_pretty(&json).unwrap_or_default();
if let Some(root) = json.as_object_mut() {
let hooks = root
.entry("hooks".to_string())
.or_insert_with(|| serde_json::json!({}));
if let Some(hooks_obj) = hooks.as_object_mut() {
let pre = hooks_obj
.entry("PreToolUse".to_string())
.or_insert_with(|| serde_json::json!([]));
if let Some(pre_arr) = pre.as_array_mut() {
ensure_command_hook(pre_arr, bash_matcher, &rewrite_cmd);
ensure_command_hook(pre_arr, REDIRECT_MATCHER, &redirect_cmd);
}
ensure_codebuddy_project_observe_hooks(hooks_obj, &observe_cmd);
}
}
let after = serde_json::to_string_pretty(&json).unwrap_or_default();
if after != before {
write_file(&settings_path, &after);
}
}
if !mcp_server_quiet_mode() {
eprintln!("Created .codebuddy/settings.local.json (project-local hooks with observe).");
}
}
fn ensure_codebuddy_project_observe_hooks(
hooks_obj: &mut serde_json::Map<String, serde_json::Value>,
observe_cmd: &str,
) {
let project_events = ["PostToolUse", "UserPromptSubmit", "Stop", "PreCompact"];
for event in project_events {
let entry = hooks_obj
.entry(event.to_string())
.or_insert_with(|| serde_json::json!([]));
if let Some(arr) = entry.as_array() {
let already = arr.iter().any(|group| {
group
.get("hooks")
.and_then(|h| h.as_array())
.is_some_and(|hooks| {
hooks.iter().any(|hook| {
hook.get("command")
.and_then(|c| c.as_str())
.is_some_and(|c| c.contains("hook observe"))
})
})
});
if already {
continue;
}
}
if let Some(arr) = entry.as_array_mut() {
arr.push(serde_json::json!({
"matcher": ".*",
"hooks": [{ "type": "command", "command": observe_cmd }]
}));
} else {
*entry = serde_json::json!([{
"matcher": ".*",
"hooks": [{ "type": "command", "command": observe_cmd }]
}]);
}
}
}
+6
View File
@@ -2,6 +2,7 @@ mod amp;
mod antigravity;
mod claude;
mod cline;
mod codebuddy;
mod codex;
mod copilot;
mod crush;
@@ -23,10 +24,15 @@ pub(crate) use antigravity::{
};
pub(super) use antigravity::{install_antigravity_cli_hook, install_antigravity_hook};
pub(crate) use claude::CLAUDE_MD_BLOCK_START;
pub(crate) use codebuddy::CODEBUDDY_MD_BLOCK_START;
pub(super) use claude::{
install_claude_hook_config, install_claude_hook_scripts, install_claude_hook_with_mode,
install_claude_project_hooks,
};
pub(super) use codebuddy::{
install_codebuddy_hook_config, install_codebuddy_hook_scripts,
install_codebuddy_hook_with_mode, install_codebuddy_project_hooks,
};
pub(super) use cline::install_cline_rules;
pub use codex::install_codex_hook;
pub(super) use copilot::install_copilot_hook;
+32 -2
View File
@@ -88,7 +88,10 @@ pub fn recommend_hook_mode(agent_key: &str) -> HookMode {
use agents::{
install_amp_hook, install_antigravity_cli_hook, install_antigravity_hook,
install_claude_hook_config, install_claude_hook_scripts, install_claude_hook_with_mode,
install_claude_project_hooks, install_cline_rules, install_codex_hook, install_copilot_hook,
install_claude_project_hooks, install_cline_rules,
install_codebuddy_hook_config, install_codebuddy_hook_scripts,
install_codebuddy_hook_with_mode, install_codebuddy_project_hooks,
install_codex_hook, install_copilot_hook,
install_crush_hook_with_mode, install_cursor_hook_config, install_cursor_hook_scripts,
install_cursor_hook_with_mode, install_gemini_hook, install_gemini_hook_config,
install_gemini_hook_scripts, install_hermes_hook_with_mode, install_jetbrains_hook,
@@ -170,6 +173,11 @@ fn hooks_installed_for(agent: &str, home: &std::path::Path) -> bool {
dir.join("hooks/lean-ctx-rewrite.sh").exists()
|| file_contains_lean_ctx(&dir.join("settings.json"))
}
"codebuddy" => {
let dir = crate::core::editor_registry::codebuddy_state_dir(home);
dir.join("hooks/lean-ctx-rewrite.sh").exists()
|| file_contains_lean_ctx(&dir.join("settings.json"))
}
"cursor" => {
home.join(".cursor/hooks/lean-ctx-rewrite.sh").exists()
|| file_contains_lean_ctx(&home.join(".cursor/hooks.json"))
@@ -204,6 +212,10 @@ fn refresh_agent_hooks(agent: &str, home: &std::path::Path) {
install_claude_hook_scripts(home);
install_claude_hook_config(home);
}
"codebuddy" => {
install_codebuddy_hook_scripts(home);
install_codebuddy_hook_config(home);
}
"cursor" => {
install_cursor_hook_scripts(home);
install_cursor_hook_config(home);
@@ -444,6 +456,22 @@ pub fn install_project_rules_for_agents(agents: &[&str]) {
install_claude_project_hooks(&cwd);
}
if wants("codebuddy") {
let codebuddy_rules_file = cwd.join(".codebuddy").join("rules").join("lean-ctx.md");
if let Ok(existing) = std::fs::read_to_string(&codebuddy_rules_file) {
if existing.contains("<!-- lean-ctx-rules-")
&& std::fs::remove_file(&codebuddy_rules_file).is_ok()
&& !mcp_server_quiet_mode()
{
eprintln!(
"Removed .codebuddy/rules/lean-ctx.md (always-loaded duplicate; CODEBUDDY.md block + skill replace it)."
);
}
}
install_codebuddy_project_hooks(&cwd);
}
if wants("kiro") {
let kiro_dir = cwd.join(".kiro");
if kiro_dir.exists() {
@@ -614,6 +642,7 @@ pub fn install_agent_hook_with_mode(agent: &str, global: bool, mode: HookMode) {
let home = crate::core::home::resolve_home_dir().unwrap_or_default();
match agent {
"claude" | "claude-code" => install_claude_hook_with_mode(global, mode),
"codebuddy" => install_codebuddy_hook_with_mode(global, mode),
"cursor" => install_cursor_hook_with_mode(global, mode),
"gemini" => {
install_gemini_hook();
@@ -697,7 +726,7 @@ pub fn install_agent_hook_with_mode(agent: &str, global: bool, mode: HookMode) {
_ => {
eprintln!("Unknown agent: {agent}");
eprintln!(" Supported: aider, amazonq, amp, antigravity, antigravity-cli, augment,");
eprintln!(" claude, cline, codex, continue, copilot, crush, cursor, emacs, gemini,");
eprintln!(" claude, cline, codebuddy, codex, continue, copilot, crush, cursor, emacs, gemini,");
eprintln!(" hermes, jetbrains, kiro, neovim, openclaw, opencode, pi, qoder,");
eprintln!(" qoderwork, qwen, roo, sublime, trae, verdent, vscode, windsurf, zed");
std::process::exit(1);
@@ -708,6 +737,7 @@ pub fn install_agent_hook_with_mode(agent: &str, global: bool, mode: HookMode) {
pub fn install_agent_project_hooks(agent: &str, cwd: &std::path::Path) {
match agent {
"claude" | "claude-code" => agents::install_claude_project_hooks(cwd),
"codebuddy" => agents::install_codebuddy_project_hooks(cwd),
_ => {}
}
}
+8 -3
View File
@@ -35,7 +35,7 @@ pub fn build_instructions(crp_mode: CrpMode) -> String {
}
pub fn build_instructions_with_client(crp_mode: CrpMode, client_name: &str) -> String {
if is_claude_code_client(client_name) {
if is_claude_code_client(client_name) || is_codebuddy_client(client_name) {
return build_claude_code_instructions();
}
build_full_instructions(crp_mode, client_name)
@@ -48,7 +48,7 @@ pub fn build_instructions_for_test(crp_mode: CrpMode) -> String {
}
pub fn build_instructions_with_client_for_test(crp_mode: CrpMode, client_name: &str) -> String {
if is_claude_code_client(client_name) {
if is_claude_code_client(client_name) || is_codebuddy_client(client_name) {
return build_claude_code_instructions();
}
build_full_instructions_for_test(crp_mode, client_name)
@@ -63,7 +63,7 @@ pub fn build_instructions_with_client_for_compiler(
client_name: &str,
unified_tool_mode: bool,
) -> String {
if is_claude_code_client(client_name) {
if is_claude_code_client(client_name) || is_codebuddy_client(client_name) {
return build_claude_code_instructions();
}
build_full_instructions_for_compiler(crp_mode, client_name, unified_tool_mode)
@@ -74,6 +74,11 @@ fn is_claude_code_client(client_name: &str) -> bool {
lower.contains("claude") && !lower.contains("cursor")
}
fn is_codebuddy_client(client_name: &str) -> bool {
let lower = client_name.to_lowercase();
lower.contains("codebuddy")
}
/// LITM calibration manifest rotation (#539).
///
/// Settles the previous manifest — every entry the agent never re-recalled is
+2
View File
@@ -609,9 +609,11 @@ fn check_mcp_configs() -> String {
let mut found = Vec::new();
let claude_cfg = crate::setup::claude_config_json_path(&home);
let codebuddy_cfg = crate::core::editor_registry::codebuddy_mcp_json_path(&home);
let configs: Vec<(std::path::PathBuf, &str)> = vec![
(home.join(".cursor/mcp.json"), "Cursor"),
(claude_cfg, "Claude Code"),
(codebuddy_cfg, "CodeBuddy"),
(home.join(".codeium/windsurf/mcp_config.json"), "Windsurf"),
];
+2 -2
View File
@@ -6,7 +6,7 @@ use std::path::PathBuf;
use super::RulesFormat;
/// The canonical shared rules block lean-ctx injects into a host instruction file
/// (`CLAUDE.md` / `AGENTS.md`). Exposed for honest per-turn overhead accounting
/// (`CLAUDE.md` / `CODEBUDDY.md` / `AGENTS.md`). Exposed for honest per-turn overhead accounting
/// (see `core::context_overhead`, GitHub #361).
#[must_use]
pub fn canonical_rules_block() -> &'static str {
@@ -62,7 +62,7 @@ pub const GEMINI_DEDICATED_CONTEXT_FILENAME: &str = "LEANCTX.md";
// ---------------------------------------------------------------------------
// Rules content for SHARED config files (appended to user's existing config).
// LITM-optimized: critical instruction at START and END of block.
// Used for: CLAUDE.md, instructions.md, GEMINI.md, copilot-instructions.md
// Used for: CLAUDE.md, CODEBUDDY.md, instructions.md, GEMINI.md, copilot-instructions.md
// ---------------------------------------------------------------------------
pub(super) const RULES_SHARED: &str = r#"# lean-ctx — Context Engineering Layer
<!-- lean-ctx-rules-v12 -->
+7
View File
@@ -13,6 +13,13 @@ pub(super) fn is_tool_detected(target: &RulesTarget, home: &std::path::Path) ->
let state_dir = crate::core::editor_registry::claude_state_dir(home);
crate::core::editor_registry::claude_mcp_json_path(home).exists() || state_dir.exists()
}
"CodeBuddy" => {
if command_exists("codebuddy") {
return true;
}
let state_dir = crate::core::editor_registry::codebuddy_state_dir(home);
crate::core::editor_registry::codebuddy_mcp_json_path(home).exists() || state_dir.exists()
}
"Codex CLI" => {
let codex_dir =
crate::core::home::resolve_codex_dir().unwrap_or_else(|| home.join(".codex"));
+1
View File
@@ -195,6 +195,7 @@ fn match_agent_name(cli_key: &str, target_name: &str) -> bool {
|| tn.contains(&needle)
|| (needle.contains("cursor") && tn.contains("cursor"))
|| (needle.contains("claude") && tn.contains("claude"))
|| (needle.contains("codebuddy") && tn.contains("codebuddy"))
|| (needle.contains("windsurf") && tn.contains("windsurf"))
|| (needle.contains("codex") && tn.contains("claude"))
|| (needle.contains("zed") && tn.contains("zed"))
+11
View File
@@ -23,6 +23,12 @@ pub(super) fn build_skill_targets(home: &std::path::Path) -> Vec<SkillTarget> {
display_name: "Claude Code",
skill_dir: crate::setup::claude_config_dir(home).join("skills/lean-ctx"),
},
SkillTarget {
agent_key: "codebuddy",
display_name: "CodeBuddy",
skill_dir: crate::core::editor_registry::codebuddy_state_dir(home)
.join("skills/lean-ctx"),
},
SkillTarget {
agent_key: "cursor",
display_name: "Cursor",
@@ -55,6 +61,11 @@ fn is_skill_agent_detected(agent_key: &str, home: &std::path::Path) -> bool {
|| crate::core::editor_registry::claude_mcp_json_path(home).exists()
|| crate::core::editor_registry::claude_state_dir(home).exists()
}
"codebuddy" => {
command_exists("codebuddy")
|| crate::core::editor_registry::codebuddy_mcp_json_path(home).exists()
|| crate::core::editor_registry::codebuddy_state_dir(home).exists()
}
"cursor" => home.join(".cursor").exists(),
"codex" => {
let codex_dir =
+6
View File
@@ -44,6 +44,12 @@ pub(super) fn build_rules_targets(
// footprints, GL #555). Claude guidance lives in the CLAUDE.md block
// (hooks/agents/claude.rs) + the on-demand skill; uninstall still removes
// legacy ~/.claude/rules/lean-ctx.md files from older installs.
//
// CodeBuddy follows the exact same pattern as Claude Code: NO rules target.
// CodeBuddy installs (and auto-loads) the CODEBUDDY.md block every session,
// so a separate ~/.codebuddy/rules/lean-ctx.md would duplicate it (GL #555/#558).
// Guidance lives in the CODEBUDDY.md block + the on-demand skill; uninstall
// still removes legacy ~/.codebuddy/rules/lean-ctx.md files from older installs.
vec![
// --- Shared config files (append-only) ---
RulesTarget {
+7 -1
View File
@@ -176,6 +176,7 @@ fn target_count() {
// 24, not 25: Claude Code intentionally has no rules target — its rules
// file loaded unconditionally every session and duplicated the CLAUDE.md
// block (GL #555/#558). Guidance lives in CLAUDE.md + the on-demand skill.
// CodeBuddy also has no rules target (same pattern as Claude Code).
let home = std::path::PathBuf::from("/tmp/fake_home");
let targets = build_rules_targets(&home, crate::core::config::RulesInjection::Shared);
assert_eq!(targets.len(), 24);
@@ -183,6 +184,10 @@ fn target_count() {
!targets.iter().any(|t| t.name == "Claude Code"),
"Claude Code must not get a rules target (always-loaded duplicate)"
);
assert!(
!targets.iter().any(|t| t.name == "CodeBuddy"),
"CodeBuddy must not get a rules target (always-loaded duplicate, same as Claude Code)"
);
// Dedicated mode swaps paths/formats but never changes the target count.
let dedicated = build_rules_targets(&home, crate::core::config::RulesInjection::Dedicated);
assert_eq!(dedicated.len(), 24);
@@ -237,7 +242,7 @@ fn skill_template_not_empty() {
fn skill_targets_count() {
let home = std::path::PathBuf::from("/tmp/fake_home");
let targets = build_skill_targets(&home);
assert_eq!(targets.len(), 5);
assert_eq!(targets.len(), 6);
}
#[test]
@@ -301,6 +306,7 @@ fn match_agent_name_basic() {
assert!(match_agent_name("verdent", "Verdent"));
assert!(match_agent_name("continue", "Continue"));
assert!(match_agent_name("antigravity", "Antigravity"));
assert!(match_agent_name("codebuddy", "CodeBuddy"));
assert!(match_agent_name("gemini", "Gemini CLI"));
assert!(match_agent_name("augment", "Augment"));
assert!(match_agent_name("openclaw", "OpenClaw"));
+2
View File
@@ -52,8 +52,10 @@ fn is_home_or_agent_dir(dir: &std::path::Path) -> bool {
}
let dir_str = dir.to_string_lossy();
dir_str.ends_with("/.claude")
|| dir_str.ends_with("/.codebuddy")
|| dir_str.ends_with("/.codex")
|| dir_str.contains("/.claude/")
|| dir_str.contains("/.codebuddy/")
|| dir_str.contains("/.codex/")
}
+1
View File
@@ -154,6 +154,7 @@ impl ServerHandler for LeanCtxServer {
let heuristic_role = match agent_name.to_lowercase().as_str() {
n if n.contains("cursor") => Some("coder"),
n if n.contains("claude") => Some("coder"),
n if n.contains("codebuddy") => Some("coder"),
n if n.contains("codex") => Some("coder"),
n if n.contains("antigravity") || n.contains("gemini") => Some("coder"),
n if n.contains("review") => Some("reviewer"),
+20 -1
View File
@@ -38,8 +38,9 @@ pub(crate) fn configure_plan_mode_settings(newly_configured: &[&str], already_co
let has_vscode = all_configured.contains(&"VS Code");
let has_claude = all_configured.contains(&"Claude Code");
let has_codebuddy = all_configured.contains(&"CodeBuddy");
if !has_vscode && !has_claude {
if !has_vscode && !has_claude && !has_codebuddy {
return;
}
@@ -78,6 +79,24 @@ pub(crate) fn configure_plan_mode_settings(newly_configured: &[&str], already_co
}
}
}
if has_codebuddy {
match crate::core::editor_registry::plan_mode::write_claude_code_plan_permissions() {
Ok(r) if r.action == WriteAction::Already => {
terminal_ui::print_status_ok(
"CodeBuddy \x1b[2mplan mode permissions present\x1b[0m",
);
}
Ok(_) => {
terminal_ui::print_status_new(
"CodeBuddy \x1b[2mplan mode permissions added\x1b[0m",
);
}
Err(e) => {
terminal_ui::print_status_warn(&format!("CodeBuddy plan mode: {e}"));
}
}
}
}
pub(crate) fn shorten_path(path: &str, home: &str) -> String {
+19
View File
@@ -94,6 +94,13 @@ pub fn configure_agent_mcp(agent: &str) -> Result<(), String> {
eprintln!("\x1b[33m⚠\x1b[0m Claude Code plan mode: {e}");
}
}
if agent == "codebuddy" {
if let Err(e) =
crate::core::editor_registry::plan_mode::write_claude_code_plan_permissions()
{
eprintln!("\x1b[33m⚠\x1b[0m CodeBuddy plan mode: {e}");
}
}
if errors.is_empty() {
Ok(())
@@ -137,6 +144,12 @@ pub(crate) fn agent_mcp_targets(
crate::core::editor_registry::claude_mcp_json_path(home),
ConfigType::McpJson,
),
"codebuddy" => push(
&mut targets,
"CodeBuddy",
crate::core::editor_registry::codebuddy_mcp_json_path(home),
ConfigType::McpJson,
),
"augment" => {
push(
&mut targets,
@@ -380,6 +393,12 @@ pub fn disable_agent_mcp(agent: &str, overwrite_invalid: bool) -> Result<(), Str
crate::core::editor_registry::claude_mcp_json_path(&home),
ConfigType::McpJson,
),
"codebuddy" => push(
&mut targets,
"CodeBuddy",
crate::core::editor_registry::codebuddy_mcp_json_path(&home),
ConfigType::McpJson,
),
"augment" => {
push(
&mut targets,
+3
View File
@@ -16,12 +16,14 @@ const DROPIN_SH: &str = "00-lean-ctx.sh";
const KNOWN_AGENT_ENV_VARS: &[&str] = &[
"LEAN_CTX_AGENT",
"CLAUDECODE",
"CODEBUDDY",
"CODEX_CLI_SESSION",
"GEMINI_SESSION",
];
const AGENT_ALIASES: &[(&str, &str)] = &[
("claude", "claude"),
("codebuddy", "codebuddy"),
("codex", "codex"),
("gemini", "gemini"),
];
@@ -521,6 +523,7 @@ mod tests {
let check = build_env_check();
assert!(check.contains("LEAN_CTX_AGENT"));
assert!(check.contains("CLAUDECODE"));
assert!(check.contains("CODEBUDDY"));
assert!(check.contains("||"));
}
+2
View File
@@ -18,8 +18,10 @@ pub(super) fn has_project_marker(dir: &std::path::Path) -> bool {
pub(super) fn is_suspicious_root(dir: &std::path::Path) -> bool {
let s = dir.to_string_lossy();
s.contains("/.claude")
|| s.contains("/.codebuddy")
|| s.contains("/.codex")
|| s.contains("\\.claude")
|| s.contains("\\.codebuddy")
|| s.contains("\\.codex")
}
+59
View File
@@ -60,6 +60,7 @@ pub(super) fn remove_project_agent_files(dry_run: bool) -> bool {
".kiro/steering/lean-ctx.md",
".cursor/rules/lean-ctx.mdc",
".claude/rules/lean-ctx.md",
".codebuddy/rules/lean-ctx.md",
];
for rel in &dedicated_project_files {
let path = cwd.join(rel);
@@ -144,6 +145,22 @@ pub(super) fn remove_project_agent_files(dry_run: bool) -> bool {
}
}
// Project-level .codebuddy/settings.local.json: surgically remove lean-ctx hooks
let codebuddy_settings = cwd.join(".codebuddy/settings.local.json");
if codebuddy_settings.exists() {
if let Ok(content) = fs::read_to_string(&codebuddy_settings) {
if content.contains("lean-ctx") {
backup_before_modify(&codebuddy_settings, dry_run);
removed |= apply_hook_cleanup(
&codebuddy_settings,
"Project .codebuddy/settings.local.json",
&content,
dry_run,
);
}
}
}
removed
}
@@ -295,10 +312,16 @@ pub(super) fn remove_mcp_configs(home: &Path, dry_run: bool) -> bool {
|| PathBuf::from("/nonexistent"),
|d| PathBuf::from(d).join(".claude.json"),
);
let codebuddy_cfg_dir_json = std::env::var("CODEBUDDY_CONFIG_DIR").ok().map_or_else(
|| PathBuf::from("/nonexistent"),
|d| PathBuf::from(d).join(".codebuddy.json"),
);
let mut configs: Vec<(&str, PathBuf)> = vec![
("Cursor", home.join(".cursor/mcp.json")),
("Claude Code (config dir)", claude_cfg_dir_json),
("Claude Code (home)", home.join(".claude.json")),
("CodeBuddy (config dir)", codebuddy_cfg_dir_json),
("CodeBuddy (home)", home.join(".codebuddy.json")),
("Windsurf", home.join(".codeium/windsurf/mcp_config.json")),
("Gemini CLI", home.join(".gemini/settings.json")),
(
@@ -504,6 +527,10 @@ pub(super) fn remove_rules_files(home: &Path, dry_run: bool) -> bool {
"Claude Code",
crate::core::editor_registry::claude_rules_dir(home).join("lean-ctx.md"),
),
(
"CodeBuddy",
crate::core::editor_registry::codebuddy_rules_dir(home).join("lean-ctx.md"),
),
("Cursor", home.join(".cursor/rules/lean-ctx.mdc")),
(
"Gemini CLI (legacy)",
@@ -565,6 +592,11 @@ pub(super) fn remove_rules_files(home: &Path, dry_run: bool) -> bool {
crate::core::editor_registry::claude_state_dir(home).join("CLAUDE.md"),
),
("Claude Code (legacy home)", home.join(".claude/CLAUDE.md")),
(
"CodeBuddy",
crate::core::editor_registry::codebuddy_state_dir(home).join("CODEBUDDY.md"),
),
("CodeBuddy (legacy home)", home.join(".codebuddy/CODEBUDDY.md")),
("Gemini CLI", home.join(".gemini/GEMINI.md")),
(
"Codex CLI",
@@ -780,11 +812,16 @@ fn apply_hook_cleanup(path: &Path, label: &str, content: &str, dry_run: bool) ->
pub(super) fn remove_hook_files(home: &Path, dry_run: bool) -> bool {
let claude_hooks_dir = crate::core::editor_registry::claude_state_dir(home).join("hooks");
let codebuddy_hooks_dir = crate::core::editor_registry::codebuddy_state_dir(home).join("hooks");
let hook_files: Vec<PathBuf> = vec![
claude_hooks_dir.join("lean-ctx-rewrite.sh"),
claude_hooks_dir.join("lean-ctx-redirect.sh"),
claude_hooks_dir.join("lean-ctx-rewrite-native"),
claude_hooks_dir.join("lean-ctx-redirect-native"),
codebuddy_hooks_dir.join("lean-ctx-rewrite.sh"),
codebuddy_hooks_dir.join("lean-ctx-redirect.sh"),
codebuddy_hooks_dir.join("lean-ctx-rewrite-native"),
codebuddy_hooks_dir.join("lean-ctx-redirect-native"),
home.join(".cursor/hooks/lean-ctx-rewrite.sh"),
home.join(".cursor/hooks/lean-ctx-redirect.sh"),
home.join(".cursor/hooks/lean-ctx-rewrite-native"),
@@ -842,6 +879,28 @@ pub(super) fn remove_hook_files(home: &Path, dry_run: bool) -> bool {
);
}
// CodeBuddy global settings: surgically remove lean-ctx hook entries
for codebuddy_settings_name in ["settings.json", "settings.local.json"] {
let codebuddy_settings =
crate::core::editor_registry::codebuddy_state_dir(home).join(codebuddy_settings_name);
if !codebuddy_settings.exists() {
continue;
}
let Ok(content) = fs::read_to_string(&codebuddy_settings) else {
continue;
};
if !content.contains("lean-ctx") {
continue;
}
backup_before_modify(&codebuddy_settings, dry_run);
removed |= apply_hook_cleanup(
&codebuddy_settings,
&format!("CodeBuddy {codebuddy_settings_name}"),
&content,
dry_run,
);
}
// Antigravity CLI (`agy`) installs hooks as a *plugin* under
// ~/.gemini/config/plugins/lean-ctx (registered in import_manifest.json),
// not as a hooks block in any settings.json (GH #284). Remove that plugin
+13
View File
@@ -224,8 +224,10 @@ pub(super) fn remove_marked_block(content: &str, start: &str, end: &str) -> Stri
fn remove_skill_dirs(home: &Path, dry_run: bool) -> bool {
let claude_state = crate::core::editor_registry::claude_state_dir(home);
let codebuddy_state = crate::core::editor_registry::codebuddy_state_dir(home);
let mut skill_dirs: Vec<(&str, PathBuf)> = vec![
("Claude Code", claude_state.join("skills/lean-ctx")),
("CodeBuddy", codebuddy_state.join("skills/lean-ctx")),
("Cursor", home.join(".cursor/skills/lean-ctx")),
(
"Codex CLI",
@@ -243,6 +245,12 @@ fn remove_skill_dirs(home: &Path, dry_run: bool) -> bool {
skill_dirs.push(("Claude Code (default)", default_claude_skill));
}
// If CODEBUDDY_CONFIG_DIR differs from ~/.codebuddy, also clean default path
let default_codebuddy_skill = home.join(".codebuddy/skills/lean-ctx");
if !skill_dirs.iter().any(|(_, p)| *p == default_codebuddy_skill) {
skill_dirs.push(("CodeBuddy (default)", default_codebuddy_skill));
}
let mut removed = false;
for (name, dir) in &skill_dirs {
if !dir.exists() {
@@ -354,6 +362,8 @@ fn scan_dirs(home: &Path) -> Vec<PathBuf> {
home.join(".cursor"),
home.join(".claude"),
crate::core::editor_registry::claude_state_dir(home),
home.join(".codebuddy"),
crate::core::editor_registry::codebuddy_state_dir(home),
crate::core::editor_registry::zed_config_dir(home),
home.join(".gemini"),
home.join(".gemini/antigravity"),
@@ -414,6 +424,9 @@ fn scan_dirs(home: &Path) -> Vec<PathBuf> {
".claude",
".claude/rules",
".claude/hooks",
".codebuddy",
".codebuddy/rules",
".codebuddy/hooks",
".kiro/steering",
".github",
".github/hooks",
+1
View File
@@ -79,6 +79,7 @@ Use `full` mode only when you will edit the file.
lean-ctx init --global # Install shell aliases
lean-ctx init --agent cursor # Hybrid (MCP reads/search + shell hooks)
lean-ctx init --agent claude # Hybrid (Claude Code)
lean-ctx init --agent codebuddy # Hybrid (CodeBuddy)
lean-ctx init --agent codex # Hybrid (Codex CLI)
lean-ctx init --agent opencode # Hybrid (OpenCode)