codefreedom 0.0.1__tar.gz → 0.0.2__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.
Files changed (38) hide show
  1. {codefreedom-0.0.1 → codefreedom-0.0.2}/PKG-INFO +1 -1
  2. {codefreedom-0.0.1 → codefreedom-0.0.2}/pyproject.toml +1 -1
  3. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/launcher.py +107 -131
  4. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom.egg-info/PKG-INFO +1 -1
  5. {codefreedom-0.0.1 → codefreedom-0.0.2}/LICENSE +0 -0
  6. {codefreedom-0.0.1 → codefreedom-0.0.2}/README.md +0 -0
  7. {codefreedom-0.0.1 → codefreedom-0.0.2}/setup.cfg +0 -0
  8. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/__init__.py +0 -0
  9. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/__main__.py +0 -0
  10. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/cli/__init__.py +0 -0
  11. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/cli/claude.py +0 -0
  12. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/cli/litellm_cli.py +0 -0
  13. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/cli/main.py +0 -0
  14. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/cli/proxy.py +0 -0
  15. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/env_loader.py +0 -0
  16. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/.env.example +0 -0
  17. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/.env.secrets.example +0 -0
  18. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/profiles/claude-code-profiles.json +0 -0
  19. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/profiles/claude-code-profiles.schema.json +0 -0
  20. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/config.yaml +0 -0
  21. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/docker-compose.yaml +0 -0
  22. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/providers/anthropic-compatible.yaml +0 -0
  23. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/providers/azure-foundry.yaml +0 -0
  24. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/providers/deepseek.yaml +0 -0
  25. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/providers/local.yaml +0 -0
  26. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/providers/nvidia.yaml +0 -0
  27. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/providers/openai-compatible.yaml +0 -0
  28. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/examples/proxy/providers/opencode-zen.yaml +0 -0
  29. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom/profiles.py +0 -0
  30. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom.egg-info/SOURCES.txt +0 -0
  31. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom.egg-info/dependency_links.txt +0 -0
  32. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom.egg-info/entry_points.txt +0 -0
  33. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom.egg-info/requires.txt +0 -0
  34. {codefreedom-0.0.1 → codefreedom-0.0.2}/src/codefreedom.egg-info/top_level.txt +0 -0
  35. {codefreedom-0.0.1 → codefreedom-0.0.2}/tests/test_env_loader.py +0 -0
  36. {codefreedom-0.0.1 → codefreedom-0.0.2}/tests/test_init.py +0 -0
  37. {codefreedom-0.0.1 → codefreedom-0.0.2}/tests/test_profiles.py +0 -0
  38. {codefreedom-0.0.1 → codefreedom-0.0.2}/tests/test_proxy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codefreedom
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: CLI for running Claude Code through LiteLLM — local AI inference, anywhere
5
5
  Author-email: Nilay Parikh <nilay.parikh@gmail.com>
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codefreedom"
7
- version = "0.0.1"
7
+ version = "0.0.2"
8
8
  description = "CLI for running Claude Code through LiteLLM — local AI inference, anywhere"
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -1,11 +1,13 @@
1
- """Claude Code Docker launcher -- runs Claude Code in a persistent container.
1
+ """Claude Code Docker launcher -- runs Claude Code in ephemeral sandbox containers.
2
2
 
3
3
  Provides the core execution engine for launching Claude Code through Docker
4
- with profile-based model routing."""
4
+ with profile-based model routing. Each sandbox session gets a fresh container
5
+ with a random name -- no more container-locking from shared reuse."""
5
6
 
6
7
  from __future__ import annotations
7
8
 
8
9
  import os
10
+ import secrets
9
11
  import shutil
10
12
  import signal
11
13
  import subprocess
@@ -16,7 +18,6 @@ from codefreedom.env_loader import eprint
16
18
 
17
19
  # ── Constants ──────────────────────────────────────────────────────────────────
18
20
 
19
- CONTAINER_NAME = os.environ.get("CLAUDE_CODE_CONTAINER_NAME", "claude-dev-workspace")
20
21
  REGISTRY = os.environ.get("CLAUDE_CODE_REGISTRY", "ghcr.io/nilayparikh")
21
22
  IMAGE_NAME = os.environ.get("CLAUDE_CODE_IMAGE_NAME", "claude-code")
22
23
  IMAGE_TAG = os.environ.get("CLAUDE_CODE_IMAGE_TAG", "latest")
@@ -25,6 +26,14 @@ TARGET_IMAGE = f"{REGISTRY}/{IMAGE_NAME}:{IMAGE_TAG}"
25
26
  HOME_DIR = Path.home()
26
27
  CODEFREEDOM_DIR = HOME_DIR / ".codefreedom"
27
28
 
29
+ _CONTAINER_PREFIX = "codefreedom-"
30
+
31
+
32
+ def _generate_container_name() -> str:
33
+ """Generate a random container name: codefreedom-XXXX (4 alphanumeric chars)."""
34
+ suffix = secrets.token_hex(2) # 4 hex chars
35
+ return f"{_CONTAINER_PREFIX}{suffix}"
36
+
28
37
 
29
38
  # ── Helpers ────────────────────────────────────────────────────────────────────
30
39
 
@@ -68,110 +77,79 @@ def _forward_signal(
68
77
 
69
78
 
70
79
  def status() -> int:
71
- """Show persistent container status. Returns exit code."""
80
+ """Show all codefreedom sandbox containers. Returns exit code."""
72
81
  try:
73
- running = subprocess.run(
74
- ["docker", "ps", "-q", "-f", f"name=^{CONTAINER_NAME}$"],
75
- capture_output=True,
76
- text=True,
77
- timeout=10,
78
- check=False,
79
- )
80
- exists = subprocess.run(
81
- ["docker", "ps", "-aq", "-f", f"name=^{CONTAINER_NAME}$"],
82
+ result = subprocess.run(
83
+ [
84
+ "docker",
85
+ "ps",
86
+ "-a",
87
+ "--filter",
88
+ f"name={_CONTAINER_PREFIX}",
89
+ "--format",
90
+ "{{.Names}}\t{{.Status}}\t{{.CreatedAt}}",
91
+ ],
82
92
  capture_output=True,
83
93
  text=True,
84
94
  timeout=10,
85
95
  check=False,
86
96
  )
87
97
 
88
- if running.stdout.strip():
89
- info = subprocess.run(
90
- [
91
- "docker",
92
- "ps",
93
- "--filter",
94
- f"name=^{CONTAINER_NAME}$",
95
- "--format",
96
- "{{.Status}}",
97
- ],
98
- capture_output=True,
99
- text=True,
100
- timeout=5,
101
- check=False,
102
- )
103
- status_line = info.stdout.strip() if info.returncode == 0 else "running"
104
-
105
- sessions = subprocess.run(
106
- ["docker", "top", CONTAINER_NAME, "-eo", "comm"],
107
- capture_output=True,
108
- text=True,
109
- timeout=5,
110
- check=False,
111
- )
112
- session_count = (
113
- sessions.stdout.count("claude") if sessions.returncode == 0 else "?"
114
- )
115
-
116
- eprint(f"[STATUS] Container '{CONTAINER_NAME}' is running.")
117
- eprint(f" Status: {status_line}")
118
- eprint(f" Active Claude sessions: {session_count}")
119
- eprint("\n Attach a new session: codefreedom claude")
120
- eprint(" Stop the container: codefreedom claude --stop")
121
- elif exists.stdout.strip():
122
- eprint(f"[STATUS] Container '{CONTAINER_NAME}' exists but is stopped.")
123
- eprint(" Restart it: codefreedom claude")
124
- eprint(" Remove it: codefreedom claude --stop")
98
+ containers = [l for l in result.stdout.strip().split("\n") if l]
99
+ if containers:
100
+ eprint(f"[STATUS] {len(containers)} codefreedom sandbox container(s):")
101
+ for line in containers:
102
+ name, status_line, _created = line.split("\t", 2)
103
+ marker = "[RUNNING]" if "Up " in status_line else "[STOPPED]"
104
+ eprint(f" {marker} {name} ({status_line})")
105
+ eprint("\n Stop all: codefreedom claude --stop")
125
106
  else:
126
- eprint(f"[STATUS] No container named '{CONTAINER_NAME}'.")
127
- eprint(" Create one: codefreedom claude")
107
+ eprint("[STATUS] No codefreedom sandbox containers found.")
128
108
  return 0
129
109
  except subprocess.TimeoutExpired:
130
110
  eprint("[STATUS] Docker command timed out. Is Docker running?")
131
111
  return 1
132
112
  except FileNotFoundError:
133
- eprint("[ERROR] Docker not found. Install Docker or use --sandbox.")
113
+ eprint("[ERROR] Docker not found.")
134
114
  return 1
135
115
 
136
116
 
137
117
  def stop() -> int:
138
- """Stop and remove the persistent container. Returns exit code."""
118
+ """Stop and remove all codefreedom sandbox containers. Returns exit code."""
139
119
  try:
140
- running = subprocess.run(
141
- ["docker", "ps", "-q", "-f", f"name=^{CONTAINER_NAME}$"],
120
+ result = subprocess.run(
121
+ [
122
+ "docker",
123
+ "ps",
124
+ "-aq",
125
+ "--filter",
126
+ f"name={_CONTAINER_PREFIX}",
127
+ ],
142
128
  capture_output=True,
143
129
  text=True,
144
130
  timeout=10,
145
131
  check=False,
146
132
  )
147
- if running.stdout.strip():
148
- eprint(f"[CLEAN] Stopping container '{CONTAINER_NAME}'...")
149
- subprocess.run(
150
- ["docker", "stop", CONTAINER_NAME],
151
- capture_output=True,
152
- timeout=30,
153
- check=False,
154
- )
155
- eprint(" [OK] Container stopped.")
156
-
157
- exists = subprocess.run(
158
- ["docker", "ps", "-aq", "-f", f"name=^{CONTAINER_NAME}$"],
133
+
134
+ ids = [c for c in result.stdout.strip().split("\n") if c]
135
+ if not ids:
136
+ eprint("[CLEAN] No codefreedom sandbox containers to remove.")
137
+ return 0
138
+
139
+ eprint(f"[CLEAN] Stopping {len(ids)} container(s)...")
140
+ subprocess.run(
141
+ ["docker", "stop"] + ids,
159
142
  capture_output=True,
160
- text=True,
161
- timeout=10,
143
+ timeout=30,
162
144
  check=False,
163
145
  )
164
- if exists.stdout.strip():
165
- eprint("[CLEAN] Removing container...")
166
- subprocess.run(
167
- ["docker", "rm", "-f", CONTAINER_NAME],
168
- capture_output=True,
169
- timeout=30,
170
- check=False,
171
- )
172
- eprint(" [OK] Container removed.")
173
- else:
174
- eprint("[CLEAN] No container to remove.")
146
+ subprocess.run(
147
+ ["docker", "rm", "-f"] + ids,
148
+ capture_output=True,
149
+ timeout=30,
150
+ check=False,
151
+ )
152
+ eprint(" [OK] All sandbox containers removed.")
175
153
  return 0
176
154
  except subprocess.TimeoutExpired:
177
155
  eprint("[ERROR] Docker command timed out.")
@@ -258,10 +236,15 @@ def run_docker(
258
236
  profile_name: str,
259
237
  sandbox_image: str | None = None,
260
238
  ) -> int:
261
- """Run claude inside a persistent Docker container. Returns exit code."""
262
- # Resolve the image to use: profile override TARGET_IMAGE constant
239
+ """Run claude inside an ephemeral Docker container. Each session gets a fresh
240
+ container with a random name -- cleaned up on exit (including Ctrl+C)."""
241
+
263
242
  image = sandbox_image or TARGET_IMAGE
243
+ container_name = _generate_container_name()
244
+
264
245
  eprint(f"[IMAGE] Using sandbox image: {image}")
246
+ eprint(f"[CONTAINER] Name: {container_name}")
247
+
265
248
  env_flags: List[str] = []
266
249
  for key in sorted(profile_env.keys()):
267
250
  val = profile_env[key]
@@ -303,8 +286,8 @@ def run_docker(
303
286
  f"{workspace_dir / '.claude'}:/workspace/.claude",
304
287
  ]
305
288
 
306
- # ── Step A: Ensure image is pulled ─────────────────────────────────────
307
- eprint(f"[IMAGE] Ensuring '{image}' is available...")
289
+ # ── Ensure image is pulled ────────────────────────────────────────────
290
+ eprint(f"[IMAGE] Pulling '{image}'...")
308
291
  pull = subprocess.run(
309
292
  ["docker", "pull", image],
310
293
  capture_output=True,
@@ -318,71 +301,64 @@ def run_docker(
318
301
  eprint(f" {pull.stderr.strip()}")
319
302
  return 1
320
303
 
321
- # ── Step B: Ensure workspace .claude dir exists ────────────────────────
304
+ # ── Ensure workspace .claude dir exists ───────────────────────────────
322
305
  (workspace_dir / ".claude").mkdir(parents=True, exist_ok=True)
323
306
 
324
- # ── Step C: Start persistent container if not running ──────────────────
325
- running = subprocess.run(
326
- ["docker", "ps", "-q", "-f", f"name=^{CONTAINER_NAME}$"],
307
+ # ── Start ephemeral container ─────────────────────────────────────────
308
+ eprint(f"[RUN] Creating ephemeral container '{container_name}'...")
309
+ create = subprocess.run(
310
+ ["docker", "run", "-d", "--rm", "--name", container_name]
311
+ + base_opts
312
+ + env_flags
313
+ + [image, "sleep", "infinity"],
327
314
  capture_output=True,
328
315
  text=True,
329
- timeout=10,
316
+ timeout=60,
330
317
  check=False,
331
318
  )
319
+ if create.returncode != 0:
320
+ eprint("[ERROR] Failed to start container.")
321
+ if create.stderr:
322
+ eprint(f" {create.stderr.strip()}")
323
+ return 1
324
+ eprint(" [OK] Container started.")
332
325
 
333
- if not running.stdout.strip():
334
- stale = subprocess.run(
335
- ["docker", "ps", "-aq", "-f", f"name=^{CONTAINER_NAME}$"],
336
- capture_output=True,
337
- text=True,
338
- timeout=10,
339
- check=False,
340
- )
341
- if stale.stdout.strip():
342
- eprint(f"[CLEAN] Removing stale container '{CONTAINER_NAME}'...")
343
- subprocess.run(
344
- ["docker", "rm", "-f", CONTAINER_NAME],
345
- capture_output=True,
346
- timeout=30,
347
- check=False,
348
- )
349
-
350
- eprint(f"[RUN] Starting persistent container '{CONTAINER_NAME}'...")
351
- create = subprocess.run(
352
- ["docker", "run", "-d", "--name", CONTAINER_NAME]
353
- + base_opts
354
- + env_flags
355
- + [image, "sleep", "infinity"],
356
- capture_output=True,
357
- text=True,
358
- timeout=60,
359
- check=False,
360
- )
361
- if create.returncode != 0:
362
- eprint("[ERROR] Failed to start container.")
363
- if create.stderr:
364
- eprint(f" {create.stderr.strip()}")
365
- return 1
366
- eprint(" [OK] Container started (background).")
367
- else:
368
- eprint(f"[RUN] Reusing running container '{CONTAINER_NAME}'.")
369
-
370
- # ── Step D: Exec claude into the container ─────────────────────────────
326
+ # ── Exec claude into the container ────────────────────────────────────
371
327
  eprint("[EXEC] Attaching Claude Code session...")
372
328
 
373
329
  exec_cmd = (
374
330
  ["docker", "exec", "-it"]
375
331
  + ["-u", f"{uid}:{gid}", "-e", f"HOME=/home/{HOME_DIR.name}"]
376
332
  + env_flags
377
- + [CONTAINER_NAME, "claude", "--dangerously-skip-permissions"]
333
+ + [container_name, "claude", "--dangerously-skip-permissions"]
378
334
  + claude_args
379
335
  )
380
336
 
337
+ exit_code = 1
381
338
  try:
382
339
  proc = subprocess.Popen(exec_cmd)
383
340
  signal.signal(signal.SIGINT, lambda s, f: _forward_signal(proc, s, f))
384
341
  signal.signal(signal.SIGTERM, lambda s, f: _forward_signal(proc, s, f))
385
342
  proc.wait()
386
- return proc.returncode
343
+ exit_code = proc.returncode
387
344
  except KeyboardInterrupt:
388
- return 130
345
+ exit_code = 130
346
+ finally:
347
+ # ── Clean up the ephemeral container ──────────────────────────
348
+ eprint(f"[CLEAN] Stopping container '{container_name}'...")
349
+ subprocess.run(
350
+ ["docker", "stop", container_name],
351
+ capture_output=True,
352
+ timeout=15,
353
+ check=False,
354
+ )
355
+ # --rm flag handles auto-removal; force-remove as fallback
356
+ subprocess.run(
357
+ ["docker", "rm", "-f", container_name],
358
+ capture_output=True,
359
+ timeout=5,
360
+ check=False,
361
+ )
362
+ eprint(" [OK] Container cleaned up.")
363
+
364
+ return exit_code
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codefreedom
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: CLI for running Claude Code through LiteLLM — local AI inference, anywhere
5
5
  Author-email: Nilay Parikh <nilay.parikh@gmail.com>
6
6
  License: Apache-2.0
File without changes
File without changes
File without changes