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
@@ -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
+ }