projects
Jasper
Jasper imports your Instagram photos to Grain.social while preserving original timestamps. Your memories appear with their original dates, not the import date.
The name follows ATProto import tools using mineral names — a nod to the pattern established by Malachite. Jasper is a red-orange quartz, fitting for something that preserves photographic memories.
What it does
- Preserves timestamps — Photos appear with their original Instagram dates
- Handles all export formats — Works with 2022, 2023, 2024, and 2025 Instagram exports
- Skips duplicates — Already-imported photos are detected and skipped
- Dry run mode — Preview what would be imported before committing
- OAuth authentication — Secure login via your existing AT Protocol identity
Installation
# Install globally with pnpm
pnpm install -g @ewanc26/jasper
# Or use directly with npx
npx @ewanc26/jasper -i instagram-export.zip --dry-run
Usage
Interactive Mode
Run without arguments for guided prompts:
jasper
Command Line
# Import from ZIP
jasper -i instagram-export.zip
# Import from extracted directory
jasper -i instagram-export/
# Preview without posting (dry run)
jasper -i instagram-export.zip --dry-run
# Limit to first 50 posts
jasper -i instagram-export.zip --limit 50
# Skip confirmation prompts
jasper -i instagram-export.zip -y
# More verbose output
jasper -i instagram-export.zip -v
Authentication
OAuth (Recommended)
# Sign in via browser
jasper --oauth-login
# Sessions are stored in ~/.jasper/oauth.json
OAuth uses a loopback callback server on port 8766. Your browser opens to your PDS's authorisation screen, you approve, and the session is saved automatically. Subsequent imports use the stored session — no re-authentication needed.
App Password
If OAuth isn't available, you can use an app password:
jasper -i export.zip --handle your.handle --password your-app-password
Generate an app password at bsky.app/settings/app-passwords.
Getting Your Instagram Export
- Go to Instagram's download page
- Request a download of your information
- Select "Some of your information" → "Posts" and "Archived posts"
- Choose JSON format
- Download the ZIP file when ready
Jasper will locate posts_1.json automatically, handling all export format variations.
What Gets Imported
Imported:
- ✅ Photos (JPEG, PNG, WebP, GIF)
- ✅ Original timestamps
- ✅ Captions (as alt text)
- ✅ Carousel posts (multiple photos)
Not imported:
- ❌ Videos (Grain doesn't support video posts yet)
- ❌ Stories
- ❌ Reels
Options
| Option | Description |
|---|---|
-i, --input <path> |
Path to Instagram export ZIP or directory |
--dry-run |
Preview posts without importing |
--limit <N> |
Import at most N posts |
--reverse |
Process newest posts first (default: oldest first) |
-v, --verbose |
Enable debug logging |
-q, --quiet |
Suppress non-essential output |
-y, --yes |
Skip confirmation prompts |
--oauth-login |
Sign in via OAuth |
--logout [DID] |
Sign out (removes stored session) |
--list-sessions |
List stored OAuth sessions |
Data Storage
All data stays on your machine:
| Location | Content |
|---|---|
~/.jasper/oauth.json |
OAuth session tokens |
~/.jasper/logs/ |
Debug log files |
No data is sent to any server except your chosen Grain account.
OAuth Scope
Jasper requests minimal permissions:
atproto blob:*/* repo:social.grain.photo
blob:*/*— Upload images as blobsrepo:social.grain.photo— Write to Grain's photo collection
This follows ATProto's granular permission model — no broad transition:generic scope.
Development
Build from source:
git clone https://github.com/ewanc26/pkgs.git
cd pkgs
pnpm install
pnpm --filter @ewanc26/jasper build
Run in dev mode:
pnpm --filter @ewanc26/jasper dev
Requirements
- Node.js 18+
- A Grain.social account (use your existing AT Protocol identity)
License
AGPL-3.0-only.
Contact
Ewan Croft — contact@ewancroft.uk
← all docs