feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
use async_trait::async_trait;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
use std::process::Stdio;
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::types::{Tool, ToolError, ToolInputSchema, ToolResult, ToolUseContext};
|
||||
|
||||
const DEFAULT_HEAD_LIMIT: usize = 250;
|
||||
|
||||
pub struct GrepTool;
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for GrepTool {
|
||||
fn name(&self) -> &str {
|
||||
"Grep"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"A powerful search tool built on ripgrep. Supports full regex syntax, file type filtering, and multiple output modes."
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> ToolInputSchema {
|
||||
ToolInputSchema {
|
||||
schema_type: "object".to_string(),
|
||||
properties: HashMap::from([
|
||||
(
|
||||
"pattern".to_string(),
|
||||
json!({
|
||||
"type": "string",
|
||||
"description": "The regex pattern to search for"
|
||||
}),
|
||||
),
|
||||
(
|
||||
"path".to_string(),
|
||||
json!({
|
||||
"type": "string",
|
||||
"description": "File or directory to search in"
|
||||
}),
|
||||
),
|
||||
(
|
||||
"output_mode".to_string(),
|
||||
json!({
|
||||
"type": "string",
|
||||
"enum": ["content", "files_with_matches", "count"],
|
||||
"description": "Output mode (default: files_with_matches)"
|
||||
}),
|
||||
),
|
||||
(
|
||||
"glob".to_string(),
|
||||
json!({
|
||||
"type": "string",
|
||||
"description": "Glob pattern to filter files (e.g. \"*.rs\")"
|
||||
}),
|
||||
),
|
||||
(
|
||||
"type".to_string(),
|
||||
json!({
|
||||
"type": "string",
|
||||
"description": "File type to search (e.g. rust, js, py)"
|
||||
}),
|
||||
),
|
||||
(
|
||||
"-i".to_string(),
|
||||
json!({
|
||||
"type": "boolean",
|
||||
"description": "Case insensitive search"
|
||||
}),
|
||||
),
|
||||
(
|
||||
"-n".to_string(),
|
||||
json!({
|
||||
"type": "boolean",
|
||||
"description": "Show line numbers"
|
||||
}),
|
||||
),
|
||||
(
|
||||
"-A".to_string(),
|
||||
json!({
|
||||
"type": "number",
|
||||
"description": "Lines to show after each match"
|
||||
}),
|
||||
),
|
||||
(
|
||||
"-B".to_string(),
|
||||
json!({
|
||||
"type": "number",
|
||||
"description": "Lines to show before each match"
|
||||
}),
|
||||
),
|
||||
(
|
||||
"-C".to_string(),
|
||||
json!({
|
||||
"type": "number",
|
||||
"description": "Lines of context around each match"
|
||||
}),
|
||||
),
|
||||
(
|
||||
"head_limit".to_string(),
|
||||
json!({
|
||||
"type": "number",
|
||||
"description": "Limit output to first N entries (default 250)"
|
||||
}),
|
||||
),
|
||||
(
|
||||
"multiline".to_string(),
|
||||
json!({
|
||||
"type": "boolean",
|
||||
"description": "Enable multiline matching"
|
||||
}),
|
||||
),
|
||||
]),
|
||||
required: vec!["pattern".to_string()],
|
||||
additional_properties: Some(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_read_only(&self, _input: &Value) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_concurrency_safe(&self, _input: &Value) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn call(&self, input: Value, context: &ToolUseContext) -> Result<ToolResult, ToolError> {
|
||||
let pattern = input
|
||||
.get("pattern")
|
||||
.and_then(|p| p.as_str())
|
||||
.ok_or_else(|| ToolError::InvalidInput("Missing 'pattern' field".to_string()))?;
|
||||
|
||||
let search_path = input
|
||||
.get("path")
|
||||
.and_then(|p| p.as_str())
|
||||
.unwrap_or(&context.working_dir);
|
||||
|
||||
let output_mode = input
|
||||
.get("output_mode")
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or("files_with_matches");
|
||||
|
||||
let head_limit = input
|
||||
.get("head_limit")
|
||||
.and_then(|h| h.as_u64())
|
||||
.unwrap_or(DEFAULT_HEAD_LIMIT as u64) as usize;
|
||||
|
||||
// Try rg first, fall back to grep
|
||||
let result = run_ripgrep(pattern, search_path, output_mode, &input).await;
|
||||
|
||||
match result {
|
||||
Ok(output) => {
|
||||
if output.is_empty() {
|
||||
return Ok(ToolResult::text("No matches found.".to_string()));
|
||||
}
|
||||
|
||||
let lines: Vec<&str> = output.lines().collect();
|
||||
let total = lines.len();
|
||||
let truncated = total > head_limit;
|
||||
let lines: Vec<&str> = lines.into_iter().take(head_limit).collect();
|
||||
let mut result = lines.join("\n");
|
||||
|
||||
if truncated {
|
||||
result.push_str(&format!(
|
||||
"\n\n(showing {} of {} results)",
|
||||
head_limit, total
|
||||
));
|
||||
}
|
||||
|
||||
Ok(ToolResult::text(result))
|
||||
}
|
||||
Err(e) => {
|
||||
// Fall back to grep
|
||||
match run_grep(pattern, search_path, output_mode, &input).await {
|
||||
Ok(output) => {
|
||||
if output.is_empty() {
|
||||
return Ok(ToolResult::text("No matches found.".to_string()));
|
||||
}
|
||||
Ok(ToolResult::text(output))
|
||||
}
|
||||
Err(_) => Ok(ToolResult::error(format!("Search failed: {}", e))),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_ripgrep(
|
||||
pattern: &str,
|
||||
path: &str,
|
||||
output_mode: &str,
|
||||
input: &Value,
|
||||
) -> Result<String, String> {
|
||||
let mut args = vec!["--no-heading".to_string()];
|
||||
|
||||
match output_mode {
|
||||
"files_with_matches" => args.push("-l".to_string()),
|
||||
"count" => args.push("-c".to_string()),
|
||||
"content" => {
|
||||
if input.get("-n").and_then(|n| n.as_bool()).unwrap_or(true) {
|
||||
args.push("-n".to_string());
|
||||
}
|
||||
}
|
||||
_ => args.push("-l".to_string()),
|
||||
}
|
||||
|
||||
if input.get("-i").and_then(|i| i.as_bool()).unwrap_or(false) {
|
||||
args.push("-i".to_string());
|
||||
}
|
||||
|
||||
if let Some(after) = input.get("-A").and_then(|a| a.as_u64()) {
|
||||
args.push(format!("-A{}", after));
|
||||
}
|
||||
if let Some(before) = input.get("-B").and_then(|b| b.as_u64()) {
|
||||
args.push(format!("-B{}", before));
|
||||
}
|
||||
if let Some(ctx) = input.get("-C").and_then(|c| c.as_u64()) {
|
||||
args.push(format!("-C{}", ctx));
|
||||
}
|
||||
|
||||
if let Some(glob_pattern) = input.get("glob").and_then(|g| g.as_str()) {
|
||||
args.push("--glob".to_string());
|
||||
args.push(glob_pattern.to_string());
|
||||
}
|
||||
|
||||
if let Some(file_type) = input.get("type").and_then(|t| t.as_str()) {
|
||||
args.push("--type".to_string());
|
||||
args.push(file_type.to_string());
|
||||
}
|
||||
|
||||
if input
|
||||
.get("multiline")
|
||||
.and_then(|m| m.as_bool())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
args.push("-U".to_string());
|
||||
args.push("--multiline-dotall".to_string());
|
||||
}
|
||||
|
||||
args.push(pattern.to_string());
|
||||
args.push(path.to_string());
|
||||
|
||||
let output = Command::new("rg")
|
||||
.args(&args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
if output.status.success() || output.status.code() == Some(1) {
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
} else {
|
||||
Err(String::from_utf8_lossy(&output.stderr).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_grep(
|
||||
pattern: &str,
|
||||
path: &str,
|
||||
output_mode: &str,
|
||||
input: &Value,
|
||||
) -> Result<String, String> {
|
||||
let mut args = vec!["-r".to_string()];
|
||||
|
||||
match output_mode {
|
||||
"files_with_matches" => args.push("-l".to_string()),
|
||||
"count" => args.push("-c".to_string()),
|
||||
"content" => {
|
||||
args.push("-n".to_string());
|
||||
}
|
||||
_ => args.push("-l".to_string()),
|
||||
}
|
||||
|
||||
if input.get("-i").and_then(|i| i.as_bool()).unwrap_or(false) {
|
||||
args.push("-i".to_string());
|
||||
}
|
||||
|
||||
args.push(pattern.to_string());
|
||||
args.push(path.to_string());
|
||||
|
||||
let output = Command::new("grep")
|
||||
.args(&args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
Reference in New Issue
Block a user