YuKumo
Changelog

v1.9.0 — First-Class NodeLink

NodeLink node type and capability system, mixed pools, server-side plugin filters, youtube-source config, Player.skipTo, and a zero-warning lint pass.

Released 2026-09-20 · npm install yukumo@1.9.0

Connect to Lavalink, NodeLink, or both at once in one pool with the same API. Each node now reports what it can do, so NodeLink-only features are routed to a capable node and never fail silently on a plain Lavalink node.

Added

  • NodeConfig.type: "lavalink" | "nodelink" | "auto" (default "auto", detected from /v4/info). The legacy isNodeLink flag still works. New createNodeLinkNode() / createLavalinkNode() config helpers.
  • node.type, node.version, node.capabilities, node.sourceManagers — resolved on ready.
import { YuKumo, createLavalinkNode, createNodeLinkNode } from "yukumo";

const kumo = new YuKumo({
  nodes: [
    createLavalinkNode({ host: "ll", port: 2333, password: "x" }),
    createNodeLinkNode({ host: "nl", port: 2333, password: "x" }),
  ],
  send: (guildId, payload) => client.guilds.cache.get(guildId)?.shard.send(payload),
});

Capability system

node.supports(feature), node.supportsFilter(name), and node.assertSupports(feature) over a typed NodeFeature union. Feature requests are routed to a capable node, and a mismatch throws a typed error instead of failing silently.

node.supports("voiceReceive");                    // true on NodeLink
const n = kumo.nodes.pick(guildId, { feature: "lyrics" }); // capability-aware pick
  • New YukumoUnsupportedFeatureError (code: "UNSUPPORTED_FEATURE") with feature, nodeId, nodeType.
  • Cross-family failover: Player.setNode() drops filters the target node can't run (NodeLink extras onto Lavalink) with a debug log — audio keeps playing rather than erroring.

Server-side plugin filters

player.setPluginFilter(name, settings) (and FilterChain.setPluginFilter() / getPluginFilter() / hasPluginFilter()) pass arbitrary filter-plugin settings through Lavalink v4's pluginFilters — works with LavaDSPX and any filter plugin. Pass false/null to remove.

await player.setPluginFilter("normalization", { maxAmplitude: 0.75, adaptive: true });

youtube-source config

node.rest.getYouTubeStatus(), setYouTubePoToken(poToken, visitorData), and setYouTubeRefreshToken(refreshToken?, skipInitialization?) drive the youtube-source plugin's poToken / OAuth to bypass YouTube bot-detection.

Player.skipTo

player.skipTo(index) jumps to a queue index and starts it on the node, so the queue cursor never points at a not-yet-played track while old audio is still running. Skipped-over tracks go to history.

Typed server-plugin markers

createLavaSrcPlugin() / createSponsorBlockPlugin() / createFloweryTTSPlugin() return introspectable markers (isServerPlugin, recorded options) so manager.plugins.get(name) can confirm the plugin is expected.

Changed

  • Zero-warning lint — the WebSocket client, framework adapters, REST client, and lyrics client no longer use any; a shared isVoicePacket() guard replaces per-adapter packet checks. The manager's event forwarding is fully typed against the public EventMap.

Fixed

  • Concurrent WebSocketClient.connect() returns the same in-flight promise, so all callers await the real socket open (or failure) instead of resolving optimistically.
  • Stale-socket handler guards, timed-out-connect abort, close() keeps manager wiring, and unref()'d reconnect timers (a dead node no longer holds the process alive).
  • Node failover is deferred during a resuming node's window; a removed playing track is held as "current" until it ends; previous() no longer duplicates in queue-repeat; noReplace keeps the position baseline; a paused-start track stays paused; resolveTrack honors the playlist selectedTrack; Track.toJSON() round-trips the requester.

On this page