luv-cli 0.0.19__tar.gz → 0.0.21__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.0.19 → luv_cli-0.0.21}/PKG-INFO +3 -3
- {luv_cli-0.0.19 → luv_cli-0.0.21}/README.md +2 -2
- {luv_cli-0.0.19 → luv_cli-0.0.21}/luv/__init__.py +36 -18
- {luv_cli-0.0.19 → luv_cli-0.0.21}/pyproject.toml +1 -1
- {luv_cli-0.0.19 → luv_cli-0.0.21}/.claude/settings.json +0 -0
- {luv_cli-0.0.19 → luv_cli-0.0.21}/.failproofai/policies-config.json +0 -0
- {luv_cli-0.0.19 → luv_cli-0.0.21}/.github/workflows/publish.yml +0 -0
- {luv_cli-0.0.19 → luv_cli-0.0.21}/.gitignore +0 -0
- {luv_cli-0.0.19 → luv_cli-0.0.21}/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: luv-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.21
|
|
4
4
|
Summary: Launch Claude Code 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
|
|
@@ -72,7 +72,7 @@ luv --clean
|
|
|
72
72
|
1. Clones the repo into `~/prs/{repo}-{number}/`
|
|
73
73
|
2. Creates a new branch `luv-{number}`
|
|
74
74
|
3. Trusts the project in Claude Code config
|
|
75
|
-
4. Launches Claude with Opus
|
|
75
|
+
4. Launches Claude with Opus 4.8 at max effort; prompt-less sessions auto-run `/effort ultracode` as their first input to enter ultracode mode
|
|
76
76
|
|
|
77
77
|
All workspaces live under `~/prs/`. The number comes from the repo's GitHub issue counter to avoid collisions.
|
|
78
78
|
|
|
@@ -97,7 +97,7 @@ All workspaces live under `~/prs/`. The number comes from the repo's GitHub issu
|
|
|
97
97
|
| `-r` | Resume: resume the last Claude session |
|
|
98
98
|
| `-p` | Launch Claude in plan permission mode (default: `bypassPermissions`) |
|
|
99
99
|
| `-nit` | Non-interactive: run `claude -p <prompt>` and exit (no REPL); streams `stream-json` events to stdout |
|
|
100
|
-
| `-m MODEL` | Claude model to use (default: `claude-opus-4-
|
|
100
|
+
| `-m MODEL` | Claude model to use (default: `claude-opus-4-8`); passed through to `claude --model`, so aliases like `opus`/`sonnet`/`haiku` work |
|
|
101
101
|
| `-e` | Env: pass `LUV_*` environment variables (with prefix stripped) into the session |
|
|
102
102
|
| `-f`, `--force` | Skip safety checks (with `--clean`) |
|
|
103
103
|
| `--safe` | With `--clean -f`, only delete workspaces older than 24h (mtime) |
|
|
@@ -49,7 +49,7 @@ luv --clean
|
|
|
49
49
|
1. Clones the repo into `~/prs/{repo}-{number}/`
|
|
50
50
|
2. Creates a new branch `luv-{number}`
|
|
51
51
|
3. Trusts the project in Claude Code config
|
|
52
|
-
4. Launches Claude with Opus
|
|
52
|
+
4. Launches Claude with Opus 4.8 at max effort; prompt-less sessions auto-run `/effort ultracode` as their first input to enter ultracode mode
|
|
53
53
|
|
|
54
54
|
All workspaces live under `~/prs/`. The number comes from the repo's GitHub issue counter to avoid collisions.
|
|
55
55
|
|
|
@@ -74,7 +74,7 @@ All workspaces live under `~/prs/`. The number comes from the repo's GitHub issu
|
|
|
74
74
|
| `-r` | Resume: resume the last Claude session |
|
|
75
75
|
| `-p` | Launch Claude in plan permission mode (default: `bypassPermissions`) |
|
|
76
76
|
| `-nit` | Non-interactive: run `claude -p <prompt>` and exit (no REPL); streams `stream-json` events to stdout |
|
|
77
|
-
| `-m MODEL` | Claude model to use (default: `claude-opus-4-
|
|
77
|
+
| `-m MODEL` | Claude model to use (default: `claude-opus-4-8`); passed through to `claude --model`, so aliases like `opus`/`sonnet`/`haiku` work |
|
|
78
78
|
| `-e` | Env: pass `LUV_*` environment variables (with prefix stripped) into the session |
|
|
79
79
|
| `-f`, `--force` | Skip safety checks (with `--clean`) |
|
|
80
80
|
| `--safe` | With `--clean -f`, only delete workspaces older than 24h (mtime) |
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
|
-
import random
|
|
4
3
|
import re
|
|
5
4
|
import shutil
|
|
6
5
|
import stat
|
|
@@ -16,13 +15,6 @@ PRS_DIR = Path.home() / "prs"
|
|
|
16
15
|
CLAUDE_JSON = Path.home() / ".claude.json"
|
|
17
16
|
CLAUDE_SETTINGS_JSON = Path.home() / ".claude" / "settings.json"
|
|
18
17
|
|
|
19
|
-
COLORS = ["red", "blue", "green", "yellow", "purple", "orange", "pink", "cyan", "default"]
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def pick_color() -> str:
|
|
23
|
-
"""Pick a random /color value so each luv session is visually distinct."""
|
|
24
|
-
return random.choice(COLORS)
|
|
25
|
-
|
|
26
18
|
PR_RULES = """
|
|
27
19
|
# Pull Request Management
|
|
28
20
|
|
|
@@ -318,7 +310,7 @@ def navigate(clone_dir: Path, extra_env: dict[str, str] = {}) -> None:
|
|
|
318
310
|
|
|
319
311
|
|
|
320
312
|
def resume(clone_dir: Path, extra_env: dict[str, str] | None = None,
|
|
321
|
-
model: str = "claude-opus-4-
|
|
313
|
+
model: str = "claude-opus-4-8") -> None:
|
|
322
314
|
"""Trust, chdir, and exec claude --resume — replacing this process."""
|
|
323
315
|
extra_env = extra_env or {}
|
|
324
316
|
trust_project(clone_dir)
|
|
@@ -353,7 +345,7 @@ def resume(clone_dir: Path, extra_env: dict[str, str] | None = None,
|
|
|
353
345
|
|
|
354
346
|
def launch(clone_dir: Path, prompt: str | None, plan_mode: bool = False,
|
|
355
347
|
non_interactive: bool = False, extra_env: dict[str, str] | None = None,
|
|
356
|
-
model: str = "claude-opus-4-
|
|
348
|
+
model: str = "claude-opus-4-8") -> None:
|
|
357
349
|
"""Trust, resolve claude, chdir, and exec — replacing this process."""
|
|
358
350
|
extra_env = extra_env or {}
|
|
359
351
|
trust_project(clone_dir)
|
|
@@ -374,10 +366,10 @@ def launch(clone_dir: Path, prompt: str | None, plan_mode: bool = False,
|
|
|
374
366
|
initial_args = ["-p", prompt]
|
|
375
367
|
elif plan_mode:
|
|
376
368
|
mode_flags = ["--permission-mode", "plan"]
|
|
377
|
-
initial_args = [prompt] if prompt else [
|
|
369
|
+
initial_args = [prompt] if prompt else ["/effort ultracode"]
|
|
378
370
|
else:
|
|
379
371
|
mode_flags = ["--permission-mode", "bypassPermissions"]
|
|
380
|
-
initial_args = [prompt] if prompt else [
|
|
372
|
+
initial_args = [prompt] if prompt else ["/effort ultracode"]
|
|
381
373
|
|
|
382
374
|
if compose_file:
|
|
383
375
|
project = docker_project_name(clone_dir)
|
|
@@ -411,10 +403,36 @@ def _on_rm_error(func, path, _exc):
|
|
|
411
403
|
func(path)
|
|
412
404
|
|
|
413
405
|
|
|
406
|
+
def _docker_wipe(path: Path) -> bool:
|
|
407
|
+
"""rm -rf `path` from inside a busybox container so the container's root can
|
|
408
|
+
delete files that a previous container bind-mounted in as root (e.g. Rust
|
|
409
|
+
target dirs built inside the workspace's dev-environment). Returns True on
|
|
410
|
+
success. No-op if docker isn't available."""
|
|
411
|
+
if shutil.which("docker") is None:
|
|
412
|
+
return False
|
|
413
|
+
parent = path.resolve().parent
|
|
414
|
+
name = path.name
|
|
415
|
+
r = subprocess.run(
|
|
416
|
+
["docker", "run", "--rm",
|
|
417
|
+
"-v", f"{parent}:/p",
|
|
418
|
+
"busybox", "rm", "-rf", f"/p/{name}"],
|
|
419
|
+
capture_output=True, text=True,
|
|
420
|
+
)
|
|
421
|
+
if r.returncode != 0:
|
|
422
|
+
sys.stderr.write(f"luv: docker rm fallback failed for {path}: {r.stderr.strip()}\n")
|
|
423
|
+
return False
|
|
424
|
+
return not path.exists()
|
|
425
|
+
|
|
426
|
+
|
|
414
427
|
def _force_rmtree(path: Path) -> None:
|
|
415
|
-
"""rmtree that
|
|
428
|
+
"""rmtree that survives read-only files (chmod-and-retry) and root-owned
|
|
429
|
+
files left behind by Docker bind-mounts (containerized rm -rf fallback)."""
|
|
416
430
|
kwargs = {"onexc": _on_rm_error} if sys.version_info >= (3, 12) else {"onerror": _on_rm_error}
|
|
417
|
-
|
|
431
|
+
try:
|
|
432
|
+
shutil.rmtree(path, **kwargs)
|
|
433
|
+
except PermissionError:
|
|
434
|
+
if not _docker_wipe(path):
|
|
435
|
+
raise
|
|
418
436
|
|
|
419
437
|
|
|
420
438
|
def cmd_clean(force: bool = False, safe: bool = False) -> None:
|
|
@@ -526,7 +544,7 @@ def find_latest_clone(repo: str) -> Path | None:
|
|
|
526
544
|
return best
|
|
527
545
|
|
|
528
546
|
|
|
529
|
-
def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool = False, resume_mode: bool = False, plan_mode: bool = False, non_interactive: bool = False, extra_env: dict[str, str] | None = None, model: str = "claude-opus-4-
|
|
547
|
+
def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool = False, resume_mode: bool = False, plan_mode: bool = False, non_interactive: bool = False, extra_env: dict[str, str] | None = None, model: str = "claude-opus-4-8") -> None:
|
|
530
548
|
"""Open an existing work folder or remote branch by number."""
|
|
531
549
|
extra_env = extra_env or {}
|
|
532
550
|
clone_dir = PRS_DIR / f"{repo}-{number}"
|
|
@@ -570,7 +588,7 @@ def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode
|
|
|
570
588
|
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env, model=model)
|
|
571
589
|
|
|
572
590
|
|
|
573
|
-
def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool = False, resume_mode: bool = False, plan_mode: bool = False, non_interactive: bool = False, extra_env: dict[str, str] | None = None, model: str = "claude-opus-4-
|
|
591
|
+
def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool = False, resume_mode: bool = False, plan_mode: bool = False, non_interactive: bool = False, extra_env: dict[str, str] | None = None, model: str = "claude-opus-4-8") -> None:
|
|
574
592
|
"""Open any GitHub PR by org/repo/number, cloning if needed."""
|
|
575
593
|
extra_env = extra_env or {}
|
|
576
594
|
clone_dir = PRS_DIR / f"{repo}-{number}"
|
|
@@ -625,7 +643,7 @@ def main() -> None:
|
|
|
625
643
|
env_mode = "-e" in args
|
|
626
644
|
|
|
627
645
|
# -m takes a value, so extract it before the boolean-flag strip below
|
|
628
|
-
model = "claude-opus-4-
|
|
646
|
+
model = "claude-opus-4-8"
|
|
629
647
|
if args.count("-m") > 1:
|
|
630
648
|
die("-m may only be provided once")
|
|
631
649
|
if "-m" in args:
|
|
@@ -649,7 +667,7 @@ Flags:
|
|
|
649
667
|
-r resume: resume the last Claude session in the work folder
|
|
650
668
|
-p launch Claude in plan permission mode (default: bypassPermissions)
|
|
651
669
|
-nit non-interactive: run claude -p <prompt> and exit (no REPL)
|
|
652
|
-
-m MODEL claude model to use (default: claude-opus-4-
|
|
670
|
+
-m MODEL claude model to use (default: claude-opus-4-8)
|
|
653
671
|
-e env: pass LUV_* environment variables (with prefix stripped) into the session
|
|
654
672
|
-f, --force (with --clean) skip safety checks and delete all work folders
|
|
655
673
|
--safe (with --clean -f) only delete folders older than 24h
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "luv-cli"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.21"
|
|
8
8
|
description = "Launch Claude Code agents on GitHub repos with isolated workspaces and optional Docker dev environments"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
license = "MIT"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|