Skip to content
Merged
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
6 changes: 5 additions & 1 deletion src/yieldagent/agents/campaign_setup/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

from langgraph.types import Command

from yieldagent.env import load_dotenv

from .graph import build_graph
from .nodes import DEFAULT_MODEL

Expand Down Expand Up @@ -100,6 +102,7 @@ async def _run(brief_path: Path, *, auto_approve: bool, dry_run: bool, model_nam


def main() -> int:
load_dotenv()
parser = argparse.ArgumentParser(prog="yieldagent-campaign-setup")
parser.add_argument("brief", type=Path, help="Path to a markdown campaign brief")
parser.add_argument(
Expand All @@ -111,7 +114,8 @@ def main() -> int:
"--dry-run",
action="store_true",
help="Replace the Meta MCP server with a stub. No Meta credentials needed; "
"nothing is sent to Meta. Use this to try the agent end-to-end before wiring up real ad accounts.",
"nothing is sent to Meta. Use this to try the agent end-to-end before "
"wiring up real ad accounts.",
)
parser.add_argument("--model", default=DEFAULT_MODEL, help="Claude model name")
args = parser.parse_args()
Expand Down
57 changes: 57 additions & 0 deletions src/yieldagent/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Small `.env` loader for local CLI and MCP server entry points."""

from __future__ import annotations

import os
from pathlib import Path


def _find_dotenv(start: Path | None = None) -> Path | None:
current = (start or Path.cwd()).resolve()
if current.is_file():
current = current.parent
for directory in (current, *current.parents):
candidate = directory / ".env"
if candidate.is_file():
return candidate
return None


def _parse_value(raw: str) -> str:
value = raw.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
return value[1:-1]

for index, char in enumerate(value):
if char == "#" and (index == 0 or value[index - 1].isspace()):
return value[:index].strip()
return value


def load_dotenv(path: Path | None = None, *, override: bool = False) -> Path | None:
"""Load KEY=VALUE pairs from `.env` into `os.environ`.

This intentionally handles the simple format used by this repo without
adding a runtime dependency. Existing shell variables win unless
Comment on lines +31 to +35
``override=True`` is passed.
"""
dotenv_path = path or _find_dotenv()
if dotenv_path is None:
return None

for line in dotenv_path.read_text().splitlines():
Comment on lines +38 to +42
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
if stripped.startswith("export "):
stripped = stripped.removeprefix("export ").lstrip()
if "=" not in stripped:
continue
key, raw_value = stripped.split("=", 1)
key = key.strip()
if not key or not key.replace("_", "").isalnum() or key[0].isdigit():
continue
if override or key not in os.environ:
os.environ[key] = _parse_value(raw_value)

return dotenv_path
4 changes: 3 additions & 1 deletion src/yieldagent/integrations/meta/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
from mcp.server.fastmcp import FastMCP

from yieldagent.domain import Campaign
from yieldagent.env import load_dotenv

from .client import MetaClient
from .config import MetaConfig
from .mapping import (
audience_to_targeting,
campaign_objective,
creative_payload,
flight_to_meta_times,
audience_to_targeting,
to_minor_units,
)

Expand Down Expand Up @@ -138,6 +139,7 @@ async def publish_draft_campaign(campaign: dict[str, Any]) -> dict[str, Any]:


def main() -> None:
load_dotenv()
asyncio.run(mcp.run_stdio_async())


Expand Down