Move .env into skills/ for easy install

- Added skills/_env_loader.py - shared env loader for all scripts
- Updated 17 scripts to use load_unified_env()
- Updated install-skills.sh to copy .env into skills/
- Updated README with simpler OpenClaw install instructions
- .env in skills/ is gitignored (credentials stay private)
This commit is contained in:
Kunthawat Greethong
2026-03-27 17:49:20 +07:00
parent 4e92ef953b
commit e4d41e3ae5
20 changed files with 219 additions and 212 deletions

View File

@@ -58,27 +58,21 @@ cp -r skills/* .opencode/skills/
## OpenClaw Installation ## 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 ```bash
# Option 1: Local OpenClaw folder # Option 1: Local OpenClaw folder
mkdir -p ~/.openclaw/skills cp -r skills ~/.openclaw/skills
cp -r skills/* ~/.openclaw/skills/
cp .env ~/.openclaw/.env
# Option 2: Remote server SSH mount (e.g. ~/openclaw-vps/) # Option 2: Remote server SSH mount (e.g. ~/openclaw-vps/)
mkdir -p ~/openclaw-vps/.openclaw/skills cp -r skills ~/openclaw-vps/.openclaw/skills
cp -r skills/* ~/openclaw-vps/.openclaw/skills/
cp .env ~/openclaw-vps/.openclaw/.env
# Option 3: rsync for faster sync over SSH # Option 3: rsync for faster sync over SSH
rsync -av skills/ user@remote-server:.openclaw/skills/ rsync -av skills/ user@remote-server:.openclaw/skills/
rsync -av .env user@remote-server:.openclaw/.env
``` ```
**What to copy:** **What to copy:**
- `skills/` - All skill folders - `skills/` - Includes all skills AND `.env` with credentials
- `.env` - Credentials (API keys, tokens)
**Note:** OpenClaw searches for skills in `~/.openclaw/skills` or any `*/.openclaw/skills` folder. **Note:** OpenClaw searches for skills in `~/.openclaw/skills` or any `*/.openclaw/skills` folder.

View File

@@ -151,10 +151,11 @@ setup_unified_env() {
fi fi
} }
# Copy unified .env to global location # Copy unified .env to global location and into skills/
copy_unified_env() { copy_unified_env() {
local source_env="${REPO_ROOT}/.env" local source_env="${REPO_ROOT}/.env"
local target_env="${GLOBAL_DIR}/.env" local target_env="${GLOBAL_DIR}/.env"
local skills_env="${SKILLS_DIR}/.env"
if [ -f "$source_env" ]; then if [ -f "$source_env" ]; then
print_info "Copying unified .env to global location..." print_info "Copying unified .env to global location..."
@@ -162,6 +163,11 @@ copy_unified_env() {
cp "$source_env" "$target_env" cp "$source_env" "$target_env"
chmod 600 "$target_env" chmod 600 "$target_env"
print_success "Created: ${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 "" echo ""
fi fi
} }
@@ -266,26 +272,7 @@ main() {
copy_unified_env copy_unified_env
# Create skill-specific .env files that reference unified .env # Create skill-specific .env files that reference unified .env
print_info "Creating skill configuration files..." # (No longer needed - .env is in skills/ folder)
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
print_success "All skills configured" print_success "All skills configured"
echo "" echo ""

14
skills/_env_loader.py Normal file
View File

@@ -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)

View File

@@ -6,17 +6,16 @@ from typing import List, Optional
from loguru import logger from loguru import logger
from pandas.tseries.offsets import BusinessDay from pandas.tseries.offsets import BusinessDay
import os import os
from dotenv import load_dotenv
load_dotenv(os.path.expanduser("~/.config/opencode/.env"))
# Fix for Kronos internal imports
import sys import sys
KRONOS_DIR = os.path.join(os.path.dirname(__file__), "predictor") KRONOS_DIR = os.path.join(os.path.dirname(__file__), "predictor")
if KRONOS_DIR not in sys.path: if KRONOS_DIR not in sys.path:
sys.path.append(KRONOS_DIR) sys.path.append(KRONOS_DIR)
from skills._env_loader import load_unified_env
load_unified_env()
import glob import glob
from sentence_transformers import SentenceTransformer from sentence_transformers import SentenceTransformer

View File

@@ -72,9 +72,9 @@ class ModelCapabilityRegistry:
if __name__ == "__main__": if __name__ == "__main__":
import os 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") p = os.getenv("LLM_PROVIDER", "minimax")

View File

@@ -2,12 +2,11 @@ import os
from typing import Optional, List, Dict, Any, Union from typing import Optional, List, Dict, Any, Union
from agno.models.base import Model from agno.models.base import Model
from loguru import logger from loguru import logger
from dotenv import load_dotenv
from ..llm.factory import get_model from ..llm.factory import get_model
from ..llm.capability import ModelCapabilityRegistry from ..llm.capability import ModelCapabilityRegistry
from skills._env_loader import load_unified_env
# Load environment variables from universal .env load_unified_env()
load_dotenv(os.path.expanduser("~/.config/opencode/.env"))
class ModelRouter: class ModelRouter:

View File

@@ -10,9 +10,9 @@ import random
from loguru import logger from loguru import logger
from datetime import datetime, timedelta from datetime import datetime, timedelta
from sentence_transformers import SentenceTransformer 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 # Setup paths
KRONOS_DIR = os.path.dirname(os.path.abspath(__file__)) KRONOS_DIR = os.path.dirname(os.path.abspath(__file__))

View File

@@ -72,9 +72,9 @@ class ModelCapabilityRegistry:
if __name__ == "__main__": if __name__ == "__main__":
import os 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") p = os.getenv("LLM_PROVIDER", "minimax")

View File

@@ -2,11 +2,11 @@ import os
from typing import Optional, List, Dict, Any, Union from typing import Optional, List, Dict, Any, Union
from agno.models.base import Model from agno.models.base import Model
from loguru import logger from loguru import logger
from dotenv import load_dotenv
from ..llm.factory import get_model from ..llm.factory import get_model
from ..llm.capability import ModelCapabilityRegistry 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: class ModelRouter:

View File

@@ -10,9 +10,9 @@ import random
from loguru import logger from loguru import logger
from datetime import datetime, timedelta from datetime import datetime, timedelta
from sentence_transformers import SentenceTransformer 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 # Setup paths
KRONOS_DIR = os.path.dirname(os.path.abspath(__file__)) KRONOS_DIR = os.path.dirname(os.path.abspath(__file__))

View File

@@ -72,9 +72,9 @@ class ModelCapabilityRegistry:
if __name__ == "__main__": if __name__ == "__main__":
import os 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") p = os.getenv("LLM_PROVIDER", "minimax")

View File

@@ -2,11 +2,11 @@ import os
from typing import Optional, List, Dict, Any, Union from typing import Optional, List, Dict, Any, Union
from agno.models.base import Model from agno.models.base import Model
from loguru import logger from loguru import logger
from dotenv import load_dotenv
from .factory import get_model from .factory import get_model
from .capability import ModelCapabilityRegistry 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: class ModelRouter:

View File

@@ -72,9 +72,9 @@ class ModelCapabilityRegistry:
if __name__ == "__main__": if __name__ == "__main__":
import os 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") p = os.getenv("LLM_PROVIDER", "minimax")

View File

@@ -2,11 +2,11 @@ import os
from typing import Optional, List, Dict, Any, Union from typing import Optional, List, Dict, Any, Union
from agno.models.base import Model from agno.models.base import Model
from loguru import logger from loguru import logger
from dotenv import load_dotenv
from .llm.factory import get_model from .llm.factory import get_model
from utils.llm.capability import ModelCapabilityRegistry 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: class ModelRouter:

View File

@@ -72,9 +72,9 @@ class ModelCapabilityRegistry:
if __name__ == "__main__": if __name__ == "__main__":
import os 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") p = os.getenv("LLM_PROVIDER", "minimax")

View File

@@ -2,11 +2,11 @@ import os
from typing import Optional, List, Dict, Any, Union from typing import Optional, List, Dict, Any, Union
from agno.models.base import Model from agno.models.base import Model
from loguru import logger from loguru import logger
from dotenv import load_dotenv
from ..llm.factory import get_model from ..llm.factory import get_model
from ..llm.capability import ModelCapabilityRegistry 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: class ModelRouter:

View File

@@ -10,9 +10,9 @@ import random
from loguru import logger from loguru import logger
from datetime import datetime, timedelta from datetime import datetime, timedelta
from sentence_transformers import SentenceTransformer 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 # Setup paths
KRONOS_DIR = os.path.dirname(os.path.abspath(__file__)) KRONOS_DIR = os.path.dirname(os.path.abspath(__file__))

View File

@@ -18,9 +18,9 @@ from typing import Dict, List, Optional, Any
import yaml import yaml
# Load environment variables # Load environment variables
from dotenv import load_dotenv from skills._env_loader import load_unified_env
load_dotenv() load_unified_env()
# Thai language processing # Thai language processing
try: try:

View File

@@ -115,9 +115,9 @@ def ask_analytics_setup():
print("\n 📈 Umami Analytics Setup") print("\n 📈 Umami Analytics Setup")
# Check if Umami credentials are configured # 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_url = os.getenv("UMAMI_URL", "")
umami_username = os.getenv("UMAMI_USERNAME", "") umami_username = os.getenv("UMAMI_USERNAME", "")
@@ -148,9 +148,9 @@ def ask_analytics_setup():
print("\n Please provide your existing GA4 details:") print("\n Please provide your existing GA4 details:")
# Check unified .env for GA4 credentials # 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_property_id = os.getenv("GA4_PROPERTY_ID", "")
ga4_credentials_path = os.getenv("GA4_CREDENTIALS_PATH", "") ga4_credentials_path = os.getenv("GA4_CREDENTIALS_PATH", "")

View File

@@ -17,17 +17,21 @@ import argparse
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Dict, Optional, List from typing import Dict, Optional, List
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv from skills._env_loader import load_unified_env
# Load credentials from unified .env load_unified_env()
load_dotenv(os.path.expanduser('~/.config/opencode/.env'))
load_dotenv() # Also load local .env for development
class UmamiClient: class UmamiClient:
"""Umami Analytics API client with username/password auth""" """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 Initialize Umami client
@@ -38,9 +42,9 @@ class UmamiClient:
token: Bearer token (optional, if already have) token: Bearer token (optional, if already have)
""" """
# Load from environment if not provided # Load from environment if not provided
self.umami_url = umami_url or os.getenv('UMAMI_URL', '') self.umami_url = umami_url or os.getenv("UMAMI_URL", "")
self.username = username or os.getenv('UMAMI_USERNAME', '') self.username = username or os.getenv("UMAMI_USERNAME", "")
self.password = password or os.getenv('UMAMI_PASSWORD', '') self.password = password or os.getenv("UMAMI_PASSWORD", "")
self.token = token self.token = token
self.user_id = None self.user_id = None
@@ -52,30 +56,27 @@ class UmamiClient:
"""Login to Umami and get bearer token""" """Login to Umami and get bearer token"""
try: try:
url = f"{self.umami_url}/api/auth/login" url = f"{self.umami_url}/api/auth/login"
data = { data = {"username": self.username, "password": self.password}
'username': self.username,
'password': self.password
}
response = requests.post(url, json=data) response = requests.post(url, json=data)
response.raise_for_status() response.raise_for_status()
result = response.json() result = response.json()
if 'token' in result: if "token" in result:
self.token = result['token'] self.token = result["token"]
self.user_id = result.get('user', {}).get('id') self.user_id = result.get("user", {}).get("id")
return { return {
'success': True, "success": True,
'token': self.token, "token": self.token,
'user_id': self.user_id, "user_id": self.user_id,
'username': result.get('user', {}).get('username') "username": result.get("user", {}).get("username"),
} }
else: else:
return {'success': False, 'error': 'No token in response'} return {"success": False, "error": "No token in response"}
except Exception as e: except Exception as e:
return {'success': False, 'error': str(e)} return {"success": False, "error": str(e)}
def _get_headers(self) -> Dict: def _get_headers(self) -> Dict:
"""Get request headers with auth""" """Get request headers with auth"""
@@ -84,43 +85,40 @@ class UmamiClient:
self.login() self.login()
return { return {
'Authorization': f'Bearer {self.token}', "Authorization": f"Bearer {self.token}",
'Content-Type': 'application/json', "Content-Type": "application/json",
'Accept': 'application/json' "Accept": "application/json",
} }
def create_website(self, name: str, domain: str) -> Dict: def create_website(self, name: str, domain: str) -> Dict:
"""Create new Umami website""" """Create new Umami website"""
try: try:
url = f"{self.umami_url}/api/websites" url = f"{self.umami_url}/api/websites"
data = { data = {"name": name, "domain": domain}
'name': name,
'domain': domain
}
response = requests.post(url, json=data, headers=self._get_headers()) response = requests.post(url, json=data, headers=self._get_headers())
response.raise_for_status() response.raise_for_status()
result = response.json() result = response.json()
return { return {
'success': True, "success": True,
'website_id': result.get('id'), "website_id": result.get("id"),
'name': result.get('name'), "name": result.get("name"),
'domain': result.get('domain'), "domain": result.get("domain"),
'created_at': result.get('createdAt'), "created_at": result.get("createdAt"),
'tracking_url': f"{self.umami_url}/script.js", "tracking_url": f"{self.umami_url}/script.js",
'tracking_script': self._get_tracking_script(result.get('id')) "tracking_script": self._get_tracking_script(result.get("id")),
} }
except Exception as e: 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]: def get_website_by_domain(self, domain: str) -> Optional[Dict]:
"""Find website by domain""" """Find website by domain"""
try: try:
websites = self.list_websites() websites = self.list_websites()
for site in websites: for site in websites:
if domain in site.get('domain', ''): if domain in site.get("domain", ""):
return site return site
return None return None
except: except:
@@ -137,8 +135,8 @@ class UmamiClient:
# Handle both array and paginated response # Handle both array and paginated response
if isinstance(result, list): if isinstance(result, list):
return result return result
elif 'data' in result: elif "data" in result:
return result['data'] return result["data"]
else: else:
return [] return []
@@ -154,8 +152,8 @@ class UmamiClient:
url = f"{self.umami_url}/api/websites/{website_id}/stats" url = f"{self.umami_url}/api/websites/{website_id}/stats"
params = { params = {
'startAt': int(start_date.timestamp() * 1000), "startAt": int(start_date.timestamp() * 1000),
'endAt': int(end_date.timestamp() * 1000) "endAt": int(end_date.timestamp() * 1000),
} }
response = requests.get(url, headers=self._get_headers(), params=params) response = requests.get(url, headers=self._get_headers(), params=params)
@@ -163,19 +161,22 @@ class UmamiClient:
stats = response.json() stats = response.json()
return { return {
'success': True, "success": True,
'website_id': website_id, "website_id": website_id,
'period': f'last_{days}_days', "period": f"last_{days}_days",
'pageviews': stats.get('pageviews', 0), "pageviews": stats.get("pageviews", 0),
'uniques': stats.get('uniques', 0), "uniques": stats.get("uniques", 0),
'bounces': stats.get('bounces', 0), "bounces": stats.get("bounces", 0),
'totaltime': stats.get('totaltime', 0), "totaltime": stats.get("totaltime", 0),
'avg_session_duration': stats.get('totaltime', 0) / max(stats.get('visits', 1), 1), "avg_session_duration": stats.get("totaltime", 0)
'bounce_rate': stats.get('bounces', 0) / max(stats.get('visits', 1), 1) * 100 / max(stats.get("visits", 1), 1),
"bounce_rate": stats.get("bounces", 0)
/ max(stats.get("visits", 1), 1)
* 100,
} }
except Exception as e: 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: def _get_tracking_script(self, website_id: str) -> str:
"""Generate tracking script HTML""" """Generate tracking script HTML"""
@@ -188,10 +189,10 @@ class UmamiClient:
# Find Astro layout file # Find Astro layout file
layout_paths = [ layout_paths = [
os.path.join(website_repo, 'src/layouts/Layout.astro'), os.path.join(website_repo, "src/layouts/Layout.astro"),
os.path.join(website_repo, 'src/layouts/BaseHead.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/pages/_document.tsx"),
os.path.join(website_repo, 'src/app.html') os.path.join(website_repo, "src/app.html"),
] ]
layout_file = None layout_file = None
@@ -201,51 +202,66 @@ class UmamiClient:
break break
if not layout_file: 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): if os.path.exists(layouts_dir):
for f in os.listdir(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) layout_file = os.path.join(layouts_dir, f)
break break
if not layout_file: if not layout_file:
return {'success': False, 'error': 'No Astro layout file found'} return {"success": False, "error": "No Astro layout file found"}
with open(layout_file, 'r', encoding='utf-8') as f: with open(layout_file, "r", encoding="utf-8") as f:
content = f.read() content = f.read()
if '</head>' in content: if "</head>" in content:
content = content.replace('</head>', f' {tracking_script}\n </head>') content = content.replace(
"</head>", f" {tracking_script}\n </head>"
)
else: else:
content += f'\n{tracking_script}\n' content += f"\n{tracking_script}\n"
with open(layout_file, 'w', encoding='utf-8') as f: with open(layout_file, "w", encoding="utf-8") as f:
f.write(content) f.write(content)
return { return {"success": True, "layout_file": layout_file, "tracking_added": True}
'success': True,
'layout_file': layout_file,
'tracking_added': True
}
except Exception as e: except Exception as e:
return {'success': False, 'error': str(e)} return {"success": False, "error": str(e)}
def main(): def main():
"""Main CLI entry point""" """Main CLI entry point"""
parser = argparse.ArgumentParser(description='Umami Analytics Client') parser = argparse.ArgumentParser(description="Umami Analytics Client")
parser.add_argument('--action', required=True, parser.add_argument(
choices=['create-website', 'get-tracking', 'add-tracking', 'get-stats', 'list-websites']) "--action",
parser.add_argument('--umami-url', help='Umami instance URL (or set UMAMI_URL in .env)') required=True,
parser.add_argument('--username', help='Umami username (or set UMAMI_USERNAME in .env)') choices=[
parser.add_argument('--password', help='Umami password (or set UMAMI_PASSWORD in .env)') "create-website",
parser.add_argument('--website-name', help='Website name (for create)') "get-tracking",
parser.add_argument('--website-domain', help='Website domain (for create/find)') "add-tracking",
parser.add_argument('--website-id', help='Website ID (for stats)') "get-stats",
parser.add_argument('--website-repo', help='Path to website repo (for add-tracking)') "list-websites",
parser.add_argument('--days', type=int, default=30, help='Days for stats') ],
)
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() args = parser.parse_args()
@@ -254,12 +270,10 @@ def main():
# Initialize client (loads credentials from .env automatically) # Initialize client (loads credentials from .env automatically)
client = UmamiClient( client = UmamiClient(
umami_url=args.umami_url, umami_url=args.umami_url, username=args.username, password=args.password
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: if not args.website_name or not args.website_domain:
print("Error: --website-name and --website-domain required") print("Error: --website-name and --website-domain required")
return return
@@ -267,7 +281,7 @@ def main():
print(f"Creating website: {args.website_name} ({args.website_domain})") print(f"Creating website: {args.website_name} ({args.website_domain})")
result = client.create_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"\n✅ Website created!")
print(f" ID: {result['website_id']}") print(f" ID: {result['website_id']}")
print(f" Name: {result['name']}") print(f" Name: {result['name']}")
@@ -277,7 +291,7 @@ def main():
else: else:
print(f"\n❌ Failed: {result['error']}") print(f"\n❌ Failed: {result['error']}")
elif args.action == 'get-tracking': elif args.action == "get-tracking":
if not args.website_id: if not args.website_id:
print("Error: --website-id required") print("Error: --website-id required")
return return
@@ -286,7 +300,7 @@ def main():
print(f"\nTracking script for {args.website_id}:") print(f"\nTracking script for {args.website_id}:")
print(script) print(script)
elif args.action == 'add-tracking': elif args.action == "add-tracking":
if not args.website_id or not args.website_repo: if not args.website_id or not args.website_repo:
print("Error: --website-id and --website-repo required") print("Error: --website-id and --website-repo required")
return return
@@ -294,13 +308,13 @@ def main():
print(f"Adding tracking to: {args.website_repo}") print(f"Adding tracking to: {args.website_repo}")
result = client.add_tracking_to_astro(args.website_repo, args.website_id) 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"\n✅ Tracking added!")
print(f" Layout: {result['layout_file']}") print(f" Layout: {result['layout_file']}")
else: else:
print(f"\n❌ Failed: {result['error']}") print(f"\n❌ Failed: {result['error']}")
elif args.action == 'get-stats': elif args.action == "get-stats":
if not args.website_id: if not args.website_id:
print("Error: --website-id required") print("Error: --website-id required")
return return
@@ -308,7 +322,7 @@ def main():
print(f"Getting stats for last {args.days} days...") print(f"Getting stats for last {args.days} days...")
stats = client.get_stats(args.website_id, args.days) stats = client.get_stats(args.website_id, args.days)
if stats['success']: if stats["success"]:
print(f"\n📊 Analytics ({stats['period']}):") print(f"\n📊 Analytics ({stats['period']}):")
print(f" Pageviews: {stats['pageviews']:,}") print(f" Pageviews: {stats['pageviews']:,}")
print(f" Unique visitors: {stats['uniques']:,}") print(f" Unique visitors: {stats['uniques']:,}")
@@ -318,7 +332,7 @@ def main():
else: else:
print(f"\n❌ Failed: {stats['error']}") print(f"\n❌ Failed: {stats['error']}")
elif args.action == 'list-websites': elif args.action == "list-websites":
print("Listing websites...") print("Listing websites...")
websites = client.list_websites() websites = client.list_websites()
@@ -327,5 +341,5 @@ def main():
print(f"{site.get('name')} - {site.get('domain')}") print(f"{site.get('name')} - {site.get('domain')}")
if __name__ == '__main__': if __name__ == "__main__":
main() main()