Files
gitlore/src/cli/mod.rs
teernisse 69b16a51d9 feat(me): add --full flag to show untruncated note content
By default, the `me` command truncates note/comment bodies to 200
characters in both the activity feed and since-last-check inbox. This
is sensible for overview displays but loses context when you need to
see the full comment text.

The new `--full` flag disables truncation, returning complete note
bodies. This affects:

- Activity feed: Note summaries now show full content
- Since-last-check inbox: Both regular comments and @mentions show
  full bodies

Implementation:
- CLI: Added `--full` boolean arg under "Output" help heading
- queries.rs: `query_activity()` and `query_since_last_check()` now
  accept a `full_body: bool` parameter that controls SQL body
  selection (`n.body` vs `SUBSTR(n.body, 1, 200)`)
- mod.rs: Wired `args.full` through to both query functions
- Tests: Added `activity_full_body_flag` and
  `since_last_check_full_body_flag` tests that verify:
  - 300-char bodies truncated to 200 without --full
  - 300-char bodies preserved in full with --full

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 15:30:18 -05:00

1269 lines
42 KiB
Rust

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<String>,
/// 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<String>,
/// 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<Commands>,
}
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<String> = 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<String>,
/// Environment variable name holding GitLab token (required in robot mode)
#[arg(long)]
token_env_var: Option<String>,
/// Comma-separated project paths (required in robot mode)
#[arg(long)]
projects: Option<String>,
/// Default project path (used when -p is omitted)
#[arg(long)]
default_project: Option<String>,
},
/// 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<String>,
},
/// 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<i64>,
/// Maximum results
#[arg(short = 'n', long, default_value = "10")]
limit: usize,
/// Scope to project (fuzzy match)
#[arg(short, long)]
project: Option<String>,
},
/// 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<String>,
#[arg(long)]
state: Option<String>,
#[arg(long)]
author: Option<String>,
#[arg(long)]
assignee: Option<String>,
#[arg(long)]
label: Option<Vec<String>>,
#[arg(long)]
milestone: Option<String>,
#[arg(long)]
since: Option<String>,
#[arg(long)]
due_before: Option<String>,
#[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<String>,
#[arg(long)]
target_branch: Option<String>,
#[arg(long)]
source_branch: Option<String>,
},
#[command(hide = true)]
Show {
#[arg(value_parser = ["issue", "mr"])]
entity: String,
iid: i64,
#[arg(long)]
project: Option<String>,
},
#[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<i64>,
/// 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<Vec<String>>,
/// Filter by state (opened, closed, all)
#[arg(short = 's', long, help_heading = "Filters", value_parser = ["opened", "closed", "all"])]
pub state: Option<String>,
/// Filter by project path
#[arg(short = 'p', long, help_heading = "Filters")]
pub project: Option<String>,
/// Filter by author username
#[arg(short = 'a', long, help_heading = "Filters")]
pub author: Option<String>,
/// Filter by assignee username
#[arg(short = 'A', long, help_heading = "Filters")]
pub assignee: Option<String>,
/// Filter by label (repeatable, AND logic)
#[arg(short = 'l', long, help_heading = "Filters")]
pub label: Option<Vec<String>>,
/// Filter by milestone title
#[arg(short = 'm', long, help_heading = "Filters")]
pub milestone: Option<String>,
/// Filter by work-item status name (repeatable, OR logic)
#[arg(long, help_heading = "Filters")]
pub status: Vec<String>,
/// Filter by time (7d, 2w, 1m, or YYYY-MM-DD)
#[arg(long, help_heading = "Filters")]
pub since: Option<String>,
/// Filter by due date (before this date, YYYY-MM-DD)
#[arg(long = "due-before", help_heading = "Filters")]
pub due_before: Option<String>,
/// 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<i64>,
/// 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<Vec<String>>,
/// 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<String>,
/// Filter by project path
#[arg(short = 'p', long, help_heading = "Filters")]
pub project: Option<String>,
/// Filter by author username
#[arg(short = 'a', long, help_heading = "Filters")]
pub author: Option<String>,
/// Filter by assignee username
#[arg(short = 'A', long, help_heading = "Filters")]
pub assignee: Option<String>,
/// Filter by reviewer username
#[arg(short = 'r', long, help_heading = "Filters")]
pub reviewer: Option<String>,
/// Filter by label (repeatable, AND logic)
#[arg(short = 'l', long, help_heading = "Filters")]
pub label: Option<Vec<String>>,
/// Filter by time (7d, 2w, 1m, or YYYY-MM-DD)
#[arg(long, help_heading = "Filters")]
pub since: Option<String>,
/// 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<String>,
/// Filter by source branch
#[arg(long, help_heading = "Filters")]
pub source: Option<String>,
/// 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<Vec<String>>,
/// Filter by author username
#[arg(short = 'a', long, help_heading = "Filters")]
pub author: Option<String>,
/// Filter by note type (DiffNote, DiscussionNote)
#[arg(long, help_heading = "Filters")]
pub note_type: Option<String>,
/// Filter by body text (substring match)
#[arg(long, help_heading = "Filters")]
pub contains: Option<String>,
/// Filter by internal note ID
#[arg(long, help_heading = "Filters")]
pub note_id: Option<i64>,
/// Filter by GitLab note ID
#[arg(long, help_heading = "Filters")]
pub gitlab_note_id: Option<i64>,
/// Filter by discussion ID
#[arg(long, help_heading = "Filters")]
pub discussion_id: Option<String>,
/// 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<i64>,
/// 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<i64>,
/// Filter by project path
#[arg(short = 'p', long, help_heading = "Filters")]
pub project: Option<String>,
/// Filter by time (7d, 2w, 1m, or YYYY-MM-DD)
#[arg(long, help_heading = "Filters")]
pub since: Option<String>,
/// Filter until date (YYYY-MM-DD, inclusive end-of-day)
#[arg(long, help_heading = "Filters")]
pub until: Option<String>,
/// Filter by file path (exact match or prefix with trailing /)
#[arg(long, help_heading = "Filters")]
pub path: Option<String>,
/// Filter by resolution status (any, unresolved, resolved)
#[arg(
long,
value_parser = ["any", "unresolved", "resolved"],
help_heading = "Filters"
)]
pub resolution: Option<String>,
/// 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<String>,
/// Filter to single project
#[arg(short = 'p', long)]
pub project: Option<String>,
/// 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<String>,
/// Filter by author username
#[arg(long, help_heading = "Filters")]
pub author: Option<String>,
/// Filter by project path
#[arg(short = 'p', long, help_heading = "Filters")]
pub project: Option<String>,
/// Filter by label (repeatable, AND logic)
#[arg(long, action = clap::ArgAction::Append, help_heading = "Filters")]
pub label: Vec<String>,
/// Filter by file path (trailing / for prefix match)
#[arg(long, help_heading = "Filters")]
pub path: Option<String>,
/// Filter by created since (7d, 2w, or YYYY-MM-DD)
#[arg(long, help_heading = "Filters")]
pub since: Option<String>,
/// Filter by updated since (7d, 2w, or YYYY-MM-DD)
#[arg(long = "updated-since", help_heading = "Filters")]
pub updated_since: Option<String>,
/// 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<Vec<String>>,
/// 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<String>,
}
#[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<u64>,
/// 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<u64>,
/// Scope to a single project (required when --issue or --mr is used)
#[arg(short = 'p', long)]
pub project: Option<String>,
/// 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<String>,
/// Only show events after this date (e.g. "6m", "2w", "2024-01-01")
#[arg(long, help_heading = "Filters")]
pub since: Option<String>,
/// 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<Vec<String>>,
/// 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<String>,
/// 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<String>,
/// 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<String>,
/// 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<String>,
/// Scope to a project (supports fuzzy matching)
#[arg(short = 'p', long, help_heading = "Filters")]
pub project: Option<String>,
/// 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<u16>,
/// Select output fields (comma-separated, or 'minimal' preset; varies by mode)
#[arg(long, help_heading = "Output", value_delimiter = ',')]
pub fields: Option<Vec<String>>,
/// 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<String>,
/// 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<String>,
/// Scope to a project (supports fuzzy matching)
#[arg(short = 'p', long, help_heading = "Filters", conflicts_with = "all")]
pub project: Option<String>,
/// 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<String>,
/// Select output fields (comma-separated, or 'minimal' preset)
#[arg(long, help_heading = "Output", value_delimiter = ',')]
pub fields: Option<Vec<String>>,
/// Show full note content (no truncation)
#[arg(long, help_heading = "Output")]
pub full: bool,
/// Reset the since-last-check cursor (next run shows no new events)
#[arg(long, help_heading = "Output")]
pub reset_cursor: bool,
}
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<String>,
/// 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<String>,
/// 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<String>,
}
#[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<String>,
},
/// Show the current token (masked by default)
Show {
/// Show the full unmasked token
#[arg(long)]
unmask: bool,
},
}