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.
- agent_cli/__init__.py +5 -0
- agent_cli/__main__.py +6 -0
- agent_cli/_extras.json +14 -0
- agent_cli/_requirements/.gitkeep +0 -0
- agent_cli/_requirements/audio.txt +79 -0
- agent_cli/_requirements/faster-whisper.txt +215 -0
- agent_cli/_requirements/kokoro.txt +425 -0
- agent_cli/_requirements/llm.txt +183 -0
- agent_cli/_requirements/memory.txt +355 -0
- agent_cli/_requirements/mlx-whisper.txt +222 -0
- agent_cli/_requirements/piper.txt +176 -0
- agent_cli/_requirements/rag.txt +402 -0
- agent_cli/_requirements/server.txt +154 -0
- agent_cli/_requirements/speed.txt +77 -0
- agent_cli/_requirements/vad.txt +155 -0
- agent_cli/_requirements/wyoming.txt +71 -0
- agent_cli/_tools.py +368 -0
- agent_cli/agents/__init__.py +23 -0
- agent_cli/agents/_voice_agent_common.py +136 -0
- agent_cli/agents/assistant.py +383 -0
- agent_cli/agents/autocorrect.py +284 -0
- agent_cli/agents/chat.py +496 -0
- agent_cli/agents/memory/__init__.py +31 -0
- agent_cli/agents/memory/add.py +190 -0
- agent_cli/agents/memory/proxy.py +160 -0
- agent_cli/agents/rag_proxy.py +128 -0
- agent_cli/agents/speak.py +209 -0
- agent_cli/agents/transcribe.py +671 -0
- agent_cli/agents/transcribe_daemon.py +499 -0
- agent_cli/agents/voice_edit.py +291 -0
- agent_cli/api.py +22 -0
- agent_cli/cli.py +106 -0
- agent_cli/config.py +503 -0
- agent_cli/config_cmd.py +307 -0
- agent_cli/constants.py +27 -0
- agent_cli/core/__init__.py +1 -0
- agent_cli/core/audio.py +461 -0
- agent_cli/core/audio_format.py +299 -0
- agent_cli/core/chroma.py +88 -0
- agent_cli/core/deps.py +191 -0
- agent_cli/core/openai_proxy.py +139 -0
- agent_cli/core/process.py +195 -0
- agent_cli/core/reranker.py +120 -0
- agent_cli/core/sse.py +87 -0
- agent_cli/core/transcription_logger.py +70 -0
- agent_cli/core/utils.py +526 -0
- agent_cli/core/vad.py +175 -0
- agent_cli/core/watch.py +65 -0
- agent_cli/dev/__init__.py +14 -0
- agent_cli/dev/cli.py +1588 -0
- agent_cli/dev/coding_agents/__init__.py +19 -0
- agent_cli/dev/coding_agents/aider.py +24 -0
- agent_cli/dev/coding_agents/base.py +167 -0
- agent_cli/dev/coding_agents/claude.py +39 -0
- agent_cli/dev/coding_agents/codex.py +24 -0
- agent_cli/dev/coding_agents/continue_dev.py +15 -0
- agent_cli/dev/coding_agents/copilot.py +24 -0
- agent_cli/dev/coding_agents/cursor_agent.py +48 -0
- agent_cli/dev/coding_agents/gemini.py +28 -0
- agent_cli/dev/coding_agents/opencode.py +15 -0
- agent_cli/dev/coding_agents/registry.py +49 -0
- agent_cli/dev/editors/__init__.py +19 -0
- agent_cli/dev/editors/base.py +89 -0
- agent_cli/dev/editors/cursor.py +15 -0
- agent_cli/dev/editors/emacs.py +46 -0
- agent_cli/dev/editors/jetbrains.py +56 -0
- agent_cli/dev/editors/nano.py +31 -0
- agent_cli/dev/editors/neovim.py +33 -0
- agent_cli/dev/editors/registry.py +59 -0
- agent_cli/dev/editors/sublime.py +20 -0
- agent_cli/dev/editors/vim.py +42 -0
- agent_cli/dev/editors/vscode.py +15 -0
- agent_cli/dev/editors/zed.py +20 -0
- agent_cli/dev/project.py +568 -0
- agent_cli/dev/registry.py +52 -0
- agent_cli/dev/skill/SKILL.md +141 -0
- agent_cli/dev/skill/examples.md +571 -0
- agent_cli/dev/terminals/__init__.py +19 -0
- agent_cli/dev/terminals/apple_terminal.py +82 -0
- agent_cli/dev/terminals/base.py +56 -0
- agent_cli/dev/terminals/gnome.py +51 -0
- agent_cli/dev/terminals/iterm2.py +84 -0
- agent_cli/dev/terminals/kitty.py +77 -0
- agent_cli/dev/terminals/registry.py +48 -0
- agent_cli/dev/terminals/tmux.py +58 -0
- agent_cli/dev/terminals/warp.py +132 -0
- agent_cli/dev/terminals/zellij.py +78 -0
- agent_cli/dev/worktree.py +856 -0
- agent_cli/docs_gen.py +417 -0
- agent_cli/example-config.toml +185 -0
- agent_cli/install/__init__.py +5 -0
- agent_cli/install/common.py +89 -0
- agent_cli/install/extras.py +174 -0
- agent_cli/install/hotkeys.py +48 -0
- agent_cli/install/services.py +87 -0
- agent_cli/memory/__init__.py +7 -0
- agent_cli/memory/_files.py +250 -0
- agent_cli/memory/_filters.py +63 -0
- agent_cli/memory/_git.py +157 -0
- agent_cli/memory/_indexer.py +142 -0
- agent_cli/memory/_ingest.py +408 -0
- agent_cli/memory/_persistence.py +182 -0
- agent_cli/memory/_prompt.py +91 -0
- agent_cli/memory/_retrieval.py +294 -0
- agent_cli/memory/_store.py +169 -0
- agent_cli/memory/_streaming.py +44 -0
- agent_cli/memory/_tasks.py +48 -0
- agent_cli/memory/api.py +113 -0
- agent_cli/memory/client.py +272 -0
- agent_cli/memory/engine.py +361 -0
- agent_cli/memory/entities.py +43 -0
- agent_cli/memory/models.py +112 -0
- agent_cli/opts.py +433 -0
- agent_cli/py.typed +0 -0
- agent_cli/rag/__init__.py +3 -0
- agent_cli/rag/_indexer.py +67 -0
- agent_cli/rag/_indexing.py +226 -0
- agent_cli/rag/_prompt.py +30 -0
- agent_cli/rag/_retriever.py +156 -0
- agent_cli/rag/_store.py +48 -0
- agent_cli/rag/_utils.py +218 -0
- agent_cli/rag/api.py +175 -0
- agent_cli/rag/client.py +299 -0
- agent_cli/rag/engine.py +302 -0
- agent_cli/rag/models.py +55 -0
- agent_cli/scripts/.runtime/.gitkeep +0 -0
- agent_cli/scripts/__init__.py +1 -0
- agent_cli/scripts/check_plugin_skill_sync.py +50 -0
- agent_cli/scripts/linux-hotkeys/README.md +63 -0
- agent_cli/scripts/linux-hotkeys/toggle-autocorrect.sh +45 -0
- agent_cli/scripts/linux-hotkeys/toggle-transcription.sh +58 -0
- agent_cli/scripts/linux-hotkeys/toggle-voice-edit.sh +58 -0
- agent_cli/scripts/macos-hotkeys/README.md +45 -0
- agent_cli/scripts/macos-hotkeys/skhd-config-example +5 -0
- agent_cli/scripts/macos-hotkeys/toggle-autocorrect.sh +12 -0
- agent_cli/scripts/macos-hotkeys/toggle-transcription.sh +37 -0
- agent_cli/scripts/macos-hotkeys/toggle-voice-edit.sh +37 -0
- agent_cli/scripts/nvidia-asr-server/README.md +99 -0
- agent_cli/scripts/nvidia-asr-server/pyproject.toml +27 -0
- agent_cli/scripts/nvidia-asr-server/server.py +255 -0
- agent_cli/scripts/nvidia-asr-server/shell.nix +32 -0
- agent_cli/scripts/nvidia-asr-server/uv.lock +4654 -0
- agent_cli/scripts/run-openwakeword.sh +11 -0
- agent_cli/scripts/run-piper-windows.ps1 +30 -0
- agent_cli/scripts/run-piper.sh +24 -0
- agent_cli/scripts/run-whisper-linux.sh +40 -0
- agent_cli/scripts/run-whisper-macos.sh +6 -0
- agent_cli/scripts/run-whisper-windows.ps1 +51 -0
- agent_cli/scripts/run-whisper.sh +9 -0
- agent_cli/scripts/run_faster_whisper_server.py +136 -0
- agent_cli/scripts/setup-linux-hotkeys.sh +72 -0
- agent_cli/scripts/setup-linux.sh +108 -0
- agent_cli/scripts/setup-macos-hotkeys.sh +61 -0
- agent_cli/scripts/setup-macos.sh +76 -0
- agent_cli/scripts/setup-windows.ps1 +63 -0
- agent_cli/scripts/start-all-services-windows.ps1 +53 -0
- agent_cli/scripts/start-all-services.sh +178 -0
- agent_cli/scripts/sync_extras.py +138 -0
- agent_cli/server/__init__.py +3 -0
- agent_cli/server/cli.py +721 -0
- agent_cli/server/common.py +222 -0
- agent_cli/server/model_manager.py +288 -0
- agent_cli/server/model_registry.py +225 -0
- agent_cli/server/proxy/__init__.py +3 -0
- agent_cli/server/proxy/api.py +444 -0
- agent_cli/server/streaming.py +67 -0
- agent_cli/server/tts/__init__.py +3 -0
- agent_cli/server/tts/api.py +335 -0
- agent_cli/server/tts/backends/__init__.py +82 -0
- agent_cli/server/tts/backends/base.py +139 -0
- agent_cli/server/tts/backends/kokoro.py +403 -0
- agent_cli/server/tts/backends/piper.py +253 -0
- agent_cli/server/tts/model_manager.py +201 -0
- agent_cli/server/tts/model_registry.py +28 -0
- agent_cli/server/tts/wyoming_handler.py +249 -0
- agent_cli/server/whisper/__init__.py +3 -0
- agent_cli/server/whisper/api.py +413 -0
- agent_cli/server/whisper/backends/__init__.py +89 -0
- agent_cli/server/whisper/backends/base.py +97 -0
- agent_cli/server/whisper/backends/faster_whisper.py +225 -0
- agent_cli/server/whisper/backends/mlx.py +270 -0
- agent_cli/server/whisper/languages.py +116 -0
- agent_cli/server/whisper/model_manager.py +157 -0
- agent_cli/server/whisper/model_registry.py +28 -0
- agent_cli/server/whisper/wyoming_handler.py +203 -0
- agent_cli/services/__init__.py +343 -0
- agent_cli/services/_wyoming_utils.py +64 -0
- agent_cli/services/asr.py +506 -0
- agent_cli/services/llm.py +228 -0
- agent_cli/services/tts.py +450 -0
- agent_cli/services/wake_word.py +142 -0
- agent_cli-0.70.5.dist-info/METADATA +2118 -0
- agent_cli-0.70.5.dist-info/RECORD +196 -0
- agent_cli-0.70.5.dist-info/WHEEL +4 -0
- agent_cli-0.70.5.dist-info/entry_points.txt +4 -0
- 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
|