remarkable
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.
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 Related in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.