Nyora is a good tool for AI agents and automation: a tiny, typed surface that turns natural-language intents ("find X on source Y", "get chapter 1's pages") into deterministic calls. This page is example-dense and copy-pasteable.
There are three ways to drive Nyora programmatically — pick one:
import { Nyora }) — best for Node agents.nyora-cli --json — best for shell/tool-calling agents.NyoraServer REST — best for cross-process / cross-language agents.You almost never need more than this:
import { Nyora } from "nyora-sdk";
// optional, for typed handling:
import type { Source, Manga, MangaChapter, MangaPage } from "nyora-sdk";
import { ParserRuntimeError } from "nyora-sdk";
The whole agent loop is: find a source → search → details → pages → (download).
import { Nyora } from "nyora-sdk";
import { createWriteStream } from "node:fs";
import { mkdir } from "node:fs/promises";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import * as path from "node:path";
async function fetchFirstChapter(sourceQuery: string, title: string, outDir: string) {
const client = new Nyora();
try {
// find a source by id or fuzzy name
const source = client.sources.find(sourceQuery);
// search it
const results = await client.manga.search(source.id, title);
if (!results.entries.length) return { ok: false, reason: "no search results" };
const manga = results.entries[0];
// details + chapters
const { chapters } = await client.manga.details(source.id, manga.url, {
title: manga.title,
});
if (!chapters.length) return { ok: false, reason: "no chapters" };
// resolve page image URLs for the first chapter
const pages = await client.manga.pages(source.id, chapters[0].url, {
branch: chapters[0].branch,
});
// download the images (honoring per-page headers like Referer)
await mkdir(outDir, { recursive: true });
const saved: string[] = [];
for (let i = 0; i < pages.length; i++) {
const p = pages[i];
const headers = { "User-Agent": "Mozilla/5.0", ...p.headers };
const res = await fetch(p.url, { headers, redirect: "follow" });
if (!res.ok || !res.body) continue;
const ext = path.extname(new URL(p.url).pathname) || ".jpg";
const file = path.join(outDir, `${String(i + 1).padStart(3, "0")}${ext}`);
await pipeline(Readable.fromWeb(res.body as never), createWriteStream(file));
saved.push(file);
}
return { ok: true, manga: manga.title, pages: pages.length, saved };
} catch (err) {
if (err instanceof ParserRuntimeError) return { ok: false, reason: err.message };
throw err;
} finally {
client.close();
}
}
// fetchFirstChapter("mangadex", "Frieren", "./out");
The CLI's
downloadsubcommand does the same fetch loop but packs the pages into a single.cbzarchive instead of loose files — prefer it for shell agents (next section).
nyora-cli --json from a shell agentEvery subcommand supports --json (place it before the subcommand). Parse the
stdout; check the exit code (0 ok, 1 handled error, 2 unknown command).
# list sources as JSON
nyora-cli --json sources --search asura
# search → first URL
URL=$(nyora-cli --json search -s mangadex "Frieren" | jq -r '.entries[0].url')
# details → first chapter URL
CH=$(nyora-cli --json details -s mangadex "$URL" | jq -r '.chapters[0].url')
# pages → list of image URLs
nyora-cli --json pages -s mangadex "$CH" | jq -r '.[].url'
# download as a .cbz (exit 0 if any page was packed); capture the file path
nyora-cli --json download -s mangadex -o ./out "$CH" | jq -r '.file'
JSON shapes returned by --json:
| Command | JSON |
|---|---|
sources |
Source[] |
search / popular / latest |
{ entries: Manga[], hasNextPage: boolean } |
details |
{ manga: Manga, chapters: MangaChapter[] } |
pages |
MangaPage[] ({ url, headers }) |
download |
{ file: string, pages: number, total: number } (.cbz path + counts) |
update |
{ updated, version, bundlePath, sourcesPath } |
version |
{ package, ota } |
serve |
{ baseUrl } |
NyoraServer REST APIFor a long-running agent or a non-Node caller, run the server once and hit its
endpoints. It auto-writes a helper.port file for discovery.
import { NyoraServer, readBaseUrlFromPortFile } from "nyora-sdk";
// start once
const server = new NyoraServer({ port: 0 });
const baseUrl = await server.start(); // also discoverable via readBaseUrlFromPortFile()
const j = async (p: string) => (await fetch(`${baseUrl}${p}`)).json();
const { sources } = await j("/sources");
const id = sources[0].id;
const popular = await j(`/sources/popular?id=${id}&page=1`);
const details = await j(
`/manga/details?id=${id}&url=${encodeURIComponent(popular.entries[0].url)}`,
);
const pages = await j(
`/manga/pages?id=${id}&url=${encodeURIComponent(details.chapters[0].url)}`,
);
await server.stop();
See the Server guide for the full endpoint and error table.
Before a batch run, refresh the OTA bundle so the agent uses the latest parsers:
const client = new Nyora();
const { available } = await client.checkUpdate();
if (available) await client.update(); // sha256-verified, then reloads the runtime
nyora-cli update # CLI equivalent
| Intent | SDK (in-process) | CLI (--json) |
REST endpoint |
|---|---|---|---|
| List all sources | client.sources.list() |
nyora-cli sources |
GET /sources |
| Find a source by name/id | client.sources.find("asura") |
nyora-cli sources --search asura |
GET /sources (filter client-side) |
| Popular manga | client.manga.popular(id, page) |
nyora-cli popular -s id -p N |
GET /sources/popular?id=&page= |
| Latest manga | client.manga.latest(id, page) |
nyora-cli latest -s id -p N |
GET /sources/latest?id=&page= |
| Search a source | client.manga.search(id, q, page) |
nyora-cli search -s id -p N "q" |
GET /sources/search?id=&q=&page= |
| Manga details + chapters | client.manga.details(id, url, { title }) |
nyora-cli details -s id "url" |
GET /manga/details?id=&url=&title= |
| Chapter page image URLs | client.manga.pages(id, url, { branch }) |
nyora-cli pages -s id "url" |
GET /manga/pages?id=&url=&branch= |
Download a chapter (.cbz) |
(loop fetch over pages) |
nyora-cli download -s id -o OUT "url" |
(loop over /manga/pages) |
| Check for updates | client.checkUpdate() |
— | — |
| Apply OTA update | client.update({ force }) |
nyora-cli update [--force] |
— |
| Run a helper | new NyoraServer().start() |
nyora-cli serve |
— |
| Health check | — | — | GET /health |
client.close() (or server.stop()) when finished — the engine owns
a jsdom window. Use try/finally.source.id, not the display name, for manga.* calls and REST id=.entries/pages array rather than throwing.
Check entries.length / pages.length, and treat ParserRuntimeError as
the hard-failure case.headers when downloading. Some sources require Referer; each
MangaPage carries the headers it needs in page.headers.update() before long batch runs so fixes apply.