feat(desktop): embed BoosAPI Desktop client as a subproject

Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as
a monorepo subdirectory under desktop/. Source only — node_modules,
dist, Cargo target and the upstream .git history are excluded.

desktop/ keeps its own independent Cargo workspace (desktop/src-tauri
with crates/*); it is not part of the backend boosapi crate, so a
`cargo check` at the metapi root still compiles only the backend. Build
the desktop app from desktop/src-tauri per desktop/README.md.

Note: app icons are still placeholder assets pending the final BoosAPI
logo. The updater signing keys live outside the repo and are not
included.
This commit is contained in:
boos
2026-06-22 11:32:57 +08:00
parent d9c78ecc73
commit e0a5f57793
2305 changed files with 449355 additions and 0 deletions
@@ -0,0 +1,297 @@
use serde_json::Value;
use std::sync::Arc;
/// Hook event types.
#[derive(Debug, Clone, PartialEq)]
pub enum HookEvent {
PreToolUse,
PostToolUse,
PostToolUseFailure,
PostSampling,
SessionStart,
SessionEnd,
Stop,
SubagentStart,
SubagentStop,
UserPromptSubmit,
PermissionRequest,
PermissionDenied,
TaskCreated,
TaskCompleted,
ConfigChange,
CwdChanged,
FileChanged,
Notification,
PreCompact,
PostCompact,
TeammateIdle,
}
/// Input passed to a hook handler.
#[derive(Debug, Clone)]
pub struct HookInput {
pub event: HookEvent,
pub tool_name: Option<String>,
pub tool_input: Option<Value>,
pub tool_output: Option<String>,
pub tool_use_id: Option<String>,
pub session_id: Option<String>,
pub cwd: Option<String>,
pub error: Option<String>,
}
/// Output from a hook handler.
#[derive(Debug, Clone, Default)]
pub struct HookOutput {
/// If true, block the tool execution.
pub blocked: bool,
/// Optional message to return instead.
pub message: Option<String>,
/// Permission update (tool name + behavior).
pub permission_update: Option<PermissionUpdate>,
/// Notification to emit.
pub notification: Option<HookNotification>,
}
/// Permission update returned by a hook.
#[derive(Debug, Clone)]
pub struct PermissionUpdate {
pub tool: String,
pub behavior: PermissionBehavior,
}
/// Permission behavior: allow or deny.
#[derive(Debug, Clone, PartialEq)]
pub enum PermissionBehavior {
Allow,
Deny,
}
/// Notification emitted by a hook.
#[derive(Debug, Clone)]
pub struct HookNotification {
pub title: String,
pub body: String,
pub level: Option<NotificationLevel>,
}
/// Notification severity level.
#[derive(Debug, Clone, PartialEq)]
pub enum NotificationLevel {
Info,
Warning,
Error,
}
/// Hook function type.
pub type HookFn =
Arc<dyn Fn(HookInput) -> futures::future::BoxFuture<'static, HookOutput> + Send + Sync>;
/// A hook rule with a matcher pattern and handler.
pub struct HookRule {
pub matcher: String,
pub handler: HookFn,
}
/// Hook configuration for the agent.
pub struct HookConfig {
pub pre_tool_use: Vec<HookRule>,
pub post_tool_use: Vec<HookRule>,
pub post_tool_use_failure: Vec<HookRule>,
pub post_sampling: Vec<HookRule>,
pub session_start: Vec<HookRule>,
pub session_end: Vec<HookRule>,
pub stop: Vec<HookRule>,
pub subagent_start: Vec<HookRule>,
pub subagent_stop: Vec<HookRule>,
pub user_prompt_submit: Vec<HookRule>,
pub permission_request: Vec<HookRule>,
pub permission_denied: Vec<HookRule>,
pub task_created: Vec<HookRule>,
pub task_completed: Vec<HookRule>,
pub config_change: Vec<HookRule>,
pub cwd_changed: Vec<HookRule>,
pub file_changed: Vec<HookRule>,
pub notification: Vec<HookRule>,
pub pre_compact: Vec<HookRule>,
pub post_compact: Vec<HookRule>,
pub teammate_idle: Vec<HookRule>,
}
impl Default for HookConfig {
fn default() -> Self {
Self {
pre_tool_use: Vec::new(),
post_tool_use: Vec::new(),
post_tool_use_failure: Vec::new(),
post_sampling: Vec::new(),
session_start: Vec::new(),
session_end: Vec::new(),
stop: Vec::new(),
subagent_start: Vec::new(),
subagent_stop: Vec::new(),
user_prompt_submit: Vec::new(),
permission_request: Vec::new(),
permission_denied: Vec::new(),
task_created: Vec::new(),
task_completed: Vec::new(),
config_change: Vec::new(),
cwd_changed: Vec::new(),
file_changed: Vec::new(),
notification: Vec::new(),
pre_compact: Vec::new(),
post_compact: Vec::new(),
teammate_idle: Vec::new(),
}
}
}
impl HookConfig {
/// Get the hook rules for a given event.
pub fn rules_for_event(&self, event: &HookEvent) -> &[HookRule] {
match event {
HookEvent::PreToolUse => &self.pre_tool_use,
HookEvent::PostToolUse => &self.post_tool_use,
HookEvent::PostToolUseFailure => &self.post_tool_use_failure,
HookEvent::PostSampling => &self.post_sampling,
HookEvent::SessionStart => &self.session_start,
HookEvent::SessionEnd => &self.session_end,
HookEvent::Stop => &self.stop,
HookEvent::SubagentStart => &self.subagent_start,
HookEvent::SubagentStop => &self.subagent_stop,
HookEvent::UserPromptSubmit => &self.user_prompt_submit,
HookEvent::PermissionRequest => &self.permission_request,
HookEvent::PermissionDenied => &self.permission_denied,
HookEvent::TaskCreated => &self.task_created,
HookEvent::TaskCompleted => &self.task_completed,
HookEvent::ConfigChange => &self.config_change,
HookEvent::CwdChanged => &self.cwd_changed,
HookEvent::FileChanged => &self.file_changed,
HookEvent::Notification => &self.notification,
HookEvent::PreCompact => &self.pre_compact,
HookEvent::PostCompact => &self.post_compact,
HookEvent::TeammateIdle => &self.teammate_idle,
}
}
/// Run hooks for a given event with tool context.
pub async fn run_event(
&self,
event: HookEvent,
tool_name: Option<&str>,
tool_input: Option<&Value>,
tool_output: Option<&str>,
) -> Vec<HookOutput> {
let rules = self.rules_for_event(&event);
let mut outputs = Vec::new();
for rule in rules {
if let Some(name) = tool_name {
if !matches_tool(&rule.matcher, name) {
continue;
}
}
let input = HookInput {
event: event.clone(),
tool_name: tool_name.map(|s| s.to_string()),
tool_input: tool_input.cloned(),
tool_output: tool_output.map(|s| s.to_string()),
tool_use_id: None,
session_id: None,
cwd: None,
error: None,
};
let output = (rule.handler)(input).await;
outputs.push(output);
}
outputs
}
/// Run pre-tool-use hooks for a given tool name.
pub async fn run_pre_tool_use(
&self,
tool_name: &str,
tool_input: &Value,
) -> Option<HookOutput> {
for rule in &self.pre_tool_use {
if matches_tool(&rule.matcher, tool_name) {
let input = HookInput {
event: HookEvent::PreToolUse,
tool_name: Some(tool_name.to_string()),
tool_input: Some(tool_input.clone()),
tool_output: None,
tool_use_id: None,
session_id: None,
cwd: None,
error: None,
};
let output = (rule.handler)(input).await;
if output.blocked {
return Some(output);
}
}
}
None
}
/// Run post-tool-use hooks for a given tool name.
pub async fn run_post_tool_use(
&self,
tool_name: &str,
tool_input: &Value,
tool_output: &str,
) {
for rule in &self.post_tool_use {
if matches_tool(&rule.matcher, tool_name) {
let input = HookInput {
event: HookEvent::PostToolUse,
tool_name: Some(tool_name.to_string()),
tool_input: Some(tool_input.clone()),
tool_output: Some(tool_output.to_string()),
tool_use_id: None,
session_id: None,
cwd: None,
error: None,
};
(rule.handler)(input).await;
}
}
}
/// Run stop hooks.
pub async fn run_stop(&self) {
for rule in &self.stop {
let input = HookInput {
event: HookEvent::Stop,
tool_name: None,
tool_input: None,
tool_output: None,
tool_use_id: None,
session_id: None,
cwd: None,
error: None,
};
(rule.handler)(input).await;
}
}
}
/// Check if a tool name matches a hook matcher pattern.
fn matches_tool(matcher: &str, tool_name: &str) -> bool {
if matcher == "*" || matcher.is_empty() {
return true;
}
// Support pipe-separated patterns: "Bash|Edit|Write"
if matcher.contains('|') {
return matcher.split('|').any(|p| matches_tool(p.trim(), tool_name));
}
// Support prefix matching: "mcp__*"
if matcher.ends_with('*') {
let prefix = &matcher[..matcher.len() - 1];
return tool_name.starts_with(prefix);
}
// Exact match
matcher == tool_name
}