Skip to main content

eli_cli/
dispatch.rs

1pub async fn run() -> Result<()> {
2    tracing_subscriber::fmt()
3        .with_env_filter(
4            std::env::var("RUST_LOG")
5                .unwrap_or_else(|_| {
6                    "error,eli=warn,eli_cli=warn,chromiumoxide=off,chromiumoxide::conn::raw_ws::parse_errors=off".to_string()
7                }),
8        )
9        .with_writer(std::io::stderr)
10        .init();
11
12    let cli = Cli::try_parse()?;
13
14    match cli.cmd {
15        None => cmd_chat(cli.provider, cli.model, None).await,
16        Some(Command::Setup) => cmd_setup().await,
17        Some(Command::Init) => cmd_init().await,
18        Some(Command::Config { set, value }) => cmd_config(set, value).await,
19        Some(Command::ToolInfo { path }) => cmd_tool_info(path),
20        Some(Command::Chat) => cmd_chat(cli.provider, cli.model, None).await,
21        Some(Command::Debug) => cmd_chat(cli.provider, cli.model, Some(DisplayMode::Debug)).await,
22        Some(Command::Raw) => cmd_chat(cli.provider, cli.model, Some(DisplayMode::Raw)).await,
23        Some(Command::Research { query }) => cmd_research(query, cli.provider, cli.model).await,
24        Some(Command::Tui) => cmd_tui(cli.provider, cli.model).await,
25        Some(Command::Finance { cmd }) => cmd_finance(cmd).await,
26        Some(Command::Web { cmd }) => cmd_web(cmd).await,
27        Some(Command::Agent { cmd }) => cmd_agent(cmd, cli.provider, cli.model).await,
28        Some(Command::Code(args)) => cmd_code(args).await,
29        Some(Command::Sentinel { cmd }) => cmd_sentinel(cmd).await,
30        Some(Command::Mcp(args)) => {
31            if let Some(McpSubcommand::Share(share_args)) = args.cmd {
32                cmd_mcp_share(share_args).await
33            } else if args.http {
34                cmd_mcp_http(args.port).await
35            } else {
36                cmd_mcp().await
37            }
38        }
39        Some(Command::Picks { cmd }) => cmd_picks(cmd).await,
40        Some(Command::Serve(_args)) => anyhow::bail!("serve command temporarily disabled"),
41    }
42}
43
44async fn cmd_research(
45    query: String,
46    provider: Option<String>,
47    model: Option<String>,
48) -> Result<()> {
49    let paths = Paths::discover().context("discover paths")?;
50    let mut cfg = config::load_or_create(&paths).context("load/create config")?;
51    apply_overrides(&mut cfg, provider, model)?;
52
53    // Research defaults: safe, autonomous, non-destructive.
54    cfg.chat.mode = RunMode::Read;
55    cfg.chat.approvals = ApprovalMode::Auto;
56    cfg.chat.auto = true;
57    cfg.chat.max_auto = cfg.chat.max_auto.min(12).max(1);
58    cfg.chat.compact_trigger_tokens = Some(
59        cfg.chat
60            .resolved_compact_trigger_tokens()
61            .unwrap_or(100_000)
62            .min(30_000),
63    );
64    if env_truthy("ELI_AGENT_FAST") {
65        cfg.chat.max_auto = cfg.chat.max_auto.min(6).max(1);
66        cfg.chat.compact_trigger_tokens = Some(
67            cfg.chat
68                .resolved_compact_trigger_tokens()
69                .unwrap_or(30_000)
70                .min(15_000),
71        );
72        cfg.chat.max_cmds = cfg.chat.max_cmds.min(3).max(1);
73        if cfg.chat.max_tokens.is_none() {
74            cfg.chat.max_tokens = Some(3200);
75        }
76    }
77    // Force plain/non-footer output when external clients request it.
78    if env_truthy("ELI_PLAIN_OUTPUT") || env_truthy("ELI_NO_FOOTER") {
79        cfg.chat.display_mode = DisplayMode::Brain;
80    }
81
82    let adapter = eli_adapters::build_from_chat_config(&cfg.chat).context("build adapter")?;
83    let adapter: Arc<dyn LlmAdapter> = Arc::from(adapter);
84
85    let cwd = std::env::current_dir().context("get cwd")?;
86    let project_root = cfg
87        .chat
88        .resolved_project_root(&cwd)
89        .map_err(|e| anyhow::anyhow!(e))
90        .context("resolve project root")?;
91
92    ensure_eli_research_brain(&project_root).context("ensure eli_research/ELI.md")?;
93
94    let diff_engine = DiffEngine::new(project_root.clone()).context("init diff engine")?;
95    let command_runner = CommandRunner::new(
96        cfg.chat.timeout_secs,
97        cfg.chat.max_cmds,
98        cfg.chat.parallel_commands,
99        project_root.clone(),
100    );
101
102    let store = SessionStore::new(&paths);
103    let session_id = uuid::Uuid::new_v4().to_string();
104
105    // Ensure instincts directory exists
106    let instincts_dir = project_root.join("instincts");
107    if !instincts_dir.exists() {
108        let _ = std::fs::create_dir_all(&instincts_dir);
109    }
110
111    info!(session_id = %session_id, provider = %cfg.chat.provider, model = %cfg.chat.model, "starting research");
112
113    let mut memory = eli_core::memory::Memory::new(cfg.chat.mem_steps);
114    memory.set_system(eli_core::contract::system_prompt());
115
116    // Inject existing instincts into memory
117    if instincts_dir.exists() {
118        if let Ok(entries) = std::fs::read_dir(&instincts_dir) {
119            for entry in entries.flatten() {
120                if let Ok(content) = std::fs::read_to_string(entry.path()) {
121                    let filename = entry.file_name().to_string_lossy().to_string();
122                    memory.push(ChatMessage::system(format!(
123                        "INSTINCT ({filename}):\n{content}"
124                    )));
125                }
126            }
127        }
128    }
129    let mut undo_stack: Vec<Vec<DiffResult>> = Vec::new();
130    let mut state = SessionState::new(&cfg.chat);
131    state.load_recent_research(&project_root, 12);
132    if let Ok(agent_context) = std::env::var("ELI_AGENT_CONTEXT") {
133        let ctx = agent_context.trim();
134        if !ctx.is_empty() {
135            memory.push(ChatMessage::system(format!(
136                "AGENT EXECUTION CONTEXT:\n{ctx}"
137            )));
138        }
139    }
140
141    if is_trivial_query(&query) {
142        let answer = "Hello. What should I focus on?";
143        let assistant = serde_json::json!({
144            "plan": format!(
145                "MODE: READ | APPROVALS: AUTO | ROOT: {} | Trivial query detected; no tool calls needed.",
146                project_root.display()
147            ),
148            "checklist": [],
149            "focus": "Clarify user intent",
150            "status": "DONE",
151            "commands": [],
152            "commands_parallel": false,
153            "screen": [],
154            "diffs": [],
155            "subagents": [],
156            "synthesis": {
157                "summary": [],
158                "answer": answer,
159                "next_steps": []
160            },
161            "ask_user": "",
162            "notes": answer
163        })
164        .to_string();
165
166        store
167            .append(
168                &session_id,
169                &SessionEvent {
170                    ts: chrono::Utc::now(),
171                    kind: EventKind::UserMessage {
172                        content: query.clone(),
173                    },
174                },
175            )
176            .await?;
177        store
178            .append(
179                &session_id,
180                &SessionEvent {
181                    ts: chrono::Utc::now(),
182                    kind: EventKind::AssistantMessage { content: assistant },
183                },
184            )
185            .await?;
186
187        println!("{answer}");
188        return Ok(());
189    }
190
191    if has_interactive_terminal() {
192        print_banner(&cfg.chat, &project_root, &state);
193    }
194
195    run_agent_steps(
196        &cfg.chat,
197        adapter.clone(),
198        &diff_engine,
199        &command_runner,
200        &store,
201        &paths.data_dir,
202        &session_id,
203        &project_root,
204        &mut memory,
205        &mut undo_stack,
206        &mut state,
207        AgentProfile::Research,
208        query,
209        Vec::new(),
210    )
211    .await?;
212
213    if has_interactive_terminal() && !env_truthy("ELI_PLAIN_OUTPUT") && !env_truthy("ELI_NO_FOOTER")
214    {
215        print_cost_stats(&state, &cfg.chat);
216    }
217
218    Ok(())
219}
220
221fn is_trivial_query(query: &str) -> bool {
222    let q = query.trim().to_ascii_lowercase();
223    if q.is_empty() {
224        return true;
225    }
226    matches!(
227        q.as_str(),
228        "hi" | "hello" | "hey" | "yo" | "sup" | "hola" | "good morning" | "good afternoon"
229    )
230}
231
232fn is_quick_market_query(query: &str) -> bool {
233    let q = query.trim().to_ascii_lowercase();
234    if q.is_empty() {
235        return false;
236    }
237    q.contains("market today")
238        || q.contains("what happened")
239        || q.contains("what did you think")
240        || q.contains("price of")
241        || q.contains("stock price")
242}
243
244async fn cmd_finance(cmd: FinanceCommand) -> Result<()> {
245    match cmd {
246        FinanceCommand::Timeseries(args) => cmd_finance_timeseries(args).await,
247        FinanceCommand::Fundamentals(args) => cmd_finance_fundamentals(args).await,
248        FinanceCommand::Search(args) => cmd_finance_search(args).await,
249        FinanceCommand::Filings(args) | FinanceCommand::Sec(args) => {
250            cmd_finance_filings(args).await
251        }
252        FinanceCommand::Schedule(args) => cmd_finance_schedule(args).await,
253        FinanceCommand::RatePath(args) => cmd_finance_rate_path(args).await,
254        FinanceCommand::Odds(args) => cmd_finance_odds(args).await,
255        FinanceCommand::Options(args) => cmd_finance_options(args).await,
256        FinanceCommand::Sync(args) => cmd_finance_sync(args).await,
257        FinanceCommand::Paper(args) => cmd_finance_paper(args).await,
258        FinanceCommand::Ibkr(args) => cmd_finance_ibkr(args).await,
259        FinanceCommand::Auctions(args) => cmd_finance_auctions(args).await,
260        FinanceCommand::Cot(args) => cmd_finance_cot(args).await,
261        FinanceCommand::Curve(args) => cmd_finance_curve(args).await,
262        FinanceCommand::Nyfed(args) => cmd_finance_nyfed(args).await,
263        FinanceCommand::Volsurface(args) => cmd_finance_volsurface(args).await,
264        FinanceCommand::Stress(args) => cmd_finance_stress(args).await,
265        FinanceCommand::Fiscal(args) => cmd_finance_fiscal(args).await,
266        FinanceCommand::Ecb(args) => cmd_finance_ecb(args).await,
267        FinanceCommand::Eia(args) => cmd_finance_eia(args).await,
268        FinanceCommand::Bis(args) => cmd_finance_bis(args).await,
269        FinanceCommand::Boj(args) => cmd_finance_boj(args).await,
270        FinanceCommand::Boe(args) => cmd_finance_boe(args).await,
271    }
272}