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.
Files changed (73) hide show
  1. agent_cli/_extras.json +13 -0
  2. agent_cli/_requirements/.gitkeep +0 -0
  3. agent_cli/_requirements/audio.txt +79 -0
  4. agent_cli/_requirements/faster-whisper.txt +215 -0
  5. agent_cli/_requirements/kokoro.txt +425 -0
  6. agent_cli/_requirements/llm.txt +183 -0
  7. agent_cli/_requirements/memory.txt +355 -0
  8. agent_cli/_requirements/mlx-whisper.txt +222 -0
  9. agent_cli/_requirements/piper.txt +176 -0
  10. agent_cli/_requirements/rag.txt +402 -0
  11. agent_cli/_requirements/server.txt +154 -0
  12. agent_cli/_requirements/speed.txt +77 -0
  13. agent_cli/_requirements/vad.txt +155 -0
  14. agent_cli/agents/assistant.py +3 -1
  15. agent_cli/agents/autocorrect.py +5 -2
  16. agent_cli/agents/chat.py +3 -1
  17. agent_cli/agents/memory/__init__.py +2 -1
  18. agent_cli/agents/memory/add.py +2 -0
  19. agent_cli/agents/memory/proxy.py +7 -12
  20. agent_cli/agents/rag_proxy.py +5 -10
  21. agent_cli/agents/speak.py +3 -1
  22. agent_cli/agents/transcribe.py +7 -2
  23. agent_cli/agents/transcribe_daemon.py +3 -1
  24. agent_cli/agents/voice_edit.py +3 -1
  25. agent_cli/cli.py +19 -3
  26. agent_cli/config_cmd.py +1 -0
  27. agent_cli/core/chroma.py +4 -4
  28. agent_cli/core/deps.py +177 -25
  29. agent_cli/core/openai_proxy.py +9 -4
  30. agent_cli/core/process.py +2 -2
  31. agent_cli/core/reranker.py +5 -4
  32. agent_cli/core/utils.py +5 -3
  33. agent_cli/core/vad.py +2 -1
  34. agent_cli/core/watch.py +8 -6
  35. agent_cli/dev/cli.py +31 -34
  36. agent_cli/dev/coding_agents/base.py +1 -2
  37. agent_cli/dev/skill/SKILL.md +141 -0
  38. agent_cli/dev/skill/examples.md +571 -0
  39. agent_cli/dev/worktree.py +53 -5
  40. agent_cli/docs_gen.py +12 -42
  41. agent_cli/install/__init__.py +1 -1
  42. agent_cli/install/extras.py +174 -0
  43. agent_cli/memory/__init__.py +1 -18
  44. agent_cli/memory/_files.py +4 -1
  45. agent_cli/memory/_indexer.py +3 -2
  46. agent_cli/memory/_ingest.py +6 -5
  47. agent_cli/memory/_retrieval.py +18 -8
  48. agent_cli/memory/_streaming.py +2 -2
  49. agent_cli/memory/api.py +1 -1
  50. agent_cli/memory/client.py +1 -1
  51. agent_cli/memory/engine.py +1 -1
  52. agent_cli/rag/__init__.py +0 -19
  53. agent_cli/rag/_indexer.py +3 -2
  54. agent_cli/rag/api.py +1 -0
  55. agent_cli/scripts/.runtime/.gitkeep +0 -0
  56. agent_cli/scripts/check_plugin_skill_sync.py +50 -0
  57. agent_cli/scripts/sync_extras.py +138 -0
  58. agent_cli/server/cli.py +26 -24
  59. agent_cli/server/common.py +3 -4
  60. agent_cli/server/tts/api.py +1 -1
  61. agent_cli/server/whisper/backends/faster_whisper.py +30 -23
  62. agent_cli/server/whisper/wyoming_handler.py +22 -27
  63. agent_cli/services/_wyoming_utils.py +4 -2
  64. agent_cli/services/asr.py +13 -3
  65. agent_cli/services/llm.py +2 -1
  66. agent_cli/services/tts.py +5 -2
  67. agent_cli/services/wake_word.py +6 -3
  68. {agent_cli-0.61.2.dist-info → agent_cli-0.70.2.dist-info}/METADATA +168 -73
  69. {agent_cli-0.61.2.dist-info → agent_cli-0.70.2.dist-info}/RECORD +72 -54
  70. {agent_cli-0.61.2.dist-info → agent_cli-0.70.2.dist-info}/WHEEL +1 -2
  71. agent_cli-0.61.2.dist-info/top_level.txt +0 -1
  72. {agent_cli-0.61.2.dist-info → agent_cli-0.70.2.dist-info}/entry_points.txt +0 -0
  73. {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 sys
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
- # Dependencies that don't support Python 3.14 yet
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
- def ensure_optional_dependencies(
13
- required: dict[str, str],
14
- *,
15
- extra_name: str,
16
- install_hint: str | None = None,
17
- ) -> None:
18
- """Ensure optional dependencies are present, otherwise raise ImportError."""
19
- missing = [
20
- pkg_name for module_name, pkg_name in required.items() if find_spec(module_name) is None
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
- # Check if running on Python 3.14+ with incompatible dependencies
26
- is_py314 = sys.version_info >= (3, 14)
27
- incompatible = set(missing) & _PYTHON_314_INCOMPATIBLE
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
- if is_py314 and incompatible:
30
- msg = (
31
- f"The '{extra_name}' feature requires {', '.join(sorted(incompatible))}, "
32
- f"which {'does' if len(incompatible) == 1 else 'do'} not support Python 3.14 yet. "
33
- f"Please use Python 3.13 or earlier for this feature."
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
- raise ImportError(msg)
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
- hint = install_hint or f"`pip install agent-cli[{extra_name}]`"
38
- msg = f"Missing required dependencies for {extra_name}: {', '.join(missing)}. Please install with {hint}."
39
- raise ImportError(msg)
191
+ return decorator
@@ -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
@@ -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
- from watchfiles import Change, awatch
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Callable
10
11
 
11
- ChangeHandler = Callable[[Change, Path], None]
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: ChangeHandler,
23
+ handler: Callable[[Change, Path], None],
24
24
  *,
25
25
  skip_hidden: bool = True,
26
- ignore_filter: PathFilter | None = None,
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: PLR0912, PLR0915
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())