From ccad85a11a82049b7e41b2d593e6a5725b375996 Mon Sep 17 00:00:00 2001 From: Sion Smith Date: Tue, 1 Sep 2026 09:14:54 +0100 Subject: [PATCH] fix: address v0.1.8 review follow-ups --- CHANGELOG.md | 8 ++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- src/cli/time.rs | 6 +++--- src/commands/time.rs | 41 ++++++++++++++++++++++++++++++++++++++++- src/output/table.rs | 29 +++++++++++++++++++++++++++++ 6 files changed, 82 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ea0319..1e3c40c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). +## [0.1.9] - 2026-09-01 + +### Fixed + +- Render task responses containing currency fields with the task table and its billable column instead of misclassifying them as clients. +- Show the actual human-readable duration format in `time stop`, `time log`, and `time running` help examples. +- Validate agent lifecycle metadata against the 4KB API limit after adding required skill and duration fields. + ## [0.1.8] - 2026-09-01 ### Added diff --git a/Cargo.lock b/Cargo.lock index a8b4ae3..0e78b82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -872,7 +872,7 @@ dependencies = [ [[package]] name = "keito-cli" -version = "0.1.8" +version = "0.1.9" dependencies = [ "assert_cmd", "chrono", diff --git a/Cargo.toml b/Cargo.toml index e4273b2..80b0d94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keito-cli" -version = "0.1.8" +version = "0.1.9" edition = "2021" description = "AI agent time tracking CLI for Keito: capture billable human and agent work for client billing, agency projects, and AI-native services" license = "MIT" diff --git a/src/cli/time.rs b/src/cli/time.rs index 112a169..2b95885 100644 --- a/src/cli/time.rs +++ b/src/cli/time.rs @@ -98,7 +98,7 @@ EXAMPLE: \"project\": \"Acme Website\", \"task\": \"Development\", \"duration_hours\": 1.5, - \"duration\": \"1:30\", + \"duration\": \"1h 30m\", \"spent_date\": \"2026-03-04\", \"billable\": true, \"source\": \"cli\" @@ -146,7 +146,7 @@ EXAMPLE: \"project\": \"Acme Website\", \"task\": \"Development\", \"duration_hours\": 1.5, - \"duration\": \"1:30\", + \"duration\": \"1h 30m\", \"spent_date\": \"2025-01-15\", \"date\": \"2025-01-15\", \"billable\": true, @@ -367,7 +367,7 @@ EXAMPLE: \"source\": \"cli\", \"started_at\": \"2026-03-04T09:00:00Z\", \"elapsed_hours\": 1.5, - \"elapsed\": \"1:30\" + \"elapsed\": \"1h 30m\" } EXIT CODES: diff --git a/src/commands/time.rs b/src/commands/time.rs index cce66a0..55f5dcb 100644 --- a/src/commands/time.rs +++ b/src/commands/time.rs @@ -833,7 +833,16 @@ fn prepare_agent_log_metadata( "duration_seconds".into(), Value::Number(duration_seconds.into()), ); - Ok(Some(Value::Object(map))) + let value = Value::Object(map); + let size = serde_json::to_string(&value) + .map_err(|err| AppError::InvalidInput(format!("failed to serialize metadata: {err}")))? + .len(); + if size > 4096 { + return Err(AppError::InvalidInput( + "--metadata payload must be 4KB or smaller".into(), + )); + } + Ok(Some(value)) } fn insert_string_metadata(map: &mut Map, key: &str, value: Option) { @@ -972,4 +981,34 @@ mod tests { assert_eq!(metadata["skill"], "keito-time-track"); assert_eq!(metadata["duration_seconds"], 900); } + + #[test] + fn agent_log_lifecycle_metadata_checks_final_size() { + let raw = serde_json::json!({ + "session_id": "session-123", + "padding": "x".repeat(4020), + }) + .to_string(); + assert!(raw.len() <= 4096); + + let error = prepare_agent_log_metadata( + build_metadata(MetadataInput { + metadata: Some(raw), + session_id: None, + agent_id: None, + agent_type: None, + skill: None, + }) + .unwrap(), + "agent", + 900, + ) + .unwrap_err(); + + assert!(matches!( + error, + AppError::InvalidInput(message) + if message == "--metadata payload must be 4KB or smaller" + )); + } } diff --git a/src/output/table.rs b/src/output/table.rs index aaf9909..31c4102 100644 --- a/src/output/table.rs +++ b/src/output/table.rs @@ -196,6 +196,7 @@ fn try_as_clients(arr: &[serde_json::Value]) -> Option> { let first = arr.first()?.as_object()?; if first.contains_key("name") && first.contains_key("currency") + && !first.contains_key("billable_by_default") && !first.contains_key("project_id") && !first.contains_key("task_id") && !first.contains_key("is_billable") @@ -349,3 +350,31 @@ fn format_me_table(me_list: &[MeResponse]) -> String { "No data.".into() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn task_with_currency_uses_task_table() { + let task = Task { + id: "task-1".into(), + name: "Development".into(), + is_active: true, + billable_by_default: true, + default_hourly_rate: Some(100.0), + effective_billable_rate: Some(120.0), + currency: Some("GBP".into()), + budget: None, + is_default: false, + parent_task_id: None, + created_at: None, + updated_at: None, + }; + + let output = to_table(&[task]); + + assert!(output.contains("Billable")); + assert!(!output.contains("Currency")); + } +}