diff --git a/CLAUDE.md b/CLAUDE.md index 27089be..aeeb367 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## What this repo is -Edgee is an **open-source AI Gateway** written in Rust. It sits between coding agents (Claude Code, Codex, OpenCode — Cursor and OpenClaw coming soon) or any llm client and LLM providers (Anthropic, OpenAI) and compresses token-heavy traffic on the fly. A hosted / edge version of the same gateway is available at [`www.edgee.ai`](https://www.edgee.ai); **this repository is the OSS core** you can self-host. +Edgee is an **open-source AI Gateway** written in Rust. It sits between coding agents (Claude Code, CodeBuddy, Codex, OpenCode — Cursor and OpenClaw coming soon) or any llm client and LLM providers (Anthropic, OpenAI) and compresses token-heavy traffic on the fly. A hosted / edge version of the same gateway is available at [`www.edgee.ai`](https://www.edgee.ai); **this repository is the OSS core** you can self-host. The distinguishing feature is the compression engine. Today it ships a single technique — **tool-results compression** — but the architecture is explicitly designed to host **multiple composable techniques** that a developer selects and combines per request. When extending compression, add a new technique alongside the existing ones rather than threading a new code path through the provider dispatch layer. @@ -20,7 +20,7 @@ If `edgee stats` fails, you have the wrong package installed. Entry point: `crates/cli/src/main.rs`. Subcommands declared in `crates/cli/src/commands/mod.rs`: -- `edgee launch {claude|codex|opencode}` — launches the agent with `ANTHROPIC_BASE_URL` and custom headers pointing at the local gateway. Implementation per agent under `crates/cli/src/commands/launch/`. +- `edgee launch {claude|codebuddy|codex|opencode}` — launches the agent with `ANTHROPIC_BASE_URL` and custom headers pointing at the local gateway. Implementation per agent under `crates/cli/src/commands/launch/`. - `edgee auth {login|status|list|switch}` — OAuth-style flow against the Edgee console. See `crates/cli/src/api.rs` and `crates/cli/src/commands/auth/`. - `edgee stats` (visible alias `report`) — prints session token counts and compression savings. - `edgee alias` — installs shell aliases for quick access. diff --git a/README.md b/README.md index 73c97f4..7f6923f 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,9 @@ edgee launch codex # Opencode edgee launch opencode + +# CodeBuddy +edgee launch codebuddy ``` Any extra flags you pass after the subcommand are forwarded straight to the underlying agent. For example, to resume the most recent session: @@ -83,6 +86,7 @@ Any extra flags you pass after the subcommand are forwarded straight to the unde edgee launch claude --resume abcd # continue the last Claude Code session edgee launch codex resume # resume the last Codex session edgee launch opencode -c # continue the last OpenCode session +edgee launch codebuddy --resume # resume the last CodeBuddy session ``` ### Route plain `claude` / `codex` / `opencode` through Edgee @@ -90,14 +94,14 @@ edgee launch opencode -c # continue the last OpenCode session If you'd rather type `claude` (or have another tool spawn `claude` for you), install Edgee's shims: ```bash -edgee alias # installs all three; pass `claude`, `codex`, `opencode` to scope +edgee alias # installs all three; pass `claude`, `codebuddy`, `codex`, `opencode` to scope edgee alias remove # to undo ``` This does two things: 1. Adds a shell alias to `~/.bashrc`, `~/.zshrc`, and `~/.config/fish/config.fish` (`alias claude='edgee launch claude'`, etc.) so interactive shells route through Edgee. -2. Writes executable shim scripts to `~/.edgee/bin/{claude,codex,opencode}` and prepends `~/.edgee/bin` to `PATH` in the same rc block. This means **non-interactive** shells, including `bash -c '...'`, scripts, and tools that spawn Claude Code via `exec`, also get routed through Edgee. Reopen your terminal (or `exec $SHELL -l`) once after install. +2. Writes executable shim scripts to `~/.edgee/bin/{claude,codebuddy,codex,opencode}` and prepends `~/.edgee/bin` to `PATH` in the same rc block. This means **non-interactive** shells, including `bash -c '...'`, scripts, and tools that spawn Claude Code via `exec`, also get routed through Edgee. Reopen your terminal (or `exec $SHELL -l`) once after install. ### Use as a standalone gateway @@ -192,6 +196,7 @@ The `SessionStart` hook installed by `edgee statusline claude install` (or by th | Claude Code | `edgee launch claude` | ✅ Supported | | Codex | `edgee launch codex` | ✅ Supported | | Opencode | `edgee launch opencode` | ✅ Supported | +| CodeBuddy | `edgee launch codebuddy` | ✅ Supported (local gateway) | | Cursor | `edgee launch cursor` | 🔜 Coming soon | | Any OpenAI-compatible client | `edgee serve` | ✅ Supported | diff --git a/crates/cli/src/commands/alias.rs b/crates/cli/src/commands/alias.rs index 418bfbd..f6a0a79 100644 --- a/crates/cli/src/commands/alias.rs +++ b/crates/cli/src/commands/alias.rs @@ -13,10 +13,11 @@ const SHIM_DIR_REL: &str = ".edgee/bin"; const USES_SHIMS: bool = cfg!(unix); const CLAUDE_ALIAS: AliasSpec = AliasSpec::new("claude", "edgee launch claude"); +const CODEBUDDY_ALIAS: AliasSpec = AliasSpec::new("codebuddy", "edgee launch codebuddy"); const CODEX_ALIAS: AliasSpec = AliasSpec::new("codex", "edgee launch codex"); const OPENCODE_ALIAS: AliasSpec = AliasSpec::new("opencode", "edgee launch opencode"); -const ALL_ALIASES: [AliasSpec; 3] = [CLAUDE_ALIAS, CODEX_ALIAS, OPENCODE_ALIAS]; +const ALL_ALIASES: [AliasSpec; 4] = [CLAUDE_ALIAS, CODEBUDDY_ALIAS, CODEX_ALIAS, OPENCODE_ALIAS]; const PATH_EXPORT_POSIX: &str = "case \":$PATH:\" in\n *\":$HOME/.edgee/bin:\"*) ;;\n *) export PATH=\"$HOME/.edgee/bin:$PATH\" ;;\nesac\n"; const PATH_EXPORT_FISH: &str = "fish_add_path -p \"$HOME/.edgee/bin\"\n"; @@ -24,6 +25,7 @@ const PATH_EXPORT_FISH: &str = "fish_add_path -p \"$HOME/.edgee/bin\"\n"; #[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)] pub enum Agent { Claude, + Codebuddy, Codex, Opencode, All, @@ -33,6 +35,7 @@ impl Agent { fn aliases(self) -> &'static [AliasSpec] { match self { Self::Claude => std::slice::from_ref(&CLAUDE_ALIAS), + Self::Codebuddy => std::slice::from_ref(&CODEBUDDY_ALIAS), Self::Codex => std::slice::from_ref(&CODEX_ALIAS), Self::Opencode => std::slice::from_ref(&OPENCODE_ALIAS), Self::All => &ALL_ALIASES, @@ -42,9 +45,10 @@ impl Agent { fn label(self) -> &'static str { match self { Self::Claude => "claude", + Self::Codebuddy => "codebuddy", Self::Codex => "codex", Self::Opencode => "opencode", - Self::All => "claude, codex, and opencode", + Self::All => "claude, codebuddy, codex, and opencode", } } } @@ -646,11 +650,13 @@ mod tests { let dir = tmp.path().join("bin"); write_shims(&dir, &ALL_ALIASES).unwrap(); assert!(dir.join("claude").exists()); + assert!(dir.join("codebuddy").exists()); assert!(dir.join("codex").exists()); assert!(dir.join("opencode").exists()); remove_shims(&dir, &codex_only()).unwrap(); assert!(dir.join("claude").exists()); + assert!(dir.join("codebuddy").exists()); assert!(!dir.join("codex").exists()); assert!(dir.join("opencode").exists()); } diff --git a/crates/cli/src/commands/auth/login.rs b/crates/cli/src/commands/auth/login.rs index e04bfaf..d197ee8 100644 --- a/crates/cli/src/commands/auth/login.rs +++ b/crates/cli/src/commands/auth/login.rs @@ -204,6 +204,7 @@ pub async fn ensure_onboarded(provider: &str) -> Result<()> { pub fn agent_label(provider: &str) -> &'static str { match provider { "claude" => "Claude Code", + "codebuddy" => "CodeBuddy", "codex" => "Codex", "opencode" => "OpenCode", _ => "your agent", @@ -283,6 +284,7 @@ pub async fn fetch_provider_key(provider: &str) -> Result Result<&'static str> { match provider { "claude" => Ok("claude_code"), + "codebuddy" => Ok("codebuddy"), "codex" => Ok("codex"), "opencode" => Ok("opencode"), _ => anyhow::bail!("Unsupported provider `{provider}`"), @@ -295,6 +297,7 @@ fn provider_config_mut<'a>( ) -> Result<&'a mut Option> { match provider { "claude" => Ok(&mut creds.claude), + "codebuddy" => Ok(&mut creds.codebuddy), "codex" => Ok(&mut creds.codex), "opencode" => Ok(&mut creds.opencode), _ => anyhow::bail!("Unsupported provider `{provider}`"), @@ -307,6 +310,7 @@ fn provider_config<'a>( ) -> Result> { match provider { "claude" => Ok(creds.claude.as_ref()), + "codebuddy" => Ok(creds.codebuddy.as_ref()), "codex" => Ok(creds.codex.as_ref()), "opencode" => Ok(creds.opencode.as_ref()), _ => anyhow::bail!("Unsupported provider `{provider}`"), @@ -471,6 +475,7 @@ mod tests { #[test] fn maps_provider_to_coding_assistant_name() { assert_eq!(coding_assistant_name("claude").unwrap(), "claude_code"); + assert_eq!(coding_assistant_name("codebuddy").unwrap(), "codebuddy"); assert_eq!(coding_assistant_name("codex").unwrap(), "codex"); assert_eq!(coding_assistant_name("opencode").unwrap(), "opencode"); assert!(coding_assistant_name("unknown").is_err()); @@ -483,6 +488,9 @@ mod tests { provider_config_mut(&mut creds, "claude") .unwrap() .replace(crate::config::ProviderConfig::default()); + provider_config_mut(&mut creds, "codebuddy") + .unwrap() + .replace(crate::config::ProviderConfig::default()); provider_config_mut(&mut creds, "codex") .unwrap() .replace(crate::config::ProviderConfig::default()); @@ -491,6 +499,7 @@ mod tests { .replace(crate::config::ProviderConfig::default()); assert!(creds.claude.is_some()); + assert!(creds.codebuddy.is_some()); assert!(creds.codex.is_some()); assert!(creds.opencode.is_some()); } diff --git a/crates/cli/src/commands/launch/codebuddy.rs b/crates/cli/src/commands/launch/codebuddy.rs new file mode 100644 index 0000000..31d4da9 --- /dev/null +++ b/crates/cli/src/commands/launch/codebuddy.rs @@ -0,0 +1,47 @@ +use anyhow::Result; + +use super::util; + +#[derive(Debug, clap::Parser)] +#[command(disable_help_flag = true)] +pub struct Options { + /// Extra args passed through to the codebuddy CLI + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + pub args: Vec, +} + +/// Launch CodeBuddy routed through a local Edgee gateway. +/// +/// CodeBuddy only supports the local-gateway flow today: its traffic is +/// Anthropic Messages API, so the gateway's `/v1/messages` route handles it +/// directly. Hosted mode is not yet supported because there is no documented +/// way to inject Edgee's `x-edgee-api-key` / `x-edgee-session-id` headers +/// into CodeBuddy's outbound requests. +pub async fn run(opts: Options) -> Result<()> { + use std::net::Ipv4Addr; + + // First-run: install the persistent user-level statusline integration + // exactly once. CodeBuddy itself doesn't render an Edgee statusline today, + // but users typically also use Claude Code in the same shell — running + // the installer on the first `edgee launch` of any agent matches the + // "set it up once" flow we want. + util::ensure_first_run_installed().await; + + let log_path = crate::config::local_gateway_log_path(); + crate::local_gateway::init_file_tracing(&log_path)?; + eprintln!("edgee: gateway logs -> {}", log_path.display()); + + let gateway = crate::local_gateway::start((Ipv4Addr::LOCALHOST, 0).into()).await?; + let addr = gateway.addr; + + let mut cmd = tokio::process::Command::new(util::resolve_binary("codebuddy")); + cmd.env("CODEBUDDY_BASE_URL", format!("http://{addr}")); + cmd.args(&opts.args); + + util::run_with_gateway( + gateway, + cmd, + "CodeBuddy is not installed. Install it from https://cnb.cool/codebuddy/codebuddy-code", + ) + .await +} diff --git a/crates/cli/src/commands/launch/mod.rs b/crates/cli/src/commands/launch/mod.rs index 1b66d08..42cb491 100644 --- a/crates/cli/src/commands/launch/mod.rs +++ b/crates/cli/src/commands/launch/mod.rs @@ -1,4 +1,5 @@ pub mod claude; +pub mod codebuddy; pub mod codex; pub mod opencode; mod util; @@ -17,6 +18,9 @@ enum Command { /// Launch OpenCode routed through Edgee #[command(name = "opencode")] OpenCode(opencode::Options), + /// Launch CodeBuddy routed through Edgee + #[command(name = "codebuddy")] + CodeBuddy(codebuddy::Options), } #[derive(Debug, clap::Parser)] @@ -28,6 +32,7 @@ pub struct Options { pub async fn run(opts: Options) -> anyhow::Result<()> { match opts.command { Command::Claude(o) => claude::run(o).await, + Command::CodeBuddy(o) => codebuddy::run(o).await, Command::Codex(o) => codex::run(o).await, Command::OpenCode(o) => opencode::run(o).await, } diff --git a/crates/cli/src/commands/settings.rs b/crates/cli/src/commands/settings.rs index c5ba576..faadb92 100644 --- a/crates/cli/src/commands/settings.rs +++ b/crates/cli/src/commands/settings.rs @@ -8,7 +8,7 @@ use crate::api::{ApiClient, Compression, GatewayModel, KeySettings, ModelRoute, use crate::commands::auth::login; /// Coding agents whose keys can be configured. Order is reused for the interactive picker. -const PROVIDERS: [&str; 3] = ["claude", "codex", "opencode"]; +const PROVIDERS: [&str; 4] = ["claude", "codebuddy", "codex", "opencode"]; #[derive(Debug, clap::Parser)] pub struct Options { diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index cc22154..94f215a 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -33,6 +33,7 @@ pub struct Profile { /// When false, no MCP config or system prompt is injected into the coding assistant. pub enable_mcp: Option, pub claude: Option, + pub codebuddy: Option, pub codex: Option, pub opencode: Option, } diff --git a/crates/compression-layer/README.md b/crates/compression-layer/README.md index a1a88c9..3cca80e 100644 --- a/crates/compression-layer/README.md +++ b/crates/compression-layer/README.md @@ -7,7 +7,7 @@ Tower `Layer`/`Service` that compresses LLM tool outputs in-flight. This crate wraps any downstream Tower service and intercepts requests before they leave the gateway. It calls `edgee-compressor` to shrink tool-result payloads, then forwards the mutated request to the inner service. Only tool results are modified; all other request fields pass through unchanged. ``` -coding agent (Claude Code / Codex / OpenCode) +coding agent (Claude Code / CodeBuddy / Codex / OpenCode) | edgee-compression-layer <-- this crate CompressionLayer diff --git a/crates/gateway-core/README.md b/crates/gateway-core/README.md index c5f8be8..c94b14a 100644 --- a/crates/gateway-core/README.md +++ b/crates/gateway-core/README.md @@ -7,7 +7,7 @@ Core LLM request/response pipeline for the Edgee AI Gateway. This crate is the foundation that all other gateway crates build on. It defines the canonical request/response types (OpenAI Chat Completions format), the `Provider` trait, and the two working passthrough services for Anthropic and OpenAI. It has no hard dependency on tokio or reqwest, making it portable to any async runtime including `wasm32-wasip1`. ``` -coding agent (Claude Code / Codex / OpenCode) +coding agent (Claude Code / CodeBuddy / Codex / OpenCode) | compression-layer (edgee-compression-layer) | diff --git a/doc/architecture.md b/doc/architecture.md index cec0a4f..39f04b5 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -4,7 +4,7 @@ This document describes the design of the Edgee AI Gateway OSS codebase: the Tow ## Overview -Edgee is an LLM gateway that sits between a coding agent (Claude Code, Codex, OpenCode) and an LLM provider (Anthropic, OpenAI). Its primary function today is **tool-output compression**: before each API request is forwarded, tool results in the context window are analyzed and shrunk to reduce token count without changing the model's view of the conversation. +Edgee is an LLM gateway that sits between a coding agent (Claude Code, CodeBuddy, Codex, OpenCode) and an LLM provider (Anthropic, OpenAI). Its primary function today is **tool-output compression**: before each API request is forwarded, tool results in the context window are analyzed and shrunk to reduce token count without changing the model's view of the conversation. The gateway is built on **[Tower](https://docs.rs/tower/latest/tower/)**, a Rust middleware framework. Every processing step is a Tower [`Service`](https://docs.rs/tower/latest/tower/trait.Service.html), and processing steps are composed by stacking Tower [`Layer`](https://docs.rs/tower/latest/tower/trait.Layer.html)s. This design means the compression pipeline, the HTTP boundary, and the provider dispatch are all independently testable units that can be composed in different configurations using [`ServiceBuilder`](https://docs.rs/tower/latest/tower/builder/struct.ServiceBuilder.html).