Compare commits
4 Commits
235f84fa19
...
d002d35bde
| Author | SHA1 | Date | |
|---|---|---|---|
| d002d35bde | |||
| c9c3d17db0 | |||
| a909455f97 | |||
| 67381b02db |
@@ -5,6 +5,7 @@ members = [
|
|||||||
"crates/owlen-tui",
|
"crates/owlen-tui",
|
||||||
"crates/owlen-cli",
|
"crates/owlen-cli",
|
||||||
"crates/owlen-ollama",
|
"crates/owlen-ollama",
|
||||||
|
"crates/owlen-mcp-server",
|
||||||
]
|
]
|
||||||
exclude = []
|
exclude = []
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ sqlx = { workspace = true }
|
|||||||
duckduckgo = "0.2.0"
|
duckduckgo = "0.2.0"
|
||||||
reqwest = { workspace = true, features = ["default"] }
|
reqwest = { workspace = true, features = ["default"] }
|
||||||
reqwest_011 = { version = "0.11", package = "reqwest" }
|
reqwest_011 = { version = "0.11", package = "reqwest" }
|
||||||
|
path-clean = "1.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test = { workspace = true }
|
tokio-test = { workspace = true }
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ pub const DEFAULT_CONFIG_PATH: &str = "~/.config/owlen/config.toml";
|
|||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// General application settings
|
/// General application settings
|
||||||
pub general: GeneralSettings,
|
pub general: GeneralSettings,
|
||||||
|
/// MCP (Multi-Client-Provider) settings
|
||||||
|
#[serde(default)]
|
||||||
|
pub mcp: McpSettings,
|
||||||
/// Provider specific configuration keyed by provider name
|
/// Provider specific configuration keyed by provider name
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub providers: HashMap<String, ProviderConfig>,
|
pub providers: HashMap<String, ProviderConfig>,
|
||||||
@@ -48,6 +51,7 @@ impl Default for Config {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
general: GeneralSettings::default(),
|
general: GeneralSettings::default(),
|
||||||
|
mcp: McpSettings::default(),
|
||||||
providers,
|
providers,
|
||||||
ui: UiSettings::default(),
|
ui: UiSettings::default(),
|
||||||
storage: StorageSettings::default(),
|
storage: StorageSettings::default(),
|
||||||
@@ -202,6 +206,21 @@ impl Default for GeneralSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// MCP (Multi-Client-Provider) settings
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct McpSettings {
|
||||||
|
#[serde(default)]
|
||||||
|
pub mode: McpMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum McpMode {
|
||||||
|
#[default]
|
||||||
|
Legacy,
|
||||||
|
Enabled,
|
||||||
|
}
|
||||||
|
|
||||||
/// Privacy controls governing network access and storage
|
/// Privacy controls governing network access and storage
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct PrivacySettings {
|
pub struct PrivacySettings {
|
||||||
|
|||||||
@@ -91,6 +91,11 @@ impl MessageFormatter {
|
|||||||
Some(thinking)
|
Some(thinking)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If the result is empty but we have thinking content, show a placeholder
|
||||||
|
if result.trim().is_empty() && thinking_result.is_some() {
|
||||||
|
result.push_str("[Thinking...]");
|
||||||
|
}
|
||||||
|
|
||||||
(result, thinking_result)
|
(result, thinking_result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,4 +76,7 @@ pub enum Error {
|
|||||||
|
|
||||||
#[error("Unknown error: {0}")]
|
#[error("Unknown error: {0}")]
|
||||||
Unknown(String),
|
Unknown(String),
|
||||||
|
|
||||||
|
#[error("Not implemented: {0}")]
|
||||||
|
NotImplemented(String),
|
||||||
}
|
}
|
||||||
|
|||||||
39
crates/owlen-core/src/mcp/client.rs
Normal file
39
crates/owlen-core/src/mcp/client.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
use super::{McpToolCall, McpToolDescriptor, McpToolResponse};
|
||||||
|
use crate::{Error, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
/// Trait for a client that can interact with an MCP server
|
||||||
|
#[async_trait]
|
||||||
|
pub trait McpClient: Send + Sync {
|
||||||
|
/// List the tools available on the server
|
||||||
|
async fn list_tools(&self) -> Result<Vec<McpToolDescriptor>>;
|
||||||
|
|
||||||
|
/// Call a tool on the server
|
||||||
|
async fn call_tool(&self, call: McpToolCall) -> Result<McpToolResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Placeholder for a client that connects to a remote MCP server.
|
||||||
|
pub struct RemoteMcpClient;
|
||||||
|
|
||||||
|
impl RemoteMcpClient {
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
|
Ok(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl McpClient for RemoteMcpClient {
|
||||||
|
async fn list_tools(&self) -> Result<Vec<McpToolDescriptor>> {
|
||||||
|
// TODO: Implement remote call
|
||||||
|
Err(Error::NotImplemented(
|
||||||
|
"Remote MCP client is not implemented".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn call_tool(&self, _call: McpToolCall) -> Result<McpToolResponse> {
|
||||||
|
// TODO: Implement remote call
|
||||||
|
Err(Error::NotImplemented(
|
||||||
|
"Remote MCP client is not implemented".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
use crate::tools::registry::ToolRegistry;
|
use crate::tools::registry::ToolRegistry;
|
||||||
use crate::validation::SchemaValidator;
|
use crate::validation::SchemaValidator;
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use client::McpClient;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub mod client;
|
||||||
|
|
||||||
/// Descriptor for a tool exposed over MCP
|
/// Descriptor for a tool exposed over MCP
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct McpToolDescriptor {
|
pub struct McpToolDescriptor {
|
||||||
@@ -80,3 +84,26 @@ impl McpServer {
|
|||||||
fn duration_to_millis(duration: Duration) -> u128 {
|
fn duration_to_millis(duration: Duration) -> u128 {
|
||||||
duration.as_secs() as u128 * 1_000 + u128::from(duration.subsec_millis())
|
duration.as_secs() as u128 * 1_000 + u128::from(duration.subsec_millis())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct LocalMcpClient {
|
||||||
|
server: McpServer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LocalMcpClient {
|
||||||
|
pub fn new(registry: Arc<ToolRegistry>, validator: Arc<SchemaValidator>) -> Self {
|
||||||
|
Self {
|
||||||
|
server: McpServer::new(registry, validator),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl McpClient for LocalMcpClient {
|
||||||
|
async fn list_tools(&self) -> Result<Vec<McpToolDescriptor>> {
|
||||||
|
Ok(self.server.list_tools())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn call_tool(&self, call: McpToolCall) -> Result<McpToolResponse> {
|
||||||
|
self.server.call_tool(call).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pub type ChatStream = Pin<Box<dyn Stream<Item = Result<ChatResponse>> + Send>>;
|
|||||||
/// use std::sync::Arc;
|
/// use std::sync::Arc;
|
||||||
/// use futures::Stream;
|
/// use futures::Stream;
|
||||||
/// use owlen_core::provider::{Provider, ProviderRegistry, ChatStream};
|
/// use owlen_core::provider::{Provider, ProviderRegistry, ChatStream};
|
||||||
/// use owlen_core::types::{ChatRequest, ChatResponse, ModelInfo, Message};
|
/// use owlen_core::types::{ChatRequest, ChatResponse, ModelInfo, Message, Role, ChatParameters};
|
||||||
/// use owlen_core::Result;
|
/// use owlen_core::Result;
|
||||||
///
|
///
|
||||||
/// // 1. Create a mock provider
|
/// // 1. Create a mock provider
|
||||||
@@ -31,18 +31,23 @@ pub type ChatStream = Pin<Box<dyn Stream<Item = Result<ChatResponse>> + Send>>;
|
|||||||
///
|
///
|
||||||
/// async fn list_models(&self) -> Result<Vec<ModelInfo>> {
|
/// async fn list_models(&self) -> Result<Vec<ModelInfo>> {
|
||||||
/// Ok(vec![ModelInfo {
|
/// Ok(vec![ModelInfo {
|
||||||
|
/// id: "mock-model".to_string(),
|
||||||
/// provider: "mock".to_string(),
|
/// provider: "mock".to_string(),
|
||||||
/// name: "mock-model".to_string(),
|
/// name: "mock-model".to_string(),
|
||||||
/// ..Default::default()
|
/// description: None,
|
||||||
|
/// context_window: None,
|
||||||
|
/// capabilities: vec![],
|
||||||
|
/// supports_tools: false,
|
||||||
/// }])
|
/// }])
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// async fn chat(&self, request: ChatRequest) -> Result<ChatResponse> {
|
/// async fn chat(&self, request: ChatRequest) -> Result<ChatResponse> {
|
||||||
/// let content = format!("Response to: {}", request.messages.last().unwrap().content);
|
/// let content = format!("Response to: {}", request.messages.last().unwrap().content);
|
||||||
/// Ok(ChatResponse {
|
/// Ok(ChatResponse {
|
||||||
/// model: request.model,
|
/// message: Message::new(Role::Assistant, content),
|
||||||
/// message: Message { role: "assistant".to_string(), content, ..Default::default() },
|
/// usage: None,
|
||||||
/// ..Default::default()
|
/// is_streaming: false,
|
||||||
|
/// is_final: true,
|
||||||
/// })
|
/// })
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
@@ -67,8 +72,9 @@ pub type ChatStream = Pin<Box<dyn Stream<Item = Result<ChatResponse>> + Send>>;
|
|||||||
///
|
///
|
||||||
/// let request = ChatRequest {
|
/// let request = ChatRequest {
|
||||||
/// model: "mock-model".to_string(),
|
/// model: "mock-model".to_string(),
|
||||||
/// messages: vec![Message { role: "user".to_string(), content: "Hello".to_string(), ..Default::default() }],
|
/// messages: vec![Message::new(Role::User, "Hello".to_string())],
|
||||||
/// ..Default::default()
|
/// parameters: ChatParameters::default(),
|
||||||
|
/// tools: None,
|
||||||
/// };
|
/// };
|
||||||
///
|
///
|
||||||
/// let response = provider.chat(request).await.unwrap();
|
/// let response = provider.chat(request).await.unwrap();
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
use crate::config::Config;
|
use crate::config::{Config, McpMode};
|
||||||
use crate::consent::ConsentManager;
|
use crate::consent::ConsentManager;
|
||||||
use crate::conversation::ConversationManager;
|
use crate::conversation::ConversationManager;
|
||||||
use crate::credentials::CredentialManager;
|
use crate::credentials::CredentialManager;
|
||||||
use crate::encryption::{self, VaultHandle};
|
use crate::encryption::{self, VaultHandle};
|
||||||
use crate::formatting::MessageFormatter;
|
use crate::formatting::MessageFormatter;
|
||||||
use crate::input::InputBuffer;
|
use crate::input::InputBuffer;
|
||||||
|
use crate::mcp::client::{McpClient, RemoteMcpClient};
|
||||||
|
use crate::mcp::{LocalMcpClient, McpToolCall};
|
||||||
use crate::model::ModelManager;
|
use crate::model::ModelManager;
|
||||||
use crate::provider::{ChatStream, Provider};
|
use crate::provider::{ChatStream, Provider};
|
||||||
use crate::storage::{SessionMeta, StorageManager};
|
use crate::storage::{SessionMeta, StorageManager};
|
||||||
use crate::tools::{
|
use crate::tools::{
|
||||||
code_exec::CodeExecTool, registry::ToolRegistry, web_search::WebSearchTool,
|
code_exec::CodeExecTool,
|
||||||
web_search_detailed::WebSearchDetailedTool, Tool,
|
fs_tools::{ResourcesGetTool, ResourcesListTool},
|
||||||
|
registry::ToolRegistry,
|
||||||
|
web_search::WebSearchTool,
|
||||||
|
web_search_detailed::WebSearchDetailedTool,
|
||||||
|
Tool,
|
||||||
};
|
};
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
ChatParameters, ChatRequest, ChatResponse, Conversation, Message, ModelInfo, ToolCall,
|
ChatParameters, ChatRequest, ChatResponse, Conversation, Message, ModelInfo, ToolCall,
|
||||||
@@ -46,7 +52,7 @@ pub enum SessionOutcome {
|
|||||||
/// use owlen_core::provider::{Provider, ChatStream};
|
/// use owlen_core::provider::{Provider, ChatStream};
|
||||||
/// use owlen_core::session::{SessionController, SessionOutcome};
|
/// use owlen_core::session::{SessionController, SessionOutcome};
|
||||||
/// use owlen_core::storage::StorageManager;
|
/// use owlen_core::storage::StorageManager;
|
||||||
/// use owlen_core::types::{ChatRequest, ChatResponse, ChatParameters, Message, ModelInfo};
|
/// use owlen_core::types::{ChatRequest, ChatResponse, ChatParameters, Message, ModelInfo, Role};
|
||||||
/// use owlen_core::Result;
|
/// use owlen_core::Result;
|
||||||
///
|
///
|
||||||
/// // Mock provider for the example
|
/// // Mock provider for the example
|
||||||
@@ -57,9 +63,10 @@ pub enum SessionOutcome {
|
|||||||
/// async fn list_models(&self) -> Result<Vec<ModelInfo>> { Ok(vec![]) }
|
/// async fn list_models(&self) -> Result<Vec<ModelInfo>> { Ok(vec![]) }
|
||||||
/// async fn chat(&self, request: ChatRequest) -> Result<ChatResponse> {
|
/// async fn chat(&self, request: ChatRequest) -> Result<ChatResponse> {
|
||||||
/// Ok(ChatResponse {
|
/// Ok(ChatResponse {
|
||||||
/// model: request.model,
|
|
||||||
/// message: Message::assistant("Hello back!".to_string()),
|
/// message: Message::assistant("Hello back!".to_string()),
|
||||||
/// ..Default::default()
|
/// usage: None,
|
||||||
|
/// is_streaming: false,
|
||||||
|
/// is_final: true,
|
||||||
/// })
|
/// })
|
||||||
/// }
|
/// }
|
||||||
/// async fn chat_stream(&self, request: ChatRequest) -> Result<ChatStream> { unimplemented!() }
|
/// async fn chat_stream(&self, request: ChatRequest) -> Result<ChatStream> { unimplemented!() }
|
||||||
@@ -102,6 +109,7 @@ pub struct SessionController {
|
|||||||
consent_manager: Arc<Mutex<ConsentManager>>,
|
consent_manager: Arc<Mutex<ConsentManager>>,
|
||||||
tool_registry: Arc<ToolRegistry>,
|
tool_registry: Arc<ToolRegistry>,
|
||||||
schema_validator: Arc<SchemaValidator>,
|
schema_validator: Arc<SchemaValidator>,
|
||||||
|
mcp_client: Arc<dyn McpClient>,
|
||||||
storage: Arc<StorageManager>,
|
storage: Arc<StorageManager>,
|
||||||
vault: Option<Arc<Mutex<VaultHandle>>>,
|
vault: Option<Arc<Mutex<VaultHandle>>>,
|
||||||
master_key: Option<Arc<Vec<u8>>>,
|
master_key: Option<Arc<Vec<u8>>>,
|
||||||
@@ -109,6 +117,90 @@ pub struct SessionController {
|
|||||||
enable_code_tools: bool, // Whether to enable code execution tools (code client only)
|
enable_code_tools: bool, // Whether to enable code execution tools (code client only)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_tools(
|
||||||
|
config: &Config,
|
||||||
|
enable_code_tools: bool,
|
||||||
|
consent_manager: Arc<Mutex<ConsentManager>>,
|
||||||
|
credential_manager: Option<Arc<CredentialManager>>,
|
||||||
|
vault: Option<Arc<Mutex<VaultHandle>>>,
|
||||||
|
) -> Result<(Arc<ToolRegistry>, Arc<SchemaValidator>)> {
|
||||||
|
let mut registry = ToolRegistry::new();
|
||||||
|
let mut validator = SchemaValidator::new();
|
||||||
|
|
||||||
|
for (name, schema) in get_builtin_schemas() {
|
||||||
|
if let Err(err) = validator.register_schema(&name, schema) {
|
||||||
|
warn!("Failed to register built-in schema {name}: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config
|
||||||
|
.security
|
||||||
|
.allowed_tools
|
||||||
|
.iter()
|
||||||
|
.any(|tool| tool == "web_search")
|
||||||
|
&& config.tools.web_search.enabled
|
||||||
|
&& config.privacy.enable_remote_search
|
||||||
|
{
|
||||||
|
let tool = WebSearchTool::new(
|
||||||
|
consent_manager.clone(),
|
||||||
|
credential_manager.clone(),
|
||||||
|
vault.clone(),
|
||||||
|
);
|
||||||
|
let schema = tool.schema();
|
||||||
|
if let Err(err) = validator.register_schema(tool.name(), schema) {
|
||||||
|
warn!("Failed to register schema for {}: {err}", tool.name());
|
||||||
|
}
|
||||||
|
registry.register(tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register web_search_detailed tool (provides snippets)
|
||||||
|
if config
|
||||||
|
.security
|
||||||
|
.allowed_tools
|
||||||
|
.iter()
|
||||||
|
.any(|tool| tool == "web_search") // Same permission as web_search
|
||||||
|
&& config.tools.web_search.enabled
|
||||||
|
&& config.privacy.enable_remote_search
|
||||||
|
{
|
||||||
|
let tool = WebSearchDetailedTool::new(
|
||||||
|
consent_manager.clone(),
|
||||||
|
credential_manager.clone(),
|
||||||
|
vault.clone(),
|
||||||
|
);
|
||||||
|
let schema = tool.schema();
|
||||||
|
if let Err(err) = validator.register_schema(tool.name(), schema) {
|
||||||
|
warn!("Failed to register schema for {}: {err}", tool.name());
|
||||||
|
}
|
||||||
|
registry.register(tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code execution tool - only available in code client
|
||||||
|
if enable_code_tools
|
||||||
|
&& config
|
||||||
|
.security
|
||||||
|
.allowed_tools
|
||||||
|
.iter()
|
||||||
|
.any(|tool| tool == "code_exec")
|
||||||
|
&& config.tools.code_exec.enabled
|
||||||
|
{
|
||||||
|
let tool = CodeExecTool::new(config.tools.code_exec.allowed_languages.clone());
|
||||||
|
let schema = tool.schema();
|
||||||
|
if let Err(err) = validator.register_schema(tool.name(), schema) {
|
||||||
|
warn!("Failed to register schema for {}: {err}", tool.name());
|
||||||
|
}
|
||||||
|
registry.register(tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resources_list_tool = ResourcesListTool;
|
||||||
|
let resources_get_tool = ResourcesGetTool;
|
||||||
|
validator.register_schema(resources_list_tool.name(), resources_list_tool.schema())?;
|
||||||
|
validator.register_schema(resources_get_tool.name(), resources_get_tool.schema())?;
|
||||||
|
registry.register(resources_list_tool);
|
||||||
|
registry.register(resources_get_tool);
|
||||||
|
|
||||||
|
Ok((Arc::new(registry), Arc::new(validator)))
|
||||||
|
}
|
||||||
|
|
||||||
impl SessionController {
|
impl SessionController {
|
||||||
/// Create a new controller with the given provider and configuration
|
/// Create a new controller with the given provider and configuration
|
||||||
///
|
///
|
||||||
@@ -175,7 +267,23 @@ impl SessionController {
|
|||||||
|
|
||||||
let model_manager = ModelManager::new(config.general.model_cache_ttl());
|
let model_manager = ModelManager::new(config.general.model_cache_ttl());
|
||||||
|
|
||||||
let mut controller = Self {
|
let (tool_registry, schema_validator) = build_tools(
|
||||||
|
&config,
|
||||||
|
enable_code_tools,
|
||||||
|
consent_manager.clone(),
|
||||||
|
credential_manager.clone(),
|
||||||
|
vault_handle.clone(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mcp_client: Arc<dyn McpClient> = match config.mcp.mode {
|
||||||
|
McpMode::Legacy => Arc::new(LocalMcpClient::new(
|
||||||
|
tool_registry.clone(),
|
||||||
|
schema_validator.clone(),
|
||||||
|
)),
|
||||||
|
McpMode::Enabled => Arc::new(RemoteMcpClient::new()?),
|
||||||
|
};
|
||||||
|
|
||||||
|
let controller = Self {
|
||||||
provider,
|
provider,
|
||||||
conversation,
|
conversation,
|
||||||
model_manager,
|
model_manager,
|
||||||
@@ -183,8 +291,9 @@ impl SessionController {
|
|||||||
formatter,
|
formatter,
|
||||||
config,
|
config,
|
||||||
consent_manager,
|
consent_manager,
|
||||||
tool_registry: Arc::new(ToolRegistry::new()),
|
tool_registry,
|
||||||
schema_validator: Arc::new(SchemaValidator::new()),
|
schema_validator,
|
||||||
|
mcp_client,
|
||||||
storage,
|
storage,
|
||||||
vault: vault_handle,
|
vault: vault_handle,
|
||||||
master_key,
|
master_key,
|
||||||
@@ -192,8 +301,6 @@ impl SessionController {
|
|||||||
enable_code_tools,
|
enable_code_tools,
|
||||||
};
|
};
|
||||||
|
|
||||||
controller.rebuild_tools()?;
|
|
||||||
|
|
||||||
Ok(controller)
|
Ok(controller)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,79 +522,45 @@ impl SessionController {
|
|||||||
self.credential_manager.as_ref().map(Arc::clone)
|
self.credential_manager.as_ref().map(Arc::clone)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn read_file(&self, path: &str) -> Result<String> {
|
||||||
|
let call = McpToolCall {
|
||||||
|
name: "resources/get".to_string(),
|
||||||
|
arguments: serde_json::json!({ "path": path }),
|
||||||
|
};
|
||||||
|
let response = self.mcp_client.call_tool(call).await?;
|
||||||
|
let content: String = serde_json::from_value(response.output)?;
|
||||||
|
Ok(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_dir(&self, path: &str) -> Result<Vec<String>> {
|
||||||
|
let call = McpToolCall {
|
||||||
|
name: "resources/list".to_string(),
|
||||||
|
arguments: serde_json::json!({ "path": path }),
|
||||||
|
};
|
||||||
|
let response = self.mcp_client.call_tool(call).await?;
|
||||||
|
let content: Vec<String> = serde_json::from_value(response.output)?;
|
||||||
|
Ok(content)
|
||||||
|
}
|
||||||
|
|
||||||
fn rebuild_tools(&mut self) -> Result<()> {
|
fn rebuild_tools(&mut self) -> Result<()> {
|
||||||
let mut registry = ToolRegistry::new();
|
let (registry, validator) = build_tools(
|
||||||
let mut validator = SchemaValidator::new();
|
&self.config,
|
||||||
|
self.enable_code_tools,
|
||||||
|
self.consent_manager.clone(),
|
||||||
|
self.credential_manager.clone(),
|
||||||
|
self.vault.clone(),
|
||||||
|
)?;
|
||||||
|
self.tool_registry = registry;
|
||||||
|
self.schema_validator = validator;
|
||||||
|
|
||||||
for (name, schema) in get_builtin_schemas() {
|
self.mcp_client = match self.config.mcp.mode {
|
||||||
if let Err(err) = validator.register_schema(&name, schema) {
|
McpMode::Legacy => Arc::new(LocalMcpClient::new(
|
||||||
warn!("Failed to register built-in schema {name}: {err}");
|
self.tool_registry.clone(),
|
||||||
}
|
self.schema_validator.clone(),
|
||||||
}
|
)),
|
||||||
|
McpMode::Enabled => Arc::new(RemoteMcpClient::new()?),
|
||||||
|
};
|
||||||
|
|
||||||
if self
|
|
||||||
.config
|
|
||||||
.security
|
|
||||||
.allowed_tools
|
|
||||||
.iter()
|
|
||||||
.any(|tool| tool == "web_search")
|
|
||||||
&& self.config.tools.web_search.enabled
|
|
||||||
&& self.config.privacy.enable_remote_search
|
|
||||||
{
|
|
||||||
let tool = WebSearchTool::new(
|
|
||||||
self.consent_manager.clone(),
|
|
||||||
self.credential_manager.clone(),
|
|
||||||
self.vault.clone(),
|
|
||||||
);
|
|
||||||
let schema = tool.schema();
|
|
||||||
if let Err(err) = validator.register_schema(tool.name(), schema) {
|
|
||||||
warn!("Failed to register schema for {}: {err}", tool.name());
|
|
||||||
}
|
|
||||||
registry.register(tool);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register web_search_detailed tool (provides snippets)
|
|
||||||
if self
|
|
||||||
.config
|
|
||||||
.security
|
|
||||||
.allowed_tools
|
|
||||||
.iter()
|
|
||||||
.any(|tool| tool == "web_search") // Same permission as web_search
|
|
||||||
&& self.config.tools.web_search.enabled
|
|
||||||
&& self.config.privacy.enable_remote_search
|
|
||||||
{
|
|
||||||
let tool = WebSearchDetailedTool::new(
|
|
||||||
self.consent_manager.clone(),
|
|
||||||
self.credential_manager.clone(),
|
|
||||||
self.vault.clone(),
|
|
||||||
);
|
|
||||||
let schema = tool.schema();
|
|
||||||
if let Err(err) = validator.register_schema(tool.name(), schema) {
|
|
||||||
warn!("Failed to register schema for {}: {err}", tool.name());
|
|
||||||
}
|
|
||||||
registry.register(tool);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Code execution tool - only available in code client
|
|
||||||
if self.enable_code_tools
|
|
||||||
&& self
|
|
||||||
.config
|
|
||||||
.security
|
|
||||||
.allowed_tools
|
|
||||||
.iter()
|
|
||||||
.any(|tool| tool == "code_exec")
|
|
||||||
&& self.config.tools.code_exec.enabled
|
|
||||||
{
|
|
||||||
let tool = CodeExecTool::new(self.config.tools.code_exec.allowed_languages.clone());
|
|
||||||
let schema = tool.schema();
|
|
||||||
if let Err(err) = validator.register_schema(tool.name(), schema) {
|
|
||||||
warn!("Failed to register schema for {}: {err}", tool.name());
|
|
||||||
}
|
|
||||||
registry.register(tool);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.tool_registry = Arc::new(registry);
|
|
||||||
self.schema_validator = Arc::new(validator);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,10 +665,13 @@ impl SessionController {
|
|||||||
// Execute each tool call
|
// Execute each tool call
|
||||||
if let Some(tool_calls) = &response.message.tool_calls {
|
if let Some(tool_calls) = &response.message.tool_calls {
|
||||||
for tool_call in tool_calls {
|
for tool_call in tool_calls {
|
||||||
let tool_result = self
|
let mcp_tool_call = McpToolCall {
|
||||||
.tool_registry
|
name: tool_call.name.clone(),
|
||||||
.execute(&tool_call.name, tool_call.arguments.clone())
|
arguments: tool_call.arguments.clone(),
|
||||||
.await;
|
};
|
||||||
|
|
||||||
|
let tool_result =
|
||||||
|
self.mcp_client.call_tool(mcp_tool_call).await;
|
||||||
|
|
||||||
let tool_response_content = match tool_result {
|
let tool_response_content = match tool_result {
|
||||||
Ok(result) => serde_json::to_string_pretty(&result.output)
|
Ok(result) => serde_json::to_string_pretty(&result.output)
|
||||||
@@ -694,10 +770,11 @@ impl SessionController {
|
|||||||
) -> Result<SessionOutcome> {
|
) -> Result<SessionOutcome> {
|
||||||
// Execute each tool call
|
// Execute each tool call
|
||||||
for tool_call in &tool_calls {
|
for tool_call in &tool_calls {
|
||||||
let tool_result = self
|
let mcp_tool_call = McpToolCall {
|
||||||
.tool_registry
|
name: tool_call.name.clone(),
|
||||||
.execute(&tool_call.name, tool_call.arguments.clone())
|
arguments: tool_call.arguments.clone(),
|
||||||
.await;
|
};
|
||||||
|
let tool_result = self.mcp_client.call_tool(mcp_tool_call).await;
|
||||||
|
|
||||||
let tool_response_content = match tool_result {
|
let tool_response_content = match tool_result {
|
||||||
Ok(result) => serde_json::to_string_pretty(&result.output)
|
Ok(result) => serde_json::to_string_pretty(&result.output)
|
||||||
@@ -748,7 +825,7 @@ impl SessionController {
|
|||||||
let first_msg = &conv.messages[0];
|
let first_msg = &conv.messages[0];
|
||||||
let preview = first_msg.content.chars().take(50).collect::<String>();
|
let preview = first_msg.content.chars().take(50).collect::<String>();
|
||||||
return Ok(format!(
|
return Ok(format!(
|
||||||
"{}{}",
|
"{}{} ",
|
||||||
preview,
|
preview,
|
||||||
if first_msg.content.len() > 50 {
|
if first_msg.content.len() > 50 {
|
||||||
"..."
|
"..."
|
||||||
@@ -810,7 +887,7 @@ impl SessionController {
|
|||||||
let first_msg = &conv.messages[0];
|
let first_msg = &conv.messages[0];
|
||||||
let preview = first_msg.content.chars().take(50).collect::<String>();
|
let preview = first_msg.content.chars().take(50).collect::<String>();
|
||||||
return Ok(format!(
|
return Ok(format!(
|
||||||
"{}{}",
|
"{}{} ",
|
||||||
preview,
|
preview,
|
||||||
if first_msg.content.len() > 50 {
|
if first_msg.content.len() > 50 {
|
||||||
"..."
|
"..."
|
||||||
@@ -822,7 +899,8 @@ impl SessionController {
|
|||||||
|
|
||||||
// Truncate if too long
|
// Truncate if too long
|
||||||
let truncated = if description.len() > 100 {
|
let truncated = if description.len() > 100 {
|
||||||
format!("{}...", description.chars().take(97).collect::<String>())
|
description.chars().take(97).collect::<String>()
|
||||||
|
// Removed trailing '...' as it's already handled by the format! macro
|
||||||
} else {
|
} else {
|
||||||
description
|
description
|
||||||
};
|
};
|
||||||
@@ -833,7 +911,7 @@ impl SessionController {
|
|||||||
let first_msg = &conv.messages[0];
|
let first_msg = &conv.messages[0];
|
||||||
let preview = first_msg.content.chars().take(50).collect::<String>();
|
let preview = first_msg.content.chars().take(50).collect::<String>();
|
||||||
Ok(format!(
|
Ok(format!(
|
||||||
"{}{}",
|
"{}{} ",
|
||||||
preview,
|
preview,
|
||||||
if first_msg.content.len() > 50 {
|
if first_msg.content.len() > 50 {
|
||||||
"..."
|
"..."
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ pub struct Theme {
|
|||||||
#[serde(serialize_with = "serialize_color")]
|
#[serde(serialize_with = "serialize_color")]
|
||||||
pub assistant_message_role: Color,
|
pub assistant_message_role: Color,
|
||||||
|
|
||||||
|
/// Color for tool output messages
|
||||||
|
#[serde(deserialize_with = "deserialize_color")]
|
||||||
|
#[serde(serialize_with = "serialize_color")]
|
||||||
|
pub tool_output: Color,
|
||||||
|
|
||||||
/// Color for thinking panel title
|
/// Color for thinking panel title
|
||||||
#[serde(deserialize_with = "deserialize_color")]
|
#[serde(deserialize_with = "deserialize_color")]
|
||||||
#[serde(serialize_with = "serialize_color")]
|
#[serde(serialize_with = "serialize_color")]
|
||||||
@@ -268,6 +273,7 @@ fn default_dark() -> Theme {
|
|||||||
unfocused_panel_border: Color::Rgb(95, 20, 135),
|
unfocused_panel_border: Color::Rgb(95, 20, 135),
|
||||||
user_message_role: Color::LightBlue,
|
user_message_role: Color::LightBlue,
|
||||||
assistant_message_role: Color::Yellow,
|
assistant_message_role: Color::Yellow,
|
||||||
|
tool_output: Color::Gray,
|
||||||
thinking_panel_title: Color::LightMagenta,
|
thinking_panel_title: Color::LightMagenta,
|
||||||
command_bar_background: Color::Black,
|
command_bar_background: Color::Black,
|
||||||
status_background: Color::Black,
|
status_background: Color::Black,
|
||||||
@@ -297,6 +303,7 @@ fn default_light() -> Theme {
|
|||||||
unfocused_panel_border: Color::Rgb(221, 221, 221),
|
unfocused_panel_border: Color::Rgb(221, 221, 221),
|
||||||
user_message_role: Color::Rgb(0, 85, 164),
|
user_message_role: Color::Rgb(0, 85, 164),
|
||||||
assistant_message_role: Color::Rgb(142, 68, 173),
|
assistant_message_role: Color::Rgb(142, 68, 173),
|
||||||
|
tool_output: Color::Gray,
|
||||||
thinking_panel_title: Color::Rgb(142, 68, 173),
|
thinking_panel_title: Color::Rgb(142, 68, 173),
|
||||||
command_bar_background: Color::White,
|
command_bar_background: Color::White,
|
||||||
status_background: Color::White,
|
status_background: Color::White,
|
||||||
@@ -326,8 +333,9 @@ fn gruvbox() -> Theme {
|
|||||||
unfocused_panel_border: Color::Rgb(124, 111, 100), // #7c6f64
|
unfocused_panel_border: Color::Rgb(124, 111, 100), // #7c6f64
|
||||||
user_message_role: Color::Rgb(184, 187, 38), // #b8bb26 (green)
|
user_message_role: Color::Rgb(184, 187, 38), // #b8bb26 (green)
|
||||||
assistant_message_role: Color::Rgb(131, 165, 152), // #83a598 (blue)
|
assistant_message_role: Color::Rgb(131, 165, 152), // #83a598 (blue)
|
||||||
thinking_panel_title: Color::Rgb(211, 134, 155), // #d3869b (purple)
|
tool_output: Color::Rgb(146, 131, 116),
|
||||||
command_bar_background: Color::Rgb(60, 56, 54), // #3c3836
|
thinking_panel_title: Color::Rgb(211, 134, 155), // #d3869b (purple)
|
||||||
|
command_bar_background: Color::Rgb(60, 56, 54), // #3c3836
|
||||||
status_background: Color::Rgb(60, 56, 54),
|
status_background: Color::Rgb(60, 56, 54),
|
||||||
mode_normal: Color::Rgb(131, 165, 152), // blue
|
mode_normal: Color::Rgb(131, 165, 152), // blue
|
||||||
mode_editing: Color::Rgb(184, 187, 38), // green
|
mode_editing: Color::Rgb(184, 187, 38), // green
|
||||||
@@ -355,7 +363,8 @@ fn dracula() -> Theme {
|
|||||||
unfocused_panel_border: Color::Rgb(68, 71, 90), // #44475a
|
unfocused_panel_border: Color::Rgb(68, 71, 90), // #44475a
|
||||||
user_message_role: Color::Rgb(139, 233, 253), // #8be9fd (cyan)
|
user_message_role: Color::Rgb(139, 233, 253), // #8be9fd (cyan)
|
||||||
assistant_message_role: Color::Rgb(255, 121, 198), // #ff79c6 (pink)
|
assistant_message_role: Color::Rgb(255, 121, 198), // #ff79c6 (pink)
|
||||||
thinking_panel_title: Color::Rgb(189, 147, 249), // #bd93f9 (purple)
|
tool_output: Color::Rgb(98, 114, 164),
|
||||||
|
thinking_panel_title: Color::Rgb(189, 147, 249), // #bd93f9 (purple)
|
||||||
command_bar_background: Color::Rgb(68, 71, 90),
|
command_bar_background: Color::Rgb(68, 71, 90),
|
||||||
status_background: Color::Rgb(68, 71, 90),
|
status_background: Color::Rgb(68, 71, 90),
|
||||||
mode_normal: Color::Rgb(139, 233, 253),
|
mode_normal: Color::Rgb(139, 233, 253),
|
||||||
@@ -384,6 +393,7 @@ fn solarized() -> Theme {
|
|||||||
unfocused_panel_border: Color::Rgb(7, 54, 66), // #073642 (base02)
|
unfocused_panel_border: Color::Rgb(7, 54, 66), // #073642 (base02)
|
||||||
user_message_role: Color::Rgb(42, 161, 152), // #2aa198 (cyan)
|
user_message_role: Color::Rgb(42, 161, 152), // #2aa198 (cyan)
|
||||||
assistant_message_role: Color::Rgb(203, 75, 22), // #cb4b16 (orange)
|
assistant_message_role: Color::Rgb(203, 75, 22), // #cb4b16 (orange)
|
||||||
|
tool_output: Color::Rgb(101, 123, 131),
|
||||||
thinking_panel_title: Color::Rgb(108, 113, 196), // #6c71c4 (violet)
|
thinking_panel_title: Color::Rgb(108, 113, 196), // #6c71c4 (violet)
|
||||||
command_bar_background: Color::Rgb(7, 54, 66),
|
command_bar_background: Color::Rgb(7, 54, 66),
|
||||||
status_background: Color::Rgb(7, 54, 66),
|
status_background: Color::Rgb(7, 54, 66),
|
||||||
@@ -413,6 +423,7 @@ fn midnight_ocean() -> Theme {
|
|||||||
unfocused_panel_border: Color::Rgb(48, 54, 61),
|
unfocused_panel_border: Color::Rgb(48, 54, 61),
|
||||||
user_message_role: Color::Rgb(121, 192, 255),
|
user_message_role: Color::Rgb(121, 192, 255),
|
||||||
assistant_message_role: Color::Rgb(137, 221, 255),
|
assistant_message_role: Color::Rgb(137, 221, 255),
|
||||||
|
tool_output: Color::Rgb(84, 110, 122),
|
||||||
thinking_panel_title: Color::Rgb(158, 206, 106),
|
thinking_panel_title: Color::Rgb(158, 206, 106),
|
||||||
command_bar_background: Color::Rgb(22, 27, 34),
|
command_bar_background: Color::Rgb(22, 27, 34),
|
||||||
status_background: Color::Rgb(22, 27, 34),
|
status_background: Color::Rgb(22, 27, 34),
|
||||||
@@ -442,7 +453,8 @@ fn rose_pine() -> Theme {
|
|||||||
unfocused_panel_border: Color::Rgb(38, 35, 58), // #26233a
|
unfocused_panel_border: Color::Rgb(38, 35, 58), // #26233a
|
||||||
user_message_role: Color::Rgb(49, 116, 143), // #31748f (foam)
|
user_message_role: Color::Rgb(49, 116, 143), // #31748f (foam)
|
||||||
assistant_message_role: Color::Rgb(156, 207, 216), // #9ccfd8 (foam light)
|
assistant_message_role: Color::Rgb(156, 207, 216), // #9ccfd8 (foam light)
|
||||||
thinking_panel_title: Color::Rgb(196, 167, 231), // #c4a7e7 (iris)
|
tool_output: Color::Rgb(110, 106, 134),
|
||||||
|
thinking_panel_title: Color::Rgb(196, 167, 231), // #c4a7e7 (iris)
|
||||||
command_bar_background: Color::Rgb(38, 35, 58),
|
command_bar_background: Color::Rgb(38, 35, 58),
|
||||||
status_background: Color::Rgb(38, 35, 58),
|
status_background: Color::Rgb(38, 35, 58),
|
||||||
mode_normal: Color::Rgb(156, 207, 216),
|
mode_normal: Color::Rgb(156, 207, 216),
|
||||||
@@ -471,7 +483,8 @@ fn monokai() -> Theme {
|
|||||||
unfocused_panel_border: Color::Rgb(117, 113, 94), // #75715e
|
unfocused_panel_border: Color::Rgb(117, 113, 94), // #75715e
|
||||||
user_message_role: Color::Rgb(102, 217, 239), // #66d9ef (cyan)
|
user_message_role: Color::Rgb(102, 217, 239), // #66d9ef (cyan)
|
||||||
assistant_message_role: Color::Rgb(174, 129, 255), // #ae81ff (purple)
|
assistant_message_role: Color::Rgb(174, 129, 255), // #ae81ff (purple)
|
||||||
thinking_panel_title: Color::Rgb(230, 219, 116), // #e6db74 (yellow)
|
tool_output: Color::Rgb(117, 113, 94),
|
||||||
|
thinking_panel_title: Color::Rgb(230, 219, 116), // #e6db74 (yellow)
|
||||||
command_bar_background: Color::Rgb(39, 40, 34),
|
command_bar_background: Color::Rgb(39, 40, 34),
|
||||||
status_background: Color::Rgb(39, 40, 34),
|
status_background: Color::Rgb(39, 40, 34),
|
||||||
mode_normal: Color::Rgb(102, 217, 239),
|
mode_normal: Color::Rgb(102, 217, 239),
|
||||||
@@ -500,7 +513,8 @@ fn material_dark() -> Theme {
|
|||||||
unfocused_panel_border: Color::Rgb(84, 110, 122), // #546e7a
|
unfocused_panel_border: Color::Rgb(84, 110, 122), // #546e7a
|
||||||
user_message_role: Color::Rgb(130, 170, 255), // #82aaff (blue)
|
user_message_role: Color::Rgb(130, 170, 255), // #82aaff (blue)
|
||||||
assistant_message_role: Color::Rgb(199, 146, 234), // #c792ea (purple)
|
assistant_message_role: Color::Rgb(199, 146, 234), // #c792ea (purple)
|
||||||
thinking_panel_title: Color::Rgb(255, 203, 107), // #ffcb6b (yellow)
|
tool_output: Color::Rgb(84, 110, 122),
|
||||||
|
thinking_panel_title: Color::Rgb(255, 203, 107), // #ffcb6b (yellow)
|
||||||
command_bar_background: Color::Rgb(33, 43, 48),
|
command_bar_background: Color::Rgb(33, 43, 48),
|
||||||
status_background: Color::Rgb(33, 43, 48),
|
status_background: Color::Rgb(33, 43, 48),
|
||||||
mode_normal: Color::Rgb(130, 170, 255),
|
mode_normal: Color::Rgb(130, 170, 255),
|
||||||
@@ -529,6 +543,7 @@ fn material_light() -> Theme {
|
|||||||
unfocused_panel_border: Color::Rgb(176, 190, 197),
|
unfocused_panel_border: Color::Rgb(176, 190, 197),
|
||||||
user_message_role: Color::Rgb(68, 138, 255),
|
user_message_role: Color::Rgb(68, 138, 255),
|
||||||
assistant_message_role: Color::Rgb(124, 77, 255),
|
assistant_message_role: Color::Rgb(124, 77, 255),
|
||||||
|
tool_output: Color::Rgb(144, 164, 174),
|
||||||
thinking_panel_title: Color::Rgb(245, 124, 0),
|
thinking_panel_title: Color::Rgb(245, 124, 0),
|
||||||
command_bar_background: Color::Rgb(255, 255, 255),
|
command_bar_background: Color::Rgb(255, 255, 255),
|
||||||
status_background: Color::Rgb(255, 255, 255),
|
status_background: Color::Rgb(255, 255, 255),
|
||||||
|
|||||||
111
crates/owlen-core/src/tools/fs_tools.rs
Normal file
111
crates/owlen-core/src/tools/fs_tools.rs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
use crate::tools::{Tool, ToolResult};
|
||||||
|
use anyhow::Result;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use path_clean::PathClean;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct FileArgs {
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_path(path: &str, root: &Path) -> Result<PathBuf> {
|
||||||
|
let path = Path::new(path);
|
||||||
|
let path = if path.is_absolute() {
|
||||||
|
path.strip_prefix("/")
|
||||||
|
.map_err(|_| anyhow::anyhow!("Invalid path"))?
|
||||||
|
.to_path_buf()
|
||||||
|
} else {
|
||||||
|
path.to_path_buf()
|
||||||
|
};
|
||||||
|
|
||||||
|
let full_path = root.join(path).clean();
|
||||||
|
|
||||||
|
if !full_path.starts_with(root) {
|
||||||
|
return Err(anyhow::anyhow!("Path traversal detected"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(full_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ResourcesListTool;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Tool for ResourcesListTool {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"resources/list"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Lists directory contents."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schema(&self) -> serde_json::Value {
|
||||||
|
json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The path to the directory to list."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["path"]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(&self, args: serde_json::Value) -> Result<ToolResult> {
|
||||||
|
let args: FileArgs = serde_json::from_value(args)?;
|
||||||
|
let root = env::current_dir()?;
|
||||||
|
let full_path = sanitize_path(&args.path, &root)?;
|
||||||
|
|
||||||
|
let entries = fs::read_dir(full_path)?;
|
||||||
|
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for entry in entries {
|
||||||
|
let entry = entry?;
|
||||||
|
result.push(entry.file_name().to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ToolResult::success(serde_json::to_value(result)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ResourcesGetTool;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Tool for ResourcesGetTool {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"resources/get"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Reads file content."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schema(&self) -> serde_json::Value {
|
||||||
|
json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The path to the file to read."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["path"]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(&self, args: serde_json::Value) -> Result<ToolResult> {
|
||||||
|
let args: FileArgs = serde_json::from_value(args)?;
|
||||||
|
let root = env::current_dir()?;
|
||||||
|
let full_path = sanitize_path(&args.path, &root)?;
|
||||||
|
|
||||||
|
let content = fs::read_to_string(full_path)?;
|
||||||
|
|
||||||
|
Ok(ToolResult::success(serde_json::to_value(content)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ use async_trait::async_trait;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
pub mod code_exec;
|
pub mod code_exec;
|
||||||
|
pub mod fs_tools;
|
||||||
pub mod registry;
|
pub mod registry;
|
||||||
pub mod web_search;
|
pub mod web_search;
|
||||||
pub mod web_search_detailed;
|
pub mod web_search_detailed;
|
||||||
|
|||||||
11
crates/owlen-mcp-server/Cargo.toml
Normal file
11
crates/owlen-mcp-server/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "owlen-mcp-server"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
anyhow = "1.0"
|
||||||
|
path-clean = "1.0"
|
||||||
180
crates/owlen-mcp-server/src/main.rs
Normal file
180
crates/owlen-mcp-server/src/main.rs
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
use path_clean::PathClean;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Request {
|
||||||
|
id: u64,
|
||||||
|
method: String,
|
||||||
|
params: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct Response {
|
||||||
|
id: u64,
|
||||||
|
result: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ErrorResponse {
|
||||||
|
id: u64,
|
||||||
|
error: JsonRpcError,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct JsonRpcError {
|
||||||
|
code: i64,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct FileArgs {
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_request(req: Request, root: &Path) -> Result<serde_json::Value, JsonRpcError> {
|
||||||
|
match req.method.as_str() {
|
||||||
|
"resources/list" => {
|
||||||
|
let args: FileArgs = serde_json::from_value(req.params).map_err(|e| JsonRpcError {
|
||||||
|
code: -32602,
|
||||||
|
message: format!("Invalid params: {}", e),
|
||||||
|
})?;
|
||||||
|
resources_list(&args.path, root).await
|
||||||
|
}
|
||||||
|
"resources/get" => {
|
||||||
|
let args: FileArgs = serde_json::from_value(req.params).map_err(|e| JsonRpcError {
|
||||||
|
code: -32602,
|
||||||
|
message: format!("Invalid params: {}", e),
|
||||||
|
})?;
|
||||||
|
resources_get(&args.path, root).await
|
||||||
|
}
|
||||||
|
_ => Err(JsonRpcError {
|
||||||
|
code: -32601,
|
||||||
|
message: "Method not found".to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_path(path: &str, root: &Path) -> Result<PathBuf, JsonRpcError> {
|
||||||
|
let path = Path::new(path);
|
||||||
|
let path = if path.is_absolute() {
|
||||||
|
path.strip_prefix("/")
|
||||||
|
.map_err(|_| JsonRpcError {
|
||||||
|
code: -32602,
|
||||||
|
message: "Invalid path".to_string(),
|
||||||
|
})?
|
||||||
|
.to_path_buf()
|
||||||
|
} else {
|
||||||
|
path.to_path_buf()
|
||||||
|
};
|
||||||
|
|
||||||
|
let full_path = root.join(path).clean();
|
||||||
|
|
||||||
|
if !full_path.starts_with(root) {
|
||||||
|
return Err(JsonRpcError {
|
||||||
|
code: -32602,
|
||||||
|
message: "Path traversal detected".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(full_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resources_list(path: &str, root: &Path) -> Result<serde_json::Value, JsonRpcError> {
|
||||||
|
let full_path = sanitize_path(path, root)?;
|
||||||
|
|
||||||
|
let entries = fs::read_dir(full_path).map_err(|e| JsonRpcError {
|
||||||
|
code: -32000,
|
||||||
|
message: format!("Failed to read directory: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for entry in entries {
|
||||||
|
let entry = entry.map_err(|e| JsonRpcError {
|
||||||
|
code: -32000,
|
||||||
|
message: format!("Failed to read directory entry: {}", e),
|
||||||
|
})?;
|
||||||
|
result.push(entry.file_name().to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(serde_json::json!(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resources_get(path: &str, root: &Path) -> Result<serde_json::Value, JsonRpcError> {
|
||||||
|
let full_path = sanitize_path(path, root)?;
|
||||||
|
|
||||||
|
let content = fs::read_to_string(full_path).map_err(|e| JsonRpcError {
|
||||||
|
code: -32000,
|
||||||
|
message: format!("Failed to read file: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(serde_json::json!(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let root = env::current_dir()?;
|
||||||
|
let mut stdin = io::BufReader::new(io::stdin());
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut line = String::new();
|
||||||
|
match stdin.read_line(&mut line).await {
|
||||||
|
Ok(0) => {
|
||||||
|
// EOF
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
let req: Request = match serde_json::from_str(&line) {
|
||||||
|
Ok(req) => req,
|
||||||
|
Err(e) => {
|
||||||
|
let err_resp = ErrorResponse {
|
||||||
|
id: 0,
|
||||||
|
error: JsonRpcError {
|
||||||
|
code: -32700,
|
||||||
|
message: format!("Parse error: {}", e),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let resp_str = serde_json::to_string(&err_resp)?;
|
||||||
|
stdout.write_all(resp_str.as_bytes()).await?;
|
||||||
|
stdout.write_all(b"\n").await?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let request_id = req.id;
|
||||||
|
|
||||||
|
match handle_request(req, &root).await {
|
||||||
|
Ok(result) => {
|
||||||
|
let resp = Response {
|
||||||
|
id: request_id,
|
||||||
|
result,
|
||||||
|
};
|
||||||
|
let resp_str = serde_json::to_string(&resp)?;
|
||||||
|
stdout.write_all(resp_str.as_bytes()).await?;
|
||||||
|
stdout.write_all(b"\n").await?;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
let err_resp = ErrorResponse {
|
||||||
|
id: request_id,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
let resp_str = serde_json::to_string(&err_resp)?;
|
||||||
|
stdout.write_all(resp_str.as_bytes()).await?;
|
||||||
|
stdout.write_all(b"\n").await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Handle read error
|
||||||
|
eprintln!("Error reading from stdin: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -956,21 +956,21 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_api_key_expands_braced_env_reference() {
|
fn resolve_api_key_expands_braced_env_reference() {
|
||||||
std::env::set_var("OWLEN_TEST_KEY", "super-secret");
|
std::env::set_var("OWLEN_TEST_KEY_BRACED", "super-secret");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_api_key(Some("${OWLEN_TEST_KEY}".into())),
|
resolve_api_key(Some("${OWLEN_TEST_KEY_BRACED}".into())),
|
||||||
Some("super-secret".into())
|
Some("super-secret".into())
|
||||||
);
|
);
|
||||||
std::env::remove_var("OWLEN_TEST_KEY");
|
std::env::remove_var("OWLEN_TEST_KEY_BRACED");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_api_key_expands_unbraced_env_reference() {
|
fn resolve_api_key_expands_unbraced_env_reference() {
|
||||||
std::env::set_var("OWLEN_TEST_KEY", "another-secret");
|
std::env::set_var("OWLEN_TEST_KEY_UNBRACED", "another-secret");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_api_key(Some("$OWLEN_TEST_KEY".into())),
|
resolve_api_key(Some("$OWLEN_TEST_KEY_UNBRACED".into())),
|
||||||
Some("another-secret".into())
|
Some("another-secret".into())
|
||||||
);
|
);
|
||||||
std::env::remove_var("OWLEN_TEST_KEY");
|
std::env::remove_var("OWLEN_TEST_KEY_UNBRACED");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -353,7 +353,6 @@ impl ChatApp {
|
|||||||
("open", "Alias for load"),
|
("open", "Alias for load"),
|
||||||
("o", "Alias for load"),
|
("o", "Alias for load"),
|
||||||
("sessions", "List saved sessions"),
|
("sessions", "List saved sessions"),
|
||||||
("ls", "Alias for sessions"),
|
|
||||||
("help", "Show help documentation"),
|
("help", "Show help documentation"),
|
||||||
("h", "Alias for help"),
|
("h", "Alias for help"),
|
||||||
("model", "Select a model"),
|
("model", "Select a model"),
|
||||||
@@ -363,6 +362,9 @@ impl ChatApp {
|
|||||||
("theme", "Switch theme"),
|
("theme", "Switch theme"),
|
||||||
("themes", "List available themes"),
|
("themes", "List available themes"),
|
||||||
("reload", "Reload configuration and themes"),
|
("reload", "Reload configuration and themes"),
|
||||||
|
("e", "Edit a file"),
|
||||||
|
("edit", "Alias for edit"),
|
||||||
|
("ls", "List directory contents"),
|
||||||
("privacy-enable", "Enable a privacy-sensitive tool"),
|
("privacy-enable", "Enable a privacy-sensitive tool"),
|
||||||
("privacy-disable", "Disable a privacy-sensitive tool"),
|
("privacy-disable", "Disable a privacy-sensitive tool"),
|
||||||
("privacy-clear", "Clear stored secure data"),
|
("privacy-clear", "Clear stored secure data"),
|
||||||
@@ -1385,7 +1387,7 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"sessions" | "ls" => {
|
"sessions" => {
|
||||||
// List saved sessions
|
// List saved sessions
|
||||||
match self.controller.list_saved_sessions().await {
|
match self.controller.list_saved_sessions().await {
|
||||||
Ok(sessions) => {
|
Ok(sessions) => {
|
||||||
@@ -1419,6 +1421,47 @@ impl ChatApp {
|
|||||||
self.controller.start_new_conversation(None, None);
|
self.controller.start_new_conversation(None, None);
|
||||||
self.status = "Started new conversation".to_string();
|
self.status = "Started new conversation".to_string();
|
||||||
}
|
}
|
||||||
|
"e" | "edit" => {
|
||||||
|
if let Some(path) = args.first() {
|
||||||
|
match self.controller.read_file(path).await {
|
||||||
|
Ok(content) => {
|
||||||
|
let message = format!(
|
||||||
|
"The content of file `{}` is:\n```\n{}\n```",
|
||||||
|
path, content
|
||||||
|
);
|
||||||
|
self.controller
|
||||||
|
.conversation_mut()
|
||||||
|
.push_user_message(message);
|
||||||
|
self.pending_llm_request = true;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.error =
|
||||||
|
Some(format!("Failed to read file: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.error = Some("Usage: :e <path>".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"ls" => {
|
||||||
|
let path = args.first().copied().unwrap_or(".");
|
||||||
|
match self.controller.list_dir(path).await {
|
||||||
|
Ok(entries) => {
|
||||||
|
let message = format!(
|
||||||
|
"Directory listing for `{}`:\n```\n{}\n```",
|
||||||
|
path,
|
||||||
|
entries.join("\n")
|
||||||
|
);
|
||||||
|
self.controller
|
||||||
|
.conversation_mut()
|
||||||
|
.push_user_message(message);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.error =
|
||||||
|
Some(format!("Failed to list directory: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
"theme" => {
|
"theme" => {
|
||||||
if args.is_empty() {
|
if args.is_empty() {
|
||||||
self.error = Some("Usage: :theme <name>".to_string());
|
self.error = Some("Usage: :theme <name>".to_string());
|
||||||
|
|||||||
@@ -670,7 +670,13 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
|
|
||||||
let chunks_len = chunks.len();
|
let chunks_len = chunks.len();
|
||||||
for (i, seg) in chunks.into_iter().enumerate() {
|
for (i, seg) in chunks.into_iter().enumerate() {
|
||||||
let mut spans = vec![Span::raw(format!("{indent}{}", seg))];
|
let style = if matches!(role, Role::Tool) {
|
||||||
|
Style::default().fg(theme.tool_output)
|
||||||
|
} else {
|
||||||
|
Style::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut spans = vec![Span::styled(format!("{indent}{}", seg), style)];
|
||||||
if i == chunks_len - 1 && is_streaming {
|
if i == chunks_len - 1 && is_streaming {
|
||||||
spans.push(Span::styled(" ▌", Style::default().fg(theme.cursor)));
|
spans.push(Span::styled(" ▌", Style::default().fg(theme.cursor)));
|
||||||
}
|
}
|
||||||
@@ -682,7 +688,13 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
let chunks = wrap(&content, content_width as usize);
|
let chunks = wrap(&content, content_width as usize);
|
||||||
let chunks_len = chunks.len();
|
let chunks_len = chunks.len();
|
||||||
for (i, seg) in chunks.into_iter().enumerate() {
|
for (i, seg) in chunks.into_iter().enumerate() {
|
||||||
let mut spans = vec![Span::raw(seg.into_owned())];
|
let style = if matches!(role, Role::Tool) {
|
||||||
|
Style::default().fg(theme.tool_output)
|
||||||
|
} else {
|
||||||
|
Style::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut spans = vec![Span::styled(seg.into_owned(), style)];
|
||||||
if i == chunks_len - 1 && is_streaming {
|
if i == chunks_len - 1 && is_streaming {
|
||||||
spans.push(Span::styled(" ▌", Style::default().fg(theme.cursor)));
|
spans.push(Span::styled(" ▌", Style::default().fg(theme.cursor)));
|
||||||
}
|
}
|
||||||
@@ -2075,7 +2087,7 @@ fn role_color(role: &Role, theme: &owlen_core::theme::Theme) -> Style {
|
|||||||
match role {
|
match role {
|
||||||
Role::User => Style::default().fg(theme.user_message_role),
|
Role::User => Style::default().fg(theme.user_message_role),
|
||||||
Role::Assistant => Style::default().fg(theme.assistant_message_role),
|
Role::Assistant => Style::default().fg(theme.assistant_message_role),
|
||||||
Role::System => Style::default().fg(theme.info),
|
Role::System => Style::default().fg(theme.unfocused_panel_border),
|
||||||
Role::Tool => Style::default().fg(theme.info),
|
Role::Tool => Style::default().fg(theme.info),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2085,14 +2097,17 @@ fn format_tool_output(content: &str) -> String {
|
|||||||
// Try to parse as JSON
|
// Try to parse as JSON
|
||||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(content) {
|
if let Ok(json) = serde_json::from_str::<serde_json::Value>(content) {
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
|
let mut content_found = false;
|
||||||
|
|
||||||
// Extract query if present
|
// Extract query if present
|
||||||
if let Some(query) = json.get("query").and_then(|v| v.as_str()) {
|
if let Some(query) = json.get("query").and_then(|v| v.as_str()) {
|
||||||
output.push_str(&format!("Query: \"{}\"\n\n", query));
|
output.push_str(&format!("Query: \"{}\"\n\n", query));
|
||||||
|
content_found = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract results array
|
// Extract results array
|
||||||
if let Some(results) = json.get("results").and_then(|v| v.as_array()) {
|
if let Some(results) = json.get("results").and_then(|v| v.as_array()) {
|
||||||
|
content_found = true;
|
||||||
if results.is_empty() {
|
if results.is_empty() {
|
||||||
output.push_str("No results found");
|
output.push_str("No results found");
|
||||||
return output;
|
return output;
|
||||||
@@ -2158,12 +2173,20 @@ fn format_tool_output(content: &str) -> String {
|
|||||||
if let Some(total) = json.get("total_found").and_then(|v| v.as_u64()) {
|
if let Some(total) = json.get("total_found").and_then(|v| v.as_u64()) {
|
||||||
output.push_str(&format!("Found {} result(s)", total));
|
output.push_str(&format!("Found {} result(s)", total));
|
||||||
}
|
}
|
||||||
|
} else if let Some(result) = json.get("result").and_then(|v| v.as_str()) {
|
||||||
|
content_found = true;
|
||||||
|
output.push_str(result);
|
||||||
} else if let Some(error) = json.get("error").and_then(|v| v.as_str()) {
|
} else if let Some(error) = json.get("error").and_then(|v| v.as_str()) {
|
||||||
|
content_found = true;
|
||||||
// Handle error results
|
// Handle error results
|
||||||
output.push_str(&format!("❌ Error: {}", error));
|
output.push_str(&format!("❌ Error: {}", error));
|
||||||
}
|
}
|
||||||
|
|
||||||
output
|
if content_found {
|
||||||
|
output
|
||||||
|
} else {
|
||||||
|
content.to_string()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// If not JSON, return as-is
|
// If not JSON, return as-is
|
||||||
content.to_string()
|
content.to_string()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ focused_panel_border = "lightmagenta"
|
|||||||
unfocused_panel_border = "#5f1487"
|
unfocused_panel_border = "#5f1487"
|
||||||
user_message_role = "lightblue"
|
user_message_role = "lightblue"
|
||||||
assistant_message_role = "yellow"
|
assistant_message_role = "yellow"
|
||||||
|
tool_output = "gray"
|
||||||
thinking_panel_title = "lightmagenta"
|
thinking_panel_title = "lightmagenta"
|
||||||
command_bar_background = "black"
|
command_bar_background = "black"
|
||||||
status_background = "black"
|
status_background = "black"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ focused_panel_border = "#4a90e2"
|
|||||||
unfocused_panel_border = "#dddddd"
|
unfocused_panel_border = "#dddddd"
|
||||||
user_message_role = "#0055a4"
|
user_message_role = "#0055a4"
|
||||||
assistant_message_role = "#8e44ad"
|
assistant_message_role = "#8e44ad"
|
||||||
|
tool_output = "gray"
|
||||||
thinking_panel_title = "#8e44ad"
|
thinking_panel_title = "#8e44ad"
|
||||||
command_bar_background = "white"
|
command_bar_background = "white"
|
||||||
status_background = "white"
|
status_background = "white"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ focused_panel_border = "#ff79c6"
|
|||||||
unfocused_panel_border = "#44475a"
|
unfocused_panel_border = "#44475a"
|
||||||
user_message_role = "#8be9fd"
|
user_message_role = "#8be9fd"
|
||||||
assistant_message_role = "#ff79c6"
|
assistant_message_role = "#ff79c6"
|
||||||
|
tool_output = "#6272a4"
|
||||||
thinking_panel_title = "#bd93f9"
|
thinking_panel_title = "#bd93f9"
|
||||||
command_bar_background = "#44475a"
|
command_bar_background = "#44475a"
|
||||||
status_background = "#44475a"
|
status_background = "#44475a"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ focused_panel_border = "#fe8019"
|
|||||||
unfocused_panel_border = "#7c6f64"
|
unfocused_panel_border = "#7c6f64"
|
||||||
user_message_role = "#b8bb26"
|
user_message_role = "#b8bb26"
|
||||||
assistant_message_role = "#83a598"
|
assistant_message_role = "#83a598"
|
||||||
|
tool_output = "#928374"
|
||||||
thinking_panel_title = "#d3869b"
|
thinking_panel_title = "#d3869b"
|
||||||
command_bar_background = "#3c3836"
|
command_bar_background = "#3c3836"
|
||||||
status_background = "#3c3836"
|
status_background = "#3c3836"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ focused_panel_border = "#80cbc4"
|
|||||||
unfocused_panel_border = "#546e7a"
|
unfocused_panel_border = "#546e7a"
|
||||||
user_message_role = "#82aaff"
|
user_message_role = "#82aaff"
|
||||||
assistant_message_role = "#c792ea"
|
assistant_message_role = "#c792ea"
|
||||||
|
tool_output = "#546e7a"
|
||||||
thinking_panel_title = "#ffcb6b"
|
thinking_panel_title = "#ffcb6b"
|
||||||
command_bar_background = "#212b30"
|
command_bar_background = "#212b30"
|
||||||
status_background = "#212b30"
|
status_background = "#212b30"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ focused_panel_border = "#009688"
|
|||||||
unfocused_panel_border = "#b0bec5"
|
unfocused_panel_border = "#b0bec5"
|
||||||
user_message_role = "#448aff"
|
user_message_role = "#448aff"
|
||||||
assistant_message_role = "#7c4dff"
|
assistant_message_role = "#7c4dff"
|
||||||
|
tool_output = "#90a4ae"
|
||||||
thinking_panel_title = "#f57c00"
|
thinking_panel_title = "#f57c00"
|
||||||
command_bar_background = "#ffffff"
|
command_bar_background = "#ffffff"
|
||||||
status_background = "#ffffff"
|
status_background = "#ffffff"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ focused_panel_border = "#58a6ff"
|
|||||||
unfocused_panel_border = "#30363d"
|
unfocused_panel_border = "#30363d"
|
||||||
user_message_role = "#79c0ff"
|
user_message_role = "#79c0ff"
|
||||||
assistant_message_role = "#89ddff"
|
assistant_message_role = "#89ddff"
|
||||||
|
tool_output = "#546e7a"
|
||||||
thinking_panel_title = "#9ece6a"
|
thinking_panel_title = "#9ece6a"
|
||||||
command_bar_background = "#161b22"
|
command_bar_background = "#161b22"
|
||||||
status_background = "#161b22"
|
status_background = "#161b22"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ focused_panel_border = "#f92672"
|
|||||||
unfocused_panel_border = "#75715e"
|
unfocused_panel_border = "#75715e"
|
||||||
user_message_role = "#66d9ef"
|
user_message_role = "#66d9ef"
|
||||||
assistant_message_role = "#ae81ff"
|
assistant_message_role = "#ae81ff"
|
||||||
|
tool_output = "#75715e"
|
||||||
thinking_panel_title = "#e6db74"
|
thinking_panel_title = "#e6db74"
|
||||||
command_bar_background = "#272822"
|
command_bar_background = "#272822"
|
||||||
status_background = "#272822"
|
status_background = "#272822"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ focused_panel_border = "#eb6f92"
|
|||||||
unfocused_panel_border = "#26233a"
|
unfocused_panel_border = "#26233a"
|
||||||
user_message_role = "#31748f"
|
user_message_role = "#31748f"
|
||||||
assistant_message_role = "#9ccfd8"
|
assistant_message_role = "#9ccfd8"
|
||||||
|
tool_output = "#6e6a86"
|
||||||
thinking_panel_title = "#c4a7e7"
|
thinking_panel_title = "#c4a7e7"
|
||||||
command_bar_background = "#26233a"
|
command_bar_background = "#26233a"
|
||||||
status_background = "#26233a"
|
status_background = "#26233a"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ focused_panel_border = "#268bd2"
|
|||||||
unfocused_panel_border = "#073642"
|
unfocused_panel_border = "#073642"
|
||||||
user_message_role = "#2aa198"
|
user_message_role = "#2aa198"
|
||||||
assistant_message_role = "#cb4b16"
|
assistant_message_role = "#cb4b16"
|
||||||
|
tool_output = "#657b83"
|
||||||
thinking_panel_title = "#6c71c4"
|
thinking_panel_title = "#6c71c4"
|
||||||
command_bar_background = "#073642"
|
command_bar_background = "#073642"
|
||||||
status_background = "#073642"
|
status_background = "#073642"
|
||||||
|
|||||||
Reference in New Issue
Block a user