agent-cli 0.61.2__py3-none-any.whl → 0.70.2__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/_extras.json +13 -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/agents/assistant.py +3 -1
- agent_cli/agents/autocorrect.py +5 -2
- agent_cli/agents/chat.py +3 -1
- agent_cli/agents/memory/__init__.py +2 -1
- agent_cli/agents/memory/add.py +2 -0
- agent_cli/agents/memory/proxy.py +7 -12
- agent_cli/agents/rag_proxy.py +5 -10
- agent_cli/agents/speak.py +3 -1
- agent_cli/agents/transcribe.py +7 -2
- agent_cli/agents/transcribe_daemon.py +3 -1
- agent_cli/agents/voice_edit.py +3 -1
- agent_cli/cli.py +19 -3
- agent_cli/config_cmd.py +1 -0
- agent_cli/core/chroma.py +4 -4
- agent_cli/core/deps.py +177 -25
- agent_cli/core/openai_proxy.py +9 -4
- agent_cli/core/process.py +2 -2
- agent_cli/core/reranker.py +5 -4
- agent_cli/core/utils.py +5 -3
- agent_cli/core/vad.py +2 -1
- agent_cli/core/watch.py +8 -6
- agent_cli/dev/cli.py +31 -34
- agent_cli/dev/coding_agents/base.py +1 -2
- agent_cli/dev/skill/SKILL.md +141 -0
- agent_cli/dev/skill/examples.md +571 -0
- agent_cli/dev/worktree.py +53 -5
- agent_cli/docs_gen.py +12 -42
- agent_cli/install/__init__.py +1 -1
- agent_cli/install/extras.py +174 -0
- agent_cli/memory/__init__.py +1 -18
- agent_cli/memory/_files.py +4 -1
- agent_cli/memory/_indexer.py +3 -2
- agent_cli/memory/_ingest.py +6 -5
- agent_cli/memory/_retrieval.py +18 -8
- agent_cli/memory/_streaming.py +2 -2
- agent_cli/memory/api.py +1 -1
- agent_cli/memory/client.py +1 -1
- agent_cli/memory/engine.py +1 -1
- agent_cli/rag/__init__.py +0 -19
- agent_cli/rag/_indexer.py +3 -2
- agent_cli/rag/api.py +1 -0
- agent_cli/scripts/.runtime/.gitkeep +0 -0
- agent_cli/scripts/check_plugin_skill_sync.py +50 -0
- agent_cli/scripts/sync_extras.py +138 -0
- agent_cli/server/cli.py +26 -24
- agent_cli/server/common.py +3 -4
- agent_cli/server/tts/api.py +1 -1
- agent_cli/server/whisper/backends/faster_whisper.py +30 -23
- agent_cli/server/whisper/wyoming_handler.py +22 -27
- agent_cli/services/_wyoming_utils.py +4 -2
- agent_cli/services/asr.py +13 -3
- agent_cli/services/llm.py +2 -1
- agent_cli/services/tts.py +5 -2
- agent_cli/services/wake_word.py +6 -3
- {agent_cli-0.61.2.dist-info → agent_cli-0.70.2.dist-info}/METADATA +168 -73
- {agent_cli-0.61.2.dist-info → agent_cli-0.70.2.dist-info}/RECORD +72 -54
- {agent_cli-0.61.2.dist-info → agent_cli-0.70.2.dist-info}/WHEEL +1 -2
- agent_cli-0.61.2.dist-info/top_level.txt +0 -1
- {agent_cli-0.61.2.dist-info → agent_cli-0.70.2.dist-info}/entry_points.txt +0 -0
- {agent_cli-0.61.2.dist-info → agent_cli-0.70.2.dist-info}/licenses/LICENSE +0 -0
agent_cli/core/deps.py
CHANGED
|
@@ -2,38 +2,190 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import functools
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
6
8
|
from importlib.util import find_spec
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
_PYTHON_314_INCOMPATIBLE = {"chromadb", "onnxruntime"}
|
|
12
|
+
import typer
|
|
10
13
|
|
|
14
|
+
from agent_cli.config import load_config
|
|
15
|
+
from agent_cli.core.utils import console, print_error_message
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
|
|
20
|
+
F = TypeVar("F", bound="Callable[..., object]")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_auto_install_setting() -> bool:
|
|
24
|
+
"""Check if auto-install is enabled (default: True)."""
|
|
25
|
+
if os.environ.get("AGENT_CLI_NO_AUTO_INSTALL", "").lower() in ("1", "true", "yes"):
|
|
26
|
+
return False
|
|
27
|
+
return load_config().get("settings", {}).get("auto_install_extras", True)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Load extras from JSON file
|
|
31
|
+
_EXTRAS_FILE = Path(__file__).parent.parent / "_extras.json"
|
|
32
|
+
EXTRAS: dict[str, tuple[str, list[str]]] = {
|
|
33
|
+
k: (v[0], v[1]) for k, v in json.loads(_EXTRAS_FILE.read_text()).items()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _check_package_installed(pkg: str) -> bool:
|
|
38
|
+
"""Check if a single package is installed."""
|
|
39
|
+
top_module = pkg.split(".")[0]
|
|
40
|
+
try:
|
|
41
|
+
return find_spec(top_module) is not None
|
|
42
|
+
except (ValueError, ModuleNotFoundError):
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check_extra_installed(extra: str) -> bool:
|
|
47
|
+
"""Check if packages for an extra are installed using find_spec (no actual import).
|
|
48
|
+
|
|
49
|
+
Supports `|` syntax for alternatives: "piper|kokoro" means ANY of these extras.
|
|
50
|
+
For regular extras, ALL packages must be installed.
|
|
51
|
+
"""
|
|
52
|
+
# Handle "extra1|extra2" syntax - any of these extras is sufficient
|
|
53
|
+
if "|" in extra:
|
|
54
|
+
return any(check_extra_installed(e) for e in extra.split("|"))
|
|
55
|
+
|
|
56
|
+
if extra not in EXTRAS:
|
|
57
|
+
return False # Unknown extra, trigger install attempt to surface error
|
|
58
|
+
_, packages = EXTRAS[extra]
|
|
59
|
+
|
|
60
|
+
# All packages must be installed
|
|
61
|
+
return all(_check_package_installed(pkg) for pkg in packages)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _format_extra_item(extra: str) -> str:
|
|
65
|
+
"""Format a single extra as a list item with description."""
|
|
66
|
+
desc, _ = EXTRAS.get(extra, ("", []))
|
|
67
|
+
if desc:
|
|
68
|
+
return f" - '{extra}' ({desc})"
|
|
69
|
+
return f" - '{extra}'"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _format_install_commands(extras: list[str]) -> list[str]:
|
|
73
|
+
"""Format install commands for one or more extras."""
|
|
74
|
+
combined = ",".join(extras)
|
|
75
|
+
extras_args = " ".join(extras)
|
|
76
|
+
return [
|
|
77
|
+
"Install with:",
|
|
78
|
+
f' [bold cyan]uv tool install -p 3.13 "agent-cli\\[{combined}]"[/bold cyan]',
|
|
79
|
+
" # or",
|
|
80
|
+
f" [bold cyan]agent-cli install-extras {extras_args}[/bold cyan]",
|
|
21
81
|
]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_install_hint(extra: str) -> str:
|
|
85
|
+
"""Get install command hint for a single extra.
|
|
86
|
+
|
|
87
|
+
Supports `|` syntax for alternatives: "piper|kokoro" shows both options.
|
|
88
|
+
"""
|
|
89
|
+
# Handle "extra1|extra2" syntax - show all options
|
|
90
|
+
if "|" in extra:
|
|
91
|
+
alternatives = extra.split("|")
|
|
92
|
+
lines = ["This command requires one of:"]
|
|
93
|
+
lines.extend(_format_extra_item(alt) for alt in alternatives)
|
|
94
|
+
lines.append("")
|
|
95
|
+
lines.append("Install one with:")
|
|
96
|
+
lines.extend(
|
|
97
|
+
f' [bold cyan]uv tool install -p 3.13 "agent-cli\\[{alt}]"[/bold cyan]'
|
|
98
|
+
for alt in alternatives
|
|
99
|
+
)
|
|
100
|
+
lines.append(" # or")
|
|
101
|
+
lines.extend(
|
|
102
|
+
f" [bold cyan]agent-cli install-extras {alt}[/bold cyan]" for alt in alternatives
|
|
103
|
+
)
|
|
104
|
+
return "\n".join(lines)
|
|
105
|
+
|
|
106
|
+
desc, _ = EXTRAS.get(extra, ("", []))
|
|
107
|
+
header = f"This command requires the '{extra}' extra"
|
|
108
|
+
if desc:
|
|
109
|
+
header += f" ({desc})"
|
|
110
|
+
header += "."
|
|
111
|
+
|
|
112
|
+
lines = [header, ""]
|
|
113
|
+
lines.extend(_format_install_commands([extra]))
|
|
114
|
+
return "\n".join(lines)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_combined_install_hint(extras: list[str]) -> str:
|
|
118
|
+
"""Get a combined install hint for multiple missing extras."""
|
|
119
|
+
if len(extras) == 1:
|
|
120
|
+
return get_install_hint(extras[0])
|
|
121
|
+
|
|
122
|
+
lines = ["This command requires the following extras:"]
|
|
123
|
+
lines.extend(_format_extra_item(extra) for extra in extras)
|
|
124
|
+
lines.append("")
|
|
125
|
+
lines.extend(_format_install_commands(extras))
|
|
126
|
+
return "\n".join(lines)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _try_auto_install(missing: list[str]) -> bool:
|
|
130
|
+
"""Attempt to auto-install missing extras. Returns True if successful."""
|
|
131
|
+
from agent_cli.install.extras import install_extras_programmatic # noqa: PLC0415
|
|
132
|
+
|
|
133
|
+
# Flatten alternatives (e.g., "piper|kokoro" -> just pick the first one)
|
|
134
|
+
extras_to_install = []
|
|
135
|
+
for extra in missing:
|
|
136
|
+
if "|" in extra:
|
|
137
|
+
# For alternatives, install the first option
|
|
138
|
+
extras_to_install.append(extra.split("|")[0])
|
|
139
|
+
else:
|
|
140
|
+
extras_to_install.append(extra)
|
|
141
|
+
|
|
142
|
+
console.print(
|
|
143
|
+
f"[yellow]Auto-installing missing extras: {', '.join(extras_to_install)}[/]",
|
|
144
|
+
)
|
|
145
|
+
return install_extras_programmatic(extras_to_install, quiet=True)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _check_and_install_extras(extras: tuple[str, ...]) -> list[str]:
|
|
149
|
+
"""Check for missing extras and attempt auto-install. Returns list of still-missing."""
|
|
150
|
+
missing = [e for e in extras if not check_extra_installed(e)]
|
|
22
151
|
if not missing:
|
|
23
|
-
return
|
|
152
|
+
return []
|
|
153
|
+
|
|
154
|
+
if not _get_auto_install_setting():
|
|
155
|
+
print_error_message(get_combined_install_hint(missing))
|
|
156
|
+
return missing
|
|
24
157
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
158
|
+
if not _try_auto_install(missing):
|
|
159
|
+
print_error_message("Auto-install failed.\n" + get_combined_install_hint(missing))
|
|
160
|
+
return missing
|
|
28
161
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
162
|
+
console.print("[green]Installation complete![/]")
|
|
163
|
+
still_missing = [e for e in extras if not check_extra_installed(e)]
|
|
164
|
+
if still_missing:
|
|
165
|
+
print_error_message(
|
|
166
|
+
"Auto-install completed but some dependencies are still missing.\n"
|
|
167
|
+
+ get_combined_install_hint(still_missing),
|
|
34
168
|
)
|
|
35
|
-
|
|
169
|
+
return still_missing
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def requires_extras(*extras: str) -> Callable[[F], F]:
|
|
173
|
+
"""Decorator to declare required extras for a command.
|
|
174
|
+
|
|
175
|
+
Auto-installs missing extras by default. Disable via AGENT_CLI_NO_AUTO_INSTALL=1
|
|
176
|
+
or config [settings] auto_install_extras = false.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def decorator(func: F) -> F:
|
|
180
|
+
func._required_extras = extras # type: ignore[attr-defined]
|
|
181
|
+
|
|
182
|
+
@functools.wraps(func)
|
|
183
|
+
def wrapper(*args: object, **kwargs: object) -> object:
|
|
184
|
+
if _check_and_install_extras(extras):
|
|
185
|
+
raise typer.Exit(1)
|
|
186
|
+
return func(*args, **kwargs)
|
|
187
|
+
|
|
188
|
+
wrapper._required_extras = extras # type: ignore[attr-defined]
|
|
189
|
+
return wrapper # type: ignore[return-value]
|
|
36
190
|
|
|
37
|
-
|
|
38
|
-
msg = f"Missing required dependencies for {extra_name}: {', '.join(missing)}. Please install with {hint}."
|
|
39
|
-
raise ImportError(msg)
|
|
191
|
+
return decorator
|
agent_cli/core/openai_proxy.py
CHANGED
|
@@ -6,13 +6,11 @@ import json
|
|
|
6
6
|
import logging
|
|
7
7
|
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
8
8
|
|
|
9
|
-
import httpx
|
|
10
|
-
from fastapi import HTTPException, Request, Response
|
|
11
|
-
from fastapi.responses import StreamingResponse
|
|
12
|
-
|
|
13
9
|
if TYPE_CHECKING:
|
|
14
10
|
from collections.abc import AsyncGenerator, Iterable
|
|
15
11
|
|
|
12
|
+
from fastapi import Request, Response
|
|
13
|
+
|
|
16
14
|
LOGGER = logging.getLogger(__name__)
|
|
17
15
|
|
|
18
16
|
|
|
@@ -33,6 +31,9 @@ async def proxy_request_to_upstream(
|
|
|
33
31
|
api_key: str | None = None,
|
|
34
32
|
) -> Response:
|
|
35
33
|
"""Forward a raw HTTP request to an upstream OpenAI-compatible provider."""
|
|
34
|
+
import httpx # noqa: PLC0415
|
|
35
|
+
from fastapi import Response # noqa: PLC0415
|
|
36
|
+
|
|
36
37
|
auth_header = request.headers.get("Authorization")
|
|
37
38
|
headers = {}
|
|
38
39
|
if auth_header:
|
|
@@ -82,6 +83,10 @@ async def forward_chat_request(
|
|
|
82
83
|
exclude_fields: Iterable[str] = (),
|
|
83
84
|
) -> Any:
|
|
84
85
|
"""Forward a chat request to a backend LLM."""
|
|
86
|
+
import httpx # noqa: PLC0415
|
|
87
|
+
from fastapi import HTTPException # noqa: PLC0415
|
|
88
|
+
from fastapi.responses import StreamingResponse # noqa: PLC0415
|
|
89
|
+
|
|
85
90
|
forward_payload = request.model_dump(exclude=set(exclude_fields))
|
|
86
91
|
headers = {"Authorization": f"Bearer {api_key}"} if api_key else None
|
|
87
92
|
|
agent_cli/core/process.py
CHANGED
|
@@ -10,8 +10,6 @@ from contextlib import contextmanager
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import TYPE_CHECKING
|
|
12
12
|
|
|
13
|
-
import setproctitle
|
|
14
|
-
|
|
15
13
|
if TYPE_CHECKING:
|
|
16
14
|
from collections.abc import Generator
|
|
17
15
|
|
|
@@ -36,6 +34,8 @@ def set_process_title(process_name: str) -> None:
|
|
|
36
34
|
process_name: The name of the process (e.g., 'transcribe', 'chat').
|
|
37
35
|
|
|
38
36
|
"""
|
|
37
|
+
import setproctitle # noqa: PLC0415
|
|
38
|
+
|
|
39
39
|
global _original_proctitle
|
|
40
40
|
|
|
41
41
|
# Capture the original command line only once, before any modification
|
agent_cli/core/reranker.py
CHANGED
|
@@ -4,15 +4,13 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
|
|
7
|
-
from huggingface_hub import hf_hub_download
|
|
8
|
-
from onnxruntime import InferenceSession
|
|
9
|
-
from transformers import AutoTokenizer
|
|
10
|
-
|
|
11
7
|
LOGGER = logging.getLogger(__name__)
|
|
12
8
|
|
|
13
9
|
|
|
14
10
|
def _download_onnx_model(model_name: str, onnx_filename: str) -> str:
|
|
15
11
|
"""Download the ONNX model, favoring the common `onnx/` folder layout."""
|
|
12
|
+
from huggingface_hub import hf_hub_download # noqa: PLC0415
|
|
13
|
+
|
|
16
14
|
if "/" in onnx_filename:
|
|
17
15
|
return hf_hub_download(repo_id=model_name, filename=onnx_filename)
|
|
18
16
|
|
|
@@ -45,6 +43,9 @@ class OnnxCrossEncoder:
|
|
|
45
43
|
onnx_filename: str = "model.onnx",
|
|
46
44
|
) -> None:
|
|
47
45
|
"""Initialize the ONNX CrossEncoder."""
|
|
46
|
+
from onnxruntime import InferenceSession # noqa: PLC0415
|
|
47
|
+
from transformers import AutoTokenizer # noqa: PLC0415
|
|
48
|
+
|
|
48
49
|
self.model_name = model_name
|
|
49
50
|
|
|
50
51
|
# Download model if needed
|
agent_cli/core/utils.py
CHANGED
|
@@ -18,7 +18,6 @@ from contextlib import (
|
|
|
18
18
|
)
|
|
19
19
|
from typing import TYPE_CHECKING, Any
|
|
20
20
|
|
|
21
|
-
import pyperclip
|
|
22
21
|
from rich.console import Console
|
|
23
22
|
from rich.live import Live
|
|
24
23
|
from rich.panel import Panel
|
|
@@ -40,6 +39,7 @@ if TYPE_CHECKING:
|
|
|
40
39
|
from pathlib import Path
|
|
41
40
|
|
|
42
41
|
console = Console()
|
|
42
|
+
err_console = Console(stderr=True)
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
def enable_json_mode() -> None:
|
|
@@ -211,8 +211,8 @@ def print_output_panel(
|
|
|
211
211
|
|
|
212
212
|
|
|
213
213
|
def print_error_message(message: str, suggestion: str | None = None) -> None:
|
|
214
|
-
"""Prints an error message in a panel."""
|
|
215
|
-
error_text = Text(message)
|
|
214
|
+
"""Prints an error message in a panel with rich markup support."""
|
|
215
|
+
error_text = Text.from_markup(message)
|
|
216
216
|
if suggestion:
|
|
217
217
|
error_text.append("\n\n")
|
|
218
218
|
error_text.append(suggestion)
|
|
@@ -233,6 +233,8 @@ def print_device_index(input_device_index: int | None, input_device_name: str |
|
|
|
233
233
|
|
|
234
234
|
def get_clipboard_text(*, quiet: bool = False) -> str | None:
|
|
235
235
|
"""Get text from clipboard, with an optional status message."""
|
|
236
|
+
import pyperclip # noqa: PLC0415
|
|
237
|
+
|
|
236
238
|
text = pyperclip.paste()
|
|
237
239
|
if not text:
|
|
238
240
|
if not quiet:
|
agent_cli/core/vad.py
CHANGED
|
@@ -12,7 +12,6 @@ from agent_cli import constants
|
|
|
12
12
|
try:
|
|
13
13
|
import numpy as np
|
|
14
14
|
import torch
|
|
15
|
-
from silero_vad.utils_vad import OnnxWrapper
|
|
16
15
|
except ImportError as e:
|
|
17
16
|
msg = (
|
|
18
17
|
"silero-vad is required for the transcribe-daemon command. "
|
|
@@ -57,6 +56,8 @@ class VoiceActivityDetector:
|
|
|
57
56
|
msg = f"Sample rate must be 8000 or 16000, got {sample_rate}"
|
|
58
57
|
raise ValueError(msg)
|
|
59
58
|
|
|
59
|
+
from silero_vad.utils_vad import OnnxWrapper # noqa: PLC0415
|
|
60
|
+
|
|
60
61
|
self.sample_rate = sample_rate
|
|
61
62
|
self.threshold = threshold
|
|
62
63
|
self.silence_threshold_ms = silence_threshold_ms
|
agent_cli/core/watch.py
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
from collections.abc import Callable
|
|
7
6
|
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Callable
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
PathFilter = Callable[[Path, Path], bool]
|
|
12
|
+
from watchfiles import Change
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def _default_skip_hidden(path: Path, root: Path) -> bool:
|
|
@@ -20,10 +20,10 @@ def _default_skip_hidden(path: Path, root: Path) -> bool:
|
|
|
20
20
|
|
|
21
21
|
async def watch_directory(
|
|
22
22
|
root: Path,
|
|
23
|
-
handler:
|
|
23
|
+
handler: Callable[[Change, Path], None],
|
|
24
24
|
*,
|
|
25
25
|
skip_hidden: bool = True,
|
|
26
|
-
ignore_filter:
|
|
26
|
+
ignore_filter: Callable[[Path, Path], bool] | None = None,
|
|
27
27
|
use_executor: bool = True,
|
|
28
28
|
) -> None:
|
|
29
29
|
"""Watch a directory for file changes and invoke handler(change, path).
|
|
@@ -38,6 +38,8 @@ async def watch_directory(
|
|
|
38
38
|
use_executor: If True, run handler in a thread pool executor.
|
|
39
39
|
|
|
40
40
|
"""
|
|
41
|
+
from watchfiles import awatch # noqa: PLC0415
|
|
42
|
+
|
|
41
43
|
loop = asyncio.get_running_loop()
|
|
42
44
|
|
|
43
45
|
# Determine which filter to use
|
agent_cli/dev/cli.py
CHANGED
|
@@ -13,7 +13,6 @@ from pathlib import Path
|
|
|
13
13
|
from typing import TYPE_CHECKING, Annotated, NoReturn
|
|
14
14
|
|
|
15
15
|
import typer
|
|
16
|
-
from rich.console import Console
|
|
17
16
|
from rich.panel import Panel
|
|
18
17
|
from rich.table import Table
|
|
19
18
|
|
|
@@ -21,6 +20,7 @@ from agent_cli.cli import app as main_app
|
|
|
21
20
|
from agent_cli.cli import set_config_defaults
|
|
22
21
|
from agent_cli.config import load_config
|
|
23
22
|
from agent_cli.core.process import set_process_title
|
|
23
|
+
from agent_cli.core.utils import console, err_console
|
|
24
24
|
|
|
25
25
|
# Word lists for generating random branch names (like Docker container names)
|
|
26
26
|
_ADJECTIVES = [
|
|
@@ -112,9 +112,6 @@ if TYPE_CHECKING:
|
|
|
112
112
|
from .coding_agents.base import CodingAgent
|
|
113
113
|
from .editors.base import Editor
|
|
114
114
|
|
|
115
|
-
console = Console()
|
|
116
|
-
err_console = Console(stderr=True)
|
|
117
|
-
|
|
118
115
|
app = typer.Typer(
|
|
119
116
|
name="dev",
|
|
120
117
|
help="Parallel development environment manager using git worktrees.",
|
|
@@ -122,7 +119,7 @@ app = typer.Typer(
|
|
|
122
119
|
rich_markup_mode="markdown",
|
|
123
120
|
no_args_is_help=True,
|
|
124
121
|
)
|
|
125
|
-
main_app.add_typer(app, name="dev")
|
|
122
|
+
main_app.add_typer(app, name="dev", rich_help_panel="Development")
|
|
126
123
|
|
|
127
124
|
|
|
128
125
|
@app.callback()
|
|
@@ -344,6 +341,19 @@ def _launch_editor(path: Path, editor: Editor) -> None:
|
|
|
344
341
|
_warn(f"Could not open editor: {e}")
|
|
345
342
|
|
|
346
343
|
|
|
344
|
+
def _write_prompt_to_worktree(worktree_path: Path, prompt: str) -> Path:
|
|
345
|
+
"""Write the prompt to .claude/TASK.md in the worktree.
|
|
346
|
+
|
|
347
|
+
This makes the task description available to the spawned agent
|
|
348
|
+
and provides a record of what was requested.
|
|
349
|
+
"""
|
|
350
|
+
claude_dir = worktree_path / ".claude"
|
|
351
|
+
claude_dir.mkdir(parents=True, exist_ok=True)
|
|
352
|
+
task_file = claude_dir / "TASK.md"
|
|
353
|
+
task_file.write_text(prompt + "\n")
|
|
354
|
+
return task_file
|
|
355
|
+
|
|
356
|
+
|
|
347
357
|
def _format_env_prefix(env: dict[str, str]) -> str:
|
|
348
358
|
"""Format environment variables as shell prefix.
|
|
349
359
|
|
|
@@ -465,7 +475,7 @@ def _launch_agent(
|
|
|
465
475
|
|
|
466
476
|
|
|
467
477
|
@app.command("new")
|
|
468
|
-
def new( # noqa: PLR0912, PLR0915
|
|
478
|
+
def new( # noqa: C901, PLR0912, PLR0915
|
|
469
479
|
branch: Annotated[
|
|
470
480
|
str | None,
|
|
471
481
|
typer.Argument(help="Branch name (auto-generated if not provided)"),
|
|
@@ -552,6 +562,10 @@ def new( # noqa: PLR0912, PLR0915
|
|
|
552
562
|
if prompt_file is not None:
|
|
553
563
|
prompt = prompt_file.read_text().strip()
|
|
554
564
|
|
|
565
|
+
# If a prompt is provided, automatically enable agent mode
|
|
566
|
+
if prompt:
|
|
567
|
+
agent = True
|
|
568
|
+
|
|
555
569
|
repo_root = _ensure_git_repo()
|
|
556
570
|
|
|
557
571
|
# Generate branch name if not provided
|
|
@@ -627,6 +641,11 @@ def new( # noqa: PLR0912, PLR0915
|
|
|
627
641
|
# Only warn if user explicitly requested direnv
|
|
628
642
|
_warn("direnv not installed, skipping .envrc setup")
|
|
629
643
|
|
|
644
|
+
# Write prompt to worktree (makes task available to the spawned agent)
|
|
645
|
+
if prompt:
|
|
646
|
+
task_file = _write_prompt_to_worktree(result.path, prompt)
|
|
647
|
+
_success(f"Wrote task to {task_file.relative_to(result.path)}")
|
|
648
|
+
|
|
630
649
|
# Resolve editor and agent
|
|
631
650
|
resolved_editor = _resolve_editor(editor, editor_name, default_editor)
|
|
632
651
|
resolved_agent = _resolve_agent(agent, agent_name, default_agent)
|
|
@@ -655,10 +674,6 @@ def new( # noqa: PLR0912, PLR0915
|
|
|
655
674
|
|
|
656
675
|
@app.command("list")
|
|
657
676
|
def list_envs(
|
|
658
|
-
porcelain: Annotated[
|
|
659
|
-
bool,
|
|
660
|
-
typer.Option("--porcelain", "-p", help="Machine-readable output"),
|
|
661
|
-
] = False,
|
|
662
677
|
json_output: Annotated[
|
|
663
678
|
bool,
|
|
664
679
|
typer.Option("--json", help="Output as JSON for automation"),
|
|
@@ -692,11 +707,6 @@ def list_envs(
|
|
|
692
707
|
print(json.dumps({"worktrees": data}))
|
|
693
708
|
return
|
|
694
709
|
|
|
695
|
-
if porcelain:
|
|
696
|
-
for wt in worktrees:
|
|
697
|
-
print(f"{wt.path.as_posix()}\t{wt.branch or '(detached)'}")
|
|
698
|
-
return
|
|
699
|
-
|
|
700
710
|
table = Table(title="Dev Environments (Git Worktrees)")
|
|
701
711
|
table.add_column("Name", style="cyan")
|
|
702
712
|
table.add_column("Branch", style="green")
|
|
@@ -766,15 +776,11 @@ def _is_stale(status: worktree.WorktreeStatus, stale_days: int) -> bool:
|
|
|
766
776
|
|
|
767
777
|
|
|
768
778
|
@app.command("status")
|
|
769
|
-
def status_cmd( # noqa:
|
|
779
|
+
def status_cmd( # noqa: PLR0915
|
|
770
780
|
stale_days: Annotated[
|
|
771
781
|
int,
|
|
772
782
|
typer.Option("--stale-days", "-s", help="Highlight worktrees inactive for N+ days"),
|
|
773
783
|
] = 7,
|
|
774
|
-
porcelain: Annotated[
|
|
775
|
-
bool,
|
|
776
|
-
typer.Option("--porcelain", "-p", help="Machine-readable output"),
|
|
777
|
-
] = False,
|
|
778
784
|
json_output: Annotated[
|
|
779
785
|
bool,
|
|
780
786
|
typer.Option("--json", help="Output as JSON for automation"),
|
|
@@ -818,20 +824,6 @@ def status_cmd( # noqa: PLR0912, PLR0915
|
|
|
818
824
|
print(json.dumps({"worktrees": data, "stale_days": stale_days}))
|
|
819
825
|
return
|
|
820
826
|
|
|
821
|
-
if porcelain:
|
|
822
|
-
# Machine-readable: name\tbranch\tmodified\tstaged\tuntracked\tahead\tbehind\ttimestamp
|
|
823
|
-
for wt in worktrees:
|
|
824
|
-
status = worktree.get_worktree_status(wt.path)
|
|
825
|
-
if status:
|
|
826
|
-
print(
|
|
827
|
-
f"{wt.name}\t{wt.branch or ''}\t"
|
|
828
|
-
f"{status.modified}\t{status.staged}\t{status.untracked}\t"
|
|
829
|
-
f"{status.ahead}\t{status.behind}\t{status.last_commit_timestamp or ''}",
|
|
830
|
-
)
|
|
831
|
-
else:
|
|
832
|
-
print(f"{wt.name}\t{wt.branch or ''}\t\t\t\t\t\t")
|
|
833
|
-
return
|
|
834
|
-
|
|
835
827
|
table = Table(title="Dev Environment Status")
|
|
836
828
|
table.add_column("Name", style="cyan")
|
|
837
829
|
table.add_column("Branch", style="green")
|
|
@@ -1039,6 +1031,11 @@ def start_agent(
|
|
|
1039
1031
|
if not agent.is_available():
|
|
1040
1032
|
_error(f"{agent.name} is not installed. Install from: {agent.install_url}")
|
|
1041
1033
|
|
|
1034
|
+
# Write prompt to worktree (makes task available to the agent)
|
|
1035
|
+
if prompt:
|
|
1036
|
+
task_file = _write_prompt_to_worktree(wt.path, prompt)
|
|
1037
|
+
_success(f"Wrote task to {task_file.relative_to(wt.path)}")
|
|
1038
|
+
|
|
1042
1039
|
merged_args = _merge_agent_args(agent, agent_args)
|
|
1043
1040
|
agent_env = _get_agent_env(agent)
|
|
1044
1041
|
_info(f"Starting {agent.name} in {wt.path}...")
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import os
|
|
6
6
|
import shutil
|
|
7
7
|
from abc import ABC
|
|
8
|
+
from pathlib import PurePath
|
|
8
9
|
from typing import TYPE_CHECKING
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
@@ -131,8 +132,6 @@ def _get_parent_process_names() -> list[str]:
|
|
|
131
132
|
- CLI tools that set process.title (like Claude) show their name directly
|
|
132
133
|
"""
|
|
133
134
|
try:
|
|
134
|
-
from pathlib import PurePath # noqa: PLC0415
|
|
135
|
-
|
|
136
135
|
import psutil # noqa: PLC0415
|
|
137
136
|
|
|
138
137
|
process = psutil.Process(os.getpid())
|