diff --git a/README.md b/README.md index 4c82845..e998e32 100644 --- a/README.md +++ b/README.md @@ -58,27 +58,21 @@ cp -r skills/* .opencode/skills/ ## OpenClaw Installation -For OpenClaw, you need to install both skills AND credentials: +For OpenClaw, just copy the skills folder - it now includes `.env` with all credentials: ```bash # Option 1: Local OpenClaw folder -mkdir -p ~/.openclaw/skills -cp -r skills/* ~/.openclaw/skills/ -cp .env ~/.openclaw/.env +cp -r skills ~/.openclaw/skills # Option 2: Remote server SSH mount (e.g. ~/openclaw-vps/) -mkdir -p ~/openclaw-vps/.openclaw/skills -cp -r skills/* ~/openclaw-vps/.openclaw/skills/ -cp .env ~/openclaw-vps/.openclaw/.env +cp -r skills ~/openclaw-vps/.openclaw/skills # Option 3: rsync for faster sync over SSH rsync -av skills/ user@remote-server:.openclaw/skills/ -rsync -av .env user@remote-server:.openclaw/.env ``` **What to copy:** -- `skills/` - All skill folders -- `.env` - Credentials (API keys, tokens) +- `skills/` - Includes all skills AND `.env` with credentials **Note:** OpenClaw searches for skills in `~/.openclaw/skills` or any `*/.openclaw/skills` folder. diff --git a/scripts/install-skills.sh b/scripts/install-skills.sh index fd2edfe..bb87747 100755 --- a/scripts/install-skills.sh +++ b/scripts/install-skills.sh @@ -151,10 +151,11 @@ setup_unified_env() { fi } -# Copy unified .env to global location +# Copy unified .env to global location and into skills/ copy_unified_env() { local source_env="${REPO_ROOT}/.env" local target_env="${GLOBAL_DIR}/.env" + local skills_env="${SKILLS_DIR}/.env" if [ -f "$source_env" ]; then print_info "Copying unified .env to global location..." @@ -162,6 +163,11 @@ copy_unified_env() { cp "$source_env" "$target_env" chmod 600 "$target_env" print_success "Created: ${target_env}" + + print_info "Copying .env into skills/ folder..." + cp "$source_env" "$skills_env" + chmod 600 "$skills_env" + print_success "Created: ${skills_env}" echo "" fi } @@ -266,26 +272,7 @@ main() { copy_unified_env # Create skill-specific .env files that reference unified .env - print_info "Creating skill configuration files..." - - for skill in $SKILLS; do - dest="${TARGET}/${skill}" - scripts_dir="${dest}/scripts" - - [ -d "$scripts_dir" ] || continue - - # Create .env file that tells script where to find unified .env - cat > "${scripts_dir}/.env" << EOF -# AUTO-GENERATED - DO NOT EDIT -# This skill uses the unified .env file -# Location: ${GLOBAL_DIR}/.env -# -# Edit that file instead to update credentials. -# This file is overwritten on each install. -EOF - - chmod 600 "${scripts_dir}/.env" - done + # (No longer needed - .env is in skills/ folder) print_success "All skills configured" echo "" diff --git a/skills/_env_loader.py b/skills/_env_loader.py new file mode 100644 index 0000000..435e216 --- /dev/null +++ b/skills/_env_loader.py @@ -0,0 +1,14 @@ +import os +from pathlib import Path +from dotenv import load_dotenv + + +def load_unified_env(): + skills_root = Path(__file__).resolve().parent.parent + env_path = skills_root / ".env" + if env_path.exists(): + load_dotenv(env_path) + return + legacy = Path.home() / ".config" / "opencode" / ".env" + if legacy.exists(): + load_dotenv(legacy) diff --git a/skills/alphaear-predictor/scripts/kronos_predictor.py b/skills/alphaear-predictor/scripts/kronos_predictor.py index f713910..b46ee6e 100644 --- a/skills/alphaear-predictor/scripts/kronos_predictor.py +++ b/skills/alphaear-predictor/scripts/kronos_predictor.py @@ -6,17 +6,16 @@ from typing import List, Optional from loguru import logger from pandas.tseries.offsets import BusinessDay import os -from dotenv import load_dotenv - -load_dotenv(os.path.expanduser("~/.config/opencode/.env")) - -# Fix for Kronos internal imports import sys KRONOS_DIR = os.path.join(os.path.dirname(__file__), "predictor") if KRONOS_DIR not in sys.path: sys.path.append(KRONOS_DIR) +from skills._env_loader import load_unified_env + +load_unified_env() + import glob from sentence_transformers import SentenceTransformer diff --git a/skills/alphaear-predictor/scripts/utils/llm/capability.py b/skills/alphaear-predictor/scripts/utils/llm/capability.py index 04c9d5a..d07ca4f 100644 --- a/skills/alphaear-predictor/scripts/utils/llm/capability.py +++ b/skills/alphaear-predictor/scripts/utils/llm/capability.py @@ -72,9 +72,9 @@ class ModelCapabilityRegistry: if __name__ == "__main__": import os - from dotenv import load_dotenv + from skills._env_loader import load_unified_env - load_dotenv(os.path.expanduser("~/.config/opencode/.env")) + load_unified_env() # 测试当前配置的模型 p = os.getenv("LLM_PROVIDER", "minimax") diff --git a/skills/alphaear-predictor/scripts/utils/llm/router.py b/skills/alphaear-predictor/scripts/utils/llm/router.py index 7516120..8c69958 100644 --- a/skills/alphaear-predictor/scripts/utils/llm/router.py +++ b/skills/alphaear-predictor/scripts/utils/llm/router.py @@ -2,12 +2,11 @@ import os from typing import Optional, List, Dict, Any, Union from agno.models.base import Model from loguru import logger -from dotenv import load_dotenv from ..llm.factory import get_model from ..llm.capability import ModelCapabilityRegistry +from skills._env_loader import load_unified_env -# Load environment variables from universal .env -load_dotenv(os.path.expanduser("~/.config/opencode/.env")) +load_unified_env() class ModelRouter: diff --git a/skills/alphaear-predictor/scripts/utils/predictor/training.py b/skills/alphaear-predictor/scripts/utils/predictor/training.py index e3c69f3..3b41724 100644 --- a/skills/alphaear-predictor/scripts/utils/predictor/training.py +++ b/skills/alphaear-predictor/scripts/utils/predictor/training.py @@ -10,9 +10,9 @@ import random from loguru import logger from datetime import datetime, timedelta from sentence_transformers import SentenceTransformer -from dotenv import load_dotenv +from skills._env_loader import load_unified_env -load_dotenv(os.path.expanduser("~/.config/opencode/.env")) +load_unified_env() # Setup paths KRONOS_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/skills/alphaear-reporter/scripts/utils/llm/capability.py b/skills/alphaear-reporter/scripts/utils/llm/capability.py index 04c9d5a..d07ca4f 100644 --- a/skills/alphaear-reporter/scripts/utils/llm/capability.py +++ b/skills/alphaear-reporter/scripts/utils/llm/capability.py @@ -72,9 +72,9 @@ class ModelCapabilityRegistry: if __name__ == "__main__": import os - from dotenv import load_dotenv + from skills._env_loader import load_unified_env - load_dotenv(os.path.expanduser("~/.config/opencode/.env")) + load_unified_env() # 测试当前配置的模型 p = os.getenv("LLM_PROVIDER", "minimax") diff --git a/skills/alphaear-reporter/scripts/utils/llm/router.py b/skills/alphaear-reporter/scripts/utils/llm/router.py index 1492686..8c69958 100644 --- a/skills/alphaear-reporter/scripts/utils/llm/router.py +++ b/skills/alphaear-reporter/scripts/utils/llm/router.py @@ -2,11 +2,11 @@ import os from typing import Optional, List, Dict, Any, Union from agno.models.base import Model from loguru import logger -from dotenv import load_dotenv from ..llm.factory import get_model from ..llm.capability import ModelCapabilityRegistry +from skills._env_loader import load_unified_env -load_dotenv(os.path.expanduser("~/.config/opencode/.env")) +load_unified_env() class ModelRouter: diff --git a/skills/alphaear-reporter/scripts/utils/predictor/training.py b/skills/alphaear-reporter/scripts/utils/predictor/training.py index e3c69f3..3b41724 100644 --- a/skills/alphaear-reporter/scripts/utils/predictor/training.py +++ b/skills/alphaear-reporter/scripts/utils/predictor/training.py @@ -10,9 +10,9 @@ import random from loguru import logger from datetime import datetime, timedelta from sentence_transformers import SentenceTransformer -from dotenv import load_dotenv +from skills._env_loader import load_unified_env -load_dotenv(os.path.expanduser("~/.config/opencode/.env")) +load_unified_env() # Setup paths KRONOS_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/skills/alphaear-search/scripts/llm/capability.py b/skills/alphaear-search/scripts/llm/capability.py index 6d896be..d3fb2d7 100644 --- a/skills/alphaear-search/scripts/llm/capability.py +++ b/skills/alphaear-search/scripts/llm/capability.py @@ -72,9 +72,9 @@ class ModelCapabilityRegistry: if __name__ == "__main__": import os - from dotenv import load_dotenv + from skills._env_loader import load_unified_env - load_dotenv(os.path.expanduser("~/.config/opencode/.env")) + load_unified_env() # 测试当前配置的模型 p = os.getenv("LLM_PROVIDER", "minimax") diff --git a/skills/alphaear-search/scripts/llm/router.py b/skills/alphaear-search/scripts/llm/router.py index 46f7aa9..20e7d83 100644 --- a/skills/alphaear-search/scripts/llm/router.py +++ b/skills/alphaear-search/scripts/llm/router.py @@ -2,11 +2,11 @@ import os from typing import Optional, List, Dict, Any, Union from agno.models.base import Model from loguru import logger -from dotenv import load_dotenv from .factory import get_model from .capability import ModelCapabilityRegistry +from skills._env_loader import load_unified_env -load_dotenv(os.path.expanduser("~/.config/opencode/.env")) +load_unified_env() class ModelRouter: diff --git a/skills/alphaear-sentiment/scripts/llm/capability.py b/skills/alphaear-sentiment/scripts/llm/capability.py index 6f65027..de9de32 100644 --- a/skills/alphaear-sentiment/scripts/llm/capability.py +++ b/skills/alphaear-sentiment/scripts/llm/capability.py @@ -72,9 +72,9 @@ class ModelCapabilityRegistry: if __name__ == "__main__": import os - from dotenv import load_dotenv + from skills._env_loader import load_unified_env - load_dotenv(os.path.expanduser("~/.config/opencode/.env")) + load_unified_env() # 测试当前配置的模型 p = os.getenv("LLM_PROVIDER", "minimax") diff --git a/skills/alphaear-sentiment/scripts/llm/router.py b/skills/alphaear-sentiment/scripts/llm/router.py index 8eb33ec..3a3cede 100644 --- a/skills/alphaear-sentiment/scripts/llm/router.py +++ b/skills/alphaear-sentiment/scripts/llm/router.py @@ -2,11 +2,11 @@ import os from typing import Optional, List, Dict, Any, Union from agno.models.base import Model from loguru import logger -from dotenv import load_dotenv from .llm.factory import get_model from utils.llm.capability import ModelCapabilityRegistry +from skills._env_loader import load_unified_env -load_dotenv(os.path.expanduser("~/.config/opencode/.env")) +load_unified_env() class ModelRouter: diff --git a/skills/alphaear-signal-tracker/scripts/utils/llm/capability.py b/skills/alphaear-signal-tracker/scripts/utils/llm/capability.py index 04c9d5a..d07ca4f 100644 --- a/skills/alphaear-signal-tracker/scripts/utils/llm/capability.py +++ b/skills/alphaear-signal-tracker/scripts/utils/llm/capability.py @@ -72,9 +72,9 @@ class ModelCapabilityRegistry: if __name__ == "__main__": import os - from dotenv import load_dotenv + from skills._env_loader import load_unified_env - load_dotenv(os.path.expanduser("~/.config/opencode/.env")) + load_unified_env() # 测试当前配置的模型 p = os.getenv("LLM_PROVIDER", "minimax") diff --git a/skills/alphaear-signal-tracker/scripts/utils/llm/router.py b/skills/alphaear-signal-tracker/scripts/utils/llm/router.py index 1492686..8c69958 100644 --- a/skills/alphaear-signal-tracker/scripts/utils/llm/router.py +++ b/skills/alphaear-signal-tracker/scripts/utils/llm/router.py @@ -2,11 +2,11 @@ import os from typing import Optional, List, Dict, Any, Union from agno.models.base import Model from loguru import logger -from dotenv import load_dotenv from ..llm.factory import get_model from ..llm.capability import ModelCapabilityRegistry +from skills._env_loader import load_unified_env -load_dotenv(os.path.expanduser("~/.config/opencode/.env")) +load_unified_env() class ModelRouter: diff --git a/skills/alphaear-signal-tracker/scripts/utils/predictor/training.py b/skills/alphaear-signal-tracker/scripts/utils/predictor/training.py index e3c69f3..3b41724 100644 --- a/skills/alphaear-signal-tracker/scripts/utils/predictor/training.py +++ b/skills/alphaear-signal-tracker/scripts/utils/predictor/training.py @@ -10,9 +10,9 @@ import random from loguru import logger from datetime import datetime, timedelta from sentence_transformers import SentenceTransformer -from dotenv import load_dotenv +from skills._env_loader import load_unified_env -load_dotenv(os.path.expanduser("~/.config/opencode/.env")) +load_unified_env() # Setup paths KRONOS_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/skills/seo-multi-channel/scripts/generate_content.py b/skills/seo-multi-channel/scripts/generate_content.py index e3d5c55..1ad5902 100644 --- a/skills/seo-multi-channel/scripts/generate_content.py +++ b/skills/seo-multi-channel/scripts/generate_content.py @@ -18,9 +18,9 @@ from typing import Dict, List, Optional, Any import yaml # Load environment variables -from dotenv import load_dotenv +from skills._env_loader import load_unified_env -load_dotenv() +load_unified_env() # Thai language processing try: diff --git a/skills/thai-frontend-dev/scripts/create_astro_website.py b/skills/thai-frontend-dev/scripts/create_astro_website.py index 3524fe8..0d54eab 100644 --- a/skills/thai-frontend-dev/scripts/create_astro_website.py +++ b/skills/thai-frontend-dev/scripts/create_astro_website.py @@ -115,9 +115,9 @@ def ask_analytics_setup(): print("\n 📈 Umami Analytics Setup") # Check if Umami credentials are configured - from dotenv import load_dotenv + from skills._env_loader import load_unified_env - load_dotenv(os.path.join(os.path.dirname(__file__), "../../../.env")) + load_unified_env() umami_url = os.getenv("UMAMI_URL", "") umami_username = os.getenv("UMAMI_USERNAME", "") @@ -148,9 +148,9 @@ def ask_analytics_setup(): print("\n Please provide your existing GA4 details:") # Check unified .env for GA4 credentials - from dotenv import load_dotenv + from skills._env_loader import load_unified_env - load_dotenv(os.path.join(os.path.dirname(__file__), "../../../.env")) + load_unified_env() ga4_property_id = os.getenv("GA4_PROPERTY_ID", "") ga4_credentials_path = os.getenv("GA4_CREDENTIALS_PATH", "") diff --git a/skills/umami/scripts/umami_client.py b/skills/umami/scripts/umami_client.py index 5a936cd..090a681 100644 --- a/skills/umami/scripts/umami_client.py +++ b/skills/umami/scripts/umami_client.py @@ -17,20 +17,24 @@ import argparse from datetime import datetime, timedelta from typing import Dict, Optional, List from pathlib import Path -from dotenv import load_dotenv +from skills._env_loader import load_unified_env -# Load credentials from unified .env -load_dotenv(os.path.expanduser('~/.config/opencode/.env')) -load_dotenv() # Also load local .env for development +load_unified_env() class UmamiClient: """Umami Analytics API client with username/password auth""" - - def __init__(self, umami_url: str = None, username: str = None, password: str = None, token: str = None): + + def __init__( + self, + umami_url: str = None, + username: str = None, + password: str = None, + token: str = None, + ): """ Initialize Umami client - + Args: umami_url: Umami instance URL (loaded from .env if not provided) username: Umami username/email (loaded from .env if not provided) @@ -38,94 +42,88 @@ class UmamiClient: token: Bearer token (optional, if already have) """ # Load from environment if not provided - self.umami_url = umami_url or os.getenv('UMAMI_URL', '') - self.username = username or os.getenv('UMAMI_USERNAME', '') - self.password = password or os.getenv('UMAMI_PASSWORD', '') + self.umami_url = umami_url or os.getenv("UMAMI_URL", "") + self.username = username or os.getenv("UMAMI_USERNAME", "") + self.password = password or os.getenv("UMAMI_PASSWORD", "") self.token = token self.user_id = None - + # Auto-login if credentials provided if self.username and self.password and not token: self.login() - + def login(self) -> Dict: """Login to Umami and get bearer token""" try: url = f"{self.umami_url}/api/auth/login" - data = { - 'username': self.username, - 'password': self.password - } - + data = {"username": self.username, "password": self.password} + response = requests.post(url, json=data) response.raise_for_status() result = response.json() - - if 'token' in result: - self.token = result['token'] - self.user_id = result.get('user', {}).get('id') - + + if "token" in result: + self.token = result["token"] + self.user_id = result.get("user", {}).get("id") + return { - 'success': True, - 'token': self.token, - 'user_id': self.user_id, - 'username': result.get('user', {}).get('username') + "success": True, + "token": self.token, + "user_id": self.user_id, + "username": result.get("user", {}).get("username"), } else: - return {'success': False, 'error': 'No token in response'} - + return {"success": False, "error": "No token in response"} + except Exception as e: - return {'success': False, 'error': str(e)} - + return {"success": False, "error": str(e)} + def _get_headers(self) -> Dict: """Get request headers with auth""" if not self.token: if self.username and self.password: self.login() - + return { - 'Authorization': f'Bearer {self.token}', - 'Content-Type': 'application/json', - 'Accept': 'application/json' + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/json", + "Accept": "application/json", } - + def create_website(self, name: str, domain: str) -> Dict: """Create new Umami website""" try: url = f"{self.umami_url}/api/websites" - data = { - 'name': name, - 'domain': domain - } - + data = {"name": name, "domain": domain} + response = requests.post(url, json=data, headers=self._get_headers()) response.raise_for_status() result = response.json() - + return { - 'success': True, - 'website_id': result.get('id'), - 'name': result.get('name'), - 'domain': result.get('domain'), - 'created_at': result.get('createdAt'), - 'tracking_url': f"{self.umami_url}/script.js", - 'tracking_script': self._get_tracking_script(result.get('id')) + "success": True, + "website_id": result.get("id"), + "name": result.get("name"), + "domain": result.get("domain"), + "created_at": result.get("createdAt"), + "tracking_url": f"{self.umami_url}/script.js", + "tracking_script": self._get_tracking_script(result.get("id")), } - + except Exception as e: - return {'success': False, 'error': str(e)} - + return {"success": False, "error": str(e)} + def get_website_by_domain(self, domain: str) -> Optional[Dict]: """Find website by domain""" try: websites = self.list_websites() for site in websites: - if domain in site.get('domain', ''): + if domain in site.get("domain", ""): return site return None except: return None - + def list_websites(self) -> List[Dict]: """Get all websites""" try: @@ -133,141 +131,157 @@ class UmamiClient: response = requests.get(url, headers=self._get_headers()) response.raise_for_status() result = response.json() - + # Handle both array and paginated response if isinstance(result, list): return result - elif 'data' in result: - return result['data'] + elif "data" in result: + return result["data"] else: return [] - + except Exception as e: print(f"Error listing websites: {e}") return [] - + def get_stats(self, website_id: str, days: int = 30) -> Dict: """Get website statistics""" try: end_date = datetime.now() start_date = end_date - timedelta(days=days) - + url = f"{self.umami_url}/api/websites/{website_id}/stats" params = { - 'startAt': int(start_date.timestamp() * 1000), - 'endAt': int(end_date.timestamp() * 1000) + "startAt": int(start_date.timestamp() * 1000), + "endAt": int(end_date.timestamp() * 1000), } - + response = requests.get(url, headers=self._get_headers(), params=params) response.raise_for_status() stats = response.json() - + return { - 'success': True, - 'website_id': website_id, - 'period': f'last_{days}_days', - 'pageviews': stats.get('pageviews', 0), - 'uniques': stats.get('uniques', 0), - 'bounces': stats.get('bounces', 0), - 'totaltime': stats.get('totaltime', 0), - 'avg_session_duration': stats.get('totaltime', 0) / max(stats.get('visits', 1), 1), - 'bounce_rate': stats.get('bounces', 0) / max(stats.get('visits', 1), 1) * 100 + "success": True, + "website_id": website_id, + "period": f"last_{days}_days", + "pageviews": stats.get("pageviews", 0), + "uniques": stats.get("uniques", 0), + "bounces": stats.get("bounces", 0), + "totaltime": stats.get("totaltime", 0), + "avg_session_duration": stats.get("totaltime", 0) + / max(stats.get("visits", 1), 1), + "bounce_rate": stats.get("bounces", 0) + / max(stats.get("visits", 1), 1) + * 100, } - + except Exception as e: - return {'success': False, 'error': str(e)} - + return {"success": False, "error": str(e)} + def _get_tracking_script(self, website_id: str) -> str: """Generate tracking script HTML""" return f'' - + def add_tracking_to_astro(self, website_repo: str, website_id: str) -> Dict: """Add Umami tracking to Astro website""" try: tracking_script = self._get_tracking_script(website_id) - + # Find Astro layout file layout_paths = [ - os.path.join(website_repo, 'src/layouts/Layout.astro'), - os.path.join(website_repo, 'src/layouts/BaseHead.astro'), - os.path.join(website_repo, 'src/pages/_document.tsx'), - os.path.join(website_repo, 'src/app.html') + os.path.join(website_repo, "src/layouts/Layout.astro"), + os.path.join(website_repo, "src/layouts/BaseHead.astro"), + os.path.join(website_repo, "src/pages/_document.tsx"), + os.path.join(website_repo, "src/app.html"), ] - + layout_file = None for path in layout_paths: if os.path.exists(path): layout_file = path break - + if not layout_file: - layouts_dir = os.path.join(website_repo, 'src/layouts') + layouts_dir = os.path.join(website_repo, "src/layouts") if os.path.exists(layouts_dir): for f in os.listdir(layouts_dir): - if f.endswith('.astro'): + if f.endswith(".astro"): layout_file = os.path.join(layouts_dir, f) break - + if not layout_file: - return {'success': False, 'error': 'No Astro layout file found'} - - with open(layout_file, 'r', encoding='utf-8') as f: + return {"success": False, "error": "No Astro layout file found"} + + with open(layout_file, "r", encoding="utf-8") as f: content = f.read() - - if '' in content: - content = content.replace('', f' {tracking_script}\n ') + + if "" in content: + content = content.replace( + "", f" {tracking_script}\n " + ) else: - content += f'\n{tracking_script}\n' - - with open(layout_file, 'w', encoding='utf-8') as f: + content += f"\n{tracking_script}\n" + + with open(layout_file, "w", encoding="utf-8") as f: f.write(content) - - return { - 'success': True, - 'layout_file': layout_file, - 'tracking_added': True - } - + + return {"success": True, "layout_file": layout_file, "tracking_added": True} + except Exception as e: - return {'success': False, 'error': str(e)} + return {"success": False, "error": str(e)} def main(): """Main CLI entry point""" - parser = argparse.ArgumentParser(description='Umami Analytics Client') - - parser.add_argument('--action', required=True, - choices=['create-website', 'get-tracking', 'add-tracking', 'get-stats', 'list-websites']) - parser.add_argument('--umami-url', help='Umami instance URL (or set UMAMI_URL in .env)') - parser.add_argument('--username', help='Umami username (or set UMAMI_USERNAME in .env)') - parser.add_argument('--password', help='Umami password (or set UMAMI_PASSWORD in .env)') - parser.add_argument('--website-name', help='Website name (for create)') - parser.add_argument('--website-domain', help='Website domain (for create/find)') - parser.add_argument('--website-id', help='Website ID (for stats)') - parser.add_argument('--website-repo', help='Path to website repo (for add-tracking)') - parser.add_argument('--days', type=int, default=30, help='Days for stats') - + parser = argparse.ArgumentParser(description="Umami Analytics Client") + + parser.add_argument( + "--action", + required=True, + choices=[ + "create-website", + "get-tracking", + "add-tracking", + "get-stats", + "list-websites", + ], + ) + parser.add_argument( + "--umami-url", help="Umami instance URL (or set UMAMI_URL in .env)" + ) + parser.add_argument( + "--username", help="Umami username (or set UMAMI_USERNAME in .env)" + ) + parser.add_argument( + "--password", help="Umami password (or set UMAMI_PASSWORD in .env)" + ) + parser.add_argument("--website-name", help="Website name (for create)") + parser.add_argument("--website-domain", help="Website domain (for create/find)") + parser.add_argument("--website-id", help="Website ID (for stats)") + parser.add_argument( + "--website-repo", help="Path to website repo (for add-tracking)" + ) + parser.add_argument("--days", type=int, default=30, help="Days for stats") + args = parser.parse_args() - + print(f"\n📊 Umami Analytics Client") print(f"URL: {args.umami_url or os.getenv('UMAMI_URL', 'Not set')}\n") - + # Initialize client (loads credentials from .env automatically) client = UmamiClient( - umami_url=args.umami_url, - username=args.username, - password=args.password + umami_url=args.umami_url, username=args.username, password=args.password ) - - if args.action == 'create-website': + + if args.action == "create-website": if not args.website_name or not args.website_domain: print("Error: --website-name and --website-domain required") return - + print(f"Creating website: {args.website_name} ({args.website_domain})") result = client.create_website(args.website_name, args.website_domain) - - if result['success']: + + if result["success"]: print(f"\n✅ Website created!") print(f" ID: {result['website_id']}") print(f" Name: {result['name']}") @@ -276,39 +290,39 @@ def main(): print(f"\nScript:\n{result['tracking_script']}") else: print(f"\n❌ Failed: {result['error']}") - - elif args.action == 'get-tracking': + + elif args.action == "get-tracking": if not args.website_id: print("Error: --website-id required") return - + script = client._get_tracking_script(args.website_id) print(f"\nTracking script for {args.website_id}:") print(script) - - elif args.action == 'add-tracking': + + elif args.action == "add-tracking": if not args.website_id or not args.website_repo: print("Error: --website-id and --website-repo required") return - + print(f"Adding tracking to: {args.website_repo}") result = client.add_tracking_to_astro(args.website_repo, args.website_id) - - if result['success']: + + if result["success"]: print(f"\n✅ Tracking added!") print(f" Layout: {result['layout_file']}") else: print(f"\n❌ Failed: {result['error']}") - - elif args.action == 'get-stats': + + elif args.action == "get-stats": if not args.website_id: print("Error: --website-id required") return - + print(f"Getting stats for last {args.days} days...") stats = client.get_stats(args.website_id, args.days) - - if stats['success']: + + if stats["success"]: print(f"\n📊 Analytics ({stats['period']}):") print(f" Pageviews: {stats['pageviews']:,}") print(f" Unique visitors: {stats['uniques']:,}") @@ -317,15 +331,15 @@ def main(): print(f" Avg session: {stats['avg_session_duration']:.1f}s") else: print(f"\n❌ Failed: {stats['error']}") - - elif args.action == 'list-websites': + + elif args.action == "list-websites": print("Listing websites...") websites = client.list_websites() - + print(f"\nFound {len(websites)} websites:") for site in websites: print(f" • {site.get('name')} - {site.get('domain')}") -if __name__ == '__main__': +if __name__ == "__main__": main()