grok-cli-acp 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +42 -0
- package/.github/workflows/ci.yml +30 -0
- package/.github/workflows/rust.yml +22 -0
- package/.grok/.env.example +85 -0
- package/.grok/COMPLETE_FIX_SUMMARY.md +466 -0
- package/.grok/ENV_CONFIG_GUIDE.md +173 -0
- package/.grok/QUICK_REFERENCE.md +180 -0
- package/.grok/README.md +104 -0
- package/.grok/TESTING_GUIDE.md +393 -0
- package/CHANGELOG.md +465 -0
- package/CODE_REVIEW_SUMMARY.md +414 -0
- package/COMPLETE_FIX_SUMMARY.md +415 -0
- package/CONFIGURATION.md +489 -0
- package/CONTEXT_FILES_GUIDE.md +419 -0
- package/CONTRIBUTING.md +55 -0
- package/CURSOR_POSITION_FIX.md +206 -0
- package/Cargo.toml +88 -0
- package/ERROR_HANDLING_REPORT.md +361 -0
- package/FINAL_FIX_SUMMARY.md +462 -0
- package/FIXES.md +37 -0
- package/FIXES_SUMMARY.md +87 -0
- package/GROK_API_MIGRATION_SUMMARY.md +111 -0
- package/LICENSE +22 -0
- package/MIGRATION_TO_GROK_API.md +223 -0
- package/README.md +504 -0
- package/REVIEW_COMPLETE.md +416 -0
- package/REVIEW_QUICK_REFERENCE.md +173 -0
- package/SECURITY.md +463 -0
- package/SECURITY_AUDIT.md +661 -0
- package/SETUP.md +287 -0
- package/TESTING_TOOLS.md +88 -0
- package/TESTING_TOOL_EXECUTION.md +239 -0
- package/TOOL_EXECUTION_FIX.md +491 -0
- package/VERIFICATION_CHECKLIST.md +419 -0
- package/docs/API.md +74 -0
- package/docs/CHAT_LOGGING.md +39 -0
- package/docs/CURSOR_FIX_DEMO.md +306 -0
- package/docs/ERROR_HANDLING_GUIDE.md +547 -0
- package/docs/FILE_OPERATIONS.md +449 -0
- package/docs/INTERACTIVE.md +401 -0
- package/docs/PROJECT_CREATION_GUIDE.md +570 -0
- package/docs/QUICKSTART.md +378 -0
- package/docs/QUICK_REFERENCE.md +691 -0
- package/docs/RELEASE_NOTES_0.1.2.md +240 -0
- package/docs/TOOLS.md +459 -0
- package/docs/TOOLS_QUICK_REFERENCE.md +210 -0
- package/docs/ZED_INTEGRATION.md +371 -0
- package/docs/extensions.md +464 -0
- package/docs/settings.md +293 -0
- package/examples/extensions/logging-hook/README.md +91 -0
- package/examples/extensions/logging-hook/extension.json +22 -0
- package/package.json +30 -0
- package/scripts/test_acp.py +252 -0
- package/scripts/test_acp.sh +143 -0
- package/scripts/test_acp_simple.sh +72 -0
- package/src/acp/mod.rs +741 -0
- package/src/acp/protocol.rs +323 -0
- package/src/acp/security.rs +298 -0
- package/src/acp/tools.rs +697 -0
- package/src/bin/banner_demo.rs +216 -0
- package/src/bin/docgen.rs +18 -0
- package/src/bin/installer.rs +217 -0
- package/src/cli/app.rs +310 -0
- package/src/cli/commands/acp.rs +721 -0
- package/src/cli/commands/chat.rs +485 -0
- package/src/cli/commands/code.rs +513 -0
- package/src/cli/commands/config.rs +394 -0
- package/src/cli/commands/health.rs +442 -0
- package/src/cli/commands/history.rs +421 -0
- package/src/cli/commands/mod.rs +14 -0
- package/src/cli/commands/settings.rs +1384 -0
- package/src/cli/mod.rs +166 -0
- package/src/config/mod.rs +2212 -0
- package/src/display/ascii_art.rs +139 -0
- package/src/display/banner.rs +289 -0
- package/src/display/components/input.rs +323 -0
- package/src/display/components/mod.rs +2 -0
- package/src/display/components/settings_list.rs +306 -0
- package/src/display/interactive.rs +1255 -0
- package/src/display/mod.rs +62 -0
- package/src/display/terminal.rs +42 -0
- package/src/display/tips.rs +316 -0
- package/src/grok_client_ext.rs +177 -0
- package/src/hooks/loader.rs +407 -0
- package/src/hooks/mod.rs +158 -0
- package/src/lib.rs +174 -0
- package/src/main.rs +65 -0
- package/src/mcp/client.rs +195 -0
- package/src/mcp/config.rs +20 -0
- package/src/mcp/mod.rs +6 -0
- package/src/mcp/protocol.rs +67 -0
- package/src/utils/auth.rs +41 -0
- package/src/utils/chat_logger.rs +568 -0
- package/src/utils/context.rs +390 -0
- package/src/utils/mod.rs +16 -0
- package/src/utils/network.rs +320 -0
- package/src/utils/rate_limiter.rs +166 -0
- package/src/utils/session.rs +73 -0
- package/src/utils/shell_permissions.rs +389 -0
- package/src/utils/telemetry.rs +41 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
//! ACP (Agent Client Protocol) command handler for Zed integration
|
|
2
|
+
//!
|
|
3
|
+
//! This module handles all Agent Client Protocol operations, including starting
|
|
4
|
+
//! the ACP server for Zed editor integration, testing connections, and managing
|
|
5
|
+
//! ACP capabilities.
|
|
6
|
+
|
|
7
|
+
use anyhow::{Result, anyhow};
|
|
8
|
+
use colored::*;
|
|
9
|
+
use serde_json::{Value, json};
|
|
10
|
+
use std::net::SocketAddr;
|
|
11
|
+
use std::sync::Arc;
|
|
12
|
+
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
|
13
|
+
use tokio::net::TcpListener;
|
|
14
|
+
use tokio::sync::RwLock;
|
|
15
|
+
use tracing::{debug, error, info, warn};
|
|
16
|
+
|
|
17
|
+
use crate::acp::protocol::{
|
|
18
|
+
AGENT_METHOD_NAMES, AgentCapabilities, ContentBlock, ContentChunk, Implementation,
|
|
19
|
+
InitializeRequest, InitializeResponse, NewSessionRequest, NewSessionResponse, PromptRequest,
|
|
20
|
+
PromptResponse, ProtocolVersion, SessionId, SessionNotification, SessionUpdate, StopReason,
|
|
21
|
+
TextContent,
|
|
22
|
+
};
|
|
23
|
+
use crate::acp::{GrokAcpAgent, SessionConfig};
|
|
24
|
+
use crate::cli::{create_spinner, print_error, print_info, print_success, print_warning};
|
|
25
|
+
use crate::config::Config;
|
|
26
|
+
use crate::utils::chat_logger;
|
|
27
|
+
|
|
28
|
+
/// Handle ACP-related commands
|
|
29
|
+
pub async fn handle_acp_action(action: crate::AcpAction, config: &Config) -> Result<()> {
|
|
30
|
+
match action {
|
|
31
|
+
crate::AcpAction::Server { port, host } => start_acp_server(port, &host, config).await,
|
|
32
|
+
crate::AcpAction::Stdio { model } => start_acp_stdio(config, model).await,
|
|
33
|
+
crate::AcpAction::Test { address } => test_acp_connection(&address, config).await,
|
|
34
|
+
crate::AcpAction::Capabilities => show_acp_capabilities().await,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Start the ACP server for Zed integration
|
|
39
|
+
async fn start_acp_server(port: Option<u16>, host: &str, config: &Config) -> Result<()> {
|
|
40
|
+
if !config.acp.enabled {
|
|
41
|
+
print_warning(
|
|
42
|
+
"ACP is disabled in configuration. Enable it with 'grok config set acp.enabled true'",
|
|
43
|
+
);
|
|
44
|
+
return Ok(());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let bind_port = port.or(config.acp.default_port).unwrap_or(0);
|
|
48
|
+
let bind_addr = format!("{}:{}", host, bind_port);
|
|
49
|
+
|
|
50
|
+
print_info(&format!("Starting ACP server on {}", bind_addr));
|
|
51
|
+
|
|
52
|
+
let listener = TcpListener::bind(&bind_addr)
|
|
53
|
+
.await
|
|
54
|
+
.map_err(|e| anyhow!("Failed to bind ACP server to {}: {}", bind_addr, e))?;
|
|
55
|
+
|
|
56
|
+
let actual_addr = listener.local_addr()?;
|
|
57
|
+
print_success(&format!("ACP server listening on {}", actual_addr));
|
|
58
|
+
|
|
59
|
+
if config.acp.dev_mode {
|
|
60
|
+
print_info("Development mode enabled - additional debugging features available");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
println!();
|
|
64
|
+
println!("{}", "🔗 Zed Integration Instructions:".cyan().bold());
|
|
65
|
+
println!("1. Open Zed editor");
|
|
66
|
+
println!("2. Go to Settings → Extensions → Agent Client Protocol");
|
|
67
|
+
println!("3. Add a new agent configuration:");
|
|
68
|
+
println!(" {}", " - Name: Grok AI".green());
|
|
69
|
+
println!(
|
|
70
|
+
" {}",
|
|
71
|
+
format!(
|
|
72
|
+
" - Command: grok acp server --port {}",
|
|
73
|
+
actual_addr.port()
|
|
74
|
+
)
|
|
75
|
+
.green()
|
|
76
|
+
);
|
|
77
|
+
println!(" {}", format!(" - Address: {}", actual_addr).green());
|
|
78
|
+
println!("4. Enable the agent and start coding!");
|
|
79
|
+
println!();
|
|
80
|
+
println!("{}", "Press Ctrl+C to stop the server".dimmed());
|
|
81
|
+
|
|
82
|
+
let server_stats = Arc::new(RwLock::new(ServerStats::new()));
|
|
83
|
+
let server_config = config.clone();
|
|
84
|
+
|
|
85
|
+
loop {
|
|
86
|
+
match listener.accept().await {
|
|
87
|
+
Ok((stream, client_addr)) => {
|
|
88
|
+
info!("New ACP client connected: {}", client_addr);
|
|
89
|
+
|
|
90
|
+
let stats = Arc::clone(&server_stats);
|
|
91
|
+
let config = server_config.clone();
|
|
92
|
+
|
|
93
|
+
tokio::spawn(async move {
|
|
94
|
+
if let Err(e) = handle_acp_client(stream, client_addr, stats, config).await {
|
|
95
|
+
error!("ACP client error ({}): {}", client_addr, e);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
Err(e) => {
|
|
100
|
+
error!("Failed to accept ACP connection: {}", e);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
Ok(())
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async fn start_acp_stdio(config: &Config, model: Option<String>) -> Result<()> {
|
|
110
|
+
let stdin = tokio::io::stdin();
|
|
111
|
+
let stdout = tokio::io::stdout();
|
|
112
|
+
let agent = GrokAcpAgent::new(config.clone(), model).await?;
|
|
113
|
+
|
|
114
|
+
info!("Starting ACP session on stdio");
|
|
115
|
+
run_acp_session(stdin, stdout, agent).await
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Handle individual ACP client connections
|
|
119
|
+
async fn handle_acp_client(
|
|
120
|
+
stream: tokio::net::TcpStream,
|
|
121
|
+
client_addr: SocketAddr,
|
|
122
|
+
stats: Arc<RwLock<ServerStats>>,
|
|
123
|
+
config: Config,
|
|
124
|
+
) -> Result<()> {
|
|
125
|
+
info!("Handling ACP client: {}", client_addr);
|
|
126
|
+
|
|
127
|
+
// Update connection stats
|
|
128
|
+
{
|
|
129
|
+
let mut stats_guard = stats.write().await;
|
|
130
|
+
stats_guard.connections += 1;
|
|
131
|
+
stats_guard.active_connections += 1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Create the Grok ACP agent
|
|
135
|
+
let agent = GrokAcpAgent::new(config, None).await?;
|
|
136
|
+
|
|
137
|
+
// Split stream
|
|
138
|
+
let (reader, writer) = stream.into_split();
|
|
139
|
+
|
|
140
|
+
// Handle the ACP protocol over the stream
|
|
141
|
+
let result = run_acp_session(reader, writer, agent).await;
|
|
142
|
+
|
|
143
|
+
// Update stats on disconnect
|
|
144
|
+
{
|
|
145
|
+
let mut stats_guard = stats.write().await;
|
|
146
|
+
stats_guard.active_connections -= 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
match result {
|
|
150
|
+
Ok(()) => info!("ACP client {} disconnected cleanly", client_addr),
|
|
151
|
+
Err(e) => {
|
|
152
|
+
warn!("ACP client {} disconnected with error: {}", client_addr, e);
|
|
153
|
+
return Err(e);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
Ok(())
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Run an ACP session with a connected client
|
|
161
|
+
async fn run_acp_session<R, W>(reader: R, mut writer: W, agent: GrokAcpAgent) -> Result<()>
|
|
162
|
+
where
|
|
163
|
+
R: tokio::io::AsyncRead + Unpin,
|
|
164
|
+
W: tokio::io::AsyncWrite + Unpin,
|
|
165
|
+
{
|
|
166
|
+
let mut reader = BufReader::new(reader);
|
|
167
|
+
let mut line = String::new();
|
|
168
|
+
|
|
169
|
+
loop {
|
|
170
|
+
line.clear();
|
|
171
|
+
match reader.read_line(&mut line).await {
|
|
172
|
+
Ok(0) => break, // EOF
|
|
173
|
+
Ok(_) => {
|
|
174
|
+
let trimmed = line.trim();
|
|
175
|
+
if trimmed.is_empty() {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
debug!("Received message: {}", trimmed);
|
|
180
|
+
|
|
181
|
+
// Attempt to parse as JSON
|
|
182
|
+
match serde_json::from_str::<Value>(trimmed) {
|
|
183
|
+
Ok(json_msg) => {
|
|
184
|
+
// Handle JSON-RPC message
|
|
185
|
+
if let Err(e) = handle_json_rpc(&json_msg, &mut writer, &agent).await {
|
|
186
|
+
error!("Error handling message: {}", e);
|
|
187
|
+
// Optionally send error response back to client
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
Err(e) => {
|
|
191
|
+
warn!("Invalid JSON received: {} (Error: {})", trimmed, e);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
Err(e) => return Err(e.into()),
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
info!("ACP session completed");
|
|
200
|
+
|
|
201
|
+
// End chat logging session
|
|
202
|
+
if let Err(e) = chat_logger::end_session() {
|
|
203
|
+
warn!("Failed to end chat logging session: {}", e);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
Ok(())
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async fn handle_json_rpc<W>(msg: &Value, writer: &mut W, agent: &GrokAcpAgent) -> Result<()>
|
|
210
|
+
where
|
|
211
|
+
W: tokio::io::AsyncWrite + Unpin,
|
|
212
|
+
{
|
|
213
|
+
// Check if it's a request (has "method" and "id")
|
|
214
|
+
if let (Some(method), Some(id)) = (msg.get("method").and_then(|m| m.as_str()), msg.get("id")) {
|
|
215
|
+
info!("Handling request: {} (id: {})", method, id);
|
|
216
|
+
|
|
217
|
+
let params = msg.get("params").cloned().unwrap_or(json!({}));
|
|
218
|
+
|
|
219
|
+
let response_result = if method == AGENT_METHOD_NAMES.initialize {
|
|
220
|
+
handle_initialize(¶ms, agent).await
|
|
221
|
+
} else if method == AGENT_METHOD_NAMES.session_new {
|
|
222
|
+
handle_session_new(¶ms, agent).await
|
|
223
|
+
} else if method == AGENT_METHOD_NAMES.session_prompt {
|
|
224
|
+
handle_session_prompt(¶ms, agent, writer).await
|
|
225
|
+
} else {
|
|
226
|
+
warn!("Unknown method: {}", method);
|
|
227
|
+
Err(anyhow!("Method not found: {}", method))
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
let response = match response_result {
|
|
231
|
+
Ok(result) => json!({
|
|
232
|
+
"jsonrpc": "2.0",
|
|
233
|
+
"id": id,
|
|
234
|
+
"result": result
|
|
235
|
+
}),
|
|
236
|
+
Err(e) => json!({
|
|
237
|
+
"jsonrpc": "2.0",
|
|
238
|
+
"id": id,
|
|
239
|
+
"error": {
|
|
240
|
+
"code": -32601,
|
|
241
|
+
"message": e.to_string()
|
|
242
|
+
}
|
|
243
|
+
}),
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Send response
|
|
247
|
+
let response_str = serde_json::to_string(&response)?;
|
|
248
|
+
writer.write_all(response_str.as_bytes()).await?;
|
|
249
|
+
writer.write_all(b"\n").await?;
|
|
250
|
+
writer.flush().await?;
|
|
251
|
+
} else if let Some(method) = msg.get("method").and_then(|m| m.as_str()) {
|
|
252
|
+
// Notification
|
|
253
|
+
info!("Received notification: {}", method);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
Ok(())
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async fn handle_initialize(params: &Value, _agent: &GrokAcpAgent) -> Result<Value> {
|
|
260
|
+
info!("Received initialize request with params: {}", params);
|
|
261
|
+
|
|
262
|
+
// Parse the initialize request with better error handling
|
|
263
|
+
let req: InitializeRequest = match serde_json::from_value::<InitializeRequest>(params.clone()) {
|
|
264
|
+
Ok(req) => {
|
|
265
|
+
info!(
|
|
266
|
+
"Successfully parsed initialize request: protocol_version={}",
|
|
267
|
+
req.protocol_version
|
|
268
|
+
);
|
|
269
|
+
req
|
|
270
|
+
}
|
|
271
|
+
Err(e) => {
|
|
272
|
+
error!("Failed to parse initialize parameters: {}", e);
|
|
273
|
+
error!(
|
|
274
|
+
"Raw params received: {}",
|
|
275
|
+
serde_json::to_string_pretty(params).unwrap_or_default()
|
|
276
|
+
);
|
|
277
|
+
return Err(anyhow!(
|
|
278
|
+
"Invalid initialize parameters: {}. Received: {}",
|
|
279
|
+
e,
|
|
280
|
+
params
|
|
281
|
+
));
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
info!("Client info: {}", req.client_info);
|
|
286
|
+
info!("Client capabilities: {}", req.capabilities);
|
|
287
|
+
|
|
288
|
+
let mut caps = AgentCapabilities::new();
|
|
289
|
+
// Enable session capabilities
|
|
290
|
+
caps.session_capabilities = crate::acp::protocol::SessionCapabilities::new();
|
|
291
|
+
// Configure other capabilities as needed
|
|
292
|
+
|
|
293
|
+
// Echo back the client's protocol version
|
|
294
|
+
let response = InitializeResponse::new(&req.protocol_version)
|
|
295
|
+
.agent_capabilities(caps)
|
|
296
|
+
.agent_info(Implementation::new("grok-cli", env!("CARGO_PKG_VERSION")));
|
|
297
|
+
|
|
298
|
+
info!(
|
|
299
|
+
"Sending initialize response: protocol_version={}",
|
|
300
|
+
req.protocol_version
|
|
301
|
+
);
|
|
302
|
+
Ok(serde_json::to_value(response)?)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async fn handle_session_new(params: &Value, agent: &GrokAcpAgent) -> Result<Value> {
|
|
306
|
+
let req: NewSessionRequest = serde_json::from_value(params.clone())
|
|
307
|
+
.map_err(|e| anyhow!("Invalid session/new parameters: {}", e))?;
|
|
308
|
+
|
|
309
|
+
// Extract workspace context from request or environment
|
|
310
|
+
let workspace_root = req
|
|
311
|
+
.workspace_root
|
|
312
|
+
.or(req.working_directory)
|
|
313
|
+
.or_else(|| std::env::var("CODER_AGENT_WORKSPACE_PATH").ok())
|
|
314
|
+
.or_else(|| std::env::var("WORKSPACE_ROOT").ok());
|
|
315
|
+
|
|
316
|
+
// If workspace root is provided, update trusted directories
|
|
317
|
+
if let Some(workspace_path) = &workspace_root {
|
|
318
|
+
use std::path::PathBuf;
|
|
319
|
+
let path = PathBuf::from(workspace_path);
|
|
320
|
+
if let Ok(canonical_path) = path.canonicalize() {
|
|
321
|
+
info!(
|
|
322
|
+
"Adding workspace root to trusted directories: {:?}",
|
|
323
|
+
canonical_path
|
|
324
|
+
);
|
|
325
|
+
agent.security.add_trusted_directory(canonical_path);
|
|
326
|
+
} else {
|
|
327
|
+
warn!("Failed to canonicalize workspace path: {}", workspace_path);
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
info!("No workspace root provided, using current directory from agent initialization");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Generate a session ID
|
|
334
|
+
let session_id_str = uuid::Uuid::new_v4().to_string();
|
|
335
|
+
let session_id = SessionId::new(session_id_str.clone());
|
|
336
|
+
|
|
337
|
+
// Initialize session in GrokAcpAgent
|
|
338
|
+
agent
|
|
339
|
+
.initialize_session(session_id, Some(SessionConfig::default()))
|
|
340
|
+
.await?;
|
|
341
|
+
|
|
342
|
+
// Start chat logging for this session
|
|
343
|
+
if let Err(e) = chat_logger::start_session(&session_id_str) {
|
|
344
|
+
warn!(
|
|
345
|
+
"Failed to start chat logging for session {}: {}",
|
|
346
|
+
session_id_str, e
|
|
347
|
+
);
|
|
348
|
+
} else {
|
|
349
|
+
info!("Chat logging started for session: {}", session_id_str);
|
|
350
|
+
// Log session initialization
|
|
351
|
+
if let Err(e) = chat_logger::log_system(format!("Session {} initialized", session_id_str)) {
|
|
352
|
+
warn!("Failed to log system message: {}", e);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
let response = NewSessionResponse::new(SessionId::new(session_id_str));
|
|
357
|
+
Ok(serde_json::to_value(response)?)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async fn handle_session_prompt<W>(
|
|
361
|
+
params: &Value,
|
|
362
|
+
agent: &GrokAcpAgent,
|
|
363
|
+
writer: &mut W,
|
|
364
|
+
) -> Result<Value>
|
|
365
|
+
where
|
|
366
|
+
W: tokio::io::AsyncWrite + Unpin,
|
|
367
|
+
{
|
|
368
|
+
let req: PromptRequest = serde_json::from_value(params.clone())
|
|
369
|
+
.map_err(|e| anyhow!("Invalid session/prompt parameters: {}", e))?;
|
|
370
|
+
|
|
371
|
+
let session_id = SessionId::new(req.session_id.0.clone());
|
|
372
|
+
|
|
373
|
+
// Extract text from prompt
|
|
374
|
+
let mut message_text = String::new();
|
|
375
|
+
for block in req.prompt {
|
|
376
|
+
match block {
|
|
377
|
+
ContentBlock::Text(text) => message_text.push_str(&text.text),
|
|
378
|
+
ContentBlock::ResourceLink(link) => {
|
|
379
|
+
message_text.push_str(&format!("\n[Resource: {} ({})]", link.name, link.uri));
|
|
380
|
+
}
|
|
381
|
+
ContentBlock::Resource(res) => {
|
|
382
|
+
let crate::acp::protocol::EmbeddedResourceResource::TextResourceContents(text_res) =
|
|
383
|
+
res.resource;
|
|
384
|
+
message_text.push_str(&format!(
|
|
385
|
+
"\n[Context: {}]\n{}\n",
|
|
386
|
+
text_res.uri, text_res.text
|
|
387
|
+
));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if message_text.is_empty() {
|
|
393
|
+
return Err(anyhow!("Empty prompt received"));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Log user prompt
|
|
397
|
+
if let Err(e) = chat_logger::log_user(&message_text) {
|
|
398
|
+
warn!("Failed to log user message: {}", e);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Call Grok agent
|
|
402
|
+
// Note: options mapping is simplified here. Real implementation would map model/temp params.
|
|
403
|
+
info!(
|
|
404
|
+
"Calling Grok API for session {} with message: {}",
|
|
405
|
+
session_id.0, message_text
|
|
406
|
+
);
|
|
407
|
+
let response_text = agent
|
|
408
|
+
.handle_chat_completion(&session_id, &message_text, None)
|
|
409
|
+
.await?;
|
|
410
|
+
|
|
411
|
+
info!("Received response from Grok: {} chars", response_text.len());
|
|
412
|
+
debug!("Response text: {}", response_text);
|
|
413
|
+
|
|
414
|
+
let final_text = if response_text.is_empty() {
|
|
415
|
+
warn!("Grok returned empty response text!");
|
|
416
|
+
"[No response content received from model]".to_string()
|
|
417
|
+
} else {
|
|
418
|
+
response_text
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// Log assistant response
|
|
422
|
+
if let Err(e) = chat_logger::log_assistant(&final_text) {
|
|
423
|
+
warn!("Failed to log assistant response: {}", e);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Send update notification with response text
|
|
427
|
+
info!("About to send text update notification...");
|
|
428
|
+
send_text_update(writer, &session_id.0, &final_text).await?;
|
|
429
|
+
info!("Text update notification sent");
|
|
430
|
+
|
|
431
|
+
// Return response indicating turn end
|
|
432
|
+
info!("Returning final response with stopReason: EndTurn");
|
|
433
|
+
let response = PromptResponse::new(StopReason::EndTurn);
|
|
434
|
+
Ok(serde_json::to_value(response)?)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/// Helper to send text update notification
|
|
438
|
+
async fn send_text_update<W>(writer: &mut W, session_id: &str, text: &str) -> Result<()>
|
|
439
|
+
where
|
|
440
|
+
W: tokio::io::AsyncWrite + Unpin,
|
|
441
|
+
{
|
|
442
|
+
info!(
|
|
443
|
+
"Sending text update for session {}: {} chars",
|
|
444
|
+
session_id,
|
|
445
|
+
text.len()
|
|
446
|
+
);
|
|
447
|
+
debug!("Text content: {}", text);
|
|
448
|
+
|
|
449
|
+
// Create the content block with text
|
|
450
|
+
let content = ContentBlock::Text(TextContent::new(text));
|
|
451
|
+
debug!("Created content block: {:?}", content);
|
|
452
|
+
|
|
453
|
+
// Create the update chunk
|
|
454
|
+
let update = SessionUpdate::AgentMessageChunk(ContentChunk::new(content));
|
|
455
|
+
debug!("Created update: {:?}", update);
|
|
456
|
+
|
|
457
|
+
// Create the notification params
|
|
458
|
+
let params = SessionNotification::new(SessionId::new(session_id), update);
|
|
459
|
+
debug!("Created notification params: {:?}", params);
|
|
460
|
+
|
|
461
|
+
// ACP uses `session/update` notification
|
|
462
|
+
let notification = json!({
|
|
463
|
+
"jsonrpc": "2.0",
|
|
464
|
+
"method": "session/update",
|
|
465
|
+
"params": params
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
let msg = serde_json::to_string(¬ification)?;
|
|
469
|
+
info!("Sending notification JSON:");
|
|
470
|
+
info!("{}", msg);
|
|
471
|
+
|
|
472
|
+
// Pretty print for debugging
|
|
473
|
+
if let Ok(pretty) = serde_json::to_string_pretty(¬ification) {
|
|
474
|
+
debug!("Pretty notification:\n{}", pretty);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
writer.write_all(msg.as_bytes()).await?;
|
|
478
|
+
writer.write_all(b"\n").await?;
|
|
479
|
+
writer.flush().await?;
|
|
480
|
+
info!("Text update notification sent successfully");
|
|
481
|
+
Ok(())
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/// Test ACP connection to a running server
|
|
485
|
+
async fn test_acp_connection(address: &str, config: &Config) -> Result<()> {
|
|
486
|
+
print_info(&format!("Testing ACP connection to {}", address));
|
|
487
|
+
|
|
488
|
+
let spinner = create_spinner("Connecting...");
|
|
489
|
+
|
|
490
|
+
let result = tokio::time::timeout(
|
|
491
|
+
std::time::Duration::from_secs(10),
|
|
492
|
+
test_acp_connection_inner(address, config),
|
|
493
|
+
)
|
|
494
|
+
.await;
|
|
495
|
+
|
|
496
|
+
spinner.finish_and_clear();
|
|
497
|
+
|
|
498
|
+
match result {
|
|
499
|
+
Ok(Ok(())) => {
|
|
500
|
+
print_success("ACP connection test successful");
|
|
501
|
+
Ok(())
|
|
502
|
+
}
|
|
503
|
+
Ok(Err(e)) => {
|
|
504
|
+
print_error(&format!("ACP connection test failed: {}", e));
|
|
505
|
+
Err(e)
|
|
506
|
+
}
|
|
507
|
+
Err(_) => {
|
|
508
|
+
let err = anyhow!("ACP connection test timed out");
|
|
509
|
+
print_error(&err.to_string());
|
|
510
|
+
Err(err)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async fn test_acp_connection_inner(address: &str, _config: &Config) -> Result<()> {
|
|
516
|
+
// Try to connect to the ACP server
|
|
517
|
+
let stream = tokio::net::TcpStream::connect(address)
|
|
518
|
+
.await
|
|
519
|
+
.map_err(|e| anyhow!("Failed to connect to ACP server at {}: {}", address, e))?;
|
|
520
|
+
|
|
521
|
+
debug!("Connected to ACP server at {}", address);
|
|
522
|
+
|
|
523
|
+
// Send a basic ping/test message
|
|
524
|
+
// This would be a proper ACP message in a full implementation
|
|
525
|
+
|
|
526
|
+
// Close connection cleanly
|
|
527
|
+
drop(stream);
|
|
528
|
+
|
|
529
|
+
Ok(())
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/// Show ACP capabilities and information
|
|
533
|
+
async fn show_acp_capabilities() -> Result<()> {
|
|
534
|
+
println!("{}", "🔧 Grok CLI ACP Capabilities".cyan().bold());
|
|
535
|
+
println!();
|
|
536
|
+
|
|
537
|
+
let capabilities = get_acp_capabilities();
|
|
538
|
+
|
|
539
|
+
// Protocol Information
|
|
540
|
+
println!("{}", "Protocol Information:".green().bold());
|
|
541
|
+
println!(" Version: {}", capabilities.protocol_version);
|
|
542
|
+
println!(" Implementation: {}", capabilities.implementation);
|
|
543
|
+
println!(" Features: {}", capabilities.features.join(", "));
|
|
544
|
+
println!();
|
|
545
|
+
|
|
546
|
+
// Tools
|
|
547
|
+
println!("{}", "Available Tools:".green().bold());
|
|
548
|
+
for tool in &capabilities.tools {
|
|
549
|
+
println!(" • {} - {}", tool.name.cyan(), tool.description);
|
|
550
|
+
if !tool.parameters.is_empty() {
|
|
551
|
+
println!(" Parameters: {}", tool.parameters.join(", "));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
println!();
|
|
555
|
+
|
|
556
|
+
// Models
|
|
557
|
+
println!("{}", "Supported Models:".green().bold());
|
|
558
|
+
for model in &capabilities.models {
|
|
559
|
+
println!(" • {} - {}", model.name.cyan(), model.description);
|
|
560
|
+
println!(
|
|
561
|
+
" Max tokens: {}, Context: {}",
|
|
562
|
+
model.max_tokens, model.context_length
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
println!();
|
|
566
|
+
|
|
567
|
+
// Configuration
|
|
568
|
+
println!("{}", "Configuration:".green().bold());
|
|
569
|
+
println!(" Default timeout: {}s", capabilities.default_timeout);
|
|
570
|
+
println!(" Max retries: {}", capabilities.max_retries);
|
|
571
|
+
println!(
|
|
572
|
+
" Concurrent sessions: {}",
|
|
573
|
+
capabilities.max_concurrent_sessions
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
Ok(())
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/// Get the current ACP capabilities
|
|
580
|
+
fn get_acp_capabilities() -> AcpCapabilities {
|
|
581
|
+
AcpCapabilities {
|
|
582
|
+
protocol_version: "1.0".to_string(),
|
|
583
|
+
implementation: "grok-cli".to_string(),
|
|
584
|
+
features: vec![
|
|
585
|
+
"chat_completion".to_string(),
|
|
586
|
+
"code_generation".to_string(),
|
|
587
|
+
"code_review".to_string(),
|
|
588
|
+
"code_explanation".to_string(),
|
|
589
|
+
"file_operations".to_string(),
|
|
590
|
+
],
|
|
591
|
+
tools: vec![
|
|
592
|
+
ToolInfo {
|
|
593
|
+
name: "chat_complete".to_string(),
|
|
594
|
+
description: "Generate chat completions using Grok AI".to_string(),
|
|
595
|
+
parameters: vec![
|
|
596
|
+
"message".to_string(),
|
|
597
|
+
"temperature".to_string(),
|
|
598
|
+
"max_tokens".to_string(),
|
|
599
|
+
],
|
|
600
|
+
},
|
|
601
|
+
ToolInfo {
|
|
602
|
+
name: "code_explain".to_string(),
|
|
603
|
+
description: "Explain code functionality and structure".to_string(),
|
|
604
|
+
parameters: vec!["code".to_string(), "language".to_string()],
|
|
605
|
+
},
|
|
606
|
+
ToolInfo {
|
|
607
|
+
name: "code_review".to_string(),
|
|
608
|
+
description: "Review code for issues and improvements".to_string(),
|
|
609
|
+
parameters: vec!["code".to_string(), "focus".to_string()],
|
|
610
|
+
},
|
|
611
|
+
ToolInfo {
|
|
612
|
+
name: "code_generate".to_string(),
|
|
613
|
+
description: "Generate code from natural language descriptions".to_string(),
|
|
614
|
+
parameters: vec!["description".to_string(), "language".to_string()],
|
|
615
|
+
},
|
|
616
|
+
],
|
|
617
|
+
models: vec![
|
|
618
|
+
ModelInfo {
|
|
619
|
+
name: "grok-3".to_string(),
|
|
620
|
+
description: "Grok 3 flagship model".to_string(),
|
|
621
|
+
max_tokens: 131072,
|
|
622
|
+
context_length: 131072,
|
|
623
|
+
},
|
|
624
|
+
ModelInfo {
|
|
625
|
+
name: "grok-3-mini".to_string(),
|
|
626
|
+
description: "Efficient Grok 3 mini model".to_string(),
|
|
627
|
+
max_tokens: 131072,
|
|
628
|
+
context_length: 131072,
|
|
629
|
+
},
|
|
630
|
+
ModelInfo {
|
|
631
|
+
name: "grok-2".to_string(),
|
|
632
|
+
description: "Grok 2 model".to_string(),
|
|
633
|
+
max_tokens: 131072,
|
|
634
|
+
context_length: 131072,
|
|
635
|
+
},
|
|
636
|
+
ModelInfo {
|
|
637
|
+
name: "grok-beta".to_string(),
|
|
638
|
+
description: "Grok Beta model".to_string(),
|
|
639
|
+
max_tokens: 131072,
|
|
640
|
+
context_length: 131072,
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
default_timeout: 30,
|
|
644
|
+
max_retries: 3,
|
|
645
|
+
max_concurrent_sessions: 10,
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/// ACP capabilities structure
|
|
650
|
+
#[derive(Debug)]
|
|
651
|
+
struct AcpCapabilities {
|
|
652
|
+
protocol_version: String,
|
|
653
|
+
implementation: String,
|
|
654
|
+
features: Vec<String>,
|
|
655
|
+
tools: Vec<ToolInfo>,
|
|
656
|
+
models: Vec<ModelInfo>,
|
|
657
|
+
default_timeout: u64,
|
|
658
|
+
max_retries: u32,
|
|
659
|
+
max_concurrent_sessions: u32,
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/// Tool information for ACP capabilities
|
|
663
|
+
#[derive(Debug)]
|
|
664
|
+
struct ToolInfo {
|
|
665
|
+
name: String,
|
|
666
|
+
description: String,
|
|
667
|
+
parameters: Vec<String>,
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/// Model information for ACP capabilities
|
|
671
|
+
#[derive(Debug)]
|
|
672
|
+
struct ModelInfo {
|
|
673
|
+
name: String,
|
|
674
|
+
description: String,
|
|
675
|
+
max_tokens: u32,
|
|
676
|
+
context_length: u32,
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/// Server statistics tracking
|
|
680
|
+
#[derive(Debug, Default)]
|
|
681
|
+
struct ServerStats {
|
|
682
|
+
connections: u64,
|
|
683
|
+
active_connections: u64,
|
|
684
|
+
requests_processed: u64,
|
|
685
|
+
errors: u64,
|
|
686
|
+
start_time: Option<std::time::Instant>,
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
impl ServerStats {
|
|
690
|
+
fn new() -> Self {
|
|
691
|
+
Self {
|
|
692
|
+
start_time: Some(std::time::Instant::now()),
|
|
693
|
+
..Default::default()
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
#[cfg(test)]
|
|
699
|
+
mod tests {
|
|
700
|
+
use super::*;
|
|
701
|
+
|
|
702
|
+
#[tokio::test]
|
|
703
|
+
async fn test_acp_capabilities() {
|
|
704
|
+
let capabilities = get_acp_capabilities();
|
|
705
|
+
assert_eq!(capabilities.protocol_version, "1.0");
|
|
706
|
+
assert!(!capabilities.tools.is_empty());
|
|
707
|
+
assert!(!capabilities.models.is_empty());
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
#[test]
|
|
711
|
+
fn test_server_stats() {
|
|
712
|
+
let mut stats = ServerStats::new();
|
|
713
|
+
assert_eq!(stats.connections, 0);
|
|
714
|
+
assert_eq!(stats.active_connections, 0);
|
|
715
|
+
|
|
716
|
+
stats.connections += 1;
|
|
717
|
+
stats.active_connections += 1;
|
|
718
|
+
assert_eq!(stats.connections, 1);
|
|
719
|
+
assert_eq!(stats.active_connections, 1);
|
|
720
|
+
}
|
|
721
|
+
}
|