fix: Umami skill auto-load from unified .env
- Load credentials from ~/.config/opencode/.env automatically - No need to configure .env in each skill directory - Fixes issue where new skills couldn't find credentials
This commit is contained in:
@@ -4,6 +4,10 @@ Umami Analytics Client
|
||||
|
||||
Self-hosted Umami integration with username/password authentication.
|
||||
Creates websites, gets tracking codes, and fetches analytics data.
|
||||
|
||||
Automatically loads credentials from:
|
||||
- ~/.config/opencode/.env (unified .env)
|
||||
- .env (local, for development)
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -13,36 +17,41 @@ import argparse
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Optional, List
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load credentials from unified .env
|
||||
load_dotenv(os.path.expanduser('~/.config/opencode/.env'))
|
||||
load_dotenv() # Also load local .env for development
|
||||
|
||||
|
||||
class UmamiClient:
|
||||
"""Umami Analytics API client with username/password auth"""
|
||||
|
||||
def __init__(self, umami_url: str, 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 (e.g., https://analytics.example.com)
|
||||
username: Umami username/email (for self-hosted)
|
||||
password: Umami password (for self-hosted)
|
||||
umami_url: Umami instance URL (loaded from .env if not provided)
|
||||
username: Umami username/email (loaded from .env if not provided)
|
||||
password: Umami password (loaded from .env if not provided)
|
||||
token: Bearer token (optional, if already have)
|
||||
"""
|
||||
self.umami_url = umami_url.rstrip('/')
|
||||
self.api_url = f"{self.umami_url}/api"
|
||||
self.username = username
|
||||
self.password = password
|
||||
# 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.token = token
|
||||
self.user_id = None
|
||||
|
||||
# Auto-login if credentials provided
|
||||
if username and password and not token:
|
||||
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.api_url}/auth/login"
|
||||
url = f"{self.umami_url}/api/auth/login"
|
||||
data = {
|
||||
'username': self.username,
|
||||
'password': self.password
|
||||
@@ -81,18 +90,9 @@ class UmamiClient:
|
||||
}
|
||||
|
||||
def create_website(self, name: str, domain: str) -> Dict:
|
||||
"""
|
||||
Create new Umami website
|
||||
|
||||
Args:
|
||||
name: Website name
|
||||
domain: Website domain (full URL)
|
||||
|
||||
Returns:
|
||||
Website creation result
|
||||
"""
|
||||
"""Create new Umami website"""
|
||||
try:
|
||||
url = f"{self.api_url}/websites"
|
||||
url = f"{self.umami_url}/api/websites"
|
||||
data = {
|
||||
'name': name,
|
||||
'domain': domain
|
||||
@@ -129,7 +129,7 @@ class UmamiClient:
|
||||
def list_websites(self) -> List[Dict]:
|
||||
"""Get all websites"""
|
||||
try:
|
||||
url = f"{self.api_url}/websites"
|
||||
url = f"{self.umami_url}/api/websites"
|
||||
response = requests.get(url, headers=self._get_headers())
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
@@ -147,21 +147,12 @@ class UmamiClient:
|
||||
return []
|
||||
|
||||
def get_stats(self, website_id: str, days: int = 30) -> Dict:
|
||||
"""
|
||||
Get website statistics
|
||||
|
||||
Args:
|
||||
website_id: Umami website ID
|
||||
days: Number of days to look back
|
||||
|
||||
Returns:
|
||||
Analytics stats
|
||||
"""
|
||||
"""Get website statistics"""
|
||||
try:
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
url = f"{self.api_url}/websites/{website_id}/stats"
|
||||
url = f"{self.umami_url}/api/websites/{website_id}/stats"
|
||||
params = {
|
||||
'startAt': int(start_date.timestamp() * 1000),
|
||||
'endAt': int(end_date.timestamp() * 1000)
|
||||
@@ -191,16 +182,7 @@ class UmamiClient:
|
||||
return f'<script defer src="{self.umami_url}/script.js" data-website-id="{website_id}"></script>'
|
||||
|
||||
def add_tracking_to_astro(self, website_repo: str, website_id: str) -> Dict:
|
||||
"""
|
||||
Add Umami tracking to Astro website
|
||||
|
||||
Args:
|
||||
website_repo: Path to Astro website repo
|
||||
website_id: Umami website ID
|
||||
|
||||
Returns:
|
||||
Result of adding tracking
|
||||
"""
|
||||
"""Add Umami tracking to Astro website"""
|
||||
try:
|
||||
tracking_script = self._get_tracking_script(website_id)
|
||||
|
||||
@@ -219,7 +201,6 @@ class UmamiClient:
|
||||
break
|
||||
|
||||
if not layout_file:
|
||||
# Try to find any .astro file in src/layouts
|
||||
layouts_dir = os.path.join(website_repo, 'src/layouts')
|
||||
if os.path.exists(layouts_dir):
|
||||
for f in os.listdir(layouts_dir):
|
||||
@@ -230,18 +211,14 @@ class UmamiClient:
|
||||
if not layout_file:
|
||||
return {'success': False, 'error': 'No Astro layout file found'}
|
||||
|
||||
# Read layout file
|
||||
with open(layout_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Add tracking before </head>
|
||||
if '</head>' in content:
|
||||
content = content.replace('</head>', f' {tracking_script}\n </head>')
|
||||
else:
|
||||
# If no </head>, add at end of file
|
||||
content += f'\n{tracking_script}\n'
|
||||
|
||||
# Write back
|
||||
with open(layout_file, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
@@ -261,9 +238,9 @@ def main():
|
||||
|
||||
parser.add_argument('--action', required=True,
|
||||
choices=['create-website', 'get-tracking', 'add-tracking', 'get-stats', 'list-websites'])
|
||||
parser.add_argument('--umami-url', required=True, help='Umami instance URL')
|
||||
parser.add_argument('--username', help='Umami username')
|
||||
parser.add_argument('--password', help='Umami password')
|
||||
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)')
|
||||
@@ -273,10 +250,14 @@ def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"\n📊 Umami Analytics Client")
|
||||
print(f"URL: {args.umami_url}\n")
|
||||
print(f"URL: {args.umami_url or os.getenv('UMAMI_URL', 'Not set')}\n")
|
||||
|
||||
# Initialize client
|
||||
client = UmamiClient(args.umami_url, args.username, args.password)
|
||||
# Initialize client (loads credentials from .env automatically)
|
||||
client = UmamiClient(
|
||||
umami_url=args.umami_url,
|
||||
username=args.username,
|
||||
password=args.password
|
||||
)
|
||||
|
||||
if args.action == 'create-website':
|
||||
if not args.website_name or not args.website_domain:
|
||||
|
||||
Reference in New Issue
Block a user