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

128 lines
7.8 KiB
Markdown

# 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
```bash
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
```bash
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](#trust-badges) and [Publishing + getting approved](#publishing--getting-approved)).
| Plugin | What it does | Hooks | Keys needed | Author |
| --- | --- | --- | --- | --- |
| [career-ops-plugin-tavily](https://github.com/Schlaflied/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](https://github.com/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](https://github.com/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](https://github.com/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](https://github.com/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](#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](https://github.com/santifer/career-ops/discussions/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.