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;
9pub mod process;
10
11pub use help::{print_command_help, print_help, print_subcommand_help};
12
13use anyhow::{Context, Result};
14use mi6_core::{CodexSessionScanner, Storage, StorageError, TranscriptScanner};
15
16pub use args::{Cli, Commands, IngestCommands, OtelCommands, TuiArgs, UpgradeMethod};
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 { json, verbose } => {
117            commands::run_status_command(json, verbose)?;
118            Ok(RunResult::Success)
119        }
120
121        Commands::Watch {
122            session,
123            event_type,
124            mode,
125            framework,
126            poll_ms,
127        } => {
128            let storage = storage_factory().context("failed to open storage")?;
129            commands::run_watch(
130                &storage,
131                commands::WatchOptions {
132                    session,
133                    event_type,
134                    permission_mode: mode,
135                    framework,
136                    poll_ms,
137                },
138            )?;
139            Ok(RunResult::Success)
140        }
141
142        Commands::Gc { dry_run } => {
143            let storage = storage_factory().context("failed to open storage")?;
144            commands::run_gc(&storage, dry_run)?;
145            Ok(RunResult::Success)
146        }
147
148        Commands::Otel(otel_cmd) => run_otel(otel_cmd, storage_factory),
149
150        Commands::Upgrade {
151            version,
152            yes,
153            dry_run,
154            method,
155            source_path,
156        } => {
157            commands::run_upgrade(commands::UpgradeOptions {
158                version,
159                yes,
160                dry_run,
161                method,
162                source_path,
163            })?;
164            Ok(RunResult::Success)
165        }
166
167        Commands::Uninstall {
168            confirm,
169            keep_data,
170            dry_run,
171        } => {
172            commands::run_uninstall(commands::UninstallOptions {
173                confirm,
174                keep_data,
175                dry_run,
176            })?;
177            Ok(RunResult::Success)
178        }
179    }
180}
181
182/// Handle ingest subcommands.
183fn run_ingest<S, F>(cmd: IngestCommands, storage_factory: F) -> Result<RunResult>
184where
185    S: Storage,
186    F: FnOnce() -> Result<S, StorageError>,
187{
188    match cmd {
189        IngestCommands::Event {
190            event_type,
191            json_payload,
192            framework,
193        } => {
194            // The event ingest is called by AI assistant hooks during tool execution.
195            // It must always exit 0 to avoid blocking the workflow, even if logging fails.
196            // Errors are printed to stderr for debugging but don't affect the exit code.
197            let result: Result<(), anyhow::Error> = (|| {
198                let storage = storage_factory().context("failed to open storage")?;
199                let log_result =
200                    commands::run_log(&storage, event_type, json_payload, framework.clone())?;
201
202                // Scan transcript file after every hook (if path is available)
203                // This is cheap due to incremental position tracking
204                if let Some(ref path) = log_result.transcript_path {
205                    let scanner = TranscriptScanner::new(&storage, &log_result.machine_id);
206                    if let Err(e) = scanner.scan_file(std::path::Path::new(path)) {
207                        eprintln!("mi6: warning: failed to scan transcript: {e:#}");
208                    }
209
210                    // Backfill initial prompt if the UserPromptSubmit hook didn't fire
211                    // (e.g., when Claude is started with an inline prompt like `claude 'prompt'`)
212                    if let Err(e) = scanner
213                        .backfill_initial_prompt(&log_result.session_id, std::path::Path::new(path))
214                    {
215                        eprintln!("mi6: warning: failed to backfill initial prompt: {e:#}");
216                    }
217                }
218
219                // Scan Codex session file after every Codex hook event
220                // This fills in token counts, tool calls, and other data that hooks don't provide
221                if framework.as_deref() == Some("codex") {
222                    let scanner = CodexSessionScanner::new(&storage, &log_result.machine_id);
223                    if let Err(e) = scanner.scan_session(&log_result.session_id) {
224                        eprintln!("mi6: warning: failed to scan codex session: {e:#}");
225                    }
226                }
227
228                Ok(())
229            })();
230
231            if let Err(e) = result {
232                eprintln!("mi6: {:#}", e);
233            }
234            Ok(RunResult::ExitWithCode(0))
235        }
236
237        IngestCommands::Transcript { path } => {
238            let storage = storage_factory().context("failed to open storage")?;
239            commands::run_scan(&storage, &path)?;
240            Ok(RunResult::Success)
241        }
242
243        IngestCommands::CodexSession {
244            session_or_path,
245            file,
246        } => {
247            let storage = storage_factory().context("failed to open storage")?;
248            if file {
249                commands::run_codex_session_scan_file(
250                    &storage,
251                    std::path::Path::new(&session_or_path),
252                )?;
253            } else {
254                commands::run_codex_session_scan_by_id(&storage, &session_or_path)?;
255            }
256            Ok(RunResult::Success)
257        }
258
259        IngestCommands::Otel => {
260            let storage = storage_factory().context("failed to open storage")?;
261            commands::run_otel_ingest(&storage)?;
262            Ok(RunResult::Success)
263        }
264    }
265}
266
267/// Handle otel subcommands.
268fn run_otel<S, F>(cmd: OtelCommands, storage_factory: F) -> Result<RunResult>
269where
270    S: Storage,
271    F: FnOnce() -> Result<S, StorageError>,
272{
273    match cmd {
274        OtelCommands::Start { port, mode } => {
275            if commands::ensure_running(port, false, mode)? {
276                eprintln!("OTel server is running on port {}", port);
277            }
278            Ok(RunResult::Success)
279        }
280        OtelCommands::Stop { port } => {
281            commands::stop_server(port)?;
282            Ok(RunResult::Success)
283        }
284        OtelCommands::Restart { port, mode } => {
285            if commands::ensure_running(port, true, mode)? {
286                eprintln!("OTel server restarted on port {}", port);
287            }
288            Ok(RunResult::Success)
289        }
290        OtelCommands::Status { port } => {
291            commands::get_status(port)?;
292            Ok(RunResult::Success)
293        }
294        OtelCommands::Run { port, mode } => {
295            let storage = storage_factory().context("failed to open storage")?;
296            commands::run_server(&storage, port, mode)?;
297            Ok(RunResult::Success)
298        }
299    }
300}