agalwood/Motrix · error · CommandsError

plugin.commands.not_found

plugin.commands.not_found

Error message

command "${commandId}" is not registered

What it means

Thrown by CommandsCapabilityHost.execute() when a plugin calls a command whose ID starts with its own namespace (e.g. `<callerId>.foo`) but no handler was ever registered for that exact commandId. The capability dispatches own-namespace calls directly from an in-memory handler map; a miss is a programming error, not a runtime race. The error code is `plugin.commands.not_found`.

Source

Thrown at src/core/plugin/capabilities/commands.ts:162

  /**
   * Execute a command.
   *
   * - Own-namespace (`commandId` starts with `${callerId}.`): dispatches to
   *   the registered handler. Rejects with `not_found` if absent. Any error
   *   thrown by the handler surfaces as-is.
   * - Foreign-namespace: forwarded to the bound CrossPluginInvoker. Rejects
   *   with `access_denied` if no invoker is bound.
   */
  async execute(
    callerId: string,
    commandId: string,
    args: unknown
  ): Promise<unknown> {
    if (commandId.startsWith(`${callerId}.`)) {
      const handler = this.handlers.get(commandId)
      if (!handler) {
        throw new CommandsError(
          'plugin.commands.not_found',
          `command "${commandId}" is not registered`
        )
      }
      const startTs = Date.now()
      try {
        const result = await handler(args)
        this.onSelfInvoke?.({
          callerId,
          commandId,
          durMs: Date.now() - startTs,
          ok: true,
        })
        return result
      } catch (err) {
        this.onSelfInvoke?.({
          callerId,
          commandId,

View on GitHub (pinned to 1a708ee577)

Solutions

  1. Verify the commandId string passed to execute() byte-for-byte matches the one passed to register(); check for trailing whitespace, casing, or a missing/extra segment.
  2. Ensure register() is called during plugin activation and that its returned CommandsRegistration has not been disposed before execute() runs.
  3. If the command is meant to be invoked cross-plugin (foreign namespace), drop the callerId prefix so it routes through the CrossPluginInvoker path instead.
  4. Search the plugin source for every register() call and confirm the target id is among them before invoking.

Example fix

// before
await cmds.execute('myPlugin', 'myPlugin.runTask', {})
// register() was never called for 'myPlugin.runTask'

// after
const reg = cmds.register('myPlugin.runTask', async (args) => doWork(args))
await cmds.execute('myPlugin', 'myPlugin.runTask', {})
Defensive patterns

Strategy: validation

Validate before calling

function isRegistered(cmds: CommandsCapabilityHost, id: string): boolean {
  // expose a list/has on the host if available; otherwise track locally
  return registeredIds.has(id)
}
if (!isRegistered(cmds, 'myPlugin.runTask')) {
  throw new Error('command not registered; cannot execute')
}

Type guard

function isCommandsError(e: unknown, code = 'plugin.commands.not_found'): e is CommandsError {
  return e instanceof Error && (e as CommandsError).code === code
}

Try / catch

try {
  await cmds.execute(callerId, commandId, args)
} catch (e) {
  if (isCommandsError(e, 'plugin.commands.not_found')) {
    // register lazily or surface a clear 'command unavailable' message
  } else throw e
}

Prevention

When it happens

Trigger: Calling `execute(callerId, 'myPlugin.doThing', args)` where `callerId === 'myPlugin'` but `register('myPlugin.doThing', handler)` was never called (or was disposed). Also triggered by a typo in the commandId that still matches the namespace prefix, or by attempting to invoke a command after teardown/unregister.

Common situations: Plugin author forgot to register the command in their activate() lifecycle; manifest declares `contributes.commands[]` with id X but the handler registers under a slightly different id; a dispose() ran during teardown and a queued callback still calls execute().

Related errors


AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12). Data as JSON: /api/errors/5df19d9f09a27ea1. Report an issue: GitHub.