Skip to content

Add Limitless Pendant skill for querying lifelogs#40

Merged
TechNickAI merged 3 commits intomainfrom
feat/limitless-skill
Jan 30, 2026
Merged

Add Limitless Pendant skill for querying lifelogs#40
TechNickAI merged 3 commits intomainfrom
feat/limitless-skill

Conversation

@TechNickAI
Copy link
Owner

Summary

Adds a new skill for querying Limitless Pendant lifelogs - conversations, meetings, and ambient recordings captured by the wearable AI device.

Features:

  • Python CLI tool using uv inline script dependencies for zero-setup execution
  • Query recent, today, or date-specific conversations
  • Semantic search across all lifelogs
  • Smart .env file loading from multiple locations (~/.env, ~/.clawdbot/clawdbot.json, ./.env, ./.env.local)
  • Beautiful rich terminal output with markdown formatting
  • Helpful setup instructions when API key is missing

Test plan

  • Script executes with help command
  • Setup instructions display correctly when API key missing
  • Script uses modern Python (3.11+, uv inline deps)
  • Test with actual LIMITLESS_API_KEY

🤖 Generated with Claude Code

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 <noreply@anthropic.com>
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Points LLMs to Context7 and developer portal for API lookups

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Jan 30, 2026

Code Review: Limitless Pendant Skill

Summary

This PR adds a well-crafted skill for querying Limitless Pendant lifelogs. The implementation is clean, follows modern Python practices, and includes excellent UX with helpful setup instructions. Overall, this is high-quality code with just a few minor suggestions for improvement.

✅ Strengths

Code Quality

  • Modern Python: Great use of Python 3.11+ features (match/case, union types with |)
  • uv inline script dependencies: Excellent choice for zero-setup execution - this is the right pattern for skill scripts
  • Clean error handling: Proper exception handling with helpful error messages
  • Good UX: The setup instructions when API key is missing are excellent and user-friendly

Documentation

  • SKILL.md follows conventions: Proper prettier-ignore for long description, good triggers array, clear examples
  • Semantic triggers: The triggers array includes natural phrases users would say ("what did I say", "what was discussed")
  • Comprehensive examples: Both SKILL.md and the CLI help show multiple usage patterns

Security & Best Practices

  • API key handling: Secure loading from multiple .env locations with proper priority order
  • No secrets in code: No hardcoded credentials or sensitive data
  • HTTP timeout: 30s timeout prevents hanging requests
  • Permission handling: Script is properly executable (chmod +x)

🔧 Issues & Suggestions

1. Potential Bug: Integer Parsing Without Validation

Location: plugins/core/skills/limitless/scripts/limitless:237

case "recent":
    limit = int(args[1]) if len(args) > 1 else 5
    cmd_recent(limit)

Issue: If user passes non-integer value like limitless recent abc, this will crash with ValueError.

Suggestion: Add validation:

case "recent":
    limit = 5
    if len(args) > 1:
        try:
            limit = int(args[1])
        except ValueError:
            error(f"Invalid limit: {args[1]}. Must be an integer.")
            sys.exit(1)
    cmd_recent(limit)

2. Security: API Key Logging Risk

Location: plugins/core/skills/limitless/scripts/limitless:120

error(f"API error: {e.response.status_code} - {e.response.text}")

Issue: If the API returns an error that includes the API key in the response body, this would print it to stderr. This is unlikely but possible.

Suggestion: Consider truncating or sanitizing error responses:

error_text = e.response.text[:200]  # Truncate to prevent key leakage
error(f"API error: {e.response.status_code} - {error_text}")

3. Code Consistency: Mixed Error Handling Patterns

Locations: Multiple functions

Issue: Some functions check if data before calling format_lifelogs, but this check doesn't distinguish between None (error) and empty dict (valid empty response). The API might return {"data": {"lifelogs": []}} for no results, which is valid.

Current pattern:

data = call_api(...)
if data:
    format_lifelogs(data)

Suggestion: This is actually fine since call_api returns None on errors and format_lifelogs handles empty results. No change needed, but documenting this behavior in call_api docstring would help.

4. Minor: Hardcoded Timezone Default

Location: plugins/core/skills/limitless/scripts/limitless:93

TIMEZONE = os.environ.get("LIMITLESS_TIMEZONE", "America/Chicago")

Issue: Hardcoded America/Chicago may surprise users in other timezones.

Suggestion: Consider using system timezone as default:

import zoneinfo
TIMEZONE = os.environ.get("LIMITLESS_TIMEZONE") or datetime.now().astimezone().tzinfo.key or "America/Chicago"

Or at minimum, document this clearly in the SKILL.md prerequisites (which you already do - so this is fine as-is).

5. Enhancement: Raw Params Parsing Could Be More Robust

Location: plugins/core/skills/limitless/scripts/limitless:186-192

def cmd_raw(params: str) -> None:
    param_dict = {}
    if params:
        for pair in params.split("&"):
            if "=" in pair:
                key, value = pair.split("=", 1)
                param_dict[key] = value

Issue: This doesn't handle URL-encoded values or validate parameter names.

Suggestion: Consider using urllib.parse.parse_qs for proper parsing:

from urllib.parse import parse_qs

def cmd_raw(params: str) -> None:
    param_dict = {}
    if params:
        parsed = parse_qs(params, keep_blank_values=True)
        param_dict = {k: v[0] if len(v) == 1 else v for k, v in parsed.items()}
    
    data = call_api("/lifelogs", param_dict)
    if data:
        console.print_json(data=data)

6. Documentation: Missing Color Field

Location: plugins/core/skills/limitless/SKILL.md:1-15

Issue: According to CLAUDE.md conventions, skills should have a color field in frontmatter for categorization. Research skills typically use a specific color from the scheme.

Suggestion: Add color to frontmatter (check existing research skills for the standard):

---
name: limitless
color: magenta  # or whatever color research skills use
# prettier-ignore
description: "Use when recalling conversations..."

🧪 Test Coverage

PR mentions:

  • ✅ Script executes with help command
  • ✅ Setup instructions display correctly
  • ✅ Modern Python and uv inline deps
  • ⚠️ Missing: Test with actual LIMITLESS_API_KEY

Recommendation: Since this requires hardware and API access, consider adding a mock test or at minimum document manual testing steps for future contributors.

📊 Performance Considerations

  • HTTP timeout: 30s is reasonable for API calls
  • No rate limiting: The API has 180 req/min limit. For typical CLI usage this is fine, but consider adding a note in SKILL.md if users plan to batch process
  • Memory: Loading entire response into memory is fine for typical lifelog sizes

🎯 Recommendations

Must Fix (before merge):

  1. Add integer validation for limit parameter to prevent crashes

Should Fix (nice to have):

  1. Add color field to SKILL.md frontmatter
  2. Truncate error responses to prevent potential key leakage

Optional (future enhancement):

  1. Consider system timezone as default instead of hardcoded Chicago
  2. Use proper URL parsing for raw params
  3. Add mock tests or document manual testing procedure

🎉 Overall Assessment

Verdict: ✅ Approve with minor fixes

This is excellent work. The code is clean, well-documented, and follows the project's conventions. The user experience is thoughtful with helpful setup instructions. The only blocking issue is the integer parsing bug which could cause crashes. Once that's fixed, this is ready to merge.

The skill fills a useful niche for Limitless Pendant users and demonstrates good patterns for future skill development (uv inline scripts, multi-location .env loading, rich terminal output).

Great job! 🚀

@claude
Copy link

claude bot commented Jan 30, 2026

Code Review - Limitless Pendant Skill

Overall this is a well-implemented skill with clean Python code and good documentation. The uv inline script approach is modern and eliminates dependency setup friction. Here are my findings:

✅ Strengths

Code Quality:

  • Excellent use of modern Python (3.11+, pattern matching, type hints with | syntax)
  • Clean separation of concerns with dedicated functions for each command
  • Good error handling with user-friendly messages
  • Beautiful use of rich library for terminal output

Documentation:

  • SKILL.md follows established patterns perfectly
  • Clear triggers array for semantic matching
  • Helpful setup instructions and examples
  • Good use of # prettier-ignore for long description

Security:

  • API key loading from multiple locations in sensible priority order
  • No hardcoded credentials
  • Good handling of missing credentials with helpful setup instructions

User Experience:

  • Excellent setup instructions when API key is missing
  • Shows which config file locations were searched
  • Clear error messages with actionable next steps

🟡 Suggestions

1. Security: API Key Exposure in JSON Parsing

In plugins/core/skills/limitless/scripts/limitless:42-48, loading from clawdbot.json could silently fail and expose errors:

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

Consider logging when JSON parsing fails so users know their config file is malformed.

2. Type Safety: Union Syntax

Good use of str | None return type (line 35), but consider using it consistently. Function signatures like call_api() return dict | None but could be more specific with TypedDict for the expected response structure.

3. Error Handling: HTTP Response Details

In plugins/core/skills/limitless/scripts/limitless:118-119, non-401 errors just print the raw response:

else:
    error(f"API error: {e.response.status_code} - {e.response.text}")

For 429 (rate limit), could give more helpful guidance about the 180/min limit mentioned in docs.

4. Potential Bug: Date Validation

cmd_date() (line 163) doesn't validate the date format. Malformed dates will fail at the API level. Consider adding basic validation:

def cmd_date(date_str: str) -> None:
    """Get conversations for a specific date."""
    try:
        datetime.strptime(date_str, "%Y-%m-%d")
    except ValueError:
        error("Date must be in YYYY-MM-DD format")
        return
    # ... rest of function

5. Test Coverage

The PR checklist shows "Test with actual LIMITLESS_API_KEY" is unchecked. While the script executes with help command, consider adding:

  • A mock test to verify API call structure
  • Validation that environment loading works correctly
  • Test that date validation works (if added)

6. Documentation: Context7 Reference

The <llm-api-reference> section in SKILL.md mentions Context7 which may not be available to all users. Consider making this optional or documenting whether Context7 is required.

🟢 Minor Polish

1. Timezone Handling

Default timezone is America/Chicago - consider documenting why this specific timezone is the default, or use UTC as a more universal default.

2. Script Permissions

The file has executable permissions (good!), but consider documenting in SKILL.md that users may need to chmod +x after cloning.

3. Help Text Formatting

The help text at line 204 could include a note about the setup instructions being displayed automatically when the API key is missing.

📊 Metrics

  • Files changed: 2
  • Lines added: 342
  • Test coverage: Partial (help command tested, API calls not tested)
  • Breaking changes: None
  • Dependencies: httpx, rich, python-dotenv (managed by uv inline)

🎯 Verdict

Recommend: Approve with minor suggestions

This is production-ready code with excellent UX. The suggestions above are optimizations, not blockers. The skill will work well as-is and follows all project conventions from CLAUDE.md.

The only item I'd strongly recommend before merge is completing the test plan item "Test with actual LIMITLESS_API_KEY" to ensure the API integration works end-to-end.


Review generated by Claude Code following CLAUDE.md conventions

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: df5d2c412b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 43 to 47
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"]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow env override when clawdbot.json is present

Because load_api_key returns immediately when clawdbot.json contains LIMITLESS_API_KEY, later sources (e.g., ./.env, ./.env.local, or an exported LIMITLESS_API_KEY) are never considered. This means a stale key in ~/.clawdbot/clawdbot.json will override a newer key set in the shell, leading to persistent 401s even after a user updates their environment. Consider loading the JSON into the environment and continuing the loop, or deferring the decision until all sources are processed so higher-precedence sources can override.

Useful? React with 👍 / 👎.

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 <noreply@anthropic.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 5 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Environment variable priority inverted from documented behavior

Medium Severity

The docstring states "Environment variable (highest priority)" but using load_dotenv(env_path, override=True) causes .env files to overwrite any pre-existing environment variable. This inverts the documented priority - the environment variable actually has the lowest priority, and .env.local has the highest. Users following the documentation who set LIMITLESS_API_KEY in their shell will have it unexpectedly overwritten by values in .env files.

Fix in Cursor Fix in Web


match cmd:
case "recent":
limit = int(args[1]) if len(args) > 1 else 5
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unhandled ValueError when parsing limit argument

Low Severity

The recent command parses its argument with int(args[1]) without validation. Running limitless recent abc causes an unhandled ValueError and Python traceback instead of a user-friendly error message. Other commands like date validate their input and call sys.exit(1) with a helpful message, but recent lacks this handling.

Fix in Cursor Fix in Web


def cmd_today() -> None:
"""Get today's conversations."""
today = datetime.now().strftime("%Y-%m-%d")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Today command uses machine timezone instead of configured timezone

Medium Severity

The cmd_today() function uses datetime.now() which returns the date in the machine's local timezone, but then passes LIMITLESS_TIMEZONE to the API. If the script runs on a server in UTC while LIMITLESS_TIMEZONE is set to a different timezone like America/Chicago, the computed date can be off by a day during timezone boundary hours. For example, at 11pm Chicago time (5am UTC), the UTC server calculates "tomorrow's" date, causing the wrong day's conversations to be retrieved.

Fix in Cursor Fix in Web

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:
Copy link

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", {}) returns None (since the key exists) rather than the default empty dict. The subsequent "LIMITLESS_API_KEY" in env_vars check raises an unhandled TypeError. The except clause only catches JSONDecodeError and KeyError, so the script crashes instead of gracefully warning and continuing.

Fix in Cursor Fix in Web

return None
except httpx.RequestError as e:
error(f"Request failed: {e}")
return None
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Script exits with success code despite API errors

Medium Severity

When call_api encounters HTTP errors (401, 429, 5xx) or network failures, it prints an error message but returns None instead of exiting. Command functions check if data: and simply skip processing, causing the script to exit with code 0 (success). This contradicts the behavior for missing API key which correctly calls sys.exit(1). Shell scripts using set -e, &&, or checking $? will incorrectly believe the command succeeded despite the error being reported to stderr.

Additional Locations (1)

Fix in Cursor Fix in Web

@TechNickAI TechNickAI merged commit fa00476 into main Jan 30, 2026
8 checks passed
@TechNickAI TechNickAI deleted the feat/limitless-skill branch January 30, 2026 15:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant