A modern, elegant file downloader — CLI tool, Node.js/Bun library, and web UI.
| TUI Dashboard | Web UI Dashboard |
|---|---|
![]() |
![]() |
grabr is a local-first download manager featuring chunked parallel downloading, resumable transfers, and automatic database synchronization across all platforms. No cloud, no tracking.
- Unified Database Sync: The CLI, Bun/Hono daemon, and web UI all read/write to the same SQLite database file (
~/.grabr/grabr.db), keeping downloads instantly synced. - YouTube Format Selector: Intercepts YouTube links in browser extensions, Web UI, and the CLI to present a format selector for combined video resolutions (4K, 1080p, 720p, etc.) or audio-only downloads.
- Interactive CLI Dashboard: Responsive split-pane TUI dashboard with live chunk visualizer, inline job addition form, delete confirmation prompts, and shortcuts to open target save directories.
- Browser Integration: Route browser downloads directly to the Grabr daemon using official extensions for Chrome, Edge, and Firefox. Firefox add-on now available on Mozilla Add-ons.
- Version Checking: Automatically alerts you in both the CLI dashboard and Web UI when a newer release of Grabr is available on NPM.
Works with: Node.js ≥18, Bun ≥1.0
npm install -g @linuxctrl/grabr # CLI globally
npm install @linuxctrl/grabr # or as a libraryyt-dlp is required for downloading YouTube videos and fetching format metadata. Install it with your package manager:
| OS | Command |
|---|---|
| macOS | brew install yt-dlp |
| Linux | sudo apt install yt-dlp (Debian/Ubuntu) or sudo pacman -S yt-dlp (Arch) or pip install yt-dlp |
| Windows | winget install yt-dlp or pip install yt-dlp |
Make sure yt-dlp is available on your PATH. Verify with yt-dlp --version.
grabr --help
grabr add <url> # Start a download
grabr add <url> --output ./videos # Custom output directory
grabr add <url> --chunks 8 # 8 parallel chunks
grabr add <url> --name file.zip # Custom filename
grabr list # List all jobs
grabr pause <id|all> # Pause active downloads
grabr resume <id|all> # Resume paused/failed downloads
grabr remove <id> # Remove a job
grabr clear --completed # Clear completed jobs
grabr ui # Open Web UI in browser
grabr daemon start # Run server in background
grabr daemon stop # Stop background server
grabr daemon status # Check daemon statusRun grabr without arguments to open the interactive full-screen TUI dashboard.
Grabr comes with browser extensions to capture downloads and route them directly to the Grabr daemon.
| Browser | Store Status |
|---|---|
| Firefox | ✅ Available on Mozilla Add-ons |
| Google Chrome | 🔜 Coming soon — install manually for now |
| Microsoft Edge | 🔜 Coming soon — install manually for now |
- Automatic Interception: Captures browser clicks on files matching specific extensions (like
.zip,.tar.gz,.dmg,.iso,.exe) or files exceeding custom size thresholds. - Context Menus: Right-click any hyperlink and select "Send link to Grabr" to prompt download.
- Batch Downloader: Highlight text containing multiple URLs, right-click, and select "Download links in selection with Grabr" to queue them in parallel.
- Real-time Toolbar Popup: Includes a live WebSocket progress dashboard, dynamic active badge count, and action controls (Pause/Resume/Delete) right from your toolbar.
- Open Google Chrome and navigate to
chrome://extensions. - Toggle on Developer mode in the top-right corner.
- Click Load unpacked in the top-left and select the
/extension/googledirectory inside this repository.
- Open Microsoft Edge and navigate to
edge://extensions. - Toggle on Developer mode in the bottom-left corner.
- Click Load unpacked and select the
/extension/edgedirectory inside this repository.
- Install directly from Mozilla Add-ons.
- Or for development: open
about:debugging#/runtime/this-firefox, click Load Temporary Add-on..., and selectmanifest.jsonfrom/extension/firefox.
import { Downloader, SpeedMeter, loadConfig, saveConfig } from '@linuxctrl/grabr'
import type { DownloadJob, DownloadOptions, JobStatus, ChunkInfo, GrabrConfig } from '@linuxctrl/grabr'
const downloader = new Downloader()
await downloader.start()
// Add a download
const job = await downloader.addJob('https://example.com/file.zip', {
outputDir: './downloads',
chunks: 4,
filename: 'myfile.zip',
})
// Listen for events
downloader.on('job:progress', ({ jobId, downloadedBytes, speed, eta }) => {
console.log(`${jobId}: ${downloadedBytes} bytes at ${speed} B/s`)
})
downloader.on('job:status', ({ jobId, status }) => {
console.log(`${jobId}: ${status}`)
})
// Control downloads
await downloader.pauseJob(job.id)
await downloader.resumeJob(job.id)
await downloader.removeJob(job.id)
await downloader.stop()interface DownloadJob {
id: string
url: string
filename: string
destination: string
totalBytes: number
downloadedBytes: number
chunks: ChunkInfo[]
status: 'queued' | 'downloading' | 'paused' | 'completed' | 'failed'
speed: number // bytes/sec (EMA smoothed)
eta: number // seconds remaining
createdAt: number
updatedAt: number
error?: string
}
interface ChunkInfo {
index: number
start: number
end: number
downloaded: number
status: 'pending' | 'downloading' | 'done' | 'failed'
}grabr/
├── dist/ # Built output (published to npm)
│ ├── index.js # ESM library
│ ├── index.cjs # CJS library
│ ├── index.d.ts # TypeScript declarations
│ └── cli.js # CLI executable
├── extension/ # Browser extensions
│ ├── google/ # Chrome extension folder
│ ├── edge/ # Edge extension folder
│ └── firefox/ # Firefox extension folder (GECKO compatible)
├── src/
│ ├── index.ts # Public library entry point
│ ├── core/ # Download engine (UI-agnostic)
│ │ ├── downloader.ts # Orchestrator (EventEmitter)
│ │ ├── chunker.ts # HEAD request → split into ranges
│ │ ├── worker.ts # Single chunk fetch
│ │ ├── merger.ts # Assembles chunks into final file
│ │ ├── resume.ts # Resume state files
│ │ ├── config.ts # ~/.grabr/config.json
│ │ └── types.ts # Shared types
│ ├── store/
│ │ ├── db.ts # sql.js SQLite setup
│ │ └── jobs.ts # CRUD for download jobs
│ ├── cli/
│ │ ├── index.tsx # CLI entry (Ink/React)
│ │ ├── commands/ # add, list, pause, resume, remove, clear, ui, daemon
│ │ └── ui/ # Ink components (Dashboard, JobRow, ProgressBar)
│ ├── server/
│ │ ├── index.ts # Hono HTTP + WebSocket server
│ │ └── static/ # Built Web UI assets
│ └── web/ # Web UI source (vanilla TS)
│ ├── index.html
│ ├── main.ts
│ ├── components/
│ └── styles/
├── downloads/ # Default output directory
├── .grabr/ # State directory (db + resume files)
├── package.json
├── tsconfig.json
└── bun.lock
URL → chunker → [chunk workers] → merger → final file
↓
EventEmitter
↓
CLI Dashboard | WebSocket → Browser UI
- Checks
Accept-Rangesheader → parallel chunk download or single stream - Speed calculated with EMA (exponential moving average, α=0.2)
- Retry logic: 3 attempts with exponential backoff
- Filename collision:
file(1).zip,file(2).zip...
Uses sql.js — SQLite compiled to WebAssembly. Works on both Node.js and Bun without native compilation.
Resume state is also saved as JSON files in .grabr/<jobId>.json for crash recovery.
The daemon server uses Hono with Bun's native WebSocket support. The Web UI is vanilla TypeScript with SVG progress rings and CSS custom properties.
# Install
bun install
# Run CLI in dev mode
bun run cli --help
bun run src/cli/index.tsx add https://example.com/file.zip
# Run server
bun run src/server/index.ts
# TypeScript check
bun run typecheck
# Build for production
bun run build
# Build outputs:
# dist/index.js — ESM library
# dist/index.cjs — CJS library
# dist/cli.js — CLI executable
# dist/*.d.ts — TypeScript declarations- Bun (recommended for development) —
curl -fsSL https://bun.sh/install | bash - Node.js 18+ (for production use)
Config file: ~/.grabr/config.json
{
"outputDir": "~/Downloads",
"maxConcurrent": 3,
"defaultChunks": 4,
"serverPort": 7474,
"theme": "dark"
}bun run build
npm publishThe dist/ directory is published, containing the compiled CLI and library.
MIT


