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:
14
README.md
14
README.md
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
14
skills/_env_loader.py
Normal 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)
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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__))
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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__))
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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__))
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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", "")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user