Skip to content

Commit ca7500f

Browse files
NiveditJainclaude
andauthored
[luv-21] Add -p (plan mode) and -nit (non-interactive); drop plan-mode default for prompts (#21)
Prompts no longer auto-enable Claude's plan permission mode. Pass -p to opt into plan mode; pass -nit to run claude -p <prompt> non-interactively. Bumps version to 0.0.15. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 7693499 commit ca7500f

3 files changed

Lines changed: 28 additions & 16 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ All workspaces live under `~/prs/`. The number comes from the repo's GitHub issu
7171
|------|-------------|
7272
| `-n` | Navigate: open a shell instead of Claude |
7373
| `-r` | Resume: resume the last Claude session |
74+
| `-p` | Launch Claude in plan permission mode (default: `bypassPermissions`) |
75+
| `-nit` | Non-interactive: run `claude -p <prompt>` and exit (no REPL) |
7476
| `-e` | Env: pass `LUV_*` environment variables (with prefix stripped) into the session |
7577
| `-f`, `--force` | Skip safety checks (with `--clean`) |
7678

‎luv/__init__.py‎

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,8 @@ def resume(clone_dir: Path, extra_env: dict[str, str] = {}) -> None:
347347
"--remote-control-session-name-prefix", clone_dir.name])
348348

349349

350-
def launch(clone_dir: Path, prompt: str | None, extra_env: dict[str, str] = {}) -> None:
350+
def launch(clone_dir: Path, prompt: str | None, plan_mode: bool = False,
351+
non_interactive: bool = False, extra_env: dict[str, str] = {}) -> None:
351352
"""Trust, resolve claude, chdir, and exec — replacing this process."""
352353
trust_project(clone_dir)
353354
os.chdir(str(clone_dir))
@@ -359,12 +360,17 @@ def launch(clone_dir: Path, prompt: str | None, extra_env: dict[str, str] = {})
359360
"--effort", "max",
360361
"--remote-control",
361362
"--remote-control-session-name-prefix", clone_dir.name]
362-
if prompt:
363+
if non_interactive:
364+
if not prompt:
365+
die("-nit requires a prompt")
366+
mode_flags = []
367+
initial_args = ["-p", prompt]
368+
elif plan_mode:
363369
mode_flags = ["--permission-mode", "plan"]
364-
initial_args = [prompt]
370+
initial_args = [prompt] if prompt else [f"/color {pick_color()}"]
365371
else:
366372
mode_flags = ["--permission-mode", "bypassPermissions"]
367-
initial_args = [f"/color {pick_color()}"]
373+
initial_args = [prompt] if prompt else [f"/color {pick_color()}"]
368374

369375
if compose_file:
370376
project = docker_project_name(clone_dir)
@@ -489,7 +495,7 @@ def find_latest_clone(repo: str) -> Path | None:
489495
return best
490496

491497

492-
def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool = False, resume_mode: bool = False, extra_env: dict[str, str] = {}) -> None:
498+
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:
493499
"""Open an existing work folder or remote branch by number."""
494500
clone_dir = PRS_DIR / f"{repo}-{number}"
495501

@@ -502,7 +508,7 @@ def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode
502508
elif resume_mode:
503509
resume(clone_dir, extra_env=extra_env)
504510
else:
505-
launch(clone_dir, prompt, extra_env=extra_env)
511+
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env)
506512
return # unreachable
507513

508514
# 2. Check remote branch luv-{number}
@@ -529,10 +535,10 @@ def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode
529535
elif resume_mode:
530536
resume(clone_dir, extra_env=extra_env)
531537
else:
532-
launch(clone_dir, prompt, extra_env=extra_env)
538+
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env)
533539

534540

535-
def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool = False, resume_mode: bool = False, extra_env: dict[str, str] = {}) -> None:
541+
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:
536542
"""Open any GitHub PR by org/repo/number, cloning if needed."""
537543
clone_dir = PRS_DIR / f"{repo}-{number}"
538544

@@ -544,7 +550,7 @@ def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool
544550
elif resume_mode:
545551
resume(clone_dir, extra_env=extra_env)
546552
else:
547-
launch(clone_dir, prompt, extra_env=extra_env)
553+
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env)
548554
return # unreachable
549555

550556
# Resolve the actual branch name via GitHub API
@@ -571,17 +577,19 @@ def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool
571577
elif resume_mode:
572578
resume(clone_dir, extra_env=extra_env)
573579
else:
574-
launch(clone_dir, prompt, extra_env=extra_env)
580+
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env)
575581

576582

577583
def main() -> None:
578584
args = sys.argv[1:]
579585

580586
nav_mode = "-n" in args
581587
resume_mode = "-r" in args
588+
plan_mode = "-p" in args
589+
non_interactive = "-nit" in args
582590
force = "-f" in args or "--force" in args
583591
env_mode = "-e" in args
584-
args = [a for a in args if a not in ("-n", "-r", "-e", "-f", "--force")]
592+
args = [a for a in args if a not in ("-n", "-r", "-e", "-f", "--force", "-p", "-nit")]
585593
extra_env = collect_luv_env() if env_mode else {}
586594

587595
if not args or args[0] in ("-h", "--help"):
@@ -591,6 +599,8 @@ def main() -> None:
591599
Flags:
592600
-n navigate: open a shell in the work folder instead of launching Claude
593601
-r resume: resume the last Claude session in the work folder
602+
-p launch Claude in plan permission mode (default: bypassPermissions)
603+
-nit non-interactive: run claude -p <prompt> and exit (no REPL)
594604
-e env: pass LUV_* environment variables (with prefix stripped) into the session
595605
-f, --force (with --clean) skip safety checks and delete all work folders
596606
@@ -632,7 +642,7 @@ def main() -> None:
632642
die(f"cannot parse PR URL: {url}")
633643
org, repo, number = m.group(1), m.group(2), int(m.group(3))
634644
prompt = " ".join(args[2:]) or None
635-
open_pr(org, repo, number, prompt, nav_mode, resume_mode, extra_env=extra_env)
645+
open_pr(org, repo, number, prompt, nav_mode, resume_mode, plan_mode, non_interactive, extra_env=extra_env)
636646
return
637647

638648
raw = args[0].rstrip("/")
@@ -652,14 +662,14 @@ def main() -> None:
652662
die(f"expected a PR number after -pr, got '{args[idx + 1]}'")
653663
prompt_parts = [a for i, a in enumerate(args) if i not in (0, idx, idx + 1)]
654664
prompt = " ".join(prompt_parts) or None
655-
open_pr(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode, extra_env=extra_env)
665+
open_pr(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode, plan_mode, non_interactive, extra_env=extra_env)
656666
return
657667

658668
# Detect optional numeric second argument
659669
if len(args) > 1 and args[1].isdigit():
660670
number = int(args[1])
661671
prompt = " ".join(args[2:]) or None
662-
open_existing(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode, extra_env=extra_env)
672+
open_existing(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode, plan_mode, non_interactive, extra_env=extra_env)
663673
return
664674

665675
org = resolve_org(explicit_org)
@@ -728,4 +738,4 @@ def _latest(endpoint: str) -> int:
728738
elif resume_mode:
729739
resume(clone_dir, extra_env=extra_env)
730740
else:
731-
launch(clone_dir, prompt, extra_env=extra_env)
741+
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env)

‎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.14"
7+
version = "0.0.15"
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)