Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 50 additions & 5 deletions lib/env_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,64 @@
from __future__ import annotations

import os
import re
from pathlib import Path
from typing import Optional

from dotenv import load_dotenv
# Try to import python-dotenv, but fall back gracefully
try:
from dotenv import load_dotenv as _load_dotenv_lib
HAS_DOTENV = True
except ImportError:
HAS_DOTENV = False


def load_env(project_root: Optional[Path] = None) -> None:
"""Load .env file from project root."""
def load_env(project_root: Optional[Path] = None, use_dotenv: bool = False) -> None:
"""Load .env file from project root.

Args:
project_root: Project root directory (defaults to parent of this file)
use_dotenv: If True, use python-dotenv if available; otherwise use manual parsing
"""
if project_root is None:
project_root = Path(__file__).resolve().parent.parent
env_path = project_root / ".env"
if env_path.exists():
load_dotenv(env_path)

if not env_path.exists():
return

if use_dotenv and HAS_DOTENV:
_load_dotenv_lib(env_path)
else:
_load_env_manual(env_path)


def _load_env_manual(env_path: Path) -> None:
"""Manual .env parsing (no dependencies) - matches original behavior."""
with open(env_path, encoding="utf-8", errors="ignore") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
key = key.strip()
value = value.strip()

# Quoted value: take the content inside the quotes verbatim
if value[:1] in ("'", '"'):
quote = value[0]
end = value.find(quote, 1)
value = value[1:end] if end != -1 else value[1:]
else:
# Strip an inline comment ('#' at line start or after
# whitespace) so "VAR= # note" yields "" not "# note"
match = re.search(r"(^|\s)#", value)
if match:
value = value[: match.start()]
value = value.strip()

if key and key not in os.environ:
os.environ[key] = value


def get_env(key: str, default: Optional[str] = None) -> Optional[str]:
Expand Down
38 changes: 3 additions & 35 deletions tools/base_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,43 +21,11 @@
from pathlib import Path
from typing import Any, Callable, Optional

from lib.env_loader import load_env

def _load_dotenv() -> None:
"""Load .env into os.environ once at import time.

This ensures API keys are available before any tool is instantiated,
even when tools are imported directly without going through the registry.
Only sets variables that are not already in the environment.
"""
env_path = Path(__file__).resolve().parent.parent / ".env"
if not env_path.is_file():
return
import re
with open(env_path, encoding="utf-8", errors="ignore") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
key = key.strip()
value = value.strip()
# Quoted value: take the content inside the quotes verbatim.
if value[:1] in ("'", '"'):
quote = value[0]
end = value.find(quote, 1)
value = value[1:end] if end != -1 else value[1:]
else:
# Strip an inline comment ('#' at line start or after
# whitespace) so "VAR= # note" yields "" not "# note".
match = re.search(r"(^|\s)#", value)
if match:
value = value[: match.start()]
value = value.strip()
if key and key not in os.environ:
os.environ[key] = value


_load_dotenv()
# Load .env file on import - use manual parsing for backward compatibility
load_env()


class ToolTier(str, Enum):
Expand Down
35 changes: 2 additions & 33 deletions tools/tool_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from types import ModuleType
from typing import Any, Optional

from lib.env_loader import load_env
from tools.base_tool import BaseTool, ToolStatus, ToolTier, ToolStability


Expand Down Expand Up @@ -83,41 +84,9 @@ def register_module(self, module: ModuleType) -> list[str]:
registered.append(tool.name)
return registered

@staticmethod
def _load_dotenv() -> None:
"""Load .env file into os.environ if present, so tools can find API keys."""
from pathlib import Path
import os
env_path = Path(__file__).resolve().parent.parent / ".env"
if not env_path.is_file():
return
import re
with open(env_path, encoding="utf-8", errors="ignore") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
key = key.strip()
value = value.strip()
# Quoted value: take the content inside the quotes verbatim.
if value[:1] in ("'", '"'):
quote = value[0]
end = value.find(quote, 1)
value = value[1:end] if end != -1 else value[1:]
else:
# Strip an inline comment ('#' at line start or after
# whitespace) so "KEY= # note" yields "" not "# note".
match = re.search(r"(^|\s)#", value)
if match:
value = value[: match.start()]
value = value.strip()
if key and key not in os.environ:
os.environ[key] = value

def discover(self, package_name: str = "tools") -> list[str]:
"""Import a package tree and register any concrete tools it defines."""
self._load_dotenv()
load_env()
package = importlib.import_module(package_name)
discovered: list[str] = []
package_paths = getattr(package, "__path__", None)
Expand Down