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
- Run node doctor.mjs to complete/verify setup
- Enable the plugin in config/plugins.yml (set enabled: true) or via the plugins.mjs enable command
- Set the plugin's required API key env variable
- 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
- Run node doctor.mjs on every new machine/checkout before scanning
- Keep provider API keys in .env and verify they are loaded
- After plugin upgrades, confirm imports succeed with a quick smoke test
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
- a16z-speedrun-talent: invalid URL
- agentic-jobs: invalid URL
- capacity must be a finite number >= 1, got
- collage: API URL must be /v1/positions/<job-site-address>
- collage: invalid URL
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)