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::{Storage, StorageError, TranscriptScanner};
15
16pub use args::{Cli, Commands, IngestCommands, OtelCommands, TuiArgs};
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(db_path) = result.db_path
73            {
74                let _ = storage_factory().context("failed to initialize storage")?;
75                eprintln!("Database initialized at: {}", db_path.display());
76            }
77
78            if !print {
79                eprintln!(
80                    "\nRemember to reload hooks in your AI assistant to enable the new configuration."
81                );
82            }
83
84            Ok(RunResult::Success)
85        }
86
87        Commands::Disable {
88            frameworks,
89            local,
90            settings_local,
91            print,
92        } => {
93            commands::run_disable(commands::DisableOptions {
94                frameworks,
95                local,
96                settings_local,
97                print,
98            })?;
99            Ok(RunResult::Success)
100        }
101
102        Commands::Ingest(ingest_cmd) => run_ingest(ingest_cmd, storage_factory),
103
104        Commands::Session {
105            session_or_pid,
106            machine,
107            json,
108            fields,
109        } => {
110            let storage = storage_factory().context("failed to open storage")?;
111            commands::run_session(&storage, session_or_pid, machine, json, fields)?;
112            Ok(RunResult::Success)
113        }
114
115        Commands::Status { json } => {
116            commands::run_status_command(json)?;
117            Ok(RunResult::Success)
118        }
119
120        Commands::Watch {
121            session,
122            event_type,
123            mode,
124            framework,
125            poll_ms,
126        } => {
127            let storage = storage_factory().context("failed to open storage")?;
128            commands::run_watch(
129                &storage,
130                commands::WatchOptions {
131                    session,
132                    event_type,
133                    permission_mode: mode,
134                    framework,
135                    poll_ms,
136                },
137            )?;
138            Ok(RunResult::Success)
139        }
140
141        Commands::Gc { dry_run, retention } => {
142            let storage = storage_factory().context("failed to open storage")?;
143            commands::run_gc(&storage, dry_run, retention)?;
144            Ok(RunResult::Success)
145        }
146
147        Commands::Otel(otel_cmd) => run_otel(otel_cmd, storage_factory),
148
149        Commands::Upgrade {
150            version,
151            yes,
152            dry_run,
153        } => {
154            commands::run_upgrade(commands::UpgradeOptions {
155                version,
156                yes,
157                dry_run,
158            })?;
159            Ok(RunResult::Success)
160        }
161
162        Commands::Uninstall {
163            confirm,
164            keep_data,
165            dry_run,
166        } => {
167            commands::run_uninstall(commands::UninstallOptions {
168                confirm,
169                keep_data,
170                dry_run,
171            })?;
172            Ok(RunResult::Success)
173        }
174    }
175}
176
177/// Handle ingest subcommands.
178fn run_ingest<S, F>(cmd: IngestCommands, storage_factory: F) -> Result<RunResult>
179where
180    S: Storage,
181    F: FnOnce() -> Result<S, StorageError>,
182{
183    match cmd {
184        IngestCommands::Event {
185            event_type,
186            json_payload,
187            framework,
188        } => {
189            // The event ingest is called by AI assistant hooks during tool execution.
190            // It must always exit 0 to avoid blocking the workflow, even if logging fails.
191            // Errors are printed to stderr for debugging but don't affect the exit code.
192            let result: Result<(), anyhow::Error> = (|| {
193                let storage = storage_factory().context("failed to open storage")?;
194                let log_result = commands::run_log(&storage, event_type, json_payload, framework)?;
195
196                // Scan transcript file after every hook (if path is available)
197                // This is cheap due to incremental position tracking
198                if let Some(ref path) = log_result.transcript_path {
199                    let scanner = TranscriptScanner::new(&storage, &log_result.machine_id);
200                    let _ = scanner.scan_file(std::path::Path::new(path));
201
202                    // Backfill initial prompt if the UserPromptSubmit hook didn't fire
203                    // (e.g., when Claude is started with an inline prompt like `claude 'prompt'`)
204                    let _ = scanner.backfill_initial_prompt(
205                        &log_result.session_id,
206                        std::path::Path::new(path),
207                    );
208                }
209                Ok(())
210            })();
211
212            if let Err(e) = result {
213                eprintln!("mi6: {:#}", e);
214            }
215            Ok(RunResult::ExitWithCode(0))
216        }
217
218        IngestCommands::Transcript { path } => {
219            let storage = storage_factory().context("failed to open storage")?;
220            commands::run_scan(&storage, &path)?;
221            Ok(RunResult::Success)
222        }
223
224        IngestCommands::Otel => {
225            let storage = storage_factory().context("failed to open storage")?;
226            commands::run_otel_ingest(&storage)?;
227            Ok(RunResult::Success)
228        }
229    }
230}
231
232/// Handle otel subcommands.
233fn run_otel<S, F>(cmd: OtelCommands, storage_factory: F) -> Result<RunResult>
234where
235    S: Storage,
236    F: FnOnce() -> Result<S, StorageError>,
237{
238    match cmd {
239        OtelCommands::Start { port, mode } => {
240            if commands::ensure_running(port, false, mode)? {
241                eprintln!("OTel server is running on port {}", port);
242            }
243            Ok(RunResult::Success)
244        }
245        OtelCommands::Stop { port } => {
246            commands::stop_server(port)?;
247            Ok(RunResult::Success)
248        }
249        OtelCommands::Restart { port, mode } => {
250            if commands::ensure_running(port, true, mode)? {
251                eprintln!("OTel server restarted on port {}", port);
252            }
253            Ok(RunResult::Success)
254        }
255        OtelCommands::Status { port } => {
256            commands::get_status(port)?;
257            Ok(RunResult::Success)
258        }
259        OtelCommands::Run { port, mode } => {
260            let storage = storage_factory().context("failed to open storage")?;
261            commands::run_server(&storage, port, mode)?;
262            Ok(RunResult::Success)
263        }
264    }
265}