fix: post-plugins-merge cleanroom fixes and workspace deps

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-04-01 18:48:39 +09:00
parent bb4d2f364a
commit 409073c10c
15 changed files with 378 additions and 125 deletions

1
rust/Cargo.lock generated
View File

@@ -113,6 +113,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"plugins", "plugins",
"runtime", "runtime",
"serde_json",
] ]
[[package]] [[package]]

View File

@@ -8,6 +8,9 @@ edition = "2021"
license = "MIT" license = "MIT"
publish = false publish = false
[workspace.dependencies]
serde_json = "1"
[workspace.lints.rust] [workspace.lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"

View File

@@ -101,7 +101,7 @@ impl From<OAuthTokenSet> for AuthSource {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AnthropicClient { pub struct ApiHttpClient {
http: reqwest::Client, http: reqwest::Client,
auth: AuthSource, auth: AuthSource,
base_url: String, base_url: String,
@@ -110,7 +110,7 @@ pub struct AnthropicClient {
max_backoff: Duration, max_backoff: Duration,
} }
impl AnthropicClient { impl ApiHttpClient {
#[must_use] #[must_use]
pub fn new(api_key: impl Into<String>) -> Self { pub fn new(api_key: impl Into<String>) -> Self {
Self { Self {
@@ -429,7 +429,7 @@ fn resolve_saved_oauth_token_set(
let Some(refresh_token) = token_set.refresh_token.clone() else { let Some(refresh_token) = token_set.refresh_token.clone() else {
return Err(ApiError::ExpiredOAuthToken); return Err(ApiError::ExpiredOAuthToken);
}; };
let client = AnthropicClient::from_auth(AuthSource::None).with_base_url(read_base_url()); let client = ApiHttpClient::from_auth(AuthSource::None).with_base_url(read_base_url());
let refreshed = client_runtime_block_on(async { let refreshed = client_runtime_block_on(async {
client client
.refresh_oauth_token( .refresh_oauth_token(
@@ -614,7 +614,7 @@ mod tests {
use crate::client::{ use crate::client::{
now_unix_timestamp, oauth_token_is_expired, resolve_saved_oauth_token, now_unix_timestamp, oauth_token_is_expired, resolve_saved_oauth_token,
resolve_startup_auth_source, AnthropicClient, AuthSource, OAuthTokenSet, resolve_startup_auth_source, ApiHttpClient, AuthSource, OAuthTokenSet,
}; };
use crate::types::{ContentBlockDelta, MessageRequest}; use crate::types::{ContentBlockDelta, MessageRequest};
@@ -671,7 +671,7 @@ mod tests {
let _guard = env_lock(); let _guard = env_lock();
std::env::remove_var("ANTHROPIC_AUTH_TOKEN"); std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
std::env::remove_var("ANTHROPIC_API_KEY"); std::env::remove_var("ANTHROPIC_API_KEY");
std::env::remove_var("CLAUDE_CONFIG_HOME"); std::env::remove_var("CLAW_CONFIG_HOME");
let error = super::read_api_key().expect_err("missing key should error"); let error = super::read_api_key().expect_err("missing key should error");
assert!(matches!(error, crate::error::ApiError::MissingApiKey)); assert!(matches!(error, crate::error::ApiError::MissingApiKey));
} }
@@ -735,7 +735,7 @@ mod tests {
fn auth_source_from_saved_oauth_when_env_absent() { fn auth_source_from_saved_oauth_when_env_absent() {
let _guard = env_lock(); let _guard = env_lock();
let config_home = temp_config_home(); let config_home = temp_config_home();
std::env::set_var("CLAUDE_CONFIG_HOME", &config_home); std::env::set_var("CLAW_CONFIG_HOME", &config_home);
std::env::remove_var("ANTHROPIC_AUTH_TOKEN"); std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
std::env::remove_var("ANTHROPIC_API_KEY"); std::env::remove_var("ANTHROPIC_API_KEY");
save_oauth_credentials(&runtime::OAuthTokenSet { save_oauth_credentials(&runtime::OAuthTokenSet {
@@ -750,7 +750,7 @@ mod tests {
assert_eq!(auth.bearer_token(), Some("saved-access-token")); assert_eq!(auth.bearer_token(), Some("saved-access-token"));
clear_oauth_credentials().expect("clear credentials"); clear_oauth_credentials().expect("clear credentials");
std::env::remove_var("CLAUDE_CONFIG_HOME"); std::env::remove_var("CLAW_CONFIG_HOME");
std::fs::remove_dir_all(config_home).expect("cleanup temp dir"); std::fs::remove_dir_all(config_home).expect("cleanup temp dir");
} }
@@ -774,7 +774,7 @@ mod tests {
fn resolve_saved_oauth_token_refreshes_expired_credentials() { fn resolve_saved_oauth_token_refreshes_expired_credentials() {
let _guard = env_lock(); let _guard = env_lock();
let config_home = temp_config_home(); let config_home = temp_config_home();
std::env::set_var("CLAUDE_CONFIG_HOME", &config_home); std::env::set_var("CLAW_CONFIG_HOME", &config_home);
std::env::remove_var("ANTHROPIC_AUTH_TOKEN"); std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
std::env::remove_var("ANTHROPIC_API_KEY"); std::env::remove_var("ANTHROPIC_API_KEY");
save_oauth_credentials(&runtime::OAuthTokenSet { save_oauth_credentials(&runtime::OAuthTokenSet {
@@ -798,7 +798,7 @@ mod tests {
assert_eq!(stored.access_token, "refreshed-token"); assert_eq!(stored.access_token, "refreshed-token");
clear_oauth_credentials().expect("clear credentials"); clear_oauth_credentials().expect("clear credentials");
std::env::remove_var("CLAUDE_CONFIG_HOME"); std::env::remove_var("CLAW_CONFIG_HOME");
std::fs::remove_dir_all(config_home).expect("cleanup temp dir"); std::fs::remove_dir_all(config_home).expect("cleanup temp dir");
} }
@@ -806,7 +806,7 @@ mod tests {
fn resolve_startup_auth_source_uses_saved_oauth_without_loading_config() { fn resolve_startup_auth_source_uses_saved_oauth_without_loading_config() {
let _guard = env_lock(); let _guard = env_lock();
let config_home = temp_config_home(); let config_home = temp_config_home();
std::env::set_var("CLAUDE_CONFIG_HOME", &config_home); std::env::set_var("CLAW_CONFIG_HOME", &config_home);
std::env::remove_var("ANTHROPIC_AUTH_TOKEN"); std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
std::env::remove_var("ANTHROPIC_API_KEY"); std::env::remove_var("ANTHROPIC_API_KEY");
save_oauth_credentials(&runtime::OAuthTokenSet { save_oauth_credentials(&runtime::OAuthTokenSet {
@@ -822,7 +822,7 @@ mod tests {
assert_eq!(auth.bearer_token(), Some("saved-access-token")); assert_eq!(auth.bearer_token(), Some("saved-access-token"));
clear_oauth_credentials().expect("clear credentials"); clear_oauth_credentials().expect("clear credentials");
std::env::remove_var("CLAUDE_CONFIG_HOME"); std::env::remove_var("CLAW_CONFIG_HOME");
std::fs::remove_dir_all(config_home).expect("cleanup temp dir"); std::fs::remove_dir_all(config_home).expect("cleanup temp dir");
} }
@@ -830,7 +830,7 @@ mod tests {
fn resolve_startup_auth_source_errors_when_refreshable_token_lacks_config() { fn resolve_startup_auth_source_errors_when_refreshable_token_lacks_config() {
let _guard = env_lock(); let _guard = env_lock();
let config_home = temp_config_home(); let config_home = temp_config_home();
std::env::set_var("CLAUDE_CONFIG_HOME", &config_home); std::env::set_var("CLAW_CONFIG_HOME", &config_home);
std::env::remove_var("ANTHROPIC_AUTH_TOKEN"); std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
std::env::remove_var("ANTHROPIC_API_KEY"); std::env::remove_var("ANTHROPIC_API_KEY");
save_oauth_credentials(&runtime::OAuthTokenSet { save_oauth_credentials(&runtime::OAuthTokenSet {
@@ -854,7 +854,7 @@ mod tests {
assert_eq!(stored.refresh_token.as_deref(), Some("refresh-token")); assert_eq!(stored.refresh_token.as_deref(), Some("refresh-token"));
clear_oauth_credentials().expect("clear credentials"); clear_oauth_credentials().expect("clear credentials");
std::env::remove_var("CLAUDE_CONFIG_HOME"); std::env::remove_var("CLAW_CONFIG_HOME");
std::fs::remove_dir_all(config_home).expect("cleanup temp dir"); std::fs::remove_dir_all(config_home).expect("cleanup temp dir");
} }
@@ -862,7 +862,7 @@ mod tests {
fn resolve_saved_oauth_token_preserves_refresh_token_when_refresh_response_omits_it() { fn resolve_saved_oauth_token_preserves_refresh_token_when_refresh_response_omits_it() {
let _guard = env_lock(); let _guard = env_lock();
let config_home = temp_config_home(); let config_home = temp_config_home();
std::env::set_var("CLAUDE_CONFIG_HOME", &config_home); std::env::set_var("CLAW_CONFIG_HOME", &config_home);
std::env::remove_var("ANTHROPIC_AUTH_TOKEN"); std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
std::env::remove_var("ANTHROPIC_API_KEY"); std::env::remove_var("ANTHROPIC_API_KEY");
save_oauth_credentials(&runtime::OAuthTokenSet { save_oauth_credentials(&runtime::OAuthTokenSet {
@@ -887,7 +887,7 @@ mod tests {
assert_eq!(stored.refresh_token.as_deref(), Some("refresh-token")); assert_eq!(stored.refresh_token.as_deref(), Some("refresh-token"));
clear_oauth_credentials().expect("clear credentials"); clear_oauth_credentials().expect("clear credentials");
std::env::remove_var("CLAUDE_CONFIG_HOME"); std::env::remove_var("CLAW_CONFIG_HOME");
std::fs::remove_dir_all(config_home).expect("cleanup temp dir"); std::fs::remove_dir_all(config_home).expect("cleanup temp dir");
} }
@@ -908,7 +908,7 @@ mod tests {
#[test] #[test]
fn backoff_doubles_until_maximum() { fn backoff_doubles_until_maximum() {
let client = AnthropicClient::new("test-key").with_retry_policy( let client = ApiHttpClient::new("test-key").with_retry_policy(
3, 3,
Duration::from_millis(10), Duration::from_millis(10),
Duration::from_millis(25), Duration::from_millis(25),

View File

@@ -5,7 +5,7 @@ mod types;
pub use client::{ pub use client::{
oauth_token_is_expired, read_base_url, resolve_saved_oauth_token, resolve_startup_auth_source, oauth_token_is_expired, read_base_url, resolve_saved_oauth_token, resolve_startup_auth_source,
AnthropicClient, AuthSource, MessageStream, OAuthTokenSet, ApiHttpClient, AuthSource, MessageStream, OAuthTokenSet,
}; };
pub use error::ApiError; pub use error::ApiError;
pub use sse::{parse_frame, SseParser}; pub use sse::{parse_frame, SseParser};

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use api::{ use api::{
AnthropicClient, ApiError, ContentBlockDelta, ContentBlockDeltaEvent, ContentBlockStartEvent, ApiHttpClient, ApiError, ContentBlockDelta, ContentBlockDeltaEvent, ContentBlockStartEvent,
InputContentBlock, InputMessage, MessageDeltaEvent, MessageRequest, OutputContentBlock, InputContentBlock, InputMessage, MessageDeltaEvent, MessageRequest, OutputContentBlock,
StreamEvent, ToolChoice, ToolDefinition, StreamEvent, ToolChoice, ToolDefinition,
}; };
@@ -34,7 +34,7 @@ async fn send_message_posts_json_and_parses_response() {
) )
.await; .await;
let client = AnthropicClient::new("test-key") let client = ApiHttpClient::new("test-key")
.with_auth_token(Some("proxy-token".to_string())) .with_auth_token(Some("proxy-token".to_string()))
.with_base_url(server.base_url()); .with_base_url(server.base_url());
let response = client let response = client
@@ -99,7 +99,7 @@ async fn send_message_parses_response_with_thinking_blocks() {
) )
.await; .await;
let client = AnthropicClient::new("test-key").with_base_url(server.base_url()); let client = ApiHttpClient::new("test-key").with_base_url(server.base_url());
let response = client let response = client
.send_message(&sample_request(false)) .send_message(&sample_request(false))
.await .await
@@ -146,7 +146,7 @@ async fn stream_message_parses_sse_events_with_tool_use() {
) )
.await; .await;
let client = AnthropicClient::new("test-key") let client = ApiHttpClient::new("test-key")
.with_auth_token(Some("proxy-token".to_string())) .with_auth_token(Some("proxy-token".to_string()))
.with_base_url(server.base_url()); .with_base_url(server.base_url());
let mut stream = client let mut stream = client
@@ -234,7 +234,7 @@ async fn stream_message_parses_sse_events_with_thinking_blocks() {
) )
.await; .await;
let client = AnthropicClient::new("test-key").with_base_url(server.base_url()); let client = ApiHttpClient::new("test-key").with_base_url(server.base_url());
let mut stream = client let mut stream = client
.stream_message(&sample_request(false)) .stream_message(&sample_request(false))
.await .await
@@ -303,7 +303,7 @@ async fn retries_retryable_failures_before_succeeding() {
) )
.await; .await;
let client = AnthropicClient::new("test-key") let client = ApiHttpClient::new("test-key")
.with_base_url(server.base_url()) .with_base_url(server.base_url())
.with_retry_policy(2, Duration::from_millis(1), Duration::from_millis(2)); .with_retry_policy(2, Duration::from_millis(1), Duration::from_millis(2));
@@ -336,7 +336,7 @@ async fn surfaces_retry_exhaustion_for_persistent_retryable_errors() {
) )
.await; .await;
let client = AnthropicClient::new("test-key") let client = ApiHttpClient::new("test-key")
.with_base_url(server.base_url()) .with_base_url(server.base_url())
.with_retry_policy(1, Duration::from_millis(1), Duration::from_millis(2)); .with_retry_policy(1, Duration::from_millis(1), Duration::from_millis(2));
@@ -367,7 +367,7 @@ async fn surfaces_retry_exhaustion_for_persistent_retryable_errors() {
#[tokio::test] #[tokio::test]
#[ignore = "requires ANTHROPIC_API_KEY and network access"] #[ignore = "requires ANTHROPIC_API_KEY and network access"]
async fn live_stream_smoke_test() { async fn live_stream_smoke_test() {
let client = AnthropicClient::from_env().expect("ANTHROPIC_API_KEY must be set"); let client = ApiHttpClient::from_env().expect("ANTHROPIC_API_KEY must be set");
let mut stream = client let mut stream = client
.stream_message(&MessageRequest { .stream_message(&MessageRequest {
model: std::env::var("ANTHROPIC_MODEL") model: std::env::var("ANTHROPIC_MODEL")

View File

@@ -11,3 +11,4 @@ workspace = true
[dependencies] [dependencies]
plugins = { path = "../plugins" } plugins = { path = "../plugins" }
runtime = { path = "../runtime" } runtime = { path = "../runtime" }
serde_json = "1"

View File

@@ -48,138 +48,161 @@ pub struct SlashCommandSpec {
const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
SlashCommandSpec { SlashCommandSpec {
name: "help", name: "help",
aliases: &[],
summary: "Show available slash commands", summary: "Show available slash commands",
argument_hint: None, argument_hint: None,
resume_supported: true, resume_supported: true,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "status", name: "status",
aliases: &[],
summary: "Show current session status", summary: "Show current session status",
argument_hint: None, argument_hint: None,
resume_supported: true, resume_supported: true,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "compact", name: "compact",
aliases: &[],
summary: "Compact local session history", summary: "Compact local session history",
argument_hint: None, argument_hint: None,
resume_supported: true, resume_supported: true,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "model", name: "model",
aliases: &[],
summary: "Show or switch the active model", summary: "Show or switch the active model",
argument_hint: Some("[model]"), argument_hint: Some("[model]"),
resume_supported: false, resume_supported: false,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "permissions", name: "permissions",
aliases: &[],
summary: "Show or switch the active permission mode", summary: "Show or switch the active permission mode",
argument_hint: Some("[read-only|workspace-write|danger-full-access]"), argument_hint: Some("[read-only|workspace-write|danger-full-access]"),
resume_supported: false, resume_supported: false,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "clear", name: "clear",
aliases: &[],
summary: "Start a fresh local session", summary: "Start a fresh local session",
argument_hint: Some("[--confirm]"), argument_hint: Some("[--confirm]"),
resume_supported: true, resume_supported: true,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "cost", name: "cost",
aliases: &[],
summary: "Show cumulative token usage for this session", summary: "Show cumulative token usage for this session",
argument_hint: None, argument_hint: None,
resume_supported: true, resume_supported: true,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "resume", name: "resume",
aliases: &[],
summary: "Load a saved session into the REPL", summary: "Load a saved session into the REPL",
argument_hint: Some("<session-path>"), argument_hint: Some("<session-path>"),
resume_supported: false, resume_supported: false,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "config", name: "config",
aliases: &[],
summary: "Inspect Claude config files or merged sections", summary: "Inspect Claude config files or merged sections",
argument_hint: Some("[env|hooks|model|plugins]"), argument_hint: Some("[env|hooks|model|plugins]"),
resume_supported: true, resume_supported: true,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "memory", name: "memory",
aliases: &[],
summary: "Inspect loaded Claude instruction memory files", summary: "Inspect loaded Claude instruction memory files",
argument_hint: None, argument_hint: None,
resume_supported: true, resume_supported: true,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "init", name: "init",
aliases: &[],
summary: "Create a starter CLAUDE.md for this repo", summary: "Create a starter CLAUDE.md for this repo",
argument_hint: None, argument_hint: None,
resume_supported: true, resume_supported: true,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "diff", name: "diff",
aliases: &[],
summary: "Show git diff for current workspace changes", summary: "Show git diff for current workspace changes",
argument_hint: None, argument_hint: None,
resume_supported: true, resume_supported: true,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "version", name: "version",
aliases: &[],
summary: "Show CLI version and build information", summary: "Show CLI version and build information",
argument_hint: None, argument_hint: None,
resume_supported: true, resume_supported: true,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "bughunter", name: "bughunter",
aliases: &[],
summary: "Inspect the codebase for likely bugs", summary: "Inspect the codebase for likely bugs",
argument_hint: Some("[scope]"), argument_hint: Some("[scope]"),
resume_supported: false, resume_supported: false,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "commit", name: "commit",
aliases: &[],
summary: "Generate a commit message and create a git commit", summary: "Generate a commit message and create a git commit",
argument_hint: None, argument_hint: None,
resume_supported: false, resume_supported: false,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "pr", name: "pr",
aliases: &[],
summary: "Draft or create a pull request from the conversation", summary: "Draft or create a pull request from the conversation",
argument_hint: Some("[context]"), argument_hint: Some("[context]"),
resume_supported: false, resume_supported: false,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "issue", name: "issue",
aliases: &[],
summary: "Draft or create a GitHub issue from the conversation", summary: "Draft or create a GitHub issue from the conversation",
argument_hint: Some("[context]"), argument_hint: Some("[context]"),
resume_supported: false, resume_supported: false,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "ultraplan", name: "ultraplan",
aliases: &[],
summary: "Run a deep planning prompt with multi-step reasoning", summary: "Run a deep planning prompt with multi-step reasoning",
argument_hint: Some("[task]"), argument_hint: Some("[task]"),
resume_supported: false, resume_supported: false,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "teleport", name: "teleport",
aliases: &[],
summary: "Jump to a file or symbol by searching the workspace", summary: "Jump to a file or symbol by searching the workspace",
argument_hint: Some("<symbol-or-path>"), argument_hint: Some("<symbol-or-path>"),
resume_supported: false, resume_supported: false,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "debug-tool-call", name: "debug-tool-call",
aliases: &[],
summary: "Replay the last tool call with debug details", summary: "Replay the last tool call with debug details",
argument_hint: None, argument_hint: None,
resume_supported: false, resume_supported: false,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "export", name: "export",
aliases: &[],
summary: "Export the current conversation to a file", summary: "Export the current conversation to a file",
argument_hint: Some("[file]"), argument_hint: Some("[file]"),
resume_supported: true, resume_supported: true,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "session", name: "session",
aliases: &[],
summary: "List or switch managed local sessions", summary: "List or switch managed local sessions",
argument_hint: Some("[list|switch <session-id>]"), argument_hint: Some("[list|switch <session-id>]"),
resume_supported: false, resume_supported: false,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "plugins", name: "plugins",
aliases: &[],
summary: "List or manage plugins", summary: "List or manage plugins",
argument_hint: Some( argument_hint: Some(
"[list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]", "[list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]",
@@ -584,6 +607,210 @@ pub fn handle_slash_command(
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DefinitionSource {
ProjectCodex,
UserCodex,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentDef {
pub name: String,
pub description: String,
pub model: String,
pub temperature: String,
pub source: DefinitionSource,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkillDef {
pub name: String,
pub description: String,
pub source: DefinitionSource,
}
pub fn load_agents_from_roots(
roots: &[(DefinitionSource, std::path::PathBuf)],
) -> Result<Vec<AgentDef>, String> {
let mut agents = Vec::new();
let mut seen: std::collections::HashMap<String, DefinitionSource> =
std::collections::HashMap::new();
for (source, path) in roots {
if !path.exists() {
continue;
}
for entry in std::fs::read_dir(path).map_err(|e| e.to_string())? {
let entry = entry.map_err(|e| e.to_string())?;
let file_path = entry.path();
if file_path.extension().map_or(false, |ext| ext == "json") {
let name = file_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();
let content = std::fs::read_to_string(&file_path).map_err(|e| e.to_string())?;
let json: serde_json::Value =
serde_json::from_str(&content).map_err(|e| e.to_string())?;
let description = json
.get("description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let model = json
.get("model")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
let temperature = json
.get("temperature")
.and_then(|v| v.as_str())
.unwrap_or("medium")
.to_string();
let shadowed_by = seen.get(&name).copied();
seen.insert(name.clone(), *source);
let final_name = if let Some(shadow_source) = shadowed_by {
let source_name = match shadow_source {
DefinitionSource::ProjectCodex => "Project (.codex)",
DefinitionSource::UserCodex => "User (~/.codex)",
};
format!("(shadowed by {}) {}", source_name, name)
} else {
name
};
agents.push(AgentDef {
name: final_name,
description,
model,
temperature,
source: *source,
});
}
}
}
Ok(agents)
}
pub fn load_skills_from_roots(
roots: &[(DefinitionSource, std::path::PathBuf)],
) -> Result<Vec<SkillDef>, String> {
let mut skills = Vec::new();
let mut seen: std::collections::HashMap<String, DefinitionSource> =
std::collections::HashMap::new();
for (source, path) in roots {
if !path.exists() {
continue;
}
for entry in std::fs::read_dir(path).map_err(|e| e.to_string())? {
let entry = entry.map_err(|e| e.to_string())?;
let file_path = entry.path();
if file_path.extension().map_or(false, |ext| ext == "md") {
let name = file_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();
let content = std::fs::read_to_string(&file_path).map_err(|e| e.to_string())?;
let description = content.lines().next().unwrap_or("").to_string();
let shadowed_by = seen.get(&name).copied();
seen.insert(name.clone(), *source);
let final_name = if let Some(shadow_source) = shadowed_by {
let source_name = match shadow_source {
DefinitionSource::ProjectCodex => "Project (.codex)",
DefinitionSource::UserCodex => "User (~/.codex)",
};
format!("(shadowed by {}) {}", source_name, name)
} else {
name
};
skills.push(SkillDef {
name: final_name,
description,
source: *source,
});
}
}
}
Ok(skills)
}
pub fn render_agents_report(agents: &[AgentDef]) -> String {
let mut lines = vec!["Agents".to_string()];
let project_agents: Vec<_> = agents
.iter()
.filter(|a| matches!(a.source, DefinitionSource::ProjectCodex))
.collect();
let user_agents: Vec<_> = agents
.iter()
.filter(|a| matches!(a.source, DefinitionSource::UserCodex))
.collect();
let unique_count = agents
.iter()
.filter(|a| !a.name.starts_with("(shadowed"))
.count();
lines.push(format!("{} active agents", unique_count));
if !project_agents.is_empty() {
lines.push("Project (.codex):".to_string());
for agent in project_agents {
lines.push(format!(
"{} · {} · {} · {}",
agent.name, agent.description, agent.model, agent.temperature
));
}
}
if !user_agents.is_empty() {
lines.push("User (~/.codex):".to_string());
for agent in user_agents {
lines.push(format!(
"{} · {} · {} · {}",
agent.name, agent.description, agent.model, agent.temperature
));
}
}
lines.join("\n")
}
pub fn render_skills_report(skills: &[SkillDef]) -> String {
let mut lines = vec!["Skills".to_string()];
let project_skills: Vec<_> = skills
.iter()
.filter(|s| matches!(s.source, DefinitionSource::ProjectCodex))
.collect();
let user_skills: Vec<_> = skills
.iter()
.filter(|s| matches!(s.source, DefinitionSource::UserCodex))
.collect();
let unique_count = skills
.iter()
.filter(|s| !s.name.starts_with("(shadowed"))
.count();
lines.push(format!("{} available skills", unique_count));
if !project_skills.is_empty() {
lines.push("Project (.codex):".to_string());
for skill in project_skills {
lines.push(format!("{} · {}", skill.name, skill.description));
}
}
if !user_skills.is_empty() {
lines.push("User (~/.codex):".to_string());
for skill in user_skills {
lines.push(format!("{} · {}", skill.name, skill.description));
}
}
lines.join("\n")
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use super::{
@@ -622,13 +849,27 @@ mod tests {
fs::write( fs::write(
root.join(".claude-plugin").join("plugin.json"), root.join(".claude-plugin").join("plugin.json"),
format!( format!(
"{{\n \"name\": \"{name}\",\n \"version\": \"{version}\",\n \"description\": \"bundled commands plugin\",\n \"defaultEnabled\": {}\n}}", "{{\n \"name\": \"{name}\",\n \"version\": \"{version}\",\n \"description\": \"bundled commands plugin\",\n \"defaultEnabled\": {}}}",
if default_enabled { "true" } else { "false" } if default_enabled { "true" } else { "false" }
), ),
) )
.expect("write bundled manifest"); .expect("write bundled manifest");
} }
fn write_agent(dir: &Path, name: &str, description: &str, model: &str, temperature: &str) {
fs::create_dir_all(dir).expect("agent dir");
let json = format!(
"{{\n \"name\": \"{name}\",\n \"description\": \"{description}\",\n \"model\": \"{model}\",\n \"temperature\": \"{temperature}\"\n}}"
);
fs::write(dir.join(format!("{name}.json")), json).expect("write agent");
}
fn write_skill(dir: &Path, name: &str, description: &str) {
fs::create_dir_all(dir).expect("skill dir");
let content = format!("{description}\n\nSkill content here.\n");
fs::write(dir.join(format!("{name}.md")), content).expect("write skill");
}
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
#[test] #[test]
fn parses_supported_slash_commands() { fn parses_supported_slash_commands() {
@@ -961,9 +1202,8 @@ mod tests {
(DefinitionSource::ProjectCodex, project_agents), (DefinitionSource::ProjectCodex, project_agents),
(DefinitionSource::UserCodex, user_agents), (DefinitionSource::UserCodex, user_agents),
]; ];
let report = render_agents_report( let report =
&load_agents_from_roots(&roots).expect("agent roots should load"), render_agents_report(&load_agents_from_roots(&roots).expect("agent roots should load"));
);
assert!(report.contains("Agents")); assert!(report.contains("Agents"));
assert!(report.contains("2 active agents")); assert!(report.contains("2 active agents"));
@@ -992,9 +1232,8 @@ mod tests {
(DefinitionSource::ProjectCodex, project_skills), (DefinitionSource::ProjectCodex, project_skills),
(DefinitionSource::UserCodex, user_skills), (DefinitionSource::UserCodex, user_skills),
]; ];
let report = render_skills_report( let report =
&load_skills_from_roots(&roots).expect("skill roots should load"), render_skills_report(&load_skills_from_roots(&roots).expect("skill roots should load"));
);
assert!(report.contains("Skills")); assert!(report.contains("Skills"));
assert!(report.contains("2 available skills")); assert!(report.contains("2 available skills"));

View File

@@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
use crate::json::JsonValue; use crate::json::JsonValue;
use crate::sandbox::{FilesystemIsolationMode, SandboxConfig}; use crate::sandbox::{FilesystemIsolationMode, SandboxConfig};
pub const CLAUDE_CODE_SETTINGS_SCHEMA_NAME: &str = "SettingsSchema"; pub const CLAW_SETTINGS_SCHEMA_NAME: &str = "SettingsSchema";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ConfigSource { pub enum ConfigSource {
@@ -79,7 +79,7 @@ pub enum McpTransport {
Http, Http,
Ws, Ws,
Sdk, Sdk,
ClaudeAiProxy, ManagedProxy,
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -89,7 +89,7 @@ pub enum McpServerConfig {
Http(McpRemoteServerConfig), Http(McpRemoteServerConfig),
Ws(McpWebSocketServerConfig), Ws(McpWebSocketServerConfig),
Sdk(McpSdkServerConfig), Sdk(McpSdkServerConfig),
ClaudeAiProxy(McpClaudeAiProxyServerConfig), ManagedProxy(McpManagedProxyServerConfig),
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -120,7 +120,7 @@ pub struct McpSdkServerConfig {
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct McpClaudeAiProxyServerConfig { pub struct McpManagedProxyServerConfig {
pub url: String, pub url: String,
pub id: String, pub id: String,
} }
@@ -196,8 +196,8 @@ impl ConfigLoader {
#[must_use] #[must_use]
pub fn discover(&self) -> Vec<ConfigEntry> { pub fn discover(&self) -> Vec<ConfigEntry> {
let user_legacy_path = self.config_home.parent().map_or_else( let user_legacy_path = self.config_home.parent().map_or_else(
|| PathBuf::from(".claude.json"), || PathBuf::from(".claw.json"),
|parent| parent.join(".claude.json"), |parent| parent.join(".claw.json"),
); );
vec![ vec![
ConfigEntry { ConfigEntry {
@@ -210,15 +210,15 @@ impl ConfigLoader {
}, },
ConfigEntry { ConfigEntry {
source: ConfigSource::Project, source: ConfigSource::Project,
path: self.cwd.join(".claude.json"), path: self.cwd.join(".claw.json"),
}, },
ConfigEntry { ConfigEntry {
source: ConfigSource::Project, source: ConfigSource::Project,
path: self.cwd.join(".claude").join("settings.json"), path: self.cwd.join(".claw").join("settings.json"),
}, },
ConfigEntry { ConfigEntry {
source: ConfigSource::Local, source: ConfigSource::Local,
path: self.cwd.join(".claude").join("settings.local.json"), path: self.cwd.join(".claw").join("settings.local.json"),
}, },
] ]
} }
@@ -420,10 +420,10 @@ impl RuntimePluginConfig {
#[must_use] #[must_use]
pub fn default_config_home() -> PathBuf { pub fn default_config_home() -> PathBuf {
std::env::var_os("CLAUDE_CONFIG_HOME") std::env::var_os("CLAW_CONFIG_HOME")
.map(PathBuf::from) .map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".claude"))) .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".claw")))
.unwrap_or_else(|| PathBuf::from(".claude")) .unwrap_or_else(|| PathBuf::from(".claw"))
} }
impl RuntimeHookConfig { impl RuntimeHookConfig {
@@ -486,7 +486,7 @@ impl McpServerConfig {
Self::Http(_) => McpTransport::Http, Self::Http(_) => McpTransport::Http,
Self::Ws(_) => McpTransport::Ws, Self::Ws(_) => McpTransport::Ws,
Self::Sdk(_) => McpTransport::Sdk, Self::Sdk(_) => McpTransport::Sdk,
Self::ClaudeAiProxy(_) => McpTransport::ClaudeAiProxy, Self::ManagedProxy(_) => McpTransport::ManagedProxy,
} }
} }
} }
@@ -494,7 +494,7 @@ impl McpServerConfig {
fn read_optional_json_object( fn read_optional_json_object(
path: &Path, path: &Path,
) -> Result<Option<BTreeMap<String, JsonValue>>, ConfigError> { ) -> Result<Option<BTreeMap<String, JsonValue>>, ConfigError> {
let is_legacy_config = path.file_name().and_then(|name| name.to_str()) == Some(".claude.json"); let is_legacy_config = path.file_name().and_then(|name| name.to_str()) == Some(".claw.json");
let contents = match fs::read_to_string(path) { let contents = match fs::read_to_string(path) {
Ok(contents) => contents, Ok(contents) => contents,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None), Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
@@ -724,12 +724,10 @@ fn parse_mcp_server_config(
"sdk" => Ok(McpServerConfig::Sdk(McpSdkServerConfig { "sdk" => Ok(McpServerConfig::Sdk(McpSdkServerConfig {
name: expect_string(object, "name", context)?.to_string(), name: expect_string(object, "name", context)?.to_string(),
})), })),
"claudeai-proxy" => Ok(McpServerConfig::ClaudeAiProxy( "claudeai-proxy" => Ok(McpServerConfig::ManagedProxy(McpManagedProxyServerConfig {
McpClaudeAiProxyServerConfig { url: expect_string(object, "url", context)?.to_string(),
url: expect_string(object, "url", context)?.to_string(), id: expect_string(object, "id", context)?.to_string(),
id: expect_string(object, "id", context)?.to_string(), })),
},
)),
other => Err(ConfigError::Parse(format!( other => Err(ConfigError::Parse(format!(
"{context}: unsupported MCP server type for {server_name}: {other}" "{context}: unsupported MCP server type for {server_name}: {other}"
))), ))),
@@ -942,7 +940,7 @@ fn push_unique(target: &mut Vec<String>, value: String) {
mod tests { mod tests {
use super::{ use super::{
ConfigLoader, ConfigSource, McpServerConfig, McpTransport, ResolvedPermissionMode, ConfigLoader, ConfigSource, McpServerConfig, McpTransport, ResolvedPermissionMode,
CLAUDE_CODE_SETTINGS_SCHEMA_NAME, CLAW_SETTINGS_SCHEMA_NAME,
}; };
use crate::json::JsonValue; use crate::json::JsonValue;
use crate::sandbox::FilesystemIsolationMode; use crate::sandbox::FilesystemIsolationMode;
@@ -961,7 +959,7 @@ mod tests {
fn rejects_non_object_settings_files() { fn rejects_non_object_settings_files() {
let root = temp_dir(); let root = temp_dir();
let cwd = root.join("project"); let cwd = root.join("project");
let home = root.join("home").join(".claude"); let home = root.join("home").join(".claw");
fs::create_dir_all(&home).expect("home config dir"); fs::create_dir_all(&home).expect("home config dir");
fs::create_dir_all(&cwd).expect("project dir"); fs::create_dir_all(&cwd).expect("project dir");
fs::write(home.join("settings.json"), "[]").expect("write bad settings"); fs::write(home.join("settings.json"), "[]").expect("write bad settings");
@@ -980,12 +978,12 @@ mod tests {
fn loads_and_merges_claude_code_config_files_by_precedence() { fn loads_and_merges_claude_code_config_files_by_precedence() {
let root = temp_dir(); let root = temp_dir();
let cwd = root.join("project"); let cwd = root.join("project");
let home = root.join("home").join(".claude"); let home = root.join("home").join(".claw");
fs::create_dir_all(cwd.join(".claude")).expect("project config dir"); fs::create_dir_all(cwd.join(".claw")).expect("project config dir");
fs::create_dir_all(&home).expect("home config dir"); fs::create_dir_all(&home).expect("home config dir");
fs::write( fs::write(
home.parent().expect("home parent").join(".claude.json"), home.parent().expect("home parent").join(".claw.json"),
r#"{"model":"haiku","env":{"A":"1"},"mcpServers":{"home":{"command":"uvx","args":["home"]}}}"#, r#"{"model":"haiku","env":{"A":"1"},"mcpServers":{"home":{"command":"uvx","args":["home"]}}}"#,
) )
.expect("write user compat config"); .expect("write user compat config");
@@ -995,17 +993,17 @@ mod tests {
) )
.expect("write user settings"); .expect("write user settings");
fs::write( fs::write(
cwd.join(".claude.json"), cwd.join(".claw.json"),
r#"{"model":"project-compat","env":{"B":"2"}}"#, r#"{"model":"project-compat","env":{"B":"2"}}"#,
) )
.expect("write project compat config"); .expect("write project compat config");
fs::write( fs::write(
cwd.join(".claude").join("settings.json"), cwd.join(".claw").join("settings.json"),
r#"{"env":{"C":"3"},"hooks":{"PostToolUse":["project"]},"mcpServers":{"project":{"command":"uvx","args":["project"]}}}"#, r#"{"env":{"C":"3"},"hooks":{"PostToolUse":["project"]},"mcpServers":{"project":{"command":"uvx","args":["project"]}}}"#,
) )
.expect("write project settings"); .expect("write project settings");
fs::write( fs::write(
cwd.join(".claude").join("settings.local.json"), cwd.join(".claw").join("settings.local.json"),
r#"{"model":"opus","permissionMode":"acceptEdits"}"#, r#"{"model":"opus","permissionMode":"acceptEdits"}"#,
) )
.expect("write local settings"); .expect("write local settings");
@@ -1014,7 +1012,7 @@ mod tests {
.load() .load()
.expect("config should load"); .expect("config should load");
assert_eq!(CLAUDE_CODE_SETTINGS_SCHEMA_NAME, "SettingsSchema"); assert_eq!(CLAW_SETTINGS_SCHEMA_NAME, "SettingsSchema");
assert_eq!(loaded.loaded_entries().len(), 5); assert_eq!(loaded.loaded_entries().len(), 5);
assert_eq!(loaded.loaded_entries()[0].source, ConfigSource::User); assert_eq!(loaded.loaded_entries()[0].source, ConfigSource::User);
assert_eq!( assert_eq!(
@@ -1056,12 +1054,12 @@ mod tests {
fn parses_sandbox_config() { fn parses_sandbox_config() {
let root = temp_dir(); let root = temp_dir();
let cwd = root.join("project"); let cwd = root.join("project");
let home = root.join("home").join(".claude"); let home = root.join("home").join(".claw");
fs::create_dir_all(cwd.join(".claude")).expect("project config dir"); fs::create_dir_all(cwd.join(".claw")).expect("project config dir");
fs::create_dir_all(&home).expect("home config dir"); fs::create_dir_all(&home).expect("home config dir");
fs::write( fs::write(
cwd.join(".claude").join("settings.local.json"), cwd.join(".claw").join("settings.local.json"),
r#"{ r#"{
"sandbox": { "sandbox": {
"enabled": true, "enabled": true,
@@ -1094,8 +1092,8 @@ mod tests {
fn parses_typed_mcp_and_oauth_config() { fn parses_typed_mcp_and_oauth_config() {
let root = temp_dir(); let root = temp_dir();
let cwd = root.join("project"); let cwd = root.join("project");
let home = root.join("home").join(".claude"); let home = root.join("home").join(".claw");
fs::create_dir_all(cwd.join(".claude")).expect("project config dir"); fs::create_dir_all(cwd.join(".claw")).expect("project config dir");
fs::create_dir_all(&home).expect("home config dir"); fs::create_dir_all(&home).expect("home config dir");
fs::write( fs::write(
@@ -1132,7 +1130,7 @@ mod tests {
) )
.expect("write user settings"); .expect("write user settings");
fs::write( fs::write(
cwd.join(".claude").join("settings.local.json"), cwd.join(".claw").join("settings.local.json"),
r#"{ r#"{
"mcpServers": { "mcpServers": {
"remote-server": { "remote-server": {
@@ -1185,8 +1183,8 @@ mod tests {
fn parses_plugin_config_from_enabled_plugins() { fn parses_plugin_config_from_enabled_plugins() {
let root = temp_dir(); let root = temp_dir();
let cwd = root.join("project"); let cwd = root.join("project");
let home = root.join("home").join(".claude"); let home = root.join("home").join(".claw");
fs::create_dir_all(cwd.join(".claude")).expect("project config dir"); fs::create_dir_all(cwd.join(".claw")).expect("project config dir");
fs::create_dir_all(&home).expect("home config dir"); fs::create_dir_all(&home).expect("home config dir");
fs::write( fs::write(
@@ -1223,8 +1221,8 @@ mod tests {
fn parses_plugin_config() { fn parses_plugin_config() {
let root = temp_dir(); let root = temp_dir();
let cwd = root.join("project"); let cwd = root.join("project");
let home = root.join("home").join(".claude"); let home = root.join("home").join(".claw");
fs::create_dir_all(cwd.join(".claude")).expect("project config dir"); fs::create_dir_all(cwd.join(".claw")).expect("project config dir");
fs::create_dir_all(&home).expect("home config dir"); fs::create_dir_all(&home).expect("home config dir");
fs::write( fs::write(
@@ -1275,7 +1273,7 @@ mod tests {
fn rejects_invalid_mcp_server_shapes() { fn rejects_invalid_mcp_server_shapes() {
let root = temp_dir(); let root = temp_dir();
let cwd = root.join("project"); let cwd = root.join("project");
let home = root.join("home").join(".claude"); let home = root.join("home").join(".claw");
fs::create_dir_all(&home).expect("home config dir"); fs::create_dir_all(&home).expect("home config dir");
fs::create_dir_all(&cwd).expect("project dir"); fs::create_dir_all(&cwd).expect("project dir");
fs::write( fs::write(

View File

@@ -24,11 +24,11 @@ pub use compact::{
get_compact_continuation_message, should_compact, CompactionConfig, CompactionResult, get_compact_continuation_message, should_compact, CompactionConfig, CompactionResult,
}; };
pub use config::{ pub use config::{
ConfigEntry, ConfigError, ConfigLoader, ConfigSource, McpClaudeAiProxyServerConfig, ConfigEntry, ConfigError, ConfigLoader, ConfigSource, McpManagedProxyServerConfig,
McpConfigCollection, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig, McpConfigCollection, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig,
McpServerConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig, McpServerConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig,
ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig, RuntimeHookConfig, ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig, RuntimeHookConfig,
RuntimePluginConfig, ScopedMcpServerConfig, CLAUDE_CODE_SETTINGS_SCHEMA_NAME, RuntimePluginConfig, ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME,
}; };
pub use conversation::{ pub use conversation::{
auto_compaction_threshold_from_env, ApiClient, ApiRequest, AssistantEvent, AutoCompactionEvent, auto_compaction_threshold_from_env, ApiClient, ApiRequest, AssistantEvent, AutoCompactionEvent,
@@ -45,7 +45,7 @@ pub use mcp::{
scoped_mcp_config_hash, unwrap_ccr_proxy_url, scoped_mcp_config_hash, unwrap_ccr_proxy_url,
}; };
pub use mcp_client::{ pub use mcp_client::{
McpClaudeAiProxyTransport, McpClientAuth, McpClientBootstrap, McpClientTransport, McpManagedProxyTransport, McpClientAuth, McpClientBootstrap, McpClientTransport,
McpRemoteTransport, McpSdkTransport, McpStdioTransport, McpRemoteTransport, McpSdkTransport, McpStdioTransport,
}; };
pub use mcp_stdio::{ pub use mcp_stdio::{

View File

@@ -73,7 +73,7 @@ pub fn mcp_server_signature(config: &McpServerConfig) -> Option<String> {
Some(format!("url:{}", unwrap_ccr_proxy_url(&config.url))) Some(format!("url:{}", unwrap_ccr_proxy_url(&config.url)))
} }
McpServerConfig::Ws(config) => Some(format!("url:{}", unwrap_ccr_proxy_url(&config.url))), McpServerConfig::Ws(config) => Some(format!("url:{}", unwrap_ccr_proxy_url(&config.url))),
McpServerConfig::ClaudeAiProxy(config) => { McpServerConfig::ManagedProxy(config) => {
Some(format!("url:{}", unwrap_ccr_proxy_url(&config.url))) Some(format!("url:{}", unwrap_ccr_proxy_url(&config.url)))
} }
McpServerConfig::Sdk(_) => None, McpServerConfig::Sdk(_) => None,
@@ -110,7 +110,7 @@ pub fn scoped_mcp_config_hash(config: &ScopedMcpServerConfig) -> String {
ws.headers_helper.as_deref().unwrap_or("") ws.headers_helper.as_deref().unwrap_or("")
), ),
McpServerConfig::Sdk(sdk) => format!("sdk|{}", sdk.name), McpServerConfig::Sdk(sdk) => format!("sdk|{}", sdk.name),
McpServerConfig::ClaudeAiProxy(proxy) => { McpServerConfig::ManagedProxy(proxy) => {
format!("claudeai-proxy|{}|{}", proxy.url, proxy.id) format!("claudeai-proxy|{}|{}", proxy.url, proxy.id)
} }
}; };

View File

@@ -10,7 +10,7 @@ pub enum McpClientTransport {
Http(McpRemoteTransport), Http(McpRemoteTransport),
WebSocket(McpRemoteTransport), WebSocket(McpRemoteTransport),
Sdk(McpSdkTransport), Sdk(McpSdkTransport),
ClaudeAiProxy(McpClaudeAiProxyTransport), ManagedProxy(McpManagedProxyTransport),
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -34,7 +34,7 @@ pub struct McpSdkTransport {
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct McpClaudeAiProxyTransport { pub struct McpManagedProxyTransport {
pub url: String, pub url: String,
pub id: String, pub id: String,
} }
@@ -97,8 +97,8 @@ impl McpClientTransport {
McpServerConfig::Sdk(config) => Self::Sdk(McpSdkTransport { McpServerConfig::Sdk(config) => Self::Sdk(McpSdkTransport {
name: config.name.clone(), name: config.name.clone(),
}), }),
McpServerConfig::ClaudeAiProxy(config) => { McpServerConfig::ManagedProxy(config) => {
Self::ClaudeAiProxy(McpClaudeAiProxyTransport { Self::ManagedProxy(McpManagedProxyTransport {
url: config.url.clone(), url: config.url.clone(),
id: config.id.clone(), id: config.id.clone(),
}) })

View File

@@ -324,12 +324,12 @@ fn generate_random_token(bytes: usize) -> io::Result<String> {
} }
fn credentials_home_dir() -> io::Result<PathBuf> { fn credentials_home_dir() -> io::Result<PathBuf> {
if let Some(path) = std::env::var_os("CLAUDE_CONFIG_HOME") { if let Some(path) = std::env::var_os("CLAW_CONFIG_HOME") {
return Ok(PathBuf::from(path)); return Ok(PathBuf::from(path));
} }
let home = std::env::var_os("HOME") let home = std::env::var_os("HOME")
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "HOME is not set"))?; .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "HOME is not set"))?;
Ok(PathBuf::from(home).join(".claude")) Ok(PathBuf::from(home).join(".claw"))
} }
fn read_credentials_root(path: &PathBuf) -> io::Result<Map<String, Value>> { fn read_credentials_root(path: &PathBuf) -> io::Result<Map<String, Value>> {
@@ -541,7 +541,7 @@ mod tests {
fn oauth_credentials_round_trip_and_clear_preserves_other_fields() { fn oauth_credentials_round_trip_and_clear_preserves_other_fields() {
let _guard = env_lock(); let _guard = env_lock();
let config_home = temp_config_home(); let config_home = temp_config_home();
std::env::set_var("CLAUDE_CONFIG_HOME", &config_home); std::env::set_var("CLAW_CONFIG_HOME", &config_home);
let path = credentials_path().expect("credentials path"); let path = credentials_path().expect("credentials path");
std::fs::create_dir_all(path.parent().expect("parent")).expect("create parent"); std::fs::create_dir_all(path.parent().expect("parent")).expect("create parent");
std::fs::write(&path, "{\"other\":\"value\"}\n").expect("seed credentials"); std::fs::write(&path, "{\"other\":\"value\"}\n").expect("seed credentials");
@@ -567,7 +567,7 @@ mod tests {
assert!(cleared.contains("\"other\": \"value\"")); assert!(cleared.contains("\"other\": \"value\""));
assert!(!cleared.contains("\"oauth\"")); assert!(!cleared.contains("\"oauth\""));
std::env::remove_var("CLAUDE_CONFIG_HOME"); std::env::remove_var("CLAW_CONFIG_HOME");
std::fs::remove_dir_all(config_home).expect("cleanup temp dir"); std::fs::remove_dir_all(config_home).expect("cleanup temp dir");
} }

View File

@@ -203,8 +203,8 @@ fn discover_instruction_files(cwd: &Path) -> std::io::Result<Vec<ContextFile>> {
for candidate in [ for candidate in [
dir.join("CLAUDE.md"), dir.join("CLAUDE.md"),
dir.join("CLAUDE.local.md"), dir.join("CLAUDE.local.md"),
dir.join(".claude").join("CLAUDE.md"), dir.join(".claw").join("CLAUDE.md"),
dir.join(".claude").join("instructions.md"), dir.join(".claw").join("instructions.md"),
] { ] {
push_context_file(&mut files, candidate)?; push_context_file(&mut files, candidate)?;
} }
@@ -517,23 +517,23 @@ mod tests {
fn discovers_instruction_files_from_ancestor_chain() { fn discovers_instruction_files_from_ancestor_chain() {
let root = temp_dir(); let root = temp_dir();
let nested = root.join("apps").join("api"); let nested = root.join("apps").join("api");
fs::create_dir_all(nested.join(".claude")).expect("nested claude dir"); fs::create_dir_all(nested.join(".claw")).expect("nested claw dir");
fs::write(root.join("CLAUDE.md"), "root instructions").expect("write root instructions"); fs::write(root.join("CLAUDE.md"), "root instructions").expect("write root instructions");
fs::write(root.join("CLAUDE.local.md"), "local instructions") fs::write(root.join("CLAUDE.local.md"), "local instructions")
.expect("write local instructions"); .expect("write local instructions");
fs::create_dir_all(root.join("apps")).expect("apps dir"); fs::create_dir_all(root.join("apps")).expect("apps dir");
fs::create_dir_all(root.join("apps").join(".claude")).expect("apps claude dir"); fs::create_dir_all(root.join("apps").join(".claw")).expect("apps claw dir");
fs::write(root.join("apps").join("CLAUDE.md"), "apps instructions") fs::write(root.join("apps").join("CLAUDE.md"), "apps instructions")
.expect("write apps instructions"); .expect("write apps instructions");
fs::write( fs::write(
root.join("apps").join(".claude").join("instructions.md"), root.join("apps").join(".claw").join("instructions.md"),
"apps dot claude instructions", "apps dot claude instructions",
) )
.expect("write apps dot claude instructions"); .expect("write apps dot claude instructions");
fs::write(nested.join(".claude").join("CLAUDE.md"), "nested rules") fs::write(nested.join(".claw").join("CLAUDE.md"), "nested rules")
.expect("write nested rules"); .expect("write nested rules");
fs::write( fs::write(
nested.join(".claude").join("instructions.md"), nested.join(".claw").join("instructions.md"),
"nested instructions", "nested instructions",
) )
.expect("write nested instructions"); .expect("write nested instructions");
@@ -593,7 +593,7 @@ mod tests {
#[test] #[test]
fn displays_context_paths_compactly() { fn displays_context_paths_compactly() {
assert_eq!( assert_eq!(
display_context_path(Path::new("/tmp/project/.claude/CLAUDE.md")), display_context_path(Path::new("/tmp/project/.claw/CLAUDE.md")),
"CLAUDE.md" "CLAUDE.md"
); );
} }
@@ -667,10 +667,10 @@ mod tests {
#[test] #[test]
fn load_system_prompt_reads_claude_files_and_config() { fn load_system_prompt_reads_claude_files_and_config() {
let root = temp_dir(); let root = temp_dir();
fs::create_dir_all(root.join(".claude")).expect("claude dir"); fs::create_dir_all(root.join(".claw")).expect("claw dir");
fs::write(root.join("CLAUDE.md"), "Project rules").expect("write instructions"); fs::write(root.join("CLAUDE.md"), "Project rules").expect("write instructions");
fs::write( fs::write(
root.join(".claude").join("settings.json"), root.join(".claw").join("settings.json"),
r#"{"permissionMode":"acceptEdits"}"#, r#"{"permissionMode":"acceptEdits"}"#,
) )
.expect("write settings"); .expect("write settings");
@@ -678,9 +678,9 @@ mod tests {
let _guard = env_lock(); let _guard = env_lock();
let previous = std::env::current_dir().expect("cwd"); let previous = std::env::current_dir().expect("cwd");
let original_home = std::env::var("HOME").ok(); let original_home = std::env::var("HOME").ok();
let original_claude_home = std::env::var("CLAUDE_CONFIG_HOME").ok(); let original_claw_home = std::env::var("CLAW_CONFIG_HOME").ok();
std::env::set_var("HOME", &root); std::env::set_var("HOME", &root);
std::env::set_var("CLAUDE_CONFIG_HOME", root.join("missing-home")); std::env::set_var("CLAW_CONFIG_HOME", root.join("missing-home"));
std::env::set_current_dir(&root).expect("change cwd"); std::env::set_current_dir(&root).expect("change cwd");
let prompt = super::load_system_prompt(&root, "2026-03-31", "linux", "6.8") let prompt = super::load_system_prompt(&root, "2026-03-31", "linux", "6.8")
.expect("system prompt should load") .expect("system prompt should load")
@@ -695,10 +695,10 @@ mod tests {
} else { } else {
std::env::remove_var("HOME"); std::env::remove_var("HOME");
} }
if let Some(value) = original_claude_home { if let Some(value) = original_claw_home {
std::env::set_var("CLAUDE_CONFIG_HOME", value); std::env::set_var("CLAW_CONFIG_HOME", value);
} else { } else {
std::env::remove_var("CLAUDE_CONFIG_HOME"); std::env::remove_var("CLAW_CONFIG_HOME");
} }
assert!(prompt.contains("Project rules")); assert!(prompt.contains("Project rules"));
@@ -709,10 +709,10 @@ mod tests {
#[test] #[test]
fn renders_claude_code_style_sections_with_project_context() { fn renders_claude_code_style_sections_with_project_context() {
let root = temp_dir(); let root = temp_dir();
fs::create_dir_all(root.join(".claude")).expect("claude dir"); fs::create_dir_all(root.join(".claw")).expect("claw dir");
fs::write(root.join("CLAUDE.md"), "Project rules").expect("write CLAUDE.md"); fs::write(root.join("CLAUDE.md"), "Project rules").expect("write CLAUDE.md");
fs::write( fs::write(
root.join(".claude").join("settings.json"), root.join(".claw").join("settings.json"),
r#"{"permissionMode":"acceptEdits"}"#, r#"{"permissionMode":"acceptEdits"}"#,
) )
.expect("write settings"); .expect("write settings");
@@ -751,9 +751,9 @@ mod tests {
fn discovers_dot_claude_instructions_markdown() { fn discovers_dot_claude_instructions_markdown() {
let root = temp_dir(); let root = temp_dir();
let nested = root.join("apps").join("api"); let nested = root.join("apps").join("api");
fs::create_dir_all(nested.join(".claude")).expect("nested claude dir"); fs::create_dir_all(nested.join(".claw")).expect("nested claw dir");
fs::write( fs::write(
nested.join(".claude").join("instructions.md"), nested.join(".claw").join("instructions.md"),
"instruction markdown", "instruction markdown",
) )
.expect("write instructions.md"); .expect("write instructions.md");
@@ -762,7 +762,7 @@ mod tests {
assert!(context assert!(context
.instruction_files .instruction_files
.iter() .iter()
.any(|file| file.path.ends_with(".claude/instructions.md"))); .any(|file| file.path.ends_with(".claw/instructions.md")));
assert!( assert!(
render_instruction_files(&context.instruction_files).contains("instruction markdown") render_instruction_files(&context.instruction_files).contains("instruction markdown")
); );

View File

@@ -16,7 +16,7 @@ use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use api::{ use api::{
resolve_startup_auth_source, AnthropicClient, AuthSource, ContentBlockDelta, InputContentBlock, resolve_startup_auth_source, ApiHttpClient, AuthSource, ContentBlockDelta, InputContentBlock,
InputMessage, MessageRequest, MessageResponse, OutputContentBlock, InputMessage, MessageRequest, MessageResponse, OutputContentBlock,
StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock, StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock,
}; };
@@ -305,7 +305,13 @@ fn resolve_model_alias(model: &str) -> &str {
} }
fn normalize_allowed_tools(values: &[String]) -> Result<Option<AllowedToolSet>, String> { fn normalize_allowed_tools(values: &[String]) -> Result<Option<AllowedToolSet>, String> {
current_tool_registry()?.normalize_allowed_tools(values) if values.is_empty() {
return Ok(None);
}
match current_tool_registry() {
Ok(registry) => registry.normalize_allowed_tools(values),
Err(_) => GlobalToolRegistry::builtin().normalize_allowed_tools(values),
}
} }
fn current_tool_registry() -> Result<GlobalToolRegistry, String> { fn current_tool_registry() -> Result<GlobalToolRegistry, String> {
@@ -473,7 +479,7 @@ fn run_login() -> Result<(), Box<dyn std::error::Error>> {
return Err(io::Error::new(io::ErrorKind::InvalidData, "oauth state mismatch").into()); return Err(io::Error::new(io::ErrorKind::InvalidData, "oauth state mismatch").into());
} }
let client = AnthropicClient::from_auth(AuthSource::None).with_base_url(api::read_base_url()); let client = ApiHttpClient::from_auth(AuthSource::None).with_base_url(api::read_base_url());
let exchange_request = let exchange_request =
OAuthTokenExchangeRequest::from_config(oauth, code, state, pkce.verifier, redirect_uri); OAuthTokenExchangeRequest::from_config(oauth, code, state, pkce.verifier, redirect_uri);
let runtime = tokio::runtime::Runtime::new()?; let runtime = tokio::runtime::Runtime::new()?;
@@ -1697,7 +1703,7 @@ impl LiveCli {
fn sessions_dir() -> Result<PathBuf, Box<dyn std::error::Error>> { fn sessions_dir() -> Result<PathBuf, Box<dyn std::error::Error>> {
let cwd = env::current_dir()?; let cwd = env::current_dir()?;
let path = cwd.join(".claude").join("sessions"); let path = cwd.join(".claw").join("sessions");
fs::create_dir_all(&path)?; fs::create_dir_all(&path)?;
Ok(path) Ok(path)
} }
@@ -2811,7 +2817,7 @@ impl runtime::PermissionPrompter for CliPermissionPrompter {
struct AnthropicRuntimeClient { struct AnthropicRuntimeClient {
runtime: tokio::runtime::Runtime, runtime: tokio::runtime::Runtime,
client: AnthropicClient, client: ApiHttpClient,
model: String, model: String,
enable_tools: bool, enable_tools: bool,
emit_output: bool, emit_output: bool,
@@ -2831,7 +2837,7 @@ impl AnthropicRuntimeClient {
) -> Result<Self, Box<dyn std::error::Error>> { ) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self { Ok(Self {
runtime: tokio::runtime::Runtime::new()?, runtime: tokio::runtime::Runtime::new()?,
client: AnthropicClient::from_auth(resolve_cli_auth_source()?) client: ApiHttpClient::from_auth(resolve_cli_auth_source()?)
.with_base_url(api::read_base_url()), .with_base_url(api::read_base_url()),
model, model,
enable_tools, enable_tools,

View File

@@ -4,7 +4,7 @@ use std::process::Command;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use api::{ use api::{
read_base_url, AnthropicClient, ContentBlockDelta, InputContentBlock, InputMessage, read_base_url, ApiHttpClient, ContentBlockDelta, InputContentBlock, InputMessage,
MessageRequest, MessageResponse, OutputContentBlock, StreamEvent as ApiStreamEvent, ToolChoice, MessageRequest, MessageResponse, OutputContentBlock, StreamEvent as ApiStreamEvent, ToolChoice,
ToolDefinition, ToolResultContentBlock, ToolDefinition, ToolResultContentBlock,
}; };
@@ -1542,6 +1542,11 @@ fn resolve_skill_path(skill: &str) -> Result<std::path::PathBuf, String> {
if let Ok(codex_home) = std::env::var("CODEX_HOME") { if let Ok(codex_home) = std::env::var("CODEX_HOME") {
candidates.push(std::path::PathBuf::from(codex_home).join("skills")); candidates.push(std::path::PathBuf::from(codex_home).join("skills"));
} }
if let Ok(home) = std::env::var("HOME") {
let home = std::path::PathBuf::from(home);
candidates.push(home.join(".agents").join("skills"));
candidates.push(home.join(".codex").join("skills"));
}
candidates.push(std::path::PathBuf::from("/home/bellman/.codex/skills")); candidates.push(std::path::PathBuf::from("/home/bellman/.codex/skills"));
for root in candidates { for root in candidates {
@@ -1875,7 +1880,7 @@ fn format_agent_terminal_output(status: &str, result: Option<&str>, error: Optio
struct AnthropicRuntimeClient { struct AnthropicRuntimeClient {
runtime: tokio::runtime::Runtime, runtime: tokio::runtime::Runtime,
client: AnthropicClient, client: ApiHttpClient,
model: String, model: String,
allowed_tools: BTreeSet<String>, allowed_tools: BTreeSet<String>,
tool_registry: GlobalToolRegistry, tool_registry: GlobalToolRegistry,
@@ -1887,7 +1892,7 @@ impl AnthropicRuntimeClient {
allowed_tools: BTreeSet<String>, allowed_tools: BTreeSet<String>,
tool_registry: GlobalToolRegistry, tool_registry: GlobalToolRegistry,
) -> Result<Self, String> { ) -> Result<Self, String> {
let client = AnthropicClient::from_env() let client = ApiHttpClient::from_env()
.map_err(|error| error.to_string())? .map_err(|error| error.to_string())?
.with_base_url(read_base_url()); .with_base_url(read_base_url());
Ok(Self { Ok(Self {
@@ -2877,16 +2882,16 @@ fn config_file_for_scope(scope: ConfigScope) -> Result<PathBuf, String> {
let cwd = std::env::current_dir().map_err(|error| error.to_string())?; let cwd = std::env::current_dir().map_err(|error| error.to_string())?;
Ok(match scope { Ok(match scope {
ConfigScope::Global => config_home_dir()?.join("settings.json"), ConfigScope::Global => config_home_dir()?.join("settings.json"),
ConfigScope::Settings => cwd.join(".claude").join("settings.local.json"), ConfigScope::Settings => cwd.join(".claw").join("settings.local.json"),
}) })
} }
fn config_home_dir() -> Result<PathBuf, String> { fn config_home_dir() -> Result<PathBuf, String> {
if let Ok(path) = std::env::var("CLAUDE_CONFIG_HOME") { if let Ok(path) = std::env::var("CLAW_CONFIG_HOME") {
return Ok(PathBuf::from(path)); return Ok(PathBuf::from(path));
} }
let home = std::env::var("HOME").map_err(|_| String::from("HOME is not set"))?; let home = std::env::var("HOME").map_err(|_| String::from("HOME is not set"))?;
Ok(PathBuf::from(home).join(".claude")) Ok(PathBuf::from(home).join(".claw"))
} }
fn read_json_object(path: &Path) -> Result<serde_json::Map<String, Value>, String> { fn read_json_object(path: &Path) -> Result<serde_json::Map<String, Value>, String> {
@@ -4490,19 +4495,19 @@ mod tests {
)); ));
let home = root.join("home"); let home = root.join("home");
let cwd = root.join("cwd"); let cwd = root.join("cwd");
std::fs::create_dir_all(home.join(".claude")).expect("home dir"); std::fs::create_dir_all(home.join(".claw")).expect("home dir");
std::fs::create_dir_all(cwd.join(".claude")).expect("cwd dir"); std::fs::create_dir_all(cwd.join(".claw")).expect("cwd dir");
std::fs::write( std::fs::write(
home.join(".claude").join("settings.json"), home.join(".claw").join("settings.json"),
r#"{"verbose":false}"#, r#"{"verbose":false}"#,
) )
.expect("write global settings"); .expect("write global settings");
let original_home = std::env::var("HOME").ok(); let original_home = std::env::var("HOME").ok();
let original_claude_home = std::env::var("CLAUDE_CONFIG_HOME").ok(); let original_claw_home = std::env::var("CLAW_CONFIG_HOME").ok();
let original_dir = std::env::current_dir().expect("cwd"); let original_dir = std::env::current_dir().expect("cwd");
std::env::set_var("HOME", &home); std::env::set_var("HOME", &home);
std::env::remove_var("CLAUDE_CONFIG_HOME"); std::env::remove_var("CLAW_CONFIG_HOME");
std::env::set_current_dir(&cwd).expect("set cwd"); std::env::set_current_dir(&cwd).expect("set cwd");
let get = execute_tool("Config", &json!({"setting": "verbose"})).expect("get config"); let get = execute_tool("Config", &json!({"setting": "verbose"})).expect("get config");
@@ -4535,9 +4540,9 @@ mod tests {
Some(value) => std::env::set_var("HOME", value), Some(value) => std::env::set_var("HOME", value),
None => std::env::remove_var("HOME"), None => std::env::remove_var("HOME"),
} }
match original_claude_home { match original_claw_home {
Some(value) => std::env::set_var("CLAUDE_CONFIG_HOME", value), Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value),
None => std::env::remove_var("CLAUDE_CONFIG_HOME"), None => std::env::remove_var("CLAW_CONFIG_HOME"),
} }
let _ = std::fs::remove_dir_all(root); let _ = std::fs::remove_dir_all(root);
} }