summaryrefslogtreecommitdiff
path: root/makima/src/bin/makima.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-20 17:23:34 +0000
committersoryu <soryu@soryu.co>2026-01-20 17:23:46 +0000
commit055e2c4a72e3b2331a18fdc9f8132ef990af7e38 (patch)
tree75b92637b9132594b76a5a86f5f854bca1ddee49 /makima/src/bin/makima.rs
parent54c6d409e1d5667f4ab7f63a43e1459e68575c94 (diff)
downloadsoryu-055e2c4a72e3b2331a18fdc9f8132ef990af7e38.tar.gz
soryu-055e2c4a72e3b2331a18fdc9f8132ef990af7e38.zip
Update CLI to show log history as well
Diffstat (limited to 'makima/src/bin/makima.rs')
-rw-r--r--makima/src/bin/makima.rs212
1 files changed, 204 insertions, 8 deletions
diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs
index 8b3e4dc..37aa045 100644
--- a/makima/src/bin/makima.rs
+++ b/makima/src/bin/makima.rs
@@ -4,9 +4,9 @@ use std::io::{self, Read};
use std::path::Path;
use std::sync::Arc;
-use makima::daemon::api::ApiClient;
+use makima::daemon::api::{ApiClient, CreateContractRequest};
use makima::daemon::cli::{
- Cli, Commands, ContractCommand, SupervisorCommand, ViewArgs,
+ Cli, CliConfig, Commands, ConfigCommand, ContractCommand, SupervisorCommand, ViewArgs,
};
use makima::daemon::tui::{self, Action, App, ListItem, ViewType, TuiWsClient, WsEvent, OutputLine, OutputMessageType, WsConnectionState};
use makima::daemon::config::{DaemonConfig, RepoEntry};
@@ -28,6 +28,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Commands::Supervisor(cmd) => run_supervisor(cmd).await,
Commands::Contract(cmd) => run_contract(cmd).await,
Commands::View(args) => run_view(args).await,
+ Commands::Config(cmd) => run_config(cmd).await,
}
}
@@ -559,11 +560,32 @@ async fn run_contract(
/// Run the TUI view command.
async fn run_view(args: ViewArgs) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
+ // Load CLI config for defaults
+ let config = CliConfig::load();
+
+ // Get API URL and key, preferring CLI args > env vars > config file
+ // Filter out empty strings
+ let api_url = args.api_url
+ .filter(|s| !s.is_empty())
+ .unwrap_or_else(|| config.get_api_url());
+ let api_key = match args.api_key.filter(|s| !s.is_empty()) {
+ Some(key) => key,
+ None => config.get_api_key().ok_or_else(|| {
+ eprintln!("Error: No API key provided.");
+ eprintln!();
+ eprintln!("Set your API key using one of these methods:");
+ eprintln!(" 1. Run: makima config set-key YOUR_API_KEY");
+ eprintln!(" 2. Set environment variable: export MAKIMA_API_KEY=YOUR_API_KEY");
+ eprintln!(" 3. Pass via CLI: makima view --api-key YOUR_API_KEY");
+ "No API key configured"
+ })?,
+ };
+
// Create API client
- let client = ApiClient::new(args.api_url.clone(), args.api_key.clone())?;
+ let client = ApiClient::new(api_url.clone(), api_key.clone())?;
// Start WebSocket client for task output streaming
- let ws_client = TuiWsClient::start(args.api_url.clone(), args.api_key.clone());
+ let ws_client = TuiWsClient::start(api_url, api_key);
// Start at contracts view
let mut app = App::new(ViewType::Contracts);
@@ -583,6 +605,49 @@ async fn run_view(args: ViewArgs) -> Result<(), Box<dyn std::error::Error + Send
result
}
+/// Run config commands.
+async fn run_config(cmd: ConfigCommand) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
+ match cmd {
+ ConfigCommand::SetKey(args) => {
+ let mut config = CliConfig::load();
+ config.api_key = Some(args.api_key);
+ config.save()?;
+ println!("API key saved to {:?}", CliConfig::config_path().unwrap_or_default());
+ Ok(())
+ }
+ ConfigCommand::SetUrl(args) => {
+ let mut config = CliConfig::load();
+ config.api_url = args.api_url;
+ config.save()?;
+ println!("API URL saved to {:?}", CliConfig::config_path().unwrap_or_default());
+ Ok(())
+ }
+ ConfigCommand::Show => {
+ let config = CliConfig::load();
+ println!("Configuration:");
+ println!(" API URL: {}", config.api_url);
+ println!(" API Key: {}", config.api_key.as_ref().map(|k| {
+ if k.len() > 10 {
+ format!("{}...{}", &k[..6], &k[k.len()-4..])
+ } else {
+ "***".to_string()
+ }
+ }).unwrap_or_else(|| "(not set)".to_string()));
+ println!();
+ println!("Config file: {:?}", CliConfig::config_path().unwrap_or_default());
+ Ok(())
+ }
+ ConfigCommand::Path => {
+ if let Some(path) = CliConfig::config_path() {
+ println!("{}", path.display());
+ } else {
+ eprintln!("Could not determine config path");
+ }
+ Ok(())
+ }
+ }
+}
+
/// Load contracts from API
async fn load_contracts(client: &ApiClient) -> Result<Vec<ListItem>, Box<dyn std::error::Error + Send + Sync>> {
let result = client.list_contracts().await?;
@@ -595,10 +660,51 @@ async fn load_contracts(client: &ApiClient) -> Result<Vec<ListItem>, Box<dyn std
/// Load tasks for a contract from API
async fn load_tasks(client: &ApiClient, contract_id: uuid::Uuid) -> Result<Vec<ListItem>, Box<dyn std::error::Error + Send + Sync>> {
- let result = client.supervisor_tasks(contract_id).await?;
- let items = result.0.as_array()
+ // Use get_contract which returns tasks as part of the response (works with regular API key auth)
+ let result = client.get_contract(contract_id).await?;
+ let mut items: Vec<ListItem> = result.0.get("tasks")
+ .and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(ListItem::from_task).collect())
.unwrap_or_default();
+
+ // Sort tasks: supervisor first, then by status (running first), then by name
+ items.sort_by(|a, b| {
+ // Check if task is supervisor (role field in extra data)
+ let a_is_supervisor = a.extra.get("role")
+ .and_then(|v| v.as_str())
+ .map(|s| s == "supervisor")
+ .unwrap_or(false);
+ let b_is_supervisor = b.extra.get("role")
+ .and_then(|v| v.as_str())
+ .map(|s| s == "supervisor")
+ .unwrap_or(false);
+
+ // Supervisor first
+ match (a_is_supervisor, b_is_supervisor) {
+ (true, false) => std::cmp::Ordering::Less,
+ (false, true) => std::cmp::Ordering::Greater,
+ _ => {
+ // Then by status: running/working tasks first
+ let status_order = |s: Option<&String>| -> i32 {
+ match s.map(|x| x.as_str()) {
+ Some("running") | Some("working") => 0,
+ Some("pending") | Some("queued") => 1,
+ Some("completed") | Some("done") => 2,
+ Some("failed") | Some("error") => 3,
+ _ => 4,
+ }
+ };
+ let a_order = status_order(a.status.as_ref());
+ let b_order = status_order(b.status.as_ref());
+
+ match a_order.cmp(&b_order) {
+ std::cmp::Ordering::Equal => a.name.cmp(&b.name),
+ other => other,
+ }
+ }
+ }
+ });
+
Ok(items)
}
@@ -712,10 +818,29 @@ async fn run_tui_loop(
ws_client.unsubscribe(old_task_id);
}
- // Subscribe to new task output
+ // Load task output history first
+ app.status_message = Some("Loading output history...".to_string());
+ match client.get_task_output(task_id).await {
+ Ok(result) => {
+ // Parse the entries array from response
+ if let Some(entries) = result.0.get("entries").and_then(|v| v.as_array()) {
+ for entry in entries {
+ if let Some(line) = parse_output_entry(entry) {
+ app.output_buffer.add_line(line);
+ }
+ }
+ let count = entries.len();
+ app.status_message = Some(format!("Loaded {} history entries, streaming live...", count));
+ }
+ }
+ Err(e) => {
+ app.status_message = Some(format!("Failed to load history: {}", e));
+ }
+ }
+
+ // Subscribe to new task output for live updates
ws_client.subscribe(task_id);
subscribed_task_id = Some(task_id);
- app.status_message = Some("Connecting to task output...".to_string());
}
Action::PerformDelete { id, item_type } => {
// Perform the delete API call
@@ -822,6 +947,47 @@ async fn run_tui_loop(
app.ws_state = WsConnectionState::Disconnected;
}
}
+ Action::PerformCreateContract { name, description, contract_type, repository_url } => {
+ // Create the contract via API
+ let req = CreateContractRequest {
+ name: name.clone(),
+ description: if description.is_empty() { None } else { Some(description) },
+ contract_type: Some(contract_type),
+ initial_phase: None,
+ autonomous_loop: None,
+ phase_guard: None,
+ };
+
+ match client.create_contract(req).await {
+ Ok(result) => {
+ let contract_name = result.0.get("name")
+ .and_then(|v| v.as_str())
+ .unwrap_or(&name);
+ app.status_message = Some(format!("Created contract: {}", contract_name));
+
+ // Refresh the contracts list
+ match load_contracts(client).await {
+ Ok(items) => app.set_items(items),
+ Err(e) => app.status_message = Some(format!("Created but refresh failed: {}", e)),
+ }
+
+ // TODO: If repository_url was provided, add it to the contract
+ if let Some(repo_url) = repository_url {
+ if !repo_url.is_empty() {
+ // We'd need to add a method to add repository to contract
+ // For now, just note it in the status
+ app.status_message = Some(format!(
+ "Created contract: {} (Note: Add repository {} manually)",
+ contract_name, repo_url
+ ));
+ }
+ }
+ }
+ Err(e) => {
+ app.status_message = Some(format!("Create failed: {}", e));
+ }
+ }
+ }
_ => {}
}
}
@@ -837,6 +1003,36 @@ async fn run_tui_loop(
Ok(None)
}
+/// Parse an output entry from the API response into an OutputLine
+fn parse_output_entry(entry: &serde_json::Value) -> Option<OutputLine> {
+ let message_type = entry.get("messageType")
+ .and_then(|v| v.as_str())
+ .unwrap_or("raw");
+ let content = entry.get("content")
+ .and_then(|v| v.as_str())
+ .unwrap_or("")
+ .to_string();
+ let tool_name = entry.get("toolName")
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_string());
+ let is_error = entry.get("isError")
+ .and_then(|v| v.as_bool())
+ .unwrap_or(false);
+ let cost_usd = entry.get("costUsd")
+ .and_then(|v| v.as_f64());
+ let duration_ms = entry.get("durationMs")
+ .and_then(|v| v.as_u64());
+
+ Some(OutputLine {
+ message_type: OutputMessageType::from_str(message_type),
+ content,
+ tool_name,
+ is_error,
+ cost_usd,
+ duration_ms,
+ })
+}
+
/// Handle a WebSocket event and update app state
fn handle_ws_event(app: &mut App, event: WsEvent) {
match event {