job_ops-mcp 0.3.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.
- package/.env.example +33 -0
- package/LICENSE +21 -0
- package/README.md +400 -0
- package/config/profile.example.yml +67 -0
- package/cv.example.md +53 -0
- package/dist/cli.js +385 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +63 -0
- package/dist/config.js.map +1 -0
- package/dist/core/browser.js +27 -0
- package/dist/core/browser.js.map +1 -0
- package/dist/core/content_hash.js +11 -0
- package/dist/core/content_hash.js.map +1 -0
- package/dist/core/csv.js +107 -0
- package/dist/core/csv.js.map +1 -0
- package/dist/core/cv_parse.js +201 -0
- package/dist/core/cv_parse.js.map +1 -0
- package/dist/core/html.js +10 -0
- package/dist/core/html.js.map +1 -0
- package/dist/core/jd_normalize.js +99 -0
- package/dist/core/jd_normalize.js.map +1 -0
- package/dist/core/jobs.js +106 -0
- package/dist/core/jobs.js.map +1 -0
- package/dist/core/llm.js +227 -0
- package/dist/core/llm.js.map +1 -0
- package/dist/core/modes.js +55 -0
- package/dist/core/modes.js.map +1 -0
- package/dist/core/outreach_safety.js +77 -0
- package/dist/core/outreach_safety.js.map +1 -0
- package/dist/core/profile.js +88 -0
- package/dist/core/profile.js.map +1 -0
- package/dist/core/providers/amazon.js +36 -0
- package/dist/core/providers/amazon.js.map +1 -0
- package/dist/core/providers/ashby.js +31 -0
- package/dist/core/providers/ashby.js.map +1 -0
- package/dist/core/providers/google.js +46 -0
- package/dist/core/providers/google.js.map +1 -0
- package/dist/core/providers/greenhouse.js +55 -0
- package/dist/core/providers/greenhouse.js.map +1 -0
- package/dist/core/providers/http.js +36 -0
- package/dist/core/providers/http.js.map +1 -0
- package/dist/core/providers/index.js +53 -0
- package/dist/core/providers/index.js.map +1 -0
- package/dist/core/providers/lever.js +32 -0
- package/dist/core/providers/lever.js.map +1 -0
- package/dist/core/providers/playwright_generic.js +53 -0
- package/dist/core/providers/playwright_generic.js.map +1 -0
- package/dist/core/providers/types.js +2 -0
- package/dist/core/providers/types.js.map +1 -0
- package/dist/core/providers/workday.js +44 -0
- package/dist/core/providers/workday.js.map +1 -0
- package/dist/core/render.js +253 -0
- package/dist/core/render.js.map +1 -0
- package/dist/core/reports.js +257 -0
- package/dist/core/reports.js.map +1 -0
- package/dist/core/resources.js +40 -0
- package/dist/core/resources.js.map +1 -0
- package/dist/core/scan_engine.js +164 -0
- package/dist/core/scan_engine.js.map +1 -0
- package/dist/core/scheduler.js +117 -0
- package/dist/core/scheduler.js.map +1 -0
- package/dist/db.js +60 -0
- package/dist/db.js.map +1 -0
- package/dist/http/app.js +35 -0
- package/dist/http/app.js.map +1 -0
- package/dist/http/dashboard.js +131 -0
- package/dist/http/dashboard.js.map +1 -0
- package/dist/mcp/define.js +35 -0
- package/dist/mcp/define.js.map +1 -0
- package/dist/mcp/server.js +103 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools/apply_prefill.js +167 -0
- package/dist/mcp/tools/apply_prefill.js.map +1 -0
- package/dist/mcp/tools/batch_evaluate.js +143 -0
- package/dist/mcp/tools/batch_evaluate.js.map +1 -0
- package/dist/mcp/tools/evaluate_job.js +181 -0
- package/dist/mcp/tools/evaluate_job.js.map +1 -0
- package/dist/mcp/tools/generate_materials.js +126 -0
- package/dist/mcp/tools/generate_materials.js.map +1 -0
- package/dist/mcp/tools/get_report.js +24 -0
- package/dist/mcp/tools/get_report.js.map +1 -0
- package/dist/mcp/tools/ops.js +321 -0
- package/dist/mcp/tools/ops.js.map +1 -0
- package/dist/mcp/tools/outreach.js +481 -0
- package/dist/mcp/tools/outreach.js.map +1 -0
- package/dist/mcp/tools/render_pdf.js +27 -0
- package/dist/mcp/tools/render_pdf.js.map +1 -0
- package/dist/mcp/tools/scan_portals.js +35 -0
- package/dist/mcp/tools/scan_portals.js.map +1 -0
- package/dist/mcp/tools/scheduler.js +32 -0
- package/dist/mcp/tools/scheduler.js.map +1 -0
- package/dist/mcp/tools/stories.js +172 -0
- package/dist/mcp/tools/stories.js.map +1 -0
- package/dist/mcp/tools/tracker.js +183 -0
- package/dist/mcp/tools/tracker.js.map +1 -0
- package/dist/mcp/tools/visa.js +219 -0
- package/dist/mcp/tools/visa.js.map +1 -0
- package/dist/migrations/001_initial.sql +505 -0
- package/dist/migrations/002_llm_and_digest.sql +42 -0
- package/dist/server.js +55 -0
- package/dist/server.js.map +1 -0
- package/fonts/dm-sans-latin-ext.woff2 +0 -0
- package/fonts/dm-sans-latin.woff2 +0 -0
- package/fonts/space-grotesk-latin-ext.woff2 +0 -0
- package/fonts/space-grotesk-latin.woff2 +0 -0
- package/modes/career_packet.md +91 -0
- package/modes/negotiation_playbook.md +64 -0
- package/modes/outreach_tone.md +80 -0
- package/modes/report_format.md +83 -0
- package/modes/rubric.md +119 -0
- package/modes/tailoring_rules.md +102 -0
- package/package.json +67 -0
- package/portals.example.yml +95 -0
- package/templates/cover-template.html +64 -0
- package/templates/cv-template.html +421 -0
- package/templates/cv-template.tex +123 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// eval_reports repo + HTML rendering.
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { config } from '../config.js';
|
|
6
|
+
import { getDb, runInWriteLock } from '../db.js';
|
|
7
|
+
import { escapeHtml } from './html.js';
|
|
8
|
+
const REPORTS_SUBDIR = 'reports';
|
|
9
|
+
export async function saveReport(args) {
|
|
10
|
+
return runInWriteLock(async () => {
|
|
11
|
+
const db = getDb();
|
|
12
|
+
const id = randomUUID();
|
|
13
|
+
const html = renderReportHtml({
|
|
14
|
+
reportId: id,
|
|
15
|
+
jobId: args.job_id,
|
|
16
|
+
mode: args.mode,
|
|
17
|
+
blocks: args.blocks,
|
|
18
|
+
scores: args.scores,
|
|
19
|
+
rawInput: args.raw_input,
|
|
20
|
+
});
|
|
21
|
+
const outDir = resolve(config.outputDir, REPORTS_SUBDIR);
|
|
22
|
+
mkdirSync(outDir, { recursive: true });
|
|
23
|
+
const relativeHtmlPath = `${REPORTS_SUBDIR}/${id}.html`;
|
|
24
|
+
const absoluteHtmlPath = resolve(config.outputDir, relativeHtmlPath);
|
|
25
|
+
writeFileSync(absoluteHtmlPath, html, 'utf-8');
|
|
26
|
+
db.prepare(`
|
|
27
|
+
INSERT INTO eval_reports (
|
|
28
|
+
id, job_id, mode, archetype_detected,
|
|
29
|
+
block_role_summary, block_cv_match, block_level, block_comp,
|
|
30
|
+
block_personalize, block_interview, block_legitimacy,
|
|
31
|
+
keywords, raw_input, html_path
|
|
32
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
33
|
+
`).run(id, args.job_id, args.mode, args.blocks.archetype_detected ?? null, args.blocks.block_role_summary ?? null, args.blocks.block_cv_match ?? null, args.blocks.block_level ?? null, args.blocks.block_comp ?? null, args.blocks.block_personalize ?? null, args.blocks.block_interview ?? null, args.blocks.block_legitimacy ?? null, args.blocks.keywords ? JSON.stringify(args.blocks.keywords) : null, args.raw_input, relativeHtmlPath);
|
|
34
|
+
if (args.scores) {
|
|
35
|
+
// Server is the source of truth for score_total — when visa is disabled, we
|
|
36
|
+
// recompute from resume/taste regardless of what the chat (or LLM) returned.
|
|
37
|
+
const resumeFit = nonNeg(args.scores.resume_fit);
|
|
38
|
+
const tasteFit = nonNeg(args.scores.taste_fit);
|
|
39
|
+
const visaFit = config.visaScoringEnabled ? nonNeg(args.scores.visa_fit) : null;
|
|
40
|
+
const scoreTotal = config.visaScoringEnabled
|
|
41
|
+
? nonNeg(args.scores.score_total)
|
|
42
|
+
: combineNoVisa(resumeFit, tasteFit);
|
|
43
|
+
db.prepare(`
|
|
44
|
+
UPDATE jobs SET
|
|
45
|
+
score_resume_fit = ?,
|
|
46
|
+
score_taste_fit = ?,
|
|
47
|
+
score_visa_fit = ?,
|
|
48
|
+
score_total = ?,
|
|
49
|
+
role_category = COALESCE(?, role_category),
|
|
50
|
+
seniority = COALESCE(?, seniority),
|
|
51
|
+
score_detail = ?,
|
|
52
|
+
scored_at = CURRENT_TIMESTAMP,
|
|
53
|
+
updated_at = CURRENT_TIMESTAMP
|
|
54
|
+
WHERE id = ?
|
|
55
|
+
`).run(resumeFit, tasteFit, visaFit, scoreTotal, args.scores.role_category ?? null, args.scores.seniority ?? null, JSON.stringify({
|
|
56
|
+
reasoning: args.scores.reasoning ?? null,
|
|
57
|
+
concerns: args.scores.concerns ?? null,
|
|
58
|
+
mode: args.mode,
|
|
59
|
+
eval_report_id: id,
|
|
60
|
+
visa_scoring_enabled: config.visaScoringEnabled,
|
|
61
|
+
}), args.job_id);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
id,
|
|
65
|
+
relativeHtmlPath,
|
|
66
|
+
absoluteHtmlPath,
|
|
67
|
+
url: `${config.baseUrl}/files/${relativeHtmlPath}`,
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export function getLatestReport(jobId) {
|
|
72
|
+
const row = getDb().prepare(`
|
|
73
|
+
SELECT * FROM eval_reports
|
|
74
|
+
WHERE job_id = ?
|
|
75
|
+
ORDER BY datetime(created_at) DESC
|
|
76
|
+
LIMIT 1
|
|
77
|
+
`).get(jobId);
|
|
78
|
+
if (!row)
|
|
79
|
+
return null;
|
|
80
|
+
return {
|
|
81
|
+
...row,
|
|
82
|
+
keywords: row.keywords ? JSON.parse(row.keywords) : null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function nonNeg(n) {
|
|
86
|
+
if (n === undefined || n === null)
|
|
87
|
+
return null;
|
|
88
|
+
return Number.isFinite(n) ? Math.max(0, Math.min(100, Math.round(n))) : null;
|
|
89
|
+
}
|
|
90
|
+
// Renormalized weights when visa scoring is off. Mirrors VISA_DISABLED_PREFIX in modes.ts.
|
|
91
|
+
export function combineNoVisa(resume, taste) {
|
|
92
|
+
if (resume == null || taste == null)
|
|
93
|
+
return null;
|
|
94
|
+
return Math.round(resume * 0.6 + taste * 0.4);
|
|
95
|
+
}
|
|
96
|
+
// ── Rendering ────────────────────────────────────────────────────────────────
|
|
97
|
+
function renderReportHtml(args) {
|
|
98
|
+
const block = (title, body) => body
|
|
99
|
+
? `<section><h2>${escapeHtml(title)}</h2><div class="prose">${mdToHtml(body)}</div></section>`
|
|
100
|
+
: '';
|
|
101
|
+
const visaSeg = config.visaScoringEnabled
|
|
102
|
+
? ` v:${args.scores?.visa_fit ?? '—'}`
|
|
103
|
+
: '';
|
|
104
|
+
const scoreBadge = args.scores?.score_total != null
|
|
105
|
+
? `<span class="score">total ${args.scores.score_total}</span>
|
|
106
|
+
<span class="sub">r:${args.scores.resume_fit ?? '—'}
|
|
107
|
+
t:${args.scores.taste_fit ?? '—'}${visaSeg}</span>`
|
|
108
|
+
: '<span class="score muted">unscored</span>';
|
|
109
|
+
const keywords = (args.blocks.keywords ?? []).map(k => `<code>${escapeHtml(k)}</code>`).join(' ');
|
|
110
|
+
return `<!DOCTYPE html>
|
|
111
|
+
<html lang="en">
|
|
112
|
+
<head>
|
|
113
|
+
<meta charset="utf-8" />
|
|
114
|
+
<title>Evaluation ${args.reportId.slice(0, 8)}</title>
|
|
115
|
+
<style>
|
|
116
|
+
:root { color-scheme: light dark; }
|
|
117
|
+
body { font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; max-width: 880px;
|
|
118
|
+
margin: 2rem auto; padding: 0 1rem; line-height: 1.55; color: #1a1a2e; }
|
|
119
|
+
h1 { margin-bottom: 0.25rem; }
|
|
120
|
+
.meta { color: #555; font-size: 0.9rem; margin-bottom: 1.25rem; }
|
|
121
|
+
.score { background: #1a1a2e; color: #fff; padding: 0.15rem 0.55rem; border-radius: 3px; font-weight: 600; }
|
|
122
|
+
.score.muted { background: #999; }
|
|
123
|
+
.sub { color: #555; font-size: 0.85rem; margin-left: 0.5rem; }
|
|
124
|
+
section { margin-bottom: 1.5rem; }
|
|
125
|
+
h2 { font-size: 1.05rem; text-transform: uppercase; letter-spacing: 0.05em;
|
|
126
|
+
color: hsl(187, 74%, 32%); border-bottom: 1.5px solid #e2e2e2; padding-bottom: 4px; }
|
|
127
|
+
.prose pre { white-space: pre-wrap; background: #f6f6f8; padding: 0.6rem 0.8rem;
|
|
128
|
+
border-left: 3px solid #ccc; border-radius: 3px; font-size: 0.9em; }
|
|
129
|
+
.keywords code { background: #f0f0f3; padding: 0.1rem 0.4rem; border-radius: 2px;
|
|
130
|
+
margin: 0.1rem 0.15rem 0.1rem 0; display: inline-block; font-size: 0.85em; }
|
|
131
|
+
.raw { max-height: 280px; overflow: auto; }
|
|
132
|
+
a { color: hsl(270, 70%, 45%); }
|
|
133
|
+
</style>
|
|
134
|
+
</head>
|
|
135
|
+
<body>
|
|
136
|
+
<h1>Evaluation Report</h1>
|
|
137
|
+
<div class="meta">
|
|
138
|
+
${scoreBadge} ·
|
|
139
|
+
mode <code>${args.mode}</code> ·
|
|
140
|
+
archetype <code>${escapeHtml(args.blocks.archetype_detected ?? 'unset')}</code> ·
|
|
141
|
+
role <code>${escapeHtml(args.scores?.role_category ?? '—')}</code> ·
|
|
142
|
+
level <code>${escapeHtml(args.scores?.seniority ?? '—')}</code> ·
|
|
143
|
+
<a href="/">tracker</a>
|
|
144
|
+
</div>
|
|
145
|
+
${args.scores?.reasoning
|
|
146
|
+
? `<section><h2>Why</h2><p>${escapeHtml(args.scores.reasoning)}</p>${args.scores.concerns ? `<p><strong>Concerns:</strong> ${escapeHtml(args.scores.concerns)}</p>` : ''}</section>`
|
|
147
|
+
: ''}
|
|
148
|
+
${block('A) Role Summary', args.blocks.block_role_summary)}
|
|
149
|
+
${block('B) CV Match', args.blocks.block_cv_match)}
|
|
150
|
+
${block('C) Level & Strategy', args.blocks.block_level)}
|
|
151
|
+
${block('D) Comp & Demand', args.blocks.block_comp)}
|
|
152
|
+
${block('E) Personalization Plan', args.blocks.block_personalize)}
|
|
153
|
+
${block('F) Interview Plan', args.blocks.block_interview)}
|
|
154
|
+
${block('G) Posting Legitimacy', args.blocks.block_legitimacy)}
|
|
155
|
+
${keywords ? `<section class="keywords"><h2>Keywords</h2><div>${keywords}</div></section>` : ''}
|
|
156
|
+
<section><h2>Normalized JD (input)</h2><pre class="raw">${escapeHtml(args.rawInput)}</pre></section>
|
|
157
|
+
</body></html>`;
|
|
158
|
+
}
|
|
159
|
+
// Minimal markdown → HTML: headers, lists, paragraphs, fenced code, inline code, links, bold.
|
|
160
|
+
// We deliberately avoid pulling in a dependency — the chat will mostly send tables and lists
|
|
161
|
+
// and this covers ~95% of cases without DOM tooling.
|
|
162
|
+
function mdToHtml(src) {
|
|
163
|
+
const lines = src.replace(/\r\n/g, '\n').split('\n');
|
|
164
|
+
const out = [];
|
|
165
|
+
let inCode = false;
|
|
166
|
+
let inList = false;
|
|
167
|
+
let inTable = false;
|
|
168
|
+
let paraBuf = [];
|
|
169
|
+
const flushPara = () => {
|
|
170
|
+
if (paraBuf.length) {
|
|
171
|
+
out.push(`<p>${inlineMd(paraBuf.join(' '))}</p>`);
|
|
172
|
+
paraBuf = [];
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const closeList = () => { if (inList) {
|
|
176
|
+
out.push('</ul>');
|
|
177
|
+
inList = false;
|
|
178
|
+
} };
|
|
179
|
+
const closeTable = () => { if (inTable) {
|
|
180
|
+
out.push('</tbody></table>');
|
|
181
|
+
inTable = false;
|
|
182
|
+
} };
|
|
183
|
+
for (let i = 0; i < lines.length; i++) {
|
|
184
|
+
const line = lines[i];
|
|
185
|
+
if (line.startsWith('```')) {
|
|
186
|
+
flushPara();
|
|
187
|
+
closeList();
|
|
188
|
+
closeTable();
|
|
189
|
+
if (!inCode) {
|
|
190
|
+
out.push('<pre><code>');
|
|
191
|
+
inCode = true;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
out.push('</code></pre>');
|
|
195
|
+
inCode = false;
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (inCode) {
|
|
200
|
+
out.push(escapeHtml(line));
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (/^\|.*\|/.test(line)) {
|
|
204
|
+
flushPara();
|
|
205
|
+
closeList();
|
|
206
|
+
const cells = line.split('|').slice(1, -1).map(c => c.trim());
|
|
207
|
+
const isSep = cells.every(c => /^:?-+:?$/.test(c));
|
|
208
|
+
if (isSep)
|
|
209
|
+
continue;
|
|
210
|
+
if (!inTable) {
|
|
211
|
+
out.push('<table><tbody>');
|
|
212
|
+
inTable = true;
|
|
213
|
+
}
|
|
214
|
+
out.push('<tr>' + cells.map(c => `<td>${inlineMd(c)}</td>`).join('') + '</tr>');
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
closeTable();
|
|
219
|
+
}
|
|
220
|
+
if (/^\s*[-*]\s+/.test(line)) {
|
|
221
|
+
flushPara();
|
|
222
|
+
if (!inList) {
|
|
223
|
+
out.push('<ul>');
|
|
224
|
+
inList = true;
|
|
225
|
+
}
|
|
226
|
+
out.push(`<li>${inlineMd(line.replace(/^\s*[-*]\s+/, ''))}</li>`);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
closeList();
|
|
231
|
+
}
|
|
232
|
+
const h = line.match(/^(#{1,6})\s+(.*)$/);
|
|
233
|
+
if (h) {
|
|
234
|
+
flushPara();
|
|
235
|
+
out.push(`<h${h[1].length}>${inlineMd(h[2])}</h${h[1].length}>`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (line.trim() === '') {
|
|
239
|
+
flushPara();
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
paraBuf.push(line);
|
|
243
|
+
}
|
|
244
|
+
flushPara();
|
|
245
|
+
closeList();
|
|
246
|
+
closeTable();
|
|
247
|
+
if (inCode)
|
|
248
|
+
out.push('</code></pre>');
|
|
249
|
+
return out.join('\n');
|
|
250
|
+
}
|
|
251
|
+
function inlineMd(s) {
|
|
252
|
+
return escapeHtml(s)
|
|
253
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
254
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
255
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=reports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reports.js","sourceRoot":"","sources":["../../src/core/reports.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAuBvC,MAAM,cAAc,GAAG,SAAS,CAAC;AAEjC,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAOhC;IACC,OAAO,cAAc,CAAC,KAAK,IAAI,EAAE;QAC/B,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;QACnB,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,gBAAgB,CAAC;YAC5B,QAAQ,EAAE,EAAE;YACZ,KAAK,EAAK,IAAI,CAAC,MAAM;YACrB,IAAI,EAAM,IAAI,CAAC,IAAI;YACnB,MAAM,EAAI,IAAI,CAAC,MAAM;YACrB,MAAM,EAAI,IAAI,CAAC,MAAM;YACrB,QAAQ,EAAE,IAAI,CAAC,SAAS;SACzB,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;QACzD,SAAS,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACvC,MAAM,gBAAgB,GAAG,GAAG,cAAc,IAAI,EAAE,OAAO,CAAC;QACxD,MAAM,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC;QACrE,aAAa,CAAC,gBAAgB,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QAE/C,EAAE,CAAC,OAAO,CAAC;;;;;;;KAOV,CAAC,CAAC,GAAG,CACJ,EAAE,EACF,IAAI,CAAC,MAAM,EACX,IAAI,CAAC,IAAI,EACT,IAAI,CAAC,MAAM,CAAC,kBAAkB,IAAI,IAAI,EACtC,IAAI,CAAC,MAAM,CAAC,kBAAkB,IAAI,IAAI,EACtC,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,EAClC,IAAI,CAAC,MAAM,CAAC,WAAW,IAAI,IAAI,EAC/B,IAAI,CAAC,MAAM,CAAC,UAAU,IAAI,IAAI,EAC9B,IAAI,CAAC,MAAM,CAAC,iBAAiB,IAAI,IAAI,EACrC,IAAI,CAAC,MAAM,CAAC,eAAe,IAAI,IAAI,EACnC,IAAI,CAAC,MAAM,CAAC,gBAAgB,IAAI,IAAI,EACpC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,EAClE,IAAI,CAAC,SAAS,EACd,gBAAgB,CACjB,CAAC;QAEF,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,4EAA4E;YAC5E,6EAA6E;YAC7E,MAAM,SAAS,GAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YAClD,MAAM,QAAQ,GAAK,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YACjD,MAAM,OAAO,GAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YACnF,MAAM,UAAU,GAAG,MAAM,CAAC,kBAAkB;gBAC1C,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;gBACjC,CAAC,CAAC,aAAa,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;YACvC,EAAE,CAAC,OAAO,CAAC;;;;;;;;;;;;OAYV,CAAC,CAAC,GAAG,CACJ,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EACxC,IAAI,CAAC,MAAM,CAAC,aAAa,IAAI,IAAI,EACjC,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,IAAI,EAC7B,IAAI,CAAC,SAAS,CAAC;gBACb,SAAS,EAAG,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,IAAI;gBACzC,QAAQ,EAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,IAAI;gBACxC,IAAI,EAAQ,IAAI,CAAC,IAAI;gBACrB,cAAc,EAAE,EAAE;gBAClB,oBAAoB,EAAE,MAAM,CAAC,kBAAkB;aAChD,CAAC,EACF,IAAI,CAAC,MAAM,CACZ,CAAC;QACJ,CAAC;QAED,OAAO;YACL,EAAE;YACF,gBAAgB;YAChB,gBAAgB;YAChB,GAAG,EAAE,GAAG,MAAM,CAAC,OAAO,UAAU,gBAAgB,EAAE;SACnD,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,MAAM,GAAG,GAAG,KAAK,EAAE,CAAC,OAAO,CAAC;;;;;GAK3B,CAAC,CAAC,GAAG,CAAC,KAAK,CAAQ,CAAC;IACrB,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,OAAO;QACL,GAAG,GAAG;QACN,QAAQ,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI;KAC5C,CAAC;AACjB,CAAC;AAED,SAAS,MAAM,CAAC,CAAqB;IACnC,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC/C,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/E,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,aAAa,CAAC,MAAqB,EAAE,KAAoB;IACvE,IAAI,MAAM,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IACjD,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,GAAG,GAAG,KAAK,GAAG,GAAG,CAAC,CAAC;AAChD,CAAC;AAED,gFAAgF;AAEhF,SAAS,gBAAgB,CAAC,IAQzB;IACC,MAAM,KAAK,GAAG,CAAC,KAAa,EAAE,IAA+B,EAAE,EAAE,CAAC,IAAI;QACpE,CAAC,CAAC,gBAAgB,UAAU,CAAC,KAAK,CAAC,2BAA2B,QAAQ,CAAC,IAAI,CAAC,kBAAkB;QAC9F,CAAC,CAAC,EAAE,CAAC;IACP,MAAM,OAAO,GAAG,MAAM,CAAC,kBAAkB;QACvC,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,QAAQ,IAAI,GAAG,EAAE;QACtC,CAAC,CAAC,EAAE,CAAC;IACP,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,EAAE,WAAW,IAAI,IAAI;QACjD,CAAC,CAAC,6BAA6B,IAAI,CAAC,MAAM,CAAC,WAAW;6BAC7B,IAAI,CAAC,MAAM,CAAC,UAAU,IAAI,GAAG;6BAC7B,IAAI,CAAC,MAAM,CAAC,SAAS,IAAK,GAAG,GAAG,OAAO,SAAS;QACzE,CAAC,CAAC,2CAA2C,CAAC;IAChD,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAElG,OAAO;;;;oBAIW,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;IAwBzC,UAAU;eACC,IAAI,CAAC,IAAI;oBACJ,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,kBAAkB,IAAI,OAAO,CAAC;eAC1D,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,aAAa,IAAI,GAAG,CAAC;gBAC5C,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,IAAI,GAAG,CAAC;;;EAGvD,IAAI,CAAC,MAAM,EAAE,SAAS;QACtB,CAAC,CAAC,2BAA2B,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,iCAAiC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,YAAY;QACpL,CAAC,CAAC,EAAE;EACJ,KAAK,CAAC,iBAAiB,EAAS,IAAI,CAAC,MAAM,CAAC,kBAAkB,CAAC;EAC/D,KAAK,CAAC,aAAa,EAAa,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC;EAC3D,KAAK,CAAC,qBAAqB,EAAK,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;EACxD,KAAK,CAAC,kBAAkB,EAAQ,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC;EACvD,KAAK,CAAC,yBAAyB,EAAC,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC;EAC9D,KAAK,CAAC,mBAAmB,EAAO,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC;EAC5D,KAAK,CAAC,uBAAuB,EAAG,IAAI,CAAC,MAAM,CAAC,gBAAgB,CAAC;EAC7D,QAAQ,CAAC,CAAC,CAAC,mDAAmD,QAAQ,kBAAkB,CAAC,CAAC,CAAC,EAAE;0DACrC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC;eACpE,CAAC;AAChB,CAAC;AAED,8FAA8F;AAC9F,6FAA6F;AAC7F,qDAAqD;AACrD,SAAS,QAAQ,CAAC,GAAW;IAC3B,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACrD,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,OAAO,GAAa,EAAE,CAAC;IAE3B,MAAM,SAAS,GAAG,GAAG,EAAE;QACrB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnB,GAAG,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;YAClD,OAAO,GAAG,EAAE,CAAC;QACf,CAAC;IACH,CAAC,CAAC;IACF,MAAM,SAAS,GAAG,GAAG,EAAE,GAAG,IAAI,MAAM,EAAE,CAAC;QAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAAC,MAAM,GAAG,KAAK,CAAC;IAAC,CAAC,CAAC,CAAC,CAAC;IAC/E,MAAM,UAAU,GAAG,GAAG,EAAE,GAAG,IAAI,OAAO,EAAE,CAAC;QAAC,GAAG,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;QAAC,OAAO,GAAG,KAAK,CAAC;IAAC,CAAC,CAAC,CAAC,CAAC;IAE7F,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3B,SAAS,EAAE,CAAC;YAAC,SAAS,EAAE,CAAC;YAAC,UAAU,EAAE,CAAC;YACvC,IAAI,CAAC,MAAM,EAAE,CAAC;gBAAC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;gBAAC,MAAM,GAAG,IAAI,CAAC;YAAC,CAAC;iBAC3C,CAAC;gBAAC,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;gBAAE,MAAM,GAAG,KAAK,CAAC;YAAC,CAAC;YAC5D,SAAS;QACX,CAAC;QACD,IAAI,MAAM,EAAE,CAAC;YAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;YAAC,SAAS;QAAC,CAAC;QAErD,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,SAAS,EAAE,CAAC;YAAC,SAAS,EAAE,CAAC;YACzB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YAC9D,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YACnD,IAAI,KAAK;gBAAE,SAAS;YACpB,IAAI,CAAC,OAAO,EAAE,CAAC;gBAAC,GAAG,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;gBAAC,OAAO,GAAG,IAAI,CAAC;YAAC,CAAC;YAC7D,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC;YAChF,SAAS;QACX,CAAC;aAAM,CAAC;YAAC,UAAU,EAAE,CAAC;QAAC,CAAC;QAExB,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7B,SAAS,EAAE,CAAC;YACZ,IAAI,CAAC,MAAM,EAAE,CAAC;gBAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBAAC,MAAM,GAAG,IAAI,CAAC;YAAC,CAAC;YACjD,GAAG,CAAC,IAAI,CAAC,OAAO,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;YAClE,SAAS;QACX,CAAC;aAAM,CAAC;YAAC,SAAS,EAAE,CAAC;QAAC,CAAC;QAEvB,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAC1C,IAAI,CAAC,EAAE,CAAC;YACN,SAAS,EAAE,CAAC;YACZ,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;YACjE,SAAS;QACX,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YAAC,SAAS,EAAE,CAAC;YAAC,SAAS;QAAC,CAAC;QAClD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC;IACD,SAAS,EAAE,CAAC;IAAC,SAAS,EAAE,CAAC;IAAC,UAAU,EAAE,CAAC;IAAC,IAAI,MAAM;QAAE,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAC9E,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,SAAS,QAAQ,CAAC,CAAS;IACzB,OAAO,UAAU,CAAC,CAAC,CAAC;SACjB,OAAO,CAAC,YAAY,EAAE,iBAAiB,CAAC;SACxC,OAAO,CAAC,kBAAkB,EAAE,qBAAqB,CAAC;SAClD,OAAO,CAAC,0BAA0B,EAAE,qBAAqB,CAAC,CAAC;AAChE,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Loads the markdown files in modes/ as MCP resources.
|
|
2
|
+
//
|
|
3
|
+
// Chat clients READ these to do the actual reasoning (scoring, drafting). The server is
|
|
4
|
+
// deliberately not the brain — it just hands these documents over so the chat stays
|
|
5
|
+
// aligned with the rubric + tone rules every turn.
|
|
6
|
+
import { getActiveCareerPacket } from './profile.js';
|
|
7
|
+
import { getMode } from './modes.js';
|
|
8
|
+
export function listResources() {
|
|
9
|
+
return [
|
|
10
|
+
fileResource('rubric', 'Rating Rubric', 'How to score a job (resume / taste / visa fit, weights, hard rules).'),
|
|
11
|
+
fileResource('report_format', 'Evaluation Report Format', '6-block A–F (+G) report shape that chat fills in.'),
|
|
12
|
+
fileResource('tailoring_rules', 'Materials Tailoring Rules', 'How generate_materials picks bullets/projects per JD.'),
|
|
13
|
+
fileResource('outreach_tone', 'Outreach Tone', 'Warm-intro + founder DM rules (char caps, never-refer-me, etc.).'),
|
|
14
|
+
fileResource('negotiation_playbook', 'Negotiation Playbook', 'Scripts + frames for negotiation_brief.'),
|
|
15
|
+
{
|
|
16
|
+
uri: 'mcp-jsa://career_packet/active',
|
|
17
|
+
name: 'career_packet',
|
|
18
|
+
title: 'Active Career Packet',
|
|
19
|
+
mimeType: 'text/markdown',
|
|
20
|
+
description: 'The active, versioned superset of every claim the candidate may make. Seeded from cv.md + modes/career_packet.md.',
|
|
21
|
+
body: () => {
|
|
22
|
+
const row = getActiveCareerPacket();
|
|
23
|
+
return row?.content ?? '_no active career packet — start the server once to seed_';
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
function fileResource(slug, title, description) {
|
|
29
|
+
// Go through getMode() so dynamic prefixes (e.g. the VISA SCORING DISABLED block
|
|
30
|
+
// applied to rubric.md when MCP_JSA_VISA_SCORING=false) reach the chat too.
|
|
31
|
+
return {
|
|
32
|
+
uri: `mcp-jsa://modes/${slug}`,
|
|
33
|
+
name: slug,
|
|
34
|
+
title,
|
|
35
|
+
mimeType: 'text/markdown',
|
|
36
|
+
description,
|
|
37
|
+
body: () => getMode(`${slug}.md`),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=resources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resources.js","sourceRoot":"","sources":["../../src/core/resources.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,EAAE;AACF,wFAAwF;AACxF,oFAAoF;AACpF,mDAAmD;AAEnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAWrC,MAAM,UAAU,aAAa;IAC3B,OAAO;QACL,YAAY,CAAC,QAAQ,EAAgB,eAAe,EAAW,sEAAsE,CAAC;QACtI,YAAY,CAAC,eAAe,EAAS,0BAA0B,EAAC,mDAAmD,CAAC;QACpH,YAAY,CAAC,iBAAiB,EAAO,2BAA2B,EAAC,uDAAuD,CAAC;QACzH,YAAY,CAAC,eAAe,EAAS,eAAe,EAAY,kEAAkE,CAAC;QACnI,YAAY,CAAC,sBAAsB,EAAE,sBAAsB,EAAK,yCAAyC,CAAC;QAC1G;YACE,GAAG,EAAG,gCAAgC;YACtC,IAAI,EAAE,eAAe;YACrB,KAAK,EAAE,sBAAsB;YAC7B,QAAQ,EAAE,eAAe;YACzB,WAAW,EAAE,mHAAmH;YAChI,IAAI,EAAE,GAAG,EAAE;gBACT,MAAM,GAAG,GAAG,qBAAqB,EAAE,CAAC;gBACpC,OAAO,GAAG,EAAE,OAAO,IAAI,2DAA2D,CAAC;YACrF,CAAC;SACF;KACF,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,IAAY,EAAE,KAAa,EAAE,WAAmB;IACpE,iFAAiF;IACjF,4EAA4E;IAC5E,OAAO;QACL,GAAG,EAAG,mBAAmB,IAAI,EAAE;QAC/B,IAAI,EAAE,IAAI;QACV,KAAK;QACL,QAAQ,EAAE,eAAe;QACzB,WAAW;QACX,IAAI,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,IAAI,KAAK,CAAC;KAClC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Scan engine — reads portals.yml, fans out across providers, upserts via upsertJob
|
|
2
|
+
// (which does cross-source content-hash dedupe). Records a run summary in scan_runs.
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
import { getDb, runInWriteLock } from '../db.js';
|
|
6
|
+
import { upsertJob } from './jobs.js';
|
|
7
|
+
import { loadProjectFiles } from './profile.js';
|
|
8
|
+
import { closeProviderBrowser, makeProviderCtx, PROVIDERS, resolveProvider } from './providers/index.js';
|
|
9
|
+
// ── portals.yml parser ───────────────────────────────────────────────────────
|
|
10
|
+
function buildTitleFilter(yml, override) {
|
|
11
|
+
const pos = (override.title_positive ?? yml?.title_filter?.positive ?? []).map((s) => s.toLowerCase());
|
|
12
|
+
const neg = (override.title_negative ?? yml?.title_filter?.negative ?? []).map((s) => s.toLowerCase());
|
|
13
|
+
return (title) => {
|
|
14
|
+
const t = (title ?? '').toLowerCase();
|
|
15
|
+
const okPos = pos.length === 0 || pos.some((k) => t.includes(k));
|
|
16
|
+
const okNeg = !neg.some((k) => t.includes(k));
|
|
17
|
+
return okPos && okNeg;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function buildLocationFilter(yml, override) {
|
|
21
|
+
const allow = (override.location_allow ?? yml?.location_filter?.allow ?? []).map((s) => s.toLowerCase());
|
|
22
|
+
const block = (override.location_block ?? yml?.location_filter?.block ?? []).map((s) => s.toLowerCase());
|
|
23
|
+
const always = (yml?.location_filter?.always_allow ?? []).map((s) => s.toLowerCase());
|
|
24
|
+
return (loc) => {
|
|
25
|
+
if (!loc || !loc.trim())
|
|
26
|
+
return true;
|
|
27
|
+
const l = loc.toLowerCase();
|
|
28
|
+
if (always.length && always.some((k) => l.includes(k)))
|
|
29
|
+
return true;
|
|
30
|
+
if (block.length && block.some((k) => l.includes(k)))
|
|
31
|
+
return false;
|
|
32
|
+
if (!allow.length)
|
|
33
|
+
return true;
|
|
34
|
+
return allow.some((k) => l.includes(k));
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function readPortalsYml() {
|
|
38
|
+
const { portalsYml } = loadProjectFiles();
|
|
39
|
+
if (!portalsYml)
|
|
40
|
+
return { yml: null, companies: [] };
|
|
41
|
+
const doc = yaml.load(portalsYml);
|
|
42
|
+
const companies = (Array.isArray(doc?.tracked_companies) ? doc.tracked_companies : [])
|
|
43
|
+
.filter((c) => c && typeof c === 'object' && c.enabled !== false && typeof c.name === 'string');
|
|
44
|
+
return { yml: doc, companies };
|
|
45
|
+
}
|
|
46
|
+
// ── Engine ───────────────────────────────────────────────────────────────────
|
|
47
|
+
export async function runScan(filters = {}, opts = {}) {
|
|
48
|
+
const runId = randomUUID();
|
|
49
|
+
const started = new Date().toISOString();
|
|
50
|
+
const { yml, companies } = readPortalsYml();
|
|
51
|
+
const passTitle = buildTitleFilter(yml, filters);
|
|
52
|
+
const passLoc = buildLocationFilter(yml, filters);
|
|
53
|
+
const allowSources = new Set((filters.sources ?? []).map(s => s.toLowerCase()));
|
|
54
|
+
const allowCompanies = (filters.companies ?? []).map(s => s.toLowerCase());
|
|
55
|
+
const ctx = makeProviderCtx();
|
|
56
|
+
const errors = [];
|
|
57
|
+
const targets = [];
|
|
58
|
+
for (const entry of companies) {
|
|
59
|
+
if (allowCompanies.length && !allowCompanies.some(k => entry.name.toLowerCase().includes(k)))
|
|
60
|
+
continue;
|
|
61
|
+
const provider = resolveProvider(entry);
|
|
62
|
+
if (!provider)
|
|
63
|
+
continue;
|
|
64
|
+
if (allowSources.size && !allowSources.has(provider.id))
|
|
65
|
+
continue;
|
|
66
|
+
targets.push({ entry, provider });
|
|
67
|
+
}
|
|
68
|
+
const sourcesUsed = new Set();
|
|
69
|
+
const topNew = [];
|
|
70
|
+
let jobsFound = 0, jobsNew = 0, jobsDupes = 0;
|
|
71
|
+
const polledNames = [];
|
|
72
|
+
// Fetch in parallel across companies with bounded concurrency. The upserts still
|
|
73
|
+
// serialize through runInWriteLock so SQLite stays happy. Career-ops uses 10; 8 is
|
|
74
|
+
// a safer default given Workday/Google providers are heavier.
|
|
75
|
+
const CONCURRENCY = 8;
|
|
76
|
+
let cursor = 0;
|
|
77
|
+
const workers = Array.from({ length: Math.min(CONCURRENCY, targets.length) }, async () => {
|
|
78
|
+
while (cursor < targets.length) {
|
|
79
|
+
const idx = cursor++;
|
|
80
|
+
const { entry, provider } = targets[idx];
|
|
81
|
+
if (!provider)
|
|
82
|
+
continue;
|
|
83
|
+
sourcesUsed.add(provider.id);
|
|
84
|
+
let raws = [];
|
|
85
|
+
try {
|
|
86
|
+
raws = await provider.fetch(entry, ctx);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
errors.push({ company: entry.name, error: `${provider.id}: ${err?.message ?? String(err)}` });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
for (const j of raws) {
|
|
93
|
+
jobsFound++;
|
|
94
|
+
if (!passTitle(j.title))
|
|
95
|
+
continue;
|
|
96
|
+
if (!passLoc(j.location))
|
|
97
|
+
continue;
|
|
98
|
+
try {
|
|
99
|
+
const up = await upsertJob({
|
|
100
|
+
source: provider.id,
|
|
101
|
+
source_url: j.url,
|
|
102
|
+
company_name: j.company || entry.name,
|
|
103
|
+
title: j.title,
|
|
104
|
+
location: j.location,
|
|
105
|
+
});
|
|
106
|
+
if (up.created) {
|
|
107
|
+
jobsNew++;
|
|
108
|
+
if (topNew.length < 25)
|
|
109
|
+
topNew.push({ company: j.company, title: j.title, url: j.url, job_id: up.id });
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
jobsDupes++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
errors.push({ company: entry.name, error: `upsert: ${err?.message ?? String(err)}` });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
polledNames.push(entry.name);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
await Promise.all(workers);
|
|
123
|
+
// Stamp last_polled_at for all scanned companies in one transaction.
|
|
124
|
+
await stampLastPolledBatch(polledNames);
|
|
125
|
+
const finished = new Date().toISOString();
|
|
126
|
+
await runInWriteLock(() => {
|
|
127
|
+
getDb().prepare(`
|
|
128
|
+
INSERT INTO scan_runs
|
|
129
|
+
(id, started_at, finished_at, sources, companies_n, jobs_found, jobs_new, jobs_dupes, errors_json, triggered_by)
|
|
130
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
131
|
+
`).run(runId, started, finished, JSON.stringify([...sourcesUsed]), targets.length, jobsFound, jobsNew, jobsDupes, errors.length ? JSON.stringify(errors) : null, opts.triggeredBy ?? 'manual');
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
run_id: runId, triggered_by: opts.triggeredBy ?? 'manual',
|
|
135
|
+
started_at: started, finished_at: finished,
|
|
136
|
+
sources: [...sourcesUsed],
|
|
137
|
+
companies_scanned: targets.length,
|
|
138
|
+
jobs_found: jobsFound, jobs_new: jobsNew, jobs_dupes: jobsDupes,
|
|
139
|
+
top_new: topNew, errors,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
async function stampLastPolledBatch(names) {
|
|
143
|
+
if (!names.length)
|
|
144
|
+
return;
|
|
145
|
+
return runInWriteLock(() => {
|
|
146
|
+
const db = getDb();
|
|
147
|
+
const stmt = db.prepare(`
|
|
148
|
+
UPDATE target_companies SET last_polled_at = CURRENT_TIMESTAMP
|
|
149
|
+
WHERE company_id IN (SELECT id FROM companies WHERE name_normalized = ?)
|
|
150
|
+
`);
|
|
151
|
+
const tx = db.transaction((batch) => {
|
|
152
|
+
for (const n of batch)
|
|
153
|
+
stmt.run(n.toLowerCase().trim());
|
|
154
|
+
});
|
|
155
|
+
tx(names);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
export async function shutdownScanResources() {
|
|
159
|
+
await closeProviderBrowser();
|
|
160
|
+
}
|
|
161
|
+
export function knownProviderIds() {
|
|
162
|
+
return [...PROVIDERS.keys()];
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=scan_engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan_engine.js","sourceRoot":"","sources":["../../src/core/scan_engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Opt-in scheduler. Lives entirely inside this process — no external cron — but stays
|
|
2
|
+
// OFF by default. State persists in `scheduler_state.enabled_jobs` (JSON array of names).
|
|
3
|
+
//
|
|
4
|
+
// Supported jobs:
|
|
5
|
+
// - scan_portals_4h → runScan() every 4h
|
|
6
|
+
// - batch_evaluate_30m → batch_evaluate (limit 20) every 30m, only when an LLM is configured
|
|
7
|
+
// - followups_due_1h → no-op; just stamps activity (the chat reads v_followups_due directly)
|
|
8
|
+
// - daily_digest_morning → stamps digest_state at 08:00 local
|
|
9
|
+
//
|
|
10
|
+
// Concurrency: one tick at a time per job. Misses are not catched up.
|
|
11
|
+
import { getDb, runInWriteLock } from '../db.js';
|
|
12
|
+
import { runScan } from './scan_engine.js';
|
|
13
|
+
import { llmAvailable } from './llm.js';
|
|
14
|
+
export const JOB_DEFS = {
|
|
15
|
+
scan_portals_4h: { intervalMs: 4 * 60 * 60 * 1000, description: 'Run scan_portals across all enabled tracked_companies' },
|
|
16
|
+
batch_evaluate_30m: { intervalMs: 30 * 60 * 1000, description: 'Batch-rate up to 20 unrated jobs via the configured LLM' },
|
|
17
|
+
followups_due_1h: { intervalMs: 1 * 60 * 60 * 1000, description: 'Touch state when followups are due (informational)' },
|
|
18
|
+
daily_digest_morning: { intervalMs: 60 * 60 * 1000, description: 'Once-per-day digest stamp (08:00 local window)' },
|
|
19
|
+
};
|
|
20
|
+
const RUNTIME = new Map();
|
|
21
|
+
export function readEnabledJobs() {
|
|
22
|
+
const row = getDb().prepare(`SELECT enabled_jobs FROM scheduler_state WHERE id = 1`).get();
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(row?.enabled_jobs ?? '[]').filter(n => n in JOB_DEFS);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function setEnabledJobs(jobs) {
|
|
31
|
+
const filtered = [...new Set(jobs)].filter(j => j in JOB_DEFS);
|
|
32
|
+
await runInWriteLock(() => {
|
|
33
|
+
getDb().prepare(`UPDATE scheduler_state SET enabled_jobs = ? WHERE id = 1`).run(JSON.stringify(filtered));
|
|
34
|
+
});
|
|
35
|
+
applyState();
|
|
36
|
+
return filtered;
|
|
37
|
+
}
|
|
38
|
+
export function disableAll() {
|
|
39
|
+
void runInWriteLock(() => {
|
|
40
|
+
getDb().prepare(`UPDATE scheduler_state SET enabled_jobs = '[]' WHERE id = 1`).run();
|
|
41
|
+
});
|
|
42
|
+
applyState();
|
|
43
|
+
}
|
|
44
|
+
// Called once at boot (and after enable/disable) — adds new timers, removes stale ones.
|
|
45
|
+
export function applyState() {
|
|
46
|
+
const enabled = new Set(readEnabledJobs());
|
|
47
|
+
for (const [name, rt] of RUNTIME) {
|
|
48
|
+
if (!enabled.has(name)) {
|
|
49
|
+
clearInterval(rt.timer);
|
|
50
|
+
RUNTIME.delete(name);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const name of enabled) {
|
|
54
|
+
if (RUNTIME.has(name))
|
|
55
|
+
continue;
|
|
56
|
+
const def = JOB_DEFS[name];
|
|
57
|
+
const timer = setInterval(() => { void tick(name); }, def.intervalMs);
|
|
58
|
+
timer.unref(); // never block process exit
|
|
59
|
+
RUNTIME.set(name, { name, timer, running: false });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Job-name → handler dispatch table. Adding a job is now one table entry, not another
|
|
63
|
+
// branch in `tick`. Lazy imports keep us free of circular module deps.
|
|
64
|
+
const JOB_HANDLERS = {
|
|
65
|
+
scan_portals_4h: () => runScan({}, { triggeredBy: 'scheduler:scan_portals_4h' }).then(() => undefined),
|
|
66
|
+
batch_evaluate_30m: async () => {
|
|
67
|
+
if (!llmAvailable())
|
|
68
|
+
return;
|
|
69
|
+
const { batchEvaluateTool } = await import('../mcp/tools/batch_evaluate.js');
|
|
70
|
+
await batchEvaluateTool.handler({ limit: 20, concurrency: 2 });
|
|
71
|
+
},
|
|
72
|
+
daily_digest_morning: async () => {
|
|
73
|
+
const hour = new Date().getHours();
|
|
74
|
+
if (hour < 7 || hour > 9)
|
|
75
|
+
return;
|
|
76
|
+
const { dailyDigestTool } = await import('../mcp/tools/ops.js');
|
|
77
|
+
await dailyDigestTool.handler({ dry_run: false, min_score: 75 });
|
|
78
|
+
},
|
|
79
|
+
followups_due_1h: async () => {
|
|
80
|
+
// Slot exists for future hooks (e.g. desktop notif). No work today.
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
async function tick(name) {
|
|
84
|
+
const rt = RUNTIME.get(name);
|
|
85
|
+
if (!rt || rt.running)
|
|
86
|
+
return;
|
|
87
|
+
rt.running = true;
|
|
88
|
+
try {
|
|
89
|
+
await JOB_HANDLERS[name]();
|
|
90
|
+
await runInWriteLock(() => {
|
|
91
|
+
getDb().prepare(`UPDATE scheduler_state SET last_run_at = CURRENT_TIMESTAMP, notes = ? WHERE id = 1`).run(`last fired: ${name}`);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
// Surface the error rather than silently swallowing — easier to spot when a
|
|
96
|
+
// scheduled run starts hitting an API rate limit or a broken provider.
|
|
97
|
+
// eslint-disable-next-line no-console
|
|
98
|
+
console.error(`[scheduler] ${name} tick failed:`, e?.message ?? e);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
rt.running = false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function status() {
|
|
105
|
+
const enabled = readEnabledJobs();
|
|
106
|
+
const last = getDb().prepare(`SELECT last_run_at, notes FROM scheduler_state WHERE id = 1`).get();
|
|
107
|
+
return {
|
|
108
|
+
enabled_jobs: enabled,
|
|
109
|
+
runtime: [...RUNTIME.keys()],
|
|
110
|
+
last_run_at: last?.last_run_at ?? null,
|
|
111
|
+
notes: last?.notes ?? null,
|
|
112
|
+
available_jobs: Object.entries(JOB_DEFS).map(([name, def]) => ({
|
|
113
|
+
name, interval_ms: def.intervalMs, description: def.description,
|
|
114
|
+
})),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../src/core/scheduler.ts"],"names":[],"mappings":"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"}
|