Vyrgent Command

Developer Guide

Building a plugin for Vyrgent

A plugin is a single JavaScript file that gives your assistant a new tool it can call — a custom action, integration, or workflow. This guide covers exactly what a valid plugin needs, a copy-paste template, and how to load it.

The idea

What a plugin is

One .js file that exports a tool. When the file is valid, Vyrgent registers it automatically and the assistant can call it whenever a request matches what the tool does. Plugins live in your ~/.vyrgent/plugins/ folder and hot-reload — save the file and it updates live, no restart.

Under the hood, a plugin is a CommonJS module: it sets module.exports to an object with four required fields.

The contract

What every plugin must export

Vyrgent validates each file on load. If any of the four required fields is missing, the plugin is rejected with a clear error and won't appear as a tool.

FieldTypeWhat it does
name required string Unique tool ID in snake_case — letters, numbers, underscores, no spaces (e.g. currency_convert).
description required string Plain-language summary of what the tool does and when to use it. The assistant reads this to decide whether to call your plugin — so write it carefully.
input_schema required object A JSON Schema describing the arguments your tool accepts. Always type: "object" with properties and a required list.
handler required async function The code that runs. Receives { input, config, services } and returns a string (shown in chat). Throw an Error to report failure.
version optional string Your version tag, e.g. "1.0.0". Handy for tracking changes.
configKeys optional string[] Names of saved settings your plugin reads (like an API key). Their values arrive on config.
timeout optional number Max run time in milliseconds for long jobs (default is a few minutes).

Copy this

The template

A complete, working plugin. Save it as e.g. currency_convert.js, change the four fields to fit your tool, and load it.

currency_convert.js
// A Vyrgent plugin is a CommonJS module: set module.exports to an object.
module.exports = {
  // 1) name — unique, snake_case, no spaces. This is the tool's ID.
  name: "currency_convert",

  // 2) description — the assistant reads THIS to decide when to call the
  //    tool. Say plainly what it does and when to use it.
  description: "Convert an amount from one currency to another (e.g. 100 USD to EUR). Use when the user asks to convert money between currencies.",

  // Optional metadata.
  version: "1.0.0",
  configKeys: [],        // e.g. ["exchangeApiKey"] to read a saved key
  timeout: 30000,        // 30s (optional)

  // 3) input_schema — JSON Schema for the arguments. Give every property a
  //    clear "description"; the assistant fills them from the user's request.
  input_schema: {
    type: "object",
    properties: {
      amount: { type: "number", description: "How much to convert." },
      from:   { type: "string", description: "Source currency code, e.g. USD." },
      to:     { type: "string", description: "Target currency code, e.g. EUR." },
    },
    required: ["amount", "from", "to"],
  },

  // 4) handler — runs when the tool is called. Return a STRING; the text
  //    is what the assistant sees and relays. Throw an Error to fail.
  async handler({ input, config, services }) {
    const { amount, from, to } = input;

    // Validate inputs and return a clear message on bad data.
    if (!amount || !from || !to) return "Please provide amount, from, and to.";

    // Do the work. Fetch is available; so are Node built-ins.
    const res = await fetch(`https://api.exchangerate.host/convert?from=${from}&to=${to}&amount=${amount}`);
    if (!res.ok) throw new Error(`Exchange API error (HTTP ${res.status}).`);
    const data = await res.json();

    // Return a concise, human-readable string.
    return `${amount} ${from} = ${data.result.toFixed(2)} ${to}`;
  },
};
That's a valid plugin. The four required fields are present, the handler is async, and it returns a string. Load it and the assistant can convert currencies on request.

Inside the handler

What your handler receives

Every handler is called with one object: { input, config, services }.

input

The arguments the assistant filled in, matching your input_schema. Read them as input.amount, input.from, etc.

config

The app's saved settings. Anything you listed in configKeys is here — e.g. config.exchangeApiKey. Never hard-code secrets; read them from config.

services

Built-in helpers so you don't reinvent common actions. Call them with await.

services.webSearch() services.sendEmail() services.readEmails() services.whatsappSend() services.readFile() services.writeFile() services.listDirectory() services.runCommand() services.listCalendarEvents() services.createCalendarEvent() services.reportProgress()
Return value: always return a string — it's what the assistant reads and relays to the user. To signal a failure, throw new Error("what went wrong"); the assistant will report it and stop, rather than pretend it worked.

Load it

Installing your plugin

  1. Open Vyrgent → Settings → Plugins.
  2. Click Install from file and choose your .js file. (Or copy it straight into ~/.vyrgent/plugins/.)
  3. It loads instantly and hot-reloads on every save — no restart needed.
  4. Ask the assistant to do the thing your plugin does. It routes to your tool automatically when the request matches the name or description.

Do it well

Best practices

If it won't load

Common validation errors

Vyrgent will refuse a file and tell you exactly what's wrong. The four messages map straight to the required fields:

ErrorFix
missing nameAdd a non-empty name string.
missing descriptionAdd a non-empty description string.
missing input_schemaAdd an input_schema object (use { type:"object", properties:{}, required:[] } if the tool takes no arguments).
missing handler functionAdd a handler that is a function.
must export an objectMake sure you set module.exports = { … }.
Nothing happens when I ask for it? The file loaded, but the assistant didn't route to it. Sharpen the description (and use words the user would say), then try again — matching is driven by the tool's name and description.