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.
Files changed (100) hide show
  1. package/.env.example +42 -0
  2. package/.github/workflows/ci.yml +30 -0
  3. package/.github/workflows/rust.yml +22 -0
  4. package/.grok/.env.example +85 -0
  5. package/.grok/COMPLETE_FIX_SUMMARY.md +466 -0
  6. package/.grok/ENV_CONFIG_GUIDE.md +173 -0
  7. package/.grok/QUICK_REFERENCE.md +180 -0
  8. package/.grok/README.md +104 -0
  9. package/.grok/TESTING_GUIDE.md +393 -0
  10. package/CHANGELOG.md +465 -0
  11. package/CODE_REVIEW_SUMMARY.md +414 -0
  12. package/COMPLETE_FIX_SUMMARY.md +415 -0
  13. package/CONFIGURATION.md +489 -0
  14. package/CONTEXT_FILES_GUIDE.md +419 -0
  15. package/CONTRIBUTING.md +55 -0
  16. package/CURSOR_POSITION_FIX.md +206 -0
  17. package/Cargo.toml +88 -0
  18. package/ERROR_HANDLING_REPORT.md +361 -0
  19. package/FINAL_FIX_SUMMARY.md +462 -0
  20. package/FIXES.md +37 -0
  21. package/FIXES_SUMMARY.md +87 -0
  22. package/GROK_API_MIGRATION_SUMMARY.md +111 -0
  23. package/LICENSE +22 -0
  24. package/MIGRATION_TO_GROK_API.md +223 -0
  25. package/README.md +504 -0
  26. package/REVIEW_COMPLETE.md +416 -0
  27. package/REVIEW_QUICK_REFERENCE.md +173 -0
  28. package/SECURITY.md +463 -0
  29. package/SECURITY_AUDIT.md +661 -0
  30. package/SETUP.md +287 -0
  31. package/TESTING_TOOLS.md +88 -0
  32. package/TESTING_TOOL_EXECUTION.md +239 -0
  33. package/TOOL_EXECUTION_FIX.md +491 -0
  34. package/VERIFICATION_CHECKLIST.md +419 -0
  35. package/docs/API.md +74 -0
  36. package/docs/CHAT_LOGGING.md +39 -0
  37. package/docs/CURSOR_FIX_DEMO.md +306 -0
  38. package/docs/ERROR_HANDLING_GUIDE.md +547 -0
  39. package/docs/FILE_OPERATIONS.md +449 -0
  40. package/docs/INTERACTIVE.md +401 -0
  41. package/docs/PROJECT_CREATION_GUIDE.md +570 -0
  42. package/docs/QUICKSTART.md +378 -0
  43. package/docs/QUICK_REFERENCE.md +691 -0
  44. package/docs/RELEASE_NOTES_0.1.2.md +240 -0
  45. package/docs/TOOLS.md +459 -0
  46. package/docs/TOOLS_QUICK_REFERENCE.md +210 -0
  47. package/docs/ZED_INTEGRATION.md +371 -0
  48. package/docs/extensions.md +464 -0
  49. package/docs/settings.md +293 -0
  50. package/examples/extensions/logging-hook/README.md +91 -0
  51. package/examples/extensions/logging-hook/extension.json +22 -0
  52. package/package.json +30 -0
  53. package/scripts/test_acp.py +252 -0
  54. package/scripts/test_acp.sh +143 -0
  55. package/scripts/test_acp_simple.sh +72 -0
  56. package/src/acp/mod.rs +741 -0
  57. package/src/acp/protocol.rs +323 -0
  58. package/src/acp/security.rs +298 -0
  59. package/src/acp/tools.rs +697 -0
  60. package/src/bin/banner_demo.rs +216 -0
  61. package/src/bin/docgen.rs +18 -0
  62. package/src/bin/installer.rs +217 -0
  63. package/src/cli/app.rs +310 -0
  64. package/src/cli/commands/acp.rs +721 -0
  65. package/src/cli/commands/chat.rs +485 -0
  66. package/src/cli/commands/code.rs +513 -0
  67. package/src/cli/commands/config.rs +394 -0
  68. package/src/cli/commands/health.rs +442 -0
  69. package/src/cli/commands/history.rs +421 -0
  70. package/src/cli/commands/mod.rs +14 -0
  71. package/src/cli/commands/settings.rs +1384 -0
  72. package/src/cli/mod.rs +166 -0
  73. package/src/config/mod.rs +2212 -0
  74. package/src/display/ascii_art.rs +139 -0
  75. package/src/display/banner.rs +289 -0
  76. package/src/display/components/input.rs +323 -0
  77. package/src/display/components/mod.rs +2 -0
  78. package/src/display/components/settings_list.rs +306 -0
  79. package/src/display/interactive.rs +1255 -0
  80. package/src/display/mod.rs +62 -0
  81. package/src/display/terminal.rs +42 -0
  82. package/src/display/tips.rs +316 -0
  83. package/src/grok_client_ext.rs +177 -0
  84. package/src/hooks/loader.rs +407 -0
  85. package/src/hooks/mod.rs +158 -0
  86. package/src/lib.rs +174 -0
  87. package/src/main.rs +65 -0
  88. package/src/mcp/client.rs +195 -0
  89. package/src/mcp/config.rs +20 -0
  90. package/src/mcp/mod.rs +6 -0
  91. package/src/mcp/protocol.rs +67 -0
  92. package/src/utils/auth.rs +41 -0
  93. package/src/utils/chat_logger.rs +568 -0
  94. package/src/utils/context.rs +390 -0
  95. package/src/utils/mod.rs +16 -0
  96. package/src/utils/network.rs +320 -0
  97. package/src/utils/rate_limiter.rs +166 -0
  98. package/src/utils/session.rs +73 -0
  99. package/src/utils/shell_permissions.rs +389 -0
  100. package/src/utils/telemetry.rs +41 -0
package/src/lib.rs ADDED
@@ -0,0 +1,174 @@
1
+ //! Grok CLI Library
2
+ //!
3
+ //! This library provides the core functionality for the Grok CLI,
4
+ //! including API integration, configuration management, and display utilities.
5
+
6
+ use clap::Subcommand;
7
+
8
+ pub mod acp;
9
+ pub mod cli;
10
+ pub mod config;
11
+ pub mod display;
12
+ pub mod grok_client_ext;
13
+ pub mod hooks;
14
+ pub mod mcp;
15
+ pub mod utils;
16
+
17
+ // Re-export grok_api types for use throughout the crate
18
+ pub use grok_api::{
19
+ ChatResponse, Message, ToolCall, FunctionCall, Choice, Usage,
20
+ Error as GrokApiError,
21
+ };
22
+
23
+ // Re-export the extended GrokClient
24
+ pub use grok_client_ext::GrokClient;
25
+
26
+ #[derive(Subcommand, Clone, Debug)]
27
+ pub enum CodeAction {
28
+ /// Explain code functionality
29
+ Explain {
30
+ /// File path or code snippet
31
+ input: String,
32
+ /// Input is a file path (default: auto-detect)
33
+ #[arg(short, long)]
34
+ file: bool,
35
+ },
36
+ /// Review code for improvements
37
+ Review {
38
+ /// File path or code snippet
39
+ input: String,
40
+ /// Input is a file path (default: auto-detect)
41
+ #[arg(short, long)]
42
+ file: bool,
43
+ /// Focus on specific aspects (security, performance, style, etc.)
44
+ #[arg(long)]
45
+ focus: Option<String>,
46
+ },
47
+ /// Generate code from description
48
+ Generate {
49
+ /// Description of what to generate
50
+ description: Vec<String>,
51
+ /// Programming language
52
+ #[arg(short, long)]
53
+ language: Option<String>,
54
+ /// Output file path
55
+ #[arg(short, long)]
56
+ output: Option<String>,
57
+ },
58
+ /// Fix code issues
59
+ Fix {
60
+ /// File path containing code to fix
61
+ file: String,
62
+ /// Description of the issue to fix
63
+ issue: Vec<String>,
64
+ },
65
+ }
66
+
67
+ #[derive(Subcommand, Clone, Debug)]
68
+ pub enum AcpAction {
69
+ /// Start ACP server for Zed integration
70
+ Server {
71
+ /// Port to bind to (default: auto-assign)
72
+ #[arg(short, long)]
73
+ port: Option<u16>,
74
+ /// Host to bind to
75
+ #[arg(short = 'H', long, default_value = "127.0.0.1")]
76
+ host: String,
77
+ },
78
+ /// Start ACP in stdio mode (default for Zed)
79
+ Stdio {
80
+ /// Model to use (overrides default)
81
+ #[arg(long)]
82
+ model: Option<String>,
83
+ },
84
+ /// Test ACP connection
85
+ Test {
86
+ /// ACP server address
87
+ #[arg(short, long)]
88
+ address: String,
89
+ },
90
+ /// Show ACP capabilities
91
+ Capabilities,
92
+ }
93
+
94
+ #[derive(Subcommand, Clone, Debug)]
95
+ pub enum ConfigAction {
96
+ /// Show current configuration
97
+ Show,
98
+ /// Set configuration value
99
+ Set {
100
+ /// Configuration key
101
+ key: String,
102
+ /// Configuration value
103
+ value: String,
104
+ },
105
+ /// Get configuration value
106
+ Get {
107
+ /// Configuration key
108
+ key: String,
109
+ },
110
+ /// Initialize configuration with defaults
111
+ Init {
112
+ /// Force overwrite existing config
113
+ #[arg(long)]
114
+ force: bool,
115
+ },
116
+ /// Validate configuration
117
+ Validate,
118
+ }
119
+
120
+ #[derive(Subcommand, Clone, Debug)]
121
+ pub enum SettingsAction {
122
+ /// Show interactive settings browser
123
+ Show,
124
+ /// Edit settings interactively
125
+ Edit,
126
+ /// Reset settings to defaults
127
+ Reset {
128
+ /// Category to reset (optional, resets all if not specified)
129
+ #[arg(short, long)]
130
+ category: Option<String>,
131
+ },
132
+ /// Export settings to file
133
+ Export {
134
+ /// Export file path
135
+ #[arg(short, long)]
136
+ path: Option<String>,
137
+ },
138
+ /// Import settings from file
139
+ Import {
140
+ /// Import file path
141
+ #[arg(short, long)]
142
+ path: String,
143
+ },
144
+ }
145
+
146
+ #[derive(Subcommand, Clone, Debug)]
147
+ pub enum HistoryAction {
148
+ /// List all chat sessions
149
+ List,
150
+ /// View a specific chat session
151
+ View {
152
+ /// Session ID to view
153
+ session_id: String,
154
+ },
155
+ /// Search through chat sessions
156
+ Search {
157
+ /// Search query
158
+ query: String,
159
+ },
160
+ /// Clear chat history
161
+ Clear {
162
+ /// Confirm deletion
163
+ #[arg(long)]
164
+ confirm: bool,
165
+ },
166
+ }
167
+
168
+ // Re-export commonly used types and functions
169
+ pub use config::{Config, ConfigSource, RateLimitConfig};
170
+ pub use display::{
171
+ ascii_art::{get_logo_for_width, print_grok_logo},
172
+ banner::{BannerConfig, BannerType, print_banner, print_welcome_banner},
173
+ get_terminal_size,
174
+ };
package/src/main.rs ADDED
@@ -0,0 +1,65 @@
1
+ use anyhow::Result;
2
+ use std::path::PathBuf;
3
+ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
4
+
5
+ // Import directly from the library crate to avoid duplicate compilation
6
+ use grok_cli::cli;
7
+ use grok_cli::utils::chat_logger;
8
+
9
+ #[tokio::main]
10
+ async fn main() -> Result<()> {
11
+ // Load environment variables from .env file
12
+ dotenvy::dotenv().ok();
13
+
14
+ // Initialize tracing
15
+ tracing_subscriber::registry()
16
+ .with(
17
+ tracing_subscriber::EnvFilter::try_from_default_env()
18
+ .unwrap_or_else(|_| "grok_cli=info".into()),
19
+ )
20
+ .with(tracing_subscriber::fmt::layer())
21
+ .init();
22
+
23
+ // Initialize chat logger
24
+ let chat_logger_enabled = std::env::var("GROK_CHAT_LOGGING_ENABLED")
25
+ .unwrap_or_else(|_| "true".to_string())
26
+ .parse::<bool>()
27
+ .unwrap_or(true);
28
+
29
+ let chat_log_dir = std::env::var("GROK_CHAT_LOG_DIR")
30
+ .ok()
31
+ .map(PathBuf::from)
32
+ .unwrap_or_else(|| {
33
+ dirs::home_dir()
34
+ .unwrap_or_else(|| PathBuf::from("."))
35
+ .join(".grok")
36
+ .join("logs")
37
+ .join("chat_sessions")
38
+ });
39
+
40
+ let config = chat_logger::ChatLoggerConfig {
41
+ enabled: chat_logger_enabled,
42
+ log_dir: chat_log_dir,
43
+ json_format: true,
44
+ text_format: true,
45
+ max_file_size_mb: std::env::var("GROK_CHAT_LOG_MAX_SIZE_MB")
46
+ .ok()
47
+ .and_then(|s| s.parse().ok())
48
+ .unwrap_or(10),
49
+ rotation_count: std::env::var("GROK_CHAT_LOG_ROTATION_COUNT")
50
+ .ok()
51
+ .and_then(|s| s.parse().ok())
52
+ .unwrap_or(5),
53
+ include_system: std::env::var("GROK_CHAT_LOG_INCLUDE_SYSTEM")
54
+ .unwrap_or_else(|_| "true".to_string())
55
+ .parse::<bool>()
56
+ .unwrap_or(true),
57
+ };
58
+
59
+ if let Err(e) = chat_logger::init(config) {
60
+ eprintln!("Warning: Failed to initialize chat logger: {}", e);
61
+ }
62
+
63
+ // Run the application
64
+ cli::app::run().await
65
+ }
@@ -0,0 +1,195 @@
1
+ use anyhow::{anyhow, Result};
2
+ use serde_json::{json, Value};
3
+ use std::collections::HashMap;
4
+ use std::process::Stdio;
5
+ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
6
+ use tokio::process::{Child, ChildStdin, ChildStdout, Command};
7
+ use tokio::sync::Mutex;
8
+ use tracing::{debug, error, info};
9
+
10
+ use crate::mcp::config::McpServerConfig;
11
+ use crate::mcp::protocol::{ClientCapabilities, ClientInfo, Tool};
12
+
13
+ pub struct McpClient {
14
+ servers: HashMap<String, ServerConnection>,
15
+ }
16
+
17
+ struct ServerConnection {
18
+ process: Child,
19
+ stdin: Mutex<ChildStdin>,
20
+ reader: Mutex<BufReader<ChildStdout>>,
21
+ }
22
+
23
+ impl Default for McpClient {
24
+ fn default() -> Self {
25
+ Self::new()
26
+ }
27
+ }
28
+
29
+ impl McpClient {
30
+ pub fn new() -> Self {
31
+ Self {
32
+ servers: HashMap::new(),
33
+ }
34
+ }
35
+
36
+ pub async fn connect(&mut self, name: &str, config: &McpServerConfig) -> Result<()> {
37
+ match config {
38
+ McpServerConfig::Stdio { command, args, env } => {
39
+ info!(
40
+ "Connecting to MCP server '{}' via stdio: {} {:?}",
41
+ name, command, args
42
+ );
43
+
44
+ let mut cmd = Command::new(command);
45
+ cmd.args(args);
46
+ cmd.envs(env);
47
+ cmd.stdin(Stdio::piped());
48
+ cmd.stdout(Stdio::piped());
49
+ cmd.stderr(Stdio::inherit()); // Log stderr to parent's stderr
50
+
51
+ let mut child = cmd
52
+ .spawn()
53
+ .map_err(|e| anyhow!("Failed to spawn MCP server: {}", e))?;
54
+
55
+ let stdin = child
56
+ .stdin
57
+ .take()
58
+ .ok_or_else(|| anyhow!("Failed to open stdin"))?;
59
+ let stdout = child
60
+ .stdout
61
+ .take()
62
+ .ok_or_else(|| anyhow!("Failed to open stdout"))?;
63
+
64
+ let connection = ServerConnection {
65
+ process: child,
66
+ stdin: Mutex::new(stdin),
67
+ reader: Mutex::new(BufReader::new(stdout)),
68
+ };
69
+
70
+ // Initialize handshake
71
+ self.initialize_handshake(&connection).await?;
72
+
73
+ self.servers.insert(name.to_string(), connection);
74
+ info!("Connected to MCP server '{}'", name);
75
+ Ok(())
76
+ }
77
+ McpServerConfig::Sse { .. } => Err(anyhow!("SSE transport not yet implemented")),
78
+ }
79
+ }
80
+
81
+ async fn initialize_handshake(&self, connection: &ServerConnection) -> Result<()> {
82
+ let init_msg = json!({
83
+ "jsonrpc": "2.0",
84
+ "id": 1,
85
+ "method": "initialize",
86
+ "params": {
87
+ "protocolVersion": "0.1.0",
88
+ "capabilities": {},
89
+ "clientInfo": {
90
+ "name": "grok-cli",
91
+ "version": env!("CARGO_PKG_VERSION")
92
+ }
93
+ }
94
+ });
95
+
96
+ self.send_message(connection, &init_msg).await?;
97
+ let response = self.read_response(connection).await?;
98
+
99
+ // TODO: Validate response?
100
+ debug!("Initialize response: {:?}", response);
101
+
102
+ // Send initialized notification
103
+ let initialized_msg = json!({
104
+ "jsonrpc": "2.0",
105
+ "method": "notifications/initialized"
106
+ });
107
+ self.send_message(connection, &initialized_msg).await?;
108
+
109
+ Ok(())
110
+ }
111
+
112
+ async fn send_message(&self, connection: &ServerConnection, message: &Value) -> Result<()> {
113
+ let mut stdin = connection.stdin.lock().await;
114
+ let json_str = serde_json::to_string(message)?;
115
+ stdin.write_all(json_str.as_bytes()).await?;
116
+ stdin.write_all(b"\n").await?;
117
+ stdin.flush().await?;
118
+ Ok(())
119
+ }
120
+
121
+ async fn read_response(&self, connection: &ServerConnection) -> Result<Value> {
122
+ let mut reader = connection.reader.lock().await;
123
+ let mut line = String::new();
124
+ reader.read_line(&mut line).await?;
125
+
126
+ if line.is_empty() {
127
+ return Err(anyhow!("MCP server closed connection"));
128
+ }
129
+
130
+ let value: Value = serde_json::from_str(&line)?;
131
+ Ok(value)
132
+ }
133
+
134
+ pub async fn list_tools(&self, server_name: &str) -> Result<Vec<Tool>> {
135
+ let connection = self
136
+ .servers
137
+ .get(server_name)
138
+ .ok_or_else(|| anyhow!("Server not connected: {}", server_name))?;
139
+
140
+ let msg = json!({
141
+ "jsonrpc": "2.0",
142
+ "id": 2, // simple id gen needed
143
+ "method": "tools/list",
144
+ "params": {}
145
+ });
146
+
147
+ self.send_message(connection, &msg).await?;
148
+ let response = self.read_response(connection).await?;
149
+
150
+ // Parse response
151
+ if let Some(result) = response.get("result")
152
+ && let Some(tools_val) = result.get("tools") {
153
+ let tools: Vec<Tool> = serde_json::from_value(tools_val.clone())?;
154
+ return Ok(tools);
155
+ }
156
+
157
+ Ok(Vec::new())
158
+ }
159
+
160
+ pub async fn call_tool(
161
+ &self,
162
+ server_name: &str,
163
+ tool_name: &str,
164
+ args: Value,
165
+ ) -> Result<Value> {
166
+ let connection = self
167
+ .servers
168
+ .get(server_name)
169
+ .ok_or_else(|| anyhow!("Server not connected: {}", server_name))?;
170
+
171
+ let msg = json!({
172
+ "jsonrpc": "2.0",
173
+ "id": 3,
174
+ "method": "tools/call",
175
+ "params": {
176
+ "name": tool_name,
177
+ "arguments": args
178
+ }
179
+ });
180
+
181
+ self.send_message(connection, &msg).await?;
182
+ let response = self.read_response(connection).await?;
183
+
184
+ // Check for error
185
+ if let Some(error) = response.get("error") {
186
+ return Err(anyhow!("Tool call failed: {:?}", error));
187
+ }
188
+
189
+ if let Some(result) = response.get("result") {
190
+ return Ok(result.clone());
191
+ }
192
+
193
+ Err(anyhow!("Invalid response from tool call"))
194
+ }
195
+ }
@@ -0,0 +1,20 @@
1
+ use serde::{Deserialize, Serialize};
2
+ use std::collections::HashMap;
3
+
4
+ #[derive(Debug, Clone, Serialize, Deserialize, Default)]
5
+ pub struct McpConfig {
6
+ pub servers: HashMap<String, McpServerConfig>,
7
+ }
8
+
9
+ #[derive(Debug, Clone, Serialize, Deserialize)]
10
+ #[serde(tag = "type")]
11
+ pub enum McpServerConfig {
12
+ #[serde(rename = "stdio")]
13
+ Stdio {
14
+ command: String,
15
+ args: Vec<String>,
16
+ env: HashMap<String, String>,
17
+ },
18
+ #[serde(rename = "sse")]
19
+ Sse { url: String },
20
+ }
package/src/mcp/mod.rs ADDED
@@ -0,0 +1,6 @@
1
+ pub mod client;
2
+ pub mod config;
3
+ pub mod protocol;
4
+
5
+ pub use client::McpClient;
6
+ pub use config::McpConfig;
@@ -0,0 +1,67 @@
1
+ use serde::{Deserialize, Serialize};
2
+ use serde_json::Value;
3
+
4
+ #[derive(Debug, Serialize, Deserialize)]
5
+ #[serde(tag = "method", content = "params")]
6
+ pub enum JsonRpcMessage {
7
+ #[serde(rename = "initialize")]
8
+ Initialize {
9
+ protocol_version: String,
10
+ capabilities: ClientCapabilities,
11
+ client_info: ClientInfo,
12
+ },
13
+ #[serde(rename = "tools/list")]
14
+ ListTools {},
15
+ #[serde(rename = "tools/call")]
16
+ CallTool { name: String, arguments: Value },
17
+ }
18
+
19
+ #[derive(Debug, Serialize, Deserialize)]
20
+ pub struct ClientCapabilities {
21
+ pub tools: Option<ToolsCapability>,
22
+ }
23
+
24
+ #[derive(Debug, Serialize, Deserialize)]
25
+ pub struct ToolsCapability {
26
+ pub list_changed: Option<bool>,
27
+ }
28
+
29
+ #[derive(Debug, Serialize, Deserialize)]
30
+ pub struct ClientInfo {
31
+ pub name: String,
32
+ pub version: String,
33
+ }
34
+
35
+ #[derive(Debug, Serialize, Deserialize)]
36
+ pub struct Tool {
37
+ pub name: String,
38
+ pub description: Option<String>,
39
+ pub input_schema: Value,
40
+ }
41
+
42
+ #[derive(Debug, Serialize, Deserialize)]
43
+ pub struct ListToolsResult {
44
+ pub tools: Vec<Tool>,
45
+ }
46
+
47
+ #[derive(Debug, Serialize, Deserialize)]
48
+ pub struct CallToolResult {
49
+ pub content: Vec<ToolContent>,
50
+ pub is_error: Option<bool>,
51
+ }
52
+
53
+ #[derive(Debug, Serialize, Deserialize)]
54
+ #[serde(tag = "type")]
55
+ pub enum ToolContent {
56
+ #[serde(rename = "text")]
57
+ Text { text: String },
58
+ #[serde(rename = "image")]
59
+ Image { data: String, mime_type: String },
60
+ #[serde(rename = "resource")]
61
+ Resource {
62
+ uri: String,
63
+ mime_type: Option<String>,
64
+ text: Option<String>,
65
+ blob: Option<String>,
66
+ },
67
+ }
@@ -0,0 +1,41 @@
1
+ use anyhow::{anyhow, Result};
2
+ use colored::*;
3
+ use std::process;
4
+ use tracing::error;
5
+
6
+ use crate::config::Config;
7
+
8
+ /// Resolves the API key from various sources (CLI arg, config, env vars).
9
+ pub fn resolve_api_key(cli_key: Option<String>, config: &Config) -> Option<String> {
10
+ cli_key
11
+ .or(config.api_key.clone())
12
+ .or_else(|| std::env::var("GROK_API_KEY").ok())
13
+ .or_else(|| std::env::var("X_API_KEY").ok())
14
+ }
15
+
16
+ /// Ensures an API key is present, or exits with a helpful error message.
17
+ ///
18
+ /// Returns the API key if present.
19
+ pub fn require_api_key(
20
+ api_key: Option<String>,
21
+ hide_banner: bool,
22
+ show_banner_fn: impl FnOnce(),
23
+ ) -> String {
24
+ if let Some(key) = api_key {
25
+ return key;
26
+ }
27
+
28
+ // Show welcome banner even without API key if requested
29
+ if !hide_banner {
30
+ show_banner_fn();
31
+ }
32
+
33
+ error!("No API key provided. Set GROK_API_KEY environment variable or use --api-key option");
34
+ eprintln!("{}", "Error: No API key provided".red());
35
+ eprintln!(
36
+ "Set the {} environment variable or use the {} option",
37
+ "GROK_API_KEY".yellow(),
38
+ "--api-key".yellow()
39
+ );
40
+ process::exit(1);
41
+ }