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.
|
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:
|
||||||
|
|||||||
Reference in New Issue
Block a user