pub mod autocorrect; pub mod commands; pub mod progress; pub mod render; pub mod robot; use clap::{Args, 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 --force # Overwrite existing config lore --robot init --gitlab-url https://gitlab.com \\ --token-env-var GITLAB_TOKEN --projects group/repo # Non-interactive setup")] Init { /// 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), /// 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, }, /// 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)] Show { #[arg(value_parser = ["issue", "mr"])] entity: String, iid: i64, #[arg(long)] project: Option, }, #[command(hide = true, name = "auth-test")] AuthTest, #[command(hide = true, name = "sync-status")] SyncStatus, } #[derive(Parser)] #[command(after_help = "\x1b[1mExamples:\x1b[0m lore issues -n 10 # List 10 most recently updated issues lore issues -s opened -l bug # Open issues labeled 'bug' lore issues 42 -p group/repo # Show issue #42 in a specific project lore issues --since 7d -a jsmith # Issues updated in last 7 days by jsmith")] pub struct IssuesArgs { /// Issue IID (omit to list, provide to show details) pub iid: Option, /// Maximum results #[arg( short = 'n', long = "limit", default_value = "50", help_heading = "Output" )] pub limit: usize, /// Select output fields (comma-separated, or 'minimal' preset: iid,title,state,updated_at_iso) #[arg(long, help_heading = "Output", value_delimiter = ',')] pub fields: Option>, /// Filter by state (opened, closed, all) #[arg(short = 's', long, help_heading = "Filters", value_parser = ["opened", "closed", "all"])] pub state: Option, /// Filter by project path #[arg(short = 'p', long, help_heading = "Filters")] pub project: Option, /// Filter by author username #[arg(short = 'a', long, help_heading = "Filters")] pub author: Option, /// Filter by assignee username #[arg(short = 'A', long, help_heading = "Filters")] pub assignee: Option, /// Filter by label (repeatable, AND logic) #[arg(short = 'l', long, help_heading = "Filters")] pub label: Option>, /// Filter by milestone title #[arg(short = 'm', long, help_heading = "Filters")] pub milestone: Option, /// Filter by work-item status name (repeatable, OR logic) #[arg(long, help_heading = "Filters")] pub status: Vec, /// Filter by time (7d, 2w, 1m, or YYYY-MM-DD) #[arg(long, help_heading = "Filters")] pub since: Option, /// Filter by due date (before this date, YYYY-MM-DD) #[arg(long = "due-before", help_heading = "Filters")] pub due_before: Option, /// Show only issues with a due date #[arg( long = "has-due", help_heading = "Filters", overrides_with = "no_has_due" )] pub has_due: bool, #[arg(long = "no-has-due", hide = true, overrides_with = "has_due")] pub no_has_due: bool, /// Sort field (updated, created, iid) #[arg(long, value_parser = ["updated", "created", "iid"], default_value = "updated", help_heading = "Sorting")] pub sort: String, /// Sort ascending (default: descending) #[arg(long, help_heading = "Sorting", overrides_with = "no_asc")] pub asc: bool, #[arg(long = "no-asc", hide = true, overrides_with = "asc")] pub no_asc: bool, /// Open first matching item in browser #[arg( short = 'o', long, help_heading = "Actions", overrides_with = "no_open" )] pub open: bool, #[arg(long = "no-open", hide = true, overrides_with = "open")] pub no_open: bool, } #[derive(Parser)] #[command(after_help = "\x1b[1mExamples:\x1b[0m lore mrs -s opened # List open merge requests lore mrs -s merged --since 2w # MRs merged in the last 2 weeks lore mrs 99 -p group/repo # Show MR !99 in a specific project lore mrs -D --reviewer jsmith # Non-draft MRs reviewed by jsmith")] pub struct MrsArgs { /// MR IID (omit to list, provide to show details) pub iid: Option, /// Maximum results #[arg( short = 'n', long = "limit", default_value = "50", help_heading = "Output" )] pub limit: usize, /// Select output fields (comma-separated, or 'minimal' preset: iid,title,state,updated_at_iso) #[arg(long, help_heading = "Output", value_delimiter = ',')] pub fields: Option>, /// Filter by state (opened, merged, closed, locked, all) #[arg(short = 's', long, help_heading = "Filters", value_parser = ["opened", "merged", "closed", "locked", "all"])] pub state: Option, /// Filter by project path #[arg(short = 'p', long, help_heading = "Filters")] pub project: Option, /// Filter by author username #[arg(short = 'a', long, help_heading = "Filters")] pub author: Option, /// Filter by assignee username #[arg(short = 'A', long, help_heading = "Filters")] pub assignee: Option, /// Filter by reviewer username #[arg(short = 'r', long, help_heading = "Filters")] pub reviewer: Option, /// Filter by label (repeatable, AND logic) #[arg(short = 'l', long, help_heading = "Filters")] pub label: Option>, /// Filter by time (7d, 2w, 1m, or YYYY-MM-DD) #[arg(long, help_heading = "Filters")] pub since: Option, /// Show only draft MRs #[arg( short = 'd', long, conflicts_with = "no_draft", help_heading = "Filters" )] pub draft: bool, /// Exclude draft MRs #[arg( short = 'D', long = "no-draft", conflicts_with = "draft", help_heading = "Filters" )] pub no_draft: bool, /// Filter by target branch #[arg(long, help_heading = "Filters")] pub target: Option, /// Filter by source branch #[arg(long, help_heading = "Filters")] pub source: Option, /// Sort field (updated, created, iid) #[arg(long, value_parser = ["updated", "created", "iid"], default_value = "updated", help_heading = "Sorting")] pub sort: String, /// Sort ascending (default: descending) #[arg(long, help_heading = "Sorting", overrides_with = "no_asc")] pub asc: bool, #[arg(long = "no-asc", hide = true, overrides_with = "asc")] pub no_asc: bool, /// Open first matching item in browser #[arg( short = 'o', long, help_heading = "Actions", overrides_with = "no_open" )] pub open: bool, #[arg(long = "no-open", hide = true, overrides_with = "open")] pub no_open: bool, } #[derive(Parser)] #[command(after_help = "\x1b[1mExamples:\x1b[0m lore notes # List 50 most recent notes lore notes --author alice --since 7d # Notes by alice in last 7 days lore notes --for-issue 42 -p group/repo # Notes on issue #42 lore notes --path src/ --resolution unresolved # Unresolved diff notes in src/")] pub struct NotesArgs { /// Maximum results #[arg( short = 'n', long = "limit", default_value = "50", help_heading = "Output" )] pub limit: usize, /// Select output fields (comma-separated, or 'minimal' preset: id,author_username,body,created_at_iso) #[arg(long, help_heading = "Output", value_delimiter = ',')] pub fields: Option>, /// Filter by author username #[arg(short = 'a', long, help_heading = "Filters")] pub author: Option, /// Filter by note type (DiffNote, DiscussionNote) #[arg(long, help_heading = "Filters")] pub note_type: Option, /// Filter by body text (substring match) #[arg(long, help_heading = "Filters")] pub contains: Option, /// Filter by internal note ID #[arg(long, help_heading = "Filters")] pub note_id: Option, /// Filter by GitLab note ID #[arg(long, help_heading = "Filters")] pub gitlab_note_id: Option, /// Filter by discussion ID #[arg(long, help_heading = "Filters")] pub discussion_id: Option, /// Include system notes (excluded by default) #[arg(long, help_heading = "Filters")] pub include_system: bool, /// Filter to notes on a specific issue IID (requires --project or default_project) #[arg(long, conflicts_with = "for_mr", help_heading = "Filters")] pub for_issue: Option, /// Filter to notes on a specific MR IID (requires --project or default_project) #[arg(long, conflicts_with = "for_issue", help_heading = "Filters")] pub for_mr: Option, /// Filter by project path #[arg(short = 'p', long, help_heading = "Filters")] pub project: Option, /// Filter by time (7d, 2w, 1m, or YYYY-MM-DD) #[arg(long, help_heading = "Filters")] pub since: Option, /// Filter until date (YYYY-MM-DD, inclusive end-of-day) #[arg(long, help_heading = "Filters")] pub until: Option, /// Filter by file path (exact match or prefix with trailing /) #[arg(long, help_heading = "Filters")] pub path: Option, /// Filter by resolution status (any, unresolved, resolved) #[arg( long, value_parser = ["any", "unresolved", "resolved"], help_heading = "Filters" )] pub resolution: Option, /// Sort field (created, updated) #[arg( long, value_parser = ["created", "updated"], default_value = "created", help_heading = "Sorting" )] pub sort: String, /// Sort ascending (default: descending) #[arg(long, help_heading = "Sorting")] pub asc: bool, /// Open first matching item in browser #[arg(long, help_heading = "Actions")] pub open: bool, } #[derive(Parser)] pub struct IngestArgs { /// Entity to ingest (issues, mrs). Omit to ingest everything #[arg(value_parser = ["issues", "mrs"])] pub entity: Option, /// Filter to single project #[arg(short = 'p', long)] pub project: Option, /// Override stale sync lock #[arg(short = 'f', long, overrides_with = "no_force")] pub force: bool, #[arg(long = "no-force", hide = true, overrides_with = "force")] pub no_force: bool, /// Full re-sync: reset cursors and fetch all data from scratch #[arg(long, overrides_with = "no_full")] pub full: bool, #[arg(long = "no-full", hide = true, overrides_with = "full")] pub no_full: bool, /// Preview what would be synced without making changes #[arg(long, overrides_with = "no_dry_run")] pub dry_run: bool, #[arg(long = "no-dry-run", hide = true, overrides_with = "dry_run")] pub no_dry_run: bool, } #[derive(Parser)] #[command(after_help = "\x1b[1mExamples:\x1b[0m lore stats # Show document and index statistics lore stats --check # Run integrity checks lore stats --repair --dry-run # Preview what repair would fix lore --robot stats # JSON output for automation")] pub struct StatsArgs { /// Run integrity checks #[arg(long, overrides_with = "no_check")] pub check: bool, #[arg(long = "no-check", hide = true, overrides_with = "check")] pub no_check: bool, /// Repair integrity issues (auto-enables --check) #[arg(long)] pub repair: bool, /// Preview what would be repaired without making changes (requires --repair) #[arg(long, overrides_with = "no_dry_run")] pub dry_run: bool, #[arg(long = "no-dry-run", hide = true, overrides_with = "dry_run")] pub no_dry_run: bool, } #[derive(Parser)] #[command(after_help = "\x1b[1mExamples:\x1b[0m lore search 'authentication bug' # Hybrid search (default) lore search 'deploy' --mode lexical --type mr # Lexical search, MRs only lore search 'API rate limit' --since 30d # Recent results only lore search 'config' -p group/repo --explain # With ranking explanation")] pub struct SearchArgs { /// Search query string pub query: String, /// Search mode (lexical, hybrid, semantic) #[arg(long, default_value = "hybrid", value_parser = ["lexical", "hybrid", "semantic"], help_heading = "Mode")] pub mode: String, /// Filter by source type (issue, mr, discussion, note) #[arg(long = "type", value_name = "TYPE", value_parser = ["issue", "mr", "discussion", "note"], help_heading = "Filters")] pub source_type: Option, /// Filter by author username #[arg(long, help_heading = "Filters")] pub author: Option, /// Filter by project path #[arg(short = 'p', long, help_heading = "Filters")] pub project: Option, /// Filter by label (repeatable, AND logic) #[arg(long, action = clap::ArgAction::Append, help_heading = "Filters")] pub label: Vec, /// Filter by file path (trailing / for prefix match) #[arg(long, help_heading = "Filters")] pub path: Option, /// Filter by created since (7d, 2w, or YYYY-MM-DD) #[arg(long, help_heading = "Filters")] pub since: Option, /// Filter by updated since (7d, 2w, or YYYY-MM-DD) #[arg(long = "updated-since", help_heading = "Filters")] pub updated_since: Option, /// Maximum results (default 20, max 100) #[arg( short = 'n', long = "limit", default_value = "20", help_heading = "Output" )] pub limit: usize, /// Select output fields (comma-separated, or 'minimal' preset: document_id,title,source_type,score) #[arg(long, help_heading = "Output", value_delimiter = ',')] pub fields: Option>, /// Show ranking explanation per result #[arg(long, help_heading = "Output", overrides_with = "no_explain")] pub explain: bool, #[arg(long = "no-explain", hide = true, overrides_with = "explain")] pub no_explain: bool, /// FTS query mode: safe (default) or raw #[arg(long = "fts-mode", default_value = "safe", value_parser = ["safe", "raw"], help_heading = "Mode")] pub fts_mode: String, } #[derive(Parser)] #[command(after_help = "\x1b[1mExamples:\x1b[0m lore generate-docs # Generate docs for dirty entities lore generate-docs --full # Full rebuild of all documents lore generate-docs --full -p group/repo # Full rebuild for one project")] pub struct GenerateDocsArgs { /// Full rebuild: seed all entities into dirty queue, then drain #[arg(long)] pub full: bool, /// Filter to single project #[arg(short = 'p', long)] pub project: Option, } #[derive(Parser)] #[command(after_help = "\x1b[1mExamples:\x1b[0m lore sync # Full pipeline: ingest + docs + embed lore sync --no-embed # Skip embedding step lore sync --no-status # Skip work-item status enrichment lore sync --full --force # Full re-sync, override stale lock lore sync --dry-run # Preview what would change lore sync --issue 42 -p group/repo # Surgically sync one issue lore sync --mr 10 --mr 20 -p g/r # Surgically sync two MRs")] pub struct SyncArgs { /// Reset cursors, fetch everything #[arg(long, overrides_with = "no_full")] pub full: bool, #[arg(long = "no-full", hide = true, overrides_with = "full")] pub no_full: bool, /// Override stale lock #[arg(long, overrides_with = "no_force")] pub force: bool, #[arg(long = "no-force", hide = true, overrides_with = "force")] pub no_force: bool, /// Skip embedding step #[arg(long)] pub no_embed: bool, /// Skip document regeneration #[arg(long)] pub no_docs: bool, /// Skip resource event fetching (overrides config) #[arg(long = "no-events")] pub no_events: bool, /// Skip MR file change fetching (overrides config) #[arg(long = "no-file-changes")] pub no_file_changes: bool, /// Skip work-item status enrichment via GraphQL (overrides config) #[arg(long = "no-status")] pub no_status: bool, /// Preview what would be synced without making changes #[arg(long, overrides_with = "no_dry_run")] pub dry_run: bool, #[arg(long = "no-dry-run", hide = true, overrides_with = "dry_run")] pub no_dry_run: bool, /// Show detailed timing breakdown for sync stages #[arg(short = 't', long = "timings")] pub timings: bool, /// Acquire file lock before syncing (skip if another sync is running) #[arg(long)] pub lock: bool, /// Surgically sync specific issues by IID (repeatable, must be positive) #[arg(long, value_parser = clap::value_parser!(u64).range(1..), action = clap::ArgAction::Append)] pub issue: Vec, /// Surgically sync specific merge requests by IID (repeatable, must be positive) #[arg(long, value_parser = clap::value_parser!(u64).range(1..), action = clap::ArgAction::Append)] pub mr: Vec, /// Scope to a single project (required when --issue or --mr is used) #[arg(short = 'p', long)] pub project: Option, /// Validate remote entities exist without DB writes (preflight only) #[arg(long)] pub preflight_only: bool, } #[derive(Parser)] #[command(after_help = "\x1b[1mExamples:\x1b[0m lore embed # Embed new/changed documents lore embed --full # Re-embed all documents from scratch lore embed --retry-failed # Retry previously failed embeddings")] pub struct EmbedArgs { /// Re-embed all documents (clears existing embeddings first) #[arg(long, overrides_with = "no_full")] pub full: bool, #[arg(long = "no-full", hide = true, overrides_with = "full")] pub no_full: bool, /// Retry previously failed embeddings #[arg(long, overrides_with = "no_retry_failed")] pub retry_failed: bool, #[arg(long = "no-retry-failed", hide = true, overrides_with = "retry_failed")] pub no_retry_failed: bool, } #[derive(Parser)] #[command(after_help = "\x1b[1mExamples:\x1b[0m lore timeline 'deployment' # Search-based seeding lore timeline issue:42 # Direct: issue #42 and related entities lore timeline i:42 # Shorthand for issue:42 lore timeline mr:99 # Direct: MR !99 and related entities lore timeline 'auth' --since 30d -p group/repo # Scoped to project and time lore timeline 'migration' --depth 2 # Deep cross-reference expansion lore timeline 'auth' --no-mentions # Only 'closes' and 'related' edges")] pub struct TimelineArgs { /// Search text or entity reference (issue:N, i:N, mr:N, m:N) pub query: String, /// Scope to a specific project (fuzzy match) #[arg(short = 'p', long, help_heading = "Filters")] pub project: Option, /// Only show events after this date (e.g. "6m", "2w", "2024-01-01") #[arg(long, help_heading = "Filters")] pub since: Option, /// Cross-reference expansion depth (0 = no expansion) #[arg(long, default_value = "1", help_heading = "Expansion")] pub depth: u32, /// Skip 'mentioned' edges during expansion (only follow 'closes' and 'related') #[arg(long = "no-mentions", help_heading = "Expansion")] pub no_mentions: bool, /// Maximum number of events to display #[arg( short = 'n', long = "limit", default_value = "100", help_heading = "Output" )] pub limit: usize, /// Select output fields (comma-separated, or 'minimal' preset: timestamp,type,entity_iid,detail) #[arg(long, help_heading = "Output", value_delimiter = ',')] pub fields: Option>, /// Maximum seed entities from search #[arg(long = "max-seeds", default_value = "10", help_heading = "Expansion")] pub max_seeds: usize, /// Maximum expanded entities via cross-references #[arg( long = "max-entities", default_value = "50", help_heading = "Expansion" )] pub max_entities: usize, /// Maximum evidence notes included #[arg( long = "max-evidence", default_value = "10", help_heading = "Expansion" )] pub max_evidence: usize, } #[derive(Parser)] #[command(after_help = "\x1b[1mExamples:\x1b[0m lore who src/features/auth/ # Who knows about this area? lore who @asmith # What is asmith working on? lore who @asmith --reviews # What review patterns does asmith have? lore who --active # What discussions need attention? lore who --overlap src/features/auth/ # Who else is touching these files? lore who --path README.md # Expert lookup for a root file lore who --path Makefile # Expert lookup for a dotless root file")] pub struct WhoArgs { /// Username or file path (path if contains /) pub target: Option, /// Force expert mode for a file/directory path. /// Root files (README.md, LICENSE, Makefile) are treated as exact matches. /// Use a trailing `/` to force directory-prefix matching. #[arg(long, help_heading = "Mode", conflicts_with_all = ["active", "overlap", "reviews"])] pub path: Option, /// Show active unresolved discussions #[arg(long, help_heading = "Mode", conflicts_with_all = ["target", "overlap", "reviews", "path"])] pub active: bool, /// Find users with MRs/notes touching this file path #[arg(long, help_heading = "Mode", conflicts_with_all = ["target", "active", "reviews", "path"])] pub overlap: Option, /// Show review pattern analysis (requires username target) #[arg(long, help_heading = "Mode", requires = "target", conflicts_with_all = ["active", "overlap", "path"])] pub reviews: bool, /// Time window (7d, 2w, 6m, YYYY-MM-DD). Default varies by mode. #[arg(long, help_heading = "Filters")] pub since: Option, /// Scope to a project (supports fuzzy matching) #[arg(short = 'p', long, help_heading = "Filters")] pub project: Option, /// Maximum results per section (1..=500); omit for unlimited #[arg( short = 'n', long = "limit", value_parser = clap::value_parser!(u16).range(1..=500), help_heading = "Output" )] pub limit: Option, /// Select output fields (comma-separated, or 'minimal' preset; varies by mode) #[arg(long, help_heading = "Output", value_delimiter = ',')] pub fields: Option>, /// Show per-MR detail breakdown (expert mode only) #[arg( long, help_heading = "Output", overrides_with = "no_detail", conflicts_with = "explain_score" )] pub detail: bool, #[arg(long = "no-detail", hide = true, overrides_with = "detail")] pub no_detail: bool, /// Score as if "now" is this date (ISO 8601 or duration like 30d). Expert mode only. #[arg(long = "as-of", help_heading = "Scoring")] pub as_of: Option, /// Show per-component score breakdown in output. Expert mode only. #[arg(long = "explain-score", help_heading = "Scoring")] pub explain_score: bool, /// Include bot users in results (normally excluded via scoring.excluded_usernames). #[arg(long = "include-bots", help_heading = "Scoring")] pub include_bots: bool, /// Include discussions on closed issues and merged/closed MRs #[arg(long, help_heading = "Filters")] pub include_closed: bool, /// Remove the default time window (query all history). Conflicts with --since. #[arg( long = "all-history", help_heading = "Filters", conflicts_with = "since" )] pub all_history: bool, } #[derive(Parser)] #[command(after_help = "\x1b[1mExamples:\x1b[0m lore me # Full dashboard (default project or all) lore me --issues # Issues section only lore me --mrs # MRs section only lore me --activity # Activity feed only lore me --all # All synced projects lore me --since 2d # Activity window (default: 30d) lore me --project group/repo # Scope to one project lore me --user jdoe # Override configured username")] pub struct MeArgs { /// Show open issues section #[arg(long, help_heading = "Sections")] pub issues: bool, /// Show authored + reviewing MRs section #[arg(long, help_heading = "Sections")] pub mrs: bool, /// Show activity feed section #[arg(long, help_heading = "Sections")] pub activity: bool, /// Activity window (e.g. 7d, 2w, 30d). Default: 30d. Only affects activity section. #[arg(long, help_heading = "Filters")] pub since: Option, /// Scope to a project (supports fuzzy matching) #[arg(short = 'p', long, help_heading = "Filters", conflicts_with = "all")] pub project: Option, /// Show all synced projects (overrides default_project) #[arg(long, help_heading = "Filters", conflicts_with = "project")] pub all: bool, /// Override configured username #[arg(long = "user", help_heading = "Filters")] pub user: Option, /// Select output fields (comma-separated, or 'minimal' preset) #[arg(long, help_heading = "Output", value_delimiter = ',')] pub fields: Option>, } impl MeArgs { /// Returns true if no section flags were passed (show all sections). pub fn show_all_sections(&self) -> bool { !self.issues && !self.mrs && !self.activity } } #[derive(Parser)] #[command(after_help = "\x1b[1mExamples:\x1b[0m lore file-history src/main.rs # MRs that touched this file lore file-history src/auth/ -p group/repo # Scoped to project lore file-history src/foo.rs --discussions # Include DiffNote snippets lore file-history src/bar.rs --no-follow-renames # Skip rename chain")] pub struct FileHistoryArgs { /// File path to trace history for pub path: String, /// Scope to a specific project (fuzzy match) #[arg(short = 'p', long, help_heading = "Filters")] pub project: Option, /// Include discussion snippets from DiffNotes on this file #[arg(long, help_heading = "Output")] pub discussions: bool, /// Disable rename chain resolution #[arg(long = "no-follow-renames", help_heading = "Filters")] pub no_follow_renames: bool, /// Only show merged MRs #[arg(long, help_heading = "Filters")] pub merged: bool, /// Maximum results #[arg( short = 'n', long = "limit", default_value = "50", help_heading = "Output" )] pub limit: usize, } #[derive(Parser)] #[command(after_help = "\x1b[1mExamples:\x1b[0m lore trace src/main.rs # Why was this file changed? lore trace src/auth/ -p group/repo # Scoped to project lore trace src/foo.rs --discussions # Include DiffNote context lore trace src/bar.rs:42 # Line hint (Tier 2 warning)")] pub struct TraceArgs { /// File path to trace (supports :line suffix for future Tier 2) pub path: String, /// Scope to a specific project (fuzzy match) #[arg(short = 'p', long, help_heading = "Filters")] pub project: Option, /// Include DiffNote discussion snippets #[arg(long, help_heading = "Output")] pub discussions: bool, /// Disable rename chain resolution #[arg(long = "no-follow-renames", help_heading = "Filters")] pub no_follow_renames: bool, /// Maximum trace chains to display #[arg( short = 'n', long = "limit", default_value = "20", help_heading = "Output" )] pub limit: usize, } #[derive(Parser)] #[command(after_help = "\x1b[1mExamples:\x1b[0m lore count issues # Total issues in local database lore count notes --for mr # Notes on merge requests only lore count discussions --for issue # Discussions on issues only")] pub struct CountArgs { /// Entity type to count (issues, mrs, discussions, notes, events) #[arg(value_parser = ["issues", "mrs", "discussions", "notes", "events"])] pub entity: String, /// Parent type filter: issue or mr (for discussions/notes) #[arg(short = 'f', long = "for", value_parser = ["issue", "mr"])] pub for_entity: Option, } #[derive(Parser)] pub struct CronArgs { #[command(subcommand)] pub action: CronAction, } #[derive(Subcommand)] pub enum CronAction { /// Install cron job for automatic syncing Install { /// Sync interval in minutes (default: 8) #[arg(long, default_value = "8")] interval: u32, }, /// Remove cron job Uninstall, /// Show current cron configuration Status, } #[derive(Args)] pub struct TokenArgs { #[command(subcommand)] pub action: TokenAction, } #[derive(Subcommand)] pub enum TokenAction { /// Store a GitLab token in the config file Set { /// Token value (reads from stdin if omitted in non-interactive mode) #[arg(long)] token: Option, }, /// Show the current token (masked by default) Show { /// Show the full unmasked token #[arg(long)] unmask: bool, }, }