saturnolab 0.1.0__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,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .ipynb_checkpoints/
9
+ frontend/node_modules/
10
+ frontend/dist/
11
+ saturnolab/static/
12
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maicon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.5
2
+ Name: saturnolab
3
+ Version: 0.1.0
4
+ Summary: Lightweight JupyterLab-compatible Python notebook environment with native inline AI connected to live kernel memory.
5
+ Project-URL: Homepage, https://github.com/maicondallg/saturnolab
6
+ Project-URL: Repository, https://github.com/maicondallg/saturnolab
7
+ Project-URL: Issues, https://github.com/maicondallg/saturnolab/issues
8
+ Author: Maicon
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,data-science,ipynb,jupyter,jupyterlab,llm,notebook,python
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Framework :: FastAPI
15
+ Classifier: Framework :: Jupyter
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Science/Research
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: fastapi>=0.115.0
27
+ Requires-Dist: httpx>=0.27.0
28
+ Requires-Dist: ipykernel>=6.29.0
29
+ Requires-Dist: jupyter-client>=8.6.0
30
+ Requires-Dist: nbformat>=5.10.0
31
+ Requires-Dist: pydantic>=2.7.0
32
+ Requires-Dist: uvicorn[standard]>=0.30.0
33
+ Requires-Dist: websockets>=12.0
34
+ Provides-Extra: data
35
+ Requires-Dist: matplotlib>=3.8.0; extra == 'data'
36
+ Requires-Dist: numpy>=1.26.0; extra == 'data'
37
+ Requires-Dist: pandas>=2.0.0; extra == 'data'
38
+ Requires-Dist: seaborn>=0.13.0; extra == 'data'
39
+ Description-Content-Type: text/markdown
40
+
41
+ # SaturnoLab
42
+
43
+ **SaturnoLab** (`saturnolab`) is a fast, lightweight, `.ipynb`-compatible Python notebook environment built from the ground up with **Inline AI (`Ctrl+I` / `Ctrl+K`)** connected directly to live Python kernel memory.
44
+
45
+ ## Features
46
+
47
+ - **100% `.ipynb` Compatible**: Open, edit, execute, and save standard Jupyter notebooks (`nbformat` v4).
48
+ - **Zero-bloat Web UI**: Built with React, CodeMirror 6, and FastAPI (~305 KB gzipped) with 6 technical themes (*Graphite*, *Obsidian*, *Gruvbox*, *Nord*, *Jupyter Light*, *Warm Stone*).
49
+ - **Inline Cell AI (`Ctrl+I` / `Ctrl+K`)**:
50
+ - Generate or edit Python code directly inside any cell or over a selected snippet.
51
+ - Automatically injects **live kernel variable schemas** (`DataFrame` shapes, column names, `dtypes`, array dimensions) and preceding cell context into the prompt.
52
+ - Instant `Tab` to accept, `Shift+Tab` to toggle *Replace* vs *Append*, and `Ctrl+Enter` to accept & execute immediately.
53
+ - **Multi-Provider LLM Support**:
54
+ - **Google Gemini API** (`gemini-2.5-flash`, `gemini-2.5-pro`, etc.)
55
+ - **OpenAI Codex CLI / SDK** (`@openai/codex`)
56
+ - **OpenAI-compatible APIs** (OpenAI, local **Ollama**, OpenRouter, LM Studio)
57
+ - **Anthropic Claude API**
58
+ - **Automatic Virtualenv Detection**: Automatically detects `.venv` / `venv` / `$VIRTUAL_ENV` in the target project folder.
59
+
60
+ ---
61
+
62
+ ## Installation
63
+
64
+ ```bash
65
+ pip install saturnolab
66
+
67
+ # Optional: include common data science stack (pandas, numpy, matplotlib, seaborn)
68
+ pip install "saturnolab[data]"
69
+ ```
70
+
71
+ Or install globally as an isolated CLI tool with **`uv`**:
72
+
73
+ ```bash
74
+ uv tool install saturnolab
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Usage
80
+
81
+ Run `saturnolab` inside **any** project directory, just like `jupyter lab`:
82
+
83
+ ```bash
84
+ # Open in the current directory
85
+ saturnolab
86
+
87
+ # Or point to any folder
88
+ saturnolab /path/to/project
89
+
90
+ # Custom port or headless server mode
91
+ saturnolab --port 8900 --no-browser
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Keyboard Shortcuts
97
+
98
+ | Shortcut | Action |
99
+ | --- | --- |
100
+ | `Ctrl + I` or `Ctrl + K` | Open Inline AI inside active cell or selection |
101
+ | `Shift + Tab` | Toggle between **Replace cell** and **Append to end** |
102
+ | `Tab` / `Enter` | Accept generated code |
103
+ | `Ctrl + Enter` | Run active cell (or Accept & Run in AI preview) |
104
+ | `Shift + Enter` | Run active cell and advance |
105
+ | `Alt + Enter` | Run active cell and insert new cell below |
106
+ | `Alt + ↑ / ↓` | Move cell up / down |
107
+ | `Ctrl + M` | Toggle cell between Python and Markdown |
108
+ | `Ctrl + Shift + Backspace` | Delete active cell |
109
+ | `Ctrl + S` | Save notebook |
@@ -0,0 +1,69 @@
1
+ # SaturnoLab
2
+
3
+ **SaturnoLab** (`saturnolab`) is a fast, lightweight, `.ipynb`-compatible Python notebook environment built from the ground up with **Inline AI (`Ctrl+I` / `Ctrl+K`)** connected directly to live Python kernel memory.
4
+
5
+ ## Features
6
+
7
+ - **100% `.ipynb` Compatible**: Open, edit, execute, and save standard Jupyter notebooks (`nbformat` v4).
8
+ - **Zero-bloat Web UI**: Built with React, CodeMirror 6, and FastAPI (~305 KB gzipped) with 6 technical themes (*Graphite*, *Obsidian*, *Gruvbox*, *Nord*, *Jupyter Light*, *Warm Stone*).
9
+ - **Inline Cell AI (`Ctrl+I` / `Ctrl+K`)**:
10
+ - Generate or edit Python code directly inside any cell or over a selected snippet.
11
+ - Automatically injects **live kernel variable schemas** (`DataFrame` shapes, column names, `dtypes`, array dimensions) and preceding cell context into the prompt.
12
+ - Instant `Tab` to accept, `Shift+Tab` to toggle *Replace* vs *Append*, and `Ctrl+Enter` to accept & execute immediately.
13
+ - **Multi-Provider LLM Support**:
14
+ - **Google Gemini API** (`gemini-2.5-flash`, `gemini-2.5-pro`, etc.)
15
+ - **OpenAI Codex CLI / SDK** (`@openai/codex`)
16
+ - **OpenAI-compatible APIs** (OpenAI, local **Ollama**, OpenRouter, LM Studio)
17
+ - **Anthropic Claude API**
18
+ - **Automatic Virtualenv Detection**: Automatically detects `.venv` / `venv` / `$VIRTUAL_ENV` in the target project folder.
19
+
20
+ ---
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install saturnolab
26
+
27
+ # Optional: include common data science stack (pandas, numpy, matplotlib, seaborn)
28
+ pip install "saturnolab[data]"
29
+ ```
30
+
31
+ Or install globally as an isolated CLI tool with **`uv`**:
32
+
33
+ ```bash
34
+ uv tool install saturnolab
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Usage
40
+
41
+ Run `saturnolab` inside **any** project directory, just like `jupyter lab`:
42
+
43
+ ```bash
44
+ # Open in the current directory
45
+ saturnolab
46
+
47
+ # Or point to any folder
48
+ saturnolab /path/to/project
49
+
50
+ # Custom port or headless server mode
51
+ saturnolab --port 8900 --no-browser
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Keyboard Shortcuts
57
+
58
+ | Shortcut | Action |
59
+ | --- | --- |
60
+ | `Ctrl + I` or `Ctrl + K` | Open Inline AI inside active cell or selection |
61
+ | `Shift + Tab` | Toggle between **Replace cell** and **Append to end** |
62
+ | `Tab` / `Enter` | Accept generated code |
63
+ | `Ctrl + Enter` | Run active cell (or Accept & Run in AI preview) |
64
+ | `Shift + Enter` | Run active cell and advance |
65
+ | `Alt + Enter` | Run active cell and insert new cell below |
66
+ | `Alt + ↑ / ↓` | Move cell up / down |
67
+ | `Ctrl + M` | Toggle cell between Python and Markdown |
68
+ | `Ctrl + Shift + Backspace` | Delete active cell |
69
+ | `Ctrl + S` | Save notebook |
@@ -0,0 +1,78 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "saturnolab"
7
+ version = "0.1.0"
8
+ description = "Lightweight JupyterLab-compatible Python notebook environment with native inline AI connected to live kernel memory."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Maicon" }
14
+ ]
15
+ keywords = [
16
+ "jupyter",
17
+ "jupyterlab",
18
+ "notebook",
19
+ "ipynb",
20
+ "python",
21
+ "ai",
22
+ "llm",
23
+ "data-science",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 4 - Beta",
27
+ "Environment :: Web Environment",
28
+ "Framework :: FastAPI",
29
+ "Framework :: Jupyter",
30
+ "Intended Audience :: Developers",
31
+ "Intended Audience :: Science/Research",
32
+ "Operating System :: OS Independent",
33
+ "Programming Language :: Python :: 3",
34
+ "Programming Language :: Python :: 3.10",
35
+ "Programming Language :: Python :: 3.11",
36
+ "Programming Language :: Python :: 3.12",
37
+ "Programming Language :: Python :: 3.13",
38
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
39
+ ]
40
+ dependencies = [
41
+ "fastapi>=0.115.0",
42
+ "uvicorn[standard]>=0.30.0",
43
+ "websockets>=12.0",
44
+ "jupyter-client>=8.6.0",
45
+ "ipykernel>=6.29.0",
46
+ "nbformat>=5.10.0",
47
+ "httpx>=0.27.0",
48
+ "pydantic>=2.7.0",
49
+ ]
50
+
51
+ [project.optional-dependencies]
52
+ data = [
53
+ "pandas>=2.0.0",
54
+ "numpy>=1.26.0",
55
+ "matplotlib>=3.8.0",
56
+ "seaborn>=0.13.0",
57
+ ]
58
+
59
+ [project.urls]
60
+ Homepage = "https://github.com/maicondallg/saturnolab"
61
+ Repository = "https://github.com/maicondallg/saturnolab"
62
+ Issues = "https://github.com/maicondallg/saturnolab/issues"
63
+
64
+ [project.scripts]
65
+ saturnolab = "saturnolab.cli:main"
66
+
67
+ [tool.hatch.build.targets.wheel]
68
+ packages = ["saturnolab"]
69
+ artifacts = ["saturnolab/static/**"]
70
+
71
+ [tool.hatch.build.targets.sdist]
72
+ include = [
73
+ "/saturnolab",
74
+ "/README.md",
75
+ "/LICENSE",
76
+ "/pyproject.toml",
77
+ ]
78
+ artifacts = ["saturnolab/static/**"]
@@ -0,0 +1,3 @@
1
+ """SaturnoLab — Lightweight AI-first Python Notebook Environment."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+ import socket
6
+ import threading
7
+ import time
8
+ import webbrowser
9
+ from pathlib import Path
10
+
11
+ import uvicorn
12
+
13
+ from . import __version__
14
+
15
+
16
+ def find_available_port(host: str, preferred_port: int, max_tries: int = 50) -> int:
17
+ for offset in range(max_tries):
18
+ port = preferred_port + offset
19
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
20
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
21
+ try:
22
+ sock.bind((host, port))
23
+ return port
24
+ except OSError:
25
+ continue
26
+ return preferred_port
27
+
28
+
29
+ def main(argv: list[str] | None = None) -> None:
30
+ parser = argparse.ArgumentParser(
31
+ prog="saturnolab",
32
+ description="SaturnoLab — Ambiente leve de notebooks Python com IA Inline integrada ao Kernel.",
33
+ )
34
+ parser.add_argument(
35
+ "directory",
36
+ nargs="?",
37
+ default=".",
38
+ help="Diretório do projeto para abrir no SaturnoLab (padrão: diretório atual).",
39
+ )
40
+ parser.add_argument(
41
+ "-p",
42
+ "--port",
43
+ type=int,
44
+ default=int(os.environ.get("SATURNOLAB_PORT", "8899")),
45
+ help="Porta HTTP inicial (padrão: 8899; avança automaticamente se ocupada).",
46
+ )
47
+ parser.add_argument(
48
+ "--host",
49
+ default=os.environ.get("SATURNOLAB_HOST", "127.0.0.1"),
50
+ help="Host para escutar conexões (padrão: 127.0.0.1).",
51
+ )
52
+ parser.add_argument(
53
+ "--no-browser",
54
+ action="store_true",
55
+ help="Não abrir o navegador automaticamente ao iniciar.",
56
+ )
57
+ parser.add_argument(
58
+ "--reload",
59
+ action="store_true",
60
+ help="Habilitar auto-reload do servidor (desenvolvimento).",
61
+ )
62
+ parser.add_argument(
63
+ "-v",
64
+ "--version",
65
+ action="version",
66
+ version=f"saturnolab {__version__}",
67
+ )
68
+
69
+ args = parser.parse_args(argv)
70
+
71
+ workspace_path = Path(args.directory).expanduser().resolve()
72
+ if not workspace_path.exists():
73
+ workspace_path.mkdir(parents=True, exist_ok=True)
74
+ elif not workspace_path.is_dir():
75
+ parser.error(f"O caminho informado não é um diretório: {workspace_path}")
76
+
77
+ os.environ["SATURNOLAB_WORKSPACE"] = str(workspace_path)
78
+
79
+ port = find_available_port(args.host, args.port)
80
+ url = f"http://{args.host}:{port}"
81
+
82
+ print(f"🪐 SaturnoLab v{__version__}")
83
+ print(f"📂 Diretório : {workspace_path}")
84
+ print(f"🌐 Servidor : {url}")
85
+ print(" Pressione Ctrl+C para encerrar.\n")
86
+
87
+ if not args.no_browser:
88
+ def _open_browser() -> None:
89
+ time.sleep(0.8)
90
+ try:
91
+ webbrowser.open(url)
92
+ except Exception:
93
+ pass
94
+
95
+ threading.Thread(target=_open_browser, daemon=True).start()
96
+
97
+ uvicorn.run(
98
+ "saturnolab.main:app",
99
+ host=args.host,
100
+ port=port,
101
+ reload=args.reload,
102
+ log_level="warning",
103
+ )
104
+
105
+
106
+ if __name__ == "__main__":
107
+ main()
@@ -0,0 +1,209 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+ CONFIG_DIR = Path.home() / ".saturnolab"
12
+ CONFIG_FILE = CONFIG_DIR / "config.json"
13
+ LEGACY_CONFIG_FILE = Path.home() / ".saturno" / "config.json"
14
+
15
+ VALID_THEMES = {
16
+ "graphite",
17
+ "obsidian",
18
+ "gruvbox",
19
+ "nord",
20
+ "paper",
21
+ "solarized_light",
22
+ "dark",
23
+ "light",
24
+ }
25
+
26
+
27
+ class ProviderSettings(BaseModel):
28
+ api_key: str = ""
29
+ model: str = ""
30
+ base_url: str = ""
31
+ reasoning_effort: str = "low"
32
+
33
+
34
+ class SaturnoConfig(BaseModel):
35
+ active_provider: str = "gemini" # gemini | codex | openai_compatible | anthropic
36
+ theme: str = "graphite"
37
+ default_ai_apply_mode: str = "replace_cell" # replace_cell | append_cell
38
+ include_kernel_variables: bool = True
39
+ providers: dict[str, ProviderSettings] = Field(
40
+ default_factory=lambda: {
41
+ "gemini": ProviderSettings(
42
+ api_key="",
43
+ model="gemini-2.5-flash",
44
+ base_url="https://generativelanguage.googleapis.com/v1beta",
45
+ ),
46
+ "codex": ProviderSettings(
47
+ api_key="",
48
+ model="",
49
+ base_url="",
50
+ reasoning_effort="low",
51
+ ),
52
+ "openai_compatible": ProviderSettings(
53
+ api_key="",
54
+ model="gpt-4.1-mini",
55
+ base_url="https://api.openai.com/v1",
56
+ ),
57
+ "anthropic": ProviderSettings(
58
+ api_key="",
59
+ model="claude-sonnet-4-5",
60
+ base_url="https://api.anthropic.com/v1",
61
+ ),
62
+ }
63
+ )
64
+
65
+
66
+ class ConfigManager:
67
+ def __init__(self) -> None:
68
+ self._migrate_legacy_if_needed()
69
+ self._config = self._load()
70
+ self._workspace_path = self._resolve_initial_workspace()
71
+
72
+ def _migrate_legacy_if_needed(self) -> None:
73
+ if not CONFIG_FILE.exists() and LEGACY_CONFIG_FILE.exists():
74
+ try:
75
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
76
+ shutil.copy2(LEGACY_CONFIG_FILE, CONFIG_FILE)
77
+ except Exception:
78
+ pass
79
+
80
+ def _resolve_initial_workspace(self) -> Path:
81
+ env_ws = (
82
+ os.environ.get("SATURNOLAB_WORKSPACE", "").strip()
83
+ or os.environ.get("SATURNO_WORKSPACE", "").strip()
84
+ )
85
+ if env_ws:
86
+ target = Path(env_ws).expanduser().resolve()
87
+ target.mkdir(parents=True, exist_ok=True)
88
+ return target
89
+ return Path.cwd().resolve()
90
+
91
+ def _load(self) -> SaturnoConfig:
92
+ cfg = SaturnoConfig()
93
+ if CONFIG_FILE.exists():
94
+ try:
95
+ raw = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
96
+ # Ignore deprecated workspace_dir key from older configs
97
+ raw.pop("workspace_dir", None)
98
+ loaded = SaturnoConfig.model_validate(raw)
99
+ for key, default_val in cfg.providers.items():
100
+ if key not in loaded.providers:
101
+ loaded.providers[key] = default_val
102
+ cfg = loaded
103
+ except Exception:
104
+ pass
105
+ return cfg
106
+
107
+ def save(self) -> None:
108
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
109
+ CONFIG_FILE.write_text(
110
+ json.dumps(self._config.model_dump(), indent=2, ensure_ascii=False),
111
+ encoding="utf-8",
112
+ )
113
+
114
+ @property
115
+ def config(self) -> SaturnoConfig:
116
+ return self._config
117
+
118
+ def get_workspace_path(self) -> Path:
119
+ if not self._workspace_path.exists():
120
+ self._workspace_path.mkdir(parents=True, exist_ok=True)
121
+ return self._workspace_path
122
+
123
+ def set_workspace_path(self, new_dir: str) -> Path:
124
+ target = Path(new_dir).expanduser().resolve()
125
+ if not target.exists() or not target.is_dir():
126
+ raise ValueError(f"Diretório não encontrado: {new_dir}")
127
+ self._workspace_path = target
128
+ return target
129
+
130
+ def update_settings(self, patch: dict[str, Any]) -> SaturnoConfig:
131
+ if "active_provider" in patch and patch["active_provider"]:
132
+ self._config.active_provider = str(patch["active_provider"])
133
+ if "theme" in patch and patch["theme"] in VALID_THEMES:
134
+ self._config.theme = str(patch["theme"])
135
+ if "default_ai_apply_mode" in patch and patch["default_ai_apply_mode"] in (
136
+ "replace_cell",
137
+ "append_cell",
138
+ ):
139
+ self._config.default_ai_apply_mode = patch["default_ai_apply_mode"]
140
+ if "include_kernel_variables" in patch:
141
+ self._config.include_kernel_variables = bool(patch["include_kernel_variables"])
142
+ if "providers" in patch and isinstance(patch["providers"], dict):
143
+ for prov_name, prov_data in patch["providers"].items():
144
+ if not isinstance(prov_data, dict):
145
+ continue
146
+ existing = self._config.providers.get(prov_name, ProviderSettings())
147
+ updated_dict = existing.model_dump()
148
+ for k, v in prov_data.items():
149
+ if k == "api_key" and v == "********":
150
+ continue
151
+ if k in updated_dict and v is not None:
152
+ updated_dict[k] = v
153
+ self._config.providers[prov_name] = ProviderSettings(**updated_dict)
154
+ self.save()
155
+ return self._config
156
+
157
+ def resolve_api_key(self, provider: str) -> str:
158
+ prov = self._config.providers.get(provider)
159
+ if prov and prov.api_key.strip():
160
+ return prov.api_key.strip()
161
+ env_map = {
162
+ "gemini": ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
163
+ "codex": ["CODEX_API_KEY", "OPENAI_API_KEY"],
164
+ "openai_compatible": ["OPENAI_API_KEY"],
165
+ "anthropic": ["ANTHROPIC_API_KEY"],
166
+ }
167
+ for env_name in env_map.get(provider, []):
168
+ val = os.environ.get(env_name, "").strip()
169
+ if val:
170
+ return val
171
+ return ""
172
+
173
+ @staticmethod
174
+ def resolve_codex_binary() -> str | None:
175
+ found = shutil.which("codex")
176
+ if found:
177
+ return found
178
+ local_bin = Path.home() / ".local" / "bin" / "codex"
179
+ if local_bin.exists():
180
+ return str(local_bin)
181
+ return None
182
+
183
+ def public_dict(self) -> dict[str, Any]:
184
+ data = self._config.model_dump()
185
+ codex_bin = self.resolve_codex_binary()
186
+ npx_bin = shutil.which("npx")
187
+ codex_auth_exists = (Path.home() / ".codex" / "auth.json").exists()
188
+
189
+ providers_out: dict[str, Any] = {}
190
+ for name, p in data["providers"].items():
191
+ resolved_key = self.resolve_api_key(name)
192
+ providers_out[name] = {
193
+ **p,
194
+ "api_key": "********" if p.get("api_key") else "",
195
+ "has_key": bool(resolved_key),
196
+ }
197
+ return {
198
+ **data,
199
+ "workspace_dir": str(self.get_workspace_path()),
200
+ "providers": providers_out,
201
+ "codex_status": {
202
+ "cli_installed": bool(codex_bin),
203
+ "auth_found": codex_auth_exists,
204
+ "sdk_ready": bool(codex_bin or npx_bin),
205
+ },
206
+ }
207
+
208
+
209
+ config_manager = ConfigManager()