From df5d2c412bf8fc4ae7cfa508ac75b7a695de3bb8 Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Fri, 30 Jan 2026 08:48:13 -0600 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20Add=20Limitless=20Pendant=20ski?= =?UTF-8?q?ll=20for=20querying=20lifelogs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python CLI tool using uv inline script dependencies for zero-setup execution. Features: - Query recent, today, or date-specific conversations - Semantic search across all lifelogs - Smart .env file loading from multiple locations - Beautiful rich terminal output - Helpful setup instructions when API key missing Co-Authored-By: Claude Opus 4.5 --- plugins/core/skills/limitless/SKILL.md | 72 +++++ .../core/skills/limitless/scripts/limitless | 259 ++++++++++++++++++ 2 files changed, 331 insertions(+) create mode 100644 plugins/core/skills/limitless/SKILL.md create mode 100755 plugins/core/skills/limitless/scripts/limitless diff --git a/plugins/core/skills/limitless/SKILL.md b/plugins/core/skills/limitless/SKILL.md new file mode 100644 index 0000000..465973a --- /dev/null +++ b/plugins/core/skills/limitless/SKILL.md @@ -0,0 +1,72 @@ +--- +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 +- API docs: [docs.limitless.ai](https://docs.limitless.ai) + diff --git a/plugins/core/skills/limitless/scripts/limitless b/plugins/core/skills/limitless/scripts/limitless new file mode 100755 index 0000000..9dfb2e9 --- /dev/null +++ b/plugins/core/skills/limitless/scripts/limitless @@ -0,0 +1,259 @@ +#!/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.""" + # First, load any .env files + for env_path in ENV_LOCATIONS: + if env_path.exists(): + if env_path.suffix == ".json": + # Handle JSON config files (like clawdbot.json) + try: + config = json.loads(env_path.read_text()) + env_vars = config.get("env", {}) + if "LIMITLESS_API_KEY" in env_vars: + return env_vars["LIMITLESS_API_KEY"] + except (json.JSONDecodeError, KeyError): + pass + else: + load_dotenv(env_path) + + 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]") + 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.""" + 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() From f409edc4ef3afa241a17768c813506bf1097471e Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Fri, 30 Jan 2026 08:50:35 -0600 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=93=9A=20Add=20LLM=20API=20reference?= =?UTF-8?q?=20section=20to=20Limitless=20skill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Points LLMs to Context7 and developer portal for API lookups Co-Authored-By: Claude Opus 4.5 --- plugins/core/skills/limitless/SKILL.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/plugins/core/skills/limitless/SKILL.md b/plugins/core/skills/limitless/SKILL.md index 465973a..5971f5d 100644 --- a/plugins/core/skills/limitless/SKILL.md +++ b/plugins/core/skills/limitless/SKILL.md @@ -68,5 +68,16 @@ Speaker names show as "Unknown" unless voice profiles are trained in the Limitle - Rate limit: 180 requests/minute - Requires Limitless Pendant hardware -- API docs: [docs.limitless.ai](https://docs.limitless.ai) +- 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 + From 83f0ede4adb45ad56dfdeab8286781b8894208da Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Fri, 30 Jan 2026 08:53:50 -0600 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=90=9B=20Fix=20env=20loading=20priori?= =?UTF-8?q?ty=20and=20improve=20error=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes from bot feedback: - Fix Codex P2: Env loading now properly allows later sources to override earlier - Add date format validation with helpful error message - Improve 429 rate limit error message - Add warning when JSON config parsing fails Co-Authored-By: Claude Opus 4.5 --- .../core/skills/limitless/scripts/limitless | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/plugins/core/skills/limitless/scripts/limitless b/plugins/core/skills/limitless/scripts/limitless index 9dfb2e9..5c01977 100755 --- a/plugins/core/skills/limitless/scripts/limitless +++ b/plugins/core/skills/limitless/scripts/limitless @@ -34,21 +34,28 @@ ENV_LOCATIONS = [ def load_api_key() -> str | None: - """Load API key from environment or .env files.""" - # First, load any .env files + """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": - # Handle JSON config files (like clawdbot.json) try: config = json.loads(env_path.read_text()) env_vars = config.get("env", {}) if "LIMITLESS_API_KEY" in env_vars: - return env_vars["LIMITLESS_API_KEY"] - except (json.JSONDecodeError, KeyError): - pass + 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) + load_dotenv(env_path, override=True) return os.environ.get("LIMITLESS_API_KEY") @@ -116,6 +123,9 @@ def call_api(endpoint: str, params: dict | None = None) -> dict | None: 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 @@ -164,6 +174,13 @@ def cmd_today() -> None: 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"},