// qbittorrent's tools — living in the module (novox/hq ADR 0039), importing qbittorrent's own // client. They return structured data; the mesh serves them through the sdk's tool harness. import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"; import { QbittorrentClient } from "../client.js"; export function getQbittorrentTools(qb: QbittorrentClient): ToolDefinition[] { return [ { name: "qbittorrent_status", description: "qBittorrent status: version, global transfer rates, and how many torrents are active.", input: {}, run: async () => { const [version, transfer, torrents] = await Promise.all([ qb.getVersion(), qb.getTransferInfo(), qb.getTorrents(), ]); const downloading = torrents.filter((t) => t.progress < 1).length; return { version, transfer, torrents: torrents.length, downloading, seeding: torrents.length - downloading }; }, }, { name: "qbittorrent_torrents", description: "List torrents — name, state, progress and speed. Optional filter narrows the set.", input: { filter: { type: "string", description: "one of all|downloading|seeding|completed|paused|active|inactive|stalled" }, }, run: async (args) => { const items = await qb.getTorrents(args.filter ? String(args.filter) : undefined); return { count: items.length, torrents: items }; }, }, { name: "qbittorrent_add", description: "Add a torrent by magnet link or .torrent URL, optionally into a category.", input: { url: { type: "string", description: "magnet link or http(s) URL to a .torrent" }, category: { type: "string", description: "category name (determines save directory)" }, savepath: { type: "string", description: "explicit save path (overrides the category default)" }, paused: { type: "boolean", description: "add in paused state (default false)" }, }, run: async (args) => { await qb.add( String(args.url), args.category ? String(args.category) : "", args.savepath ? String(args.savepath) : "", args.paused === true || args.paused === "true", ); return { added: String(args.url), category: args.category ? String(args.category) : null }; }, }, { name: "qbittorrent_pause", description: "Pause torrents — a pipe-separated hash list, or 'all' (the default).", input: { hashes: { type: "string", description: "pipe-separated torrent hashes, or 'all' (default)" } }, run: async (args) => { const hashes = args.hashes ? String(args.hashes) : "all"; await qb.pause(hashes); return { paused: hashes }; }, }, { name: "qbittorrent_resume", description: "Resume torrents — a pipe-separated hash list, or 'all' (the default).", input: { hashes: { type: "string", description: "pipe-separated torrent hashes, or 'all' (default)" } }, run: async (args) => { const hashes = args.hashes ? String(args.hashes) : "all"; await qb.resume(hashes); return { resumed: hashes }; }, }, { name: "qbittorrent_delete", description: "Remove torrents by hash, optionally deleting their files on disk.", input: { hashes: { type: "string", description: "pipe-separated torrent hashes, or 'all'" }, deleteFiles: { type: "boolean", description: "also delete downloaded files (default false)" }, }, run: async (args) => { const hashes = String(args.hashes); const deleteFiles = args.deleteFiles === true || args.deleteFiles === "true"; await qb.delete(hashes, deleteFiles); return { deleted: hashes, deleteFiles }; }, }, ]; } // The tools exist only when qBittorrent is configured; without a URL and password, qbittorrent // contributes none rather than failing the whole runtime. registerModuleTools("qbittorrent", (env) => { try { return getQbittorrentTools(QbittorrentClient.fromEnv(env)); } catch { return []; } });