use super::error::Result;
use super::r#trait::{Tool, ToolCapability, ToolExecutionContext, ToolResult};
use crate::brain::mission_control::types::{McActivity, McAnalytics, McInboxItem, McScheduleItem};
use crate::brain::mission_control::{
activity_service, analytics_service, inbox_service, schedule_service,
};
use crate::db::Pool;
use async_trait::async_trait;
use serde_json::Value;
pub struct MissionControlReportTool {
pool: Pool,
}
impl MissionControlReportTool {
pub fn new(pool: Pool) -> Self {
Self { pool }
}
}
#[async_trait]
impl Tool for MissionControlReportTool {
fn name(&self) -> &str {
"mission_control_report"
}
fn description(&self) -> &str {
"Generate a shareable mission control report of this OpenCrabs instance: analytics \
(tool usage, failure rates, RSI improvements, brain files), activity feed, inbox \
proposals, and scheduled cron jobs. Returns Markdown you can send straight to a chat. \
Reads only aggregate stats from the local database, brain file sizes, RSI proposals, \
and cron jobs. Never exposes message content or secrets."
}
fn input_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {},
"additionalProperties": false
})
}
fn capabilities(&self) -> Vec<ToolCapability> {
vec![ToolCapability::ReadFiles]
}
fn requires_approval(&self) -> bool {
false
}
async fn execute(&self, _input: Value, _context: &ToolExecutionContext) -> Result<ToolResult> {
let analytics = analytics_service::summary(self.pool.clone()).await;
let activity = activity_service::recent(5);
let inbox = inbox_service::list();
let schedule = schedule_service::list(self.pool.clone()).await;
Ok(ToolResult::success(render_markdown(
&analytics, &activity, &inbox, &schedule,
)))
}
}
fn activity_icon(level: &crate::brain::mission_control::types::McActivityLevel) -> &'static str {
use crate::brain::mission_control::types::McActivityLevel;
match level {
McActivityLevel::Success => "✅",
McActivityLevel::Warn => "⚠️",
McActivityLevel::Error => "❌",
McActivityLevel::Info => "ℹ️",
}
}
fn inbox_icon(kind: &crate::brain::mission_control::types::McInboxKind) -> &'static str {
use crate::brain::mission_control::types::McInboxKind;
match kind {
McInboxKind::ProposedTool => "🔧",
McInboxKind::ProposedCommand => "📋",
McInboxKind::ProposedSkill => "🧠",
McInboxKind::ProposedBrainDedup => "🧹",
}
}
fn schedule_icon(item: &McScheduleItem) -> &'static str {
if item.awaiting_user { "⏸️" } else { "⏰" }
}
pub(crate) fn render_markdown(
a: &McAnalytics,
activity: &[McActivity],
inbox: &[McInboxItem],
schedule: &[McScheduleItem],
) -> String {
use crate::utils::string::md_table;
let fail_pct = if a.tool_total_calls > 0 {
(a.tool_total_fails as f64 / a.tool_total_calls as f64) * 100.0
} else {
0.0
};
let mut blocks: Vec<String> = vec!["# 🦀 Mission Control".to_string()];
blocks.push("## 📊 Analytics".to_string());
blocks.push(
md_table(
&["Metric", "Value"],
&[
vec![
"Tools".to_string(),
format!(
"{} calls, {} fails ({:.1}%)",
a.tool_total_calls, a.tool_total_fails, fail_pct
),
],
vec!["RSI applied".to_string(), a.rsi_applied_total.to_string()],
vec![
"Brain".to_string(),
format!(
"{:.1} KB across {} files",
a.brain_total_kb,
a.brain_files.len()
),
],
],
)
.trim_end()
.to_string(),
);
let mut table_section = |title: &str, headers: &[&str], rows: Vec<Vec<String>>| {
if !rows.is_empty() {
blocks.push(format!("### {title}"));
blocks.push(md_table(headers, &rows).trim_end().to_string());
}
};
table_section(
"Top Tools",
&["Tool", "Calls", "Fail"],
a.top_tools
.iter()
.take(10)
.map(|t| {
vec![
t.name.clone(),
format!("{} calls", t.total),
format!("{:.1}% fail", t.fail_rate),
]
})
.collect(),
);
table_section(
"Flakiest (>=5 calls)",
&["Tool", "Fail", "Calls"],
a.flakiest_tools
.iter()
.take(8)
.map(|t| {
vec![
t.name.clone(),
format!("{:.1}% fail", t.fail_rate),
format!("{} calls", t.total),
]
})
.collect(),
);
table_section(
"RSI by Dimension",
&["Dimension", "Count"],
a.rsi_top_dimensions
.iter()
.take(8)
.map(|(dim, n)| vec![dim.clone(), n.to_string()])
.collect(),
);
table_section(
"Brain Files",
&["File", "Size"],
a.brain_files
.iter()
.take(10)
.map(|f| vec![f.name.clone(), format!("{:.1} KB", f.kb)])
.collect(),
);
if !inbox.is_empty() {
let rows: Vec<Vec<String>> = inbox
.iter()
.take(5)
.map(|item| {
vec![
format!("{} {}", inbox_icon(&item.kind), item.label),
item.summary.clone(),
]
})
.collect();
blocks.push("## 📥 Inbox (RSI Proposals)".to_string());
blocks.push(
md_table(&["Proposal", "Detail"], &rows)
.trim_end()
.to_string(),
);
}
if !activity.is_empty() {
let rows: Vec<Vec<String>> = activity
.iter()
.take(5)
.map(|entry| {
vec![
format!(
"{} {}",
activity_icon(&entry.level),
entry.timestamp.format("%Y-%m-%d")
),
entry.detail.clone(),
]
})
.collect();
blocks.push("## 📰 Activity Feed".to_string());
blocks.push(md_table(&["When", "Detail"], &rows).trim_end().to_string());
}
if !schedule.is_empty() {
let rows: Vec<Vec<String>> = schedule
.iter()
.map(|item| {
vec![
format!("{} {}", schedule_icon(item), item.label),
item.schedule.clone(),
]
})
.collect();
blocks.push("## ⏰ Schedule".to_string());
blocks.push(md_table(&["Job", "Schedule"], &rows).trim_end().to_string());
}
blocks.join("\n\n")
}