add cli config and mcp
This commit is contained in:
parent
536da41775
commit
4ffef6c666
5 changed files with 242 additions and 11 deletions
|
|
@ -38,6 +38,7 @@ thiserror = "1.0"
|
|||
# Utilities
|
||||
chrono = "0.4"
|
||||
walkdir = "2.5"
|
||||
dirs = "5.0"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
|
|
|
|||
|
|
@ -1,29 +1,153 @@
|
|||
use anyhow::{Context, Result};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
const DEFAULT_API_URL: &str = "http://localhost:8080";
|
||||
const CONFIG_FILE_NAME: &str = "config";
|
||||
const CONFIG_DIR_NAME: &str = ".memora";
|
||||
|
||||
pub struct Config {
|
||||
pub api_url: String,
|
||||
pub source: ConfigSource,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ConfigSource {
|
||||
LocalFile,
|
||||
Environment,
|
||||
Default,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ConfigSource {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ConfigSource::LocalFile => write!(f, "config file"),
|
||||
ConfigSource::Environment => write!(f, "environment variable"),
|
||||
ConfigSource::Default => write!(f, "default"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_env() -> Result<Self> {
|
||||
let api_url = env::var("MEMORA_API_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:8080".to_string());
|
||||
/// Load configuration with the following priority:
|
||||
/// 1. Environment variable (MEMORA_API_URL) - highest priority, for overrides
|
||||
/// 2. Local config file (~/.memora/config.toml)
|
||||
/// 3. Default (http://localhost:8080)
|
||||
pub fn load() -> Result<Self> {
|
||||
// 1. Environment variable takes highest priority (for overrides)
|
||||
if let Ok(api_url) = env::var("MEMORA_API_URL") {
|
||||
return Self::validate_and_create(api_url, ConfigSource::Environment);
|
||||
}
|
||||
|
||||
// Validate URL format
|
||||
// 2. Try local config file
|
||||
if let Some(api_url) = Self::load_from_file()? {
|
||||
return Self::validate_and_create(api_url, ConfigSource::LocalFile);
|
||||
}
|
||||
|
||||
// 3. Fall back to default
|
||||
Self::validate_and_create(DEFAULT_API_URL.to_string(), ConfigSource::Default)
|
||||
}
|
||||
|
||||
/// Legacy method for backwards compatibility
|
||||
pub fn from_env() -> Result<Self> {
|
||||
Self::load()
|
||||
}
|
||||
|
||||
fn validate_and_create(api_url: String, source: ConfigSource) -> Result<Self> {
|
||||
if !api_url.starts_with("http://") && !api_url.starts_with("https://") {
|
||||
anyhow::bail!(
|
||||
"Invalid API URL: {}. Must start with http:// or https://",
|
||||
api_url
|
||||
);
|
||||
}
|
||||
Ok(Config { api_url, source })
|
||||
}
|
||||
|
||||
Ok(Config { api_url })
|
||||
fn config_dir() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|home| home.join(CONFIG_DIR_NAME))
|
||||
}
|
||||
|
||||
fn config_file_path() -> Option<PathBuf> {
|
||||
Self::config_dir().map(|dir| dir.join(CONFIG_FILE_NAME))
|
||||
}
|
||||
|
||||
fn load_from_file() -> Result<Option<String>> {
|
||||
let config_path = match Self::config_file_path() {
|
||||
Some(path) => path,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
if !config_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&config_path)
|
||||
.with_context(|| format!("Failed to read config file: {}", config_path.display()))?;
|
||||
|
||||
// Simple TOML parsing for api_url
|
||||
for line in content.lines() {
|
||||
let line = line.trim();
|
||||
if line.starts_with("api_url") {
|
||||
if let Some(value) = line.split('=').nth(1) {
|
||||
let value = value.trim().trim_matches('"').trim_matches('\'');
|
||||
if !value.is_empty() {
|
||||
return Ok(Some(value.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn save_api_url(api_url: &str) -> Result<PathBuf> {
|
||||
let config_dir = Self::config_dir()
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
|
||||
|
||||
// Create config directory if it doesn't exist
|
||||
if !config_dir.exists() {
|
||||
fs::create_dir_all(&config_dir)
|
||||
.with_context(|| format!("Failed to create config directory: {}", config_dir.display()))?;
|
||||
}
|
||||
|
||||
let config_path = config_dir.join(CONFIG_FILE_NAME);
|
||||
let content = format!("api_url = \"{}\"\n", api_url);
|
||||
|
||||
fs::write(&config_path, content)
|
||||
.with_context(|| format!("Failed to write config file: {}", config_path.display()))?;
|
||||
|
||||
Ok(config_path)
|
||||
}
|
||||
|
||||
pub fn api_url(&self) -> &str {
|
||||
&self.api_url
|
||||
}
|
||||
|
||||
pub fn config_file_path_display() -> String {
|
||||
Self::config_file_path()
|
||||
.map(|p| p.display().to_string())
|
||||
.unwrap_or_else(|| "~/.memora/config.toml".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Prompt user for API URL interactively
|
||||
pub fn prompt_api_url(current_url: Option<&str>) -> Result<String> {
|
||||
let default = current_url.unwrap_or(DEFAULT_API_URL);
|
||||
|
||||
print!("Enter API URL [{}]: ", default);
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
|
||||
let input = input.trim();
|
||||
if input.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(input.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_doc_id() -> String {
|
||||
|
|
|
|||
|
|
@ -172,9 +172,15 @@ fn format_error_message(err: &anyhow::Error, api_url: &str) -> String {
|
|||
|
||||
pub fn print_config_help() {
|
||||
println!("\n{}", "Configuration:".bright_cyan().bold());
|
||||
println!(" Set the API URL using an environment variable:");
|
||||
println!(" {}", "export MEMORA_API_URL=http://localhost:8080".bright_white());
|
||||
println!("\n Add to your shell profile to make it permanent:");
|
||||
println!(" {}", "echo 'export MEMORA_API_URL=http://localhost:8080' >> ~/.zshrc".bright_black());
|
||||
println!(" Run the configure command to set the API URL:");
|
||||
println!(" {}", "memora configure".bright_white());
|
||||
println!();
|
||||
println!(" Or set it directly:");
|
||||
println!(" {}", "memora configure --api-url http://your-api:8080".bright_white());
|
||||
println!();
|
||||
println!(" {}", "Configuration priority:".bright_yellow());
|
||||
println!(" 1. Environment variable (MEMORA_API_URL) - highest priority");
|
||||
println!(" 2. Config file (~/.memora/config)");
|
||||
println!(" 3. Default (http://localhost:8080)");
|
||||
println!();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ impl From<Format> for OutputFormat {
|
|||
#[command(name = "memora")]
|
||||
#[command(about = "Memora CLI - Semantic memory system", long_about = None)]
|
||||
#[command(version)]
|
||||
#[command(after_help = get_after_help())]
|
||||
struct Cli {
|
||||
/// Output format (pretty, json, yaml)
|
||||
#[arg(short = 'o', long, global = true, default_value = "pretty")]
|
||||
|
|
@ -47,6 +48,18 @@ struct Cli {
|
|||
command: Commands,
|
||||
}
|
||||
|
||||
fn get_after_help() -> String {
|
||||
let config = config::Config::load().ok();
|
||||
let (api_url, source) = match &config {
|
||||
Some(c) => (c.api_url.as_str(), c.source.to_string()),
|
||||
None => ("http://localhost:8080", "default".to_string()),
|
||||
};
|
||||
format!(
|
||||
"Current API URL: {} (from {})\n\nRun 'memora configure' to change the API URL.",
|
||||
api_url, source
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Manage agents (list, profile, stats)
|
||||
|
|
@ -64,6 +77,14 @@ enum Commands {
|
|||
/// Manage async operations (list, cancel)
|
||||
#[command(subcommand)]
|
||||
Operation(OperationCommands),
|
||||
|
||||
/// Configure the CLI (API URL, etc.)
|
||||
#[command(after_help = "Configuration priority:\n 1. Environment variable (MEMORA_API_URL) - highest priority\n 2. Config file (~/.memora/config)\n 3. Default (http://localhost:8080)")]
|
||||
Configure {
|
||||
/// API URL to connect to (interactive prompt if not provided)
|
||||
#[arg(long)]
|
||||
api_url: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
|
|
@ -285,6 +306,11 @@ fn run() -> Result<()> {
|
|||
let output_format: OutputFormat = cli.output.into();
|
||||
let verbose = cli.verbose;
|
||||
|
||||
// Handle configure command before loading full config (it doesn't need API client)
|
||||
if let Commands::Configure { api_url } = cli.command {
|
||||
return handle_configure(api_url, output_format);
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
let config = Config::from_env().unwrap_or_else(|e| {
|
||||
ui::print_error(&format!("Configuration error: {}", e));
|
||||
|
|
@ -301,6 +327,7 @@ fn run() -> Result<()> {
|
|||
|
||||
// Execute command and handle errors
|
||||
let result: Result<()> = match cli.command {
|
||||
Commands::Configure { .. } => unreachable!(), // Handled above
|
||||
Commands::Agent(agent_cmd) => match agent_cmd {
|
||||
AgentCommands::List => {
|
||||
let spinner = if output_format == OutputFormat::Pretty {
|
||||
|
|
@ -1075,3 +1102,58 @@ fn run() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_configure(api_url: Option<String>, output_format: OutputFormat) -> Result<()> {
|
||||
// Load current config to show current state
|
||||
let current_config = Config::load().ok();
|
||||
|
||||
if output_format == OutputFormat::Pretty {
|
||||
ui::print_info("Memora CLI Configuration");
|
||||
println!();
|
||||
|
||||
// Show current configuration
|
||||
if let Some(ref config) = current_config {
|
||||
println!(" Current API URL: {}", config.api_url);
|
||||
println!(" Source: {}", config.source);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
// Get the new API URL (from argument or prompt)
|
||||
let new_api_url = match api_url {
|
||||
Some(url) => url,
|
||||
None => {
|
||||
// Interactive prompt
|
||||
let current = current_config.as_ref().map(|c| c.api_url.as_str());
|
||||
config::prompt_api_url(current)?
|
||||
}
|
||||
};
|
||||
|
||||
// Validate the URL
|
||||
if !new_api_url.starts_with("http://") && !new_api_url.starts_with("https://") {
|
||||
ui::print_error(&format!(
|
||||
"Invalid API URL: {}. Must start with http:// or https://",
|
||||
new_api_url
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Save to config file
|
||||
let config_path = Config::save_api_url(&new_api_url)?;
|
||||
|
||||
if output_format == OutputFormat::Pretty {
|
||||
ui::print_success(&format!("Configuration saved to {}", config_path.display()));
|
||||
println!();
|
||||
println!(" API URL: {}", new_api_url);
|
||||
println!();
|
||||
println!("Note: Environment variable MEMORA_API_URL will override this setting.");
|
||||
} else {
|
||||
let result = serde_json::json!({
|
||||
"api_url": new_api_url,
|
||||
"config_path": config_path.display().to_string(),
|
||||
});
|
||||
output::print_output(&result, output_format)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@ def create_mcp_server(memory: TemporalSemanticMemory) -> FastMCP:
|
|||
"""
|
||||
**CRITICAL: Store important user information to long-term memory.**
|
||||
|
||||
**⚠️ PER-USER TOOL - REQUIRES USER IDENTIFICATION:**
|
||||
- This tool is STRICTLY per-user. Each user MUST have a unique `agent_id`.
|
||||
- ONLY use this tool if you have a valid user identifier (user ID, email, session ID, etc.) to map to `agent_id`.
|
||||
- DO NOT use this tool if you cannot identify the specific user.
|
||||
- DO NOT share memories between different users - each user's memories are isolated by their `agent_id`.
|
||||
- If you don't have a user identifier, DO NOT use this tool at all.
|
||||
|
||||
Use this tool PROACTIVELY whenever the user shares:
|
||||
- Personal facts, preferences, or interests (e.g., "I love hiking", "I'm a vegetarian")
|
||||
- Important events or milestones (e.g., "I got promoted", "My birthday is June 15")
|
||||
|
|
@ -44,7 +51,9 @@ def create_mcp_server(memory: TemporalSemanticMemory) -> FastMCP:
|
|||
"career_goals", "project_details", etc. This helps organize and retrieve related memories later.
|
||||
|
||||
Args:
|
||||
agent_id: The unique identifier for the agent/user storing the memory
|
||||
agent_id: **REQUIRED** - The unique, persistent identifier for this specific user (e.g., user_id, email, session_id).
|
||||
This MUST be consistent across all interactions with the same user.
|
||||
Example: "user_12345", "alice@example.com", "session_abc123"
|
||||
content: The fact/memory to store (be specific and include relevant details)
|
||||
context: Categorize the memory (e.g., 'personal_preferences', 'work_history', 'hobbies', 'family')
|
||||
explanation: Optional explanation for why this memory is being stored
|
||||
|
|
@ -69,6 +78,13 @@ def create_mcp_server(memory: TemporalSemanticMemory) -> FastMCP:
|
|||
"""
|
||||
**CRITICAL: Search user's memory to provide personalized, context-aware responses.**
|
||||
|
||||
**⚠️ PER-USER TOOL - REQUIRES USER IDENTIFICATION:**
|
||||
- This tool is STRICTLY per-user. Each user MUST have a unique `agent_id`.
|
||||
- ONLY use this tool if you have a valid user identifier (user ID, email, session ID, etc.) to map to `agent_id`.
|
||||
- DO NOT use this tool if you cannot identify the specific user.
|
||||
- DO NOT search across multiple users - each user's memories are isolated by their `agent_id`.
|
||||
- If you don't have a user identifier, DO NOT use this tool at all.
|
||||
|
||||
Use this tool PROACTIVELY at the start of conversations or when making recommendations to:
|
||||
- Check user's preferences before making suggestions (e.g., "what foods does the user like?")
|
||||
- Recall user's history to provide continuity (e.g., "what projects has the user worked on?")
|
||||
|
|
@ -87,7 +103,9 @@ def create_mcp_server(memory: TemporalSemanticMemory) -> FastMCP:
|
|||
"user's work experience", "user's dietary restrictions", "what does the user know about X?"
|
||||
|
||||
Args:
|
||||
agent_id: The unique identifier for the agent/user whose memories to search
|
||||
agent_id: **REQUIRED** - The unique, persistent identifier for this specific user (e.g., user_id, email, session_id).
|
||||
This MUST be consistent across all interactions with the same user.
|
||||
Example: "user_12345", "alice@example.com", "session_abc123"
|
||||
query: Natural language search query to find relevant memories
|
||||
max_tokens: Maximum tokens for search context (default: 4096)
|
||||
explanation: Optional explanation for why this search is being performed
|
||||
|
|
|
|||
Loading…
Reference in a new issue