371 lines
10 KiB
Rust
371 lines
10 KiB
Rust
//! OpenViking CLI Sidecar Integration
|
|
//!
|
|
//! Wraps the OpenViking Rust CLI (`ov`) as a Tauri sidecar for local memory operations.
|
|
//! This eliminates the need for a Python server dependency.
|
|
//!
|
|
//! Reference: https://github.com/volcengine/OpenViking
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use std::process::Command;
|
|
|
|
// === Types ===
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct VikingStatus {
|
|
pub available: bool,
|
|
pub version: Option<String>,
|
|
pub data_dir: Option<String>,
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct VikingResource {
|
|
pub uri: String,
|
|
pub name: String,
|
|
#[serde(rename = "type")]
|
|
pub resource_type: String,
|
|
pub size: Option<u64>,
|
|
pub modified_at: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct VikingFindResult {
|
|
pub uri: String,
|
|
pub score: f64,
|
|
pub content: String,
|
|
pub level: String,
|
|
pub overview: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct VikingGrepResult {
|
|
pub uri: String,
|
|
pub line: u32,
|
|
pub content: String,
|
|
pub match_start: u32,
|
|
pub match_end: u32,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct VikingAddResult {
|
|
pub uri: String,
|
|
pub status: String,
|
|
}
|
|
|
|
// === CLI Path Resolution ===
|
|
|
|
fn get_viking_cli_path() -> Result<String, String> {
|
|
// Try environment variable first
|
|
if let Ok(path) = std::env::var("ZCLAW_VIKING_BIN") {
|
|
if std::path::Path::new(&path).exists() {
|
|
return Ok(path);
|
|
}
|
|
}
|
|
|
|
// Try bundled sidecar location
|
|
let binary_name = if cfg!(target_os = "windows") {
|
|
"ov-x86_64-pc-windows-msvc.exe"
|
|
} else if cfg!(target_os = "macos") {
|
|
if cfg!(target_arch = "aarch64") {
|
|
"ov-aarch64-apple-darwin"
|
|
} else {
|
|
"ov-x86_64-apple-darwin"
|
|
}
|
|
} else {
|
|
"ov-x86_64-unknown-linux-gnu"
|
|
};
|
|
|
|
// Check common locations
|
|
let locations = vec![
|
|
format!("./binaries/{}", binary_name),
|
|
format!("./resources/viking/{}", binary_name),
|
|
format!("./{}", binary_name),
|
|
];
|
|
|
|
for loc in locations {
|
|
if std::path::Path::new(&loc).exists() {
|
|
return Ok(loc);
|
|
}
|
|
}
|
|
|
|
// Fallback to system PATH
|
|
Ok("ov".to_string())
|
|
}
|
|
|
|
fn run_viking_cli(args: &[&str]) -> Result<String, String> {
|
|
let cli_path = get_viking_cli_path()?;
|
|
|
|
let output = Command::new(&cli_path)
|
|
.args(args)
|
|
.output()
|
|
.map_err(|e| {
|
|
if e.kind() == std::io::ErrorKind::NotFound {
|
|
format!(
|
|
"OpenViking CLI not found. Please install 'ov' or set ZCLAW_VIKING_BIN. Tried: {}",
|
|
cli_path
|
|
)
|
|
} else {
|
|
format!("Failed to run OpenViking CLI: {}", e)
|
|
}
|
|
})?;
|
|
|
|
if output.status.success() {
|
|
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
|
} else {
|
|
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
|
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
|
|
if !stderr.is_empty() {
|
|
Err(stderr)
|
|
} else if !stdout.is_empty() {
|
|
Err(stdout)
|
|
} else {
|
|
Err(format!("OpenViking CLI failed with status: {}", output.status))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper function to run Viking CLI and parse JSON output
|
|
/// Reserved for future JSON-based commands
|
|
#[allow(dead_code)]
|
|
fn run_viking_cli_json<T: for<'de> Deserialize<'de>>(args: &[&str]) -> Result<T, String> {
|
|
let output = run_viking_cli(args)?;
|
|
|
|
// Handle empty output
|
|
if output.is_empty() {
|
|
return Err("OpenViking CLI returned empty output".to_string());
|
|
}
|
|
|
|
// Try to parse as JSON
|
|
serde_json::from_str(&output)
|
|
.map_err(|e| format!("Failed to parse OpenViking output as JSON: {}\nOutput: {}", e, output))
|
|
}
|
|
|
|
// === Tauri Commands ===
|
|
|
|
/// Check if OpenViking CLI is available
|
|
#[tauri::command]
|
|
pub fn viking_status() -> Result<VikingStatus, String> {
|
|
let result = run_viking_cli(&["--version"]);
|
|
|
|
match result {
|
|
Ok(version_output) => {
|
|
// Parse version from output like "ov 0.1.0"
|
|
let version = version_output
|
|
.lines()
|
|
.next()
|
|
.map(|s| s.trim().to_string());
|
|
|
|
Ok(VikingStatus {
|
|
available: true,
|
|
version,
|
|
data_dir: None, // TODO: Get from CLI
|
|
error: None,
|
|
})
|
|
}
|
|
Err(e) => Ok(VikingStatus {
|
|
available: false,
|
|
version: None,
|
|
data_dir: None,
|
|
error: Some(e),
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Add a resource to OpenViking
|
|
#[tauri::command]
|
|
pub fn viking_add(uri: String, content: String) -> Result<VikingAddResult, String> {
|
|
// Create a temporary file for the content
|
|
let temp_dir = std::env::temp_dir();
|
|
let timestamp = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_millis())
|
|
.unwrap_or(0);
|
|
let temp_file = temp_dir.join(format!("viking_add_{}.txt", timestamp));
|
|
|
|
std::fs::write(&temp_file, &content)
|
|
.map_err(|e| format!("Failed to write temp file: {}", e))?;
|
|
|
|
let temp_path = temp_file.to_string_lossy();
|
|
let result = run_viking_cli(&["add", &uri, "--file", &temp_path]);
|
|
|
|
// Clean up temp file
|
|
let _ = std::fs::remove_file(&temp_file);
|
|
|
|
match result {
|
|
Ok(_) => Ok(VikingAddResult {
|
|
uri,
|
|
status: "added".to_string(),
|
|
}),
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
/// Add a resource with inline content (for small content)
|
|
#[tauri::command]
|
|
pub fn viking_add_inline(uri: String, content: String) -> Result<VikingAddResult, String> {
|
|
// Use stdin for content
|
|
let cli_path = get_viking_cli_path()?;
|
|
|
|
let output = Command::new(&cli_path)
|
|
.args(["add", &uri])
|
|
.stdin(std::process::Stdio::piped())
|
|
.stdout(std::process::Stdio::piped())
|
|
.stderr(std::process::Stdio::piped())
|
|
.spawn()
|
|
.map_err(|e| format!("Failed to spawn OpenViking CLI: {}", e))?;
|
|
|
|
// Write content to stdin
|
|
if let Some(mut stdin) = output.stdin.as_ref() {
|
|
use std::io::Write;
|
|
stdin.write_all(content.as_bytes())
|
|
.map_err(|e| format!("Failed to write to stdin: {}", e))?;
|
|
}
|
|
|
|
let result = output.wait_with_output()
|
|
.map_err(|e| format!("Failed to read output: {}", e))?;
|
|
|
|
if result.status.success() {
|
|
Ok(VikingAddResult {
|
|
uri,
|
|
status: "added".to_string(),
|
|
})
|
|
} else {
|
|
let stderr = String::from_utf8_lossy(&result.stderr).trim().to_string();
|
|
Err(if !stderr.is_empty() { stderr } else { "Failed to add resource".to_string() })
|
|
}
|
|
}
|
|
|
|
/// Find resources by semantic search
|
|
#[tauri::command]
|
|
pub fn viking_find(
|
|
query: String,
|
|
scope: Option<String>,
|
|
limit: Option<usize>,
|
|
) -> Result<Vec<VikingFindResult>, String> {
|
|
let mut args = vec!["find", "--json", &query];
|
|
|
|
let scope_arg;
|
|
if let Some(ref s) = scope {
|
|
scope_arg = format!("--scope={}", s);
|
|
args.push(&scope_arg);
|
|
}
|
|
|
|
let limit_arg;
|
|
if let Some(l) = limit {
|
|
limit_arg = format!("--limit={}", l);
|
|
args.push(&limit_arg);
|
|
}
|
|
|
|
// CLI returns JSON array directly
|
|
let output = run_viking_cli(&args)?;
|
|
|
|
// Handle empty or null results
|
|
if output.is_empty() || output == "null" || output == "[]" {
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
serde_json::from_str(&output)
|
|
.map_err(|e| format!("Failed to parse find results: {}\nOutput: {}", e, output))
|
|
}
|
|
|
|
/// Grep resources by pattern
|
|
#[tauri::command]
|
|
pub fn viking_grep(
|
|
pattern: String,
|
|
uri: Option<String>,
|
|
case_sensitive: Option<bool>,
|
|
limit: Option<usize>,
|
|
) -> Result<Vec<VikingGrepResult>, String> {
|
|
let mut args = vec!["grep", "--json", &pattern];
|
|
|
|
let uri_arg;
|
|
if let Some(ref u) = uri {
|
|
uri_arg = format!("--uri={}", u);
|
|
args.push(&uri_arg);
|
|
}
|
|
|
|
if case_sensitive.unwrap_or(false) {
|
|
args.push("--case-sensitive");
|
|
}
|
|
|
|
let limit_arg;
|
|
if let Some(l) = limit {
|
|
limit_arg = format!("--limit={}", l);
|
|
args.push(&limit_arg);
|
|
}
|
|
|
|
let output = run_viking_cli(&args)?;
|
|
|
|
if output.is_empty() || output == "null" || output == "[]" {
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
serde_json::from_str(&output)
|
|
.map_err(|e| format!("Failed to parse grep results: {}\nOutput: {}", e, output))
|
|
}
|
|
|
|
/// List resources at a path
|
|
#[tauri::command]
|
|
pub fn viking_ls(path: String) -> Result<Vec<VikingResource>, String> {
|
|
let output = run_viking_cli(&["ls", "--json", &path])?;
|
|
|
|
if output.is_empty() || output == "null" || output == "[]" {
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
serde_json::from_str(&output)
|
|
.map_err(|e| format!("Failed to parse ls results: {}\nOutput: {}", e, output))
|
|
}
|
|
|
|
/// Read resource content
|
|
#[tauri::command]
|
|
pub fn viking_read(uri: String, level: Option<String>) -> Result<String, String> {
|
|
let level_val = level.unwrap_or_else(|| "L1".to_string());
|
|
let level_arg = format!("--level={}", level_val);
|
|
|
|
run_viking_cli(&["read", &uri, &level_arg])
|
|
}
|
|
|
|
/// Remove a resource
|
|
#[tauri::command]
|
|
pub fn viking_remove(uri: String) -> Result<(), String> {
|
|
run_viking_cli(&["remove", &uri])?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Get resource tree
|
|
#[tauri::command]
|
|
pub fn viking_tree(path: String, depth: Option<usize>) -> Result<serde_json::Value, String> {
|
|
let depth_val = depth.unwrap_or(2);
|
|
let depth_arg = format!("--depth={}", depth_val);
|
|
|
|
let output = run_viking_cli(&["tree", "--json", &path, &depth_arg])?;
|
|
|
|
if output.is_empty() || output == "null" {
|
|
return Ok(serde_json::json!({}));
|
|
}
|
|
|
|
serde_json::from_str(&output)
|
|
.map_err(|e| format!("Failed to parse tree result: {}\nOutput: {}", e, output))
|
|
}
|
|
|
|
// === Tests ===
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_status_unavailable_without_cli() {
|
|
// This test will fail if ov is installed, which is fine
|
|
let result = viking_status();
|
|
assert!(result.is_ok());
|
|
}
|
|
}
|