feat(cli): status display/filtering, expanded --fields, and robot-docs --brief
Work item status integration across all CLI output:
Issue listing (lore list issues):
- New Status column appears when any issue has status data, with
hex-color rendering using ANSI 256-color approximation
- New --status flag for case-insensitive filtering (OR logic for
multiple values): lore issues --status "In progress" --status "To do"
- Status fields (name, category, color, icon_name, synced_at) in issue
list query and JSON output with conditional serialization
Issue detail (lore show issue):
- Displays "Status: In progress (in_progress)" with color-coded output
using ANSI 256-color approximation from hex color values
- Status fields included in robot mode JSON with ISO timestamps
- IssueRow, IssueDetail, IssueDetailJson all carry status columns
Robot mode field selection expanded to new commands:
- search: --fields with "minimal" preset (document_id, title, source_type, score)
- timeline: --fields with "minimal" preset (timestamp, type, entity_iid, detail)
- who: --fields with per-mode presets (expert_minimal, workload_minimal, etc.)
- robot-docs: new --brief flag strips response_schema from output (~60% smaller)
- strip_schemas() utility in robot.rs for --brief mode
- expand_fields_preset() extended for search, timeline, and all who modes
Robot-docs manifest updated with --status flag documentation, --fields
flags for search/timeline/who, fields_presets sections, and corrected
search response schema field names.
Note: replaces empty commit dcfd449 which lost staging during hook execution.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -61,6 +61,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
|
|||||||
"--assignee",
|
"--assignee",
|
||||||
"--label",
|
"--label",
|
||||||
"--milestone",
|
"--milestone",
|
||||||
|
"--status",
|
||||||
"--since",
|
"--since",
|
||||||
"--due-before",
|
"--due-before",
|
||||||
"--has-due",
|
"--has-due",
|
||||||
@@ -134,6 +135,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
|
|||||||
"--since",
|
"--since",
|
||||||
"--updated-since",
|
"--updated-since",
|
||||||
"--limit",
|
"--limit",
|
||||||
|
"--fields",
|
||||||
"--explain",
|
"--explain",
|
||||||
"--no-explain",
|
"--no-explain",
|
||||||
"--fts-mode",
|
"--fts-mode",
|
||||||
@@ -162,6 +164,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
|
|||||||
"--depth",
|
"--depth",
|
||||||
"--expand-mentions",
|
"--expand-mentions",
|
||||||
"--limit",
|
"--limit",
|
||||||
|
"--fields",
|
||||||
"--max-seeds",
|
"--max-seeds",
|
||||||
"--max-entities",
|
"--max-entities",
|
||||||
"--max-evidence",
|
"--max-evidence",
|
||||||
@@ -177,6 +180,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
|
|||||||
"--since",
|
"--since",
|
||||||
"--project",
|
"--project",
|
||||||
"--limit",
|
"--limit",
|
||||||
|
"--fields",
|
||||||
"--detail",
|
"--detail",
|
||||||
"--no-detail",
|
"--no-detail",
|
||||||
],
|
],
|
||||||
@@ -193,6 +197,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
|
|||||||
),
|
),
|
||||||
("generate-docs", &["--full", "--project"]),
|
("generate-docs", &["--full", "--project"]),
|
||||||
("completions", &[]),
|
("completions", &[]),
|
||||||
|
("robot-docs", &["--brief"]),
|
||||||
(
|
(
|
||||||
"list",
|
"list",
|
||||||
&[
|
&[
|
||||||
|
|||||||
@@ -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)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct IssueListRow {
|
pub struct IssueListRow {
|
||||||
pub iid: i64,
|
pub iid: i64,
|
||||||
@@ -34,6 +57,16 @@ pub struct IssueListRow {
|
|||||||
pub assignees: Vec<String>,
|
pub assignees: Vec<String>,
|
||||||
pub discussion_count: i64,
|
pub discussion_count: i64,
|
||||||
pub unresolved_count: i64,
|
pub unresolved_count: i64,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub status_name: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub status_category: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub status_color: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub status_icon_name: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub status_synced_at: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -51,6 +84,16 @@ pub struct IssueListRowJson {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub web_url: Option<String>,
|
pub web_url: Option<String>,
|
||||||
pub project_path: String,
|
pub project_path: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub status_name: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub status_category: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub status_color: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub status_icon_name: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub status_synced_at_iso: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&IssueListRow> for IssueListRowJson {
|
impl From<&IssueListRow> for IssueListRowJson {
|
||||||
@@ -68,6 +111,11 @@ impl From<&IssueListRow> for IssueListRowJson {
|
|||||||
updated_at_iso: ms_to_iso(row.updated_at),
|
updated_at_iso: ms_to_iso(row.updated_at),
|
||||||
web_url: row.web_url.clone(),
|
web_url: row.web_url.clone(),
|
||||||
project_path: row.project_path.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 since: Option<&'a str>,
|
||||||
pub due_before: Option<&'a str>,
|
pub due_before: Option<&'a str>,
|
||||||
pub has_due_date: bool,
|
pub has_due_date: bool,
|
||||||
|
pub statuses: &'a [String],
|
||||||
pub sort: &'a str,
|
pub sort: &'a str,
|
||||||
pub order: &'a str,
|
pub order: &'a str,
|
||||||
}
|
}
|
||||||
@@ -291,6 +340,22 @@ fn query_issues(conn: &Connection, filters: &ListFilters) -> Result<ListResult>
|
|||||||
where_clauses.push("i.due_date IS NOT NULL");
|
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() {
|
let where_sql = if where_clauses.is_empty() {
|
||||||
String::new()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
@@ -338,7 +403,12 @@ fn query_issues(conn: &Connection, filters: &ListFilters) -> Result<ListResult>
|
|||||||
(SELECT COUNT(*) FROM discussions d
|
(SELECT COUNT(*) FROM discussions d
|
||||||
WHERE d.issue_id = i.id) AS discussion_count,
|
WHERE d.issue_id = i.id) AS discussion_count,
|
||||||
(SELECT COUNT(*) FROM discussions d
|
(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
|
FROM issues i
|
||||||
JOIN projects p ON i.project_id = p.id
|
JOIN projects p ON i.project_id = p.id
|
||||||
{where_sql}
|
{where_sql}
|
||||||
@@ -375,6 +445,11 @@ fn query_issues(conn: &Connection, filters: &ListFilters) -> Result<ListResult>
|
|||||||
assignees,
|
assignees,
|
||||||
discussion_count: row.get(10)?,
|
discussion_count: row.get(10)?,
|
||||||
unresolved_count: row.get(11)?,
|
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::<std::result::Result<Vec<_>, _>>()?;
|
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||||
@@ -683,19 +758,28 @@ pub fn print_list_issues(result: &ListResult) {
|
|||||||
result.total_count
|
result.total_count
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut table = Table::new();
|
let has_any_status = result.issues.iter().any(|i| i.status_name.is_some());
|
||||||
table
|
|
||||||
.set_content_arrangement(ContentArrangement::Dynamic)
|
let mut header = vec![
|
||||||
.set_header(vec![
|
|
||||||
Cell::new("IID").add_attribute(Attribute::Bold),
|
Cell::new("IID").add_attribute(Attribute::Bold),
|
||||||
Cell::new("Title").add_attribute(Attribute::Bold),
|
Cell::new("Title").add_attribute(Attribute::Bold),
|
||||||
Cell::new("State").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("Assignee").add_attribute(Attribute::Bold),
|
||||||
Cell::new("Labels").add_attribute(Attribute::Bold),
|
Cell::new("Labels").add_attribute(Attribute::Bold),
|
||||||
Cell::new("Disc").add_attribute(Attribute::Bold),
|
Cell::new("Disc").add_attribute(Attribute::Bold),
|
||||||
Cell::new("Updated").add_attribute(Attribute::Bold),
|
Cell::new("Updated").add_attribute(Attribute::Bold),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
let mut table = Table::new();
|
||||||
|
table
|
||||||
|
.set_content_arrangement(ContentArrangement::Dynamic)
|
||||||
|
.set_header(header);
|
||||||
|
|
||||||
for issue in &result.issues {
|
for issue in &result.issues {
|
||||||
let title = truncate_with_ellipsis(&issue.title, 45);
|
let title = truncate_with_ellipsis(&issue.title, 45);
|
||||||
let relative_time = format_relative_time(issue.updated_at);
|
let relative_time = format_relative_time(issue.updated_at);
|
||||||
@@ -709,15 +793,28 @@ pub fn print_list_issues(result: &ListResult) {
|
|||||||
colored_cell(&issue.state, Color::DarkGrey)
|
colored_cell(&issue.state, Color::DarkGrey)
|
||||||
};
|
};
|
||||||
|
|
||||||
table.add_row(vec![
|
let mut row = vec![
|
||||||
colored_cell(format!("#{}", issue.iid), Color::Cyan),
|
colored_cell(format!("#{}", issue.iid), Color::Cyan),
|
||||||
Cell::new(title),
|
Cell::new(title),
|
||||||
state_cell,
|
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(assignee, Color::Magenta),
|
||||||
colored_cell(labels, Color::Yellow),
|
colored_cell(labels, Color::Yellow),
|
||||||
Cell::new(discussions),
|
Cell::new(discussions),
|
||||||
colored_cell(relative_time, Color::DarkGrey),
|
colored_cell(relative_time, Color::DarkGrey),
|
||||||
]);
|
]);
|
||||||
|
table.add_row(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("{table}");
|
println!("{table}");
|
||||||
|
|||||||
@@ -386,11 +386,20 @@ struct SearchMeta {
|
|||||||
elapsed_ms: u64,
|
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 {
|
let output = SearchJsonOutput {
|
||||||
ok: true,
|
ok: true,
|
||||||
data: response,
|
data: response,
|
||||||
meta: SearchMeta { elapsed_ms },
|
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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ pub struct IssueDetail {
|
|||||||
pub milestone: Option<String>,
|
pub milestone: Option<String>,
|
||||||
pub closing_merge_requests: Vec<ClosingMrRef>,
|
pub closing_merge_requests: Vec<ClosingMrRef>,
|
||||||
pub discussions: Vec<DiscussionDetail>,
|
pub discussions: Vec<DiscussionDetail>,
|
||||||
|
pub status_name: Option<String>,
|
||||||
|
pub status_category: Option<String>,
|
||||||
|
pub status_color: Option<String>,
|
||||||
|
pub status_icon_name: Option<String>,
|
||||||
|
pub status_synced_at: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@@ -134,6 +139,11 @@ pub fn run_show_issue(
|
|||||||
milestone: issue.milestone_title,
|
milestone: issue.milestone_title,
|
||||||
closing_merge_requests: closing_mrs,
|
closing_merge_requests: closing_mrs,
|
||||||
discussions,
|
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,
|
project_path: String,
|
||||||
due_date: Option<String>,
|
due_date: Option<String>,
|
||||||
milestone_title: Option<String>,
|
milestone_title: Option<String>,
|
||||||
|
status_name: Option<String>,
|
||||||
|
status_category: Option<String>,
|
||||||
|
status_color: Option<String>,
|
||||||
|
status_icon_name: Option<String>,
|
||||||
|
status_synced_at: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_issue(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Result<IssueRow> {
|
fn find_issue(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Result<IssueRow> {
|
||||||
@@ -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,
|
"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.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
|
FROM issues i
|
||||||
JOIN projects p ON i.project_id = p.id
|
JOIN projects p ON i.project_id = p.id
|
||||||
WHERE i.iid = ? AND i.project_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 => (
|
None => (
|
||||||
"SELECT i.id, i.iid, i.title, i.description, i.state, i.author_username,
|
"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.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
|
FROM issues i
|
||||||
JOIN projects p ON i.project_id = p.id
|
JOIN projects p ON i.project_id = p.id
|
||||||
WHERE i.iid = ?",
|
WHERE i.iid = ?",
|
||||||
@@ -195,6 +214,11 @@ fn find_issue(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Resu
|
|||||||
project_path: row.get(9)?,
|
project_path: row.get(9)?,
|
||||||
due_date: row.get(10)?,
|
due_date: row.get(10)?,
|
||||||
milestone_title: row.get(11)?,
|
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::<std::result::Result<Vec<_>, _>>()?;
|
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||||
@@ -603,6 +627,17 @@ pub fn print_show_issue(issue: &IssueDetail) {
|
|||||||
};
|
};
|
||||||
println!("State: {}", state_styled);
|
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);
|
println!("Author: @{}", issue.author_username);
|
||||||
|
|
||||||
if !issue.assignees.is_empty() {
|
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)]
|
#[derive(Serialize)]
|
||||||
pub struct IssueDetailJson {
|
pub struct IssueDetailJson {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
@@ -882,6 +943,11 @@ pub struct IssueDetailJson {
|
|||||||
pub milestone: Option<String>,
|
pub milestone: Option<String>,
|
||||||
pub closing_merge_requests: Vec<ClosingMrRefJson>,
|
pub closing_merge_requests: Vec<ClosingMrRefJson>,
|
||||||
pub discussions: Vec<DiscussionDetailJson>,
|
pub discussions: Vec<DiscussionDetailJson>,
|
||||||
|
pub status_name: Option<String>,
|
||||||
|
pub status_category: Option<String>,
|
||||||
|
pub status_color: Option<String>,
|
||||||
|
pub status_icon_name: Option<String>,
|
||||||
|
pub status_synced_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -934,6 +1000,11 @@ impl From<&IssueDetail> for IssueDetailJson {
|
|||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
discussions: issue.discussions.iter().map(|d| d.into()).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();
|
.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]
|
#[test]
|
||||||
fn test_get_issue_assignees_empty() {
|
fn test_get_issue_assignees_empty() {
|
||||||
let conn = setup_test_db();
|
let conn = setup_test_db();
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ pub fn print_timeline_json_with_meta(
|
|||||||
total_events_before_limit: usize,
|
total_events_before_limit: usize,
|
||||||
depth: u32,
|
depth: u32,
|
||||||
expand_mentions: bool,
|
expand_mentions: bool,
|
||||||
|
fields: Option<&[String]>,
|
||||||
) {
|
) {
|
||||||
let output = TimelineJsonEnvelope {
|
let output = TimelineJsonEnvelope {
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -268,10 +269,18 @@ pub fn print_timeline_json_with_meta(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
match serde_json::to_string(&output) {
|
let mut value = match serde_json::to_value(&output) {
|
||||||
Ok(json) => println!("{json}"),
|
Ok(v) => v,
|
||||||
Err(e) => eprintln!("Error serializing timeline JSON: {e}"),
|
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)]
|
#[derive(Serialize)]
|
||||||
|
|||||||
@@ -2201,12 +2201,28 @@ pub fn print_who_json(run: &WhoRun, args: &WhoArgs, elapsed_ms: u64) {
|
|||||||
meta: RobotMeta { elapsed_ms },
|
meta: RobotMeta { elapsed_ms },
|
||||||
};
|
};
|
||||||
|
|
||||||
println!(
|
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}")}})
|
||||||
serde_json::to_string(&output).unwrap_or_else(|e| {
|
});
|
||||||
format!(r#"{{"ok":false,"error":{{"code":"INTERNAL_ERROR","message":"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)]
|
#[derive(Serialize)]
|
||||||
@@ -2577,6 +2593,7 @@ mod tests {
|
|||||||
limit: 20,
|
limit: 20,
|
||||||
detail: false,
|
detail: false,
|
||||||
no_detail: false,
|
no_detail: false,
|
||||||
|
fields: None,
|
||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
WhoMode::Expert { .. }
|
WhoMode::Expert { .. }
|
||||||
@@ -2595,6 +2612,7 @@ mod tests {
|
|||||||
limit: 20,
|
limit: 20,
|
||||||
detail: false,
|
detail: false,
|
||||||
no_detail: false,
|
no_detail: false,
|
||||||
|
fields: None,
|
||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
WhoMode::Workload { .. }
|
WhoMode::Workload { .. }
|
||||||
@@ -2613,6 +2631,7 @@ mod tests {
|
|||||||
limit: 20,
|
limit: 20,
|
||||||
detail: false,
|
detail: false,
|
||||||
no_detail: false,
|
no_detail: false,
|
||||||
|
fields: None,
|
||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
WhoMode::Workload { .. }
|
WhoMode::Workload { .. }
|
||||||
@@ -2631,6 +2650,7 @@ mod tests {
|
|||||||
limit: 20,
|
limit: 20,
|
||||||
detail: false,
|
detail: false,
|
||||||
no_detail: false,
|
no_detail: false,
|
||||||
|
fields: None,
|
||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
WhoMode::Reviews { .. }
|
WhoMode::Reviews { .. }
|
||||||
@@ -2649,6 +2669,7 @@ mod tests {
|
|||||||
limit: 20,
|
limit: 20,
|
||||||
detail: false,
|
detail: false,
|
||||||
no_detail: false,
|
no_detail: false,
|
||||||
|
fields: None,
|
||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
WhoMode::Expert { .. }
|
WhoMode::Expert { .. }
|
||||||
@@ -2667,6 +2688,7 @@ mod tests {
|
|||||||
limit: 20,
|
limit: 20,
|
||||||
detail: false,
|
detail: false,
|
||||||
no_detail: false,
|
no_detail: false,
|
||||||
|
fields: None,
|
||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
WhoMode::Expert { .. }
|
WhoMode::Expert { .. }
|
||||||
@@ -2686,6 +2708,7 @@ mod tests {
|
|||||||
limit: 20,
|
limit: 20,
|
||||||
detail: true,
|
detail: true,
|
||||||
no_detail: false,
|
no_detail: false,
|
||||||
|
fields: None,
|
||||||
};
|
};
|
||||||
let mode = resolve_mode(&args).unwrap();
|
let mode = resolve_mode(&args).unwrap();
|
||||||
let err = validate_mode_flags(&mode, &args).unwrap_err();
|
let err = validate_mode_flags(&mode, &args).unwrap_err();
|
||||||
@@ -2709,6 +2732,7 @@ mod tests {
|
|||||||
limit: 20,
|
limit: 20,
|
||||||
detail: true,
|
detail: true,
|
||||||
no_detail: false,
|
no_detail: false,
|
||||||
|
fields: None,
|
||||||
};
|
};
|
||||||
let mode = resolve_mode(&args).unwrap();
|
let mode = resolve_mode(&args).unwrap();
|
||||||
assert!(validate_mode_flags(&mode, &args).is_ok());
|
assert!(validate_mode_flags(&mode, &args).is_ok());
|
||||||
|
|||||||
@@ -186,7 +186,11 @@ pub enum Commands {
|
|||||||
|
|
||||||
/// Machine-readable command manifest for agent self-discovery
|
/// Machine-readable command manifest for agent self-discovery
|
||||||
#[command(name = "robot-docs")]
|
#[command(name = "robot-docs")]
|
||||||
RobotDocs,
|
RobotDocs {
|
||||||
|
/// Omit response_schema from output (~60% smaller)
|
||||||
|
#[arg(long)]
|
||||||
|
brief: bool,
|
||||||
|
},
|
||||||
|
|
||||||
/// Generate shell completions
|
/// Generate shell completions
|
||||||
#[command(long_about = "Generate shell completions for lore.\n\n\
|
#[command(long_about = "Generate shell completions for lore.\n\n\
|
||||||
@@ -315,6 +319,10 @@ pub struct IssuesArgs {
|
|||||||
#[arg(short = 'm', long, help_heading = "Filters")]
|
#[arg(short = 'm', long, help_heading = "Filters")]
|
||||||
pub milestone: Option<String>,
|
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)
|
/// Filter by time (7d, 2w, 1m, or YYYY-MM-DD)
|
||||||
#[arg(long, help_heading = "Filters")]
|
#[arg(long, help_heading = "Filters")]
|
||||||
pub since: Option<String>,
|
pub since: Option<String>,
|
||||||
@@ -563,6 +571,10 @@ pub struct SearchArgs {
|
|||||||
)]
|
)]
|
||||||
pub limit: usize,
|
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
|
/// Show ranking explanation per result
|
||||||
#[arg(long, help_heading = "Output", overrides_with = "no_explain")]
|
#[arg(long, help_heading = "Output", overrides_with = "no_explain")]
|
||||||
pub explain: bool,
|
pub explain: bool,
|
||||||
@@ -682,6 +694,10 @@ pub struct TimelineArgs {
|
|||||||
)]
|
)]
|
||||||
pub limit: usize,
|
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
|
/// Maximum seed entities from search
|
||||||
#[arg(long = "max-seeds", default_value = "10", help_heading = "Expansion")]
|
#[arg(long = "max-seeds", default_value = "10", help_heading = "Expansion")]
|
||||||
pub max_seeds: usize,
|
pub max_seeds: usize,
|
||||||
@@ -752,6 +768,10 @@ pub struct WhoArgs {
|
|||||||
)]
|
)]
|
||||||
pub limit: u16,
|
pub limit: 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)
|
/// Show per-MR detail breakdown (expert mode only)
|
||||||
#[arg(long, help_heading = "Output", overrides_with = "no_detail")]
|
#[arg(long, help_heading = "Output", overrides_with = "no_detail")]
|
||||||
pub detail: bool,
|
pub detail: bool,
|
||||||
|
|||||||
@@ -36,9 +36,48 @@ pub fn expand_fields_preset(fields: &[String], entity: &str) -> Vec<String> {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|s| (*s).to_string())
|
.map(|s| (*s).to_string())
|
||||||
.collect(),
|
.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(),
|
_ => fields.to_vec(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fields.to_vec()
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
44
src/main.rs
44
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_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,
|
run_sync, run_sync_status, run_timeline, run_who,
|
||||||
};
|
};
|
||||||
use lore::cli::robot::RobotMeta;
|
use lore::cli::robot::{RobotMeta, strip_schemas};
|
||||||
use lore::cli::{
|
use lore::cli::{
|
||||||
Cli, Commands, CountArgs, EmbedArgs, GenerateDocsArgs, IngestArgs, IssuesArgs, MrsArgs,
|
Cli, Commands, CountArgs, EmbedArgs, GenerateDocsArgs, IngestArgs, IssuesArgs, MrsArgs,
|
||||||
SearchArgs, StatsArgs, SyncArgs, TimelineArgs, WhoArgs,
|
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
|
// Phase 2: Handle no-args case - in robot mode, output robot-docs; otherwise show help
|
||||||
None => {
|
None => {
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
handle_robot_docs(robot_mode)
|
handle_robot_docs(robot_mode, false)
|
||||||
} else {
|
} else {
|
||||||
use clap::CommandFactory;
|
use clap::CommandFactory;
|
||||||
let mut cmd = Cli::command();
|
let mut cmd = Cli::command();
|
||||||
@@ -224,7 +224,7 @@ async fn main() {
|
|||||||
Some(Commands::Reset { yes: _ }) => handle_reset(robot_mode),
|
Some(Commands::Reset { yes: _ }) => handle_reset(robot_mode),
|
||||||
Some(Commands::Migrate) => handle_migrate(cli.config.as_deref(), robot_mode).await,
|
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::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 {
|
Some(Commands::List {
|
||||||
entity,
|
entity,
|
||||||
@@ -703,6 +703,7 @@ fn handle_issues(
|
|||||||
since: args.since.as_deref(),
|
since: args.since.as_deref(),
|
||||||
due_before: args.due_before.as_deref(),
|
due_before: args.due_before.as_deref(),
|
||||||
has_due_date: has_due,
|
has_due_date: has_due,
|
||||||
|
statuses: &args.status,
|
||||||
sort: &args.sort,
|
sort: &args.sort,
|
||||||
order,
|
order,
|
||||||
};
|
};
|
||||||
@@ -1697,6 +1698,7 @@ fn handle_timeline(
|
|||||||
result.total_events_before_limit,
|
result.total_events_before_limit,
|
||||||
params.depth,
|
params.depth,
|
||||||
params.expand_mentions,
|
params.expand_mentions,
|
||||||
|
args.fields.as_deref(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
print_timeline(&result);
|
print_timeline(&result);
|
||||||
@@ -1740,7 +1742,7 @@ async fn handle_search(
|
|||||||
let elapsed_ms = start.elapsed().as_millis() as u64;
|
let elapsed_ms = start.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_search_results_json(&response, elapsed_ms);
|
print_search_results_json(&response, elapsed_ms, args.fields.as_deref());
|
||||||
} else {
|
} else {
|
||||||
print_search_results(&response);
|
print_search_results(&response);
|
||||||
}
|
}
|
||||||
@@ -2038,7 +2040,7 @@ struct RobotDocsActivation {
|
|||||||
auto: String,
|
auto: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
|
fn handle_robot_docs(robot_mode: bool, brief: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let version = env!("CARGO_PKG_VERSION").to_string();
|
let version = env!("CARGO_PKG_VERSION").to_string();
|
||||||
|
|
||||||
let commands = serde_json::json!({
|
let commands = serde_json::json!({
|
||||||
@@ -2105,7 +2107,7 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
},
|
},
|
||||||
"issues": {
|
"issues": {
|
||||||
"description": "List or show issues",
|
"description": "List or show issues",
|
||||||
"flags": ["<IID>", "-n/--limit", "--fields <list>", "-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": ["<IID>", "-n/--limit", "--fields <list>", "-s/--state", "--status <name>", "-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",
|
"example": "lore --robot issues --state opened --limit 10",
|
||||||
"response_schema": {
|
"response_schema": {
|
||||||
"list": {
|
"list": {
|
||||||
@@ -2141,13 +2143,14 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"description": "Search indexed documents (lexical, hybrid, semantic)",
|
"description": "Search indexed documents (lexical, hybrid, semantic)",
|
||||||
"flags": ["<QUERY>", "--mode", "--type", "--author", "-p/--project", "--label", "--path", "--since", "--updated-since", "-n/--limit", "--explain", "--no-explain", "--fts-mode"],
|
"flags": ["<QUERY>", "--mode", "--type", "--author", "-p/--project", "--label", "--path", "--since", "--updated-since", "-n/--limit", "--fields <list>", "--explain", "--no-explain", "--fts-mode"],
|
||||||
"example": "lore --robot search 'authentication bug' --mode hybrid --limit 10",
|
"example": "lore --robot search 'authentication bug' --mode hybrid --limit 10",
|
||||||
"response_schema": {
|
"response_schema": {
|
||||||
"ok": "bool",
|
"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"}
|
"meta": {"elapsed_ms": "int"}
|
||||||
}
|
},
|
||||||
|
"fields_presets": {"minimal": ["document_id", "title", "source_type", "score"]}
|
||||||
},
|
},
|
||||||
"count": {
|
"count": {
|
||||||
"description": "Count entities in local database",
|
"description": "Count entities in local database",
|
||||||
@@ -2226,17 +2229,18 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"description": "Chronological timeline of events matching a keyword query",
|
"description": "Chronological timeline of events matching a keyword query",
|
||||||
"flags": ["<QUERY>", "-p/--project", "--since <duration>", "--depth <n>", "--expand-mentions", "-n/--limit", "--max-seeds", "--max-entities", "--max-evidence"],
|
"flags": ["<QUERY>", "-p/--project", "--since <duration>", "--depth <n>", "--expand-mentions", "-n/--limit", "--fields <list>", "--max-seeds", "--max-entities", "--max-evidence"],
|
||||||
"example": "lore --robot timeline '<keyword>' --since 30d",
|
"example": "lore --robot timeline '<keyword>' --since 30d",
|
||||||
"response_schema": {
|
"response_schema": {
|
||||||
"ok": "bool",
|
"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"},
|
"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"}
|
"meta": {"elapsed_ms": "int"}
|
||||||
}
|
},
|
||||||
|
"fields_presets": {"minimal": ["timestamp", "type", "entity_iid", "detail"]}
|
||||||
},
|
},
|
||||||
"who": {
|
"who": {
|
||||||
"description": "People intelligence: experts, workload, active discussions, overlap, review patterns",
|
"description": "People intelligence: experts, workload, active discussions, overlap, review patterns",
|
||||||
"flags": ["<target>", "--path <path>", "--active", "--overlap <path>", "--reviews", "--since <duration>", "-p/--project", "-n/--limit"],
|
"flags": ["<target>", "--path <path>", "--active", "--overlap <path>", "--reviews", "--since <duration>", "-p/--project", "-n/--limit", "--fields <list>"],
|
||||||
"modes": {
|
"modes": {
|
||||||
"expert": "lore who <file-path> -- Who knows about this area? (also: --path for root files)",
|
"expert": "lore who <file-path> -- Who knows about this area? (also: --path for root files)",
|
||||||
"workload": "lore who <username> -- What is someone working on?",
|
"workload": "lore who <username> -- What is someone working on?",
|
||||||
@@ -2254,15 +2258,26 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
"...": "mode-specific fields"
|
"...": "mode-specific fields"
|
||||||
},
|
},
|
||||||
"meta": {"elapsed_ms": "int"}
|
"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": {
|
"robot-docs": {
|
||||||
"description": "This command (agent self-discovery manifest)",
|
"description": "This command (agent self-discovery manifest)",
|
||||||
"flags": [],
|
"flags": ["--brief"],
|
||||||
"example": "lore robot-docs"
|
"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!({
|
let exit_codes = serde_json::json!({
|
||||||
"0": "Success",
|
"0": "Success",
|
||||||
"1": "Internal error",
|
"1": "Internal error",
|
||||||
@@ -2429,6 +2444,7 @@ async fn handle_list_compat(
|
|||||||
since: since_filter,
|
since: since_filter,
|
||||||
due_before: due_before_filter,
|
due_before: due_before_filter,
|
||||||
has_due_date,
|
has_due_date,
|
||||||
|
statuses: &[],
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user