glymonir-cli
v0.8.0
Published
Official CLI + Node.js client for the Glymonir image hosting API. Uploads, batch operations, and gallery read in one command.
Maintainers
Readme
glymonir-cli
Official command-line tool for the Glymonir image hosting API.
Upload one picture or ten thousand in a single command — the CLI runs the same client-side preprocessing the web app does (thumb / preview WebP, 512×512 embedding JPEG, thumbhash placeholder, EXIF, colour palette, SHA-256 dedup) and drives the R2 two-stage upload flow end-to-end.
What's new in 0.7.0 (memory — lower peak RSS, no upload-content changes):
Per-picture variant generation now runs serially (one variant at a time) off the single decoded RGBA buffer, instead of fanning all six encodes + extractors out at once. On the low-core hosts these batches typically run on, the old parallel fan-out gave near-zero speed-up (the ops just contended for the same 1-2 cores) while multiplying peak memory — measured ~11% slower but ~40% lower peak RSS on a pinned 2-core box. Parallelism now lives where it belongs: across pictures via--concurrency N(in-process) orxargs -P(OS-isolated). The--low-memoryflag is removed — the default is now the memory-safe path. A 50 MB per-file size guard (matching the backend) rejects oversized files before decode.
Install
# Zero-install via npx (recommended):
npx glymonir-cli --help
# Or install globally:
npm install -g glymonir-cli
glymonir --helpRequires Node.js ≥ 20.
Quick start
export GLYMONIR_API_KEY=gly_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Upload a single image to the public gallery
npx glymonir-cli upload photo.jpg
# Upload to a specific library
npx glymonir-cli upload --library 1234 photo.jpg
# Upload with metadata
npx glymonir-cli upload \
--name "Mt. Fuji at sunrise" \
--introduction "Captured 2024-09-15 from Hakone" \
--tags mountain,sunrise,japan \
photo.jpg
# Batch upload (shell expands the glob, default runs sequentially)
npx glymonir-cli upload ~/photos/*.jpgConcurrency & memory
Each picture is processed one variant at a time from a single decoded
buffer, so a single in-flight task peaks around 60-100 MB resident (the
decoded source + one encode working set + Node heap headroom). The CLI
defaults to --concurrency 1 — sequential, lowest memory, safe on any
laptop or 2 GB VPS with no special flags.
To go faster, pick one of two parallelism models:
1. In-process — --concurrency N (simplest; shared progress bar +
resume state). Each extra in-flight task adds another ~60-100 MB, so size
N to your RAM:
| Total system RAM | Suggested --concurrency |
|---|---|
| ≤ 2 GB | 1 (default) |
| 4 GB | 2 |
| 8 GB | 4 |
| 16 GB+ | 8 |
The CLI auto-warns when it detects total RAM < 2 GB and concurrency > 1.
On a tight host you can also cap V8's heap: prefix
NODE_OPTIONS='--max-old-space-size=512'.
2. Multi-process — xargs -P (maximum isolation; each picture gets
its own OS process and heap, so one bad file can never affect another):
# Linux / macOS — one process per core, --concurrency 1 inside each
ls ~/photos/*.jpg | xargs -P "$(nproc)" -n 1 \
glymonir upload --resumeUse model 1 for the built-in progress bar and resume state; use model 2 when you want hard process-level isolation and already drive parallelism from your shell or CI.
Batch & advanced flags
| Flag | What it does |
|---|---|
| -c, --concurrency <N> | Run N pictures in parallel inside one process (shared progress bar + resume state). Default 1 (sequential). Each picture is processed serially internally; N scales the number of pictures in flight, so peak memory ≈ N × single-task. For hard process isolation instead, drive parallelism with xargs -P (see above). |
| --retries <N> | Retry budget per network call. Only transient errors (HTTP 5xx, 408, 429, classic socket failures) retry; 4xx fails fast. Default 3. |
| --retry-backoff <S> | Base seconds for exponential backoff with full jitter. Default 2. |
| --manifest <jsonl> | One JSON object per line, {"file":"…","name":"…","intro":"…","tags":[…],"libraryId":N}. Per-file metadata overrides CLI defaults. |
| --resume | Skip files already marked ok in .glymonir-state.jsonl. Failed and skipped entries re-attempt. Crash-resumable, latest-status-wins. |
| --state-file <path> | Override the default .glymonir-state.jsonl path (e.g. for parallel batches into different libraries). |
| --dry-run | Preprocess + /check only — no R2 PUT, no /finalize, no DB write. Reports per-file fresh vs. dedup status + summary. |
| --output json | Emit one JSONL line per file to stdout: {file, status, id?, sha?, code?, message?, ts}. Suppresses the progress bar. |
| --no-skip-small | Disable the local "skip files < 1 MB when targeting the public gallery" filter. By default the CLI saves a full round-trip on guaranteed-reject files. |
Realistic workflows
Resumable 10k-file batch:
npx glymonir-cli upload \
--concurrency 2 \
--retries 3 \
--resume \
~/photos/*.jpg
# Power cut / network drop / Ctrl-C → re-run the same command. Already-
# uploaded files skip instantly; failures retry automatically.Per-file metadata via manifest:
# batch.jsonl (relative paths resolve against the manifest's directory)
{"file":"./shot-01.jpg","name":"Mt. Fuji at sunrise","tags":["nature","mountain"]}
{"file":"./shot-02.jpg","name":"Tokyo skyline","intro":"Captured 2024-09 from Tokyo Tower"}npx glymonir-cli upload \
--manifest batch.jsonl \
--concurrency 2 \
--resumeMachine-readable output (for scripting / CI):
npx glymonir-cli upload --output json ~/photos/*.jpg | jq -c .
# {"file":"/home/me/photos/a.jpg","status":"ok","id":"2059...","sha":"abc…","ts":"2026-…"}
# {"file":"/home/me/photos/b.jpg","status":"fail","code":40000,"message":"…","ts":"2026-…"}Memory-constrained hosts (≤2 GB RAM):
# The default (--concurrency 1, serial per-picture processing) is already
# memory-safe on a 2 GB box. Optionally cap V8's heap to keep RSS tight:
NODE_OPTIONS='--max-old-space-size=512' \
npx glymonir-cli upload --resume ~/photos/*.jpgWhere to get an API key
- Open your Glymonir profile → Settings → API Keys
- Click Create new key
- Pick the scopes you need:
gallery:upload— required for theuploadcommand (admin only — public gallery uploads bypass the daily count quota)gallery:read— required for reading the public gallery (Pro plan or admin)
- Copy the
gly_live_…plaintext — it's shown exactly once
Eligibility: API keys require a Pro plan or admin role. Free / Starter accounts cannot issue keys.
Configuration
| Setting | Env variable | CLI flag | Default |
| --- | --- | --- | --- |
| API key | GLYMONIR_API_KEY | -k, --api-key | (required) |
| Backend base URL | GLYMONIR_API_BASE | -b, --api-base | https://api.glymonir.com/api |
Local-dev users with a self-hosted backend: set GLYMONIR_API_BASE=http://localhost:8123/api.
Picture upload requirements
The backend enforces these rules on every upload:
| Rule | Public gallery (no --library) | Library (with --library) |
| --- | --- | --- |
| Format | JPEG / PNG / WebP | JPEG / PNG / WebP |
| Max size | 50 MB | 50 MB |
| Min size | 1 MB (quality floor) | none |
| Per-user daily quota | 30/day (Pro) · 200/day (whitelist) · ∞ (admin) | none |
A picture that violates a rule produces a non-zero code from /picture/upload/r2/check; the CLI prints it inline and exits non-zero. With --no-skip-small off (default), public-gallery uploads under 1 MB are skipped locally — no round-trip wasted.
Using as a library
The package also exports its primitives so you can build your own flows:
import { preprocess, uploadPicture } from 'glymonir-cli'
const variants = await preprocess('photo.jpg')
const picture = await uploadPicture(
{ variants, name: 'Mt. Fuji', tags: ['nature'] },
{
apiBase: 'https://api.glymonir.com/api',
apiKey: process.env.GLYMONIR_API_KEY!,
retry: { retries: 3, backoffSeconds: 2 },
},
)
console.log('uploaded id =', picture.id)Error codes (subset)
The CLI surfaces the backend's BaseResponse.code directly. The most common ones you'll see:
| Code | What it means |
| --- | --- |
| 0 | Success |
| 40000 | Invalid request (size below 1 MB gallery floor, unsupported format, missing field…) |
| 40100 | Missing / wrong / revoked / expired API key |
| 40101 | Key doesn't carry the required scope (e.g. upload without gallery:upload) |
| 42900 | Per-key rate limit hit — response carries Retry-After in seconds |
See the Glymonir API reference for the complete table.
License
MIT
