rename app-code crate to repo-code

This commit is contained in:
Chris Beck
2025-12-17 23:46:29 -07:00
parent c38c035d0b
commit 1a67da9e3d
8 changed files with 147 additions and 92 deletions
Generated
+18 -18
View File
@@ -1886,23 +1886,6 @@ dependencies = [
"walkdir", "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]] [[package]]
name = "signal-gateway-assistant" name = "signal-gateway-assistant"
version = "0.1.0" version = "0.1.0"
@@ -1951,9 +1934,9 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"signal-gateway", "signal-gateway",
"signal-gateway-app-code",
"signal-gateway-assistant-claude", "signal-gateway-assistant-claude",
"signal-gateway-log-ingest", "signal-gateway-log-ingest",
"signal-gateway-repo-code",
"tokio", "tokio",
"tokio-util", "tokio-util",
"toml", "toml",
@@ -1977,6 +1960,23 @@ dependencies = [
"tracing", "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]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.7" version = "1.4.7"
+1 -1
View File
@@ -3,7 +3,7 @@ resolver = "2"
members = [ members = [
"prometheus-http-client", "prometheus-http-client",
"signal-gateway", "signal-gateway",
"signal-gateway-app-code", "signal-gateway-repo-code",
"signal-gateway-assistant", "signal-gateway-assistant",
"signal-gateway-assistant/claude", "signal-gateway-assistant/claude",
"signal-gateway-bin", "signal-gateway-bin",
+1 -1
View File
@@ -18,7 +18,7 @@ rustls-tls = ["signal-gateway/rustls-tls"]
[dependencies] [dependencies]
signal-gateway = { workspace = true } signal-gateway = { workspace = true }
signal-gateway-assistant-claude = { 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" } signal-gateway-log-ingest = { path = "../signal-gateway-log-ingest" }
async-trait = { workspace = true } async-trait = { workspace = true }
+6 -6
View File
@@ -5,8 +5,8 @@
use conf::Conf; use conf::Conf;
use metrics_exporter_prometheus::PrometheusBuilder; use metrics_exporter_prometheus::PrometheusBuilder;
use signal_gateway::{CommandRouter, Gateway, GatewayConfig, Handling}; use signal_gateway::{CommandRouter, Gateway, GatewayConfig, Handling};
use signal_gateway_app_code::AppCodeTools;
use signal_gateway_assistant_claude::{ClaudeAssistant, ClaudeConfig}; use signal_gateway_assistant_claude::{ClaudeAssistant, ClaudeConfig};
use signal_gateway_repo_code::RepoCodeTools;
use std::{env, fs, net::SocketAddr, path::PathBuf, sync::Arc}; use std::{env, fs, net::SocketAddr, path::PathBuf, sync::Arc};
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
@@ -16,8 +16,8 @@ use tracing_subscriber::EnvFilter;
mod admin_http; mod admin_http;
use admin_http::AdminHttpConfig; use admin_http::AdminHttpConfig;
mod app_code; mod repo_code;
use app_code::AppCodeConfigExt; use repo_code::RepoCodeConfigExt;
mod listen_http; mod listen_http;
use listen_http::start_http_task; use listen_http::start_http_task;
@@ -51,7 +51,7 @@ pub struct Config {
admin_http: Option<AdminHttpConfig>, admin_http: Option<AdminHttpConfig>,
/// Application source code configurations for Claude tools. /// Application source code configurations for Claude tools.
#[conf(long, env, value_parser = serde_json::from_str, default, default_help_str = "[]")] #[conf(long, env, value_parser = serde_json::from_str, default, default_help_str = "[]")]
app_code: Vec<AppCodeConfigExt>, app_code: Vec<RepoCodeConfigExt>,
/// Claude API configuration for AI-powered responses. /// Claude API configuration for AI-powered responses.
#[conf(flatten, prefix)] #[conf(flatten, prefix)]
claude: Option<ClaudeConfig>, claude: Option<ClaudeConfig>,
@@ -150,7 +150,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
router_builder.build() router_builder.build()
}; };
// Build AppCode tools if configured // Build RepoCode tools if configured
let app_code_tools = if !config.app_code.is_empty() { let app_code_tools = if !config.app_code.is_empty() {
let mut apps = Vec::new(); let mut apps = Vec::new();
for app_config in config.app_code { for app_config in config.app_code {
@@ -163,7 +163,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
} }
} }
Some(Arc::new(AppCodeTools::new(apps))) Some(Arc::new(RepoCodeTools::new(apps)))
} else { } else {
None None
}; };
@@ -1,24 +1,24 @@
//! Extended configuration for application source code browsing. //! Extended configuration for application source code browsing.
use serde::Deserialize; use serde::Deserialize;
use signal_gateway_app_code::{AppCode, AppCodeConfig, ShaCallback}; use signal_gateway_repo_code::{RepoCode, RepoCodeConfig, ShaCallback};
use std::sync::Arc; use std::sync::Arc;
use tracing::warn; use tracing::warn;
use url::Url; 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)] #[derive(Clone, Debug, Deserialize)]
pub struct AppCodeConfigExt { pub struct RepoCodeConfigExt {
/// The base AppCode configuration. /// The base RepoCode configuration.
#[serde(flatten)] #[serde(flatten)]
pub config: AppCodeConfig, pub config: RepoCodeConfig,
/// URL to GET the current deployed version SHA. /// URL to GET the current deployed version SHA.
pub version_sha_http_get: Url, pub version_sha_http_get: Url,
} }
impl AppCodeConfigExt { impl RepoCodeConfigExt {
/// Convert to an AppCode instance with HTTP-based SHA callback. /// Convert to an RepoCode instance with HTTP-based SHA callback.
pub fn into_app_code(self) -> Result<AppCode, std::io::Error> { pub fn into_app_code(self) -> Result<RepoCode, std::io::Error> {
let url = self.version_sha_http_get.clone(); let url = self.version_sha_http_get.clone();
let client = reqwest::Client::new(); let client = reqwest::Client::new();
@@ -66,6 +66,6 @@ impl AppCodeConfigExt {
}) })
}); });
AppCode::new(self.config, sha_callback) RepoCode::new(self.config, sha_callback)
} }
} }
@@ -1,5 +1,5 @@
[package] [package]
name = "signal-gateway-app-code" name = "signal-gateway-repo-code"
version = "0.1.0" version = "0.1.0"
edition.workspace = true edition.workspace = true
@@ -55,7 +55,7 @@ impl TryFrom<String> for GitHubRepo {
/// Configuration for an application's source code access. /// Configuration for an application's source code access.
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
pub struct AppCodeConfig { pub struct RepoCodeConfig {
/// Name of the application (used to identify it in tool calls). /// Name of the application (used to identify it in tool calls).
pub name: String, pub name: String,
/// GitHub repository in "owner/repo" format. /// GitHub repository in "owner/repo" format.
@@ -101,8 +101,8 @@ pub type ShaCallback = Arc<
/// Application source code browser. /// Application source code browser.
/// ///
/// Downloads and caches GitHub tarballs for browsing application source code. /// Downloads and caches GitHub tarballs for browsing application source code.
pub struct AppCode { pub struct RepoCode {
config: AppCodeConfig, config: RepoCodeConfig,
token: Option<String>, token: Option<String>,
glob_filter: Option<GlobSet>, glob_filter: Option<GlobSet>,
get_sha: ShaCallback, get_sha: ShaCallback,
@@ -110,12 +110,12 @@ pub struct AppCode {
cache: Mutex<Option<CachedTarball>>, cache: Mutex<Option<CachedTarball>>,
} }
impl AppCode { impl RepoCode {
/// Create a new AppCode instance from configuration. /// Create a new RepoCode instance from configuration.
/// ///
/// The `get_sha` callback is called to determine which git SHA to download. /// The `get_sha` callback is called to determine which git SHA to download.
/// It should return `None` if the SHA is not yet known. /// It should return `None` if the SHA is not yet known.
pub fn new(config: AppCodeConfig, get_sha: ShaCallback) -> Result<Self, std::io::Error> { pub fn new(config: RepoCodeConfig, get_sha: ShaCallback) -> Result<Self, std::io::Error> {
let token = config let token = config
.token_file .token_file
.as_ref() .as_ref()
@@ -123,7 +123,8 @@ impl AppCode {
.transpose()?; .transpose()?;
// Compile glob patterns if any are specified // Compile glob patterns if any are specified
let glob_filter = if config.glob.is_empty() { let glob_filter =
if config.glob.is_empty() {
None None
} else { } else {
let mut builder = GlobSetBuilder::new(); let mut builder = GlobSetBuilder::new();
@@ -589,18 +590,18 @@ fn looks_binary(content: &str) -> bool {
} }
/// Tool executor for multiple application source code browsers. /// Tool executor for multiple application source code browsers.
pub struct AppCodeTools { pub struct RepoCodeTools {
apps: Vec<AppCode>, apps: Vec<RepoCode>,
} }
impl AppCodeTools { impl RepoCodeTools {
/// Create a new AppCodeTools instance. /// Create a new RepoCodeTools instance.
pub fn new(apps: Vec<AppCode>) -> Self { pub fn new(apps: Vec<RepoCode>) -> Self {
Self { apps } Self { apps }
} }
/// Find an app by name. /// 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) self.apps.iter().find(|app| app.name() == name)
} }
@@ -644,7 +645,7 @@ struct SearchInput {
} }
#[async_trait] #[async_trait]
impl ToolExecutor for AppCodeTools { impl ToolExecutor for RepoCodeTools {
fn tools(&self) -> Vec<Tool> { fn tools(&self) -> Vec<Tool> {
vec![ vec![
Tool { Tool {
@@ -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 //! These tests exercise the GitHub tarball download and file browsing functionality
//! against a real public repository at a pinned commit. //! 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; use std::sync::Arc;
/// Test against cbeck88/ver-stub-rs at a known commit. /// 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_REPO: &str = "ver-stub-rs";
const TEST_SHA: &str = "79b98e25f27ae4f5dd73a5a3d8f37dad655a57e8"; const TEST_SHA: &str = "79b98e25f27ae4f5dd73a5a3d8f37dad655a57e8";
fn create_test_app_code() -> AppCode { fn create_test_app_code() -> RepoCode {
create_test_app_code_with_glob(vec![]) create_test_app_code_with_glob(vec![])
} }
fn create_test_app_code_with_glob(glob: Vec<String>) -> AppCode { fn create_test_app_code_with_glob(glob: Vec<String>) -> RepoCode {
let config = AppCodeConfig { let config = RepoCodeConfig {
name: "test-app".to_string(), name: "test-app".to_string(),
github: GitHubRepo { github: GitHubRepo {
owner: TEST_OWNER.to_string(), owner: TEST_OWNER.to_string(),
@@ -34,7 +34,7 @@ fn create_test_app_code_with_glob(glob: Vec<String>) -> AppCode {
Box::pin(async move { Ok(sha) }) 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] #[tokio::test]
@@ -46,8 +46,14 @@ async fn test_ls_root() {
// Verify expected top-level entries exist // Verify expected top-level entries exist
assert!(result.contains("Cargo.toml"), "Should contain Cargo.toml"); assert!(result.contains("Cargo.toml"), "Should contain Cargo.toml");
assert!(result.contains("README.md"), "Should contain README.md"); assert!(result.contains("README.md"), "Should contain README.md");
assert!(result.contains("ver-stub/"), "Should contain ver-stub/ directory"); assert!(
assert!(result.contains("ver-stub-build/"), "Should contain ver-stub-build/ directory"); 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"); 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"); let result = app.find(Some("*.rs")).await.expect("find failed");
// Should find Rust source files // Should find Rust source files
assert!(result.contains("ver-stub/src/lib.rs"), "Should find ver-stub/src/lib.rs"); assert!(
assert!(result.contains("ver-stub-build/src/lib.rs"), "Should find ver-stub-build/src/lib.rs"); 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] #[tokio::test]
async fn test_find_with_path_pattern() { async fn test_find_with_path_pattern() {
let app = create_test_app_code(); 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/ // 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] #[tokio::test]
async fn test_read_cargo_toml() { async fn test_read_cargo_toml() {
let app = create_test_app_code(); 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 // Verify content matches what we know is in the file
assert!(result.contains("[workspace]"), "Should contain [workspace]"); assert!(result.contains("[workspace]"), "Should contain [workspace]");
assert!(result.contains("ver-stub"), "Should contain ver-stub"); 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] #[tokio::test]
@@ -100,14 +124,20 @@ async fn test_read_with_line_range() {
let app = create_test_app_code(); let app = create_test_app_code();
// Read just the first 5 lines // 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 // Should only have 5 lines
let line_count = result.lines().count(); let line_count = result.lines().count();
assert_eq!(line_count, 5, "Should have exactly 5 lines"); assert_eq!(line_count, 5, "Should have exactly 5 lines");
// First line should be [workspace] // 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] #[tokio::test]
@@ -127,22 +157,37 @@ async fn test_read_nonexistent_file() {
async fn test_search_simple() { async fn test_search_simple() {
let app = create_test_app_code(); 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 // 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] #[tokio::test]
async fn test_search_with_context() { async fn test_search_with_context() {
let app = create_test_app_code(); 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 // 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 // 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] #[tokio::test]
@@ -191,8 +236,14 @@ async fn test_glob_filter_rust_files_only() {
assert!(result.contains(".rs"), "Should contain .rs files"); assert!(result.contains(".rs"), "Should contain .rs files");
// Should NOT find non-Rust files // Should NOT find non-Rust files
assert!(!result.contains("Cargo.toml"), "Should not contain Cargo.toml"); assert!(
assert!(!result.contains("README.md"), "Should not contain README.md"); !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"); 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/"), !result.contains("ver-stub-build/"),
"Should not contain ver-stub-build files" "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] #[tokio::test]
async fn test_glob_filter_multiple_patterns() { async fn test_glob_filter_multiple_patterns() {
// Include both Cargo.toml files and shell scripts // Include both Cargo.toml files and shell scripts
let app = create_test_app_code_with_glob(vec![ let app = create_test_app_code_with_glob(vec!["**/Cargo.toml".to_string(), "*.sh".to_string()]);
"**/Cargo.toml".to_string(),
"*.sh".to_string(),
]);
let result = app.find(Some("*")).await.expect("find failed"); 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"); assert!(result.contains("tests.sh"), "Should contain tests.sh");
// Should NOT find other files // 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"); assert!(!result.contains(".rs"), "Should not contain .rs files");
} }