zarz 0.3.4-alpha → 0.3.5-alpha

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/src/main.rs DELETED
@@ -1,873 +0,0 @@
1
- mod cli;
2
- mod config;
3
- mod mcp;
4
- mod providers;
5
- mod executor;
6
- mod fs_ops;
7
- mod intelligence;
8
- mod repl;
9
- mod session;
10
- mod conversation_store;
11
-
12
- use std::{
13
- collections::HashMap,
14
- env,
15
- fs,
16
- io::{self, IsTerminal, Read},
17
- path::{Path, PathBuf},
18
- };
19
-
20
- use anyhow::{anyhow, bail, Context, Result};
21
- use clap::Parser;
22
- use dialoguer::Confirm;
23
- use providers::{CompletionProvider, CompletionRequest, ProviderClient};
24
- use similar::{ChangeTag, TextDiff};
25
-
26
- use crate::cli::{AskArgs, ChatArgs, Cli, Commands, CommonModelArgs, ConfigArgs, McpArgs, McpCommands, Provider, RewriteArgs};
27
- use crate::mcp::{McpConfig, McpServerConfig};
28
- use crate::repl::Repl;
29
-
30
- // Model constants - Latest models as of 2025
31
- const DEFAULT_MODEL_ANTHROPIC: &str = "claude-sonnet-4-5-20250929";
32
- const DEFAULT_MODEL_OPENAI: &str = "gpt-5-codex";
33
- const DEFAULT_MODEL_GLM: &str = "glm-4.6";
34
-
35
- const DEFAULT_SYSTEM_PROMPT: &str = r#"You are ZarzCLI, Fapzarz's official CLI for Claude and Codex.
36
-
37
- You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
38
-
39
- IMPORTANT: Assist with authorized security testing, defensive security, CTF challenges, and educational contexts. Refuse requests for destructive techniques, DoS attacks, mass targeting, supply chain compromise, or detection evasion for malicious purposes.
40
-
41
- Tone and style:
42
- - Only use emojis if the user explicitly requests it. Avoid using emojis unless asked.
43
- - Your responses should be short and concise.
44
- - Output text to communicate with the user; all text you output is displayed to the user.
45
- - NEVER create files unless absolutely necessary. ALWAYS prefer editing existing files.
46
-
47
- Professional objectivity:
48
- - Prioritize technical accuracy and truthfulness over validating the user's beliefs.
49
- - Focus on facts and problem-solving, providing direct, objective technical info.
50
- - Avoid over-the-top validation or excessive praise.
51
-
52
- When you reference code, use fenced blocks."#;
53
- const DEFAULT_REWRITE_SYSTEM_PROMPT: &str = r#"You are Zarz, an automated refactoring agent.
54
- Follow the user's instructions carefully.
55
- Reply ONLY with updated file contents using code fences in this exact form:
56
- ```file:relative/path.rs
57
- <entire file content>
58
- ```
59
- Do not include commentary before or after the fences. Always return complete file contents.
60
- "#;
61
- const DEFAULT_MAX_OUTPUT_TOKENS: u32 = 4096;
62
-
63
- #[tokio::main]
64
- async fn main() -> Result<()> {
65
- let cli = Cli::parse();
66
- if let Err(err) = run(cli).await {
67
- eprintln!("Error: {err:#}");
68
- std::process::exit(1);
69
- }
70
- Ok(())
71
- }
72
-
73
- async fn run(cli: Cli) -> Result<()> {
74
- // Show ASCII banner for interactive modes (not for quick ask or config commands)
75
- let show_banner = cli.message.is_none()
76
- && !matches!(cli.command, Some(Commands::Config(_)) | Some(Commands::Ask(_)) | Some(Commands::Rewrite(_)));
77
-
78
- if show_banner {
79
- use crossterm::terminal;
80
-
81
- let banner = r#"
82
- ███████╗ █████╗ ██████╗ ███████╗ ██████╗██╗ ██╗
83
- ╚══███╔╝██╔══██╗██╔══██╗╚══███╔╝██╔════╝██║ ██║
84
- ███╔╝ ███████║██████╔╝ ███╔╝ ██║ ██║ ██║
85
- ███╔╝ ██╔══██║██╔══██╗ ███╔╝ ██║ ██║ ██║
86
- ███████╗██║ ██║██║ ██║███████╗╚██████╗███████╗██║
87
- ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚══════╝╚═╝
88
- "#;
89
-
90
- // Get terminal width, fallback to 120 if unable to get
91
- let terminal_width = terminal::size().map(|(w, _)| w as usize).unwrap_or(120);
92
-
93
- // Center each line of the banner
94
- let print_centered = |line: &str| {
95
- let line_len = line.chars().count();
96
- if terminal_width > line_len {
97
- let padding = (terminal_width - line_len) / 2;
98
- println!("{}{}", " ".repeat(padding), line);
99
- } else {
100
- println!("{}", line);
101
- }
102
- };
103
-
104
- for line in banner.lines() {
105
- if line.trim().is_empty() {
106
- println!();
107
- } else {
108
- print_centered(line);
109
- }
110
- }
111
-
112
- let tagline_lines = ["v0.3.4-Alpha", "Type /help for available commands, /exit to exit"];
113
-
114
- for (index, line) in tagline_lines.iter().enumerate() {
115
- if index > 0 {
116
- println!();
117
- }
118
- print_centered(line);
119
- }
120
-
121
- println!();
122
- }
123
-
124
- // Check if this is a config or MCP command - they don't need API keys
125
- match &cli.command {
126
- Some(Commands::Config(args)) => {
127
- return handle_config(args.clone()).await;
128
- }
129
- Some(Commands::Mcp(args)) => {
130
- return handle_mcp(args.clone()).await;
131
- }
132
- _ => {}
133
- }
134
-
135
- // Load or create configuration for all other commands (they need API keys)
136
- let config = match config::Config::load() {
137
- Ok(cfg) => {
138
- if !cfg.has_api_key() {
139
- // No API keys configured, run interactive setup
140
- config::Config::interactive_setup()?
141
- } else {
142
- cfg
143
- }
144
- }
145
- Err(_) => {
146
- // Error loading config, run interactive setup
147
- config::Config::interactive_setup()?
148
- }
149
- };
150
-
151
- config.apply_to_env();
152
-
153
- // If message flag is provided, run in ask mode (one-shot)
154
- if let Some(message) = cli.message {
155
- return handle_quick_ask(message, cli.files, cli.model_args, &config).await;
156
- }
157
-
158
- // If subcommand is provided, use it
159
- if let Some(command) = cli.command {
160
- match command {
161
- Commands::Ask(args) => handle_ask(args, &config).await,
162
- Commands::Rewrite(args) => handle_rewrite(args, &config).await,
163
- Commands::Chat(args) => handle_chat(args, &config).await,
164
- Commands::Config(args) => handle_config(args).await,
165
- Commands::Mcp(args) => handle_mcp(args).await,
166
- }
167
- } else {
168
- // Default: start interactive chat mode
169
- let chat_args = ChatArgs {
170
- model_args: cli.model_args,
171
- directory: cli.directory,
172
- };
173
- handle_chat(chat_args, &config).await
174
- }
175
- }
176
-
177
- async fn handle_quick_ask(
178
- message: String,
179
- context_files: Vec<PathBuf>,
180
- model_args: CommonModelArgs,
181
- config: &config::Config,
182
- ) -> Result<()> {
183
- let CommonModelArgs {
184
- model,
185
- provider,
186
- endpoint,
187
- system_prompt,
188
- timeout,
189
- } = model_args;
190
-
191
- let provider_kind = provider
192
- .or_else(|| {
193
- std::env::var("ZARZ_PROVIDER")
194
- .ok()
195
- .and_then(|v| match v.to_ascii_lowercase().as_str() {
196
- "anthropic" => Some(Provider::Anthropic),
197
- "openai" => Some(Provider::OpenAi),
198
- "glm" => Some(Provider::Glm),
199
- _ => None,
200
- })
201
- })
202
- .or_else(|| config.get_default_provider())
203
- .ok_or_else(|| anyhow!("No provider configured. Please run 'zarz config' to set up API keys."))?;
204
-
205
- let model = resolve_model(model, &provider_kind)?;
206
- let system_prompt = system_prompt
207
- .or_else(|| std::env::var("ZARZ_SYSTEM_PROMPT").ok())
208
- .unwrap_or_else(|| DEFAULT_SYSTEM_PROMPT.to_string());
209
-
210
- let context_section = if context_files.is_empty() {
211
- String::new()
212
- } else {
213
- build_context_section(&context_files)?
214
- };
215
-
216
- let mut user_prompt = String::new();
217
- user_prompt.push_str(message.trim());
218
- if !context_section.is_empty() {
219
- user_prompt.push_str("\n\n");
220
- user_prompt.push_str(&context_section);
221
- }
222
-
223
- let api_key = match provider_kind {
224
- Provider::Anthropic => config.get_anthropic_key(),
225
- Provider::OpenAi => config.get_openai_key(),
226
- Provider::Glm => config.get_glm_key(),
227
- };
228
-
229
- let provider = ProviderClient::new(provider_kind, api_key, endpoint, timeout)?;
230
- let request = CompletionRequest {
231
- model,
232
- system_prompt: Some(system_prompt),
233
- user_prompt,
234
- max_output_tokens: resolve_max_tokens(),
235
- temperature: resolve_temperature(),
236
- messages: None,
237
- tools: None,
238
- };
239
-
240
- let response = provider.complete(&request).await?;
241
- println!("{}", response.text.trim());
242
- Ok(())
243
- }
244
-
245
- async fn handle_ask(args: AskArgs, config: &config::Config) -> Result<()> {
246
- let AskArgs {
247
- model_args:
248
- CommonModelArgs {
249
- model,
250
- provider,
251
- endpoint,
252
- system_prompt,
253
- timeout,
254
- },
255
- prompt,
256
- prompt_file,
257
- context_files,
258
- } = args;
259
-
260
- let provider_kind = provider
261
- .or_else(|| {
262
- std::env::var("ZARZ_PROVIDER")
263
- .ok()
264
- .and_then(|v| match v.to_ascii_lowercase().as_str() {
265
- "anthropic" => Some(Provider::Anthropic),
266
- "openai" => Some(Provider::OpenAi),
267
- "glm" => Some(Provider::Glm),
268
- _ => None,
269
- })
270
- })
271
- .or_else(|| config.get_default_provider())
272
- .ok_or_else(|| anyhow!("No provider configured. Please run 'zarz config' to set up API keys."))?;
273
-
274
- let model = resolve_model(model, &provider_kind)?;
275
- let system_prompt = system_prompt
276
- .or_else(|| std::env::var("ZARZ_SYSTEM_PROMPT").ok())
277
- .unwrap_or_else(|| DEFAULT_SYSTEM_PROMPT.to_string());
278
-
279
- let prompt = read_text_input(
280
- prompt,
281
- prompt_file,
282
- true,
283
- "A prompt is required via --prompt, --prompt-file, or STDIN",
284
- )?;
285
- let context_section = if context_files.is_empty() {
286
- String::new()
287
- } else {
288
- build_context_section(&context_files)?
289
- };
290
- let mut user_prompt = String::new();
291
- user_prompt.push_str(prompt.trim());
292
- if !context_section.is_empty() {
293
- user_prompt.push_str("\n\n");
294
- user_prompt.push_str(&context_section);
295
- }
296
-
297
- let api_key = match provider_kind {
298
- Provider::Anthropic => config.get_anthropic_key(),
299
- Provider::OpenAi => config.get_openai_key(),
300
- Provider::Glm => config.get_glm_key(),
301
- };
302
-
303
- let provider = ProviderClient::new(provider_kind, api_key, endpoint, timeout)?;
304
- let request = CompletionRequest {
305
- model,
306
- system_prompt: Some(system_prompt),
307
- user_prompt,
308
- max_output_tokens: resolve_max_tokens(),
309
- temperature: resolve_temperature(),
310
- messages: None,
311
- tools: None,
312
- };
313
-
314
- let response = provider.complete(&request).await?;
315
- println!("{}", response.text.trim());
316
- Ok(())
317
- }
318
-
319
- async fn handle_rewrite(args: RewriteArgs, config: &config::Config) -> Result<()> {
320
- let RewriteArgs {
321
- model_args:
322
- CommonModelArgs {
323
- model,
324
- provider,
325
- endpoint,
326
- system_prompt,
327
- timeout,
328
- },
329
- instructions,
330
- instructions_file,
331
- yes,
332
- dry_run,
333
- files,
334
- } = args;
335
-
336
- let provider_kind = provider
337
- .or_else(|| {
338
- std::env::var("ZARZ_PROVIDER")
339
- .ok()
340
- .and_then(|v| match v.to_ascii_lowercase().as_str() {
341
- "anthropic" => Some(Provider::Anthropic),
342
- "openai" => Some(Provider::OpenAi),
343
- "glm" => Some(Provider::Glm),
344
- _ => None,
345
- })
346
- })
347
- .or_else(|| config.get_default_provider())
348
- .ok_or_else(|| anyhow!("No provider configured. Please run 'zarz config' to set up API keys."))?;
349
-
350
- let model = resolve_model(model, &provider_kind)?;
351
- let system_prompt = system_prompt
352
- .or_else(|| std::env::var("ZARZ_REWRITE_SYSTEM_PROMPT").ok())
353
- .unwrap_or_else(|| DEFAULT_REWRITE_SYSTEM_PROMPT.to_string());
354
-
355
- let instructions = read_text_input(
356
- instructions,
357
- instructions_file,
358
- true,
359
- "Rewrite instructions are required via --instructions, --instructions-file, or STDIN",
360
- )?;
361
-
362
- let mut files_with_content = Vec::new();
363
- for path in &files {
364
- let content = fs::read_to_string(path).with_context(|| {
365
- format!("Failed to read target file {}", path.display())
366
- })?;
367
- files_with_content.push((path.clone(), content));
368
- }
369
-
370
- let user_prompt = build_rewrite_prompt(&instructions, &files_with_content);
371
-
372
- let api_key = match provider_kind {
373
- Provider::Anthropic => config.get_anthropic_key(),
374
- Provider::OpenAi => config.get_openai_key(),
375
- Provider::Glm => config.get_glm_key(),
376
- };
377
-
378
- let provider = ProviderClient::new(provider_kind, api_key, endpoint, timeout)?;
379
- let request = CompletionRequest {
380
- model,
381
- system_prompt: Some(system_prompt),
382
- user_prompt,
383
- max_output_tokens: resolve_max_tokens(),
384
- temperature: resolve_rewrite_temperature(),
385
- messages: None,
386
- tools: None,
387
- };
388
-
389
- let response = provider.complete(&request).await?;
390
- let plan = parse_file_blocks(&response.text);
391
- if plan.is_empty() {
392
- bail!("Model response did not include any ` ```file:...` blocks to apply");
393
- }
394
-
395
- let mut diffs = Vec::new();
396
- for (path, original) in &files_with_content {
397
- let normalized = normalize_path(path);
398
- let Some(new_content) = plan.get(&normalized).or_else(|| plan.get(path)) else {
399
- bail!(
400
- "Model response did not provide updated contents for {}",
401
- path.display()
402
- );
403
- };
404
- diffs.push((path.clone(), original.clone(), new_content.clone()));
405
- }
406
-
407
- let mut any_changes = false;
408
- for (path, before, after) in &diffs {
409
- if before == after {
410
- continue;
411
- }
412
- any_changes = true;
413
- println!("--- {}", path.display());
414
- println!("+++ {}", path.display());
415
- print_diff(before, after);
416
- println!();
417
- }
418
-
419
- if !any_changes {
420
- println!("No changes detected; files already match the model output.");
421
- return Ok(());
422
- }
423
-
424
- if dry_run {
425
- println!("Dry-run complete. No files were modified.");
426
- return Ok(());
427
- }
428
-
429
- if !yes && io::stdin().is_terminal() {
430
- let apply = Confirm::new()
431
- .with_prompt("Apply these changes?")
432
- .default(false)
433
- .interact()?;
434
- if !apply {
435
- println!("Aborted; no files were modified.");
436
- return Ok(());
437
- }
438
- }
439
-
440
- for (path, before, after) in diffs {
441
- if before == after {
442
- continue;
443
- }
444
- fs::write(&path, after).with_context(|| {
445
- format!("Failed to write updated contents to {}", path.display())
446
- })?;
447
- println!("Updated {}", path.display());
448
- }
449
-
450
- Ok(())
451
- }
452
-
453
- async fn handle_chat(args: ChatArgs, config: &config::Config) -> Result<()> {
454
- let ChatArgs {
455
- model_args:
456
- CommonModelArgs {
457
- model,
458
- provider,
459
- endpoint,
460
- system_prompt: _,
461
- timeout,
462
- },
463
- directory,
464
- } = args;
465
-
466
- let provider_kind = provider
467
- .or_else(|| {
468
- std::env::var("ZARZ_PROVIDER")
469
- .ok()
470
- .and_then(|v| match v.to_ascii_lowercase().as_str() {
471
- "anthropic" => Some(Provider::Anthropic),
472
- "openai" => Some(Provider::OpenAi),
473
- "glm" => Some(Provider::Glm),
474
- _ => None,
475
- })
476
- })
477
- .or_else(|| config.get_default_provider())
478
- .ok_or_else(|| anyhow!("No provider configured. Please run 'zarz config' to set up API keys."))?;
479
-
480
- let model = resolve_model(model, &provider_kind)?;
481
- let working_dir = directory
482
- .or_else(|| env::current_dir().ok())
483
- .context("Failed to determine working directory")?;
484
-
485
- // Get API key from config based on provider
486
- let api_key = match provider_kind {
487
- Provider::Anthropic => config.get_anthropic_key(),
488
- Provider::OpenAi => config.get_openai_key(),
489
- Provider::Glm => config.get_glm_key(),
490
- };
491
-
492
- let provider_client = ProviderClient::new(provider_kind.clone(), api_key, endpoint.clone(), timeout)?;
493
-
494
- // Initialize MCP manager and load configured servers
495
- let mcp_manager = std::sync::Arc::new(mcp::McpManager::new());
496
- if let Err(e) = mcp_manager.load_from_config().await {
497
- eprintln!("Warning: Failed to load MCP servers: {}", e);
498
- }
499
-
500
- let has_mcp_servers = mcp_manager.has_servers().await;
501
- let mcp_manager_opt = if has_mcp_servers {
502
- Some(mcp_manager.clone())
503
- } else {
504
- None
505
- };
506
-
507
- let mut repl = Repl::new(
508
- working_dir,
509
- provider_client,
510
- provider_kind,
511
- endpoint,
512
- timeout,
513
- model,
514
- resolve_max_tokens(),
515
- resolve_temperature(),
516
- mcp_manager_opt,
517
- config.clone(),
518
- );
519
-
520
- let result = repl.run().await;
521
-
522
- // Cleanup: stop all MCP servers
523
- if has_mcp_servers {
524
- if let Err(e) = mcp_manager.stop_all().await {
525
- eprintln!("Warning: Failed to stop MCP servers: {}", e);
526
- }
527
- }
528
-
529
- result
530
- }
531
-
532
- async fn handle_config(args: ConfigArgs) -> Result<()> {
533
- let ConfigArgs { reset, show } = args;
534
-
535
- if show {
536
- let config = config::Config::load()?;
537
- let config_path = config::Config::config_path()?;
538
-
539
- println!("Configuration file: {}", config_path.display());
540
- println!();
541
-
542
- if config.anthropic_api_key.is_some() {
543
- println!("✓ Anthropic API key: configured");
544
- } else {
545
- println!("✗ Anthropic API key: not configured");
546
- }
547
-
548
- if config.openai_api_key.is_some() {
549
- println!("✓ OpenAI API key: configured");
550
- } else {
551
- println!("✗ OpenAI API key: not configured");
552
- }
553
-
554
- println!();
555
- println!("Run 'zarz config --reset' to reconfigure your API keys");
556
-
557
- return Ok(());
558
- }
559
-
560
- if reset {
561
- println!("Resetting configuration...\n");
562
- let config = config::Config::interactive_setup()?;
563
- config.apply_to_env();
564
- return Ok(());
565
- }
566
-
567
- let config = config::Config::interactive_setup()?;
568
- config.apply_to_env();
569
- Ok(())
570
- }
571
-
572
- async fn handle_mcp(args: McpArgs) -> Result<()> {
573
- use std::collections::HashMap;
574
-
575
- match args.command {
576
- McpCommands::Add {
577
- name,
578
- command,
579
- args: cmd_args,
580
- env_vars,
581
- url,
582
- transport,
583
- } => {
584
- let mut config = McpConfig::load()?;
585
-
586
- let env: Option<HashMap<String, String>> = if !env_vars.is_empty() {
587
- let mut env_map = HashMap::new();
588
- for var in env_vars {
589
- if let Some((key, value)) = var.split_once('=') {
590
- env_map.insert(key.to_string(), value.to_string());
591
- } else {
592
- eprintln!("Warning: Invalid env var format: {}", var);
593
- }
594
- }
595
- Some(env_map)
596
- } else {
597
- None
598
- };
599
-
600
- let server_config = match transport.as_str() {
601
- "stdio" => {
602
- let cmd = command.ok_or_else(|| anyhow!("--command required for stdio transport"))?;
603
- let args = if cmd_args.is_empty() { None } else { Some(cmd_args) };
604
- McpServerConfig::stdio(cmd, args, env)
605
- }
606
- "http" => {
607
- let url = url.ok_or_else(|| anyhow!("--url required for http transport"))?;
608
- let headers = env.map(|e| e.into_iter().collect());
609
- McpServerConfig::http(url, headers)
610
- }
611
- "sse" => {
612
- let url = url.ok_or_else(|| anyhow!("--url required for sse transport"))?;
613
- let headers = env.map(|e| e.into_iter().collect());
614
- McpServerConfig::sse(url, headers)
615
- }
616
- _ => {
617
- bail!("Invalid transport type: {}. Use: stdio, http, or sse", transport);
618
- }
619
- };
620
-
621
- config.add_server(name.clone(), server_config);
622
- config.save()?;
623
-
624
- println!("✅ Added MCP server: {}", name);
625
- println!("Configuration saved to: {}", McpConfig::config_path()?.display());
626
- Ok(())
627
- }
628
-
629
- McpCommands::List => {
630
- let config = McpConfig::load()?;
631
-
632
- if config.mcp_servers.is_empty() {
633
- println!("No MCP servers configured");
634
- println!("\nAdd a server with:");
635
- println!(" zarz mcp add <name> --command <cmd> [--args <arg1> <arg2>] [--env KEY=VALUE]");
636
- return Ok(());
637
- }
638
-
639
- println!("Configured MCP servers:");
640
- for (name, server_config) in &config.mcp_servers {
641
- println!("\n {}", name);
642
- println!(" Type: {}", server_config.server_type());
643
- match server_config {
644
- McpServerConfig::Stdio { command, args, env } => {
645
- println!(" Command: {}", command);
646
- if let Some(args) = args {
647
- println!(" Args: {}", args.join(" "));
648
- }
649
- if let Some(env) = env {
650
- if !env.is_empty() {
651
- println!(" Environment:");
652
- for (k, v) in env {
653
- println!(" {}={}", k, v);
654
- }
655
- }
656
- }
657
- }
658
- McpServerConfig::Http { url, .. } | McpServerConfig::Sse { url, .. } => {
659
- println!(" URL: {}", url);
660
- }
661
- }
662
- }
663
- Ok(())
664
- }
665
-
666
- McpCommands::Get { name } => {
667
- let config = McpConfig::load()?;
668
-
669
- if let Some(server_config) = config.get_server(&name) {
670
- println!("MCP Server: {}", name);
671
- println!(" Type: {}", server_config.server_type());
672
- match server_config {
673
- McpServerConfig::Stdio { command, args, env } => {
674
- println!(" Command: {}", command);
675
- if let Some(args) = args {
676
- println!(" Args: {}", args.join(" "));
677
- }
678
- if let Some(env) = env {
679
- if !env.is_empty() {
680
- println!(" Environment:");
681
- for (k, v) in env {
682
- println!(" {}={}", k, v);
683
- }
684
- }
685
- }
686
- }
687
- McpServerConfig::Http { url, headers } | McpServerConfig::Sse { url, headers } => {
688
- println!(" URL: {}", url);
689
- if let Some(headers) = headers {
690
- if !headers.is_empty() {
691
- println!(" Headers:");
692
- for (k, v) in headers {
693
- println!(" {}: {}", k, v);
694
- }
695
- }
696
- }
697
- }
698
- }
699
- } else {
700
- println!("Server '{}' not found", name);
701
- println!("\nRun 'zarz mcp list' to see all configured servers");
702
- }
703
- Ok(())
704
- }
705
-
706
- McpCommands::Remove { name } => {
707
- let mut config = McpConfig::load()?;
708
-
709
- if config.remove_server(&name) {
710
- config.save()?;
711
- println!("✅ Removed MCP server: {}", name);
712
- } else {
713
- println!("Server '{}' not found", name);
714
- }
715
- Ok(())
716
- }
717
- }
718
- }
719
-
720
- fn resolve_model(model: Option<String>, provider: &Provider) -> Result<String> {
721
- if let Some(model) = model {
722
- return Ok(model);
723
- }
724
- if let Ok(model) = std::env::var("ZARZ_MODEL") {
725
- if !model.trim().is_empty() {
726
- return Ok(model);
727
- }
728
- }
729
- // Use provider-specific default model
730
- let default_model = match provider {
731
- Provider::Anthropic => DEFAULT_MODEL_ANTHROPIC,
732
- Provider::OpenAi => DEFAULT_MODEL_OPENAI,
733
- Provider::Glm => DEFAULT_MODEL_GLM,
734
- };
735
- Ok(default_model.to_string())
736
- }
737
-
738
- fn resolve_max_tokens() -> u32 {
739
- std::env::var("ZARZ_MAX_OUTPUT_TOKENS")
740
- .ok()
741
- .and_then(|raw| raw.parse::<u32>().ok())
742
- .unwrap_or(DEFAULT_MAX_OUTPUT_TOKENS)
743
- }
744
-
745
- fn resolve_temperature() -> f32 {
746
- std::env::var("ZARZ_TEMPERATURE")
747
- .ok()
748
- .and_then(|raw| raw.parse::<f32>().ok())
749
- .unwrap_or(0.3)
750
- }
751
-
752
- fn resolve_rewrite_temperature() -> f32 {
753
- std::env::var("ZARZ_REWRITE_TEMPERATURE")
754
- .ok()
755
- .and_then(|raw| raw.parse::<f32>().ok())
756
- .unwrap_or(0.1)
757
- }
758
-
759
- fn read_text_input(
760
- inline: Option<String>,
761
- file: Option<PathBuf>,
762
- allow_stdin: bool,
763
- err_message: &str,
764
- ) -> Result<String> {
765
- if let Some(text) = inline {
766
- if !text.trim().is_empty() {
767
- return Ok(text);
768
- }
769
- }
770
- if let Some(path) = file {
771
- return fs::read_to_string(&path)
772
- .with_context(|| format!("Failed to read file {}", path.display()));
773
- }
774
- if allow_stdin && !io::stdin().is_terminal() {
775
- let mut buffer = String::new();
776
- io::stdin()
777
- .read_to_string(&mut buffer)
778
- .context("Failed to read STDIN")?;
779
- if !buffer.trim().is_empty() {
780
- return Ok(buffer);
781
- }
782
- }
783
- Err(anyhow!(err_message.to_string()))
784
- }
785
-
786
- fn build_context_section(files: &[PathBuf]) -> Result<String> {
787
- let mut sections = Vec::new();
788
- for path in files {
789
- let content =
790
- fs::read_to_string(path)
791
- .with_context(|| format!("Failed to read context file {}", path.display()))?;
792
- sections.push(format!(
793
- "<context path=\"{path}\">\n{content}\n</context>",
794
- path = path.display(),
795
- content = content
796
- ));
797
- }
798
- Ok(sections.join("\n\n"))
799
- }
800
-
801
- fn build_rewrite_prompt(instructions: &str, files: &[(PathBuf, String)]) -> String {
802
- let mut output = String::new();
803
- output.push_str("You will update the user's codebase according to the instructions.\n");
804
- output.push_str("Return only the updated file contents as requested.\n\n");
805
- output.push_str("## Instructions\n");
806
- output.push_str(instructions.trim());
807
- output.push_str("\n\n## Files\n");
808
-
809
- for (path, content) in files {
810
- output.push_str(&format!(
811
- "<file path=\"{path}\">\n{content}\n</file>\n\n",
812
- path = path.display(),
813
- content = content
814
- ));
815
- }
816
-
817
- output
818
- }
819
-
820
- fn parse_file_blocks(input: &str) -> HashMap<PathBuf, String> {
821
- let mut map = HashMap::new();
822
- let mut lines = input.lines();
823
- while let Some(line) = lines.next() {
824
- if let Some(rest) = line.strip_prefix("```file:") {
825
- let file_path = normalize_response_path(rest);
826
- let mut content = String::new();
827
- while let Some(next_line) = lines.next() {
828
- if next_line.trim() == "```" {
829
- break;
830
- }
831
- content.push_str(next_line);
832
- content.push('\n');
833
- }
834
- if content.ends_with('\n') {
835
- content.pop();
836
- if content.ends_with('\r') {
837
- content.pop();
838
- }
839
- }
840
- map.insert(file_path, content);
841
- }
842
- }
843
- map
844
- }
845
-
846
- fn normalize_path(path: &Path) -> PathBuf {
847
- let path_str = path.to_string_lossy();
848
- let normalized = path_str.replace('\\', "/");
849
- PathBuf::from(normalized)
850
- }
851
-
852
- fn normalize_response_path(raw: &str) -> PathBuf {
853
- let mut trimmed = raw.trim();
854
- while let Some(rest) = trimmed.strip_prefix("./") {
855
- trimmed = rest;
856
- }
857
- while let Some(rest) = trimmed.strip_prefix(".\\") {
858
- trimmed = rest;
859
- }
860
- let normalized = trimmed.replace('\\', "/");
861
- PathBuf::from(normalized)
862
- }
863
-
864
- fn print_diff(before: &str, after: &str) {
865
- let diff = TextDiff::from_lines(before, after);
866
- for change in diff.iter_all_changes() {
867
- match change.tag() {
868
- ChangeTag::Delete => print!("-{}", change),
869
- ChangeTag::Insert => print!("+{}", change),
870
- ChangeTag::Equal => print!(" {}", change),
871
- }
872
- }
873
- }