init
No files matched your search
@@ -0,0 +1,110 @@
|
||||
# Architecture
|
||||
|
||||
## System Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ AI Coding CLI Agent │
|
||||
│ (reads AGENTS.md + modes/*.md) │
|
||||
└──────────┬──────────────────────┘
|
||||
│
|
||||
┌──────────────────┼──────────────────────┐
|
||||
│ │ │
|
||||
┌──────▼──────┐ ┌──────▼──────┐ ┌───────────▼────────┐
|
||||
│ Single Eval │ │ Portal Scan │ │ Batch Process │
|
||||
│ (auto-pipe) │ │ (scan.md) │ │ (batch-runner) │
|
||||
└──────┬──────┘ └──────┬──────┘ └───────────┬────────┘
|
||||
│ │ │
|
||||
│ ┌──────▼──────┐ ┌────▼─────┐
|
||||
│ │ pipeline.md │ │ N workers│
|
||||
│ │ (URL inbox) │ │ (headless)
|
||||
│ └─────────────┘ └────┬─────┘
|
||||
│ │
|
||||
┌──────▼──────────────────────────────────────────▼──────┐
|
||||
│ Output Pipeline │
|
||||
│ ┌──────────┐ ┌────────────┐ ┌───────────────────┐ │
|
||||
│ │ Report.md│ │ PDF (HTML │ │ Tracker TSV │ │
|
||||
│ │ (A-F eval)│ │ → Puppeteer)│ │ (merge-tracker) │ │
|
||||
│ └──────────┘ └────────────┘ └───────────────────┘ │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌──────────▼──────────┐
|
||||
│ data/applications.md │
|
||||
│ (canonical tracker) │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
## Evaluation Flow (Single Offer)
|
||||
|
||||
1. **Input**: User pastes JD text or URL
|
||||
2. **Extract**: Playwright/WebFetch extracts JD from URL
|
||||
3. **Classify**: Detect archetype (1 of 6 types)
|
||||
4. **Evaluate**: 6 blocks (A-F):
|
||||
- A: Role summary
|
||||
- B: CV match (gaps + mitigation)
|
||||
- C: Level strategy
|
||||
- D: Comp research (WebSearch)
|
||||
- E: CV personalization plan
|
||||
- F: Interview prep (STAR stories)
|
||||
5. **Score**: Weighted average across 10 dimensions (1-5)
|
||||
6. **Report**: Save as `reports/{num}-{company}-{date}.md`
|
||||
7. **PDF**: Generate ATS-optimized CV (`generate-pdf.mjs`)
|
||||
8. **Track**: Write TSV to `batch/tracker-additions/`, auto-merged
|
||||
|
||||
## Batch Processing
|
||||
|
||||
The batch system processes multiple offers in parallel:
|
||||
|
||||
```
|
||||
batch-input.tsv → batch-runner.sh → N × headless CLI workers
|
||||
(id, url, source) (orchestrator) (self-contained prompt)
|
||||
│
|
||||
batch-state.tsv
|
||||
(tracks progress)
|
||||
```
|
||||
|
||||
Each worker is a headless AI CLI instance — the bundled `batch-runner.sh` supports multiple CLIs via the `--cli` flag (`--cli claude` or `--cli opencode`). See the Headless / Batch Mode table in `AGENTS.md`. Workers produce:
|
||||
- Report .md
|
||||
- PDF
|
||||
- Tracker TSV line
|
||||
|
||||
The orchestrator manages parallelism, state, retries, and resume.
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
cv.md → Evaluation context
|
||||
article-digest.md → Proof points for matching
|
||||
config/profile.yml → Candidate identity
|
||||
portals.yml → Scanner configuration
|
||||
templates/states.yml → Canonical status values
|
||||
templates/cv-template.html → PDF generation template
|
||||
```
|
||||
|
||||
## File Naming Conventions
|
||||
|
||||
- Reports: `{###}-{company-slug}-{YYYY-MM-DD}.md` (3-digit zero-padded)
|
||||
- PDFs: `cv-candidate-{company-slug}-{YYYY-MM-DD}.pdf`
|
||||
- Tracker TSVs: `batch/tracker-additions/{id}.tsv`
|
||||
|
||||
## Pipeline Integrity
|
||||
|
||||
Scripts maintain data consistency:
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `merge-tracker.mjs` | Merges batch TSV additions into applications.md |
|
||||
| `verify-pipeline.mjs` | Health check: statuses, duplicates, links |
|
||||
| `dedup-tracker.mjs` | Removes duplicate entries by company+role |
|
||||
| `normalize-statuses.mjs` | Maps status aliases to canonical values |
|
||||
| `cv-sync-check.mjs` | Validates setup consistency |
|
||||
|
||||
## Dashboard TUI
|
||||
|
||||
The `dashboard/` directory contains a standalone Go TUI application that visualizes the pipeline:
|
||||
|
||||
- Filter tabs: All, Evaluada, Aplicado, Entrevista, Top >=4, No Aplicar
|
||||
- Sort modes: Score, Date, Company, Status
|
||||
- Grouped/flat view
|
||||
- Lazy-loaded report previews
|
||||
- Inline status picker
|
||||
@@ -0,0 +1,48 @@
|
||||
# Codex Guide
|
||||
|
||||
Career-ops supports Codex through the same shared router used by the other CLI integrations.
|
||||
|
||||
## How Codex maps to career-ops
|
||||
|
||||
- `AGENTS.md` is the shared instruction source.
|
||||
- Root `CODEX.md` is the thin Codex wrapper that imports `AGENTS.md`.
|
||||
- This file is the human-facing guide for running career-ops workflows from Codex.
|
||||
|
||||
## Interactive Codex
|
||||
|
||||
Start Codex in the repository root:
|
||||
|
||||
```bash
|
||||
cd career-ops
|
||||
codex
|
||||
```
|
||||
|
||||
Codex may not expose a native `/career-ops` slash command. When it does not, ask for the same workflow in plain language:
|
||||
|
||||
```text
|
||||
Evaluate this JD with career-ops auto-pipeline: https://company.com/jobs/123
|
||||
Run the career-ops scan mode and summarize new matches.
|
||||
Run the career-ops pipeline mode for data/pipeline.md.
|
||||
Run the career-ops pdf mode for the latest evaluated role.
|
||||
Run the career-ops email mode for the latest evaluated role. Draft only; never sends, submits, or clicks.
|
||||
Run the career-ops tracker mode and summarize the current statuses.
|
||||
```
|
||||
|
||||
## One-shot workers
|
||||
|
||||
For single commands or batch workers, use `codex exec`:
|
||||
|
||||
```bash
|
||||
codex exec "Evaluate this JD with career-ops auto-pipeline: https://company.com/jobs/123"
|
||||
codex exec "Run career-ops scan mode in this repo and summarize new matches."
|
||||
codex exec "Run career-ops pipeline mode for data/pipeline.md."
|
||||
codex exec "Run career-ops pdf mode for the latest evaluated role."
|
||||
codex exec "Run career-ops email mode for the latest evaluated role. Draft only; do not send, submit, or click anything."
|
||||
codex exec "Run career-ops tracker mode and summarize the current statuses."
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- If your Codex environment exposes slash commands, the shared `/career-ops` router semantics still apply.
|
||||
- If it does not, use the same mode names through prompts or `codex exec`.
|
||||
- Browser-heavy flows such as `scan`, `pipeline`, and `apply` still depend on Playwright browser tools being available in the active agent setup.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Customization Guide
|
||||
|
||||
## Profile (config/profile.yml)
|
||||
|
||||
This is the single source of truth for your identity. All modes read from here.
|
||||
|
||||
Key sections:
|
||||
- **candidate**: Name, email, phone, location, LinkedIn, portfolio
|
||||
- **target_roles**: Your North Star roles and archetypes
|
||||
- **narrative**: Your headline, exit story, superpowers, proof points
|
||||
- **compensation**: Target range, minimum, currency
|
||||
- **location**: Country, timezone, visa status, on-site availability
|
||||
|
||||
## Target Roles (modes/_profile.md)
|
||||
|
||||
The archetype table in `_profile.md` determines how offers are scored and CVs are framed. Edit the table to match YOUR career targets:
|
||||
|
||||
```markdown
|
||||
| Archetype | Thematic axes | What they buy |
|
||||
|-----------|---------------|---------------|
|
||||
| **Your Role 1** | key skills | what they need |
|
||||
| **Your Role 2** | key skills | what they need |
|
||||
```
|
||||
|
||||
Also update the "Adaptive Framing" table to map YOUR specific projects to each archetype.
|
||||
|
||||
## Portals (portals.yml)
|
||||
|
||||
Copy from `templates/portals.example.yml` and customize:
|
||||
|
||||
1. **title_filter.positive**: Keywords matching your target roles
|
||||
2. **title_filter.negative**: Tech stacks or domains to exclude
|
||||
3. **search_queries**: WebSearch queries for job boards (Ashby, Greenhouse, Lever)
|
||||
4. **tracked_companies**: Companies to check directly
|
||||
|
||||
## CV Template (templates/cv-template.html)
|
||||
|
||||
The HTML template uses these design tokens:
|
||||
- **Fonts**: Space Grotesk (headings) + DM Sans (body) -- self-hosted in `fonts/`
|
||||
- **Colors**: Cyan primary (`hsl(187,74%,32%)`) + Purple accent (`hsl(270,70%,45%)`)
|
||||
- **Layout**: Single-column, ATS-optimized
|
||||
|
||||
To customize fonts/colors, edit the CSS in the template. Update font files in `fonts/` if switching fonts.
|
||||
|
||||
## Negotiation Scripts (modes/_shared.md)
|
||||
|
||||
The negotiation section provides frameworks for salary discussions. Replace the example scripts with your own:
|
||||
- Target ranges
|
||||
- Geographic arbitrage strategy
|
||||
- Pushback responses
|
||||
|
||||
## Hooks (Optional)
|
||||
|
||||
Career-ops can integrate with external systems via Claude Code hooks. Example hooks:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "echo 'Career-ops session started'"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Save hooks in `.claude/settings.json` (Claude Code). OpenCode does not support hooks. For equivalent functionality, use custom commands (`.opencode/commands/`) or agents (`.opencode/agents/`) — see https://opencode.ai/docs/commands/.
|
||||
|
||||
## States (templates/states.yml)
|
||||
|
||||
The canonical states rarely need changing. If you add new states, update:
|
||||
1. `templates/states.yml`
|
||||
2. `normalize-statuses.mjs` (alias mappings)
|
||||
3. `modes/_shared.md` (any references)
|
||||
@@ -0,0 +1,21 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
Common questions from the community, answered in one place. For setup details see [docs/SETUP.md](SETUP.md). For anything not covered here, ask in [Discord](https://discord.gg/8pRpHETxa4) or open a [GitHub Discussion](https://github.com/santifer/career-ops/discussions).
|
||||
|
||||
---
|
||||
|
||||
## 1. Skills aren't loading on Windows — symlink error on install
|
||||
|
||||
Windows does not create symlinks by default, so Git checks out the CLI skill entrypoints (`.claude/skills/`, `.opencode/skills/`, etc.) as plain pointer files instead of real symlinks. The installer and updater both detect this automatically: run `node update-system.mjs apply` (or `npx @santifer/career-ops init` on a fresh install) and the `materializeSkillEntrypoints` step will replace the pointer files with the full canonical skill content. No manual `mklink` or Developer Mode changes are needed.
|
||||
|
||||
## 2. What is the difference between `scan` and `scan:full`?
|
||||
|
||||
`npm run scan` is the standard portal scanner — it reads the companies you have configured in `portals.yml`, hits their ATS APIs (Greenhouse, Ashby, Lever) directly, and consumes zero LLM tokens. Use it for your regular daily or weekly discovery run. `npm run scan:full` inverts the direction: instead of scanning your curated list, it walks public ATS company directories and surfaces any fresh postings that match your `title_filter` / `location_filter`, so you catch roles from companies you haven't manually added to `portals.yml`. Run `scan:full` when you want broader discovery beyond your tracked list.
|
||||
|
||||
## 3. How do I avoid hitting token or rate limits during a batch run?
|
||||
|
||||
Pass `--limit <N>` to `batch-runner.sh` to cap the number of offers processed in a single run (e.g. `./batch/batch-runner.sh --limit 5`) — this lets you inspect output quality before committing to a larger run. If a run is interrupted mid-way by a rate limit or network error, do not restart from scratch; use `./batch/batch-runner.sh --resume-paused` to skip already-completed jobs and pick up where you left off, avoiding wasted tokens on work that finished successfully.
|
||||
|
||||
## 4. Can I run career-ops on a cheaper or local model?
|
||||
|
||||
Yes — career-ops is fully AI-agnostic and works with any AI coding CLI or standalone script. See [docs/RUNNING_ON_A_BUDGET.md](RUNNING_ON_A_BUDGET.md) for a full guide covering OpenCode, Qwen CLI, DeepSeek, OpenRouter, Ollama, and other local or low-cost providers, along with recommended model sizes and token-saving best practices.
|
||||
@@ -0,0 +1,83 @@
|
||||
# Career-Ops on the Free Tier (Antigravity CLI)
|
||||
|
||||
career-ops works with **Antigravity CLI's free tier** — no API key or paid
|
||||
subscription required. This guide covers setup, limits, and trade-offs.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Install Antigravity CLI:
|
||||
|
||||
On macOS / Linux:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://antigravity.google/cli/install.sh | bash
|
||||
```
|
||||
|
||||
On Windows (PowerShell):
|
||||
|
||||
```powershell
|
||||
irm https://antigravity.google/cli/install.ps1 | iex
|
||||
```
|
||||
|
||||
On Windows (CMD):
|
||||
|
||||
```cmd
|
||||
curl -fsSL https://antigravity.google/cli/install.cmd -o install.cmd && install.cmd && del install.cmd
|
||||
```
|
||||
|
||||
2. Authenticate with your Google account:
|
||||
|
||||
```bash
|
||||
agy auth login
|
||||
```
|
||||
|
||||
3. Run career-ops as usual:
|
||||
|
||||
```bash
|
||||
agy # interactive — paste a URL, evaluate, scan, etc.
|
||||
agy -p "..." # headless / batch mode
|
||||
```
|
||||
|
||||
## Daily Limits
|
||||
|
||||
The free tier has daily request and token caps set by Google. Typical
|
||||
limits (subject to change):
|
||||
|
||||
| Resource | Approximate daily limit |
|
||||
|---------------------|------------------------|
|
||||
| Requests | 1,000 |
|
||||
| Input tokens | ~1 M |
|
||||
| Output tokens | ~100 K |
|
||||
|
||||
Limits reset at midnight Pacific Time. If you hit a cap the CLI returns
|
||||
a rate-limit error; career-ops will pause and suggest retrying tomorrow.
|
||||
|
||||
## Batch Mode Behavior
|
||||
|
||||
- `batch-runner.sh` spawns `claude -p` workers by default (Claude Code
|
||||
specific). To use Antigravity CLI workers instead, invoke them manually:
|
||||
|
||||
```bash
|
||||
agy -p "evaluate <URL>"
|
||||
```
|
||||
|
||||
- With free-tier limits, keep `--parallel 1` to avoid burning through
|
||||
your daily quota on parallel requests.
|
||||
- Large batches (50+ offers) will likely span multiple days. Use
|
||||
`--start-from` to resume where you left off.
|
||||
|
||||
## What Works Without Paying
|
||||
|
||||
| Feature | Free tier | Notes |
|
||||
|-------------------------|-----------|----------------------------------|
|
||||
| Offer evaluation (A-F) | ✅ | Full scoring pipeline |
|
||||
| Report generation (.md) | ✅ | Markdown reports |
|
||||
| Portal scanning | ✅ | Zero-token — hits APIs directly |
|
||||
| PDF generation | ✅ | Uses local Playwright, no tokens |
|
||||
| Batch processing | ⚠️ | Limited by daily quota |
|
||||
|
||||
## Upgrading
|
||||
|
||||
If you outgrow the free tier, you can switch to a paid Google AI plan
|
||||
or use Claude Code (`claude` CLI) with a Claude Max subscription. Both
|
||||
are fully supported — just authenticate with your preferred provider.
|
||||
@@ -0,0 +1,127 @@
|
||||
# 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 entry to `plugins-registry.json`, 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.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Plugin review (maintainer guide)
|
||||
|
||||
How to review a registry PR safely + fast. CI does the mechanical checks; you
|
||||
make the security + fit judgment calls. The registry pinned-SHA is the single
|
||||
chokepoint — a community plugin can never reach a user without a merged entry
|
||||
here, so review is the real control.
|
||||
|
||||
## Two tiers
|
||||
|
||||
- **Listed** — one entry in `plugins-registry.json`; the code stays in the
|
||||
author's `career-ops-plugin-<name>` repo; users `add` it. The default, low
|
||||
burden.
|
||||
- **Bundled** — promoted into `plugins/` (shipped, auto-updated). Reserve for
|
||||
broadly-useful, low/zero-key, well-tested plugins (how `apify`/`gmail`/`notion`
|
||||
were absorbed). Adds a maintenance commitment + a `config/plugins.example.yml`
|
||||
block + an `.env.example` entry.
|
||||
|
||||
## What CI already checked (don't re-do by hand)
|
||||
|
||||
`plugin-registry-validate` validates the entry shape + uniqueness, clones each
|
||||
entry at its pinned SHA, and runs the min-file / manifest / static-audit checks
|
||||
in a no-secret, read-only sandbox (no plugin code is executed). If it's red,
|
||||
stop.
|
||||
|
||||
## Your judgment calls (the checklist)
|
||||
|
||||
- **Naming + identity:** repo is `career-ops-plugin-<name>`, `id` == name minus
|
||||
prefix, `sha` pinned to the commit you actually read.
|
||||
- **Read the diff** (for an update, the old→new diff): does it do only what it
|
||||
says? Watch for time-bombs, env-gated branches, obfuscation.
|
||||
- **Egress:** `allowedHosts` are real public hosts; no IP literals / metadata /
|
||||
`*.internal`; localhost only with a stated reason.
|
||||
- **Capability surface:** hooks ⊆ the five; no apply/submit; no core-owned
|
||||
secrets in `requiredEnv`. For an **update**, any growth in hooks / env / hosts
|
||||
is a fresh consent surface — review as a new listing.
|
||||
- **Data direction:** reads PUBLIC data or the user's OWN account only. No
|
||||
centralized infrastructure, no auto-submit, no blind-apply.
|
||||
- **Wording (public-forever):** description / README / skill carry no commercial
|
||||
/ hosted-service / monetization language. career-ops is free and local-first;
|
||||
"approved" means "we reviewed this commit", nothing more.
|
||||
- **Skill (if any):** domain-scoped — it teaches how to drive the plugin, and
|
||||
does NOT instruct the agent to edit core files, change scoring, reveal secrets,
|
||||
or act outside the plugin's hooks.
|
||||
- **License:** MIT-compatible.
|
||||
|
||||
## Auto-merge (only when it's truly safe)
|
||||
|
||||
Most updates are human-reviewed. An update may auto-merge ONLY when ALL hold: a
|
||||
known author (2FA + a verified commit signature on the pinned SHA), the diff is
|
||||
**provably non-logic** (metadata / version / strings / comments / whitespace
|
||||
only — no change to control flow or executable statements), **zero** new
|
||||
capability (hooks/env/hosts/deps), every deterministic gate green, and the
|
||||
agentic reviewer raised no flag. Auto-merge lands the row **staged**; the user's
|
||||
shipped pin advances only after the canary window. First listings and any
|
||||
capability or logic change are reviewed by a human regardless of author trust.
|
||||
The agentic reviewer can only **raise** risk (escalate to a human) — never
|
||||
approve.
|
||||
|
||||
## ToS-grey / authenticated integrations
|
||||
|
||||
Anything that scrapes a platform behind a login or whose terms forbid automated
|
||||
access (authenticated LinkedIn, session-gated boards) is **not** bundled and
|
||||
**not** registry-listed. It can still be a `career-ops-plugin-<name>` repo users
|
||||
install explicitly into `plugins.local/` with the full "you're trusting this
|
||||
author" prompt — the project doesn't host that liability in-tree.
|
||||
|
||||
## Bundled plugins are reference seeds (no feature PRs on `plugins/`)
|
||||
|
||||
A bundled plugin (`plugins/apify`, `plugins/gmail`, `plugins/notion`) is a
|
||||
**reference seed**: a reviewed, minimal, stable example. We do **not** accept
|
||||
feature PRs against it — close-redirect them to "publish `career-ops-plugin-<id>`
|
||||
and we'll register it as the maintained successor." Bundled plugins only take
|
||||
PRs for **security or release-compat fixes** (keeping the seed working across
|
||||
core releases).
|
||||
|
||||
**Reviewing a `supersedesBundled: true` registry entry.** Such an entry says
|
||||
"when installed at this pin, my plugin should take precedence over the bundled
|
||||
plugin of the same id." Review it with that weight:
|
||||
|
||||
- The entry's `id` MUST match an existing bundled plugin in `plugins/` (a
|
||||
successor for a non-existent seed is meaningless — reject).
|
||||
- It is the **same trust bar as any registry entry** (naming, manifest, egress,
|
||||
static audit, pinned sha) — plus the awareness that approving it lets users
|
||||
*replace* a reviewed bundled integration. Read the diff against the seed.
|
||||
- Precedence is enforced engine-side **only** for a user who installs it at the
|
||||
exact pinned sha; the bundled seed remains the always-present fallback, so an
|
||||
abandoned successor degrades gracefully (the seed simply stays in charge).
|
||||
- Original-author successors (the contributor whose PR seeded the bundled
|
||||
plugin) are the natural, encouraged path — hand them the migration warmly.
|
||||
@@ -0,0 +1,239 @@
|
||||
# Running Career-Ops on a Budget
|
||||
|
||||
Token usage costs and rate limits are the most common bottlenecks when setting up a high-volume job search pipeline. Since Career-Ops processes full job descriptions, evaluates them against your CV across 10 dimensions, and tailors resumes/cover letters, the context size can grow quickly.
|
||||
|
||||
Fortunately, **Career-Ops is completely AI-agnostic.** The pipeline relies on the AI coding CLI (or standalone scripts) to process prompt files under `modes/`. This means you can point your CLI to cheaper API providers or local models with **zero code changes** in Career-Ops.
|
||||
|
||||
---
|
||||
|
||||
## 1. The Core Concept: Model Agnosticism
|
||||
|
||||
Career-Ops is composed of local templates, Markdown prompts, and Node/Playwright scripts. The AI logic is driven entirely by whichever AI coding CLI you run it in (e.g., Claude Code, OpenCode, Qwen CLI, Codex, Antigravity CLI, or Grok Build CLI).
|
||||
|
||||
By choosing a CLI that supports custom model configurations and routing it to a cheaper API provider or local LLM, you can drastically reduce your pipeline running costs without losing any functionality.
|
||||
|
||||
---
|
||||
|
||||
## 2. Configuring Alternative CLI Setups
|
||||
|
||||
Different CLIs offer different levels of flexibility for model routing. The two most common options for budget setups are **OpenCode** and **Qwen CLI**.
|
||||
|
||||
### OpenCode CLI
|
||||
OpenCode is an open-source coding agent that easily routes to custom API providers (like DeepSeek, OpenRouter, Together AI) or local endpoints (Ollama).
|
||||
|
||||
To configure OpenCode with a custom provider:
|
||||
1. Initialize/open OpenCode in the project directory:
|
||||
```bash
|
||||
opencode
|
||||
```
|
||||
2. Open its configuration settings (usually located in `.opencode/config.json` or configured via CLI prompts/settings).
|
||||
3. Set the `provider` to your chosen endpoint (e.g., OpenRouter or a custom OpenAI-compatible endpoint).
|
||||
4. Configure the environment variables for custom endpoints if needed:
|
||||
```bash
|
||||
# For Git Bash / Linux / macOS:
|
||||
export OPENAI_API_BASE="https://openrouter.ai/api/v1"
|
||||
export OPENAI_API_KEY="your_openrouter_api_key_here"
|
||||
|
||||
# For Windows CMD:
|
||||
set OPENAI_API_BASE=https://openrouter.ai/api/v1
|
||||
set OPENAI_API_KEY=your_openrouter_api_key_here
|
||||
|
||||
# For Windows PowerShell:
|
||||
$env:OPENAI_API_BASE="https://openrouter.ai/api/v1"
|
||||
$env:OPENAI_API_KEY="your_openrouter_api_key_here"
|
||||
```
|
||||
|
||||
### Qwen CLI
|
||||
Qwen CLI natively supports Qwen models but can be configured to point to any custom OpenAI-compatible API base URL:
|
||||
```bash
|
||||
# For Git Bash / Linux / macOS:
|
||||
export QWEN_API_BASE="https://api.deepseek.com/v1"
|
||||
export QWEN_API_KEY="your_deepseek_api_key_here"
|
||||
|
||||
# For Windows CMD:
|
||||
set QWEN_API_BASE=https://api.deepseek.com/v1
|
||||
set QWEN_API_KEY=your_deepseek_api_key_here
|
||||
|
||||
# For Windows PowerShell:
|
||||
$env:QWEN_API_BASE="https://api.deepseek.com/v1"
|
||||
$env:QWEN_API_KEY="your_deepseek_api_key_here"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Recommended Cost-Efficient Models
|
||||
|
||||
When choosing a budget-friendly model, you need strong reasoning capabilities to handle the multi-dimensional scoring and resume tailoring. Here are the recommended models that hold up well under evaluation:
|
||||
|
||||
| Model | Provider / Endpoint | Price per 1M Input / Output Tokens | Why use it |
|
||||
|-------|---------------------|------------------------------------|------------|
|
||||
| **DeepSeek V3** | DeepSeek API / OpenRouter | ~$0.14 / ~$0.28 | **Top Recommendation.** Unmatched reasoning-to-price ratio; performs close to frontier models at a fraction of the cost. |
|
||||
| **DeepSeek-Coder-V2** | DeepSeek API / OpenRouter | ~$0.14 / ~$0.28 | Excellent instruction-following for structured Markdown and resume tailoring. |
|
||||
| **Qwen-2.5-Coder (32B / 72B)** | OpenRouter / DeepInfra | ~$0.07 - ~$0.30 | Strong coding and structured reasoning, highly cost-effective. |
|
||||
| **GLM-4-Air / GLM-4** | Zhipu AI / OpenRouter | Very Cheap | Reliable multi-turn reasoning and JSON/Markdown generation. |
|
||||
| **Gemini 2.5 Flash** | Google AI Studio | Free Tier (15 RPM) | Available via the standalone script `node gemini-eval.mjs`. Excellent for zero-cost low-volume runs, but subject to rate limits. |
|
||||
|
||||
> **Standalone evaluator (no CLI config needed):** every OpenAI-compatible provider above (DeepSeek, Qwen, GLM, Together, Groq, OpenRouter, …) works directly through `node openai-eval.mjs` — just set a base URL, model, and key:
|
||||
> ```bash
|
||||
> OPENAI_BASE_URL=https://openrouter.ai/api/v1 \
|
||||
> OPENAI_MODEL=deepseek/deepseek-chat \
|
||||
> OPENAI_API_KEY=your_key \
|
||||
> node openai-eval.mjs --file ./jds/job.txt
|
||||
> ```
|
||||
> Run `node openai-eval.mjs --help` for per-provider examples. For 100% local/private use, point `--url` at a local server (LM Studio / llama.cpp / vLLM) or use `node ollama-eval.mjs`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Local LLM Tradeoffs (Ollama / Llama.cpp)
|
||||
|
||||
Running a model 100% locally via Ollama is completely free, but it comes with significant tradeoffs:
|
||||
|
||||
### The Size vs. Quality Tradeoff
|
||||
- **Avoid Small Models (e.g., 8B parameters)**: Models like Llama 3 8B or Qwen-2.5-Coder 7B are generally **too weak** for Career-Ops. They frequently fail to follow the complex evaluation schemas (A-G blocks), fail to output valid Markdown/JSON structures, or generate low-quality, generic resume customizations.
|
||||
- **Minimum Recommended Size**: Use at least a **32B+ or 70B+ model** (such as Qwen 2.5 Coder 32B/72B or Llama 3.1 70B) for reliable scoring and high-quality resume tailoring.
|
||||
|
||||
### Hardware & VRAM Requirements
|
||||
Running 32B or 70B models locally requires substantial system resources:
|
||||
- A **32B model** requires a GPU with at least **16GB - 24GB VRAM** (e.g., RTX 3090/4090, Mac Studio, or Apple Silicon Mac with 32GB+ unified memory).
|
||||
- A **70B model** requires at least **48GB VRAM** to run at decent speeds.
|
||||
|
||||
> 💡 **Budget Tip**: For most users, running **DeepSeek V3** or **Qwen 2.5 Coder 72B** via a cheap hosted API (like DeepSeek directly or OpenRouter) is far more efficient and cost-effective than investing in local hardware, costing only a few cents for dozens of evaluations.
|
||||
|
||||
---
|
||||
|
||||
## 5. Token-Saving Best Practices
|
||||
|
||||
To prevent unnecessary API costs or hitting rate limits, implement the following practices:
|
||||
|
||||
1. **Use the Batch Limit Flag**:
|
||||
Instead of manually splitting `batch/batch-input.tsv`, use the `--limit <N>` flag to process only a small capped number of offers (e.g. 5-10) in a single run. This lets you inspect the output quality before committing to a larger run:
|
||||
```bash
|
||||
./batch/batch-runner.sh --limit 5
|
||||
```
|
||||
2. **Use the Dry Run Flag**:
|
||||
Always run a dry run first to verify which offers will be processed:
|
||||
```bash
|
||||
./batch/batch-runner.sh --dry-run
|
||||
```
|
||||
3. **Resume Interrupted Runs**:
|
||||
If a batch run is interrupted by a rate limit or network error, do not restart from scratch. Use the `--resume-paused` flag to continue from where it left off, skipping completed jobs and preventing wasted tokens:
|
||||
```bash
|
||||
./batch/batch-runner.sh --resume-paused
|
||||
```
|
||||
4. **Use `--verify` on Scans**:
|
||||
When running job board scans, use the liveness verifier to filter out expired postings before they enter your pipeline. This prevents wasting LLM tokens evaluating closed jobs:
|
||||
```bash
|
||||
npm run scan -- --verify
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Worked Example: Running the Pipeline Cheaply
|
||||
|
||||
Here is a concrete, end-to-end walkthrough of scanning for jobs and evaluating a single posting using **DeepSeek V3 via OpenRouter** and the standalone `openai-eval.mjs` evaluator. This bypasses the need for an expensive CLI agent for the heavy evaluation block.
|
||||
|
||||
### Step 1: Scan for Job Offers (0 Tokens)
|
||||
The portal scanner queries ATS APIs directly using Playwright and standard HTTPS requests. It doesn't use the LLM to read job boards.
|
||||
```bash
|
||||
node scan.mjs
|
||||
```
|
||||
**Cost:** 0 tokens, $0.00.
|
||||
*(This generates a list of new job URLs and populates `data/pipeline.md`.)*
|
||||
|
||||
### Step 2: Fetch the Job Description (0 Tokens)
|
||||
Open one of the URLs found by the scanner, copy the text of the job description, and save it locally (e.g., `jds/my-target-role.txt`).
|
||||
|
||||
### Step 3: Evaluate the Offer (~4,500 Tokens)
|
||||
We'll run the evaluation against OpenRouter's DeepSeek V3 endpoint. The script reads your `cv.md` and the job description, then generates the full A-G evaluation report and tracker entry.
|
||||
|
||||
```bash
|
||||
OPENAI_API_KEY="sk-or-your_openrouter_key" \
|
||||
node openai-eval.mjs \
|
||||
--url https://openrouter.ai/api/v1 \
|
||||
--model deepseek/deepseek-chat \
|
||||
--file ./jds/my-target-role.txt
|
||||
```
|
||||
|
||||
**Approximate Token Usage:**
|
||||
- **Input:** ~3,500 tokens (System prompt + your `cv.md` + JD)
|
||||
- **Output:** ~1,000 tokens (The A-G evaluation report)
|
||||
- **Cost:** ~4,500 tokens total. At DeepSeek V3 prices (~$0.14/1M input, ~$0.28/1M output), this costs **less than $0.001** per evaluation.
|
||||
|
||||
### Step 4: Generate ATS-Optimized PDF (0 Tokens)
|
||||
Once you have the evaluation report, the PDF generator uses Playwright to compile your local HTML/CSS into a tailored CV.
|
||||
|
||||
```bash
|
||||
node generate-pdf.mjs
|
||||
```
|
||||
**Cost:** 0 tokens, $0.00.
|
||||
|
||||
By routing the heaviest step (Evaluation) to a cheap OpenAI-compatible endpoint, a complete end-to-end job application cycle drops from ~$0.05 - $0.15 on frontier models to a fraction of a cent, allowing you to run bulk batch processing affordably.
|
||||
|
||||
---
|
||||
|
||||
## 7. Zero-Cost Paths (No Claude / Paid CLI Required)
|
||||
|
||||
Career-ops ships a full pipeline that runs **entirely on free models** — no Claude Code, no Anthropic API key, no paid CLI subscription. Everything below works out of the box after a one-time `.env` setup.
|
||||
|
||||
### Path A: OpenRouter Free Models (`or:*` scripts)
|
||||
|
||||
No Claude Code CLI required — uses OpenRouter free models with automatic fallback.
|
||||
|
||||
**npm shortcuts** (cover the whole pipeline):
|
||||
|
||||
```bash
|
||||
npm run or:scan # Scan portals for new listings (Greenhouse API, 0 tokens)
|
||||
npm run or:pipeline # Process all pending URLs from data/pipeline.md
|
||||
npm run or:eval # Evaluate a single offer (paste URL or text)
|
||||
npm run or:apply # Generate draft application answers for a report
|
||||
```
|
||||
|
||||
**Usage**
|
||||
|
||||
```bash
|
||||
node openrouter-runner.mjs scan # Scan Greenhouse API companies for new listings
|
||||
node openrouter-runner.mjs evaluate <url> # Evaluate a job by URL
|
||||
node openrouter-runner.mjs evaluate # Paste job text interactively
|
||||
node openrouter-runner.mjs pipeline # Process all pending URLs from pipeline.md
|
||||
node openrouter-runner.mjs apply <report_no> # Generate draft application form answers
|
||||
node openrouter-runner.mjs models # List available free models
|
||||
node openrouter-runner.mjs help # Show this help
|
||||
```
|
||||
|
||||
**Setup:**
|
||||
|
||||
```bash
|
||||
1. copy .env.example .env
|
||||
2. Add OPENROUTER_API_KEY=sk-or-v1-... to .env
|
||||
3. Free API key: https://openrouter.ai
|
||||
```
|
||||
|
||||
### Path B: Fully Local with Ollama (`ollama:eval`)
|
||||
|
||||
If you want **zero network calls** and complete privacy, run evaluations against a local Ollama instance:
|
||||
|
||||
```bash
|
||||
npm run ollama:eval
|
||||
```
|
||||
|
||||
This calls `ollama-eval.mjs` which hits your local Ollama server. No API key, no internet, no cost. See [Section 4](#4-local-llm-tradeoffs-ollama--llamacpp) for model size recommendations (32B+ minimum for reliable scoring).
|
||||
|
||||
### Path C: Any OpenAI-Compatible Endpoint (`openai:eval`)
|
||||
|
||||
Point at **any** endpoint that speaks the OpenAI chat-completions API — NVIDIA NIM (free tier), Zhipu GLM, Together, Groq, LM Studio, llama.cpp, vLLM, or even Ollama's `/v1` route:
|
||||
|
||||
```bash
|
||||
npm run openai:eval
|
||||
```
|
||||
|
||||
Configure via `.env`:
|
||||
|
||||
```dotenv
|
||||
OPENAI_BASE_URL=https://integrate.api.nvidia.com/v1 # or any compatible base URL
|
||||
OPENAI_MODEL=meta/llama-3.1-70b-instruct # model name at that endpoint
|
||||
OPENAI_API_KEY=your_provider_key_here # some free endpoints need a key
|
||||
```
|
||||
|
||||
Run `node openai-eval.mjs --help` for per-provider examples with exact URLs and model names.
|
||||
|
||||
**Which to pick:** Start with **Path A** (one env var, full pipeline). Use B for air-gapped/local-only, C if you already run your own inference endpoint.
|
||||
@@ -0,0 +1,322 @@
|
||||
# Scripts Reference
|
||||
|
||||
All scripts live in the project root as `.mjs` modules and are exposed via `npm run <name>`.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Command | Script | Purpose |
|
||||
|---------|--------|---------|
|
||||
| `npm run doctor` | `doctor.mjs` | Validate setup prerequisites |
|
||||
| `npm run verify` | `verify-pipeline.mjs` | Check pipeline data integrity |
|
||||
| `npm run normalize` | `normalize-statuses.mjs` | Fix non-canonical statuses |
|
||||
| `npm run dedup` | `dedup-tracker.mjs` | Remove duplicate tracker entries |
|
||||
| `npm run merge` | `merge-tracker.mjs` | Merge batch TSVs into applications.md |
|
||||
| `npm run pdf` | `generate-pdf.mjs` | Convert HTML to ATS-optimized PDF |
|
||||
| `npm run build:latex` | `build-cv-latex.mjs` | Build .tex from structured JSON payload |
|
||||
| `npm run sync-check` | `cv-sync-check.mjs` | Validate CV/profile consistency |
|
||||
| `npm run patterns` | `analyze-patterns.mjs` | Analyze tracker outcomes and report patterns |
|
||||
| `npm run add` | `add-entry.mjs` | Dedup + insert a `/career-ops add` entry into cv.md / article-digest.md |
|
||||
| `npm run update:check` | `update-system.mjs check` | Check for upstream updates |
|
||||
| `npm run update` | `update-system.mjs apply` | Apply upstream update |
|
||||
| `npm run rollback` | `update-system.mjs rollback` | Rollback last update |
|
||||
| `npm run liveness` | `check-liveness.mjs` | Test if job URLs are still active |
|
||||
| `npm run scan` | `scan.mjs` | Zero-token portal scanner |
|
||||
| `npm run scan:full` | `scan-ats-full.mjs` | Reverse ATS discovery scanner |
|
||||
| `npm run validate:portals` | `validate-portals.mjs` | Validate portals.yml shape before scanning |
|
||||
| `npm run tracker` | `tracker.mjs` | SQLite derived index over applications.md — sync/query/history/export |
|
||||
| `npm run find` | `find.mjs` | Resolve a report#/tracker#/company query to its full pipeline identity |
|
||||
|
||||
---
|
||||
|
||||
## doctor
|
||||
|
||||
Validates that all prerequisites are in place: Node.js >= 18, dependencies installed, Playwright chromium, required files (`cv.md`, `config/profile.yml`, `portals.yml`), fonts directory, and auto-creates `data/`, `output/`, `reports/` if missing.
|
||||
|
||||
```bash
|
||||
npm run doctor
|
||||
```
|
||||
|
||||
**Exit codes:** `0` all checks passed, `1` one or more checks failed (fix messages printed).
|
||||
|
||||
---
|
||||
|
||||
## verify
|
||||
|
||||
Health check for pipeline data integrity. Validates `data/applications.md` against nine rules: canonical statuses (per `templates/states.yml`), no duplicate company+role pairs, all report links point to existing files, scores match `X.XX/5` / `N/A` / `DUP`, rows have proper pipe-delimited format, no pending TSVs in `batch/tracker-additions/`, no markdown bold in scores, no two `reports/*.md` files covering the same company+role, and no orphan reports without a tracker row (#1425). The report checks are warning-level: duplicate reports can be legitimate (re-evaluation after a JD change), so they never fail the run.
|
||||
|
||||
```bash
|
||||
npm run verify
|
||||
```
|
||||
|
||||
**Exit codes:** `0` pipeline clean (zero errors), `1` errors found. Warnings (e.g. possible duplicates) do not cause a non-zero exit.
|
||||
|
||||
---
|
||||
|
||||
## normalize
|
||||
|
||||
Maps non-canonical statuses to their canonical equivalents and strips markdown bold and dates from the status column. Aliases like `Enviada` become `Aplicado`, `CERRADA` becomes `Descartado`, etc. DUPLICADO info is moved to the notes column.
|
||||
|
||||
```bash
|
||||
npm run normalize # apply changes
|
||||
npm run normalize -- --dry-run # preview without writing
|
||||
```
|
||||
|
||||
Creates a `.bak` backup of `applications.md` before writing.
|
||||
|
||||
**Exit codes:** `0` always (changes or no changes).
|
||||
|
||||
---
|
||||
|
||||
## dedup
|
||||
|
||||
Removes duplicate entries from `applications.md` by grouping on normalized company name + fuzzy role match. Keeps the entry with the highest score. If a removed entry had a more advanced pipeline status, that status is promoted to the keeper.
|
||||
|
||||
```bash
|
||||
npm run dedup # apply changes
|
||||
npm run dedup -- --dry-run # preview without writing
|
||||
```
|
||||
|
||||
Creates a `.bak` backup before writing.
|
||||
|
||||
**Exit codes:** `0` always.
|
||||
|
||||
---
|
||||
|
||||
## merge
|
||||
|
||||
Merges batch tracker additions (`batch/tracker-additions/*.tsv`) into `applications.md`. Handles 9-column TSV, 8-column TSV, and pipe-delimited markdown formats. Detects duplicates by report number, entry number, and company+role fuzzy match. Higher-scored re-evaluations update existing entries in place.
|
||||
|
||||
```bash
|
||||
npm run merge # apply merge
|
||||
npm run merge -- --dry-run # preview without writing
|
||||
npm run merge -- --verify # merge then run verify-pipeline
|
||||
```
|
||||
|
||||
Processed TSVs are moved to `batch/tracker-additions/merged/`.
|
||||
|
||||
**Exit codes:** `0` success, `1` verification errors (with `--verify`).
|
||||
|
||||
---
|
||||
|
||||
## validate:portals
|
||||
|
||||
Validates `portals.yml` before running the scanner. The validator is offline: it reads YAML, loads local provider IDs from `providers/*.mjs`, and checks common configuration mistakes without fetching any job boards.
|
||||
|
||||
It reports errors for invalid YAML shape, unknown explicit providers, malformed URLs, empty filter keywords, and invalid local parser blocks. Duplicate enabled company names are warnings because they may be intentional during migrations, but they are worth reviewing.
|
||||
|
||||
```bash
|
||||
npm run validate:portals
|
||||
npm run validate:portals -- --file templates/portals.example.yml
|
||||
node validate-portals.mjs --self-test
|
||||
```
|
||||
|
||||
**Exit codes:** `0` no errors (warnings allowed), `1` one or more errors found.
|
||||
|
||||
---
|
||||
|
||||
## pdf
|
||||
|
||||
Renders an HTML file to a print-quality, ATS-parseable PDF via headless Chromium. Resolves font paths from `fonts/`, normalizes Unicode for ATS compatibility (em-dashes, smart quotes, zero-width characters), and reports page count and file size.
|
||||
|
||||
```bash
|
||||
npm run pdf -- input.html output.pdf
|
||||
npm run pdf -- input.html output.pdf --format=letter # US letter
|
||||
npm run pdf -- input.html output.pdf --format=a4 # A4 (default)
|
||||
```
|
||||
|
||||
**Exit codes:** `0` PDF generated, `1` missing arguments or generation failure.
|
||||
|
||||
---
|
||||
|
||||
## build:latex
|
||||
|
||||
Builds a `.tex` file from a structured JSON payload, handling template merge and LaTeX escaping automatically. The JSON is produced by the agent during evaluation — this script replaces the manual LaTeX generation step in `modes/latex.md`.
|
||||
|
||||
```bash
|
||||
node build-cv-latex.mjs input.json output.tex
|
||||
node build-cv-latex.mjs --test
|
||||
```
|
||||
|
||||
**Exit codes:** `0` file generated, `1` missing inputs, invalid JSON, unresolved placeholders, or template not found.
|
||||
|
||||
---
|
||||
|
||||
## sync-check
|
||||
|
||||
Validates that the career-ops setup is internally consistent: `cv.md` exists and is not too short, `config/profile.yml` exists with required fields, no hardcoded metrics in `modes/_shared.md` or `batch/batch-prompt.md`, and `article-digest.md` freshness (warns if older than 30 days).
|
||||
|
||||
```bash
|
||||
npm run sync-check
|
||||
```
|
||||
|
||||
**Exit codes:** `0` no errors (warnings allowed), `1` errors found.
|
||||
|
||||
---
|
||||
|
||||
## patterns
|
||||
|
||||
Analyzes application outcomes, scores, archetypes, blockers, remote policy, and company size from `data/applications.md` and linked reports. New reports should include `## Machine Summary` YAML; `analyze-patterns.mjs` uses it first and falls back to legacy markdown parsing for older reports.
|
||||
|
||||
```bash
|
||||
npm run patterns
|
||||
npm run patterns -- --summary
|
||||
npm run patterns -- --min-threshold 3
|
||||
node analyze-patterns.mjs --self-test
|
||||
```
|
||||
|
||||
**Exit codes:** `0` analysis succeeded, `1` insufficient data or parser self-test failure.
|
||||
|
||||
---
|
||||
|
||||
## update:check
|
||||
|
||||
Checks whether a newer version of career-ops is available upstream. Outputs JSON to stdout:
|
||||
|
||||
```bash
|
||||
npm run update:check
|
||||
```
|
||||
|
||||
Possible JSON responses:
|
||||
|
||||
| `status` | Meaning |
|
||||
|----------|---------|
|
||||
| `up-to-date` | Local version matches remote |
|
||||
| `update-available` | Newer version exists (includes `local`, `remote`, `changelog`) |
|
||||
| `dismissed` | User dismissed the update prompt |
|
||||
| `offline` | Could not reach GitHub |
|
||||
|
||||
**Exit codes:** `0` always.
|
||||
|
||||
---
|
||||
|
||||
## update
|
||||
|
||||
Applies the upstream update. Creates a timestamped backup branch (`backup-pre-update-<version>-<YYYYMMDDTHHMMSSZ>`), fetches from the canonical repo, checks out only system-layer files, runs `npm install`, and commits. The timestamp is derived from UTC ISO time with separators and milliseconds removed (for example, `backup-pre-update-1.8.1-20260608T071302Z`). User-layer files (`cv.md`, `config/profile.yml`, `data/`, etc.) are never touched.
|
||||
|
||||
```bash
|
||||
npm run update
|
||||
```
|
||||
|
||||
**Exit codes:** `0` success, `1` lock conflict or safety violation.
|
||||
|
||||
---
|
||||
|
||||
## rollback
|
||||
|
||||
Restores system-layer files from the most recent backup branch created during an update. Rollback prefers the newest timestamped branch matching `backup-pre-update-<version>-<YYYYMMDDTHHMMSSZ>` and still accepts legacy `backup-pre-update-<version>` branches for older installs.
|
||||
|
||||
```bash
|
||||
npm run rollback
|
||||
```
|
||||
|
||||
**Exit codes:** `0` success, `1` no backup branch found or git error.
|
||||
|
||||
---
|
||||
|
||||
## liveness
|
||||
|
||||
Tests whether job posting URLs are still live using headless Chromium. Detects expired patterns (e.g. "job no longer available"), HTTP 404/410, ATS redirect patterns, and apply-button presence. Supports multi-language expired patterns (English, German, French).
|
||||
|
||||
```bash
|
||||
npm run liveness -- https://example.com/job/123
|
||||
npm run liveness -- https://a.com/job/1 https://b.com/job/2
|
||||
npm run liveness -- --file urls.txt
|
||||
```
|
||||
|
||||
Each URL gets a verdict: `active`, `expired`, or `uncertain` with a reason.
|
||||
|
||||
**Exit codes:** `0` all URLs active, `1` any expired or uncertain.
|
||||
|
||||
---
|
||||
|
||||
## scan
|
||||
|
||||
Zero-token portal scanner. Runs configured local parsers for SSR/static career pages and hits ATS APIs (Greenhouse, Ashby, Lever) directly — no LLM tokens consumed. Reads `portals.yml` for target companies, outputs matching listings to stdout, and optionally appends to `data/pipeline.md`.
|
||||
|
||||
`scan_history.recheck_after_days` in `portals.yml` lets old `added` URLs become eligible for recheck after the configured number of days. If absent, scan-history dedup keeps the historical behavior and dedups forever. Permanent invalid statuses such as blocked host and malformed URL remain permanent.
|
||||
|
||||
For custom SSR pages, configure a tracked company with `scan_method: local_parser` and a `parser` block. The parser can be written in JavaScript, Python, or any language available as a local executable. Company-specific parsers usually already know their source URL and only need to print JSON jobs to stdout:
|
||||
|
||||
```yaml
|
||||
parser:
|
||||
command: node
|
||||
script: scripts/parsers/example-company-jobs.js
|
||||
format: jobs-json-v1
|
||||
```
|
||||
|
||||
Use `args` only for reusable parsers that intentionally accept runtime parameters such as `{careers_url}` or `{company}`.
|
||||
|
||||
If a parser writes full extraction artifacts for debugging or audit, store them under `data/parser-output/{company}/`. `scan.mjs` reads stdout and does not require those JSON files after parsing. Keep generated JSON artifacts out of git; `.gitkeep` placeholders are the only exception for preserving directory structure.
|
||||
|
||||
```bash
|
||||
npm run scan
|
||||
```
|
||||
|
||||
**Exit codes:** `0` scan completed, `1` configuration error or no portals.yml found.
|
||||
|
||||
---
|
||||
|
||||
## scan:full
|
||||
|
||||
Reverse ATS discovery scanner. Where `scan.mjs` scans the companies you track in `portals.yml`, this inverts the direction: it walks public directories of companies per ATS (Greenhouse, Lever, Ashby, Workday) and surfaces fresh postings matching your `portals.yml` `title_filter` / `location_filter` — no manual company curation. Company directories come from the public [job-board-aggregator](https://github.com/Feashliaa/job-board-aggregator) dataset, cached in `data/cache/` for 24 hours.
|
||||
|
||||
Postings without a usable publish date are skipped — a reverse scan is only useful for fresh postings. New matches are appended to `data/pipeline.md` and `data/scan-history.tsv` in the same format as `scan.mjs`.
|
||||
|
||||
```bash
|
||||
npm run scan:full # all ATS directories, last 3 days
|
||||
node scan-ats-full.mjs --since 7 # postings from the last 7 days
|
||||
node scan-ats-full.mjs --ats greenhouse,workday # subset of sources
|
||||
node scan-ats-full.mjs --limit 200 # max companies per ATS
|
||||
node scan-ats-full.mjs --dry-run # preview without writing
|
||||
node scan-ats-full.mjs --liveness # Playwright-verify matches first
|
||||
node scan-ats-full.mjs --md-out notes/scans # also write a dated markdown digest
|
||||
```
|
||||
|
||||
**Exit codes:** `0` scan completed, `1` configuration error (no portals.yml, unknown `--ats` source) or fatal scan error.
|
||||
|
||||
---
|
||||
|
||||
## tracker
|
||||
|
||||
SQLite **derived index** for the applications tracker (RFC #918, phase 1). `data/applications.md` stays the source of truth; `data/applications.db` is built from it by `sync` and is safe to delete at any time — it regenerates on the next sync. All writes keep going to the markdown exactly as today (`merge-tracker.mjs`, hand edits); the index is read-only infrastructure.
|
||||
|
||||
Why: at hundreds of rows a markdown table degrades structurally (encoding corruption, column drift, `|` inside cells shifting columns), and agents grepping it get model-dependent results. The index normalizes on sync, so a query returns the same rows for every model on every CLI — and corruption is detected at sync time instead of propagating silently.
|
||||
|
||||
Zero new dependencies — uses `node:sqlite`, built into Node ≥ 22.5.
|
||||
|
||||
```bash
|
||||
node tracker.mjs sync # (re)build applications.db from applications.md
|
||||
node tracker.mjs sync --check # diagnose corruption only, no write (exit 1 if issues found)
|
||||
node tracker.mjs query --status Applied --since 2026-05-01
|
||||
node tracker.mjs query --company acme --json
|
||||
node tracker.mjs history --id 42 # status transitions observed across syncs (Applied → Interview → ...)
|
||||
node tracker.mjs export # inverse: index → canonical markdown table on stdout
|
||||
node tracker.mjs export --out repaired.md # write to a file (existing file backed up to .bak first)
|
||||
```
|
||||
|
||||
`query` and `history` auto-resync when the markdown changed since the last sync, so the index can never serve stale reads.
|
||||
|
||||
`sync` detects and reports the corruption classes markdown accumulates — mojibake placeholder cells, scores stranded in the status column, non-canonical statuses (resolved via `templates/states.yml` aliases), missing/duplicate ids, stray pipes — and normalizes them **in the index only**; the markdown is never modified. Fix at the source with `normalize-statuses.mjs` / `dedup-tracker.mjs`, then re-sync. Status changes between syncs accumulate in a `status_events` table, which gives `analyze-patterns.mjs` a real funnel instead of only the current snapshot.
|
||||
|
||||
`export` is the inverse of `sync` (round-trip `md → db → md` is lossless for clean input — enforced by `test-all.mjs`). It writes to stdout by default and never touches `applications.md` unless you explicitly pass it as `--out`. Phase 2 of #918 (DB becomes source of truth, markdown becomes a rendered view) is a separate, explicit per-user opt-in — not part of this script yet.
|
||||
|
||||
**Exit codes:** `0` success, `1` validation error, missing prerequisites (Node < 22.5, no `applications.md` to index), or corruption found by `sync --check`.
|
||||
|
||||
---
|
||||
|
||||
## find
|
||||
|
||||
Resolves a report number, tracker number, or company/role fragment to its full pipeline identity: company, role, tracker#, report#, canonical status, PDF path (from `data/pdf-index.tsv`), and report path. "Apply to #13" is ambiguous — report numbers and tracker row numbers diverge — and answering it used to require opening three files; this does it in one read-only lookup.
|
||||
|
||||
Zero dependencies, strictly read-only. Numeric queries match **both** the tracker # column and the report number from the Report link (`012` and `12` are the same number), so collisions between the two numbering schemes surface as multiple rows instead of a silent wrong pick. Text queries match company/role by case-insensitive substring, with the shared fuzzy matcher (`role-matcher.mjs`) as fallback for multi-word phrases.
|
||||
|
||||
```bash
|
||||
node find.mjs 13 # report# OR tracker# 13 — shows both if they differ
|
||||
node find.mjs acme # company fragment
|
||||
node find.mjs "data engineer" # role phrase (fuzzy via role-matcher)
|
||||
node find.mjs acme --json # machine-readable output
|
||||
```
|
||||
|
||||
Multiple matches print as a table; zero matches print a clean message.
|
||||
|
||||
**Exit codes:** `0` at least one match, `1` no match, missing query, or no `applications.md`.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Setup Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An AI coding CLI — [Claude Code](https://claude.ai/code), Gemini CLI, Codex, Qwen Code, OpenCode, GitHub Copilot CLI, Antigravity CLI, or Grok Build CLI (see [Supported CLIs](SUPPORTED_CLIS.md))
|
||||
- [Node.js](https://nodejs.org) 18+ and `git` (`npx` ships with Node — the installer refuses to run without them) — note: the Gemini CLI integration requires Node.js 20+
|
||||
- (Optional) Go 1.21+ (for the dashboard TUI)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Recommended — one command
|
||||
|
||||
```bash
|
||||
npx @santifer/career-ops init
|
||||
```
|
||||
|
||||
`npx` ships with Node.js — it runs the installer once without installing anything globally. This clones the latest release into `./career-ops` and installs dependencies. Then move into the workspace and open your AI CLI:
|
||||
|
||||
```bash
|
||||
cd career-ops
|
||||
claude # or gemini / codex / qwen / opencode / agy / grok
|
||||
```
|
||||
|
||||
**On first launch, career-ops walks you through setup by chatting** — it asks for your CV, your details (name, target roles, salary), and sets up the job scanner with pre-configured companies. Nothing to edit by hand: just answer its questions. Then paste a job offer URL or description and it evaluates it, writes a report, generates a tailored PDF, and tracks it.
|
||||
|
||||
If you are using Codex, start the interactive session with `codex`. Slash commands are not guaranteed in Codex, so use the same mode names in a prompt if `/career-ops` is unavailable:
|
||||
|
||||
```text
|
||||
Evaluate this JD with career-ops auto-pipeline: https://company.com/jobs/123
|
||||
Run the career-ops scan mode.
|
||||
Run the career-ops pipeline mode.
|
||||
Run the career-ops pdf mode.
|
||||
Run the career-ops email mode for the latest evaluated role. Draft only; never sends, submits, or clicks.
|
||||
Run the career-ops tracker mode.
|
||||
```
|
||||
|
||||
For one-shot workers or batch tasks in Codex, use `codex exec`. See [docs/CODEX.md](CODEX.md) for the full guide.
|
||||
|
||||
```bash
|
||||
codex exec "Evaluate this JD with career-ops auto-pipeline: https://company.com/jobs/123"
|
||||
codex exec "Run career-ops scan mode in this repo."
|
||||
codex exec "Run career-ops pipeline mode for data/pipeline.md."
|
||||
codex exec "Run career-ops pdf mode for the latest evaluated role."
|
||||
codex exec "Run career-ops email mode for the latest evaluated role. Draft only; do not send, submit, or click anything."
|
||||
codex exec "Run career-ops tracker mode and summarize the current statuses."
|
||||
```
|
||||
|
||||
### Advanced — clone manually
|
||||
|
||||
<details>
|
||||
<summary>Prefer to clone the repo yourself?</summary>
|
||||
|
||||
```bash
|
||||
git clone https://github.com/santifer/career-ops.git
|
||||
cd career-ops
|
||||
npm install
|
||||
```
|
||||
|
||||
Then open your AI CLI in the folder — the same first-run onboarding applies. Use this path if you want to track a specific branch, contribute, or audit the code before installing dependencies.
|
||||
|
||||
</details>
|
||||
|
||||
### PDF rendering (one-time)
|
||||
|
||||
PDFs are rendered with a headless Chromium. Install it once per machine:
|
||||
|
||||
```bash
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
## Available Commands
|
||||
|
||||
| Action | How |
|
||||
|--------|-----|
|
||||
| Evaluate an offer | Paste a URL or JD text |
|
||||
| Search for offers | `/career-ops scan` or ask the agent to run `scan` |
|
||||
| Process pending URLs | `/career-ops pipeline` or ask the agent to run `pipeline` |
|
||||
| Generate a PDF | `/career-ops pdf` or ask the agent to run `pdf` |
|
||||
| Draft application email | `/career-ops email` or ask the agent to run `email`; draft-only, never sends, submits, or clicks |
|
||||
| Batch evaluate | `/career-ops batch` or use `codex exec "Run career-ops batch mode ..."` |
|
||||
| Check tracker status | `/career-ops tracker` or ask the agent to run `tracker` |
|
||||
| Fill application form | `/career-ops apply` or ask the agent to run `apply` |
|
||||
|
||||
## Verify Setup
|
||||
|
||||
```bash
|
||||
node cv-sync-check.mjs # Check configuration
|
||||
node verify-pipeline.mjs # Check pipeline integrity
|
||||
```
|
||||
|
||||
## Build Dashboard (Optional)
|
||||
|
||||
```bash
|
||||
npm run serve:dashboard # Opens TUI pipeline viewer
|
||||
npm run build:dashboard # Optional: build the standalone binary
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
# Supported CLIs
|
||||
|
||||
Career-ops is AI-agnostic and runs on several command-line agent tools. The core logic is shared via `AGENTS.md`, while CLI-specific nuances are handled through entry wrappers in the repository root.
|
||||
|
||||
| CLI | Entry File | How to Invoke |
|
||||
| --- | --- | --- |
|
||||
| Claude Code | `CLAUDE.md` | Interactive: `claude` (then `/career-ops`). Headless/Batch: `claude -p "prompt"` |
|
||||
| OpenCode | `OPENCODE.md` | Interactive: `opencode` (then `/career-ops`). Headless/Batch: `opencode run "prompt"` |
|
||||
| Antigravity CLI | `AGENTS.md` | Interactive: `agy` (then `/career-ops`). Headless/Batch: `agy -p "prompt"` |
|
||||
| Codex | `CODEX.md` (see [`docs/CODEX.md`](CODEX.md)) | Interactive: `codex` (then use plain text). Headless/Batch: `codex exec "prompt"` |
|
||||
| Grok Build CLI | `AGENTS.md` | Interactive: `grok` (then `/career-ops`). Headless/Batch: `grok -p "prompt"` |
|
||||
| Qwen | `AGENTS.md` | Interactive: `qwen`. Headless/Batch: `qwen -p "prompt"` |
|
||||
| Kimi | `KIMI.md` | Interactive: `kimi` |
|
||||
| GitHub Copilot CLI | `AGENTS.md` | Headless/Batch: `copilot -p "prompt"` |
|
||||
| Gemini | `GEMINI.md` | Legacy wrapper redirecting to `AGENTS.md` (transitioned to Antigravity CLI). |
|
||||
@@ -0,0 +1,46 @@
|
||||
# Supported Job Boards
|
||||
|
||||
Career-Ops scans job sources through provider modules in `providers/`. Each
|
||||
non-helper `*.mjs` file maps to one supported source. Files prefixed with `_`
|
||||
are shared helpers and are not loaded as providers.
|
||||
|
||||
| Board | Type (API / RSS / parser) | Notes |
|
||||
| --- | --- | --- |
|
||||
| 4 Day Week | API | Reads the public `https://4dayweek.io/api/jobs` JSON feed (4-day-week / reduced-hours roles). Configure with `provider: 4dayweek`; paginates `?page=N` up to `max_pages` (default 3), drops expired postings, then scanner filters apply. |
|
||||
| Amazon / AWS | API | Auto-detects `amazon.jobs` careers URLs and queries the public amazon.jobs search API. The board is one global endpoint, so narrow it with an `amazon:` config block (`loc_query`, `base_query`, `category`, …) whose keys pass through as query params. Configure with `provider: amazon`. |
|
||||
| Arbeitnow | API | Reads the public `https://www.arbeitnow.com/api/job-board-api` JSON feed (EU/DACH-heavy, newest-first). Configure with `provider: arbeitnow`; paginates `?page=N` up to `max_pages` (default 3), then scanner filters apply. |
|
||||
| Arbeitsagentur | API | Uses the public Bundesagentur fuer Arbeit Jobsuche REST API. Configure with `provider: arbeitsagentur`; title, location, and dedup filters run after fetch. |
|
||||
| Ashby | API | Auto-detects `https://jobs.ashbyhq.com/<slug>` boards and uses Ashby's public posting API. |
|
||||
| Avature | Parser | Auto-detects `<tenant>.avature.net` career sites and parses the public server-rendered job list (`/careers/SearchJobs?jobOffset=N`, 6 results/page). A branded custom domain that proxies Avature needs `provider: avature` + `api:` pointing at the Avature origin. Paginates up to `max_pages` (default 50). |
|
||||
| BambooHR | API | Auto-detects `<tenant>.bamboohr.com` careers pages, reads `/careers/list`, and follows public detail endpoints for job URLs. |
|
||||
| Breezy HR | API | Auto-detects `<tenant>.breezy.hr` boards and reads the public JSON position feed. |
|
||||
| Comeet / Spark Hire Recruit | API | Uses Comeet's public careers API. Provide the full API URL with `api:` or `careers_url`; it cannot derive the endpoint from a branded careers page. |
|
||||
| Get on Board | API | Reads the public `https://www.getonbrd.com/api/v0/categories/programming/jobs` JSON:API feed (remote/LatAm-heavy tech roles). Configure with `provider: getonbrd`; paginates `?page=N` up to `max_pages` (default 3) over the programming category (`expand[]=company`), then scanner filters apply. |
|
||||
| Glints | API | Uses Glints' public GraphQL job search endpoint. Configure with `provider: glints`; query and filters can be set on the portal entry. |
|
||||
| Greenhouse | API | Handles explicit `api:` URLs and auto-detects public Greenhouse board URLs for the boards API. |
|
||||
| HigherEdJobs | RSS | Reads the public `https://www.higheredjobs.com/rss/categoryFeed.cfm?catID={catID}` feed and parses it in-process. Configure with `provider: higheredjobs` and optional `cat_id` (default 68 = Higher Education). Not auto-detected — requires explicit `provider:` config. |
|
||||
| IBM Careers | API | Posts to IBM's public careers search API and supports optional IBM facet filters in the portal entry. |
|
||||
| JibeApply | API | Auto-detects `https://<slug>.jibeapply.com/jobs` careers URLs (rewriting `/jobs` to the public `/api/jobs` endpoint); paginates `?page=N` up to `max_pages` (default 50), warning if a tenant's postings exceed the cap. Also supports branded/iCIMS-hosted sites at their own `/jobs` path via an explicit `provider: jibeapply` + `api:` URL. |
|
||||
| Jobstreet / SEEK | API | Uses the public SEEK chalice-search JSON API for Jobstreet and SEEK sites. Configure explicitly with `provider: jobstreet`. |
|
||||
| Landing.jobs | API | Reads the board-wide `https://landing.jobs/api/v1/jobs` JSON feed (tech, Europe). Configure with `provider: landingjobs`; company is derived from the posting URL slug. |
|
||||
| Lever | API | Auto-detects `https://jobs.lever.co/<slug>` boards and uses Lever's public postings endpoint. |
|
||||
| Local parser | Parser | Runs an in-repo parser command from `portals.yml`. Use this for stable SSR or HTML pages that need a custom extractor. |
|
||||
| NoDesk | RSS | Reads the public `https://nodesk.co/remote-jobs/index.xml` feed and parses it in-process. Configure with `provider: nodesk`. |
|
||||
| Personio | RSS | Auto-detects `<slug>.jobs.personio.de` or `.com` hosts and parses the public XML jobs feed. |
|
||||
| Pinpoint | API | Auto-detects `<slug>.pinpointhq.com` boards and reads the public zero-auth `/postings.json` per-tenant feed. |
|
||||
| Recruitee | API | Auto-detects `<slug>.recruitee.com` boards and uses the public per-tenant offers API. |
|
||||
| RemoteOK | API | Reads the board-wide `https://remoteok.com/api` JSON feed; scanner filters decide which rows are relevant. |
|
||||
| Remotive | API | Reads the board-wide `https://remotive.com/api/remote-jobs` JSON feed, then applies local scanner filters. |
|
||||
| Rippling | API | Auto-detects `https://ats.rippling.com/<slug>/jobs` careers pages and reads the public zero-auth board API (`api.rippling.com/platform/api/ats/v1/board/<slug>/jobs`). |
|
||||
| SAP SuccessFactors | Parser | Reads SF Recruiting Marketing (RMK) career sites — branded boards like `jobs.sap.com`, `jobs.zf.com`, `jobs.schaeffler.com` — via the public no-auth `/tile-search-results/?startrow=N` HTML fragment. Branded hosts carry no "successfactors" string, so select with `provider: successfactors` + `api:` the board origin. |
|
||||
| SmartRecruiters | API | Auto-detects SmartRecruiters careers URLs or uses `provider: smartrecruiters` for branded custom domains. |
|
||||
| SolidJobs | API | Auto-detects `https://solid.jobs/public-api/offers/<division>` and reads the public offers API. |
|
||||
| Teamtailor | RSS | Auto-detects `<slug>.teamtailor.com` career sites and reads the public zero-auth `/jobs.rss` per-tenant feed. For a branded careers domain, set `provider: teamtailor` and it reads `/jobs.rss` off that host. Job links may point at a branded custom domain; location comes from the `tt:` city/country tags, falling back to `Remote` when a posting carries no `tt:city`/`tt:country` but its `remoteStatus` is remote (`fully` or `temporary`). |
|
||||
| The Hub | API | Reads the board-wide `https://thehub.io/api/jobs` JSON feed (Nordic/EU startups). Configure with `provider: thehub`; paginates `?page=N` up to `max_pages` (default 3), then scanner filters apply. |
|
||||
| We Work Remotely | RSS | Reads the public `https://weworkremotely.com/remote-jobs.rss` feed and parses it in-process. |
|
||||
| Workable | Parser | Auto-detects `https://apply.workable.com/<slug>` and parses Workable's public markdown jobs feed. |
|
||||
| Workday | API | Auto-detects `<tenant>.<instance>.myworkdayjobs.com[/<locale>]/<site>` careers URLs and posts to the public CXS jobs endpoint; paginates via offset up to `max_pages` (default 100), warning if a tenant's postings exceed the cap. |
|
||||
| Working Nomads | API | Reads the board-wide `https://www.workingnomads.com/api/exposed_jobs/` JSON feed, then applies scanner filters. |
|
||||
|
||||
When adding a new provider, add a new non-helper module under `providers/` and
|
||||
update this table in the same PR.
|
||||
|
After Width: | Height: | Size: 8.3 MiB |
|
After Width: | Height: | Size: 671 KiB |
@@ -0,0 +1,72 @@
|
||||
# Local Parser Cookbook
|
||||
|
||||
Local parsers let `scan.mjs` read SSR or static career pages without asking an agent to browse the page. The parser runs as a local command, prints normalized jobs JSON to stdout, and lets the scanner keep using the same title filtering, deduplication, and pipeline output flow.
|
||||
|
||||
## When To Use This
|
||||
|
||||
Use `scan_method: local_parser` when a company career page has stable HTML, a documented endpoint, or another deterministic source that is easier to parse locally than with Playwright. The parser can be written in JavaScript, Python, shell, Go, or any executable available on the user's machine. `career-ops` does not bundle company-specific parser scripts; users bring their own script and point `portals.yml` at it.
|
||||
|
||||
## Portal Configuration
|
||||
|
||||
Most local parsers are company-specific: the script already knows the source URL, selectors, endpoint quirks, pagination, and normalization rules. In that common case, the scanner only needs to know which command to run:
|
||||
|
||||
```yaml
|
||||
- name: Example Company
|
||||
careers_url: https://example.com/careers
|
||||
scan_method: local_parser
|
||||
parser:
|
||||
command: node
|
||||
script: scripts/parsers/example-company-jobs.js
|
||||
format: jobs-json-v1
|
||||
enabled: true
|
||||
```
|
||||
|
||||
`args` are optional. Use them in whatever way helps the parser author: to make one script reusable across multiple companies, pass `{careers_url}` or `{company}`, enable a debug flag, store a JSON snapshot, or control any other script-specific behavior. `scan.mjs` executes the parser without shell interpolation and expands `{careers_url}` and `{company}` in parser arguments before execution.
|
||||
|
||||
## Token savings
|
||||
|
||||
`scan.mjs` uses **0 LLM tokens** for discovery: parsers run locally and only normalized job rows enter the pipeline.
|
||||
|
||||
In agent scan mode (`/career-ops scan`), Playwright and API niveles send large page or JSON payloads into the model. When Nivel 0 succeeds, `modes/scan.md` requires skipping those niveles for the same company (`local_parser_ok`).
|
||||
|
||||
Measured benchmarks (Cohere + Mobileye fixtures, `tiktoken` `cl100k_base`, Playwright vs parser vs API) live on branch `feature/local-parser-integration-tests` with `npm run test:scan-tokens` and full tables in that branch's copy of this cookbook.
|
||||
|
||||
## Stdout Contract
|
||||
|
||||
The parser must print one of these JSON shapes to stdout:
|
||||
|
||||
```json
|
||||
[
|
||||
{ "title": "Senior AI Engineer", "url": "https://example.com/jobs/123", "location": "Remote" }
|
||||
]
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"jobs": [
|
||||
{ "title": "Senior AI Engineer", "url": "https://example.com/jobs/123", "location": "Remote" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{ "title": "Senior AI Engineer", "url": "https://example.com/jobs/123", "location": "Remote" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`title` and `url` are required. `company` is optional; when omitted, the scanner uses the `tracked_companies` entry name. Relative URLs are resolved against `careers_url`.
|
||||
|
||||
## Artifact Storage
|
||||
|
||||
The scanner only needs stdout. If a parser also writes full JSON snapshots for debugging or audit, store them under `data/parser-output/{company}/`. Generated JSON artifacts must stay out of git; `.gitkeep` placeholders are the only committed exception for preserving directory structure.
|
||||
|
||||
## Failure Handling
|
||||
|
||||
Local parsers run before ATS API detection. If a local parser fails and the company has a detectable Greenhouse, Ashby, or Lever API source, `scan.mjs` records the parser failure and falls back to the API path for that company instead of dropping it from the scan.
|
||||
|
||||
## Agent scan (`/career-ops scan`)
|
||||
|
||||
`scan.mjs` already uses one provider per company (local parser only, no duplicate API pass). In full agent scan mode (`modes/scan.md`), when Nivel 0 succeeds for a company, the agent must **skip** Playwright (Nivel 1) and API (Nivel 2) for that company, and filter Nivel 3 WebSearch hits that match the same company. General portal queries (`site:jobs.ashbyhq.com`, role keywords) still run for discovery of other employers.
|
||||
|
After Width: | Height: | Size: 5.7 MiB |
@@ -0,0 +1,9 @@
|
||||
<svg class="svg-icon logo-BI-icon logo-vertically-center-icon" aria-labelledby="title desc" role="img" xmlns="http://www.w3.org/2000/svg" width="103" height="32" viewBox="0 0 103 32">
|
||||
|
||||
<title id="title">Business Insider logo</title>
|
||||
<desc id="desc">The words "Business Insider".</desc>
|
||||
|
||||
<g class="logo-BI logo-vertically-center-icon-fill" fill="#fff" fill-rule="evenodd">
|
||||
<path d="M7.984 32h2.396V19.2H7.984zM16.767 31.808h2.146v-8.076L27.946 32V19.392h-2.14v7.938l-9.039-8.13zM32.845 28.708c1.168.91 2.57 1.436 4.073 1.436 1.464 0 2.491-.504 2.491-1.617 0-.97-.655-1.35-1.802-1.775l-1.857-.645c-1.712-.608-3.014-1.496-3.014-3.537 0-2.183 1.971-3.37 4.328-3.37 1.444 0 2.76.363 3.788 1.006v2.165c-1.069-.79-2.239-1.334-3.742-1.334-1.294 0-2.261.483-2.261 1.49 0 .91.607 1.274 1.676 1.64l2.069.726c2.005.705 2.925 1.798 2.925 3.596 0 2.262-1.99 3.511-4.601 3.511-1.716 0-3.175-.48-4.073-1.086v-2.206zM47.109 32h2.395V19.2h-2.395zM60.74 30.063c2.7 0 4.143-1.732 4.143-4.466 0-2.754-1.444-4.48-4.143-4.48h-2.67v8.946h2.67zM55.89 19.2h4.93c3.795 0 6.249 2.5 6.249 6.397 0 3.9-2.454 6.403-6.249 6.403h-4.93V19.2zM79.047 19.2v1.917H73.25v3.08h5.199v1.895h-5.2v3.971h5.798V32h-7.985V19.2zM87.549 24.996h2.377c1.407 0 2.2-.772 2.2-1.959s-.793-1.96-2.2-1.96h-2.377v3.92zm0 1.81V32h-2.115V19.2h4.532c2.584 0 4.256 1.331 4.256 3.837 0 2.143-.928 3.374-2.905 3.69L95.814 32h-2.406l-4.354-5.193H87.55zM7.147 6.59c.937-.343 1.667-1.372 1.667-2.787 0-2.358-1.667-3.58-4.334-3.58H0v13.163h4.896c2.667 0 4.334-1.308 4.334-3.773 0-1.394-.875-2.616-2.083-3.023zm-5.043-.836V2.153H4.46c1.48 0 2.271.536 2.271 1.779 0 1.243-.792 1.822-2.271 1.822H2.104zm0 1.844h2.75c1.46 0 2.251.728 2.251 1.929 0 1.179-.792 1.93-2.25 1.93h-2.75v-3.86zM19.005 13.6c3.063 0 5.126-1.844 5.126-5.445V.223h-2.167v7.674c0 2.53-1.042 3.71-2.959 3.71s-2.958-1.18-2.958-3.71V.223H13.88v7.91c0 3.623 2.083 5.467 5.125 5.467zm10.228-1.158c.896.644 2.355 1.158 4.063 1.158 2.605 0 4.584-1.33 4.584-3.73 0-1.908-.916-3.066-2.917-3.816l-2.062-.772c-1.063-.385-1.667-.771-1.667-1.736 0-1.072.958-1.586 2.25-1.586 1.5 0 2.667.578 3.73 1.415V1.08C36.193.395 34.88.009 33.442.009c-2.354 0-4.313 1.265-4.313 3.58 0 2.165 1.292 3.108 3 3.751l1.855.686c1.146.45 1.792.858 1.792 1.887 0 1.179-1.021 1.715-2.48 1.715-1.5 0-2.896-.558-4.063-1.522v2.336zM45.623.223h-2.166v13.162h2.166V.223zm6.182 13.146h2.088V4.806l8.788 8.767V.203h-2.082V8.62L51.805 0v13.369zM76.222.226v1.971h-5.626v3.168h5.045v1.949h-5.045v4.083h5.626v1.992h-7.75V.226h7.75zm5.045 12.207c.896.644 2.354 1.158 4.063 1.158 2.604 0 4.584-1.329 4.584-3.73 0-1.908-.917-3.065-2.917-3.816l-2.063-.771c-1.063-.386-1.667-.772-1.667-1.737 0-1.072.958-1.586 2.25-1.586 1.5 0 2.667.579 3.73 1.415V1.072C88.226.386 86.913 0 85.475 0c-2.354 0-4.313 1.265-4.313 3.58 0 2.165 1.292 3.108 3 3.751l1.855.686c1.146.45 1.792.858 1.792 1.887 0 1.179-1.02 1.715-2.48 1.715-1.5 0-2.896-.557-4.062-1.522v2.336zm13.086 0c.896.644 2.355 1.158 4.063 1.158 2.605 0 4.584-1.329 4.584-3.73 0-1.908-.917-3.065-2.917-3.816l-2.063-.771c-1.062-.386-1.667-.772-1.667-1.737 0-1.072.959-1.586 2.25-1.586 1.5 0 2.668.579 3.73 1.415V1.072C101.313.386 100 0 98.562 0c-2.355 0-4.313 1.265-4.313 3.58 0 2.165 1.292 3.108 3 3.751l1.855.686c1.146.45 1.792.858 1.792 1.887 0 1.179-1.021 1.715-2.48 1.715-1.5 0-2.896-.557-4.063-1.522v2.336z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg class="svg-icon logo-BI-icon logo-vertically-center-icon" aria-labelledby="title desc" role="img" xmlns="http://www.w3.org/2000/svg" width="103" height="32" viewBox="0 0 103 32">
|
||||
|
||||
<title id="title">Business Insider logo</title>
|
||||
<desc id="desc">The words "Business Insider".</desc>
|
||||
|
||||
<g class="logo-BI logo-vertically-center-icon-fill" fill="#111" fill-rule="evenodd">
|
||||
<path d="M7.984 32h2.396V19.2H7.984zM16.767 31.808h2.146v-8.076L27.946 32V19.392h-2.14v7.938l-9.039-8.13zM32.845 28.708c1.168.91 2.57 1.436 4.073 1.436 1.464 0 2.491-.504 2.491-1.617 0-.97-.655-1.35-1.802-1.775l-1.857-.645c-1.712-.608-3.014-1.496-3.014-3.537 0-2.183 1.971-3.37 4.328-3.37 1.444 0 2.76.363 3.788 1.006v2.165c-1.069-.79-2.239-1.334-3.742-1.334-1.294 0-2.261.483-2.261 1.49 0 .91.607 1.274 1.676 1.64l2.069.726c2.005.705 2.925 1.798 2.925 3.596 0 2.262-1.99 3.511-4.601 3.511-1.716 0-3.175-.48-4.073-1.086v-2.206zM47.109 32h2.395V19.2h-2.395zM60.74 30.063c2.7 0 4.143-1.732 4.143-4.466 0-2.754-1.444-4.48-4.143-4.48h-2.67v8.946h2.67zM55.89 19.2h4.93c3.795 0 6.249 2.5 6.249 6.397 0 3.9-2.454 6.403-6.249 6.403h-4.93V19.2zM79.047 19.2v1.917H73.25v3.08h5.199v1.895h-5.2v3.971h5.798V32h-7.985V19.2zM87.549 24.996h2.377c1.407 0 2.2-.772 2.2-1.959s-.793-1.96-2.2-1.96h-2.377v3.92zm0 1.81V32h-2.115V19.2h4.532c2.584 0 4.256 1.331 4.256 3.837 0 2.143-.928 3.374-2.905 3.69L95.814 32h-2.406l-4.354-5.193H87.55zM7.147 6.59c.937-.343 1.667-1.372 1.667-2.787 0-2.358-1.667-3.58-4.334-3.58H0v13.163h4.896c2.667 0 4.334-1.308 4.334-3.773 0-1.394-.875-2.616-2.083-3.023zm-5.043-.836V2.153H4.46c1.48 0 2.271.536 2.271 1.779 0 1.243-.792 1.822-2.271 1.822H2.104zm0 1.844h2.75c1.46 0 2.251.728 2.251 1.929 0 1.179-.792 1.93-2.25 1.93h-2.75v-3.86zM19.005 13.6c3.063 0 5.126-1.844 5.126-5.445V.223h-2.167v7.674c0 2.53-1.042 3.71-2.959 3.71s-2.958-1.18-2.958-3.71V.223H13.88v7.91c0 3.623 2.083 5.467 5.125 5.467zm10.228-1.158c.896.644 2.355 1.158 4.063 1.158 2.605 0 4.584-1.33 4.584-3.73 0-1.908-.916-3.066-2.917-3.816l-2.062-.772c-1.063-.385-1.667-.771-1.667-1.736 0-1.072.958-1.586 2.25-1.586 1.5 0 2.667.578 3.73 1.415V1.08C36.193.395 34.88.009 33.442.009c-2.354 0-4.313 1.265-4.313 3.58 0 2.165 1.292 3.108 3 3.751l1.855.686c1.146.45 1.792.858 1.792 1.887 0 1.179-1.021 1.715-2.48 1.715-1.5 0-2.896-.558-4.063-1.522v2.336zM45.623.223h-2.166v13.162h2.166V.223zm6.182 13.146h2.088V4.806l8.788 8.767V.203h-2.082V8.62L51.805 0v13.369zM76.222.226v1.971h-5.626v3.168h5.045v1.949h-5.045v4.083h5.626v1.992h-7.75V.226h7.75zm5.045 12.207c.896.644 2.354 1.158 4.063 1.158 2.604 0 4.584-1.329 4.584-3.73 0-1.908-.917-3.065-2.917-3.816l-2.063-.771c-1.063-.386-1.667-.772-1.667-1.737 0-1.072.958-1.586 2.25-1.586 1.5 0 2.667.579 3.73 1.415V1.072C88.226.386 86.913 0 85.475 0c-2.354 0-4.313 1.265-4.313 3.58 0 2.165 1.292 3.108 3 3.751l1.855.686c1.146.45 1.792.858 1.792 1.887 0 1.179-1.02 1.715-2.48 1.715-1.5 0-2.896-.557-4.062-1.522v2.336zm13.086 0c.896.644 2.355 1.158 4.063 1.158 2.605 0 4.584-1.329 4.584-3.73 0-1.908-.917-3.065-2.917-3.816l-2.063-.771c-1.062-.386-1.667-.772-1.667-1.737 0-1.072.959-1.586 2.25-1.586 1.5 0 2.668.579 3.73 1.415V1.072C101.313.386 100 0 98.562 0c-2.355 0-4.313 1.265-4.313 3.58 0 2.165 1.292 3.108 3 3.751l1.855.686c1.146.45 1.792.858 1.792 1.887 0 1.179-1.021 1.715-2.48 1.715-1.5 0-2.896-.557-4.063-1.522v2.336z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="206" height="54" viewBox="0 0 206 54" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(-130.000000, -73.000000)">
|
||||
<g transform="translate(130.000000, 73.000000)">
|
||||
<rect stroke="#FF6154" stroke-width="1" fill="#FFFFFF" x="0.5" y="0.5" width="205" height="53" rx="10"></rect>
|
||||
<text font-family="Helvetica-Bold, Helvetica" font-size="9" font-weight="bold" fill="#FF6154">
|
||||
<tspan x="58" y="20">FEATURED ON</tspan>
|
||||
</text>
|
||||
<text font-family="Helvetica-Bold, Helvetica" font-size="21" font-weight="bold" fill="#FF6154">
|
||||
<tspan x="57" y="40">Product Hunt</tspan>
|
||||
</text>
|
||||
<g transform="translate(16.000000, 12.000000)"><path d="M31,15.5 C31,24.0603917 24.0603917,31 15.5,31 C6.93960833,31 0,24.0603917 0,15.5 C0,6.93960833 6.93960833,0 15.5,0 C24.0603917,0 31,6.93960833 31,15.5" fill="#FF6154"></path><path d="M17.4329412,15.9558824 L17.4329412,15.9560115 L13.0929412,15.9560115 L13.0929412,11.3060115 L17.4329412,11.3060115 L17.4329412,11.3058824 C18.7018806,11.3058824 19.7305882,12.3468365 19.7305882,13.6308824 C19.7305882,14.9149282 18.7018806,15.9558824 17.4329412,15.9558824 M17.4329412,8.20588235 L17.4329412,8.20601152 L10.0294118,8.20588235 L10.0294118,23.7058824 L13.0929412,23.7058824 L13.0929412,19.0560115 L17.4329412,19.0560115 L17.4329412,19.0558824 C20.3938424,19.0558824 22.7941176,16.6270324 22.7941176,13.6308824 C22.7941176,10.6347324 20.3938424,8.20588235 17.4329412,8.20588235" fill="#FFFFFF"></path></g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0.11 10.99 124.78 24.98"><path d="M105.375 14.875v17.25h8.5c2.375 0 3.75-.375 4.75-1.25 1.25-1.125 1.875-3.125 1.875-7.375s-.625-6.25-1.875-7.375c-1-.875-2.375-1.25-4.75-1.25zM117 23.5c0 3.75-.25 4.625-1 5.125-.5.375-1.125.5-2.375.5h-4.75V17.75h4.75c1.25 0 1.875 0 2.375.5.75.625 1 1.5 1 5.25zm7.875 12.438H99.937V11h24.938zM79.563 17.75v-2.875h14.75v5.5h-3.126V17.75h-6v4.125h4.75v2.75h-4.75v4.625h6.126v-3h3.124v5.875H79.564V29.25h2.374v-11.5zM66.188 27.625c0 1.875.124 3.25.374 4.375h3.376c-.126-.875-.25-2.5-.25-4.625-.126-2.5-.876-2.875-2.626-3.25 2-.375 2.876-1.25 2.876-4.375 0-2.5-.376-3.5-1.126-4.125-.5-.5-1.374-.75-2.75-.75h-10.5v17.25h3.5v-6.75h4.876c1 0 1.374.125 1.75.375s.5.625.5 1.875zm-7.126-5v-4.75h5.626c.75 0 1 .125 1.124.25.25.25.5.625.5 2.125s-.25 2-.5 2.25c-.124.125-.374.25-1.124.25zm15.876 13.313h-25V11h24.937v24.938zM43.438 29.25v2.875H31.562V29.25h4.25v-11.5h-4.25v-2.875h11.875v2.875h-4.25v11.5zM23.375 14.875h-3.25L17.75 28.5 15 15.875c-.125-.875-.5-1-1.25-1H12c-.75 0-1.125.25-1.25 1L8 28.5 5.625 14.875h-3.5L5.5 31.25c.125.75.375.875 1.25.875h2.375c.75 0 1-.125 1.25-.875L13 19.375l2.625 11.875c.125.75.375.875 1.25.875h2.25c.75 0 1.125-.125 1.25-.875zm1.75 21.063h-25V11h24.938v24.938z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0.11 10.99 124.78 24.98"><path d="M105.375 14.875v17.25h8.5c2.375 0 3.75-.375 4.75-1.25 1.25-1.125 1.875-3.125 1.875-7.375s-.625-6.25-1.875-7.375c-1-.875-2.375-1.25-4.75-1.25zM117 23.5c0 3.75-.25 4.625-1 5.125-.5.375-1.125.5-2.375.5h-4.75V17.75h4.75c1.25 0 1.875 0 2.375.5.75.625 1 1.5 1 5.25zm7.875 12.438H99.937V11h24.938zM79.563 17.75v-2.875h14.75v5.5h-3.126V17.75h-6v4.125h4.75v2.75h-4.75v4.625h6.126v-3h3.124v5.875H79.564V29.25h2.374v-11.5zM66.188 27.625c0 1.875.124 3.25.374 4.375h3.376c-.126-.875-.25-2.5-.25-4.625-.126-2.5-.876-2.875-2.626-3.25 2-.375 2.876-1.25 2.876-4.375 0-2.5-.376-3.5-1.126-4.125-.5-.5-1.374-.75-2.75-.75h-10.5v17.25h3.5v-6.75h4.876c1 0 1.374.125 1.75.375s.5.625.5 1.875zm-7.126-5v-4.75h5.626c.75 0 1 .125 1.124.25.25.25.5.625.5 2.125s-.25 2-.5 2.25c-.124.125-.374.25-1.124.25zm15.876 13.313h-25V11h24.937v24.938zM43.438 29.25v2.875H31.562V29.25h4.25v-11.5h-4.25v-2.875h11.875v2.875h-4.25v11.5zM23.375 14.875h-3.25L17.75 28.5 15 15.875c-.125-.875-.5-1-1.25-1H12c-.75 0-1.125.25-1.25 1L8 28.5 5.625 14.875h-3.5L5.5 31.25c.125.75.375.875 1.25.875h2.375c.75 0 1-.125 1.25-.875L13 19.375l2.625 11.875c.125.75.375.875 1.25.875h2.25c.75 0 1.125-.125 1.25-.875zm1.75 21.063h-25V11h24.938v24.938z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 6.6 MiB |
|
After Width: | Height: | Size: 6.7 MiB |