Skip to content

Commit 678bf34

Browse files
NiveditJainclaude
andauthored
[luv-24] Add -m MODEL flag to select claude model (#24)
* [luv-24] Add -m MODEL flag to select claude model Previously the model was hardcoded to claude-opus-4-7. Pass -m sonnet (or any value claude --model accepts) to override per invocation; default is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-24] Address CodeRabbit review - Reject repeated -m flags so a stray second -m can't fall through and be treated as positional input. - Replace mutable extra_env={} defaults with None + local init in the four signatures touched by this PR (resume, launch, open_existing, open_pr) to avoid the shared-dict-instance footgun. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Bump to 0.0.18 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-24] Reject flag-like -m values Previously 'luv -m -r myrepo' would swallow -r as the model name and silently drop the flag. Validate that the value is non-empty and does not start with a hyphen. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent faf5f81 commit 678bf34

3 files changed

Lines changed: 44 additions & 22 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ All workspaces live under `~/prs/`. The number comes from the repo's GitHub issu
7474
| `-r` | Resume: resume the last Claude session |
7575
| `-p` | Launch Claude in plan permission mode (default: `bypassPermissions`) |
7676
| `-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 |
7778
| `-e` | Env: pass `LUV_*` environment variables (with prefix stripped) into the session |
7879
| `-f`, `--force` | Skip safety checks (with `--clean`) |
7980
| `--safe` | With `--clean -f`, only delete workspaces older than 24h (mtime) |

luv/__init__.py

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -316,8 +316,10 @@ def navigate(clone_dir: Path, extra_env: dict[str, str] = {}) -> None:
316316
os.execv(shell, [shell])
317317

318318

319-
def resume(clone_dir: Path, extra_env: dict[str, str] = {}) -> None:
319+
def resume(clone_dir: Path, extra_env: dict[str, str] | None = None,
320+
model: str = "claude-opus-4-7") -> None:
320321
"""Trust, chdir, and exec claude --resume — replacing this process."""
322+
extra_env = extra_env or {}
321323
trust_project(clone_dir)
322324
os.chdir(str(clone_dir))
323325
settings = load_luv_settings(clone_dir)
@@ -330,7 +332,7 @@ def resume(clone_dir: Path, extra_env: dict[str, str] = {}) -> None:
330332
base = docker_compose_base(clone_dir, compose_file, project)
331333
r = subprocess.run(base + ["exec", "-it"] + docker_env_flags(extra_env) + ["dev-environment",
332334
"claude", "--dangerously-skip-permissions",
333-
"--model", "claude-opus-4-7",
335+
"--model", model,
334336
"--effort", "max", "--resume",
335337
"--remote-control",
336338
"--remote-control-session-name-prefix", clone_dir.name])
@@ -343,21 +345,23 @@ def resume(clone_dir: Path, extra_env: dict[str, str] = {}) -> None:
343345
die("'claude' not found in PATH")
344346
os.environ.update(extra_env)
345347
os.execv(claude_bin, [claude_bin, "--dangerously-skip-permissions",
346-
"--model", "claude-opus-4-7", "--effort", "max", "--resume",
348+
"--model", model, "--effort", "max", "--resume",
347349
"--remote-control",
348350
"--remote-control-session-name-prefix", clone_dir.name])
349351

350352

351353
def launch(clone_dir: Path, prompt: str | None, plan_mode: bool = False,
352-
non_interactive: bool = False, extra_env: dict[str, str] = {}) -> None:
354+
non_interactive: bool = False, extra_env: dict[str, str] | None = None,
355+
model: str = "claude-opus-4-7") -> None:
353356
"""Trust, resolve claude, chdir, and exec — replacing this process."""
357+
extra_env = extra_env or {}
354358
trust_project(clone_dir)
355359
os.chdir(str(clone_dir))
356360
settings = load_luv_settings(clone_dir)
357361
compose_file = (settings or {}).get("compose_file")
358362

359363
common_flags = ["--dangerously-skip-permissions",
360-
"--model", "claude-opus-4-7",
364+
"--model", model,
361365
"--effort", "max",
362366
"--remote-control",
363367
"--remote-control-session-name-prefix", clone_dir.name]
@@ -504,8 +508,9 @@ def find_latest_clone(repo: str) -> Path | None:
504508
return best
505509

506510

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] = {}) -> None:
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:
508512
"""Open an existing work folder or remote branch by number."""
513+
extra_env = extra_env or {}
509514
clone_dir = PRS_DIR / f"{repo}-{number}"
510515

511516
# 1. Local folder takes priority
@@ -515,9 +520,9 @@ def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode
515520
if nav_mode:
516521
navigate(clone_dir, extra_env=extra_env)
517522
elif resume_mode:
518-
resume(clone_dir, extra_env=extra_env)
523+
resume(clone_dir, extra_env=extra_env, model=model)
519524
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)
521526
return # unreachable
522527

523528
# 2. Check remote branch luv-{number}
@@ -542,13 +547,14 @@ def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode
542547
if nav_mode:
543548
navigate(clone_dir, extra_env=extra_env)
544549
elif resume_mode:
545-
resume(clone_dir, extra_env=extra_env)
550+
resume(clone_dir, extra_env=extra_env, model=model)
546551
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)
548553

549554

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] = {}) -> None:
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:
551556
"""Open any GitHub PR by org/repo/number, cloning if needed."""
557+
extra_env = extra_env or {}
552558
clone_dir = PRS_DIR / f"{repo}-{number}"
553559

554560
if clone_dir.exists():
@@ -557,9 +563,9 @@ def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool
557563
if nav_mode:
558564
navigate(clone_dir, extra_env=extra_env)
559565
elif resume_mode:
560-
resume(clone_dir, extra_env=extra_env)
566+
resume(clone_dir, extra_env=extra_env, model=model)
561567
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)
563569
return # unreachable
564570

565571
# 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
584590
if nav_mode:
585591
navigate(clone_dir, extra_env=extra_env)
586592
elif resume_mode:
587-
resume(clone_dir, extra_env=extra_env)
593+
resume(clone_dir, extra_env=extra_env, model=model)
588594
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)
590596

591597

592598
def main() -> None:
@@ -599,6 +605,20 @@ def main() -> None:
599605
force = "-f" in args or "--force" in args
600606
safe = "--safe" in args
601607
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+
602622
args = [a for a in args if a not in ("-n", "-r", "-e", "-f", "--force", "-p", "-nit", "--safe")]
603623
extra_env = collect_luv_env() if env_mode else {}
604624

@@ -611,6 +631,7 @@ def main() -> None:
611631
-r resume: resume the last Claude session in the work folder
612632
-p launch Claude in plan permission mode (default: bypassPermissions)
613633
-nit non-interactive: run claude -p <prompt> and exit (no REPL)
634+
-m MODEL claude model to use (default: claude-opus-4-7)
614635
-e env: pass LUV_* environment variables (with prefix stripped) into the session
615636
-f, --force (with --clean) skip safety checks and delete all work folders
616637
--safe (with --clean -f) only delete folders older than 24h
@@ -656,7 +677,7 @@ def main() -> None:
656677
die(f"cannot parse PR URL: {url}")
657678
org, repo, number = m.group(1), m.group(2), int(m.group(3))
658679
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)
660681
return
661682

662683
raw = args[0].rstrip("/")
@@ -676,14 +697,14 @@ def main() -> None:
676697
die(f"expected a PR number after -pr, got '{args[idx + 1]}'")
677698
prompt_parts = [a for i, a in enumerate(args) if i not in (0, idx, idx + 1)]
678699
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)
680701
return
681702

682703
# Detect optional numeric second argument
683704
if len(args) > 1 and args[1].isdigit():
684705
number = int(args[1])
685706
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)
687708
return
688709

689710
org = resolve_org(explicit_org)
@@ -698,7 +719,7 @@ def main() -> None:
698719
if nav_mode:
699720
navigate(clone_dir, extra_env=extra_env)
700721
else:
701-
resume(clone_dir, extra_env=extra_env)
722+
resume(clone_dir, extra_env=extra_env, model=model)
702723
return
703724

704725
# 1. Verify repo exists
@@ -750,6 +771,6 @@ def _latest(endpoint: str) -> int:
750771
if nav_mode:
751772
navigate(clone_dir, extra_env=extra_env)
752773
elif resume_mode:
753-
resume(clone_dir, extra_env=extra_env)
774+
resume(clone_dir, extra_env=extra_env, model=model)
754775
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)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "luv-cli"
7-
version = "0.0.17"
7+
version = "0.0.18"
88
description = "Launch Claude Code agents on GitHub repos with isolated workspaces and optional Docker dev environments"
99
requires-python = ">=3.10"
1010
license = "MIT"

0 commit comments

Comments
 (0)