#Building an Action Pack

An action pack is a folder. Everything the assistant needs to load and run your actions lives inside it, and installing a pack is really just dropping that folder into the app's actions/ directory.

#Folder layout

text
my-pack/
    info.json        # metadata about the pack (required)
    phrases.json     # which phrases trigger which action (required)
    setVolume.js     # one file per action
    hello.js
    package.json     # optional — only if you need npm dependencies

The action files can be named whatever you like — the name just has to match the key you use in phrases.json.

#info.json

Metadata for the whole pack. id and name are required; the rest are recommended.

json
{
    "name": "My Pack",
    "description": "A short sentence about what this pack does.",
    "id": "my-pack",
    "version": "1.0.0",
    "apiVersion": 1
}
Field Required What it's for
id yes Unique identifier. Becomes the pack's folder name once installed.
name yes Human-readable name shown in the install prompt.
description no Shown in the install prompt so users know what they're getting.
version no Your pack's version. Bump it when you ship changes.
apiVersion no The pack API your pack targets. Current packs use 1.

Pick an id that's unlikely to collide with someone else's — it's how installs are keyed. If a pack with the same id is already installed, the install is treated as a conflict rather than silently overwriting.

#phrases.json

This maps each action name to a list of phrases that should trigger it. The key is the action name; the value is an array of trigger phrases.

json
{
    "hello": ["hello", "hi", "howdy", "yo"],
    "setVolume": ["set volume to {target}", "change volume to {target}"]
}

Two rules connect this file to the rest of the pack:

  • Each key needs a matching file. The key setVolume requires a setVolume.js next to it. If the file is missing, that action is skipped at load time.
  • {placeholders} become parameters. Anything in curly braces is captured from the user's input and handed to your action. set volume to {target} captures whatever follows into params.target.

Phrase matching has a few more rules worth knowing before you write a lot of them — see Phrases & Matching.

#Action modules

Each action is a CommonJS module that exports an async run function. It receives the captured parameters and returns a result object.

js
// hello.js — the simplest possible action
module.exports = {
    run: async () => {
        return { success: true, message: "hi :D" };
    }
};

#The run contract

  • Signature: run(params)params is an object whose keys are the {placeholder} names from the phrase that matched. It's always safe to default it: run: async (params = {}) => { ... }.
  • Async: run may be async / return a Promise. The assistant awaits it.
  • Return value: an object with:
    • success — a boolean saying whether the action worked.
    • message — the text shown back to the user in the window.

If your action throws, Personal Goober catches it and shows a generic failure message instead, so prefer returning { success: false, message: "..." } with something helpful when you can.

#Using parameters

Read parameters by their placeholder name. Because users phrase things differently, it's common to accept a few aliases and fall back gracefully:

js
// setVolume.js
const loudness = require("loudness");

module.exports = {
    run: async (params = {}) => {
        const target = params.target ?? params.level ?? params.value;
        try {
            loudness.setVolume(target);
            return { success: true, message: "volume set!" };
        } catch {
            return { success: false, message: "i couldn't set the volume" };
        }
    }
};

Parameters always arrive as strings (they're pulled straight from the input text). If you need a number, a boolean, or a specific format, parse and validate it inside run.

#Dependencies

Need an npm package? Add a package.json to your pack with its dependencies:

json
{
    "name": "my-pack",
    "version": "1.0.0",
    "dependencies": {
        "loudness": "^0.4.2"
    }
}

When the pack is installed, Personal Goober runs npm install inside the pack folder for you. A couple of things to keep in mind:

  • Dependencies are installed without running install scripts (--ignore-scripts), so don't rely on postinstall hooks to set things up.
  • The dependency list is surfaced to the user in the install prompt, so keep it honest and minimal.

#A note on where code runs

Your run function executes in the Electron main process — full Node.js, full access to the machine. You can read files, spawn processes, call native modules, and reach the network. That power is the whole point, but it also means users are trusting your pack completely. Write accordingly.

Next: Phrases & Matching to make your triggers fire reliably.