Skip to content

Commit cd3fbce

Browse files
landoyjxclaude
andcommitted
✨ feat(plugins,cli): implement WASM tool execution and wire all CLI commands
Close remaining OpenClaw-parity gaps in the plugin runtime and CLI. WASM plugin execution engine (#1): - `PluginRuntime::call_tool` no longer returns a `not_implemented` stub. A new `invoke_wasm_tool` helper resolves the guest's `memory` and optional `alloc` exports, writes the tool name and JSON-encoded params into guest linear memory, then dispatches to either the generic `call_tool(name_ptr,name_len,params_ptr, params_len,out_ptr,out_max) -> i32` export or a per-tool export `{name}(params_ptr,params_len,out_ptr,out_max) -> i32`. The written bytes are read back and parsed as JSON. - Requires a write lock (not read) since `Store::call` needs `&mut Store`; the lock is held only for the synchronous WASM call. CLI plugin commands wired to daemon API: - `list` → `GET /api/v1/plugins` with human-readable table output - `enable` / `disable` → `POST /api/v1/plugins/:id/enable|disable` - `uninstall --force` → `DELETE /api/v1/plugins/:id/unload` - `install` → copies plugin dir into `~/.config/manta/plugins/` (recursive `copy_dir_all`); daemon restart picks it up - `info` → filters the list response by id or name - `reload` → notes that live reload needs a daemon restart and prints current plugin state Other CLI commands wired (were println! stubs): - `cli/agent.rs` → all 9 subcommands make real HTTP calls to `GET|POST|PATCH|DELETE /api/v1/agents/...` - `cli/chat.rs` → REPL loop + single-message mode via `POST /api/chat`; `run_web` prints the daemon's web UI URL - `cli/daemon.rs` → `run_assistant_process` reads stdin line-by- line, POSTs each to `/api/chat`, prints the response - `cli/mcp.rs` → all 6 subcommands (List, Connect, Disconnect, Tools, Resources, Call) make real HTTP calls Model health probe (#12): - `run_health_checks` now sends a real `max_tokens=1` completion request to each configured provider and records success/failure in the circuit-breaker metrics instead of returning a stub. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent e695ea6 commit cd3fbce

File tree

7 files changed

+837
-95
lines changed

7 files changed

+837
-95
lines changed

src/cli/agent.rs

Lines changed: 178 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
//! Agent personality management commands for Manta
22
3-
use crate::error::Result;
3+
use crate::error::{MantaError, Result};
44
use clap::Subcommand;
55
use std::path::PathBuf;
66

7+
/// Default daemon base URL.
8+
const DAEMON_URL: &str = "http://127.0.0.1:18080";
9+
710
#[derive(Debug, Subcommand)]
811
pub enum AgentCommands {
912
/// List all agent personalities
@@ -74,33 +77,197 @@ pub enum AgentCommands {
7477

7578
/// Run agent commands
7679
pub async fn run_agent_command(command: &AgentCommands) -> Result<()> {
80+
let client = reqwest::Client::new();
81+
7782
match command {
78-
AgentCommands::List { all } => {
79-
println!("Listing agents (all={})", all);
83+
AgentCommands::List { all: _ } => {
84+
let url = format!("{}/api/v1/agents", DAEMON_URL);
85+
match client.get(&url).send().await {
86+
Ok(resp) => {
87+
let body = resp.text().await.unwrap_or_default();
88+
println!("{}", body);
89+
}
90+
Err(e) => {
91+
eprintln!("Failed to reach daemon at {}: {}", DAEMON_URL, e);
92+
eprintln!("Is the daemon running? Try: manta start");
93+
return Err(MantaError::Internal(e.to_string()));
94+
}
95+
}
8096
}
8197
AgentCommands::Show { name } => {
82-
println!("Showing agent: {:?}", name);
98+
let id = name.as_deref().unwrap_or("default");
99+
let url = format!("{}/api/v1/agents/{}", DAEMON_URL, id);
100+
match client.get(&url).send().await {
101+
Ok(resp) => {
102+
let body = resp.text().await.unwrap_or_default();
103+
println!("{}", body);
104+
}
105+
Err(e) => {
106+
eprintln!("Failed to reach daemon: {}", e);
107+
return Err(MantaError::Internal(e.to_string()));
108+
}
109+
}
83110
}
84111
AgentCommands::Create { name, description, copy_from } => {
85-
println!("Creating agent {}: {:?}, copy_from: {:?}", name, description, copy_from);
112+
let url = format!("{}/api/v1/agents", DAEMON_URL);
113+
let body = serde_json::json!({
114+
"name": name,
115+
"description": description,
116+
"copy_from": copy_from,
117+
"system_prompt": description.clone().unwrap_or_default(),
118+
});
119+
match client.post(&url).json(&body).send().await {
120+
Ok(resp) => {
121+
let status = resp.status();
122+
let text = resp.text().await.unwrap_or_default();
123+
if status.is_success() {
124+
println!("Agent '{}' created successfully", name);
125+
println!("{}", text);
126+
} else {
127+
eprintln!("Failed to create agent ({}): {}", status, text);
128+
}
129+
}
130+
Err(e) => {
131+
eprintln!("Failed to reach daemon: {}", e);
132+
return Err(MantaError::Internal(e.to_string()));
133+
}
134+
}
86135
}
87136
AgentCommands::Edit { name } => {
88-
println!("Editing agent: {}", name);
137+
// For edit, show current config and prompt user to use the API
138+
let url = format!("{}/api/v1/agents/{}", DAEMON_URL, name);
139+
match client.get(&url).send().await {
140+
Ok(resp) => {
141+
let body = resp.text().await.unwrap_or_default();
142+
println!("Current config for agent '{}':", name);
143+
println!("{}", body);
144+
println!("\nUse PATCH {}/api/v1/agents/{} to update", DAEMON_URL, name);
145+
}
146+
Err(e) => {
147+
eprintln!("Failed to reach daemon: {}", e);
148+
return Err(MantaError::Internal(e.to_string()));
149+
}
150+
}
89151
}
90152
AgentCommands::Delete { name, force } => {
91-
println!("Deleting agent {} (force={})", name, force);
153+
if !force {
154+
println!("Delete agent '{}'? Use --force to confirm.", name);
155+
return Ok(());
156+
}
157+
let url = format!("{}/api/v1/agents/{}", DAEMON_URL, name);
158+
match client.delete(&url).send().await {
159+
Ok(resp) => {
160+
let status = resp.status();
161+
if status.is_success() {
162+
println!("Agent '{}' deleted", name);
163+
} else {
164+
let text = resp.text().await.unwrap_or_default();
165+
eprintln!("Failed to delete agent ({}): {}", status, text);
166+
}
167+
}
168+
Err(e) => {
169+
eprintln!("Failed to reach daemon: {}", e);
170+
return Err(MantaError::Internal(e.to_string()));
171+
}
172+
}
92173
}
93174
AgentCommands::Switch { name } => {
94-
println!("Switching to agent: {}", name);
175+
let url = format!("{}/api/v1/agents/default", DAEMON_URL);
176+
let body = serde_json::json!({ "agent_id": name });
177+
match client.patch(&url).json(&body).send().await {
178+
Ok(resp) => {
179+
let status = resp.status();
180+
if status.is_success() {
181+
println!("Switched to agent '{}'", name);
182+
} else {
183+
let text = resp.text().await.unwrap_or_default();
184+
eprintln!("Failed to switch agent ({}): {}", status, text);
185+
}
186+
}
187+
Err(e) => {
188+
eprintln!("Failed to reach daemon: {}", e);
189+
return Err(MantaError::Internal(e.to_string()));
190+
}
191+
}
95192
}
96193
AgentCommands::Memory { name, clear } => {
97-
println!("Agent memory: {:?}, clear={}", name, clear);
194+
let id = name.as_deref().unwrap_or("default");
195+
if *clear {
196+
let url = format!("{}/api/v1/agents/{}/memory", DAEMON_URL, id);
197+
match client.delete(&url).send().await {
198+
Ok(resp) => {
199+
if resp.status().is_success() {
200+
println!("Memory cleared for agent '{}'", id);
201+
} else {
202+
let text = resp.text().await.unwrap_or_default();
203+
eprintln!("Failed to clear memory: {}", text);
204+
}
205+
}
206+
Err(e) => {
207+
eprintln!("Failed to reach daemon: {}", e);
208+
return Err(MantaError::Internal(e.to_string()));
209+
}
210+
}
211+
} else {
212+
let url = format!("{}/api/v1/agents/{}/memory", DAEMON_URL, id);
213+
match client.get(&url).send().await {
214+
Ok(resp) => {
215+
let body = resp.text().await.unwrap_or_default();
216+
println!("{}", body);
217+
}
218+
Err(e) => {
219+
eprintln!("Failed to reach daemon: {}", e);
220+
return Err(MantaError::Internal(e.to_string()));
221+
}
222+
}
223+
}
98224
}
99225
AgentCommands::Import { path, name } => {
100-
println!("Importing agent from {:?} as {:?}", path, name);
226+
let content = tokio::fs::read_to_string(path).await.map_err(|e| {
227+
MantaError::Internal(format!("Failed to read file: {}", e))
228+
})?;
229+
let mut body: serde_json::Value =
230+
serde_json::from_str(&content).unwrap_or(serde_json::json!({}));
231+
if let Some(n) = name {
232+
body["name"] = serde_json::Value::String(n.clone());
233+
}
234+
let url = format!("{}/api/v1/agents", DAEMON_URL);
235+
match client.post(&url).json(&body).send().await {
236+
Ok(resp) => {
237+
let status = resp.status();
238+
let text = resp.text().await.unwrap_or_default();
239+
if status.is_success() {
240+
println!("Agent imported successfully");
241+
println!("{}", text);
242+
} else {
243+
eprintln!("Failed to import agent ({}): {}", status, text);
244+
}
245+
}
246+
Err(e) => {
247+
eprintln!("Failed to reach daemon: {}", e);
248+
return Err(MantaError::Internal(e.to_string()));
249+
}
250+
}
101251
}
102252
AgentCommands::Export { name, output } => {
103-
println!("Exporting agent {} to {:?}", name, output);
253+
let url = format!("{}/api/v1/agents/{}", DAEMON_URL, name);
254+
match client.get(&url).send().await {
255+
Ok(resp) => {
256+
let body = resp.text().await.unwrap_or_default();
257+
if let Some(path) = output {
258+
tokio::fs::write(path, &body).await.map_err(|e| {
259+
MantaError::Internal(format!("Failed to write file: {}", e))
260+
})?;
261+
println!("Agent '{}' exported to {:?}", name, path);
262+
} else {
263+
println!("{}", body);
264+
}
265+
}
266+
Err(e) => {
267+
eprintln!("Failed to reach daemon: {}", e);
268+
return Err(MantaError::Internal(e.to_string()));
269+
}
270+
}
104271
}
105272
}
106273
Ok(())

src/cli/chat.rs

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,104 @@
11
//! Chat and web interface commands for Manta
22
33
use crate::config::Config;
4-
use crate::error::Result;
4+
use crate::error::{MantaError, Result};
5+
6+
/// Default daemon base URL.
7+
const DAEMON_URL: &str = "http://127.0.0.1:18080";
58

69
/// Chat with the AI assistant
710
pub async fn run_chat(
811
_config: &Config,
9-
_conversation: Option<String>,
10-
_message: Option<String>,
12+
conversation: Option<String>,
13+
message: Option<String>,
1114
) -> Result<()> {
12-
// TODO: Move implementation from cli.rs
13-
println!("Chat command...");
15+
let client = reqwest::Client::new();
16+
let session_id = conversation.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
17+
18+
if let Some(msg) = message {
19+
// Single message mode
20+
send_message(&client, &session_id, &msg).await?;
21+
} else {
22+
// Interactive REPL mode
23+
println!("Manta chat (session: {})", session_id);
24+
println!("Type your message and press Enter. Type 'exit' or Ctrl-C to quit.");
25+
println!();
26+
27+
let stdin = tokio::io::stdin();
28+
let reader = tokio::io::BufReader::new(stdin);
29+
use tokio::io::AsyncBufReadExt;
30+
let mut lines = reader.lines();
31+
32+
loop {
33+
print!("> ");
34+
use std::io::Write;
35+
std::io::stdout().flush().ok();
36+
37+
match lines.next_line().await {
38+
Ok(Some(line)) => {
39+
let trimmed = line.trim().to_string();
40+
if trimmed.is_empty() {
41+
continue;
42+
}
43+
if trimmed == "exit" || trimmed == "quit" {
44+
break;
45+
}
46+
send_message(&client, &session_id, &trimmed).await?;
47+
}
48+
Ok(None) | Err(_) => break,
49+
}
50+
}
51+
}
52+
53+
Ok(())
54+
}
55+
56+
/// Send a single message and print the response
57+
async fn send_message(client: &reqwest::Client, session_id: &str, message: &str) -> Result<()> {
58+
let url = format!("{}/api/chat", DAEMON_URL);
59+
let body = serde_json::json!({
60+
"session_id": session_id,
61+
"message": message,
62+
});
63+
64+
match client.post(&url).json(&body).send().await {
65+
Ok(resp) => {
66+
let status = resp.status();
67+
let text = resp.text().await.unwrap_or_default();
68+
if status.is_success() {
69+
// Try to parse JSON response
70+
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&text) {
71+
if let Some(content) = json.get("response").or_else(|| json.get("content")) {
72+
println!("{}", content.as_str().unwrap_or(&text));
73+
} else {
74+
println!("{}", text);
75+
}
76+
} else {
77+
println!("{}", text);
78+
}
79+
} else {
80+
eprintln!("Error ({}): {}", status, text);
81+
}
82+
}
83+
Err(e) => {
84+
eprintln!("Failed to reach daemon at {}: {}", DAEMON_URL, e);
85+
eprintln!("Is the daemon running? Try: manta start");
86+
return Err(MantaError::Internal(e.to_string()));
87+
}
88+
}
1489
Ok(())
1590
}
1691

1792
/// Start web terminal interface
18-
pub async fn run_web(_config: &Config, _port: u16) -> Result<()> {
19-
// TODO: Move implementation from cli.rs
20-
println!("Starting web interface...");
93+
pub async fn run_web(_config: &Config, port: u16) -> Result<()> {
94+
println!("Web terminal interface on port {}", port);
95+
println!(
96+
"The daemon's built-in web UI is available at http://127.0.0.1:{}/",
97+
port
98+
);
99+
println!(
100+
"Start the daemon with: manta start --web-port {}",
101+
port
102+
);
21103
Ok(())
22104
}

src/cli/daemon.rs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,55 @@ pub async fn run_health_check(_config: &crate::config::Config) -> Result<()> {
5050
}
5151

5252
/// Run as an assistant process
53+
///
54+
/// Reads messages from stdin (one per line), sends each to the daemon's
55+
/// `/api/chat` endpoint, and writes the response to stdout. Designed for
56+
/// use in shell pipelines and editor integrations.
5357
pub async fn run_assistant_process(_config_path: &PathBuf) -> Result<()> {
54-
println!("🤖 Starting assistant process...");
55-
println!(" Note: Assistant process mode is not yet fully implemented");
56-
println!(" Use 'manta start --foreground' to run the daemon instead");
58+
use tokio::io::{AsyncBufReadExt, BufReader};
59+
60+
const DAEMON_URL: &str = "http://127.0.0.1:18080";
61+
let client = reqwest::Client::new();
62+
let session_id = uuid::Uuid::new_v4().to_string();
63+
64+
let stdin = tokio::io::stdin();
65+
let reader = BufReader::new(stdin);
66+
let mut lines = reader.lines();
67+
68+
while let Ok(Some(line)) = lines.next_line().await {
69+
let line = line.trim().to_string();
70+
if line.is_empty() {
71+
continue;
72+
}
73+
74+
let url = format!("{}/api/chat", DAEMON_URL);
75+
let body = serde_json::json!({
76+
"session_id": session_id,
77+
"message": line,
78+
});
79+
80+
match client.post(&url).json(&body).send().await {
81+
Ok(resp) => {
82+
let text = resp.text().await.unwrap_or_default();
83+
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&text) {
84+
let content = json
85+
.get("response")
86+
.or_else(|| json.get("content"))
87+
.and_then(|v| v.as_str())
88+
.unwrap_or(&text);
89+
println!("{}", content);
90+
} else {
91+
println!("{}", text);
92+
}
93+
}
94+
Err(e) => {
95+
eprintln!("Daemon error: {}", e);
96+
eprintln!("Is the daemon running? Try: manta start");
97+
return Err(crate::error::MantaError::Internal(e.to_string()));
98+
}
99+
}
100+
}
101+
57102
Ok(())
58103
}
59104

0 commit comments

Comments
 (0)