feat: add OpenClaw integration module to miyabi-core

- Add openclaw.rs module for cluster communication
- Expand CLI with OpenClaw-related commands (264 lines)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Shunsuke Hayashi 2026-03-11 06:35:05 +09:00
parent 6d5b55ad74
commit 0e37360f8b
3 changed files with 618 additions and 0 deletions

View file

@ -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<String>,
},
/// 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::<serde_json::Value>(&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 <agent> <message>");
println!(" → 特定のエージェントにメッセージを送信");
println!();
println!(" miyabi openclaw broadcast <message>");
println!(" → 全コアエージェントにブロードキャスト");
println!();
println!(" miyabi openclaw broadcast-society <society> <message>");
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(())

View file

@ -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,
};

View file

@ -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<String, OpenClawError> {
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<Vec<String>, 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<AgentInfo> {
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<T> = Result<T, OpenClawError>;