diff --git a/Cargo.lock b/Cargo.lock index 71a9d42..f0c900f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1886,23 +1886,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "signal-gateway-app-code" -version = "0.1.0" -dependencies = [ - "async-trait", - "flate2", - "globset", - "regex", - "reqwest", - "serde", - "serde_json", - "signal-gateway-assistant", - "tar", - "tokio", - "tracing", -] - [[package]] name = "signal-gateway-assistant" version = "0.1.0" @@ -1951,9 +1934,9 @@ dependencies = [ "serde", "serde_json", "signal-gateway", - "signal-gateway-app-code", "signal-gateway-assistant-claude", "signal-gateway-log-ingest", + "signal-gateway-repo-code", "tokio", "tokio-util", "toml", @@ -1977,6 +1960,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "signal-gateway-repo-code" +version = "0.1.0" +dependencies = [ + "async-trait", + "flate2", + "globset", + "regex", + "reqwest", + "serde", + "serde_json", + "signal-gateway-assistant", + "tar", + "tokio", + "tracing", +] + [[package]] name = "signal-hook-registry" version = "1.4.7" diff --git a/Cargo.toml b/Cargo.toml index 71506eb..1655887 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = [ "prometheus-http-client", "signal-gateway", - "signal-gateway-app-code", + "signal-gateway-repo-code", "signal-gateway-assistant", "signal-gateway-assistant/claude", "signal-gateway-bin", diff --git a/signal-gateway-bin/Cargo.toml b/signal-gateway-bin/Cargo.toml index f7a97b5..d2a9875 100644 --- a/signal-gateway-bin/Cargo.toml +++ b/signal-gateway-bin/Cargo.toml @@ -18,7 +18,7 @@ rustls-tls = ["signal-gateway/rustls-tls"] [dependencies] signal-gateway = { workspace = true } signal-gateway-assistant-claude = { workspace = true } -signal-gateway-app-code = { path = "../signal-gateway-app-code" } +signal-gateway-repo-code = { path = "../signal-gateway-repo-code" } signal-gateway-log-ingest = { path = "../signal-gateway-log-ingest" } async-trait = { workspace = true } diff --git a/signal-gateway-bin/src/main.rs b/signal-gateway-bin/src/main.rs index 08338d3..6057956 100644 --- a/signal-gateway-bin/src/main.rs +++ b/signal-gateway-bin/src/main.rs @@ -5,8 +5,8 @@ use conf::Conf; use metrics_exporter_prometheus::PrometheusBuilder; use signal_gateway::{CommandRouter, Gateway, GatewayConfig, Handling}; -use signal_gateway_app_code::AppCodeTools; use signal_gateway_assistant_claude::{ClaudeAssistant, ClaudeConfig}; +use signal_gateway_repo_code::RepoCodeTools; use std::{env, fs, net::SocketAddr, path::PathBuf, sync::Arc}; use tokio::net::TcpListener; use tokio_util::sync::CancellationToken; @@ -16,8 +16,8 @@ use tracing_subscriber::EnvFilter; mod admin_http; use admin_http::AdminHttpConfig; -mod app_code; -use app_code::AppCodeConfigExt; +mod repo_code; +use repo_code::RepoCodeConfigExt; mod listen_http; use listen_http::start_http_task; @@ -51,7 +51,7 @@ pub struct Config { admin_http: Option, /// Application source code configurations for Claude tools. #[conf(long, env, value_parser = serde_json::from_str, default, default_help_str = "[]")] - app_code: Vec, + app_code: Vec, /// Claude API configuration for AI-powered responses. #[conf(flatten, prefix)] claude: Option, @@ -150,7 +150,7 @@ async fn main() -> Result<(), Box> { router_builder.build() }; - // Build AppCode tools if configured + // Build RepoCode tools if configured let app_code_tools = if !config.app_code.is_empty() { let mut apps = Vec::new(); for app_config in config.app_code { @@ -163,7 +163,7 @@ async fn main() -> Result<(), Box> { } } } - Some(Arc::new(AppCodeTools::new(apps))) + Some(Arc::new(RepoCodeTools::new(apps))) } else { None }; diff --git a/signal-gateway-bin/src/app_code.rs b/signal-gateway-bin/src/repo_code.rs similarity index 81% rename from signal-gateway-bin/src/app_code.rs rename to signal-gateway-bin/src/repo_code.rs index f27a12d..40bc12d 100644 --- a/signal-gateway-bin/src/app_code.rs +++ b/signal-gateway-bin/src/repo_code.rs @@ -1,24 +1,24 @@ //! Extended configuration for application source code browsing. use serde::Deserialize; -use signal_gateway_app_code::{AppCode, AppCodeConfig, ShaCallback}; +use signal_gateway_repo_code::{RepoCode, RepoCodeConfig, ShaCallback}; use std::sync::Arc; use tracing::warn; use url::Url; -/// Extended configuration for AppCode with HTTP-based SHA fetching. +/// Extended configuration for RepoCode with HTTP-based SHA fetching. #[derive(Clone, Debug, Deserialize)] -pub struct AppCodeConfigExt { - /// The base AppCode configuration. +pub struct RepoCodeConfigExt { + /// The base RepoCode configuration. #[serde(flatten)] - pub config: AppCodeConfig, + pub config: RepoCodeConfig, /// URL to GET the current deployed version SHA. pub version_sha_http_get: Url, } -impl AppCodeConfigExt { - /// Convert to an AppCode instance with HTTP-based SHA callback. - pub fn into_app_code(self) -> Result { +impl RepoCodeConfigExt { + /// Convert to an RepoCode instance with HTTP-based SHA callback. + pub fn into_app_code(self) -> Result { let url = self.version_sha_http_get.clone(); let client = reqwest::Client::new(); @@ -66,6 +66,6 @@ impl AppCodeConfigExt { }) }); - AppCode::new(self.config, sha_callback) + RepoCode::new(self.config, sha_callback) } } diff --git a/signal-gateway-app-code/Cargo.toml b/signal-gateway-repo-code/Cargo.toml similarity index 94% rename from signal-gateway-app-code/Cargo.toml rename to signal-gateway-repo-code/Cargo.toml index 58e36db..d66ba30 100644 --- a/signal-gateway-app-code/Cargo.toml +++ b/signal-gateway-repo-code/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "signal-gateway-app-code" +name = "signal-gateway-repo-code" version = "0.1.0" edition.workspace = true diff --git a/signal-gateway-app-code/src/lib.rs b/signal-gateway-repo-code/src/lib.rs similarity index 95% rename from signal-gateway-app-code/src/lib.rs rename to signal-gateway-repo-code/src/lib.rs index aaae314..bd4ad56 100644 --- a/signal-gateway-app-code/src/lib.rs +++ b/signal-gateway-repo-code/src/lib.rs @@ -55,7 +55,7 @@ impl TryFrom for GitHubRepo { /// Configuration for an application's source code access. #[derive(Clone, Debug, Deserialize)] -pub struct AppCodeConfig { +pub struct RepoCodeConfig { /// Name of the application (used to identify it in tool calls). pub name: String, /// GitHub repository in "owner/repo" format. @@ -101,8 +101,8 @@ pub type ShaCallback = Arc< /// Application source code browser. /// /// Downloads and caches GitHub tarballs for browsing application source code. -pub struct AppCode { - config: AppCodeConfig, +pub struct RepoCode { + config: RepoCodeConfig, token: Option, glob_filter: Option, get_sha: ShaCallback, @@ -110,12 +110,12 @@ pub struct AppCode { cache: Mutex>, } -impl AppCode { - /// Create a new AppCode instance from configuration. +impl RepoCode { + /// Create a new RepoCode instance from configuration. /// /// The `get_sha` callback is called to determine which git SHA to download. /// It should return `None` if the SHA is not yet known. - pub fn new(config: AppCodeConfig, get_sha: ShaCallback) -> Result { + pub fn new(config: RepoCodeConfig, get_sha: ShaCallback) -> Result { let token = config .token_file .as_ref() @@ -123,20 +123,21 @@ impl AppCode { .transpose()?; // Compile glob patterns if any are specified - let glob_filter = if config.glob.is_empty() { - None - } else { - let mut builder = GlobSetBuilder::new(); - for pattern in &config.glob { - let glob = Glob::new(pattern).map_err(|e| { - std::io::Error::other(format!("invalid glob pattern '{}': {}", pattern, e)) - })?; - builder.add(glob); - } - Some(builder.build().map_err(|e| { - std::io::Error::other(format!("failed to build glob set: {}", e)) - })?) - }; + let glob_filter = + if config.glob.is_empty() { + None + } else { + let mut builder = GlobSetBuilder::new(); + for pattern in &config.glob { + let glob = Glob::new(pattern).map_err(|e| { + std::io::Error::other(format!("invalid glob pattern '{}': {}", pattern, e)) + })?; + builder.add(glob); + } + Some(builder.build().map_err(|e| { + std::io::Error::other(format!("failed to build glob set: {}", e)) + })?) + }; Ok(Self { config, @@ -589,18 +590,18 @@ fn looks_binary(content: &str) -> bool { } /// Tool executor for multiple application source code browsers. -pub struct AppCodeTools { - apps: Vec, +pub struct RepoCodeTools { + apps: Vec, } -impl AppCodeTools { - /// Create a new AppCodeTools instance. - pub fn new(apps: Vec) -> Self { +impl RepoCodeTools { + /// Create a new RepoCodeTools instance. + pub fn new(apps: Vec) -> Self { Self { apps } } /// Find an app by name. - fn find_app(&self, name: &str) -> Option<&AppCode> { + fn find_app(&self, name: &str) -> Option<&RepoCode> { self.apps.iter().find(|app| app.name() == name) } @@ -644,7 +645,7 @@ struct SearchInput { } #[async_trait] -impl ToolExecutor for AppCodeTools { +impl ToolExecutor for RepoCodeTools { fn tools(&self) -> Vec { vec![ Tool { diff --git a/signal-gateway-app-code/tests/integration.rs b/signal-gateway-repo-code/tests/integration.rs similarity index 72% rename from signal-gateway-app-code/tests/integration.rs rename to signal-gateway-repo-code/tests/integration.rs index a3c2bd2..44358f4 100644 --- a/signal-gateway-app-code/tests/integration.rs +++ b/signal-gateway-repo-code/tests/integration.rs @@ -1,9 +1,9 @@ -//! Integration tests for signal-gateway-app-code. +//! Integration tests for signal-gateway-repo-code. //! //! These tests exercise the GitHub tarball download and file browsing functionality //! against a real public repository at a pinned commit. -use signal_gateway_app_code::{AppCode, AppCodeConfig, GitHubRepo, ShaCallback}; +use signal_gateway_repo_code::{GitHubRepo, RepoCode, RepoCodeConfig, ShaCallback}; use std::sync::Arc; /// Test against cbeck88/ver-stub-rs at a known commit. @@ -12,12 +12,12 @@ const TEST_OWNER: &str = "cbeck88"; const TEST_REPO: &str = "ver-stub-rs"; const TEST_SHA: &str = "79b98e25f27ae4f5dd73a5a3d8f37dad655a57e8"; -fn create_test_app_code() -> AppCode { +fn create_test_app_code() -> RepoCode { create_test_app_code_with_glob(vec![]) } -fn create_test_app_code_with_glob(glob: Vec) -> AppCode { - let config = AppCodeConfig { +fn create_test_app_code_with_glob(glob: Vec) -> RepoCode { + let config = RepoCodeConfig { name: "test-app".to_string(), github: GitHubRepo { owner: TEST_OWNER.to_string(), @@ -34,7 +34,7 @@ fn create_test_app_code_with_glob(glob: Vec) -> AppCode { Box::pin(async move { Ok(sha) }) }); - AppCode::new(config, sha_callback).expect("Failed to create AppCode") + RepoCode::new(config, sha_callback).expect("Failed to create RepoCode") } #[tokio::test] @@ -46,8 +46,14 @@ async fn test_ls_root() { // Verify expected top-level entries exist assert!(result.contains("Cargo.toml"), "Should contain Cargo.toml"); assert!(result.contains("README.md"), "Should contain README.md"); - assert!(result.contains("ver-stub/"), "Should contain ver-stub/ directory"); - assert!(result.contains("ver-stub-build/"), "Should contain ver-stub-build/ directory"); + assert!( + result.contains("ver-stub/"), + "Should contain ver-stub/ directory" + ); + assert!( + result.contains("ver-stub-build/"), + "Should contain ver-stub-build/ directory" + ); assert!(result.contains("tests.sh"), "Should contain tests.sh"); } @@ -69,30 +75,48 @@ async fn test_find_rust_files() { let result = app.find(Some("*.rs")).await.expect("find failed"); // Should find Rust source files - assert!(result.contains("ver-stub/src/lib.rs"), "Should find ver-stub/src/lib.rs"); - assert!(result.contains("ver-stub-build/src/lib.rs"), "Should find ver-stub-build/src/lib.rs"); + assert!( + result.contains("ver-stub/src/lib.rs"), + "Should find ver-stub/src/lib.rs" + ); + assert!( + result.contains("ver-stub-build/src/lib.rs"), + "Should find ver-stub-build/src/lib.rs" + ); } #[tokio::test] async fn test_find_with_path_pattern() { let app = create_test_app_code(); - let result = app.find(Some("ver-stub-build/src/*.rs")).await.expect("find failed"); + let result = app + .find(Some("ver-stub-build/src/*.rs")) + .await + .expect("find failed"); // Should find files in ver-stub-build/src/ - assert!(result.contains("ver-stub-build/src/lib.rs"), "Should find lib.rs"); + assert!( + result.contains("ver-stub-build/src/lib.rs"), + "Should find lib.rs" + ); } #[tokio::test] async fn test_read_cargo_toml() { let app = create_test_app_code(); - let result = app.read("Cargo.toml", None, None).await.expect("read failed"); + let result = app + .read("Cargo.toml", None, None) + .await + .expect("read failed"); // Verify content matches what we know is in the file assert!(result.contains("[workspace]"), "Should contain [workspace]"); assert!(result.contains("ver-stub"), "Should contain ver-stub"); - assert!(result.contains("ver-stub-build"), "Should contain ver-stub-build"); + assert!( + result.contains("ver-stub-build"), + "Should contain ver-stub-build" + ); } #[tokio::test] @@ -100,14 +124,20 @@ async fn test_read_with_line_range() { let app = create_test_app_code(); // Read just the first 5 lines - let result = app.read("Cargo.toml", Some(1), Some(5)).await.expect("read failed"); + let result = app + .read("Cargo.toml", Some(1), Some(5)) + .await + .expect("read failed"); // Should only have 5 lines let line_count = result.lines().count(); assert_eq!(line_count, 5, "Should have exactly 5 lines"); // First line should be [workspace] - assert!(result.contains("[workspace]"), "First lines should contain [workspace]"); + assert!( + result.contains("[workspace]"), + "First lines should contain [workspace]" + ); } #[tokio::test] @@ -127,22 +157,37 @@ async fn test_read_nonexistent_file() { async fn test_search_simple() { let app = create_test_app_code(); - let result = app.search("workspace", 0, None).await.expect("search failed"); + let result = app + .search("workspace", 0, None) + .await + .expect("search failed"); // Should find "workspace" in Cargo.toml - assert!(result.contains("Cargo.toml"), "Should find match in Cargo.toml"); + assert!( + result.contains("Cargo.toml"), + "Should find match in Cargo.toml" + ); } #[tokio::test] async fn test_search_with_context() { let app = create_test_app_code(); - let result = app.search("resolver", 2, None).await.expect("search failed"); + let result = app + .search("resolver", 2, None) + .await + .expect("search failed"); // Should have context lines around the match - assert!(result.contains("Cargo.toml"), "Should find match in Cargo.toml"); + assert!( + result.contains("Cargo.toml"), + "Should find match in Cargo.toml" + ); // With context, should see surrounding lines - assert!(result.contains("[workspace]"), "Should show context including [workspace]"); + assert!( + result.contains("[workspace]"), + "Should show context including [workspace]" + ); } #[tokio::test] @@ -191,8 +236,14 @@ async fn test_glob_filter_rust_files_only() { assert!(result.contains(".rs"), "Should contain .rs files"); // Should NOT find non-Rust files - assert!(!result.contains("Cargo.toml"), "Should not contain Cargo.toml"); - assert!(!result.contains("README.md"), "Should not contain README.md"); + assert!( + !result.contains("Cargo.toml"), + "Should not contain Cargo.toml" + ); + assert!( + !result.contains("README.md"), + "Should not contain README.md" + ); assert!(!result.contains("tests.sh"), "Should not contain tests.sh"); } @@ -214,16 +265,16 @@ async fn test_glob_filter_specific_directory() { !result.contains("ver-stub-build/"), "Should not contain ver-stub-build files" ); - assert!(!result.contains("Cargo.toml"), "Should not contain root Cargo.toml"); + assert!( + !result.contains("Cargo.toml"), + "Should not contain root Cargo.toml" + ); } #[tokio::test] async fn test_glob_filter_multiple_patterns() { // Include both Cargo.toml files and shell scripts - let app = create_test_app_code_with_glob(vec![ - "**/Cargo.toml".to_string(), - "*.sh".to_string(), - ]); + let app = create_test_app_code_with_glob(vec!["**/Cargo.toml".to_string(), "*.sh".to_string()]); let result = app.find(Some("*")).await.expect("find failed"); @@ -234,7 +285,10 @@ async fn test_glob_filter_multiple_patterns() { assert!(result.contains("tests.sh"), "Should contain tests.sh"); // Should NOT find other files - assert!(!result.contains("README.md"), "Should not contain README.md"); + assert!( + !result.contains("README.md"), + "Should not contain README.md" + ); assert!(!result.contains(".rs"), "Should not contain .rs files"); }