agent-cli 0.70.5__py3-none-any.whl

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 (196) hide show
  1. agent_cli/__init__.py +5 -0
  2. agent_cli/__main__.py +6 -0
  3. agent_cli/_extras.json +14 -0
  4. agent_cli/_requirements/.gitkeep +0 -0
  5. agent_cli/_requirements/audio.txt +79 -0
  6. agent_cli/_requirements/faster-whisper.txt +215 -0
  7. agent_cli/_requirements/kokoro.txt +425 -0
  8. agent_cli/_requirements/llm.txt +183 -0
  9. agent_cli/_requirements/memory.txt +355 -0
  10. agent_cli/_requirements/mlx-whisper.txt +222 -0
  11. agent_cli/_requirements/piper.txt +176 -0
  12. agent_cli/_requirements/rag.txt +402 -0
  13. agent_cli/_requirements/server.txt +154 -0
  14. agent_cli/_requirements/speed.txt +77 -0
  15. agent_cli/_requirements/vad.txt +155 -0
  16. agent_cli/_requirements/wyoming.txt +71 -0
  17. agent_cli/_tools.py +368 -0
  18. agent_cli/agents/__init__.py +23 -0
  19. agent_cli/agents/_voice_agent_common.py +136 -0
  20. agent_cli/agents/assistant.py +383 -0
  21. agent_cli/agents/autocorrect.py +284 -0
  22. agent_cli/agents/chat.py +496 -0
  23. agent_cli/agents/memory/__init__.py +31 -0
  24. agent_cli/agents/memory/add.py +190 -0
  25. agent_cli/agents/memory/proxy.py +160 -0
  26. agent_cli/agents/rag_proxy.py +128 -0
  27. agent_cli/agents/speak.py +209 -0
  28. agent_cli/agents/transcribe.py +671 -0
  29. agent_cli/agents/transcribe_daemon.py +499 -0
  30. agent_cli/agents/voice_edit.py +291 -0
  31. agent_cli/api.py +22 -0
  32. agent_cli/cli.py +106 -0
  33. agent_cli/config.py +503 -0
  34. agent_cli/config_cmd.py +307 -0
  35. agent_cli/constants.py +27 -0
  36. agent_cli/core/__init__.py +1 -0
  37. agent_cli/core/audio.py +461 -0
  38. agent_cli/core/audio_format.py +299 -0
  39. agent_cli/core/chroma.py +88 -0
  40. agent_cli/core/deps.py +191 -0
  41. agent_cli/core/openai_proxy.py +139 -0
  42. agent_cli/core/process.py +195 -0
  43. agent_cli/core/reranker.py +120 -0
  44. agent_cli/core/sse.py +87 -0
  45. agent_cli/core/transcription_logger.py +70 -0
  46. agent_cli/core/utils.py +526 -0
  47. agent_cli/core/vad.py +175 -0
  48. agent_cli/core/watch.py +65 -0
  49. agent_cli/dev/__init__.py +14 -0
  50. agent_cli/dev/cli.py +1588 -0
  51. agent_cli/dev/coding_agents/__init__.py +19 -0
  52. agent_cli/dev/coding_agents/aider.py +24 -0
  53. agent_cli/dev/coding_agents/base.py +167 -0
  54. agent_cli/dev/coding_agents/claude.py +39 -0
  55. agent_cli/dev/coding_agents/codex.py +24 -0
  56. agent_cli/dev/coding_agents/continue_dev.py +15 -0
  57. agent_cli/dev/coding_agents/copilot.py +24 -0
  58. agent_cli/dev/coding_agents/cursor_agent.py +48 -0
  59. agent_cli/dev/coding_agents/gemini.py +28 -0
  60. agent_cli/dev/coding_agents/opencode.py +15 -0
  61. agent_cli/dev/coding_agents/registry.py +49 -0
  62. agent_cli/dev/editors/__init__.py +19 -0
  63. agent_cli/dev/editors/base.py +89 -0
  64. agent_cli/dev/editors/cursor.py +15 -0
  65. agent_cli/dev/editors/emacs.py +46 -0
  66. agent_cli/dev/editors/jetbrains.py +56 -0
  67. agent_cli/dev/editors/nano.py +31 -0
  68. agent_cli/dev/editors/neovim.py +33 -0
  69. agent_cli/dev/editors/registry.py +59 -0
  70. agent_cli/dev/editors/sublime.py +20 -0
  71. agent_cli/dev/editors/vim.py +42 -0
  72. agent_cli/dev/editors/vscode.py +15 -0
  73. agent_cli/dev/editors/zed.py +20 -0
  74. agent_cli/dev/project.py +568 -0
  75. agent_cli/dev/registry.py +52 -0
  76. agent_cli/dev/skill/SKILL.md +141 -0
  77. agent_cli/dev/skill/examples.md +571 -0
  78. agent_cli/dev/terminals/__init__.py +19 -0
  79. agent_cli/dev/terminals/apple_terminal.py +82 -0
  80. agent_cli/dev/terminals/base.py +56 -0
  81. agent_cli/dev/terminals/gnome.py +51 -0
  82. agent_cli/dev/terminals/iterm2.py +84 -0
  83. agent_cli/dev/terminals/kitty.py +77 -0
  84. agent_cli/dev/terminals/registry.py +48 -0
  85. agent_cli/dev/terminals/tmux.py +58 -0
  86. agent_cli/dev/terminals/warp.py +132 -0
  87. agent_cli/dev/terminals/zellij.py +78 -0
  88. agent_cli/dev/worktree.py +856 -0
  89. agent_cli/docs_gen.py +417 -0
  90. agent_cli/example-config.toml +185 -0
  91. agent_cli/install/__init__.py +5 -0
  92. agent_cli/install/common.py +89 -0
  93. agent_cli/install/extras.py +174 -0
  94. agent_cli/install/hotkeys.py +48 -0
  95. agent_cli/install/services.py +87 -0
  96. agent_cli/memory/__init__.py +7 -0
  97. agent_cli/memory/_files.py +250 -0
  98. agent_cli/memory/_filters.py +63 -0
  99. agent_cli/memory/_git.py +157 -0
  100. agent_cli/memory/_indexer.py +142 -0
  101. agent_cli/memory/_ingest.py +408 -0
  102. agent_cli/memory/_persistence.py +182 -0
  103. agent_cli/memory/_prompt.py +91 -0
  104. agent_cli/memory/_retrieval.py +294 -0
  105. agent_cli/memory/_store.py +169 -0
  106. agent_cli/memory/_streaming.py +44 -0
  107. agent_cli/memory/_tasks.py +48 -0
  108. agent_cli/memory/api.py +113 -0
  109. agent_cli/memory/client.py +272 -0
  110. agent_cli/memory/engine.py +361 -0
  111. agent_cli/memory/entities.py +43 -0
  112. agent_cli/memory/models.py +112 -0
  113. agent_cli/opts.py +433 -0
  114. agent_cli/py.typed +0 -0
  115. agent_cli/rag/__init__.py +3 -0
  116. agent_cli/rag/_indexer.py +67 -0
  117. agent_cli/rag/_indexing.py +226 -0
  118. agent_cli/rag/_prompt.py +30 -0
  119. agent_cli/rag/_retriever.py +156 -0
  120. agent_cli/rag/_store.py +48 -0
  121. agent_cli/rag/_utils.py +218 -0
  122. agent_cli/rag/api.py +175 -0
  123. agent_cli/rag/client.py +299 -0
  124. agent_cli/rag/engine.py +302 -0
  125. agent_cli/rag/models.py +55 -0
  126. agent_cli/scripts/.runtime/.gitkeep +0 -0
  127. agent_cli/scripts/__init__.py +1 -0
  128. agent_cli/scripts/check_plugin_skill_sync.py +50 -0
  129. agent_cli/scripts/linux-hotkeys/README.md +63 -0
  130. agent_cli/scripts/linux-hotkeys/toggle-autocorrect.sh +45 -0
  131. agent_cli/scripts/linux-hotkeys/toggle-transcription.sh +58 -0
  132. agent_cli/scripts/linux-hotkeys/toggle-voice-edit.sh +58 -0
  133. agent_cli/scripts/macos-hotkeys/README.md +45 -0
  134. agent_cli/scripts/macos-hotkeys/skhd-config-example +5 -0
  135. agent_cli/scripts/macos-hotkeys/toggle-autocorrect.sh +12 -0
  136. agent_cli/scripts/macos-hotkeys/toggle-transcription.sh +37 -0
  137. agent_cli/scripts/macos-hotkeys/toggle-voice-edit.sh +37 -0
  138. agent_cli/scripts/nvidia-asr-server/README.md +99 -0
  139. agent_cli/scripts/nvidia-asr-server/pyproject.toml +27 -0
  140. agent_cli/scripts/nvidia-asr-server/server.py +255 -0
  141. agent_cli/scripts/nvidia-asr-server/shell.nix +32 -0
  142. agent_cli/scripts/nvidia-asr-server/uv.lock +4654 -0
  143. agent_cli/scripts/run-openwakeword.sh +11 -0
  144. agent_cli/scripts/run-piper-windows.ps1 +30 -0
  145. agent_cli/scripts/run-piper.sh +24 -0
  146. agent_cli/scripts/run-whisper-linux.sh +40 -0
  147. agent_cli/scripts/run-whisper-macos.sh +6 -0
  148. agent_cli/scripts/run-whisper-windows.ps1 +51 -0
  149. agent_cli/scripts/run-whisper.sh +9 -0
  150. agent_cli/scripts/run_faster_whisper_server.py +136 -0
  151. agent_cli/scripts/setup-linux-hotkeys.sh +72 -0
  152. agent_cli/scripts/setup-linux.sh +108 -0
  153. agent_cli/scripts/setup-macos-hotkeys.sh +61 -0
  154. agent_cli/scripts/setup-macos.sh +76 -0
  155. agent_cli/scripts/setup-windows.ps1 +63 -0
  156. agent_cli/scripts/start-all-services-windows.ps1 +53 -0
  157. agent_cli/scripts/start-all-services.sh +178 -0
  158. agent_cli/scripts/sync_extras.py +138 -0
  159. agent_cli/server/__init__.py +3 -0
  160. agent_cli/server/cli.py +721 -0
  161. agent_cli/server/common.py +222 -0
  162. agent_cli/server/model_manager.py +288 -0
  163. agent_cli/server/model_registry.py +225 -0
  164. agent_cli/server/proxy/__init__.py +3 -0
  165. agent_cli/server/proxy/api.py +444 -0
  166. agent_cli/server/streaming.py +67 -0
  167. agent_cli/server/tts/__init__.py +3 -0
  168. agent_cli/server/tts/api.py +335 -0
  169. agent_cli/server/tts/backends/__init__.py +82 -0
  170. agent_cli/server/tts/backends/base.py +139 -0
  171. agent_cli/server/tts/backends/kokoro.py +403 -0
  172. agent_cli/server/tts/backends/piper.py +253 -0
  173. agent_cli/server/tts/model_manager.py +201 -0
  174. agent_cli/server/tts/model_registry.py +28 -0
  175. agent_cli/server/tts/wyoming_handler.py +249 -0
  176. agent_cli/server/whisper/__init__.py +3 -0
  177. agent_cli/server/whisper/api.py +413 -0
  178. agent_cli/server/whisper/backends/__init__.py +89 -0
  179. agent_cli/server/whisper/backends/base.py +97 -0
  180. agent_cli/server/whisper/backends/faster_whisper.py +225 -0
  181. agent_cli/server/whisper/backends/mlx.py +270 -0
  182. agent_cli/server/whisper/languages.py +116 -0
  183. agent_cli/server/whisper/model_manager.py +157 -0
  184. agent_cli/server/whisper/model_registry.py +28 -0
  185. agent_cli/server/whisper/wyoming_handler.py +203 -0
  186. agent_cli/services/__init__.py +343 -0
  187. agent_cli/services/_wyoming_utils.py +64 -0
  188. agent_cli/services/asr.py +506 -0
  189. agent_cli/services/llm.py +228 -0
  190. agent_cli/services/tts.py +450 -0
  191. agent_cli/services/wake_word.py +142 -0
  192. agent_cli-0.70.5.dist-info/METADATA +2118 -0
  193. agent_cli-0.70.5.dist-info/RECORD +196 -0
  194. agent_cli-0.70.5.dist-info/WHEEL +4 -0
  195. agent_cli-0.70.5.dist-info/entry_points.txt +4 -0
  196. agent_cli-0.70.5.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,19 @@
1
+ """AI coding agent adapters for the dev module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .base import CodingAgent
6
+ from .registry import (
7
+ detect_current_agent,
8
+ get_agent,
9
+ get_all_agents,
10
+ get_available_agents,
11
+ )
12
+
13
+ __all__ = [
14
+ "CodingAgent",
15
+ "detect_current_agent",
16
+ "get_agent",
17
+ "get_all_agents",
18
+ "get_available_agents",
19
+ ]
@@ -0,0 +1,24 @@
1
+ """Aider AI coding agent adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .base import CodingAgent
6
+
7
+
8
+ class Aider(CodingAgent):
9
+ """Aider - AI pair programming in your terminal."""
10
+
11
+ name = "aider"
12
+ command = "aider"
13
+ install_url = "https://aider.chat"
14
+ detect_process_name = "aider"
15
+
16
+ def prompt_args(self, prompt: str) -> list[str]:
17
+ """Return prompt using --message flag.
18
+
19
+ Aider uses -m/--message for initial prompts:
20
+ `aider --message "your prompt here"`
21
+
22
+ See: https://aider.chat/docs/scripting.html
23
+ """
24
+ return ["--message", prompt]
@@ -0,0 +1,167 @@
1
+ """Base class for AI coding agent adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ from abc import ABC
8
+ from pathlib import PurePath
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from pathlib import Path
13
+
14
+
15
+ class CodingAgent(ABC):
16
+ """Abstract base class for AI coding agent adapters."""
17
+
18
+ # Display name for the agent
19
+ name: str
20
+
21
+ # CLI command to invoke the agent
22
+ command: str
23
+
24
+ # Alternative command names (for detection)
25
+ alt_commands: tuple[str, ...] = ()
26
+
27
+ # URL for installation instructions
28
+ install_url: str = ""
29
+
30
+ # Declarative detection: env var that indicates running inside this agent
31
+ # e.g., "CLAUDECODE" for Claude Code (checks if env var is set to "1")
32
+ detect_env_var: str | None = None
33
+
34
+ # Declarative detection: process name to look for in parent processes
35
+ # e.g., "aider" will match any parent process containing "aider"
36
+ detect_process_name: str | None = None
37
+
38
+ def detect(self) -> bool:
39
+ """Check if this agent is currently running/active in the environment.
40
+
41
+ Default implementation uses declarative detection attributes.
42
+ Override for custom detection logic.
43
+ """
44
+ # Check env var first (faster) - checks for "1" specifically
45
+ if self.detect_env_var and os.environ.get(self.detect_env_var) == "1":
46
+ return True
47
+
48
+ # Fall back to parent process detection
49
+ if self.detect_process_name:
50
+ parent_names = _get_parent_process_names()
51
+ return any(self.detect_process_name in name for name in parent_names)
52
+
53
+ return False
54
+
55
+ def is_available(self) -> bool:
56
+ """Check if this agent is installed and available."""
57
+ if shutil.which(self.command):
58
+ return True
59
+ return any(shutil.which(cmd) for cmd in self.alt_commands)
60
+
61
+ def get_executable(self) -> str | None:
62
+ """Get the path to the executable."""
63
+ if exe := shutil.which(self.command):
64
+ return exe
65
+ for cmd in self.alt_commands:
66
+ if exe := shutil.which(cmd):
67
+ return exe
68
+ return None
69
+
70
+ def prompt_args(self, prompt: str) -> list[str]:
71
+ """Return the CLI arguments to pass an initial prompt to the agent.
72
+
73
+ Override this method in subclasses for agents that support initial prompts.
74
+ Default implementation returns empty list (prompt not supported).
75
+
76
+ Args:
77
+ prompt: The initial prompt to pass to the agent
78
+
79
+ Returns:
80
+ List of CLI arguments (e.g., ["prompt text"] or ["-m", "prompt text"])
81
+
82
+ """
83
+ del prompt # unused in base implementation
84
+ return []
85
+
86
+ def launch_command(
87
+ self,
88
+ path: Path, # noqa: ARG002
89
+ extra_args: list[str] | None = None,
90
+ prompt: str | None = None,
91
+ ) -> list[str]:
92
+ """Return the command to launch this agent in a directory.
93
+
94
+ Args:
95
+ path: The directory to launch the agent in
96
+ extra_args: Additional arguments to pass to the agent
97
+ prompt: Optional initial prompt to pass to the agent
98
+
99
+ Returns:
100
+ List of command arguments
101
+
102
+ """
103
+ exe = self.get_executable()
104
+ if exe is None:
105
+ msg = f"{self.name} is not installed"
106
+ if self.install_url:
107
+ msg += f". Install from {self.install_url}"
108
+ raise RuntimeError(msg)
109
+ cmd = [exe]
110
+ if extra_args:
111
+ cmd.extend(extra_args)
112
+ if prompt:
113
+ cmd.extend(self.prompt_args(prompt))
114
+ return cmd
115
+
116
+ def get_env(self) -> dict[str, str]:
117
+ """Get any additional environment variables needed."""
118
+ return {}
119
+
120
+ def __repr__(self) -> str: # noqa: D105
121
+ status = "available" if self.is_available() else "not installed"
122
+ return f"<{self.__class__.__name__} {self.name!r} ({status})>"
123
+
124
+
125
+ def _get_parent_process_names() -> list[str]:
126
+ """Get names of parent processes (for detecting current agent).
127
+
128
+ Extracts names from both process.name() and cmdline.
129
+ This handles Node.js CLIs that don't set process.title:
130
+ - process.name() returns 'node' for most Node.js CLI tools
131
+ - cmdline contains the actual script path like '/path/to/cn'
132
+ - CLI tools that set process.title (like Claude) show their name directly
133
+ """
134
+ try:
135
+ import psutil # noqa: PLC0415
136
+
137
+ process = psutil.Process(os.getpid())
138
+ names = []
139
+ for _ in range(10): # Limit depth
140
+ process = process.parent()
141
+ if process is None:
142
+ break
143
+ # Add the process name (works for native binaries and tools that set process.title)
144
+ names.append(process.name().lower())
145
+
146
+ # Also check cmdline for the actual command (handles Node.js/Python CLIs)
147
+ # e.g., cmdline=['node', '/path/to/cn', '--version'] → extract 'cn'
148
+ try:
149
+ cmdline = process.cmdline()
150
+ if len(cmdline) >= 2: # noqa: PLR2004
151
+ # Get the script/command from cmdline[1] (the actual CLI tool)
152
+ cmd_name = PurePath(cmdline[1]).name.lower()
153
+ # Remove common extensions
154
+ for ext in (".js", ".py", ".sh", ".exe"):
155
+ if cmd_name.endswith(ext):
156
+ cmd_name = cmd_name[: -len(ext)]
157
+ break
158
+ if cmd_name and cmd_name not in names:
159
+ names.append(cmd_name)
160
+ except (psutil.AccessDenied, psutil.ZombieProcess, IndexError):
161
+ pass
162
+ return names
163
+ except ImportError:
164
+ # psutil not available, return empty list
165
+ return []
166
+ except Exception:
167
+ return []
@@ -0,0 +1,39 @@
1
+ """Claude Code AI coding agent adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from .base import CodingAgent
9
+
10
+
11
+ class ClaudeCode(CodingAgent):
12
+ """Claude Code (Anthropic's CLI coding agent)."""
13
+
14
+ name = "claude"
15
+ command = "claude"
16
+ alt_commands = ("claude-code",)
17
+ install_url = "https://code.claude.com/docs/en/overview"
18
+ detect_env_var = "CLAUDECODE"
19
+ detect_process_name = "claude"
20
+
21
+ def prompt_args(self, prompt: str) -> list[str]:
22
+ """Return prompt as positional argument.
23
+
24
+ Claude Code accepts prompt as a positional argument:
25
+ `claude "your prompt here"`
26
+
27
+ See: claude --help
28
+ """
29
+ return [prompt]
30
+
31
+ def get_executable(self) -> str | None:
32
+ """Get the Claude executable path."""
33
+ # Check common installation path first
34
+ local_claude = Path.home() / ".claude" / "local" / "claude"
35
+ if local_claude.exists() and os.access(local_claude, os.X_OK):
36
+ return str(local_claude)
37
+
38
+ # Fall back to PATH lookup
39
+ return super().get_executable()
@@ -0,0 +1,24 @@
1
+ """OpenAI Codex CLI coding agent adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .base import CodingAgent
6
+
7
+
8
+ class Codex(CodingAgent):
9
+ """OpenAI Codex CLI coding agent."""
10
+
11
+ name = "codex"
12
+ command = "codex"
13
+ install_url = "https://github.com/openai/codex"
14
+ detect_process_name = "codex"
15
+
16
+ def prompt_args(self, prompt: str) -> list[str]:
17
+ """Return prompt as positional argument.
18
+
19
+ Codex accepts prompt as a positional argument:
20
+ `codex "your prompt here"`
21
+
22
+ See: codex --help
23
+ """
24
+ return [prompt]
@@ -0,0 +1,15 @@
1
+ """Continue Dev CLI coding agent adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .base import CodingAgent
6
+
7
+
8
+ class ContinueDev(CodingAgent):
9
+ """Continue Dev - AI code assistant."""
10
+
11
+ name = "continue"
12
+ command = "cn"
13
+ install_url = "https://continue.dev"
14
+ # Detection via cmdline extraction (cn runs as 'node' but cmdline contains '/path/to/cn')
15
+ detect_process_name = "cn"
@@ -0,0 +1,24 @@
1
+ """GitHub Copilot CLI coding agent adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .base import CodingAgent
6
+
7
+
8
+ class Copilot(CodingAgent):
9
+ """GitHub Copilot CLI coding agent."""
10
+
11
+ name = "copilot"
12
+ command = "copilot"
13
+ install_url = "https://github.com/github/copilot-cli"
14
+ detect_process_name = "copilot"
15
+
16
+ def prompt_args(self, prompt: str) -> list[str]:
17
+ """Return prompt using --prompt flag.
18
+
19
+ Copilot CLI uses -p/--prompt for initial prompts:
20
+ `copilot --prompt "your prompt here"`
21
+
22
+ See: https://github.com/github/copilot-cli
23
+ """
24
+ return ["--prompt", prompt]
@@ -0,0 +1,48 @@
1
+ """Cursor Agent CLI coding agent adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import TYPE_CHECKING
7
+
8
+ from .base import CodingAgent
9
+
10
+ if TYPE_CHECKING:
11
+ from pathlib import Path
12
+
13
+
14
+ class CursorAgent(CodingAgent):
15
+ """Cursor Agent - AI agent mode for Cursor editor."""
16
+
17
+ name = "cursor-agent"
18
+ command = "cursor-agent"
19
+ alt_commands = ("cursor",)
20
+ install_url = "https://cursor.com"
21
+
22
+ def detect(self) -> bool:
23
+ """Detect if running inside Cursor Agent.
24
+
25
+ CURSOR_AGENT uses presence check (not == "1"), so custom detection needed.
26
+ """
27
+ return os.environ.get("CURSOR_AGENT") is not None
28
+
29
+ def launch_command(
30
+ self,
31
+ path: Path, # noqa: ARG002
32
+ extra_args: list[str] | None = None,
33
+ prompt: str | None = None,
34
+ ) -> list[str]:
35
+ """Return the command to launch Cursor Agent."""
36
+ exe = self.get_executable()
37
+ if exe is None:
38
+ msg = f"{self.name} is not installed"
39
+ if self.install_url:
40
+ msg += f". Install from {self.install_url}"
41
+ raise RuntimeError(msg)
42
+ # Try cursor-agent first, fall back to cursor cli
43
+ cmd = [exe] if exe.endswith("cursor-agent") else [exe, "cli"]
44
+ if extra_args:
45
+ cmd.extend(extra_args)
46
+ if prompt:
47
+ cmd.extend(self.prompt_args(prompt))
48
+ return cmd
@@ -0,0 +1,28 @@
1
+ """Google Gemini CLI coding agent adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .base import CodingAgent
6
+
7
+
8
+ class Gemini(CodingAgent):
9
+ """Google Gemini CLI coding agent."""
10
+
11
+ name = "gemini"
12
+ command = "gemini"
13
+ install_url = "https://github.com/google-gemini/gemini-cli"
14
+ detect_process_name = "gemini"
15
+
16
+ def prompt_args(self, prompt: str) -> list[str]:
17
+ """Return prompt using -i/--prompt-interactive flag.
18
+
19
+ Gemini CLI uses -i for interactive mode with initial prompt:
20
+ `gemini -i "your prompt here"`
21
+
22
+ Note: -p/--prompt is non-interactive (exits after response).
23
+
24
+ Evidence: `gemini --help` shows:
25
+ -i, --prompt-interactive Execute the provided prompt and continue
26
+ in interactive mode
27
+ """
28
+ return ["-i", prompt]
@@ -0,0 +1,15 @@
1
+ """OpenCode CLI coding agent adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .base import CodingAgent
6
+
7
+
8
+ class OpenCode(CodingAgent):
9
+ """OpenCode - AI coding assistant."""
10
+
11
+ name = "opencode"
12
+ command = "opencode"
13
+ install_url = "https://opencode.ai"
14
+ detect_env_var = "OPENCODE"
15
+ detect_process_name = "opencode"
@@ -0,0 +1,49 @@
1
+ """Registry for AI coding agent adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from agent_cli.dev.registry import Registry
6
+
7
+ from .aider import Aider
8
+ from .base import CodingAgent # noqa: TC001
9
+ from .claude import ClaudeCode
10
+ from .codex import Codex
11
+ from .continue_dev import ContinueDev
12
+ from .copilot import Copilot
13
+ from .cursor_agent import CursorAgent
14
+ from .gemini import Gemini
15
+ from .opencode import OpenCode
16
+
17
+ # All available coding agents (in priority order for detection)
18
+ _AGENTS: list[type[CodingAgent]] = [
19
+ ClaudeCode,
20
+ Codex,
21
+ Gemini,
22
+ Aider,
23
+ Copilot,
24
+ ContinueDev,
25
+ OpenCode,
26
+ CursorAgent,
27
+ ]
28
+
29
+ _registry: Registry[CodingAgent] = Registry(_AGENTS)
30
+
31
+
32
+ def get_all_agents() -> list[CodingAgent]:
33
+ """Get instances of all registered coding agents."""
34
+ return _registry.get_all()
35
+
36
+
37
+ def get_available_agents() -> list[CodingAgent]:
38
+ """Get all installed/available coding agents."""
39
+ return _registry.get_available()
40
+
41
+
42
+ def detect_current_agent() -> CodingAgent | None:
43
+ """Detect which coding agent we're currently running in."""
44
+ return _registry.detect_current()
45
+
46
+
47
+ def get_agent(name: str) -> CodingAgent | None:
48
+ """Get a coding agent by name."""
49
+ return _registry.get_by_name(name)
@@ -0,0 +1,19 @@
1
+ """Editor adapters for the dev module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .base import Editor
6
+ from .registry import (
7
+ detect_current_editor,
8
+ get_all_editors,
9
+ get_available_editors,
10
+ get_editor,
11
+ )
12
+
13
+ __all__ = [
14
+ "Editor",
15
+ "detect_current_editor",
16
+ "get_all_editors",
17
+ "get_available_editors",
18
+ "get_editor",
19
+ ]
@@ -0,0 +1,89 @@
1
+ """Base class for editor adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ from abc import ABC
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from pathlib import Path
12
+
13
+
14
+ class Editor(ABC):
15
+ """Abstract base class for editor adapters."""
16
+
17
+ # Display name for the editor
18
+ name: str
19
+
20
+ # CLI command to invoke the editor
21
+ command: str
22
+
23
+ # Alternative command names
24
+ alt_commands: tuple[str, ...] = ()
25
+
26
+ # URL for installation instructions
27
+ install_url: str = ""
28
+
29
+ # Declarative detection: env vars that indicate running inside this editor
30
+ # e.g., ("NVIM", "NVIM_LISTEN_ADDRESS") for Neovim
31
+ detect_env_vars: tuple[str, ...] = ()
32
+
33
+ # Declarative detection: value to look for in TERM_PROGRAM
34
+ # e.g., "vscode" will match if TERM_PROGRAM contains "vscode" (case-insensitive)
35
+ detect_term_program: str | None = None
36
+
37
+ def detect(self) -> bool:
38
+ """Check if currently running inside this editor's terminal.
39
+
40
+ Default implementation uses declarative detection attributes.
41
+ Override for custom detection logic.
42
+ """
43
+ # Check env vars first
44
+ for env_var in self.detect_env_vars:
45
+ if os.environ.get(env_var):
46
+ return True
47
+
48
+ # Check TERM_PROGRAM
49
+ if self.detect_term_program:
50
+ term_program = os.environ.get("TERM_PROGRAM")
51
+ if term_program and self.detect_term_program.lower() in term_program.lower():
52
+ return True
53
+
54
+ return False
55
+
56
+ def is_available(self) -> bool:
57
+ """Check if this editor is installed and available."""
58
+ if shutil.which(self.command):
59
+ return True
60
+ return any(shutil.which(cmd) for cmd in self.alt_commands)
61
+
62
+ def get_executable(self) -> str | None:
63
+ """Get the path to the executable."""
64
+ if exe := shutil.which(self.command):
65
+ return exe
66
+ for cmd in self.alt_commands:
67
+ if exe := shutil.which(cmd):
68
+ return exe
69
+ return None
70
+
71
+ def open_command(self, path: Path) -> list[str]:
72
+ """Return the command to open a directory in this editor.
73
+
74
+ Args:
75
+ path: The directory to open
76
+
77
+ Returns:
78
+ List of command arguments
79
+
80
+ """
81
+ exe = self.get_executable()
82
+ if exe is None:
83
+ msg = f"{self.name} is not installed"
84
+ raise RuntimeError(msg)
85
+ return [exe, path.as_posix()]
86
+
87
+ def __repr__(self) -> str: # noqa: D105
88
+ status = "available" if self.is_available() else "not installed"
89
+ return f"<{self.__class__.__name__} {self.name!r} ({status})>"
@@ -0,0 +1,15 @@
1
+ """Cursor editor adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .base import Editor
6
+
7
+
8
+ class Cursor(Editor):
9
+ """Cursor - AI-first code editor."""
10
+
11
+ name = "cursor"
12
+ command = "cursor"
13
+ install_url = "https://cursor.com"
14
+ detect_env_vars = ("CURSOR_AGENT",)
15
+ # No detect_term_program - not verified (Cursor is proprietary)
@@ -0,0 +1,46 @@
1
+ """Emacs editor adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from .base import Editor
8
+
9
+ if TYPE_CHECKING:
10
+ from pathlib import Path
11
+
12
+
13
+ class Emacs(Editor):
14
+ """Emacs - An extensible, customizable text editor.
15
+
16
+ Detection via INSIDE_EMACS only. The legacy EMACS env var is deprecated.
17
+
18
+ Evidence: https://github.com/emacs-mirror/emacs/blob/master/etc/NEWS.25
19
+ Quote: "'M-x shell' and 'M-x compile' no longer set the EMACS environment
20
+ variable. This avoids clashing when other programs use the variable
21
+ for other purposes. [...] Use the INSIDE_EMACS environment variable
22
+ instead."
23
+ """
24
+
25
+ name = "emacs"
26
+ command = "emacs"
27
+ alt_commands = ("emacsclient",)
28
+ install_url = "https://www.gnu.org/software/emacs/"
29
+ detect_env_vars = ("INSIDE_EMACS",) # EMACS is deprecated since Emacs 25
30
+ # No detect_term_program - Emacs doesn't set TERM_PROGRAM
31
+
32
+ def open_command(self, path: Path) -> list[str]:
33
+ """Return the command to open a directory in Emacs.
34
+
35
+ Uses background mode (&) for standalone emacs to match GTR behavior.
36
+ emacsclient uses -n flag which already runs in background.
37
+ """
38
+ exe = self.get_executable()
39
+ if exe is None:
40
+ msg = f"{self.name} is not installed"
41
+ raise RuntimeError(msg)
42
+ # Use emacsclient if available for faster opening (-n = don't wait)
43
+ if "emacsclient" in exe:
44
+ return [exe, "-n", path.as_posix()]
45
+ # Run standalone emacs in background like GTR does
46
+ return ["sh", "-c", f'{exe} "{path.as_posix()}" &']