use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
use crate::demo::protocol_harness::{DemoProtocol, ProtocolMetadata};
pub struct CliDemoAdapter;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CliRequest {
pub path: String,
pub command: String,
pub args: Vec<String>,
pub show_api: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CliResponse {
pub command: String,
pub execution_time_ms: u64,
pub output_format: String,
pub cache_key: String,
pub result: Value,
pub api_trace: Option<CliApiTrace>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CliApiTrace {
pub command_line: String,
pub working_directory: String,
pub environment: Vec<(String, String)>,
pub exit_code: i32,
pub stdout_lines: usize,
pub stderr_lines: usize,
}
#[derive(Debug, Error)]
pub enum CliDemoError {
#[error("Invalid path: {0}")]
InvalidPath(String),
#[error("Command execution failed: {0}")]
ExecutionFailed(String),
#[error("JSON parsing error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
}
impl CliDemoAdapter {
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn new() -> Self {
Self
}
async fn execute_context_analysis(&self, path: &str) -> Result<Value, CliDemoError> {
use crate::services::deep_context::{AnalysisType, DeepContextAnalyzer, DeepContextConfig};
use std::path::PathBuf;
let path_buf = if path.starts_with("https://") || path.starts_with("git@") {
return Err(CliDemoError::InvalidPath(format!(
"URL should have been cloned before reaching CLI adapter: {path}"
)));
} else {
let p = PathBuf::from(path);
if !p.exists() {
return Err(CliDemoError::InvalidPath(format!(
"Path does not exist: {path}"
)));
}
p
};
let config = DeepContextConfig {
include_analyses: vec![
AnalysisType::Ast,
AnalysisType::Complexity,
AnalysisType::Churn,
AnalysisType::Dag,
AnalysisType::DeadCode,
AnalysisType::Satd,
AnalysisType::TechnicalDebtGradient,
],
period_days: 30,
..DeepContextConfig::default()
};
let analyzer = DeepContextAnalyzer::new(config);
let deep_context = analyzer.analyze_project(&path_buf).await.map_err(|e| {
CliDemoError::ExecutionFailed(format!("Deep context analysis failed: {e}"))
})?;
let mut result = serde_json::to_value(&deep_context)?;
if let Some(obj) = result.as_object_mut() {
obj.insert(
"cli_metadata".to_string(),
serde_json::json!({
"command": "paiml-mcp-agent-toolkit analyze context --format json",
"version": env!("CARGO_PKG_VERSION"),
"protocol": "cli"
}),
);
}
Ok(result)
}
fn generate_cache_key(&self, path: &str) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
path.hash(&mut hasher);
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("internal error")
.as_nanos()
.hash(&mut hasher);
format!("sha256:{:x}", hasher.finish())
}
fn create_api_trace(
&self,
request: &CliRequest,
_execution_time_ms: u64,
result: &Value,
) -> CliApiTrace {
let command_line = format!(
"paiml-mcp-agent-toolkit {} {}",
request.command,
request.args.join(" ")
);
let result_str = serde_json::to_string_pretty(result).unwrap_or_default();
let stdout_lines = result_str.lines().count();
CliApiTrace {
command_line,
working_directory: std::env::current_dir().map_or_else(
|_| "unknown".to_string(),
|p| p.to_string_lossy().to_string(),
),
environment: vec![
(
"RUST_LOG".to_string(),
std::env::var("RUST_LOG").unwrap_or_default(),
),
(
"PATH".to_string(),
std::env::var("PATH").unwrap_or_default(),
),
],
exit_code: 0,
stdout_lines,
stderr_lines: 0,
}
}
}
impl Default for CliDemoAdapter {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl DemoProtocol for CliDemoAdapter {
type Request = CliRequest;
type Response = CliResponse;
type Error = CliDemoError;
async fn decode_request(&self, raw: &[u8]) -> Result<Self::Request, Self::Error> {
let value: Value = serde_json::from_slice(raw)?;
let path = value
.get("path")
.and_then(|v| v.as_str())
.unwrap_or(".")
.to_string();
let show_api = value
.get("show_api")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
Ok(CliRequest {
path,
command: "analyze context".to_string(),
args: vec!["--format".to_string(), "json".to_string()],
show_api,
})
}
async fn encode_response(&self, resp: Self::Response) -> Result<Vec<u8>, Self::Error> {
let json = serde_json::to_vec_pretty(&resp)?;
Ok(json)
}
async fn get_protocol_metadata(&self) -> ProtocolMetadata {
ProtocolMetadata {
name: "cli",
version: "1.0.0",
description: "Command-line interface protocol for direct execution".to_string(),
request_schema: serde_json::json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to analyze",
"default": "."
},
"show_api": {
"type": "boolean",
"description": "Show API introspection information",
"default": false
}
},
"required": []
}),
response_schema: serde_json::json!({
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Executed command"
},
"execution_time_ms": {
"type": "integer",
"description": "Execution time in milliseconds"
},
"output_format": {
"type": "string",
"description": "Output format used"
},
"cache_key": {
"type": "string",
"description": "Cache key for this result"
},
"result": {
"type": "object",
"description": "Analysis result data"
},
"api_trace": {
"type": "object",
"description": "API execution trace (optional)"
}
},
"required": ["command", "execution_time_ms", "result"]
}),
example_requests: vec![
serde_json::json!({
"path": "/path/to/repo",
"show_api": false
}),
serde_json::json!({
"path": ".",
"show_api": true
}),
],
capabilities: vec![
"direct_execution".to_string(),
"filesystem_access".to_string(),
"binary_invocation".to_string(),
"api_introspection".to_string(),
],
}
}
async fn execute_demo(&self, request: Self::Request) -> Result<Self::Response, Self::Error> {
let start_time = std::time::Instant::now();
let result = self.execute_context_analysis(&request.path).await?;
let execution_time_ms = start_time.elapsed().as_millis() as u64;
let cache_key = self.generate_cache_key(&request.path);
let api_trace = if request.show_api {
Some(self.create_api_trace(&request, execution_time_ms, &result))
} else {
None
};
Ok(CliResponse {
command: format!("{} {}", request.command, request.args.join(" ")),
execution_time_ms,
output_format: "JSON".to_string(),
cache_key,
result,
api_trace,
})
}
}
impl From<Value> for CliRequest {
fn from(value: Value) -> Self {
let path = value
.get("path")
.and_then(|v| v.as_str())
.unwrap_or(".")
.to_string();
let show_api = value
.get("show_api")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
CliRequest {
path,
command: "analyze context".to_string(),
args: vec!["--format".to_string(), "json".to_string()],
show_api,
}
}
}
impl From<CliResponse> for Value {
fn from(val: CliResponse) -> Self {
serde_json::to_value(val).unwrap_or(Value::Null)
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_cli_adapter_metadata() {
let adapter = CliDemoAdapter::new();
let metadata = adapter.get_protocol_metadata().await;
assert_eq!(metadata.name, "cli");
assert_eq!(metadata.version, "1.0.0");
assert!(!metadata.capabilities.is_empty());
assert!(!metadata.example_requests.is_empty());
}
#[test]
fn test_cli_request_from_value() {
let value = serde_json::json!({
"path": "/test/path",
"show_api": true
});
let request = CliRequest::from(value);
assert_eq!(request.path, "/test/path");
assert!(request.show_api);
assert_eq!(request.command, "analyze context");
}
#[test]
fn test_cache_key_generation() {
let adapter = CliDemoAdapter::new();
let key1 = adapter.generate_cache_key("/test/path");
let key2 = adapter.generate_cache_key("/test/path");
assert_ne!(key1, key2);
assert!(key1.starts_with("sha256:"));
assert!(key2.starts_with("sha256:"));
}
#[test]
fn test_api_trace_creation() {
let adapter = CliDemoAdapter::new();
let request = CliRequest {
path: "/test".to_string(),
command: "analyze context".to_string(),
args: vec!["--format".to_string(), "json".to_string()],
show_api: true,
};
let result = serde_json::json!({"test": "data"});
let trace = adapter.create_api_trace(&request, 1000, &result);
assert!(trace.command_line.contains("analyze context"));
assert_eq!(trace.exit_code, 0);
assert!(trace.stdout_lines > 0);
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}