From 9be4ddc43e5f90c3b4a3315e6af65b2575624bbe Mon Sep 17 00:00:00 2001 From: yeshan333 Date: Sun, 26 Oct 2025 00:29:46 +0800 Subject: [PATCH 1/5] refactor(src): reorganize and modularize src directory structure This commit refactors and modularizes the source directory structure by splitting large files into smaller modules. Specifically: - Moved message-related types and utilities into their own submodules under `src/message/`. - Created separate submodules for configuration (`src/config/*`) and connection handling (`src/connection/*`). - Introduced protocol-specific submodules (`src/protocol/*`) to handle ACP protocol logic. - Updated imports across the project to reflect these changes. Additionally, some files were renamed or moved around within tests directories to better align with their respective feature areas. These changes aim to improve readability, maintainability, and ease of navigation through the codebase. --- .claude | 1 + .iflow/agents/rust-expert.md | 67 ++ .iflow/agents/rust-pro.md | 43 - .iflow/settings.local.json | 18 + CLAUDE.md | 1 + Cargo.lock | 14 +- Cargo.toml | 19 +- examples/{ => advanced}/explore_api.rs | 0 examples/{ => advanced}/logging_example.rs | 2 +- examples/{ => advanced}/mcp_example.rs | 4 +- examples/{ => advanced}/permission_modes.rs | 11 +- examples/{ => basic}/README.md | 0 examples/{ => basic}/basic_client.rs | 6 +- examples/{ => query}/query.rs | 0 examples/{ => query}/query_with_config.rs | 0 examples/{ => query}/test_response.rs | 2 +- examples/{ => websocket}/websocket_client.rs | 4 +- .../{ => websocket}/websocket_client_debug.rs | 4 +- .../websocket_existing_process.rs | 2 +- examples/{ => websocket}/websocket_mcp.rs | 6 +- src/acp_protocol.rs | 72 +- src/client.rs | 905 ------------------ src/client/acp_handler.rs | 186 ++++ src/client/handler.rs | 223 +++++ src/client/mod.rs | 11 + src/client/stream.rs | 57 ++ src/config/file_access.rs | 29 + src/config/logging.rs | 26 + src/config/mod.rs | 16 + src/config/options.rs | 213 +++++ src/config/process.rs | 71 ++ src/config/websocket.rs | 69 ++ src/connection/handler.rs | 91 ++ src/connection/mod.rs | 13 + src/connection/stdio.rs | 278 ++++++ src/connection/websocket.rs | 387 ++++++++ src/error.rs | 11 + src/lib.rs | 8 +- src/message/mod.rs | 10 + src/message/types.rs | 411 ++++++++ src/message/utils.rs | 261 +++++ src/protocol/auth.rs | 94 ++ src/protocol/core.rs | 279 ++++++ src/protocol/mod.rs | 11 + src/protocol/notification.rs | 483 ++++++++++ src/protocol/session.rs | 154 +++ src/query.rs | 9 +- src/types.rs | 811 ---------------- src/websocket_transport.rs | 2 +- .../client_exception_additional_tests.rs | 0 tests/{ => client}/client_exception_tests.rs | 0 tests/{ => core}/message_tests.rs | 9 +- tests/{ => core}/timeout_test.rs | 0 tests/{ => e2e}/e2e_tests.rs | 0 tests/{ => mcp}/mcp_integration_tests.rs | 3 +- tests/{ => mcp}/mcp_tests.rs | 3 +- tests/plan_message_test.rs | 44 - tests/{ => process}/process_cleanup_tests.rs | 0 tests/{ => process}/process_manager_tests.rs | 0 tests/{ => query}/query_additional_tests.rs | 0 .../{ => query}/query_comprehensive_tests.rs | 0 tests/{ => query}/query_tests.rs | 0 .../{ => websocket}/websocket_config_tests.rs | 4 +- .../websocket_existing_process_tests.rs | 2 +- .../websocket_integration_tests.rs | 4 +- 65 files changed, 3577 insertions(+), 1887 deletions(-) create mode 120000 .claude create mode 100644 .iflow/agents/rust-expert.md delete mode 100644 .iflow/agents/rust-pro.md create mode 100644 .iflow/settings.local.json create mode 120000 CLAUDE.md rename examples/{ => advanced}/explore_api.rs (100%) rename examples/{ => advanced}/logging_example.rs (97%) rename examples/{ => advanced}/mcp_example.rs (96%) rename examples/{ => advanced}/permission_modes.rs (95%) rename examples/{ => basic}/README.md (100%) rename examples/{ => basic}/basic_client.rs (96%) rename examples/{ => query}/query.rs (100%) rename examples/{ => query}/query_with_config.rs (100%) rename examples/{ => query}/test_response.rs (98%) rename examples/{ => websocket}/websocket_client.rs (96%) rename examples/{ => websocket}/websocket_client_debug.rs (97%) rename examples/{ => websocket}/websocket_existing_process.rs (98%) rename examples/{ => websocket}/websocket_mcp.rs (95%) delete mode 100644 src/client.rs create mode 100644 src/client/acp_handler.rs create mode 100644 src/client/handler.rs create mode 100644 src/client/mod.rs create mode 100644 src/client/stream.rs create mode 100644 src/config/file_access.rs create mode 100644 src/config/logging.rs create mode 100644 src/config/mod.rs create mode 100644 src/config/options.rs create mode 100644 src/config/process.rs create mode 100644 src/config/websocket.rs create mode 100644 src/connection/handler.rs create mode 100644 src/connection/mod.rs create mode 100644 src/connection/stdio.rs create mode 100644 src/connection/websocket.rs create mode 100644 src/message/mod.rs create mode 100644 src/message/types.rs create mode 100644 src/message/utils.rs create mode 100644 src/protocol/auth.rs create mode 100644 src/protocol/core.rs create mode 100644 src/protocol/mod.rs create mode 100644 src/protocol/notification.rs create mode 100644 src/protocol/session.rs delete mode 100644 src/types.rs rename tests/{ => client}/client_exception_additional_tests.rs (100%) rename tests/{ => client}/client_exception_tests.rs (100%) rename tests/{ => core}/message_tests.rs (97%) rename tests/{ => core}/timeout_test.rs (100%) rename tests/{ => e2e}/e2e_tests.rs (100%) rename tests/{ => mcp}/mcp_integration_tests.rs (98%) rename tests/{ => mcp}/mcp_tests.rs (99%) delete mode 100644 tests/plan_message_test.rs rename tests/{ => process}/process_cleanup_tests.rs (100%) rename tests/{ => process}/process_manager_tests.rs (100%) rename tests/{ => query}/query_additional_tests.rs (100%) rename tests/{ => query}/query_comprehensive_tests.rs (100%) rename tests/{ => query}/query_tests.rs (100%) rename tests/{ => websocket}/websocket_config_tests.rs (96%) rename tests/{ => websocket}/websocket_existing_process_tests.rs (97%) rename tests/{ => websocket}/websocket_integration_tests.rs (95%) diff --git a/.claude b/.claude new file mode 120000 index 0000000..52a7e45 --- /dev/null +++ b/.claude @@ -0,0 +1 @@ +.iflow/ \ No newline at end of file diff --git a/.iflow/agents/rust-expert.md b/.iflow/agents/rust-expert.md new file mode 100644 index 0000000..c92e167 --- /dev/null +++ b/.iflow/agents/rust-expert.md @@ -0,0 +1,67 @@ +--- +agent-type: rust-expert +name: rust-expert +description: Write idiomatic Rust with ownership patterns, lifetimes, and trait implementations. Masters async/await, safe concurrency, and zero-cost abstractions. Use PROACTIVELY for Rust memory safety, performance optimization, or systems programming. +when-to-use: Write idiomatic Rust with ownership patterns, lifetimes, and trait implementations. Masters async/await, safe concurrency, and zero-cost abstractions. Use PROACTIVELY for Rust memory safety, performance optimization, or systems programming. +allowed-tools: +model: sonnet +inherit-tools: true +inherit-mcps: true +color: red +--- + +# Role + +You are a Rust expert specializing in safe, performant systems programming. + +## Focus Areas + +- Ownership and Borrowing concepts +- Memory safety and zero-cost abstractions +- Concurrency with threads and async/await +- Pattern matching and control flow +- Traits and generics for reusable code +- Enums and Option/Result types +- Error handling with custom error types +- Efficient data structures (Vec, HashMap, etc.) +- Unsafe Rust and FFI for performance-critical code +- Cargo for package management and builds + +## Approach + +- Embrace ownership and borrowing for memory safety +- Use pattern matching for clear and concise logic +- Implement traits for polymorphism and code reuse +- Prefer async/await for concurrent programming +- Optimize with zero-cost abstractions +- Always handle potential errors explicitly +- Write modular code with traits and generics +- Leverage Rust's type system for compile-time checks +- Profile and optimize using Rust's built-in tools +- Follow idiomatic Rust practices for clean code + +## Quality Checklist + +- Compile without warnings using `#![deny(warnings)]` +- Use `clippy` for linting and code improvement suggestions +- Maintain 100% test coverage with Rust's testing framework +- Use `rustfmt` for consistent code formatting +- Document code with doc comments and examples +- Ensure thread-safety with `Send` and `Sync` checks +- Minimize use of `unsafe` for better safety +- Implement meaningful error messages and handling +- Use `cargo-audit` to check for known vulnerabilities +- Benchmark critical code paths for performance insights + +## Output + +- Safe and performant Rust code adhering to best practices +- Concurrent code using async/await or multi-threading +- Clear error handling with `Result` and custom types +- Memory-efficient data structures and algorithms +- Well-documented code with examples and explanations +- Comprehensive tests with `cargo test` +- Consistently formatted with `rustfmt` +- Linted, optimized, and vulnerability-checked code +- Deliverables that follow Rust community standards +- Readable and maintainable code with idiomatic Rust syntax diff --git a/.iflow/agents/rust-pro.md b/.iflow/agents/rust-pro.md deleted file mode 100644 index d6348a5..0000000 --- a/.iflow/agents/rust-pro.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -agent-type: rust-pro -name: rust-pro -description: Write idiomatic Rust with ownership patterns, lifetimes, and trait implementations. Masters async/await, safe concurrency, and zero-cost abstractions. Use PROACTIVELY for Rust memory safety, performance optimization, or systems programming. -when-to-use: Write idiomatic Rust with ownership patterns, lifetimes, and trait implementations. Masters async/await, safe concurrency, and zero-cost abstractions. Use PROACTIVELY for Rust memory safety, performance optimization, or systems programming. -allowed-tools: -model: sonnet -inherit-tools: true -inherit-mcps: true -color: red ---- - -# Role - -You are a Rust expert specializing in safe, performant systems programming. - -## Focus Areas - -- Ownership, borrowing, and lifetime annotations -- Trait design and generic programming -- Async/await with Tokio/async-std -- Safe concurrency with Arc, Mutex, channels -- Error handling with Result and custom errors -- FFI and unsafe code when necessary - -## Approach - -1. Leverage the type system for correctness -2. Zero-cost abstractions over runtime checks -3. Explicit error handling - no panics in libraries -4. Use iterators over manual loops -5. Minimize unsafe blocks with clear invariants - -## Output - -- Idiomatic Rust with proper error handling -- Trait implementations with derive macros -- Async code with proper cancellation -- Unit tests and documentation tests -- Benchmarks with criterion.rs -- Cargo.toml with feature flags - -Follow clippy lints. Include examples in doc comments. diff --git a/.iflow/settings.local.json b/.iflow/settings.local.json new file mode 100644 index 0000000..bb3d700 --- /dev/null +++ b/.iflow/settings.local.json @@ -0,0 +1,18 @@ +{ + "permissions": { + "allow": [ + "Bash(while read dir)", + "Bash(done)", + "Bash(cargo test:*)", + "Read(//tmp/**)", + "Bash(cargo check:*)", + "Bash(cargo doc:*)", + "Bash(cargo add:*)", + "Bash(cargo tree:*)", + "Bash(find:*)", + "Bash(cargo clippy:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0154c9d..ef0c48e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ "async-broadcast", "async-trait", "derive_more", - "futures", + "futures 0.3.31", "log", "parking_lot", "serde", @@ -250,6 +250,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" + [[package]] name = "futures" version = "0.3.31" @@ -327,6 +333,7 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures 0.1.31", "futures-channel", "futures-core", "futures-io", @@ -492,7 +499,8 @@ dependencies = [ "agent-client-protocol", "anyhow", "async-trait", - "futures", + "futures 0.3.31", + "futures-util", "serde", "serde_json", "serial_test", @@ -879,7 +887,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" dependencies = [ - "futures", + "futures 0.3.31", "log", "once_cell", "parking_lot", diff --git a/Cargo.toml b/Cargo.toml index 22574fb..8cac428 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1.18", features = ["v4"] } url = "2.5" tokio-tungstenite = "0.28" +futures-util = { version = "0.3.31", features = ["compat"] } # Optional dependencies for enhanced functionality # base64 = { version = "0.22", optional = true } @@ -56,28 +57,32 @@ serial_test = "3.2.0" [[example]] name = "basic_client" -path = "examples/basic_client.rs" +path = "examples/basic/basic_client.rs" [[example]] name = "query" -path = "examples/query.rs" +path = "examples/query/query.rs" [[example]] name = "logging_example" -path = "examples/logging_example.rs" +path = "examples/advanced/logging_example.rs" [[example]] name = "websocket_client" -path = "examples/websocket_client.rs" +path = "examples/websocket/websocket_client.rs" [[example]] name = "permission_modes" -path = "examples/permission_modes.rs" +path = "examples/advanced/permission_modes.rs" [[example]] name = "mcp_example" -path = "examples/mcp_example.rs" +path = "examples/advanced/mcp_example.rs" [[test]] name = "message_tests" -path = "tests/message_tests.rs" +path = "tests/core/message_tests.rs" + +[[test]] +name = "timeout_test" +path = "tests/core/timeout_test.rs" diff --git a/examples/explore_api.rs b/examples/advanced/explore_api.rs similarity index 100% rename from examples/explore_api.rs rename to examples/advanced/explore_api.rs diff --git a/examples/logging_example.rs b/examples/advanced/logging_example.rs similarity index 97% rename from examples/logging_example.rs rename to examples/advanced/logging_example.rs index 418a5a1..e9d6f73 100644 --- a/examples/logging_example.rs +++ b/examples/advanced/logging_example.rs @@ -1,6 +1,6 @@ //! Logging example - Record raw iflow messages (Debug format) -use iflow_cli_sdk_rust::types::{PlanEntry, PlanPriority, PlanStatus}; +use iflow_cli_sdk_rust::message::types::{PlanEntry, PlanPriority, PlanStatus}; use iflow_cli_sdk_rust::{LoggerConfig, Message, MessageLogger}; #[tokio::main] diff --git a/examples/mcp_example.rs b/examples/advanced/mcp_example.rs similarity index 96% rename from examples/mcp_example.rs rename to examples/advanced/mcp_example.rs index 3ba51cc..e88ee4d 100644 --- a/examples/mcp_example.rs +++ b/examples/advanced/mcp_example.rs @@ -40,11 +40,11 @@ async fn main() -> Result<(), Box> { let options = IFlowOptions::new() .with_mcp_servers(mcp_servers) .with_process_config( - iflow_cli_sdk_rust::types::ProcessConfig::new() + iflow_cli_sdk_rust::config::ProcessConfig::new() .enable_auto_start() .start_port(8090) ) - .with_logging_config(iflow_cli_sdk_rust::types::LoggingConfig { + .with_logging_config(iflow_cli_sdk_rust::config::LoggingConfig { enabled: true, level: "INFO".to_string(), logger_config: iflow_cli_sdk_rust::logger::LoggerConfig { diff --git a/examples/permission_modes.rs b/examples/advanced/permission_modes.rs similarity index 95% rename from examples/permission_modes.rs rename to examples/advanced/permission_modes.rs index 2635c56..5227203 100644 --- a/examples/permission_modes.rs +++ b/examples/advanced/permission_modes.rs @@ -1,8 +1,9 @@ //! Example showing different permission modes with iFlow use futures::stream::StreamExt; -use iflow_cli_sdk_rust::types::PermissionMode; -use iflow_cli_sdk_rust::{IFlowClient, IFlowOptions, Message}; +use iflow_cli_sdk_rust::message::types::PermissionMode; +use iflow_cli_sdk_rust::{IFlowClient, IFlowOptions}; +use iflow_cli_sdk_rust::message::types::Message; use iflow_cli_sdk_rust::error::IFlowError; use std::io::Write; @@ -36,9 +37,9 @@ async fn demonstrate_permission_mode( // Configure client options with WebSocket configuration and specific permission mode // In auto start mode, we let the SDK generate the WebSocket URL let options = IFlowOptions::new() - .with_websocket_config(iflow_cli_sdk_rust::types::WebSocketConfig::auto_start()) + .with_websocket_config(iflow_cli_sdk_rust::config::WebSocketConfig::auto_start()) .with_process_config( - iflow_cli_sdk_rust::types::ProcessConfig::new().enable_auto_start(), + iflow_cli_sdk_rust::config::ProcessConfig::new().enable_auto_start(), ) .with_permission_mode(mode); @@ -119,7 +120,7 @@ async fn demonstrate_permission_mode( let mut prompt = String::from("请按顺序执行下面 9=10 个独立操作。\n"); for s in steps { prompt.push_str(s); - prompt.push_str("\n"); + prompt.push('\n'); } prompt.push_str(""); prompt diff --git a/examples/README.md b/examples/basic/README.md similarity index 100% rename from examples/README.md rename to examples/basic/README.md diff --git a/examples/basic_client.rs b/examples/basic/basic_client.rs similarity index 96% rename from examples/basic_client.rs rename to examples/basic/basic_client.rs index 6af375c..d8cff0d 100644 --- a/examples/basic_client.rs +++ b/examples/basic/basic_client.rs @@ -21,11 +21,11 @@ async fn main() -> Result<(), Box> { // Configure client options with auto-start enabled for stdio mode let options = IFlowOptions::new() .with_process_config( - iflow_cli_sdk_rust::types::ProcessConfig::new() + iflow_cli_sdk_rust::config::ProcessConfig::new() .enable_auto_start() .stdio_mode(), ) - .with_logging_config(iflow_cli_sdk_rust::types::LoggingConfig { + .with_logging_config(iflow_cli_sdk_rust::config::LoggingConfig { enabled: true, level: "INFO".to_string(), logger_config: iflow_cli_sdk_rust::logger::LoggerConfig { @@ -88,7 +88,7 @@ async fn main() -> Result<(), Box> { // Send a message let prompt = "Create a plan to introduce this project."; println!("📤 Sending: {}", prompt); - + // Handle the send_message result to catch timeout errors match client.send_message(prompt, None).await { Ok(()) => { diff --git a/examples/query.rs b/examples/query/query.rs similarity index 100% rename from examples/query.rs rename to examples/query/query.rs diff --git a/examples/query_with_config.rs b/examples/query/query_with_config.rs similarity index 100% rename from examples/query_with_config.rs rename to examples/query/query_with_config.rs diff --git a/examples/test_response.rs b/examples/query/test_response.rs similarity index 98% rename from examples/test_response.rs rename to examples/query/test_response.rs index 4950026..dd3ef43 100644 --- a/examples/test_response.rs +++ b/examples/query/test_response.rs @@ -21,7 +21,7 @@ async fn main() -> Result<(), Box> { .run_until(async { // Configure client options with auto-start enabled for stdio mode let options = IFlowOptions::new().with_timeout(30.0).with_process_config( - iflow_cli_sdk_rust::types::ProcessConfig::new() + iflow_cli_sdk_rust::config::ProcessConfig::new() .enable_auto_start() .stdio_mode(), ); diff --git a/examples/websocket_client.rs b/examples/websocket/websocket_client.rs similarity index 96% rename from examples/websocket_client.rs rename to examples/websocket/websocket_client.rs index ff5410d..919bbc3 100644 --- a/examples/websocket_client.rs +++ b/examples/websocket/websocket_client.rs @@ -21,10 +21,10 @@ async fn main() -> Result<(), Box> { // Configure client options with WebSocket configuration and custom timeout let custom_timeout_secs = 120.0; let options = IFlowOptions::new() - .with_websocket_config(iflow_cli_sdk_rust::types::WebSocketConfig::auto_start()) + .with_websocket_config(iflow_cli_sdk_rust::config::WebSocketConfig::auto_start()) .with_timeout(custom_timeout_secs) .with_process_config( - iflow_cli_sdk_rust::types::ProcessConfig::new() + iflow_cli_sdk_rust::config::ProcessConfig::new() .enable_auto_start() .start_port(8090), ); diff --git a/examples/websocket_client_debug.rs b/examples/websocket/websocket_client_debug.rs similarity index 97% rename from examples/websocket_client_debug.rs rename to examples/websocket/websocket_client_debug.rs index 57cbc73..66ceaab 100644 --- a/examples/websocket_client_debug.rs +++ b/examples/websocket/websocket_client_debug.rs @@ -39,11 +39,11 @@ async fn main() -> Result<(), Box> { // Configure client options with WebSocket configuration and debug mode let custom_timeout_secs = 120.0; // Back to default timeout let options = IFlowOptions::new() - .with_websocket_config(iflow_cli_sdk_rust::types::WebSocketConfig::auto_start()) + .with_websocket_config(iflow_cli_sdk_rust::config::WebSocketConfig::auto_start()) .with_timeout(custom_timeout_secs) .with_mcp_servers(mcp_servers) .with_process_config( - iflow_cli_sdk_rust::types::ProcessConfig::new() + iflow_cli_sdk_rust::config::ProcessConfig::new() .enable_auto_start() .start_port(8090) .enable_debug(), // Enable debug mode diff --git a/examples/websocket_existing_process.rs b/examples/websocket/websocket_existing_process.rs similarity index 98% rename from examples/websocket_existing_process.rs rename to examples/websocket/websocket_existing_process.rs index f3800e1..e9605f6 100644 --- a/examples/websocket_existing_process.rs +++ b/examples/websocket/websocket_existing_process.rs @@ -69,7 +69,7 @@ async fn main() -> Result<(), Box> { // Configure client options to connect to the existing process let custom_timeout_secs = 120.0; let options = IFlowOptions::new() - .with_websocket_config(iflow_cli_sdk_rust::types::WebSocketConfig::new( + .with_websocket_config(iflow_cli_sdk_rust::config::WebSocketConfig::new( "ws://localhost:8093/acp?peer=iflow".to_string(), )) .with_timeout(custom_timeout_secs); diff --git a/examples/websocket_mcp.rs b/examples/websocket/websocket_mcp.rs similarity index 95% rename from examples/websocket_mcp.rs rename to examples/websocket/websocket_mcp.rs index 6e47f9b..b5e84ea 100644 --- a/examples/websocket_mcp.rs +++ b/examples/websocket/websocket_mcp.rs @@ -41,11 +41,11 @@ async fn main() -> Result<(), Box> { // Configure client options with WebSocket configuration and MCP servers let options = IFlowOptions::new() - .with_websocket_config(iflow_cli_sdk_rust::types::WebSocketConfig::auto_start()) + .with_websocket_config(iflow_cli_sdk_rust::config::WebSocketConfig::auto_start()) .with_mcp_servers(mcp_servers) // Set a reasonable timeout .with_timeout(timeout_secs) - .with_logging_config(iflow_cli_sdk_rust::types::LoggingConfig { + .with_logging_config(iflow_cli_sdk_rust::config::LoggingConfig { enabled: true, level: "INFO".to_string(), logger_config: iflow_cli_sdk_rust::logger::LoggerConfig { @@ -55,7 +55,7 @@ async fn main() -> Result<(), Box> { max_files: 5, }, }) - .with_permission_mode(iflow_cli_sdk_rust::types::PermissionMode::Auto); + .with_permission_mode(iflow_cli_sdk_rust::message::types::PermissionMode::Auto); // Create and connect client let mut client = IFlowClient::new(Some(options)); diff --git a/src/acp_protocol.rs b/src/acp_protocol.rs index 511bcfe..f26b43f 100644 --- a/src/acp_protocol.rs +++ b/src/acp_protocol.rs @@ -5,7 +5,8 @@ //! and protocol flow. use crate::error::{IFlowError, Result}; -use crate::types::{IFlowOptions, Message, PermissionMode}; +use crate::config::options::IFlowOptions; +use crate::message::types::{Message, PermissionMode}; use crate::websocket_transport::WebSocketTransport; use serde_json::{Value, json}; use std::collections::HashMap; @@ -540,10 +541,9 @@ impl ACPProtocol { }; // Check if this is the response we're waiting for - if let Some(id) = data.get("id").and_then(|v| v.as_u64()) { - if id == request_id as u64 { - return Ok(data); - } + if let Some(id) = data.get("id").and_then(|v| v.as_u64()) + && id == request_id as u64 { + return Ok(data); } // If not our response, process as a notification @@ -610,16 +610,15 @@ impl ACPProtocol { // Check if this is the response we're waiting for if let Some(id) = data.get("id").and_then(|v| v.as_u64()) { // Handle permission requests that come with an ID - if let Some(method) = data.get("method").and_then(|v| v.as_str()) { - if method == "session/request_permission" { - tracing::debug!("Handling session/request_permission with ID: {}", id); - // Process the permission request immediately - if let Err(e) = self.handle_client_method(method, data.clone()).await { - tracing::warn!("Failed to handle permission request: {}", e); - } - // Continue waiting for the main response - continue; + if let Some(method) = data.get("method").and_then(|v| v.as_str()) + && method == "session/request_permission" { + tracing::debug!("Handling session/request_permission with ID: {}", id); + // Process the permission request immediately + if let Err(e) = self.handle_client_method(method, data.clone()).await { + tracing::warn!("Failed to handle permission request: {}", e); } + // Continue waiting for the main response + continue; } // If this is the response we're waiting for, return it @@ -646,10 +645,9 @@ impl ACPProtocol { /// * `Err(IFlowError)` if handling failed async fn handle_notification(&mut self, data: Value) -> Result<()> { // Handle method calls from server (client interface) - if let Some(method) = data.get("method").and_then(|v| v.as_str()) { - if data.get("result").is_none() && data.get("error").is_none() { - self.handle_client_method(method, data.clone()).await?; - } + if let Some(method) = data.get("method").and_then(|v| v.as_str()) + && data.get("result").is_none() && data.get("error").is_none() { + self.handle_client_method(method, data.clone()).await?; } Ok(()) @@ -670,13 +668,12 @@ impl ACPProtocol { match method { "session/update" => { - if let Some(update_obj) = params.get("update").and_then(|v| v.as_object()) { - if let Some(session_update) = + if let Some(update_obj) = params.get("update").and_then(|v| v.as_object()) + && let Some(session_update) = update_obj.get("sessionUpdate").and_then(|v| v.as_str()) - { - self.handle_session_update(session_update, update_obj, request_id) - .await?; - } + { + self.handle_session_update(session_update, update_obj, request_id) + .await?; } } "session/request_permission" => { @@ -772,10 +769,9 @@ impl ACPProtocol { } } // Fallback to first option's optionId if no specific option found - if selected_option == "proceed_once" && !options_array.is_empty() { - if let Some(first_option_id) = options_array[0].get("optionId").and_then(|v| v.as_str()) { - selected_option = first_option_id.to_string(); - } + if selected_option == "proceed_once" && !options_array.is_empty() + && let Some(first_option_id) = options_array[0].get("optionId").and_then(|v| v.as_str()) { + selected_option = first_option_id.to_string(); } } RequestPermissionResponse { @@ -885,7 +881,7 @@ impl ACPProtocol { } "plan" => { if let Some(entries) = update.get("entries").and_then(|v| v.as_array()) { - let entries: Vec = entries + let entries: Vec = entries .iter() .filter_map(|entry| { let content = @@ -900,20 +896,20 @@ impl ACPProtocol { .unwrap_or("pending"); let priority = match priority_str { - "high" => super::types::PlanPriority::High, - "medium" => super::types::PlanPriority::Medium, - "low" => super::types::PlanPriority::Low, - _ => super::types::PlanPriority::Medium, + "high" => crate::message::types::PlanPriority::High, + "medium" => crate::message::types::PlanPriority::Medium, + "low" => crate::message::types::PlanPriority::Low, + _ => crate::message::types::PlanPriority::Medium, }; let status = match status_str { - "pending" => super::types::PlanStatus::Pending, - "in_progress" => super::types::PlanStatus::InProgress, - "completed" => super::types::PlanStatus::Completed, - _ => super::types::PlanStatus::Pending, + "pending" => crate::message::types::PlanStatus::Pending, + "in_progress" => crate::message::types::PlanStatus::InProgress, + "completed" => crate::message::types::PlanStatus::Completed, + _ => crate::message::types::PlanStatus::Pending, }; - Some(super::types::PlanEntry { + Some(crate::message::types::PlanEntry { content, priority, status, diff --git a/src/client.rs b/src/client.rs deleted file mode 100644 index 0138fa9..0000000 --- a/src/client.rs +++ /dev/null @@ -1,905 +0,0 @@ -//! Main client implementation for iFlow SDK -//! -//! This module provides the core client functionality for communicating with iFlow -//! using the [Agent Client Protocol (ACP)](https://github.com/agentclientprotocol/agent-client-protocol) over stdio or WebSocket. - -use crate::acp_protocol::ACPProtocol; -use crate::error::{IFlowError, Result}; -use crate::logger::MessageLogger; -use crate::process_manager::IFlowProcessManager; -use crate::types::*; -use crate::websocket_transport::WebSocketTransport; -use agent_client_protocol::{ - Agent, Client, ClientSideConnection, ContentBlock, SessionId, SessionUpdate, -}; -use futures::{FutureExt, pin_mut, stream::Stream}; -use serde_json; -use std::path::Path; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll}; - -// ChildStdin import moved to where it's used -use tokio::sync::{Mutex, mpsc}; -use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; -use tracing::debug; - -/// Connection type for iFlow client -enum Connection { - /// Stdio connection using agent-client-protocol - Stdio { - acp_client: ClientSideConnection, - process_manager: Option, - session_id: Option, - initialized: bool, - }, - /// WebSocket connection using custom implementation - WebSocket { - acp_protocol: ACPProtocol, - session_id: Option, - process_manager: Option, - }, -} - -/// Main client for bidirectional communication with iFlow -/// -/// This client handles the full lifecycle of communication with iFlow, -/// including process management, connection handling, and message passing. -pub struct IFlowClient { - options: IFlowOptions, - message_receiver: Arc>>, - message_sender: mpsc::UnboundedSender, - connected: Arc>, - connection: Option, - logger: Option, -} - -/// Stream of messages from iFlow -/// -/// This stream provides asynchronous access to messages received from iFlow. -/// It implements the `futures::Stream` trait for easy integration with async code. -pub struct MessageStream { - receiver: Arc>>, -} - -impl Stream for MessageStream { - type Item = Message; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let mut receiver = match self.receiver.try_lock() { - Ok(guard) => guard, - Err(_) => { - cx.waker().wake_by_ref(); - return Poll::Pending; - } - }; - - // Use asynchronous receiving - match receiver.try_recv() { - Ok(msg) => Poll::Ready(Some(msg)), - Err(mpsc::error::TryRecvError::Empty) => { - // Register a waker to be notified when new messages arrive - let recv_future = receiver.recv(); - pin_mut!(recv_future); - match recv_future.poll_unpin(cx) { - Poll::Ready(msg) => Poll::Ready(msg), - Poll::Pending => Poll::Pending, - } - } - Err(mpsc::error::TryRecvError::Disconnected) => Poll::Ready(None), - } - } -} - -// Implement the Client trait for handling ACP messages -struct IFlowClientHandler { - message_sender: mpsc::UnboundedSender, - logger: Option, -} - -#[async_trait::async_trait(?Send)] -impl Client for IFlowClientHandler { - async fn request_permission( - &self, - _args: agent_client_protocol::RequestPermissionRequest, - ) -> anyhow::Result< - agent_client_protocol::RequestPermissionResponse, - agent_client_protocol::Error, - > { - // For now, cancel all permissions - Ok(agent_client_protocol::RequestPermissionResponse { - outcome: agent_client_protocol::RequestPermissionOutcome::Cancelled, - meta: None, - }) - } - - async fn write_text_file( - &self, - _args: agent_client_protocol::WriteTextFileRequest, - ) -> anyhow::Result - { - Err(agent_client_protocol::Error::method_not_found()) - } - - async fn read_text_file( - &self, - _args: agent_client_protocol::ReadTextFileRequest, - ) -> anyhow::Result - { - Err(agent_client_protocol::Error::method_not_found()) - } - - async fn create_terminal( - &self, - _args: agent_client_protocol::CreateTerminalRequest, - ) -> anyhow::Result - { - Err(agent_client_protocol::Error::method_not_found()) - } - - async fn terminal_output( - &self, - _args: agent_client_protocol::TerminalOutputRequest, - ) -> anyhow::Result - { - Err(agent_client_protocol::Error::method_not_found()) - } - - async fn release_terminal( - &self, - _args: agent_client_protocol::ReleaseTerminalRequest, - ) -> anyhow::Result - { - Err(agent_client_protocol::Error::method_not_found()) - } - - async fn wait_for_terminal_exit( - &self, - _args: agent_client_protocol::WaitForTerminalExitRequest, - ) -> anyhow::Result< - agent_client_protocol::WaitForTerminalExitResponse, - agent_client_protocol::Error, - > { - Err(agent_client_protocol::Error::method_not_found()) - } - - async fn kill_terminal_command( - &self, - _args: agent_client_protocol::KillTerminalCommandRequest, - ) -> anyhow::Result< - agent_client_protocol::KillTerminalCommandResponse, - agent_client_protocol::Error, - > { - Err(agent_client_protocol::Error::method_not_found()) - } - - async fn session_notification( - &self, - args: agent_client_protocol::SessionNotification, - ) -> anyhow::Result<(), agent_client_protocol::Error> { - match args.update { - SessionUpdate::AgentMessageChunk { content } => { - let text = match content { - ContentBlock::Text(text_content) => text_content.text, - ContentBlock::Image(_) => "".into(), - ContentBlock::Audio(_) => "