mergegate/crates/miyabi-cli/src/main.rs
林 駿甫 (Shunsuke Hayashi) dfdd52ec1d feat(collab): add miyabi collab subcommand for Collaborator canvas control
Add `miyabi collab` subcommand (Phase 3 of Miyabi canvas integration):
  miyabi collab list [--json] [--type TYPE] [--count]
  miyabi collab add <type> [--file PATH] [--pos x,y] [--size w,h] [--idempotent]
  miyabi collab rm <tile_id>
  miyabi collab move <tile_id> --pos x,y
  miyabi collab resize <tile_id> --size w,h
  miyabi collab viewport [--pan x,y] [--zoom 1.0]
  miyabi collab status

Delegates to ~/.local/bin/collab (collab CLI v0.2.0+).
Error handling includes installation hint on binary not found.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 10:06:15 +09:00

1000 lines
40 KiB
Rust

//! Miyabi CLI - Main entry point
use clap::{Parser, Subcommand};
use miyabi_core::{FeatureFlagManager, RulesLoader};
use std::collections::HashMap;
use std::path::PathBuf;
use tracing_subscriber::EnvFilter;
/// Global feature flags manager
static FEATURE_FLAGS: std::sync::OnceLock<FeatureFlagManager> = std::sync::OnceLock::new();
/// Get the global feature flags manager
pub fn feature_flags() -> &'static FeatureFlagManager {
FEATURE_FLAGS.get_or_init(|| {
let manager = FeatureFlagManager::new();
// Default feature flags
manager.set_flag("extended_thinking", true);
manager.set_flag("auto_save_sessions", true);
manager.set_flag("syntax_highlighting", true);
manager.set_flag("vim_mode", false);
manager
})
}
#[derive(Parser)]
#[command(name = "miyabi")]
#[command(author, version, about = "Miyabi - Autonomous AI Development Framework", long_about = None)]
struct Cli {
/// Model to use (overrides config)
#[arg(short, long)]
model: Option<String>,
/// Maximum tokens for responses (overrides config)
#[arg(long)]
max_tokens: Option<u32>,
/// Enable Extended Thinking (Claude 4.5+)
#[arg(long)]
thinking: bool,
/// Path to config file
#[arg(short, long)]
config: Option<PathBuf>,
/// Session ID to load on startup
#[arg(short, long)]
session: Option<String>,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
/// Start the TUI interface
Tui,
/// Show status
Status,
/// Generate default config file
Init,
/// Manage sessions
Sessions {
/// Delete a session by ID
#[arg(short, long)]
delete: Option<String>,
/// Export a session to JSON file
#[arg(short, long)]
export: Option<String>,
/// Export a session to Markdown file
#[arg(short, long)]
markdown: Option<String>,
},
/// Show version and system information
Version,
/// Show project rules (.miyabirules)
Rules {
/// Show detailed rule information
#[arg(short, long)]
verbose: bool,
},
/// Run agent with a prompt (autonomous execution)
Agent {
/// The prompt to execute
prompt: String,
/// Maximum iterations (default: 10)
#[arg(long, default_value = "10")]
max_iterations: usize,
/// Auto-approve all tool executions
#[arg(long)]
auto_approve: bool,
/// Output format: text or json
#[arg(long, default_value = "text")]
format: String,
/// System prompt for the agent
#[arg(long)]
system: Option<String>,
},
/// OpenClaw integration - control OpenClaw agents
Openclaw {
/// OpenClaw subcommand
#[command(subcommand)]
command: OpenclawCommand,
},
/// Collaborator canvas control via collab CLI
Collab {
/// Canvas subcommand
#[command(subcommand)]
command: CollabCommand,
},
}
/// Collab canvas subcommands — wraps the collab CLI at ~/.local/bin/collab
#[derive(Subcommand)]
enum CollabCommand {
/// List tiles on the canvas
List {
/// Output as JSON array
#[arg(long)]
json: bool,
/// Filter by tile type (note, code, term, image, graph)
#[arg(long)]
r#type: Option<String>,
/// Count only
#[arg(long)]
count: bool,
},
/// Add a tile to the canvas
Add {
/// Tile type (note, code, term, image, graph)
tile_type: String,
/// File to attach (required for note/code)
#[arg(long)]
file: Option<String>,
/// Position in grid units "x,y"
#[arg(long)]
pos: Option<String>,
/// Size in grid units "w,h"
#[arg(long)]
size: Option<String>,
/// Skip if tile with same file already exists
#[arg(long)]
idempotent: bool,
},
/// Remove a tile from the canvas
Rm {
/// Tile ID to remove
tile_id: String,
},
/// Move a tile to a new position
Move {
/// Tile ID to move
tile_id: String,
/// New position in grid units "x,y"
#[arg(long)]
pos: String,
},
/// Resize a tile
Resize {
/// Tile ID to resize
tile_id: String,
/// New size in grid units "w,h"
#[arg(long)]
size: String,
},
/// Get or set the canvas viewport
Viewport {
/// Set pan position "x,y"
#[arg(long)]
pan: Option<String>,
/// Set zoom level (e.g. 1.0)
#[arg(long)]
zoom: Option<f64>,
},
/// Show Collaborator connection status
Status,
}
#[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]
async fn main() -> anyhow::Result<()> {
// Initialize logging
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
let cli = Cli::parse();
match cli.command {
Some(Commands::Tui) | None => {
// Run TUI
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
execute,
terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
},
};
use miyabi_core::config::Config;
use miyabi_tui::App;
use ratatui::prelude::*;
use std::io;
// Load config (from custom path or default)
let mut config = if let Some(config_path) = &cli.config {
Config::load_from(config_path)?
} else {
Config::load().unwrap_or_default()
};
// Apply CLI overrides
if let Some(model) = &cli.model {
config.api.model = model.clone();
}
if let Some(max_tokens) = cli.max_tokens {
config.api.max_tokens = max_tokens;
}
if cli.thinking {
config.api.thinking = true;
}
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::with_config(config);
// Load session if specified
if let Some(session_id) = &cli.session {
if let Err(e) = app.load_session(session_id) {
eprintln!("Warning: Failed to load session {}: {}", session_id, e);
}
}
let res = app.run(&mut terminal).await;
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
eprintln!("Error: {}", err);
}
}
Some(Commands::Status) => {
use miyabi_core::config::Config;
let config = Config::load().unwrap_or_default();
println!("Miyabi Status: Ready");
println!();
println!("Config: {}", Config::default_path().display());
println!("Sessions: {}", config.sessions_dir().display());
println!("Model: {}", config.api.model);
println!();
// Load and show rules info
let cwd = std::env::current_dir().unwrap_or_default();
let loader = RulesLoader::new(cwd);
match loader.load() {
Ok(Some(rules)) => {
println!("Rules: {} rules loaded", rules.rules.len());
if !rules.agent_preferences.is_empty() {
println!("Agents: {} agent preferences", rules.agent_preferences.len());
}
}
Ok(None) => {
println!("Rules: No .miyabirules found");
}
Err(e) => {
println!("Rules: Error loading - {}", e);
}
}
// Show feature flags
let flags = feature_flags();
let all_flags = flags.get_all_flags();
let enabled_count = all_flags.iter().filter(|f| f.enabled).count();
println!("Flags: {}/{} enabled", enabled_count, all_flags.len());
}
Some(Commands::Init) => {
use miyabi_core::config::Config;
let path = Config::generate_default()?;
println!("Generated default config at: {:?}", path);
}
Some(Commands::Sessions {
delete,
export,
markdown,
}) => {
use miyabi_core::anthropic::{ContentBlock, Role};
use miyabi_core::config::Config;
use miyabi_core::session::SessionStorage;
let config = Config::load().unwrap_or_default();
let storage = SessionStorage::new(config.sessions_dir());
if let Some(id) = delete {
// Delete session
match storage.delete(&id) {
Ok(_) => println!("Deleted session: {}", id),
Err(e) => eprintln!("Failed to delete session {}: {}", id, e),
}
} else if let Some(id) = export {
// Export session to JSON
match storage.load(&id) {
Ok(session) => {
let filename = format!("{}.json", id);
let json = serde_json::to_string_pretty(&session)?;
std::fs::write(&filename, json)?;
println!("Exported session to: {}", filename);
}
Err(e) => eprintln!("Failed to load session {}: {}", id, e),
}
} else if let Some(id) = markdown {
// Export session to Markdown
match storage.load(&id) {
Ok(session) => {
let filename = format!("{}.md", id);
let mut md = String::new();
// Header
md.push_str(&format!("# Session: {}\n\n", session.title));
md.push_str(&format!("**Model**: {}\n", session.model));
md.push_str(&format!(
"**Date**: {}\n",
session.created_at.format("%Y-%m-%d %H:%M")
));
md.push_str(&format!("**Tokens**: {}\n\n", session.tokens_used));
md.push_str("---\n\n");
// Messages
for message in &session.messages {
let role = match message.role {
Role::User => "You",
Role::Assistant => "Assistant",
};
md.push_str(&format!("## {}\n\n", role));
for content in &message.content {
match content {
ContentBlock::Text { text } => {
md.push_str(text);
md.push_str("\n\n");
}
ContentBlock::ToolUse { name, input, .. } => {
md.push_str(&format!(
"**Tool**: {}\n```json\n{}\n```\n\n",
name, input
));
}
ContentBlock::ToolResult { content, .. } => {
md.push_str(&format!(
"**Result**:\n```\n{}\n```\n\n",
content
));
}
}
}
}
std::fs::write(&filename, md)?;
println!("Exported session to: {}", filename);
}
Err(e) => eprintln!("Failed to load session {}: {}", id, e),
}
} else {
// List all sessions
match storage.list() {
Ok(sessions) => {
if sessions.is_empty() {
println!("No sessions found.");
} else {
println!(
"{:<36} {:<20} {:<8} {:<10} Updated",
"ID", "Title", "Messages", "Tokens"
);
println!("{}", "-".repeat(90));
for session in sessions {
let updated = session.updated_at.format("%Y-%m-%d %H:%M");
println!(
"{:<36} {:<20} {:<8} {:<10} {}",
session.id,
truncate_str(&session.title, 18),
session.messages.len(),
session.tokens_used,
updated
);
}
}
}
Err(e) => eprintln!("Failed to list sessions: {}", e),
}
}
}
Some(Commands::Version) => {
use miyabi_core::config::Config;
let config = Config::load().unwrap_or_default();
println!("Miyabi v{}", env!("CARGO_PKG_VERSION"));
println!();
println!("Model: {}", config.api.model);
println!("Config: {}", Config::default_path().display());
println!("Sessions: {}", config.sessions_dir().display());
println!();
println!(
"Platform: {} ({})",
std::env::consts::OS,
std::env::consts::ARCH
);
}
Some(Commands::Rules { verbose }) => {
let cwd = std::env::current_dir().unwrap_or_default();
let loader = RulesLoader::new(cwd.clone());
match loader.load() {
Ok(Some(rules)) => {
println!("Project Rules (.miyabirules)");
println!("============================");
println!();
if rules.rules.is_empty() {
println!("No rules defined.");
} else {
println!("Rules ({}):", rules.rules.len());
for rule in &rules.rules {
let status = if rule.enabled { "" } else { "" };
let severity = match rule.severity.as_str() {
"error" => "🔴",
"warning" => "🟡",
_ => "🔵",
};
println!(" {} {} {} - {}", status, severity, rule.name, rule.suggestion);
if verbose {
if let Some(pattern) = &rule.pattern {
println!(" Pattern: {}", pattern);
}
if !rule.file_extensions.is_empty() {
println!(" Extensions: {}", rule.file_extensions.join(", "));
}
println!();
}
}
}
if !rules.agent_preferences.is_empty() {
println!();
println!("Agent Preferences ({}):", rules.agent_preferences.len());
for (agent, prefs) in &rules.agent_preferences {
println!(" {}:", agent);
if let Some(style) = &prefs.style {
println!(" Style: {}", style);
}
if let Some(handling) = &prefs.error_handling {
println!(" Error Handling: {}", handling);
}
if let Some(score) = prefs.min_score {
println!(" Min Score: {}", score);
}
}
}
if verbose && !rules.settings.is_empty() {
println!();
println!("Settings:");
for (key, value) in &rules.settings {
println!(" {}: {}", key, value);
}
}
}
Ok(None) => {
println!("No .miyabirules file found in {} or parent directories.", cwd.display());
println!();
println!("Create a .miyabirules file to define project-specific rules.");
println!("See: miyabi --help for more information.");
}
Err(e) => {
eprintln!("Error loading rules: {}", e);
std::process::exit(1);
}
}
}
Some(Commands::Agent {
prompt,
max_iterations,
auto_approve,
format,
system,
}) => {
use miyabi_core::{
config::Config, Agent, AgentConfig, AgentEvent, AnthropicClient, ExecutorRegistry,
};
use tokio::sync::mpsc;
// Load config
let mut config = if let Some(config_path) = &cli.config {
Config::load_from(config_path)?
} else {
Config::load().unwrap_or_default()
};
// Apply CLI overrides
if let Some(model) = &cli.model {
config.api.model = model.clone();
}
if let Some(max_tokens) = cli.max_tokens {
config.api.max_tokens = max_tokens;
}
if cli.thinking {
config.api.thinking = true;
}
// Get API key
let api_key = config
.api
.api_key
.clone()
.or_else(|| std::env::var("ANTHROPIC_API_KEY").ok())
.ok_or_else(|| {
anyhow::anyhow!("No API key found. Set ANTHROPIC_API_KEY or add to config.")
})?;
// Create client
let client = AnthropicClient::new(api_key)?
.with_model(&config.api.model)
.with_max_tokens(config.api.max_tokens)
.with_thinking(config.api.thinking);
// Create executor registry with standard tools
let registry = ExecutorRegistry::with_standard_tools();
// Configure agent
let agent_config = AgentConfig {
max_iterations,
max_tokens_per_turn: config.api.max_tokens,
require_approval: !auto_approve,
auto_approve_patterns: if auto_approve {
vec![
"read".to_string(),
"glob".to_string(),
"grep".to_string(),
"write".to_string(),
"edit".to_string(),
"bash".to_string(),
]
} else {
vec!["read".to_string(), "glob".to_string(), "grep".to_string()]
},
..Default::default()
};
// Create agent
let mut agent = Agent::new(client, registry).with_config(agent_config);
// Set system prompt
if let Some(sys) = system {
agent = agent.with_system_prompt(sys);
} else if let Some(sys) = config.api.system_prompt {
agent = agent.with_system_prompt(sys);
}
// Create event channel for progress
let (tx, mut rx) = mpsc::channel(100);
let agent = agent.with_event_channel(tx);
// Spawn agent execution
let agent_handle = tokio::spawn(async move { agent.run(&prompt).await });
// Process events
while let Some(event) = rx.recv().await {
match &event {
AgentEvent::Started { prompt } => {
if format != "json" {
eprintln!("🚀 Agent started with prompt: {}", truncate_str(prompt, 50));
}
}
AgentEvent::Thinking { iteration } => {
if format != "json" {
eprintln!("💭 Iteration {}", iteration + 1);
}
}
AgentEvent::ToolDetected { name, .. } => {
if format != "json" {
eprintln!("🔧 Tool detected: {}", name);
}
}
AgentEvent::ToolExecuting { name, .. } => {
if format != "json" {
eprintln!("⚡ Executing: {}", name);
}
}
AgentEvent::ToolCompleted { name, .. } => {
if format != "json" {
eprintln!("✅ Completed: {}", name);
}
}
AgentEvent::ToolFailed { name, error } => {
if format != "json" {
eprintln!("❌ Failed {}: {}", name, error);
}
}
AgentEvent::Completed { result } => {
if format == "json" {
println!("{}", serde_json::to_string_pretty(&result)?);
} else {
println!("\n{}", result.output);
eprintln!(
"\n📊 Stats: {} iterations, {} tool calls, {} tokens",
result.iterations, result.tool_calls, result.total_tokens
);
}
}
AgentEvent::Failed { error } => {
eprintln!("❌ Agent failed: {}", error);
}
_ => {}
}
}
// Wait for agent to complete
match agent_handle.await? {
Ok(_) => {}
Err(e) => {
eprintln!("Agent error: {}", e);
std::process::exit(1);
}
}
}
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!();
}
}
}
Some(Commands::Collab { command }) => {
use std::process::Command;
use std::env;
let collab_bin = {
let home = env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
format!("{}/.local/bin/collab", home)
};
let mut args: Vec<String> = Vec::new();
match command {
CollabCommand::List { json, r#type, count } => {
args.push("tile".to_string());
args.push("list".to_string());
if json { args.push("--json".to_string()); }
if count { args.push("--count".to_string()); }
if let Some(t) = r#type {
args.push("--type".to_string());
args.push(t);
}
}
CollabCommand::Add { tile_type, file, pos, size, idempotent } => {
args.push("tile".to_string());
args.push("add".to_string());
args.push(tile_type);
if let Some(f) = file {
args.push("--file".to_string());
args.push(f);
}
if let Some(p) = pos {
args.push("--pos".to_string());
args.push(p);
}
if let Some(s) = size {
args.push("--size".to_string());
args.push(s);
}
if idempotent { args.push("--idempotent".to_string()); }
}
CollabCommand::Rm { tile_id } => {
args.push("tile".to_string());
args.push("rm".to_string());
args.push(tile_id);
}
CollabCommand::Move { tile_id, pos } => {
args.push("tile".to_string());
args.push("move".to_string());
args.push(tile_id);
args.push("--pos".to_string());
args.push(pos);
}
CollabCommand::Resize { tile_id, size } => {
args.push("tile".to_string());
args.push("resize".to_string());
args.push(tile_id);
args.push("--size".to_string());
args.push(size);
}
CollabCommand::Viewport { pan, zoom } => {
if pan.is_some() || zoom.is_some() {
args.push("viewport".to_string());
args.push("set".to_string());
if let Some(p) = pan {
args.push("--pan".to_string());
args.push(p);
}
if let Some(z) = zoom {
args.push("--zoom".to_string());
args.push(z.to_string());
}
} else {
args.push("viewport".to_string());
}
}
CollabCommand::Status => {
args.push("status".to_string());
}
}
let status = Command::new(&collab_bin)
.args(&args)
.status();
match status {
Ok(s) => {
if !s.success() {
std::process::exit(s.code().unwrap_or(1));
}
}
Err(e) => {
eprintln!("error: failed to run collab CLI ({}): {}", collab_bin, e);
eprintln!(" → Install collab CLI: https://github.com/ShunsukeHayashi/collab-cli");
std::process::exit(1);
}
}
}
}
Ok(())
}
fn truncate_str(s: &str, max_len: usize) -> String {
if s.len() > max_len {
format!("{}...", &s[..max_len.saturating_sub(3)])
} else {
s.to_string()
}
}