luv-cli 0.0.17__tar.gz → 0.0.18__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.17 → luv_cli-0.0.18}/PKG-INFO +2 -1
- {luv_cli-0.0.17 → luv_cli-0.0.18}/README.md +1 -0
- {luv_cli-0.0.17 → luv_cli-0.0.18}/luv/__init__.py +42 -21
- {luv_cli-0.0.17 → luv_cli-0.0.18}/pyproject.toml +1 -1
- {luv_cli-0.0.17 → luv_cli-0.0.18}/.claude/settings.json +0 -0
- {luv_cli-0.0.17 → luv_cli-0.0.18}/.failproofai/policies-config.json +0 -0
- {luv_cli-0.0.17 → luv_cli-0.0.18}/.github/workflows/publish.yml +0 -0
- {luv_cli-0.0.17 → luv_cli-0.0.18}/.gitignore +0 -0
- {luv_cli-0.0.17 → luv_cli-0.0.18}/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.18
|
|
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
|
|
@@ -97,6 +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-7`); passed through to `claude --model`, so aliases like `opus`/`sonnet`/`haiku` work |
|
|
100
101
|
| `-e` | Env: pass `LUV_*` environment variables (with prefix stripped) into the session |
|
|
101
102
|
| `-f`, `--force` | Skip safety checks (with `--clean`) |
|
|
102
103
|
| `--safe` | With `--clean -f`, only delete workspaces older than 24h (mtime) |
|
|
@@ -74,6 +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-7`); passed through to `claude --model`, so aliases like `opus`/`sonnet`/`haiku` work |
|
|
77
78
|
| `-e` | Env: pass `LUV_*` environment variables (with prefix stripped) into the session |
|
|
78
79
|
| `-f`, `--force` | Skip safety checks (with `--clean`) |
|
|
79
80
|
| `--safe` | With `--clean -f`, only delete workspaces older than 24h (mtime) |
|
|
@@ -316,8 +316,10 @@ def navigate(clone_dir: Path, extra_env: dict[str, str] = {}) -> None:
|
|
|
316
316
|
os.execv(shell, [shell])
|
|
317
317
|
|
|
318
318
|
|
|
319
|
-
def resume(clone_dir: Path, extra_env: dict[str, str]
|
|
319
|
+
def resume(clone_dir: Path, extra_env: dict[str, str] | None = None,
|
|
320
|
+
model: str = "claude-opus-4-7") -> None:
|
|
320
321
|
"""Trust, chdir, and exec claude --resume — replacing this process."""
|
|
322
|
+
extra_env = extra_env or {}
|
|
321
323
|
trust_project(clone_dir)
|
|
322
324
|
os.chdir(str(clone_dir))
|
|
323
325
|
settings = load_luv_settings(clone_dir)
|
|
@@ -330,7 +332,7 @@ def resume(clone_dir: Path, extra_env: dict[str, str] = {}) -> None:
|
|
|
330
332
|
base = docker_compose_base(clone_dir, compose_file, project)
|
|
331
333
|
r = subprocess.run(base + ["exec", "-it"] + docker_env_flags(extra_env) + ["dev-environment",
|
|
332
334
|
"claude", "--dangerously-skip-permissions",
|
|
333
|
-
"--model",
|
|
335
|
+
"--model", model,
|
|
334
336
|
"--effort", "max", "--resume",
|
|
335
337
|
"--remote-control",
|
|
336
338
|
"--remote-control-session-name-prefix", clone_dir.name])
|
|
@@ -343,21 +345,23 @@ def resume(clone_dir: Path, extra_env: dict[str, str] = {}) -> None:
|
|
|
343
345
|
die("'claude' not found in PATH")
|
|
344
346
|
os.environ.update(extra_env)
|
|
345
347
|
os.execv(claude_bin, [claude_bin, "--dangerously-skip-permissions",
|
|
346
|
-
"--model",
|
|
348
|
+
"--model", model, "--effort", "max", "--resume",
|
|
347
349
|
"--remote-control",
|
|
348
350
|
"--remote-control-session-name-prefix", clone_dir.name])
|
|
349
351
|
|
|
350
352
|
|
|
351
353
|
def launch(clone_dir: Path, prompt: str | None, plan_mode: bool = False,
|
|
352
|
-
non_interactive: bool = False, extra_env: dict[str, str]
|
|
354
|
+
non_interactive: bool = False, extra_env: dict[str, str] | None = None,
|
|
355
|
+
model: str = "claude-opus-4-7") -> None:
|
|
353
356
|
"""Trust, resolve claude, chdir, and exec — replacing this process."""
|
|
357
|
+
extra_env = extra_env or {}
|
|
354
358
|
trust_project(clone_dir)
|
|
355
359
|
os.chdir(str(clone_dir))
|
|
356
360
|
settings = load_luv_settings(clone_dir)
|
|
357
361
|
compose_file = (settings or {}).get("compose_file")
|
|
358
362
|
|
|
359
363
|
common_flags = ["--dangerously-skip-permissions",
|
|
360
|
-
"--model",
|
|
364
|
+
"--model", model,
|
|
361
365
|
"--effort", "max",
|
|
362
366
|
"--remote-control",
|
|
363
367
|
"--remote-control-session-name-prefix", clone_dir.name]
|
|
@@ -504,8 +508,9 @@ def find_latest_clone(repo: str) -> Path | None:
|
|
|
504
508
|
return best
|
|
505
509
|
|
|
506
510
|
|
|
507
|
-
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] =
|
|
511
|
+
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-7") -> None:
|
|
508
512
|
"""Open an existing work folder or remote branch by number."""
|
|
513
|
+
extra_env = extra_env or {}
|
|
509
514
|
clone_dir = PRS_DIR / f"{repo}-{number}"
|
|
510
515
|
|
|
511
516
|
# 1. Local folder takes priority
|
|
@@ -515,9 +520,9 @@ def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode
|
|
|
515
520
|
if nav_mode:
|
|
516
521
|
navigate(clone_dir, extra_env=extra_env)
|
|
517
522
|
elif resume_mode:
|
|
518
|
-
resume(clone_dir, extra_env=extra_env)
|
|
523
|
+
resume(clone_dir, extra_env=extra_env, model=model)
|
|
519
524
|
else:
|
|
520
|
-
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env)
|
|
525
|
+
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env, model=model)
|
|
521
526
|
return # unreachable
|
|
522
527
|
|
|
523
528
|
# 2. Check remote branch luv-{number}
|
|
@@ -542,13 +547,14 @@ def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode
|
|
|
542
547
|
if nav_mode:
|
|
543
548
|
navigate(clone_dir, extra_env=extra_env)
|
|
544
549
|
elif resume_mode:
|
|
545
|
-
resume(clone_dir, extra_env=extra_env)
|
|
550
|
+
resume(clone_dir, extra_env=extra_env, model=model)
|
|
546
551
|
else:
|
|
547
|
-
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env)
|
|
552
|
+
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env, model=model)
|
|
548
553
|
|
|
549
554
|
|
|
550
|
-
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] =
|
|
555
|
+
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-7") -> None:
|
|
551
556
|
"""Open any GitHub PR by org/repo/number, cloning if needed."""
|
|
557
|
+
extra_env = extra_env or {}
|
|
552
558
|
clone_dir = PRS_DIR / f"{repo}-{number}"
|
|
553
559
|
|
|
554
560
|
if clone_dir.exists():
|
|
@@ -557,9 +563,9 @@ def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool
|
|
|
557
563
|
if nav_mode:
|
|
558
564
|
navigate(clone_dir, extra_env=extra_env)
|
|
559
565
|
elif resume_mode:
|
|
560
|
-
resume(clone_dir, extra_env=extra_env)
|
|
566
|
+
resume(clone_dir, extra_env=extra_env, model=model)
|
|
561
567
|
else:
|
|
562
|
-
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env)
|
|
568
|
+
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env, model=model)
|
|
563
569
|
return # unreachable
|
|
564
570
|
|
|
565
571
|
# Resolve the actual branch name via GitHub API
|
|
@@ -584,9 +590,9 @@ def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool
|
|
|
584
590
|
if nav_mode:
|
|
585
591
|
navigate(clone_dir, extra_env=extra_env)
|
|
586
592
|
elif resume_mode:
|
|
587
|
-
resume(clone_dir, extra_env=extra_env)
|
|
593
|
+
resume(clone_dir, extra_env=extra_env, model=model)
|
|
588
594
|
else:
|
|
589
|
-
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env)
|
|
595
|
+
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env, model=model)
|
|
590
596
|
|
|
591
597
|
|
|
592
598
|
def main() -> None:
|
|
@@ -599,6 +605,20 @@ def main() -> None:
|
|
|
599
605
|
force = "-f" in args or "--force" in args
|
|
600
606
|
safe = "--safe" in args
|
|
601
607
|
env_mode = "-e" in args
|
|
608
|
+
|
|
609
|
+
# -m takes a value, so extract it before the boolean-flag strip below
|
|
610
|
+
model = "claude-opus-4-7"
|
|
611
|
+
if args.count("-m") > 1:
|
|
612
|
+
die("-m may only be provided once")
|
|
613
|
+
if "-m" in args:
|
|
614
|
+
idx = args.index("-m")
|
|
615
|
+
if idx + 1 >= len(args):
|
|
616
|
+
die("-m requires a model name")
|
|
617
|
+
model = args[idx + 1].strip()
|
|
618
|
+
if not model or model.startswith("-"):
|
|
619
|
+
die("-m requires a model name")
|
|
620
|
+
args = args[:idx] + args[idx + 2:]
|
|
621
|
+
|
|
602
622
|
args = [a for a in args if a not in ("-n", "-r", "-e", "-f", "--force", "-p", "-nit", "--safe")]
|
|
603
623
|
extra_env = collect_luv_env() if env_mode else {}
|
|
604
624
|
|
|
@@ -611,6 +631,7 @@ Flags:
|
|
|
611
631
|
-r resume: resume the last Claude session in the work folder
|
|
612
632
|
-p launch Claude in plan permission mode (default: bypassPermissions)
|
|
613
633
|
-nit non-interactive: run claude -p <prompt> and exit (no REPL)
|
|
634
|
+
-m MODEL claude model to use (default: claude-opus-4-7)
|
|
614
635
|
-e env: pass LUV_* environment variables (with prefix stripped) into the session
|
|
615
636
|
-f, --force (with --clean) skip safety checks and delete all work folders
|
|
616
637
|
--safe (with --clean -f) only delete folders older than 24h
|
|
@@ -656,7 +677,7 @@ Docker:
|
|
|
656
677
|
die(f"cannot parse PR URL: {url}")
|
|
657
678
|
org, repo, number = m.group(1), m.group(2), int(m.group(3))
|
|
658
679
|
prompt = " ".join(args[2:]) or None
|
|
659
|
-
open_pr(org, repo, number, prompt, nav_mode, resume_mode, plan_mode, non_interactive, extra_env=extra_env)
|
|
680
|
+
open_pr(org, repo, number, prompt, nav_mode, resume_mode, plan_mode, non_interactive, extra_env=extra_env, model=model)
|
|
660
681
|
return
|
|
661
682
|
|
|
662
683
|
raw = args[0].rstrip("/")
|
|
@@ -676,14 +697,14 @@ Docker:
|
|
|
676
697
|
die(f"expected a PR number after -pr, got '{args[idx + 1]}'")
|
|
677
698
|
prompt_parts = [a for i, a in enumerate(args) if i not in (0, idx, idx + 1)]
|
|
678
699
|
prompt = " ".join(prompt_parts) or None
|
|
679
|
-
open_pr(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode, plan_mode, non_interactive, extra_env=extra_env)
|
|
700
|
+
open_pr(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode, plan_mode, non_interactive, extra_env=extra_env, model=model)
|
|
680
701
|
return
|
|
681
702
|
|
|
682
703
|
# Detect optional numeric second argument
|
|
683
704
|
if len(args) > 1 and args[1].isdigit():
|
|
684
705
|
number = int(args[1])
|
|
685
706
|
prompt = " ".join(args[2:]) or None
|
|
686
|
-
open_existing(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode, plan_mode, non_interactive, extra_env=extra_env)
|
|
707
|
+
open_existing(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode, plan_mode, non_interactive, extra_env=extra_env, model=model)
|
|
687
708
|
return
|
|
688
709
|
|
|
689
710
|
org = resolve_org(explicit_org)
|
|
@@ -698,7 +719,7 @@ Docker:
|
|
|
698
719
|
if nav_mode:
|
|
699
720
|
navigate(clone_dir, extra_env=extra_env)
|
|
700
721
|
else:
|
|
701
|
-
resume(clone_dir, extra_env=extra_env)
|
|
722
|
+
resume(clone_dir, extra_env=extra_env, model=model)
|
|
702
723
|
return
|
|
703
724
|
|
|
704
725
|
# 1. Verify repo exists
|
|
@@ -750,6 +771,6 @@ Docker:
|
|
|
750
771
|
if nav_mode:
|
|
751
772
|
navigate(clone_dir, extra_env=extra_env)
|
|
752
773
|
elif resume_mode:
|
|
753
|
-
resume(clone_dir, extra_env=extra_env)
|
|
774
|
+
resume(clone_dir, extra_env=extra_env, model=model)
|
|
754
775
|
else:
|
|
755
|
-
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env)
|
|
776
|
+
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env, model=model)
|
|
@@ -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.18"
|
|
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
|