1#[allow(clippy::wildcard_imports)]
7use super::*;
8
9#[derive(Debug, Default)]
11pub struct AgentSetupResult {
12 pub mcp_ok: bool,
13 pub mcp_skipped: bool,
16 pub rules: crate::rules_inject::InjectResult,
17 pub skill_installed: bool,
18 pub errors: Vec<String>,
19}
20
21pub fn setup_single_agent(
24 agent_name: &str,
25 global: bool,
26 mode: crate::hooks::HookMode,
27) -> AgentSetupResult {
28 let home = crate::core::home::resolve_home_dir().unwrap_or_default();
31 let mut result = AgentSetupResult::default();
32
33 crate::hooks::install_agent_hook_with_mode(agent_name, global, mode);
34
35 if crate::core::config::Config::load()
39 .setup
40 .should_update_mcp()
41 {
42 match configure_agent_mcp(agent_name) {
43 Ok(()) => result.mcp_ok = true,
44 Err(e) => result.errors.push(format!("MCP config: {e}")),
45 }
46 } else {
47 result.mcp_skipped = true;
48 }
49
50 result.rules = crate::rules_inject::inject_rules_for_agent(&home, agent_name);
51
52 if let Ok(path) = crate::rules_inject::install_skill_for_agent(&home, agent_name) {
53 result.skill_installed = path.exists();
54 }
55
56 result
57}
58
59pub fn configure_agent_mcp(agent: &str) -> Result<(), String> {
60 let home = crate::core::home::resolve_home_dir()
62 .ok_or_else(|| "Cannot determine home directory".to_string())?;
63 let binary = resolve_portable_binary();
64
65 let targets = agent_mcp_targets(agent, &home)?;
66
67 let mut errors = Vec::new();
68 for t in &targets {
69 if let Err(e) = crate::core::editor_registry::write_config_with_options(
70 t,
71 &binary,
72 WriteOptions {
73 overwrite_invalid: true,
74 },
75 ) {
76 eprintln!(
77 "\x1b[33m⚠\x1b[0m Could not configure {}: {}",
78 t.config_path.display(),
79 e
80 );
81 errors.push(e);
82 }
83 }
84
85 if agent == "kiro" {
86 install_kiro_steering(&home);
87 }
88
89 if (agent == "vscode" || agent == "copilot")
90 && let Err(e) = crate::core::editor_registry::plan_mode::write_vscode_plan_settings()
91 {
92 eprintln!("\x1b[33m⚠\x1b[0m VS Code plan mode: {e}");
93 }
94 if (agent == "claude" || agent == "claude-code")
95 && let Err(e) =
96 crate::core::editor_registry::plan_mode::write_claude_code_plan_permissions()
97 {
98 eprintln!("\x1b[33m⚠\x1b[0m Claude Code plan mode: {e}");
99 }
100 if agent == "codebuddy"
101 && let Err(e) =
102 crate::core::editor_registry::plan_mode::write_claude_code_plan_permissions()
103 {
104 eprintln!("\x1b[33m⚠\x1b[0m CodeBuddy plan mode: {e}");
105 }
106
107 if errors.is_empty() {
108 Ok(())
109 } else {
110 Err(format!(
111 "{} config(s) could not be written. See warnings above.",
112 errors.len()
113 ))
114 }
115}
116
117pub(crate) fn agent_mcp_targets(
118 agent: &str,
119 home: &std::path::Path,
120) -> Result<Vec<EditorTarget>, String> {
121 let mut targets = Vec::<EditorTarget>::new();
122
123 let push = |targets: &mut Vec<EditorTarget>,
124 name: &'static str,
125 config_path: PathBuf,
126 config_type: ConfigType| {
127 targets.push(EditorTarget {
128 name,
129 agent_key: agent.to_string(),
130 detect_path: PathBuf::from("/nonexistent"), config_path,
132 config_type,
133 });
134 };
135
136 match agent {
137 "cursor" => push(
138 &mut targets,
139 "Cursor",
140 home.join(".cursor/mcp.json"),
141 ConfigType::McpJson,
142 ),
143 "claude" | "claude-code" => push(
144 &mut targets,
145 "Claude Code",
146 crate::core::editor_registry::claude_mcp_json_path(home),
147 ConfigType::McpJson,
148 ),
149 "codebuddy" => push(
150 &mut targets,
151 "CodeBuddy",
152 crate::core::editor_registry::codebuddy_mcp_json_path(home),
153 ConfigType::McpJson,
154 ),
155 "commandcode" => push(
156 &mut targets,
157 "Command Code",
158 home.join(".commandcode/mcp.json"),
159 ConfigType::CommandCode,
160 ),
161 "augment" => {
162 push(
163 &mut targets,
164 "Augment CLI",
165 crate::core::editor_registry::augment_cli_settings_path(home),
166 ConfigType::McpJson,
167 );
168 push(
169 &mut targets,
170 "Augment (VS Code)",
171 crate::core::editor_registry::augment_vscode_mcp_path(home),
172 ConfigType::AugmentVsCode,
173 );
174 }
175 "windsurf" => push(
176 &mut targets,
177 "Windsurf",
178 home.join(".codeium/windsurf/mcp_config.json"),
179 ConfigType::McpJson,
180 ),
181 "codex" => push(
182 &mut targets,
183 "Codex CLI",
184 crate::core::home::resolve_codex_config_path()
185 .unwrap_or_else(|| home.join(".codex/config.toml")),
186 ConfigType::Codex,
187 ),
188 "grok" => push(
189 &mut targets,
190 "Grok",
191 home.join(".grok/config.toml"),
192 ConfigType::Codex,
193 ),
194 "gemini" => {
195 push(
196 &mut targets,
197 "Gemini CLI",
198 home.join(".gemini/settings.json"),
199 ConfigType::GeminiSettings,
200 );
201 push(
202 &mut targets,
203 "Antigravity IDE",
204 home.join(".gemini/antigravity/mcp_config.json"),
205 ConfigType::McpJson,
206 );
207 push(
208 &mut targets,
209 "Antigravity CLI",
210 home.join(".gemini/antigravity-cli/mcp_config.json"),
211 ConfigType::McpJson,
212 );
213 }
214 "antigravity" => push(
215 &mut targets,
216 "Antigravity IDE",
217 home.join(".gemini/antigravity/mcp_config.json"),
218 ConfigType::McpJson,
219 ),
220 "antigravity-cli" => push(
221 &mut targets,
222 "Antigravity CLI",
223 home.join(".gemini/antigravity-cli/mcp_config.json"),
224 ConfigType::McpJson,
225 ),
226 "copilot" => push(
227 &mut targets,
228 "Copilot CLI",
229 home.join(".copilot/mcp-config.json"),
230 ConfigType::CopilotCli,
231 ),
232 "crush" => push(
233 &mut targets,
234 "Crush",
235 home.join(".config/crush/crush.json"),
236 ConfigType::Crush,
237 ),
238 "qoder" => {
239 for path in crate::core::editor_registry::qoder_all_mcp_paths(home) {
240 push(&mut targets, "Qoder", path, ConfigType::QoderSettings);
241 }
242 }
243 "qoderwork" => push(
244 &mut targets,
245 "QoderWork",
246 crate::core::editor_registry::qoderwork_mcp_path(home),
247 ConfigType::McpJson,
248 ),
249 "qodercli" => push(
250 &mut targets,
251 "Qoder CLI",
252 crate::core::editor_registry::qodercli_settings_path(home),
253 ConfigType::QoderSettings,
254 ),
255 "cline" => push(
256 &mut targets,
257 "Cline",
258 crate::core::editor_registry::cline_mcp_path(),
259 ConfigType::McpJson,
260 ),
261 "roo" => push(
262 &mut targets,
263 "Roo Code",
264 crate::core::editor_registry::roo_mcp_path(),
265 ConfigType::McpJson,
266 ),
267 "kiro" => push(
268 &mut targets,
269 "AWS Kiro",
270 home.join(".kiro/settings/mcp.json"),
271 ConfigType::McpJson,
272 ),
273 "verdent" => push(
274 &mut targets,
275 "Verdent",
276 home.join(".verdent/mcp.json"),
277 ConfigType::McpJson,
278 ),
279 "pi" | "jetbrains" | "amp" | "openclaw" => {
285 }
288 "qwen" => push(
289 &mut targets,
290 "Qwen Code",
291 home.join(".qwen/settings.json"),
292 ConfigType::McpJson,
293 ),
294 "trae" => push(
295 &mut targets,
296 "Trae",
297 home.join(".trae/mcp.json"),
298 ConfigType::McpJson,
299 ),
300 "amazonq" => push(
301 &mut targets,
302 "Amazon Q Developer",
303 home.join(".aws/amazonq/default.json"),
304 ConfigType::McpJson,
305 ),
306 "opencode" => {
307 #[cfg(windows)]
308 let opencode_path = if let Ok(appdata) = std::env::var("APPDATA") {
309 std::path::PathBuf::from(appdata)
310 .join("opencode")
311 .join("opencode.json")
312 } else {
313 home.join(".config/opencode/opencode.json")
314 };
315 #[cfg(not(windows))]
316 let opencode_path = home.join(".config/opencode/opencode.json");
317 push(
318 &mut targets,
319 "OpenCode",
320 opencode_path,
321 ConfigType::OpenCode,
322 );
323 }
324 "hermes" => push(
325 &mut targets,
326 "Hermes Agent",
327 home.join(".hermes/config.yaml"),
328 ConfigType::HermesYaml,
329 ),
330 "vscode" => push(
331 &mut targets,
332 "VS Code",
333 crate::core::editor_registry::vscode_mcp_path(),
334 ConfigType::VsCodeMcp,
335 ),
336 "vscode-insiders" => push(
337 &mut targets,
338 "VS Code Insiders",
339 crate::core::editor_registry::vscode_insiders_mcp_path(),
340 ConfigType::VsCodeMcp,
341 ),
342 "zed" => push(
343 &mut targets,
344 "Zed",
345 crate::core::editor_registry::zed_settings_path(home),
346 ConfigType::Zed,
347 ),
348 "aider" => push(
349 &mut targets,
350 "Aider",
351 home.join(".aider/mcp.json"),
352 ConfigType::McpJson,
353 ),
354 "continue" => push(
355 &mut targets,
356 "Continue",
357 home.join(".continue/mcp.json"),
358 ConfigType::McpJson,
359 ),
360 "neovim" => push(
361 &mut targets,
362 "Neovim (mcphub.nvim)",
363 home.join(".config/mcphub/servers.json"),
364 ConfigType::McpJson,
365 ),
366 "emacs" => push(
367 &mut targets,
368 "Emacs (mcp.el)",
369 home.join(".emacs.d/mcp.json"),
370 ConfigType::McpJson,
371 ),
372 "sublime" => push(
373 &mut targets,
374 "Sublime Text",
375 home.join(".config/sublime-text/mcp.json"),
376 ConfigType::McpJson,
377 ),
378 _ => {
379 return Err(format!("Unknown agent '{agent}'"));
380 }
381 }
382
383 Ok(targets)
384}
385
386pub fn disable_agent_mcp(agent: &str, overwrite_invalid: bool) -> Result<(), String> {
387 let home = crate::core::home::resolve_home_dir()
389 .ok_or_else(|| "Cannot determine home directory".to_string())?;
390
391 let mut targets = Vec::<EditorTarget>::new();
392
393 let push = |targets: &mut Vec<EditorTarget>,
394 name: &'static str,
395 config_path: PathBuf,
396 config_type: ConfigType| {
397 targets.push(EditorTarget {
398 name,
399 agent_key: agent.to_string(),
400 detect_path: PathBuf::from("/nonexistent"),
401 config_path,
402 config_type,
403 });
404 };
405
406 let pi_cfg = home.join(".pi").join("agent").join("mcp.json");
407
408 match agent {
409 "cursor" => push(
410 &mut targets,
411 "Cursor",
412 home.join(".cursor/mcp.json"),
413 ConfigType::McpJson,
414 ),
415 "claude" | "claude-code" => push(
416 &mut targets,
417 "Claude Code",
418 crate::core::editor_registry::claude_mcp_json_path(&home),
419 ConfigType::McpJson,
420 ),
421 "codebuddy" => push(
422 &mut targets,
423 "CodeBuddy",
424 crate::core::editor_registry::codebuddy_mcp_json_path(&home),
425 ConfigType::McpJson,
426 ),
427 "commandcode" => push(
428 &mut targets,
429 "Command Code",
430 home.join(".commandcode/mcp.json"),
431 ConfigType::CommandCode,
432 ),
433 "augment" => {
434 push(
435 &mut targets,
436 "Augment CLI",
437 crate::core::editor_registry::augment_cli_settings_path(&home),
438 ConfigType::McpJson,
439 );
440 push(
441 &mut targets,
442 "Augment (VS Code)",
443 crate::core::editor_registry::augment_vscode_mcp_path(&home),
444 ConfigType::AugmentVsCode,
445 );
446 }
447 "windsurf" => push(
448 &mut targets,
449 "Windsurf",
450 home.join(".codeium/windsurf/mcp_config.json"),
451 ConfigType::McpJson,
452 ),
453 "codex" => push(
454 &mut targets,
455 "Codex CLI",
456 crate::core::home::resolve_codex_config_path()
457 .unwrap_or_else(|| home.join(".codex/config.toml")),
458 ConfigType::Codex,
459 ),
460 "grok" => push(
461 &mut targets,
462 "Grok",
463 home.join(".grok/config.toml"),
464 ConfigType::Codex,
465 ),
466 "gemini" => {
467 push(
468 &mut targets,
469 "Gemini CLI",
470 home.join(".gemini/settings.json"),
471 ConfigType::GeminiSettings,
472 );
473 push(
474 &mut targets,
475 "Antigravity IDE",
476 home.join(".gemini/antigravity/mcp_config.json"),
477 ConfigType::McpJson,
478 );
479 push(
480 &mut targets,
481 "Antigravity CLI",
482 home.join(".gemini/antigravity-cli/mcp_config.json"),
483 ConfigType::McpJson,
484 );
485 }
486 "antigravity" => push(
487 &mut targets,
488 "Antigravity IDE",
489 home.join(".gemini/antigravity/mcp_config.json"),
490 ConfigType::McpJson,
491 ),
492 "antigravity-cli" => push(
493 &mut targets,
494 "Antigravity CLI",
495 home.join(".gemini/antigravity-cli/mcp_config.json"),
496 ConfigType::McpJson,
497 ),
498 "copilot" => push(
499 &mut targets,
500 "Copilot CLI",
501 home.join(".copilot/mcp-config.json"),
502 ConfigType::CopilotCli,
503 ),
504 "crush" => push(
505 &mut targets,
506 "Crush",
507 home.join(".config/crush/crush.json"),
508 ConfigType::Crush,
509 ),
510 "pi" => push(&mut targets, "Pi Coding Agent", pi_cfg, ConfigType::McpJson),
511 "qoder" => {
512 for path in crate::core::editor_registry::qoder_all_mcp_paths(&home) {
513 push(&mut targets, "Qoder", path, ConfigType::QoderSettings);
514 }
515 }
516 "qoderwork" => push(
517 &mut targets,
518 "QoderWork",
519 crate::core::editor_registry::qoderwork_mcp_path(&home),
520 ConfigType::McpJson,
521 ),
522 "qodercli" => push(
523 &mut targets,
524 "Qoder CLI",
525 crate::core::editor_registry::qodercli_settings_path(&home),
526 ConfigType::QoderSettings,
527 ),
528 "cline" => push(
529 &mut targets,
530 "Cline",
531 crate::core::editor_registry::cline_mcp_path(),
532 ConfigType::McpJson,
533 ),
534 "roo" => push(
535 &mut targets,
536 "Roo Code",
537 crate::core::editor_registry::roo_mcp_path(),
538 ConfigType::McpJson,
539 ),
540 "kiro" => push(
541 &mut targets,
542 "AWS Kiro",
543 home.join(".kiro/settings/mcp.json"),
544 ConfigType::McpJson,
545 ),
546 "verdent" => push(
547 &mut targets,
548 "Verdent",
549 home.join(".verdent/mcp.json"),
550 ConfigType::McpJson,
551 ),
552 "jetbrains" | "amp" | "openclaw" => {
553 }
555 "qwen" => push(
556 &mut targets,
557 "Qwen Code",
558 home.join(".qwen/settings.json"),
559 ConfigType::McpJson,
560 ),
561 "trae" => push(
562 &mut targets,
563 "Trae",
564 home.join(".trae/mcp.json"),
565 ConfigType::McpJson,
566 ),
567 "amazonq" => push(
568 &mut targets,
569 "Amazon Q Developer",
570 home.join(".aws/amazonq/default.json"),
571 ConfigType::McpJson,
572 ),
573 "opencode" => {
574 #[cfg(windows)]
575 let opencode_path = if let Ok(appdata) = std::env::var("APPDATA") {
576 std::path::PathBuf::from(appdata)
577 .join("opencode")
578 .join("opencode.json")
579 } else {
580 home.join(".config/opencode/opencode.json")
581 };
582 #[cfg(not(windows))]
583 let opencode_path = home.join(".config/opencode/opencode.json");
584 push(
585 &mut targets,
586 "OpenCode",
587 opencode_path,
588 ConfigType::OpenCode,
589 );
590 }
591 "hermes" => push(
592 &mut targets,
593 "Hermes Agent",
594 home.join(".hermes/config.yaml"),
595 ConfigType::HermesYaml,
596 ),
597 "vscode" => push(
598 &mut targets,
599 "VS Code",
600 crate::core::editor_registry::vscode_mcp_path(),
601 ConfigType::VsCodeMcp,
602 ),
603 "vscode-insiders" => push(
604 &mut targets,
605 "VS Code Insiders",
606 crate::core::editor_registry::vscode_insiders_mcp_path(),
607 ConfigType::VsCodeMcp,
608 ),
609 "zed" => push(
610 &mut targets,
611 "Zed",
612 crate::core::editor_registry::zed_settings_path(&home),
613 ConfigType::Zed,
614 ),
615 "aider" => push(
616 &mut targets,
617 "Aider",
618 home.join(".aider/mcp.json"),
619 ConfigType::McpJson,
620 ),
621 "continue" => push(
622 &mut targets,
623 "Continue",
624 home.join(".continue/mcp.json"),
625 ConfigType::McpJson,
626 ),
627 "neovim" => push(
628 &mut targets,
629 "Neovim (mcphub.nvim)",
630 home.join(".config/mcphub/servers.json"),
631 ConfigType::McpJson,
632 ),
633 "emacs" => push(
634 &mut targets,
635 "Emacs (mcp.el)",
636 home.join(".emacs.d/mcp.json"),
637 ConfigType::McpJson,
638 ),
639 "sublime" => push(
640 &mut targets,
641 "Sublime Text",
642 home.join(".config/sublime-text/mcp.json"),
643 ConfigType::McpJson,
644 ),
645 _ => {
646 return Err(format!("Unknown agent '{agent}'"));
647 }
648 }
649
650 for t in &targets {
651 crate::core::editor_registry::remove_lean_ctx_server(
652 t,
653 WriteOptions { overwrite_invalid },
654 )?;
655 }
656
657 Ok(())
658}
659
660#[cfg(test)]
661mod qodercli_tests {
662 use super::*;
663
664 #[test]
665 fn commandcode_agent_target_uses_command_code_schema() {
666 let home = std::path::Path::new("/home/tester");
667 let targets = agent_mcp_targets("commandcode", home).unwrap();
668
669 assert_eq!(targets.len(), 1);
670 assert_eq!(targets[0].name, "Command Code");
671 assert_eq!(targets[0].config_path, home.join(".commandcode/mcp.json"));
672 assert_eq!(targets[0].config_type, ConfigType::CommandCode);
673 }
674
675 #[test]
676 fn qodercli_agent_target_uses_settings_json() {
677 let home = std::path::Path::new("/home/tester");
678 let targets = agent_mcp_targets("qodercli", home).unwrap();
679
680 assert_eq!(targets.len(), 1);
681 assert_eq!(targets[0].name, "Qoder CLI");
682 assert_eq!(targets[0].config_path, home.join(".qoder/settings.json"));
683 }
684
685 #[test]
686 fn qodercli_agent_target_uses_qoder_settings_writer() {
687 let home = std::path::Path::new("/home/tester");
688 let targets = agent_mcp_targets("qodercli", home).unwrap();
689
690 assert_eq!(targets[0].agent_key, "qodercli");
691 assert_eq!(targets[0].config_type, ConfigType::QoderSettings);
692 }
693
694 #[test]
695 fn qodercli_agent_target_is_separate_from_qoder_ide_targets() {
696 let home = std::path::Path::new("/home/tester");
697 let cli = agent_mcp_targets("qodercli", home).unwrap();
698 let ide = agent_mcp_targets("qoder", home).unwrap();
699
700 assert!(
701 ide.iter()
702 .all(|target| target.config_path != cli[0].config_path)
703 );
704 }
705}