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
- Run `node doctor.mjs` — it reports which plugins are inactive and the exact reason (missing key, disabled, import failure).
- If the reason is a missing key, add it to .env (e.g. APIFY_TOKEN=...) and enable the plugin in config/plugins.yml.
- If the reason is 'disabled', set the plugin's `enabled: true` in config/plugins.yml.
- If the reason is 'failed import', check the plugin's .mjs file for syntax/runtime errors, or update the plugin.
- 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
- Run `node doctor.mjs` after every portals.yml or config/plugins.yml change.
- Gate scans behind a doctor check in CI before invoking providers.
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
- plugin egress: cannot resolve ${hostname} — ${err.message}
- APIFY_TOKEN not set — enable apify in config/plugins.yml and
- apify: entry ${entry.name} missing 'actor' (e.g. misceres/in
- apify: entry ${entry.name} has invalid field_map. Each of ti
- No "Applications" database found under the Career Ops page —
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/d026f5fa2861674c.
Report an issue: GitHub.