intent_engine/cli_handlers/
other.rs

1// Note: CurrentAction and EventCommands removed in v0.10.1 CLI simplification
2// These functions are kept for potential Dashboard/MCP use but not exposed in CLI
3// use crate::cli::{CurrentAction, EventCommands};
4use crate::cli_handlers::read_stdin;
5use crate::error::{IntentError, Result};
6use crate::events::EventManager;
7use crate::project::ProjectContext;
8use crate::report::ReportManager;
9use crate::workspace::WorkspaceManager;
10use std::path::PathBuf;
11
12// Stub types for deprecated CLI commands (no longer in cli.rs)
13#[allow(dead_code)]
14pub enum CurrentAction {
15    Set { task_id: i64 },
16    Clear,
17}
18
19#[allow(dead_code)]
20pub enum EventCommands {
21    Add {
22        task_id: Option<i64>,
23        log_type: String,
24        data_stdin: bool,
25    },
26    List {
27        task_id: Option<i64>,
28        log_type: Option<String>,
29        since: Option<String>,
30        limit: Option<i64>,
31    },
32}
33
34pub async fn handle_current_command(
35    set: Option<i64>,
36    command: Option<CurrentAction>,
37) -> Result<()> {
38    let ctx = ProjectContext::load().await?;
39    let workspace_mgr = WorkspaceManager::new(&ctx.pool);
40
41    // Handle backward compatibility: --set flag takes precedence
42    if let Some(task_id) = set {
43        eprintln!("⚠️  Warning: 'ie current --set' is a low-level atomic command.");
44        eprintln!(
45            "   For normal use, prefer 'ie task start {}' which ensures data consistency.",
46            task_id
47        );
48        eprintln!();
49        let response = workspace_mgr.set_current_task(task_id, None).await?;
50        println!("✓ Switched to task #{}", task_id);
51        println!("{}", serde_json::to_string_pretty(&response)?);
52        return Ok(());
53    }
54
55    // Handle subcommands
56    match command {
57        Some(CurrentAction::Set { task_id }) => {
58            eprintln!("⚠️  Warning: 'ie current set' is a low-level atomic command.");
59            eprintln!(
60                "   For normal use, prefer 'ie task start {}' which ensures data consistency.",
61                task_id
62            );
63            eprintln!();
64            let response = workspace_mgr.set_current_task(task_id, None).await?;
65            println!("✓ Switched to task #{}", task_id);
66            println!("{}", serde_json::to_string_pretty(&response)?);
67        },
68        Some(CurrentAction::Clear) => {
69            eprintln!("⚠️  Warning: 'ie current clear' is a low-level atomic command.");
70            eprintln!("   For normal use, prefer 'ie task done' or 'ie task switch' which ensures data consistency.");
71            eprintln!();
72            workspace_mgr.clear_current_task(None).await?;
73            println!("✓ Current task cleared");
74        },
75        None => {
76            // Default: display current task in JSON format
77            let response = workspace_mgr.get_current_task(None).await?;
78            println!("{}", serde_json::to_string_pretty(&response)?);
79        },
80    }
81
82    Ok(())
83}
84
85pub async fn handle_report_command(
86    since: Option<String>,
87    status: Option<String>,
88    filter_name: Option<String>,
89    filter_spec: Option<String>,
90    summary_only: bool,
91) -> Result<()> {
92    let ctx = ProjectContext::load().await?;
93    let report_mgr = ReportManager::new(&ctx.pool);
94
95    let report = report_mgr
96        .generate_report(since, status, filter_name, filter_spec, summary_only)
97        .await?;
98    println!("{}", serde_json::to_string_pretty(&report)?);
99
100    Ok(())
101}
102
103pub async fn handle_event_command(cmd: EventCommands) -> Result<()> {
104    match cmd {
105        EventCommands::Add {
106            task_id,
107            log_type,
108            data_stdin,
109        } => {
110            let ctx = ProjectContext::load_or_init().await?;
111            let project_path = ctx.root.to_string_lossy().to_string();
112            let event_mgr = EventManager::with_project_path(&ctx.pool, project_path);
113
114            let data = if data_stdin {
115                read_stdin()?
116            } else {
117                return Err(IntentError::InvalidInput(
118                    "--data-stdin is required".to_string(),
119                ));
120            };
121
122            // Determine the target task ID
123            let target_task_id = if let Some(id) = task_id {
124                // Use the provided task_id
125                id
126            } else {
127                // Fall back to current_task_id from sessions table for this session
128                let session_id = crate::workspace::resolve_session_id(None);
129                let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
130                    "SELECT current_task_id FROM sessions WHERE session_id = ?",
131                )
132                .bind(&session_id)
133                .fetch_optional(&ctx.pool)
134                .await?
135                .flatten();
136
137                current_task_id
138                    .ok_or_else(|| IntentError::InvalidInput(
139                        "No current task is set and --task-id was not provided. Use 'current --set <ID>' to set a task first.".to_string(),
140                    ))?
141            };
142
143            let event = event_mgr
144                .add_event(target_task_id, &log_type, &data)
145                .await?;
146            println!("{}", serde_json::to_string_pretty(&event)?);
147        },
148
149        EventCommands::List {
150            task_id,
151            limit,
152            log_type,
153            since,
154        } => {
155            let ctx = ProjectContext::load().await?;
156            let event_mgr = EventManager::new(&ctx.pool);
157
158            let events = event_mgr
159                .list_events(task_id, limit, log_type, since)
160                .await?;
161            println!("{}", serde_json::to_string_pretty(&events)?);
162        },
163    }
164
165    Ok(())
166}
167
168/// Check if query is a status keyword combination (todo, doing, done)
169/// Returns Some(statuses) if it's a status query, None otherwise
170fn parse_status_keywords(query: &str) -> Option<Vec<String>> {
171    let query_lower = query.to_lowercase();
172    let words: Vec<&str> = query_lower.split_whitespace().collect();
173
174    // Must have at least one word
175    if words.is_empty() {
176        return None;
177    }
178
179    // All words must be status keywords
180    let valid_statuses = ["todo", "doing", "done"];
181    let mut statuses: Vec<String> = Vec::new();
182
183    for word in words {
184        if valid_statuses.contains(&word) {
185            if !statuses.iter().any(|s| s == word) {
186                statuses.push(word.to_string());
187            }
188        } else {
189            // Found a non-status word, not a status query
190            return None;
191        }
192    }
193
194    Some(statuses)
195}
196
197pub async fn handle_search_command(
198    query: &str,
199    include_tasks: bool,
200    include_events: bool,
201    limit: Option<i64>,
202    offset: Option<i64>,
203    format: &str,
204) -> Result<()> {
205    use crate::search::SearchManager;
206    use crate::tasks::TaskManager;
207
208    let ctx = ProjectContext::load_or_init().await?;
209
210    // Check if query is a status keyword combination
211    if let Some(statuses) = parse_status_keywords(query) {
212        // Use TaskManager::find_tasks for status filtering
213        let task_mgr = TaskManager::new(&ctx.pool);
214
215        // Collect tasks for each status
216        let mut all_tasks = Vec::new();
217        for status in &statuses {
218            let result = task_mgr
219                .find_tasks(Some(status), None, None, limit, offset)
220                .await?;
221            all_tasks.extend(result.tasks);
222        }
223
224        // Sort by priority, then by id
225        all_tasks.sort_by(|a, b| {
226            let pri_a = a.priority.unwrap_or(999);
227            let pri_b = b.priority.unwrap_or(999);
228            pri_a.cmp(&pri_b).then_with(|| a.id.cmp(&b.id))
229        });
230
231        // Apply limit if specified
232        let limit = limit.unwrap_or(100) as usize;
233        if all_tasks.len() > limit {
234            all_tasks.truncate(limit);
235        }
236
237        if format == "json" {
238            println!("{}", serde_json::to_string_pretty(&all_tasks)?);
239        } else {
240            // Text format: status filter results
241            let status_str = statuses.join(", ");
242            println!(
243                "Tasks with status [{}]: {} found",
244                status_str,
245                all_tasks.len()
246            );
247            println!();
248            for task in &all_tasks {
249                let status_icon = match task.status.as_str() {
250                    "todo" => "○",
251                    "doing" => "●",
252                    "done" => "✓",
253                    _ => "?",
254                };
255                let parent_info = task
256                    .parent_id
257                    .map(|p| format!(" (parent: #{})", p))
258                    .unwrap_or_default();
259                let priority_info = task
260                    .priority
261                    .map(|p| format!(" [P{}]", p))
262                    .unwrap_or_default();
263                println!(
264                    "  {} #{} {}{}{}",
265                    status_icon, task.id, task.name, parent_info, priority_info
266                );
267                if let Some(spec) = &task.spec {
268                    if !spec.is_empty() {
269                        let truncated = if spec.len() > 60 {
270                            format!("{}...", &spec[..57])
271                        } else {
272                            spec.clone()
273                        };
274                        println!("      Spec: {}", truncated);
275                    }
276                }
277                println!("      Owner: {}", task.owner);
278                if let Some(ts) = task.first_todo_at {
279                    print!("      todo: {} ", ts.format("%m-%d %H:%M:%S"));
280                }
281                if let Some(ts) = task.first_doing_at {
282                    print!("doing: {} ", ts.format("%m-%d %H:%M:%S"));
283                }
284                if let Some(ts) = task.first_done_at {
285                    print!("done: {}", ts.format("%m-%d %H:%M:%S"));
286                }
287                if task.first_todo_at.is_some()
288                    || task.first_doing_at.is_some()
289                    || task.first_done_at.is_some()
290                {
291                    println!();
292                }
293            }
294        }
295        return Ok(());
296    }
297
298    // Regular FTS5 search
299    let search_mgr = SearchManager::new(&ctx.pool);
300
301    let results = search_mgr
302        .search(query, include_tasks, include_events, limit, offset, false)
303        .await?;
304
305    if format == "json" {
306        println!("{}", serde_json::to_string_pretty(&results)?);
307    } else {
308        use crate::db::models::SearchResult;
309
310        // Text format: FTS5 search results
311        println!(
312            "Search: \"{}\" → {} tasks, {} events (limit: {}, offset: {})",
313            query, results.total_tasks, results.total_events, results.limit, results.offset
314        );
315        println!();
316
317        for result in &results.results {
318            match result {
319                SearchResult::Task {
320                    task,
321                    match_field,
322                    match_snippet,
323                } => {
324                    let status_icon = match task.status.as_str() {
325                        "todo" => "○",
326                        "doing" => "●",
327                        "done" => "✓",
328                        _ => "?",
329                    };
330                    let parent_info = task
331                        .parent_id
332                        .map(|p| format!(" (parent: #{})", p))
333                        .unwrap_or_default();
334                    let priority_info = task
335                        .priority
336                        .map(|p| format!(" [P{}]", p))
337                        .unwrap_or_default();
338                    println!(
339                        "  {} #{} {} [match: {}]{}{}",
340                        status_icon, task.id, task.name, match_field, parent_info, priority_info
341                    );
342                    if let Some(spec) = &task.spec {
343                        if !spec.is_empty() {
344                            let truncated = if spec.len() > 60 {
345                                format!("{}...", &spec[..57])
346                            } else {
347                                spec.clone()
348                            };
349                            println!("      Spec: {}", truncated);
350                        }
351                    }
352                    if !match_snippet.is_empty() {
353                        println!("      Snippet: {}", match_snippet);
354                    }
355                    println!("      Owner: {}", task.owner);
356                    if let Some(ts) = task.first_todo_at {
357                        print!("      todo: {} ", ts.format("%m-%d %H:%M:%S"));
358                    }
359                    if let Some(ts) = task.first_doing_at {
360                        print!("doing: {} ", ts.format("%m-%d %H:%M:%S"));
361                    }
362                    if let Some(ts) = task.first_done_at {
363                        print!("done: {}", ts.format("%m-%d %H:%M:%S"));
364                    }
365                    if task.first_todo_at.is_some()
366                        || task.first_doing_at.is_some()
367                        || task.first_done_at.is_some()
368                    {
369                        println!();
370                    }
371                },
372                SearchResult::Event {
373                    event,
374                    task_chain,
375                    match_snippet,
376                } => {
377                    let icon = match event.log_type.as_str() {
378                        "decision" => "💡",
379                        "blocker" => "🚫",
380                        "milestone" => "🎯",
381                        _ => "📝",
382                    };
383                    println!(
384                        "  {} #{} [{}] (task #{}) {}",
385                        icon,
386                        event.id,
387                        event.log_type,
388                        event.task_id,
389                        event.timestamp.format("%Y-%m-%d %H:%M:%S")
390                    );
391                    println!("      Message: {}", event.discussion_data);
392                    if !match_snippet.is_empty() {
393                        println!("      Snippet: {}", match_snippet);
394                    }
395                    if !task_chain.is_empty() {
396                        let chain_str: Vec<String> = task_chain
397                            .iter()
398                            .map(|t| format!("#{} {}", t.id, t.name))
399                            .collect();
400                        println!("      Task chain: {}", chain_str.join(" → "));
401                    }
402                },
403            }
404        }
405
406        if results.has_more {
407            println!();
408            println!(
409                "  ... more results available (use --offset {})",
410                results.offset + results.limit
411            );
412        }
413    }
414    Ok(())
415}
416
417pub async fn handle_doctor_command() -> Result<()> {
418    use crate::cli_handlers::dashboard::{check_dashboard_health, DASHBOARD_PORT};
419
420    // Get database path info
421    let db_path_info = ProjectContext::get_database_path_info();
422
423    // Print database location
424    println!("Database:");
425    if let Some(db_path) = &db_path_info.final_database_path {
426        println!("  {}", db_path);
427    } else {
428        println!("  Not found");
429    }
430    println!();
431
432    // Print ancestor directories with databases
433    let dirs_with_db: Vec<&String> = db_path_info
434        .directories_checked
435        .iter()
436        .filter(|d| d.has_intent_engine)
437        .map(|d| &d.path)
438        .collect();
439
440    if !dirs_with_db.is_empty() {
441        println!("Ancestor directories with databases:");
442        for dir in dirs_with_db {
443            println!("  {}", dir);
444        }
445    } else {
446        println!("Ancestor directories with databases: None");
447    }
448    println!();
449
450    // Check dashboard status
451    print!("Dashboard: ");
452    let dashboard_health = check_dashboard_health(DASHBOARD_PORT).await;
453    if dashboard_health {
454        println!("Running (http://127.0.0.1:{})", DASHBOARD_PORT);
455    } else {
456        println!("Not running (start with 'ie dashboard start')");
457    }
458
459    Ok(())
460}
461
462pub async fn handle_init_command(at: Option<String>, force: bool) -> Result<()> {
463    use serde_json::json;
464
465    // Determine target directory
466    let target_dir = if let Some(path) = &at {
467        let p = PathBuf::from(path);
468        if !p.exists() {
469            return Err(IntentError::InvalidInput(format!(
470                "Directory does not exist: {}",
471                path
472            )));
473        }
474        if !p.is_dir() {
475            return Err(IntentError::InvalidInput(format!(
476                "Path is not a directory: {}",
477                path
478            )));
479        }
480        p
481    } else {
482        // Use current working directory
483        std::env::current_dir().expect("Failed to get current directory")
484    };
485
486    let intent_dir = target_dir.join(".intent-engine");
487
488    // Check if already exists
489    if intent_dir.exists() && !force {
490        let error_msg = format!(
491            ".intent-engine already exists at {}\nUse --force to re-initialize",
492            intent_dir.display()
493        );
494        return Err(IntentError::InvalidInput(error_msg));
495    }
496
497    // Perform initialization
498    let ctx = ProjectContext::initialize_project_at(target_dir).await?;
499
500    // Success output
501    let result = json!({
502        "success": true,
503        "root": ctx.root.display().to_string(),
504        "database_path": ctx.db_path.display().to_string(),
505        "message": "Intent-Engine initialized successfully"
506    });
507
508    println!("{}", serde_json::to_string_pretty(&result)?);
509    Ok(())
510}
511
512pub async fn handle_session_restore(
513    include_events: usize,
514    workspace: Option<String>,
515) -> Result<()> {
516    use crate::session_restore::SessionRestoreManager;
517
518    // If workspace path is specified, change to that directory
519    if let Some(ws_path) = workspace {
520        std::env::set_current_dir(&ws_path)?;
521    }
522
523    // Try to load project context
524    let ctx = match ProjectContext::load().await {
525        Ok(ctx) => ctx,
526        Err(_) => {
527            // Workspace not found
528            let result = crate::session_restore::SessionRestoreResult {
529                status: crate::session_restore::SessionStatus::Error,
530                workspace_path: std::env::current_dir()
531                    .ok()
532                    .and_then(|p| p.to_str().map(String::from)),
533                current_task: None,
534                parent_task: None,
535                siblings: None,
536                children: None,
537                recent_events: None,
538                suggested_commands: Some(vec![
539                    "ie workspace init".to_string(),
540                    "ie help".to_string(),
541                ]),
542                stats: None,
543                recommended_task: None,
544                top_pending_tasks: None,
545                error_type: Some(crate::session_restore::ErrorType::WorkspaceNotFound),
546                message: Some("No Intent-Engine workspace found in current directory".to_string()),
547                recovery_suggestion: Some(
548                    "Run 'ie workspace init' to create a new workspace".to_string(),
549                ),
550            };
551            println!("{}", serde_json::to_string_pretty(&result)?);
552            return Ok(());
553        },
554    };
555
556    let restore_mgr = SessionRestoreManager::new(&ctx.pool);
557    let result = restore_mgr.restore(include_events).await?;
558
559    println!("{}", serde_json::to_string_pretty(&result)?);
560
561    Ok(())
562}
563
564pub async fn handle_setup(
565    target: Option<String>,
566    scope: &str,
567    force: bool,
568    config_path: Option<String>,
569) -> Result<()> {
570    use crate::setup::claude_code::ClaudeCodeSetup;
571    use crate::setup::{SetupModule, SetupOptions, SetupScope};
572
573    println!("Intent-Engine Unified Setup");
574    println!("============================\n");
575
576    // Parse scope
577    let setup_scope: SetupScope = scope.parse()?;
578
579    // Build options
580    let opts = SetupOptions {
581        scope: setup_scope,
582        force,
583        config_path: config_path.map(PathBuf::from),
584    };
585
586    // Determine target (interactive if not specified)
587    let target_tool = if let Some(t) = target {
588        // Direct mode: target specified via CLI
589        t
590    } else {
591        // Interactive mode: launch wizard
592        use crate::setup::interactive::SetupWizard;
593
594        let wizard = SetupWizard::new();
595        let result = wizard.run(&opts)?;
596
597        // Print result and exit
598        if result.success {
599            println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
600            println!("✅ {}", result.message);
601            println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
602
603            if !result.files_modified.is_empty() {
604                println!("Files modified:");
605                for file in &result.files_modified {
606                    println!("  - {}", file.display());
607                }
608                println!();
609            }
610
611            if let Some(test) = result.connectivity_test {
612                if test.passed {
613                    println!("✓ Connectivity test: {}", test.details);
614                } else {
615                    println!("✗ Connectivity test: {}", test.details);
616                }
617                println!();
618            }
619
620            println!("Next steps:");
621            println!("  - Restart Claude Code to load MCP server");
622            println!("  - Run 'ie doctor' to verify configuration");
623            println!("  - Try 'ie task add --name \"Test task\"'");
624            println!();
625        } else {
626            println!("\n{}", result.message);
627        }
628
629        return Ok(());
630    };
631
632    // Setup mode
633    match target_tool.as_str() {
634        "claude-code" => {
635            let setup = ClaudeCodeSetup;
636            let result = setup.setup(&opts)?;
637
638            println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
639            println!("✅ {}", result.message);
640            println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
641
642            println!("Files modified:");
643            for file in &result.files_modified {
644                println!("  - {}", file.display());
645            }
646
647            if let Some(conn_test) = result.connectivity_test {
648                println!("\nConnectivity test:");
649                if conn_test.passed {
650                    println!("  ✅ {}", conn_test.details);
651                } else {
652                    println!("  ⚠️  {}", conn_test.details);
653                }
654            }
655
656            println!("\nNext steps:");
657            println!("  1. Restart Claude Code completely");
658            println!("  2. Open a new session in a project directory");
659            println!("  3. You should see Intent-Engine context restored");
660            println!("\nTo verify setup:");
661            println!("  ie setup --target claude-code --diagnose");
662
663            Ok(())
664        },
665        "gemini-cli" | "codex" => {
666            println!("⚠️  Target '{}' is not yet supported.", target_tool);
667            println!("Currently supported: claude-code");
668            Err(IntentError::InvalidInput(format!(
669                "Unsupported target: {}",
670                target_tool
671            )))
672        },
673        _ => Err(IntentError::InvalidInput(format!(
674            "Unknown target: {}. Available: claude-code, gemini-cli, codex",
675            target_tool
676        ))),
677    }
678}
679
680/// Check SessionStart hook configuration and effectiveness
681pub fn check_session_start_hook() -> serde_json::Value {
682    use crate::setup::common::get_home_dir;
683    use serde_json::json;
684
685    let home = match get_home_dir() {
686        Ok(h) => h,
687        Err(_) => {
688            return json!({
689                "check": "SessionStart Hook",
690                "status": "⚠ WARNING",
691                "details": {"error": "Unable to determine home directory"}
692            })
693        },
694    };
695
696    let user_hook = home.join(".claude/hooks/session-start.sh");
697    let user_settings = home.join(".claude/settings.json");
698
699    let script_exists = user_hook.exists();
700    let script_executable = script_exists && {
701        #[cfg(unix)]
702        {
703            use std::os::unix::fs::PermissionsExt;
704            std::fs::metadata(&user_hook)
705                .map(|m| m.permissions().mode() & 0o111 != 0)
706                .unwrap_or(false)
707        }
708        #[cfg(not(unix))]
709        {
710            true
711        }
712    };
713
714    let is_configured = if user_settings.exists() {
715        std::fs::read_to_string(&user_settings)
716            .ok()
717            .and_then(|content| serde_json::from_str::<serde_json::Value>(&content).ok())
718            .map(|settings| {
719                settings
720                    .get("hooks")
721                    .and_then(|h| h.get("SessionStart"))
722                    .is_some()
723            })
724            .unwrap_or(false)
725    } else {
726        false
727    };
728
729    let is_active = script_exists && script_executable && is_configured;
730
731    if is_active {
732        json!({
733            "check": "SessionStart Hook",
734            "status": "✓ PASS",
735            "details": {
736                "script": user_hook.display().to_string(),
737                "configured": true,
738                "executable": true,
739                "message": "SessionStart hook is active"
740            }
741        })
742    } else if is_configured && !script_exists {
743        json!({
744            "check": "SessionStart Hook",
745            "status": "✗ FAIL",
746            "details": {
747                "configured": true,
748                "exists": false,
749                "message": "Hook configured but script file missing"
750            }
751        })
752    } else if script_exists && !script_executable {
753        json!({
754            "check": "SessionStart Hook",
755            "status": "✗ FAIL",
756            "details": {
757                "executable": false,
758                "message": "Script not executable. Run: chmod +x ~/.claude/hooks/session-start.sh"
759            }
760        })
761    } else {
762        json!({
763            "check": "SessionStart Hook",
764            "status": "⚠ WARNING",
765            "details": {
766                "configured": false,
767                "message": "Not configured. Run 'ie setup --target claude-code'",
768                "setup_command": "ie setup --target claude-code"
769            }
770        })
771    }
772}
773
774pub fn handle_logs_command(
775    mode: Option<String>,
776    level: Option<String>,
777    since: Option<String>,
778    until: Option<String>,
779    limit: Option<usize>,
780    follow: bool,
781    export: String,
782) -> Result<()> {
783    use crate::logs::{
784        follow_logs, format_entry_json, format_entry_text, parse_duration, query_logs, LogQuery,
785    };
786
787    // Build query
788    let mut query = LogQuery {
789        mode,
790        level,
791        limit,
792        ..Default::default()
793    };
794
795    if let Some(since_str) = since {
796        query.since = parse_duration(&since_str);
797        if query.since.is_none() {
798            return Err(IntentError::InvalidInput(format!(
799                "Invalid duration format: {}. Use format like '1h', '24h', '7d'",
800                since_str
801            )));
802        }
803    }
804
805    if let Some(until_str) = until {
806        use chrono::DateTime;
807        match DateTime::parse_from_rfc3339(&until_str) {
808            Ok(dt) => query.until = Some(dt.with_timezone(&chrono::Utc)),
809            Err(e) => {
810                return Err(IntentError::InvalidInput(format!(
811                    "Invalid timestamp format: {}. Error: {}",
812                    until_str, e
813                )))
814            },
815        }
816    }
817
818    // Handle follow mode
819    if follow {
820        return follow_logs(&query).map_err(IntentError::IoError);
821    }
822
823    // Query logs
824    let entries = query_logs(&query).map_err(IntentError::IoError)?;
825
826    if entries.is_empty() {
827        eprintln!("No log entries found matching the criteria");
828        return Ok(());
829    }
830
831    // Display results
832    match export.as_str() {
833        "json" => {
834            println!("[");
835            for (i, entry) in entries.iter().enumerate() {
836                print!("  {}", format_entry_json(entry));
837                if i < entries.len() - 1 {
838                    println!(",");
839                } else {
840                    println!();
841                }
842            }
843            println!("]");
844        },
845        _ => {
846            for entry in entries {
847                println!("{}", format_entry_text(&entry));
848            }
849        },
850    }
851
852    Ok(())
853}