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:
@@ -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 |
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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 1–4 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
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) | — |
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,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\
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"),
|
||||
];
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -15,6 +15,7 @@ const IDE_CONFIG_DIRS: &[&str] = &[
|
||||
".amp",
|
||||
".aider",
|
||||
".continue",
|
||||
".codebuddy",
|
||||
];
|
||||
|
||||
/// Expands `~`, `$VAR` and `${VAR}` in a config-supplied path entry.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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\\")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }]
|
||||
}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
];
|
||||
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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/")
|
||||
}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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("||"));
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user