Skip to content

Commit 4ffef6c

Browse files
committed
add cli config and mcp
1 parent 536da41 commit 4ffef6c

File tree

5 files changed

+241
-10
lines changed

5 files changed

+241
-10
lines changed

memora-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ thiserror = "1.0"
3838
# Utilities
3939
chrono = "0.4"
4040
walkdir = "2.5"
41+
dirs = "5.0"
4142

4243
[profile.release]
4344
opt-level = "z"

memora-cli/src/config.rs

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,153 @@
11
use anyhow::{Context, Result};
22
use std::env;
3+
use std::fs;
4+
use std::io::{self, Write};
5+
use std::path::PathBuf;
6+
7+
const DEFAULT_API_URL: &str = "http://localhost:8080";
8+
const CONFIG_FILE_NAME: &str = "config";
9+
const CONFIG_DIR_NAME: &str = ".memora";
310

411
pub struct Config {
512
pub api_url: String,
13+
pub source: ConfigSource,
14+
}
15+
16+
#[derive(Debug, Clone, PartialEq)]
17+
pub enum ConfigSource {
18+
LocalFile,
19+
Environment,
20+
Default,
21+
}
22+
23+
impl std::fmt::Display for ConfigSource {
24+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25+
match self {
26+
ConfigSource::LocalFile => write!(f, "config file"),
27+
ConfigSource::Environment => write!(f, "environment variable"),
28+
ConfigSource::Default => write!(f, "default"),
29+
}
30+
}
631
}
732

833
impl Config {
34+
/// Load configuration with the following priority:
35+
/// 1. Environment variable (MEMORA_API_URL) - highest priority, for overrides
36+
/// 2. Local config file (~/.memora/config.toml)
37+
/// 3. Default (http://localhost:8080)
38+
pub fn load() -> Result<Self> {
39+
// 1. Environment variable takes highest priority (for overrides)
40+
if let Ok(api_url) = env::var("MEMORA_API_URL") {
41+
return Self::validate_and_create(api_url, ConfigSource::Environment);
42+
}
43+
44+
// 2. Try local config file
45+
if let Some(api_url) = Self::load_from_file()? {
46+
return Self::validate_and_create(api_url, ConfigSource::LocalFile);
47+
}
48+
49+
// 3. Fall back to default
50+
Self::validate_and_create(DEFAULT_API_URL.to_string(), ConfigSource::Default)
51+
}
52+
53+
/// Legacy method for backwards compatibility
954
pub fn from_env() -> Result<Self> {
10-
let api_url = env::var("MEMORA_API_URL")
11-
.unwrap_or_else(|_| "http://localhost:8080".to_string());
55+
Self::load()
56+
}
1257

13-
// Validate URL format
58+
fn validate_and_create(api_url: String, source: ConfigSource) -> Result<Self> {
1459
if !api_url.starts_with("http://") && !api_url.starts_with("https://") {
1560
anyhow::bail!(
1661
"Invalid API URL: {}. Must start with http:// or https://",
1762
api_url
1863
);
1964
}
65+
Ok(Config { api_url, source })
66+
}
67+
68+
fn config_dir() -> Option<PathBuf> {
69+
dirs::home_dir().map(|home| home.join(CONFIG_DIR_NAME))
70+
}
71+
72+
fn config_file_path() -> Option<PathBuf> {
73+
Self::config_dir().map(|dir| dir.join(CONFIG_FILE_NAME))
74+
}
75+
76+
fn load_from_file() -> Result<Option<String>> {
77+
let config_path = match Self::config_file_path() {
78+
Some(path) => path,
79+
None => return Ok(None),
80+
};
81+
82+
if !config_path.exists() {
83+
return Ok(None);
84+
}
85+
86+
let content = fs::read_to_string(&config_path)
87+
.with_context(|| format!("Failed to read config file: {}", config_path.display()))?;
88+
89+
// Simple TOML parsing for api_url
90+
for line in content.lines() {
91+
let line = line.trim();
92+
if line.starts_with("api_url") {
93+
if let Some(value) = line.split('=').nth(1) {
94+
let value = value.trim().trim_matches('"').trim_matches('\'');
95+
if !value.is_empty() {
96+
return Ok(Some(value.to_string()));
97+
}
98+
}
99+
}
100+
}
101+
102+
Ok(None)
103+
}
104+
105+
pub fn save_api_url(api_url: &str) -> Result<PathBuf> {
106+
let config_dir = Self::config_dir()
107+
.ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
108+
109+
// Create config directory if it doesn't exist
110+
if !config_dir.exists() {
111+
fs::create_dir_all(&config_dir)
112+
.with_context(|| format!("Failed to create config directory: {}", config_dir.display()))?;
113+
}
20114

21-
Ok(Config { api_url })
115+
let config_path = config_dir.join(CONFIG_FILE_NAME);
116+
let content = format!("api_url = \"{}\"\n", api_url);
117+
118+
fs::write(&config_path, content)
119+
.with_context(|| format!("Failed to write config file: {}", config_path.display()))?;
120+
121+
Ok(config_path)
22122
}
23123

24124
pub fn api_url(&self) -> &str {
25125
&self.api_url
26126
}
127+
128+
pub fn config_file_path_display() -> String {
129+
Self::config_file_path()
130+
.map(|p| p.display().to_string())
131+
.unwrap_or_else(|| "~/.memora/config.toml".to_string())
132+
}
133+
}
134+
135+
/// Prompt user for API URL interactively
136+
pub fn prompt_api_url(current_url: Option<&str>) -> Result<String> {
137+
let default = current_url.unwrap_or(DEFAULT_API_URL);
138+
139+
print!("Enter API URL [{}]: ", default);
140+
io::stdout().flush()?;
141+
142+
let mut input = String::new();
143+
io::stdin().read_line(&mut input)?;
144+
145+
let input = input.trim();
146+
if input.is_empty() {
147+
Ok(default.to_string())
148+
} else {
149+
Ok(input.to_string())
150+
}
27151
}
28152

29153
pub fn generate_doc_id() -> String {

memora-cli/src/errors.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,15 @@ fn format_error_message(err: &anyhow::Error, api_url: &str) -> String {
172172

173173
pub fn print_config_help() {
174174
println!("\n{}", "Configuration:".bright_cyan().bold());
175-
println!(" Set the API URL using an environment variable:");
176-
println!(" {}", "export MEMORA_API_URL=http://localhost:8080".bright_white());
177-
println!("\n Add to your shell profile to make it permanent:");
178-
println!(" {}", "echo 'export MEMORA_API_URL=http://localhost:8080' >> ~/.zshrc".bright_black());
175+
println!(" Run the configure command to set the API URL:");
176+
println!(" {}", "memora configure".bright_white());
177+
println!();
178+
println!(" Or set it directly:");
179+
println!(" {}", "memora configure --api-url http://your-api:8080".bright_white());
180+
println!();
181+
println!(" {}", "Configuration priority:".bright_yellow());
182+
println!(" 1. Environment variable (MEMORA_API_URL) - highest priority");
183+
println!(" 2. Config file (~/.memora/config)");
184+
println!(" 3. Default (http://localhost:8080)");
179185
println!();
180186
}

memora-cli/src/main.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ impl From<Format> for OutputFormat {
3434
#[command(name = "memora")]
3535
#[command(about = "Memora CLI - Semantic memory system", long_about = None)]
3636
#[command(version)]
37+
#[command(after_help = get_after_help())]
3738
struct Cli {
3839
/// Output format (pretty, json, yaml)
3940
#[arg(short = 'o', long, global = true, default_value = "pretty")]
@@ -47,6 +48,18 @@ struct Cli {
4748
command: Commands,
4849
}
4950

51+
fn get_after_help() -> String {
52+
let config = config::Config::load().ok();
53+
let (api_url, source) = match &config {
54+
Some(c) => (c.api_url.as_str(), c.source.to_string()),
55+
None => ("http://localhost:8080", "default".to_string()),
56+
};
57+
format!(
58+
"Current API URL: {} (from {})\n\nRun 'memora configure' to change the API URL.",
59+
api_url, source
60+
)
61+
}
62+
5063
#[derive(Subcommand)]
5164
enum Commands {
5265
/// Manage agents (list, profile, stats)
@@ -64,6 +77,14 @@ enum Commands {
6477
/// Manage async operations (list, cancel)
6578
#[command(subcommand)]
6679
Operation(OperationCommands),
80+
81+
/// Configure the CLI (API URL, etc.)
82+
#[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)")]
83+
Configure {
84+
/// API URL to connect to (interactive prompt if not provided)
85+
#[arg(long)]
86+
api_url: Option<String>,
87+
},
6788
}
6889

6990
#[derive(Subcommand)]
@@ -285,6 +306,11 @@ fn run() -> Result<()> {
285306
let output_format: OutputFormat = cli.output.into();
286307
let verbose = cli.verbose;
287308

309+
// Handle configure command before loading full config (it doesn't need API client)
310+
if let Commands::Configure { api_url } = cli.command {
311+
return handle_configure(api_url, output_format);
312+
}
313+
288314
// Load configuration
289315
let config = Config::from_env().unwrap_or_else(|e| {
290316
ui::print_error(&format!("Configuration error: {}", e));
@@ -301,6 +327,7 @@ fn run() -> Result<()> {
301327

302328
// Execute command and handle errors
303329
let result: Result<()> = match cli.command {
330+
Commands::Configure { .. } => unreachable!(), // Handled above
304331
Commands::Agent(agent_cmd) => match agent_cmd {
305332
AgentCommands::List => {
306333
let spinner = if output_format == OutputFormat::Pretty {
@@ -1075,3 +1102,58 @@ fn run() -> Result<()> {
10751102

10761103
Ok(())
10771104
}
1105+
1106+
fn handle_configure(api_url: Option<String>, output_format: OutputFormat) -> Result<()> {
1107+
// Load current config to show current state
1108+
let current_config = Config::load().ok();
1109+
1110+
if output_format == OutputFormat::Pretty {
1111+
ui::print_info("Memora CLI Configuration");
1112+
println!();
1113+
1114+
// Show current configuration
1115+
if let Some(ref config) = current_config {
1116+
println!(" Current API URL: {}", config.api_url);
1117+
println!(" Source: {}", config.source);
1118+
println!();
1119+
}
1120+
}
1121+
1122+
// Get the new API URL (from argument or prompt)
1123+
let new_api_url = match api_url {
1124+
Some(url) => url,
1125+
None => {
1126+
// Interactive prompt
1127+
let current = current_config.as_ref().map(|c| c.api_url.as_str());
1128+
config::prompt_api_url(current)?
1129+
}
1130+
};
1131+
1132+
// Validate the URL
1133+
if !new_api_url.starts_with("http://") && !new_api_url.starts_with("https://") {
1134+
ui::print_error(&format!(
1135+
"Invalid API URL: {}. Must start with http:// or https://",
1136+
new_api_url
1137+
));
1138+
return Ok(());
1139+
}
1140+
1141+
// Save to config file
1142+
let config_path = Config::save_api_url(&new_api_url)?;
1143+
1144+
if output_format == OutputFormat::Pretty {
1145+
ui::print_success(&format!("Configuration saved to {}", config_path.display()));
1146+
println!();
1147+
println!(" API URL: {}", new_api_url);
1148+
println!();
1149+
println!("Note: Environment variable MEMORA_API_URL will override this setting.");
1150+
} else {
1151+
let result = serde_json::json!({
1152+
"api_url": new_api_url,
1153+
"config_path": config_path.display().to_string(),
1154+
});
1155+
output::print_output(&result, output_format)?;
1156+
}
1157+
1158+
Ok(())
1159+
}

memora/memora/api/mcp.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ async def memora_put(agent_id: str, content: str, context: str, explanation: str
2828
"""
2929
**CRITICAL: Store important user information to long-term memory.**
3030
31+
**⚠️ PER-USER TOOL - REQUIRES USER IDENTIFICATION:**
32+
- This tool is STRICTLY per-user. Each user MUST have a unique `agent_id`.
33+
- ONLY use this tool if you have a valid user identifier (user ID, email, session ID, etc.) to map to `agent_id`.
34+
- DO NOT use this tool if you cannot identify the specific user.
35+
- DO NOT share memories between different users - each user's memories are isolated by their `agent_id`.
36+
- If you don't have a user identifier, DO NOT use this tool at all.
37+
3138
Use this tool PROACTIVELY whenever the user shares:
3239
- Personal facts, preferences, or interests (e.g., "I love hiking", "I'm a vegetarian")
3340
- Important events or milestones (e.g., "I got promoted", "My birthday is June 15")
@@ -44,7 +51,9 @@ async def memora_put(agent_id: str, content: str, context: str, explanation: str
4451
"career_goals", "project_details", etc. This helps organize and retrieve related memories later.
4552
4653
Args:
47-
agent_id: The unique identifier for the agent/user storing the memory
54+
agent_id: **REQUIRED** - The unique, persistent identifier for this specific user (e.g., user_id, email, session_id).
55+
This MUST be consistent across all interactions with the same user.
56+
Example: "user_12345", "alice@example.com", "session_abc123"
4857
content: The fact/memory to store (be specific and include relevant details)
4958
context: Categorize the memory (e.g., 'personal_preferences', 'work_history', 'hobbies', 'family')
5059
explanation: Optional explanation for why this memory is being stored
@@ -69,6 +78,13 @@ async def memora_search(agent_id: str, query: str, max_tokens: int = 4096, expla
6978
"""
7079
**CRITICAL: Search user's memory to provide personalized, context-aware responses.**
7180
81+
**⚠️ PER-USER TOOL - REQUIRES USER IDENTIFICATION:**
82+
- This tool is STRICTLY per-user. Each user MUST have a unique `agent_id`.
83+
- ONLY use this tool if you have a valid user identifier (user ID, email, session ID, etc.) to map to `agent_id`.
84+
- DO NOT use this tool if you cannot identify the specific user.
85+
- DO NOT search across multiple users - each user's memories are isolated by their `agent_id`.
86+
- If you don't have a user identifier, DO NOT use this tool at all.
87+
7288
Use this tool PROACTIVELY at the start of conversations or when making recommendations to:
7389
- Check user's preferences before making suggestions (e.g., "what foods does the user like?")
7490
- Recall user's history to provide continuity (e.g., "what projects has the user worked on?")
@@ -87,7 +103,9 @@ async def memora_search(agent_id: str, query: str, max_tokens: int = 4096, expla
87103
"user's work experience", "user's dietary restrictions", "what does the user know about X?"
88104
89105
Args:
90-
agent_id: The unique identifier for the agent/user whose memories to search
106+
agent_id: **REQUIRED** - The unique, persistent identifier for this specific user (e.g., user_id, email, session_id).
107+
This MUST be consistent across all interactions with the same user.
108+
Example: "user_12345", "alice@example.com", "session_abc123"
91109
query: Natural language search query to find relevant memories
92110
max_tokens: Maximum tokens for search context (default: 4096)
93111
explanation: Optional explanation for why this search is being performed

0 commit comments

Comments
 (0)