Registrars for local Gateway discovery, plugin-owned CLI commands, and local AI CLI backends. Part of the Plugin SDK overview.

Gateway discovery registration

api.registerGatewayDiscoveryService(...) lets a plugin advertise the active Gateway on a local discovery transport such as mDNS/Bonjour. Apus calls the service during Gateway startup when local discovery is enabled, passes the current Gateway ports and non-secret TXT hint data, and calls the returned stop handler during Gateway shutdown.

api.registerGatewayDiscoveryService({
  id: "my-discovery",
  async advertise(ctx) {
    // startMyAdvertiser is your plugin's own mDNS/Bonjour helper, not an SDK export.
    const handle = await startMyAdvertiser({
      gatewayPort: ctx.gatewayPort,
      tls: ctx.gatewayTlsEnabled,
      displayName: ctx.machineDisplayName,
    });
    return { stop: () => handle.stop() };
  },
});

Gateway discovery plugins must not treat advertised TXT values as secrets or authentication. Discovery is a routing hint; Gateway auth and TLS pinning still own trust.

CLI registration metadata

The executable CLI owns uncached plugin registries acquired for help and command metadata, along with provider callbacks borrowed through apus/plugin-sdk/provider-catalog-runtime during registration, actions, or cleanup. It releases those resources after the invocation's actual work settles, even when a cleanup warning has already reported a timeout. Forced process exit can still interrupt asynchronous disposal. Caller-owned programs and Gateway startup do not become executable CLI owners merely by calling a CLI helper; see retained SDK contracts.

api.registerCli(registrar, opts?) accepts two kinds of command metadata:

For paired-node features, prefer api.registerNodeCliFeature(registrar, opts?). It is a small wrapper around api.registerCli(..., { parentPath: ["nodes"] }) and makes commands such as apus nodes canvas explicit plugin-owned node features.

Reuse the core node CLI owners when a plugin-owned node command needs the same Gateway flags, invoke envelope, terminal presentation, and authorization hints:

import {
  buildNodeInvokeParams,
  getNodesTheme,
  nodesCallOpts,
  runNodesCommand,
} from "apus/plugin-sdk/node-cli-runtime";

If you want a plugin command to stay lazy-loaded in the normal root CLI path, provide descriptors that cover every top-level command root exposed by that registrar.

api.registerCli(
  async ({ program }) => {
    const { registerMatrixCli } = await import("./src/cli.js");
    registerMatrixCli({ program });
  },
  {
    descriptors: [
      {
        name: "matrix",
        description: "Manage Matrix accounts, verification, devices, and profile state",
        hasSubcommands: true,
      },
    ],
  },
);

A root descriptor can also declare machineOutput({ argv, stdoutIsTTY }) when the command reserves stdout for JSON, JSONL, or another machine-readable format without relying exclusively on a literal --json flag. Apus evaluates this resolver before plugin activation so startup diagnostics can be routed to stderr. The resolver must be synchronous, pure, and dependency-light: inspect only the supplied raw argv and stdout TTY state. Reuse the same resolver in lightweight CLI metadata and full registration so discovery and execution do not disagree. Use getRootOptionAwareCommandPath from apus/plugin-sdk/cli-argv when the resolver needs command-path tokens; it accepts supported root options before or after the command root. machineOutput is root metadata; nested descriptors cannot use it because their owning root must already be active before they are visible.

Nested commands receive the resolved parent command as program:

api.registerCli(
  async ({ program }) => {
    const { registerNodesCanvasCommands } = await import("./src/cli.js");
    registerNodesCanvasCommands(program);
  },
  {
    parentPath: ["nodes"],
    descriptors: [
      {
        name: "canvas",
        description: "Present hosted widgets on a paired Mac",
        hasSubcommands: true,
      },
    ],
  },
);

Use commands by itself only when you do not need lazy root CLI registration. That eager compatibility path remains supported, but it does not install descriptor-backed placeholders for parse-time lazy loading.

The registrar receives native Commander objects. Callbacks registered inside the registrar retain the plugin instance for later actions, hooks, parsing, help, and events. A preconfigured command tree added with program.addCommand also transfers its callbacks to the adding instance. Commands already attached to the host before registration remain caller-owned; command identity and function-valued argument or option data do not change. Reload or disablement rejects new callback invocations while admitted asynchronous actions finish.

CLI backend registration

api.registerCliBackend(...) lets a plugin own the default config for a local AI CLI backend such as claude-cli or my-cli.

For an end-to-end authoring guide, see CLI backend plugins.