santifer/career-ops · error · Error

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

Error message

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

What it means

Thrown by `inactiveProviderStub`, a synthetic provider object injected by the plugin engine (`mergeProviderPlugins` in plugins/_engine.mjs) when a known provider plugin is registered in portals.yml with an explicit `provider: <id>` entry but the plugin itself is inactive (disabled, missing its API key, or failed to import). Instead of producing a confusing 'unknown provider' error, the stub yields an actionable message directing you to `node doctor.mjs`. The `reason` string names the specific cause (e.g. 'missing key', 'disabled', 'failed import').

Source

Thrown at plugins/_engine.mjs:673

 *     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 9b17a8ac97)

Solutions

  1. Run `node doctor.mjs` — it reports which plugins are inactive and the exact reason (missing key, disabled, import failure).
  2. If the reason is a missing key, add it to .env (e.g. APIFY_TOKEN=...) and enable the plugin in config/plugins.yml.
  3. If the reason is 'disabled', set the plugin's `enabled: true` in config/plugins.yml.
  4. If the reason is 'failed import', check the plugin's .mjs file for syntax/runtime errors, or update the plugin.
  5. If you no longer want that provider, remove the `provider: <id>` line from portals.yml so scan.mjs stops referencing it.

Example fix

// portals.yml — before
- name: indeed
  provider: apify
  actor: misceres/indeed-scraper
// doctor.mjs reports: plugin "apify" inactive: missing key APIFY_TOKEN
// .env — after
APIFY_TOKEN=apify_api_xxxxxxxxxxxxx
Defensive patterns

Strategy: validation

Validate before calling

import { execSync } from 'node:child_process';
// Before referencing a provider, confirm its plugin is active.
function assertPluginActive(id) {
  const out = execSync('node doctor.mjs --json', { encoding: 'utf-8' });
  const { plugins } = JSON.parse(out);
  const p = plugins?.[id];
  if (!p || p.status !== 'active') {
    throw new Error(`Refusing to scan: provider '${id}' is ${p?.reason || 'not configured'}. Run: node doctor.mjs`);
  }
}
assertPluginActive('apify');

Try / catch

// When invoking a known-keyed provider, catch and surface the setup hint.
try {
  await provider.fetch(entry, ctx);
} catch (err) {
  if (/plugin ".+" inactive/.test(err.message)) {
    console.error(`Setup required: ${err.message}`);
    process.exitCode = 2;
  } else throw err;
}

Prevention

When it happens

Trigger: A portals.yml entry sets `provider: apify` (or gmail, etc.) but the corresponding plugin is not active. The stub's `fetch()` is invoked by scan.mjs when it tries to pull jobs from that provider. The `detect()` always returns null (so auto-detection never fires), and only an explicit `provider:` reference triggers the stub's fetch path.

Common situations: Apify plugin referenced but `APIFY_TOKEN` not added to .env; gmail plugin referenced but not enabled in config/plugins.yml; a plugin's manifest declared `keyed: true` and the key file is missing; a plugin failed dynamic import due to a syntax error and the engine marked it inactive. Also happens after a config edit that adds a provider line before running `node doctor.mjs` to verify setup.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/d026f5fa2861674c. Report an issue: GitHub.