job-forge 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/.codex/config.toml +8 -0
  2. package/.cursor/mcp.json +21 -0
  3. package/.cursor/rules/main.mdc +519 -0
  4. package/.mcp.json +21 -0
  5. package/.opencode/agents/general-free.md +85 -0
  6. package/.opencode/agents/general-paid.md +39 -0
  7. package/.opencode/agents/glm-minimal.md +50 -0
  8. package/.opencode/skills/job-forge.md +185 -0
  9. package/AGENTS.md +514 -0
  10. package/CLAUDE.md +514 -0
  11. package/LICENSE +21 -0
  12. package/README.md +195 -0
  13. package/batch/README.md +60 -0
  14. package/batch/batch-prompt.md +399 -0
  15. package/batch/batch-runner.sh +673 -0
  16. package/bin/create-job-forge.mjs +375 -0
  17. package/bin/job-forge.mjs +120 -0
  18. package/bin/sync.mjs +141 -0
  19. package/config/profile.example.yml +67 -0
  20. package/cv-sync-check.mjs +128 -0
  21. package/dedup-tracker.mjs +201 -0
  22. package/docs/ARCHITECTURE.md +220 -0
  23. package/docs/CUSTOMIZATION.md +101 -0
  24. package/docs/MODEL-ROUTING.md +195 -0
  25. package/docs/README.md +54 -0
  26. package/docs/SETUP.md +186 -0
  27. package/docs/demo.gif +0 -0
  28. package/fonts/dm-sans-latin-ext.woff2 +0 -0
  29. package/fonts/dm-sans-latin.woff2 +0 -0
  30. package/fonts/space-grotesk-latin-ext.woff2 +0 -0
  31. package/fonts/space-grotesk-latin.woff2 +0 -0
  32. package/generate-pdf.mjs +168 -0
  33. package/iso/agents/general-free.md +90 -0
  34. package/iso/agents/general-paid.md +44 -0
  35. package/iso/agents/glm-minimal.md +55 -0
  36. package/iso/commands/job-forge.md +188 -0
  37. package/iso/config.json +7 -0
  38. package/iso/instructions.md +514 -0
  39. package/iso/mcp.json +15 -0
  40. package/merge-tracker.mjs +377 -0
  41. package/modes/README.md +30 -0
  42. package/modes/_shared-calibration.md +26 -0
  43. package/modes/_shared.md +272 -0
  44. package/modes/apply.md +257 -0
  45. package/modes/auto-pipeline.md +70 -0
  46. package/modes/batch.md +110 -0
  47. package/modes/compare.md +23 -0
  48. package/modes/contact.md +82 -0
  49. package/modes/deep.md +99 -0
  50. package/modes/followup.md +68 -0
  51. package/modes/negotiation.md +146 -0
  52. package/modes/offer.md +199 -0
  53. package/modes/pdf.md +121 -0
  54. package/modes/pipeline.md +83 -0
  55. package/modes/project.md +30 -0
  56. package/modes/rejection.md +92 -0
  57. package/modes/scan.md +185 -0
  58. package/modes/tracker.md +31 -0
  59. package/modes/training.md +27 -0
  60. package/normalize-statuses.mjs +152 -0
  61. package/opencode.json +28 -0
  62. package/package.json +78 -0
  63. package/scripts/add-tags.mjs +894 -0
  64. package/scripts/cursor-agent-loop.sh +211 -0
  65. package/scripts/cursor-agent-stream-format.py +134 -0
  66. package/scripts/next-num.mjs +33 -0
  67. package/scripts/release/check-source.mjs +37 -0
  68. package/scripts/render-report-header.mjs +78 -0
  69. package/scripts/session-report.mjs +129 -0
  70. package/scripts/slugify.mjs +27 -0
  71. package/scripts/today.mjs +20 -0
  72. package/scripts/token-usage-report.mjs +315 -0
  73. package/scripts/tracker-line.mjs +67 -0
  74. package/scripts/verify-greenhouse-urls.mjs +195 -0
  75. package/templates/cv-template.html +395 -0
  76. package/templates/portals.example.yml +3140 -0
  77. package/templates/states.yml +62 -0
  78. package/tracker-lib.mjs +257 -0
  79. package/verify-pipeline.mjs +267 -0
package/modes/scan.md ADDED
@@ -0,0 +1,185 @@
1
+ # Mode: scan — Portal Scanner (Offer Discovery)
2
+
3
+ Scans configured job portals, filters by title relevance, and adds new offers to the pipeline for later evaluation.
4
+
5
+ ## Recommended Execution
6
+
7
+ Run as a subagent to avoid consuming main context:
8
+
9
+ ```
10
+ Agent(
11
+ subagent_type="general-purpose",
12
+ prompt="[contents of this file + specific data]",
13
+ run_in_background=True
14
+ )
15
+ ```
16
+
17
+ ## Read This Configuration
18
+
19
+ Read `portals.yml` which contains:
20
+ - `search_queries`: List of WebSearch queries with `site:` filters per portal (broad discovery)
21
+ - `tracked_companies`: Specific companies with `careers_url` for direct navigation
22
+ - `title_filter`: Positive/negative/seniority_boost keywords for title filtering
23
+
24
+ ## Apply This Discovery Strategy (3 levels)
25
+
26
+ ### Use Level 1 — Direct Geometra (PRIMARY)
27
+
28
+ **For each company in `tracked_companies`:** Connect to its `careers_url` with Geometra MCP (`geometra_connect` + `geometra_page_model` / `geometra_list_items`), read ALL visible job listings, and extract the title + URL of each one. Direct Geometra is the most reliable method because:
29
+
30
+ - It sees the page in real time (not cached Google results).
31
+ - It works with SPAs (Ashby, Lever, Workday).
32
+ - It detects new offers instantly.
33
+ - It doesn't depend on Google indexing.
34
+
35
+ **Every company MUST have a `careers_url` in portals.yml.** If it doesn't, search for it once, save it, and use it in future scans.
36
+
37
+ ### Use Level 2 — Greenhouse API (COMPLEMENTARY)
38
+
39
+ For companies using Greenhouse, the JSON API (`boards-api.greenhouse.io/v1/boards/{slug}/jobs`) returns clean structured data. Use as a quick complement to Level 1 — it's faster than Geometra but only works with Greenhouse.
40
+
41
+ ### Use Level 3 — WebSearch Queries (BROAD DISCOVERY)
42
+
43
+ The `search_queries` with `site:` filters cover portals broadly (all Ashby boards, all Greenhouse boards, all Lever boards, all Workday boards). Useful for discovering NEW companies not yet in `tracked_companies`, but results may be outdated.
44
+
45
+ **Execution priority:**
46
+ 1. Level 1: Geometra → all `tracked_companies` with `careers_url`
47
+ 2. Level 2: API → all `tracked_companies` with `api:`
48
+ 3. Level 3: WebSearch → all `search_queries` with `enabled: true`
49
+
50
+ The levels are additive — all are executed, results are merged and deduplicated.
51
+
52
+ ## Run This Workflow
53
+
54
+ 1. **Read configuration**: `portals.yml`
55
+ 2. **Read history**: `data/scan-history.tsv` → previously seen URLs
56
+ 3. **Read dedup sources**: all day files in `data/applications/` + `data/pipeline.md`
57
+
58
+ 4. **Level 1 — Geometra scan** (sequential, or ≤2 parallel via `task` subagents per Hard Limit #1 in `AGENTS.md`):
59
+ For each company in `tracked_companies` with `enabled: true` and `careers_url` defined:
60
+ a. `geometra_connect` to the `careers_url`
61
+ b. `geometra_page_model` or `geometra_list_items` to read all job listings
62
+ c. If the page has filters/departments, navigate the relevant sections
63
+ d. For each job listing extract: `{title, url, company}`
64
+ e. If the page paginates results, navigate additional pages
65
+ f. Accumulate in candidates list
66
+ g. If `careers_url` fails (404, redirect), try `scan_query` as fallback and note for URL update
67
+
68
+ 5. **Level 2 — Greenhouse APIs** (WebFetch can batch freely — it's cheap and doesn't use Geometra sessions):
69
+ For each company in `tracked_companies` with `api:` defined and `enabled: true`:
70
+ a. WebFetch the API URL → JSON with job list
71
+ b. For each job extract: `{title, url, company}`
72
+ c. Accumulate in candidates list (dedup with Level 1)
73
+
74
+ 6. **Level 3 — WebSearch queries** (WebSearch is parallel-safe; batch freely):
75
+ For each query in `search_queries` with `enabled: true`:
76
+ a. Execute WebSearch with the defined `query`
77
+ b. From each result extract: `{title, url, company}`
78
+ - **title**: from the result title (before " @ " or " | ")
79
+ - **url**: result URL
80
+ - **company**: after " @ " in the title, or extract from domain/path
81
+ c. Accumulate in candidates list (dedup with Level 1+2)
82
+
83
+ 6. **Filter by title** using `title_filter` from `portals.yml`:
84
+ - At least 1 keyword from `positive` must appear in the title (case-insensitive)
85
+ - 0 keywords from `negative` must appear
86
+ - `seniority_boost` keywords give priority but are not required
87
+
88
+ 7. **Deduplicate** against 3 sources (URL-exact + fuzzy company+role):
89
+
90
+ **Layer 1 — URL-exact:**
91
+ - `scan-history.tsv` → exact URL already seen
92
+ - `pipeline.md` → exact URL already in pending or processed
93
+
94
+ **Layer 2 — Company + role fuzzy match (catches reposts with new URLs):**
95
+ - all day files in `data/applications/` → normalize company name (lowercase, strip non-alphanumeric) + fuzzy role match (2+ significant words in common, words > 3 chars). This is the same logic used in `dedup-tracker.mjs` and `merge-tracker.mjs`.
96
+ - `scan-history.tsv` → same fuzzy match against company + title columns (not just URL). A role reposted on a new URL but with the same company and similar title is a duplicate.
97
+ - `pipeline.md` → same fuzzy match against company + title in pending items that include metadata (format: `- [ ] {url} | {company} | {title}`)
98
+
99
+ **Fuzzy match rules:**
100
+ - Normalize company: `company.toLowerCase().replace(/[^a-z0-9]/g, '')`
101
+ - Fuzzy role match: split both titles into words > 3 chars, match if 2+ words overlap (substring match, case-insensitive). E.g., "Senior AI Engineer" and "Staff AI Engineer" share "engineer" — only 1 overlap, not a match. But "AI Platform Engineer" and "AI Platform Eng" share "platform" + partial "engineer" — match.
102
+ - When a fuzzy match is found but the URL is new, log it as `skipped_repost` (not `skipped_dup`) with a note referencing the original entry number.
103
+
104
+ 8. **For each new offer that passes filters**:
105
+ a. Add to `pipeline.md` section "Pending": `- [ ] {url} | {company} | {title}`
106
+ b. Record in `scan-history.tsv`: `{url}\t{date}\t{query_name}\t{title}\t{company}\tadded`
107
+
108
+ 9. **Offers filtered by title**: record in `scan-history.tsv` with status `skipped_title`
109
+ 10. **Duplicate offers (URL-exact)**: record with status `skipped_dup`
110
+ 11. **Duplicate offers (fuzzy repost)**: record with status `skipped_repost` and note `repost of #{original_entry_num}`
111
+
112
+ ## Extract Title And Company From WebSearch Results
113
+
114
+ WebSearch results come in the format: `"Job Title @ Company"` or `"Job Title | Company"` or `"Job Title — Company"`.
115
+
116
+ Extraction patterns by portal:
117
+ - **Ashby**: `"Senior AI PM (Remote) @ EverAI"` → title: `Senior AI PM`, company: `EverAI`
118
+ - **Greenhouse**: `"AI Engineer at Anthropic"` → title: `AI Engineer`, company: `Anthropic`
119
+ - **Lever**: `"Product Manager - AI @ Temporal"` → title: `Product Manager - AI`, company: `Temporal`
120
+
121
+ Generic regex: `(.+?)(?:\s*[@|—–-]\s*|\s+at\s+)(.+?)$`
122
+
123
+ ## Resolve Private URLs
124
+
125
+ If a publicly inaccessible URL is found:
126
+ 1. Save the JD to `jds/{company}-{role-slug}.md`
127
+ 2. Add to pipeline.md as: `- [ ] local:jds/{company}-{role-slug}.md | {company} | {title}`
128
+
129
+ ## Scan History
130
+
131
+ `data/scan-history.tsv` tracks ALL seen URLs:
132
+
133
+ ```
134
+ url first_seen portal title company status
135
+ https://... 2026-02-10 Ashby — AI PM PM AI Acme added
136
+ https://... 2026-02-10 Greenhouse — SA Junior Dev BigCo skipped_title
137
+ https://... 2026-02-10 Ashby — AI PM SA AI OldCo skipped_dup
138
+ ```
139
+
140
+ ## Output Summary
141
+
142
+ ```
143
+ Portal Scan — {YYYY-MM-DD}
144
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━
145
+ Queries executed: N
146
+ Offers found: N total
147
+ Filtered by title: N relevant
148
+ Duplicates: N (already evaluated or in pipeline)
149
+ New added to pipeline.md: N
150
+
151
+ + {company} | {title} | {query_name}
152
+ ...
153
+
154
+ → Run /job-forge pipeline to evaluate the new offers.
155
+ ```
156
+
157
+ ## Update careers_url
158
+
159
+ Each company in `tracked_companies` MUST have a `careers_url` — the direct URL to its job listings page. The stored URL avoids searching for it every time.
160
+
161
+ **Known patterns by platform:**
162
+ - **Ashby:** `https://jobs.ashbyhq.com/{slug}`
163
+ - **Greenhouse:** `https://job-boards.greenhouse.io/{slug}` or `https://job-boards.eu.greenhouse.io/{slug}`
164
+ - **Lever:** `https://jobs.lever.co/{slug}`
165
+ - **Custom:** The company's own URL (e.g., `https://openai.com/careers`)
166
+
167
+ **If `careers_url` doesn't exist** for a company:
168
+ 1. Try the pattern for its known platform
169
+ 2. If that fails, do a quick WebSearch: `"{company}" careers jobs`
170
+ 3. Navigate with Geometra (`geometra_connect`) to confirm it works
171
+ 4. **Save the found URL in portals.yml** for future scans
172
+
173
+ **If `careers_url` returns 404 or redirect:**
174
+ 1. Note in the output summary
175
+ 2. Try scan_query as fallback
176
+ 3. Flag for manual update
177
+
178
+ ## Update portals.yml
179
+
180
+ - **ALWAYS save `careers_url`** when adding a new company
181
+ - Add new queries as interesting portals or roles are discovered
182
+ - Disable queries with `enabled: false` if they generate too much noise
183
+ - Adjust filtering keywords as target roles evolve
184
+ - Add companies to `tracked_companies` when you want to follow them closely
185
+ - Verify `careers_url` periodically — companies change ATS platforms
@@ -0,0 +1,31 @@
1
+ # Mode: tracker — Application Tracker
2
+
3
+ Reads and displays the application tracker: day-based files in `data/applications/` (format: `YYYY-MM-DD.md`).
4
+
5
+ **Tracker format:**
6
+ ```markdown
7
+ | # | Date | Company | Role | Score | Status | PDF | Report | Notes |
8
+ |---|------|---------|------|-------|--------|-----|--------|-------|
9
+ ```
10
+
11
+ Possible states (canonical, per `templates/states.yml`):
12
+
13
+ `Evaluated` → `Applied` → `Contacted` → `Responded` → `Interview` → `Offer` / `Rejected` / `Discarded` / `SKIP`
14
+
15
+ - `Applied` = the candidate submitted their application
16
+ - `Contacted` = the candidate proactively reached out to someone at the company (outbound, e.g., LinkedIn power move via `/job-forge contact`)
17
+ - `Responded` = a recruiter/company contacted back and the candidate responded (inbound)
18
+
19
+ If the user asks to update a status, edit the corresponding row in the day file where the entry exists.
20
+
21
+ Also display statistics:
22
+ - Total applications
23
+ - By status
24
+ - Average score
25
+ - % with generated PDF
26
+ - % with generated report
27
+
28
+ If any entries look overdue for follow-up (Applied 7+ days ago, Contacted 5+ days ago, Interviewed with no update 7+ days), mention it:
29
+ > "3 entries may need follow-up. Run `/job-forge followup` for details."
30
+
31
+ The followup reminder above is a passive hint — it does NOT change tracker behavior or output format.
@@ -0,0 +1,27 @@
1
+ # Mode: training — Training Evaluation
2
+
3
+ For each course/cert the candidate asks about, evaluate across 6 dimensions:
4
+
5
+ | Dimension | What it evaluates |
6
+ |-----------|-------------------|
7
+ | North Star alignment | Does it move closer to or away from the goal? |
8
+ | Recruiter signal | What do HMs think when they see this on a CV? |
9
+ | Time and effort | Weeks x hours/week |
10
+ | Opportunity cost | What can't they do during that time? |
11
+ | Risks | Outdated content? Weak brand? Too basic? |
12
+ | Portfolio deliverable | Does it produce a demonstrable artifact? |
13
+
14
+ ## Return One Of These Verdicts
15
+
16
+ - **DO IT** → 4-12 week plan with weekly deliverables and scoreboard
17
+ - **DON'T DO IT** → better alternative with justification
18
+ - **DO IT WITH TIMEBOX** (max X weeks) → condensed plan, essentials only
19
+
20
+ ## Apply This Priority Order
21
+
22
+ Training that improves credibility in "production-grade AI":
23
+ 1. Evals and LLM testing
24
+ 2. Observability and monitoring
25
+ 3. Cost/reliability trade-offs
26
+ 4. AI governance and safety
27
+ 5. Enterprise AI architecture
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * normalize-statuses.mjs — Clean non-canonical states in the application tracker
4
+ *
5
+ * Supports both layouts:
6
+ * - Day-based: data/applications/YYYY-MM-DD.md (preferred)
7
+ * - Single-file: data/applications.md or applications.md (legacy)
8
+ *
9
+ * Maps all non-canonical statuses to canonical ones per templates/states.yml:
10
+ * Evaluated, Applied, Responded, Contacted, Interview, Offer, Rejected, Discarded, SKIP
11
+ *
12
+ * Also strips markdown bold (**) and dates from the status field,
13
+ * moving DUPLICADO info to the notes column.
14
+ *
15
+ * Run: node normalize-statuses.mjs [--dry-run] (from repo root)
16
+ */
17
+
18
+ import { readFileSync, writeFileSync, copyFileSync, existsSync } from 'fs';
19
+ import { join, relative, dirname } from 'path';
20
+ import { fileURLToPath } from 'url';
21
+ import {
22
+ PROJECT_DIR, DATA_APPS_DIR, DATA_APPS_FILE, ROOT_APPS_FILE,
23
+ usesDayFiles, ensureDayDir, parseAppLine, formatAppLine,
24
+ readAllEntries, writeToDayFiles, listDayFiles,
25
+ } from './tracker-lib.mjs';
26
+
27
+ const DRY_RUN = process.argv.includes('--dry-run');
28
+
29
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
30
+ console.log(`normalize-statuses.mjs — map tracker status column to canonical labels
31
+
32
+ Supports day-based (data/applications/YYYY-MM-DD.md) and single-file layouts.
33
+ Uses templates/states.yml display labels when present. Strips markdown bold
34
+ and dates from the status field; moves duplicate/repost markers into notes
35
+ where applicable.
36
+
37
+ Usage:
38
+ node normalize-statuses.mjs [--dry-run]
39
+ npm run normalize [-- --dry-run]
40
+
41
+ Exits successfully when no tracker entries exist (nothing to do).
42
+ Creates a .bak copy next to the tracker before writing (single-file mode).
43
+
44
+ Run from the repository root.`);
45
+ process.exit(0);
46
+ }
47
+
48
+ function normalizeStatus(raw) {
49
+ let s = raw.replace(/\*\*/g, '').trim();
50
+ const lower = s.toLowerCase();
51
+
52
+ if (/^dup(licate)?/i.test(s)) {
53
+ return { status: 'Discarded', moveToNotes: raw.trim() };
54
+ }
55
+
56
+ if (/^contacted$/i.test(s)) return { status: 'Contacted' };
57
+
58
+ if (/^hold$/i.test(s)) return { status: 'Evaluated' };
59
+
60
+ if (/^repost/i.test(s)) return { status: 'Discarded', moveToNotes: raw.trim() };
61
+
62
+ if (s === '—' || s === '-' || s === '') return { status: 'Discarded' };
63
+
64
+ const canonical = [
65
+ 'Evaluated', 'Applied', 'Contacted', 'Responded', 'Interview',
66
+ 'Offer', 'Rejected', 'Discarded', 'SKIP',
67
+ ];
68
+ for (const c of canonical) {
69
+ if (lower === c.toLowerCase()) return { status: c };
70
+ }
71
+
72
+ if (['applied', 'sent'].includes(lower)) return { status: 'Applied' };
73
+ if (['skip'].includes(lower)) return { status: 'SKIP' };
74
+
75
+ return { status: null, unknown: true };
76
+ }
77
+
78
+ // Read entries
79
+ const { entries, source } = readAllEntries();
80
+
81
+ if (entries.length === 0) {
82
+ console.log('No tracker entries found. Nothing to normalize.');
83
+ process.exit(0);
84
+ }
85
+
86
+ let changes = 0;
87
+ let unknowns = [];
88
+ const updated = entries.map(app => {
89
+ const result = normalizeStatus(app.status);
90
+
91
+ if (result.unknown) {
92
+ unknowns.push({ num: app.num, rawStatus: app.status });
93
+ return app;
94
+ }
95
+
96
+ if (result.status === app.status) return app;
97
+
98
+ changes++;
99
+ console.log(`#${app.num}: "${app.status}" → "${result.status}"`);
100
+
101
+ let notes = app.notes || '';
102
+ if (result.moveToNotes && !notes.includes(result.moveToNotes)) {
103
+ notes = result.moveToNotes + (notes ? '. ' + notes : '');
104
+ }
105
+
106
+ // Also strip bold from score
107
+ const score = app.score ? app.score.replace(/\*\*/g, '') : app.score;
108
+
109
+ return { ...app, status: result.status, notes, score };
110
+ });
111
+
112
+ if (unknowns.length > 0) {
113
+ console.log(`\nāš ļø ${unknowns.length} unknown statuses:`);
114
+ for (const u of unknowns) {
115
+ console.log(` #${u.num}: "${u.rawStatus}"`);
116
+ }
117
+ }
118
+
119
+ console.log(`\nšŸ“Š ${changes} statuses normalized`);
120
+
121
+ if (!DRY_RUN && changes > 0) {
122
+ if (source === 'day') {
123
+ writeToDayFiles(updated);
124
+ console.log('āœ… Written to day files');
125
+ } else {
126
+ const APPS_FILE = existsSync(DATA_APPS_FILE) ? DATA_APPS_FILE : ROOT_APPS_FILE;
127
+ const appsDisplay = relative(PROJECT_DIR, APPS_FILE).replace(/\\/g, '/');
128
+ copyFileSync(APPS_FILE, APPS_FILE + '.bak');
129
+ // Rewrite single-file
130
+ const filePath = APPS_FILE;
131
+ const content = readFileSync(filePath, 'utf-8');
132
+ const lines = content.split('\n');
133
+ const updatedLines = [];
134
+ for (const line of lines) {
135
+ const app = parseAppLine(line);
136
+ if (app) {
137
+ const newApp = updated.find(u => u.num === app.num);
138
+ if (newApp) {
139
+ updatedLines.push(formatAppLine(newApp));
140
+ continue;
141
+ }
142
+ }
143
+ updatedLines.push(line);
144
+ }
145
+ writeFileSync(filePath, updatedLines.join('\n'));
146
+ console.log(`āœ… Written to ${appsDisplay} (backup: ${appsDisplay}.bak)`);
147
+ }
148
+ } else if (DRY_RUN) {
149
+ console.log('(dry-run — no changes written)');
150
+ } else {
151
+ console.log('āœ… No changes needed');
152
+ }
package/opencode.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "mcp": {
4
+ "geometra": {
5
+ "type": "local",
6
+ "command": [
7
+ "npx",
8
+ "-y",
9
+ "@geometra/mcp"
10
+ ],
11
+ "environment": {}
12
+ },
13
+ "gmail": {
14
+ "type": "local",
15
+ "command": [
16
+ "npx",
17
+ "-y",
18
+ "@razroo/gmail-mcp"
19
+ ],
20
+ "environment": {
21
+ "DISABLE_HTTP": "true"
22
+ }
23
+ }
24
+ },
25
+ "instructions": [
26
+ "templates/states.yml"
27
+ ]
28
+ }
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "job-forge",
3
+ "version": "2.0.0",
4
+ "description": "AI-powered job search pipeline built on opencode",
5
+ "type": "module",
6
+ "bin": {
7
+ "job-forge": "bin/job-forge.mjs",
8
+ "create-job-forge": "bin/create-job-forge.mjs"
9
+ },
10
+ "scripts": {
11
+ "verify": "node verify-pipeline.mjs",
12
+ "build:dashboard": "cd dashboard && go build .",
13
+ "normalize": "node normalize-statuses.mjs",
14
+ "dedup": "node dedup-tracker.mjs",
15
+ "merge": "node merge-tracker.mjs",
16
+ "pdf": "node generate-pdf.mjs",
17
+ "sync-check": "node cv-sync-check.mjs",
18
+ "tokens": "node scripts/token-usage-report.mjs",
19
+ "tokens:today": "node scripts/token-usage-report.mjs --days 1",
20
+ "tokens:log": "node scripts/token-usage-report.mjs --days 1 --append",
21
+ "build:config": "iso-harness build --source iso --out .",
22
+ "prepack": "iso-harness build --source iso --out .",
23
+ "release:check-source": "node ./scripts/release/check-source.mjs",
24
+ "postinstall": "node bin/sync.mjs"
25
+ },
26
+ "files": [
27
+ "bin/",
28
+ "iso/",
29
+ ".cursor/mcp.json",
30
+ ".cursor/rules/",
31
+ ".opencode/",
32
+ ".codex/",
33
+ ".mcp.json",
34
+ "CLAUDE.md",
35
+ "AGENTS.md",
36
+ "opencode.json",
37
+ "modes/",
38
+ "templates/",
39
+ "config/profile.example.yml",
40
+ "fonts/",
41
+ "scripts/",
42
+ "batch/batch-prompt.md",
43
+ "batch/batch-runner.sh",
44
+ "batch/README.md",
45
+ "docs/",
46
+ "tracker-lib.mjs",
47
+ "merge-tracker.mjs",
48
+ "dedup-tracker.mjs",
49
+ "verify-pipeline.mjs",
50
+ "normalize-statuses.mjs",
51
+ "generate-pdf.mjs",
52
+ "cv-sync-check.mjs",
53
+ "README.md",
54
+ "LICENSE"
55
+ ],
56
+ "keywords": [
57
+ "ai",
58
+ "job-search",
59
+ "opencode",
60
+ "career",
61
+ "automation"
62
+ ],
63
+ "author": "Charlie Greenman",
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "https://github.com/razroo/JobForge"
67
+ },
68
+ "license": "MIT",
69
+ "engines": {
70
+ "node": ">=18"
71
+ },
72
+ "dependencies": {
73
+ "playwright": "^1.58.1"
74
+ },
75
+ "devDependencies": {
76
+ "@razroo/iso-harness": "^0.1.3"
77
+ }
78
+ }