mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-04 05:14:48 +08:00
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:
@@ -101,7 +101,7 @@ impl From<OAuthTokenSet> for AuthSource {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AnthropicClient {
|
||||
pub struct ApiHttpClient {
|
||||
http: reqwest::Client,
|
||||
auth: AuthSource,
|
||||
base_url: String,
|
||||
@@ -110,7 +110,7 @@ pub struct AnthropicClient {
|
||||
max_backoff: Duration,
|
||||
}
|
||||
|
||||
impl AnthropicClient {
|
||||
impl ApiHttpClient {
|
||||
#[must_use]
|
||||
pub fn new(api_key: impl Into<String>) -> Self {
|
||||
Self {
|
||||
@@ -429,7 +429,7 @@ fn resolve_saved_oauth_token_set(
|
||||
let Some(refresh_token) = token_set.refresh_token.clone() else {
|
||||
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 {
|
||||
client
|
||||
.refresh_oauth_token(
|
||||
@@ -614,7 +614,7 @@ mod tests {
|
||||
|
||||
use crate::client::{
|
||||
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};
|
||||
|
||||
@@ -671,7 +671,7 @@ mod tests {
|
||||
let _guard = env_lock();
|
||||
std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
|
||||
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");
|
||||
assert!(matches!(error, crate::error::ApiError::MissingApiKey));
|
||||
}
|
||||
@@ -735,7 +735,7 @@ mod tests {
|
||||
fn auth_source_from_saved_oauth_when_env_absent() {
|
||||
let _guard = env_lock();
|
||||
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_API_KEY");
|
||||
save_oauth_credentials(&runtime::OAuthTokenSet {
|
||||
@@ -750,7 +750,7 @@ mod tests {
|
||||
assert_eq!(auth.bearer_token(), Some("saved-access-token"));
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -774,7 +774,7 @@ mod tests {
|
||||
fn resolve_saved_oauth_token_refreshes_expired_credentials() {
|
||||
let _guard = env_lock();
|
||||
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_API_KEY");
|
||||
save_oauth_credentials(&runtime::OAuthTokenSet {
|
||||
@@ -798,7 +798,7 @@ mod tests {
|
||||
assert_eq!(stored.access_token, "refreshed-token");
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -806,7 +806,7 @@ mod tests {
|
||||
fn resolve_startup_auth_source_uses_saved_oauth_without_loading_config() {
|
||||
let _guard = env_lock();
|
||||
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_API_KEY");
|
||||
save_oauth_credentials(&runtime::OAuthTokenSet {
|
||||
@@ -822,7 +822,7 @@ mod tests {
|
||||
assert_eq!(auth.bearer_token(), Some("saved-access-token"));
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -830,7 +830,7 @@ mod tests {
|
||||
fn resolve_startup_auth_source_errors_when_refreshable_token_lacks_config() {
|
||||
let _guard = env_lock();
|
||||
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_API_KEY");
|
||||
save_oauth_credentials(&runtime::OAuthTokenSet {
|
||||
@@ -854,7 +854,7 @@ mod tests {
|
||||
assert_eq!(stored.refresh_token.as_deref(), Some("refresh-token"));
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -862,7 +862,7 @@ mod tests {
|
||||
fn resolve_saved_oauth_token_preserves_refresh_token_when_refresh_response_omits_it() {
|
||||
let _guard = env_lock();
|
||||
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_API_KEY");
|
||||
save_oauth_credentials(&runtime::OAuthTokenSet {
|
||||
@@ -887,7 +887,7 @@ mod tests {
|
||||
assert_eq!(stored.refresh_token.as_deref(), Some("refresh-token"));
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -908,7 +908,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn backoff_doubles_until_maximum() {
|
||||
let client = AnthropicClient::new("test-key").with_retry_policy(
|
||||
let client = ApiHttpClient::new("test-key").with_retry_policy(
|
||||
3,
|
||||
Duration::from_millis(10),
|
||||
Duration::from_millis(25),
|
||||
|
||||
@@ -5,7 +5,7 @@ mod types;
|
||||
|
||||
pub use client::{
|
||||
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 sse::{parse_frame, SseParser};
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use api::{
|
||||
AnthropicClient, ApiError, ContentBlockDelta, ContentBlockDeltaEvent, ContentBlockStartEvent,
|
||||
ApiHttpClient, ApiError, ContentBlockDelta, ContentBlockDeltaEvent, ContentBlockStartEvent,
|
||||
InputContentBlock, InputMessage, MessageDeltaEvent, MessageRequest, OutputContentBlock,
|
||||
StreamEvent, ToolChoice, ToolDefinition,
|
||||
};
|
||||
@@ -34,7 +34,7 @@ async fn send_message_posts_json_and_parses_response() {
|
||||
)
|
||||
.await;
|
||||
|
||||
let client = AnthropicClient::new("test-key")
|
||||
let client = ApiHttpClient::new("test-key")
|
||||
.with_auth_token(Some("proxy-token".to_string()))
|
||||
.with_base_url(server.base_url());
|
||||
let response = client
|
||||
@@ -99,7 +99,7 @@ async fn send_message_parses_response_with_thinking_blocks() {
|
||||
)
|
||||
.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
|
||||
.send_message(&sample_request(false))
|
||||
.await
|
||||
@@ -146,7 +146,7 @@ async fn stream_message_parses_sse_events_with_tool_use() {
|
||||
)
|
||||
.await;
|
||||
|
||||
let client = AnthropicClient::new("test-key")
|
||||
let client = ApiHttpClient::new("test-key")
|
||||
.with_auth_token(Some("proxy-token".to_string()))
|
||||
.with_base_url(server.base_url());
|
||||
let mut stream = client
|
||||
@@ -234,7 +234,7 @@ async fn stream_message_parses_sse_events_with_thinking_blocks() {
|
||||
)
|
||||
.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
|
||||
.stream_message(&sample_request(false))
|
||||
.await
|
||||
@@ -303,7 +303,7 @@ async fn retries_retryable_failures_before_succeeding() {
|
||||
)
|
||||
.await;
|
||||
|
||||
let client = AnthropicClient::new("test-key")
|
||||
let client = ApiHttpClient::new("test-key")
|
||||
.with_base_url(server.base_url())
|
||||
.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;
|
||||
|
||||
let client = AnthropicClient::new("test-key")
|
||||
let client = ApiHttpClient::new("test-key")
|
||||
.with_base_url(server.base_url())
|
||||
.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]
|
||||
#[ignore = "requires ANTHROPIC_API_KEY and network access"]
|
||||
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
|
||||
.stream_message(&MessageRequest {
|
||||
model: std::env::var("ANTHROPIC_MODEL")
|
||||
|
||||
@@ -11,3 +11,4 @@ workspace = true
|
||||
[dependencies]
|
||||
plugins = { path = "../plugins" }
|
||||
runtime = { path = "../runtime" }
|
||||
serde_json = "1"
|
||||
|
||||
@@ -48,138 +48,161 @@ pub struct SlashCommandSpec {
|
||||
const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
SlashCommandSpec {
|
||||
name: "help",
|
||||
aliases: &[],
|
||||
summary: "Show available slash commands",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "status",
|
||||
aliases: &[],
|
||||
summary: "Show current session status",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "compact",
|
||||
aliases: &[],
|
||||
summary: "Compact local session history",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "model",
|
||||
aliases: &[],
|
||||
summary: "Show or switch the active model",
|
||||
argument_hint: Some("[model]"),
|
||||
resume_supported: false,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "permissions",
|
||||
aliases: &[],
|
||||
summary: "Show or switch the active permission mode",
|
||||
argument_hint: Some("[read-only|workspace-write|danger-full-access]"),
|
||||
resume_supported: false,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "clear",
|
||||
aliases: &[],
|
||||
summary: "Start a fresh local session",
|
||||
argument_hint: Some("[--confirm]"),
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "cost",
|
||||
aliases: &[],
|
||||
summary: "Show cumulative token usage for this session",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "resume",
|
||||
aliases: &[],
|
||||
summary: "Load a saved session into the REPL",
|
||||
argument_hint: Some("<session-path>"),
|
||||
resume_supported: false,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "config",
|
||||
aliases: &[],
|
||||
summary: "Inspect Claude config files or merged sections",
|
||||
argument_hint: Some("[env|hooks|model|plugins]"),
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "memory",
|
||||
aliases: &[],
|
||||
summary: "Inspect loaded Claude instruction memory files",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "init",
|
||||
aliases: &[],
|
||||
summary: "Create a starter CLAUDE.md for this repo",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "diff",
|
||||
aliases: &[],
|
||||
summary: "Show git diff for current workspace changes",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "version",
|
||||
aliases: &[],
|
||||
summary: "Show CLI version and build information",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "bughunter",
|
||||
aliases: &[],
|
||||
summary: "Inspect the codebase for likely bugs",
|
||||
argument_hint: Some("[scope]"),
|
||||
resume_supported: false,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "commit",
|
||||
aliases: &[],
|
||||
summary: "Generate a commit message and create a git commit",
|
||||
argument_hint: None,
|
||||
resume_supported: false,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "pr",
|
||||
aliases: &[],
|
||||
summary: "Draft or create a pull request from the conversation",
|
||||
argument_hint: Some("[context]"),
|
||||
resume_supported: false,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "issue",
|
||||
aliases: &[],
|
||||
summary: "Draft or create a GitHub issue from the conversation",
|
||||
argument_hint: Some("[context]"),
|
||||
resume_supported: false,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "ultraplan",
|
||||
aliases: &[],
|
||||
summary: "Run a deep planning prompt with multi-step reasoning",
|
||||
argument_hint: Some("[task]"),
|
||||
resume_supported: false,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "teleport",
|
||||
aliases: &[],
|
||||
summary: "Jump to a file or symbol by searching the workspace",
|
||||
argument_hint: Some("<symbol-or-path>"),
|
||||
resume_supported: false,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "debug-tool-call",
|
||||
aliases: &[],
|
||||
summary: "Replay the last tool call with debug details",
|
||||
argument_hint: None,
|
||||
resume_supported: false,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "export",
|
||||
aliases: &[],
|
||||
summary: "Export the current conversation to a file",
|
||||
argument_hint: Some("[file]"),
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "session",
|
||||
aliases: &[],
|
||||
summary: "List or switch managed local sessions",
|
||||
argument_hint: Some("[list|switch <session-id>]"),
|
||||
resume_supported: false,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "plugins",
|
||||
aliases: &[],
|
||||
summary: "List or manage plugins",
|
||||
argument_hint: Some(
|
||||
"[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)]
|
||||
mod tests {
|
||||
use super::{
|
||||
@@ -622,13 +849,27 @@ mod tests {
|
||||
fs::write(
|
||||
root.join(".claude-plugin").join("plugin.json"),
|
||||
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" }
|
||||
),
|
||||
)
|
||||
.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)]
|
||||
#[test]
|
||||
fn parses_supported_slash_commands() {
|
||||
@@ -961,9 +1202,8 @@ mod tests {
|
||||
(DefinitionSource::ProjectCodex, project_agents),
|
||||
(DefinitionSource::UserCodex, user_agents),
|
||||
];
|
||||
let report = render_agents_report(
|
||||
&load_agents_from_roots(&roots).expect("agent roots should load"),
|
||||
);
|
||||
let report =
|
||||
render_agents_report(&load_agents_from_roots(&roots).expect("agent roots should load"));
|
||||
|
||||
assert!(report.contains("Agents"));
|
||||
assert!(report.contains("2 active agents"));
|
||||
@@ -992,9 +1232,8 @@ mod tests {
|
||||
(DefinitionSource::ProjectCodex, project_skills),
|
||||
(DefinitionSource::UserCodex, user_skills),
|
||||
];
|
||||
let report = render_skills_report(
|
||||
&load_skills_from_roots(&roots).expect("skill roots should load"),
|
||||
);
|
||||
let report =
|
||||
render_skills_report(&load_skills_from_roots(&roots).expect("skill roots should load"));
|
||||
|
||||
assert!(report.contains("Skills"));
|
||||
assert!(report.contains("2 available skills"));
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
|
||||
use crate::json::JsonValue;
|
||||
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)]
|
||||
pub enum ConfigSource {
|
||||
@@ -79,7 +79,7 @@ pub enum McpTransport {
|
||||
Http,
|
||||
Ws,
|
||||
Sdk,
|
||||
ClaudeAiProxy,
|
||||
ManagedProxy,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -89,7 +89,7 @@ pub enum McpServerConfig {
|
||||
Http(McpRemoteServerConfig),
|
||||
Ws(McpWebSocketServerConfig),
|
||||
Sdk(McpSdkServerConfig),
|
||||
ClaudeAiProxy(McpClaudeAiProxyServerConfig),
|
||||
ManagedProxy(McpManagedProxyServerConfig),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -120,7 +120,7 @@ pub struct McpSdkServerConfig {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct McpClaudeAiProxyServerConfig {
|
||||
pub struct McpManagedProxyServerConfig {
|
||||
pub url: String,
|
||||
pub id: String,
|
||||
}
|
||||
@@ -196,8 +196,8 @@ impl ConfigLoader {
|
||||
#[must_use]
|
||||
pub fn discover(&self) -> Vec<ConfigEntry> {
|
||||
let user_legacy_path = self.config_home.parent().map_or_else(
|
||||
|| PathBuf::from(".claude.json"),
|
||||
|parent| parent.join(".claude.json"),
|
||||
|| PathBuf::from(".claw.json"),
|
||||
|parent| parent.join(".claw.json"),
|
||||
);
|
||||
vec![
|
||||
ConfigEntry {
|
||||
@@ -210,15 +210,15 @@ impl ConfigLoader {
|
||||
},
|
||||
ConfigEntry {
|
||||
source: ConfigSource::Project,
|
||||
path: self.cwd.join(".claude.json"),
|
||||
path: self.cwd.join(".claw.json"),
|
||||
},
|
||||
ConfigEntry {
|
||||
source: ConfigSource::Project,
|
||||
path: self.cwd.join(".claude").join("settings.json"),
|
||||
path: self.cwd.join(".claw").join("settings.json"),
|
||||
},
|
||||
ConfigEntry {
|
||||
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]
|
||||
pub fn default_config_home() -> PathBuf {
|
||||
std::env::var_os("CLAUDE_CONFIG_HOME")
|
||||
std::env::var_os("CLAW_CONFIG_HOME")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".claude")))
|
||||
.unwrap_or_else(|| PathBuf::from(".claude"))
|
||||
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".claw")))
|
||||
.unwrap_or_else(|| PathBuf::from(".claw"))
|
||||
}
|
||||
|
||||
impl RuntimeHookConfig {
|
||||
@@ -486,7 +486,7 @@ impl McpServerConfig {
|
||||
Self::Http(_) => McpTransport::Http,
|
||||
Self::Ws(_) => McpTransport::Ws,
|
||||
Self::Sdk(_) => McpTransport::Sdk,
|
||||
Self::ClaudeAiProxy(_) => McpTransport::ClaudeAiProxy,
|
||||
Self::ManagedProxy(_) => McpTransport::ManagedProxy,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -494,7 +494,7 @@ impl McpServerConfig {
|
||||
fn read_optional_json_object(
|
||||
path: &Path,
|
||||
) -> 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) {
|
||||
Ok(contents) => contents,
|
||||
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 {
|
||||
name: expect_string(object, "name", context)?.to_string(),
|
||||
})),
|
||||
"claudeai-proxy" => Ok(McpServerConfig::ClaudeAiProxy(
|
||||
McpClaudeAiProxyServerConfig {
|
||||
url: expect_string(object, "url", context)?.to_string(),
|
||||
id: expect_string(object, "id", context)?.to_string(),
|
||||
},
|
||||
)),
|
||||
"claudeai-proxy" => Ok(McpServerConfig::ManagedProxy(McpManagedProxyServerConfig {
|
||||
url: expect_string(object, "url", context)?.to_string(),
|
||||
id: expect_string(object, "id", context)?.to_string(),
|
||||
})),
|
||||
other => Err(ConfigError::Parse(format!(
|
||||
"{context}: unsupported MCP server type for {server_name}: {other}"
|
||||
))),
|
||||
@@ -942,7 +940,7 @@ fn push_unique(target: &mut Vec<String>, value: String) {
|
||||
mod tests {
|
||||
use super::{
|
||||
ConfigLoader, ConfigSource, McpServerConfig, McpTransport, ResolvedPermissionMode,
|
||||
CLAUDE_CODE_SETTINGS_SCHEMA_NAME,
|
||||
CLAW_SETTINGS_SCHEMA_NAME,
|
||||
};
|
||||
use crate::json::JsonValue;
|
||||
use crate::sandbox::FilesystemIsolationMode;
|
||||
@@ -961,7 +959,7 @@ mod tests {
|
||||
fn rejects_non_object_settings_files() {
|
||||
let root = temp_dir();
|
||||
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(&cwd).expect("project dir");
|
||||
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() {
|
||||
let root = temp_dir();
|
||||
let cwd = root.join("project");
|
||||
let home = root.join("home").join(".claude");
|
||||
fs::create_dir_all(cwd.join(".claude")).expect("project config dir");
|
||||
let home = root.join("home").join(".claw");
|
||||
fs::create_dir_all(cwd.join(".claw")).expect("project config dir");
|
||||
fs::create_dir_all(&home).expect("home config dir");
|
||||
|
||||
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"]}}}"#,
|
||||
)
|
||||
.expect("write user compat config");
|
||||
@@ -995,17 +993,17 @@ mod tests {
|
||||
)
|
||||
.expect("write user settings");
|
||||
fs::write(
|
||||
cwd.join(".claude.json"),
|
||||
cwd.join(".claw.json"),
|
||||
r#"{"model":"project-compat","env":{"B":"2"}}"#,
|
||||
)
|
||||
.expect("write project compat config");
|
||||
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"]}}}"#,
|
||||
)
|
||||
.expect("write project settings");
|
||||
fs::write(
|
||||
cwd.join(".claude").join("settings.local.json"),
|
||||
cwd.join(".claw").join("settings.local.json"),
|
||||
r#"{"model":"opus","permissionMode":"acceptEdits"}"#,
|
||||
)
|
||||
.expect("write local settings");
|
||||
@@ -1014,7 +1012,7 @@ mod tests {
|
||||
.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()[0].source, ConfigSource::User);
|
||||
assert_eq!(
|
||||
@@ -1056,12 +1054,12 @@ mod tests {
|
||||
fn parses_sandbox_config() {
|
||||
let root = temp_dir();
|
||||
let cwd = root.join("project");
|
||||
let home = root.join("home").join(".claude");
|
||||
fs::create_dir_all(cwd.join(".claude")).expect("project config dir");
|
||||
let home = root.join("home").join(".claw");
|
||||
fs::create_dir_all(cwd.join(".claw")).expect("project config dir");
|
||||
fs::create_dir_all(&home).expect("home config dir");
|
||||
|
||||
fs::write(
|
||||
cwd.join(".claude").join("settings.local.json"),
|
||||
cwd.join(".claw").join("settings.local.json"),
|
||||
r#"{
|
||||
"sandbox": {
|
||||
"enabled": true,
|
||||
@@ -1094,8 +1092,8 @@ mod tests {
|
||||
fn parses_typed_mcp_and_oauth_config() {
|
||||
let root = temp_dir();
|
||||
let cwd = root.join("project");
|
||||
let home = root.join("home").join(".claude");
|
||||
fs::create_dir_all(cwd.join(".claude")).expect("project config dir");
|
||||
let home = root.join("home").join(".claw");
|
||||
fs::create_dir_all(cwd.join(".claw")).expect("project config dir");
|
||||
fs::create_dir_all(&home).expect("home config dir");
|
||||
|
||||
fs::write(
|
||||
@@ -1132,7 +1130,7 @@ mod tests {
|
||||
)
|
||||
.expect("write user settings");
|
||||
fs::write(
|
||||
cwd.join(".claude").join("settings.local.json"),
|
||||
cwd.join(".claw").join("settings.local.json"),
|
||||
r#"{
|
||||
"mcpServers": {
|
||||
"remote-server": {
|
||||
@@ -1185,8 +1183,8 @@ mod tests {
|
||||
fn parses_plugin_config_from_enabled_plugins() {
|
||||
let root = temp_dir();
|
||||
let cwd = root.join("project");
|
||||
let home = root.join("home").join(".claude");
|
||||
fs::create_dir_all(cwd.join(".claude")).expect("project config dir");
|
||||
let home = root.join("home").join(".claw");
|
||||
fs::create_dir_all(cwd.join(".claw")).expect("project config dir");
|
||||
fs::create_dir_all(&home).expect("home config dir");
|
||||
|
||||
fs::write(
|
||||
@@ -1223,8 +1221,8 @@ mod tests {
|
||||
fn parses_plugin_config() {
|
||||
let root = temp_dir();
|
||||
let cwd = root.join("project");
|
||||
let home = root.join("home").join(".claude");
|
||||
fs::create_dir_all(cwd.join(".claude")).expect("project config dir");
|
||||
let home = root.join("home").join(".claw");
|
||||
fs::create_dir_all(cwd.join(".claw")).expect("project config dir");
|
||||
fs::create_dir_all(&home).expect("home config dir");
|
||||
|
||||
fs::write(
|
||||
@@ -1275,7 +1273,7 @@ mod tests {
|
||||
fn rejects_invalid_mcp_server_shapes() {
|
||||
let root = temp_dir();
|
||||
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(&cwd).expect("project dir");
|
||||
fs::write(
|
||||
|
||||
@@ -24,11 +24,11 @@ pub use compact::{
|
||||
get_compact_continuation_message, should_compact, CompactionConfig, CompactionResult,
|
||||
};
|
||||
pub use config::{
|
||||
ConfigEntry, ConfigError, ConfigLoader, ConfigSource, McpClaudeAiProxyServerConfig,
|
||||
ConfigEntry, ConfigError, ConfigLoader, ConfigSource, McpManagedProxyServerConfig,
|
||||
McpConfigCollection, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig,
|
||||
McpServerConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig,
|
||||
ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig, RuntimeHookConfig,
|
||||
RuntimePluginConfig, ScopedMcpServerConfig, CLAUDE_CODE_SETTINGS_SCHEMA_NAME,
|
||||
RuntimePluginConfig, ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME,
|
||||
};
|
||||
pub use conversation::{
|
||||
auto_compaction_threshold_from_env, ApiClient, ApiRequest, AssistantEvent, AutoCompactionEvent,
|
||||
@@ -45,7 +45,7 @@ pub use mcp::{
|
||||
scoped_mcp_config_hash, unwrap_ccr_proxy_url,
|
||||
};
|
||||
pub use mcp_client::{
|
||||
McpClaudeAiProxyTransport, McpClientAuth, McpClientBootstrap, McpClientTransport,
|
||||
McpManagedProxyTransport, McpClientAuth, McpClientBootstrap, McpClientTransport,
|
||||
McpRemoteTransport, McpSdkTransport, McpStdioTransport,
|
||||
};
|
||||
pub use mcp_stdio::{
|
||||
|
||||
@@ -73,7 +73,7 @@ pub fn mcp_server_signature(config: &McpServerConfig) -> Option<String> {
|
||||
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)))
|
||||
}
|
||||
McpServerConfig::Sdk(_) => None,
|
||||
@@ -110,7 +110,7 @@ pub fn scoped_mcp_config_hash(config: &ScopedMcpServerConfig) -> String {
|
||||
ws.headers_helper.as_deref().unwrap_or("")
|
||||
),
|
||||
McpServerConfig::Sdk(sdk) => format!("sdk|{}", sdk.name),
|
||||
McpServerConfig::ClaudeAiProxy(proxy) => {
|
||||
McpServerConfig::ManagedProxy(proxy) => {
|
||||
format!("claudeai-proxy|{}|{}", proxy.url, proxy.id)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ pub enum McpClientTransport {
|
||||
Http(McpRemoteTransport),
|
||||
WebSocket(McpRemoteTransport),
|
||||
Sdk(McpSdkTransport),
|
||||
ClaudeAiProxy(McpClaudeAiProxyTransport),
|
||||
ManagedProxy(McpManagedProxyTransport),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -34,7 +34,7 @@ pub struct McpSdkTransport {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct McpClaudeAiProxyTransport {
|
||||
pub struct McpManagedProxyTransport {
|
||||
pub url: String,
|
||||
pub id: String,
|
||||
}
|
||||
@@ -97,8 +97,8 @@ impl McpClientTransport {
|
||||
McpServerConfig::Sdk(config) => Self::Sdk(McpSdkTransport {
|
||||
name: config.name.clone(),
|
||||
}),
|
||||
McpServerConfig::ClaudeAiProxy(config) => {
|
||||
Self::ClaudeAiProxy(McpClaudeAiProxyTransport {
|
||||
McpServerConfig::ManagedProxy(config) => {
|
||||
Self::ManagedProxy(McpManagedProxyTransport {
|
||||
url: config.url.clone(),
|
||||
id: config.id.clone(),
|
||||
})
|
||||
|
||||
@@ -324,12 +324,12 @@ fn generate_random_token(bytes: usize) -> io::Result<String> {
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
let home = std::env::var_os("HOME")
|
||||
.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>> {
|
||||
@@ -541,7 +541,7 @@ mod tests {
|
||||
fn oauth_credentials_round_trip_and_clear_preserves_other_fields() {
|
||||
let _guard = env_lock();
|
||||
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");
|
||||
std::fs::create_dir_all(path.parent().expect("parent")).expect("create parent");
|
||||
std::fs::write(&path, "{\"other\":\"value\"}\n").expect("seed credentials");
|
||||
@@ -567,7 +567,7 @@ mod tests {
|
||||
assert!(cleared.contains("\"other\": \"value\""));
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
@@ -203,8 +203,8 @@ fn discover_instruction_files(cwd: &Path) -> std::io::Result<Vec<ContextFile>> {
|
||||
for candidate in [
|
||||
dir.join("CLAUDE.md"),
|
||||
dir.join("CLAUDE.local.md"),
|
||||
dir.join(".claude").join("CLAUDE.md"),
|
||||
dir.join(".claude").join("instructions.md"),
|
||||
dir.join(".claw").join("CLAUDE.md"),
|
||||
dir.join(".claw").join("instructions.md"),
|
||||
] {
|
||||
push_context_file(&mut files, candidate)?;
|
||||
}
|
||||
@@ -517,23 +517,23 @@ mod tests {
|
||||
fn discovers_instruction_files_from_ancestor_chain() {
|
||||
let root = temp_dir();
|
||||
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.local.md"), "local instructions")
|
||||
.expect("write local instructions");
|
||||
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")
|
||||
.expect("write apps instructions");
|
||||
fs::write(
|
||||
root.join("apps").join(".claude").join("instructions.md"),
|
||||
root.join("apps").join(".claw").join("instructions.md"),
|
||||
"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");
|
||||
fs::write(
|
||||
nested.join(".claude").join("instructions.md"),
|
||||
nested.join(".claw").join("instructions.md"),
|
||||
"nested instructions",
|
||||
)
|
||||
.expect("write nested instructions");
|
||||
@@ -593,7 +593,7 @@ mod tests {
|
||||
#[test]
|
||||
fn displays_context_paths_compactly() {
|
||||
assert_eq!(
|
||||
display_context_path(Path::new("/tmp/project/.claude/CLAUDE.md")),
|
||||
display_context_path(Path::new("/tmp/project/.claw/CLAUDE.md")),
|
||||
"CLAUDE.md"
|
||||
);
|
||||
}
|
||||
@@ -667,10 +667,10 @@ mod tests {
|
||||
#[test]
|
||||
fn load_system_prompt_reads_claude_files_and_config() {
|
||||
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").join("settings.json"),
|
||||
root.join(".claw").join("settings.json"),
|
||||
r#"{"permissionMode":"acceptEdits"}"#,
|
||||
)
|
||||
.expect("write settings");
|
||||
@@ -678,9 +678,9 @@ mod tests {
|
||||
let _guard = env_lock();
|
||||
let previous = std::env::current_dir().expect("cwd");
|
||||
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("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");
|
||||
let prompt = super::load_system_prompt(&root, "2026-03-31", "linux", "6.8")
|
||||
.expect("system prompt should load")
|
||||
@@ -695,10 +695,10 @@ mod tests {
|
||||
} else {
|
||||
std::env::remove_var("HOME");
|
||||
}
|
||||
if let Some(value) = original_claude_home {
|
||||
std::env::set_var("CLAUDE_CONFIG_HOME", value);
|
||||
if let Some(value) = original_claw_home {
|
||||
std::env::set_var("CLAW_CONFIG_HOME", value);
|
||||
} else {
|
||||
std::env::remove_var("CLAUDE_CONFIG_HOME");
|
||||
std::env::remove_var("CLAW_CONFIG_HOME");
|
||||
}
|
||||
|
||||
assert!(prompt.contains("Project rules"));
|
||||
@@ -709,10 +709,10 @@ mod tests {
|
||||
#[test]
|
||||
fn renders_claude_code_style_sections_with_project_context() {
|
||||
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").join("settings.json"),
|
||||
root.join(".claw").join("settings.json"),
|
||||
r#"{"permissionMode":"acceptEdits"}"#,
|
||||
)
|
||||
.expect("write settings");
|
||||
@@ -751,9 +751,9 @@ mod tests {
|
||||
fn discovers_dot_claude_instructions_markdown() {
|
||||
let root = temp_dir();
|
||||
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(
|
||||
nested.join(".claude").join("instructions.md"),
|
||||
nested.join(".claw").join("instructions.md"),
|
||||
"instruction markdown",
|
||||
)
|
||||
.expect("write instructions.md");
|
||||
@@ -762,7 +762,7 @@ mod tests {
|
||||
assert!(context
|
||||
.instruction_files
|
||||
.iter()
|
||||
.any(|file| file.path.ends_with(".claude/instructions.md")));
|
||||
.any(|file| file.path.ends_with(".claw/instructions.md")));
|
||||
assert!(
|
||||
render_instruction_files(&context.instruction_files).contains("instruction markdown")
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ use std::thread;
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use api::{
|
||||
resolve_startup_auth_source, AnthropicClient, AuthSource, ContentBlockDelta, InputContentBlock,
|
||||
resolve_startup_auth_source, ApiHttpClient, AuthSource, ContentBlockDelta, InputContentBlock,
|
||||
InputMessage, MessageRequest, MessageResponse, OutputContentBlock,
|
||||
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> {
|
||||
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> {
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
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 =
|
||||
OAuthTokenExchangeRequest::from_config(oauth, code, state, pkce.verifier, redirect_uri);
|
||||
let runtime = tokio::runtime::Runtime::new()?;
|
||||
@@ -1697,7 +1703,7 @@ impl LiveCli {
|
||||
|
||||
fn sessions_dir() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
let cwd = env::current_dir()?;
|
||||
let path = cwd.join(".claude").join("sessions");
|
||||
let path = cwd.join(".claw").join("sessions");
|
||||
fs::create_dir_all(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
@@ -2811,7 +2817,7 @@ impl runtime::PermissionPrompter for CliPermissionPrompter {
|
||||
|
||||
struct AnthropicRuntimeClient {
|
||||
runtime: tokio::runtime::Runtime,
|
||||
client: AnthropicClient,
|
||||
client: ApiHttpClient,
|
||||
model: String,
|
||||
enable_tools: bool,
|
||||
emit_output: bool,
|
||||
@@ -2831,7 +2837,7 @@ impl AnthropicRuntimeClient {
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
Ok(Self {
|
||||
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()),
|
||||
model,
|
||||
enable_tools,
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::process::Command;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use api::{
|
||||
read_base_url, AnthropicClient, ContentBlockDelta, InputContentBlock, InputMessage,
|
||||
read_base_url, ApiHttpClient, ContentBlockDelta, InputContentBlock, InputMessage,
|
||||
MessageRequest, MessageResponse, OutputContentBlock, StreamEvent as ApiStreamEvent, ToolChoice,
|
||||
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") {
|
||||
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"));
|
||||
|
||||
for root in candidates {
|
||||
@@ -1875,7 +1880,7 @@ fn format_agent_terminal_output(status: &str, result: Option<&str>, error: Optio
|
||||
|
||||
struct AnthropicRuntimeClient {
|
||||
runtime: tokio::runtime::Runtime,
|
||||
client: AnthropicClient,
|
||||
client: ApiHttpClient,
|
||||
model: String,
|
||||
allowed_tools: BTreeSet<String>,
|
||||
tool_registry: GlobalToolRegistry,
|
||||
@@ -1887,7 +1892,7 @@ impl AnthropicRuntimeClient {
|
||||
allowed_tools: BTreeSet<String>,
|
||||
tool_registry: GlobalToolRegistry,
|
||||
) -> Result<Self, String> {
|
||||
let client = AnthropicClient::from_env()
|
||||
let client = ApiHttpClient::from_env()
|
||||
.map_err(|error| error.to_string())?
|
||||
.with_base_url(read_base_url());
|
||||
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())?;
|
||||
Ok(match scope {
|
||||
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> {
|
||||
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));
|
||||
}
|
||||
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> {
|
||||
@@ -4490,19 +4495,19 @@ mod tests {
|
||||
));
|
||||
let home = root.join("home");
|
||||
let cwd = root.join("cwd");
|
||||
std::fs::create_dir_all(home.join(".claude")).expect("home dir");
|
||||
std::fs::create_dir_all(cwd.join(".claude")).expect("cwd dir");
|
||||
std::fs::create_dir_all(home.join(".claw")).expect("home dir");
|
||||
std::fs::create_dir_all(cwd.join(".claw")).expect("cwd dir");
|
||||
std::fs::write(
|
||||
home.join(".claude").join("settings.json"),
|
||||
home.join(".claw").join("settings.json"),
|
||||
r#"{"verbose":false}"#,
|
||||
)
|
||||
.expect("write global settings");
|
||||
|
||||
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");
|
||||
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");
|
||||
|
||||
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),
|
||||
None => std::env::remove_var("HOME"),
|
||||
}
|
||||
match original_claude_home {
|
||||
Some(value) => std::env::set_var("CLAUDE_CONFIG_HOME", value),
|
||||
None => std::env::remove_var("CLAUDE_CONFIG_HOME"),
|
||||
match original_claw_home {
|
||||
Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value),
|
||||
None => std::env::remove_var("CLAW_CONFIG_HOME"),
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user