Files
santifer--career-ops/docs/PLUGINS.md
T
wehub-resource-sync d083df1fdb
CodeQL Analysis / Analyze (javascript-typescript) (push) Failing after 2s
Web CI / web typecheck + build (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
CodeQL Analysis / Analyze (go) (push) Failing after 16s
chore: import upstream snapshot with attribution
2026-07-13 12:02:43 +08:00

7.8 KiB

Plugins

Plugins extend career-ops with integrations that need an API key or talk to an external service — things the zero-keys, local-first core doesn't carry. They are opt-in, sandboxed-by-convention, and additive: with no plugins enabled, the core runs exactly as it always has.

This is not the Claude Code plugin (.claude-plugin/). These plugins extend career-ops itself.

Using plugins

node plugins.mjs list          # what's installed + its trust badge
node plugins.mjs available     # bundled + community plugins we've approved
node plugins.mjs add <name>    # install an approved community plugin
node plugins.mjs enable <id>   # show the capability card (then add --confirm)
node plugins.mjs skill <id>    # print a plugin's how-to (if it ships one)

Two gates must both be satisfied for a plugin to run: it must be enabled (node plugins.mjs enable <id> --confirm, which records your consent) and its keys must be in your .env. node doctor.mjs shows what's missing.

Trust badges

Badge Meaning
📦 bundled Shipped in plugins/, reviewed in-tree, auto-updated with the core.
✓ approved A community plugin we reviewed at an exact pinned commit (in the registry).
❓ community-unverified You installed it from a repo we haven't reviewed — you're trusting the author.
⚠️ off-registry Installed commit differs from the approved one.

If a plugin's files change without a version bump, career-ops blocks it and asks you to review + node plugins.mjs trust <id> to re-pin (tamper detection).

Writing a plugin

node plugins.mjs new my-plugin     # scaffolds plugins.local/my-plugin/

A plugin is a directory with a manifest.json (validated before any code is imported), an index.mjs (default-exports your hooks), and optionally a skill.md + _helpers. Hooks: provider / ingest / search / notify / export — there is no auto-submit hook. Producers return Job[]; the engine writes them. Reach the network only through ctx.fetch (your manifest allowedHosts is enforced, with SSRF protection). Keys arrive via ctx.env, non-secret settings via ctx.settings.

See plugins/README.md for the full contract + the honest trust model (plain ESM has no hard sandbox — bundled plugins are code-reviewed; your own are your trust).

Publishing + getting approved

  1. Develop locally, then publish your plugin as its own public GitHub repo named exactly career-ops-plugin-<name> (the template repo gives you the right shape + a release workflow). Minimum files: manifest.json, index.mjs, README.md, LICENSE, plus skill.md + test/smoke.mjs to be listable.
  2. File a Plugin registration issue (becomes your plugin's home/changelog).
  3. Open a registry PR (the ?template=plugin-registry.md template — the template repo's release workflow can open it for you on a release tag) that adds your plugins-registry/<id>.json file, pinned to an exact commit. CI (plugin-registry-validate) checks the naming, manifest, min-files, license, egress, and a static audit before a maintainer reviews. Once merged, users can node plugins.mjs add <name> and your plugin ships to them via the normal update.
  4. Updates = one more registry PR bumping your entry's sha + version (your release workflow opens it from your own fork). Users only ever get the commit we approved.

Broadly-useful, low/zero-key plugins may be shipped bundled in plugins/ (e.g. apify, gmail, notion). Bundled plugins are reference seeds: reviewed in-tree, always present, and a working example to copy — kept minimal and stable on purpose, not a home for ongoing feature work.

Improving a bundled plugin → publish a maintained successor

We don't take feature PRs against bundled plugins. If you want to extend one (more options, a richer mapping, new behavior), own it properly:

  1. Publish career-ops-plugin-<id> with the same id as the bundled plugin (start from the bundled plugin's code — it's MIT and credits its origins).
  2. In your registry PR, set "supersedesBundled": true on your entry.
  3. Once approved + pinned, anyone who runs node plugins.mjs add career-ops-plugin-<id> installs your version, and the engine gives your maintained successor precedence over the bundled reference of the same id. node plugins.mjs available surfaces the link: "gmail — 🔁 maintained version: career-ops-plugin-gmail".

This keeps the core lean and puts the integration in your hands, with your name on it — while the bundled seed stays as the always-present fallback, so the feature never breaks even if a successor goes quiet. Precedence is granted ONLY to a registry-approved successor installed at its exact pinned commit — so an unprompted, unreviewed community plugin can never shadow a bundled one.

Trust boundary (plainly). The thing this protects is the supply chain: the registry is a reviewed system file and installs pin an exact commit, so no upstream author can push code over a bundled plugin without a maintainer merging their entry. It does not try to stop you from running your own modified code on your own machine — career-ops is local-first and the source is yours; if you edit plugins.local/ or your plugins.lock, you're choosing to run your own version, exactly as you always could. Removing a successor restores the bundled reference.

Community plugins

Every community plugin in the registry is reviewed and pinned to an exact commit (see Trust badges and Publishing + getting approved).

Plugin What it does Hooks Keys needed Author
career-ops-plugin-tavily Tavily search/extract for job scanning, liveness checks, and company research. search TAVILY_API_KEY @Schlaflied
career-ops-plugin-google-calendar Google Calendar ingest — detect upcoming interview events and surface them in the career-ops pipeline. ingest GOOGLE_CALENDAR_CLIENT_ID, GOOGLE_CALENDAR_CLIENT_SECRET, GOOGLE_CALENDAR_REFRESH_TOKEN @Schlaflied
career-ops-plugin-linkedin-alerts LinkedIn job alert ingest — parse LinkedIn alert emails from your Gmail inbox, normalize tracking links to canonical job URLs, and surface them in the career-ops pipeline. ingest GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REFRESH_TOKEN @Schlaflied
career-ops-plugin-outlook-interviews Outlook interview ingest — detect interview invitation emails via Microsoft Graph, extract company / role / meeting link, and surface them in the career-ops pipeline. ingest MSGRAPH_CLIENT_ID, MSGRAPH_REFRESH_TOKEN (optional: MSGRAPH_CLIENT_SECRET) @Schlaflied
career-ops-plugin-obsidian Obsidian export — mirror the tracker into your vault as frontmatter notes queryable by Dataview/Bases; frontmatter belongs to the machine, the note body belongs to you. export None @Schlaflied

To add your own plugin to the registry, follow the Publishing + getting approved flow above.

Not a plugin

  • Centralized infrastructure the project would run (hosted aggregation, shared services, proxies) → a separate, opt-in service, see Discussion #904.
  • Auto-submitting / blind-applying → out of the core everywhere. career-ops drafts for you to review and submit; it is a decision-support tool, not a bot.