mod ai_router;
mod editor;
mod input;
mod investigate;
use std::collections::HashMap;
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Arc;
use reedline::{Reedline, Signal};
use tracing::{info, warn};
use crate::ai::{ConversationState, JarvisAI};
use crate::cli::prompt::{JarvisPrompt, EXIT_CODE_NONE};
use crate::config::JarvishConfig;
use crate::engine::classifier::InputClassifier;
use crate::engine::expand;
use crate::storage::BlackBox;
pub struct Shell {
editor: Reedline,
prompt: JarvisPrompt,
ai_client: Option<JarvisAI>,
black_box: Option<BlackBox>,
conversation_state: Option<ConversationState>,
last_exit_code: Arc<AtomicI32>,
classifier: Arc<InputClassifier>,
aliases: HashMap<String, String>,
farewell_shown: bool,
}
impl Shell {
pub fn new() -> Self {
let config = JarvishConfig::load();
Self::apply_exports(&config);
let classifier = Arc::new(InputClassifier::new());
let data_dir = BlackBox::data_dir();
let db_path = data_dir.join("history.db");
let reedline = editor::build_editor(Arc::clone(&classifier), db_path);
let last_exit_code = Arc::new(AtomicI32::new(EXIT_CODE_NONE));
let prompt = JarvisPrompt::new(Arc::clone(&last_exit_code), config.prompt.clone());
let black_box = match BlackBox::open_at(data_dir) {
Ok(bb) => {
info!("BlackBox initialized successfully");
Some(bb)
}
Err(e) => {
warn!("Failed to initialize BlackBox: {e}");
eprintln!("jarvish: warning: failed to initialize black box: {e}");
None
}
};
let ai_client = match JarvisAI::new(&config.ai) {
Ok(ai) => {
info!("AI client initialized successfully");
Some(ai)
}
Err(e) => {
warn!("AI disabled: {e}");
eprintln!("jarvish: warning: AI disabled: {e}");
None }
};
Self {
editor: reedline,
prompt,
ai_client,
black_box,
conversation_state: None,
last_exit_code,
classifier,
aliases: config.alias,
farewell_shown: false,
}
}
fn apply_exports(config: &JarvishConfig) {
for (key, value) in &config.export {
let expanded = expand::expand_token(value);
info!(key = %key, value = %expanded, "Applying export from config");
unsafe {
std::env::set_var(key, &expanded);
}
}
}
pub(super) fn reload_config(&mut self, path: &std::path::Path) -> crate::engine::CommandResult {
use crate::engine::CommandResult;
let config = match JarvishConfig::load_from(path) {
Ok(c) => c,
Err(msg) => {
let err = format!("jarvish: source: {msg}\n");
eprint!("{err}");
return CommandResult::error(err, 1);
}
};
let path_before = std::env::var("PATH").ok();
self.aliases = config.alias.clone();
Self::apply_exports(&config);
if let Some(ref mut ai) = self.ai_client {
ai.update_config(&config.ai);
}
self.prompt.update_config(config.prompt.clone());
let path_after = std::env::var("PATH").ok();
if path_before != path_after {
info!("PATH changed via source, reloading classifier cache");
self.classifier.reload_path_cache();
}
let summary = format!(
"Loaded {} (alias: {}, export: {}, ai.model: {}, nerd_font: {})\n",
path.display(),
config.alias.len(),
config.export.len(),
config.ai.model,
config.prompt.nerd_font,
);
print!("{summary}");
CommandResult::success(summary)
}
pub async fn run(&mut self) -> i32 {
crate::cli::banner::print_welcome();
let mut repl_error = false;
loop {
match self.editor.read_line(&self.prompt) {
Ok(Signal::Success(line)) => {
if !self.handle_input(&line).await {
break;
}
}
Ok(Signal::CtrlC) => {
info!("\n!!!! Ctrl-C received: do it nothing !!!!!\n");
println!(); }
Ok(Signal::CtrlD) => {
info!("\n!!!! Ctrl-D received: exiting shell !!!!!\n");
break;
}
Err(e) => {
warn!(error = %e, "REPL error, exiting");
eprintln!("jarvish: error: {e}");
repl_error = true;
break;
}
}
}
if !self.farewell_shown {
crate::cli::banner::print_goodbye();
}
if repl_error {
1
} else {
let code = self.last_exit_code.load(Ordering::Relaxed);
if code == EXIT_CODE_NONE {
0
} else {
code
}
}
}
}