Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions terminator-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ dotenvy = "0.15.7"
reqwest = { version = "0.12.22", features = ["json", "rustls-tls"] }

anthropic-sdk = "0.1.5"
openai-api-rs = "6.0.8"
gemini-rs = "2.0.0"

# Cron scheduling support
tokio-cron-scheduler = "0.14"
Expand Down
162 changes: 162 additions & 0 deletions terminator-cli/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
use clap::{Parser, Subcommand, ValueEnum};

#[derive(Parser)]
#[command(name = "terminator")]
#[command(about = "🤖 Terminator CLI - AI-native GUI automation")]
#[command(
long_about = "Terminator CLI provides tools for managing the Terminator project, including version management, releases, and development workflows."
)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}

#[derive(ValueEnum, Clone, Copy, Debug, Default)]
#[clap(rename_all = "lower")]
pub enum BumpLevel {
#[default]
Patch,
Minor,
Major,
}

#[derive(ValueEnum, Clone, Copy, Debug, Default)]
#[clap(rename_all = "lower")]
pub enum AIProvider {
#[default]
Anthropic,
OpenAI,
Gemini,
}

impl std::fmt::Display for BumpLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", format!("{self:?}").to_lowercase())
}
}

#[derive(Parser, Debug)]
pub struct ReleaseArgs {
/// The part of the version to bump: patch, minor, or major.
#[clap(value_enum, default_value_t = BumpLevel::Patch)]
pub level: BumpLevel,
}

#[derive(Parser, Debug)]
pub struct McpChatArgs {
/// MCP server URL (e.g., http://localhost:3000)
#[clap(long, short = 'u', conflicts_with = "command")]
pub url: Option<String>,

/// Command to start MCP server via stdio (e.g., "npx -y terminator-mcp-agent")
#[clap(long, short = 'c', conflicts_with = "url")]
pub command: Option<String>,

/// Specify AIProvider
#[clap(long, short = 'a', default_value_t = AIProvider::Anthropic, value_enum)]
pub aiprovider: AIProvider,
}

#[derive(Parser, Debug)]
pub struct McpExecArgs {
/// MCP server URL
#[clap(long, short = 'u', conflicts_with = "command")]
pub url: Option<String>,

/// Command to start MCP server via stdio
#[clap(long, short = 'c', conflicts_with = "url")]
pub command: Option<String>,

/// Tool name to execute
pub tool: String,

/// Arguments for the tool (as JSON or simple string)
pub args: Option<String>,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
#[clap(rename_all = "lower")]
pub enum InputType {
Auto,
Gist,
Raw,
File,
}

#[derive(Parser, Debug, Clone)]
pub struct McpRunArgs {
/// MCP server URL (e.g., http://localhost:3000)
#[clap(long, short = 'u', conflicts_with = "command")]
pub url: Option<String>,

/// Command to start MCP server via stdio (e.g., "npx -y terminator-mcp-agent")
#[clap(long, short = 'c', conflicts_with = "url")]
pub command: Option<String>,

/// Input source - can be a GitHub gist URL, raw gist URL, or local file path (JSON/YAML)
pub input: String,

/// Input type (auto-detected by default)
#[clap(long, value_enum, default_value = "auto")]
pub input_type: InputType,

/// Dry run - parse and validate the workflow without executing
#[clap(long)]
pub dry_run: bool,

/// Verbose output
#[clap(long, short)]
pub verbose: bool,

/// Stop on first error (default: true)
#[clap(long)]
pub no_stop_on_error: bool,

/// Include detailed results (default: true)
#[clap(long)]
pub no_detailed_results: bool,

/// Skip retry logic on errors (default: false, will retry on errors)
#[clap(long)]
pub no_retry: bool,
}

#[derive(Subcommand)]
pub enum McpCommands {
/// Interactive chat with MCP server
Chat(McpChatArgs),
/// Interactive AI-powered chat with MCP server
AiChat(McpChatArgs),
/// Execute a single MCP tool
Exec(McpExecArgs),
/// Execute a workflow sequence from a local file or GitHub gist
Run(McpRunArgs),
}

#[derive(Subcommand)]
pub enum VersionCommands {
/// Bump patch version (x.y.Z+1)
Patch,
/// Bump minor version (x.Y+1.0)
Minor,
/// Bump major version (X+1.0.0)
Major,
/// Sync all package versions without bumping
Sync,
/// Show current version status
Status,
/// Tag current version and push (triggers CI)
Tag,
/// Full release: bump version + tag + push
Release(ReleaseArgs),
}

#[derive(Subcommand)]
pub enum Commands {
/// Version management commands
#[command(subcommand)]
Version(VersionCommands),
/// MCP client commands
#[command(subcommand)]
Mcp(McpCommands),
}
119 changes: 119 additions & 0 deletions terminator-cli/src/command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use anyhow::Result;
use std::process::{Command, Stdio};
use crate::{
workflow_exec::{exec::execute_command, workflow::{run_workflow, Transport}},
mpc_client::{natural_lang::aichat, interactive_chat::interactive_chat},
cli::{McpCommands, VersionCommands},
version_control::{ensure_project_root, full_release,
sync_all_versions, bump_version, tag_and_push, show_status}
};

pub fn run_command(program: &str, args: &[&str]) -> Result<(), Box<dyn std::error::Error>> {
let output = Command::new(program)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()?;

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!(
"Command failed: {} {}\nError: {}",
program,
args.join(" "),
stderr
)
.into());
}

Ok(())
}

pub fn parse_transport(url: Option<String>, command: Option<String>) -> Transport {
if let Some(url) = url {
Transport::Http(url)
} else if let Some(command) = command {
let parts = parse_command(&command);
Transport::Stdio(parts)
} else {
// Default to spawning local MCP agent via npx for convenience
let default_cmd = "npx -y terminator-mcp-agent@latest";
println!("ℹ️ No --url or --command specified. Falling back to '{default_cmd}'");
let parts = parse_command(default_cmd);
Transport::Stdio(parts)
}
}


pub fn parse_command(command: &str) -> Vec<String> {
// Simple command parsing - splits by spaces but respects quotes
let mut parts = Vec::new();
let mut current = String::new();
let mut in_quotes = false;

for c in command.chars() {
match c {
'"' => in_quotes = !in_quotes,
' ' if !in_quotes => {
if !current.is_empty() {
parts.push(current.clone());
current.clear();
}
}
_ => current.push(c),
}
}

if !current.is_empty() {
parts.push(current);
}

parts
}

pub fn handle_mcp_command(cmd: McpCommands) {
let transport = match cmd {
McpCommands::Chat(ref args) => parse_transport(args.url.clone(), args.command.clone()),
McpCommands::AiChat(ref args) => parse_transport(args.url.clone(), args.command.clone()),
McpCommands::Exec(ref args) => parse_transport(args.url.clone(), args.command.clone()),
McpCommands::Run(ref args) => parse_transport(args.url.clone(), args.command.clone()),
};

let rt = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");

let result = rt.block_on(async {
match cmd {
McpCommands::Chat(_) => {
interactive_chat(transport).await
}
McpCommands::AiChat(args) => {
aichat(transport, args.aiprovider).await
}
McpCommands::Exec(args) => {
execute_command(transport, args.tool, args.args).await
}
McpCommands::Run(args) => {
run_workflow(transport, args).await
}
}
});

if let Err(e) = result {
eprintln!("❌ MCP command error: {e}");
std::process::exit(1);
}
}

pub fn handle_version_command(version_cmd: VersionCommands) {
ensure_project_root();
match version_cmd {
VersionCommands::Patch => bump_version("patch"),
VersionCommands::Minor => bump_version("minor"),
VersionCommands::Major => bump_version("major"),
VersionCommands::Sync => sync_all_versions(),
VersionCommands::Status => show_status(),
VersionCommands::Tag => tag_and_push(),
VersionCommands::Release(args) => { full_release(&args.level.to_string()) }
}
}

Loading
Loading