luv-cli 0.6.1__tar.gz → 0.6.3__tar.gz
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.
- {luv_cli-0.6.1 → luv_cli-0.6.3}/PKG-INFO +4 -4
- {luv_cli-0.6.1 → luv_cli-0.6.3}/README.md +2 -2
- {luv_cli-0.6.1 → luv_cli-0.6.3}/luv/__init__.py +13 -8
- {luv_cli-0.6.1 → luv_cli-0.6.3}/pyproject.toml +1 -1
- {luv_cli-0.6.1 → luv_cli-0.6.3}/tests/test_agents.py +29 -1
- {luv_cli-0.6.1 → luv_cli-0.6.3}/.claude/settings.json +0 -0
- {luv_cli-0.6.1 → luv_cli-0.6.3}/.failproofai/policies/version-policies.mjs +0 -0
- {luv_cli-0.6.1 → luv_cli-0.6.3}/.failproofai/policies-config.json +0 -0
- {luv_cli-0.6.1 → luv_cli-0.6.3}/.github/workflows/publish.yml +0 -0
- {luv_cli-0.6.1 → luv_cli-0.6.3}/.gitignore +0 -0
- {luv_cli-0.6.1 → luv_cli-0.6.3}/LICENSE +0 -0
- {luv_cli-0.6.1 → luv_cli-0.6.3}/claude-output.txt +0 -0
- {luv_cli-0.6.1 → luv_cli-0.6.3}/docs/configuration.md +0 -0
- {luv_cli-0.6.1 → luv_cli-0.6.3}/docs/remote-sessions.md +0 -0
- {luv_cli-0.6.1 → luv_cli-0.6.3}/docs/sessions.md +0 -0
- {luv_cli-0.6.1 → luv_cli-0.6.3}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: luv-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: Launch Claude Code or Codex agents on GitHub repos with isolated workspaces and optional Docker dev environments
|
|
5
5
|
Project-URL: Homepage, https://github.com/exospherehost/luv
|
|
6
6
|
Project-URL: Repository, https://github.com/exospherehost/luv
|
|
@@ -89,7 +89,7 @@ luv continue
|
|
|
89
89
|
1. Clones the repo into `~/prs/{repo}-{machine}-{number}/`
|
|
90
90
|
2. Creates a new branch `luv-{machine}-{number}`
|
|
91
91
|
3. Configures the selected agent with the workspace's PR conventions
|
|
92
|
-
4. Launches Claude with Opus 5 at max effort, or Codex in YOLO mode
|
|
92
|
+
4. Launches Claude with Opus 5.5 at max effort, or Codex in YOLO mode
|
|
93
93
|
|
|
94
94
|
All workspaces live under `~/prs/`. The number comes from the repo's GitHub issue counter, and `{machine}` is a short name for the machine that created the workspace — by default the hostname, or whatever you set with `luv config set machine mbp`.
|
|
95
95
|
|
|
@@ -149,7 +149,7 @@ also keeps reusing its workspace — it names a workspace, where a URL names a P
|
|
|
149
149
|
| `-r` | Resume: resume the selected agent's last session |
|
|
150
150
|
| `-p` | Launch Claude in plan permission mode (default: `bypassPermissions`) |
|
|
151
151
|
| `-nit` | Non-interactive: run the selected agent and exit (no REPL); Claude streams `stream-json` events to stdout |
|
|
152
|
-
| `-m MODEL` | Model to use; Claude defaults to `claude-opus-5`, while Codex uses its configured CLI default |
|
|
152
|
+
| `-m MODEL` | Model to use; Claude defaults to `claude-opus-5-5`, while Codex uses its configured CLI default |
|
|
153
153
|
| `-b BRANCH` | Base a new workspace off `BRANCH` (clone + branch from it); recorded in `git config luv.base` so the PR can target it |
|
|
154
154
|
| `-e` | Env: pass `LUV_*` environment variables (with prefix stripped) into the session |
|
|
155
155
|
| `-s HOST` | Run on `HOST` over SSH, overriding the configured remote host |
|
|
@@ -66,7 +66,7 @@ luv continue
|
|
|
66
66
|
1. Clones the repo into `~/prs/{repo}-{machine}-{number}/`
|
|
67
67
|
2. Creates a new branch `luv-{machine}-{number}`
|
|
68
68
|
3. Configures the selected agent with the workspace's PR conventions
|
|
69
|
-
4. Launches Claude with Opus 5 at max effort, or Codex in YOLO mode
|
|
69
|
+
4. Launches Claude with Opus 5.5 at max effort, or Codex in YOLO mode
|
|
70
70
|
|
|
71
71
|
All workspaces live under `~/prs/`. The number comes from the repo's GitHub issue counter, and `{machine}` is a short name for the machine that created the workspace — by default the hostname, or whatever you set with `luv config set machine mbp`.
|
|
72
72
|
|
|
@@ -126,7 +126,7 @@ also keeps reusing its workspace — it names a workspace, where a URL names a P
|
|
|
126
126
|
| `-r` | Resume: resume the selected agent's last session |
|
|
127
127
|
| `-p` | Launch Claude in plan permission mode (default: `bypassPermissions`) |
|
|
128
128
|
| `-nit` | Non-interactive: run the selected agent and exit (no REPL); Claude streams `stream-json` events to stdout |
|
|
129
|
-
| `-m MODEL` | Model to use; Claude defaults to `claude-opus-5`, while Codex uses its configured CLI default |
|
|
129
|
+
| `-m MODEL` | Model to use; Claude defaults to `claude-opus-5-5`, while Codex uses its configured CLI default |
|
|
130
130
|
| `-b BRANCH` | Base a new workspace off `BRANCH` (clone + branch from it); recorded in `git config luv.base` so the PR can target it |
|
|
131
131
|
| `-e` | Env: pass `LUV_*` environment variables (with prefix stripped) into the session |
|
|
132
132
|
| `-s HOST` | Run on `HOST` over SSH, overriding the configured remote host |
|
|
@@ -59,12 +59,14 @@ def die(msg: str) -> None:
|
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
def run(cmd: list[str], *, cwd: str | None = None, timeout: float | None = None,
|
|
62
|
-
stdin: int | None = None
|
|
62
|
+
stdin: int | None = None,
|
|
63
|
+
new_session: bool = False) -> subprocess.CompletedProcess:
|
|
63
64
|
"""Capture a subprocess. A timeout surfaces as a failure, not an exception,
|
|
64
65
|
so callers keep their plain returncode checks."""
|
|
65
66
|
try:
|
|
66
67
|
return subprocess.run(cmd, capture_output=True, text=True, cwd=cwd,
|
|
67
|
-
timeout=timeout, stdin=stdin
|
|
68
|
+
timeout=timeout, stdin=stdin,
|
|
69
|
+
start_new_session=new_session)
|
|
68
70
|
except subprocess.TimeoutExpired:
|
|
69
71
|
return subprocess.CompletedProcess(cmd, 124, "", f"timed out after {timeout}s")
|
|
70
72
|
|
|
@@ -773,10 +775,13 @@ def shell_env() -> dict[str, str]:
|
|
|
773
775
|
code = ("import json,os,sys;"
|
|
774
776
|
f"sys.stdout.write({marker!r} + json.dumps(dict(os.environ)))")
|
|
775
777
|
probe = f"{shlex.quote(sys.executable)} -c {shlex.quote(code)}"
|
|
776
|
-
#
|
|
777
|
-
#
|
|
778
|
+
# A session of its own, so the probe has no controlling tty to touch. An
|
|
779
|
+
# interactive zsh opens /dev/tty whatever stdin is and makes its own process
|
|
780
|
+
# group the foreground one, then execs the probe rather than forking it, so
|
|
781
|
+
# nothing is left to hand the terminal back. luv and the agent it execs would
|
|
782
|
+
# be in the background, and the agent's first tcsetattr gets SIGTTOU.
|
|
778
783
|
r = run([shell, "-lic", probe], timeout=SHELL_ENV_TIMEOUT,
|
|
779
|
-
stdin=subprocess.DEVNULL)
|
|
784
|
+
stdin=subprocess.DEVNULL, new_session=True)
|
|
780
785
|
# Match on the marker, not the exit status: rc files greet, warn, and fail
|
|
781
786
|
# in ways that say nothing about whether the environment came back. What
|
|
782
787
|
# precedes the marker is the greeting; raw_decode drops anything an exit
|
|
@@ -1204,7 +1209,7 @@ def resume(clone_dir: Path, extra_env: dict[str, str] | None = None,
|
|
|
1204
1209
|
|
|
1205
1210
|
if agent == "claude":
|
|
1206
1211
|
agent_cmd = ["claude", "--dangerously-skip-permissions",
|
|
1207
|
-
"--model", model or "claude-opus-5",
|
|
1212
|
+
"--model", model or "claude-opus-5-5",
|
|
1208
1213
|
"--effort", "max", "--resume",
|
|
1209
1214
|
"--remote-control",
|
|
1210
1215
|
"--remote-control-session-name-prefix", clone_dir.name]
|
|
@@ -1260,7 +1265,7 @@ def launch(clone_dir: Path, prompt: str | None, plan_mode: bool = False,
|
|
|
1260
1265
|
agent_cmd.append(prompt)
|
|
1261
1266
|
else:
|
|
1262
1267
|
common_flags = ["--dangerously-skip-permissions",
|
|
1263
|
-
"--model", model or "claude-opus-5",
|
|
1268
|
+
"--model", model or "claude-opus-5-5",
|
|
1264
1269
|
"--effort", "max",
|
|
1265
1270
|
"--remote-control",
|
|
1266
1271
|
"--remote-control-session-name-prefix", clone_dir.name]
|
|
@@ -3586,7 +3591,7 @@ Flags:
|
|
|
3586
3591
|
-r resume: resume the selected agent's last session
|
|
3587
3592
|
-p launch Claude in plan permission mode (default: bypassPermissions)
|
|
3588
3593
|
-nit non-interactive: run the selected agent and exit (no REPL)
|
|
3589
|
-
-m MODEL model to use (Claude default: claude-opus-5; Codex: CLI default)
|
|
3594
|
+
-m MODEL model to use (Claude default: claude-opus-5-5; Codex: CLI default)
|
|
3590
3595
|
-b BRANCH base a new workspace off BRANCH (clone + branch from it; recorded in git config luv.base)
|
|
3591
3596
|
-e env: pass LUV_* environment variables (with prefix stripped) into the session
|
|
3592
3597
|
-s HOST run on HOST over SSH (overrides the configured remote host)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "luv-cli"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.3"
|
|
8
8
|
description = "Launch Claude Code or Codex agents on GitHub repos with isolated workspaces and optional Docker dev environments"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
license = "MIT"
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import io
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
5
|
+
import pty
|
|
4
6
|
import re
|
|
7
|
+
import shutil
|
|
5
8
|
import subprocess
|
|
6
9
|
import tempfile
|
|
7
10
|
import time
|
|
@@ -66,7 +69,7 @@ class AgentLaunchTests(unittest.TestCase):
|
|
|
66
69
|
_, argv = self._exec_args(luv.launch, self.clone_dir, None)
|
|
67
70
|
|
|
68
71
|
self.assertEqual(argv[0], "/bin/claude")
|
|
69
|
-
self.assertIn("claude-opus-5", argv)
|
|
72
|
+
self.assertIn("claude-opus-5-5", argv)
|
|
70
73
|
self.assertIn("--dangerously-skip-permissions", argv)
|
|
71
74
|
|
|
72
75
|
def test_main_routes_codex_to_existing_workspace(self):
|
|
@@ -880,8 +883,33 @@ class ShellEnvTests(unittest.TestCase):
|
|
|
880
883
|
self.assertEqual(env, {"API_BASE": "https://x"})
|
|
881
884
|
self.assertEqual(call.args[0][:2], ["/bin/zsh", "-lic"])
|
|
882
885
|
self.assertEqual(call.kwargs["stdin"], subprocess.DEVNULL)
|
|
886
|
+
self.assertTrue(call.kwargs["new_session"])
|
|
883
887
|
self.assertEqual(call.kwargs["timeout"], luv.SHELL_ENV_TIMEOUT)
|
|
884
888
|
|
|
889
|
+
@unittest.skipUnless(shutil.which("zsh"), "needs zsh")
|
|
890
|
+
def test_probe_leaves_the_terminal_in_the_foreground_of_the_caller(self):
|
|
891
|
+
# A real zsh on a real pty, because only that shows the failure: zsh
|
|
892
|
+
# takes the foreground and execs the probe, so the caller, and the agent
|
|
893
|
+
# it later execs, would be stopped by SIGTTOU on touching the terminal.
|
|
894
|
+
with tempfile.TemporaryDirectory() as zdotdir:
|
|
895
|
+
pid, fd = pty.fork()
|
|
896
|
+
if pid == 0:
|
|
897
|
+
status = 1
|
|
898
|
+
try:
|
|
899
|
+
with patch.dict(luv.os.environ,
|
|
900
|
+
{"SHELL": "zsh", "ZDOTDIR": zdotdir}):
|
|
901
|
+
luv.shell_env()
|
|
902
|
+
status = 0 if os.tcgetpgrp(0) == os.getpgrp() else 1
|
|
903
|
+
finally:
|
|
904
|
+
os._exit(status)
|
|
905
|
+
with contextlib.suppress(OSError):
|
|
906
|
+
while os.read(fd, 4096):
|
|
907
|
+
pass
|
|
908
|
+
os.close(fd)
|
|
909
|
+
_, status = os.waitpid(pid, 0)
|
|
910
|
+
|
|
911
|
+
self.assertEqual(os.waitstatus_to_exitcode(status), 0)
|
|
912
|
+
|
|
885
913
|
def test_greeting_before_the_marker_is_ignored(self):
|
|
886
914
|
env, _ = self._probe(_shell_env_output({"A": "1"}, before="welcome back!\n"))
|
|
887
915
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|