1use rustyline::completion::{Completer, Pair};
4use rustyline::highlight::Highlighter;
5use rustyline::hint::Hinter;
6use rustyline::validate::Validator;
7use rustyline::{
8 Cmd, ConditionalEventHandler, Event, EventContext, EventHandler, Helper, KeyEvent, RepeatCount,
9};
10
11pub const COMMANDS: &[(&str, &str)] = &[
13 ("/create", "Create a new .amem file"),
14 ("/info", "Display info about loaded .amem file"),
15 ("/load", "Load an .amem file for querying"),
16 ("/add", "Add a cognitive event to the graph"),
17 ("/get", "Get a node by ID"),
18 ("/search", "Search nodes by pattern"),
19 ("/text-search", "BM25 text search over contents"),
20 ("/traverse", "Run a traversal query"),
21 ("/impact", "Causal impact analysis"),
22 ("/centrality", "Compute node importance"),
23 ("/path", "Find shortest path between nodes"),
24 ("/gaps", "Reasoning gap detection"),
25 ("/stats", "Graph statistics"),
26 ("/sessions", "List sessions"),
27 ("/clear", "Clear the screen"),
28 ("/help", "Show available commands"),
29 ("/exit", "Quit the REPL"),
30];
31
32pub const EVENT_TYPES: &[&str] = &[
34 "fact",
35 "decision",
36 "inference",
37 "correction",
38 "skill",
39 "episode",
40];
41
42pub struct AmemHelper;
44
45impl Default for AmemHelper {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl AmemHelper {
52 pub fn new() -> Self {
53 Self
54 }
55
56 fn amem_files(&self) -> Vec<String> {
58 let mut files = Vec::new();
59 if let Ok(entries) = std::fs::read_dir(".") {
60 for entry in entries.flatten() {
61 let path = entry.path();
62 if path.extension().is_some_and(|e| e == "amem") {
63 if let Some(name) = path.file_name().and_then(|s| s.to_str()) {
64 files.push(name.to_string());
65 }
66 }
67 }
68 }
69 files.sort();
70 files
71 }
72}
73
74impl Completer for AmemHelper {
75 type Candidate = Pair;
76
77 fn complete(
78 &self,
79 line: &str,
80 pos: usize,
81 _ctx: &rustyline::Context<'_>,
82 ) -> rustyline::Result<(usize, Vec<Pair>)> {
83 let input = &line[..pos];
84
85 if !input.contains(' ') {
87 let matches: Vec<Pair> = COMMANDS
88 .iter()
89 .filter(|(cmd, _)| cmd.starts_with(input))
90 .map(|(cmd, desc)| Pair {
91 display: format!("{cmd:<18} {desc}"),
92 replacement: format!("{cmd} "),
93 })
94 .collect();
95 return Ok((0, matches));
96 }
97
98 let parts: Vec<&str> = input.splitn(2, ' ').collect();
99 let cmd = parts[0];
100 let args = if parts.len() > 1 { parts[1] } else { "" };
101
102 match cmd {
103 "/load" | "/info" | "/create" => {
104 let files = self.amem_files();
105 let prefix_start = input.len() - args.len();
106 let matches: Vec<Pair> = files
107 .iter()
108 .filter(|f| f.starts_with(args.trim()))
109 .map(|f| Pair {
110 display: f.clone(),
111 replacement: format!("{f} "),
112 })
113 .collect();
114 Ok((prefix_start, matches))
115 }
116
117 "/add" => {
118 if !args.contains(' ') {
119 let prefix_start = input.len() - args.len();
120 let matches: Vec<Pair> = EVENT_TYPES
121 .iter()
122 .filter(|t| t.starts_with(args.trim()))
123 .map(|t| Pair {
124 display: t.to_string(),
125 replacement: format!("{t} "),
126 })
127 .collect();
128 return Ok((prefix_start, matches));
129 }
130 Ok((pos, Vec::new()))
131 }
132
133 _ => Ok((pos, Vec::new())),
134 }
135 }
136}
137
138impl Hinter for AmemHelper {
139 type Hint = String;
140
141 fn hint(&self, line: &str, pos: usize, _ctx: &rustyline::Context<'_>) -> Option<String> {
142 if pos < line.len() || line.is_empty() {
143 return None;
144 }
145 if line.starts_with('/') && !line.contains(' ') {
146 for (cmd, _) in COMMANDS {
147 if cmd.starts_with(line) && *cmd != line {
148 return Some(cmd[line.len()..].to_string());
149 }
150 }
151 }
152 None
153 }
154}
155
156impl Highlighter for AmemHelper {}
157impl Validator for AmemHelper {}
158impl Helper for AmemHelper {}
159
160pub struct TabCompleteOrAcceptHint;
162
163impl ConditionalEventHandler for TabCompleteOrAcceptHint {
164 fn handle(
165 &self,
166 _evt: &Event,
167 _n: RepeatCount,
168 _positive: bool,
169 ctx: &EventContext<'_>,
170 ) -> Option<Cmd> {
171 if ctx.has_hint() {
172 Some(Cmd::CompleteHint)
173 } else {
174 Some(Cmd::Complete)
175 }
176 }
177}
178
179pub fn bind_keys(rl: &mut rustyline::Editor<AmemHelper, rustyline::history::DefaultHistory>) {
181 rl.bind_sequence(
182 KeyEvent::from('\t'),
183 EventHandler::Conditional(Box::new(TabCompleteOrAcceptHint)),
184 );
185}
186
187pub fn suggest_command(input: &str) -> Option<&'static str> {
189 let input_lower = input.to_lowercase();
190 let mut best: Option<(&str, usize)> = None;
191
192 for (cmd, _) in COMMANDS {
193 let cmd_name = &cmd[1..];
194 let dist = levenshtein(&input_lower, cmd_name);
195 if dist <= 3 && (best.is_none() || dist < best.unwrap().1) {
196 best = Some((cmd, dist));
197 }
198 }
199
200 best.map(|(cmd, _)| cmd)
201}
202
203fn levenshtein(a: &str, b: &str) -> usize {
204 let a_len = a.len();
205 let b_len = b.len();
206 if a_len == 0 {
207 return b_len;
208 }
209 if b_len == 0 {
210 return a_len;
211 }
212 let mut prev: Vec<usize> = (0..=b_len).collect();
213 let mut curr = vec![0; b_len + 1];
214 for (i, ca) in a.chars().enumerate() {
215 curr[0] = i + 1;
216 for (j, cb) in b.chars().enumerate() {
217 let cost = if ca == cb { 0 } else { 1 };
218 curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(prev[j] + cost);
219 }
220 std::mem::swap(&mut prev, &mut curr);
221 }
222 prev[b_len]
223}