Adds the full explain command with 7 output sections: entity summary, description, key decisions (heuristic event-note correlation), activity summary, open threads, related entities (closing MRs, cross-references), and timeline excerpt (reuses existing pipeline). Supports --sections filtering, --since time scoping, --no-timeline, --max-decisions, and robot mode JSON output. Closes: bd-2i3z, bd-a3j8, bd-wb0b, bd-3q5e, bd-nj7f, bd-9lbr
434 lines
15 KiB
Rust
434 lines
15 KiB
Rust
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<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),
|
|
|
|
/// 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<String>,
|
|
|
|
/// Select specific sections (comma-separated)
|
|
/// Valid: entity, description, key_decisions, activity, open_threads, related, timeline
|
|
#[arg(long, value_delimiter = ',', help_heading = "Output")]
|
|
sections: Option<Vec<String>>,
|
|
|
|
/// 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<String>,
|
|
},
|
|
|
|
/// 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, 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,
|
|
};
|