/docs / plugins

Plugins

A plugin gives employees something to act on: an online service, a business system, or a program on the computer. You build it as a command-line binary and describe it with a plugin.json manifest. Employees call its commands, and Nebo handles installation, per-employee accounts and updates.

How plugins work

A plugin is an executable plus a manifest. Nebo keeps no plugin process running between calls. Each time an employee uses the plugin, Nebo starts the binary with a command and arguments, waits for it to exit, and hands the output back to the employee.

  • Commands are arguments. The employee chooses a command, such as notes search, and named arguments. Nebo passes each argument as --name value. There is no shell, so pipes and redirects never apply.
  • Output is stdout. Write results to stdout. JSON works well.
  • Standard input is closed for ordinary calls. Hooks, routes and chat providers use stdin; see the plugin.json reference.

Every installed and enabled plugin appears to employees as one tool named plugin__<slug>. An employee calls it like this:

{
  "command": "notes search",
  "args": { "query": "quarterly plan", "limit": "5" },
  "display": "Search notes for the quarterly plan"
}

Nebo runs:

notes-sync notes search --query "quarterly plan" --limit 5
ParameterTypeMeaning
commandstring, requiredThe plugin command and any positional words.
argsobject of stringsNamed arguments. Each becomes --key value.
timeoutintegerSeconds to wait. Default 120, capped by the manifest’s permissions.maxTimeoutSeconds.
displaystringThe sentence the owner sees when the call needs approval.

Build your first plugin

Write a binary in any language that reads its command from the arguments and prints a result. Then place it next to a plugin.json in the user plugins directory of a bot.

<data>/user/plugins/notes-sync/
├── plugin.json      # runtime manifest
├── notes-sync       # your binary, named after the slug
└── skills/          # optional: skills that teach employees to use it
    └── notes-sync/
        └── SKILL.md
{
  "id": "notes-sync",
  "slug": "notes-sync",
  "name": "Notes Sync",
  "version": "0.1.0",
  "description": "Search, read and create notes in Example Notes.",
  "platforms": {}
}

id, slug, name, version and platforms are required. In the user plugins directory the platforms map can be empty: Nebo looks for a binary named after the slug next to plugin.json, then in target/release/ and target/debug/. When you publish, the marketplace fills in platforms for every binary you upload.

<data> is ~/Library/Application Support/Nebo on macOS, %APPDATA%\Nebo on Windows, and ~/.local/share/nebo on Linux, or the value of NEBO_HOME. A plugin in the user directory overrides a marketplace plugin with the same slug, which makes it the place to test a new build.

Teach employees to use it

Put a skills/ folder next to plugin.json. Each skill in it is available to every employee while the plugin is installed, and lists the plugin as a dependency automatically. Use the skill to say which commands exist, what they return, and when to use each. Name the most useful skills in entrySkills so the plugin's tool description points to them first.

The marketplace listing is a separate file, PLUGIN.md. Nebo does not read it at run time; it is the page people see before they install. See Shipping a plugin.

Environment

Each call inherits the bot's environment with loader and shell-injection variables removed, then adds these values. Later entries win.

VariableValue
auth.env keysDefaults from the manifest.
<SLUG>_BINPath of this plugin’s binary, and of each plugin it depends on. The slug is uppercased with hyphens as underscores.
NEBO_DATA_DIRThe plugin’s own data folder, <data>/appdata/plugins/<slug>/. It is also the working directory, and it survives uninstall.
PATHPlugin folders first, then the inherited path.
SettingsValues the owner saved for configSchema fields.
NEBO_AGENT_IDThe employee making the call.
profileDirEnvThe account folder for this employee and account, when the plugin supports accounts.

permissions.envAllow and permissions.envDeny narrow what the plugin inherits from the bot. They never remove the plugin's own auth variables.

Accounts and credentials

A plugin that signs in to a service should hold one account per employee. The bookkeeper signs in with the finance login and the assistant with its own, and neither sees the other's tokens. You declare this in the manifest; Nebo does the rest.

"auth": {
  "type": "env",
  "label": "Example Notes account",
  "env": { "EXAMPLE_NOTES_TOKEN": "" },
  "commands": { "login": "auth login", "status": "auth status", "logout": "auth logout" },
  "profileDirEnv": "EXAMPLE_NOTES_CONFIG_DIR"
},
"capabilities": {
  "configSchema": [
    { "key": "EXAMPLE_NOTES_TOKEN", "label": "API token", "fieldType": "password", "secret": true, "required": true }
  ]
}
  1. auth.env names the credentials. capabilities.configSchema labels them and sets their order in the connect form.
  2. auth.profileDirEnv names the variable your plugin reads for its credential folder. Nebo creates one folder for each employee and account and sets that variable on every call.
  3. When the owner connects an account, Nebo runs your login command with the values the owner typed and the account folder set. Your plugin saves what it needs in that folder. Nebo does not keep the values.
  4. Your status command prints JSON such as {"authenticated": true}. When a token expires, Nebo runs refresh if you declare one.

For a browser sign-in, set type to oauth_cli and have login print or open the provider's sign-in URL. Employees never sign in by themselves: when a call needs a login, the owner sees a connect card. An employee with several accounts picks one by adding --account <label> to a command. Nebo removes that flag before your binary sees it.

Name the service, not the shape. A plugin's name says what it connects to, such as "Example Notes". Never put "CLI" in a plugin name; to employees and owners it is a connection, not a command line.

Next