diff --git a/plugins/core/skills/limitless/SKILL.md b/plugins/core/skills/limitless/SKILL.md new file mode 100644 index 0000000..5971f5d --- /dev/null +++ b/plugins/core/skills/limitless/SKILL.md @@ -0,0 +1,83 @@ +--- +name: limitless +# prettier-ignore +description: "Use when recalling conversations from Limitless Pendant, finding what was discussed in meetings, searching lifelogs, or answering 'what did I say about' questions" +version: 1.0.0 +category: research +triggers: + - "limitless" + - "pendant" + - "lifelogs" + - "what did I say" + - "what was discussed" + - "conversation history" + - "ambient recording" +--- + + +Query your Limitless Pendant's lifelogs - conversations, meetings, and ambient recordings captured by the wearable AI device. Transform "what did I talk about?" into searchable, citable transcripts. + + + +Use when answering questions about past conversations, finding meeting context, recalling what someone said, searching for topics discussed, or building context from real-world interactions. + +Clear triggers: +- "What did I talk about with [person]?" +- "What happened in my meeting yesterday?" +- "Find when I mentioned [topic]" +- "What was that restaurant recommendation?" + + + +Set `LIMITLESS_API_KEY` environment variable. Get your key from [app.limitless.ai](https://app.limitless.ai) → Settings → Developer. + +Optional: Set `LIMITLESS_TIMEZONE` (defaults to America/Chicago). + + + +```bash +# Recent lifelogs (default: 5) +limitless recent +limitless recent 10 + +# Today's conversations +limitless today + +# Specific date +limitless date 2026-01-28 + +# Semantic search +limitless search "meeting with john" +limitless search "restaurant recommendation" + +# Raw API with custom params +limitless raw "limit=5&isStarred=true" +``` + + + +Lifelogs include: +- **title** - AI-generated summary +- **markdown** - Full transcript with timestamps +- **startTime/endTime** - When recorded +- **contents** - Structured segments with speaker attribution + +Speaker names show as "Unknown" unless voice profiles are trained in the Limitless app. + + + +- Rate limit: 180 requests/minute +- Requires Limitless Pendant hardware +- Developer portal: [limitless.ai/developers](https://www.limitless.ai/developers) +- API examples: [github.com/limitless-ai-inc/limitless-api-examples](https://github.com/limitless-ai-inc/limitless-api-examples) + + + +If you need to look up API details beyond this skill's commands, use Context7: +``` +resolve-library-id: limitless → /websites/help_limitless_ai_en +query-docs: /websites/help_limitless_ai_en with "lifelogs API" query +``` + +Or fetch current docs directly from https://www.limitless.ai/developers + diff --git a/plugins/core/skills/limitless/scripts/limitless b/plugins/core/skills/limitless/scripts/limitless new file mode 100755 index 0000000..5c01977 --- /dev/null +++ b/plugins/core/skills/limitless/scripts/limitless @@ -0,0 +1,276 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# dependencies = ["httpx", "rich", "python-dotenv"] +# /// +""" +Limitless CLI - Query your Pendant lifelogs. + +Requires LIMITLESS_API_KEY environment variable. +Get your key from: https://app.limitless.ai → Settings → Developer +""" + +import json +import os +import sys +from datetime import datetime +from pathlib import Path + +import httpx +from dotenv import load_dotenv +from rich.console import Console +from rich.markdown import Markdown +from rich.panel import Panel + +console = Console() + +# Load .env files in priority order (later overrides earlier) +ENV_LOCATIONS = [ + Path.home() / ".env", + Path.home() / ".clawdbot" / "clawdbot.json", + Path.cwd() / ".env", + Path.cwd() / ".env.local", +] + + +def load_api_key() -> str | None: + """Load API key from environment or .env files. + + Priority (later overrides earlier): + 1. ~/.env + 2. ~/.clawdbot/clawdbot.json + 3. ./.env + 4. ./.env.local + 5. Environment variable (highest priority) + """ + # Load all sources into environment (later overrides earlier) + for env_path in ENV_LOCATIONS: + if env_path.exists(): + if env_path.suffix == ".json": + try: + config = json.loads(env_path.read_text()) + env_vars = config.get("env", {}) + if "LIMITLESS_API_KEY" in env_vars: + os.environ["LIMITLESS_API_KEY"] = env_vars["LIMITLESS_API_KEY"] + except (json.JSONDecodeError, KeyError) as e: + console.print(f"[yellow]Warning: Failed to parse {env_path}: {e}[/yellow]", file=sys.stderr) + else: + load_dotenv(env_path, override=True) + + return os.environ.get("LIMITLESS_API_KEY") + + +def show_setup_instructions() -> None: + """Display friendly setup instructions when API key is missing.""" + instructions = """ +[bold red]API Key Required[/bold red] + +To use the Limitless CLI, you need an API key from your Limitless account. + +[bold cyan]Step 1: Get your API key[/bold cyan] +1. Go to [link=https://app.limitless.ai]app.limitless.ai[/link] +2. Click Settings → Developer +3. Generate or copy your API key + +[bold cyan]Step 2: Set your API key[/bold cyan] + +Option A: Environment variable (recommended for temporary use) +[dim]export LIMITLESS_API_KEY=your-api-key-here[/dim] + +Option B: Add to ~/.env (recommended for persistent use) +[dim]echo "LIMITLESS_API_KEY=your-api-key-here" >> ~/.env[/dim] + +Option C: Add to project .env or .env.local +[dim]echo "LIMITLESS_API_KEY=your-api-key-here" >> .env[/dim] + +[bold cyan]Searched locations:[/bold cyan] +""" + console.print(Panel(instructions, title="🎙️ Limitless Setup", border_style="yellow")) + + for loc in ENV_LOCATIONS: + status = "✓" if loc.exists() else "✗" + color = "green" if loc.exists() else "dim" + console.print(f" [{color}]{status} {loc}[/{color}]") + + console.print() + + +API_KEY = load_api_key() +BASE_URL = "https://api.limitless.ai/v1" +TIMEZONE = os.environ.get("LIMITLESS_TIMEZONE", "America/Chicago") + + +def error(msg: str) -> None: + console.print(f"[red]Error:[/red] {msg}", file=sys.stderr) + + +def call_api(endpoint: str, params: dict | None = None) -> dict | None: + """Make authenticated API call to Limitless.""" + if not API_KEY: + show_setup_instructions() + sys.exit(1) + + try: + response = httpx.get( + f"{BASE_URL}{endpoint}", + params=params, + headers={"X-API-Key": API_KEY}, + timeout=30.0, + ) + response.raise_for_status() + return response.json() + except httpx.HTTPStatusError as e: + if e.response.status_code == 401: + error("Invalid API key. Please check your LIMITLESS_API_KEY.") + console.print("[dim]Run 'limitless help' for setup instructions.[/dim]") + elif e.response.status_code == 429: + error("Rate limit exceeded. Limitless API allows 180 requests/minute.") + console.print("[dim]Wait a moment before retrying.[/dim]") + else: + error(f"API error: {e.response.status_code} - {e.response.text}") + return None + except httpx.RequestError as e: + error(f"Request failed: {e}") + return None + + +def format_lifelogs(data: dict) -> None: + """Format and display lifelogs as markdown.""" + lifelogs = data.get("data", {}).get("lifelogs", []) + if not lifelogs: + console.print("[yellow]No lifelogs found[/yellow]") + return + + for log in lifelogs: + title = log.get("title", "Untitled") + start = log.get("startTime", "") + end = log.get("endTime", "") + markdown_content = log.get("markdown", "") + + output = f"## {title}\n**{start} - {end}**\n\n{markdown_content}\n\n---\n" + console.print(Markdown(output)) + + +def cmd_recent(limit: int = 5) -> None: + """Get N most recent lifelogs.""" + data = call_api( + "/lifelogs", + {"limit": limit, "timezone": TIMEZONE, "includeContents": "false"}, + ) + if data: + format_lifelogs(data) + + +def cmd_today() -> None: + """Get today's conversations.""" + today = datetime.now().strftime("%Y-%m-%d") + data = call_api( + "/lifelogs", + {"date": today, "timezone": TIMEZONE, "limit": 10, "includeContents": "false"}, + ) + if data: + format_lifelogs(data) + + +def cmd_date(date_str: str) -> None: + """Get conversations for a specific date.""" + # Validate date format + try: + datetime.strptime(date_str, "%Y-%m-%d") + except ValueError: + error(f"Invalid date format: '{date_str}'. Use YYYY-MM-DD (e.g., 2026-01-28)") + sys.exit(1) + + data = call_api( + "/lifelogs", + {"date": date_str, "timezone": TIMEZONE, "limit": 10, "includeContents": "false"}, + ) + if data: + format_lifelogs(data) + + +def cmd_search(query: str) -> None: + """Semantic search across all lifelogs.""" + data = call_api( + "/lifelogs", + {"search": query, "timezone": TIMEZONE, "limit": 10, "includeContents": "false"}, + ) + if data: + format_lifelogs(data) + + +def cmd_raw(params: str) -> None: + """Raw API call with custom params.""" + param_dict = {} + if params: + for pair in params.split("&"): + if "=" in pair: + key, value = pair.split("=", 1) + param_dict[key] = value + + data = call_api("/lifelogs", param_dict) + if data: + console.print_json(data=data) + + +def show_help() -> None: + """Display help message.""" + help_text = """ +# Limitless CLI - Query your Pendant lifelogs + +## Commands +- `recent [N]` - Get N most recent lifelogs (default: 5) +- `today` - Get today's conversations +- `date YYYY-MM-DD` - Get conversations for a specific date +- `search "query"` - Semantic search across all lifelogs +- `raw ` - Raw API call with custom params + +## Environment +- `LIMITLESS_API_KEY` - Required - your API key +- `LIMITLESS_TIMEZONE` - Optional - timezone (default: America/Chicago) + +## Examples +``` +limitless recent 3 +limitless today +limitless date 2026-01-28 +limitless search "meeting with john" +limitless raw "limit=5&isStarred=true" +``` +""" + console.print(Markdown(help_text)) + + if not API_KEY: + console.print() + show_setup_instructions() + + +def main() -> None: + args = sys.argv[1:] + cmd = args[0] if args else "help" + + match cmd: + case "recent": + limit = int(args[1]) if len(args) > 1 else 5 + cmd_recent(limit) + case "today": + cmd_today() + case "date": + if len(args) < 2: + error("Usage: limitless date YYYY-MM-DD") + sys.exit(1) + cmd_date(args[1]) + case "search": + if len(args) < 2: + error('Usage: limitless search "query"') + sys.exit(1) + cmd_search(args[1]) + case "raw": + params = args[1] if len(args) > 1 else "" + cmd_raw(params) + case "help" | "-h" | "--help" | _: + show_help() + + +if __name__ == "__main__": + main()