-
Notifications
You must be signed in to change notification settings - Fork 2
Add Limitless Pendant skill for querying lifelogs #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| --- | ||
|
|
||
| <objective> | ||
| 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. | ||
| </objective> | ||
|
|
||
| <when-to-use> | ||
| 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?" | ||
| </when-to-use> | ||
|
|
||
| <prerequisites> | ||
| 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). | ||
| </prerequisites> | ||
|
|
||
| <commands> | ||
| ```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" | ||
| ``` | ||
| </commands> | ||
|
|
||
| <response-format> | ||
| 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. | ||
| </response-format> | ||
|
|
||
| <api-notes> | ||
| - 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) | ||
| </api-notes> | ||
|
|
||
| <llm-api-reference> | ||
| 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 | ||
| </llm-api-reference> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Environment variable priority inverted from documented behaviorMedium Severity The docstring states "Environment variable (highest priority)" but using |
||
|
|
||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Script exits with success code despite API errorsMedium Severity When Additional Locations (1) |
||
|
|
||
|
|
||
| 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") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Today command uses machine timezone instead of configured timezoneMedium Severity The |
||
| 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 <params>` - 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unhandled ValueError when parsing limit argumentLow Severity The |
||
| 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() | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JSON config with null env crashes with TypeError
Low Severity
When parsing
clawdbot.json, if the file contains{"env": null},config.get("env", {})returnsNone(since the key exists) rather than the default empty dict. The subsequent"LIMITLESS_API_KEY" in env_varscheck raises an unhandledTypeError. The except clause only catchesJSONDecodeErrorandKeyError, so the script crashes instead of gracefully warning and continuing.