diff --git a/src/cli/autocorrect.rs b/src/cli/autocorrect.rs index 2d779e0..c6a9c52 100644 --- a/src/cli/autocorrect.rs +++ b/src/cli/autocorrect.rs @@ -61,6 +61,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[ "--assignee", "--label", "--milestone", + "--status", "--since", "--due-before", "--has-due", @@ -134,6 +135,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[ "--since", "--updated-since", "--limit", + "--fields", "--explain", "--no-explain", "--fts-mode", @@ -162,6 +164,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[ "--depth", "--expand-mentions", "--limit", + "--fields", "--max-seeds", "--max-entities", "--max-evidence", @@ -177,6 +180,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[ "--since", "--project", "--limit", + "--fields", "--detail", "--no-detail", ], @@ -193,6 +197,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[ ), ("generate-docs", &["--full", "--project"]), ("completions", &[]), + ("robot-docs", &["--brief"]), ( "list", &[ diff --git a/src/cli/commands/list.rs b/src/cli/commands/list.rs index 1d1bcc2..2946008 100644 --- a/src/cli/commands/list.rs +++ b/src/cli/commands/list.rs @@ -19,6 +19,29 @@ fn colored_cell(content: impl std::fmt::Display, color: Color) -> Cell { } } +fn colored_cell_hex(content: &str, hex: Option<&str>) -> Cell { + if !console::colors_enabled() { + return Cell::new(content); + } + let Some(hex) = hex else { + return Cell::new(content); + }; + let hex = hex.trim_start_matches('#'); + if hex.len() != 6 { + return Cell::new(content); + } + let Ok(r) = u8::from_str_radix(&hex[0..2], 16) else { + return Cell::new(content); + }; + let Ok(g) = u8::from_str_radix(&hex[2..4], 16) else { + return Cell::new(content); + }; + let Ok(b) = u8::from_str_radix(&hex[4..6], 16) else { + return Cell::new(content); + }; + Cell::new(content).fg(Color::Rgb { r, g, b }) +} + #[derive(Debug, Serialize)] pub struct IssueListRow { pub iid: i64, @@ -34,6 +57,16 @@ pub struct IssueListRow { pub assignees: Vec, pub discussion_count: i64, pub unresolved_count: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub status_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub status_category: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub status_color: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub status_icon_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub status_synced_at: Option, } #[derive(Serialize)] @@ -51,6 +84,16 @@ pub struct IssueListRowJson { #[serde(skip_serializing_if = "Option::is_none")] pub web_url: Option, pub project_path: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub status_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub status_category: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub status_color: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub status_icon_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub status_synced_at_iso: Option, } impl From<&IssueListRow> for IssueListRowJson { @@ -68,6 +111,11 @@ impl From<&IssueListRow> for IssueListRowJson { updated_at_iso: ms_to_iso(row.updated_at), web_url: row.web_url.clone(), project_path: row.project_path.clone(), + status_name: row.status_name.clone(), + status_category: row.status_category.clone(), + status_color: row.status_color.clone(), + status_icon_name: row.status_icon_name.clone(), + status_synced_at_iso: row.status_synced_at.map(ms_to_iso), } } } @@ -194,6 +242,7 @@ pub struct ListFilters<'a> { pub since: Option<&'a str>, pub due_before: Option<&'a str>, pub has_due_date: bool, + pub statuses: &'a [String], pub sort: &'a str, pub order: &'a str, } @@ -291,6 +340,22 @@ fn query_issues(conn: &Connection, filters: &ListFilters) -> Result where_clauses.push("i.due_date IS NOT NULL"); } + let status_in_clause; + if filters.statuses.len() == 1 { + where_clauses.push("i.status_name = ? COLLATE NOCASE"); + params.push(Box::new(filters.statuses[0].clone())); + } else if filters.statuses.len() > 1 { + let placeholders: Vec<&str> = filters.statuses.iter().map(|_| "?").collect(); + status_in_clause = format!( + "i.status_name COLLATE NOCASE IN ({})", + placeholders.join(", ") + ); + where_clauses.push(&status_in_clause); + for s in filters.statuses { + params.push(Box::new(s.clone())); + } + } + let where_sql = if where_clauses.is_empty() { String::new() } else { @@ -338,7 +403,12 @@ fn query_issues(conn: &Connection, filters: &ListFilters) -> Result (SELECT COUNT(*) FROM discussions d WHERE d.issue_id = i.id) AS discussion_count, (SELECT COUNT(*) FROM discussions d - WHERE d.issue_id = i.id AND d.resolvable = 1 AND d.resolved = 0) AS unresolved_count + WHERE d.issue_id = i.id AND d.resolvable = 1 AND d.resolved = 0) AS unresolved_count, + i.status_name, + i.status_category, + i.status_color, + i.status_icon_name, + i.status_synced_at FROM issues i JOIN projects p ON i.project_id = p.id {where_sql} @@ -375,6 +445,11 @@ fn query_issues(conn: &Connection, filters: &ListFilters) -> Result assignees, discussion_count: row.get(10)?, unresolved_count: row.get(11)?, + status_name: row.get(12)?, + status_category: row.get(13)?, + status_color: row.get(14)?, + status_icon_name: row.get(15)?, + status_synced_at: row.get(16)?, }) })? .collect::, _>>()?; @@ -683,18 +758,27 @@ pub fn print_list_issues(result: &ListResult) { result.total_count ); + let has_any_status = result.issues.iter().any(|i| i.status_name.is_some()); + + let mut header = vec![ + Cell::new("IID").add_attribute(Attribute::Bold), + Cell::new("Title").add_attribute(Attribute::Bold), + Cell::new("State").add_attribute(Attribute::Bold), + ]; + if has_any_status { + header.push(Cell::new("Status").add_attribute(Attribute::Bold)); + } + header.extend([ + Cell::new("Assignee").add_attribute(Attribute::Bold), + Cell::new("Labels").add_attribute(Attribute::Bold), + Cell::new("Disc").add_attribute(Attribute::Bold), + Cell::new("Updated").add_attribute(Attribute::Bold), + ]); + let mut table = Table::new(); table .set_content_arrangement(ContentArrangement::Dynamic) - .set_header(vec![ - Cell::new("IID").add_attribute(Attribute::Bold), - Cell::new("Title").add_attribute(Attribute::Bold), - Cell::new("State").add_attribute(Attribute::Bold), - Cell::new("Assignee").add_attribute(Attribute::Bold), - Cell::new("Labels").add_attribute(Attribute::Bold), - Cell::new("Disc").add_attribute(Attribute::Bold), - Cell::new("Updated").add_attribute(Attribute::Bold), - ]); + .set_header(header); for issue in &result.issues { let title = truncate_with_ellipsis(&issue.title, 45); @@ -709,15 +793,28 @@ pub fn print_list_issues(result: &ListResult) { colored_cell(&issue.state, Color::DarkGrey) }; - table.add_row(vec![ + let mut row = vec![ colored_cell(format!("#{}", issue.iid), Color::Cyan), Cell::new(title), state_cell, + ]; + if has_any_status { + match &issue.status_name { + Some(status) => { + row.push(colored_cell_hex(status, issue.status_color.as_deref())); + } + None => { + row.push(Cell::new("")); + } + } + } + row.extend([ colored_cell(assignee, Color::Magenta), colored_cell(labels, Color::Yellow), Cell::new(discussions), colored_cell(relative_time, Color::DarkGrey), ]); + table.add_row(row); } println!("{table}"); diff --git a/src/cli/commands/search.rs b/src/cli/commands/search.rs index 98b3f27..56f358c 100644 --- a/src/cli/commands/search.rs +++ b/src/cli/commands/search.rs @@ -386,11 +386,20 @@ struct SearchMeta { elapsed_ms: u64, } -pub fn print_search_results_json(response: &SearchResponse, elapsed_ms: u64) { +pub fn print_search_results_json( + response: &SearchResponse, + elapsed_ms: u64, + fields: Option<&[String]>, +) { let output = SearchJsonOutput { ok: true, data: response, meta: SearchMeta { elapsed_ms }, }; - println!("{}", serde_json::to_string(&output).unwrap()); + let mut value = serde_json::to_value(&output).unwrap(); + if let Some(f) = fields { + let expanded = crate::cli::robot::expand_fields_preset(f, "search"); + crate::cli::robot::filter_fields(&mut value, "results", &expanded); + } + println!("{}", serde_json::to_string(&value).unwrap()); } diff --git a/src/cli/commands/show.rs b/src/cli/commands/show.rs index 54f0c0b..7087bc9 100644 --- a/src/cli/commands/show.rs +++ b/src/cli/commands/show.rs @@ -83,6 +83,11 @@ pub struct IssueDetail { pub milestone: Option, pub closing_merge_requests: Vec, pub discussions: Vec, + pub status_name: Option, + pub status_category: Option, + pub status_color: Option, + pub status_icon_name: Option, + pub status_synced_at: Option, } #[derive(Debug, Serialize)] @@ -134,6 +139,11 @@ pub fn run_show_issue( milestone: issue.milestone_title, closing_merge_requests: closing_mrs, discussions, + status_name: issue.status_name, + status_category: issue.status_category, + status_color: issue.status_color, + status_icon_name: issue.status_icon_name, + status_synced_at: issue.status_synced_at, }) } @@ -150,6 +160,11 @@ struct IssueRow { project_path: String, due_date: Option, milestone_title: Option, + status_name: Option, + status_category: Option, + status_color: Option, + status_icon_name: Option, + status_synced_at: Option, } fn find_issue(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Result { @@ -159,7 +174,9 @@ fn find_issue(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Resu ( "SELECT i.id, i.iid, i.title, i.description, i.state, i.author_username, i.created_at, i.updated_at, i.web_url, p.path_with_namespace, - i.due_date, i.milestone_title + i.due_date, i.milestone_title, + i.status_name, i.status_category, i.status_color, + i.status_icon_name, i.status_synced_at FROM issues i JOIN projects p ON i.project_id = p.id WHERE i.iid = ? AND i.project_id = ?", @@ -169,7 +186,9 @@ fn find_issue(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Resu None => ( "SELECT i.id, i.iid, i.title, i.description, i.state, i.author_username, i.created_at, i.updated_at, i.web_url, p.path_with_namespace, - i.due_date, i.milestone_title + i.due_date, i.milestone_title, + i.status_name, i.status_category, i.status_color, + i.status_icon_name, i.status_synced_at FROM issues i JOIN projects p ON i.project_id = p.id WHERE i.iid = ?", @@ -195,6 +214,11 @@ fn find_issue(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Resu project_path: row.get(9)?, due_date: row.get(10)?, milestone_title: row.get(11)?, + status_name: row.get(12)?, + status_category: row.get(13)?, + status_color: row.get(14)?, + status_icon_name: row.get(15)?, + status_synced_at: row.get(16)?, }) })? .collect::, _>>()?; @@ -603,6 +627,17 @@ pub fn print_show_issue(issue: &IssueDetail) { }; println!("State: {}", state_styled); + if let Some(status) = &issue.status_name { + let display = match &issue.status_category { + Some(cat) => format!("{status} ({})", cat.to_ascii_lowercase()), + None => status.clone(), + }; + println!( + "Status: {}", + style_with_hex(&display, issue.status_color.as_deref()) + ); + } + println!("Author: @{}", issue.author_username); if !issue.assignees.is_empty() { @@ -864,6 +899,32 @@ fn print_diff_position(pos: &DiffNotePosition) { } } +fn style_with_hex<'a>(text: &'a str, hex: Option<&str>) -> console::StyledObject<&'a str> { + let styled = console::style(text); + let Some(hex) = hex else { return styled }; + let hex = hex.trim_start_matches('#'); + if hex.len() != 6 { + return styled; + } + let Ok(r) = u8::from_str_radix(&hex[0..2], 16) else { + return styled; + }; + let Ok(g) = u8::from_str_radix(&hex[2..4], 16) else { + return styled; + }; + let Ok(b) = u8::from_str_radix(&hex[4..6], 16) else { + return styled; + }; + styled.color256(ansi256_from_rgb(r, g, b)) +} + +fn ansi256_from_rgb(r: u8, g: u8, b: u8) -> u8 { + let ri = (u16::from(r) * 5 + 127) / 255; + let gi = (u16::from(g) * 5 + 127) / 255; + let bi = (u16::from(b) * 5 + 127) / 255; + (16 + 36 * ri + 6 * gi + bi) as u8 +} + #[derive(Serialize)] pub struct IssueDetailJson { pub id: i64, @@ -882,6 +943,11 @@ pub struct IssueDetailJson { pub milestone: Option, pub closing_merge_requests: Vec, pub discussions: Vec, + pub status_name: Option, + pub status_category: Option, + pub status_color: Option, + pub status_icon_name: Option, + pub status_synced_at: Option, } #[derive(Serialize)] @@ -934,6 +1000,11 @@ impl From<&IssueDetail> for IssueDetailJson { }) .collect(), discussions: issue.discussions.iter().map(|d| d.into()).collect(), + status_name: issue.status_name.clone(), + status_category: issue.status_category.clone(), + status_color: issue.status_color.clone(), + status_icon_name: issue.status_icon_name.clone(), + status_synced_at: issue.status_synced_at.map(ms_to_iso), } } } @@ -1103,6 +1174,12 @@ mod tests { .unwrap(); } + #[test] + fn test_ansi256_from_rgb() { + assert_eq!(ansi256_from_rgb(0, 0, 0), 16); + assert_eq!(ansi256_from_rgb(255, 255, 255), 231); + } + #[test] fn test_get_issue_assignees_empty() { let conn = setup_test_db(); diff --git a/src/cli/commands/timeline.rs b/src/cli/commands/timeline.rs index 478a87b..d8fcf3b 100644 --- a/src/cli/commands/timeline.rs +++ b/src/cli/commands/timeline.rs @@ -252,6 +252,7 @@ pub fn print_timeline_json_with_meta( total_events_before_limit: usize, depth: u32, expand_mentions: bool, + fields: Option<&[String]>, ) { let output = TimelineJsonEnvelope { ok: true, @@ -268,10 +269,18 @@ pub fn print_timeline_json_with_meta( }, }; - match serde_json::to_string(&output) { - Ok(json) => println!("{json}"), - Err(e) => eprintln!("Error serializing timeline JSON: {e}"), + let mut value = match serde_json::to_value(&output) { + Ok(v) => v, + Err(e) => { + eprintln!("Error serializing timeline JSON: {e}"); + return; + } + }; + if let Some(f) = fields { + let expanded = crate::cli::robot::expand_fields_preset(f, "timeline"); + crate::cli::robot::filter_fields(&mut value, "events", &expanded); } + println!("{}", serde_json::to_string(&value).unwrap()); } #[derive(Serialize)] diff --git a/src/cli/commands/who.rs b/src/cli/commands/who.rs index af5f641..0f2ef8b 100644 --- a/src/cli/commands/who.rs +++ b/src/cli/commands/who.rs @@ -2201,12 +2201,28 @@ pub fn print_who_json(run: &WhoRun, args: &WhoArgs, elapsed_ms: u64) { meta: RobotMeta { elapsed_ms }, }; - println!( - "{}", - serde_json::to_string(&output).unwrap_or_else(|e| { - format!(r#"{{"ok":false,"error":{{"code":"INTERNAL_ERROR","message":"JSON serialization failed: {e}"}}}}"#) - }) - ); + let mut value = serde_json::to_value(&output).unwrap_or_else(|e| { + serde_json::json!({"ok":false,"error":{"code":"INTERNAL_ERROR","message":format!("JSON serialization failed: {e}")}}) + }); + + if let Some(f) = &args.fields { + let preset_key = format!("who_{mode}"); + let expanded = crate::cli::robot::expand_fields_preset(f, &preset_key); + // Each who mode uses a different array key; try all possible keys + for key in &[ + "experts", + "assigned_issues", + "authored_mrs", + "review_mrs", + "categories", + "discussions", + "users", + ] { + crate::cli::robot::filter_fields(&mut value, key, &expanded); + } + } + + println!("{}", serde_json::to_string(&value).unwrap()); } #[derive(Serialize)] @@ -2577,6 +2593,7 @@ mod tests { limit: 20, detail: false, no_detail: false, + fields: None, }) .unwrap(), WhoMode::Expert { .. } @@ -2595,6 +2612,7 @@ mod tests { limit: 20, detail: false, no_detail: false, + fields: None, }) .unwrap(), WhoMode::Workload { .. } @@ -2613,6 +2631,7 @@ mod tests { limit: 20, detail: false, no_detail: false, + fields: None, }) .unwrap(), WhoMode::Workload { .. } @@ -2631,6 +2650,7 @@ mod tests { limit: 20, detail: false, no_detail: false, + fields: None, }) .unwrap(), WhoMode::Reviews { .. } @@ -2649,6 +2669,7 @@ mod tests { limit: 20, detail: false, no_detail: false, + fields: None, }) .unwrap(), WhoMode::Expert { .. } @@ -2667,6 +2688,7 @@ mod tests { limit: 20, detail: false, no_detail: false, + fields: None, }) .unwrap(), WhoMode::Expert { .. } @@ -2686,6 +2708,7 @@ mod tests { limit: 20, detail: true, no_detail: false, + fields: None, }; let mode = resolve_mode(&args).unwrap(); let err = validate_mode_flags(&mode, &args).unwrap_err(); @@ -2709,6 +2732,7 @@ mod tests { limit: 20, detail: true, no_detail: false, + fields: None, }; let mode = resolve_mode(&args).unwrap(); assert!(validate_mode_flags(&mode, &args).is_ok()); diff --git a/src/cli/mod.rs b/src/cli/mod.rs index ed04919..923a57c 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -186,7 +186,11 @@ pub enum Commands { /// Machine-readable command manifest for agent self-discovery #[command(name = "robot-docs")] - RobotDocs, + 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\ @@ -315,6 +319,10 @@ pub struct IssuesArgs { #[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, @@ -563,6 +571,10 @@ pub struct SearchArgs { )] 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, @@ -682,6 +694,10 @@ pub struct TimelineArgs { )] 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, @@ -752,6 +768,10 @@ pub struct WhoArgs { )] pub limit: u16, + /// 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")] pub detail: bool, diff --git a/src/cli/robot.rs b/src/cli/robot.rs index 6335f92..619cca6 100644 --- a/src/cli/robot.rs +++ b/src/cli/robot.rs @@ -36,9 +36,48 @@ pub fn expand_fields_preset(fields: &[String], entity: &str) -> Vec { .iter() .map(|s| (*s).to_string()) .collect(), + "search" => ["document_id", "title", "source_type", "score"] + .iter() + .map(|s| (*s).to_string()) + .collect(), + "timeline" => ["timestamp", "type", "entity_iid", "detail"] + .iter() + .map(|s| (*s).to_string()) + .collect(), + "who_expert" => ["username", "score"] + .iter() + .map(|s| (*s).to_string()) + .collect(), + "who_workload" => ["iid", "title", "state"] + .iter() + .map(|s| (*s).to_string()) + .collect(), + "who_active" => ["entity_type", "iid", "title", "participants"] + .iter() + .map(|s| (*s).to_string()) + .collect(), + "who_overlap" => ["username", "touch_count"] + .iter() + .map(|s| (*s).to_string()) + .collect(), + "who_reviews" => ["name", "count", "percentage"] + .iter() + .map(|s| (*s).to_string()) + .collect(), _ => fields.to_vec(), } } else { fields.to_vec() } } + +/// Strip `response_schema` from every command entry for `--brief` mode. +pub fn strip_schemas(commands: &mut serde_json::Value) { + if let Some(map) = commands.as_object_mut() { + for (_cmd_name, cmd) in map.iter_mut() { + if let Some(obj) = cmd.as_object_mut() { + obj.remove("response_schema"); + } + } + } +} diff --git a/src/main.rs b/src/main.rs index dde285d..b518793 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,7 +24,7 @@ use lore::cli::commands::{ run_init, run_list_issues, run_list_mrs, run_search, run_show_issue, run_show_mr, run_stats, run_sync, run_sync_status, run_timeline, run_who, }; -use lore::cli::robot::RobotMeta; +use lore::cli::robot::{RobotMeta, strip_schemas}; use lore::cli::{ Cli, Commands, CountArgs, EmbedArgs, GenerateDocsArgs, IngestArgs, IssuesArgs, MrsArgs, SearchArgs, StatsArgs, SyncArgs, TimelineArgs, WhoArgs, @@ -162,7 +162,7 @@ async fn main() { // Phase 2: Handle no-args case - in robot mode, output robot-docs; otherwise show help None => { if robot_mode { - handle_robot_docs(robot_mode) + handle_robot_docs(robot_mode, false) } else { use clap::CommandFactory; let mut cmd = Cli::command(); @@ -224,7 +224,7 @@ async fn main() { Some(Commands::Reset { yes: _ }) => handle_reset(robot_mode), Some(Commands::Migrate) => handle_migrate(cli.config.as_deref(), robot_mode).await, Some(Commands::Health) => handle_health(cli.config.as_deref(), robot_mode).await, - Some(Commands::RobotDocs) => handle_robot_docs(robot_mode), + Some(Commands::RobotDocs { brief }) => handle_robot_docs(robot_mode, brief), Some(Commands::List { entity, @@ -703,6 +703,7 @@ fn handle_issues( since: args.since.as_deref(), due_before: args.due_before.as_deref(), has_due_date: has_due, + statuses: &args.status, sort: &args.sort, order, }; @@ -1697,6 +1698,7 @@ fn handle_timeline( result.total_events_before_limit, params.depth, params.expand_mentions, + args.fields.as_deref(), ); } else { print_timeline(&result); @@ -1740,7 +1742,7 @@ async fn handle_search( let elapsed_ms = start.elapsed().as_millis() as u64; if robot_mode { - print_search_results_json(&response, elapsed_ms); + print_search_results_json(&response, elapsed_ms, args.fields.as_deref()); } else { print_search_results(&response); } @@ -2038,7 +2040,7 @@ struct RobotDocsActivation { auto: String, } -fn handle_robot_docs(robot_mode: bool) -> Result<(), Box> { +fn handle_robot_docs(robot_mode: bool, brief: bool) -> Result<(), Box> { let version = env!("CARGO_PKG_VERSION").to_string(); let commands = serde_json::json!({ @@ -2105,7 +2107,7 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box> }, "issues": { "description": "List or show issues", - "flags": ["", "-n/--limit", "--fields ", "-s/--state", "-p/--project", "-a/--author", "-A/--assignee", "-l/--label", "-m/--milestone", "--since", "--due-before", "--has-due", "--no-has-due", "--sort", "--asc", "--no-asc", "-o/--open", "--no-open"], + "flags": ["", "-n/--limit", "--fields ", "-s/--state", "--status ", "-p/--project", "-a/--author", "-A/--assignee", "-l/--label", "-m/--milestone", "--since", "--due-before", "--has-due", "--no-has-due", "--sort", "--asc", "--no-asc", "-o/--open", "--no-open"], "example": "lore --robot issues --state opened --limit 10", "response_schema": { "list": { @@ -2141,13 +2143,14 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box> }, "search": { "description": "Search indexed documents (lexical, hybrid, semantic)", - "flags": ["", "--mode", "--type", "--author", "-p/--project", "--label", "--path", "--since", "--updated-since", "-n/--limit", "--explain", "--no-explain", "--fts-mode"], + "flags": ["", "--mode", "--type", "--author", "-p/--project", "--label", "--path", "--since", "--updated-since", "-n/--limit", "--fields ", "--explain", "--no-explain", "--fts-mode"], "example": "lore --robot search 'authentication bug' --mode hybrid --limit 10", "response_schema": { "ok": "bool", - "data": {"results": "[{doc_id:int, source_type:string, title:string, snippet:string, score:float, project_path:string, web_url:string?}]", "total_count": "int", "query": "string", "mode": "string"}, + "data": {"results": "[{document_id:int, source_type:string, title:string, snippet:string, score:float, url:string?, author:string?, created_at:string?, updated_at:string?, project_path:string, labels:[string], paths:[string]}]", "total_results": "int", "query": "string", "mode": "string", "warnings": "[string]"}, "meta": {"elapsed_ms": "int"} - } + }, + "fields_presets": {"minimal": ["document_id", "title", "source_type", "score"]} }, "count": { "description": "Count entities in local database", @@ -2226,17 +2229,18 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box> }, "timeline": { "description": "Chronological timeline of events matching a keyword query", - "flags": ["", "-p/--project", "--since ", "--depth ", "--expand-mentions", "-n/--limit", "--max-seeds", "--max-entities", "--max-evidence"], + "flags": ["", "-p/--project", "--since ", "--depth ", "--expand-mentions", "-n/--limit", "--fields ", "--max-seeds", "--max-entities", "--max-evidence"], "example": "lore --robot timeline '' --since 30d", "response_schema": { "ok": "bool", "data": {"entities": "[{type:string, iid:int, title:string, project_path:string}]", "events": "[{timestamp:string, type:string, entity_type:string, entity_iid:int, detail:string}]", "total_events": "int"}, "meta": {"elapsed_ms": "int"} - } + }, + "fields_presets": {"minimal": ["timestamp", "type", "entity_iid", "detail"]} }, "who": { "description": "People intelligence: experts, workload, active discussions, overlap, review patterns", - "flags": ["", "--path ", "--active", "--overlap ", "--reviews", "--since ", "-p/--project", "-n/--limit"], + "flags": ["", "--path ", "--active", "--overlap ", "--reviews", "--since ", "-p/--project", "-n/--limit", "--fields "], "modes": { "expert": "lore who -- Who knows about this area? (also: --path for root files)", "workload": "lore who -- What is someone working on?", @@ -2254,15 +2258,26 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box> "...": "mode-specific fields" }, "meta": {"elapsed_ms": "int"} + }, + "fields_presets": { + "expert_minimal": ["username", "score"], + "workload_minimal": ["entity_type", "iid", "title", "state"], + "active_minimal": ["entity_type", "iid", "title", "participants"] } }, "robot-docs": { "description": "This command (agent self-discovery manifest)", - "flags": [], - "example": "lore robot-docs" + "flags": ["--brief"], + "example": "lore robot-docs --brief" } }); + // --brief: strip response_schema from every command (~60% smaller) + let mut commands = commands; + if brief { + strip_schemas(&mut commands); + } + let exit_codes = serde_json::json!({ "0": "Success", "1": "Internal error", @@ -2429,6 +2444,7 @@ async fn handle_list_compat( since: since_filter, due_before: due_before_filter, has_due_date, + statuses: &[], sort, order, };