1use crate::cli::doctor::cortex_home;
10use crate::cli::output::{self, format_duration, format_size, Styled};
11use crate::cli::repl_complete::COMMANDS;
12use crate::cli::repl_progress;
13use crate::intelligence::cache::MapCache;
14use crate::map::types::{
15 FeatureRange, NodeQuery, PageType, PathConstraints, FEAT_PRICE, FEAT_RATING,
16};
17use anyhow::Result;
18use std::time::Instant;
19
20pub struct ReplState {
22 pub active_domain: Option<String>,
24}
25
26impl ReplState {
27 pub fn new() -> Self {
28 Self {
29 active_domain: None,
30 }
31 }
32}
33
34pub async fn execute(input: &str, state: &mut ReplState) -> Result<bool> {
36 let input = input.trim();
37 if input.is_empty() {
38 return Ok(false);
39 }
40
41 let input = input.strip_prefix('/').unwrap_or(input);
43
44 if input.is_empty() {
46 cmd_help();
47 return Ok(false);
48 }
49
50 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" | "q" => return Ok(true),
57 "help" | "h" | "?" => cmd_help(),
58 "clear" | "cls" => cmd_clear(),
59 "status" => cmd_status().await?,
60 "doctor" => cmd_doctor().await?,
61 "maps" | "ls" => cmd_maps()?,
62 "use" => cmd_use(args, state)?,
63 "map" => cmd_map(args, state).await?,
64 "query" => cmd_query(args, state)?,
65 "pathfind" | "path" => cmd_pathfind(args, state)?,
66 "perceive" => cmd_perceive(args).await?,
67 "settings" | "config" => cmd_settings()?,
68 "cache" => cmd_cache(args)?,
69 "plug" => cmd_plug().await?,
70 _ => {
71 let s = Styled::new();
72 if let Some(suggestion) = crate::cli::repl_complete::suggest_command(cmd) {
73 eprintln!(
74 " {} Unknown command '/{cmd}'. Did you mean {}?",
75 s.warn_sym(),
76 s.bold(suggestion)
77 );
78 } else {
79 eprintln!(
80 " {} Unknown command '/{cmd}'. Type {} or press {} for commands.",
81 s.warn_sym(),
82 s.bold("/help"),
83 s.bold("/")
84 );
85 }
86 }
87 }
88
89 Ok(false)
90}
91
92fn cmd_help() {
94 let s = Styled::new();
95 eprintln!();
96 eprintln!(" {}", s.bold("Commands:"));
97 eprintln!();
98 for (cmd, desc) in COMMANDS {
99 eprintln!(" {:<22} {}", s.cyan(cmd), s.dim(desc));
100 }
101 eprintln!();
102 eprintln!(
103 " {}",
104 s.dim("Tip: Tab completion works for commands and domain names.")
105 );
106 eprintln!();
107}
108
109fn cmd_clear() {
111 eprint!("\x1b[2J\x1b[H");
113}
114
115async fn cmd_status() -> Result<()> {
117 crate::cli::status::run().await
118}
119
120async fn cmd_doctor() -> Result<()> {
122 crate::cli::doctor::run().await
123}
124
125fn cmd_maps() -> Result<()> {
127 let s = Styled::new();
128 let maps_dir = cortex_home().join("maps");
129
130 let mut entries: Vec<(String, u64, std::time::SystemTime)> = Vec::new();
131 if let Ok(dir) = std::fs::read_dir(&maps_dir) {
132 for entry in dir.flatten() {
133 let path = entry.path();
134 if path.extension().is_some_and(|e| e == "ctx") {
135 if let (Some(stem), Ok(meta)) =
136 (path.file_stem().and_then(|s| s.to_str()), path.metadata())
137 {
138 let modified = meta.modified().unwrap_or(std::time::SystemTime::UNIX_EPOCH);
139 entries.push((stem.to_string(), meta.len(), modified));
140 }
141 }
142 }
143 }
144
145 entries.sort_by(|a, b| b.2.cmp(&a.2));
146
147 if entries.is_empty() {
148 eprintln!();
149 eprintln!(
150 " {} No cached maps. Map a site with: {}",
151 s.info_sym(),
152 s.bold("/map example.com")
153 );
154 eprintln!();
155 return Ok(());
156 }
157
158 eprintln!();
159 eprintln!(" {} cached map(s):", entries.len());
160 eprintln!();
161
162 let mut total_size = 0u64;
163 for (name, size, modified) in &entries {
164 total_size += size;
165 let ago = modified
166 .elapsed()
167 .map(|d| format_duration(d.as_secs()) + " ago")
168 .unwrap_or_else(|_| "unknown".to_string());
169 eprintln!(
170 " {:<25} {:>10} {}",
171 s.bold(name),
172 format_size(*size),
173 s.dim(&ago)
174 );
175 }
176 eprintln!();
177 eprintln!(" {:<25} {}", "Total:", format_size(total_size));
178 eprintln!();
179
180 Ok(())
181}
182
183fn cmd_use(args: &str, state: &mut ReplState) -> Result<()> {
185 let s = Styled::new();
186
187 if args.is_empty() {
188 if let Some(ref domain) = state.active_domain {
189 eprintln!(" Active domain: {}", s.bold(domain));
190 } else {
191 eprintln!(
192 " {} No active domain. Usage: {}",
193 s.info_sym(),
194 s.bold("/use example.com")
195 );
196 }
197 return Ok(());
198 }
199
200 let domain = args.split_whitespace().next().unwrap_or(args);
201
202 let mut cache = MapCache::default_cache()?;
204 if cache.load_map(domain)?.is_some() {
205 state.active_domain = Some(domain.to_string());
206 eprintln!(" {} Active domain set to: {}", s.ok_sym(), s.bold(domain));
207 } else {
208 eprintln!(
209 " {} No cached map for '{}'. Map it first with: /map {domain}",
210 s.warn_sym(),
211 domain
212 );
213 state.active_domain = Some(domain.to_string());
215 }
216
217 Ok(())
218}
219
220async fn cmd_map(args: &str, state: &mut ReplState) -> Result<()> {
222 let s = Styled::new();
223
224 if args.is_empty() {
225 eprintln!(" {} Usage: {}", s.info_sym(), s.bold("/map <domain>"));
226 return Ok(());
227 }
228
229 let domain = args.split_whitespace().next().unwrap_or(args);
230 let start = Instant::now();
231
232 eprintln!();
233 eprintln!(" Mapping {}...", s.bold(domain));
234 eprintln!();
235
236 let (mp, bars) = repl_progress::create_mapping_progress();
238
239 let socket_path = "/tmp/cortex.sock";
241 if !std::path::Path::new(socket_path).exists() {
242 repl_progress::set_layer_active(&bars[0], "Starting daemon", "auto-start...");
243 let _ = crate::cli::start::run().await;
244 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
245 }
246
247 repl_progress::set_layer_active(&bars[0], "Sitemap discovery", "scanning...");
249
250 use tokio::io::{AsyncReadExt, AsyncWriteExt};
252 use tokio::net::UnixStream;
253
254 let mut stream = match UnixStream::connect(socket_path).await {
255 Ok(s) => s,
256 Err(_) => {
257 for bar in &bars {
258 repl_progress::set_layer_skipped(bar, "", "failed");
259 }
260 mp.clear()?;
261 eprintln!(
262 " {} Cannot connect to daemon. Run /status to check.",
263 s.fail_sym()
264 );
265 return Ok(());
266 }
267 };
268
269 let req = serde_json::json!({
270 "id": format!("repl-map-{}", std::process::id()),
271 "method": "map",
272 "params": {
273 "domain": domain,
274 "max_nodes": 50000_u32,
275 "max_render": 200_u32,
276 "max_time_ms": 10000_u64,
277 "respect_robots": true,
278 }
279 });
280 let req_str = format!("{}\n", req);
281 stream.write_all(req_str.as_bytes()).await?;
282
283 let read_handle = tokio::spawn(async move {
286 let mut buf = vec![0u8; 1024 * 1024];
287 let timeout = std::time::Duration::from_millis(40000);
288 match tokio::time::timeout(timeout, stream.read(&mut buf)).await {
289 Ok(Ok(n)) if n > 0 => {
290 let response: serde_json::Value =
291 serde_json::from_slice(&buf[..n]).unwrap_or_default();
292 Some(response)
293 }
294 _ => None,
295 }
296 });
297
298 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
300 repl_progress::set_layer_done(&bars[0], "Sitemap discovery", "URLs discovered");
301
302 repl_progress::set_layer_active(&bars[1], "HTTP extraction", "fetching pages...");
303 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
304 repl_progress::set_layer_done(&bars[1], "HTTP extraction", "pages fetched");
305
306 repl_progress::set_layer_active(&bars[2], "Pattern engine", "extracting data...");
307 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
308 repl_progress::set_layer_done(&bars[2], "Pattern engine", "data extracted");
309
310 repl_progress::set_layer_active(&bars[3], "API discovery", "scanning endpoints...");
311 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
312 repl_progress::set_layer_done(&bars[3], "API discovery", "endpoints found");
313
314 let response = read_handle.await.ok().flatten();
316
317 repl_progress::set_layer_skipped(&bars[4], "Browser fallback", "skipped (HTTP sufficient)");
318
319 let _ = mp.clear();
321
322 eprintln!();
323
324 match response {
325 Some(resp) => {
326 if let Some(error) = resp.get("error") {
327 let msg = error
328 .get("message")
329 .and_then(|v| v.as_str())
330 .unwrap_or("unknown error");
331 eprintln!(" {} Mapping failed: {msg}", s.fail_sym());
332 } else {
333 let result = resp.get("result").cloned().unwrap_or_default();
334 let node_count = result
335 .get("node_count")
336 .and_then(|v| v.as_u64())
337 .unwrap_or(0);
338 let edge_count = result
339 .get("edge_count")
340 .and_then(|v| v.as_u64())
341 .unwrap_or(0);
342 let elapsed = start.elapsed();
343
344 eprintln!(
345 " {} Map complete: {} nodes, {} edges ({:.1}s)",
346 s.ok_sym(),
347 s.bold(&node_count.to_string()),
348 edge_count,
349 elapsed.as_secs_f64()
350 );
351
352 state.active_domain = Some(domain.to_string());
354 eprintln!();
355 eprintln!(
356 " Active domain set to {}. Try: {}",
357 s.bold(domain),
358 s.cyan("/query --type product_detail")
359 );
360 }
361 }
362 None => {
363 eprintln!(" {} Mapping timed out or connection lost.", s.fail_sym());
364 }
365 }
366
367 eprintln!();
368 Ok(())
369}
370
371fn cmd_query(args: &str, state: &mut ReplState) -> Result<()> {
373 let s = Styled::new();
374
375 let mut domain = state.active_domain.clone();
377 let mut page_type_str: Option<String> = None;
378 let mut price_lt: Option<f32> = None;
379 let mut rating_gt: Option<f32> = None;
380 let mut limit: u32 = 20;
381 let mut feature_filters: Vec<String> = Vec::new();
382
383 let tokens: Vec<&str> = args.split_whitespace().collect();
385 let mut i = 0;
386 while i < tokens.len() {
387 match tokens[i] {
388 "--type" if i + 1 < tokens.len() => {
389 page_type_str = Some(tokens[i + 1].to_string());
390 i += 2;
391 }
392 "--price-lt" if i + 1 < tokens.len() => {
393 price_lt = tokens[i + 1].parse().ok();
394 i += 2;
395 }
396 "--rating-gt" if i + 1 < tokens.len() => {
397 rating_gt = tokens[i + 1].parse().ok();
398 i += 2;
399 }
400 "--limit" if i + 1 < tokens.len() => {
401 limit = tokens[i + 1].parse().unwrap_or(20);
402 i += 2;
403 }
404 "--feature" if i + 1 < tokens.len() => {
405 feature_filters.push(tokens[i + 1].to_string());
406 i += 2;
407 }
408 s if !s.starts_with('-') && domain.is_none() => {
409 domain = Some(s.to_string());
410 i += 1;
411 }
412 _ => {
413 i += 1;
414 }
415 }
416 }
417
418 let domain = match domain {
419 Some(d) => d,
420 None => {
421 eprintln!(
422 " {} No active domain. Use: {} or {}",
423 s.info_sym(),
424 s.bold("/use example.com"),
425 s.bold("/query example.com --type article")
426 );
427 return Ok(());
428 }
429 };
430
431 let rt = tokio::runtime::Handle::current();
433 rt.block_on(async {
434 crate::cli::query_cmd::run(
435 &domain,
436 page_type_str.as_deref(),
437 price_lt,
438 rating_gt,
439 limit,
440 &feature_filters,
441 )
442 .await
443 })
444}
445
446fn cmd_pathfind(args: &str, state: &mut ReplState) -> Result<()> {
448 let s = Styled::new();
449
450 let tokens: Vec<&str> = args.split_whitespace().collect();
451 let mut domain = state.active_domain.clone();
452 let mut from: Option<u32> = None;
453 let mut to: Option<u32> = None;
454
455 let mut i = 0;
456 while i < tokens.len() {
457 match tokens[i] {
458 "--from" if i + 1 < tokens.len() => {
459 from = tokens[i + 1].parse().ok();
460 i += 2;
461 }
462 "--to" if i + 1 < tokens.len() => {
463 to = tokens[i + 1].parse().ok();
464 i += 2;
465 }
466 s if !s.starts_with('-') && domain.is_none() => {
467 domain = Some(s.to_string());
468 i += 1;
469 }
470 s if !s.starts_with('-') => {
471 if from.is_none() {
473 from = s.parse().ok();
474 } else if to.is_none() {
475 to = s.parse().ok();
476 }
477 i += 1;
478 }
479 _ => {
480 i += 1;
481 }
482 }
483 }
484
485 let domain = match domain {
486 Some(d) => d,
487 None => {
488 eprintln!(
489 " {} No active domain. Use: {}",
490 s.info_sym(),
491 s.bold("/pathfind --from 0 --to 10")
492 );
493 return Ok(());
494 }
495 };
496
497 let from = match from {
498 Some(f) => f,
499 None => {
500 eprintln!(
501 " {} Usage: {}",
502 s.info_sym(),
503 s.bold("/pathfind --from 0 --to 10")
504 );
505 return Ok(());
506 }
507 };
508
509 let to = match to {
510 Some(t) => t,
511 None => {
512 eprintln!(
513 " {} Usage: {}",
514 s.info_sym(),
515 s.bold("/pathfind --from 0 --to 10")
516 );
517 return Ok(());
518 }
519 };
520
521 let rt = tokio::runtime::Handle::current();
522 rt.block_on(async { crate::cli::pathfind_cmd::run(&domain, from, to).await })
523}
524
525async fn cmd_perceive(args: &str) -> Result<()> {
527 let s = Styled::new();
528
529 if args.is_empty() {
530 eprintln!(
531 " {} Usage: {}",
532 s.info_sym(),
533 s.bold("/perceive https://example.com/page")
534 );
535 return Ok(());
536 }
537
538 let url = args.split_whitespace().next().unwrap_or(args);
539 crate::cli::perceive_cmd::run(url, "pretty").await
540}
541
542fn cmd_settings() -> Result<()> {
544 let s = Styled::new();
545
546 eprintln!();
547 eprintln!(" {}", s.bold("Configuration"));
548 eprintln!();
549 eprintln!(" {:<22} {}", "CORTEX_HOME", cortex_home().display());
550 let socket = "/tmp/cortex.sock";
551 eprintln!(" {:<22} {}", "Socket", socket);
552 eprintln!(
553 " {:<22} {}",
554 "Chromium",
555 crate::cli::doctor::find_chromium()
556 .map(|p| p.display().to_string())
557 .unwrap_or_else(|| s.dim("not found").to_string())
558 );
559
560 eprintln!();
562 eprintln!(" {}", s.bold("Environment Variables"));
563 eprintln!();
564 let env_vars = [
565 "CORTEX_HOME",
566 "CORTEX_CHROMIUM_PATH",
567 "CORTEX_CHROMIUM_NO_SANDBOX",
568 "CORTEX_JSON",
569 "CORTEX_QUIET",
570 "CORTEX_VERBOSE",
571 "CORTEX_NO_COLOR",
572 ];
573 for var in &env_vars {
574 let val = std::env::var(var).unwrap_or_else(|_| s.dim("(not set)").to_string());
575 eprintln!(" {var:<32} {val}");
576 }
577
578 let maps_dir = cortex_home().join("maps");
580 let cache_count = std::fs::read_dir(&maps_dir)
581 .map(|d| {
582 d.flatten()
583 .filter(|e| e.path().extension().is_some_and(|x| x == "ctx"))
584 .count()
585 })
586 .unwrap_or(0);
587 eprintln!();
588 eprintln!(" {}", s.bold("Cache"));
589 eprintln!(" {:<22} {}", "Cached maps", cache_count);
590 eprintln!(" {:<22} {}", "Location", maps_dir.display());
591 eprintln!();
592
593 Ok(())
594}
595
596fn cmd_cache(args: &str) -> Result<()> {
598 let s = Styled::new();
599
600 let parts: Vec<&str> = args.split_whitespace().collect();
601 if parts.is_empty() || parts[0] != "clear" {
602 eprintln!(
603 " {} Usage: {}",
604 s.info_sym(),
605 s.bold("/cache clear [domain]")
606 );
607 return Ok(());
608 }
609
610 let domain = parts.get(1).copied();
611 let rt = tokio::runtime::Handle::current();
612 rt.block_on(async { crate::cli::cache_cmd::run_clear(domain).await })
613}
614
615async fn cmd_plug() -> Result<()> {
617 crate::cli::plug::run(false, false, true, None, None).await
618}