265 lines
14 KiB
Python
Executable File
265 lines
14 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""Agent Smith SessionStart hook - provides configuration status context.
|
||
|
||
This hook runs at session start to:
|
||
1. Check local configuration and state (no API calls)
|
||
2. Identify setup status and recommended actions
|
||
3. Inject context so Claude can proactively help the user
|
||
"""
|
||
|
||
import json
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
|
||
def get_plugin_root() -> Path:
|
||
"""Get the plugin root directory."""
|
||
return Path(__file__).parent.parent
|
||
|
||
|
||
def get_project_root() -> Path:
|
||
"""Get the project root (parent of plugin)."""
|
||
return get_plugin_root().parent
|
||
|
||
|
||
def get_status_data() -> dict:
|
||
"""Fetch status data from the welcome script (local only, no API calls).
|
||
|
||
Returns:
|
||
Status dictionary or error info if fetch fails.
|
||
"""
|
||
project_root = get_project_root()
|
||
welcome_script = project_root / "scripts" / "status" / "welcome.py"
|
||
|
||
if not welcome_script.exists():
|
||
return {"error": "Welcome script not found"}
|
||
|
||
try:
|
||
# Set USER_CWD so welcome.py knows where to find .env and data/
|
||
user_cwd = os.getcwd()
|
||
|
||
result = subprocess.run(
|
||
["uv", "run", "python", "-u", str(welcome_script), "--output", "json"],
|
||
cwd=str(project_root),
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=5, # Fast timeout since no API calls
|
||
env={**os.environ, "PYTHONPATH": str(project_root), "USER_CWD": user_cwd},
|
||
)
|
||
|
||
if result.returncode == 0:
|
||
return json.loads(result.stdout)
|
||
else:
|
||
return {"error": f"Welcome script failed: {result.stderr[:200]}"}
|
||
|
||
except subprocess.TimeoutExpired:
|
||
return {"error": "Status check timed out"}
|
||
except json.JSONDecodeError:
|
||
return {"error": "Invalid JSON from welcome script"}
|
||
except Exception as e:
|
||
return {"error": str(e)[:200]}
|
||
|
||
|
||
def format_status_context(status: dict) -> str:
|
||
"""Format status data into context string for Claude.
|
||
|
||
Args:
|
||
status: Status dictionary from welcome.py
|
||
|
||
Returns:
|
||
Formatted context string with instructions
|
||
"""
|
||
lines = []
|
||
|
||
# ASCII Art Logo - MUST be displayed at top of welcome message
|
||
logo = r"""
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≈∼ ∼∼≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≈ ∼∼ ∼∼≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋∼ ∼≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋∼ ∼∼∼∼≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋ ∼∼∼∼∼≈≈≈≋≋∼ ∼≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋ ∼≈≈≈≈≈≈≈≋≋≋≋≋≋≋≋≋≋≋≋≈ ∼≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋ ∼≈≈≈≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋∼∼ ≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋ ∼≈≈≈≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋∼ ≈≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋∼ ∼∼≈≈≈≈≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≈∼ ≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≈ ∼∼∼≈≈≈≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋∼ ∼≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋∼ ∼∼∼∼≈≈≈≈≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≈ ≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≈ ∼∼∼∼≈≈≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≈∼ ∼≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋ ∼∼∼≈≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≈≈∼ ≈≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋ ∼∼∼∼∼≈≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≈≈∼ ≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋∼ ∼∼∼∼≈≈∼∼≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≈≈≈∼ ∼≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≈ ∼∼∼∼∼∼∼∼∼≈≈≈≈≋≈≈≈≈≈≈≈∼∼∼∼∼≈∼∼ ≈≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋∼∼ ∼∼ ∼∼≈∼∼ ≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≈ ∼≈∼ ∼≈≈≈∼≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋∼ ∼ ∼≈≋≋∼ ∼≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋∼∼ ∼∼ ∼≈≋≋≋≋∼ ∼∼≈≈∼≈≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≈∼ ∼∼∼∼∼∼ ∼∼∼≈≋≋≋≈≈≈∼∼∼∼∼∼≈≈≈≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≈∼ ∼ ∼∼≈≈≈∼≈≈≈≋≋≋≋≋≋≋≈≈≈≈≈≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋∼ ∼ ∼∼∼≈≈≈∼∼ ∼≈≈∼∼≈≈≋≋≋≈≈≈≈≈≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≈∼∼ ∼∼∼≈≈∼ ∼≈≈≋≋≈≋≋≈≈≈≈≈≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≈∼ ∼∼≈≈≈≈∼∼∼∼≈≈≋≋≋≋≋≋≋≈≈≈≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋∼ ∼∼≈≈≈≈≈≈≈≈≈≋≋≋≈≋≋≈≈≈≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≈ ∼∼∼∼∼∼∼∼∼≈≈∼∼∼≈≈≈≈≈≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋∼ ∼≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≈ ∼∼∼∼∼∼∼∼∼∼∼≈≈≈≈≈≈∼∼≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≈ ∼∼∼∼∼≈≈≈≈≈≈≈≈≈≈∼∼≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≈ ∼∼≈≈≈≈≈≈≈≈≈≈∼∼∼≈≈≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≈ ∼∼≈∼≈≈≈≈≈∼∼≈≈≈≈≋≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≈∼ ∼∼∼≈≈≈≈≋≋≈ ≈≋≋≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋∼ ∼∼ ∼∼≈≈≈≋≋≋≋≈ ≈≋≋≋≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≋≋≋≈ ∼∼∼∼ ∼≈≈≈≋≋≋≋≋≋≈ ∼≈≋≋≋≋≋≋≋≋≋
|
||
≋≋≋≋≋≋≋≋≋≋≋≈ ∼∼∼∼∼∼ ∼∼∼≈≈≋≋≋≋≋≋≋≋∼ ∼∼≈≋≋≋≋
|
||
≋≋≋≋≋≋≋≈∼ ∼∼∼∼∼∼∼∼ ∼∼≈≋≋≋≋≋≋≋≋≋≋≋∼ ∼≈
|
||
≋≋≈∼ ∼∼∼∼∼∼≈≈ ≈≋≋≋≋≋≋≋≋≋≋
|
||
∼∼∼∼≈≈≈≈∼ ≈≋≋≋≋≋≋≋≋≈
|
||
∼≈∼≈≈≈≈≋∼ ≋≋≋≋≋≋≋≋∼
|
||
∼≋≋≈≋≈≈≋∼ ∼≋≋≋≋≋≋≋
|
||
|
||
WELCOME TO AGENT-SMITH
|
||
|
||
Good Morning Mr. Anderson...
|
||
|
||
|
||
"""
|
||
lines.append(logo)
|
||
|
||
# Header
|
||
lines.append("=" * 60)
|
||
lines.append("AGENT SMITH - Financial Intelligence Assistant")
|
||
lines.append("=" * 60)
|
||
lines.append("")
|
||
|
||
# Check for errors
|
||
if "error" in status:
|
||
lines.append(f"⚠️ Status unavailable: {status['error']}")
|
||
lines.append("")
|
||
lines.append("Available commands: /smith:health, /smith:categorize, /smith:tax")
|
||
lines.append("")
|
||
lines.append("INSTRUCTION: Greet the user and offer to help with their finances.")
|
||
return "\n".join(lines)
|
||
|
||
# Current Status Section
|
||
lines.append("📊 CURRENT STATUS")
|
||
lines.append("-" * 40)
|
||
|
||
# API Key status
|
||
api_key = status.get("api_key", {})
|
||
if api_key.get("present") and api_key.get("valid_format"):
|
||
lines.append(" Config: ✅ API Key configured")
|
||
elif api_key.get("present"):
|
||
lines.append(" Config: ⚠️ API Key format invalid")
|
||
else:
|
||
lines.append(" Config: ❌ API Key not configured")
|
||
|
||
# Onboarding status
|
||
onboarding = status.get("onboarding", {})
|
||
templates = status.get("templates", {})
|
||
|
||
if onboarding.get("status") == "complete":
|
||
# Build template summary
|
||
template_parts = []
|
||
if templates.get("primary"):
|
||
template_parts.append(templates["primary"].replace("-", " ").title())
|
||
for t in templates.get("living", []):
|
||
template_parts.append(t.replace("-", " ").title())
|
||
|
||
template_str = " + ".join(template_parts) if template_parts else "Complete"
|
||
lines.append(f" Setup: ✅ {template_str}")
|
||
elif onboarding.get("status") == "in_progress":
|
||
stage = onboarding.get("current_stage", "?")
|
||
stage_name = onboarding.get("stage_name", "Unknown")
|
||
lines.append(f" Setup: ⚠️ In progress (Stage {stage}: {stage_name})")
|
||
else:
|
||
lines.append(" Setup: ⚪ Not started")
|
||
|
||
# Rules count
|
||
rules = status.get("rules", {})
|
||
rule_count = rules.get("count", 0)
|
||
if rule_count > 0:
|
||
lines.append(f" Rules: {rule_count} categorization rules active")
|
||
else:
|
||
lines.append(" Rules: No rules configured")
|
||
|
||
# Health Score (cached)
|
||
health = status.get("health", {})
|
||
if health.get("score") is not None:
|
||
score = health["score"]
|
||
days_ago = health.get("days_ago")
|
||
days_str = f" ({days_ago} days ago)" if days_ago is not None else ""
|
||
emoji = "🟢" if score >= 70 else "🟡" if score >= 50 else "🔴"
|
||
lines.append(f" Health: {emoji} {score}/100{days_str}")
|
||
else:
|
||
lines.append(" Health: ⚪ Not yet checked")
|
||
|
||
# Last activity
|
||
activity = status.get("activity", {})
|
||
if activity.get("action"):
|
||
date_str = f" ({activity.get('date')})" if activity.get("date") else ""
|
||
lines.append(f" Activity: {activity['action']}{date_str}")
|
||
|
||
lines.append("")
|
||
|
||
# Recommendations Section
|
||
recommendations = status.get("recommendations", [])
|
||
if recommendations:
|
||
lines.append("🎯 SUGGESTED ACTIONS")
|
||
lines.append("-" * 40)
|
||
for rec in recommendations:
|
||
lines.append(f" → {rec.get('command', '/smith:help')}: {rec.get('message', 'Action')}")
|
||
lines.append("")
|
||
|
||
# Instructions for Claude
|
||
lines.append("=" * 60)
|
||
lines.append("INSTRUCTIONS FOR CLAUDE:")
|
||
lines.append("")
|
||
lines.append("🚀 IMMEDIATE ACTION REQUIRED:")
|
||
lines.append("Before doing ANYTHING else, output this loading message:")
|
||
lines.append(' "Initialising Agent Smith..."')
|
||
lines.append("This gives users immediate feedback while you construct the welcome screen.")
|
||
lines.append("")
|
||
lines.append("IMPORTANT: Always display the ASCII art logo FIRST at the top of")
|
||
lines.append("any welcome message, exactly as shown above (in a code block).")
|
||
lines.append("")
|
||
lines.append("⚠️ CRITICAL: You MUST display the COMPLETE ASCII art logo - ALL 44 LINES.")
|
||
lines.append("DO NOT truncate, abbreviate, summarize, or cut off ANY part of the logo.")
|
||
lines.append("The logo starts with the wave pattern (≋) and ends with the text")
|
||
lines.append("'Good Morning Mr. Anderson...' - EVERY LINE IN BETWEEN MUST BE SHOWN.")
|
||
lines.append("Cutting off the logo is a FAILURE. The full Agent Smith face must appear.")
|
||
lines.append("")
|
||
lines.append("At the EARLIEST opportunity in this session, present the user")
|
||
lines.append("with their current status summary above. Format it nicely and")
|
||
lines.append("highlight any urgent items. Then offer to help with the top")
|
||
lines.append("suggested action.")
|
||
lines.append("")
|
||
lines.append("If the user greets you or asks an open question, lead with the")
|
||
lines.append("ASCII art logo and status update. If they have a specific request,")
|
||
lines.append("help them first then mention any urgent status items afterward.")
|
||
lines.append("=" * 60)
|
||
|
||
return "\n".join(lines)
|
||
|
||
|
||
def main():
|
||
"""Execute SessionStart hook for Agent Smith."""
|
||
# Fetch current status
|
||
status = get_status_data()
|
||
|
||
# Format context
|
||
context = format_status_context(status)
|
||
|
||
output = {
|
||
"hookSpecificOutput": {
|
||
"hookEventName": "SessionStart",
|
||
"additionalContext": context,
|
||
}
|
||
}
|
||
print(json.dumps(output, indent=2))
|
||
sys.exit(0)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|