1use 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
11pub struct ReplState {
13 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
38pub 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 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 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}