Skip to main content

agentic_vision/cli/
repl_commands.rs

1//! REPL slash-command dispatch and state management.
2
3use super::commands;
4use crate::types::VisionResult;
5use std::path::PathBuf;
6
7#[derive(Default)]
8pub struct ReplState {
9    pub file_path: Option<PathBuf>,
10    pub json: bool,
11}
12
13impl ReplState {
14    pub fn new() -> Self {
15        Self::default()
16    }
17
18    pub fn dispatch(&mut self, line: &str) -> VisionResult<()> {
19        let parts: Vec<&str> = line.split_whitespace().collect();
20        if parts.is_empty() {
21            return Ok(());
22        }
23
24        let cmd = parts[0].trim_start_matches('/');
25        let args = &parts[1..];
26
27        match cmd {
28            "create" => self.cmd_create(args),
29            "load" => self.cmd_load(args),
30            "info" => self.cmd_info(),
31            "capture" => self.cmd_capture(args),
32            "query" => self.cmd_query(args),
33            "similar" => self.cmd_similar(args),
34            "compare" => self.cmd_compare(args),
35            "diff" => self.cmd_diff(args),
36            "health" => self.cmd_health(),
37            "link" => self.cmd_link(args),
38            "stats" => self.cmd_stats(),
39            "export" => self.cmd_export(args),
40            "json" => {
41                self.json = !self.json;
42                println!("JSON output: {}", if self.json { "on" } else { "off" });
43                Ok(())
44            }
45            "clear" => {
46                print!("\x1B[2J\x1B[1;1H");
47                Ok(())
48            }
49            "help" => {
50                self.print_help();
51                Ok(())
52            }
53            "exit" | "quit" => {
54                println!("Goodbye.");
55                std::process::exit(0);
56            }
57            _ => {
58                let suggestion = suggest_command(cmd);
59                if let Some(s) = suggestion {
60                    println!("Unknown command '/{cmd}'. Did you mean '/{s}'?");
61                } else {
62                    println!("Unknown command '/{cmd}'. Type /help for available commands.");
63                }
64                Ok(())
65            }
66        }
67    }
68
69    fn require_file(&self) -> VisionResult<&PathBuf> {
70        self.file_path.as_ref().ok_or_else(|| {
71            crate::types::VisionError::Io(std::io::Error::new(
72                std::io::ErrorKind::NotFound,
73                "No file loaded. Use /create <path> or /load <path> first.",
74            ))
75        })
76    }
77
78    fn cmd_create(&mut self, args: &[&str]) -> VisionResult<()> {
79        let path = args.first().ok_or_else(|| {
80            crate::types::VisionError::Io(std::io::Error::new(
81                std::io::ErrorKind::InvalidInput,
82                "Usage: /create <path> [dimension]",
83            ))
84        })?;
85        let dim: u32 = args.get(1).and_then(|s| s.parse().ok()).unwrap_or(512);
86        let p = PathBuf::from(path);
87        commands::cmd_create(&p, dim)?;
88        self.file_path = Some(p);
89        Ok(())
90    }
91
92    fn cmd_load(&mut self, args: &[&str]) -> VisionResult<()> {
93        let path = args.first().ok_or_else(|| {
94            crate::types::VisionError::Io(std::io::Error::new(
95                std::io::ErrorKind::InvalidInput,
96                "Usage: /load <path>",
97            ))
98        })?;
99        let p = PathBuf::from(path);
100        let store = crate::storage::AvisReader::read_from_file(&p)?;
101        println!("Loaded {} ({} observations)", p.display(), store.count());
102        self.file_path = Some(p);
103        Ok(())
104    }
105
106    fn cmd_info(&self) -> VisionResult<()> {
107        commands::cmd_info(self.require_file()?, self.json)
108    }
109
110    fn cmd_capture(&self, args: &[&str]) -> VisionResult<()> {
111        let p = self.require_file()?;
112        let source = args.first().ok_or_else(|| {
113            crate::types::VisionError::Io(std::io::Error::new(
114                std::io::ErrorKind::InvalidInput,
115                "Usage: /capture <image_path>",
116            ))
117        })?;
118        commands::cmd_capture(p, source, Vec::new(), None, None, self.json)
119    }
120
121    fn cmd_query(&self, args: &[&str]) -> VisionResult<()> {
122        let limit: usize = args.first().and_then(|s| s.parse().ok()).unwrap_or(20);
123        commands::cmd_query(self.require_file()?, None, None, limit, self.json)
124    }
125
126    fn cmd_similar(&self, args: &[&str]) -> VisionResult<()> {
127        let id: u64 = args.first().and_then(|s| s.parse().ok()).ok_or_else(|| {
128            crate::types::VisionError::Io(std::io::Error::new(
129                std::io::ErrorKind::InvalidInput,
130                "Usage: /similar <capture_id> [top_k]",
131            ))
132        })?;
133        let top_k: usize = args.get(1).and_then(|s| s.parse().ok()).unwrap_or(10);
134        commands::cmd_similar(self.require_file()?, id, top_k, 0.5, self.json)
135    }
136
137    fn cmd_compare(&self, args: &[&str]) -> VisionResult<()> {
138        if args.len() < 2 {
139            return Err(crate::types::VisionError::Io(std::io::Error::new(
140                std::io::ErrorKind::InvalidInput,
141                "Usage: /compare <id_a> <id_b>",
142            )));
143        }
144        let id_a: u64 = args[0].parse().map_err(|_| {
145            crate::types::VisionError::Io(std::io::Error::new(
146                std::io::ErrorKind::InvalidInput,
147                "Invalid capture ID",
148            ))
149        })?;
150        let id_b: u64 = args[1].parse().map_err(|_| {
151            crate::types::VisionError::Io(std::io::Error::new(
152                std::io::ErrorKind::InvalidInput,
153                "Invalid capture ID",
154            ))
155        })?;
156        commands::cmd_compare(self.require_file()?, id_a, id_b, self.json)
157    }
158
159    fn cmd_diff(&self, args: &[&str]) -> VisionResult<()> {
160        if args.len() < 2 {
161            return Err(crate::types::VisionError::Io(std::io::Error::new(
162                std::io::ErrorKind::InvalidInput,
163                "Usage: /diff <id_a> <id_b>",
164            )));
165        }
166        let id_a: u64 = args[0].parse().map_err(|_| {
167            crate::types::VisionError::Io(std::io::Error::new(
168                std::io::ErrorKind::InvalidInput,
169                "Invalid capture ID",
170            ))
171        })?;
172        let id_b: u64 = args[1].parse().map_err(|_| {
173            crate::types::VisionError::Io(std::io::Error::new(
174                std::io::ErrorKind::InvalidInput,
175                "Invalid capture ID",
176            ))
177        })?;
178        commands::cmd_diff(self.require_file()?, id_a, id_b, self.json)
179    }
180
181    fn cmd_health(&self) -> VisionResult<()> {
182        commands::cmd_health(self.require_file()?, 168, 0.45, 20, self.json)
183    }
184
185    fn cmd_link(&self, args: &[&str]) -> VisionResult<()> {
186        if args.len() < 2 {
187            return Err(crate::types::VisionError::Io(std::io::Error::new(
188                std::io::ErrorKind::InvalidInput,
189                "Usage: /link <capture_id> <memory_node_id>",
190            )));
191        }
192        let cid: u64 = args[0].parse().map_err(|_| {
193            crate::types::VisionError::Io(std::io::Error::new(
194                std::io::ErrorKind::InvalidInput,
195                "Invalid capture ID",
196            ))
197        })?;
198        let mid: u64 = args[1].parse().map_err(|_| {
199            crate::types::VisionError::Io(std::io::Error::new(
200                std::io::ErrorKind::InvalidInput,
201                "Invalid memory node ID",
202            ))
203        })?;
204        commands::cmd_link(self.require_file()?, cid, mid, self.json)
205    }
206
207    fn cmd_stats(&self) -> VisionResult<()> {
208        commands::cmd_stats(self.require_file()?, self.json)
209    }
210
211    fn cmd_export(&self, args: &[&str]) -> VisionResult<()> {
212        let pretty = args.iter().any(|a| *a == "--pretty" || *a == "-p");
213        commands::cmd_export(self.require_file()?, pretty)
214    }
215
216    fn print_help(&self) {
217        println!("Available commands:");
218        println!("  /create <path> [dim]        Create a new .avis file");
219        println!("  /load <path>                Load an existing .avis file");
220        println!("  /info                       Display file info");
221        println!("  /capture <image>            Capture an image");
222        println!("  /query [limit]              Search observations");
223        println!("  /similar <id> [top_k]       Find similar captures");
224        println!("  /compare <id_a> <id_b>      Compare two captures");
225        println!("  /diff <id_a> <id_b>         Pixel-level diff");
226        println!("  /health                     Quality report");
227        println!("  /link <cap_id> <mem_id>     Link to memory node");
228        println!("  /stats                      Aggregate statistics");
229        println!("  /export [--pretty]          Export as JSON");
230        println!("  /json                       Toggle JSON output");
231        println!("  /clear                      Clear screen");
232        println!("  /help                       Show this message");
233        println!("  /exit                       Quit");
234    }
235}
236
237fn suggest_command(input: &str) -> Option<&'static str> {
238    const CMDS: &[&str] = &[
239        "create", "load", "info", "capture", "query", "similar", "compare", "diff", "health",
240        "link", "stats", "export", "json", "clear", "help", "exit",
241    ];
242    let mut best: Option<(&str, usize)> = None;
243    for &cmd in CMDS {
244        let dist = levenshtein(input, cmd);
245        if dist <= 2 && (best.is_none() || dist < best.unwrap().1) {
246            best = Some((cmd, dist));
247        }
248    }
249    best.map(|(s, _)| s)
250}
251
252fn levenshtein(a: &str, b: &str) -> usize {
253    let a: Vec<char> = a.chars().collect();
254    let b: Vec<char> = b.chars().collect();
255    let (m, n) = (a.len(), b.len());
256    let mut dp = vec![vec![0usize; n + 1]; m + 1];
257    for (i, row) in dp.iter_mut().enumerate().take(m + 1) {
258        row[0] = i;
259    }
260    for (j, val) in dp[0].iter_mut().enumerate().take(n + 1) {
261        *val = j;
262    }
263    for i in 1..=m {
264        for j in 1..=n {
265            let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
266            dp[i][j] = (dp[i - 1][j] + 1)
267                .min(dp[i][j - 1] + 1)
268                .min(dp[i - 1][j - 1] + cost);
269        }
270    }
271    dp[m][n]
272}