From f66a563406aa9ffb8cd759499233b65e1b326e82 Mon Sep 17 00:00:00 2001 From: Kris Hagel Date: Mon, 26 Jan 2026 06:35:58 -0800 Subject: [PATCH 1/2] Migrate secrets from .env to 1Password CLI Replace iCloud-stored .env file with 1Password CLI for secure secret management. All skills now use centralized secrets modules that fetch credentials via `op read` command. Changes: - Add scripts/secrets.js - centralized JS module with SECRETS accessor - Add scripts/secrets.py - centralized Python module with get_secret() - Add docs/1password-setup.md - setup guide and troubleshooting Updated skills (26 files total): - freshservice-manager: 13 scripts updated - elevenlabs-tts: 2 scripts updated - image-gen: 3 scripts updated - multi-model-research: 1 script updated - research: 1 script updated - google-workspace: 2 auth scripts + package.json updated Key improvements: - No more plaintext secrets on iCloud - Centralized secret mapping in VAULT_MAP - Caching to minimize CLI calls - Clear error messages with setup instructions - Removed python-dotenv dependency from Python scripts - Removed dotenv dependency from google-workspace Closes #10 --- docs/1password-setup.md | 128 +++++++++ scripts/secrets.js | 225 +++++++++++++++ scripts/secrets.py | 261 ++++++++++++++++++ .../elevenlabs-tts/scripts/generate_audio.py | 14 +- skills/elevenlabs-tts/scripts/list_voices.py | 14 +- .../freshservice-manager/scripts/add_note.js | 23 +- .../scripts/create_ticket.js | 23 +- .../freshservice-manager/scripts/get_agent.js | 31 +-- .../scripts/get_approvals.js | 23 +- .../scripts/get_daily_summary.js | 23 +- .../scripts/get_service_request.js | 23 +- .../scripts/get_ticket.js | 23 +- .../scripts/get_weekly_summary.js | 23 +- .../scripts/get_workspaces.js | 23 +- .../scripts/list_agents.js | 23 +- .../scripts/list_tickets.js | 23 +- .../scripts/search_tickets.js | 23 +- .../scripts/update_ticket.js | 23 +- skills/google-workspace/auth/oauth_setup.js | 20 +- skills/google-workspace/auth/token_manager.js | 22 +- skills/google-workspace/package.json | 1 - skills/image-gen/scripts/compose.py | 17 +- skills/image-gen/scripts/edit.py | 17 +- skills/image-gen/scripts/generate.py | 19 +- .../scripts/llm_client.py | 18 +- skills/research/scripts/orchestrator.js | 23 +- 26 files changed, 692 insertions(+), 394 deletions(-) create mode 100644 docs/1password-setup.md create mode 100644 scripts/secrets.js create mode 100644 scripts/secrets.py diff --git a/docs/1password-setup.md b/docs/1password-setup.md new file mode 100644 index 0000000..3dfff4d --- /dev/null +++ b/docs/1password-setup.md @@ -0,0 +1,128 @@ +# 1Password Setup for Geoffrey + +Geoffrey uses 1Password CLI to securely manage API keys and secrets. This replaces the previous `.env` file approach. + +## Prerequisites + +1. **Install 1Password CLI** + ```bash + brew install --cask 1password-cli + ``` + +2. **Enable CLI Integration in 1Password App** + - Open 1Password app + - Go to Settings → Developer + - Enable "Integrate with 1Password CLI" + +3. **Authenticate** + ```bash + op signin + ``` + +## Required 1Password Items + +Create the following items in a vault named "Geoffrey" (or update `VAULT_MAP` in `scripts/secrets.js` if using a different vault): + +### Freshservice +- **Item name:** `Freshservice` +- **Fields:** + - `domain` - Your Freshservice domain (e.g., `yourcompany.freshservice.com`) + - `api-key` - Your Freshservice API key + +### OpenAI +- **Item name:** `OpenAI` +- **Fields:** + - `api-key` - Your OpenAI API key + +### Gemini +- **Item name:** `Gemini` +- **Fields:** + - `api-key` - Your Google Gemini API key + +### Perplexity +- **Item name:** `Perplexity` +- **Fields:** + - `api-key` - Your Perplexity API key + +### XAI (Grok) +- **Item name:** `XAI` +- **Fields:** + - `api-key` - Your xAI API key + +### ElevenLabs +- **Item name:** `ElevenLabs` +- **Fields:** + - `api-key` - Your ElevenLabs API key + +### Google Workspace +- **Item name:** `Google-Workspace` +- **Fields:** + - `client-id` - Your Google OAuth client ID + - `client-secret` - Your Google OAuth client secret + +### Obsidian MCP +- **Item name:** `Obsidian-MCP` +- **Fields:** + - `api-key` - Your Obsidian Local REST API key + +## Secret References + +The centralized secrets module (`scripts/secrets.js` and `scripts/secrets.py`) maps environment variable names to 1Password secret references: + +| Secret Name | 1Password Reference | +|-------------|---------------------| +| `FRESHSERVICE_DOMAIN` | `op://Geoffrey/Freshservice/domain` | +| `FRESHSERVICE_API_KEY` | `op://Geoffrey/Freshservice/api-key` | +| `OPENAI_API_KEY` | `op://Geoffrey/OpenAI/api-key` | +| `GEMINI_API_KEY` | `op://Geoffrey/Gemini/api-key` | +| `PERPLEXITY_API_KEY` | `op://Geoffrey/Perplexity/api-key` | +| `XAI_API_KEY` | `op://Geoffrey/XAI/api-key` | +| `GOOGLE_CLIENT_ID` | `op://Geoffrey/Google-Workspace/client-id` | +| `GOOGLE_CLIENT_SECRET` | `op://Geoffrey/Google-Workspace/client-secret` | +| `ELEVENLABS_API_KEY` | `op://Geoffrey/ElevenLabs/api-key` | +| `OBSIDIAN_API_KEY` | `op://Geoffrey/Obsidian-MCP/api-key` | + +## Testing + +Verify your setup by running: + +```bash +# Test that 1Password CLI is working +op whoami + +# Test secret retrieval +op read "op://Geoffrey/Freshservice/api-key" +``` + +## Troubleshooting + +### "1Password CLI is not available or not authenticated" + +1. Ensure 1Password CLI is installed: `which op` +2. Ensure CLI integration is enabled in 1Password app settings +3. Sign in: `op signin` + +### "Secret not found" + +1. Verify the item exists in your 1Password vault +2. Check that the vault name matches (default: "Geoffrey") +3. Check that field names match (use lowercase with hyphens) + +### Changing the Vault Name + +If your vault has a different name, update `VAULT_MAP` in both: +- `scripts/secrets.js` +- `scripts/secrets.py` + +Replace `op://Geoffrey/...` with `op://YourVaultName/...` + +## Migration from .env + +If you previously used the `.env` file approach: + +1. Create the required items in 1Password (see above) +2. Copy your API keys from `.env` to the corresponding 1Password fields +3. Verify scripts work with `bun` or `uv run` +4. Delete the old `.env` file from iCloud + +The `.env` file location was: `~/Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env` diff --git a/scripts/secrets.js b/scripts/secrets.js new file mode 100644 index 0000000..dc4b258 --- /dev/null +++ b/scripts/secrets.js @@ -0,0 +1,225 @@ +#!/usr/bin/env node + +/** + * Geoffrey Secrets Manager + * + * Loads secrets exclusively from 1Password CLI. + * + * Usage: + * const { getSecret, requireSecret, SECRETS } = require('./secrets.js'); + * const apiKey = requireSecret('OPENAI_API_KEY'); + * // Or use pre-defined accessors: + * const { domain, apiKey } = SECRETS.freshservice; + * + * 1Password Setup: + * 1. Install: brew install --cask 1password-cli + * 2. Enable CLI integration in 1Password desktop app settings + * 3. Create "Geoffrey" vault with items matching VAULT_MAP below + * 4. See docs/1password-setup.md for detailed setup guide + */ + +const { execSync } = require('child_process'); + +// Map environment variable names to 1Password secret references +// Format: op://vault/item/field +const VAULT_MAP = { + // Freshservice + FRESHSERVICE_DOMAIN: 'op://Geoffrey/Freshservice/domain', + FRESHSERVICE_API_KEY: 'op://Geoffrey/Freshservice/api-key', + + // Research LLMs + OPENAI_API_KEY: 'op://Geoffrey/OpenAI/api-key', + GEMINI_API_KEY: 'op://Geoffrey/Gemini/api-key', + PERPLEXITY_API_KEY: 'op://Geoffrey/Perplexity/api-key', + XAI_API_KEY: 'op://Geoffrey/XAI/api-key', + + // Google Workspace + GOOGLE_CLIENT_ID: 'op://Geoffrey/Google-Workspace/client-id', + GOOGLE_CLIENT_SECRET: 'op://Geoffrey/Google-Workspace/client-secret', + + // ElevenLabs + ELEVENLABS_API_KEY: 'op://Geoffrey/ElevenLabs/api-key', + + // Obsidian MCP + OBSIDIAN_API_KEY: 'op://Geoffrey/Obsidian-MCP/api-key', +}; + +// Cache for loaded secrets (avoid repeated CLI calls) +const secretsCache = new Map(); + +// Track 1Password availability check +let _1pChecked = false; +let _1pAvailable = false; + +/** + * Check if 1Password CLI is available and authenticated + */ +function is1PasswordAvailable() { + if (_1pChecked) return _1pAvailable; + + try { + execSync('op account list', { encoding: 'utf8', stdio: 'pipe' }); + _1pAvailable = true; + } catch { + _1pAvailable = false; + } + _1pChecked = true; + return _1pAvailable; +} + +/** + * Ensure 1Password is available, throw helpful error if not + */ +function ensure1Password() { + if (!is1PasswordAvailable()) { + throw new Error( + '1Password CLI is not available or not authenticated.\n\n' + + 'Setup required:\n' + + ' 1. Install: brew install --cask 1password-cli\n' + + ' 2. Enable CLI integration in 1Password app:\n' + + ' Settings → Developer → Enable CLI integration\n' + + ' 3. Authenticate: op signin\n\n' + + 'See docs/1password-setup.md for detailed instructions.' + ); + } +} + +/** + * Load a secret from 1Password + */ +function loadFrom1Password(secretRef) { + try { + const value = execSync(`op read "${secretRef}"`, { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + timeout: 10000, + }).trim(); + return value; + } catch (error) { + return null; + } +} + +/** + * Get a single secret by name + * + * @param {string} name - Environment variable name (e.g., 'OPENAI_API_KEY') + * @returns {string|null} - Secret value or null if not found + */ +function getSecret(name) { + // Check cache first + if (secretsCache.has(name)) { + return secretsCache.get(name); + } + + // Ensure 1Password is available + ensure1Password(); + + // Get 1Password reference + const secretRef = VAULT_MAP[name]; + if (!secretRef) { + throw new Error( + `Unknown secret: ${name}\n` + + `Available secrets: ${Object.keys(VAULT_MAP).join(', ')}` + ); + } + + // Load from 1Password + const value = loadFrom1Password(secretRef); + + // Cache the result + if (value) { + secretsCache.set(name, value); + } + + return value; +} + +/** + * Load multiple secrets at once + * + * @param {string[]} names - Array of environment variable names + * @returns {Object} - Object with secret names as keys + */ +function loadSecrets(names) { + const result = {}; + for (const name of names) { + result[name] = getSecret(name); + } + return result; +} + +/** + * Get all configured secrets (useful for debugging) + */ +function listAvailableSecrets() { + return Object.keys(VAULT_MAP); +} + +/** + * Require a secret (throws if not found) + * + * @param {string} name - Environment variable name + * @returns {string} - Secret value + * @throws {Error} - If secret is not found + */ +function requireSecret(name) { + const value = getSecret(name); + if (!value) { + const opRef = VAULT_MAP[name] || 'not configured'; + throw new Error( + `Missing required secret: ${name}\n` + + `1Password reference: ${opRef}\n\n` + + 'To add this secret:\n' + + ` 1. Open 1Password\n` + + ` 2. Create/edit item in Geoffrey vault matching: ${opRef}\n` + + ` 3. See docs/1password-setup.md for vault structure` + ); + } + return value; +} + +// Pre-defined secret accessors for common use cases +const SECRETS = { + get freshservice() { + return { + domain: requireSecret('FRESHSERVICE_DOMAIN'), + apiKey: requireSecret('FRESHSERVICE_API_KEY'), + }; + }, + get openai() { + return requireSecret('OPENAI_API_KEY'); + }, + get gemini() { + return requireSecret('GEMINI_API_KEY'); + }, + get perplexity() { + return requireSecret('PERPLEXITY_API_KEY'); + }, + get xai() { + return requireSecret('XAI_API_KEY'); + }, + get google() { + return { + clientId: requireSecret('GOOGLE_CLIENT_ID'), + clientSecret: requireSecret('GOOGLE_CLIENT_SECRET'), + }; + }, + get elevenlabs() { + return requireSecret('ELEVENLABS_API_KEY'); + }, + get obsidian() { + return requireSecret('OBSIDIAN_API_KEY'); + }, +}; + +module.exports = { + getSecret, + loadSecrets, + requireSecret, + listAvailableSecrets, + is1PasswordAvailable, + ensure1Password, + SECRETS, + VAULT_MAP, +}; diff --git a/scripts/secrets.py b/scripts/secrets.py new file mode 100644 index 0000000..6268b03 --- /dev/null +++ b/scripts/secrets.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.11" +# dependencies = [] +# /// +""" +Geoffrey Secrets Manager (Python) + +Loads secrets exclusively from 1Password CLI. + +Usage: + from secrets import get_secret, require_secret, SECRETS + api_key = require_secret('OPENAI_API_KEY') + # Or use pre-defined accessors: + api_key = SECRETS.openai + +1Password Setup: + 1. Install: brew install --cask 1password-cli + 2. Enable CLI integration in 1Password desktop app settings + 3. Create "Geoffrey" vault with items matching VAULT_MAP below + 4. See docs/1password-setup.md for detailed setup guide +""" + +import subprocess +import sys +from functools import lru_cache +from typing import Optional + +# Map environment variable names to 1Password secret references +# Format: op://vault/item/field +VAULT_MAP = { + # Freshservice + "FRESHSERVICE_DOMAIN": "op://Geoffrey/Freshservice/domain", + "FRESHSERVICE_API_KEY": "op://Geoffrey/Freshservice/api-key", + + # Research LLMs + "OPENAI_API_KEY": "op://Geoffrey/OpenAI/api-key", + "GEMINI_API_KEY": "op://Geoffrey/Gemini/api-key", + "PERPLEXITY_API_KEY": "op://Geoffrey/Perplexity/api-key", + "XAI_API_KEY": "op://Geoffrey/XAI/api-key", + + # Google Workspace + "GOOGLE_CLIENT_ID": "op://Geoffrey/Google-Workspace/client-id", + "GOOGLE_CLIENT_SECRET": "op://Geoffrey/Google-Workspace/client-secret", + + # ElevenLabs + "ELEVENLABS_API_KEY": "op://Geoffrey/ElevenLabs/api-key", + + # Obsidian MCP + "OBSIDIAN_API_KEY": "op://Geoffrey/Obsidian-MCP/api-key", +} + +# Cache for loaded secrets +_secrets_cache: dict[str, str] = {} + + +@lru_cache(maxsize=1) +def is_1password_available() -> bool: + """Check if 1Password CLI is available and authenticated.""" + try: + subprocess.run( + ["op", "account", "list"], + capture_output=True, + check=True, + timeout=5, + ) + return True + except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): + return False + + +def ensure_1password() -> None: + """Ensure 1Password is available, raise helpful error if not.""" + if not is_1password_available(): + raise RuntimeError( + "1Password CLI is not available or not authenticated.\n\n" + "Setup required:\n" + " 1. Install: brew install --cask 1password-cli\n" + " 2. Enable CLI integration in 1Password app:\n" + " Settings → Developer → Enable CLI integration\n" + " 3. Authenticate: op signin\n\n" + "See docs/1password-setup.md for detailed instructions." + ) + + +def _load_from_1password(secret_ref: str) -> Optional[str]: + """Load a secret from 1Password.""" + try: + result = subprocess.run( + ["op", "read", secret_ref], + capture_output=True, + text=True, + check=True, + timeout=10, + ) + return result.stdout.strip() + except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): + return None + + +def get_secret(name: str) -> Optional[str]: + """ + Get a single secret by name. + + Args: + name: Environment variable name (e.g., 'OPENAI_API_KEY') + + Returns: + Secret value or None if not found + """ + # Check cache first + if name in _secrets_cache: + return _secrets_cache[name] + + # Ensure 1Password is available + ensure_1password() + + # Get 1Password reference + secret_ref = VAULT_MAP.get(name) + if not secret_ref: + raise ValueError( + f"Unknown secret: {name}\n" + f"Available secrets: {', '.join(VAULT_MAP.keys())}" + ) + + # Load from 1Password + value = _load_from_1password(secret_ref) + + # Cache the result + if value: + _secrets_cache[name] = value + + return value + + +def load_secrets(names: list[str]) -> dict[str, Optional[str]]: + """ + Load multiple secrets at once. + + Args: + names: List of environment variable names + + Returns: + Dict with secret names as keys + """ + return {name: get_secret(name) for name in names} + + +def require_secret(name: str) -> str: + """ + Require a secret (raises if not found). + + Args: + name: Environment variable name + + Returns: + Secret value + + Raises: + ValueError: If secret is not found + """ + value = get_secret(name) + if not value: + op_ref = VAULT_MAP.get(name, "not configured") + raise ValueError( + f"Missing required secret: {name}\n" + f"1Password reference: {op_ref}\n\n" + f"To add this secret:\n" + f" 1. Open 1Password\n" + f" 2. Create/edit item in Geoffrey vault matching: {op_ref}\n" + f" 3. See docs/1password-setup.md for vault structure" + ) + return value + + +def list_available_secrets() -> list[str]: + """Get all configured secret names.""" + return list(VAULT_MAP.keys()) + + +class SecretsAccessor: + """Pre-defined secret accessors for common use cases.""" + + @property + def freshservice_domain(self) -> str: + return require_secret("FRESHSERVICE_DOMAIN") + + @property + def freshservice_api_key(self) -> str: + return require_secret("FRESHSERVICE_API_KEY") + + @property + def openai(self) -> str: + return require_secret("OPENAI_API_KEY") + + @property + def gemini(self) -> str: + return require_secret("GEMINI_API_KEY") + + @property + def perplexity(self) -> str: + return require_secret("PERPLEXITY_API_KEY") + + @property + def xai(self) -> str: + return require_secret("XAI_API_KEY") + + @property + def google_client_id(self) -> str: + return require_secret("GOOGLE_CLIENT_ID") + + @property + def google_client_secret(self) -> str: + return require_secret("GOOGLE_CLIENT_SECRET") + + @property + def elevenlabs(self) -> str: + return require_secret("ELEVENLABS_API_KEY") + + @property + def obsidian(self) -> str: + return require_secret("OBSIDIAN_API_KEY") + + +# Singleton accessor +SECRETS = SecretsAccessor() + + +if __name__ == "__main__": + # CLI for testing + import argparse + + parser = argparse.ArgumentParser(description="Geoffrey Secrets Manager") + parser.add_argument("--list", action="store_true", help="List available secrets") + parser.add_argument("--get", metavar="NAME", help="Get a specific secret") + parser.add_argument("--check-1p", action="store_true", help="Check 1Password availability") + + args = parser.parse_args() + + if args.list: + print("Available secrets:") + for name in list_available_secrets(): + print(f" {name}: {VAULT_MAP[name]}") + elif args.get: + try: + value = require_secret(args.get) + # Mask middle of secret for security + if len(value) > 8: + masked = value[:4] + "..." + value[-4:] + else: + masked = "***" + print(f"{args.get}: {masked}") + except (ValueError, RuntimeError) as e: + print(str(e), file=sys.stderr) + sys.exit(1) + elif args.check_1p: + available = is_1password_available() + print(f"1Password CLI: {'available' if available else 'not available'}") + sys.exit(0 if available else 1) + else: + parser.print_help() diff --git a/skills/elevenlabs-tts/scripts/generate_audio.py b/skills/elevenlabs-tts/scripts/generate_audio.py index 52b7130..bb54de7 100755 --- a/skills/elevenlabs-tts/scripts/generate_audio.py +++ b/skills/elevenlabs-tts/scripts/generate_audio.py @@ -3,7 +3,6 @@ # requires-python = ">=3.11" # dependencies = [ # "httpx", -# "python-dotenv", # "pydub", # "audioop-lts; python_version >= '3.13'", # ] @@ -21,7 +20,6 @@ import argparse import json -import os import re import sys import tempfile @@ -29,15 +27,13 @@ from pathlib import Path import httpx -from dotenv import load_dotenv from pydub import AudioSegment -# Load secrets -SECRETS_PATH = Path.home() / "Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env" -if SECRETS_PATH.exists(): - load_dotenv(SECRETS_PATH) +# Load API key from 1Password via centralized secrets module +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "scripts")) +from secrets import get_secret -API_KEY = os.getenv("ELEVENLABS_API_KEY") +API_KEY = get_secret("ELEVENLABS_API_KEY") BASE_URL = "https://api.elevenlabs.io/v1" # Curated voices mapping (name -> ID) @@ -171,7 +167,7 @@ def generate_audio( if not API_KEY: return { "success": False, - "error": "ELEVENLABS_API_KEY not set. Add to ~/Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env" + "error": "ELEVENLABS_API_KEY not available. Ensure 1Password CLI is configured. See docs/1password-setup.md" } voice_id = get_voice_id(voice) diff --git a/skills/elevenlabs-tts/scripts/list_voices.py b/skills/elevenlabs-tts/scripts/list_voices.py index 3da1ff1..9cf2582 100755 --- a/skills/elevenlabs-tts/scripts/list_voices.py +++ b/skills/elevenlabs-tts/scripts/list_voices.py @@ -3,7 +3,6 @@ # requires-python = ">=3.11" # dependencies = [ # "httpx", -# "python-dotenv", # ] # /// """ @@ -17,19 +16,16 @@ import argparse import json -import os import sys from pathlib import Path import httpx -from dotenv import load_dotenv -# Load secrets -SECRETS_PATH = Path.home() / "Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env" -if SECRETS_PATH.exists(): - load_dotenv(SECRETS_PATH) +# Load API key from 1Password via centralized secrets module +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "scripts")) +from secrets import get_secret -API_KEY = os.getenv("ELEVENLABS_API_KEY") +API_KEY = get_secret("ELEVENLABS_API_KEY") BASE_URL = "https://api.elevenlabs.io/v2" @@ -38,7 +34,7 @@ def list_voices(show_all: bool = False) -> dict: if not API_KEY: return { "success": False, - "error": "ELEVENLABS_API_KEY not set. Add to ~/Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env" + "error": "ELEVENLABS_API_KEY not available. Ensure 1Password CLI is configured. See docs/1password-setup.md" } url = f"{BASE_URL}/voices" diff --git a/skills/freshservice-manager/scripts/add_note.js b/skills/freshservice-manager/scripts/add_note.js index 9a3ed1f..d5504ae 100644 --- a/skills/freshservice-manager/scripts/add_note.js +++ b/skills/freshservice-manager/scripts/add_note.js @@ -4,28 +4,9 @@ // Usage: bun add_note.js '' // JSON: {"body": "Note text", "private": true, "notify_emails": ["email@example.com"]} -import { readFileSync } from 'fs'; -import { homedir } from 'os'; -import { join } from 'path'; +const { SECRETS } = require('../../../scripts/secrets.js'); -function loadEnv() { - const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env'); - const content = readFileSync(envPath, 'utf-8'); - const env = {}; - for (const line of content.split('\n')) { - if (line && !line.startsWith('#')) { - const [key, ...valueParts] = line.split('='); - if (key && valueParts.length) { - env[key.trim()] = valueParts.join('=').trim(); - } - } - } - return env; -} - -const env = loadEnv(); -const domain = env.FRESHSERVICE_DOMAIN; -const apiKey = env.FRESHSERVICE_API_KEY; +const { domain, apiKey } = SECRETS.freshservice; const baseUrl = `https://${domain}/api/v2`; const ticketId = process.argv[2]; diff --git a/skills/freshservice-manager/scripts/create_ticket.js b/skills/freshservice-manager/scripts/create_ticket.js index 441d52b..0157cea 100644 --- a/skills/freshservice-manager/scripts/create_ticket.js +++ b/skills/freshservice-manager/scripts/create_ticket.js @@ -4,28 +4,9 @@ // Usage: bun create_ticket.js '' // JSON: {"subject": "...", "description": "...", "email": "requester@email.com", "priority": 2, "status": 2, "workspace_id": 2} -import { readFileSync } from 'fs'; -import { homedir } from 'os'; -import { join } from 'path'; +const { SECRETS } = require('../../../scripts/secrets.js'); -function loadEnv() { - const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env'); - const content = readFileSync(envPath, 'utf-8'); - const env = {}; - for (const line of content.split('\n')) { - if (line && !line.startsWith('#')) { - const [key, ...valueParts] = line.split('='); - if (key && valueParts.length) { - env[key.trim()] = valueParts.join('=').trim(); - } - } - } - return env; -} - -const env = loadEnv(); -const domain = env.FRESHSERVICE_DOMAIN; -const apiKey = env.FRESHSERVICE_API_KEY; +const { domain, apiKey } = SECRETS.freshservice; const baseUrl = `https://${domain}/api/v2`; // Status values: 2=Open, 3=Pending, 4=Resolved, 5=Closed diff --git a/skills/freshservice-manager/scripts/get_agent.js b/skills/freshservice-manager/scripts/get_agent.js index cb3ca4a..30f5d12 100644 --- a/skills/freshservice-manager/scripts/get_agent.js +++ b/skills/freshservice-manager/scripts/get_agent.js @@ -4,37 +4,12 @@ // Usage: bun get_agent.js [email] // If no email provided, returns current agent (API key owner) -import { readFileSync } from 'fs'; -import { homedir } from 'os'; -import { join } from 'path'; +const { SECRETS } = require('../../../scripts/secrets.js'); -// Load environment from iCloud secrets -function loadEnv() { - const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env'); - const content = readFileSync(envPath, 'utf-8'); - const env = {}; - for (const line of content.split('\n')) { - if (line && !line.startsWith('#')) { - const [key, ...valueParts] = line.split('='); - if (key && valueParts.length) { - env[key.trim()] = valueParts.join('=').trim(); - } - } - } - return env; -} - -const env = loadEnv(); -const domain = env.FRESHSERVICE_DOMAIN; -const apiKey = env.FRESHSERVICE_API_KEY; - -if (!domain || !apiKey) { - console.error(JSON.stringify({ error: 'Missing FRESHSERVICE_DOMAIN or FRESHSERVICE_API_KEY in .env' })); - process.exit(1); -} +const { domain, apiKey } = SECRETS.freshservice; +const baseUrl = `https://${domain}/api/v2`; const email = process.argv[2]; -const baseUrl = `https://${domain}/api/v2`; async function getAgents(email) { let url = `${baseUrl}/agents`; diff --git a/skills/freshservice-manager/scripts/get_approvals.js b/skills/freshservice-manager/scripts/get_approvals.js index 2875518..7fee0e5 100644 --- a/skills/freshservice-manager/scripts/get_approvals.js +++ b/skills/freshservice-manager/scripts/get_approvals.js @@ -4,28 +4,9 @@ // Usage: bun get_approvals.js [status] // Status: requested (pending), approved, rejected, cancelled -import { readFileSync } from 'fs'; -import { homedir } from 'os'; -import { join } from 'path'; +const { SECRETS } = require('../../../scripts/secrets.js'); -function loadEnv() { - const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env'); - const content = readFileSync(envPath, 'utf-8'); - const env = {}; - for (const line of content.split('\n')) { - if (line && !line.startsWith('#')) { - const [key, ...valueParts] = line.split('='); - if (key && valueParts.length) { - env[key.trim()] = valueParts.join('=').trim(); - } - } - } - return env; -} - -const env = loadEnv(); -const domain = env.FRESHSERVICE_DOMAIN; -const apiKey = env.FRESHSERVICE_API_KEY; +const { domain, apiKey } = SECRETS.freshservice; const baseUrl = `https://${domain}/api/v2`; // Agent ID for Kris Hagel diff --git a/skills/freshservice-manager/scripts/get_daily_summary.js b/skills/freshservice-manager/scripts/get_daily_summary.js index 41eaf8e..25e7d0c 100644 --- a/skills/freshservice-manager/scripts/get_daily_summary.js +++ b/skills/freshservice-manager/scripts/get_daily_summary.js @@ -4,28 +4,9 @@ // Usage: bun get_daily_summary.js [date] // Date: "yesterday", "today", or specific date like "2025-11-20" -import { readFileSync } from 'fs'; -import { homedir } from 'os'; -import { join } from 'path'; - -function loadEnv() { - const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env'); - const content = readFileSync(envPath, 'utf-8'); - const env = {}; - for (const line of content.split('\n')) { - if (line && !line.startsWith('#')) { - const [key, ...valueParts] = line.split('='); - if (key && valueParts.length) { - env[key.trim()] = valueParts.join('=').trim(); - } - } - } - return env; -} +const { SECRETS } = require('../../../scripts/secrets.js'); -const env = loadEnv(); -const domain = env.FRESHSERVICE_DOMAIN; -const apiKey = env.FRESHSERVICE_API_KEY; +const { domain, apiKey } = SECRETS.freshservice; const baseUrl = `https://${domain}/api/v2`; // Parse date argument diff --git a/skills/freshservice-manager/scripts/get_service_request.js b/skills/freshservice-manager/scripts/get_service_request.js index ded6a85..1199ec3 100644 --- a/skills/freshservice-manager/scripts/get_service_request.js +++ b/skills/freshservice-manager/scripts/get_service_request.js @@ -3,28 +3,9 @@ // Get service request details including form data // Usage: bun get_service_request.js -import { readFileSync } from 'fs'; -import { homedir } from 'os'; -import { join } from 'path'; - -function loadEnv() { - const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env'); - const content = readFileSync(envPath, 'utf-8'); - const env = {}; - for (const line of content.split('\n')) { - if (line && !line.startsWith('#')) { - const [key, ...valueParts] = line.split('='); - if (key && valueParts.length) { - env[key.trim()] = valueParts.join('=').trim(); - } - } - } - return env; -} +const { SECRETS } = require('../../../scripts/secrets.js'); -const env = loadEnv(); -const domain = env.FRESHSERVICE_DOMAIN; -const apiKey = env.FRESHSERVICE_API_KEY; +const { domain, apiKey } = SECRETS.freshservice; const baseUrl = `https://${domain}/api/v2`; const ticketId = process.argv[2]; diff --git a/skills/freshservice-manager/scripts/get_ticket.js b/skills/freshservice-manager/scripts/get_ticket.js index f14e2f7..52605e2 100644 --- a/skills/freshservice-manager/scripts/get_ticket.js +++ b/skills/freshservice-manager/scripts/get_ticket.js @@ -4,28 +4,9 @@ // Usage: bun get_ticket.js [include] // Include options: conversations, requester, problem, stats, assets, change, related_tickets -import { readFileSync } from 'fs'; -import { homedir } from 'os'; -import { join } from 'path'; +const { SECRETS } = require('../../../scripts/secrets.js'); -function loadEnv() { - const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env'); - const content = readFileSync(envPath, 'utf-8'); - const env = {}; - for (const line of content.split('\n')) { - if (line && !line.startsWith('#')) { - const [key, ...valueParts] = line.split('='); - if (key && valueParts.length) { - env[key.trim()] = valueParts.join('=').trim(); - } - } - } - return env; -} - -const env = loadEnv(); -const domain = env.FRESHSERVICE_DOMAIN; -const apiKey = env.FRESHSERVICE_API_KEY; +const { domain, apiKey } = SECRETS.freshservice; const baseUrl = `https://${domain}/api/v2`; const ticketId = process.argv[2]; diff --git a/skills/freshservice-manager/scripts/get_weekly_summary.js b/skills/freshservice-manager/scripts/get_weekly_summary.js index 2f629f3..edd119a 100644 --- a/skills/freshservice-manager/scripts/get_weekly_summary.js +++ b/skills/freshservice-manager/scripts/get_weekly_summary.js @@ -4,28 +4,9 @@ // Usage: bun get_weekly_summary.js [weeks_ago] // weeks_ago: 0 = this week (default), 1 = last week, etc. -import { readFileSync } from 'fs'; -import { homedir } from 'os'; -import { join } from 'path'; - -function loadEnv() { - const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env'); - const content = readFileSync(envPath, 'utf-8'); - const env = {}; - for (const line of content.split('\n')) { - if (line && !line.startsWith('#')) { - const [key, ...valueParts] = line.split('='); - if (key && valueParts.length) { - env[key.trim()] = valueParts.join('=').trim(); - } - } - } - return env; -} +const { SECRETS } = require('../../../scripts/secrets.js'); -const env = loadEnv(); -const domain = env.FRESHSERVICE_DOMAIN; -const apiKey = env.FRESHSERVICE_API_KEY; +const { domain, apiKey } = SECRETS.freshservice; const baseUrl = `https://${domain}/api/v2`; // Get week date range (Monday to Sunday) diff --git a/skills/freshservice-manager/scripts/get_workspaces.js b/skills/freshservice-manager/scripts/get_workspaces.js index 8ac922c..c590a32 100644 --- a/skills/freshservice-manager/scripts/get_workspaces.js +++ b/skills/freshservice-manager/scripts/get_workspaces.js @@ -4,28 +4,9 @@ // Usage: bun get_workspaces.js [workspace_id] // If no ID provided, gets all workspaces the agent has access to -import { readFileSync } from 'fs'; -import { homedir } from 'os'; -import { join } from 'path'; +const { SECRETS } = require('../../../scripts/secrets.js'); -function loadEnv() { - const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env'); - const content = readFileSync(envPath, 'utf-8'); - const env = {}; - for (const line of content.split('\n')) { - if (line && !line.startsWith('#')) { - const [key, ...valueParts] = line.split('='); - if (key && valueParts.length) { - env[key.trim()] = valueParts.join('=').trim(); - } - } - } - return env; -} - -const env = loadEnv(); -const domain = env.FRESHSERVICE_DOMAIN; -const apiKey = env.FRESHSERVICE_API_KEY; +const { domain, apiKey } = SECRETS.freshservice; const baseUrl = `https://${domain}/api/v2`; async function getWorkspace(id) { diff --git a/skills/freshservice-manager/scripts/list_agents.js b/skills/freshservice-manager/scripts/list_agents.js index 6b554f4..6a720a4 100644 --- a/skills/freshservice-manager/scripts/list_agents.js +++ b/skills/freshservice-manager/scripts/list_agents.js @@ -4,28 +4,9 @@ // Usage: bun list_agents.js [query] // Query can be first name, last name, or email to filter -import { readFileSync } from 'fs'; -import { homedir } from 'os'; -import { join } from 'path'; +const { SECRETS } = require('../../../scripts/secrets.js'); -function loadEnv() { - const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env'); - const content = readFileSync(envPath, 'utf-8'); - const env = {}; - for (const line of content.split('\n')) { - if (line && !line.startsWith('#')) { - const [key, ...valueParts] = line.split('='); - if (key && valueParts.length) { - env[key.trim()] = valueParts.join('=').trim(); - } - } - } - return env; -} - -const env = loadEnv(); -const domain = env.FRESHSERVICE_DOMAIN; -const apiKey = env.FRESHSERVICE_API_KEY; +const { domain, apiKey } = SECRETS.freshservice; const baseUrl = `https://${domain}/api/v2`; const query = process.argv[2]?.toLowerCase(); diff --git a/skills/freshservice-manager/scripts/list_tickets.js b/skills/freshservice-manager/scripts/list_tickets.js index f37e516..4a8a3cb 100644 --- a/skills/freshservice-manager/scripts/list_tickets.js +++ b/skills/freshservice-manager/scripts/list_tickets.js @@ -4,28 +4,9 @@ // Usage: bun list_tickets.js [options] // Options passed as JSON: {"workspace_id": 2, "filter": "open", "agent_id": 123, "per_page": 30} -import { readFileSync } from 'fs'; -import { homedir } from 'os'; -import { join } from 'path'; - -function loadEnv() { - const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env'); - const content = readFileSync(envPath, 'utf-8'); - const env = {}; - for (const line of content.split('\n')) { - if (line && !line.startsWith('#')) { - const [key, ...valueParts] = line.split('='); - if (key && valueParts.length) { - env[key.trim()] = valueParts.join('=').trim(); - } - } - } - return env; -} +const { SECRETS } = require('../../../scripts/secrets.js'); -const env = loadEnv(); -const domain = env.FRESHSERVICE_DOMAIN; -const apiKey = env.FRESHSERVICE_API_KEY; +const { domain, apiKey } = SECRETS.freshservice; const baseUrl = `https://${domain}/api/v2`; // Parse options diff --git a/skills/freshservice-manager/scripts/search_tickets.js b/skills/freshservice-manager/scripts/search_tickets.js index b4a7114..bba2c8c 100644 --- a/skills/freshservice-manager/scripts/search_tickets.js +++ b/skills/freshservice-manager/scripts/search_tickets.js @@ -4,28 +4,9 @@ // Usage: bun search_tickets.js '' [workspace_id] // Query examples: "responder_id:123", "status:2 AND priority:3", "agent_id:123" -import { readFileSync } from 'fs'; -import { homedir } from 'os'; -import { join } from 'path'; +const { SECRETS } = require('../../../scripts/secrets.js'); -function loadEnv() { - const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env'); - const content = readFileSync(envPath, 'utf-8'); - const env = {}; - for (const line of content.split('\n')) { - if (line && !line.startsWith('#')) { - const [key, ...valueParts] = line.split('='); - if (key && valueParts.length) { - env[key.trim()] = valueParts.join('=').trim(); - } - } - } - return env; -} - -const env = loadEnv(); -const domain = env.FRESHSERVICE_DOMAIN; -const apiKey = env.FRESHSERVICE_API_KEY; +const { domain, apiKey } = SECRETS.freshservice; const baseUrl = `https://${domain}/api/v2`; const query = process.argv[2]; diff --git a/skills/freshservice-manager/scripts/update_ticket.js b/skills/freshservice-manager/scripts/update_ticket.js index 48c38ec..85b6e30 100644 --- a/skills/freshservice-manager/scripts/update_ticket.js +++ b/skills/freshservice-manager/scripts/update_ticket.js @@ -4,28 +4,9 @@ // Usage: bun update_ticket.js '' // JSON: {"status": 4, "priority": 3, "responder_id": 123, "group_id": 456} -import { readFileSync } from 'fs'; -import { homedir } from 'os'; -import { join } from 'path'; +const { SECRETS } = require('../../../scripts/secrets.js'); -function loadEnv() { - const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env'); - const content = readFileSync(envPath, 'utf-8'); - const env = {}; - for (const line of content.split('\n')) { - if (line && !line.startsWith('#')) { - const [key, ...valueParts] = line.split('='); - if (key && valueParts.length) { - env[key.trim()] = valueParts.join('=').trim(); - } - } - } - return env; -} - -const env = loadEnv(); -const domain = env.FRESHSERVICE_DOMAIN; -const apiKey = env.FRESHSERVICE_API_KEY; +const { domain, apiKey } = SECRETS.freshservice; const baseUrl = `https://${domain}/api/v2`; // Status: 2=Open, 3=Pending, 4=Resolved, 5=Closed diff --git a/skills/google-workspace/auth/oauth_setup.js b/skills/google-workspace/auth/oauth_setup.js index 7165b3c..cea6c8a 100644 --- a/skills/google-workspace/auth/oauth_setup.js +++ b/skills/google-workspace/auth/oauth_setup.js @@ -24,13 +24,8 @@ const openModule = require('open'); const open = openModule.default || openModule; const path = require('path'); -// Load environment variables from iCloud secrets -const os = require('os'); -const ENV_PATH = path.join( - os.homedir(), - 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env' -); -require('dotenv').config({ path: ENV_PATH }); +// Load secrets from 1Password via centralized secrets module +const { SECRETS } = require('../../../scripts/secrets.js'); // Configuration const REDIRECT_PORT = process.env.OAUTH_REDIRECT_PORT || 3000; @@ -93,18 +88,17 @@ async function main() { process.exit(1); } - // Load client credentials from environment - const client_id = process.env.GOOGLE_CLIENT_ID; - const client_secret = process.env.GOOGLE_CLIENT_SECRET; + // Load client credentials from 1Password + const { clientId: client_id, clientSecret: client_secret } = SECRETS.google; if (!client_id || !client_secret) { console.error(JSON.stringify({ - error: 'Missing credentials in .env', + error: 'Missing Google credentials in 1Password', instructions: [ '1. Go to Google Cloud Console', '2. Create OAuth 2.0 Client ID (Desktop app)', - '3. Copy client ID and secret to skills/google-workspace/.env', - '4. See .env.example for format' + '3. Store client ID and secret in 1Password vault "Geoffrey/Google-Workspace"', + '4. See docs/1password-setup.md for details' ] })); process.exit(1); diff --git a/skills/google-workspace/auth/token_manager.js b/skills/google-workspace/auth/token_manager.js index 42da75e..d67ad21 100644 --- a/skills/google-workspace/auth/token_manager.js +++ b/skills/google-workspace/auth/token_manager.js @@ -20,15 +20,9 @@ const { execSync } = require('child_process'); const { google } = require('googleapis'); -const path = require('path'); -// Load environment variables from iCloud secrets -const os = require('os'); -const ENV_PATH = path.join( - os.homedir(), - 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env' -); -require('dotenv').config({ path: ENV_PATH }); +// Load secrets from 1Password via centralized secrets module +const { SECRETS } = require('../../../scripts/secrets.js'); const SERVICE_NAME = 'geoffrey-google-workspace'; @@ -86,12 +80,11 @@ async function refreshTokens(account) { throw new Error('No refresh token available'); } - // Load credentials from environment - const client_id = process.env.GOOGLE_CLIENT_ID; - const client_secret = process.env.GOOGLE_CLIENT_SECRET; + // Load credentials from 1Password + const { clientId: client_id, clientSecret: client_secret } = SECRETS.google; if (!client_id || !client_secret) { - throw new Error('Missing GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET in .env'); + throw new Error('Missing Google credentials in 1Password. See docs/1password-setup.md'); } // Create OAuth2 client and refresh @@ -154,11 +147,10 @@ function deleteTokens(account) { async function getAuthClient(account) { const tokens = getTokens(account); - const client_id = process.env.GOOGLE_CLIENT_ID; - const client_secret = process.env.GOOGLE_CLIENT_SECRET; + const { clientId: client_id, clientSecret: client_secret } = SECRETS.google; if (!client_id || !client_secret) { - throw new Error('Missing GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET in .env'); + throw new Error('Missing Google credentials in 1Password. See docs/1password-setup.md'); } const oauth2Client = new google.auth.OAuth2(client_id, client_secret); diff --git a/skills/google-workspace/package.json b/skills/google-workspace/package.json index d56088c..ae22ee1 100644 --- a/skills/google-workspace/package.json +++ b/skills/google-workspace/package.json @@ -10,7 +10,6 @@ "list:accounts": "bun auth/token_manager.js list" }, "dependencies": { - "dotenv": "^16.3.1", "googleapis": "^128.0.0", "open": "^9.1.0" } diff --git a/skills/image-gen/scripts/compose.py b/skills/image-gen/scripts/compose.py index 494fe4a..7b7314c 100644 --- a/skills/image-gen/scripts/compose.py +++ b/skills/image-gen/scripts/compose.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # /// script -# dependencies = ["google-genai", "pillow", "python-dotenv"] +# dependencies = ["google-genai", "pillow"] # /// """ Compose images using multiple reference images with Google's Nano Banana Pro. @@ -19,19 +19,16 @@ """ import sys -import os import json from pathlib import Path -from dotenv import load_dotenv from google import genai from google.genai.types import GenerateContentConfig, Part from PIL import Image -# Load API key from Geoffrey secrets -SECRETS_PATH = Path.home() / "Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env" -if SECRETS_PATH.exists(): - load_dotenv(SECRETS_PATH) +# Load API key from 1Password via centralized secrets module +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "scripts")) +from secrets import require_secret def main(): @@ -62,11 +59,7 @@ def main(): sys.exit(1) # Initialize client - api_key = os.environ.get("GEMINI_API_KEY") - if not api_key: - print("Error: GEMINI_API_KEY environment variable not set") - sys.exit(1) - + api_key = require_secret("GEMINI_API_KEY") client = genai.Client(api_key=api_key) # Load reference images diff --git a/skills/image-gen/scripts/edit.py b/skills/image-gen/scripts/edit.py index 238a94b..4a70325 100644 --- a/skills/image-gen/scripts/edit.py +++ b/skills/image-gen/scripts/edit.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # /// script -# dependencies = ["google-genai", "pillow", "python-dotenv"] +# dependencies = ["google-genai", "pillow"] # /// """ Edit existing images using Google's Nano Banana Pro. @@ -19,19 +19,16 @@ """ import sys -import os import json from pathlib import Path -from dotenv import load_dotenv from google import genai from google.genai.types import GenerateContentConfig, Part from PIL import Image -# Load API key from Geoffrey secrets -SECRETS_PATH = Path.home() / "Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env" -if SECRETS_PATH.exists(): - load_dotenv(SECRETS_PATH) +# Load API key from 1Password via centralized secrets module +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "scripts")) +from secrets import require_secret def main(): @@ -49,11 +46,7 @@ def main(): sys.exit(1) # Initialize client - api_key = os.environ.get("GEMINI_API_KEY") - if not api_key: - print("Error: GEMINI_API_KEY environment variable not set") - sys.exit(1) - + api_key = require_secret("GEMINI_API_KEY") client = genai.Client(api_key=api_key) # Load input image diff --git a/skills/image-gen/scripts/generate.py b/skills/image-gen/scripts/generate.py index a6f7f87..11d127b 100644 --- a/skills/image-gen/scripts/generate.py +++ b/skills/image-gen/scripts/generate.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # /// script -# dependencies = ["google-genai", "pillow", "python-dotenv"] +# dependencies = ["google-genai", "pillow"] # /// """ Generate images using Google's Nano Banana Pro (Gemini 3 Pro Image). @@ -25,19 +25,16 @@ """ import sys -import os import json import importlib.util from pathlib import Path -from dotenv import load_dotenv from google import genai from google.genai.types import GenerateContentConfig -# Load API key from Geoffrey secrets -SECRETS_PATH = Path.home() / "Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env" -if SECRETS_PATH.exists(): - load_dotenv(SECRETS_PATH) +# Load API key from 1Password via centralized secrets module +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "scripts")) +from secrets import require_secret # Geoffrey skills directory for brand imports SKILLS_DIR = Path(__file__).parent.parent.parent @@ -217,12 +214,8 @@ def main(): print(f"Valid options: {', '.join(valid_sizes)}") sys.exit(1) - # Initialize client (uses GEMINI_API_KEY env var) - api_key = os.environ.get("GEMINI_API_KEY") - if not api_key: - print("Error: GEMINI_API_KEY environment variable not set") - sys.exit(1) - + # Initialize client + api_key = require_secret("GEMINI_API_KEY") client = genai.Client(api_key=api_key) # Configure generation diff --git a/skills/multi-model-research/scripts/llm_client.py b/skills/multi-model-research/scripts/llm_client.py index 2d72056..047110d 100755 --- a/skills/multi-model-research/scripts/llm_client.py +++ b/skills/multi-model-research/scripts/llm_client.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # /// script -# dependencies = ["httpx>=0.25.0", "python-dotenv>=1.0.0"] +# dependencies = ["httpx>=0.25.0"] # /// """ Universal LLM Client for Multi-Model Research @@ -9,14 +9,16 @@ with provider-specific formatting handled internally. """ -import os +import sys import asyncio import httpx +from pathlib import Path from typing import Optional from dataclasses import dataclass -from dotenv import load_dotenv -load_dotenv() +# Add scripts directory to path for secrets module import +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "scripts")) +from secrets import get_secret @dataclass @@ -35,10 +37,10 @@ class MultiModelClient: def __init__(self, timeout: int = 120): self.keys = { - "openai": os.getenv("OPENAI_API_KEY"), - "google": os.getenv("GEMINI_API_KEY"), - "perplexity": os.getenv("PERPLEXITY_API_KEY"), - "xai": os.getenv("XAI_API_KEY"), + "openai": get_secret("OPENAI_API_KEY"), + "google": get_secret("GEMINI_API_KEY"), + "perplexity": get_secret("PERPLEXITY_API_KEY"), + "xai": get_secret("XAI_API_KEY"), } self.timeout = timeout diff --git a/skills/research/scripts/orchestrator.js b/skills/research/scripts/orchestrator.js index 5b49baa..eee7ff2 100644 --- a/skills/research/scripts/orchestrator.js +++ b/skills/research/scripts/orchestrator.js @@ -17,28 +17,7 @@ * Usage: bun orchestrator.js --query "Research topic" [--domain travel] */ -import fs from 'fs'; -import path from 'path'; - -const SECRETS_PATH = path.join( - process.env.HOME, - 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env' -); - -// Load environment variables -function loadEnv() { - if (fs.existsSync(SECRETS_PATH)) { - const content = fs.readFileSync(SECRETS_PATH, 'utf-8'); - for (const line of content.split('\n')) { - const [key, ...valueParts] = line.split('='); - if (key && valueParts.length) { - process.env[key.trim()] = valueParts.join('=').trim(); - } - } - } -} - -loadEnv(); +// Secrets are loaded by context-loader.js which imports from centralized secrets module /** * Phase 1: Query Decomposition From 83a798e01d4d46598676739392d8d38b198c404e Mon Sep 17 00:00:00 2001 From: Kris Hagel Date: Mon, 26 Jan 2026 06:40:43 -0800 Subject: [PATCH 2/2] v0.9.0: Address PR feedback - version bump and security fix Changes per PR #11 review: 1. Bump version to 0.9.0 in all 5 required locations - .claude-plugin/plugin.json - .claude-plugin/marketplace.json (metadata + plugins array) - package.json - README.md (badge + 2 version references) 2. Add CHANGELOG.md entry for v0.9.0 - Document 1Password CLI integration - List all changed skills (26 files) - Note security improvements 3. Fix command injection vulnerability in scripts/secrets.js - Replace execSync with spawnSync for shell safety - Use array syntax for command arguments - Prevents potential injection via secret references All tests passing with updated code. --- .claude-plugin/marketplace.json | 4 ++-- .claude-plugin/plugin.json | 2 +- CHANGELOG.md | 27 +++++++++++++++++++++++++++ README.md | 6 +++--- package.json | 2 +- scripts/secrets.js | 16 +++++++++------- 6 files changed, 43 insertions(+), 14 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 7d2111e..fbea9a3 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,7 +6,7 @@ }, "metadata": { "description": "Personal AI infrastructure - learning-enabled assistant for work, travel, and personal tasks", - "version": "0.8.0" + "version": "0.9.0" }, "pluginRoot": ".", "plugins": [ @@ -14,7 +14,7 @@ "name": "geoffrey", "source": "./", "description": "Personal AI infrastructure with learning-enabled assistance for work, travel, and personal tasks", - "version": "0.8.0", + "version": "0.9.0", "category": "productivity", "keywords": [ "personal-ai", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 88f0637..2d91058 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "geoffrey", - "version": "0.8.0", + "version": "0.9.0", "description": "Personal AI infrastructure with learning-enabled assistance for work, travel, and personal tasks", "author": { "name": "Kris Hagel", diff --git a/CHANGELOG.md b/CHANGELOG.md index b3c4bb4..b8d2a00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,33 @@ All notable changes to Geoffrey will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.0] - 2026-01-26 + +### Added +- **1Password CLI integration** for secure secrets management + - Centralized secrets modules (`scripts/secrets.js`, `scripts/secrets.py`) + - Setup documentation at `docs/1password-setup.md` + - Support for 10 secrets across 6 skills + +### Changed +- **All skills now load secrets from 1Password** instead of `.env` file + - freshservice-manager: 13 scripts updated + - elevenlabs-tts: 2 scripts updated + - image-gen: 3 scripts updated + - multi-model-research: 1 script updated + - research: 1 script updated + - google-workspace: 2 auth scripts + package.json updated + +### Removed +- `python-dotenv` dependency from Python scripts +- `dotenv` dependency from google-workspace package.json +- iCloud `.env` file requirement + +### Security +- Secrets no longer stored as plaintext in iCloud +- Leverages 1Password's security and biometric authentication +- Secrets never written to disk by scripts + ## [0.8.0] - 2026-01-24 ### Added diff --git a/README.md b/README.md index be7b6d7..e3301c2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Geoffrey - Personal AI Infrastructure -[![Version](https://img.shields.io/badge/version-0.8.0-blue.svg)](https://github.com/krishagel/geoffrey/releases) +[![Version](https://img.shields.io/badge/version-0.9.0-blue.svg)](https://github.com/krishagel/geoffrey/releases) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![Claude Code](https://img.shields.io/badge/Claude_Code-plugin-purple.svg)](https://claude.com/code) @@ -20,7 +20,7 @@ claude plugin install geoffrey@geoffrey ## Current Status: Phase 2 (Core Skills) -**Version:** 0.8.0 +**Version:** 0.9.0 **Status:** In Active Development ### What Works Now @@ -396,5 +396,5 @@ MIT License - see [LICENSE](LICENSE) for details. **Built with ❤️ using Claude Code** -*Version: 0.8.0 | Phase 2 (Core Skills)* +*Version: 0.9.0 | Phase 2 (Core Skills)* *Last updated: January 24, 2026* diff --git a/package.json b/package.json index 724b819..71f04ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "geoffrey", - "version": "0.8.0", + "version": "0.9.0", "description": "Personal AI infrastructure with learning-enabled assistance for work, travel, and personal tasks", "type": "module", "author": { diff --git a/scripts/secrets.js b/scripts/secrets.js index dc4b258..2207b8c 100644 --- a/scripts/secrets.js +++ b/scripts/secrets.js @@ -18,7 +18,7 @@ * 4. See docs/1password-setup.md for detailed setup guide */ -const { execSync } = require('child_process'); +const { spawnSync } = require('child_process'); // Map environment variable names to 1Password secret references // Format: op://vault/item/field @@ -58,8 +58,8 @@ function is1PasswordAvailable() { if (_1pChecked) return _1pAvailable; try { - execSync('op account list', { encoding: 'utf8', stdio: 'pipe' }); - _1pAvailable = true; + const result = spawnSync('op', ['account', 'list'], { encoding: 'utf8' }); + _1pAvailable = result.status === 0; } catch { _1pAvailable = false; } @@ -89,12 +89,14 @@ function ensure1Password() { */ function loadFrom1Password(secretRef) { try { - const value = execSync(`op read "${secretRef}"`, { + const result = spawnSync('op', ['read', secretRef], { encoding: 'utf8', - stdio: ['pipe', 'pipe', 'pipe'], timeout: 10000, - }).trim(); - return value; + }); + if (result.status === 0 && result.stdout) { + return result.stdout.trim(); + } + return null; } catch (error) { return null; }