codefreedom 0.0.3__tar.gz → 0.0.4__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 (41) hide show
  1. {codefreedom-0.0.3/src/codefreedom.egg-info → codefreedom-0.0.4}/PKG-INFO +3 -3
  2. {codefreedom-0.0.3 → codefreedom-0.0.4}/pyproject.toml +3 -3
  3. codefreedom-0.0.4/src/codefreedom/cli/chrome.py +366 -0
  4. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/cli/main.py +82 -11
  5. codefreedom-0.0.4/src/codefreedom/examples/profiles/chrome.schema.json +55 -0
  6. codefreedom-0.0.3/src/codefreedom/examples/profiles/claude-code-profiles.json → codefreedom-0.0.4/src/codefreedom/examples/profiles/claude-code.json +1 -1
  7. codefreedom-0.0.4/src/codefreedom/examples/profiles/tools-chrome.json +22 -0
  8. {codefreedom-0.0.3 → codefreedom-0.0.4/src/codefreedom.egg-info}/PKG-INFO +3 -3
  9. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom.egg-info/SOURCES.txt +5 -2
  10. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom.egg-info/requires.txt +2 -2
  11. {codefreedom-0.0.3 → codefreedom-0.0.4}/tests/test_init.py +36 -5
  12. {codefreedom-0.0.3 → codefreedom-0.0.4}/LICENSE +0 -0
  13. {codefreedom-0.0.3 → codefreedom-0.0.4}/NOTICE +0 -0
  14. {codefreedom-0.0.3 → codefreedom-0.0.4}/README.md +0 -0
  15. {codefreedom-0.0.3 → codefreedom-0.0.4}/setup.cfg +0 -0
  16. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/__init__.py +0 -0
  17. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/__main__.py +0 -0
  18. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/cli/__init__.py +0 -0
  19. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/cli/claude.py +0 -0
  20. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/cli/proxy.py +0 -0
  21. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/env_loader.py +0 -0
  22. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/examples/.env.example +0 -0
  23. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/examples/.env.secrets.example +0 -0
  24. /codefreedom-0.0.3/src/codefreedom/examples/profiles/claude-code-profiles.schema.json → /codefreedom-0.0.4/src/codefreedom/examples/profiles/claude-code.schema.json +0 -0
  25. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/examples/proxy/config.yaml +0 -0
  26. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/examples/proxy/docker-compose.yaml +0 -0
  27. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/examples/proxy/providers/anthropic-compatible.yaml +0 -0
  28. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/examples/proxy/providers/azure-foundry.yaml +0 -0
  29. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/examples/proxy/providers/deepseek.yaml +0 -0
  30. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/examples/proxy/providers/local.yaml +0 -0
  31. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/examples/proxy/providers/nvidia.yaml +0 -0
  32. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/examples/proxy/providers/openai-compatible.yaml +0 -0
  33. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/examples/proxy/providers/opencode-zen.yaml +0 -0
  34. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/launcher.py +0 -0
  35. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom/profiles.py +0 -0
  36. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom.egg-info/dependency_links.txt +0 -0
  37. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom.egg-info/entry_points.txt +0 -0
  38. {codefreedom-0.0.3 → codefreedom-0.0.4}/src/codefreedom.egg-info/top_level.txt +0 -0
  39. {codefreedom-0.0.3 → codefreedom-0.0.4}/tests/test_env_loader.py +0 -0
  40. {codefreedom-0.0.3 → codefreedom-0.0.4}/tests/test_profiles.py +0 -0
  41. {codefreedom-0.0.3 → codefreedom-0.0.4}/tests/test_proxy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codefreedom
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: Single wrapper for all code agents — simple LLM routing, sandboxing, profile management, and isolation. All config in ~/.codefreedom.
5
5
  Author-email: Nilay Parikh <nilay.parikh@gmail.com>
6
6
  License: Apache-2.0
@@ -27,14 +27,14 @@ Requires-Dist: PyYAML>=6.0
27
27
  Requires-Dist: types-PyYAML
28
28
  Provides-Extra: litellm
29
29
  Requires-Dist: litellm[proxy]>=1.50; extra == "litellm"
30
- Requires-Dist: prometheus-client>=0.20; extra == "litellm"
30
+ Requires-Dist: prometheus-client>=0.25.0; extra == "litellm"
31
31
  Provides-Extra: dev
32
32
  Requires-Dist: mypy>=1.0; extra == "dev"
33
33
  Requires-Dist: ruff>=0.1; extra == "dev"
34
34
  Requires-Dist: pytest>=7.0; extra == "dev"
35
35
  Requires-Dist: types-PyYAML; extra == "dev"
36
36
  Provides-Extra: docs
37
- Requires-Dist: mkdocs-material[imaging]>=9.6; extra == "docs"
37
+ Requires-Dist: mkdocs-material[imaging]>=9.7.6; extra == "docs"
38
38
  Requires-Dist: mkdocs-mermaid2-plugin>=1.2; extra == "docs"
39
39
  Provides-Extra: all
40
40
  Requires-Dist: codefreedom[dev,docs,litellm]; extra == "all"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codefreedom"
7
- version = "0.0.3"
7
+ version = "0.0.4"
8
8
  description = "Single wrapper for all code agents — simple LLM routing, sandboxing, profile management, and isolation. All config in ~/.codefreedom."
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -33,9 +33,9 @@ classifiers = [
33
33
  # Not needed for Docker Compose mode.
34
34
  # dev — Development extras (type checking, linting)
35
35
  [project.optional-dependencies]
36
- litellm = ["litellm[proxy]>=1.50", "prometheus-client>=0.20"]
36
+ litellm = ["litellm[proxy]>=1.50", "prometheus-client>=0.25.0"]
37
37
  dev = ["mypy>=1.0", "ruff>=0.1", "pytest>=7.0", "types-PyYAML"]
38
- docs = ["mkdocs-material[imaging]>=9.6", "mkdocs-mermaid2-plugin>=1.2"]
38
+ docs = ["mkdocs-material[imaging]>=9.7.6", "mkdocs-mermaid2-plugin>=1.2"]
39
39
  all = ["codefreedom[litellm,dev,docs]"]
40
40
 
41
41
  [project.scripts]
@@ -0,0 +1,366 @@
1
+ """Chrome browser tool — run Chrome in Docker with Xvfb for undetectable browsing.
2
+
3
+ Usage:
4
+ codefreedom tools chrome start Start Chrome container (Xvfb + Chromium)
5
+ codefreedom tools chrome stop Stop and remove Chrome container
6
+ codefreedom tools chrome status Show Chrome container status
7
+ codefreedom tools chrome url Show CDP debug URL
8
+
9
+ Settings are loaded from ~/.codefreedom/profiles/chrome.json (generated by
10
+ 'codefreedom --init'). See src/codefreedom/examples/profiles/tools-chrome.json
11
+ for the template with all available options.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ import subprocess
19
+ from pathlib import Path
20
+
21
+ from codefreedom.env_loader import eprint
22
+
23
+ # ── Defaults ──────────────────────────────────────────────────────────────────
24
+
25
+ _DEFAULT_IMAGE = "codefreedom:Chrome-local"
26
+ _DEFAULT_CONTAINER_NAME = "codefreedom-tools-chrome"
27
+ _DEFAULT_PORT = 9222
28
+ _DEFAULT_DATA_DIR = "~/.codefreedom/sandbox/tools/chrome"
29
+
30
+ _CODEFREEDOM_DIR = Path.home() / ".codefreedom"
31
+ _PROFILE_PATH = _CODEFREEDOM_DIR / "profiles" / "chrome.json"
32
+
33
+
34
+ # ── Profile loader ────────────────────────────────────────────────────────────
35
+
36
+
37
+ def _load_profile() -> dict:
38
+ """Load chrome tool profile from ~/.codefreedom/profiles/chrome.json.
39
+
40
+ Returns a flat dict with keys: image, container_name, port, data_dir, env.
41
+ Any missing key falls back to the hardcoded default above.
42
+ """
43
+ settings: dict = {
44
+ "image": _DEFAULT_IMAGE,
45
+ "container_name": _DEFAULT_CONTAINER_NAME,
46
+ "port": _DEFAULT_PORT,
47
+ "data_dir": _DEFAULT_DATA_DIR,
48
+ "env": {},
49
+ }
50
+
51
+ if not _PROFILE_PATH.exists():
52
+ return settings
53
+
54
+ try:
55
+ with open(_PROFILE_PATH, encoding="utf-8") as f:
56
+ raw = json.load(f)
57
+ except (json.JSONDecodeError, OSError) as exc:
58
+ eprint(f"[CHROME] Warning: failed to read {_PROFILE_PATH}: {exc}")
59
+ return settings
60
+
61
+ chrome_cfg = raw.get("chrome", {})
62
+ if not isinstance(chrome_cfg, dict):
63
+ return settings
64
+
65
+ # Merge — profile values override defaults
66
+ if isinstance(chrome_cfg.get("image"), str) and chrome_cfg["image"]:
67
+ settings["image"] = chrome_cfg["image"]
68
+ if (
69
+ isinstance(chrome_cfg.get("container_name"), str)
70
+ and chrome_cfg["container_name"]
71
+ ):
72
+ settings["container_name"] = chrome_cfg["container_name"]
73
+ if isinstance(chrome_cfg.get("port"), int) and chrome_cfg["port"] > 0:
74
+ settings["port"] = chrome_cfg["port"]
75
+ if isinstance(chrome_cfg.get("data_dir"), str) and chrome_cfg["data_dir"]:
76
+ settings["data_dir"] = chrome_cfg["data_dir"]
77
+ if isinstance(chrome_cfg.get("env"), dict):
78
+ settings["env"] = chrome_cfg["env"]
79
+
80
+ return settings
81
+
82
+
83
+ # ── Helpers ────────────────────────────────────────────────────────────────────
84
+
85
+
86
+ def _resolve_data_dir(data_dir: str) -> Path:
87
+ """Resolve ~ to home directory and create the path."""
88
+ path = Path(data_dir).expanduser()
89
+ path.mkdir(parents=True, exist_ok=True)
90
+ return path
91
+
92
+
93
+ def _container_exists(name: str) -> bool:
94
+ """Check if a container exists (running or stopped)."""
95
+ try:
96
+ result = subprocess.run(
97
+ [
98
+ "docker",
99
+ "ps",
100
+ "-a",
101
+ "--filter",
102
+ f"name={name}",
103
+ "--format",
104
+ "{{.Names}}",
105
+ ],
106
+ capture_output=True,
107
+ text=True,
108
+ timeout=10,
109
+ check=False,
110
+ )
111
+ return name in result.stdout.strip().split("\n")
112
+ except (subprocess.SubprocessError, FileNotFoundError):
113
+ return False
114
+
115
+
116
+ def _container_is_running(name: str) -> bool:
117
+ """Check if a container is currently running."""
118
+ try:
119
+ result = subprocess.run(
120
+ ["docker", "ps", "--filter", f"name={name}", "--format", "{{.Names}}"],
121
+ capture_output=True,
122
+ text=True,
123
+ timeout=10,
124
+ check=False,
125
+ )
126
+ return name in result.stdout.strip().split("\n")
127
+ except (subprocess.SubprocessError, FileNotFoundError):
128
+ return False
129
+
130
+
131
+ def _ensure_image(image: str) -> bool:
132
+ """Ensure the Docker image is available locally; pull if missing. Returns True on success."""
133
+ _inspect = subprocess.run(
134
+ ["docker", "image", "inspect", image],
135
+ capture_output=True,
136
+ text=True,
137
+ timeout=10,
138
+ check=False,
139
+ )
140
+ if _inspect.returncode == 0:
141
+ eprint(f"[CHROME] Using cached image '{image}'")
142
+ return True
143
+
144
+ eprint(f"[CHROME] Image '{image}' not found locally, pulling...")
145
+ pull = subprocess.run(
146
+ ["docker", "pull", image],
147
+ capture_output=True,
148
+ text=True,
149
+ timeout=120,
150
+ check=False,
151
+ )
152
+ if pull.returncode == 0:
153
+ eprint(" [OK] Image pulled.")
154
+ return True
155
+
156
+ eprint(f"[ERROR] Failed to pull image '{image}'.")
157
+ if pull.stderr:
158
+ eprint(f" {pull.stderr.strip()}")
159
+ eprint("")
160
+ eprint(" Tips:")
161
+ eprint(
162
+ " • Build locally: docker build -t codefreedom:Chrome-local -f docker/browser/Dockerfile.Chrome docker/browser/"
163
+ )
164
+ eprint(" • Set 'image' in ~/.codefreedom/profiles/chrome.json to your local tag")
165
+ eprint(" • Wait for CI to publish the image to ghcr.io")
166
+ return False
167
+
168
+
169
+ # ── Actions ────────────────────────────────────────────────────────────────────
170
+
171
+
172
+ def start(settings: dict) -> int:
173
+ """Start the Chrome browser container. Returns exit code."""
174
+ image = settings["image"]
175
+ container_name = settings["container_name"]
176
+ port = settings["port"]
177
+ data_dir = settings["data_dir"]
178
+ env_vars = settings.get("env", {})
179
+
180
+ if _container_is_running(container_name):
181
+ eprint(f"[CHROME] Container '{container_name}' is already running.")
182
+ return 0
183
+
184
+ # Check if Docker is available
185
+ if (
186
+ not subprocess.run(
187
+ ["docker", "--version"], capture_output=True, timeout=5, check=False
188
+ ).returncode
189
+ == 0
190
+ ):
191
+ eprint("[ERROR] Docker not found. Install Docker and try again.")
192
+ return 1
193
+
194
+ # Resolve & create data directory
195
+ resolved_data = _resolve_data_dir(data_dir)
196
+ eprint(f"[CHROME] Using data dir: {resolved_data}")
197
+
198
+ # Remove existing container if stopped
199
+ if _container_exists(container_name):
200
+ eprint(f"[CHROME] Removing existing container '{container_name}'...")
201
+ subprocess.run(
202
+ ["docker", "rm", "-f", container_name],
203
+ capture_output=True,
204
+ timeout=15,
205
+ check=False,
206
+ )
207
+
208
+ # Ensure image is available
209
+ if not _ensure_image(image):
210
+ return 1
211
+
212
+ # Build environment flags
213
+ env_flags: list[str] = []
214
+ for key, val in env_vars.items():
215
+ if val:
216
+ env_flags.extend(["-e", f"{key}={val}"])
217
+ # Ensure DISPLAY is set
218
+ if "DISPLAY" not in env_vars:
219
+ env_flags.extend(["-e", "DISPLAY=:99"])
220
+
221
+ # Start container
222
+ eprint(f"[CHROME] Starting container '{container_name}'...")
223
+ create = subprocess.run(
224
+ [
225
+ "docker",
226
+ "run",
227
+ "-d",
228
+ "--name",
229
+ container_name,
230
+ "--network",
231
+ "host",
232
+ "--ipc=host",
233
+ "--cap-add=SYS_ADMIN",
234
+ "--init",
235
+ "--restart",
236
+ "unless-stopped",
237
+ "-v",
238
+ f"{resolved_data}:/data/chrome",
239
+ *env_flags,
240
+ image,
241
+ ],
242
+ capture_output=True,
243
+ text=True,
244
+ timeout=60,
245
+ check=False,
246
+ )
247
+ if create.returncode != 0:
248
+ eprint("[ERROR] Failed to start Chrome container.")
249
+ if create.stderr:
250
+ eprint(f" {create.stderr.strip()}")
251
+ return 1
252
+
253
+ eprint(" [OK] Container started.")
254
+ eprint(f" CDP debug URL: http://127.0.0.1:{port}")
255
+ eprint(
256
+ f" DevTools: devtools://devtools/bundled/inspector.html?ws=127.0.0.1:{port}"
257
+ )
258
+ return 0
259
+
260
+
261
+ def stop(settings: dict) -> int:
262
+ """Stop and remove the Chrome container. Returns exit code."""
263
+ container_name = settings["container_name"]
264
+
265
+ if not _container_exists(container_name):
266
+ eprint(f"[CHROME] Container '{container_name}' does not exist.")
267
+ return 0
268
+
269
+ eprint(f"[CHROME] Stopping container '{container_name}'...")
270
+ subprocess.run(
271
+ ["docker", "stop", container_name],
272
+ capture_output=True,
273
+ timeout=30,
274
+ check=False,
275
+ )
276
+ subprocess.run(
277
+ ["docker", "rm", container_name],
278
+ capture_output=True,
279
+ timeout=15,
280
+ check=False,
281
+ )
282
+ eprint(" [OK] Container stopped and removed.")
283
+ return 0
284
+
285
+
286
+ def status(settings: dict) -> int:
287
+ """Show Chrome container status. Returns exit code."""
288
+ container_name = settings["container_name"]
289
+ port = settings["port"]
290
+
291
+ if not _container_exists(container_name):
292
+ eprint("[CHROME] No Chrome container found.")
293
+ eprint(" Start one with: codefreedom tools chrome start")
294
+ return 0
295
+
296
+ try:
297
+ result = subprocess.run(
298
+ [
299
+ "docker",
300
+ "ps",
301
+ "-a",
302
+ "--filter",
303
+ f"name={container_name}",
304
+ "--format",
305
+ "{{.Names}}\t{{.Status}}\t{{.CreatedAt}}",
306
+ ],
307
+ capture_output=True,
308
+ text=True,
309
+ timeout=10,
310
+ check=False,
311
+ )
312
+ for line in result.stdout.strip().split("\n"):
313
+ if line:
314
+ name, status_line, created = line.split("\t", 2)
315
+ marker = "[RUNNING]" if "Up " in status_line else "[STOPPED]"
316
+ eprint(f"[CHROME] {marker} {name}")
317
+ eprint(f" Status: {status_line}")
318
+ eprint(f" Created: {created}")
319
+
320
+ if _container_is_running(container_name):
321
+ eprint(f" CDP URL: http://127.0.0.1:{port}")
322
+ eprint(
323
+ f" DevTools: devtools://devtools/bundled/inspector.html?ws=127.0.0.1:{port}"
324
+ )
325
+ except (subprocess.SubprocessError, FileNotFoundError) as e:
326
+ eprint(f"[ERROR] Failed to get container status: {e}")
327
+ return 1
328
+ return 0
329
+
330
+
331
+ def url(settings: dict) -> int:
332
+ """Print the CDP debug URL. Returns exit code."""
333
+ container_name = settings["container_name"]
334
+ port = settings["port"]
335
+
336
+ if not _container_is_running(container_name):
337
+ eprint("[CHROME] Chrome container is not running.")
338
+ eprint(" Start it with: codefreedom tools chrome start")
339
+ return 1
340
+
341
+ print(f"http://127.0.0.1:{port}")
342
+ return 0
343
+
344
+
345
+ def run(args: argparse.Namespace) -> int:
346
+ """Execute the chrome tool subcommand. Returns exit code."""
347
+ settings = _load_profile()
348
+
349
+ # CLI --port flag overrides profile and defaults
350
+ if hasattr(args, "port") and args.port:
351
+ settings["port"] = args.port
352
+
353
+ action = args.action or "status"
354
+
355
+ if action == "start":
356
+ return start(settings)
357
+ elif action == "stop":
358
+ return stop(settings)
359
+ elif action == "status":
360
+ return status(settings)
361
+ elif action == "url":
362
+ return url(settings)
363
+ else:
364
+ eprint(f"[ERROR] Unknown action: {action}")
365
+ eprint(" Valid actions: start, stop, status, url")
366
+ return 1
@@ -39,7 +39,7 @@ def _init_codefreedom(
39
39
 
40
40
  profiles_dst_dir = cf_dir / "profiles"
41
41
  profiles_dst = profiles_dst_dir / "claude-code.json"
42
- schema_dst = profiles_dst_dir / "claude-code-profiles.schema.json"
42
+ schema_dst = profiles_dst_dir / "claude-code.schema.json"
43
43
  proxy_dst = cf_dir / "proxy"
44
44
 
45
45
  created_any = False
@@ -50,28 +50,56 @@ def _init_codefreedom(
50
50
  print(f"[init] Profiles already exist: {profiles_dst}")
51
51
  print(" Use --init --force to overwrite.")
52
52
  skipped_any = True
53
- elif (profiles_src / "claude-code-profiles.json").exists():
53
+ elif (profiles_src / "claude-code.json").exists():
54
54
  profiles_dst_dir.mkdir(parents=True, exist_ok=True)
55
- shutil.copy2(profiles_src / "claude-code-profiles.json", profiles_dst)
55
+ shutil.copy2(profiles_src / "claude-code.json", profiles_dst)
56
56
  print(f"[init] [OK] Created {profiles_dst}")
57
57
  created_any = True
58
58
  else:
59
59
  print("[init] [FAIL] Bundled profiles example not found")
60
60
  print(" Reinstall the package or file a bug report.")
61
61
 
62
- # ── Schema ─────────────────────────────────────────────────────────────
62
+ # ── Claude Code schema ─────────────────────────────────────────────────
63
63
  if not force and schema_dst.exists():
64
64
  print(f"[init] Schema already exists: {schema_dst}")
65
65
  skipped_any = True
66
- elif (profiles_src / "claude-code-profiles.schema.json").exists():
66
+ elif (profiles_src / "claude-code.schema.json").exists():
67
67
  profiles_dst_dir.mkdir(parents=True, exist_ok=True)
68
- shutil.copy2(profiles_src / "claude-code-profiles.schema.json", schema_dst)
68
+ shutil.copy2(profiles_src / "claude-code.schema.json", schema_dst)
69
69
  print(f"[init] [OK] Created {schema_dst}")
70
70
  created_any = True
71
71
  else:
72
72
  print("[init] [FAIL] Bundled schema example not found")
73
73
  print(" Reinstall the package or file a bug report.")
74
74
 
75
+ # ── Tool profiles (chrome.json, etc.) ──────────────────────────────────
76
+ for tool_src in sorted(profiles_src.glob("tools-*.json")):
77
+ tool_name = tool_src.stem.replace("tools-", "") # e.g. "chrome"
78
+ tool_dst = profiles_dst_dir / f"{tool_name}.json"
79
+ if not force and tool_dst.exists():
80
+ print(f"[init] Tool profile already exists: {tool_dst}")
81
+ skipped_any = True
82
+ else:
83
+ profiles_dst_dir.mkdir(parents=True, exist_ok=True)
84
+ shutil.copy2(tool_src, tool_dst)
85
+ print(f"[init] [OK] Created tool profile: {tool_dst}")
86
+ created_any = True
87
+
88
+ # ── Tool schemas (chrome.schema.json, etc.) ────────────────────────────
89
+ for schema_src in sorted(profiles_src.glob("*.schema.json")):
90
+ schema_name = schema_src.name # e.g. "chrome.schema.json"
91
+ if schema_name == "claude-code.schema.json":
92
+ continue # already copied above
93
+ schema_dst_file = profiles_dst_dir / schema_name
94
+ if not force and schema_dst_file.exists():
95
+ print(f"[init] Schema already exists: {schema_dst_file}")
96
+ skipped_any = True
97
+ else:
98
+ profiles_dst_dir.mkdir(parents=True, exist_ok=True)
99
+ shutil.copy2(schema_src, schema_dst_file)
100
+ print(f"[init] [OK] Created schema: {schema_dst_file}")
101
+ created_any = True
102
+
75
103
  # ── Proxy configs ──────────────────────────────────────────────────────
76
104
  if not force and proxy_dst.exists():
77
105
  print(f"[init] Proxy configs already exist: {proxy_dst}")
@@ -131,13 +159,11 @@ def _init_codefreedom(
131
159
 
132
160
  # .env.secrets is optional
133
161
  if secrets_dst.exists():
134
- print(f"[init] .env.secrets already exists: {secrets_dst} (skipping)")
162
+ print("[init] .env.secrets already exists (skipping)")
135
163
  elif secrets_src.exists():
136
164
  cf_dir.mkdir(parents=True, exist_ok=True)
137
165
  shutil.copy2(secrets_src, secrets_dst)
138
- print(
139
- f"[init] [OK] Created {secrets_dst} (fully commented -- add your API keys)"
140
- )
166
+ print("[init] [OK] Created .env.secrets (fully commented -- add your API keys)")
141
167
  created_any = True
142
168
 
143
169
  if created_any:
@@ -145,7 +171,9 @@ def _init_codefreedom(
145
171
  print("[init] CodeFreedom is initialized!")
146
172
  print(f" Profiles: {profiles_dst_dir}")
147
173
  print(" - claude-code.json")
148
- print(" - claude-code-profiles.schema.json")
174
+ print(" - claude-code.schema.json")
175
+ print(" - chrome.json (tool)")
176
+ print(" - chrome.schema.json (tool)")
149
177
  print(f" Proxy: {proxy_dst}")
150
178
  print(f" Env: {cf_dir}")
151
179
  print(" - .env (fully commented)")
@@ -231,6 +259,34 @@ def main() -> None:
231
259
  help="Arguments forwarded to the 'claude' CLI",
232
260
  )
233
261
 
262
+ # ── tools subcommand ──────────────────────────────────────────────────
263
+ tools_parser = subparsers.add_parser(
264
+ "tools",
265
+ help="Manage auxiliary tools (chrome browser, etc.)",
266
+ description="Manage auxiliary tools used by coding agents (Chrome browser with Xvfb, etc.).",
267
+ )
268
+ tools_subparsers = tools_parser.add_subparsers(dest="tool", title="tools")
269
+
270
+ # ── chrome tool ─────────────────────────────────────────────────────
271
+ chrome_parser = tools_subparsers.add_parser(
272
+ "chrome",
273
+ help="Chrome browser with Xvfb for undetectable headed browsing",
274
+ description="Start/stop/manage a Chrome browser container with virtual display (Xvfb) for undetectable web browsing. Coding agents connect via Chrome DevTools Protocol (CDP) at port 9222.",
275
+ )
276
+ chrome_parser.add_argument(
277
+ "action",
278
+ nargs="?",
279
+ default="status",
280
+ choices=["start", "stop", "status", "url"],
281
+ help="Action to perform (default: status)",
282
+ )
283
+ chrome_parser.add_argument(
284
+ "--port",
285
+ type=int,
286
+ default=9222,
287
+ help="CDP debug port (default: 9222)",
288
+ )
289
+
234
290
  # ── proxy subcommand ───────────────────────────────────────────────────
235
291
  proxy_parser = subparsers.add_parser(
236
292
  "proxy",
@@ -324,6 +380,21 @@ def main() -> None:
324
380
  from codefreedom.cli.proxy import run as proxy_run
325
381
 
326
382
  sys.exit(proxy_run(args))
383
+ elif args.command == "tools":
384
+ if args.tool == "chrome":
385
+ if unknown:
386
+ eprint(f"[ERROR] Unrecognized arguments: {' '.join(unknown)}")
387
+ sys.exit(2)
388
+ from codefreedom.cli.chrome import run as chrome_run
389
+
390
+ sys.exit(chrome_run(args))
391
+ elif args.tool is None:
392
+ tools_parser.print_help()
393
+ sys.exit(0)
394
+ else:
395
+ eprint(f"[ERROR] Unknown tool: {args.tool}")
396
+ eprint(" Available tools: chrome")
397
+ sys.exit(1)
327
398
  else:
328
399
  parser.print_help()
329
400
  sys.exit(0)
@@ -0,0 +1,55 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "chrome.schema.json",
4
+ "title": "Chrome Tool Profile",
5
+ "description": "Schema for chrome.json — configures the Chrome browser container image, ports, data directory, and environment.",
6
+ "type": "object",
7
+ "properties": {
8
+ "description": {
9
+ "type": "string",
10
+ "description": "Description of the profile"
11
+ },
12
+ "notes": {
13
+ "type": "array",
14
+ "items": {
15
+ "type": "string"
16
+ },
17
+ "description": "Usage notes"
18
+ },
19
+ "chrome": {
20
+ "type": "object",
21
+ "description": "Chrome browser container settings",
22
+ "properties": {
23
+ "image": {
24
+ "type": "string",
25
+ "description": "Docker image tag (e.g. codefreedom:Chrome-local or ghcr.io/nilayparikh/codefreedom:Chrome-latest)"
26
+ },
27
+ "container_name": {
28
+ "type": "string",
29
+ "description": "Docker container name"
30
+ },
31
+ "port": {
32
+ "type": "integer",
33
+ "description": "CDP debug port",
34
+ "minimum": 1024,
35
+ "maximum": 65535
36
+ },
37
+ "data_dir": {
38
+ "type": "string",
39
+ "description": "Host path for persistent Chrome profile data (~ is expanded to $HOME)"
40
+ },
41
+ "env": {
42
+ "type": "object",
43
+ "description": "Environment variables forwarded to the container",
44
+ "additionalProperties": {
45
+ "type": "string"
46
+ }
47
+ }
48
+ },
49
+ "additionalProperties": false
50
+ }
51
+ },
52
+ "required": [
53
+ "chrome"
54
+ ]
55
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "claude-code-profiles.schema.json",
2
+ "$schema": "claude-code.schema.json",
3
3
  "description": "Claude Code profiles for codefreedom — each profile sets model selection, API endpoint, and auth explicitly.",
4
4
  "notes": [
5
5
  "In sandbox mode (--sandbox), each profile gets its own isolated ~/.codefreedom/{profile}/.claude directory.",
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "chrome.schema.json",
3
+ "description": "Chrome browser tool profile for codefreedom — configures container image, ports, data directory, and environment.",
4
+ "notes": [
5
+ "The 'chrome' block defines all settings for 'codefreedom tools chrome'.",
6
+ "'image' is the Docker image to use — change to 'ghcr.io/nilayparikh/codefreedom:Chrome-latest' for published builds.",
7
+ "'container_name' is the Docker container name (persistent, restart unless-stopped).",
8
+ "'port' is the CDP debug port exposed to the host.",
9
+ "'data_dir' is the host path mounted to /data/chrome inside the container.",
10
+ "'env' are environment variables forwarded to the container."
11
+ ],
12
+ "chrome": {
13
+ "image": "codefreedom:Chrome-local",
14
+ "container_name": "codefreedom-chrome",
15
+ "port": 9222,
16
+ "data_dir": "~/.codefreedom/sandbox/tools/chrome",
17
+ "env": {
18
+ "DISPLAY": ":99",
19
+ "CHROME_DEBUG_PORT": "9222"
20
+ }
21
+ }
22
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codefreedom
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: Single wrapper for all code agents — simple LLM routing, sandboxing, profile management, and isolation. All config in ~/.codefreedom.
5
5
  Author-email: Nilay Parikh <nilay.parikh@gmail.com>
6
6
  License: Apache-2.0
@@ -27,14 +27,14 @@ Requires-Dist: PyYAML>=6.0
27
27
  Requires-Dist: types-PyYAML
28
28
  Provides-Extra: litellm
29
29
  Requires-Dist: litellm[proxy]>=1.50; extra == "litellm"
30
- Requires-Dist: prometheus-client>=0.20; extra == "litellm"
30
+ Requires-Dist: prometheus-client>=0.25.0; extra == "litellm"
31
31
  Provides-Extra: dev
32
32
  Requires-Dist: mypy>=1.0; extra == "dev"
33
33
  Requires-Dist: ruff>=0.1; extra == "dev"
34
34
  Requires-Dist: pytest>=7.0; extra == "dev"
35
35
  Requires-Dist: types-PyYAML; extra == "dev"
36
36
  Provides-Extra: docs
37
- Requires-Dist: mkdocs-material[imaging]>=9.6; extra == "docs"
37
+ Requires-Dist: mkdocs-material[imaging]>=9.7.6; extra == "docs"
38
38
  Requires-Dist: mkdocs-mermaid2-plugin>=1.2; extra == "docs"
39
39
  Provides-Extra: all
40
40
  Requires-Dist: codefreedom[dev,docs,litellm]; extra == "all"
@@ -14,13 +14,16 @@ src/codefreedom.egg-info/entry_points.txt
14
14
  src/codefreedom.egg-info/requires.txt
15
15
  src/codefreedom.egg-info/top_level.txt
16
16
  src/codefreedom/cli/__init__.py
17
+ src/codefreedom/cli/chrome.py
17
18
  src/codefreedom/cli/claude.py
18
19
  src/codefreedom/cli/main.py
19
20
  src/codefreedom/cli/proxy.py
20
21
  src/codefreedom/examples/.env.example
21
22
  src/codefreedom/examples/.env.secrets.example
22
- src/codefreedom/examples/profiles/claude-code-profiles.json
23
- src/codefreedom/examples/profiles/claude-code-profiles.schema.json
23
+ src/codefreedom/examples/profiles/chrome.schema.json
24
+ src/codefreedom/examples/profiles/claude-code.json
25
+ src/codefreedom/examples/profiles/claude-code.schema.json
26
+ src/codefreedom/examples/profiles/tools-chrome.json
24
27
  src/codefreedom/examples/proxy/config.yaml
25
28
  src/codefreedom/examples/proxy/docker-compose.yaml
26
29
  src/codefreedom/examples/proxy/providers/anthropic-compatible.yaml
@@ -11,9 +11,9 @@ pytest>=7.0
11
11
  types-PyYAML
12
12
 
13
13
  [docs]
14
- mkdocs-material[imaging]>=9.6
14
+ mkdocs-material[imaging]>=9.7.6
15
15
  mkdocs-mermaid2-plugin>=1.2
16
16
 
17
17
  [litellm]
18
18
  litellm[proxy]>=1.50
19
- prometheus-client>=0.20
19
+ prometheus-client>=0.25.0
@@ -14,13 +14,34 @@ def _setup_bundled_examples(root: Path) -> Path:
14
14
  # Profiles
15
15
  profiles_dir = examples / "profiles"
16
16
  profiles_dir.mkdir(parents=True)
17
- (profiles_dir / "claude-code-profiles.json").write_text(
17
+ (profiles_dir / "claude-code.json").write_text(
18
18
  json.dumps({"profiles": {"default": {"description": "test", "env": {}}}})
19
19
  )
20
- (profiles_dir / "claude-code-profiles.schema.json").write_text(
20
+ (profiles_dir / "claude-code.schema.json").write_text(
21
21
  json.dumps({"$schema": "http://json-schema.org/draft-07/schema#"})
22
22
  )
23
23
 
24
+ # Tool profiles + schemas (tools-*.json, * .schema.json)
25
+ (profiles_dir / "tools-chrome.json").write_text(
26
+ json.dumps(
27
+ {
28
+ "chrome": {
29
+ "image": "codefreedom:Chrome-local",
30
+ "container_name": "codefreedom-chrome",
31
+ "port": 9222,
32
+ }
33
+ }
34
+ )
35
+ )
36
+ (profiles_dir / "chrome.schema.json").write_text(
37
+ json.dumps(
38
+ {
39
+ "$schema": "http://json-schema.org/draft-07/schema#",
40
+ "title": "Chrome Tool Profile",
41
+ }
42
+ )
43
+ )
44
+
24
45
  # Proxy
25
46
  proxy_dir = examples / "proxy"
26
47
  proxy_dir.mkdir(parents=True)
@@ -64,11 +85,21 @@ class TestInitCodefreedom:
64
85
  assert content["profiles"]["default"]["description"] == "test"
65
86
 
66
87
  # Verify schema was created
67
- schema_dst = cf_dir / "profiles" / "claude-code-profiles.schema.json"
88
+ schema_dst = cf_dir / "profiles" / "claude-code.schema.json"
68
89
  assert schema_dst.exists()
69
90
  schema_content = json.loads(schema_dst.read_text())
70
91
  assert "$schema" in schema_content
71
92
 
93
+ # Verify tool profile (chrome.json from tools-chrome.json)
94
+ tool_dst = cf_dir / "profiles" / "chrome.json"
95
+ assert tool_dst.exists()
96
+ tool_content = json.loads(tool_dst.read_text())
97
+ assert "chrome" in tool_content
98
+
99
+ # Verify tool schema (chrome.schema.json)
100
+ tool_schema_dst = cf_dir / "profiles" / "chrome.schema.json"
101
+ assert tool_schema_dst.exists()
102
+
72
103
  # Verify proxy was created with correct nested structure
73
104
  proxy_dst = cf_dir / "proxy"
74
105
  assert proxy_dst.exists()
@@ -107,7 +138,7 @@ class TestInitCodefreedom:
107
138
  # Setup source examples with different content
108
139
  examples = _setup_bundled_examples(tmp_path)
109
140
  # Override with newer content so we can verify it was NOT copied
110
- (examples / "profiles" / "claude-code-profiles.json").write_text(
141
+ (examples / "profiles" / "claude-code.json").write_text(
111
142
  json.dumps({"profiles": {"default": {"env": {}}}})
112
143
  )
113
144
  (examples / "proxy" / "config.yaml").write_text("new content")
@@ -146,7 +177,7 @@ class TestInitCodefreedom:
146
177
  new_content = json.dumps(
147
178
  {"profiles": {"test": {"description": "new", "env": {"KEY": "val"}}}}
148
179
  )
149
- (examples / "profiles" / "claude-code-profiles.json").write_text(new_content)
180
+ (examples / "profiles" / "claude-code.json").write_text(new_content)
150
181
  (examples / "proxy" / "config.yaml").write_text("new proxy")
151
182
  (examples / "proxy" / "docker-compose.yaml").write_text("new compose")
152
183
 
File without changes
File without changes
File without changes
File without changes