cortex_runtime/cli/
repl.rs1use crate::cli::doctor::cortex_home;
10use crate::cli::output::Styled;
11use crate::cli::repl_commands;
12use crate::cli::repl_complete;
13use anyhow::Result;
14use rustyline::config::CompletionType;
15use rustyline::error::ReadlineError;
16use rustyline::{Config, Editor};
17
18fn history_path() -> std::path::PathBuf {
20 cortex_home().join("repl_history")
21}
22
23async fn print_banner() {
25 let s = Styled::new();
26
27 eprintln!();
28 eprintln!(
29 " {} {} {}",
30 s.green("\u{25c9}"),
31 s.bold(&format!("Cortex v{}", env!("CARGO_PKG_VERSION"))),
32 s.dim("— Web Cartographer for AI Agents")
33 );
34
35 let daemon_status = check_daemon_status().await;
37 let cache_count = count_cached_maps();
38
39 eprintln!(" {} | Cached maps: {}", daemon_status, cache_count,);
40
41 eprintln!();
42 eprintln!(
43 " Press {} to browse commands, {} to complete, {} to quit.",
44 s.cyan("/"),
45 s.dim("Tab"),
46 s.dim("/exit")
47 );
48 eprintln!();
49}
50
51async fn check_daemon_status() -> String {
53 let s = Styled::new();
54 let socket_path = "/tmp/cortex.sock";
55
56 if !std::path::Path::new(socket_path).exists() {
57 return format!("Daemon: {}", s.yellow("not running"));
58 }
59
60 match tokio::net::UnixStream::connect(socket_path).await {
62 Ok(_) => {
63 let pid = std::fs::read_to_string(cortex_home().join("cortex.pid"))
65 .ok()
66 .and_then(|p| p.trim().parse::<i32>().ok());
67
68 match pid {
69 Some(p) => format!("Daemon: {} (pid {})", s.green("running"), p),
70 None => format!("Daemon: {}", s.green("running")),
71 }
72 }
73 Err(_) => format!("Daemon: {}", s.yellow("socket exists but unresponsive")),
74 }
75}
76
77fn count_cached_maps() -> usize {
79 let maps_dir = cortex_home().join("maps");
80 std::fs::read_dir(&maps_dir)
81 .map(|d| {
82 d.flatten()
83 .filter(|e| e.path().extension().is_some_and(|x| x == "ctx"))
84 .count()
85 })
86 .unwrap_or(0)
87}
88
89pub async fn run() -> Result<()> {
91 print_banner().await;
93
94 let config = Config::builder()
96 .history_ignore_space(true)
97 .auto_add_history(true)
98 .completion_type(CompletionType::List)
99 .completion_prompt_limit(20)
100 .build();
101
102 let helper = repl_complete::CortexHelper::new();
103 let mut rl: Editor<repl_complete::CortexHelper, rustyline::history::DefaultHistory> =
104 Editor::with_config(config)?;
105 rl.set_helper(Some(helper));
106
107 repl_complete::bind_keys(&mut rl);
109
110 let hist_path = history_path();
112 if hist_path.exists() {
113 let _ = rl.load_history(&hist_path);
114 }
115
116 let mut state = repl_commands::ReplState::new();
118
119 let prompt = format!(
121 " {} ",
122 if Styled::new().ok_sym() == "OK" {
123 "cortex>"
124 } else {
125 "\x1b[36mcortex>\x1b[0m"
126 }
127 );
128
129 loop {
130 match rl.readline(&prompt) {
131 Ok(line) => {
132 let line = line.trim();
133 if line.is_empty() {
134 continue;
135 }
136
137 match repl_commands::execute(line, &mut state).await {
139 Ok(true) => {
140 let s = Styled::new();
142 eprintln!(" {} Goodbye!", s.dim("\u{2728}"));
143 break;
144 }
145 Ok(false) => {
146 }
148 Err(e) => {
149 let s = Styled::new();
150 eprintln!(" {} {e:#}", s.fail_sym());
151 }
152 }
153 }
154 Err(ReadlineError::Interrupted) => {
155 let s = Styled::new();
157 eprintln!(" {} Type {} to quit.", s.dim("(Ctrl+C)"), s.bold("/exit"));
158 }
159 Err(ReadlineError::Eof) => {
160 let s = Styled::new();
162 eprintln!(" {} Goodbye!", s.dim("\u{2728}"));
163 break;
164 }
165 Err(err) => {
166 eprintln!(" Error: {err}");
167 break;
168 }
169 }
170 }
171
172 let _ = std::fs::create_dir_all(hist_path.parent().unwrap_or(std::path::Path::new(".")));
174 let _ = rl.save_history(&hist_path);
175
176 Ok(())
177}