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",
]
[[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"
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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 }
+6 -6
View File
@@ -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<AdminHttpConfig>,
/// Application source code configurations for Claude tools.
#[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.
#[conf(flatten, prefix)]
claude: Option<ClaudeConfig>,
@@ -150,7 +150,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
}
}
}
Some(Arc::new(AppCodeTools::new(apps)))
Some(Arc::new(RepoCodeTools::new(apps)))
} else {
None
};
@@ -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<AppCode, std::io::Error> {
impl RepoCodeConfigExt {
/// Convert to an RepoCode instance with HTTP-based SHA callback.
pub fn into_app_code(self) -> Result<RepoCode, std::io::Error> {
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)
}
}
@@ -1,5 +1,5 @@
[package]
name = "signal-gateway-app-code"
name = "signal-gateway-repo-code"
version = "0.1.0"
edition.workspace = true
@@ -55,7 +55,7 @@ impl TryFrom<String> 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<String>,
glob_filter: Option<GlobSet>,
get_sha: ShaCallback,
@@ -110,12 +110,12 @@ pub struct AppCode {
cache: Mutex<Option<CachedTarball>>,
}
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<Self, std::io::Error> {
pub fn new(config: RepoCodeConfig, get_sha: ShaCallback) -> Result<Self, std::io::Error> {
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<AppCode>,
pub struct RepoCodeTools {
apps: Vec<RepoCode>,
}
impl AppCodeTools {
/// Create a new AppCodeTools instance.
pub fn new(apps: Vec<AppCode>) -> Self {
impl RepoCodeTools {
/// Create a new RepoCodeTools instance.
pub fn new(apps: Vec<RepoCode>) -> 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<Tool> {
vec![
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
//! 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<String>) -> AppCode {
let config = AppCodeConfig {
fn create_test_app_code_with_glob(glob: Vec<String>) -> 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<String>) -> 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");
}