#![doc = include_str!("../README.md")]
pub mod agent;
pub mod cursor;
pub mod gemini;
pub mod git;
pub mod limits;
pub mod paths;
pub mod questions;
mod texts;
pub mod tools;
pub mod vibe;
pub use agent::Agent;
pub use tools::{call_id, content_text, diff_of, line_hunks, tool_of};
use serde_json::{json, Value};
pub const PROTOCOL: u64 = 1;
pub const SERVER: &str = "escouade";
pub fn mcp_servers(escouade: Option<(&str, &str)>) -> Value {
match escouade {
Some((url, token)) => json!([{
"type": "http", "name": SERVER, "url": url,
"headers": [{ "name": "Authorization", "value": format!("Bearer {token}") }],
}]),
None => json!([]),
}
}
pub fn preamble(instructions: &str) -> String {
format!("<instructions>\n{}\n</instructions>", instructions.trim())
}
pub fn relayed(relay: &str) -> String {
format!("<relais_escouade>\n{}\n</relais_escouade>", relay.trim())
}
pub fn option_of(params: &Value, kind: &str) -> String {
params["options"]
.as_array()
.into_iter()
.flatten()
.find(|o| o["kind"] == kind)
.and_then(|o| o["optionId"].as_str())
.map(str::to_string)
.unwrap_or_else(|| kind.replace('_', "-"))
}
pub fn names(text: &str, path: &str) -> bool {
let norm = |s: &str| {
let s = s.replace('\\', "/");
let s = s.trim_end_matches('/').to_string();
if cfg!(windows) {
s.to_lowercase()
} else {
s
}
};
let p = norm(path);
!p.is_empty() && norm(text).contains(&p)
}
pub fn fenced(params: &Value, fence: &[String]) -> bool {
if fence.is_empty() {
return false;
}
let tc = ¶ms["toolCall"];
let mut texts: Vec<&str> = tc["title"].as_str().into_iter().collect();
for v in tc["rawInput"]
.as_object()
.into_iter()
.flatten()
.map(|(_, v)| v)
{
texts.extend(v.as_str());
}
for k in ["content", "locations"] {
texts.extend(
tc[k]
.as_array()
.into_iter()
.flatten()
.filter_map(|c| c["path"].as_str()),
);
}
texts.iter().any(|t| fence.iter().any(|p| names(t, p)))
}
pub fn inside(path: &str, dir: &str) -> bool {
let norm = |p: &str| p.replace('\\', "/").trim_end_matches('/').to_lowercase();
let (p, d) = (norm(path), norm(dir));
!d.is_empty() && (p == d || p.starts_with(&format!("{d}/")))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_token_goes_in_a_header_never_elsewhere() {
let s = mcp_servers(Some(("http://127.0.0.1:1/mcp", "s3cr3t")));
assert_eq!(s[0]["url"], "http://127.0.0.1:1/mcp");
assert_eq!(s[0]["headers"][0]["value"], "Bearer s3cr3t");
assert_eq!(mcp_servers(None), json!([]));
}
#[test]
fn a_permission_naming_a_fenced_path_is_fenced() {
let p = json!({ "toolCall": { "title": "cat C:\\Prive\\a.txt", "locations": [] } });
assert!(
fenced(&p, &["c:/prive".into()]) == cfg!(windows) || fenced(&p, &["C:\\Prive".into()])
);
assert!(!fenced(&p, &[]));
let q = json!({ "toolCall": { "locations": [{ "path": "/srv/secret/x" }] } });
assert!(fenced(&q, &["/srv/secret".into()]));
assert!(!fenced(&q, &["/srv/other".into()]));
}
#[test]
fn an_option_is_found_by_its_kind_or_named_after_it() {
let p = json!({ "options": [{ "kind": "allow_once", "optionId": "yes" }] });
assert_eq!(option_of(&p, "allow_once"), "yes");
assert_eq!(option_of(&p, "reject_once"), "reject-once");
}
}