paperclipai/paperclip · error

Plugin UI directory not found

Error message

Plugin UI directory not found

What it means

Returned as HTTP 404 by GET /_plugins/:pluginId/ui/* (server/src/routes/plugin-ui-static.ts:420) when resolvePluginUiDir() cannot find the plugin's UI directory on disk. The resolver tries the persisted packagePath, then <localPluginDir>/node_modules/<packageName>, then <localPluginDir>/<packageName>, and finally <packageRoot>/<entrypoints.ui>; if none exists the route logs 'UI directory not found on disk' and 404s even though the plugin is 'ready' and declares a UI.

Source

Thrown at server/src/routes/plugin-ui-static.ts:420

      }
    } catch {
      // Config lookup failure is non-fatal — fall through to static serving
    }

    // Step 3: Resolve the plugin's UI directory
    const uiDir = resolvePluginUiDir(
      options.localPluginDir,
      plugin.packageName,
      manifest.entrypoints.ui,
      plugin.packagePath,
    );

    if (!uiDir) {
      log.warn(
        { pluginId: plugin.id, pluginKey: plugin.pluginKey, packageName: plugin.packageName },
        "plugin-ui-static: UI directory not found on disk",
      );
      res.status(404).json({ error: "Plugin UI directory not found" });
      return;
    }

    // Step 4: Resolve the requested file path and prevent traversal (including symlinks)
    const resolvedFilePath = path.resolve(uiDir, rawFilePath);

    // Step 5: Check that the file exists and is a regular file
    let fileStat: fs.Stats;
    try {
      fileStat = fs.statSync(resolvedFilePath);
    } catch {
      res.status(404).json({ error: "File not found" });
      return;
    }

    // Security: resolve symlinks via realpathSync and verify containment.
    // This prevents symlink-based traversal that string-based startsWith misses.
    let realFilePath: string;

View on GitHub (pinned to 120ae5428f)

Solutions

  1. Verify the UI directory physically exists: ls <localPluginDir>/node_modules/<packageName>/dist/ui (or the packagePath location)
  2. If missing, rebuild the plugin (npm run build producing dist/ui) and reinstall it so the registry re-points at a valid package
  3. Confirm the server's localPluginDir option matches the directory used at install time (default ~/.paperclip/plugins/)
  4. For local-path installs, make sure the plugin's packagePath still exists on this machine and contains the entrypoints.ui directory

Example fix

# before: package.json of the plugin publishes without building
"files": ["src"]

# after: include built UI output and build before publish
"scripts": { "prepublishOnly": "npm run build" },
"files": ["dist"]
Defensive patterns

Strategy: validation

Validate before calling

import fs from "node:fs";
import path from "node:path";
// Run after install, before marking the plugin ready
const uiDir = path.join(packageRoot, manifest.entrypoints.ui);
if (!fs.existsSync(uiDir)) {
  throw new Error(`UI bundle missing: ${uiDir} — run the plugin build before install`);
}

Prevention

When it happens

Trigger: Plugin marked ready and manifest declares entrypoints.ui='./dist/ui/', but the installed package has no dist/ui directory (build step skipped, .npmignore excluded it), the package was deleted from ~/.paperclip/plugins/node_modules, packagePath points to a moved/deleted local directory, or the server was started with a localPluginDir option different from the directory the plugin was installed into.

Common situations: Plugin published without building the UI bundle first; local-path installs after the source directory was moved or the worktree deleted; running multiple server instances with different plugin dirs; partial/corrupted npm installs during plugin upgrade.

Related errors


AI-assisted analysis of paperclipai/paperclip@120ae5428f (2026-08-18). Data as JSON: /api/errors/d04b278eab8450cf. Report an issue: GitHub.