mod app;
mod chat;
mod command;
mod extract;
mod factcheck;
mod facts_tree;
mod focus;
mod imports;
mod insert;
mod llm;
mod picker;
mod provenance;
mod rag;
mod render;
mod scholarly;
mod thread;
mod verdicts;
mod verify;
mod sync;
mod web;
mod wikidata;
mod batch;
pub(crate) use focus::Focus;
pub(super) const UNDISPUTED_TAG: &str = "fact:undisputed";
use std::io;
use std::path::Path;
use anyhow::Result;
use crossterm::execute;
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use crate::config::Config;
use crate::project::ProjectLayout;
use crate::store::Store;
use crate::store::hierarchy::Hierarchy;
use app::ResearchApp;
pub(crate) struct ResearchInvocation {
pub thread: Option<String>,
pub list_threads: bool,
pub export_thread: Option<String>,
pub format: Option<String>,
pub out: Option<String>,
pub import: Option<String>,
pub sync: Option<String>,
pub batch: Option<String>,
pub auto_confirm: bool,
pub confidence: Option<f64>,
}
pub(crate) fn run(project: &Path, inv: ResearchInvocation) -> Result<()> {
let layout = ProjectLayout::new(project);
layout.require_initialized().map_err(anyhow::Error::from)?;
let cfg = Config::load_layered(&layout.config_path()).map_err(anyhow::Error::from)?;
if let Some(path) = inv.import.as_deref() {
let store = Store::open(layout.clone(), &cfg).map_err(anyhow::Error::from)?;
return app::import_cli(&layout, &cfg, &store, path);
}
if let Some(folder) = inv.sync.as_deref() {
let store = Store::open(layout.clone(), &cfg).map_err(anyhow::Error::from)?;
return sync_cli(&layout, &cfg, &store, folder);
}
if let Some(bpath) = inv.batch.as_deref() {
let store = Store::open(layout.clone(), &cfg).map_err(anyhow::Error::from)?;
return batch::run(
&layout,
&cfg,
&store,
bpath,
inv.auto_confirm,
inv.confidence.unwrap_or(0.7),
inv.out.as_deref(),
);
}
if inv.list_threads {
return app::list_threads_cli(&layout, inv.format.as_deref());
}
if let Some(name) = inv.export_thread.as_deref() {
return app::export_thread_cli(&layout, name, inv.format.as_deref(), inv.out.as_deref());
}
let store = Store::open(layout.clone(), &cfg).map_err(anyhow::Error::from)?;
let hierarchy = Hierarchy::load(&store).map_err(anyhow::Error::from)?;
reimport_changed_folders(&layout, &cfg, &store);
launch_tui(layout, cfg, store, hierarchy, inv.thread)
}
fn sync_cli(layout: &ProjectLayout, cfg: &Config, store: &Store, folder: &str) -> Result<()> {
let now = chrono::Utc::now().timestamp();
let abs = sync::SyncManifest::register(layout, folder, now)?;
app::import_cli(layout, cfg, store, &abs)?;
println!("synced folder registered — re-imported on change at each launch");
Ok(())
}
fn reimport_changed_folders(layout: &ProjectLayout, cfg: &Config, store: &Store) {
let manifest = sync::SyncManifest::load(layout);
for (abs, last_sync) in &manifest.folders {
let path = std::path::Path::new(abs);
if !path.is_dir() {
continue;
}
if sync::newest_mtime(path) > *last_sync {
if app::import_cli(layout, cfg, store, abs).is_ok() {
sync::SyncManifest::mark_synced(layout, abs, chrono::Utc::now().timestamp());
}
}
}
}
fn launch_tui(
layout: ProjectLayout,
cfg: Config,
store: Store,
hierarchy: Hierarchy,
thread: Option<String>,
) -> Result<()> {
crate::crash::set_terminal_restore(Some(Box::new(|| {
let _ = disable_raw_mode();
let _ = execute!(io::stdout(), LeaveAlternateScreen);
})));
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let result = match picker::resolve_thread(&mut terminal, &layout, thread)? {
Some(name) => {
let mut app = ResearchApp::new(layout, cfg, store, hierarchy, Some(name))?;
app.run(&mut terminal)
}
None => Ok(()),
};
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
crate::crash::set_terminal_restore(None);
result
}