luv-cli 0.0.1__tar.gz → 0.0.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.
@@ -0,0 +1,316 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook SessionStart",
9
+ "timeout": 60000,
10
+ "__failproofai_hook__": true
11
+ }
12
+ ]
13
+ }
14
+ ],
15
+ "SessionEnd": [
16
+ {
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook SessionEnd",
21
+ "timeout": 60000,
22
+ "__failproofai_hook__": true
23
+ }
24
+ ]
25
+ }
26
+ ],
27
+ "UserPromptSubmit": [
28
+ {
29
+ "hooks": [
30
+ {
31
+ "type": "command",
32
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook UserPromptSubmit",
33
+ "timeout": 60000,
34
+ "__failproofai_hook__": true
35
+ }
36
+ ]
37
+ }
38
+ ],
39
+ "PreToolUse": [
40
+ {
41
+ "hooks": [
42
+ {
43
+ "type": "command",
44
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook PreToolUse",
45
+ "timeout": 60000,
46
+ "__failproofai_hook__": true
47
+ }
48
+ ]
49
+ }
50
+ ],
51
+ "PermissionRequest": [
52
+ {
53
+ "hooks": [
54
+ {
55
+ "type": "command",
56
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook PermissionRequest",
57
+ "timeout": 60000,
58
+ "__failproofai_hook__": true
59
+ }
60
+ ]
61
+ }
62
+ ],
63
+ "PermissionDenied": [
64
+ {
65
+ "hooks": [
66
+ {
67
+ "type": "command",
68
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook PermissionDenied",
69
+ "timeout": 60000,
70
+ "__failproofai_hook__": true
71
+ }
72
+ ]
73
+ }
74
+ ],
75
+ "PostToolUse": [
76
+ {
77
+ "hooks": [
78
+ {
79
+ "type": "command",
80
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook PostToolUse",
81
+ "timeout": 60000,
82
+ "__failproofai_hook__": true
83
+ }
84
+ ]
85
+ }
86
+ ],
87
+ "PostToolUseFailure": [
88
+ {
89
+ "hooks": [
90
+ {
91
+ "type": "command",
92
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook PostToolUseFailure",
93
+ "timeout": 60000,
94
+ "__failproofai_hook__": true
95
+ }
96
+ ]
97
+ }
98
+ ],
99
+ "Notification": [
100
+ {
101
+ "hooks": [
102
+ {
103
+ "type": "command",
104
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook Notification",
105
+ "timeout": 60000,
106
+ "__failproofai_hook__": true
107
+ }
108
+ ]
109
+ }
110
+ ],
111
+ "SubagentStart": [
112
+ {
113
+ "hooks": [
114
+ {
115
+ "type": "command",
116
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook SubagentStart",
117
+ "timeout": 60000,
118
+ "__failproofai_hook__": true
119
+ }
120
+ ]
121
+ }
122
+ ],
123
+ "SubagentStop": [
124
+ {
125
+ "hooks": [
126
+ {
127
+ "type": "command",
128
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook SubagentStop",
129
+ "timeout": 60000,
130
+ "__failproofai_hook__": true
131
+ }
132
+ ]
133
+ }
134
+ ],
135
+ "TaskCreated": [
136
+ {
137
+ "hooks": [
138
+ {
139
+ "type": "command",
140
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook TaskCreated",
141
+ "timeout": 60000,
142
+ "__failproofai_hook__": true
143
+ }
144
+ ]
145
+ }
146
+ ],
147
+ "TaskCompleted": [
148
+ {
149
+ "hooks": [
150
+ {
151
+ "type": "command",
152
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook TaskCompleted",
153
+ "timeout": 60000,
154
+ "__failproofai_hook__": true
155
+ }
156
+ ]
157
+ }
158
+ ],
159
+ "Stop": [
160
+ {
161
+ "hooks": [
162
+ {
163
+ "type": "command",
164
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook Stop",
165
+ "timeout": 60000,
166
+ "__failproofai_hook__": true
167
+ }
168
+ ]
169
+ }
170
+ ],
171
+ "StopFailure": [
172
+ {
173
+ "hooks": [
174
+ {
175
+ "type": "command",
176
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook StopFailure",
177
+ "timeout": 60000,
178
+ "__failproofai_hook__": true
179
+ }
180
+ ]
181
+ }
182
+ ],
183
+ "TeammateIdle": [
184
+ {
185
+ "hooks": [
186
+ {
187
+ "type": "command",
188
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook TeammateIdle",
189
+ "timeout": 60000,
190
+ "__failproofai_hook__": true
191
+ }
192
+ ]
193
+ }
194
+ ],
195
+ "InstructionsLoaded": [
196
+ {
197
+ "hooks": [
198
+ {
199
+ "type": "command",
200
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook InstructionsLoaded",
201
+ "timeout": 60000,
202
+ "__failproofai_hook__": true
203
+ }
204
+ ]
205
+ }
206
+ ],
207
+ "ConfigChange": [
208
+ {
209
+ "hooks": [
210
+ {
211
+ "type": "command",
212
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook ConfigChange",
213
+ "timeout": 60000,
214
+ "__failproofai_hook__": true
215
+ }
216
+ ]
217
+ }
218
+ ],
219
+ "CwdChanged": [
220
+ {
221
+ "hooks": [
222
+ {
223
+ "type": "command",
224
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook CwdChanged",
225
+ "timeout": 60000,
226
+ "__failproofai_hook__": true
227
+ }
228
+ ]
229
+ }
230
+ ],
231
+ "FileChanged": [
232
+ {
233
+ "hooks": [
234
+ {
235
+ "type": "command",
236
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook FileChanged",
237
+ "timeout": 60000,
238
+ "__failproofai_hook__": true
239
+ }
240
+ ]
241
+ }
242
+ ],
243
+ "WorktreeCreate": [
244
+ {
245
+ "hooks": [
246
+ {
247
+ "type": "command",
248
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook WorktreeCreate",
249
+ "timeout": 60000,
250
+ "__failproofai_hook__": true
251
+ }
252
+ ]
253
+ }
254
+ ],
255
+ "WorktreeRemove": [
256
+ {
257
+ "hooks": [
258
+ {
259
+ "type": "command",
260
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook WorktreeRemove",
261
+ "timeout": 60000,
262
+ "__failproofai_hook__": true
263
+ }
264
+ ]
265
+ }
266
+ ],
267
+ "PreCompact": [
268
+ {
269
+ "hooks": [
270
+ {
271
+ "type": "command",
272
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook PreCompact",
273
+ "timeout": 60000,
274
+ "__failproofai_hook__": true
275
+ }
276
+ ]
277
+ }
278
+ ],
279
+ "PostCompact": [
280
+ {
281
+ "hooks": [
282
+ {
283
+ "type": "command",
284
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook PostCompact",
285
+ "timeout": 60000,
286
+ "__failproofai_hook__": true
287
+ }
288
+ ]
289
+ }
290
+ ],
291
+ "Elicitation": [
292
+ {
293
+ "hooks": [
294
+ {
295
+ "type": "command",
296
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook Elicitation",
297
+ "timeout": 60000,
298
+ "__failproofai_hook__": true
299
+ }
300
+ ]
301
+ }
302
+ ],
303
+ "ElicitationResult": [
304
+ {
305
+ "hooks": [
306
+ {
307
+ "type": "command",
308
+ "command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook ElicitationResult",
309
+ "timeout": 60000,
310
+ "__failproofai_hook__": true
311
+ }
312
+ ]
313
+ }
314
+ ]
315
+ }
316
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "enabledPolicies": [
3
+ "sanitize-jwt",
4
+ "sanitize-api-keys",
5
+ "sanitize-connection-strings",
6
+ "sanitize-private-key-content",
7
+ "sanitize-bearer-tokens",
8
+ "protect-env-vars",
9
+ "block-env-files",
10
+ "block-read-outside-cwd",
11
+ "block-sudo",
12
+ "block-curl-pipe-sh",
13
+ "block-rm-rf",
14
+ "block-failproofai-commands",
15
+ "block-secrets-write",
16
+ "block-push-master",
17
+ "block-force-push",
18
+ "block-work-on-main",
19
+ "warn-git-amend",
20
+ "warn-git-stash-drop",
21
+ "warn-all-files-staged",
22
+ "warn-destructive-sql",
23
+ "warn-schema-alteration",
24
+ "warn-package-publish",
25
+ "warn-global-package-install",
26
+ "warn-large-file-write",
27
+ "warn-background-process",
28
+ "warn-repeated-tool-calls",
29
+ "require-commit-before-stop",
30
+ "require-push-before-stop",
31
+ "require-pr-before-stop",
32
+ "require-ci-green-before-stop"
33
+ ],
34
+ "policyParams": {
35
+ "block-force-push": {
36
+ "hint": "Create a new branch from your current HEAD (e.g. `git checkout -b <new-branch>`) and push that instead."
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,4 @@
1
+ __pycache__/
2
+ *.pyc
3
+ dist/
4
+ *.egg-info/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: luv-cli
3
- Version: 0.0.1
3
+ Version: 0.0.3
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
@@ -94,6 +94,7 @@ All workspaces live under `~/prs/`. The number comes from the repo's GitHub issu
94
94
  |------|-------------|
95
95
  | `-n` | Navigate: open a shell instead of Claude |
96
96
  | `-r` | Resume: resume the last Claude session |
97
+ | `-e` | Env: pass `LUV_*` environment variables (with prefix stripped) into the session |
97
98
  | `-f`, `--force` | Skip safety checks (with `--clean`) |
98
99
 
99
100
  ## Docker dev environments
@@ -71,6 +71,7 @@ All workspaces live under `~/prs/`. The number comes from the repo's GitHub issu
71
71
  |------|-------------|
72
72
  | `-n` | Navigate: open a shell instead of Claude |
73
73
  | `-r` | Resume: resume the last Claude session |
74
+ | `-e` | Env: pass `LUV_*` environment variables (with prefix stripped) into the session |
74
75
  | `-f`, `--force` | Skip safety checks (with `--clean`) |
75
76
 
76
77
  ## Docker dev environments
@@ -119,6 +119,23 @@ def trust_project(path: Path) -> None:
119
119
  os.replace(tmp_path, CLAUDE_JSON)
120
120
 
121
121
 
122
+ def collect_luv_env() -> dict[str, str]:
123
+ """Collect LUV_* env vars, strip prefix, return as dict."""
124
+ result = {}
125
+ for key, value in os.environ.items():
126
+ if key.startswith("LUV_") and len(key) > 4:
127
+ result[key[4:]] = value
128
+ return result
129
+
130
+
131
+ def docker_env_flags(env_vars: dict[str, str]) -> list[str]:
132
+ """Convert env dict to docker compose exec -e flags."""
133
+ flags: list[str] = []
134
+ for key, value in env_vars.items():
135
+ flags.extend(["-e", f"{key}={value}"])
136
+ return flags
137
+
138
+
122
139
  def ensure_pr_rules() -> None:
123
140
  claude_dir = Path.home() / ".claude"
124
141
  claude_md = claude_dir / "CLAUDE.md"
@@ -229,7 +246,7 @@ def stop_docker(clone_dir: Path, compose_file: str, project: str) -> None:
229
246
  subprocess.run(base + ["down", "-v", "--remove-orphans"])
230
247
 
231
248
 
232
- def navigate(clone_dir: Path) -> None:
249
+ def navigate(clone_dir: Path, extra_env: dict[str, str] = {}) -> None:
233
250
  """Chdir into the work folder and exec a shell — replacing this process."""
234
251
  os.chdir(str(clone_dir))
235
252
  settings = load_luv_settings(clone_dir)
@@ -240,16 +257,17 @@ def navigate(clone_dir: Path) -> None:
240
257
  start_docker(clone_dir, compose_file, project)
241
258
  try:
242
259
  base = docker_compose_base(clone_dir, compose_file, project)
243
- r = subprocess.run(base + ["exec", "-it", "dev-environment", "bash"])
260
+ r = subprocess.run(base + ["exec", "-it"] + docker_env_flags(extra_env) + ["dev-environment", "bash"])
244
261
  sys.exit(r.returncode)
245
262
  finally:
246
263
  stop_docker(clone_dir, compose_file, project)
247
264
  else:
248
265
  shell = os.environ.get("SHELL", "/bin/bash")
266
+ os.environ.update(extra_env)
249
267
  os.execv(shell, [shell])
250
268
 
251
269
 
252
- def resume(clone_dir: Path) -> None:
270
+ def resume(clone_dir: Path, extra_env: dict[str, str] = {}) -> None:
253
271
  """Trust, chdir, and exec claude --resume — replacing this process."""
254
272
  trust_project(clone_dir)
255
273
  os.chdir(str(clone_dir))
@@ -261,7 +279,7 @@ def resume(clone_dir: Path) -> None:
261
279
  start_docker(clone_dir, compose_file, project)
262
280
  try:
263
281
  base = docker_compose_base(clone_dir, compose_file, project)
264
- r = subprocess.run(base + ["exec", "-it", "dev-environment",
282
+ r = subprocess.run(base + ["exec", "-it"] + docker_env_flags(extra_env) + ["dev-environment",
265
283
  "claude", "--dangerously-skip-permissions",
266
284
  "--model", "claude-opus-4-6",
267
285
  "--effort", "max", "--resume"])
@@ -272,11 +290,12 @@ def resume(clone_dir: Path) -> None:
272
290
  claude_bin = shutil.which("claude")
273
291
  if not claude_bin:
274
292
  die("'claude' not found in PATH")
293
+ os.environ.update(extra_env)
275
294
  os.execv(claude_bin, [claude_bin, "--dangerously-skip-permissions",
276
295
  "--model", "claude-opus-4-6", "--effort", "max", "--resume"])
277
296
 
278
297
 
279
- def launch(clone_dir: Path, prompt: str | None) -> None:
298
+ def launch(clone_dir: Path, prompt: str | None, extra_env: dict[str, str] = {}) -> None:
280
299
  """Trust, resolve claude, chdir, and exec — replacing this process."""
281
300
  trust_project(clone_dir)
282
301
  os.chdir(str(clone_dir))
@@ -293,7 +312,7 @@ def launch(clone_dir: Path, prompt: str | None) -> None:
293
312
  "--model", "claude-opus-4-6", "--effort", "max"]
294
313
  if prompt:
295
314
  claude_cmd.append(f"/plan {prompt}")
296
- r = subprocess.run(base + ["exec", "-it", "dev-environment"] + claude_cmd)
315
+ r = subprocess.run(base + ["exec", "-it"] + docker_env_flags(extra_env) + ["dev-environment"] + claude_cmd)
297
316
  sys.exit(r.returncode)
298
317
  finally:
299
318
  stop_docker(clone_dir, compose_file, project)
@@ -304,6 +323,7 @@ def launch(clone_dir: Path, prompt: str | None) -> None:
304
323
  base_args = [claude_bin, "--dangerously-skip-permissions",
305
324
  "--permission-mode", "bypassPermissions",
306
325
  "--model", "claude-opus-4-6", "--effort", "max"]
326
+ os.environ.update(extra_env)
307
327
  if prompt:
308
328
  os.execv(claude_bin, base_args + [f"/plan {prompt}"])
309
329
  else:
@@ -398,7 +418,24 @@ def cmd_clean(force: bool = False) -> None:
398
418
  print("luv: nothing to clean")
399
419
 
400
420
 
401
- def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool = False, resume_mode: bool = False) -> None:
421
+ def find_latest_clone(repo: str) -> Path | None:
422
+ """Return the highest-numbered local {repo}-{N} folder, or None."""
423
+ if not PRS_DIR.exists():
424
+ return None
425
+ best: Path | None = None
426
+ best_num = -1
427
+ for entry in PRS_DIR.iterdir():
428
+ if not entry.is_dir():
429
+ continue
430
+ parts = entry.name.rsplit("-", 1)
431
+ if len(parts) == 2 and parts[0] == repo and parts[1].isdigit():
432
+ n = int(parts[1])
433
+ if n > best_num:
434
+ best, best_num = entry, n
435
+ return best
436
+
437
+
438
+ 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:
402
439
  """Open an existing work folder or remote branch by number."""
403
440
  clone_dir = PRS_DIR / f"{repo}-{number}"
404
441
 
@@ -407,11 +444,11 @@ def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode
407
444
  print(f"luv: opening existing folder {clone_dir.name}")
408
445
  ensure_pr_rules()
409
446
  if nav_mode:
410
- navigate(clone_dir)
447
+ navigate(clone_dir, extra_env=extra_env)
411
448
  elif resume_mode:
412
- resume(clone_dir)
449
+ resume(clone_dir, extra_env=extra_env)
413
450
  else:
414
- launch(clone_dir, prompt)
451
+ launch(clone_dir, prompt, extra_env=extra_env)
415
452
  return # unreachable
416
453
 
417
454
  # 2. Check remote branch luv-{number}
@@ -434,14 +471,14 @@ def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode
434
471
  print(f"luv: ready — {clone_dir.name}, branch {branch}")
435
472
  ensure_pr_rules()
436
473
  if nav_mode:
437
- navigate(clone_dir)
474
+ navigate(clone_dir, extra_env=extra_env)
438
475
  elif resume_mode:
439
- resume(clone_dir)
476
+ resume(clone_dir, extra_env=extra_env)
440
477
  else:
441
- launch(clone_dir, prompt)
478
+ launch(clone_dir, prompt, extra_env=extra_env)
442
479
 
443
480
 
444
- def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool = False, resume_mode: bool = False) -> None:
481
+ 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:
445
482
  """Open any GitHub PR by org/repo/number, cloning if needed."""
446
483
  clone_dir = PRS_DIR / f"{repo}-{number}"
447
484
 
@@ -449,11 +486,11 @@ def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool
449
486
  print(f"luv: opening existing folder {clone_dir.name}")
450
487
  ensure_pr_rules()
451
488
  if nav_mode:
452
- navigate(clone_dir)
489
+ navigate(clone_dir, extra_env=extra_env)
453
490
  elif resume_mode:
454
- resume(clone_dir)
491
+ resume(clone_dir, extra_env=extra_env)
455
492
  else:
456
- launch(clone_dir, prompt)
493
+ launch(clone_dir, prompt, extra_env=extra_env)
457
494
  return # unreachable
458
495
 
459
496
  # Resolve the actual branch name via GitHub API
@@ -476,11 +513,11 @@ def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool
476
513
  print(f"luv: ready — {clone_dir.name}, branch {branch}")
477
514
  ensure_pr_rules()
478
515
  if nav_mode:
479
- navigate(clone_dir)
516
+ navigate(clone_dir, extra_env=extra_env)
480
517
  elif resume_mode:
481
- resume(clone_dir)
518
+ resume(clone_dir, extra_env=extra_env)
482
519
  else:
483
- launch(clone_dir, prompt)
520
+ launch(clone_dir, prompt, extra_env=extra_env)
484
521
 
485
522
 
486
523
  def main() -> None:
@@ -489,7 +526,9 @@ def main() -> None:
489
526
  nav_mode = "-n" in args
490
527
  resume_mode = "-r" in args
491
528
  force = "-f" in args or "--force" in args
492
- args = [a for a in args if a not in ("-n", "-r", "-f", "--force")]
529
+ env_mode = "-e" in args
530
+ args = [a for a in args if a not in ("-n", "-r", "-e", "-f", "--force")]
531
+ extra_env = collect_luv_env() if env_mode else {}
493
532
 
494
533
  if not args or args[0] in ("-h", "--help"):
495
534
  print("""\
@@ -498,6 +537,7 @@ Usage: luv [flags] <command>
498
537
  Flags:
499
538
  -n navigate: open a shell in the work folder instead of launching Claude
500
539
  -r resume: resume the last Claude session in the work folder
540
+ -e env: pass LUV_* environment variables (with prefix stripped) into the session
501
541
  -f, --force (with --clean) skip safety checks and delete all work folders
502
542
 
503
543
  Commands:
@@ -506,6 +546,8 @@ Commands:
506
546
  luv [org/]<repo> <number> [prompt] reopen an existing work folder by number
507
547
  luv -l <PR URL> [prompt] open any GitHub PR by URL
508
548
  luv [org/]<repo> -pr <number> [prompt] open a GitHub PR by repo + number
549
+ luv [org/]<repo> -n open shell in latest local clone
550
+ luv [org/]<repo> -r resume Claude in latest local clone
509
551
  luv --clean [-f] delete fully-pushed work folders
510
552
 
511
553
  Org resolution:
@@ -536,7 +578,7 @@ Docker:
536
578
  die(f"cannot parse PR URL: {url}")
537
579
  org, repo, number = m.group(1), m.group(2), int(m.group(3))
538
580
  prompt = " ".join(args[2:]) or None
539
- open_pr(org, repo, number, prompt, nav_mode, resume_mode)
581
+ open_pr(org, repo, number, prompt, nav_mode, resume_mode, extra_env=extra_env)
540
582
  return
541
583
 
542
584
  raw = args[0].rstrip("/")
@@ -556,19 +598,31 @@ Docker:
556
598
  die(f"expected a PR number after -pr, got '{args[idx + 1]}'")
557
599
  prompt_parts = [a for i, a in enumerate(args) if i not in (0, idx, idx + 1)]
558
600
  prompt = " ".join(prompt_parts) or None
559
- open_pr(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode)
601
+ open_pr(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode, extra_env=extra_env)
560
602
  return
561
603
 
562
604
  # Detect optional numeric second argument
563
605
  if len(args) > 1 and args[1].isdigit():
564
606
  number = int(args[1])
565
607
  prompt = " ".join(args[2:]) or None
566
- open_existing(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode)
608
+ open_existing(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode, extra_env=extra_env)
567
609
  return
568
610
 
569
611
  org = resolve_org(explicit_org)
570
612
  prompt = " ".join(args[1:]) if len(args) > 1 else None
571
613
 
614
+ # luv <repo> -n/-r → open latest local clone (no new workspace)
615
+ if (nav_mode or resume_mode) and not prompt:
616
+ clone_dir = find_latest_clone(repo)
617
+ if clone_dir is None:
618
+ die(f"no local clones of '{repo}' found in {PRS_DIR}")
619
+ print(f"luv: opening latest clone {clone_dir.name}")
620
+ if nav_mode:
621
+ navigate(clone_dir, extra_env=extra_env)
622
+ else:
623
+ resume(clone_dir, extra_env=extra_env)
624
+ return
625
+
572
626
  # 1. Verify repo exists
573
627
  r = run(["gh", "api", f"repos/{org}/{repo}"])
574
628
  if r.returncode != 0:
@@ -610,8 +664,8 @@ Docker:
610
664
 
611
665
  # 7. Launch claude, resume session, or open shell (replace this process)
612
666
  if nav_mode:
613
- navigate(clone_dir)
667
+ navigate(clone_dir, extra_env=extra_env)
614
668
  elif resume_mode:
615
- resume(clone_dir)
669
+ resume(clone_dir, extra_env=extra_env)
616
670
  else:
617
- launch(clone_dir, prompt)
671
+ launch(clone_dir, prompt, extra_env=extra_env)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "luv-cli"
7
- version = "0.0.1"
7
+ version = "0.0.3"
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