Skip to main content

agentic_memory/cli/
repl_commands.rs

1//! Slash command dispatch for the amem REPL.
2
3use std::path::PathBuf;
4
5use crate::cli::commands;
6use crate::cli::repl_complete::COMMANDS;
7use crate::engine::PatternSort;
8use crate::graph::TraversalDirection;
9use crate::types::EventType;
10
11/// Session state.
12pub struct ReplState {
13    /// Path to the currently loaded .amem file.
14    pub file_path: Option<PathBuf>,
15}
16
17impl Default for ReplState {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23impl ReplState {
24    pub fn new() -> Self {
25        Self { file_path: None }
26    }
27
28    fn require_file(&self) -> Option<&PathBuf> {
29        if let Some(ref p) = self.file_path {
30            Some(p)
31        } else {
32            eprintln!("  No .amem file loaded. Use /load <file.amem> or /create <file.amem>");
33            None
34        }
35    }
36}
37
38/// Execute a slash command. Returns `true` if REPL should exit.
39pub fn execute(input: &str, state: &mut ReplState) -> Result<bool, Box<dyn std::error::Error>> {
40    let input = input.trim();
41    if input.is_empty() {
42        return Ok(false);
43    }
44
45    let input = input.strip_prefix('/').unwrap_or(input);
46    if input.is_empty() {
47        cmd_help();
48        return Ok(false);
49    }
50
51    let mut parts = input.splitn(2, ' ');
52    let cmd = parts.next().unwrap_or("");
53    let args = parts.next().unwrap_or("").trim();
54
55    match cmd {
56        "exit" | "quit" => return Ok(true),
57        "help" | "h" | "?" => cmd_help(),
58        "clear" | "cls" => eprint!("\x1b[2J\x1b[H"),
59        "create" => cmd_create(args, state)?,
60        "load" => cmd_load(args, state)?,
61        "info" => cmd_info(state)?,
62        "add" => cmd_add(args, state)?,
63        "get" => cmd_get(args, state)?,
64        "search" => cmd_search(args, state)?,
65        "text-search" | "ts" => cmd_text_search(args, state)?,
66        "traverse" => cmd_traverse(args, state)?,
67        "impact" => cmd_impact(args, state)?,
68        "centrality" => cmd_centrality(args, state)?,
69        "path" => cmd_path(args, state)?,
70        "gaps" => cmd_gaps(args, state)?,
71        "stats" => cmd_stats(state)?,
72        "sessions" => cmd_sessions(state)?,
73        _ => {
74            if let Some(suggestion) = crate::cli::repl_complete::suggest_command(cmd) {
75                eprintln!("  Unknown command '/{cmd}'. Did you mean {suggestion}?");
76            } else {
77                eprintln!("  Unknown command '/{cmd}'. Type /help for commands.");
78            }
79        }
80    }
81
82    Ok(false)
83}
84
85fn cmd_help() {
86    eprintln!();
87    eprintln!("  Commands:");
88    eprintln!();
89    for (cmd, desc) in COMMANDS {
90        eprintln!("    {cmd:<20} {desc}");
91    }
92    eprintln!();
93    eprintln!("  Tip: Tab completion works for commands, event types, and .amem files.");
94    eprintln!();
95}
96
97fn cmd_create(args: &str, state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
98    if args.is_empty() {
99        eprintln!("  Usage: /create <file.amem> [--dimension N]");
100        return Ok(());
101    }
102    let tokens: Vec<&str> = args.split_whitespace().collect();
103    let file = PathBuf::from(tokens[0]);
104    let dim: usize = tokens
105        .iter()
106        .position(|&t| t == "--dimension")
107        .and_then(|i| tokens.get(i + 1))
108        .and_then(|s| s.parse().ok())
109        .unwrap_or(128);
110
111    commands::cmd_create(&file, dim)?;
112    state.file_path = Some(file.clone());
113    eprintln!("  Created and loaded: {}", file.display());
114    Ok(())
115}
116
117fn cmd_load(args: &str, state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
118    if args.is_empty() {
119        eprintln!("  Usage: /load <file.amem>");
120        return Ok(());
121    }
122    let file = PathBuf::from(args.split_whitespace().next().unwrap_or(args));
123    if !file.exists() {
124        eprintln!("  File not found: {}", file.display());
125        return Ok(());
126    }
127    // Verify it's readable
128    commands::cmd_info(&file, false)?;
129    state.file_path = Some(file);
130    Ok(())
131}
132
133fn cmd_info(state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
134    let file = match state.require_file() {
135        Some(f) => f.clone(),
136        None => return Ok(()),
137    };
138    commands::cmd_info(&file, false)?;
139    Ok(())
140}
141
142fn cmd_add(args: &str, state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
143    let file = match state.require_file() {
144        Some(f) => f.clone(),
145        None => return Ok(()),
146    };
147    let tokens: Vec<&str> = args.splitn(2, ' ').collect();
148    if tokens.len() < 2 {
149        eprintln!("  Usage: /add <type> <content>");
150        eprintln!("  Types: fact, decision, inference, correction, skill, episode");
151        return Ok(());
152    }
153    let et = match EventType::from_name(tokens[0]) {
154        Some(et) => et,
155        None => {
156            eprintln!("  Invalid event type: {}", tokens[0]);
157            return Ok(());
158        }
159    };
160    commands::cmd_add(&file, et, tokens[1], 0, 1.0, None, false)?;
161    Ok(())
162}
163
164fn cmd_get(args: &str, state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
165    let file = match state.require_file() {
166        Some(f) => f.clone(),
167        None => return Ok(()),
168    };
169    let node_id: u64 = match args.split_whitespace().next().and_then(|s| s.parse().ok()) {
170        Some(id) => id,
171        None => {
172            eprintln!("  Usage: /get <node-id>");
173            return Ok(());
174        }
175    };
176    commands::cmd_get(&file, node_id, false)?;
177    Ok(())
178}
179
180fn cmd_search(args: &str, state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
181    let file = match state.require_file() {
182        Some(f) => f.clone(),
183        None => return Ok(()),
184    };
185    // Parse simple flags
186    let tokens: Vec<&str> = args.split_whitespace().collect();
187    let mut event_types = Vec::new();
188    let mut limit: usize = 20;
189    let mut sort = PatternSort::MostRecent;
190    let mut i = 0;
191    while i < tokens.len() {
192        match tokens[i] {
193            "--type" if i + 1 < tokens.len() => {
194                for t in tokens[i + 1].split(',') {
195                    if let Some(et) = EventType::from_name(t.trim()) {
196                        event_types.push(et);
197                    }
198                }
199                i += 2;
200            }
201            "--limit" if i + 1 < tokens.len() => {
202                limit = tokens[i + 1].parse().unwrap_or(20);
203                i += 2;
204            }
205            "--sort" if i + 1 < tokens.len() => {
206                sort = match tokens[i + 1] {
207                    "confidence" => PatternSort::HighestConfidence,
208                    "accessed" => PatternSort::MostAccessed,
209                    "importance" => PatternSort::MostImportant,
210                    _ => PatternSort::MostRecent,
211                };
212                i += 2;
213            }
214            _ => {
215                i += 1;
216            }
217        }
218    }
219    commands::cmd_search(
220        &file,
221        event_types,
222        vec![],
223        None,
224        None,
225        None,
226        None,
227        sort,
228        limit,
229        false,
230    )?;
231    Ok(())
232}
233
234fn cmd_text_search(args: &str, state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
235    let file = match state.require_file() {
236        Some(f) => f.clone(),
237        None => return Ok(()),
238    };
239    if args.is_empty() {
240        eprintln!("  Usage: /text-search <query>");
241        return Ok(());
242    }
243    let query = args.to_string();
244    commands::cmd_text_search(&file, &query, vec![], vec![], 20, 0.0, false)?;
245    Ok(())
246}
247
248fn cmd_traverse(args: &str, state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
249    let file = match state.require_file() {
250        Some(f) => f.clone(),
251        None => return Ok(()),
252    };
253    let start_id: u64 = match args.split_whitespace().next().and_then(|s| s.parse().ok()) {
254        Some(id) => id,
255        None => {
256            eprintln!("  Usage: /traverse <start-node-id> [--depth N]");
257            return Ok(());
258        }
259    };
260    let tokens: Vec<&str> = args.split_whitespace().collect();
261    let depth: u32 = tokens
262        .iter()
263        .position(|&t| t == "--depth")
264        .and_then(|i| tokens.get(i + 1))
265        .and_then(|s| s.parse().ok())
266        .unwrap_or(5);
267
268    commands::cmd_traverse(
269        &file,
270        start_id,
271        vec![],
272        TraversalDirection::Both,
273        depth,
274        50,
275        0.0,
276        false,
277    )?;
278    Ok(())
279}
280
281fn cmd_impact(args: &str, state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
282    let file = match state.require_file() {
283        Some(f) => f.clone(),
284        None => return Ok(()),
285    };
286    let node_id: u64 = match args.split_whitespace().next().and_then(|s| s.parse().ok()) {
287        Some(id) => id,
288        None => {
289            eprintln!("  Usage: /impact <node-id>");
290            return Ok(());
291        }
292    };
293    commands::cmd_impact(&file, node_id, 10, false)?;
294    Ok(())
295}
296
297fn cmd_centrality(args: &str, state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
298    let file = match state.require_file() {
299        Some(f) => f.clone(),
300        None => return Ok(()),
301    };
302    let algo = args.split_whitespace().next().unwrap_or("pagerank");
303    commands::cmd_centrality(&file, algo, 0.85, vec![], vec![], 20, 100, false)?;
304    Ok(())
305}
306
307fn cmd_path(args: &str, state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
308    let file = match state.require_file() {
309        Some(f) => f.clone(),
310        None => return Ok(()),
311    };
312    let tokens: Vec<&str> = args.split_whitespace().collect();
313    if tokens.len() < 2 {
314        eprintln!("  Usage: /path <source-id> <target-id>");
315        return Ok(());
316    }
317    let source: u64 = match tokens[0].parse() {
318        Ok(v) => v,
319        Err(_) => {
320            eprintln!("  Invalid source ID");
321            return Ok(());
322        }
323    };
324    let target: u64 = match tokens[1].parse() {
325        Ok(v) => v,
326        Err(_) => {
327            eprintln!("  Invalid target ID");
328            return Ok(());
329        }
330    };
331    commands::cmd_path(
332        &file,
333        source,
334        target,
335        vec![],
336        TraversalDirection::Both,
337        20,
338        false,
339        false,
340    )?;
341    Ok(())
342}
343
344fn cmd_gaps(args: &str, state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
345    let file = match state.require_file() {
346        Some(f) => f.clone(),
347        None => return Ok(()),
348    };
349    let limit: usize = args
350        .split_whitespace()
351        .next()
352        .and_then(|s| s.parse().ok())
353        .unwrap_or(20);
354    commands::cmd_gaps(&file, 0.5, 1, limit, "dangerous", None, false)?;
355    Ok(())
356}
357
358fn cmd_stats(state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
359    let file = match state.require_file() {
360        Some(f) => f.clone(),
361        None => return Ok(()),
362    };
363    commands::cmd_stats(&file, false)?;
364    Ok(())
365}
366
367fn cmd_sessions(state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
368    let file = match state.require_file() {
369        Some(f) => f.clone(),
370        None => return Ok(()),
371    };
372    commands::cmd_sessions(&file, 20, false)?;
373    Ok(())
374}