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. Self-hosted Umami integration with username/password authentication.
Creates websites, gets tracking codes, and fetches analytics data. Creates websites, gets tracking codes, and fetches analytics data.
Automatically loads credentials from:
- ~/.config/opencode/.env (unified .env)
- .env (local, for development)
""" """
import os import os
@@ -13,36 +17,41 @@ 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
# Load credentials from 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, 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
Args: Args:
umami_url: Umami instance URL (e.g., https://analytics.example.com) umami_url: Umami instance URL (loaded from .env if not provided)
username: Umami username/email (for self-hosted) username: Umami username/email (loaded from .env if not provided)
password: Umami password (for self-hosted) password: Umami password (loaded from .env if not provided)
token: Bearer token (optional, if already have) token: Bearer token (optional, if already have)
""" """
self.umami_url = umami_url.rstrip('/') # Load from environment if not provided
self.api_url = f"{self.umami_url}/api" self.umami_url = umami_url or os.getenv('UMAMI_URL', '')
self.username = username self.username = username or os.getenv('UMAMI_USERNAME', '')
self.password = password self.password = password or os.getenv('UMAMI_PASSWORD', '')
self.token = token self.token = token
self.user_id = None self.user_id = None
# Auto-login if credentials provided # Auto-login if credentials provided
if username and password and not token: if self.username and self.password and not token:
self.login() self.login()
def login(self) -> Dict: def login(self) -> Dict:
"""Login to Umami and get bearer token""" """Login to Umami and get bearer token"""
try: try:
url = f"{self.api_url}/auth/login" url = f"{self.umami_url}/api/auth/login"
data = { data = {
'username': self.username, 'username': self.username,
'password': self.password 'password': self.password
@@ -81,18 +90,9 @@ class UmamiClient:
} }
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
Args:
name: Website name
domain: Website domain (full URL)
Returns:
Website creation result
"""
try: try:
url = f"{self.api_url}/websites" url = f"{self.umami_url}/api/websites"
data = { data = {
'name': name, 'name': name,
'domain': domain 'domain': domain
@@ -129,7 +129,7 @@ class UmamiClient:
def list_websites(self) -> List[Dict]: def list_websites(self) -> List[Dict]:
"""Get all websites""" """Get all websites"""
try: try:
url = f"{self.api_url}/websites" url = f"{self.umami_url}/api/websites"
response = requests.get(url, headers=self._get_headers()) response = requests.get(url, headers=self._get_headers())
response.raise_for_status() response.raise_for_status()
result = response.json() result = response.json()
@@ -147,21 +147,12 @@ class UmamiClient:
return [] return []
def get_stats(self, website_id: str, days: int = 30) -> Dict: def get_stats(self, website_id: str, days: int = 30) -> Dict:
""" """Get website statistics"""
Get website statistics
Args:
website_id: Umami website ID
days: Number of days to look back
Returns:
Analytics stats
"""
try: try:
end_date = datetime.now() end_date = datetime.now()
start_date = end_date - timedelta(days=days) 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 = { 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)
@@ -191,16 +182,7 @@ class UmamiClient:
return f'<script defer src="{self.umami_url}/script.js" data-website-id="{website_id}"></script>' 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: def add_tracking_to_astro(self, website_repo: str, website_id: str) -> Dict:
""" """Add Umami tracking to Astro website"""
Add Umami tracking to Astro website
Args:
website_repo: Path to Astro website repo
website_id: Umami website ID
Returns:
Result of adding tracking
"""
try: try:
tracking_script = self._get_tracking_script(website_id) tracking_script = self._get_tracking_script(website_id)
@@ -219,7 +201,6 @@ class UmamiClient:
break break
if not layout_file: if not layout_file:
# Try to find any .astro file in src/layouts
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):
@@ -230,18 +211,14 @@ class UmamiClient:
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'}
# Read layout file
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()
# Add tracking before </head>
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:
# If no </head>, add at end of file
content += f'\n{tracking_script}\n' content += f'\n{tracking_script}\n'
# Write back
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)
@@ -261,9 +238,9 @@ def main():
parser.add_argument('--action', required=True, parser.add_argument('--action', required=True,
choices=['create-website', 'get-tracking', 'add-tracking', 'get-stats', 'list-websites']) 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('--umami-url', help='Umami instance URL (or set UMAMI_URL in .env)')
parser.add_argument('--username', help='Umami username') parser.add_argument('--username', help='Umami username (or set UMAMI_USERNAME in .env)')
parser.add_argument('--password', help='Umami password') 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-name', help='Website name (for create)')
parser.add_argument('--website-domain', help='Website domain (for create/find)') 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-id', help='Website ID (for stats)')
@@ -273,10 +250,14 @@ def main():
args = parser.parse_args() args = parser.parse_args()
print(f"\n📊 Umami Analytics Client") 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 # Initialize client (loads credentials from .env automatically)
client = UmamiClient(args.umami_url, args.username, args.password) client = UmamiClient(
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: if not args.website_name or not args.website_domain: