santifer/career-ops · error · Error

plugin " " inactive: . Run `node doctor.mjs` for setup.

Error message

plugin "${id}" inactive: ${reason}. Run `node doctor.mjs` for setup.

What it means

When a portals.yml entry names provider: <id> but that provider plugin is inactive (disabled in config, missing its API key, or failed to import), the engine substitutes an inactiveProviderStub whose fetch() always throws this self-explaining error. It exists so the failure names the plugin and the remedy instead of surfacing as a generic missing-provider error.

Solutions

  1. Run node doctor.mjs to complete/verify setup
  2. Enable the plugin in config/plugins.yml (set enabled: true) or via the plugins.mjs enable command
  3. Set the plugin's required API key env variable
  4. If the plugin fails to import, check its logs/fix or reinstall it

Example fix

// before: portals.yml has provider: greenhouse-plus, but key unset and plugin disabled
await provider.fetch(url);
// Error: plugin "greenhouse-plus" inactive: missing API key GREENHOUSE_PLUS_KEY. Run `node doctor.mjs` for setup.
// after
export GREENHOUSE_PLUS_KEY=...   # and enable it
// node plugins.mjs enable greenhouse-plus
Defensive patterns

Strategy: try-catch

Validate before calling

// Before scanning, confirm the provider plugin is active:
import { existsSync, readFileSync } from 'node:fs';
const cfg = existsSync('config/plugins.yml') ? readFileSync('config/plugins.yml', 'utf8') : '';
// check enabled:true and the plugin's required env key is set
if (!process.env.GREENHOUSE_PLUS_KEY) console.warn('provider key missing — run node doctor.mjs');

Type guard

const isInactiveProviderError = (e) => /plugin ".*" inactive:/.test(String(e.message));

Try / catch

try { await provider.fetch(url); } catch (e) { if (isInactiveProviderError(e)) { console.error(e.message); /* run node doctor.mjs, enable the plugin, set its key */ return null; } throw e; }

Prevention

When it happens

Trigger: A scan run against a portals.yml entry with provider: <id> where the plugin is toggled off in config/plugins.yml, its required env/API key is unset, or its module throws on import.

Common situations: New machine missing the API key env var; plugin disabled after a config cleanup; a plugin update broke its import; running doctor was never completed on this checkout.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/d026f5fa2861674c. Report an issue: GitHub.

Appendix: source

Thrown at plugins/_engine.mjs:695

 *     bundled zero-key provider).
 *  4. detect-EXEMPT: a merged provider's detect() is forced to null, so it fires
 *     ONLY on an explicit `provider: <id>` portals.yml entry — never via
 *     auto-detection (no surprise paid/keyed network during a plain scan).
 *  5. A known-but-inactive provider plugin registers a STUB whose fetch throws
 *     an actionable message (disabled / missing key) — so `provider: apify` with
 *     the plugin off yields a helpful error, not a confusing "unknown provider".
 *
 * @param {Map<string, any>} providersMap   The Map returned by scan.mjs loadProviders.
 * @param {{ root: string }} opts
 */
// A detect-exempt provider whose fetch throws an actionable message — used when
// a known provider plugin is inactive (disabled / missing key / failed import)
// so an explicit `provider: <id>` portals.yml entry stays self-explaining.
function inactiveProviderStub(id, reason) {
  return {
    id,
    detect: () => null,
    fetch: async () => { throw new Error(`plugin "${id}" inactive: ${reason}. Run \`node doctor.mjs\` for setup.`); },
  };
}

export async function mergeProviderPlugins(providersMap, { root }) {
  if (!existsSync(pluginsConfigPath(root))) return; // (1) opted out → inert (no work, no env read)

  // Everything past the opt-out gate is wrapped so an UNANTICIPATED throw
  // (a callee regression) degrades to a ⚠️ and leaves the core providers Map
  // untouched — fail-open is enforced structurally here, not just emergently.
  try {
    const cfg = await loadPluginConfig(root);
    const providerManifests = discoverPlugins(pluginRoots(root), resolveSuccessorIds(root)).filter(m => m.hooks.includes('provider'));
    if (providerManifests.length === 0) return;

    // Only the plugins the user actually switched on in plugins.yml matter.
    const configuredOn = providerManifests.filter(m => cfg?.plugins?.[m.id]?.enabled === true);
    if (configuredOn.length === 0) return;

View on GitHub (pinned to aac998c7ed)