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:
Kunthawat Greethong
2026-03-09 13:50:10 +07:00
parent 9be686f587
commit 7524c29ce5
2 changed files with 35 additions and 54 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

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