1use std::{
9 ffi::OsStr,
10 path::{Path, PathBuf},
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Login {
16 Command(&'static [&'static str]),
18 Interactive {
20 args: &'static [&'static str],
21 hint: &'static str,
22 },
23 ApiKey(KeyStore),
25 Import,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Status {
32 Command(&'static [&'static str]),
34 Stored(KeyStore),
36 Unchecked,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum OutputFormat {
46 Text,
48 ClaudeStreamJson,
51 CodexJsonl,
54 PiJson,
57 GeminiStreamJson,
62 CursorStreamJson,
65 AmpStreamJson,
67}
68
69impl OutputFormat {
70 pub(crate) fn args(self) -> &'static [&'static str] {
72 match self {
73 Self::Text => &[],
74 Self::ClaudeStreamJson => &["--output-format", "stream-json", "--verbose"],
75 Self::CodexJsonl => &["--json"],
76 Self::PiJson => &["--mode", "json"],
77 Self::GeminiStreamJson | Self::CursorStreamJson => &["--output-format", "stream-json"],
78 Self::AmpStreamJson => &["--stream-json"],
79 }
80 }
81
82 pub(crate) fn claude_like(self) -> bool {
85 matches!(
86 self,
87 Self::ClaudeStreamJson | Self::CursorStreamJson | Self::AmpStreamJson
88 )
89 }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum Transport {
95 Process,
98 ScvProtocol,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub struct AcpLaunch {
108 pub command: &'static str,
110 pub(crate) args: &'static [&'static str],
113 pub(crate) full_args: &'static [&'static str],
114 pub full_mode: Option<&'static str>,
117 pub full_environment: &'static [(&'static str, &'static str)],
120 pub session_options: bool,
125}
126
127pub fn acp_args(launch: &AcpLaunch, full: bool) -> Vec<String> {
129 let mut args = Vec::with_capacity(launch.args.len() + launch.full_args.len());
130 for arg in launch.args {
131 if *arg == "{full}" {
132 if full {
133 args.extend(launch.full_args.iter().map(|arg| (*arg).to_owned()));
134 }
135 } else {
136 args.push((*arg).to_owned());
137 }
138 }
139 args
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum Resume {
146 Unsupported,
148 Supported {
149 start: &'static [&'static str],
152 subcommand: &'static [&'static str],
155 options: &'static [&'static str],
157 positional: &'static [&'static str],
160 },
161}
162
163impl Resume {
164 pub(crate) fn is_supported(self) -> bool {
165 matches!(self, Self::Supported { .. })
166 }
167
168 pub(crate) fn assigns_id(self) -> bool {
170 matches!(self, Self::Supported { start, .. } if !start.is_empty())
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub struct ConversationFiles {
178 pub(crate) dir: &'static str,
179 pub(crate) extension: &'static str,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum StatusSummary {
186 CodexText,
188 ExitStatus,
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub enum Logout {
195 Command(&'static [&'static str]),
196 Stored(KeyStore),
198 Interactive {
200 args: &'static [&'static str],
201 hint: &'static str,
202 },
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum KeyStore {
208 ClaudeApiKey {
211 key: &'static str,
212 credentials: &'static str,
213 },
214 ClaudeAccount {
216 credentials: &'static str,
217 config: &'static str,
218 },
219 Grok {
222 auth: &'static str,
223 config: &'static str,
224 },
225 DshRefs {
227 path: &'static str,
228 variable: &'static str,
229 },
230 Pi { dir: &'static str },
233 Scv { config: &'static str },
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub enum AgentAuth {
241 None,
242 ClaudeApiKey,
243 ClaudeSubscription,
244}
245
246#[derive(Debug, Clone, Copy)]
247pub struct AdapterDescriptor {
248 pub name: &'static str,
250 pub family: &'static str,
252 pub auth: AgentAuth,
254 pub product: &'static str,
256 pub(crate) offers: &'static str,
259 pub command: &'static str,
260 pub args: &'static [&'static str],
261 pub prompt_args: &'static [&'static str],
264 pub model_args: &'static [&'static str],
265 pub effort_args: &'static [&'static str],
266 pub model_hint: &'static str,
268 pub home_environment: &'static [(&'static str, &'static str)],
271 pub fixed_environment: &'static [(&'static str, &'static str)],
273 pub(crate) removed_environment: &'static [&'static str],
276 pub full_permission_args: &'static [&'static str],
281 pub full_permission_environment: &'static [(&'static str, &'static str)],
283 pub(crate) search_dirs: &'static [&'static str],
288 pub login: Login,
289 pub status: Status,
290 pub status_summary: StatusSummary,
292 pub logout: Logout,
293 pub output: OutputFormat,
295 pub resume: Resume,
297 pub conversation_files: Option<ConversationFiles>,
299 pub credential_files: &'static [&'static str],
302 pub transport: Transport,
304 pub acp: Option<AcpLaunch>,
308 pub cli_turns: bool,
312}
313
314const USER_BIN_DIRS: &[&str] = &[".local/bin"];
316
317const COMMON_REMOVED_ENVIRONMENT: &[&str] = &[
321 "SCV_CONFIG",
322 "SCV_MODEL",
323 "SCV_PROVIDER",
324 "SCV_BASE_URL",
325 "SCV_API_KEY_ENV",
326 "GEMINI_API_KEY",
327 "GOOGLE_API_KEY",
328 "AZURE_OPENAI_API_KEY",
329 "AZURE_OPENAI_ENDPOINT",
330 "AWS_ACCESS_KEY_ID",
335 "AWS_SECRET_ACCESS_KEY",
336 "AWS_SESSION_TOKEN",
337 "AWS_BEARER_TOKEN_BEDROCK",
338];
339
340const PI_STORE: KeyStore = KeyStore::Pi { dir: ".pi/agent" };
341const SCV_STORE: KeyStore = KeyStore::Scv {
342 config: "config.toml",
343};
344const DSH_STORE: KeyStore = KeyStore::DshRefs {
345 path: ".dsh/.credentials.yaml",
346 variable: "DEEPSEEK_API_KEY",
347};
348const CLAUDE_KEY_STORE: KeyStore = KeyStore::ClaudeApiKey {
349 key: ".anthropic-api-key",
350 credentials: ".claude/.credentials.json",
351};
352const CLAUDE_ACCOUNT_STORE: KeyStore = KeyStore::ClaudeAccount {
353 credentials: ".claude/.credentials.json",
354 config: ".claude.json",
355};
356
357const CLAUDE_BASE: AdapterDescriptor = AdapterDescriptor {
358 name: "claude",
359 family: "claude",
360 auth: AgentAuth::None,
361 product: "Claude Code",
362 offers: "Anthropic's coding agent; it reads, edits, and runs code in a project and can search and fetch the web",
363 command: "claude",
364 args: &["-p"],
365 prompt_args: &[],
366 model_args: &["--model", "{model}"],
367 effort_args: &["--effort", "{effort}"],
368 model_hint: "Claude model alias or ID, such as sonnet or opus.",
369 home_environment: &[],
370 fixed_environment: &[],
371 removed_environment: &[
372 "ANTHROPIC_API_KEY",
373 "ANTHROPIC_BASE_URL",
374 "ANTHROPIC_AUTH_TOKEN",
375 "CLAUDE_CODE_OAUTH_TOKEN",
376 "CLAUDE_CONFIG_DIR",
377 ],
378 full_permission_args: &["--permission-mode", "bypassPermissions"],
379 full_permission_environment: &[],
380 search_dirs: &[],
381 login: Login::Command(&[]),
382 status: Status::Stored(CLAUDE_KEY_STORE),
383 status_summary: StatusSummary::ExitStatus,
384 logout: Logout::Command(&[]),
385 output: OutputFormat::ClaudeStreamJson,
386 resume: Resume::Supported {
387 start: &["--session-id", "{session}"],
388 subcommand: &[],
389 options: &["--resume", "{session}"],
390 positional: &[],
391 },
392 conversation_files: Some(ConversationFiles {
393 dir: ".claude/projects",
394 extension: "jsonl",
395 }),
396 credential_files: &[".anthropic-api-key", ".claude/.credentials.json"],
397 transport: Transport::Process,
398 acp: Some(AcpLaunch {
399 command: "claude-agent-acp",
400 args: &[],
401 full_args: &[],
402 full_mode: Some("bypassPermissions"),
403 full_environment: &[],
404 session_options: false,
405 }),
406 cli_turns: true,
407};
408
409pub const ADAPTERS: &[AdapterDescriptor] = &[
410 AdapterDescriptor {
411 name: "claude",
412 family: "claude",
413 auth: AgentAuth::ClaudeApiKey,
414 product: "Claude Code (API key)",
415 offers: "Anthropic's coding agent; it reads, edits, and runs code in a project and can search and fetch the web; billed per token to the Anthropic API key SCV holds for it",
416 login: Login::ApiKey(CLAUDE_KEY_STORE),
417 status: Status::Stored(CLAUDE_KEY_STORE),
418 logout: Logout::Stored(CLAUDE_KEY_STORE),
419 acp: Some(AcpLaunch {
420 command: "claude-agent-acp",
421 args: &["--hide-claude-auth"],
422 full_args: &[],
423 full_mode: Some("bypassPermissions"),
424 full_environment: &[],
425 session_options: false,
426 }),
427 ..CLAUDE_BASE
428 },
429 AdapterDescriptor {
430 name: "claude-ai",
431 family: "claude",
432 auth: AgentAuth::ClaudeSubscription,
433 product: "Claude Code (Claude subscription)",
434 offers: "Anthropic's coding agent; it reads, edits, and runs code in a project and can search and fetch the web; runs on the owner's Claude subscription",
435 login: Login::Command(&["auth", "login", "--claudeai"]),
436 status: Status::Stored(CLAUDE_ACCOUNT_STORE),
437 logout: Logout::Command(&["auth", "logout"]),
438 credential_files: &[".claude/.credentials.json"],
439 acp: Some(AcpLaunch {
440 command: "claude-agent-acp",
441 args: &[],
442 full_args: &[],
443 full_mode: Some("bypassPermissions"),
444 full_environment: &[],
445 session_options: false,
446 }),
447 ..CLAUDE_BASE
448 },
449 AdapterDescriptor {
450 name: "codex",
451 family: "codex",
452 auth: AgentAuth::None,
453 product: "Codex",
454 offers: "OpenAI's coding agent; it reads, edits, and runs code in a project, with live web search under full permissions",
455 command: "codex",
456 args: &["exec"],
457 prompt_args: &[],
458 model_args: &["-m", "{model}"],
459 effort_args: &["-c", "model_reasoning_effort=\"{effort}\""],
460 model_hint: "OpenAI model ID from the Codex configuration; not a Claude alias.",
461 home_environment: &[("CODEX_HOME", "")],
462 fixed_environment: &[],
463 removed_environment: &[
464 "OPENAI_API_KEY",
465 "OPENAI_BASE_URL",
466 "OPENAI_ORG_ID",
467 "OPENAI_PROJECT_ID",
468 "CODEX_API_KEY",
469 "CODEX_BASE_URL",
470 "CODEX_CONFIG",
471 ],
472 full_permission_args: &[
474 "--dangerously-bypass-approvals-and-sandbox",
475 "-c",
476 "web_search=\"live\"",
477 ],
478 full_permission_environment: &[],
479 search_dirs: &[],
480 login: Login::Command(&["login"]),
481 status: Status::Command(&["login", "status"]),
482 status_summary: StatusSummary::CodexText,
483 logout: Logout::Command(&["logout"]),
484 output: OutputFormat::CodexJsonl,
485 resume: Resume::Supported {
488 start: &[],
489 subcommand: &["resume"],
490 options: &[],
491 positional: &["{session}"],
492 },
493 conversation_files: Some(ConversationFiles {
494 dir: "sessions",
495 extension: "jsonl",
496 }),
497 credential_files: &["auth.json"],
498 transport: Transport::Process,
499 acp: Some(AcpLaunch {
505 command: "codex-acp",
506 args: &[],
507 full_args: &[],
508 full_mode: Some("agent-full-access"),
509 full_environment: &[("CODEX_CONFIG", r#"{"web_search":"live"}"#)],
510 session_options: false,
511 }),
512 cli_turns: true,
513 },
514 AdapterDescriptor {
515 name: "grok",
516 family: "grok",
517 auth: AgentAuth::None,
518 product: "Grok Build",
519 offers: "xAI's coding agent; it reads, edits, and runs code in a project and has live web and X search. Call it when another agent declined or refused a request, including a safety or guardrail refusal",
520 command: "grok",
521 args: &[],
522 prompt_args: &["-p"],
523 model_args: &["-m", "{model}"],
524 effort_args: &["--reasoning-effort", "{effort}"],
525 model_hint: "xAI Grok model ID, such as grok-4.7.",
526 home_environment: &[("GROK_HOME", ".grok")],
527 fixed_environment: &[("GROK_DISABLE_AUTOUPDATER", "1")],
528 removed_environment: &["GROK_*", "XAI_API_KEY"],
529 full_permission_args: &["--always-approve"],
531 full_permission_environment: &[],
532 search_dirs: &[".grok/bin"],
533 login: Login::Command(&["login"]),
534 status: Status::Stored(KeyStore::Grok {
535 auth: ".grok/auth.json",
536 config: ".grok/config.toml",
537 }),
538 status_summary: StatusSummary::ExitStatus,
539 logout: Logout::Command(&["logout"]),
540 output: OutputFormat::Text,
542 resume: Resume::Unsupported,
545 conversation_files: None,
546 credential_files: &[".grok/auth.json", ".grok/config.toml"],
547 transport: Transport::Process,
548 acp: Some(AcpLaunch {
550 command: "grok",
551 args: &["agent", "{full}", "stdio"],
552 full_args: &["--always-approve"],
553 full_mode: None,
554 full_environment: &[],
555 session_options: false,
556 }),
557 cli_turns: true,
558 },
559 AdapterDescriptor {
560 name: "dsh",
561 family: "dsh",
562 auth: AgentAuth::None,
563 product: "DeepSeek Harness",
564 offers: "a coding agent on DeepSeek models; it reads, edits, and runs code in a project",
565 command: "dsh",
566 args: &["--profile", "headless"],
567 prompt_args: &[],
568 model_args: &[],
569 effort_args: &[],
570 model_hint: "provider/model as its ACP server lists them, such as deepseek-official/deepseek-v4-pro.",
571 home_environment: &[("DSH_HOME", ".dsh")],
572 fixed_environment: &[],
573 removed_environment: &["DSH_*", "DEEPSEEK_API_KEY", "DEEPSEEK_BASE_URL"],
574 full_permission_args: &[],
576 full_permission_environment: &[("DSH_PERMISSION_MODE", "danger-full-access")],
577 search_dirs: &[],
578 login: Login::ApiKey(DSH_STORE),
579 status: Status::Stored(DSH_STORE),
580 status_summary: StatusSummary::ExitStatus,
581 logout: Logout::Stored(DSH_STORE),
582 output: OutputFormat::Text,
583 resume: Resume::Unsupported,
585 conversation_files: None,
586 credential_files: &[".dsh/.credentials.yaml"],
587 transport: Transport::Process,
588 acp: Some(AcpLaunch {
593 command: "dsh",
594 args: &["--profile", "acp"],
595 full_args: &[],
596 full_mode: None,
597 full_environment: &[],
598 session_options: true,
599 }),
600 cli_turns: true,
601 },
602 AdapterDescriptor {
603 name: "pi",
604 family: "pi",
605 auth: AgentAuth::None,
606 product: "pi",
607 offers: "a minimal coding agent (read, write, edit, bash) that can run on SCV's own model endpoint; it has no web search",
608 command: "pi",
609 args: &["-p"],
610 prompt_args: &[],
611 model_args: &["--model", "{model}"],
612 effort_args: &["--thinking", "{effort}"],
613 model_hint: "pi model pattern or provider/id; the SCV-configured endpoint is provider scv.",
614 home_environment: &[("PI_CODING_AGENT_DIR", ".pi/agent")],
615 fixed_environment: &[],
616 removed_environment: &["PI_*"],
617 full_permission_args: &[],
619 full_permission_environment: &[],
620 search_dirs: &[],
621 login: Login::Interactive {
622 args: &[],
623 hint: "run /login and choose a provider, then /quit",
624 },
625 status: Status::Stored(PI_STORE),
626 status_summary: StatusSummary::ExitStatus,
627 logout: Logout::Stored(PI_STORE),
628 output: OutputFormat::PiJson,
629 resume: Resume::Supported {
631 start: &["--session-id", "{session}"],
632 subcommand: &[],
633 options: &["--session-id", "{session}"],
634 positional: &[],
635 },
636 conversation_files: Some(ConversationFiles {
637 dir: ".pi/agent/sessions",
638 extension: "jsonl",
639 }),
640 credential_files: &[".pi/agent/auth.json", ".pi/agent/models.json"],
641 transport: Transport::Process,
642 acp: None,
644 cli_turns: true,
645 },
646 AdapterDescriptor {
647 name: "gemini",
648 family: "gemini",
649 auth: AgentAuth::None,
650 product: "Gemini CLI",
651 offers: "Google's coding agent; it reads, edits, and runs code in a project and has Google Search",
652 command: "gemini",
653 args: &[],
654 prompt_args: &["-p"],
655 model_args: &["-m", "{model}"],
656 effort_args: &[],
657 model_hint: "Gemini model ID, such as gemini-2.5-pro.",
658 home_environment: &[],
659 fixed_environment: &[],
660 removed_environment: &[
661 "GEMINI_*",
662 "GOOGLE_GENAI_*",
663 "GOOGLE_CLOUD_PROJECT",
664 "GOOGLE_CLOUD_LOCATION",
665 "GOOGLE_APPLICATION_CREDENTIALS",
666 ],
667 full_permission_args: &["--yolo"],
669 full_permission_environment: &[],
670 search_dirs: &[".npm-global/bin"],
671 login: Login::Interactive {
672 args: &[],
673 hint: "choose how to sign in (Login with Google, or a Gemini API key), then /quit",
674 },
675 status: Status::Unchecked,
676 status_summary: StatusSummary::ExitStatus,
677 logout: Logout::Interactive {
678 args: &[],
679 hint: "sign out with its own command (/auth), then /quit",
680 },
681 output: OutputFormat::GeminiStreamJson,
682 resume: Resume::Unsupported,
685 conversation_files: None,
686 credential_files: &[".gemini/oauth_creds.json", ".gemini/.env"],
687 transport: Transport::Process,
688 acp: Some(AcpLaunch {
690 command: "gemini",
691 args: &["{full}", "--experimental-acp"],
692 full_args: &["--yolo"],
693 full_mode: None,
694 full_environment: &[],
695 session_options: false,
696 }),
697 cli_turns: true,
698 },
699 AdapterDescriptor {
700 name: "qwen",
701 family: "qwen",
702 auth: AgentAuth::None,
703 product: "Qwen Code",
704 offers: "Alibaba's coding agent on Qwen models; it reads, edits, and runs code in a project",
705 command: "qwen",
706 args: &[],
707 prompt_args: &["-p"],
708 model_args: &["-m", "{model}"],
709 effort_args: &[],
710 model_hint: "Qwen model ID, such as qwen3-coder-plus.",
711 home_environment: &[],
712 fixed_environment: &[],
713 removed_environment: &["QWEN_*", "DASHSCOPE_*"],
714 full_permission_args: &["--yolo"],
715 full_permission_environment: &[],
716 search_dirs: &[".npm-global/bin"],
717 login: Login::Interactive {
718 args: &[],
719 hint: "choose how to sign in (Qwen OAuth, or an OpenAI-compatible key), then /quit",
720 },
721 status: Status::Unchecked,
722 status_summary: StatusSummary::ExitStatus,
723 logout: Logout::Interactive {
724 args: &[],
725 hint: "sign out with its own command (/auth), then /quit",
726 },
727 output: OutputFormat::Text,
729 resume: Resume::Unsupported,
730 conversation_files: None,
731 credential_files: &[".qwen/oauth_creds.json", ".qwen/.env"],
732 transport: Transport::Process,
733 acp: Some(AcpLaunch {
735 command: "qwen",
736 args: &["{full}", "--experimental-acp"],
737 full_args: &["--yolo"],
738 full_mode: None,
739 full_environment: &[],
740 session_options: false,
741 }),
742 cli_turns: true,
743 },
744 AdapterDescriptor {
745 name: "opencode",
746 family: "opencode",
747 auth: AgentAuth::None,
748 product: "OpenCode",
749 offers: "an open-source coding agent that runs on many model providers; it reads, edits, and runs code in a project",
750 command: "opencode",
751 args: &["run"],
752 prompt_args: &[],
753 model_args: &["-m", "{model}"],
754 effort_args: &[],
755 model_hint: "provider/model as OpenCode names it, such as anthropic/claude-sonnet-4-5.",
756 home_environment: &[],
757 fixed_environment: &[],
758 removed_environment: &["OPENCODE_*"],
759 full_permission_args: &[],
761 full_permission_environment: &[],
762 search_dirs: &[".opencode/bin", ".npm-global/bin"],
763 login: Login::Command(&["auth", "login"]),
764 status: Status::Unchecked,
765 status_summary: StatusSummary::ExitStatus,
766 logout: Logout::Command(&["auth", "logout"]),
767 output: OutputFormat::Text,
768 resume: Resume::Unsupported,
769 conversation_files: None,
770 credential_files: &["data/opencode/auth.json"],
771 transport: Transport::Process,
772 acp: Some(AcpLaunch {
774 command: "opencode",
775 args: &["acp"],
776 full_args: &[],
777 full_mode: None,
778 full_environment: &[],
779 session_options: false,
780 }),
781 cli_turns: true,
782 },
783 AdapterDescriptor {
784 name: "copilot",
785 family: "copilot",
786 auth: AgentAuth::None,
787 product: "GitHub Copilot CLI",
788 offers: "GitHub's coding agent; it reads, edits, and runs code in a project and knows GitHub",
789 command: "copilot",
790 args: &[],
791 prompt_args: &["-p"],
792 model_args: &["--model", "{model}"],
793 effort_args: &[],
794 model_hint: "a model Copilot CLI offers, such as claude-sonnet-4.5 or gpt-5.",
795 home_environment: &[],
796 fixed_environment: &[],
797 removed_environment: &["COPILOT_*"],
798 full_permission_args: &["--allow-all-tools"],
800 full_permission_environment: &[],
801 search_dirs: &[".npm-global/bin"],
802 login: Login::Interactive {
803 args: &[],
804 hint: "run /login, then /exit",
805 },
806 status: Status::Unchecked,
807 status_summary: StatusSummary::ExitStatus,
808 logout: Logout::Interactive {
809 args: &[],
810 hint: "run /logout, then /exit",
811 },
812 output: OutputFormat::Text,
813 resume: Resume::Unsupported,
815 conversation_files: None,
816 credential_files: &[],
817 transport: Transport::Process,
818 acp: None,
820 cli_turns: true,
821 },
822 AdapterDescriptor {
823 name: "cursor",
824 family: "cursor",
825 auth: AgentAuth::None,
826 product: "Cursor CLI",
827 offers: "Cursor's coding agent; it reads, edits, and runs code in a project",
828 command: "cursor-agent",
829 args: &["-p"],
830 prompt_args: &[],
831 model_args: &["--model", "{model}"],
832 effort_args: &[],
833 model_hint: "a model Cursor offers, such as sonnet-4.5 or gpt-5.",
834 home_environment: &[],
835 fixed_environment: &[],
836 removed_environment: &["CURSOR_*"],
837 full_permission_args: &["--force"],
839 full_permission_environment: &[],
840 search_dirs: &[],
841 login: Login::Command(&["login"]),
842 status: Status::Unchecked,
843 status_summary: StatusSummary::ExitStatus,
844 logout: Logout::Command(&["logout"]),
845 output: OutputFormat::CursorStreamJson,
846 resume: Resume::Supported {
848 start: &[],
849 subcommand: &[],
850 options: &["--resume", "{session}"],
851 positional: &[],
852 },
853 conversation_files: None,
854 credential_files: &[],
855 transport: Transport::Process,
856 acp: None,
857 cli_turns: true,
858 },
859 AdapterDescriptor {
860 name: "amp",
861 family: "amp",
862 auth: AgentAuth::None,
863 product: "Amp",
864 offers: "Sourcegraph's coding agent; it reads, edits, and runs code in a project and can search the web",
865 command: "amp",
866 args: &[],
867 prompt_args: &["-x"],
868 model_args: &[],
870 effort_args: &[],
871 model_hint: "Amp chooses its own model; omit model.",
872 home_environment: &[],
873 fixed_environment: &[],
874 removed_environment: &["AMP_*"],
875 full_permission_args: &["--dangerously-allow-all"],
876 full_permission_environment: &[],
877 search_dirs: &[".amp/bin", ".npm-global/bin"],
878 login: Login::Command(&["login"]),
879 status: Status::Unchecked,
880 status_summary: StatusSummary::ExitStatus,
881 logout: Logout::Command(&["logout"]),
882 output: OutputFormat::AmpStreamJson,
883 resume: Resume::Unsupported,
885 conversation_files: None,
886 credential_files: &[],
887 transport: Transport::Process,
888 acp: None,
889 cli_turns: true,
890 },
891 AdapterDescriptor {
892 name: "kimi",
893 family: "kimi",
894 auth: AgentAuth::None,
895 product: "Kimi CLI",
896 offers: "Moonshot AI's coding agent on Kimi models; it reads, edits, and runs code in a project",
897 command: "kimi",
898 args: &[],
899 prompt_args: &[],
900 model_args: &[],
901 effort_args: &[],
902 model_hint: "a model its ACP server lists.",
903 home_environment: &[],
904 fixed_environment: &[],
905 removed_environment: &["KIMI_*", "MOONSHOT_*"],
906 full_permission_args: &[],
907 full_permission_environment: &[],
908 search_dirs: &[],
909 login: Login::Interactive {
910 args: &[],
911 hint: "sign in with its own command (/setup or /login), then /exit",
912 },
913 status: Status::Unchecked,
914 status_summary: StatusSummary::ExitStatus,
915 logout: Logout::Interactive {
916 args: &[],
917 hint: "sign out with its own command, then /exit",
918 },
919 output: OutputFormat::Text,
920 resume: Resume::Unsupported,
921 conversation_files: None,
922 credential_files: &[],
923 transport: Transport::Process,
924 acp: Some(AcpLaunch {
927 command: "kimi",
928 args: &["--acp"],
929 full_args: &[],
930 full_mode: None,
931 full_environment: &[],
932 session_options: true,
933 }),
934 cli_turns: false,
935 },
936 AdapterDescriptor {
937 name: "scv",
938 family: "scv",
939 auth: AgentAuth::None,
940 product: "SCV",
941 offers: "a nested SCV session with its own context and tools; suited to a self-contained sub-task kept out of this conversation's context, or work in another project",
942 command: "scv",
943 args: &["server", "--stdio"],
944 prompt_args: &[],
945 model_args: &[],
947 effort_args: &[],
948 model_hint: "Model ID for the nested SCV's provider; applies to a new conversation only.",
949 home_environment: &[],
952 fixed_environment: &[],
953 removed_environment: &[],
954 full_permission_args: &[],
956 full_permission_environment: &[],
957 search_dirs: &[".cargo/bin"],
959 login: Login::Import,
960 status: Status::Stored(SCV_STORE),
961 status_summary: StatusSummary::ExitStatus,
962 logout: Logout::Stored(SCV_STORE),
963 output: OutputFormat::Text,
964 resume: Resume::Unsupported,
965 conversation_files: None,
966 credential_files: &["config.toml"],
967 transport: Transport::ScvProtocol,
968 acp: None,
969 cli_turns: true,
970 },
971];
972
973pub fn adapter(name: &str) -> Option<&'static AdapterDescriptor> {
975 ADAPTERS.iter().find(|adapter| adapter.name == name)
976}
977
978pub fn is_removed_agent_variable(variable: &OsStr) -> bool {
982 let Some(variable) = variable.to_str() else {
983 return false;
984 };
985 variable.ends_with("_API_KEY")
986 || COMMON_REMOVED_ENVIRONMENT.contains(&variable)
987 || ADAPTERS
988 .iter()
989 .flat_map(|adapter| adapter.removed_environment)
990 .any(|rule| match rule.strip_suffix('*') {
991 Some(prefix) => variable.starts_with(prefix),
992 None => variable == *rule,
993 })
994}
995
996pub fn summarize_status(summary: StatusSummary, succeeded: bool, output: &str) -> String {
999 let signed_out = "not signed in".to_owned();
1000 match summary {
1001 StatusSummary::CodexText => {
1002 let lower = output.to_ascii_lowercase();
1003 if !succeeded || lower.contains("not logged in") {
1004 signed_out
1005 } else if lower.contains("api key") {
1006 "signed in (API key)".into()
1007 } else if lower.contains("chatgpt") {
1008 "signed in (ChatGPT account)".into()
1009 } else {
1010 "signed in".into()
1011 }
1012 }
1013 StatusSummary::ExitStatus => {
1014 if succeeded {
1015 "signed in".into()
1016 } else {
1017 signed_out
1018 }
1019 }
1020 }
1021}
1022
1023pub fn resolve_agent_executable(command: &str, search_dirs: &[PathBuf]) -> Option<PathBuf> {
1026 if command.contains('/') {
1027 let path = Path::new(command);
1028 return path.is_file().then(|| path.to_path_buf());
1029 }
1030 std::env::join_paths(search_dirs)
1031 .ok()
1032 .and_then(|dirs| {
1033 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
1034 which::which_in(command, Some(dirs), cwd).ok()
1035 })
1036 .or_else(|| which::which(command).ok())
1037}
1038
1039pub fn adapter_search_dirs(adapter: &AdapterDescriptor, home: &Path) -> Vec<PathBuf> {
1041 adapter
1042 .search_dirs
1043 .iter()
1044 .chain(USER_BIN_DIRS)
1045 .map(|dir| home.join(dir))
1046 .collect()
1047}
1048
1049#[cfg(test)]
1050mod tests;