pub mod args; pub mod autocorrect; pub mod commands; pub mod progress; pub mod render; pub mod robot; use clap::{Parser, Subcommand}; use std::io::IsTerminal; #[derive(Parser)] #[command(name = "lore")] #[command(version = env!("LORE_VERSION"), about = "Local GitLab data management with semantic search", long_about = None)] #[command(subcommand_required = false)] #[command(infer_subcommands = true)] #[command(after_long_help = "\x1b[1mEnvironment:\x1b[0m GITLAB_TOKEN GitLab personal access token (or name set in config) LORE_ROBOT Enable robot/JSON mode (non-empty, non-zero value) LORE_CONFIG_PATH Override config file location NO_COLOR Disable color output (any non-empty value) LORE_ICONS Override icon set: nerd, unicode, or ascii NERD_FONTS Enable Nerd Font icons when set to a non-empty value")] pub struct Cli { /// Path to config file #[arg(short = 'c', long, global = true, help = "Path to config file")] pub config: Option, /// Machine-readable JSON output (auto-enabled when piped) #[arg( long, global = true, env = "LORE_ROBOT", help = "Machine-readable JSON output (auto-enabled when piped)" )] pub robot: bool, /// JSON output (global shorthand) #[arg( short = 'J', long = "json", global = true, help = "JSON output (global shorthand)" )] pub json: bool, /// Color output: auto (default), always, or never #[arg(long, global = true, value_parser = ["auto", "always", "never"], default_value = "auto", help = "Color output: auto (default), always, or never")] pub color: String, /// Icon set: nerd (Nerd Fonts), unicode, or ascii #[arg(long, global = true, value_parser = ["nerd", "unicode", "ascii"], help = "Icon set: nerd (Nerd Fonts), unicode, or ascii")] pub icons: Option, /// Suppress non-essential output #[arg( short = 'q', long, global = true, overrides_with = "no_quiet", help = "Suppress non-essential output" )] pub quiet: bool, #[arg( long = "no-quiet", global = true, hide = true, overrides_with = "quiet" )] pub no_quiet: bool, /// Increase log verbosity (-v, -vv, -vvv) #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count, global = true, help = "Increase log verbosity (-v, -vv, -vvv)", overrides_with = "no_verbose")] pub verbose: u8, #[arg( long = "no-verbose", global = true, hide = true, overrides_with = "verbose" )] pub no_verbose: bool, /// Log format for stderr output: text (default) or json #[arg(long = "log-format", global = true, value_parser = ["text", "json"], default_value = "text", help = "Log format for stderr output: text (default) or json")] pub log_format: String, #[command(subcommand)] pub command: Option, } impl Cli { pub fn is_robot_mode(&self) -> bool { self.robot || self.json || !std::io::stdout().is_terminal() } /// Detect robot mode from environment before parsing succeeds. /// Used for structured error output when clap parsing fails. /// Also catches common agent typos like `-robot` and `--Robot`. pub fn detect_robot_mode_from_env() -> bool { let args: Vec = std::env::args().collect(); args.iter().any(|a| { a == "-J" || a.eq_ignore_ascii_case("--robot") || a.eq_ignore_ascii_case("-robot") || a.eq_ignore_ascii_case("--json") || a.eq_ignore_ascii_case("-json") }) || std::env::var("LORE_ROBOT") .ok() .is_some_and(|v| !v.is_empty() && v != "0" && v != "false") || !std::io::stdout().is_terminal() } } #[derive(Subcommand)] #[allow(clippy::large_enum_variant)] pub enum Commands { /// List or show issues #[command(visible_alias = "issue")] Issues(IssuesArgs), /// List or show merge requests #[command( visible_alias = "mr", alias = "merge-requests", alias = "merge-request" )] Mrs(MrsArgs), /// List notes from discussions #[command(visible_alias = "note")] Notes(NotesArgs), /// Ingest data from GitLab Ingest(IngestArgs), /// Count entities in local database Count(CountArgs), /// Show sync state #[command( visible_alias = "st", after_help = "\x1b[1mExamples:\x1b[0m lore status # Show last sync times per project lore --robot status # JSON output for automation" )] Status, /// Verify GitLab authentication #[command(after_help = "\x1b[1mExamples:\x1b[0m lore auth # Verify token and show user info lore --robot auth # JSON output for automation")] Auth, /// Check environment health #[command(after_help = "\x1b[1mExamples:\x1b[0m lore doctor # Check config, token, database, Ollama lore --robot doctor # JSON output for automation")] Doctor, /// Show version information Version, /// Initialize configuration and database #[command(after_help = "\x1b[1mExamples:\x1b[0m lore init # Interactive setup lore init --refresh # Register projects from existing config lore init --force # Overwrite existing config lore --robot init --gitlab-url https://gitlab.com \\ --token-env-var GITLAB_TOKEN --projects group/repo # Non-interactive setup")] Init { /// Re-read config and register any new projects in the database #[arg(long, conflicts_with = "force")] refresh: bool, /// Skip overwrite confirmation #[arg(short = 'f', long)] force: bool, /// Fail if prompts would be shown #[arg(long)] non_interactive: bool, /// GitLab base URL (required in robot mode) #[arg(long)] gitlab_url: Option, /// Environment variable name holding GitLab token (required in robot mode) #[arg(long)] token_env_var: Option, /// Comma-separated project paths (required in robot mode) #[arg(long)] projects: Option, /// Default project path (used when -p is omitted) #[arg(long)] default_project: Option, }, /// Back up local database (not yet implemented) #[command(hide = true)] Backup, /// Reset local database (not yet implemented) #[command(hide = true)] Reset { /// Skip confirmation prompt #[arg(short = 'y', long)] yes: bool, }, /// Search indexed documents #[command(visible_alias = "find", alias = "query")] Search(SearchArgs), /// Show document and index statistics #[command(visible_alias = "stat")] Stats(StatsArgs), /// Generate searchable documents from ingested data #[command(name = "generate-docs")] GenerateDocs(GenerateDocsArgs), /// Generate vector embeddings for documents via Ollama Embed(EmbedArgs), /// Run full sync pipeline: ingest -> generate-docs -> embed Sync(SyncArgs), /// Run pending database migrations #[command(after_help = "\x1b[1mExamples:\x1b[0m lore migrate # Apply pending migrations lore --robot migrate # JSON output for automation")] Migrate, /// Quick health check: config, database, schema version #[command(after_help = "\x1b[1mExamples:\x1b[0m lore health # Quick pre-flight check (exit 0 = healthy) lore --robot health # JSON output for automation")] Health, /// Machine-readable command manifest for agent self-discovery #[command(name = "robot-docs")] RobotDocs { /// Omit response_schema from output (~60% smaller) #[arg(long)] brief: bool, }, /// Generate shell completions #[command(long_about = "Generate shell completions for lore.\n\n\ Installation:\n \ bash: lore completions bash > ~/.local/share/bash-completion/completions/lore\n \ zsh: lore completions zsh > ~/.zfunc/_lore && echo 'fpath+=~/.zfunc' >> ~/.zshrc\n \ fish: lore completions fish > ~/.config/fish/completions/lore.fish\n \ pwsh: lore completions powershell >> $PROFILE")] Completions { /// Shell to generate completions for #[arg(value_parser = ["bash", "zsh", "fish", "powershell"])] shell: String, }, /// Show a chronological timeline of events matching a query Timeline(TimelineArgs), /// People intelligence: experts, workload, active discussions, overlap Who(WhoArgs), /// Personal work dashboard: open issues, authored/reviewing MRs, activity Me(MeArgs), /// Show MRs that touched a file, with linked discussions #[command(name = "file-history")] FileHistory(FileHistoryArgs), /// Trace why code was introduced: file -> MR -> issue -> discussion Trace(TraceArgs), /// Auto-generate a structured narrative of an issue or MR #[command(after_help = "\x1b[1mExamples:\x1b[0m lore explain issues 42 # Narrative for issue #42 lore explain mrs 99 -p group/repo # Narrative for MR !99 in specific project lore -J explain issues 42 # JSON output for automation lore explain issues 42 --sections key_decisions,open_threads # Specific sections only lore explain issues 42 --since 30d # Narrative scoped to last 30 days lore explain issues 42 --no-timeline # Skip timeline (faster)")] Explain { /// Entity type: "issues" or "mrs" (singular forms also accepted) #[arg(value_parser = ["issues", "mrs", "issue", "mr"])] entity_type: String, /// Entity IID iid: i64, /// Scope to project (fuzzy match) #[arg(short, long)] project: Option, /// Select specific sections (comma-separated) /// Valid: entity, description, key_decisions, activity, open_threads, related, timeline #[arg(long, value_delimiter = ',', help_heading = "Output")] sections: Option>, /// Skip timeline excerpt (faster execution) #[arg(long, help_heading = "Output")] no_timeline: bool, /// Maximum key decisions to include #[arg(long, default_value = "10", help_heading = "Output")] max_decisions: usize, /// Time scope for events/notes (e.g. 7d, 2w, 1m, or YYYY-MM-DD) #[arg(long, help_heading = "Filters")] since: Option, }, /// Detect discussion divergence from original intent #[command(after_help = "\x1b[1mExamples:\x1b[0m lore drift issues 42 # Check drift on issue #42 lore drift issues 42 --threshold 0.3 # Custom similarity threshold lore --robot drift issues 42 -p group/repo # JSON output, scoped to project")] Drift { /// Entity type (currently only "issues" supported) #[arg(value_parser = ["issues"])] entity_type: String, /// Entity IID iid: i64, /// Similarity threshold for drift detection (0.0-1.0) #[arg(long, default_value = "0.4")] threshold: f32, /// Scope to project (fuzzy match) #[arg(short, long)] project: Option, }, /// Find semantically related entities via vector search #[command(after_help = "\x1b[1mExamples:\x1b[0m lore related issues 42 # Find entities related to issue #42 lore related mrs 99 -p group/repo # Related to MR #99 in specific project lore related 'authentication flow' # Find entities matching free text query lore --robot related issues 42 -n 5 # JSON output, limit 5 results")] Related { /// Entity type (issues, mrs) or free text query query_or_type: String, /// Entity IID (required when first arg is entity type) iid: Option, /// Maximum results #[arg(short = 'n', long, default_value = "10")] limit: usize, /// Scope to project (fuzzy match) #[arg(short, long)] project: Option, }, /// Manage cron-based automatic syncing #[command(after_help = "\x1b[1mExamples:\x1b[0m lore cron install # Install cron job (every 8 minutes) lore cron install --interval 15 # Custom interval lore cron status # Check if cron is installed lore cron uninstall # Remove cron job")] Cron(CronArgs), /// Manage stored GitLab token #[command(after_help = "\x1b[1mExamples:\x1b[0m lore token set # Interactive token entry + validation lore token set --token glpat-xxx # Non-interactive token storage echo glpat-xxx | lore token set # Pipe token from stdin lore token show # Show token (masked) lore token show --unmask # Show full token")] Token(TokenArgs), #[command(hide = true)] List { #[arg(value_parser = ["issues", "mrs"])] entity: String, #[arg(long, default_value = "50")] limit: usize, #[arg(long)] project: Option, #[arg(long)] state: Option, #[arg(long)] author: Option, #[arg(long)] assignee: Option, #[arg(long)] label: Option>, #[arg(long)] milestone: Option, #[arg(long)] since: Option, #[arg(long)] due_before: Option, #[arg(long)] has_due_date: bool, #[arg(long, value_parser = ["updated", "created", "iid"], default_value = "updated")] sort: String, #[arg(long, value_parser = ["desc", "asc"], default_value = "desc")] order: String, #[arg(long)] open: bool, #[arg(long, conflicts_with = "no_draft")] draft: bool, #[arg(long, conflicts_with = "draft")] no_draft: bool, #[arg(long)] reviewer: Option, #[arg(long)] target_branch: Option, #[arg(long)] source_branch: Option, }, #[command(hide = true, name = "auth-test")] AuthTest, #[command(hide = true, name = "sync-status")] SyncStatus, } pub use args::{ CountArgs, CronAction, CronArgs, EmbedArgs, FileHistoryArgs, GenerateDocsArgs, IngestArgs, IssuesArgs, MeArgs, MrsArgs, NotesArgs, SearchArgs, StatsArgs, SyncArgs, TimelineArgs, TokenAction, TokenArgs, TraceArgs, WhoArgs, };