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
package/dist/core/llm.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Pluggable LLM module.
|
|
2
|
+
//
|
|
3
|
+
// Provider picked by env (`MCP_JSA_LLM_PROVIDER`):
|
|
4
|
+
// - gemini → Google AI Studio free tier (default). `GEMINI_API_KEY` required.
|
|
5
|
+
// - deepseek → DeepSeek chat API (OpenAI-compatible). `DEEPSEEK_API_KEY` required.
|
|
6
|
+
// - none → throws on call. Default behaviour when neither key is set.
|
|
7
|
+
//
|
|
8
|
+
// Every call records a row in `llm_calls` (telemetry → cost_estimate()) and runs through
|
|
9
|
+
// `parseJsonStrict` when `responseFormat: 'json_object'` is requested. Strict-JSON parse
|
|
10
|
+
// failures are visible (parse_ok = 0, parse_error populated) — never silent zeros.
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
import { config } from '../config.js';
|
|
13
|
+
import { getDb } from '../db.js';
|
|
14
|
+
// ── Provider selection ───────────────────────────────────────────────────────
|
|
15
|
+
let _cached = null;
|
|
16
|
+
export function getLLM() {
|
|
17
|
+
if (_cached)
|
|
18
|
+
return _cached;
|
|
19
|
+
const p = config.llmProvider?.toLowerCase();
|
|
20
|
+
if (p === 'gemini' && process.env.GEMINI_API_KEY)
|
|
21
|
+
_cached = new GeminiProvider();
|
|
22
|
+
else if (p === 'deepseek' && process.env.DEEPSEEK_API_KEY)
|
|
23
|
+
_cached = new DeepSeekProvider();
|
|
24
|
+
else
|
|
25
|
+
_cached = new NoneProvider();
|
|
26
|
+
return _cached;
|
|
27
|
+
}
|
|
28
|
+
export function resetLLMCache() { _cached = null; }
|
|
29
|
+
export function llmAvailable() { return getLLM().name !== 'none'; }
|
|
30
|
+
// ── Helper: strict JSON parse with PARSE_ERROR fallback ──────────────────────
|
|
31
|
+
export function parseJsonStrict(text) {
|
|
32
|
+
try {
|
|
33
|
+
// Strip leading/trailing code fences and BOM/whitespace, plus any leading prose
|
|
34
|
+
// before the first '{' — LLMs sometimes preface JSON with "Sure, here's...".
|
|
35
|
+
let cleaned = text.replace(/^/, '').trim();
|
|
36
|
+
cleaned = cleaned.replace(/^```(?:json|JSON)?\s*/, '').replace(/\s*```\s*$/, '').trim();
|
|
37
|
+
const firstBrace = cleaned.indexOf('{');
|
|
38
|
+
if (firstBrace > 0)
|
|
39
|
+
cleaned = cleaned.slice(firstBrace);
|
|
40
|
+
return { ok: true, data: JSON.parse(cleaned) };
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
return { ok: false, error: e?.message ?? 'JSON parse failed', raw: text.slice(0, 800) };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Lenient JSON parser — returns `fallback` on failure. Use this for opportunistic reads
|
|
47
|
+
// (e.g. score_detail history) where missing structure is normal. For LLM contracts use
|
|
48
|
+
// parseJsonStrict so failures stay visible.
|
|
49
|
+
export function safeJson(text, fallback) {
|
|
50
|
+
if (!text)
|
|
51
|
+
return fallback;
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(text);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return fallback;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function chatLogged(tool, messages, opts = {}) {
|
|
60
|
+
const provider = getLLM();
|
|
61
|
+
const t0 = Date.now();
|
|
62
|
+
const inputChars = messages.reduce((n, m) => n + m.content.length, 0);
|
|
63
|
+
let result;
|
|
64
|
+
let errored = false;
|
|
65
|
+
try {
|
|
66
|
+
result = await provider.chat(messages, opts);
|
|
67
|
+
// Strict-JSON parsing lives here (not in each provider) so adding a provider stays
|
|
68
|
+
// 1 file. Providers return raw text + parseOk=true; we apply the contract.
|
|
69
|
+
if (opts.responseFormat === 'json_object' && !('parsed' in result)) {
|
|
70
|
+
const p = parseJsonStrict(result.text);
|
|
71
|
+
if (p.ok)
|
|
72
|
+
result.parsed = p.data;
|
|
73
|
+
else {
|
|
74
|
+
result.parseOk = false;
|
|
75
|
+
result.parseError = p.error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
errored = true;
|
|
81
|
+
result = {
|
|
82
|
+
provider: provider.name,
|
|
83
|
+
model: opts.model ?? provider.defaultModel,
|
|
84
|
+
text: '',
|
|
85
|
+
parseOk: false,
|
|
86
|
+
parseError: `provider_error: ${e?.message ?? String(e)}`,
|
|
87
|
+
durationMs: Date.now() - t0,
|
|
88
|
+
inputChars,
|
|
89
|
+
outputChars: 0,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const id = randomUUID();
|
|
93
|
+
try {
|
|
94
|
+
getDb().prepare(`
|
|
95
|
+
INSERT INTO llm_calls
|
|
96
|
+
(id, tool, provider, model, job_id, input_chars, output_chars,
|
|
97
|
+
parse_ok, parse_error, duration_ms)
|
|
98
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
99
|
+
`).run(id, tool, result.provider, result.model, opts.jobId ?? null, result.inputChars, result.outputChars, result.parseOk ? 1 : 0, result.parseError ?? null, result.durationMs);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Telemetry must never break the caller.
|
|
103
|
+
}
|
|
104
|
+
if (errored)
|
|
105
|
+
throw new Error(result.parseError);
|
|
106
|
+
return { ...result, id };
|
|
107
|
+
}
|
|
108
|
+
// ── Providers ────────────────────────────────────────────────────────────────
|
|
109
|
+
class NoneProvider {
|
|
110
|
+
name = 'none';
|
|
111
|
+
defaultModel = 'none';
|
|
112
|
+
async chat() {
|
|
113
|
+
throw new Error('No LLM provider configured. Set MCP_JSA_LLM_PROVIDER=gemini (with GEMINI_API_KEY) ' +
|
|
114
|
+
'or =deepseek (with DEEPSEEK_API_KEY). The default chat-mode tools do not need this — ' +
|
|
115
|
+
'only api/batch paths do.');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
class GeminiProvider {
|
|
119
|
+
name = 'gemini';
|
|
120
|
+
defaultModel = config.llmModel || 'gemini-2.5-flash';
|
|
121
|
+
async chat(messages, opts = {}) {
|
|
122
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
123
|
+
if (!apiKey)
|
|
124
|
+
throw new Error('GEMINI_API_KEY missing');
|
|
125
|
+
const model = opts.model ?? this.defaultModel;
|
|
126
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(apiKey)}`;
|
|
127
|
+
// Map our role/content shape to Gemini's. System → systemInstruction; user/assistant → contents.
|
|
128
|
+
const systems = messages.filter(m => m.role === 'system').map(m => m.content).join('\n\n');
|
|
129
|
+
const contents = messages
|
|
130
|
+
.filter(m => m.role !== 'system')
|
|
131
|
+
.map(m => ({ role: m.role === 'assistant' ? 'model' : 'user',
|
|
132
|
+
parts: [{ text: m.content }] }));
|
|
133
|
+
const body = {
|
|
134
|
+
contents,
|
|
135
|
+
generationConfig: {
|
|
136
|
+
temperature: opts.temperature ?? 0.2,
|
|
137
|
+
maxOutputTokens: opts.maxTokens ?? 4096,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
if (systems)
|
|
141
|
+
body.systemInstruction = { parts: [{ text: systems }] };
|
|
142
|
+
if (opts.responseFormat === 'json_object') {
|
|
143
|
+
body.generationConfig.responseMimeType = 'application/json';
|
|
144
|
+
}
|
|
145
|
+
const inputChars = messages.reduce((n, m) => n + m.content.length, 0);
|
|
146
|
+
const t0 = Date.now();
|
|
147
|
+
const res = await fetch(url, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: { 'content-type': 'application/json' },
|
|
150
|
+
body: JSON.stringify(body),
|
|
151
|
+
});
|
|
152
|
+
const json = await res.json().catch(() => ({}));
|
|
153
|
+
if (!res.ok) {
|
|
154
|
+
throw new Error(`gemini HTTP ${res.status}: ${truncate(JSON.stringify(json), 400)}`);
|
|
155
|
+
}
|
|
156
|
+
const text = json?.candidates?.[0]?.content?.parts
|
|
157
|
+
?.map((p) => p?.text ?? '').join('') ?? '';
|
|
158
|
+
return {
|
|
159
|
+
provider: this.name,
|
|
160
|
+
model,
|
|
161
|
+
text,
|
|
162
|
+
parseOk: true,
|
|
163
|
+
durationMs: Date.now() - t0,
|
|
164
|
+
inputChars,
|
|
165
|
+
outputChars: text.length,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
class DeepSeekProvider {
|
|
170
|
+
name = 'deepseek';
|
|
171
|
+
defaultModel = config.llmModel || 'deepseek-chat';
|
|
172
|
+
async chat(messages, opts = {}) {
|
|
173
|
+
const apiKey = process.env.DEEPSEEK_API_KEY;
|
|
174
|
+
if (!apiKey)
|
|
175
|
+
throw new Error('DEEPSEEK_API_KEY missing');
|
|
176
|
+
const model = opts.model ?? this.defaultModel;
|
|
177
|
+
const body = {
|
|
178
|
+
model,
|
|
179
|
+
messages: messages.map(m => ({ role: m.role, content: m.content })),
|
|
180
|
+
temperature: opts.temperature ?? 0.2,
|
|
181
|
+
max_tokens: opts.maxTokens ?? 4096,
|
|
182
|
+
};
|
|
183
|
+
if (opts.responseFormat === 'json_object') {
|
|
184
|
+
body.response_format = { type: 'json_object' };
|
|
185
|
+
}
|
|
186
|
+
const inputChars = messages.reduce((n, m) => n + m.content.length, 0);
|
|
187
|
+
const t0 = Date.now();
|
|
188
|
+
const res = await fetch('https://api.deepseek.com/chat/completions', {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: { 'content-type': 'application/json',
|
|
191
|
+
'authorization': `Bearer ${apiKey}` },
|
|
192
|
+
body: JSON.stringify(body),
|
|
193
|
+
});
|
|
194
|
+
const json = await res.json().catch(() => ({}));
|
|
195
|
+
if (!res.ok) {
|
|
196
|
+
throw new Error(`deepseek HTTP ${res.status}: ${truncate(JSON.stringify(json), 400)}`);
|
|
197
|
+
}
|
|
198
|
+
const text = json?.choices?.[0]?.message?.content ?? '';
|
|
199
|
+
return {
|
|
200
|
+
provider: this.name,
|
|
201
|
+
model,
|
|
202
|
+
text,
|
|
203
|
+
parseOk: true,
|
|
204
|
+
durationMs: Date.now() - t0,
|
|
205
|
+
inputChars,
|
|
206
|
+
outputChars: text.length,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function truncate(s, n) {
|
|
211
|
+
return s.length > n ? s.slice(0, n) + '...' : s;
|
|
212
|
+
}
|
|
213
|
+
// ── Cost table — used by cost_estimate() ─────────────────────────────────────
|
|
214
|
+
// Prices in USD per 1M tokens; we approximate tokens as chars/4 (good-enough for a UI estimate).
|
|
215
|
+
// Update as providers change.
|
|
216
|
+
export const COST_TABLE = {
|
|
217
|
+
'gemini-2.5-flash': { input: 0.075, output: 0.30 }, // free tier; cited for paid fallback
|
|
218
|
+
'gemini-2.5-pro': { input: 1.25, output: 5.00 },
|
|
219
|
+
'deepseek-chat': { input: 0.27, output: 1.10 },
|
|
220
|
+
};
|
|
221
|
+
export function estimateCostUsd(model, inputChars, outputChars) {
|
|
222
|
+
const rate = COST_TABLE[model] ?? { input: 0, output: 0 };
|
|
223
|
+
const inputTokens = inputChars / 4;
|
|
224
|
+
const outputTokens = outputChars / 4;
|
|
225
|
+
return (inputTokens / 1e6) * rate.input + (outputTokens / 1e6) * rate.output;
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=llm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm.js","sourceRoot":"","sources":["../../src/core/llm.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Shared loader for the markdown documents in modes/. Cached so chat tools don't
|
|
2
|
+
// re-read from disk on every JSON-RPC call. `fs.watch` invalidates the cache when the
|
|
3
|
+
// user edits modes/* — no restart required to tune the rubric.
|
|
4
|
+
//
|
|
5
|
+
// rubric.md gets a small dynamic prefix when `visaScoringEnabled` is false — that single
|
|
6
|
+
// injection point ensures every consumer of the rubric (chat-mode evaluate_job step 1,
|
|
7
|
+
// api-mode evaluate_job, batch_evaluate, evaluate_training, evaluate_project, the
|
|
8
|
+
// `mcp-jsa://modes/rubric` MCP resource) sees the override identically.
|
|
9
|
+
import { readFileSync, existsSync, watch as fsWatch } from 'node:fs';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
import { config } from '../config.js';
|
|
12
|
+
const cache = new Map();
|
|
13
|
+
let watcher = null;
|
|
14
|
+
function ensureWatcher() {
|
|
15
|
+
if (watcher || !existsSync(config.modesDir))
|
|
16
|
+
return;
|
|
17
|
+
try {
|
|
18
|
+
watcher = fsWatch(config.modesDir, { persistent: false }, () => cache.clear());
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Best-effort — on platforms without fs.watch we just stay cached.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const VISA_DISABLED_PREFIX = `# ⚙️ VISA SCORING DISABLED (server config)
|
|
25
|
+
|
|
26
|
+
The candidate has disabled visa scoring (\`MCP_JSA_VISA_SCORING=false\`). For this run:
|
|
27
|
+
|
|
28
|
+
- **DO NOT** include \`visa_fit\` in the output JSON.
|
|
29
|
+
- Use the renormalized formula:
|
|
30
|
+
\`\`\`
|
|
31
|
+
score_total = round(0.6 * resume_fit + 0.4 * taste_fit)
|
|
32
|
+
\`\`\`
|
|
33
|
+
- The output contract is now: \`resume_fit\`, \`taste_fit\`, \`score_total\`, \`reasoning\`,
|
|
34
|
+
\`concerns\`, \`role_category\`, \`seniority\`. Omit \`visa_fit\`.
|
|
35
|
+
- Ignore the "visa_fit (0–100)" dimension section and the "0.2·visa_fit" term in the
|
|
36
|
+
weighted formula below — they are documentary only.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
`;
|
|
41
|
+
export function getMode(name) {
|
|
42
|
+
ensureWatcher();
|
|
43
|
+
const cached = cache.get(name);
|
|
44
|
+
if (cached !== undefined)
|
|
45
|
+
return cached;
|
|
46
|
+
const path = resolve(config.modesDir, name);
|
|
47
|
+
let body = existsSync(path) ? readFileSync(path, 'utf-8') : `_missing modes/${name}_`;
|
|
48
|
+
if (name === 'rubric.md' && !config.visaScoringEnabled) {
|
|
49
|
+
body = VISA_DISABLED_PREFIX + body;
|
|
50
|
+
}
|
|
51
|
+
cache.set(name, body);
|
|
52
|
+
return body;
|
|
53
|
+
}
|
|
54
|
+
export function invalidateModesCache() { cache.clear(); }
|
|
55
|
+
//# sourceMappingURL=modes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modes.js","sourceRoot":"","sources":["../../src/core/modes.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,sFAAsF;AACtF,+DAA+D;AAC/D,EAAE;AACF,yFAAyF;AACzF,uFAAuF;AACvF,kFAAkF;AAClF,wEAAwE;AAExE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,KAAK,IAAI,OAAO,EAAE,MAAM,SAAS,CAAC;AACrE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;AACxC,IAAI,OAAO,GAAsC,IAAI,CAAC;AAEtD,SAAS,aAAa;IACpB,IAAI,OAAO,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC;QAAE,OAAO;IACpD,IAAI,CAAC;QACH,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;IACjF,CAAC;IAAC,MAAM,CAAC;QACP,mEAAmE;IACrE,CAAC;AACH,CAAC;AAED,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;CAgB5B,CAAC;AAEF,MAAM,UAAU,OAAO,CAAC,IAAY;IAClC,aAAa,EAAE,CAAC;IAChB,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IACxC,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC5C,IAAI,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,kBAAkB,IAAI,GAAG,CAAC;IACtF,IAAI,IAAI,KAAK,WAAW,IAAI,CAAC,MAAM,CAAC,kBAAkB,EAAE,CAAC;QACvD,IAAI,GAAG,oBAAoB,GAAG,IAAI,CAAC;IACrC,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACtB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,oBAAoB,KAAW,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Outreach safety rails. Distilled from modes/outreach_tone.md.
|
|
2
|
+
//
|
|
3
|
+
// We treat these as HARD invariants — any draft (chat- or api-mode) is validated by
|
|
4
|
+
// `validateOutreach()` before persisting. Failures get surfaced to the chat so it can
|
|
5
|
+
// regenerate; we never silently strip-and-save.
|
|
6
|
+
const LIMITS = {
|
|
7
|
+
warm: { maxChars: 600, type: 'warm' },
|
|
8
|
+
founder: { maxChars: 300, type: 'founder' },
|
|
9
|
+
followup: { maxChars: 300, type: 'followup' },
|
|
10
|
+
reply: { maxChars: 800, type: 'reply' },
|
|
11
|
+
generic: { maxChars: 600, type: 'generic' },
|
|
12
|
+
};
|
|
13
|
+
const REFER_PATTERNS = [
|
|
14
|
+
/refer\s+me\b/i,
|
|
15
|
+
/can you (?:put|forward|pass)\s+my\s+(?:resume|cv)/i,
|
|
16
|
+
/could you (?:put|forward|pass)\s+my\s+(?:resume|cv)/i,
|
|
17
|
+
/put\s+(?:in\s+)?a\s+good\s+word\b/i,
|
|
18
|
+
/\bget\s+me\s+(?:an?\s+)?(?:interview|referral)\b/i,
|
|
19
|
+
];
|
|
20
|
+
const VISA_PATTERNS = [
|
|
21
|
+
/\bH[\s-]?1B\b/i,
|
|
22
|
+
/\bOPT\b/,
|
|
23
|
+
/\bSTEM\s*OPT\b/i,
|
|
24
|
+
/\bEAD\b/,
|
|
25
|
+
/\bvisa\b/i,
|
|
26
|
+
/\bwork\s+authorization\b/i,
|
|
27
|
+
/\bwork\s+permit\b/i,
|
|
28
|
+
/\bsponsor(?:ship|s)?\b/i,
|
|
29
|
+
/\bgreen\s+card\b/i,
|
|
30
|
+
/\bcitizen(?:ship)?\b/i,
|
|
31
|
+
];
|
|
32
|
+
const CLICHE_FORBIDDEN = [
|
|
33
|
+
/\bI hope this (?:finds you well|email finds you well)\b/i,
|
|
34
|
+
/\bI'?d love to pick your brain\b/i,
|
|
35
|
+
];
|
|
36
|
+
export function getOutreachLimits(type) {
|
|
37
|
+
return LIMITS[type] ?? LIMITS.generic;
|
|
38
|
+
}
|
|
39
|
+
// All forbidden-pattern groups in one table so adding a new rail is one entry.
|
|
40
|
+
const RAIL_GROUPS = [
|
|
41
|
+
{ rule: 'no_visa_mentions', patterns: VISA_PATTERNS }, // loudest rail first
|
|
42
|
+
{ rule: 'no_refer_me', patterns: REFER_PATTERNS },
|
|
43
|
+
{ rule: 'no_cliches', patterns: CLICHE_FORBIDDEN },
|
|
44
|
+
];
|
|
45
|
+
export function validateOutreach(message, type) {
|
|
46
|
+
const limits = LIMITS[type] ?? LIMITS.generic;
|
|
47
|
+
const issues = [];
|
|
48
|
+
const len = (message ?? '').length;
|
|
49
|
+
if (len === 0)
|
|
50
|
+
issues.push({ rule: 'non_empty', hit: 'message is empty' });
|
|
51
|
+
if (len > limits.maxChars)
|
|
52
|
+
issues.push({ rule: 'char_cap', hit: `${len} chars > ${limits.maxChars} cap` });
|
|
53
|
+
for (const group of RAIL_GROUPS) {
|
|
54
|
+
for (const re of group.patterns) {
|
|
55
|
+
const m = message.match(re);
|
|
56
|
+
if (m)
|
|
57
|
+
issues.push({ rule: group.rule, hit: m[0] });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (/!/.test(message))
|
|
61
|
+
issues.push({ rule: 'no_exclamation_marks', hit: '!' });
|
|
62
|
+
if (/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/u.test(message)) {
|
|
63
|
+
issues.push({ rule: 'no_emojis', hit: 'emoji codepoint present' });
|
|
64
|
+
}
|
|
65
|
+
return { ok: issues.length === 0, message_len: len, limit: limits.maxChars, type: limits.type, issues };
|
|
66
|
+
}
|
|
67
|
+
// For PDFs / reports / cover letters — same visa rail.
|
|
68
|
+
export function scanForVisaLeakage(text) {
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const re of VISA_PATTERNS) {
|
|
71
|
+
const m = text.match(re);
|
|
72
|
+
if (m)
|
|
73
|
+
out.push({ rule: 'no_visa_mentions', hit: m[0] });
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=outreach_safety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outreach_safety.js","sourceRoot":"","sources":["../../src/core/outreach_safety.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Loads cv.md + config/profile.yml + portals.yml from the configured project root.
|
|
2
|
+
// Seeds career_packet table with an active row on first run.
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import yaml from 'js-yaml';
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { config } from '../config.js';
|
|
9
|
+
import { getDb } from '../db.js';
|
|
10
|
+
// ── Loaders ──────────────────────────────────────────────────────────────────
|
|
11
|
+
export function pathInProject(...parts) {
|
|
12
|
+
return resolve(config.projectRoot, ...parts);
|
|
13
|
+
}
|
|
14
|
+
export function readIfExists(p) {
|
|
15
|
+
return existsSync(p) ? readFileSync(p, 'utf-8') : null;
|
|
16
|
+
}
|
|
17
|
+
export function loadProjectFiles() {
|
|
18
|
+
const cvMd = readIfExists(pathInProject('cv.md'));
|
|
19
|
+
const profileRaw = readIfExists(pathInProject('config', 'profile.yml'));
|
|
20
|
+
const portalsYml = readIfExists(pathInProject('portals.yml'));
|
|
21
|
+
let profile = null;
|
|
22
|
+
if (profileRaw) {
|
|
23
|
+
const parsed = yaml.load(profileRaw);
|
|
24
|
+
if (parsed && typeof parsed === 'object') {
|
|
25
|
+
profile = parsed;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { cvMd, profile, portalsYml };
|
|
29
|
+
}
|
|
30
|
+
// ── Career packet seeding ─────────────────────────────────────────────────────
|
|
31
|
+
/**
|
|
32
|
+
* On first startup, ensure exactly one `is_active = 1` row exists in `career_packet`.
|
|
33
|
+
* If none exists, seed it from `modes/career_packet.md` (the markdown source of truth)
|
|
34
|
+
* combined with the candidate identity block from `config/profile.yml`. The cv.md hash
|
|
35
|
+
* is stored so we can detect drift later (`cv-sync-check` equivalent in milestone 2).
|
|
36
|
+
*/
|
|
37
|
+
export function ensureActiveCareerPacket() {
|
|
38
|
+
const db = getDb();
|
|
39
|
+
const existing = db
|
|
40
|
+
.prepare('SELECT version FROM career_packet WHERE is_active = 1')
|
|
41
|
+
.get();
|
|
42
|
+
if (existing)
|
|
43
|
+
return { version: existing.version, created: false };
|
|
44
|
+
const packetTemplatePath = resolve(config.modesDir, 'career_packet.md');
|
|
45
|
+
const packetBody = readIfExists(packetTemplatePath) ?? '# Career Packet (empty)';
|
|
46
|
+
const { cvMd, profile } = loadProjectFiles();
|
|
47
|
+
const identityBlock = renderIdentityBlock(profile);
|
|
48
|
+
const content = packetBody.replace(/## 1\. Identity[\s\S]*?(?=^## )/m, `## 1. Identity\n\n${identityBlock}\n\n`);
|
|
49
|
+
const sourceHash = cvMd ? sha256(cvMd) : null;
|
|
50
|
+
db.prepare(`
|
|
51
|
+
INSERT INTO career_packet (id, version, content, taglines, is_active, source_cv_hash, notes)
|
|
52
|
+
VALUES (?, 1, ?, NULL, 1, ?, 'seeded on first run')
|
|
53
|
+
`).run(randomUUID(), content, sourceHash);
|
|
54
|
+
return { version: 1, created: true };
|
|
55
|
+
}
|
|
56
|
+
export function getActiveCareerPacket() {
|
|
57
|
+
const row = getDb()
|
|
58
|
+
.prepare(`SELECT id, version, content, source_cv_hash FROM career_packet WHERE is_active = 1`)
|
|
59
|
+
.get();
|
|
60
|
+
return row ?? null;
|
|
61
|
+
}
|
|
62
|
+
function renderIdentityBlock(profile) {
|
|
63
|
+
if (!profile?.candidate)
|
|
64
|
+
return '_No `config/profile.yml` found — populate it to enrich._';
|
|
65
|
+
const c = profile.candidate;
|
|
66
|
+
const lines = [];
|
|
67
|
+
if (c.full_name)
|
|
68
|
+
lines.push(`- **Name:** ${c.full_name}`);
|
|
69
|
+
if (c.email)
|
|
70
|
+
lines.push(`- **Email:** ${c.email}`);
|
|
71
|
+
if (c.phone)
|
|
72
|
+
lines.push(`- **Phone:** ${c.phone}`);
|
|
73
|
+
if (c.location)
|
|
74
|
+
lines.push(`- **Location:** ${c.location}`);
|
|
75
|
+
if (c.linkedin)
|
|
76
|
+
lines.push(`- **LinkedIn:** ${c.linkedin}`);
|
|
77
|
+
if (c.portfolio_url)
|
|
78
|
+
lines.push(`- **Portfolio:** ${c.portfolio_url}`);
|
|
79
|
+
if (c.github)
|
|
80
|
+
lines.push(`- **GitHub:** ${c.github}`);
|
|
81
|
+
if (c.twitter)
|
|
82
|
+
lines.push(`- **Twitter:** ${c.twitter}`);
|
|
83
|
+
return lines.length ? lines.join('\n') : '_Profile present but no candidate fields filled._';
|
|
84
|
+
}
|
|
85
|
+
function sha256(s) {
|
|
86
|
+
return createHash('sha256').update(s, 'utf-8').digest('hex');
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=profile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile.js","sourceRoot":"","sources":["../../src/core/profile.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const amazon = {
|
|
2
|
+
id: 'amazon',
|
|
3
|
+
detect(entry) {
|
|
4
|
+
if ((entry.provider ?? '') === 'amazon')
|
|
5
|
+
return { url: 'https://www.amazon.jobs/en/search.json' };
|
|
6
|
+
const careers = entry.careers_url || '';
|
|
7
|
+
return careers.includes('amazon.jobs') ? { url: 'https://www.amazon.jobs/en/search.json' } : null;
|
|
8
|
+
},
|
|
9
|
+
async fetch(entry, ctx) {
|
|
10
|
+
const e = entry;
|
|
11
|
+
const params = new URLSearchParams({
|
|
12
|
+
'normalized_country_code[]': 'USA',
|
|
13
|
+
'radius': '24km',
|
|
14
|
+
'industry_experience': 'less_than_1_year',
|
|
15
|
+
'sort': 'recent',
|
|
16
|
+
'result_limit': String(e.amazon_limit ?? 50),
|
|
17
|
+
'offset': '0',
|
|
18
|
+
'business_category[]': '',
|
|
19
|
+
});
|
|
20
|
+
if (e.amazon_base_query)
|
|
21
|
+
params.set('base_query', e.amazon_base_query);
|
|
22
|
+
const url = `https://www.amazon.jobs/en/search.json?${params.toString()}`;
|
|
23
|
+
const json = await ctx.fetchJson(url, { headers: { 'accept': 'application/json' } });
|
|
24
|
+
const jobs = Array.isArray(json?.jobs) ? json.jobs : [];
|
|
25
|
+
return jobs.map((j) => ({
|
|
26
|
+
title: j.title || '',
|
|
27
|
+
url: j.url_next_step
|
|
28
|
+
? `https://www.amazon.jobs${j.url_next_step}`
|
|
29
|
+
: (j.job_path ? `https://www.amazon.jobs${j.job_path}` : ''),
|
|
30
|
+
company: entry.name || 'Amazon',
|
|
31
|
+
location: j.normalized_location || j.location || '',
|
|
32
|
+
})).filter((j) => j.url);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
export default amazon;
|
|
36
|
+
//# sourceMappingURL=amazon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"amazon.js","sourceRoot":"","sources":["../../../src/core/providers/amazon.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,GAAa;IACvB,EAAE,EAAE,QAAQ;IACZ,MAAM,CAAC,KAAK;QACV,IAAI,CAAC,KAAK,CAAC,QAAQ,IAAI,EAAE,CAAC,KAAK,QAAQ;YAAE,OAAO,EAAE,GAAG,EAAE,wCAAwC,EAAE,CAAC;QAClG,MAAM,OAAO,GAAG,KAAK,CAAC,WAAW,IAAI,EAAE,CAAC;QACxC,OAAO,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,wCAAwC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACpG,CAAC;IACD,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG;QACpB,MAAM,CAAC,GAAG,KAAoB,CAAC;QAC/B,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;YACjC,2BAA2B,EAAE,KAAK;YAClC,QAAQ,EAAQ,MAAM;YACtB,qBAAqB,EAAE,kBAAkB;YACzC,MAAM,EAAU,QAAQ;YACxB,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC,YAAY,IAAI,EAAE,CAAC;YAC5C,QAAQ,EAAQ,GAAG;YACnB,qBAAqB,EAAE,EAAE;SAC1B,CAAC,CAAC;QACH,IAAI,CAAC,CAAC,iBAAiB;YAAE,MAAM,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC,iBAAiB,CAAC,CAAC;QACvE,MAAM,GAAG,GAAG,0CAA0C,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;QAC1E,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,kBAAkB,EAAE,EAAE,CAAC,CAAC;QACrF,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACxD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC;YAC3B,KAAK,EAAK,CAAC,CAAC,KAAK,IAAI,EAAE;YACvB,GAAG,EAAO,CAAC,CAAC,aAAa;gBACd,CAAC,CAAC,0BAA0B,CAAC,CAAC,aAAa,EAAE;gBAC7C,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACvE,OAAO,EAAG,KAAK,CAAC,IAAI,IAAI,QAAQ;YAChC,QAAQ,EAAE,CAAC,CAAC,mBAAmB,IAAI,CAAC,CAAC,QAAQ,IAAI,EAAE;SACpD,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;CACF,CAAC;AACF,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
function resolveApi(entry) {
|
|
2
|
+
if (entry.ashby_slug)
|
|
3
|
+
return `https://api.ashbyhq.com/posting-api/job-board/${entry.ashby_slug}?includeCompensation=true`;
|
|
4
|
+
const url = entry.careers_url || '';
|
|
5
|
+
const m = url.match(/jobs\.ashbyhq\.com\/([^/?#]+)/);
|
|
6
|
+
if (!m)
|
|
7
|
+
return null;
|
|
8
|
+
return `https://api.ashbyhq.com/posting-api/job-board/${m[1]}?includeCompensation=true`;
|
|
9
|
+
}
|
|
10
|
+
const ashby = {
|
|
11
|
+
id: 'ashby',
|
|
12
|
+
detect(entry) {
|
|
13
|
+
const url = resolveApi(entry);
|
|
14
|
+
return url ? { url } : null;
|
|
15
|
+
},
|
|
16
|
+
async fetch(entry, ctx) {
|
|
17
|
+
const url = resolveApi(entry);
|
|
18
|
+
if (!url)
|
|
19
|
+
throw new Error(`ashby: cannot derive API URL for ${entry.name}`);
|
|
20
|
+
const json = await ctx.fetchJson(url);
|
|
21
|
+
const jobs = Array.isArray(json?.jobs) ? json.jobs : [];
|
|
22
|
+
return jobs.map((j) => ({
|
|
23
|
+
title: j.title || '',
|
|
24
|
+
url: j.jobUrl || '',
|
|
25
|
+
company: entry.name,
|
|
26
|
+
location: j.location || '',
|
|
27
|
+
}));
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
export default ashby;
|
|
31
|
+
//# sourceMappingURL=ashby.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ashby.js","sourceRoot":"","sources":["../../../src/core/providers/ashby.ts"],"names":[],"mappings":"AAGA,SAAS,UAAU,CAAC,KAA0B;IAC5C,IAAI,KAAK,CAAC,UAAU;QAAE,OAAO,iDAAiD,KAAK,CAAC,UAAU,2BAA2B,CAAC;IAC1H,MAAM,GAAG,GAAG,KAAK,CAAC,WAAW,IAAI,EAAE,CAAC;IACpC,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,+BAA+B,CAAC,CAAC;IACrD,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpB,OAAO,iDAAiD,CAAC,CAAC,CAAC,CAAC,2BAA2B,CAAC;AAC1F,CAAC;AAED,MAAM,KAAK,GAAa;IACtB,EAAE,EAAE,OAAO;IACX,MAAM,CAAC,KAAK;QACV,MAAM,GAAG,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;QAC9B,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9B,CAAC;IACD,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG;QACpB,MAAM,GAAG,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;QAC9B,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QAC5E,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QACtC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACxD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC;YAC3B,KAAK,EAAK,CAAC,CAAC,KAAK,IAAI,EAAE;YACvB,GAAG,EAAO,CAAC,CAAC,MAAM,IAAI,EAAE;YACxB,OAAO,EAAG,KAAK,CAAC,IAAI;YACpB,QAAQ,EAAE,CAAC,CAAC,QAAQ,IAAI,EAAE;SAC3B,CAAC,CAAC,CAAC;IACN,CAAC;CACF,CAAC;AACF,eAAe,KAAK,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const google = {
|
|
2
|
+
id: 'google',
|
|
3
|
+
detect(entry) {
|
|
4
|
+
if ((entry.provider ?? '') === 'google')
|
|
5
|
+
return { url: 'https://www.google.com/about/careers/applications/' };
|
|
6
|
+
const careers = entry.careers_url || '';
|
|
7
|
+
return /google\.com\/about\/careers/i.test(careers) ? { url: careers } : null;
|
|
8
|
+
},
|
|
9
|
+
async fetch(entry, ctx) {
|
|
10
|
+
if (!ctx.withBrowser)
|
|
11
|
+
return [];
|
|
12
|
+
const e = entry;
|
|
13
|
+
const q = encodeURIComponent(e.google_query ?? 'product manager');
|
|
14
|
+
const loc = e.google_loc ?? 'United States';
|
|
15
|
+
const url = `https://www.google.com/about/careers/applications/jobs/results/?q=${q}&location=${encodeURIComponent(loc)}`;
|
|
16
|
+
return ctx.withBrowser(async (browser) => {
|
|
17
|
+
const page = await browser.newPage();
|
|
18
|
+
try {
|
|
19
|
+
await page.goto(url, { waitUntil: 'networkidle', timeout: 30_000 });
|
|
20
|
+
// Allow ~2s for hydration; the cards appear under <ul.spHGqe> historically.
|
|
21
|
+
await page.waitForTimeout(2_000);
|
|
22
|
+
const items = await page.$$eval('a[href*="/about/careers/applications/jobs/results/"]', (els) => {
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
return els.map(el => {
|
|
25
|
+
const href = el.href;
|
|
26
|
+
const title = (el.innerText || '').split('\n')[0]?.trim() || '';
|
|
27
|
+
const text = (el.innerText || '');
|
|
28
|
+
const locMatch = text.match(/\b([A-Z][a-zA-Z]+(?:,\s*[A-Z][A-Z])?(?:;[^]+)?)\b/);
|
|
29
|
+
return { title, href, location: locMatch?.[1] ?? '' };
|
|
30
|
+
}).filter(j => j.title && j.href && j.href.includes('/results/') && !seen.has(j.href) && (seen.add(j.href), true));
|
|
31
|
+
});
|
|
32
|
+
return items.map((j) => ({
|
|
33
|
+
title: j.title,
|
|
34
|
+
url: j.href,
|
|
35
|
+
company: entry.name || 'Google',
|
|
36
|
+
location: j.location,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
await page.close();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
export default google;
|
|
46
|
+
//# sourceMappingURL=google.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"google.js","sourceRoot":"","sources":["../../../src/core/providers/google.ts"],"names":[],"mappings":"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"}
|