diff --git a/crates/miyabi-cli/src/main.rs b/crates/miyabi-cli/src/main.rs index 92517bb..96d0938 100644 --- a/crates/miyabi-cli/src/main.rs +++ b/crates/miyabi-cli/src/main.rs @@ -2,6 +2,7 @@ use clap::{Parser, Subcommand}; use miyabi_core::{FeatureFlagManager, RulesLoader}; +use std::collections::HashMap; use std::path::PathBuf; use tracing_subscriber::EnvFilter; @@ -94,6 +95,41 @@ enum Commands { #[arg(long)] system: Option, }, + /// OpenClaw integration - control OpenClaw agents + Openclaw { + /// OpenClaw subcommand + #[command(subcommand)] + command: OpenclawCommand, + }, +} + +#[derive(Subcommand)] +enum OpenclawCommand { + /// List all available agents + Agents, + /// Show OpenClaw status + Status, + /// Show detailed help for OpenClaw commands + Help, + /// Send a message to an agent + Send { + /// Agent name (e.g., maestro, kade, sakura) + agent: String, + /// Message to send + message: String, + }, + /// Broadcast a message to all agents + Broadcast { + /// Message to broadcast + message: String, + }, + /// Broadcast to a specific society + BroadcastSociety { + /// Society name (core, investment, content, marketing) + society: String, + /// Message to broadcast + message: String, + }, } #[tokio::main] @@ -554,6 +590,232 @@ async fn main() -> anyhow::Result<()> { } } } + Some(Commands::Openclaw { command }) => { + use miyabi_core::openclaw::{OpenClawClient, OpenClawResult}; + use std::env; + + // Get OpenClaw configuration + let gateway_url = env::var("OPENCLAW_GATEWAY_URL") + .unwrap_or_else(|_| "http://127.0.0.1:18789".to_string()); + let token = env::var("OPENCLAW_TOKEN") + .unwrap_or_else(|_| { + // Try to read from openclaw.json + #[allow(unused_imports)] + use std::fs; + #[allow(unused_imports)] + use std::path::PathBuf; + + let config_path = PathBuf::from(env::var("HOME").unwrap_or_default()) + .join(".openclaw") + .join("openclaw.json"); + + // Fallback: try to read token from config + if let Ok(content) = fs::read_to_string(&config_path) { + if let Ok(json) = serde_json::from_str::(&content) { + if let Some(gateway) = json.get("gateway") { + if let Some(auth) = gateway.get("auth") { + if let Some(t) = auth.get("token") { + return t.as_str().unwrap_or("").to_string(); + } + } + } + } + } + + String::new() + }); + + if token.is_empty() { + eprintln!("❌ Error: OPENCLAW_TOKEN not set"); + eprintln!(" Set environment variable: export OPENCLAW_TOKEN=your_token"); + eprintln!(" Or add to ~/.miyabi/config.toml"); + return Ok(()); + } + + let client = OpenClawClient::new(gateway_url.clone(), token.clone()); + + // Handle Status command separately to avoid borrowing issues + if let OpenclawCommand::Status = command { + let token_display = if token.len() > 4 { + format!("{}***", &token[..4]) + } else { + "***".to_string() + }; + println!("📊 OpenClaw Status:"); + println!(" Gateway: {}", gateway_url); + println!(" Token: {}", token_display); + return Ok(()); + } + + let client = OpenClawClient::new(gateway_url, token); + + match command { + OpenclawCommand::Agents => { + // List agents grouped by society + let agents = OpenClawClient::get_agents(); + println!("🎭 Miyabi エージェント一覧 ({} agents):", agents.len()); + println!(); + + // Group by society + let mut grouped: HashMap<&str, Vec<_>> = HashMap::new(); + for agent in &agents { + grouped.entry(&agent.society).or_default().push(agent); + } + + // Define society order + let society_order = vec!["Core", "Investment", "Content", "Marketing"]; + + for society in &society_order { + if let Some(society_agents) = grouped.get(*society) { + println!("【{} Society】", society); + for agent in society_agents { + println!(" {} {} ({})", agent.emoji, agent.name, agent.id); + println!(" Role: {}", agent.role); + } + println!(); + } + } + } + OpenclawCommand::Send { agent, message } => { + // Send message + let resolved_agent = OpenClawClient::resolve_agent_alias(&agent); + match client.send(&resolved_agent, &message).await { + Ok(_msg) => { + println!("✓ メッセージを送信しました:"); + println!(" Agent: {}", resolved_agent); + println!(" Message: {}", message); + } + Err(e) => { + eprintln!("❌ 送信エラー: {}", e); + } + } + } + OpenclawCommand::Broadcast { message } => { + // Broadcast message to all core agents + match client.broadcast(&message).await { + Ok(results) => { + println!("✓ ブロードキャストを送信しました:"); + println!(" Message: {}", message); + println!(); + for result in results { + println!(" {}", result); + } + } + Err(e) => { + eprintln!("❌ ブロードキャストエラー: {}", e); + } + } + } + OpenclawCommand::BroadcastSociety { society, message } => { + // Broadcast to specific society + let society_agents = match society.to_lowercase().as_str() { + "core" => vec!["maestro", "kade", "sakura", "tsubaki", "botan", "nagare"], + "investment" => vec!["scout", "crystal", "dealer", "sentinel", "architect", "watchman", "chart", "fundy", "scribe"], + "content" => vec!["tweeter", "pen", "vidpro", "artist", "optimizer", "scheduler"], + "marketing" => vec!["hiro", "kazoeru", "funnel", "adops"], + _ => { + eprintln!("❌ 不明なSociety: {}", society); + eprintln!(" 利用可能: core, investment, content, marketing"); + return Ok(()); + } + }; + + println!("✓ {} Society にブロードキャスト:", society); + println!(" Message: {}", message); + println!(); + + for agent in society_agents { + match client.send(agent, &message).await { + Ok(_) => println!(" ✓ {}", agent), + Err(e) => eprintln!(" ❌ {}: {}", agent, e), + } + } + } + OpenclawCommand::Help => { + // Show detailed help + println!("📖 Miyabi OpenClaw CLI - 詳細ヘルプ"); + println!(); + println!("【基本コマンド】"); + println!(); + println!(" miyabi openclaw agents"); + println!(" → 全エージェント一覧を表示 (Society別)"); + println!(); + println!(" miyabi openclaw status"); + println!(" → OpenClaw Gatewayの状態を確認"); + println!(); + println!(" miyabi openclaw send "); + println!(" → 特定のエージェントにメッセージを送信"); + println!(); + println!(" miyabi openclaw broadcast "); + println!(" → 全コアエージェントにブロードキャスト"); + println!(); + println!(" miyabi openclaw broadcast-society "); + println!(" → 特定のSocietyにブロードキャスト"); + println!(); + println!("【エージェントIDとエイリアス】"); + println!(); + println!(" Core Society (6):"); + println!(" maestro しきるん🎭 - shikirun, conductor, orchestrator"); + println!(" kade カエデ🍁 - kaede, creator, codegen, developer"); + println!(" sakura サクラ🌸 - reviewer, qa, critic"); + println!(" tsubaki ツバキ🌺 - integrator, pr-manager, merge-bot"); + println!(" botan ボタン🌼 - deployer, release-manager, deployment"); + println!(" nagare ながれるん🌊 - nagarerun, workflow, automation"); + println!(); + println!(" Investment Society (9):"); + println!(" scout スカウト🔍 - researcher, explorer"); + println!(" crystal クリスタル💎 - valuer, analyst"); + println!(" dealer ディーラー🎰 - trader, executor"); + println!(" sentinel センチネル🛡️ - risk-manager, guardian-rm"); + println!(" architect アーキテクト🏗️ - portfolio-manager, allocator"); + println!(" watchman ウォッチマン👁️ - news-monitor, sentinel-news"); + println!(" chart チャート📈 - technical-analyst, chart-reader"); + println!(" fundy ファンディ📊 - fundamental-analyst, value-investor"); + println!(" scribe スクライブ📝 - reporter, documenter"); + println!(); + println!(" Content Society (6):"); + println!(" tweeter ツイーター🐦 - twitter-specialist, x-poster"); + println!(" pen ペン✒️ - writer, author"); + println!(" vidpro ビッドプロ🎬 - video-producer, youtuber"); + println!(" artist アーティスト🎨 - designer, visual-creator"); + println!(" optimizer オプティマイザー🔧 - seo-specialist, seo-analyst"); + println!(" scheduler スケジューラー📅 - calendar-manager, planner"); + println!(); + println!(" Marketing Society (4):"); + println!(" hiro ヒロ🚀 - promoter, growth-hacker"); + println!(" kazoeru カゾエル🔢 - metrics-tracker, data-analyst"); + println!(" funnel ファネル🌪️ - conversion-optimizer, cro-specialist"); + println!(" adops アドオプス📢 - ad-manager, media-buyer"); + println!(); + println!("【使用例】"); + println!(); + println!(" # 個別送信"); + println!(" miyabi openclaw send maestro \"実装タスクを割り当てて\""); + println!(" miyabi openclaw send kade \"コードレビューお願いします\""); + println!(" miyabi openclaw send shikirun \"エイリアスでもOK\""); + println!(); + println!(" # ブロードキャスト"); + println!(" miyabi openclaw broadcast \"システムメンテナンス開始\""); + println!(" miyabi openclaw broadcast-society content \"新記事投稿\""); + println!(); + println!("【環境変数】"); + println!(); + println!(" OPENCLAW_GATEWAY_URL - Gateway URL (default: http://127.0.0.1:18789)"); + println!(" OPENCLAW_TOKEN - Gateway認証トークン"); + println!(); + println!("【設定ファイル】"); + println!(); + println!(" ~/.openclaw/openclaw.json - 設定ファイルから自動読み込み"); + println!(); + println!("--- + 🌸 Miyabi Framework - OpenClaw Integration"); + } + OpenclawCommand::Status => { + // Already handled above + unreachable!(); + } + } + } } Ok(()) diff --git a/crates/miyabi-core/src/lib.rs b/crates/miyabi-core/src/lib.rs index 8ec15d2..bf535b0 100644 --- a/crates/miyabi-core/src/lib.rs +++ b/crates/miyabi-core/src/lib.rs @@ -90,6 +90,8 @@ pub use workflow::{ pub use mcp::{ McpConfig, McpError, McpManager, McpRequest, McpResponse, McpServer, McpServerConfig, McpTool, }; +pub mod openclaw; +pub use openclaw::{AgentInfo, OpenClawClient, OpenClawError, OpenClawResult}; pub use dag::{ DAGError, Task as DAGTask, TaskGraph, TaskGraphBuilder, TaskId, TaskLevel, TaskNode, }; diff --git a/crates/miyabi-core/src/openclaw.rs b/crates/miyabi-core/src/openclaw.rs new file mode 100644 index 0000000..5d7215b --- /dev/null +++ b/crates/miyabi-core/src/openclaw.rs @@ -0,0 +1,354 @@ +//! Miyabi OpenClaw Integration +//! +//! This module provides OpenClaw CLI wrapper functionality for Miyabi. + +use reqwest::Client; +use serde::Serialize; +use std::time::Duration; + +/// OpenClaw client for communicating with the Gateway +#[derive(Clone)] +pub struct OpenClawClient { + gateway_url: String, + token: String, + client: Client, +} + +impl OpenClawClient { + /// Create a new OpenClaw client + pub fn new(gateway_url: String, token: String) -> Self { + let client = Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .expect("Failed to create HTTP client"); + + Self { + gateway_url, + token, + client, + } + } + + /// Send a message to an agent via the Gateway + pub async fn send(&self, agent: &str, message: &str) -> Result { + let payload = SendMessageRequest { + agent_id: agent.to_string(), + message: message.to_string(), + }; + + let response = self.client + .post(format!("{}/api/message", self.gateway_url)) + .header("Authorization", format!("Bearer {}", self.token)) + .json(&payload) + .send() + .await + .map_err(|e| OpenClawError::Network(e.to_string()))?; + + if response.status().is_success() { + Ok("Message sent successfully".to_string()) + } else { + let status = response.status().as_u16(); + let body = response.text().await.unwrap_or_default(); + Err(OpenClawError::Api(status, body)) + } + } + + /// Broadcast a message to all core agents + pub async fn broadcast(&self, message: &str) -> Result, OpenClawError> { + let agents = ["maestro", "kade", "sakura", "tsubaki", "botan", "nagare"]; + let mut results = Vec::new(); + + for agent in agents { + match self.send(agent, message).await { + Ok(msg) => results.push(format!("{}: {}", agent, msg)), + Err(e) => results.push(format!("{}: {}", agent, e)), + } + } + + Ok(results) + } + + /// Get agent list + pub fn get_agents() -> Vec { + vec![ + // Core Society + AgentInfo { + id: "maestro".to_string(), + name: "しきるん".to_string(), + emoji: "🎭".to_string(), + role: "Conductor".to_string(), + society: "Core".to_string(), + }, + AgentInfo { + id: "kade".to_string(), + name: "カエデ".to_string(), + emoji: "🍁".to_string(), + role: "CodeGen".to_string(), + society: "Core".to_string(), + }, + AgentInfo { + id: "sakura".to_string(), + name: "サクラ".to_string(), + emoji: "🌸".to_string(), + role: "Review".to_string(), + society: "Core".to_string(), + }, + AgentInfo { + id: "tsubaki".to_string(), + name: "ツバキ".to_string(), + emoji: "🌺".to_string(), + role: "PR".to_string(), + society: "Core".to_string(), + }, + AgentInfo { + id: "botan".to_string(), + name: "ボタン".to_string(), + emoji: "🌼".to_string(), + role: "Deploy".to_string(), + society: "Core".to_string(), + }, + AgentInfo { + id: "nagare".to_string(), + name: "ながれるん".to_string(), + emoji: "🌊".to_string(), + role: "Workflow".to_string(), + society: "Core".to_string(), + }, + // Investment Society + AgentInfo { + id: "scout".to_string(), + name: "スカウト".to_string(), + emoji: "🔍".to_string(), + role: "Explorer".to_string(), + society: "Investment".to_string(), + }, + AgentInfo { + id: "crystal".to_string(), + name: "クリスタル".to_string(), + emoji: "💎".to_string(), + role: "Valuer".to_string(), + society: "Investment".to_string(), + }, + AgentInfo { + id: "dealer".to_string(), + name: "ディーラー".to_string(), + emoji: "🎰".to_string(), + role: "Trader".to_string(), + society: "Investment".to_string(), + }, + AgentInfo { + id: "sentinel".to_string(), + name: "センチネル".to_string(), + emoji: "🛡️".to_string(), + role: "Risk Manager".to_string(), + society: "Investment".to_string(), + }, + AgentInfo { + id: "architect".to_string(), + name: "アーキテクト".to_string(), + emoji: "🏗️".to_string(), + role: "Portfolio Manager".to_string(), + society: "Investment".to_string(), + }, + AgentInfo { + id: "watchman".to_string(), + name: "ウォッチマン".to_string(), + emoji: "👁️".to_string(), + role: "News Monitor".to_string(), + society: "Investment".to_string(), + }, + AgentInfo { + id: "chart".to_string(), + name: "チャート".to_string(), + emoji: "📈".to_string(), + role: "Technical Analyst".to_string(), + society: "Investment".to_string(), + }, + AgentInfo { + id: "fundy".to_string(), + name: "ファンディ".to_string(), + emoji: "📊".to_string(), + role: "Fundamental Analyst".to_string(), + society: "Investment".to_string(), + }, + AgentInfo { + id: "scribe".to_string(), + name: "スクライブ".to_string(), + emoji: "📝".to_string(), + role: "Reporter".to_string(), + society: "Investment".to_string(), + }, + // Content Society + AgentInfo { + id: "tweeter".to_string(), + name: "ツイーター".to_string(), + emoji: "🐦".to_string(), + role: "X Specialist".to_string(), + society: "Content".to_string(), + }, + AgentInfo { + id: "pen".to_string(), + name: "ペン".to_string(), + emoji: "✒️".to_string(), + role: "Writer".to_string(), + society: "Content".to_string(), + }, + AgentInfo { + id: "vidpro".to_string(), + name: "ビッドプロ".to_string(), + emoji: "🎬".to_string(), + role: "Video Producer".to_string(), + society: "Content".to_string(), + }, + AgentInfo { + id: "artist".to_string(), + name: "アーティスト".to_string(), + emoji: "🎨".to_string(), + role: "Designer".to_string(), + society: "Content".to_string(), + }, + AgentInfo { + id: "optimizer".to_string(), + name: "オプティマイザー".to_string(), + emoji: "🔧".to_string(), + role: "SEO Specialist".to_string(), + society: "Content".to_string(), + }, + AgentInfo { + id: "scheduler".to_string(), + name: "スケジューラー".to_string(), + emoji: "📅".to_string(), + role: "Calendar Manager".to_string(), + society: "Content".to_string(), + }, + // Marketing Society + AgentInfo { + id: "hiro".to_string(), + name: "ヒロ".to_string(), + emoji: "🚀".to_string(), + role: "Promoter".to_string(), + society: "Marketing".to_string(), + }, + AgentInfo { + id: "kazoeru".to_string(), + name: "カゾエル".to_string(), + emoji: "🔢".to_string(), + role: "Metrics Tracker".to_string(), + society: "Marketing".to_string(), + }, + AgentInfo { + id: "funnel".to_string(), + name: "ファネル".to_string(), + emoji: "🌪️".to_string(), + role: "CRO Specialist".to_string(), + society: "Marketing".to_string(), + }, + AgentInfo { + id: "adops".to_string(), + name: "アドオプス".to_string(), + emoji: "📢".to_string(), + role: "Ad Manager".to_string(), + society: "Marketing".to_string(), + }, + ] + } + + /// Resolve agent alias to canonical ID + pub fn resolve_agent_alias(alias: &str) -> String { + match alias { + // Core Society - maestro + "shikirun" | "conductor" | "orchestrator" => "maestro".to_string(), + // Core Society - kade + "kaede" | "creator" | "codegen" | "developer" => "kade".to_string(), + // Core Society - sakura + "reviewer" | "qa" | "critic" => "sakura".to_string(), + // Core Society - tsubaki + "integrator" | "pr-manager" | "merge-bot" => "tsubaki".to_string(), + // Core Society - botan + "deployer" | "release-manager" | "deployment" => "botan".to_string(), + // Core Society - nagare + "nagarerun" | "workflow" | "automation" | "n8n-specialist" => "nagare".to_string(), + + // Investment Society - scout + "researcher" | "explorer" => "scout".to_string(), + // Investment Society - crystal + "valuer" | "analyst" => "crystal".to_string(), + // Investment Society - dealer + "trader" | "executor" => "dealer".to_string(), + // Investment Society - sentinel + "risk-manager" | "guardian-rm" => "sentinel".to_string(), + // Investment Society - architect + "portfolio-manager" | "allocator" | "architect-inv" => "architect".to_string(), + // Investment Society - watchman + "news-monitor" | "sentinel-news" => "watchman".to_string(), + // Investment Society - chart + "technical-analyst" | "chart-reader" => "chart".to_string(), + // Investment Society - fundy + "fundamental-analyst" | "value-investor" => "fundy".to_string(), + // Investment Society - scribe + "reporter" | "documenter" | "scribe-inv" => "scribe".to_string(), + + // Content Society - tweeter + "twitter-specialist" | "x-poster" => "tweeter".to_string(), + // Content Society - pen + "writer" | "author" => "pen".to_string(), + // Content Society - vidpro + "video-producer" | "youtuber" => "vidpro".to_string(), + // Content Society - artist + "designer" | "visual-creator" => "artist".to_string(), + // Content Society - optimizer + "seo-specialist" | "seo-analyst" => "optimizer".to_string(), + // Content Society - scheduler + "calendar-manager" | "planner" => "scheduler".to_string(), + + // Marketing Society - hiro + "promoter" | "growth-hacker" => "hiro".to_string(), + // Marketing Society - kazoeru + "metrics-tracker" | "data-analyst" => "kazoeru".to_string(), + // Marketing Society - funnel + "conversion-optimizer" | "cro-specialist" => "funnel".to_string(), + // Marketing Society - adops + "ad-manager" | "media-buyer" => "adops".to_string(), + + // default - return as-is + _ => alias.to_string(), + } + } +} + +/// Agent information +#[derive(Debug, Clone)] +pub struct AgentInfo { + pub id: String, + pub name: String, + pub emoji: String, + pub role: String, + pub society: String, +} + +/// Request for sending a message +#[derive(Serialize)] +struct SendMessageRequest { + #[serde(rename = "agentId")] + agent_id: String, + message: String, +} + +/// OpenClaw error types +#[derive(Debug, thiserror::Error)] +pub enum OpenClawError { + #[error("Network error: {0}")] + Network(String), + + #[error("API error (status {0}): {1}")] + Api(u16, String), + + #[error("Configuration error: {0}")] + Config(String), + + #[error("Agent '{0}' not found")] + AgentNotFound(String), +} + +/// Result type for OpenClaw operations +pub type OpenClawResult = Result;