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
- 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, plusskill.md+test/smoke.mjsto be listable. - File a Plugin registration issue (becomes your plugin's home/changelog).
- Open a registry PR (the
?template=plugin-registry.mdtemplate — the template repo's release workflow can open it for you on a release tag) that adds yourplugins-registry/<id>.jsonfile, 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 cannode plugins.mjs add <name>and your plugin ships to them via the normal update. - 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:
- Publish
career-ops-plugin-<id>with the sameidas the bundled plugin (start from the bundled plugin's code — it's MIT and credits its origins). - In your registry PR, set
"supersedesBundled": trueon your entry. - 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 availablesurfaces 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.