mi6_cli/
lib.rs

1// CLI crate needs stdout/stderr for user interaction
2#![allow(clippy::print_stdout, clippy::print_stderr)]
3
4mod args;
5pub mod commands;
6pub mod display;
7mod error;
8mod help;
9
10pub use help::{print_command_help, print_help, print_subcommand_help};
11
12use anyhow::{Context, Result};
13use mi6_core::{CodexSessionScanner, Storage, StorageError, TranscriptScanner};
14
15pub use args::{Cli, Commands, IngestCommands, OtelCommands, TuiArgs, UpgradeMethod};
16pub use commands::{UpdateInfo, check_for_update};
17
18/// Result of running a CLI command.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum RunResult {
21    /// Command completed successfully
22    Success,
23    /// Command requests the process to exit with the given code
24    ExitWithCode(i32),
25    /// Caller should launch the TUI with the provided arguments
26    RunTui(TuiArgs),
27}
28
29/// Run the CLI with a storage factory.
30///
31/// The factory is only called when a command needs storage.
32/// Returns `RunResult::RunTui` when the TUI should be launched (caller handles this).
33pub fn run<S, F>(cli: Cli, storage_factory: F) -> Result<RunResult>
34where
35    S: Storage,
36    F: FnOnce() -> Result<S, StorageError>,
37{
38    // Handle TUI mode: either no command or explicit `tui` subcommand
39    let command = match cli.command {
40        None => return Ok(RunResult::RunTui(cli.tui_args)),
41        Some(Commands::Tui(args)) => return Ok(RunResult::RunTui(args)),
42        Some(cmd) => cmd,
43    };
44
45    match command {
46        Commands::Tui(_) => unreachable!("handled above"),
47
48        Commands::Enable {
49            frameworks,
50            local,
51            settings_local,
52            print,
53            db_only,
54            hooks_only,
55            otel,
56            otel_port,
57            no_otel,
58        } => {
59            let result = commands::run_enable(commands::EnableCliOptions {
60                frameworks,
61                local,
62                settings_local,
63                print,
64                db_only,
65                hooks_only,
66                otel,
67                otel_port,
68                no_otel,
69            })?;
70
71            if result.should_init_db
72                && let Some(ref db_path) = result.db_path
73            {
74                let db_existed = db_path.exists();
75                let _ = storage_factory().context("failed to initialize storage")?;
76                if !db_existed {
77                    eprintln!("Database initialized at: {}", db_path.display());
78                }
79            }
80
81            if !print {
82                eprintln!("\nSessions may need to be restarted to use the new configuration.");
83            }
84
85            Ok(RunResult::Success)
86        }
87
88        Commands::Disable {
89            frameworks,
90            local,
91            settings_local,
92            print,
93        } => {
94            commands::run_disable(commands::DisableOptions {
95                frameworks,
96                local,
97                settings_local,
98                print,
99            })?;
100            Ok(RunResult::Success)
101        }
102
103        Commands::Ingest(ingest_cmd) => run_ingest(ingest_cmd, storage_factory),
104
105        Commands::Session {
106            session_or_pid,
107            machine,
108            json,
109            fields,
110        } => {
111            let storage = storage_factory().context("failed to open storage")?;
112            commands::run_session(&storage, session_or_pid, machine, json, fields)?;
113            Ok(RunResult::Success)
114        }
115
116        Commands::Status {
117            json,
118            verbose,
119            check,
120        } => {
121            commands::run_status_command(json, verbose, check)?;
122            Ok(RunResult::Success)
123        }
124
125        Commands::Watch {
126            session,
127            event_type,
128            mode,
129            framework,
130            poll_ms,
131        } => {
132            let storage = storage_factory().context("failed to open storage")?;
133            commands::run_watch(
134                &storage,
135                commands::WatchOptions {
136                    session,
137                    event_type,
138                    permission_mode: mode,
139                    framework,
140                    poll_ms,
141                },
142            )?;
143            Ok(RunResult::Success)
144        }
145
146        Commands::Cleanup { dry_run } => {
147            let storage = storage_factory().context("failed to open storage")?;
148            commands::run_cleanup(&storage, dry_run)?;
149            Ok(RunResult::Success)
150        }
151
152        Commands::Otel(otel_cmd) => run_otel(otel_cmd, storage_factory),
153
154        Commands::Upgrade {
155            version,
156            yes,
157            dry_run,
158            method,
159            source_path,
160        } => {
161            commands::run_upgrade(commands::UpgradeOptions {
162                version,
163                yes,
164                dry_run,
165                method,
166                source_path,
167            })?;
168            Ok(RunResult::Success)
169        }
170
171        Commands::Uninstall {
172            yes,
173            keep_data,
174            dry_run,
175        } => {
176            commands::run_uninstall(commands::UninstallOptions {
177                yes,
178                keep_data,
179                dry_run,
180            })?;
181            Ok(RunResult::Success)
182        }
183    }
184}
185
186/// Handle ingest subcommands.
187fn run_ingest<S, F>(cmd: IngestCommands, storage_factory: F) -> Result<RunResult>
188where
189    S: Storage,
190    F: FnOnce() -> Result<S, StorageError>,
191{
192    match cmd {
193        IngestCommands::Event {
194            event_type,
195            json_payload,
196            framework,
197            ping,
198        } => {
199            // Handle ping mode: just verify the command can be invoked, then exit
200            if ping {
201                println!("pong");
202                return Ok(RunResult::Success);
203            }
204
205            // The event ingest is called by AI assistant hooks during tool execution.
206            // It must always exit 0 to avoid blocking the workflow, even if logging fails.
207            // Errors are printed to stderr for debugging but don't affect the exit code.
208            let result: Result<(), anyhow::Error> = (|| {
209                let storage = storage_factory().context("failed to open storage")?;
210                let log_result =
211                    commands::run_log(&storage, event_type, json_payload, framework.clone())?;
212
213                // Scan transcript file after every hook (if path is available)
214                // This is cheap due to incremental position tracking
215                if let Some(ref path) = log_result.transcript_path {
216                    let scanner = TranscriptScanner::new(&storage, &log_result.machine_id);
217                    if let Err(e) = scanner.scan_file(std::path::Path::new(path)) {
218                        eprintln!("mi6: warning: failed to scan transcript: {e:#}");
219                    }
220
221                    // Backfill initial prompt if the UserPromptSubmit hook didn't fire
222                    // (e.g., when Claude is started with an inline prompt like `claude 'prompt'`)
223                    if let Err(e) = scanner
224                        .backfill_initial_prompt(&log_result.session_id, std::path::Path::new(path))
225                    {
226                        eprintln!("mi6: warning: failed to backfill initial prompt: {e:#}");
227                    }
228                }
229
230                // Scan Codex session file after every Codex hook event
231                // This fills in token counts, tool calls, and other data that hooks don't provide
232                if framework.as_deref() == Some("codex") {
233                    let scanner = CodexSessionScanner::new(&storage, &log_result.machine_id);
234                    if let Err(e) = scanner.scan_session(&log_result.session_id) {
235                        eprintln!("mi6: warning: failed to scan codex session: {e:#}");
236                    }
237                }
238
239                Ok(())
240            })();
241
242            if let Err(e) = result {
243                eprintln!("mi6: {:#}", e);
244            }
245            Ok(RunResult::ExitWithCode(0))
246        }
247
248        IngestCommands::Transcript { path } => {
249            let storage = storage_factory().context("failed to open storage")?;
250            commands::run_scan(&storage, &path)?;
251            Ok(RunResult::Success)
252        }
253
254        IngestCommands::CodexSession {
255            session_or_path,
256            file,
257        } => {
258            let storage = storage_factory().context("failed to open storage")?;
259            if file {
260                commands::run_codex_session_scan_file(
261                    &storage,
262                    std::path::Path::new(&session_or_path),
263                )?;
264            } else {
265                commands::run_codex_session_scan_by_id(&storage, &session_or_path)?;
266            }
267            Ok(RunResult::Success)
268        }
269
270        IngestCommands::Otel => {
271            let storage = storage_factory().context("failed to open storage")?;
272            commands::run_otel_ingest(&storage)?;
273            Ok(RunResult::Success)
274        }
275    }
276}
277
278/// Handle otel subcommands.
279fn run_otel<S, F>(cmd: OtelCommands, storage_factory: F) -> Result<RunResult>
280where
281    S: Storage,
282    F: FnOnce() -> Result<S, StorageError>,
283{
284    match cmd {
285        OtelCommands::Start { port, mode } => {
286            if commands::ensure_running(port, false, mode)? {
287                eprintln!("OTel server is running on port {}", port);
288            }
289            Ok(RunResult::Success)
290        }
291        OtelCommands::Stop { port } => {
292            commands::stop_server(port)?;
293            Ok(RunResult::Success)
294        }
295        OtelCommands::Restart { port, mode } => {
296            if commands::ensure_running(port, true, mode)? {
297                eprintln!("OTel server restarted on port {}", port);
298            }
299            Ok(RunResult::Success)
300        }
301        OtelCommands::Status { port } => {
302            commands::get_status(port)?;
303            Ok(RunResult::Success)
304        }
305        OtelCommands::Run { port, mode } => {
306            let storage = storage_factory().context("failed to open storage")?;
307            commands::run_server(&storage, port, mode)?;
308            Ok(RunResult::Success)
309        }
310    }
311}