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
+ """Terminal adapters for the dev module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .base import Terminal
6
+ from .registry import (
7
+ detect_current_terminal,
8
+ get_all_terminals,
9
+ get_available_terminals,
10
+ get_terminal,
11
+ )
12
+
13
+ __all__ = [
14
+ "Terminal",
15
+ "detect_current_terminal",
16
+ "get_all_terminals",
17
+ "get_available_terminals",
18
+ "get_terminal",
19
+ ]
@@ -0,0 +1,82 @@
1
+ """macOS Terminal.app adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from .base import Terminal, _get_term_program
10
+
11
+
12
+ class AppleTerminal(Terminal):
13
+ """macOS Terminal.app - the default macOS terminal."""
14
+
15
+ name = "terminal"
16
+
17
+ def detect(self) -> bool:
18
+ """Detect if running inside Terminal.app."""
19
+ term_program = _get_term_program()
20
+ return term_program == "Apple_Terminal"
21
+
22
+ def is_available(self) -> bool:
23
+ """Check if Terminal.app is available (macOS only)."""
24
+ if sys.platform != "darwin":
25
+ return False
26
+ # Terminal.app is always available on macOS
27
+ return Path("/System/Applications/Utilities/Terminal.app").exists()
28
+
29
+ def open_new_tab(
30
+ self,
31
+ path: Path,
32
+ command: str | None = None,
33
+ tab_name: str | None = None,
34
+ ) -> bool:
35
+ """Open a new tab in Terminal.app using AppleScript.
36
+
37
+ Requires System Events accessibility permissions in
38
+ System Preferences > Privacy & Security > Accessibility.
39
+ """
40
+ if not self.is_available():
41
+ return False
42
+
43
+ def escape_applescript(s: str) -> str:
44
+ """Escape string for AppleScript double-quoted string."""
45
+ return s.replace("\\", "\\\\").replace('"', '\\"')
46
+
47
+ # Build the command to run
48
+ shell_cmd = f'cd "{path}" && {command}' if command else f'cd "{path}"'
49
+ shell_cmd_escaped = escape_applescript(shell_cmd)
50
+
51
+ # Build custom title command if provided
52
+ title_cmd = ""
53
+ if tab_name:
54
+ tab_name_escaped = escape_applescript(tab_name)
55
+ title_cmd = (
56
+ f'\nset custom title of selected tab of front window to "{tab_name_escaped}"'
57
+ )
58
+
59
+ # AppleScript to open new tab in Terminal.app using System Events
60
+ applescript = f"""
61
+ tell application "Terminal"
62
+ activate
63
+ end tell
64
+ tell application "System Events"
65
+ keystroke "t" using command down
66
+ end tell
67
+ delay 0.3
68
+ tell application "Terminal"
69
+ do script "{shell_cmd_escaped}" in selected tab of front window{title_cmd}
70
+ end tell
71
+ """
72
+
73
+ try:
74
+ subprocess.run(
75
+ ["osascript", "-e", applescript], # noqa: S607
76
+ check=True,
77
+ capture_output=True,
78
+ text=True,
79
+ )
80
+ return True
81
+ except subprocess.CalledProcessError:
82
+ return False
@@ -0,0 +1,56 @@
1
+ """Base class for terminal adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from abc import ABC, abstractmethod
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from pathlib import Path
11
+
12
+
13
+ class Terminal(ABC):
14
+ """Abstract base class for terminal adapters."""
15
+
16
+ # Display name for the terminal
17
+ name: str
18
+
19
+ @abstractmethod
20
+ def detect(self) -> bool:
21
+ """Check if currently running inside this terminal.
22
+
23
+ This is used to auto-detect which terminal to use.
24
+ """
25
+
26
+ @abstractmethod
27
+ def is_available(self) -> bool:
28
+ """Check if this terminal is installed and available."""
29
+
30
+ @abstractmethod
31
+ def open_new_tab(
32
+ self,
33
+ path: Path,
34
+ command: str | None = None,
35
+ tab_name: str | None = None,
36
+ ) -> bool:
37
+ """Open a new tab in this terminal.
38
+
39
+ Args:
40
+ path: The directory to open in
41
+ command: Optional command to run in the new tab
42
+ tab_name: Optional name for the new tab
43
+
44
+ Returns:
45
+ True if successful, False otherwise
46
+
47
+ """
48
+
49
+ def __repr__(self) -> str: # noqa: D105
50
+ status = "available" if self.is_available() else "not installed"
51
+ return f"<{self.__class__.__name__} {self.name!r} ({status})>"
52
+
53
+
54
+ def _get_term_program() -> str | None:
55
+ """Get the TERM_PROGRAM environment variable."""
56
+ return os.environ.get("TERM_PROGRAM")
@@ -0,0 +1,51 @@
1
+ """GNOME Terminal adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ from typing import TYPE_CHECKING
9
+
10
+ from .base import Terminal
11
+
12
+ if TYPE_CHECKING:
13
+ from pathlib import Path
14
+
15
+
16
+ class GnomeTerminal(Terminal):
17
+ """GNOME Terminal - Default terminal for GNOME desktop."""
18
+
19
+ name = "gnome-terminal"
20
+
21
+ def detect(self) -> bool:
22
+ """Detect if running inside GNOME Terminal."""
23
+ return os.environ.get("GNOME_TERMINAL_SERVICE") is not None
24
+
25
+ def is_available(self) -> bool:
26
+ """Check if GNOME Terminal is available."""
27
+ return shutil.which("gnome-terminal") is not None
28
+
29
+ def open_new_tab(
30
+ self,
31
+ path: Path,
32
+ command: str | None = None,
33
+ tab_name: str | None = None,
34
+ ) -> bool:
35
+ """Open a new tab in GNOME Terminal."""
36
+ if not self.is_available():
37
+ return False
38
+
39
+ cmd = ["gnome-terminal", "--tab", f"--working-directory={path}"]
40
+
41
+ if tab_name:
42
+ cmd.extend(["--title", tab_name])
43
+
44
+ if command:
45
+ cmd.extend(["--", "bash", "-c", f"{command}; exec bash"])
46
+
47
+ try:
48
+ subprocess.Popen(cmd)
49
+ return True
50
+ except Exception:
51
+ return False
@@ -0,0 +1,84 @@
1
+ """iTerm2 terminal adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from .base import Terminal, _get_term_program
11
+
12
+
13
+ class ITerm2(Terminal):
14
+ """iTerm2 - macOS terminal emulator with advanced features."""
15
+
16
+ name = "iterm2"
17
+
18
+ def detect(self) -> bool:
19
+ """Detect if running inside iTerm2."""
20
+ # Check TERM_PROGRAM
21
+ term_program = _get_term_program()
22
+ if term_program and "iterm" in term_program.lower():
23
+ return True
24
+ # Check iTerm-specific env var
25
+ return os.environ.get("ITERM_SESSION_ID") is not None
26
+
27
+ def is_available(self) -> bool:
28
+ """Check if iTerm2 is available (macOS only)."""
29
+ if sys.platform != "darwin":
30
+ return False
31
+ # Check if iTerm2 app exists
32
+ return Path("/Applications/iTerm.app").exists()
33
+
34
+ def open_new_tab(
35
+ self,
36
+ path: Path,
37
+ command: str | None = None,
38
+ tab_name: str | None = None,
39
+ ) -> bool:
40
+ """Open a new tab in iTerm2 using AppleScript."""
41
+ if not self.is_available():
42
+ return False
43
+
44
+ def escape_applescript(s: str) -> str:
45
+ """Escape string for AppleScript double-quoted string."""
46
+ # Escape backslashes first, then double quotes
47
+ return s.replace("\\", "\\\\").replace('"', '\\"')
48
+
49
+ # Build the command to run in the new tab
50
+ shell_cmd = f'cd "{path}" && {command}' if command else f'cd "{path}"'
51
+ shell_cmd_escaped = escape_applescript(shell_cmd)
52
+
53
+ # Build name setting if provided
54
+ name_cmd = ""
55
+ if tab_name:
56
+ tab_name_escaped = escape_applescript(tab_name)
57
+ name_cmd = f'\nset name to "{tab_name_escaped}"'
58
+
59
+ # AppleScript to open new tab in iTerm2
60
+ # Handle case where no window exists by creating one
61
+ applescript = f"""
62
+ tell application "iTerm2"
63
+ if (count of windows) = 0 then
64
+ create window with default profile
65
+ end if
66
+ tell current window
67
+ create tab with default profile
68
+ tell current session{name_cmd}
69
+ write text "{shell_cmd_escaped}"
70
+ end tell
71
+ end tell
72
+ end tell
73
+ """
74
+
75
+ try:
76
+ subprocess.run(
77
+ ["osascript", "-e", applescript], # noqa: S607
78
+ check=True,
79
+ capture_output=True,
80
+ text=True,
81
+ )
82
+ return True
83
+ except subprocess.CalledProcessError:
84
+ return False
@@ -0,0 +1,77 @@
1
+ """Kitty terminal adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ from typing import TYPE_CHECKING
9
+
10
+ from .base import Terminal
11
+
12
+ if TYPE_CHECKING:
13
+ from pathlib import Path
14
+
15
+
16
+ class Kitty(Terminal):
17
+ """Kitty - GPU-accelerated terminal emulator."""
18
+
19
+ name = "kitty"
20
+
21
+ def detect(self) -> bool:
22
+ """Detect if running inside Kitty."""
23
+ # Check KITTY_WINDOW_ID
24
+ if os.environ.get("KITTY_WINDOW_ID"):
25
+ return True
26
+ # Check TERM
27
+ term = os.environ.get("TERM", "")
28
+ return "kitty" in term.lower()
29
+
30
+ def is_available(self) -> bool:
31
+ """Check if Kitty is available."""
32
+ return shutil.which("kitty") is not None
33
+
34
+ def open_new_tab(
35
+ self,
36
+ path: Path,
37
+ command: str | None = None,
38
+ tab_name: str | None = None,
39
+ ) -> bool:
40
+ """Open a new tab in Kitty using kitten."""
41
+ if not self.is_available():
42
+ return False
43
+
44
+ # Check if we're running inside Kitty
45
+ if not self.detect():
46
+ # Not in Kitty, open a new window instead
47
+ cmd = ["kitty", "--directory", str(path)]
48
+ if tab_name:
49
+ cmd.extend(["--title", tab_name])
50
+ if command:
51
+ cmd.extend(["--", "sh", "-c", command])
52
+ try:
53
+ subprocess.Popen(cmd)
54
+ return True
55
+ except Exception:
56
+ return False
57
+
58
+ # Use kitten @ to control Kitty from inside
59
+ cmd = [
60
+ "kitten",
61
+ "@",
62
+ "launch",
63
+ "--type=tab",
64
+ f"--cwd={path}",
65
+ ]
66
+
67
+ if tab_name:
68
+ cmd.append(f"--tab-title={tab_name}")
69
+
70
+ if command:
71
+ cmd.extend(["--", "sh", "-c", command])
72
+
73
+ try:
74
+ subprocess.run(cmd, check=True, capture_output=True)
75
+ return True
76
+ except subprocess.CalledProcessError:
77
+ return False
@@ -0,0 +1,48 @@
1
+ """Registry for terminal adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from agent_cli.dev.registry import Registry
6
+
7
+ from .apple_terminal import AppleTerminal
8
+ from .base import Terminal # noqa: TC001
9
+ from .gnome import GnomeTerminal
10
+ from .iterm2 import ITerm2
11
+ from .kitty import Kitty
12
+ from .tmux import Tmux
13
+ from .warp import Warp
14
+ from .zellij import Zellij
15
+
16
+ # All available terminals (in priority order for detection)
17
+ # Terminal multiplexers first (tmux, zellij) as they run inside other terminals
18
+ _TERMINALS: list[type[Terminal]] = [
19
+ Tmux,
20
+ Zellij,
21
+ ITerm2,
22
+ Kitty,
23
+ Warp,
24
+ AppleTerminal,
25
+ GnomeTerminal,
26
+ ]
27
+
28
+ _registry: Registry[Terminal] = Registry(_TERMINALS)
29
+
30
+
31
+ def get_all_terminals() -> list[Terminal]:
32
+ """Get instances of all registered terminals."""
33
+ return _registry.get_all()
34
+
35
+
36
+ def get_available_terminals() -> list[Terminal]:
37
+ """Get all installed/available terminals."""
38
+ return _registry.get_available()
39
+
40
+
41
+ def detect_current_terminal() -> Terminal | None:
42
+ """Detect which terminal we're running in."""
43
+ return _registry.detect_current()
44
+
45
+
46
+ def get_terminal(name: str) -> Terminal | None:
47
+ """Get a terminal by name."""
48
+ return _registry.get_by_name(name)
@@ -0,0 +1,58 @@
1
+ """tmux terminal multiplexer adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ from typing import TYPE_CHECKING
9
+
10
+ from .base import Terminal
11
+
12
+ if TYPE_CHECKING:
13
+ from pathlib import Path
14
+
15
+
16
+ class Tmux(Terminal):
17
+ """tmux - Terminal multiplexer."""
18
+
19
+ name = "tmux"
20
+
21
+ def detect(self) -> bool:
22
+ """Detect if running inside tmux."""
23
+ # Check TMUX environment variable
24
+ return os.environ.get("TMUX") is not None
25
+
26
+ def is_available(self) -> bool:
27
+ """Check if tmux is available."""
28
+ return shutil.which("tmux") is not None
29
+
30
+ def open_new_tab(
31
+ self,
32
+ path: Path,
33
+ command: str | None = None,
34
+ tab_name: str | None = None,
35
+ ) -> bool:
36
+ """Open a new window in tmux.
37
+
38
+ Creates a new tmux window (similar to a tab) in the current session.
39
+ """
40
+ if not self.is_available():
41
+ return False
42
+
43
+ try:
44
+ # Create new window in current session
45
+ # -c sets the working directory, so no need for cd in command
46
+ cmd = ["tmux", "new-window", "-c", str(path)]
47
+
48
+ if tab_name:
49
+ cmd.extend(["-n", tab_name])
50
+
51
+ if command:
52
+ # Run command in new window (cwd already set by -c)
53
+ cmd.append(command)
54
+
55
+ subprocess.run(cmd, check=True, capture_output=True)
56
+ return True
57
+ except subprocess.CalledProcessError:
58
+ return False
@@ -0,0 +1,132 @@
1
+ """Warp terminal adapter.
2
+
3
+ Uses Warp's URI scheme and Launch Configurations for reliable automation.
4
+ Evidence: https://docs.warp.dev/terminal/more-features/uri-scheme
5
+ Evidence: https://docs.warp.dev/terminal/sessions/launch-configurations
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import subprocess
11
+ import sys
12
+ import uuid
13
+ from pathlib import Path
14
+
15
+ from .base import Terminal, _get_term_program
16
+
17
+
18
+ def _get_warp_launch_config_dir() -> Path:
19
+ """Get the Warp launch configurations directory."""
20
+ return Path.home() / ".warp" / "launch_configurations"
21
+
22
+
23
+ class Warp(Terminal):
24
+ """Warp - Modern, Rust-based terminal with AI features."""
25
+
26
+ name = "warp"
27
+
28
+ def detect(self) -> bool:
29
+ """Detect if running inside Warp."""
30
+ term_program = _get_term_program()
31
+ return term_program is not None and "warp" in term_program.lower()
32
+
33
+ def is_available(self) -> bool:
34
+ """Check if Warp is available (macOS only for now)."""
35
+ if sys.platform != "darwin":
36
+ return False
37
+ return Path("/Applications/Warp.app").exists()
38
+
39
+ def open_new_tab(
40
+ self,
41
+ path: Path,
42
+ command: str | None = None,
43
+ tab_name: str | None = None,
44
+ ) -> bool:
45
+ """Open a new tab in Warp.
46
+
47
+ Uses URI scheme for simple path-only tabs, or Launch Configurations
48
+ when a command needs to be executed.
49
+
50
+ Evidence:
51
+ URI scheme: https://docs.warp.dev/terminal/more-features/uri-scheme
52
+ Launch configs: https://docs.warp.dev/terminal/sessions/launch-configurations
53
+ """
54
+ if not self.is_available():
55
+ return False
56
+
57
+ # Simple case: no command, just open path via URI scheme
58
+ # Note: tab_name is ignored here - URI scheme doesn't support tab naming
59
+ if command is None:
60
+ try:
61
+ subprocess.run(
62
+ ["open", f"warp://action/new_tab?path={path}"], # noqa: S607
63
+ check=True,
64
+ capture_output=True,
65
+ )
66
+ return True
67
+ except subprocess.CalledProcessError:
68
+ return False
69
+
70
+ # Complex case: need to run a command - use Launch Configuration
71
+ return self._open_with_launch_config(path, command, tab_name)
72
+
73
+ def _open_with_launch_config(
74
+ self,
75
+ path: Path,
76
+ command: str,
77
+ tab_name: str | None = None,
78
+ ) -> bool:
79
+ """Open a new tab using a temporary Launch Configuration.
80
+
81
+ Creates a YAML config file, opens it via URI scheme, then cleans up.
82
+ """
83
+ config_dir = _get_warp_launch_config_dir()
84
+ config_dir.mkdir(parents=True, exist_ok=True)
85
+
86
+ # Create unique config file name
87
+ config_name = f"agent-cli-{uuid.uuid4().hex[:8]}"
88
+ config_file = config_dir / f"{config_name}.yaml"
89
+
90
+ # Build the YAML config
91
+ # Quote values that may contain special YAML characters (: # etc.)
92
+ title = tab_name or "agent-cli"
93
+ # Escape single quotes by doubling them, then wrap in single quotes
94
+ escaped_command = command.replace("'", "''")
95
+ yaml_content = f"""---
96
+ name: {config_name}
97
+ windows:
98
+ - tabs:
99
+ - title: '{title}'
100
+ layout:
101
+ cwd: '{path.as_posix()}'
102
+ commands:
103
+ - exec: '{escaped_command}'
104
+ """
105
+
106
+ try:
107
+ # Write config file
108
+ config_file.write_text(yaml_content)
109
+
110
+ # Open via URI scheme
111
+ result = subprocess.run(
112
+ ["open", f"warp://launch/{config_file}"], # noqa: S607
113
+ check=True,
114
+ capture_output=True,
115
+ )
116
+
117
+ # Clean up after a delay (give Warp time to read the file)
118
+ # We use a background process to delete after 2 seconds
119
+ subprocess.Popen(
120
+ ["sh", "-c", f'sleep 2 && rm -f "{config_file}"'], # noqa: S607
121
+ stdout=subprocess.DEVNULL,
122
+ stderr=subprocess.DEVNULL,
123
+ )
124
+
125
+ return result.returncode == 0
126
+ except subprocess.CalledProcessError:
127
+ # Clean up on failure
128
+ config_file.unlink(missing_ok=True)
129
+ return False
130
+ except Exception:
131
+ config_file.unlink(missing_ok=True)
132
+ return False
@@ -0,0 +1,78 @@
1
+ """Zellij terminal multiplexer adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import time
9
+ from typing import TYPE_CHECKING
10
+
11
+ from .base import Terminal
12
+
13
+ if TYPE_CHECKING:
14
+ from pathlib import Path
15
+
16
+
17
+ class Zellij(Terminal):
18
+ """Zellij - A terminal workspace with batteries included."""
19
+
20
+ name = "zellij"
21
+
22
+ def detect(self) -> bool:
23
+ """Detect if running inside Zellij."""
24
+ # Check ZELLIJ environment variable
25
+ return os.environ.get("ZELLIJ") is not None
26
+
27
+ def is_available(self) -> bool:
28
+ """Check if Zellij is available."""
29
+ return shutil.which("zellij") is not None
30
+
31
+ def open_new_tab(
32
+ self,
33
+ path: Path,
34
+ command: str | None = None,
35
+ tab_name: str | None = None,
36
+ ) -> bool:
37
+ """Open a new tab in Zellij.
38
+
39
+ Creates a new tab in the current Zellij session.
40
+ """
41
+ if not self.is_available():
42
+ return False
43
+
44
+ try:
45
+ # Create new tab using zellij action
46
+ # Workaround: --cwd requires --layout due to bug (github.com/zellij-org/zellij/issues/2981)
47
+ cmd = ["zellij", "action", "new-tab", "--layout", "default", "--cwd", str(path)]
48
+ if tab_name:
49
+ cmd.extend(["--name", tab_name])
50
+ subprocess.run(
51
+ cmd,
52
+ check=True,
53
+ capture_output=True,
54
+ text=True,
55
+ )
56
+
57
+ # If command specified, write it to the new pane
58
+ # --cwd already sets the working directory, so no need for cd
59
+ if command:
60
+ # Small delay to ensure the new tab has focus
61
+ time.sleep(0.1)
62
+ subprocess.run(
63
+ ["zellij", "action", "write-chars", command], # noqa: S607
64
+ check=True,
65
+ capture_output=True,
66
+ text=True,
67
+ )
68
+ # Send enter key
69
+ subprocess.run(
70
+ ["zellij", "action", "write", "10"], # 10 is newline # noqa: S607
71
+ check=True,
72
+ capture_output=True,
73
+ text=True,
74
+ )
75
+
76
+ return True
77
+ except subprocess.CalledProcessError:
78
+ return False