Compare commits
4 Commits
a573d695d5
...
3e7fa607d3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e7fa607d3 | ||
|
|
b5f78e31a8 | ||
|
|
cf6d27435a | ||
|
|
4ce0130620 |
54
README.md
54
README.md
@@ -177,9 +177,13 @@ lore issues --has-due # Only issues with due dates
|
|||||||
lore issues -p group/repo # Filter by project
|
lore issues -p group/repo # Filter by project
|
||||||
lore issues --sort created --asc # Sort by created date, ascending
|
lore issues --sort created --asc # Sort by created date, ascending
|
||||||
lore issues -o # Open first result in browser
|
lore issues -o # Open first result in browser
|
||||||
|
|
||||||
|
# Field selection (robot mode)
|
||||||
|
lore -J issues --fields minimal # Compact: iid, title, state, updated_at_iso
|
||||||
|
lore -J issues --fields iid,title,labels,state # Custom fields
|
||||||
```
|
```
|
||||||
|
|
||||||
When listing, output includes: IID, title, state, author, assignee, labels, and update time.
|
When listing, output includes: IID, title, state, author, assignee, labels, and update time. In robot mode, the `--fields` flag controls which fields appear in the JSON response.
|
||||||
|
|
||||||
When showing a single issue (e.g., `lore issues 123`), output includes: title, description, state, author, assignees, labels, milestone, due date, web URL, and threaded discussions.
|
When showing a single issue (e.g., `lore issues 123`), output includes: title, description, state, author, assignees, labels, milestone, due date, web URL, and threaded discussions.
|
||||||
|
|
||||||
@@ -220,6 +224,10 @@ lore mrs --since 7d # Updated in last 7 days
|
|||||||
lore mrs -p group/repo # Filter by project
|
lore mrs -p group/repo # Filter by project
|
||||||
lore mrs --sort created --asc # Sort by created date, ascending
|
lore mrs --sort created --asc # Sort by created date, ascending
|
||||||
lore mrs -o # Open first result in browser
|
lore mrs -o # Open first result in browser
|
||||||
|
|
||||||
|
# Field selection (robot mode)
|
||||||
|
lore -J mrs --fields minimal # Compact: iid, title, state, updated_at_iso
|
||||||
|
lore -J mrs --fields iid,title,draft,target_branch # Custom fields
|
||||||
```
|
```
|
||||||
|
|
||||||
When listing, output includes: IID, title (with [DRAFT] prefix if applicable), state, author, assignee, labels, and update time.
|
When listing, output includes: IID, title (with [DRAFT] prefix if applicable), state, author, assignee, labels, and update time.
|
||||||
@@ -412,7 +420,7 @@ lore version
|
|||||||
|
|
||||||
## Robot Mode
|
## Robot Mode
|
||||||
|
|
||||||
Machine-readable JSON output for scripting and AI agent consumption.
|
Machine-readable JSON output for scripting and AI agent consumption. All responses use compact (single-line) JSON with a uniform envelope and timing metadata.
|
||||||
|
|
||||||
### Activation
|
### Activation
|
||||||
|
|
||||||
@@ -432,18 +440,51 @@ lore issues -n 5 | jq .
|
|||||||
|
|
||||||
### Response Format
|
### Response Format
|
||||||
|
|
||||||
All commands return consistent JSON:
|
All commands return a consistent JSON envelope to stdout:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"ok": true, "data": {...}, "meta": {...}}
|
{"ok":true,"data":{...},"meta":{"elapsed_ms":42}}
|
||||||
```
|
```
|
||||||
|
|
||||||
Errors return structured JSON to stderr:
|
Every response includes `meta.elapsed_ms` (wall-clock milliseconds for the command).
|
||||||
|
|
||||||
|
Errors return structured JSON to stderr with machine-actionable recovery steps:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"error": {"code": "CONFIG_NOT_FOUND", "message": "...", "suggestion": "Run 'lore init'"}}
|
{"error":{"code":"CONFIG_NOT_FOUND","message":"...","suggestion":"Run 'lore init'","actions":["lore init"]}}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `actions` array contains executable shell commands an agent can run to recover from the error. It is omitted when empty (e.g., for generic I/O errors).
|
||||||
|
|
||||||
|
### Field Selection
|
||||||
|
|
||||||
|
The `--fields` flag on `issues` and `mrs` list commands controls which fields appear in the JSON response, reducing token usage for AI agent workflows:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Minimal preset (~60% fewer tokens)
|
||||||
|
lore -J issues --fields minimal
|
||||||
|
|
||||||
|
# Custom field list
|
||||||
|
lore -J issues --fields iid,title,state,labels,updated_at_iso
|
||||||
|
|
||||||
|
# Available presets
|
||||||
|
# minimal: iid, title, state, updated_at_iso
|
||||||
|
```
|
||||||
|
|
||||||
|
Valid fields for issues: `iid`, `title`, `state`, `author_username`, `labels`, `assignees`, `discussion_count`, `unresolved_count`, `created_at_iso`, `updated_at_iso`, `web_url`, `project_path`
|
||||||
|
|
||||||
|
Valid fields for MRs: `iid`, `title`, `state`, `author_username`, `labels`, `draft`, `target_branch`, `source_branch`, `discussion_count`, `unresolved_count`, `created_at_iso`, `updated_at_iso`, `web_url`, `project_path`, `reviewers`
|
||||||
|
|
||||||
|
### Agent Self-Discovery
|
||||||
|
|
||||||
|
The `robot-docs` command provides a complete machine-readable manifest including response schemas for every command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lore robot-docs | jq '.data.commands.issues.response_schema'
|
||||||
|
```
|
||||||
|
|
||||||
|
Each command entry includes `response_schema` describing the shape of its JSON response, `fields_presets` for commands supporting `--fields`, and copy-paste `example` invocations.
|
||||||
|
|
||||||
### Exit Codes
|
### Exit Codes
|
||||||
|
|
||||||
| Code | Meaning |
|
| Code | Meaning |
|
||||||
@@ -467,6 +508,7 @@ Errors return structured JSON to stderr:
|
|||||||
| 16 | Embedding failed |
|
| 16 | Embedding failed |
|
||||||
| 17 | Not found (entity does not exist) |
|
| 17 | Not found (entity does not exist) |
|
||||||
| 18 | Ambiguous match (use `-p` to specify project) |
|
| 18 | Ambiguous match (use `-p` to specify project) |
|
||||||
|
| 19 | Health check failed |
|
||||||
| 20 | Config not found |
|
| 20 | Config not found |
|
||||||
|
|
||||||
## Configuration Precedence
|
## Configuration Precedence
|
||||||
|
|||||||
12
build.rs
12
build.rs
@@ -5,7 +5,17 @@ fn main() {
|
|||||||
.ok()
|
.ok()
|
||||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
println!("cargo:rustc-env=GIT_HASH={}", hash.trim());
|
let hash = hash.trim();
|
||||||
|
println!("cargo:rustc-env=GIT_HASH={hash}");
|
||||||
|
|
||||||
|
// Combined version string for clap --version flag
|
||||||
|
let pkg_version = std::env::var("CARGO_PKG_VERSION").unwrap_or_default();
|
||||||
|
if hash.is_empty() {
|
||||||
|
println!("cargo:rustc-env=LORE_VERSION={pkg_version}");
|
||||||
|
} else {
|
||||||
|
println!("cargo:rustc-env=LORE_VERSION={pkg_version} ({hash})");
|
||||||
|
}
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed=.git/HEAD");
|
println!("cargo:rerun-if-changed=.git/HEAD");
|
||||||
println!("cargo:rerun-if-changed=.git/refs/heads");
|
println!("cargo:rerun-if-changed=.git/refs/heads");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use rusqlite::Connection;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
|
use crate::cli::robot::RobotMeta;
|
||||||
use crate::core::db::create_connection;
|
use crate::core::db::create_connection;
|
||||||
use crate::core::error::Result;
|
use crate::core::error::Result;
|
||||||
use crate::core::events_db::{self, EventCounts};
|
use crate::core::events_db::{self, EventCounts};
|
||||||
@@ -196,6 +197,7 @@ fn format_number(n: i64) -> String {
|
|||||||
struct CountJsonOutput {
|
struct CountJsonOutput {
|
||||||
ok: bool,
|
ok: bool,
|
||||||
data: CountJsonData,
|
data: CountJsonData,
|
||||||
|
meta: RobotMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -228,6 +230,7 @@ pub fn run_count_events(config: &Config) -> Result<EventCounts> {
|
|||||||
struct EventCountJsonOutput {
|
struct EventCountJsonOutput {
|
||||||
ok: bool,
|
ok: bool,
|
||||||
data: EventCountJsonData,
|
data: EventCountJsonData,
|
||||||
|
meta: RobotMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -245,7 +248,7 @@ struct EventTypeCounts {
|
|||||||
total: usize,
|
total: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_event_count_json(counts: &EventCounts) {
|
pub fn print_event_count_json(counts: &EventCounts, elapsed_ms: u64) {
|
||||||
let output = EventCountJsonOutput {
|
let output = EventCountJsonOutput {
|
||||||
ok: true,
|
ok: true,
|
||||||
data: EventCountJsonData {
|
data: EventCountJsonData {
|
||||||
@@ -266,6 +269,7 @@ pub fn print_event_count_json(counts: &EventCounts) {
|
|||||||
},
|
},
|
||||||
total: counts.total(),
|
total: counts.total(),
|
||||||
},
|
},
|
||||||
|
meta: RobotMeta { elapsed_ms },
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("{}", serde_json::to_string(&output).unwrap());
|
println!("{}", serde_json::to_string(&output).unwrap());
|
||||||
@@ -317,7 +321,7 @@ pub fn print_event_count(counts: &EventCounts) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_count_json(result: &CountResult) {
|
pub fn print_count_json(result: &CountResult, elapsed_ms: u64) {
|
||||||
let breakdown = result.state_breakdown.as_ref().map(|b| CountJsonBreakdown {
|
let breakdown = result.state_breakdown.as_ref().map(|b| CountJsonBreakdown {
|
||||||
opened: b.opened,
|
opened: b.opened,
|
||||||
closed: b.closed,
|
closed: b.closed,
|
||||||
@@ -333,6 +337,7 @@ pub fn print_count_json(result: &CountResult) {
|
|||||||
system_excluded: result.system_count,
|
system_excluded: result.system_count,
|
||||||
breakdown,
|
breakdown,
|
||||||
},
|
},
|
||||||
|
meta: RobotMeta { elapsed_ms },
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("{}", serde_json::to_string(&output).unwrap());
|
println!("{}", serde_json::to_string(&output).unwrap());
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use console::style;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
|
use crate::cli::robot::RobotMeta;
|
||||||
use crate::core::db::{LATEST_SCHEMA_VERSION, create_connection, get_schema_version};
|
use crate::core::db::{LATEST_SCHEMA_VERSION, create_connection, get_schema_version};
|
||||||
use crate::core::error::{LoreError, Result};
|
use crate::core::error::{LoreError, Result};
|
||||||
use crate::core::paths::get_db_path;
|
use crate::core::paths::get_db_path;
|
||||||
@@ -112,12 +113,14 @@ pub fn print_embed(result: &EmbedCommandResult) {
|
|||||||
struct EmbedJsonOutput<'a> {
|
struct EmbedJsonOutput<'a> {
|
||||||
ok: bool,
|
ok: bool,
|
||||||
data: &'a EmbedCommandResult,
|
data: &'a EmbedCommandResult,
|
||||||
|
meta: RobotMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_embed_json(result: &EmbedCommandResult) {
|
pub fn print_embed_json(result: &EmbedCommandResult, elapsed_ms: u64) {
|
||||||
let output = EmbedJsonOutput {
|
let output = EmbedJsonOutput {
|
||||||
ok: true,
|
ok: true,
|
||||||
data: result,
|
data: result,
|
||||||
|
meta: RobotMeta { elapsed_ms },
|
||||||
};
|
};
|
||||||
println!("{}", serde_json::to_string(&output).unwrap());
|
println!("{}", serde_json::to_string(&output).unwrap());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use serde::Serialize;
|
|||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
|
use crate::cli::robot::RobotMeta;
|
||||||
use crate::core::db::create_connection;
|
use crate::core::db::create_connection;
|
||||||
use crate::core::error::Result;
|
use crate::core::error::Result;
|
||||||
use crate::core::paths::get_db_path;
|
use crate::core::paths::get_db_path;
|
||||||
@@ -150,6 +151,7 @@ pub fn print_generate_docs(result: &GenerateDocsResult) {
|
|||||||
struct GenerateDocsJsonOutput {
|
struct GenerateDocsJsonOutput {
|
||||||
ok: bool,
|
ok: bool,
|
||||||
data: GenerateDocsJsonData,
|
data: GenerateDocsJsonData,
|
||||||
|
meta: RobotMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -162,7 +164,7 @@ struct GenerateDocsJsonData {
|
|||||||
errored: usize,
|
errored: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_generate_docs_json(result: &GenerateDocsResult) {
|
pub fn print_generate_docs_json(result: &GenerateDocsResult, elapsed_ms: u64) {
|
||||||
let output = GenerateDocsJsonOutput {
|
let output = GenerateDocsJsonOutput {
|
||||||
ok: true,
|
ok: true,
|
||||||
data: GenerateDocsJsonData {
|
data: GenerateDocsJsonData {
|
||||||
@@ -180,6 +182,7 @@ pub fn print_generate_docs_json(result: &GenerateDocsResult) {
|
|||||||
unchanged: result.unchanged,
|
unchanged: result.unchanged,
|
||||||
errored: result.errored,
|
errored: result.errored,
|
||||||
},
|
},
|
||||||
|
meta: RobotMeta { elapsed_ms },
|
||||||
};
|
};
|
||||||
println!("{}", serde_json::to_string(&output).unwrap());
|
println!("{}", serde_json::to_string(&output).unwrap());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use serde::Serialize;
|
|||||||
use tracing::Instrument;
|
use tracing::Instrument;
|
||||||
|
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
|
use crate::cli::robot::RobotMeta;
|
||||||
use crate::core::db::create_connection;
|
use crate::core::db::create_connection;
|
||||||
use crate::core::error::{LoreError, Result};
|
use crate::core::error::{LoreError, Result};
|
||||||
use crate::core::lock::{AppLock, LockOptions};
|
use crate::core::lock::{AppLock, LockOptions};
|
||||||
@@ -732,6 +733,7 @@ fn print_mr_project_summary(path: &str, result: &IngestMrProjectResult) {
|
|||||||
struct IngestJsonOutput {
|
struct IngestJsonOutput {
|
||||||
ok: bool,
|
ok: bool,
|
||||||
data: IngestJsonData,
|
data: IngestJsonData,
|
||||||
|
meta: RobotMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -768,7 +770,7 @@ struct IngestMrStats {
|
|||||||
diffnotes_count: usize,
|
diffnotes_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_ingest_summary_json(result: &IngestResult) {
|
pub fn print_ingest_summary_json(result: &IngestResult, elapsed_ms: u64) {
|
||||||
let (issues, merge_requests) = if result.resource_type == "issues" {
|
let (issues, merge_requests) = if result.resource_type == "issues" {
|
||||||
(
|
(
|
||||||
Some(IngestIssueStats {
|
Some(IngestIssueStats {
|
||||||
@@ -807,6 +809,7 @@ pub fn print_ingest_summary_json(result: &IngestResult) {
|
|||||||
resource_events_fetched: result.resource_events_fetched,
|
resource_events_fetched: result.resource_events_fetched,
|
||||||
resource_events_failed: result.resource_events_failed,
|
resource_events_failed: result.resource_events_failed,
|
||||||
},
|
},
|
||||||
|
meta: RobotMeta { elapsed_ms },
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("{}", serde_json::to_string(&output).unwrap());
|
println!("{}", serde_json::to_string(&output).unwrap());
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use rusqlite::Connection;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
|
use crate::cli::robot::{RobotMeta, expand_fields_preset, filter_fields};
|
||||||
use crate::core::db::create_connection;
|
use crate::core::db::create_connection;
|
||||||
use crate::core::error::{LoreError, Result};
|
use crate::core::error::{LoreError, Result};
|
||||||
use crate::core::paths::get_db_path;
|
use crate::core::paths::get_db_path;
|
||||||
@@ -734,9 +735,20 @@ pub fn print_list_issues(result: &ListResult) {
|
|||||||
println!("{table}");
|
println!("{table}");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_list_issues_json(result: &ListResult) {
|
pub fn print_list_issues_json(result: &ListResult, elapsed_ms: u64, fields: Option<&[String]>) {
|
||||||
let json_result = ListResultJson::from(result);
|
let json_result = ListResultJson::from(result);
|
||||||
match serde_json::to_string_pretty(&json_result) {
|
let meta = RobotMeta { elapsed_ms };
|
||||||
|
let output = serde_json::json!({
|
||||||
|
"ok": true,
|
||||||
|
"data": json_result,
|
||||||
|
"meta": meta,
|
||||||
|
});
|
||||||
|
let mut output = output;
|
||||||
|
if let Some(f) = fields {
|
||||||
|
let expanded = expand_fields_preset(f, "issues");
|
||||||
|
filter_fields(&mut output, "issues", &expanded);
|
||||||
|
}
|
||||||
|
match serde_json::to_string(&output) {
|
||||||
Ok(json) => println!("{json}"),
|
Ok(json) => println!("{json}"),
|
||||||
Err(e) => eprintln!("Error serializing to JSON: {e}"),
|
Err(e) => eprintln!("Error serializing to JSON: {e}"),
|
||||||
}
|
}
|
||||||
@@ -819,9 +831,20 @@ pub fn print_list_mrs(result: &MrListResult) {
|
|||||||
println!("{table}");
|
println!("{table}");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_list_mrs_json(result: &MrListResult) {
|
pub fn print_list_mrs_json(result: &MrListResult, elapsed_ms: u64, fields: Option<&[String]>) {
|
||||||
let json_result = MrListResultJson::from(result);
|
let json_result = MrListResultJson::from(result);
|
||||||
match serde_json::to_string_pretty(&json_result) {
|
let meta = RobotMeta { elapsed_ms };
|
||||||
|
let output = serde_json::json!({
|
||||||
|
"ok": true,
|
||||||
|
"data": json_result,
|
||||||
|
"meta": meta,
|
||||||
|
});
|
||||||
|
let mut output = output;
|
||||||
|
if let Some(f) = fields {
|
||||||
|
let expanded = expand_fields_preset(f, "mrs");
|
||||||
|
filter_fields(&mut output, "mrs", &expanded);
|
||||||
|
}
|
||||||
|
match serde_json::to_string(&output) {
|
||||||
Ok(json) => println!("{json}"),
|
Ok(json) => println!("{json}"),
|
||||||
Err(e) => eprintln!("Error serializing to JSON: {e}"),
|
Err(e) => eprintln!("Error serializing to JSON: {e}"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use rusqlite::Connection;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
|
use crate::cli::robot::RobotMeta;
|
||||||
use crate::core::db::create_connection;
|
use crate::core::db::create_connection;
|
||||||
use crate::core::error::{LoreError, Result};
|
use crate::core::error::{LoreError, Result};
|
||||||
use crate::core::paths::get_db_path;
|
use crate::core::paths::get_db_path;
|
||||||
@@ -1042,17 +1043,29 @@ impl From<&MrNoteDetail> for MrNoteDetailJson {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_show_issue_json(issue: &IssueDetail) {
|
pub fn print_show_issue_json(issue: &IssueDetail, elapsed_ms: u64) {
|
||||||
let json_result = IssueDetailJson::from(issue);
|
let json_result = IssueDetailJson::from(issue);
|
||||||
match serde_json::to_string_pretty(&json_result) {
|
let meta = RobotMeta { elapsed_ms };
|
||||||
|
let output = serde_json::json!({
|
||||||
|
"ok": true,
|
||||||
|
"data": json_result,
|
||||||
|
"meta": meta,
|
||||||
|
});
|
||||||
|
match serde_json::to_string(&output) {
|
||||||
Ok(json) => println!("{json}"),
|
Ok(json) => println!("{json}"),
|
||||||
Err(e) => eprintln!("Error serializing to JSON: {e}"),
|
Err(e) => eprintln!("Error serializing to JSON: {e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_show_mr_json(mr: &MrDetail) {
|
pub fn print_show_mr_json(mr: &MrDetail, elapsed_ms: u64) {
|
||||||
let json_result = MrDetailJson::from(mr);
|
let json_result = MrDetailJson::from(mr);
|
||||||
match serde_json::to_string_pretty(&json_result) {
|
let meta = RobotMeta { elapsed_ms };
|
||||||
|
let output = serde_json::json!({
|
||||||
|
"ok": true,
|
||||||
|
"data": json_result,
|
||||||
|
"meta": meta,
|
||||||
|
});
|
||||||
|
match serde_json::to_string(&output) {
|
||||||
Ok(json) => println!("{json}"),
|
Ok(json) => println!("{json}"),
|
||||||
Err(e) => eprintln!("Error serializing to JSON: {e}"),
|
Err(e) => eprintln!("Error serializing to JSON: {e}"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use rusqlite::Connection;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
|
use crate::cli::robot::RobotMeta;
|
||||||
use crate::core::db::create_connection;
|
use crate::core::db::create_connection;
|
||||||
use crate::core::error::Result;
|
use crate::core::error::Result;
|
||||||
use crate::core::paths::get_db_path;
|
use crate::core::paths::get_db_path;
|
||||||
@@ -441,9 +442,10 @@ pub fn print_stats(result: &StatsResult) {
|
|||||||
struct StatsJsonOutput {
|
struct StatsJsonOutput {
|
||||||
ok: bool,
|
ok: bool,
|
||||||
data: StatsResult,
|
data: StatsResult,
|
||||||
|
meta: RobotMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_stats_json(result: &StatsResult) {
|
pub fn print_stats_json(result: &StatsResult, elapsed_ms: u64) {
|
||||||
let output = StatsJsonOutput {
|
let output = StatsJsonOutput {
|
||||||
ok: true,
|
ok: true,
|
||||||
data: StatsResult {
|
data: StatsResult {
|
||||||
@@ -471,6 +473,7 @@ pub fn print_stats_json(result: &StatsResult) {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
meta: RobotMeta { elapsed_ms },
|
||||||
};
|
};
|
||||||
println!("{}", serde_json::to_string(&output).unwrap());
|
println!("{}", serde_json::to_string(&output).unwrap());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use rusqlite::Connection;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
|
use crate::cli::robot::RobotMeta;
|
||||||
use crate::core::db::create_connection;
|
use crate::core::db::create_connection;
|
||||||
use crate::core::error::Result;
|
use crate::core::error::Result;
|
||||||
use crate::core::metrics::StageTiming;
|
use crate::core::metrics::StageTiming;
|
||||||
@@ -190,6 +191,7 @@ fn format_number(n: i64) -> String {
|
|||||||
struct SyncStatusJsonOutput {
|
struct SyncStatusJsonOutput {
|
||||||
ok: bool,
|
ok: bool,
|
||||||
data: SyncStatusJsonData,
|
data: SyncStatusJsonData,
|
||||||
|
meta: RobotMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -238,7 +240,7 @@ struct SummaryJsonInfo {
|
|||||||
system_notes: i64,
|
system_notes: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_sync_status_json(result: &SyncStatusResult) {
|
pub fn print_sync_status_json(result: &SyncStatusResult, elapsed_ms: u64) {
|
||||||
let runs = result
|
let runs = result
|
||||||
.runs
|
.runs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -284,6 +286,7 @@ pub fn print_sync_status_json(result: &SyncStatusResult) {
|
|||||||
system_notes: result.summary.system_note_count,
|
system_notes: result.summary.system_note_count,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
meta: RobotMeta { elapsed_ms },
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("{}", serde_json::to_string(&output).unwrap());
|
println!("{}", serde_json::to_string(&output).unwrap());
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod progress;
|
pub mod progress;
|
||||||
|
pub mod robot;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use std::io::IsTerminal;
|
use std::io::IsTerminal;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "lore")]
|
#[command(name = "lore")]
|
||||||
#[command(version, about = "Local GitLab data management with semantic search", long_about = None)]
|
#[command(version = env!("LORE_VERSION"), about = "Local GitLab data management with semantic search", long_about = None)]
|
||||||
#[command(subcommand_required = false)]
|
#[command(subcommand_required = false)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
/// Path to config file
|
/// Path to config file
|
||||||
@@ -54,9 +55,17 @@ pub struct Cli {
|
|||||||
pub no_quiet: bool,
|
pub no_quiet: bool,
|
||||||
|
|
||||||
/// Increase log verbosity (-v, -vv, -vvv)
|
/// Increase log verbosity (-v, -vv, -vvv)
|
||||||
#[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count, global = true, help = "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,
|
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
|
/// 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")]
|
#[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,
|
pub log_format: String,
|
||||||
@@ -246,6 +255,11 @@ pub enum Commands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[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 {
|
pub struct IssuesArgs {
|
||||||
/// Issue IID (omit to list, provide to show details)
|
/// Issue IID (omit to list, provide to show details)
|
||||||
pub iid: Option<i64>,
|
pub iid: Option<i64>,
|
||||||
@@ -259,7 +273,7 @@ pub struct IssuesArgs {
|
|||||||
)]
|
)]
|
||||||
pub limit: usize,
|
pub limit: usize,
|
||||||
|
|
||||||
/// Select output fields (comma-separated: iid,title,state,author,labels,updated)
|
/// Select output fields (comma-separated, or 'minimal' preset: iid,title,state,updated_at_iso)
|
||||||
#[arg(long, help_heading = "Output", value_delimiter = ',')]
|
#[arg(long, help_heading = "Output", value_delimiter = ',')]
|
||||||
pub fields: Option<Vec<String>>,
|
pub fields: Option<Vec<String>>,
|
||||||
|
|
||||||
@@ -331,6 +345,11 @@ pub struct IssuesArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[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 {
|
pub struct MrsArgs {
|
||||||
/// MR IID (omit to list, provide to show details)
|
/// MR IID (omit to list, provide to show details)
|
||||||
pub iid: Option<i64>,
|
pub iid: Option<i64>,
|
||||||
@@ -344,7 +363,7 @@ pub struct MrsArgs {
|
|||||||
)]
|
)]
|
||||||
pub limit: usize,
|
pub limit: usize,
|
||||||
|
|
||||||
/// Select output fields (comma-separated: iid,title,state,author,labels,updated)
|
/// Select output fields (comma-separated, or 'minimal' preset: iid,title,state,updated_at_iso)
|
||||||
#[arg(long, help_heading = "Output", value_delimiter = ',')]
|
#[arg(long, help_heading = "Output", value_delimiter = ',')]
|
||||||
pub fields: Option<Vec<String>>,
|
pub fields: Option<Vec<String>>,
|
||||||
|
|
||||||
@@ -480,12 +499,17 @@ pub struct StatsArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[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 {
|
pub struct SearchArgs {
|
||||||
/// Search query string
|
/// Search query string
|
||||||
pub query: String,
|
pub query: String,
|
||||||
|
|
||||||
/// Search mode (lexical, hybrid, semantic)
|
/// Search mode (lexical, hybrid, semantic)
|
||||||
#[arg(long, default_value = "hybrid", value_parser = ["lexical", "hybrid", "semantic"], help_heading = "Output")]
|
#[arg(long, default_value = "hybrid", value_parser = ["lexical", "hybrid", "semantic"], help_heading = "Mode")]
|
||||||
pub mode: String,
|
pub mode: String,
|
||||||
|
|
||||||
/// Filter by source type (issue, mr, discussion)
|
/// Filter by source type (issue, mr, discussion)
|
||||||
@@ -533,7 +557,7 @@ pub struct SearchArgs {
|
|||||||
pub no_explain: bool,
|
pub no_explain: bool,
|
||||||
|
|
||||||
/// FTS query mode: safe (default) or raw
|
/// FTS query mode: safe (default) or raw
|
||||||
#[arg(long = "fts-mode", default_value = "safe", value_parser = ["safe", "raw"], help_heading = "Output")]
|
#[arg(long = "fts-mode", default_value = "safe", value_parser = ["safe", "raw"], help_heading = "Mode")]
|
||||||
pub fts_mode: String,
|
pub fts_mode: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,6 +573,11 @@ pub struct GenerateDocsArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[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 --full --force # Full re-sync, override stale lock
|
||||||
|
lore sync --dry-run # Preview what would change")]
|
||||||
pub struct SyncArgs {
|
pub struct SyncArgs {
|
||||||
/// Reset cursors, fetch everything
|
/// Reset cursors, fetch everything
|
||||||
#[arg(long, overrides_with = "no_full")]
|
#[arg(long, overrides_with = "no_full")]
|
||||||
@@ -602,6 +631,10 @@ pub struct EmbedArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
||||||
|
lore timeline 'deployment' # Events related to deployments
|
||||||
|
lore timeline 'auth' --since 30d -p group/repo # Scoped to project and time
|
||||||
|
lore timeline 'migration' --depth 2 --expand-mentions # Deep cross-reference expansion")]
|
||||||
pub struct TimelineArgs {
|
pub struct TimelineArgs {
|
||||||
/// Search query (keywords to find in issues, MRs, and discussions)
|
/// Search query (keywords to find in issues, MRs, and discussions)
|
||||||
pub query: String,
|
pub query: String,
|
||||||
|
|||||||
44
src/cli/robot.rs
Normal file
44
src/cli/robot.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct RobotMeta {
|
||||||
|
pub elapsed_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter JSON object fields in-place for `--fields` support.
|
||||||
|
/// Retains only the specified field names on each item in the list array.
|
||||||
|
pub fn filter_fields(value: &mut serde_json::Value, list_key: &str, fields: &[String]) {
|
||||||
|
if fields.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(items) = value
|
||||||
|
.get_mut("data")
|
||||||
|
.and_then(|d| d.get_mut(list_key))
|
||||||
|
.and_then(|v| v.as_array_mut())
|
||||||
|
{
|
||||||
|
for item in items {
|
||||||
|
if let Some(obj) = item.as_object_mut() {
|
||||||
|
obj.retain(|k, _| fields.iter().any(|f| f == k));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expand the `minimal` preset into concrete field names.
|
||||||
|
pub fn expand_fields_preset(fields: &[String], entity: &str) -> Vec<String> {
|
||||||
|
if fields.len() == 1 && fields[0] == "minimal" {
|
||||||
|
match entity {
|
||||||
|
"issues" => ["iid", "title", "state", "updated_at_iso"]
|
||||||
|
.iter()
|
||||||
|
.map(|s| (*s).to_string())
|
||||||
|
.collect(),
|
||||||
|
"mrs" => ["iid", "title", "state", "updated_at_iso"]
|
||||||
|
.iter()
|
||||||
|
.map(|s| (*s).to_string())
|
||||||
|
.collect(),
|
||||||
|
_ => fields.to_vec(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fields.to_vec()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -239,11 +239,32 @@ impl LoreError {
|
|||||||
self.code().exit_code()
|
self.code().exit_code()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn actions(&self) -> Vec<&'static str> {
|
||||||
|
match self {
|
||||||
|
Self::ConfigNotFound { .. } => vec!["lore init"],
|
||||||
|
Self::ConfigInvalid { .. } => vec!["lore init --force"],
|
||||||
|
Self::GitLabAuthFailed => {
|
||||||
|
vec!["export GITLAB_TOKEN=glpat-xxx", "lore auth"]
|
||||||
|
}
|
||||||
|
Self::TokenNotSet { .. } => vec!["export GITLAB_TOKEN=glpat-xxx"],
|
||||||
|
Self::OllamaUnavailable { .. } => vec!["ollama serve"],
|
||||||
|
Self::OllamaModelNotFound { .. } => vec!["ollama pull nomic-embed-text"],
|
||||||
|
Self::DatabaseLocked { .. } => vec!["lore ingest --force"],
|
||||||
|
Self::EmbeddingsNotBuilt => vec!["lore embed"],
|
||||||
|
Self::EmbeddingFailed { .. } => vec!["lore embed --retry-failed"],
|
||||||
|
Self::MigrationFailed { .. } => vec!["lore migrate"],
|
||||||
|
Self::GitLabNetworkError { .. } => vec!["lore doctor"],
|
||||||
|
_ => vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_robot_error(&self) -> RobotError {
|
pub fn to_robot_error(&self) -> RobotError {
|
||||||
|
let actions = self.actions().into_iter().map(String::from).collect();
|
||||||
RobotError {
|
RobotError {
|
||||||
code: self.code().to_string(),
|
code: self.code().to_string(),
|
||||||
message: self.to_string(),
|
message: self.to_string(),
|
||||||
suggestion: self.suggestion().map(String::from),
|
suggestion: self.suggestion().map(String::from),
|
||||||
|
actions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,6 +275,8 @@ pub struct RobotError {
|
|||||||
pub message: String,
|
pub message: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub suggestion: Option<String>,
|
pub suggestion: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub actions: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
|
|||||||
265
src/main.rs
265
src/main.rs
@@ -23,6 +23,7 @@ use lore::cli::commands::{
|
|||||||
run_list_mrs, run_search, run_show_issue, run_show_mr, run_stats, run_sync, run_sync_status,
|
run_list_mrs, run_search, run_show_issue, run_show_mr, run_stats, run_sync, run_sync_status,
|
||||||
run_timeline,
|
run_timeline,
|
||||||
};
|
};
|
||||||
|
use lore::cli::robot::RobotMeta;
|
||||||
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,
|
SearchArgs, StatsArgs, SyncArgs, TimelineArgs,
|
||||||
@@ -229,7 +230,11 @@ async fn main() {
|
|||||||
target_branch,
|
target_branch,
|
||||||
source_branch,
|
source_branch,
|
||||||
}) => {
|
}) => {
|
||||||
if !robot_mode {
|
if robot_mode {
|
||||||
|
eprintln!(
|
||||||
|
r#"{{"warning":{{"type":"DEPRECATED","message":"'lore list' is deprecated, use 'lore issues' or 'lore mrs'","successor":"issues / mrs"}}}}"#
|
||||||
|
);
|
||||||
|
} else {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{}",
|
"{}",
|
||||||
style("warning: 'lore list' is deprecated, use 'lore issues' or 'lore mrs'")
|
style("warning: 'lore list' is deprecated, use 'lore issues' or 'lore mrs'")
|
||||||
@@ -266,7 +271,11 @@ async fn main() {
|
|||||||
iid,
|
iid,
|
||||||
project,
|
project,
|
||||||
}) => {
|
}) => {
|
||||||
if !robot_mode {
|
if robot_mode {
|
||||||
|
eprintln!(
|
||||||
|
r#"{{"warning":{{"type":"DEPRECATED","message":"'lore show' is deprecated, use 'lore {entity}s {iid}'","successor":"{entity}s"}}}}"#
|
||||||
|
);
|
||||||
|
} else {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{}",
|
"{}",
|
||||||
style(format!(
|
style(format!(
|
||||||
@@ -286,7 +295,11 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
Some(Commands::AuthTest) => {
|
Some(Commands::AuthTest) => {
|
||||||
if !robot_mode {
|
if robot_mode {
|
||||||
|
eprintln!(
|
||||||
|
r#"{{"warning":{{"type":"DEPRECATED","message":"'lore auth-test' is deprecated, use 'lore auth'","successor":"auth"}}}}"#
|
||||||
|
);
|
||||||
|
} else {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{}",
|
"{}",
|
||||||
style("warning: 'lore auth-test' is deprecated, use 'lore auth'").yellow()
|
style("warning: 'lore auth-test' is deprecated, use 'lore auth'").yellow()
|
||||||
@@ -295,7 +308,11 @@ async fn main() {
|
|||||||
handle_auth_test(cli.config.as_deref(), robot_mode).await
|
handle_auth_test(cli.config.as_deref(), robot_mode).await
|
||||||
}
|
}
|
||||||
Some(Commands::SyncStatus) => {
|
Some(Commands::SyncStatus) => {
|
||||||
if !robot_mode {
|
if robot_mode {
|
||||||
|
eprintln!(
|
||||||
|
r#"{{"warning":{{"type":"DEPRECATED","message":"'lore sync-status' is deprecated, use 'lore status'","successor":"status"}}}}"#
|
||||||
|
);
|
||||||
|
} else {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{}",
|
"{}",
|
||||||
style("warning: 'lore sync-status' is deprecated, use 'lore status'").yellow()
|
style("warning: 'lore sync-status' is deprecated, use 'lore status'").yellow()
|
||||||
@@ -498,14 +515,7 @@ fn handle_issues(
|
|||||||
args: IssuesArgs,
|
args: IssuesArgs,
|
||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Warn about unimplemented --fields
|
let start = std::time::Instant::now();
|
||||||
if args.fields.is_some() && !robot_mode {
|
|
||||||
eprintln!(
|
|
||||||
"{}",
|
|
||||||
style("warning: --fields is not yet implemented, showing all fields").yellow()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
let asc = args.asc && !args.no_asc;
|
let asc = args.asc && !args.no_asc;
|
||||||
let has_due = args.has_due && !args.no_has_due;
|
let has_due = args.has_due && !args.no_has_due;
|
||||||
@@ -515,7 +525,7 @@ fn handle_issues(
|
|||||||
if let Some(iid) = args.iid {
|
if let Some(iid) = args.iid {
|
||||||
let result = run_show_issue(&config, iid, args.project.as_deref())?;
|
let result = run_show_issue(&config, iid, args.project.as_deref())?;
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_show_issue_json(&result);
|
print_show_issue_json(&result, start.elapsed().as_millis() as u64);
|
||||||
} else {
|
} else {
|
||||||
print_show_issue(&result);
|
print_show_issue(&result);
|
||||||
}
|
}
|
||||||
@@ -540,7 +550,11 @@ fn handle_issues(
|
|||||||
if open {
|
if open {
|
||||||
open_issue_in_browser(&result);
|
open_issue_in_browser(&result);
|
||||||
} else if robot_mode {
|
} else if robot_mode {
|
||||||
print_list_issues_json(&result);
|
print_list_issues_json(
|
||||||
|
&result,
|
||||||
|
start.elapsed().as_millis() as u64,
|
||||||
|
args.fields.as_deref(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
print_list_issues(&result);
|
print_list_issues(&result);
|
||||||
}
|
}
|
||||||
@@ -554,14 +568,7 @@ fn handle_mrs(
|
|||||||
args: MrsArgs,
|
args: MrsArgs,
|
||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Warn about unimplemented --fields
|
let start = std::time::Instant::now();
|
||||||
if args.fields.is_some() && !robot_mode {
|
|
||||||
eprintln!(
|
|
||||||
"{}",
|
|
||||||
style("warning: --fields is not yet implemented, showing all fields").yellow()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
let asc = args.asc && !args.no_asc;
|
let asc = args.asc && !args.no_asc;
|
||||||
let open = args.open && !args.no_open;
|
let open = args.open && !args.no_open;
|
||||||
@@ -570,7 +577,7 @@ fn handle_mrs(
|
|||||||
if let Some(iid) = args.iid {
|
if let Some(iid) = args.iid {
|
||||||
let result = run_show_mr(&config, iid, args.project.as_deref())?;
|
let result = run_show_mr(&config, iid, args.project.as_deref())?;
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_show_mr_json(&result);
|
print_show_mr_json(&result, start.elapsed().as_millis() as u64);
|
||||||
} else {
|
} else {
|
||||||
print_show_mr(&result);
|
print_show_mr(&result);
|
||||||
}
|
}
|
||||||
@@ -597,7 +604,11 @@ fn handle_mrs(
|
|||||||
if open {
|
if open {
|
||||||
open_mr_in_browser(&result);
|
open_mr_in_browser(&result);
|
||||||
} else if robot_mode {
|
} else if robot_mode {
|
||||||
print_list_mrs_json(&result);
|
print_list_mrs_json(
|
||||||
|
&result,
|
||||||
|
start.elapsed().as_millis() as u64,
|
||||||
|
args.fields.as_deref(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
print_list_mrs(&result);
|
print_list_mrs(&result);
|
||||||
}
|
}
|
||||||
@@ -613,6 +624,7 @@ async fn handle_ingest(
|
|||||||
quiet: bool,
|
quiet: bool,
|
||||||
metrics: &MetricsLayer,
|
metrics: &MetricsLayer,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
let dry_run = args.dry_run && !args.no_dry_run;
|
let dry_run = args.dry_run && !args.no_dry_run;
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
|
|
||||||
@@ -689,7 +701,7 @@ async fn handle_ingest(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_ingest_summary_json(&result);
|
print_ingest_summary_json(&result, start.elapsed().as_millis() as u64);
|
||||||
} else {
|
} else {
|
||||||
print_ingest_summary(&result);
|
print_ingest_summary(&result);
|
||||||
}
|
}
|
||||||
@@ -730,7 +742,11 @@ async fn handle_ingest(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_combined_ingest_json(&issues_result, &mrs_result);
|
print_combined_ingest_json(
|
||||||
|
&issues_result,
|
||||||
|
&mrs_result,
|
||||||
|
start.elapsed().as_millis() as u64,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
print_ingest_summary(&issues_result);
|
print_ingest_summary(&issues_result);
|
||||||
print_ingest_summary(&mrs_result);
|
print_ingest_summary(&mrs_result);
|
||||||
@@ -778,6 +794,7 @@ async fn handle_ingest(
|
|||||||
struct CombinedIngestOutput {
|
struct CombinedIngestOutput {
|
||||||
ok: bool,
|
ok: bool,
|
||||||
data: CombinedIngestData,
|
data: CombinedIngestData,
|
||||||
|
meta: RobotMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -800,6 +817,7 @@ struct CombinedIngestEntityStats {
|
|||||||
fn print_combined_ingest_json(
|
fn print_combined_ingest_json(
|
||||||
issues: &lore::cli::commands::ingest::IngestResult,
|
issues: &lore::cli::commands::ingest::IngestResult,
|
||||||
mrs: &lore::cli::commands::ingest::IngestResult,
|
mrs: &lore::cli::commands::ingest::IngestResult,
|
||||||
|
elapsed_ms: u64,
|
||||||
) {
|
) {
|
||||||
let output = CombinedIngestOutput {
|
let output = CombinedIngestOutput {
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -822,6 +840,7 @@ fn print_combined_ingest_json(
|
|||||||
notes_upserted: mrs.notes_upserted,
|
notes_upserted: mrs.notes_upserted,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
meta: RobotMeta { elapsed_ms },
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("{}", serde_json::to_string(&output).unwrap());
|
println!("{}", serde_json::to_string(&output).unwrap());
|
||||||
@@ -861,12 +880,13 @@ async fn handle_count(
|
|||||||
args: CountArgs,
|
args: CountArgs,
|
||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
|
|
||||||
if args.entity == "events" {
|
if args.entity == "events" {
|
||||||
let counts = run_count_events(&config)?;
|
let counts = run_count_events(&config)?;
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_event_count_json(&counts);
|
print_event_count_json(&counts, start.elapsed().as_millis() as u64);
|
||||||
} else {
|
} else {
|
||||||
print_event_count(&counts);
|
print_event_count(&counts);
|
||||||
}
|
}
|
||||||
@@ -875,7 +895,7 @@ async fn handle_count(
|
|||||||
|
|
||||||
let result = run_count(&config, &args.entity, args.for_entity.as_deref())?;
|
let result = run_count(&config, &args.entity, args.for_entity.as_deref())?;
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_count_json(&result);
|
print_count_json(&result, start.elapsed().as_millis() as u64);
|
||||||
} else {
|
} else {
|
||||||
print_count(&result);
|
print_count(&result);
|
||||||
}
|
}
|
||||||
@@ -886,11 +906,12 @@ async fn handle_sync_status_cmd(
|
|||||||
config_override: Option<&str>,
|
config_override: Option<&str>,
|
||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
|
|
||||||
let result = run_sync_status(&config)?;
|
let result = run_sync_status(&config)?;
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_sync_status_json(&result);
|
print_sync_status_json(&result, start.elapsed().as_millis() as u64);
|
||||||
} else {
|
} else {
|
||||||
print_sync_status(&result);
|
print_sync_status(&result);
|
||||||
}
|
}
|
||||||
@@ -1135,6 +1156,7 @@ async fn handle_init(
|
|||||||
struct AuthTestOutput {
|
struct AuthTestOutput {
|
||||||
ok: bool,
|
ok: bool,
|
||||||
data: AuthTestData,
|
data: AuthTestData,
|
||||||
|
meta: RobotMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -1149,6 +1171,7 @@ async fn handle_auth_test(
|
|||||||
config_override: Option<&str>,
|
config_override: Option<&str>,
|
||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
match run_auth_test(config_override).await {
|
match run_auth_test(config_override).await {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
@@ -1160,6 +1183,9 @@ async fn handle_auth_test(
|
|||||||
name: result.name.clone(),
|
name: result.name.clone(),
|
||||||
gitlab_url: result.base_url.clone(),
|
gitlab_url: result.base_url.clone(),
|
||||||
},
|
},
|
||||||
|
meta: RobotMeta {
|
||||||
|
elapsed_ms: start.elapsed().as_millis() as u64,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
println!("{}", serde_json::to_string(&output)?);
|
println!("{}", serde_json::to_string(&output)?);
|
||||||
} else {
|
} else {
|
||||||
@@ -1189,6 +1215,7 @@ async fn handle_auth_test(
|
|||||||
struct DoctorOutput {
|
struct DoctorOutput {
|
||||||
ok: bool,
|
ok: bool,
|
||||||
data: DoctorData,
|
data: DoctorData,
|
||||||
|
meta: RobotMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -1201,6 +1228,7 @@ async fn handle_doctor(
|
|||||||
config_override: Option<&str>,
|
config_override: Option<&str>,
|
||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
let result = run_doctor(config_override).await;
|
let result = run_doctor(config_override).await;
|
||||||
|
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
@@ -1210,6 +1238,9 @@ async fn handle_doctor(
|
|||||||
success: result.success,
|
success: result.success,
|
||||||
checks: result.checks,
|
checks: result.checks,
|
||||||
},
|
},
|
||||||
|
meta: RobotMeta {
|
||||||
|
elapsed_ms: start.elapsed().as_millis() as u64,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
println!("{}", serde_json::to_string(&output)?);
|
println!("{}", serde_json::to_string(&output)?);
|
||||||
} else {
|
} else {
|
||||||
@@ -1227,6 +1258,7 @@ async fn handle_doctor(
|
|||||||
struct VersionOutput {
|
struct VersionOutput {
|
||||||
ok: bool,
|
ok: bool,
|
||||||
data: VersionData,
|
data: VersionData,
|
||||||
|
meta: RobotMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -1237,6 +1269,7 @@ struct VersionData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_version(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
|
fn handle_version(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
let version = env!("CARGO_PKG_VERSION").to_string();
|
let version = env!("CARGO_PKG_VERSION").to_string();
|
||||||
let git_hash = env!("GIT_HASH").to_string();
|
let git_hash = env!("GIT_HASH").to_string();
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
@@ -1250,6 +1283,9 @@ fn handle_version(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Some(git_hash)
|
Some(git_hash)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
meta: RobotMeta {
|
||||||
|
elapsed_ms: start.elapsed().as_millis() as u64,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
println!("{}", serde_json::to_string(&output)?);
|
println!("{}", serde_json::to_string(&output)?);
|
||||||
} else if git_hash.is_empty() {
|
} else if git_hash.is_empty() {
|
||||||
@@ -1322,6 +1358,7 @@ fn handle_reset(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
struct MigrateOutput {
|
struct MigrateOutput {
|
||||||
ok: bool,
|
ok: bool,
|
||||||
data: MigrateData,
|
data: MigrateData,
|
||||||
|
meta: RobotMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -1347,6 +1384,7 @@ async fn handle_migrate(
|
|||||||
config_override: Option<&str>,
|
config_override: Option<&str>,
|
||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
let db_path = get_db_path(config.storage.db_path.as_deref());
|
let db_path = get_db_path(config.storage.db_path.as_deref());
|
||||||
|
|
||||||
@@ -1395,6 +1433,9 @@ async fn handle_migrate(
|
|||||||
after_version,
|
after_version,
|
||||||
migrated: after_version > before_version,
|
migrated: after_version > before_version,
|
||||||
},
|
},
|
||||||
|
meta: RobotMeta {
|
||||||
|
elapsed_ms: start.elapsed().as_millis() as u64,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
println!("{}", serde_json::to_string(&output)?);
|
println!("{}", serde_json::to_string(&output)?);
|
||||||
} else if after_version > before_version {
|
} else if after_version > before_version {
|
||||||
@@ -1418,12 +1459,13 @@ async fn handle_stats(
|
|||||||
args: StatsArgs,
|
args: StatsArgs,
|
||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
let dry_run = args.dry_run && !args.no_dry_run;
|
let dry_run = args.dry_run && !args.no_dry_run;
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
let check = (args.check && !args.no_check) || args.repair;
|
let check = (args.check && !args.no_check) || args.repair;
|
||||||
let result = run_stats(&config, check, args.repair, dry_run)?;
|
let result = run_stats(&config, check, args.repair, dry_run)?;
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_stats_json(&result);
|
print_stats_json(&result, start.elapsed().as_millis() as u64);
|
||||||
} else {
|
} else {
|
||||||
print_stats(&result);
|
print_stats(&result);
|
||||||
}
|
}
|
||||||
@@ -1505,11 +1547,12 @@ async fn handle_generate_docs(
|
|||||||
args: GenerateDocsArgs,
|
args: GenerateDocsArgs,
|
||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
|
|
||||||
let result = run_generate_docs(&config, args.full, args.project.as_deref(), None)?;
|
let result = run_generate_docs(&config, args.full, args.project.as_deref(), None)?;
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_generate_docs_json(&result);
|
print_generate_docs_json(&result, start.elapsed().as_millis() as u64);
|
||||||
} else {
|
} else {
|
||||||
print_generate_docs(&result);
|
print_generate_docs(&result);
|
||||||
}
|
}
|
||||||
@@ -1521,6 +1564,7 @@ async fn handle_embed(
|
|||||||
args: EmbedArgs,
|
args: EmbedArgs,
|
||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
let full = args.full && !args.no_full;
|
let full = args.full && !args.no_full;
|
||||||
let retry_failed = args.retry_failed && !args.no_retry_failed;
|
let retry_failed = args.retry_failed && !args.no_retry_failed;
|
||||||
@@ -1537,7 +1581,7 @@ async fn handle_embed(
|
|||||||
|
|
||||||
let result = run_embed(&config, full, retry_failed, None, &signal).await?;
|
let result = run_embed(&config, full, retry_failed, None, &signal).await?;
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_embed_json(&result);
|
print_embed_json(&result, start.elapsed().as_millis() as u64);
|
||||||
} else {
|
} else {
|
||||||
print_embed(&result);
|
print_embed(&result);
|
||||||
}
|
}
|
||||||
@@ -1649,6 +1693,7 @@ async fn handle_sync_cmd(
|
|||||||
struct HealthOutput {
|
struct HealthOutput {
|
||||||
ok: bool,
|
ok: bool,
|
||||||
data: HealthData,
|
data: HealthData,
|
||||||
|
meta: RobotMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -1664,6 +1709,7 @@ async fn handle_health(
|
|||||||
config_override: Option<&str>,
|
config_override: Option<&str>,
|
||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
let config_path = get_config_path(config_override);
|
let config_path = get_config_path(config_override);
|
||||||
let config_found = config_path.exists();
|
let config_found = config_path.exists();
|
||||||
|
|
||||||
@@ -1701,6 +1747,9 @@ async fn handle_health(
|
|||||||
schema_current,
|
schema_current,
|
||||||
schema_version,
|
schema_version,
|
||||||
},
|
},
|
||||||
|
meta: RobotMeta {
|
||||||
|
elapsed_ms: start.elapsed().as_millis() as u64,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
println!("{}", serde_json::to_string(&output)?);
|
println!("{}", serde_json::to_string(&output)?);
|
||||||
} else {
|
} else {
|
||||||
@@ -1732,7 +1781,7 @@ async fn handle_health(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !healthy {
|
if !healthy {
|
||||||
std::process::exit(1);
|
std::process::exit(19);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1775,82 +1824,178 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
"description": "Initialize configuration and database",
|
"description": "Initialize configuration and database",
|
||||||
"flags": ["--force", "--non-interactive", "--gitlab-url <URL>", "--token-env-var <VAR>", "--projects <paths>"],
|
"flags": ["--force", "--non-interactive", "--gitlab-url <URL>", "--token-env-var <VAR>", "--projects <paths>"],
|
||||||
"robot_flags": ["--gitlab-url", "--token-env-var", "--projects"],
|
"robot_flags": ["--gitlab-url", "--token-env-var", "--projects"],
|
||||||
"example": "lore --robot init --gitlab-url https://gitlab.com --token-env-var GITLAB_TOKEN --projects group/project"
|
"example": "lore --robot init --gitlab-url https://gitlab.com --token-env-var GITLAB_TOKEN --projects group/project",
|
||||||
|
"response_schema": {
|
||||||
|
"ok": "bool",
|
||||||
|
"data": {"config_path": "string", "data_dir": "string", "user": {"username": "string", "name": "string"}, "projects": "[{path:string, name:string}]"},
|
||||||
|
"meta": {"elapsed_ms": "int"}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"health": {
|
"health": {
|
||||||
"description": "Quick pre-flight check: config, database, schema version",
|
"description": "Quick pre-flight check: config, database, schema version",
|
||||||
"flags": [],
|
"flags": [],
|
||||||
"example": "lore --robot health"
|
"example": "lore --robot health",
|
||||||
|
"response_schema": {
|
||||||
|
"ok": "bool",
|
||||||
|
"data": {"healthy": "bool", "config_found": "bool", "db_found": "bool", "schema_current": "bool", "schema_version": "int"},
|
||||||
|
"meta": {"elapsed_ms": "int"}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"description": "Verify GitLab authentication",
|
"description": "Verify GitLab authentication",
|
||||||
"flags": [],
|
"flags": [],
|
||||||
"example": "lore --robot auth"
|
"example": "lore --robot auth",
|
||||||
|
"response_schema": {
|
||||||
|
"ok": "bool",
|
||||||
|
"data": {"authenticated": "bool", "username": "string", "name": "string", "gitlab_url": "string"},
|
||||||
|
"meta": {"elapsed_ms": "int"}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"doctor": {
|
"doctor": {
|
||||||
"description": "Full environment health check (config, auth, DB, Ollama)",
|
"description": "Full environment health check (config, auth, DB, Ollama)",
|
||||||
"flags": [],
|
"flags": [],
|
||||||
"example": "lore --robot doctor"
|
"example": "lore --robot doctor",
|
||||||
|
"response_schema": {
|
||||||
|
"ok": "bool",
|
||||||
|
"data": {"success": "bool", "checks": "{config:object, auth:object, database:object, ollama:object}"},
|
||||||
|
"meta": {"elapsed_ms": "int"}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"ingest": {
|
"ingest": {
|
||||||
"description": "Sync data from GitLab",
|
"description": "Sync data from GitLab",
|
||||||
"flags": ["--project <path>", "--force", "--no-force", "--full", "--no-full", "--dry-run", "--no-dry-run", "<entity: issues|mrs>"],
|
"flags": ["--project <path>", "--force", "--no-force", "--full", "--no-full", "--dry-run", "--no-dry-run", "<entity: issues|mrs>"],
|
||||||
"example": "lore --robot ingest issues --project group/repo"
|
"example": "lore --robot ingest issues --project group/repo",
|
||||||
|
"response_schema": {
|
||||||
|
"ok": "bool",
|
||||||
|
"data": {"resource_type": "string", "projects_synced": "int", "issues_fetched?": "int", "mrs_fetched?": "int", "upserted": "int", "labels_created": "int", "discussions_fetched": "int", "notes_upserted": "int"},
|
||||||
|
"meta": {"elapsed_ms": "int"}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"sync": {
|
"sync": {
|
||||||
"description": "Full sync pipeline: ingest -> generate-docs -> embed",
|
"description": "Full sync pipeline: ingest -> generate-docs -> embed",
|
||||||
"flags": ["--full", "--no-full", "--force", "--no-force", "--no-embed", "--no-docs", "--no-events", "--dry-run", "--no-dry-run"],
|
"flags": ["--full", "--no-full", "--force", "--no-force", "--no-embed", "--no-docs", "--no-events", "--dry-run", "--no-dry-run"],
|
||||||
"example": "lore --robot sync"
|
"example": "lore --robot sync",
|
||||||
|
"response_schema": {
|
||||||
|
"ok": "bool",
|
||||||
|
"data": {"issues_updated": "int", "mrs_updated": "int", "documents_regenerated": "int", "documents_embedded": "int", "resource_events_synced": "int", "resource_events_failed": "int"},
|
||||||
|
"meta": {"elapsed_ms": "int", "stages?": "[{name:string, elapsed_ms:int, items_processed:int}]"}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"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", "-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": {
|
||||||
|
"list": {
|
||||||
|
"ok": "bool",
|
||||||
|
"data": {"issues": "[{iid:int, title:string, state:string, author_username:string, labels:[string], assignees:[string], discussion_count:int, unresolved_count:int, created_at_iso:string, updated_at_iso:string, web_url:string?, project_path:string}]", "total_count": "int", "showing": "int"},
|
||||||
|
"meta": {"elapsed_ms": "int"}
|
||||||
|
},
|
||||||
|
"show": {
|
||||||
|
"ok": "bool",
|
||||||
|
"data": "IssueDetail (full entity with description, discussions, notes, events)",
|
||||||
|
"meta": {"elapsed_ms": "int"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fields_presets": {"minimal": ["iid", "title", "state", "updated_at_iso"]}
|
||||||
},
|
},
|
||||||
"mrs": {
|
"mrs": {
|
||||||
"description": "List or show merge requests",
|
"description": "List or show merge requests",
|
||||||
"flags": ["<IID>", "-n/--limit", "--fields <list>", "-s/--state", "-p/--project", "-a/--author", "-A/--assignee", "-r/--reviewer", "-l/--label", "--since", "-d/--draft", "-D/--no-draft", "--target", "--source", "--sort", "--asc", "--no-asc", "-o/--open", "--no-open"],
|
"flags": ["<IID>", "-n/--limit", "--fields <list>", "-s/--state", "-p/--project", "-a/--author", "-A/--assignee", "-r/--reviewer", "-l/--label", "--since", "-d/--draft", "-D/--no-draft", "--target", "--source", "--sort", "--asc", "--no-asc", "-o/--open", "--no-open"],
|
||||||
"example": "lore --robot mrs --state opened"
|
"example": "lore --robot mrs --state opened",
|
||||||
|
"response_schema": {
|
||||||
|
"list": {
|
||||||
|
"ok": "bool",
|
||||||
|
"data": {"mrs": "[{iid:int, title:string, state:string, author_username:string, labels:[string], draft:bool, target_branch:string, source_branch:string, discussion_count:int, unresolved_count:int, created_at_iso:string, updated_at_iso:string, web_url:string?, project_path:string, reviewers:[string]}]", "total_count": "int", "showing": "int"},
|
||||||
|
"meta": {"elapsed_ms": "int"}
|
||||||
|
},
|
||||||
|
"show": {
|
||||||
|
"ok": "bool",
|
||||||
|
"data": "MrDetail (full entity with description, discussions, notes, events)",
|
||||||
|
"meta": {"elapsed_ms": "int"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fields_presets": {"minimal": ["iid", "title", "state", "updated_at_iso"]}
|
||||||
},
|
},
|
||||||
"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", "--after", "--updated-after", "-n/--limit", "--explain", "--no-explain", "--fts-mode"],
|
"flags": ["<QUERY>", "--mode", "--type", "--author", "-p/--project", "--label", "--path", "--after", "--updated-after", "-n/--limit", "--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": {
|
||||||
|
"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"},
|
||||||
|
"meta": {"elapsed_ms": "int"}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"count": {
|
"count": {
|
||||||
"description": "Count entities in local database",
|
"description": "Count entities in local database",
|
||||||
"flags": ["<entity: issues|mrs|discussions|notes|events>", "-f/--for <issue|mr>"],
|
"flags": ["<entity: issues|mrs|discussions|notes|events>", "-f/--for <issue|mr>"],
|
||||||
"example": "lore --robot count issues"
|
"example": "lore --robot count issues",
|
||||||
|
"response_schema": {
|
||||||
|
"ok": "bool",
|
||||||
|
"data": {"entity": "string", "count": "int", "system_excluded?": "int", "breakdown?": {"opened": "int", "closed": "int", "merged?": "int", "locked?": "int"}},
|
||||||
|
"meta": {"elapsed_ms": "int"}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"description": "Show document and index statistics",
|
"description": "Show document and index statistics",
|
||||||
"flags": ["--check", "--no-check", "--repair", "--dry-run", "--no-dry-run"],
|
"flags": ["--check", "--no-check", "--repair", "--dry-run", "--no-dry-run"],
|
||||||
"example": "lore --robot stats"
|
"example": "lore --robot stats",
|
||||||
|
"response_schema": {
|
||||||
|
"ok": "bool",
|
||||||
|
"data": {"total_documents": "int", "indexed_documents": "int", "embedded_documents": "int", "stale_documents": "int", "integrity?": "object"},
|
||||||
|
"meta": {"elapsed_ms": "int"}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"description": "Show sync state (cursors, last sync times)",
|
"description": "Show sync state (cursors, last sync times)",
|
||||||
"flags": [],
|
"flags": [],
|
||||||
"example": "lore --robot status"
|
"example": "lore --robot status",
|
||||||
|
"response_schema": {
|
||||||
|
"ok": "bool",
|
||||||
|
"data": {"projects": "[{path:string, issues_cursor:string?, mrs_cursor:string?, last_sync:string?}]"},
|
||||||
|
"meta": {"elapsed_ms": "int"}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"generate-docs": {
|
"generate-docs": {
|
||||||
"description": "Generate searchable documents from ingested data",
|
"description": "Generate searchable documents from ingested data",
|
||||||
"flags": ["--full", "-p/--project <path>"],
|
"flags": ["--full", "-p/--project <path>"],
|
||||||
"example": "lore --robot generate-docs --full"
|
"example": "lore --robot generate-docs --full",
|
||||||
|
"response_schema": {
|
||||||
|
"ok": "bool",
|
||||||
|
"data": {"generated": "int", "updated": "int", "unchanged": "int", "deleted": "int"},
|
||||||
|
"meta": {"elapsed_ms": "int"}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"embed": {
|
"embed": {
|
||||||
"description": "Generate vector embeddings for documents via Ollama",
|
"description": "Generate vector embeddings for documents via Ollama",
|
||||||
"flags": ["--full", "--no-full", "--retry-failed", "--no-retry-failed"],
|
"flags": ["--full", "--no-full", "--retry-failed", "--no-retry-failed"],
|
||||||
"example": "lore --robot embed"
|
"example": "lore --robot embed",
|
||||||
|
"response_schema": {
|
||||||
|
"ok": "bool",
|
||||||
|
"data": {"embedded": "int", "skipped": "int", "failed": "int", "total_chunks": "int"},
|
||||||
|
"meta": {"elapsed_ms": "int"}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"migrate": {
|
"migrate": {
|
||||||
"description": "Run pending database migrations",
|
"description": "Run pending database migrations",
|
||||||
"flags": [],
|
"flags": [],
|
||||||
"example": "lore --robot migrate"
|
"example": "lore --robot migrate",
|
||||||
|
"response_schema": {
|
||||||
|
"ok": "bool",
|
||||||
|
"data": {"before_version": "int", "after_version": "int", "migrated": "bool"},
|
||||||
|
"meta": {"elapsed_ms": "int"}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"description": "Show version information",
|
"description": "Show version information",
|
||||||
"flags": [],
|
"flags": [],
|
||||||
"example": "lore --robot version"
|
"example": "lore --robot version",
|
||||||
|
"response_schema": {
|
||||||
|
"ok": "bool",
|
||||||
|
"data": {"version": "string", "git_hash?": "string"},
|
||||||
|
"meta": {"elapsed_ms": "int"}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"completions": {
|
"completions": {
|
||||||
"description": "Generate shell completions",
|
"description": "Generate shell completions",
|
||||||
@@ -1860,7 +2005,12 @@ 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", "--max-seeds", "--max-entities", "--max-evidence"],
|
||||||
"example": "lore --robot timeline 'authentication' --since 30d"
|
"example": "lore --robot timeline '<keyword>' --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"}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"robot-docs": {
|
"robot-docs": {
|
||||||
"description": "This command (agent self-discovery manifest)",
|
"description": "This command (agent self-discovery manifest)",
|
||||||
@@ -1871,7 +2021,7 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
|
|
||||||
let exit_codes = serde_json::json!({
|
let exit_codes = serde_json::json!({
|
||||||
"0": "Success",
|
"0": "Success",
|
||||||
"1": "Internal error / health check failed / not implemented",
|
"1": "Internal error",
|
||||||
"2": "Usage error (invalid flags or arguments)",
|
"2": "Usage error (invalid flags or arguments)",
|
||||||
"3": "Config invalid",
|
"3": "Config invalid",
|
||||||
"4": "Token not set",
|
"4": "Token not set",
|
||||||
@@ -1889,6 +2039,7 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
"16": "Embedding failed",
|
"16": "Embedding failed",
|
||||||
"17": "Not found",
|
"17": "Not found",
|
||||||
"18": "Ambiguous match",
|
"18": "Ambiguous match",
|
||||||
|
"19": "Health check failed",
|
||||||
"20": "Config not found"
|
"20": "Config not found"
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1953,7 +2104,7 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
aliases,
|
aliases,
|
||||||
exit_codes,
|
exit_codes,
|
||||||
clap_error_codes,
|
clap_error_codes,
|
||||||
error_format: "stderr JSON: {\"error\":{\"code\":\"...\",\"message\":\"...\",\"suggestion\":\"...\"}}".to_string(),
|
error_format: "stderr JSON: {\"error\":{\"code\":\"...\",\"message\":\"...\",\"suggestion\":\"...\",\"actions\":[\"...\"]}}".to_string(),
|
||||||
workflows,
|
workflows,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -1991,6 +2142,7 @@ async fn handle_list_compat(
|
|||||||
target_branch_filter: Option<&str>,
|
target_branch_filter: Option<&str>,
|
||||||
source_branch_filter: Option<&str>,
|
source_branch_filter: Option<&str>,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
|
|
||||||
match entity {
|
match entity {
|
||||||
@@ -2015,7 +2167,7 @@ async fn handle_list_compat(
|
|||||||
if open_browser {
|
if open_browser {
|
||||||
open_issue_in_browser(&result);
|
open_issue_in_browser(&result);
|
||||||
} else if json_output {
|
} else if json_output {
|
||||||
print_list_issues_json(&result);
|
print_list_issues_json(&result, start.elapsed().as_millis() as u64, None);
|
||||||
} else {
|
} else {
|
||||||
print_list_issues(&result);
|
print_list_issues(&result);
|
||||||
}
|
}
|
||||||
@@ -2045,7 +2197,7 @@ async fn handle_list_compat(
|
|||||||
if open_browser {
|
if open_browser {
|
||||||
open_mr_in_browser(&result);
|
open_mr_in_browser(&result);
|
||||||
} else if json_output {
|
} else if json_output {
|
||||||
print_list_mrs_json(&result);
|
print_list_mrs_json(&result, start.elapsed().as_millis() as u64, None);
|
||||||
} else {
|
} else {
|
||||||
print_list_mrs(&result);
|
print_list_mrs(&result);
|
||||||
}
|
}
|
||||||
@@ -2066,13 +2218,14 @@ async fn handle_show_compat(
|
|||||||
project_filter: Option<&str>,
|
project_filter: Option<&str>,
|
||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
|
|
||||||
match entity {
|
match entity {
|
||||||
"issue" => {
|
"issue" => {
|
||||||
let result = run_show_issue(&config, iid, project_filter)?;
|
let result = run_show_issue(&config, iid, project_filter)?;
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_show_issue_json(&result);
|
print_show_issue_json(&result, start.elapsed().as_millis() as u64);
|
||||||
} else {
|
} else {
|
||||||
print_show_issue(&result);
|
print_show_issue(&result);
|
||||||
}
|
}
|
||||||
@@ -2081,7 +2234,7 @@ async fn handle_show_compat(
|
|||||||
"mr" => {
|
"mr" => {
|
||||||
let result = run_show_mr(&config, iid, project_filter)?;
|
let result = run_show_mr(&config, iid, project_filter)?;
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_show_mr_json(&result);
|
print_show_mr_json(&result, start.elapsed().as_millis() as u64);
|
||||||
} else {
|
} else {
|
||||||
print_show_mr(&result);
|
print_show_mr(&result);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user