Claude
Skills
Sign in
โ† Back

remarkable

Included with Lifetime
$97 forever

Manage reMarkable tablet documents - upload PDFs/EPUBs, download with annotations, backup notebooks, and list/search documents. Use when the user mentions reMarkable, wants to send files to their tablet, download annotated PDFs, or backup their notebooks.

Generalscriptsassets

What this skill does


# reMarkable Document Management

Upload, download, and manage documents on your reMarkable tablet via the cloud API.

## Trigger Phrases

- "upload [file] to remarkable"
- "send this PDF to my remarkable"
- "download [name] from remarkable"
- "get my annotated [document]"
- "backup my remarkable notebooks"
- "list my remarkable documents"
- "find [name] on remarkable"
- "register remarkable" (first-time setup)
- "sync morning pages to obsidian"
- "sync remarkable to obsidian"

## Prerequisites

1. **reMarkable Account**: Active reMarkable cloud sync (Connect subscription not required for basic sync)
2. **1Password CLI**: For secure token storage
3. **Node.js 18+**: For running TypeScript scripts with rmapi-js
4. **Python 3.9+** (optional): For annotation rendering

### First-Time Setup

Before using this skill, register the device:

1. Go to https://my.remarkable.com/device/browser/connect
2. Get the 8-character code displayed
3. Run the registration workflow (see Register Device section)
4. The device token is stored in 1Password item `Remarkable`, field `device_token`

## Dependencies

```bash
# Install rmapi-js for API access
npm install rmapi-js

# Install Python dependencies for annotation rendering (optional)
uv pip install rmscene PyMuPDF svgwrite
```

## Authentication

The reMarkable API uses a two-tier token system:

| Token Type | Lifetime | Storage |
|------------|----------|---------|
| Device Token | Permanent | 1Password `Remarkable.device_token` |
| User Token | 24 hours | Auto-refreshed by rmapi-js |

### Get Device Token from 1Password

```bash
op item get "Remarkable" --fields "device_token" --reveal
```

### Initialize API

```typescript
import { remarkable } from "rmapi-js";

// Get token from 1Password
const deviceToken = await $`op item get "Remarkable" --fields "device_token" --reveal`.text();

// Initialize API (handles user token refresh automatically)
const api = await remarkable(deviceToken.trim());
```

## Workflows

### 1. Register Device (First-Time Setup)

```typescript
import { register } from "rmapi-js";

// User provides 8-char code from my.remarkable.com/device/browser/connect
const code = "abcd1234"; // Get from user via AskUserQuestion

// Exchange code for permanent device token
const deviceToken = await register(code);

// Store in 1Password
await $`op item edit "Remarkable" "device_token=${deviceToken}"`;
// Or create if doesn't exist:
// await $`op item create --category="API Credential" --title="Remarkable" "device_token=${deviceToken}"`;

console.log("Registration complete! Device token stored in 1Password.");
```

### 2. List Documents

```typescript
import { remarkable } from "rmapi-js";

const deviceToken = await $`op item get "Remarkable" --fields "device_token" --reveal`.text();
const api = await remarkable(deviceToken.trim());

// Fetch all documents and folders
const items = await api.listItems();

// Build hierarchy
const folders = new Map();
const documents = [];

for (const item of items) {
  if (item.type === "CollectionType") {
    folders.set(item.id, { ...item, children: [] });
  } else {
    documents.push(item);
  }
}

// Organize documents into folders
for (const doc of documents) {
  const parent = doc.parent || "";
  if (parent && folders.has(parent)) {
    folders.get(parent).children.push(doc);
  }
}

// Display hierarchy
console.log("=== Root ===");
for (const doc of documents.filter(d => !d.parent || d.parent === "")) {
  console.log(`  ๐Ÿ“„ ${doc.visibleName} (${doc.hash.slice(0, 8)})`);
}

for (const [id, folder] of folders) {
  if (!folder.parent || folder.parent === "") {
    console.log(`\n๐Ÿ“ ${folder.visibleName}/`);
    for (const child of folder.children) {
      console.log(`  ๐Ÿ“„ ${child.visibleName}`);
    }
  }
}
```

### 3. Search Documents

```typescript
// Fuzzy search by name
function findDocuments(items, query) {
  const queryLower = query.toLowerCase();
  return items
    .filter(item => item.type !== "CollectionType")
    .filter(item => item.visibleName.toLowerCase().includes(queryLower))
    .sort((a, b) => {
      // Exact match first, then prefix match, then contains
      const aName = a.visibleName.toLowerCase();
      const bName = b.visibleName.toLowerCase();
      if (aName === queryLower) return -1;
      if (bName === queryLower) return 1;
      if (aName.startsWith(queryLower)) return -1;
      if (bName.startsWith(queryLower)) return 1;
      return aName.localeCompare(bName);
    });
}

const matches = findDocuments(items, "meeting notes");
if (matches.length === 0) {
  console.log("No documents found matching query");
} else if (matches.length === 1) {
  console.log(`Found: ${matches[0].visibleName}`);
} else {
  // Use AskUserQuestion to let user select
  console.log("Multiple matches found:");
  matches.forEach((m, i) => console.log(`${i + 1}. ${m.visibleName}`));
}
```

### 4. Upload PDF/EPUB

```typescript
import { remarkable } from "rmapi-js";
import { readFile } from "fs/promises";

const deviceToken = await $`op item get "Remarkable" --fields "device_token" --reveal`.text();
const api = await remarkable(deviceToken.trim());

// Read file from disk
const filePath = "/path/to/document.pdf";
const buffer = await readFile(filePath);
const fileName = filePath.split("/").pop().replace(/\.(pdf|epub)$/i, "");

// Detect file type from extension
const isPdf = filePath.toLowerCase().endsWith(".pdf");

// Upload to root (simple API - works with all schema versions)
let entry;
if (isPdf) {
  entry = await api.uploadPdf(fileName, new Uint8Array(buffer));
} else {
  entry = await api.uploadEpub(fileName, new Uint8Array(buffer));
}

console.log(`Uploaded: ${entry.visibleName} (hash: ${entry.hash})`);

// Alternative: Upload to specific folder using low-level API
// const folderId = "folder-uuid-here";
// entry = await api.putPdf(fileName, new Uint8Array(buffer), { parent: folderId });
```

### 5. Download Document (Raw ZIP)

```typescript
import { remarkable } from "rmapi-js";
import { writeFile, mkdir } from "fs/promises";

const deviceToken = await $`op item get "Remarkable" --fields "device_token" --reveal`.text();
const api = await remarkable(deviceToken.trim());

// Find document by name (use search workflow above)
const items = await api.listItems();
const doc = items.find(i => i.visibleName === "Target Document");

if (!doc) {
  throw new Error("Document not found");
}

// Download as ZIP archive (contains all .rm files, metadata, original PDF if applicable)
const zipData = await api.getDocument(doc.hash);

// Save to downloads directory
const today = new Date().toISOString().split("T")[0];
const downloadDir = `data/downloads/${today}`;
await mkdir(downloadDir, { recursive: true });

const safeName = doc.visibleName.replace(/[^a-zA-Z0-9-_]/g, "_");
const zipPath = `${downloadDir}/${safeName}.zip`;
await writeFile(zipPath, zipData);

console.log(`Downloaded: ${zipPath}`);

// Also get original PDF if it was a PDF document
try {
  const pdfData = await api.getPdf(doc.hash);
  const pdfPath = `${downloadDir}/${safeName}-original.pdf`;
  await writeFile(pdfPath, pdfData);
  console.log(`Original PDF: ${pdfPath}`);
} catch (e) {
  // Not a PDF document or no original available
  console.log("No original PDF available (may be a notebook)");
}
```

### 6. Download with Annotation Rendering

After downloading the raw ZIP:

```bash
# Extract ZIP
DOWNLOAD_DIR="data/downloads/2024-01-15"
DOC_NAME="Meeting_Notes"
unzip "${DOWNLOAD_DIR}/${DOC_NAME}.zip" -d "${DOWNLOAD_DIR}/${DOC_NAME}_extracted"

# Run annotation renderer (if .rm files exist)
python skills/remarkable/assets/scripts/render-annotations.py \
  "${DOWNLOAD_DIR}/${DOC_NAME}_extracted" \
  "${DOWNLOAD_DIR}/${DOC_NAME}-annotated.pdf"
```

**Python Annotation Rendering** (`render-annotations.py`):

```python
#!/usr/bin/env python3
"""Render reMarkable annotations onto PDF."""

import sys
import json
from pathlib import Path
from rmscene import read_blocks
from rmscene.scene_items import Line
import fitz  
Files: 64
Size: 271.6 KB
Complexity: 94/100
Category: General

Related in General