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 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 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 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 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}