Skill: tmustier/heypocket-reader (version 1.0.0)

--- name: pocket-transcripts description: Read transcripts and summaries from Pocket AI (heypocket.com) recording devices. Use when users want to retrieve, search, or analyze their Pocket recordings, transcripts, summaries, or action items. Triggers on requests involving Pocket device data, conversation transcripts, meeting recordings, or audio note retrieval. --- # Pocket Transcripts Read transcripts and summaries from Pocket AI devices via reverse-engineered API. ## Quick Reference | Function | Description | |----------|-------------| | `get_recordings(days, limit)` | List recent recordings | | `get_recording_full(id)` | Get transcript + summary + action items | | `get_transcript(id)` | Get raw transcript text | | `get_summarization(id)` | Get markdown summary | | `search_recordings(query)` | Search by text | ## Setup (One-Time) ### 1. Start Chrome with User Profile ```bash ~/.factory/skills/browser/start.js --profile # or ~/.claude/skills/browser/start.js --profile ``` ### 2. Log into Pocket Navigate to and log in: ```bash ~/.factory/skills/browser/nav.js https://app.heypocket.com ``` ### 3. Extract Token ```bash python3 scripts/reader.py extract ``` Token is saved to `~/.pocket_token.json` and expires in 1 hour. ## Usage ### List Recordings ```python from pathlib import Path import sys sys.path.insert(0, str(Path.home() / '.claude/skills/pocket-transcripts/scripts')) from reader import get_recordings, get_recording_full recordings = get_recordings(days=30, limit=20) for r in recordings: print(f"{r.recorded_at:%Y-%m-%d} | {r.duration_str} | {r.title}") ``` ### Get Full Transcript and Summary ```python full = get_recording_full(recording_id) print(f"Transcript ({len(full['transcript'])} chars):") print(full['transcript'][:500]) print(f"\nSummary (markdown):") print(full['summary']) print(f"\nAction Items: {len(full['action_items'])}") for item in full['action_items']: print(f" - {item}") ``` ### Search Recordings ```python results = search_recordings("meeting", days=90) for r in results: print(f"{r.title} - {r.description[:100]}") ``` ## API Details **Base URL**: `https://production.heypocketai.com/api/v1` **Auth**: Firebase Bearer token from browser IndexedDB **Key Endpoints**: - `GET /recordings` - List with pagination, filters - `GET /recordings/{id}?include=all` - Full data with transcript/summary **Data Structure**: - Transcript: `data.transcription.transcription.text` - Summary: `data.summarizations[id].v2.summary.markdown` - Action Items: `data.summarizations[id].v2.actionItems.items` ## Token Refresh Firebase tokens expire in 1 hour. When expired: 1. Ensure Chrome is running with `--profile` 2. Confirm logged into app.heypocket.com 3. Re-run: `python3 scripts/reader.py extract` ## Data Model ### PocketRecording - `id`, `title`, `description` - `duration` (seconds), `duration_str` (human readable) - `recorded_at`, `created_at` - `has_transcription`, `has_summarization` - `num_speakers` - `latitude`, `longitude` (if location enabled) - `tags` (list of strings) ### PocketSummarization - `summary` (markdown formatted) - `action_items` (list) - `transcript` (raw text)

path: README.md
content: # Pocket Transcripts Skill

A Claude Code skill for reading transcripts and summaries from [Pocket AI](https://heypocket.com) recording devices.

## What is Pocket?

Pocket is an AI-powered wearable device that records conversations and generates transcripts, summaries, and action items. This skill provides programmatic access to your Pocket data via their unofficial API.

## Features

- **List recordings** with metadata (title, duration, date, speakers)
- **Get full transcripts** of recorded conversations (50k+ characters)
- **Get AI summaries** in markdown format
- **Extract action items** from recordings
- **Search recordings** by text

## Installation

### For Claude Code Users

Download the `.skill` file from [Releases](../../releases) and add it to your Claude Code skills.

### Manual Installation

```bash
# Clone to your skills directory
git clone https://github.com/YOUR_USERNAME/pocket-transcripts-skill.git ~/.claude/skills/pocket-transcripts
```

## Setup

### Prerequisites

1. A Pocket account with recordings
2. The [browser skill](https://github.com/anthropics/skills/tree/main/skills/browser) installed
3. Chrome browser

### Authentication

Pocket uses Firebase authentication. To extract your token:

1. Start Chrome with your profile:
   ```bash
   ~/.claude/skills/browser/start.js --profile
   ```

2. Navigate to Pocket and log in:
   ```bash
   ~/.claude/skills/browser/nav.js https://app.heypocket.com
   ```

3. Extract the token:
   ```bash
   python3 ~/.claude/skills/pocket-transcripts/scripts/reader.py extract
   ```

The token is saved to `~/.pocket_token.json` and expires in 1 hour.

## Usage

### CLI

```bash
# List recent recordings
python3 scripts/reader.py

# List recordings from last 7 days
python3 scripts/reader.py 7
```

### Python

```python
from reader import get_recordings, get_recording_full, search_recordings

# List recordings
recordings = get_recordings(days=30, limit=20)
for r in recordings:
    print(f"{r.recorded_at:%Y-%m-%d} | {r.duration_str} | {r.title}")

# Get full transcript and summary
full = get_recording_full(recording_id)
print(full['transcript'])  # Raw text
print(full['summary'])     # Markdown

# Search
results = search_recordings("meeting")
```

## API Reference

| Function | Description |
|----------|-------------|
| `get_recordings(days, limit)` | List recent recordings |
| `get_recording_full(id)` | Get transcript + summary + action items + speakers |
| `get_transcript(id)` | Get raw transcript text |
| `get_summarization(id)` | Get markdown summary |
| `search_recordings(query, days, limit)` | Search recordings by text |

## Technical Details

- **API Base**: `https://production.heypocketai.com/api/v1`
- **Auth**: Firebase Bearer token from browser IndexedDB
- **Token Expiry**: 1 hour (Firebase default)

This skill was created by reverse-engineering the Pocket web app at `app.heypocket.com`.

## License

MIT License - See [LICENSE](LICENSE) for details.

## Disclaimer

This is an unofficial integration. Use at your own risk. Not affiliated with or endorsed by Pocket/Open Vision Engineering Inc.


path: scripts/reader.py
content: #!/usr/bin/env python3
"""
Pocket (heypocket.com) API client.

Reads transcripts and summaries from Pocket AI devices via reverse-engineered API.
API Base: https://production.heypocketai.com/api/v1
Auth: Firebase Bearer token extracted from browser

Usage:
    # Extract token (requires Chrome with --profile, logged into app.heypocket.com)
    python3 reader.py extract
    
    # List recordings
    python3 reader.py [days]
"""

import json
import subprocess
import sys
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Optional, List
from pathlib import Path

API_BASE = "https://production.heypocketai.com/api/v1"
TOKEN_CACHE_FILE = Path.home() / ".pocket_token.json"


@dataclass
class PocketRecording:
    """A Pocket recording with metadata."""
    id: str
    title: str
    description: str
    duration: int
    recorded_at: datetime
    created_at: datetime
    has_transcription: bool
    has_summarization: bool
    transcription_status: str
    summarization_status: str
    num_speakers: int
    latitude: Optional[float] = None
    longitude: Optional[float] = None
    tags: List[str] = None
    folder_id: Optional[str] = None
    
    @classmethod
    def from_api(cls, data: dict) -> 'PocketRecording':
        return cls(
            id=data['id'],
            title=data.get('title', 'Untitled'),
            description=data.get('description', ''),
            duration=data.get('duration', 0) or 0,
            recorded_at=_parse_datetime(data.get('recordingAt')),
            created_at=_parse_datetime(data.get('createdAt')),
            has_transcription=data.get('hasTranscription', False),
            has_summarization=data.get('hasSummarization', False),
            transcription_status=data.get('transcriptionStatus', 'unknown'),
            summarization_status=data.get('summarizationStatus', 'unknown'),
            num_speakers=int(data.get('numOfSpeakers', 0) or 0),
            latitude=data.get('latitude'),
            longitude=data.get('longitude'),
            tags=[t.get('name', '') for t in data.get('tags', [])],
            folder_id=data.get('folderId'),
        )
    
    @property
    def duration_str(self) -> str:
        if self.duration < 60:
            return f"{self.duration}s"
        elif self.duration < 3600:
            return f"{self.duration // 60}m"
        else:
            h = self.duration // 3600
            m = (self.duration % 3600) // 60
            return f"{h}h {m}m"


@dataclass
class PocketSummarization:
    """Summarization data for a recording."""
    id: str
    recording_id: str
    summary: str
    action_items: List[dict]
    transcript: Optional[str] = None
    created_at: Optional[datetime] = None


def _parse_datetime(dt_str: Optional[str]) -> Optional[datetime]:
    if not dt_str:
        return None
    try:
        for fmt in ["%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S.%fZ"]:
            try:
                return datetime.strptime(dt_str, fmt)
            except ValueError:
                continue
        return datetime.fromisoformat(dt_str.replace('Z', '+00:00'))
    except:
        return None


def get_token() -> Optional[str]:
    """Get Firebase token from cache file."""
    if TOKEN_CACHE_FILE.exists():
        try:
            data = json.loads(TOKEN_CACHE_FILE.read_text())
            if data.get('expires_at', 0) > datetime.now().timestamp():
                return data.get('access_token')
        except:
            pass
    return None


def save_token(access_token: str, refresh_token: str = None, expires_in: int = 3600):
    """Save token to cache file."""
    data = {
        'access_token': access_token,
        'refresh_token': refresh_token,
        'expires_at': (datetime.now() + timedelta(seconds=expires_in)).timestamp()
    }
    TOKEN_CACHE_FILE.write_text(json.dumps(data))
    print(f"Token saved to {TOKEN_CACHE_FILE}")


def _api_request(endpoint: str, token: str, params: dict = None) -> dict:
    """Make authenticated API request."""
    import urllib.request
    import urllib.parse
    
    url = f"{API_BASE}{endpoint}"
    if params:
        url += '?' + urllib.parse.urlencode(params)
    
    req = urllib.request.Request(url)
    req.add_header('Authorization', f'Bearer {token}')
    req.add_header('Content-Type', 'application/json')
    
    try:
        with urllib.request.urlopen(req, timeout=30) as response:
            return json.loads(response.read().decode())
    except Exception as e:
        raise Exception(f"API error: {e}")


def get_recordings(days: int = 30, limit: int = 50, token: str = None) -> List[PocketRecording]:
    """Get recent recordings."""
    token = token or get_token()
    if not token:
        raise Exception("No token. Run: python3 reader.py extract")
    
    start_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
    params = {
        'limit': limit,
        'include_empty': 'false',
        'sort_by': 'recording_at',
        'sort_order': 'desc',
        'start_date': start_date,
    }
    
    data = _api_request('/recordings', token, params)
    return [PocketRecording.from_api(r) for r in data.get('data', [])]


def get_recording_full(recording_id: str, token: str = None) -> dict:
    """Get recording with transcript, summary, and action items."""
    token = token or get_token()
    if not token:
        raise Exception("No token available.")
    
    data = _api_request(f'/recordings/{recording_id}', token, {'include': 'all'})
    
    recording = PocketRecording.from_api(data.get('recording', {}))
    
    transcription = data.get('transcription', {})
    transcript = transcription.get('transcription', {}).get('text', '') if transcription else ''
    
    summarizations = data.get('summarizations', {})
    summary = ''
    action_items = []
    if summarizations:
        first_key = next(iter(summarizations.keys()), None)
        if first_key:
            v2 = summarizations[first_key].get('v2', {})
            summary = v2.get('summary', {}).get('markdown', '')
            action_items = v2.get('actionItems', {}).get('items', [])
    
    return {
        'recording': recording,
        'transcript': transcript,
        'summary': summary,
        'action_items': action_items,
        'speakers': data.get('speakers', []),
    }


def get_transcript(recording_id: str, token: str = None) -> Optional[str]:
    """Get raw transcript text."""
    token = token or get_token()
    if not token:
        raise Exception("No token available.")
    
    data = _api_request(f'/recordings/{recording_id}', token, {'include': 'all'})
    transcription = data.get('transcription', {})
    if not transcription:
        return None
    return transcription.get('transcription', {}).get('text', None)


def get_summarization(recording_id: str, token: str = None) -> Optional[PocketSummarization]:
    """Get summary for a recording."""
    token = token or get_token()
    if not token:
        raise Exception("No token available.")
    
    data = _api_request(f'/recordings/{recording_id}', token, {'include': 'all'})
    
    summarizations = data.get('summarizations', {})
    if not summarizations:
        return None
    
    first_key = next(iter(summarizations.keys()), None)
    if not first_key:
        return None
    
    summ = summarizations[first_key]
    v2 = summ.get('v2', {})
    
    return PocketSummarization(
        id=summ.get('id', ''),
        recording_id=recording_id,
        summary=v2.get('summary', {}).get('markdown', ''),
        action_items=v2.get('actionItems', {}).get('items', []),
        transcript=None,
        created_at=None,
    )


def search_recordings(query: str, days: int = 90, limit: int = 20, token: str = None) -> List[PocketRecording]:
    """Search recordings by text."""
    token = token or get_token()
    if not token:
        raise Exception("No token available.")
    
    start_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
    params = {
        'limit': limit,
        'search': query,
        'start_date': start_date,
        'sort_by': 'recording_at',
        'sort_order': 'desc',
    }
    
    data = _api_request('/recordings', token, params)
    return [PocketRecording.from_api(r) for r in data.get('data', [])]


def extract_token_from_browser() -> Optional[str]:
    """
    Extract Firebase token from Chrome browser.
    
    Requires Chrome running with remote debugging on port 9222,
    logged into app.heypocket.com.
    """
    js_code = '''(async () => {
        const idbRequest = indexedDB.open("firebaseLocalStorageDb");
        return new Promise((resolve) => {
            idbRequest.onsuccess = () => {
                const db = idbRequest.result;
                const tx = db.transaction("firebaseLocalStorage", "readonly");
                const store = tx.objectStore("firebaseLocalStorage");
                const getAll = store.getAll();
                getAll.onsuccess = () => {
                    const item = getAll.result[0];
                    if(item && item.value && item.value.stsTokenManager) {
                        const tm = item.value.stsTokenManager;
                        resolve(JSON.stringify({
                            accessToken: tm.accessToken,
                            refreshToken: tm.refreshToken,
                            expirationTime: tm.expirationTime
                        }));
                    } else {
                        resolve(JSON.stringify({error: "No token found"}));
                    }
                };
            };
        });
    })()'''
    
    browser_eval = Path.home() / '.factory/skills/browser/eval.js'
    if not browser_eval.exists():
        browser_eval = Path.home() / '.claude/skills/browser/eval.js'
    
    if not browser_eval.exists():
        print("Browser skill not found. Install from: github.com/anthropics/skills")
        return None
    
    try:
        result = subprocess.run(
            [str(browser_eval), js_code],
            capture_output=True, text=True, timeout=30
        )
        
        if result.returncode == 0:
            data = json.loads(result.stdout.strip())
            if 'accessToken' in data:
                save_token(data['accessToken'], data.get('refreshToken'), expires_in=3600)
                return data['accessToken']
            else:
                print(f"Token extraction failed: {data.get('error', 'unknown error')}")
        else:
            print(f"Browser eval failed: {result.stderr}")
    except Exception as e:
        print(f"Token extraction error: {e}")
    
    return None


if __name__ == '__main__':
    if len(sys.argv) > 1 and sys.argv[1] == 'extract':
        print("Extracting token from browser...")
        print("Requirements:")
        print("  1. Chrome running with: browser/start.js --profile")
        print("  2. Logged into app.heypocket.com")
        print()
        [REDACTED_SECRET])
        if [REDACTED_SECRET]"Success! Token saved (first 50 chars: {token[:50]}...)")
        sys.exit(0)
    
    [REDACTED_SECRET])
    if not [REDACTED_SECRET]"No token. Run: python3 reader.py extract")
        sys.exit(1)
    
    days = int(sys.argv[1]) if len(sys.argv) > 1 else 30
    
    print(f"=== Pocket Recordings (last {days} days) ===\n")
    recordings = get_recordings(days=days, limit=20, token=token)
    
    for r in recordings:
        date = r.recorded_at.strftime('%Y-%m-%d') if r.recorded_at else '?'
        status = '✓' if r.has_summarization else '○'
        speakers = f"({r.num_speakers}sp)" if r.num_speakers else ""
        print(f"  {date} | {r.duration_str:>6} | {status} {r.title[:45]} {speakers}")


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)