diff --git a/SKILL.md b/SKILL.md index df02e2a..305e4d6 100644 --- a/SKILL.md +++ b/SKILL.md @@ -50,66 +50,64 @@ Common patterns: ## Setup Check -Verify API key configuration exists: +The skill works in three modes based on available API keys: -```bash -if [ ! -f ~/.config/last30days/.env ]; then - echo "SETUP_NEEDED" -else - echo "CONFIGURED" -fi -``` +1. **Full Mode** (both keys): Reddit + X + WebSearch - best results with engagement metrics +2. **Partial Mode** (one key): Reddit-only or X-only + WebSearch +3. **Web-Only Mode** (no keys): WebSearch only - still useful, but no engagement metrics -### If SETUP_NEEDED +**API keys are OPTIONAL.** The skill will work without them using WebSearch fallback. -Run NUX flow to configure API keys. Use AskUserQuestion to collect: +### First-Time Setup (Optional but Recommended) -1. **OpenAI API Key** (optional but recommended for Reddit research) -2. **xAI API Key** (optional but recommended for X research) - -Then create the config: +If the user wants to add API keys for better results: ```bash mkdir -p ~/.config/last30days cat > ~/.config/last30days/.env << 'ENVEOF' # last30days API Configuration -# At least one key is required +# Both keys are optional - skill works with WebSearch fallback +# For Reddit research (uses OpenAI's web_search tool) OPENAI_API_KEY= + +# For X/Twitter research (uses xAI's x_search tool) XAI_API_KEY= ENVEOF chmod 600 ~/.config/last30days/.env echo "Config created at ~/.config/last30days/.env" -echo "Please edit it to add your API keys, then run the skill again." +echo "Edit to add your API keys for enhanced research." ``` -**STOP HERE if setup was needed.** +**DO NOT stop if no keys are configured.** Proceed with web-only mode. --- ## Research Execution -**IMPORTANT: Run Reddit/X script IN BACKGROUND first, then WebSearch.** This way both run in parallel. +**IMPORTANT: The script handles API key detection automatically.** Run it and check the output to determine mode. -**Step 1: Display the research banner immediately:** -``` -šŸ” Researching "{TOPIC}" across the last 30 days... - -šŸš€ Deploying research agents in parallel... -ā”œā”€ 🟠 Reddit Agent: Scanning subreddits for gold... -ā”œā”€ šŸ”µ X Agent: Catching the latest takes... -ā”œā”€ 🌐 Web Agent: Crawling blogs, docs & news... -└─ āš–ļø Judge Agent: Standing by to synthesize... -``` - -**Step 2: Start Reddit/X script in background** +**Step 1: Run the research script** ```bash python3 ~/.claude/skills/last30days/scripts/last30days.py "$ARGUMENTS" --emit=compact 2>&1 ``` -Use `run_in_background: true` so it starts immediately and runs while we do WebSearch. -**Step 2: While script runs, do WebSearch** +The script will automatically: +- Detect available API keys +- Show a promo banner if keys are missing (this is intentional marketing) +- Run Reddit/X searches if keys exist +- Signal if WebSearch is needed + +**Step 2: Check the output mode** + +The script output will indicate the mode: +- **"Mode: both"** or **"Mode: reddit-only"** or **"Mode: x-only"**: Script found results, WebSearch is supplementary +- **"Mode: web-only"**: No API keys, Claude must do ALL research via WebSearch + +**Step 3: Do WebSearch** + +For **ALL modes**, do WebSearch to supplement (or provide all data in web-only mode). Choose search queries based on QUERY_TYPE: @@ -240,6 +238,8 @@ KEY PATTERNS I'll use: ``` **THEN - Stats (right before invitation):** + +For **full/partial mode** (has API keys): ``` --- āœ… All agents reported back! @@ -249,6 +249,18 @@ KEY PATTERNS I'll use: └─ Top voices: r/{sub1}, r/{sub2} │ @{handle1}, @{handle2} │ {web_author} on {site} ``` +For **web-only mode** (no API keys): +``` +--- +āœ… Research complete! +ā”œā”€ 🌐 Web: {n} pages │ {domains} +└─ Top sources: {author1} on {site1}, {author2} on {site2} + +šŸ’” Want engagement metrics? Add API keys to ~/.config/last30days/.env + - OPENAI_API_KEY → Reddit (real upvotes & comments) + - XAI_API_KEY → X/Twitter (real likes & reposts) +``` + **LAST - Invitation:** ``` --- @@ -358,6 +370,7 @@ Only do new research if the user explicitly asks about a DIFFERENT topic. After delivering a prompt, end with: +For **full/partial mode**: ``` --- šŸ“š Expert in: {TOPIC} for {TARGET_TOOL} @@ -365,3 +378,14 @@ After delivering a prompt, end with: Want another prompt? Just tell me what you're creating next. ``` + +For **web-only mode**: +``` +--- +šŸ“š Expert in: {TOPIC} for {TARGET_TOOL} +šŸ“Š Based on: {n} web pages from {domains} + +Want another prompt? Just tell me what you're creating next. + +šŸ’” Unlock Reddit & X data: Add API keys to ~/.config/last30days/.env +``` diff --git a/scripts/last30days.py b/scripts/last30days.py index 0a6215d..a979e05 100644 --- a/scripts/last30days.py +++ b/scripts/last30days.py @@ -79,9 +79,16 @@ def run_research( reddit_error = None x_error = None - # Check if WebSearch is needed + # Check if WebSearch is needed (always needed in web-only mode) web_needed = sources in ("all", "web", "reddit-web", "x-web") + # Web-only mode: no API calls needed, Claude handles everything + if sources == "web": + if progress: + progress.start_web_only() + progress.end_web_only() + return reddit_items, x_items, True, raw_openai, raw_xai, raw_reddit_enriched, reddit_error, x_error + # Reddit search via OpenAI if sources in ("both", "reddit", "all", "reddit-web"): if progress: @@ -263,9 +270,16 @@ def main(): # Get date range from_date, to_date = dates.get_date_range(30) + # Check what keys are missing for promo messaging + missing_keys = env.get_missing_keys(config) + # Initialize progress display progress = ui.ProgressDisplay(args.topic, show_banner=True) + # Show promo for missing keys BEFORE research + if missing_keys != 'none': + progress.show_promo(missing_keys) + # Select models if args.mock: # Use mock models @@ -361,10 +375,13 @@ def main(): render.write_outputs(report, raw_openai, raw_xai, raw_reddit_enriched) # Show completion - progress.show_complete(len(deduped_reddit), len(deduped_x)) + if sources == "web": + progress.show_web_only_complete() + else: + progress.show_complete(len(deduped_reddit), len(deduped_x)) # Output result - output_result(report, args.emit, web_needed, args.topic, from_date, to_date) + output_result(report, args.emit, web_needed, args.topic, from_date, to_date, missing_keys) def output_result( @@ -374,10 +391,11 @@ def output_result( topic: str = "", from_date: str = "", to_date: str = "", + missing_keys: str = "none", ): """Output the result based on emit mode.""" if emit_mode == "compact": - print(render.render_compact(report)) + print(render.render_compact(report, missing_keys=missing_keys)) elif emit_mode == "json": print(json.dumps(report.to_dict(), indent=2)) elif emit_mode == "md": diff --git a/scripts/lib/env.py b/scripts/lib/env.py index 4e84a2a..810e025 100644 --- a/scripts/lib/env.py +++ b/scripts/lib/env.py @@ -72,6 +72,24 @@ def get_available_sources(config: Dict[str, Any]) -> str: return 'web' # Fallback: WebSearch only (no API keys needed) +def get_missing_keys(config: Dict[str, Any]) -> str: + """Determine which API keys are missing. + + Returns: 'both', 'reddit', 'x', or 'none' + """ + has_openai = bool(config.get('OPENAI_API_KEY')) + has_xai = bool(config.get('XAI_API_KEY')) + + if has_openai and has_xai: + return 'none' + elif has_openai: + return 'x' # Missing xAI key + elif has_xai: + return 'reddit' # Missing OpenAI key + else: + return 'both' # Missing both keys + + def validate_sources(requested: str, available: str, include_web: bool = False) -> tuple[str, Optional[str]]: """Validate requested sources against available keys. diff --git a/scripts/lib/render.py b/scripts/lib/render.py index c40c66f..611e199 100644 --- a/scripts/lib/render.py +++ b/scripts/lib/render.py @@ -14,12 +14,13 @@ def ensure_output_dir(): OUTPUT_DIR.mkdir(parents=True, exist_ok=True) -def render_compact(report: schema.Report, limit: int = 15) -> str: +def render_compact(report: schema.Report, limit: int = 15, missing_keys: str = "none") -> str: """Render compact output for Claude to synthesize. Args: report: Report data limit: Max items per source + missing_keys: 'both', 'reddit', 'x', or 'none' Returns: Compact markdown string @@ -30,6 +31,18 @@ def render_compact(report: schema.Report, limit: int = 15) -> str: lines.append(f"## Research Results: {report.topic}") lines.append("") + # Web-only mode banner (when no API keys) + if report.mode == "web-only": + lines.append("**🌐 WEB SEARCH MODE** - Claude will search blogs, docs & news") + lines.append("") + lines.append("---") + lines.append("**⚔ Want better results?** Add API keys to unlock Reddit & X data:") + lines.append("- `OPENAI_API_KEY` → Reddit threads with real upvotes & comments") + lines.append("- `XAI_API_KEY` → X posts with real likes & reposts") + lines.append("- Edit `~/.config/last30days/.env` to add keys") + lines.append("---") + lines.append("") + # Cache indicator if report.from_cache: age_str = f"{report.cache_age_hours:.1f}h old" if report.cache_age_hours else "cached" @@ -44,12 +57,12 @@ def render_compact(report: schema.Report, limit: int = 15) -> str: lines.append(f"**xAI Model:** {report.xai_model_used}") lines.append("") - # Coverage note - if report.mode == "reddit-only": - lines.append("*Tip: Add xAI key for X coverage and better triangulation.*") + # Coverage note for partial coverage + if report.mode == "reddit-only" and missing_keys == "x": + lines.append("*šŸ’” Tip: Add XAI_API_KEY for X/Twitter data and better triangulation.*") lines.append("") - elif report.mode == "x-only": - lines.append("*Tip: Add OpenAI key for Reddit coverage and better triangulation.*") + elif report.mode == "x-only" and missing_keys == "reddit": + lines.append("*šŸ’” Tip: Add OPENAI_API_KEY for Reddit data and better triangulation.*") lines.append("") # Reddit items diff --git a/scripts/lib/ui.py b/scripts/lib/ui.py index 205e8af..51105cd 100644 --- a/scripts/lib/ui.py +++ b/scripts/lib/ui.py @@ -72,6 +72,61 @@ PROCESSING_MESSAGES = [ "Organizing findings...", ] +WEB_ONLY_MESSAGES = [ + "Searching the web...", + "Finding blogs and docs...", + "Crawling news sites...", + "Discovering tutorials...", +] + +# Promo message for users without API keys +PROMO_MESSAGE = f""" +{Colors.YELLOW}{Colors.BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.RESET} +{Colors.YELLOW}⚔ UNLOCK THE FULL POWER OF /last30days{Colors.RESET} + +{Colors.DIM}Right now you're using web search only. Add API keys to unlock:{Colors.RESET} + + {Colors.YELLOW}🟠 Reddit{Colors.RESET} - Real upvotes, comments, and community insights + └─ Add OPENAI_API_KEY (uses OpenAI's web_search for Reddit) + + {Colors.CYAN}šŸ”µ X (Twitter){Colors.RESET} - Real-time posts, likes, reposts from creators + └─ Add XAI_API_KEY (uses xAI's live X search) + +{Colors.DIM}Setup:{Colors.RESET} Edit {Colors.BOLD}~/.config/last30days/.env{Colors.RESET} +{Colors.YELLOW}{Colors.BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.RESET} +""" + +PROMO_MESSAGE_PLAIN = """ +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +⚔ UNLOCK THE FULL POWER OF /last30days + +Right now you're using web search only. Add API keys to unlock: + + 🟠 Reddit - Real upvotes, comments, and community insights + └─ Add OPENAI_API_KEY (uses OpenAI's web_search for Reddit) + + šŸ”µ X (Twitter) - Real-time posts, likes, reposts from creators + └─ Add XAI_API_KEY (uses xAI's live X search) + +Setup: Edit ~/.config/last30days/.env +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +""" + +# Shorter promo for single missing key +PROMO_SINGLE_KEY = { + "reddit": f""" +{Colors.DIM}šŸ’” Tip: Add {Colors.YELLOW}OPENAI_API_KEY{Colors.RESET}{Colors.DIM} to ~/.config/last30days/.env for Reddit data with real engagement metrics!{Colors.RESET} +""", + "x": f""" +{Colors.DIM}šŸ’” Tip: Add {Colors.CYAN}XAI_API_KEY{Colors.RESET}{Colors.DIM} to ~/.config/last30days/.env for X/Twitter data with real likes & reposts!{Colors.RESET} +""", +} + +PROMO_SINGLE_KEY_PLAIN = { + "reddit": "\nšŸ’” Tip: Add OPENAI_API_KEY to ~/.config/last30days/.env for Reddit data with real engagement metrics!\n", + "x": "\nšŸ’” Tip: Add XAI_API_KEY to ~/.config/last30days/.env for X/Twitter data with real likes & reposts!\n", +} + # Spinner frames SPINNER_FRAMES = ['ā ‹', 'ā ™', 'ā ¹', 'ā ø', 'ā ¼', 'ā “', 'ā ¦', 'ā §', 'ā ‡', 'ā '] DOTS_FRAMES = [' ', '. ', '.. ', '...'] @@ -214,6 +269,46 @@ class ProgressDisplay: sys.stderr.write(f"{Colors.RED}āœ— Error:{Colors.RESET} {message}\n") sys.stderr.flush() + def start_web_only(self): + """Show web-only mode indicator.""" + msg = random.choice(WEB_ONLY_MESSAGES) + self.spinner = Spinner(f"{Colors.GREEN}Web{Colors.RESET} {msg}", Colors.GREEN) + self.spinner.start() + + def end_web_only(self): + """End web-only spinner.""" + if self.spinner: + self.spinner.stop(f"{Colors.GREEN}Web{Colors.RESET} Claude will search the web") + + def show_web_only_complete(self): + """Show completion for web-only mode.""" + elapsed = time.time() - self.start_time + if IS_TTY: + sys.stderr.write(f"\n{Colors.GREEN}{Colors.BOLD}āœ“ Ready for web search{Colors.RESET} ") + sys.stderr.write(f"{Colors.DIM}({elapsed:.1f}s){Colors.RESET}\n") + sys.stderr.write(f" {Colors.GREEN}Web:{Colors.RESET} Claude will search blogs, docs & news\n\n") + else: + sys.stderr.write(f"āœ“ Ready for web search ({elapsed:.1f}s)\n") + sys.stderr.flush() + + def show_promo(self, missing: str = "both"): + """Show promotional message for missing API keys. + + Args: + missing: 'both', 'reddit', or 'x' - which keys are missing + """ + if missing == "both": + if IS_TTY: + sys.stderr.write(PROMO_MESSAGE) + else: + sys.stderr.write(PROMO_MESSAGE_PLAIN) + elif missing in PROMO_SINGLE_KEY: + if IS_TTY: + sys.stderr.write(PROMO_SINGLE_KEY[missing]) + else: + sys.stderr.write(PROMO_SINGLE_KEY_PLAIN[missing]) + sys.stderr.flush() + def print_phase(phase: str, message: str): """Print a phase message."""