1use clap::Parser;
2use oy_agent::infrastructure::persistence::{
3 find_latest_session, get_session_preview, list_all_sessions,
4};
5use oy_agent::infrastructure::tools::edit::EditTool;
6use oy_agent::infrastructure::tools::read::ReadTool;
7use oy_agent::infrastructure::tools::write::WriteTool;
8use oy_agent::infrastructure::tools::{ToolRegistry, bash::BashTool};
9use oy_ai::AiConfig;
10use serde::Deserialize;
11use std::path::{Path, PathBuf};
12use std::time::Duration;
13use tokio::process::Command;
14
15#[derive(Parser, Debug)]
17#[command(author, version, about)]
18pub struct CliArgs {
19 #[command(subcommand)]
20 pub command: Option<Commands>,
21
22 #[arg(short = 'p', long)]
24 pub prompt: Option<String>,
25
26 #[arg(short = 'm', long)]
27 pub model: Option<String>,
28
29 #[arg(short = 'c', long)]
31 pub r#continue: bool,
32
33 #[arg(short = 'r', long)]
35 pub restore: bool,
36
37 #[arg(short = 's', long = "session")]
39 pub session: Option<PathBuf>,
40}
41
42#[derive(Parser, Debug)]
43pub enum Commands {
44 Update,
46}
47
48#[derive(Debug, Deserialize, Default)]
50pub struct CliConfig {
51 pub api_key: Option<String>,
52 pub base_url: Option<String>,
53 pub model: Option<String>,
54}
55
56impl CliConfig {
57 pub fn load() -> Self {
59 let home = match dirs::home_dir() {
60 Some(h) => h,
61 None => return Self::default(),
62 };
63 let config_path = home.join(".oy-ai-agent").join("config.toml");
64 if !config_path.exists() {
65 return Self::default();
66 }
67 match std::fs::read_to_string(&config_path) {
68 Ok(content) => toml::from_str(&content).unwrap_or_default(),
69 Err(_) => Self::default(),
70 }
71 }
72}
73
74pub fn build_provider_config(cli_config: &CliConfig, cli_args: &CliArgs) -> AiConfig {
83 let api_key = cli_config.api_key.clone().unwrap_or_else(|| {
84 eprintln!(
85 "API key is not set. Set it in ~/.oy-ai-agent/config.toml:\n\n\
86 [api_key]\n\
87 api_key = \"sk-or-...\""
88 );
89 std::process::exit(1);
90 });
91
92 let base_url = cli_config
93 .base_url
94 .clone()
95 .unwrap_or_else(|| "https://openrouter.ai/api/v1".to_string());
96
97 let model = cli_args
98 .model
99 .clone()
100 .or_else(|| cli_config.model.clone())
101 .unwrap_or_else(|| "anthropic/claude-haiku-4.5".to_string());
102
103 AiConfig::new(base_url, api_key, model)
104}
105
106pub fn register_default_tools(registry: &mut ToolRegistry) {
108 registry.register(ReadTool);
109 registry.register(WriteTool);
110 registry.register(EditTool);
111 registry.register(BashTool);
112}
113
114pub async fn run(args: CliArgs) -> Result<(), anyhow::Error> {
116 if matches!(args.command, Some(Commands::Update)) {
118 return run_update().await;
119 }
120
121 if args.r#continue {
123 return run_continue_session().await;
124 }
125
126 if args.restore {
128 return run_restore_session().await;
129 }
130
131 if let Some(path) = &args.session {
133 return run_session_path(path).await;
134 }
135
136 if args.prompt.is_some() {
138 return Ok(());
140 }
141
142 oy_tui::run_tui(None)
144 .await
145 .map_err(|e| anyhow::Error::msg(format!("{}", e)))?;
146 Ok(())
147}
148
149async fn run_update() -> Result<(), anyhow::Error> {
152 let timeout = Duration::from_secs(300);
153
154 println!(
156 "⏳ Running: npm install -g @ghyper9023/oy (timeout: {}s)...",
157 timeout.as_secs()
158 );
159 match run_npm(&["install", "-g", "@ghyper9023/oy"], timeout).await {
160 Ok(output) => {
161 let stdout = String::from_utf8_lossy(&output.stdout);
162 let stderr = String::from_utf8_lossy(&output.stderr);
163 if !stderr.is_empty() {
164 println!("{}", stderr);
165 }
166 println!("✅ Update successful:\n{}", stdout);
167 return Ok(());
168 }
169 Err(e) => {
170 println!("⚠️ First attempt failed: {}", e);
171 println!("⏳ Retrying with npm official registry...");
172 }
173 }
174
175 match run_npm(
177 &[
178 "install",
179 "-g",
180 "@ghyper9023/oy",
181 "--registry",
182 "https://registry.npmjs.org/",
183 ],
184 timeout,
185 )
186 .await
187 {
188 Ok(output) => {
189 let stdout = String::from_utf8_lossy(&output.stdout);
190 let stderr = String::from_utf8_lossy(&output.stderr);
191 if !stderr.is_empty() {
192 println!("{}", stderr);
193 }
194 println!("✅ Update successful:\n{}", stdout);
195 Ok(())
196 }
197 Err(e) => {
198 eprintln!("❌ Update failed: {}", e);
199 std::process::exit(1);
200 }
201 }
202}
203
204async fn run_npm(args: &[&str], timeout: Duration) -> Result<std::process::Output, anyhow::Error> {
205 let child = Command::new("npm").args(args).kill_on_drop(true).output();
206
207 tokio::time::timeout(timeout, child)
208 .await
209 .map_err(|_| anyhow::anyhow!("Command timed out after {}s", timeout.as_secs()))?
210 .map_err(|e| anyhow::anyhow!("Failed to execute npm: {}", e))
211 .and_then(|output| {
212 if output.status.success() {
213 Ok(output)
214 } else {
215 let stderr = String::from_utf8_lossy(&output.stderr);
216 Err(anyhow::anyhow!(
217 "npm exited with code {}: {}",
218 output.status.code().unwrap_or(-1),
219 stderr.trim()
220 ))
221 }
222 })
223}
224
225async fn run_continue_session() -> Result<(), anyhow::Error> {
228 match find_latest_session() {
229 Ok(Some(entry)) => {
230 eprintln!(
231 "📂 Resuming session: {} (project: {})",
232 entry.uuid, entry.project_name
233 );
234 oy_tui::run_tui(Some(entry.path))
235 .await
236 .map_err(|e| anyhow::Error::msg(format!("{}", e)))?;
237 }
238 Ok(None) => {
239 eprintln!("ℹ️ No previous session found. Starting fresh.");
240 oy_tui::run_tui(None)
241 .await
242 .map_err(|e| anyhow::Error::msg(format!("{}", e)))?;
243 }
244 Err(e) => {
245 eprintln!("⚠️ Error finding sessions: {}", e);
246 oy_tui::run_tui(None)
247 .await
248 .map_err(|e| anyhow::Error::msg(format!("{}", e)))?;
249 }
250 }
251 Ok(())
252}
253
254async fn run_restore_session() -> Result<(), anyhow::Error> {
255 let sessions = list_all_sessions()?;
256
257 if sessions.is_empty() {
258 eprintln!("ℹ️ No sessions found. Starting fresh.");
259 oy_tui::run_tui(None)
260 .await
261 .map_err(|e| anyhow::Error::msg(format!("{}", e)))?;
262 return Ok(());
263 }
264
265 eprintln!("\n📋 Select a session to restore:\n");
267 for (i, entry) in sessions.iter().enumerate() {
268 let preview = get_session_preview(&entry.path)
269 .ok()
270 .flatten()
271 .unwrap_or_else(|| "(no user message)".to_string());
272 let uuid_str = entry.uuid.to_string();
273 let uuid_short: String = uuid_str.chars().take(12).collect();
274 eprintln!(
275 " [{:2}] {}... | {} | {}",
276 i + 1,
277 uuid_short,
278 entry.project_name,
279 preview
280 );
281 }
282 eprintln!("\n [0] Cancel");
283 eprint!("\nEnter selection (0-{}): ", sessions.len());
284 std::io::Write::flush(&mut std::io::stderr())?;
285
286 let mut input = String::new();
287 std::io::stdin().read_line(&mut input)?;
288 let input = input.trim();
289
290 if let Ok(num) = input.parse::<usize>() {
291 if num == 0 || num > sessions.len() {
292 eprintln!("❌ Cancelled.");
293 return Ok(());
294 }
295 let entry = &sessions[num - 1];
296 eprintln!("📂 Restoring session: {}", entry.uuid);
297 oy_tui::run_tui(Some(entry.path.clone()))
298 .await
299 .map_err(|e| anyhow::Error::msg(format!("{}", e)))?;
300 } else {
301 eprintln!("❌ Invalid selection.");
302 }
303
304 Ok(())
305}
306
307async fn run_session_path(path: &Path) -> Result<(), anyhow::Error> {
310 if !path.exists() {
311 eprintln!("❌ Session file not found: {}", path.display());
312 std::process::exit(1);
313 }
314 if !path.is_file() {
315 eprintln!("❌ Path is not a file: {}", path.display());
316 std::process::exit(1);
317 }
318
319 match oy_agent::infrastructure::persistence::load_session_messages(path) {
321 Ok((uuid, _msgs)) => {
322 eprintln!("📂 Loading session: {} ({})", uuid, path.display());
323 oy_tui::run_tui(Some(path.to_path_buf()))
324 .await
325 .map_err(|e| anyhow::Error::msg(format!("{}", e)))?;
326 Ok(())
327 }
328 Err(e) => {
329 eprintln!("❌ Failed to load session file: {}", e);
330 std::process::exit(1);
331 }
332 }
333}