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,2212 @@
|
|
|
1
|
+
//! Configuration management for grok-cli
|
|
2
|
+
//!
|
|
3
|
+
//! This module handles loading, saving, and validating configuration settings
|
|
4
|
+
//! for the Grok CLI application, with support for environment variables,
|
|
5
|
+
//! configuration files, and default values.
|
|
6
|
+
|
|
7
|
+
use anyhow::{Result, anyhow};
|
|
8
|
+
use dirs::config_dir;
|
|
9
|
+
use serde::{Deserialize, Serialize};
|
|
10
|
+
use std::fs;
|
|
11
|
+
use std::path::PathBuf;
|
|
12
|
+
use tracing::{debug, info, warn};
|
|
13
|
+
|
|
14
|
+
use crate::mcp::config::McpConfig;
|
|
15
|
+
|
|
16
|
+
/// Main configuration structure for grok-cli
|
|
17
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
18
|
+
pub struct Config {
|
|
19
|
+
/// Source of the configuration (for display purposes)
|
|
20
|
+
#[serde(skip)]
|
|
21
|
+
pub config_source: Option<ConfigSource>,
|
|
22
|
+
|
|
23
|
+
/// X API key for Grok access
|
|
24
|
+
pub api_key: Option<String>,
|
|
25
|
+
|
|
26
|
+
/// Default model to use
|
|
27
|
+
#[serde(default = "default_model")]
|
|
28
|
+
pub default_model: String,
|
|
29
|
+
|
|
30
|
+
/// Default temperature for completions
|
|
31
|
+
#[serde(default = "default_temperature")]
|
|
32
|
+
pub default_temperature: f32,
|
|
33
|
+
|
|
34
|
+
/// Default max tokens for completions
|
|
35
|
+
#[serde(default = "default_max_tokens")]
|
|
36
|
+
pub default_max_tokens: u32,
|
|
37
|
+
|
|
38
|
+
/// Request timeout in seconds
|
|
39
|
+
#[serde(default = "default_timeout_secs")]
|
|
40
|
+
pub timeout_secs: u64,
|
|
41
|
+
|
|
42
|
+
/// Maximum number of retries for failed requests
|
|
43
|
+
#[serde(default = "default_max_retries")]
|
|
44
|
+
pub max_retries: u32,
|
|
45
|
+
|
|
46
|
+
/// General settings
|
|
47
|
+
#[serde(default)]
|
|
48
|
+
pub general: GeneralConfig,
|
|
49
|
+
|
|
50
|
+
/// Output format settings
|
|
51
|
+
#[serde(default)]
|
|
52
|
+
pub output: OutputConfig,
|
|
53
|
+
|
|
54
|
+
/// UI and display preferences
|
|
55
|
+
#[serde(default)]
|
|
56
|
+
pub ui: UiConfig,
|
|
57
|
+
|
|
58
|
+
/// Model configuration
|
|
59
|
+
#[serde(default)]
|
|
60
|
+
pub model: ModelConfig,
|
|
61
|
+
|
|
62
|
+
/// Context and file handling settings
|
|
63
|
+
#[serde(default)]
|
|
64
|
+
pub context: ContextConfig,
|
|
65
|
+
|
|
66
|
+
/// Tools configuration
|
|
67
|
+
#[serde(default)]
|
|
68
|
+
pub tools: ToolsConfig,
|
|
69
|
+
|
|
70
|
+
/// Security settings
|
|
71
|
+
#[serde(default)]
|
|
72
|
+
pub security: SecurityConfig,
|
|
73
|
+
|
|
74
|
+
/// Experimental features
|
|
75
|
+
#[serde(default)]
|
|
76
|
+
pub experimental: ExperimentalConfig,
|
|
77
|
+
|
|
78
|
+
/// ACP (Agent Client Protocol) configuration
|
|
79
|
+
#[serde(default)]
|
|
80
|
+
pub acp: AcpConfig,
|
|
81
|
+
|
|
82
|
+
/// MCP (Model Context Protocol) configuration
|
|
83
|
+
#[serde(default)]
|
|
84
|
+
pub mcp: McpConfig,
|
|
85
|
+
|
|
86
|
+
/// Network configuration for Starlink optimization
|
|
87
|
+
#[serde(default)]
|
|
88
|
+
pub network: NetworkConfig,
|
|
89
|
+
|
|
90
|
+
/// Logging configuration
|
|
91
|
+
#[serde(default)]
|
|
92
|
+
pub logging: LoggingConfig,
|
|
93
|
+
|
|
94
|
+
/// Telemetry configuration
|
|
95
|
+
#[serde(default)]
|
|
96
|
+
pub telemetry: TelemetryConfig,
|
|
97
|
+
|
|
98
|
+
/// Rate limiting configuration
|
|
99
|
+
#[serde(default)]
|
|
100
|
+
pub rate_limits: RateLimitConfig,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Rate limiting configuration
|
|
104
|
+
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
105
|
+
pub struct RateLimitConfig {
|
|
106
|
+
pub max_tokens_per_minute: u32,
|
|
107
|
+
pub max_requests_per_minute: u32,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
impl Default for RateLimitConfig {
|
|
111
|
+
fn default() -> Self {
|
|
112
|
+
Self {
|
|
113
|
+
max_tokens_per_minute: 100000,
|
|
114
|
+
max_requests_per_minute: 60,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Telemetry configuration
|
|
120
|
+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
121
|
+
pub struct TelemetryConfig {
|
|
122
|
+
/// Enable telemetry
|
|
123
|
+
pub enabled: bool,
|
|
124
|
+
|
|
125
|
+
/// Path to telemetry log file
|
|
126
|
+
pub log_file: Option<PathBuf>,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// ACP-specific configuration
|
|
130
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
131
|
+
pub struct AcpConfig {
|
|
132
|
+
/// Enable ACP server functionality
|
|
133
|
+
pub enabled: bool,
|
|
134
|
+
|
|
135
|
+
/// Default port for ACP server
|
|
136
|
+
pub default_port: Option<u16>,
|
|
137
|
+
|
|
138
|
+
/// Host to bind ACP server to
|
|
139
|
+
pub bind_host: String,
|
|
140
|
+
|
|
141
|
+
/// ACP protocol version to use
|
|
142
|
+
pub protocol_version: String,
|
|
143
|
+
|
|
144
|
+
/// Enable development features
|
|
145
|
+
pub dev_mode: bool,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// Network configuration optimized for satellite connections
|
|
149
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
150
|
+
pub struct NetworkConfig {
|
|
151
|
+
/// Enable Starlink-specific optimizations
|
|
152
|
+
pub starlink_optimizations: bool,
|
|
153
|
+
|
|
154
|
+
/// Base retry delay in seconds
|
|
155
|
+
pub base_retry_delay: u64,
|
|
156
|
+
|
|
157
|
+
/// Maximum retry delay in seconds
|
|
158
|
+
pub max_retry_delay: u64,
|
|
159
|
+
|
|
160
|
+
/// Enable network health monitoring
|
|
161
|
+
pub health_monitoring: bool,
|
|
162
|
+
|
|
163
|
+
/// Connection timeout in seconds
|
|
164
|
+
pub connect_timeout: u64,
|
|
165
|
+
|
|
166
|
+
/// Read timeout in seconds
|
|
167
|
+
pub read_timeout: u64,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// UI and display configuration
|
|
171
|
+
/// UI configuration
|
|
172
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
173
|
+
pub struct UiConfig {
|
|
174
|
+
/// Enable colored output
|
|
175
|
+
#[serde(default = "default_true")]
|
|
176
|
+
pub colors: bool,
|
|
177
|
+
|
|
178
|
+
/// Enable progress indicators
|
|
179
|
+
#[serde(default = "default_true")]
|
|
180
|
+
pub progress_bars: bool,
|
|
181
|
+
|
|
182
|
+
/// Show detailed error information
|
|
183
|
+
#[serde(default)]
|
|
184
|
+
pub verbose_errors: bool,
|
|
185
|
+
|
|
186
|
+
/// Terminal width override (0 = auto-detect)
|
|
187
|
+
#[serde(default)]
|
|
188
|
+
pub terminal_width: usize,
|
|
189
|
+
|
|
190
|
+
/// Enable Unicode characters
|
|
191
|
+
#[serde(default = "default_true")]
|
|
192
|
+
pub unicode: bool,
|
|
193
|
+
|
|
194
|
+
/// Color theme for the UI
|
|
195
|
+
#[serde(default = "default_theme")]
|
|
196
|
+
pub theme: String,
|
|
197
|
+
|
|
198
|
+
/// Custom theme definitions
|
|
199
|
+
#[serde(default)]
|
|
200
|
+
pub custom_themes: std::collections::HashMap<String, CustomTheme>,
|
|
201
|
+
|
|
202
|
+
/// Hide window title bar
|
|
203
|
+
#[serde(default)]
|
|
204
|
+
pub hide_window_title: bool,
|
|
205
|
+
|
|
206
|
+
/// Show status information in terminal title
|
|
207
|
+
#[serde(default)]
|
|
208
|
+
pub show_status_in_title: bool,
|
|
209
|
+
|
|
210
|
+
/// Hide helpful tips in the UI
|
|
211
|
+
#[serde(default)]
|
|
212
|
+
pub hide_tips: bool,
|
|
213
|
+
|
|
214
|
+
/// Hide startup banner (ASCII art logo)
|
|
215
|
+
#[serde(default)]
|
|
216
|
+
pub hide_banner: bool,
|
|
217
|
+
|
|
218
|
+
/// Hide context summary above input
|
|
219
|
+
#[serde(default)]
|
|
220
|
+
pub hide_context_summary: bool,
|
|
221
|
+
|
|
222
|
+
/// Footer configuration
|
|
223
|
+
#[serde(default)]
|
|
224
|
+
pub footer: FooterConfig,
|
|
225
|
+
|
|
226
|
+
/// Hide the footer from the UI
|
|
227
|
+
#[serde(default)]
|
|
228
|
+
pub hide_footer: bool,
|
|
229
|
+
|
|
230
|
+
/// Display memory usage information in the UI
|
|
231
|
+
#[serde(default)]
|
|
232
|
+
pub show_memory_usage: bool,
|
|
233
|
+
|
|
234
|
+
/// Show line numbers in the chat
|
|
235
|
+
#[serde(default = "default_true")]
|
|
236
|
+
pub show_line_numbers: bool,
|
|
237
|
+
|
|
238
|
+
/// Show citations for generated text in the chat
|
|
239
|
+
#[serde(default)]
|
|
240
|
+
pub show_citations: bool,
|
|
241
|
+
|
|
242
|
+
/// Show the model name in the chat for each model turn
|
|
243
|
+
#[serde(default)]
|
|
244
|
+
pub show_model_info_in_chat: bool,
|
|
245
|
+
|
|
246
|
+
/// Use the entire width of the terminal for output
|
|
247
|
+
#[serde(default = "default_true")]
|
|
248
|
+
pub use_full_width: bool,
|
|
249
|
+
|
|
250
|
+
/// Use an alternate screen buffer for the UI, preserving shell history
|
|
251
|
+
#[serde(default)]
|
|
252
|
+
pub use_alternate_buffer: bool,
|
|
253
|
+
|
|
254
|
+
/// Enable incremental rendering for the UI
|
|
255
|
+
#[serde(default)]
|
|
256
|
+
pub incremental_rendering: bool,
|
|
257
|
+
|
|
258
|
+
/// Custom witty phrases to display during loading
|
|
259
|
+
#[serde(default)]
|
|
260
|
+
pub custom_witty_phrases: Vec<String>,
|
|
261
|
+
|
|
262
|
+
/// Accessibility settings
|
|
263
|
+
#[serde(default)]
|
|
264
|
+
pub accessibility: AccessibilityConfig,
|
|
265
|
+
|
|
266
|
+
/// Interactive mode configuration
|
|
267
|
+
#[serde(default)]
|
|
268
|
+
pub interactive: InteractiveUIConfig,
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// Footer display configuration
|
|
272
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
273
|
+
pub struct FooterConfig {
|
|
274
|
+
/// Hide current working directory in footer
|
|
275
|
+
#[serde(default)]
|
|
276
|
+
pub hide_cwd: bool,
|
|
277
|
+
|
|
278
|
+
/// Hide sandbox status indicator in footer
|
|
279
|
+
#[serde(default)]
|
|
280
|
+
pub hide_sandbox_status: bool,
|
|
281
|
+
|
|
282
|
+
/// Hide model information in footer
|
|
283
|
+
#[serde(default)]
|
|
284
|
+
pub hide_model_info: bool,
|
|
285
|
+
|
|
286
|
+
/// Hide context window percentage in footer
|
|
287
|
+
#[serde(default = "default_true")]
|
|
288
|
+
pub hide_context_percentage: bool,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
292
|
+
pub struct CustomTheme {
|
|
293
|
+
#[serde(default)]
|
|
294
|
+
pub name: String,
|
|
295
|
+
#[serde(default)]
|
|
296
|
+
pub background: ThemeColors,
|
|
297
|
+
#[serde(default)]
|
|
298
|
+
pub foreground: ThemeColors,
|
|
299
|
+
#[serde(default)]
|
|
300
|
+
pub accent: ThemeColors,
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
304
|
+
pub struct ThemeColors {
|
|
305
|
+
#[serde(default)]
|
|
306
|
+
pub primary: String,
|
|
307
|
+
#[serde(default)]
|
|
308
|
+
pub secondary: String,
|
|
309
|
+
#[serde(default)]
|
|
310
|
+
pub success: String,
|
|
311
|
+
#[serde(default)]
|
|
312
|
+
pub warning: String,
|
|
313
|
+
#[serde(default)]
|
|
314
|
+
pub error: String,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
318
|
+
pub struct AccessibilityConfig {
|
|
319
|
+
#[serde(default)]
|
|
320
|
+
pub disable_loading_phrases: bool,
|
|
321
|
+
#[serde(default)]
|
|
322
|
+
pub screen_reader: bool,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/// Interactive mode UI configuration
|
|
326
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
327
|
+
pub struct InteractiveUIConfig {
|
|
328
|
+
/// Prompt style (simple, rich, minimal)
|
|
329
|
+
#[serde(default = "default_prompt_style")]
|
|
330
|
+
pub prompt_style: String,
|
|
331
|
+
|
|
332
|
+
/// Enable context usage display
|
|
333
|
+
#[serde(default = "default_true")]
|
|
334
|
+
pub show_context_usage: bool,
|
|
335
|
+
|
|
336
|
+
/// Auto-save sessions
|
|
337
|
+
#[serde(default)]
|
|
338
|
+
pub auto_save_sessions: bool,
|
|
339
|
+
|
|
340
|
+
/// Check for home directory usage
|
|
341
|
+
#[serde(default = "default_true")]
|
|
342
|
+
pub check_directory: bool,
|
|
343
|
+
|
|
344
|
+
/// Enable startup animation
|
|
345
|
+
#[serde(default = "default_true")]
|
|
346
|
+
pub startup_animation: bool,
|
|
347
|
+
|
|
348
|
+
/// Update check frequency in hours (0 = disabled)
|
|
349
|
+
#[serde(default = "default_update_check_hours")]
|
|
350
|
+
pub update_check_hours: u64,
|
|
351
|
+
|
|
352
|
+
/// Custom key bindings
|
|
353
|
+
#[serde(default)]
|
|
354
|
+
pub key_bindings: std::collections::HashMap<String, String>,
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
fn default_prompt_style() -> String {
|
|
358
|
+
"rich".to_string()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
fn default_true() -> bool {
|
|
362
|
+
true
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
fn default_update_check_hours() -> u64 {
|
|
366
|
+
24
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
fn default_theme() -> String {
|
|
370
|
+
"default".to_string()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
fn default_model() -> String {
|
|
374
|
+
"grok-3".to_string()
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
fn default_temperature() -> f32 {
|
|
378
|
+
0.7
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
fn default_max_tokens() -> u32 {
|
|
382
|
+
4096
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
fn default_timeout_secs() -> u64 {
|
|
386
|
+
30
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
fn default_max_retries() -> u32 {
|
|
390
|
+
3
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
fn default_hide_context_percentage() -> bool {
|
|
394
|
+
true
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
398
|
+
pub struct GeneralConfig {
|
|
399
|
+
#[serde(default)]
|
|
400
|
+
pub preview_features: bool,
|
|
401
|
+
#[serde(default)]
|
|
402
|
+
pub preferred_editor: String,
|
|
403
|
+
#[serde(default)]
|
|
404
|
+
pub vim_mode: bool,
|
|
405
|
+
#[serde(default)]
|
|
406
|
+
pub disable_auto_update: bool,
|
|
407
|
+
#[serde(default)]
|
|
408
|
+
pub disable_update_nag: bool,
|
|
409
|
+
#[serde(default)]
|
|
410
|
+
pub enable_prompt_completion: bool,
|
|
411
|
+
#[serde(default)]
|
|
412
|
+
pub retry_fetch_errors: bool,
|
|
413
|
+
#[serde(default)]
|
|
414
|
+
pub debug_keystroke_logging: bool,
|
|
415
|
+
#[serde(default)]
|
|
416
|
+
pub session_retention: SessionRetentionConfig,
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
420
|
+
pub struct SessionRetentionConfig {
|
|
421
|
+
#[serde(default)]
|
|
422
|
+
pub enabled: bool,
|
|
423
|
+
#[serde(default)]
|
|
424
|
+
pub max_age: u64, // in hours
|
|
425
|
+
#[serde(default)]
|
|
426
|
+
pub max_count: u32,
|
|
427
|
+
#[serde(default)]
|
|
428
|
+
pub min_retention: u64, // in hours
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
432
|
+
pub struct OutputConfig {
|
|
433
|
+
#[serde(default = "default_output_format")]
|
|
434
|
+
pub format: String,
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
fn default_output_format() -> String {
|
|
438
|
+
"text".to_string()
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
442
|
+
pub struct ModelConfig {
|
|
443
|
+
#[serde(default)]
|
|
444
|
+
pub name: String,
|
|
445
|
+
#[serde(default)]
|
|
446
|
+
pub max_session_turns: i32,
|
|
447
|
+
#[serde(default)]
|
|
448
|
+
pub summarize_tool_output: bool,
|
|
449
|
+
#[serde(default)]
|
|
450
|
+
pub compression_threshold: f64,
|
|
451
|
+
#[serde(default = "default_true")]
|
|
452
|
+
pub skip_next_speaker_check: bool,
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
456
|
+
pub struct ContextConfig {
|
|
457
|
+
#[serde(default)]
|
|
458
|
+
pub file_name: String,
|
|
459
|
+
#[serde(default)]
|
|
460
|
+
pub import_format: String,
|
|
461
|
+
#[serde(default)]
|
|
462
|
+
pub discovery_max_dirs: u32,
|
|
463
|
+
#[serde(default)]
|
|
464
|
+
pub include_directories: Vec<String>,
|
|
465
|
+
#[serde(default)]
|
|
466
|
+
pub load_memory_from_include_directories: bool,
|
|
467
|
+
#[serde(default)]
|
|
468
|
+
pub file_filtering: FileFilteringConfig,
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
472
|
+
pub struct FileFilteringConfig {
|
|
473
|
+
#[serde(default = "default_true")]
|
|
474
|
+
pub respect_git_ignore: bool,
|
|
475
|
+
#[serde(default = "default_true")]
|
|
476
|
+
pub respect_grok_ignore: bool,
|
|
477
|
+
#[serde(default = "default_true")]
|
|
478
|
+
pub enable_recursive_file_search: bool,
|
|
479
|
+
#[serde(default)]
|
|
480
|
+
pub disable_fuzzy_search: bool,
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
484
|
+
pub struct ToolsConfig {
|
|
485
|
+
#[serde(default)]
|
|
486
|
+
pub shell: ShellConfig,
|
|
487
|
+
#[serde(default)]
|
|
488
|
+
pub auto_accept: bool,
|
|
489
|
+
#[serde(default)]
|
|
490
|
+
pub core: Vec<String>,
|
|
491
|
+
#[serde(default)]
|
|
492
|
+
pub allowed: Vec<String>,
|
|
493
|
+
#[serde(default)]
|
|
494
|
+
pub exclude: Vec<String>,
|
|
495
|
+
#[serde(default)]
|
|
496
|
+
pub discovery_command: String,
|
|
497
|
+
#[serde(default)]
|
|
498
|
+
pub call_command: String,
|
|
499
|
+
#[serde(default = "default_true")]
|
|
500
|
+
pub use_ripgrep: bool,
|
|
501
|
+
#[serde(default = "default_true")]
|
|
502
|
+
pub enable_tool_output_truncation: bool,
|
|
503
|
+
#[serde(default)]
|
|
504
|
+
pub truncate_tool_output_threshold: u32,
|
|
505
|
+
#[serde(default)]
|
|
506
|
+
pub truncate_tool_output_lines: u32,
|
|
507
|
+
#[serde(default = "default_true")]
|
|
508
|
+
pub enable_message_bus_integration: bool,
|
|
509
|
+
#[serde(default)]
|
|
510
|
+
pub enable_hooks: bool,
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
514
|
+
pub struct ShellConfig {
|
|
515
|
+
#[serde(default = "default_true")]
|
|
516
|
+
pub enable_interactive_shell: bool,
|
|
517
|
+
#[serde(default)]
|
|
518
|
+
pub pager: String,
|
|
519
|
+
#[serde(default)]
|
|
520
|
+
pub show_color: bool,
|
|
521
|
+
#[serde(default)]
|
|
522
|
+
pub inactivity_timeout: u32,
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
526
|
+
pub struct SecurityConfig {
|
|
527
|
+
#[serde(default)]
|
|
528
|
+
pub disable_yolo_mode: bool,
|
|
529
|
+
#[serde(default)]
|
|
530
|
+
pub enable_permanent_tool_approval: bool,
|
|
531
|
+
#[serde(default)]
|
|
532
|
+
pub block_git_extensions: bool,
|
|
533
|
+
#[serde(default)]
|
|
534
|
+
pub folder_trust: FolderTrustConfig,
|
|
535
|
+
#[serde(default)]
|
|
536
|
+
pub environment_variable_redaction: EnvVarRedactionConfig,
|
|
537
|
+
#[serde(default = "default_shell_approval_mode")]
|
|
538
|
+
pub shell_approval_mode: String,
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
fn default_shell_approval_mode() -> String {
|
|
542
|
+
"default".to_string()
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
546
|
+
pub struct FolderTrustConfig {
|
|
547
|
+
#[serde(default)]
|
|
548
|
+
pub enabled: bool,
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
552
|
+
pub struct EnvVarRedactionConfig {
|
|
553
|
+
#[serde(default)]
|
|
554
|
+
pub allowed: Vec<String>,
|
|
555
|
+
#[serde(default)]
|
|
556
|
+
pub blocked: Vec<String>,
|
|
557
|
+
#[serde(default)]
|
|
558
|
+
pub enabled: bool,
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
562
|
+
pub struct ExperimentalConfig {
|
|
563
|
+
#[serde(default)]
|
|
564
|
+
pub enable_agents: bool,
|
|
565
|
+
#[serde(default)]
|
|
566
|
+
pub extension_management: bool,
|
|
567
|
+
#[serde(default)]
|
|
568
|
+
pub extension_reloading: bool,
|
|
569
|
+
#[serde(default)]
|
|
570
|
+
pub jit_context: bool,
|
|
571
|
+
#[serde(default)]
|
|
572
|
+
pub codebase_investigator_settings: CodebaseInvestigatorConfig,
|
|
573
|
+
#[serde(default)]
|
|
574
|
+
pub extensions: ExtensionsConfig,
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/// Extensions configuration
|
|
578
|
+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
579
|
+
pub struct ExtensionsConfig {
|
|
580
|
+
/// Enable extensions system
|
|
581
|
+
#[serde(default)]
|
|
582
|
+
pub enabled: bool,
|
|
583
|
+
|
|
584
|
+
/// Directory to load extensions from
|
|
585
|
+
#[serde(default)]
|
|
586
|
+
pub extension_dir: Option<PathBuf>,
|
|
587
|
+
|
|
588
|
+
/// List of enabled extensions
|
|
589
|
+
#[serde(default)]
|
|
590
|
+
pub enabled_extensions: Vec<String>,
|
|
591
|
+
|
|
592
|
+
/// Allow loading extensions from config
|
|
593
|
+
#[serde(default)]
|
|
594
|
+
pub allow_config_extensions: bool,
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
598
|
+
pub struct CodebaseInvestigatorConfig {
|
|
599
|
+
#[serde(default = "default_true")]
|
|
600
|
+
pub enabled: bool,
|
|
601
|
+
#[serde(default)]
|
|
602
|
+
pub max_num_turns: u32,
|
|
603
|
+
#[serde(default)]
|
|
604
|
+
pub max_time_minutes: u32,
|
|
605
|
+
#[serde(default)]
|
|
606
|
+
pub thinking_budget: u32,
|
|
607
|
+
#[serde(default)]
|
|
608
|
+
pub model: String,
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/// Logging configuration
|
|
612
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
613
|
+
pub struct LoggingConfig {
|
|
614
|
+
/// Log level (trace, debug, info, warn, error)
|
|
615
|
+
#[serde(default = "default_log_level")]
|
|
616
|
+
pub level: String,
|
|
617
|
+
|
|
618
|
+
/// Enable file logging
|
|
619
|
+
#[serde(default)]
|
|
620
|
+
pub file_logging: bool,
|
|
621
|
+
|
|
622
|
+
/// Log file path (None = default location)
|
|
623
|
+
#[serde(default)]
|
|
624
|
+
pub log_file: Option<PathBuf>,
|
|
625
|
+
|
|
626
|
+
/// Maximum log file size in MB
|
|
627
|
+
#[serde(default = "default_max_file_size_mb")]
|
|
628
|
+
pub max_file_size_mb: u64,
|
|
629
|
+
|
|
630
|
+
/// Number of log files to rotate
|
|
631
|
+
#[serde(default = "default_rotation_count")]
|
|
632
|
+
pub rotation_count: u32,
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
fn default_log_level() -> String {
|
|
636
|
+
"info".to_string()
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
fn default_max_file_size_mb() -> u64 {
|
|
640
|
+
10
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
fn default_rotation_count() -> u32 {
|
|
644
|
+
5
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
impl Default for Config {
|
|
648
|
+
fn default() -> Self {
|
|
649
|
+
Self {
|
|
650
|
+
config_source: None,
|
|
651
|
+
api_key: None,
|
|
652
|
+
default_model: default_model(),
|
|
653
|
+
default_temperature: 0.7,
|
|
654
|
+
default_max_tokens: 4096,
|
|
655
|
+
timeout_secs: 30,
|
|
656
|
+
max_retries: 3,
|
|
657
|
+
general: GeneralConfig::default(),
|
|
658
|
+
output: OutputConfig::default(),
|
|
659
|
+
ui: UiConfig::default(),
|
|
660
|
+
model: ModelConfig::default(),
|
|
661
|
+
context: ContextConfig::default(),
|
|
662
|
+
tools: ToolsConfig::default(),
|
|
663
|
+
security: SecurityConfig::default(),
|
|
664
|
+
experimental: ExperimentalConfig::default(),
|
|
665
|
+
acp: AcpConfig::default(),
|
|
666
|
+
mcp: McpConfig::default(),
|
|
667
|
+
network: NetworkConfig::default(),
|
|
668
|
+
logging: LoggingConfig::default(),
|
|
669
|
+
telemetry: TelemetryConfig::default(),
|
|
670
|
+
rate_limits: RateLimitConfig::default(),
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
impl Default for AcpConfig {
|
|
676
|
+
fn default() -> Self {
|
|
677
|
+
Self {
|
|
678
|
+
enabled: true,
|
|
679
|
+
default_port: None, // Auto-assign
|
|
680
|
+
bind_host: "127.0.0.1".to_string(),
|
|
681
|
+
protocol_version: "1.0".to_string(),
|
|
682
|
+
dev_mode: false,
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
impl Default for NetworkConfig {
|
|
688
|
+
fn default() -> Self {
|
|
689
|
+
Self {
|
|
690
|
+
starlink_optimizations: true,
|
|
691
|
+
base_retry_delay: 1,
|
|
692
|
+
max_retry_delay: 60,
|
|
693
|
+
health_monitoring: true,
|
|
694
|
+
connect_timeout: 10,
|
|
695
|
+
read_timeout: 30,
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
impl Default for UiConfig {
|
|
701
|
+
fn default() -> Self {
|
|
702
|
+
Self {
|
|
703
|
+
colors: true,
|
|
704
|
+
progress_bars: true,
|
|
705
|
+
verbose_errors: false,
|
|
706
|
+
terminal_width: 0, // Auto-detect
|
|
707
|
+
unicode: true,
|
|
708
|
+
theme: "default".to_string(),
|
|
709
|
+
custom_themes: std::collections::HashMap::new(),
|
|
710
|
+
hide_window_title: false,
|
|
711
|
+
show_status_in_title: false,
|
|
712
|
+
hide_tips: false,
|
|
713
|
+
hide_banner: false,
|
|
714
|
+
hide_context_summary: false,
|
|
715
|
+
footer: FooterConfig::default(),
|
|
716
|
+
hide_footer: false,
|
|
717
|
+
show_memory_usage: false,
|
|
718
|
+
show_line_numbers: true,
|
|
719
|
+
show_citations: false,
|
|
720
|
+
show_model_info_in_chat: false,
|
|
721
|
+
use_full_width: true,
|
|
722
|
+
use_alternate_buffer: false,
|
|
723
|
+
incremental_rendering: false,
|
|
724
|
+
custom_witty_phrases: Vec::new(),
|
|
725
|
+
accessibility: AccessibilityConfig::default(),
|
|
726
|
+
interactive: InteractiveUIConfig::default(),
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
impl Default for FooterConfig {
|
|
732
|
+
fn default() -> Self {
|
|
733
|
+
Self {
|
|
734
|
+
hide_cwd: false,
|
|
735
|
+
hide_sandbox_status: false,
|
|
736
|
+
hide_model_info: false,
|
|
737
|
+
hide_context_percentage: true,
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
impl Default for SessionRetentionConfig {
|
|
743
|
+
fn default() -> Self {
|
|
744
|
+
Self {
|
|
745
|
+
enabled: false,
|
|
746
|
+
max_age: 168, // 7 days
|
|
747
|
+
max_count: 50,
|
|
748
|
+
min_retention: 24, // 1 day
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
impl Default for OutputConfig {
|
|
754
|
+
fn default() -> Self {
|
|
755
|
+
Self {
|
|
756
|
+
format: default_output_format(),
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
impl Default for ModelConfig {
|
|
762
|
+
fn default() -> Self {
|
|
763
|
+
Self {
|
|
764
|
+
name: String::new(),
|
|
765
|
+
max_session_turns: -1, // unlimited
|
|
766
|
+
summarize_tool_output: false,
|
|
767
|
+
compression_threshold: 0.2,
|
|
768
|
+
skip_next_speaker_check: true,
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
impl Default for ContextConfig {
|
|
774
|
+
fn default() -> Self {
|
|
775
|
+
Self {
|
|
776
|
+
file_name: String::new(),
|
|
777
|
+
import_format: String::new(),
|
|
778
|
+
discovery_max_dirs: 200,
|
|
779
|
+
include_directories: Vec::new(),
|
|
780
|
+
load_memory_from_include_directories: false,
|
|
781
|
+
file_filtering: FileFilteringConfig::default(),
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
impl Default for FileFilteringConfig {
|
|
787
|
+
fn default() -> Self {
|
|
788
|
+
Self {
|
|
789
|
+
respect_git_ignore: true,
|
|
790
|
+
respect_grok_ignore: true,
|
|
791
|
+
enable_recursive_file_search: true,
|
|
792
|
+
disable_fuzzy_search: false,
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
impl Default for ToolsConfig {
|
|
798
|
+
fn default() -> Self {
|
|
799
|
+
Self {
|
|
800
|
+
shell: ShellConfig::default(),
|
|
801
|
+
auto_accept: false,
|
|
802
|
+
core: Vec::new(),
|
|
803
|
+
allowed: Vec::new(),
|
|
804
|
+
exclude: Vec::new(),
|
|
805
|
+
discovery_command: String::new(),
|
|
806
|
+
call_command: String::new(),
|
|
807
|
+
use_ripgrep: true,
|
|
808
|
+
enable_tool_output_truncation: true,
|
|
809
|
+
truncate_tool_output_threshold: 10000,
|
|
810
|
+
truncate_tool_output_lines: 100,
|
|
811
|
+
enable_message_bus_integration: true,
|
|
812
|
+
enable_hooks: false,
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
impl Default for ShellConfig {
|
|
818
|
+
fn default() -> Self {
|
|
819
|
+
Self {
|
|
820
|
+
enable_interactive_shell: true,
|
|
821
|
+
pager: String::new(),
|
|
822
|
+
show_color: false,
|
|
823
|
+
inactivity_timeout: 0,
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
impl Default for CodebaseInvestigatorConfig {
|
|
829
|
+
fn default() -> Self {
|
|
830
|
+
Self {
|
|
831
|
+
enabled: true,
|
|
832
|
+
max_num_turns: 10,
|
|
833
|
+
max_time_minutes: 15,
|
|
834
|
+
thinking_budget: 1000,
|
|
835
|
+
model: "auto".to_string(),
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
impl Default for CustomTheme {
|
|
841
|
+
fn default() -> Self {
|
|
842
|
+
Self {
|
|
843
|
+
name: "default".to_string(),
|
|
844
|
+
background: ThemeColors::default(),
|
|
845
|
+
foreground: ThemeColors::default(),
|
|
846
|
+
accent: ThemeColors::default(),
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
impl Default for InteractiveUIConfig {
|
|
852
|
+
fn default() -> Self {
|
|
853
|
+
Self {
|
|
854
|
+
prompt_style: "rich".to_string(),
|
|
855
|
+
show_context_usage: true,
|
|
856
|
+
auto_save_sessions: false,
|
|
857
|
+
check_directory: true,
|
|
858
|
+
startup_animation: true,
|
|
859
|
+
update_check_hours: 24,
|
|
860
|
+
key_bindings: std::collections::HashMap::new(),
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
impl Default for LoggingConfig {
|
|
866
|
+
fn default() -> Self {
|
|
867
|
+
Self {
|
|
868
|
+
level: "info".to_string(),
|
|
869
|
+
file_logging: false,
|
|
870
|
+
log_file: None,
|
|
871
|
+
max_file_size_mb: 10,
|
|
872
|
+
rotation_count: 5,
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
use std::env;
|
|
878
|
+
|
|
879
|
+
/// Configuration scope
|
|
880
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
881
|
+
pub enum Scope {
|
|
882
|
+
System,
|
|
883
|
+
User,
|
|
884
|
+
Project,
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/// Configuration source tracking
|
|
888
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
889
|
+
pub enum ConfigSource {
|
|
890
|
+
/// Built-in defaults only
|
|
891
|
+
Default,
|
|
892
|
+
/// Loaded from system config (~/.grok/config.toml)
|
|
893
|
+
System(PathBuf),
|
|
894
|
+
/// Loaded from project config (.grok/config.toml)
|
|
895
|
+
Project(PathBuf),
|
|
896
|
+
/// Explicitly specified via --config flag
|
|
897
|
+
Explicit(PathBuf),
|
|
898
|
+
/// Hierarchical load (combination of sources)
|
|
899
|
+
Hierarchical {
|
|
900
|
+
project: Option<PathBuf>,
|
|
901
|
+
system: Option<PathBuf>,
|
|
902
|
+
},
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
impl ConfigSource {
|
|
906
|
+
/// Get a display string for the config source
|
|
907
|
+
pub fn display(&self) -> String {
|
|
908
|
+
match self {
|
|
909
|
+
ConfigSource::Default => "built-in defaults".to_string(),
|
|
910
|
+
ConfigSource::System(path) => format!("system config ({})", path.display()),
|
|
911
|
+
ConfigSource::Project(path) => format!("project config ({})", path.display()),
|
|
912
|
+
ConfigSource::Explicit(path) => format!("explicit config ({})", path.display()),
|
|
913
|
+
ConfigSource::Hierarchical { project, system } => {
|
|
914
|
+
let mut parts = Vec::new();
|
|
915
|
+
if let Some(p) = project {
|
|
916
|
+
parts.push(format!("project ({})", p.display()));
|
|
917
|
+
}
|
|
918
|
+
if let Some(s) = system {
|
|
919
|
+
parts.push(format!("system ({})", s.display()));
|
|
920
|
+
}
|
|
921
|
+
if parts.is_empty() {
|
|
922
|
+
"defaults".to_string()
|
|
923
|
+
} else {
|
|
924
|
+
parts.join(" + ")
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/// Get a short display string for the config source
|
|
931
|
+
pub fn display_short(&self) -> String {
|
|
932
|
+
match self {
|
|
933
|
+
ConfigSource::Default => "defaults".to_string(),
|
|
934
|
+
ConfigSource::System(_) => "system".to_string(),
|
|
935
|
+
ConfigSource::Project(_) => "project".to_string(),
|
|
936
|
+
ConfigSource::Explicit(_) => "explicit".to_string(),
|
|
937
|
+
ConfigSource::Hierarchical { project, system } => {
|
|
938
|
+
let mut parts = Vec::new();
|
|
939
|
+
if project.is_some() {
|
|
940
|
+
parts.push("project");
|
|
941
|
+
}
|
|
942
|
+
if system.is_some() {
|
|
943
|
+
parts.push("system");
|
|
944
|
+
}
|
|
945
|
+
if parts.is_empty() {
|
|
946
|
+
"defaults".to_string()
|
|
947
|
+
} else {
|
|
948
|
+
parts.join(" + ")
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
impl Config {
|
|
956
|
+
/// Load configuration from file or create default
|
|
957
|
+
pub async fn load(config_path: Option<&str>) -> Result<Self> {
|
|
958
|
+
let config_file_path = match config_path {
|
|
959
|
+
Some(path) => PathBuf::from(path),
|
|
960
|
+
None => Self::default_config_path()?,
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
debug!("Loading configuration from: {:?}", config_file_path);
|
|
964
|
+
|
|
965
|
+
if config_file_path.exists() {
|
|
966
|
+
let contents = fs::read_to_string(&config_file_path)
|
|
967
|
+
.map_err(|e| anyhow!("Failed to read config file: {}", e))?;
|
|
968
|
+
|
|
969
|
+
let mut config: Config = toml::from_str(&contents).map_err(|e| {
|
|
970
|
+
anyhow!(
|
|
971
|
+
"Failed to parse config file: {}\n\n\
|
|
972
|
+
This may be due to an outdated configuration format.\n\
|
|
973
|
+
Try running 'grok config init --force' to recreate the config file,\n\
|
|
974
|
+
or delete the existing config file at: {:?}",
|
|
975
|
+
e,
|
|
976
|
+
config_file_path
|
|
977
|
+
)
|
|
978
|
+
})?;
|
|
979
|
+
|
|
980
|
+
// Set config source
|
|
981
|
+
config.config_source = Some(if config_path.is_some() {
|
|
982
|
+
ConfigSource::Explicit(config_file_path.clone())
|
|
983
|
+
} else {
|
|
984
|
+
ConfigSource::System(config_file_path.clone())
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
// Override with environment variables
|
|
988
|
+
config.apply_env_overrides();
|
|
989
|
+
|
|
990
|
+
info!("Configuration loaded from: {:?}", config_file_path);
|
|
991
|
+
Ok(config)
|
|
992
|
+
} else {
|
|
993
|
+
warn!(
|
|
994
|
+
"Config file not found, using defaults: {:?}",
|
|
995
|
+
config_file_path
|
|
996
|
+
);
|
|
997
|
+
let mut config = Config {
|
|
998
|
+
config_source: Some(ConfigSource::Default),
|
|
999
|
+
..Config::default()
|
|
1000
|
+
};
|
|
1001
|
+
config.apply_env_overrides();
|
|
1002
|
+
Ok(config)
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/// Load configuration with hierarchical priority: project → system → defaults
|
|
1007
|
+
///
|
|
1008
|
+
/// Priority order:
|
|
1009
|
+
/// 1. Project-local: .grok/.env in current directory or parent
|
|
1010
|
+
/// 2. System-level: ~/.grok/.env (or %APPDATA%\.grok\.env on Windows)
|
|
1011
|
+
/// 3. Built-in defaults
|
|
1012
|
+
/// 4. Environment variables (highest priority, applied last)
|
|
1013
|
+
///
|
|
1014
|
+
/// Settings from higher priority sources override lower priority sources.
|
|
1015
|
+
pub async fn load_hierarchical() -> Result<Self> {
|
|
1016
|
+
debug!("Loading configuration with hierarchical priority");
|
|
1017
|
+
|
|
1018
|
+
// Start with defaults
|
|
1019
|
+
let mut config = Config::default();
|
|
1020
|
+
debug!("✓ Loaded built-in defaults");
|
|
1021
|
+
|
|
1022
|
+
let mut loaded_system: Option<PathBuf> = None;
|
|
1023
|
+
let mut loaded_project: Option<PathBuf> = None;
|
|
1024
|
+
|
|
1025
|
+
// Try system-level .env
|
|
1026
|
+
let system_env_path = Self::get_system_env_path()?;
|
|
1027
|
+
if system_env_path.exists() {
|
|
1028
|
+
debug!("Loading system .env from: {:?}", system_env_path);
|
|
1029
|
+
if let Err(e) = Self::load_env_file(&system_env_path) {
|
|
1030
|
+
warn!("Failed to load system .env: {}", e);
|
|
1031
|
+
} else {
|
|
1032
|
+
loaded_system = Some(system_env_path.clone());
|
|
1033
|
+
debug!("✓ Loaded system .env from: {:?}", system_env_path);
|
|
1034
|
+
}
|
|
1035
|
+
} else {
|
|
1036
|
+
debug!("No system .env found at: {:?}", system_env_path);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Try project-local .env
|
|
1040
|
+
match Self::find_project_env() {
|
|
1041
|
+
Ok(project_env_path) => {
|
|
1042
|
+
debug!("Loading project .env from: {:?}", project_env_path);
|
|
1043
|
+
if let Err(e) = Self::load_env_file(&project_env_path) {
|
|
1044
|
+
warn!("Failed to load project .env: {}", e);
|
|
1045
|
+
} else {
|
|
1046
|
+
loaded_project = Some(project_env_path.clone());
|
|
1047
|
+
info!(
|
|
1048
|
+
"Using project-local configuration from: {:?}",
|
|
1049
|
+
project_env_path
|
|
1050
|
+
);
|
|
1051
|
+
debug!("✓ Loaded project .env from: {:?}", project_env_path);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
Err(e) => {
|
|
1055
|
+
debug!("No project .env found in directory tree: {}", e);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Set config source based on what was loaded
|
|
1060
|
+
config.config_source = Some(if loaded_project.is_some() || loaded_system.is_some() {
|
|
1061
|
+
ConfigSource::Hierarchical {
|
|
1062
|
+
project: loaded_project,
|
|
1063
|
+
system: loaded_system,
|
|
1064
|
+
}
|
|
1065
|
+
} else {
|
|
1066
|
+
ConfigSource::Default
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
// Apply environment variable overrides (highest priority)
|
|
1070
|
+
// This reads from already-loaded env vars (system env + project .env + process env)
|
|
1071
|
+
config.apply_env_overrides();
|
|
1072
|
+
|
|
1073
|
+
Ok(config)
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/// Load configuration from a specific path without merging
|
|
1077
|
+
async fn load_config_from_path(path: &PathBuf) -> Result<Self> {
|
|
1078
|
+
let contents =
|
|
1079
|
+
fs::read_to_string(path).map_err(|e| anyhow!("Failed to read config file: {}", e))?;
|
|
1080
|
+
|
|
1081
|
+
let config: Config =
|
|
1082
|
+
toml::from_str(&contents).map_err(|e| anyhow!("Failed to parse config file: {}", e))?;
|
|
1083
|
+
|
|
1084
|
+
Ok(config)
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/// Find project-local config by walking up directory tree
|
|
1088
|
+
fn find_project_config() -> Result<PathBuf> {
|
|
1089
|
+
let mut current_dir = env::current_dir()?;
|
|
1090
|
+
|
|
1091
|
+
loop {
|
|
1092
|
+
let config_path = current_dir.join(".grok").join("config.toml");
|
|
1093
|
+
if config_path.exists() {
|
|
1094
|
+
return Ok(config_path);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// Also check for project root markers (.git, Cargo.toml, etc.)
|
|
1098
|
+
let has_project_marker = current_dir.join(".git").exists()
|
|
1099
|
+
|| current_dir.join("Cargo.toml").exists()
|
|
1100
|
+
|| current_dir.join("package.json").exists()
|
|
1101
|
+
|| current_dir.join(".grok").exists();
|
|
1102
|
+
|
|
1103
|
+
// If we found a project root but no config, stop searching
|
|
1104
|
+
if has_project_marker && !current_dir.join(".grok").join("config.toml").exists() {
|
|
1105
|
+
return Err(anyhow!("No project config found"));
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Move to parent directory
|
|
1109
|
+
if let Some(parent) = current_dir.parent() {
|
|
1110
|
+
current_dir = parent.to_path_buf();
|
|
1111
|
+
} else {
|
|
1112
|
+
// Reached filesystem root
|
|
1113
|
+
return Err(anyhow!("No project config found"));
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/// Get system-level config path (legacy TOML)
|
|
1119
|
+
fn get_system_config_path() -> Result<PathBuf> {
|
|
1120
|
+
let home_dir =
|
|
1121
|
+
dirs::home_dir().ok_or_else(|| anyhow!("Could not determine home directory"))?;
|
|
1122
|
+
Ok(home_dir.join(".grok").join("config.toml"))
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/// Get system-level .env path
|
|
1126
|
+
fn get_system_env_path() -> Result<PathBuf> {
|
|
1127
|
+
let home_dir =
|
|
1128
|
+
dirs::home_dir().ok_or_else(|| anyhow!("Could not determine home directory"))?;
|
|
1129
|
+
Ok(home_dir.join(".grok").join(".env"))
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/// Find project-local .env file by walking up directory tree
|
|
1133
|
+
fn find_project_env() -> Result<PathBuf> {
|
|
1134
|
+
let mut current_dir = env::current_dir()?;
|
|
1135
|
+
|
|
1136
|
+
loop {
|
|
1137
|
+
let env_path = current_dir.join(".grok").join(".env");
|
|
1138
|
+
if env_path.exists() {
|
|
1139
|
+
return Ok(env_path);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// Also check for project root markers (.git, Cargo.toml, etc.)
|
|
1143
|
+
let has_project_marker = current_dir.join(".git").exists()
|
|
1144
|
+
|| current_dir.join("Cargo.toml").exists()
|
|
1145
|
+
|| current_dir.join("package.json").exists()
|
|
1146
|
+
|| current_dir.join(".grok").exists();
|
|
1147
|
+
|
|
1148
|
+
// If we found a project root but no .env, stop searching
|
|
1149
|
+
if has_project_marker && !current_dir.join(".grok").join(".env").exists() {
|
|
1150
|
+
return Err(anyhow!("No project .env found"));
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Move to parent directory
|
|
1154
|
+
if let Some(parent) = current_dir.parent() {
|
|
1155
|
+
current_dir = parent.to_path_buf();
|
|
1156
|
+
} else {
|
|
1157
|
+
// Reached filesystem root
|
|
1158
|
+
return Err(anyhow!("No project .env found"));
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/// Load environment variables from a .env file
|
|
1164
|
+
fn load_env_file(path: &PathBuf) -> Result<()> {
|
|
1165
|
+
dotenvy::from_path(path)
|
|
1166
|
+
.map_err(|e| anyhow!("Failed to load .env file from {:?}: {}", path, e))?;
|
|
1167
|
+
Ok(())
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/// Merge two configs, with override taking precedence over base
|
|
1171
|
+
fn merge_configs(base: Config, override_config: Config) -> Config {
|
|
1172
|
+
// Simple override: all values from override_config replace base values
|
|
1173
|
+
// This is the correct behavior for hierarchical configs where
|
|
1174
|
+
// project config should fully override system config
|
|
1175
|
+
|
|
1176
|
+
let mut merged = base;
|
|
1177
|
+
|
|
1178
|
+
// Override API key if present
|
|
1179
|
+
if override_config.api_key.is_some() {
|
|
1180
|
+
merged.api_key = override_config.api_key;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Always override these fields (they come from config file with defaults already applied)
|
|
1184
|
+
merged.default_model = override_config.default_model;
|
|
1185
|
+
merged.default_temperature = override_config.default_temperature;
|
|
1186
|
+
merged.default_max_tokens = override_config.default_max_tokens;
|
|
1187
|
+
merged.timeout_secs = override_config.timeout_secs;
|
|
1188
|
+
|
|
1189
|
+
merged.max_retries = override_config.max_retries;
|
|
1190
|
+
|
|
1191
|
+
// Override all nested configs
|
|
1192
|
+
merged.general = override_config.general;
|
|
1193
|
+
merged.output = override_config.output;
|
|
1194
|
+
merged.ui = override_config.ui;
|
|
1195
|
+
merged.model = override_config.model;
|
|
1196
|
+
merged.context = override_config.context;
|
|
1197
|
+
merged.tools = override_config.tools;
|
|
1198
|
+
merged.security = override_config.security;
|
|
1199
|
+
merged.experimental = override_config.experimental;
|
|
1200
|
+
merged.acp = override_config.acp;
|
|
1201
|
+
merged.mcp = override_config.mcp;
|
|
1202
|
+
merged.network = override_config.network;
|
|
1203
|
+
merged.logging = override_config.logging;
|
|
1204
|
+
merged.telemetry = override_config.telemetry;
|
|
1205
|
+
|
|
1206
|
+
merged
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/// Save configuration to file
|
|
1210
|
+
pub async fn save(&self, config_path: Option<&str>) -> Result<()> {
|
|
1211
|
+
let config_file_path = match config_path {
|
|
1212
|
+
Some(path) => PathBuf::from(path),
|
|
1213
|
+
None => Self::default_config_path()?,
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
// Ensure config directory exists
|
|
1217
|
+
if let Some(parent) = config_file_path.parent() {
|
|
1218
|
+
fs::create_dir_all(parent)
|
|
1219
|
+
.map_err(|e| anyhow!("Failed to create config directory: {}", e))?;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
let contents = toml::to_string_pretty(self)
|
|
1223
|
+
.map_err(|e| anyhow!("Failed to serialize config: {}", e))?;
|
|
1224
|
+
|
|
1225
|
+
fs::write(&config_file_path, contents)
|
|
1226
|
+
.map_err(|e| anyhow!("Failed to write config file: {}", e))?;
|
|
1227
|
+
|
|
1228
|
+
info!("Configuration saved to: {:?}", config_file_path);
|
|
1229
|
+
Ok(())
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/// Save configuration to specific scope
|
|
1233
|
+
pub async fn save_to_scope(&self, scope: Scope) -> Result<()> {
|
|
1234
|
+
let path = self.get_path_for_scope(scope)?;
|
|
1235
|
+
let path_str = path
|
|
1236
|
+
.to_str()
|
|
1237
|
+
.ok_or_else(|| anyhow!("Invalid config path: contains non-UTF8 characters"))?;
|
|
1238
|
+
self.save(Some(path_str)).await
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/// Get path for a specific configuration scope
|
|
1242
|
+
pub fn get_path_for_scope(&self, scope: Scope) -> Result<PathBuf> {
|
|
1243
|
+
match scope {
|
|
1244
|
+
Scope::User => Self::default_config_path(),
|
|
1245
|
+
Scope::Project => {
|
|
1246
|
+
let current_dir = env::current_dir()?;
|
|
1247
|
+
Ok(current_dir.join(".grok").join("config.toml"))
|
|
1248
|
+
}
|
|
1249
|
+
Scope::System => {
|
|
1250
|
+
#[cfg(target_os = "windows")]
|
|
1251
|
+
{
|
|
1252
|
+
let program_data =
|
|
1253
|
+
env::var("ProgramData").unwrap_or_else(|_| "C:\\ProgramData".to_string());
|
|
1254
|
+
Ok(PathBuf::from(program_data)
|
|
1255
|
+
.join("grok-cli")
|
|
1256
|
+
.join("config.toml"))
|
|
1257
|
+
}
|
|
1258
|
+
#[cfg(not(target_os = "windows"))]
|
|
1259
|
+
{
|
|
1260
|
+
Ok(PathBuf::from("/etc/grok-cli/config.toml"))
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/// Get the default configuration file path
|
|
1267
|
+
pub fn default_config_path() -> Result<PathBuf> {
|
|
1268
|
+
let config_dir =
|
|
1269
|
+
config_dir().ok_or_else(|| anyhow!("Could not determine config directory"))?;
|
|
1270
|
+
|
|
1271
|
+
Ok(config_dir.join("grok-cli").join("config.toml"))
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/// Apply environment variable overrides
|
|
1275
|
+
fn apply_env_overrides(&mut self) {
|
|
1276
|
+
// API key from environment
|
|
1277
|
+
if let Ok(api_key) = std::env::var("GROK_API_KEY") {
|
|
1278
|
+
self.api_key = Some(api_key);
|
|
1279
|
+
} else if let Ok(api_key) = std::env::var("X_API_KEY") {
|
|
1280
|
+
self.api_key = Some(api_key);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Model configuration
|
|
1284
|
+
if let Ok(model) = std::env::var("GROK_MODEL") {
|
|
1285
|
+
self.default_model = model;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
if let Ok(temp) = std::env::var("GROK_TEMPERATURE")
|
|
1289
|
+
&& let Ok(temp_val) = temp.parse::<f32>()
|
|
1290
|
+
{
|
|
1291
|
+
self.default_temperature = temp_val;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
if let Ok(tokens) = std::env::var("GROK_MAX_TOKENS")
|
|
1295
|
+
&& let Ok(tokens_val) = tokens.parse::<u32>()
|
|
1296
|
+
{
|
|
1297
|
+
self.default_max_tokens = tokens_val;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// Network configuration
|
|
1301
|
+
if let Ok(timeout) = std::env::var("GROK_TIMEOUT")
|
|
1302
|
+
&& let Ok(timeout_val) = timeout.parse::<u64>()
|
|
1303
|
+
{
|
|
1304
|
+
self.timeout_secs = timeout_val;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
if let Ok(retries) = std::env::var("GROK_MAX_RETRIES")
|
|
1308
|
+
&& let Ok(retries_val) = retries.parse::<u32>()
|
|
1309
|
+
{
|
|
1310
|
+
self.max_retries = retries_val;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
if let Ok(val) = std::env::var("GROK_STARLINK_OPTIMIZATIONS") {
|
|
1314
|
+
self.network.starlink_optimizations = val.parse::<bool>().unwrap_or(true);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
if let Ok(delay) = std::env::var("GROK_BASE_RETRY_DELAY")
|
|
1318
|
+
&& let Ok(delay_val) = delay.parse::<u64>()
|
|
1319
|
+
{
|
|
1320
|
+
self.network.base_retry_delay = delay_val;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if let Ok(delay) = std::env::var("GROK_MAX_RETRY_DELAY")
|
|
1324
|
+
&& let Ok(delay_val) = delay.parse::<u64>()
|
|
1325
|
+
{
|
|
1326
|
+
self.network.max_retry_delay = delay_val;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
if let Ok(val) = std::env::var("GROK_HEALTH_MONITORING") {
|
|
1330
|
+
self.network.health_monitoring = val.parse::<bool>().unwrap_or(true);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
if let Ok(timeout) = std::env::var("GROK_CONNECT_TIMEOUT")
|
|
1334
|
+
&& let Ok(timeout_val) = timeout.parse::<u64>()
|
|
1335
|
+
{
|
|
1336
|
+
self.network.connect_timeout = timeout_val;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
if let Ok(timeout) = std::env::var("GROK_READ_TIMEOUT")
|
|
1340
|
+
&& let Ok(timeout_val) = timeout.parse::<u64>()
|
|
1341
|
+
{
|
|
1342
|
+
self.network.read_timeout = timeout_val;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// UI configuration
|
|
1346
|
+
if let Ok(val) = std::env::var("GROK_COLORS") {
|
|
1347
|
+
self.ui.colors = val.parse::<bool>().unwrap_or(true);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
if let Ok(val) = std::env::var("GROK_PROGRESS_BARS") {
|
|
1351
|
+
self.ui.progress_bars = val.parse::<bool>().unwrap_or(true);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if let Ok(val) = std::env::var("GROK_UNICODE") {
|
|
1355
|
+
self.ui.unicode = val.parse::<bool>().unwrap_or(true);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
if let Ok(val) = std::env::var("GROK_VERBOSE_ERRORS") {
|
|
1359
|
+
self.ui.verbose_errors = val.parse::<bool>().unwrap_or(false);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
if let Ok(width) = std::env::var("GROK_TERMINAL_WIDTH")
|
|
1363
|
+
&& let Ok(width_val) = width.parse::<usize>()
|
|
1364
|
+
{
|
|
1365
|
+
self.ui.terminal_width = width_val;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// Disable colors if NO_COLOR is set
|
|
1369
|
+
if std::env::var("NO_COLOR").is_ok() {
|
|
1370
|
+
self.ui.colors = false;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// Logging configuration
|
|
1374
|
+
if let Ok(level) = std::env::var("GROK_LOG_LEVEL") {
|
|
1375
|
+
self.logging.level = level;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
if let Ok(val) = std::env::var("GROK_FILE_LOGGING") {
|
|
1379
|
+
self.logging.file_logging = val.parse::<bool>().unwrap_or(false);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
if let Ok(path) = std::env::var("GROK_LOG_FILE") {
|
|
1383
|
+
self.logging.log_file = Some(PathBuf::from(path));
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
if let Ok(size) = std::env::var("GROK_MAX_FILE_SIZE_MB")
|
|
1387
|
+
&& let Ok(size_val) = size.parse::<u64>()
|
|
1388
|
+
{
|
|
1389
|
+
self.logging.max_file_size_mb = size_val;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
if let Ok(count) = std::env::var("GROK_ROTATION_COUNT")
|
|
1393
|
+
&& let Ok(count_val) = count.parse::<u32>()
|
|
1394
|
+
{
|
|
1395
|
+
self.logging.rotation_count = count_val;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// ACP configuration
|
|
1399
|
+
if let Ok(val) = std::env::var("GROK_ACP_ENABLED") {
|
|
1400
|
+
self.acp.enabled = val.parse::<bool>().unwrap_or(true);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if std::env::var("GROK_ACP_DISABLE").is_ok() {
|
|
1404
|
+
self.acp.enabled = false;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
if let Ok(port) = std::env::var("GROK_ACP_PORT")
|
|
1408
|
+
&& let Ok(port_val) = port.parse::<u16>()
|
|
1409
|
+
{
|
|
1410
|
+
self.acp.default_port = Some(port_val);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
if let Ok(host) = std::env::var("GROK_ACP_BIND_HOST") {
|
|
1414
|
+
self.acp.bind_host = host;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
if let Ok(version) = std::env::var("GROK_ACP_PROTOCOL_VERSION") {
|
|
1418
|
+
self.acp.protocol_version = version;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
if let Ok(val) = std::env::var("GROK_ACP_DEV_MODE") {
|
|
1422
|
+
self.acp.dev_mode = val.parse::<bool>().unwrap_or(false);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Telemetry configuration
|
|
1426
|
+
if let Ok(val) = std::env::var("GROK_TELEMETRY_ENABLED") {
|
|
1427
|
+
self.telemetry.enabled = val.parse::<bool>().unwrap_or(false);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
if let Ok(path) = std::env::var("GROK_TELEMETRY_LOG_FILE") {
|
|
1431
|
+
self.telemetry.log_file = Some(PathBuf::from(path));
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// Security configuration
|
|
1435
|
+
if let Ok(mode) = std::env::var("GROK_SHELL_APPROVAL_MODE") {
|
|
1436
|
+
self.security.shell_approval_mode = mode;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
/// Validate configuration values
|
|
1441
|
+
pub fn validate(&self) -> Result<()> {
|
|
1442
|
+
// Validate temperature range
|
|
1443
|
+
if self.default_temperature < 0.0 || self.default_temperature > 2.0 {
|
|
1444
|
+
return Err(anyhow!(
|
|
1445
|
+
"Temperature must be between 0.0 and 2.0, got {}",
|
|
1446
|
+
self.default_temperature
|
|
1447
|
+
));
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// Validate max tokens
|
|
1451
|
+
if self.default_max_tokens == 0 {
|
|
1452
|
+
return Err(anyhow!("Max tokens must be greater than 0"));
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// Validate timeout
|
|
1456
|
+
if self.timeout_secs == 0 {
|
|
1457
|
+
return Err(anyhow!("Timeout must be greater than 0"));
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// Validate retry count
|
|
1461
|
+
if self.max_retries == 0 {
|
|
1462
|
+
return Err(anyhow!("Max retries must be greater than 0"));
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// Validate log level
|
|
1466
|
+
let valid_levels = ["trace", "debug", "info", "warn", "error"];
|
|
1467
|
+
if !valid_levels.contains(&self.logging.level.as_str()) {
|
|
1468
|
+
return Err(anyhow!(
|
|
1469
|
+
"Invalid log level '{}'. Must be one of: {}",
|
|
1470
|
+
self.logging.level,
|
|
1471
|
+
valid_levels.join(", ")
|
|
1472
|
+
));
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// Validate network timeouts
|
|
1476
|
+
if self.network.connect_timeout == 0 {
|
|
1477
|
+
return Err(anyhow!("Connect timeout must be greater than 0"));
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
if self.network.read_timeout == 0 {
|
|
1481
|
+
return Err(anyhow!("Read timeout must be greater than 0"));
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// Validate ACP port range
|
|
1485
|
+
if let Some(port) = self.acp.default_port
|
|
1486
|
+
&& port < 1024
|
|
1487
|
+
{
|
|
1488
|
+
warn!(
|
|
1489
|
+
"ACP port {} is below 1024, may require elevated privileges",
|
|
1490
|
+
port
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
Ok(())
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/// Get configuration value by key path (e.g., "network.timeout")
|
|
1498
|
+
pub fn get_value(&self, key: &str) -> Result<String> {
|
|
1499
|
+
match key {
|
|
1500
|
+
// Root settings
|
|
1501
|
+
"api_key" => Ok(self.api_key.clone().unwrap_or_default()),
|
|
1502
|
+
"default_model" => Ok(self.default_model.clone()),
|
|
1503
|
+
"default_temperature" => Ok(self.default_temperature.to_string()),
|
|
1504
|
+
"default_max_tokens" => Ok(self.default_max_tokens.to_string()),
|
|
1505
|
+
"timeout_secs" => Ok(self.timeout_secs.to_string()),
|
|
1506
|
+
"max_retries" => Ok(self.max_retries.to_string()),
|
|
1507
|
+
|
|
1508
|
+
// General settings
|
|
1509
|
+
"general.preview_features" => Ok(self.general.preview_features.to_string()),
|
|
1510
|
+
"general.preferred_editor" => Ok(self.general.preferred_editor.clone()),
|
|
1511
|
+
"general.vim_mode" => Ok(self.general.vim_mode.to_string()),
|
|
1512
|
+
"general.disable_auto_update" => Ok(self.general.disable_auto_update.to_string()),
|
|
1513
|
+
"general.disable_update_nag" => Ok(self.general.disable_update_nag.to_string()),
|
|
1514
|
+
"general.enable_prompt_completion" => {
|
|
1515
|
+
Ok(self.general.enable_prompt_completion.to_string())
|
|
1516
|
+
}
|
|
1517
|
+
"general.retry_fetch_errors" => Ok(self.general.retry_fetch_errors.to_string()),
|
|
1518
|
+
"general.debug_keystroke_logging" => {
|
|
1519
|
+
Ok(self.general.debug_keystroke_logging.to_string())
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// UI settings
|
|
1523
|
+
"ui.colors" => Ok(self.ui.colors.to_string()),
|
|
1524
|
+
"ui.progress_bars" => Ok(self.ui.progress_bars.to_string()),
|
|
1525
|
+
"ui.verbose_errors" => Ok(self.ui.verbose_errors.to_string()),
|
|
1526
|
+
"ui.terminal_width" => Ok(self.ui.terminal_width.to_string()),
|
|
1527
|
+
"ui.unicode" => Ok(self.ui.unicode.to_string()),
|
|
1528
|
+
"ui.theme" => Ok(self.ui.theme.clone()),
|
|
1529
|
+
"ui.hide_window_title" => Ok(self.ui.hide_window_title.to_string()),
|
|
1530
|
+
"ui.show_status_in_title" => Ok(self.ui.show_status_in_title.to_string()),
|
|
1531
|
+
"ui.hide_tips" => Ok(self.ui.hide_tips.to_string()),
|
|
1532
|
+
"ui.hide_banner" => Ok(self.ui.hide_banner.to_string()),
|
|
1533
|
+
"ui.hide_context_summary" => Ok(self.ui.hide_context_summary.to_string()),
|
|
1534
|
+
"ui.hide_footer" => Ok(self.ui.hide_footer.to_string()),
|
|
1535
|
+
"ui.show_memory_usage" => Ok(self.ui.show_memory_usage.to_string()),
|
|
1536
|
+
"ui.show_line_numbers" => Ok(self.ui.show_line_numbers.to_string()),
|
|
1537
|
+
"ui.show_citations" => Ok(self.ui.show_citations.to_string()),
|
|
1538
|
+
"ui.show_model_info_in_chat" => Ok(self.ui.show_model_info_in_chat.to_string()),
|
|
1539
|
+
"ui.use_full_width" => Ok(self.ui.use_full_width.to_string()),
|
|
1540
|
+
"ui.use_alternate_buffer" => Ok(self.ui.use_alternate_buffer.to_string()),
|
|
1541
|
+
"ui.incremental_rendering" => Ok(self.ui.incremental_rendering.to_string()),
|
|
1542
|
+
"ui.accessibility.disable_loading_phrases" => {
|
|
1543
|
+
Ok(self.ui.accessibility.disable_loading_phrases.to_string())
|
|
1544
|
+
}
|
|
1545
|
+
"ui.accessibility.screen_reader" => Ok(self.ui.accessibility.screen_reader.to_string()),
|
|
1546
|
+
"ui.footer.hide_cwd" => Ok(self.ui.footer.hide_cwd.to_string()),
|
|
1547
|
+
"ui.footer.hide_sandbox_status" => Ok(self.ui.footer.hide_sandbox_status.to_string()),
|
|
1548
|
+
"ui.footer.hide_model_info" => Ok(self.ui.footer.hide_model_info.to_string()),
|
|
1549
|
+
"ui.footer.hide_context_percentage" => {
|
|
1550
|
+
Ok(self.ui.footer.hide_context_percentage.to_string())
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// Model settings
|
|
1554
|
+
"model.name" => Ok(self.model.name.clone()),
|
|
1555
|
+
"model.max_session_turns" => Ok(self.model.max_session_turns.to_string()),
|
|
1556
|
+
"model.summarize_tool_output" => Ok(self.model.summarize_tool_output.to_string()),
|
|
1557
|
+
"model.compression_threshold" => Ok(self.model.compression_threshold.to_string()),
|
|
1558
|
+
"model.skip_next_speaker_check" => Ok(self.model.skip_next_speaker_check.to_string()),
|
|
1559
|
+
|
|
1560
|
+
// Context settings
|
|
1561
|
+
"context.discovery_max_dirs" => Ok(self.context.discovery_max_dirs.to_string()),
|
|
1562
|
+
"context.load_memory_from_include_directories" => Ok(self
|
|
1563
|
+
.context
|
|
1564
|
+
.load_memory_from_include_directories
|
|
1565
|
+
.to_string()),
|
|
1566
|
+
"context.file_filtering.respect_git_ignore" => {
|
|
1567
|
+
Ok(self.context.file_filtering.respect_git_ignore.to_string())
|
|
1568
|
+
}
|
|
1569
|
+
"context.file_filtering.respect_grok_ignore" => {
|
|
1570
|
+
Ok(self.context.file_filtering.respect_grok_ignore.to_string())
|
|
1571
|
+
}
|
|
1572
|
+
"context.file_filtering.enable_recursive_file_search" => Ok(self
|
|
1573
|
+
.context
|
|
1574
|
+
.file_filtering
|
|
1575
|
+
.enable_recursive_file_search
|
|
1576
|
+
.to_string()),
|
|
1577
|
+
"context.file_filtering.disable_fuzzy_search" => {
|
|
1578
|
+
Ok(self.context.file_filtering.disable_fuzzy_search.to_string())
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// Tools settings
|
|
1582
|
+
"tools.shell.enable_interactive_shell" => {
|
|
1583
|
+
Ok(self.tools.shell.enable_interactive_shell.to_string())
|
|
1584
|
+
}
|
|
1585
|
+
"tools.shell.show_color" => Ok(self.tools.shell.show_color.to_string()),
|
|
1586
|
+
"tools.auto_accept" => Ok(self.tools.auto_accept.to_string()),
|
|
1587
|
+
"tools.use_ripgrep" => Ok(self.tools.use_ripgrep.to_string()),
|
|
1588
|
+
"tools.enable_tool_output_truncation" => {
|
|
1589
|
+
Ok(self.tools.enable_tool_output_truncation.to_string())
|
|
1590
|
+
}
|
|
1591
|
+
"tools.truncate_tool_output_threshold" => {
|
|
1592
|
+
Ok(self.tools.truncate_tool_output_threshold.to_string())
|
|
1593
|
+
}
|
|
1594
|
+
"tools.truncate_tool_output_lines" => {
|
|
1595
|
+
Ok(self.tools.truncate_tool_output_lines.to_string())
|
|
1596
|
+
}
|
|
1597
|
+
"tools.enable_message_bus_integration" => {
|
|
1598
|
+
Ok(self.tools.enable_message_bus_integration.to_string())
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// Security settings
|
|
1602
|
+
"security.disable_yolo_mode" => Ok(self.security.disable_yolo_mode.to_string()),
|
|
1603
|
+
"security.enable_permanent_tool_approval" => {
|
|
1604
|
+
Ok(self.security.enable_permanent_tool_approval.to_string())
|
|
1605
|
+
}
|
|
1606
|
+
"security.block_git_extensions" => Ok(self.security.block_git_extensions.to_string()),
|
|
1607
|
+
"security.folder_trust.enabled" => Ok(self.security.folder_trust.enabled.to_string()),
|
|
1608
|
+
"security.environment_variable_redaction.enabled" => Ok(self
|
|
1609
|
+
.security
|
|
1610
|
+
.environment_variable_redaction
|
|
1611
|
+
.enabled
|
|
1612
|
+
.to_string()),
|
|
1613
|
+
|
|
1614
|
+
// Experimental settings
|
|
1615
|
+
"experimental.enable_agents" => Ok(self.experimental.enable_agents.to_string()),
|
|
1616
|
+
"experimental.extension_management" => {
|
|
1617
|
+
Ok(self.experimental.extension_management.to_string())
|
|
1618
|
+
}
|
|
1619
|
+
"experimental.jit_context" => Ok(self.experimental.jit_context.to_string()),
|
|
1620
|
+
"experimental.codebase_investigator_settings.enabled" => Ok(self
|
|
1621
|
+
.experimental
|
|
1622
|
+
.codebase_investigator_settings
|
|
1623
|
+
.enabled
|
|
1624
|
+
.to_string()),
|
|
1625
|
+
"experimental.codebase_investigator_settings.max_num_turns" => Ok(self
|
|
1626
|
+
.experimental
|
|
1627
|
+
.codebase_investigator_settings
|
|
1628
|
+
.max_num_turns
|
|
1629
|
+
.to_string()),
|
|
1630
|
+
|
|
1631
|
+
// ACP settings
|
|
1632
|
+
"acp.enabled" => Ok(self.acp.enabled.to_string()),
|
|
1633
|
+
"acp.bind_host" => Ok(self.acp.bind_host.clone()),
|
|
1634
|
+
"acp.protocol_version" => Ok(self.acp.protocol_version.clone()),
|
|
1635
|
+
"acp.dev_mode" => Ok(self.acp.dev_mode.to_string()),
|
|
1636
|
+
"acp.default_port" => Ok(self
|
|
1637
|
+
.acp
|
|
1638
|
+
.default_port
|
|
1639
|
+
.map(|p| p.to_string())
|
|
1640
|
+
.unwrap_or_default()),
|
|
1641
|
+
|
|
1642
|
+
// Network settings
|
|
1643
|
+
"network.starlink_optimizations" => Ok(self.network.starlink_optimizations.to_string()),
|
|
1644
|
+
"network.base_retry_delay" => Ok(self.network.base_retry_delay.to_string()),
|
|
1645
|
+
"network.max_retry_delay" => Ok(self.network.max_retry_delay.to_string()),
|
|
1646
|
+
"network.health_monitoring" => Ok(self.network.health_monitoring.to_string()),
|
|
1647
|
+
"network.connect_timeout" => Ok(self.network.connect_timeout.to_string()),
|
|
1648
|
+
"network.read_timeout" => Ok(self.network.read_timeout.to_string()),
|
|
1649
|
+
|
|
1650
|
+
// Logging settings
|
|
1651
|
+
"logging.level" => Ok(self.logging.level.clone()),
|
|
1652
|
+
"logging.file_logging" => Ok(self.logging.file_logging.to_string()),
|
|
1653
|
+
"logging.max_file_size_mb" => Ok(self.logging.max_file_size_mb.to_string()),
|
|
1654
|
+
"logging.rotation_count" => Ok(self.logging.rotation_count.to_string()),
|
|
1655
|
+
|
|
1656
|
+
// Telemetry settings
|
|
1657
|
+
"telemetry.enabled" => Ok(self.telemetry.enabled.to_string()),
|
|
1658
|
+
|
|
1659
|
+
_ => Err(anyhow!("Unknown configuration key: {}", key)),
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/// Set configuration value by key path
|
|
1664
|
+
pub fn set_value(&mut self, key: &str, value: &str) -> Result<()> {
|
|
1665
|
+
match key {
|
|
1666
|
+
// Root settings
|
|
1667
|
+
"api_key" => {
|
|
1668
|
+
self.api_key = if value.is_empty() {
|
|
1669
|
+
None
|
|
1670
|
+
} else {
|
|
1671
|
+
Some(value.to_string())
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
"default_model" => {
|
|
1675
|
+
self.default_model = value.to_string();
|
|
1676
|
+
}
|
|
1677
|
+
"default_temperature" => {
|
|
1678
|
+
self.default_temperature = value
|
|
1679
|
+
.parse()
|
|
1680
|
+
.map_err(|_| anyhow!("Invalid temperature value: {}", value))?;
|
|
1681
|
+
}
|
|
1682
|
+
"default_max_tokens" => {
|
|
1683
|
+
self.default_max_tokens = value
|
|
1684
|
+
.parse()
|
|
1685
|
+
.map_err(|_| anyhow!("Invalid max tokens value: {}", value))?;
|
|
1686
|
+
}
|
|
1687
|
+
"timeout_secs" => {
|
|
1688
|
+
self.timeout_secs = value
|
|
1689
|
+
.parse()
|
|
1690
|
+
.map_err(|_| anyhow!("Invalid timeout value: {}", value))?;
|
|
1691
|
+
}
|
|
1692
|
+
"max_retries" => {
|
|
1693
|
+
self.max_retries = value
|
|
1694
|
+
.parse()
|
|
1695
|
+
.map_err(|_| anyhow!("Invalid max retries value: {}", value))?;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// General settings
|
|
1699
|
+
"general.preview_features" => {
|
|
1700
|
+
self.general.preview_features = value
|
|
1701
|
+
.parse()
|
|
1702
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1703
|
+
}
|
|
1704
|
+
"general.preferred_editor" => {
|
|
1705
|
+
self.general.preferred_editor = value.to_string();
|
|
1706
|
+
}
|
|
1707
|
+
"general.vim_mode" => {
|
|
1708
|
+
self.general.vim_mode = value
|
|
1709
|
+
.parse()
|
|
1710
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1711
|
+
}
|
|
1712
|
+
"general.disable_auto_update" => {
|
|
1713
|
+
self.general.disable_auto_update = value
|
|
1714
|
+
.parse()
|
|
1715
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1716
|
+
}
|
|
1717
|
+
"general.disable_update_nag" => {
|
|
1718
|
+
self.general.disable_update_nag = value
|
|
1719
|
+
.parse()
|
|
1720
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1721
|
+
}
|
|
1722
|
+
"general.enable_prompt_completion" => {
|
|
1723
|
+
self.general.enable_prompt_completion = value
|
|
1724
|
+
.parse()
|
|
1725
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1726
|
+
}
|
|
1727
|
+
"general.retry_fetch_errors" => {
|
|
1728
|
+
self.general.retry_fetch_errors = value
|
|
1729
|
+
.parse()
|
|
1730
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1731
|
+
}
|
|
1732
|
+
"general.debug_keystroke_logging" => {
|
|
1733
|
+
self.general.debug_keystroke_logging = value
|
|
1734
|
+
.parse()
|
|
1735
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// UI settings
|
|
1739
|
+
"ui.colors" => {
|
|
1740
|
+
self.ui.colors = value
|
|
1741
|
+
.parse()
|
|
1742
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1743
|
+
}
|
|
1744
|
+
"ui.progress_bars" => {
|
|
1745
|
+
self.ui.progress_bars = value
|
|
1746
|
+
.parse()
|
|
1747
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1748
|
+
}
|
|
1749
|
+
"ui.verbose_errors" => {
|
|
1750
|
+
self.ui.verbose_errors = value
|
|
1751
|
+
.parse()
|
|
1752
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1753
|
+
}
|
|
1754
|
+
"ui.terminal_width" => {
|
|
1755
|
+
self.ui.terminal_width = value
|
|
1756
|
+
.parse()
|
|
1757
|
+
.map_err(|_| anyhow!("Invalid number: {}", value))?;
|
|
1758
|
+
}
|
|
1759
|
+
"ui.unicode" => {
|
|
1760
|
+
self.ui.unicode = value
|
|
1761
|
+
.parse()
|
|
1762
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1763
|
+
}
|
|
1764
|
+
"ui.theme" => {
|
|
1765
|
+
self.ui.theme = value.to_string();
|
|
1766
|
+
}
|
|
1767
|
+
"ui.hide_window_title" => {
|
|
1768
|
+
self.ui.hide_window_title = value
|
|
1769
|
+
.parse()
|
|
1770
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1771
|
+
}
|
|
1772
|
+
"ui.show_status_in_title" => {
|
|
1773
|
+
self.ui.show_status_in_title = value
|
|
1774
|
+
.parse()
|
|
1775
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1776
|
+
}
|
|
1777
|
+
"ui.hide_tips" => {
|
|
1778
|
+
self.ui.hide_tips = value
|
|
1779
|
+
.parse()
|
|
1780
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1781
|
+
}
|
|
1782
|
+
"ui.hide_banner" => {
|
|
1783
|
+
self.ui.hide_banner = value
|
|
1784
|
+
.parse()
|
|
1785
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1786
|
+
}
|
|
1787
|
+
"ui.hide_context_summary" => {
|
|
1788
|
+
self.ui.hide_context_summary = value
|
|
1789
|
+
.parse()
|
|
1790
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1791
|
+
}
|
|
1792
|
+
"ui.hide_footer" => {
|
|
1793
|
+
self.ui.hide_footer = value
|
|
1794
|
+
.parse()
|
|
1795
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1796
|
+
}
|
|
1797
|
+
"ui.show_memory_usage" => {
|
|
1798
|
+
self.ui.show_memory_usage = value
|
|
1799
|
+
.parse()
|
|
1800
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1801
|
+
}
|
|
1802
|
+
"ui.show_line_numbers" => {
|
|
1803
|
+
self.ui.show_line_numbers = value
|
|
1804
|
+
.parse()
|
|
1805
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1806
|
+
}
|
|
1807
|
+
"ui.show_citations" => {
|
|
1808
|
+
self.ui.show_citations = value
|
|
1809
|
+
.parse()
|
|
1810
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1811
|
+
}
|
|
1812
|
+
"ui.show_model_info_in_chat" => {
|
|
1813
|
+
self.ui.show_model_info_in_chat = value
|
|
1814
|
+
.parse()
|
|
1815
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1816
|
+
}
|
|
1817
|
+
"ui.use_full_width" => {
|
|
1818
|
+
self.ui.use_full_width = value
|
|
1819
|
+
.parse()
|
|
1820
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1821
|
+
}
|
|
1822
|
+
"ui.use_alternate_buffer" => {
|
|
1823
|
+
self.ui.use_alternate_buffer = value
|
|
1824
|
+
.parse()
|
|
1825
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1826
|
+
}
|
|
1827
|
+
"ui.incremental_rendering" => {
|
|
1828
|
+
self.ui.incremental_rendering = value
|
|
1829
|
+
.parse()
|
|
1830
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1831
|
+
}
|
|
1832
|
+
"ui.accessibility.disable_loading_phrases" => {
|
|
1833
|
+
self.ui.accessibility.disable_loading_phrases = value
|
|
1834
|
+
.parse()
|
|
1835
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1836
|
+
}
|
|
1837
|
+
"ui.accessibility.screen_reader" => {
|
|
1838
|
+
self.ui.accessibility.screen_reader = value
|
|
1839
|
+
.parse()
|
|
1840
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1841
|
+
}
|
|
1842
|
+
"ui.footer.hide_cwd" => {
|
|
1843
|
+
self.ui.footer.hide_cwd = value
|
|
1844
|
+
.parse()
|
|
1845
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1846
|
+
}
|
|
1847
|
+
"ui.footer.hide_sandbox_status" => {
|
|
1848
|
+
self.ui.footer.hide_sandbox_status = value
|
|
1849
|
+
.parse()
|
|
1850
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1851
|
+
}
|
|
1852
|
+
"ui.footer.hide_model_info" => {
|
|
1853
|
+
self.ui.footer.hide_model_info = value
|
|
1854
|
+
.parse()
|
|
1855
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1856
|
+
}
|
|
1857
|
+
"ui.footer.hide_context_percentage" => {
|
|
1858
|
+
self.ui.footer.hide_context_percentage = value
|
|
1859
|
+
.parse()
|
|
1860
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
// Model settings
|
|
1864
|
+
"model.name" => {
|
|
1865
|
+
self.model.name = value.to_string();
|
|
1866
|
+
}
|
|
1867
|
+
"model.max_session_turns" => {
|
|
1868
|
+
self.model.max_session_turns = value
|
|
1869
|
+
.parse()
|
|
1870
|
+
.map_err(|_| anyhow!("Invalid number: {}", value))?;
|
|
1871
|
+
}
|
|
1872
|
+
"model.summarize_tool_output" => {
|
|
1873
|
+
self.model.summarize_tool_output = value
|
|
1874
|
+
.parse()
|
|
1875
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1876
|
+
}
|
|
1877
|
+
"model.compression_threshold" => {
|
|
1878
|
+
self.model.compression_threshold = value
|
|
1879
|
+
.parse()
|
|
1880
|
+
.map_err(|_| anyhow!("Invalid number: {}", value))?;
|
|
1881
|
+
}
|
|
1882
|
+
"model.skip_next_speaker_check" => {
|
|
1883
|
+
self.model.skip_next_speaker_check = value
|
|
1884
|
+
.parse()
|
|
1885
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// Context settings
|
|
1889
|
+
"context.discovery_max_dirs" => {
|
|
1890
|
+
self.context.discovery_max_dirs = value
|
|
1891
|
+
.parse()
|
|
1892
|
+
.map_err(|_| anyhow!("Invalid number: {}", value))?;
|
|
1893
|
+
}
|
|
1894
|
+
"context.load_memory_from_include_directories" => {
|
|
1895
|
+
self.context.load_memory_from_include_directories = value
|
|
1896
|
+
.parse()
|
|
1897
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1898
|
+
}
|
|
1899
|
+
"context.file_filtering.respect_git_ignore" => {
|
|
1900
|
+
self.context.file_filtering.respect_git_ignore = value
|
|
1901
|
+
.parse()
|
|
1902
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1903
|
+
}
|
|
1904
|
+
"context.file_filtering.respect_grok_ignore" => {
|
|
1905
|
+
self.context.file_filtering.respect_grok_ignore = value
|
|
1906
|
+
.parse()
|
|
1907
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1908
|
+
}
|
|
1909
|
+
"context.file_filtering.enable_recursive_file_search" => {
|
|
1910
|
+
self.context.file_filtering.enable_recursive_file_search = value
|
|
1911
|
+
.parse()
|
|
1912
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1913
|
+
}
|
|
1914
|
+
"context.file_filtering.disable_fuzzy_search" => {
|
|
1915
|
+
self.context.file_filtering.disable_fuzzy_search = value
|
|
1916
|
+
.parse()
|
|
1917
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// Tools settings
|
|
1921
|
+
"tools.shell.enable_interactive_shell" => {
|
|
1922
|
+
self.tools.shell.enable_interactive_shell = value
|
|
1923
|
+
.parse()
|
|
1924
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1925
|
+
}
|
|
1926
|
+
"tools.shell.show_color" => {
|
|
1927
|
+
self.tools.shell.show_color = value
|
|
1928
|
+
.parse()
|
|
1929
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1930
|
+
}
|
|
1931
|
+
"tools.auto_accept" => {
|
|
1932
|
+
self.tools.auto_accept = value
|
|
1933
|
+
.parse()
|
|
1934
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1935
|
+
}
|
|
1936
|
+
"tools.use_ripgrep" => {
|
|
1937
|
+
self.tools.use_ripgrep = value
|
|
1938
|
+
.parse()
|
|
1939
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1940
|
+
}
|
|
1941
|
+
"tools.enable_tool_output_truncation" => {
|
|
1942
|
+
self.tools.enable_tool_output_truncation = value
|
|
1943
|
+
.parse()
|
|
1944
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1945
|
+
}
|
|
1946
|
+
"tools.truncate_tool_output_threshold" => {
|
|
1947
|
+
self.tools.truncate_tool_output_threshold = value
|
|
1948
|
+
.parse()
|
|
1949
|
+
.map_err(|_| anyhow!("Invalid number: {}", value))?;
|
|
1950
|
+
}
|
|
1951
|
+
"tools.truncate_tool_output_lines" => {
|
|
1952
|
+
self.tools.truncate_tool_output_lines = value
|
|
1953
|
+
.parse()
|
|
1954
|
+
.map_err(|_| anyhow!("Invalid number: {}", value))?;
|
|
1955
|
+
}
|
|
1956
|
+
"tools.enable_message_bus_integration" => {
|
|
1957
|
+
self.tools.enable_message_bus_integration = value
|
|
1958
|
+
.parse()
|
|
1959
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// Security settings
|
|
1963
|
+
"security.disable_yolo_mode" => {
|
|
1964
|
+
self.security.disable_yolo_mode = value
|
|
1965
|
+
.parse()
|
|
1966
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1967
|
+
}
|
|
1968
|
+
"security.enable_permanent_tool_approval" => {
|
|
1969
|
+
self.security.enable_permanent_tool_approval = value
|
|
1970
|
+
.parse()
|
|
1971
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1972
|
+
}
|
|
1973
|
+
"security.block_git_extensions" => {
|
|
1974
|
+
self.security.block_git_extensions = value
|
|
1975
|
+
.parse()
|
|
1976
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1977
|
+
}
|
|
1978
|
+
"security.folder_trust.enabled" => {
|
|
1979
|
+
self.security.folder_trust.enabled = value
|
|
1980
|
+
.parse()
|
|
1981
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1982
|
+
}
|
|
1983
|
+
"security.environment_variable_redaction.enabled" => {
|
|
1984
|
+
self.security.environment_variable_redaction.enabled = value
|
|
1985
|
+
.parse()
|
|
1986
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
// Experimental settings
|
|
1990
|
+
"experimental.enable_agents" => {
|
|
1991
|
+
self.experimental.enable_agents = value
|
|
1992
|
+
.parse()
|
|
1993
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1994
|
+
}
|
|
1995
|
+
"experimental.extension_management" => {
|
|
1996
|
+
self.experimental.extension_management = value
|
|
1997
|
+
.parse()
|
|
1998
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
1999
|
+
}
|
|
2000
|
+
"experimental.jit_context" => {
|
|
2001
|
+
self.experimental.jit_context = value
|
|
2002
|
+
.parse()
|
|
2003
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
2004
|
+
}
|
|
2005
|
+
"experimental.codebase_investigator_settings.enabled" => {
|
|
2006
|
+
self.experimental.codebase_investigator_settings.enabled = value
|
|
2007
|
+
.parse()
|
|
2008
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
2009
|
+
}
|
|
2010
|
+
"experimental.codebase_investigator_settings.max_num_turns" => {
|
|
2011
|
+
self.experimental
|
|
2012
|
+
.codebase_investigator_settings
|
|
2013
|
+
.max_num_turns = value
|
|
2014
|
+
.parse()
|
|
2015
|
+
.map_err(|_| anyhow!("Invalid number: {}", value))?;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
// ACP settings
|
|
2019
|
+
"acp.enabled" => {
|
|
2020
|
+
self.acp.enabled = value
|
|
2021
|
+
.parse()
|
|
2022
|
+
.map_err(|_| anyhow!("Invalid boolean value: {}", value))?;
|
|
2023
|
+
}
|
|
2024
|
+
"acp.bind_host" => {
|
|
2025
|
+
self.acp.bind_host = value.to_string();
|
|
2026
|
+
}
|
|
2027
|
+
"acp.protocol_version" => {
|
|
2028
|
+
self.acp.protocol_version = value.to_string();
|
|
2029
|
+
}
|
|
2030
|
+
"acp.dev_mode" => {
|
|
2031
|
+
self.acp.dev_mode = value
|
|
2032
|
+
.parse()
|
|
2033
|
+
.map_err(|_| anyhow!("Invalid boolean value: {}", value))?;
|
|
2034
|
+
}
|
|
2035
|
+
"acp.default_port" => {
|
|
2036
|
+
self.acp.default_port = if value.is_empty() {
|
|
2037
|
+
None
|
|
2038
|
+
} else {
|
|
2039
|
+
Some(
|
|
2040
|
+
value
|
|
2041
|
+
.parse()
|
|
2042
|
+
.map_err(|_| anyhow!("Invalid port value: {}", value))?,
|
|
2043
|
+
)
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
// Network settings
|
|
2048
|
+
"network.starlink_optimizations" => {
|
|
2049
|
+
self.network.starlink_optimizations = value
|
|
2050
|
+
.parse()
|
|
2051
|
+
.map_err(|_| anyhow!("Invalid boolean value: {}", value))?;
|
|
2052
|
+
}
|
|
2053
|
+
"network.base_retry_delay" => {
|
|
2054
|
+
self.network.base_retry_delay = value
|
|
2055
|
+
.parse()
|
|
2056
|
+
.map_err(|_| anyhow!("Invalid number: {}", value))?;
|
|
2057
|
+
}
|
|
2058
|
+
"network.max_retry_delay" => {
|
|
2059
|
+
self.network.max_retry_delay = value
|
|
2060
|
+
.parse()
|
|
2061
|
+
.map_err(|_| anyhow!("Invalid number: {}", value))?;
|
|
2062
|
+
}
|
|
2063
|
+
"network.health_monitoring" => {
|
|
2064
|
+
self.network.health_monitoring = value
|
|
2065
|
+
.parse()
|
|
2066
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
2067
|
+
}
|
|
2068
|
+
"network.connect_timeout" => {
|
|
2069
|
+
self.network.connect_timeout = value
|
|
2070
|
+
.parse()
|
|
2071
|
+
.map_err(|_| anyhow!("Invalid number: {}", value))?;
|
|
2072
|
+
}
|
|
2073
|
+
"network.read_timeout" => {
|
|
2074
|
+
self.network.read_timeout = value
|
|
2075
|
+
.parse()
|
|
2076
|
+
.map_err(|_| anyhow!("Invalid number: {}", value))?;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
// Logging settings
|
|
2080
|
+
"logging.level" => {
|
|
2081
|
+
let valid_levels = ["trace", "debug", "info", "warn", "error"];
|
|
2082
|
+
if valid_levels.contains(&value) {
|
|
2083
|
+
self.logging.level = value.to_string();
|
|
2084
|
+
} else {
|
|
2085
|
+
return Err(anyhow!(
|
|
2086
|
+
"Invalid log level. Must be one of: {}",
|
|
2087
|
+
valid_levels.join(", ")
|
|
2088
|
+
));
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
"logging.file_logging" => {
|
|
2092
|
+
self.logging.file_logging = value
|
|
2093
|
+
.parse()
|
|
2094
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
2095
|
+
}
|
|
2096
|
+
"logging.max_file_size_mb" => {
|
|
2097
|
+
self.logging.max_file_size_mb = value
|
|
2098
|
+
.parse()
|
|
2099
|
+
.map_err(|_| anyhow!("Invalid number: {}", value))?;
|
|
2100
|
+
}
|
|
2101
|
+
"logging.rotation_count" => {
|
|
2102
|
+
self.logging.rotation_count = value
|
|
2103
|
+
.parse()
|
|
2104
|
+
.map_err(|_| anyhow!("Invalid number: {}", value))?;
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// Telemetry settings
|
|
2108
|
+
"telemetry.enabled" => {
|
|
2109
|
+
self.telemetry.enabled = value
|
|
2110
|
+
.parse()
|
|
2111
|
+
.map_err(|_| anyhow!("Invalid boolean: {}", value))?;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
_ => return Err(anyhow!("Unknown configuration key: {}", key)),
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
Ok(())
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
/// Initialize a new configuration file with defaults
|
|
2121
|
+
pub async fn init(force: bool) -> Result<PathBuf> {
|
|
2122
|
+
let config_path = Self::default_config_path()?;
|
|
2123
|
+
|
|
2124
|
+
if config_path.exists() && !force {
|
|
2125
|
+
return Err(anyhow!(
|
|
2126
|
+
"Configuration file already exists at {:?}. Use --force to overwrite.",
|
|
2127
|
+
config_path
|
|
2128
|
+
));
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
let config = Config::default();
|
|
2132
|
+
config.save(None).await?;
|
|
2133
|
+
|
|
2134
|
+
Ok(config_path)
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
#[cfg(test)]
|
|
2139
|
+
mod tests {
|
|
2140
|
+
use super::*;
|
|
2141
|
+
use tempfile::tempdir;
|
|
2142
|
+
|
|
2143
|
+
#[tokio::test]
|
|
2144
|
+
async fn test_config_default() {
|
|
2145
|
+
let config = Config::default();
|
|
2146
|
+
assert_eq!(config.default_model, "grok-3");
|
|
2147
|
+
assert_eq!(config.default_temperature, 0.7);
|
|
2148
|
+
assert!(config.validate().is_ok());
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
#[tokio::test]
|
|
2152
|
+
async fn test_config_validation() {
|
|
2153
|
+
// Invalid temperature
|
|
2154
|
+
let mut config = Config {
|
|
2155
|
+
default_temperature: -1.0,
|
|
2156
|
+
..Default::default()
|
|
2157
|
+
};
|
|
2158
|
+
assert!(config.validate().is_err());
|
|
2159
|
+
|
|
2160
|
+
// Invalid log level
|
|
2161
|
+
config.default_temperature = 0.7;
|
|
2162
|
+
config.logging.level = "invalid".to_string();
|
|
2163
|
+
assert!(config.validate().is_err());
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
#[tokio::test]
|
|
2167
|
+
async fn test_config_get_set_value() {
|
|
2168
|
+
let mut config = Config::default();
|
|
2169
|
+
|
|
2170
|
+
// Test getting values
|
|
2171
|
+
assert_eq!(config.get_value("default_model").unwrap(), "grok-3");
|
|
2172
|
+
assert_eq!(config.get_value("ui.colors").unwrap(), "true");
|
|
2173
|
+
|
|
2174
|
+
// Test setting values
|
|
2175
|
+
config.set_value("default_model", "grok-1").unwrap();
|
|
2176
|
+
assert_eq!(config.default_model, "grok-1");
|
|
2177
|
+
|
|
2178
|
+
config.set_value("ui.colors", "false").unwrap();
|
|
2179
|
+
assert!(!config.ui.colors);
|
|
2180
|
+
|
|
2181
|
+
// Test invalid key
|
|
2182
|
+
assert!(config.get_value("invalid.key").is_err());
|
|
2183
|
+
assert!(config.set_value("invalid.key", "value").is_err());
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
#[tokio::test]
|
|
2187
|
+
async fn test_config_save_load() {
|
|
2188
|
+
// Ensure env var doesn't interfere
|
|
2189
|
+
unsafe {
|
|
2190
|
+
std::env::remove_var("GROK_MODEL");
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
let temp_dir = tempdir().unwrap();
|
|
2194
|
+
let config_path = temp_dir.path().join("config.toml");
|
|
2195
|
+
|
|
2196
|
+
// Create and save config
|
|
2197
|
+
let original_config = Config {
|
|
2198
|
+
default_model: "test-model".to_string(),
|
|
2199
|
+
..Default::default()
|
|
2200
|
+
};
|
|
2201
|
+
original_config
|
|
2202
|
+
.save(Some(config_path.to_str().unwrap()))
|
|
2203
|
+
.await
|
|
2204
|
+
.unwrap();
|
|
2205
|
+
|
|
2206
|
+
// Load config and verify
|
|
2207
|
+
let loaded_config = Config::load(Some(config_path.to_str().unwrap()))
|
|
2208
|
+
.await
|
|
2209
|
+
.unwrap();
|
|
2210
|
+
assert_eq!(loaded_config.default_model, "test-model");
|
|
2211
|
+
}
|
|
2212
|
+
}
|