terminalhire 0.1.1 → 0.2.2
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/README.md +39 -12
- package/dist/bin/jpi-dispatch.js +1038 -35
- package/dist/bin/jpi-jobs.js +34 -1
- package/dist/bin/jpi-learn.js +23 -0
- package/dist/bin/jpi-login.js +23 -0
- package/dist/bin/jpi-profile.js +23 -0
- package/dist/bin/jpi-refresh.js +1897 -0
- package/dist/bin/jpi-save.js +674 -0
- package/dist/bin/jpi-spinner.js +352 -0
- package/dist/bin/jpi-sync.js +837 -0
- package/dist/bin/spinner.js +242 -0
- package/dist/src/profile.js +23 -0
- package/install.js +96 -4
- package/package.json +13 -3
|
@@ -0,0 +1,1897 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ../../packages/core/src/types.ts
|
|
13
|
+
var init_types = __esm({
|
|
14
|
+
"../../packages/core/src/types.ts"() {
|
|
15
|
+
"use strict";
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// ../../packages/core/src/vocabulary.ts
|
|
20
|
+
function normalize(tokens) {
|
|
21
|
+
const result = /* @__PURE__ */ new Set();
|
|
22
|
+
for (const raw of tokens) {
|
|
23
|
+
const lower = raw.toLowerCase().trim();
|
|
24
|
+
if (VOCAB_SET.has(lower)) {
|
|
25
|
+
result.add(lower);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const mapped = SYNONYMS[lower];
|
|
29
|
+
if (mapped && VOCAB_SET.has(mapped)) {
|
|
30
|
+
result.add(mapped);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return Array.from(result);
|
|
34
|
+
}
|
|
35
|
+
var VOCABULARY, SYNONYMS, VOCAB_SET;
|
|
36
|
+
var init_vocabulary = __esm({
|
|
37
|
+
"../../packages/core/src/vocabulary.ts"() {
|
|
38
|
+
"use strict";
|
|
39
|
+
VOCABULARY = [
|
|
40
|
+
// Languages
|
|
41
|
+
"typescript",
|
|
42
|
+
"javascript",
|
|
43
|
+
"python",
|
|
44
|
+
"go",
|
|
45
|
+
"rust",
|
|
46
|
+
"java",
|
|
47
|
+
"ruby",
|
|
48
|
+
"elixir",
|
|
49
|
+
"scala",
|
|
50
|
+
"kotlin",
|
|
51
|
+
"swift",
|
|
52
|
+
"cpp",
|
|
53
|
+
"csharp",
|
|
54
|
+
"php",
|
|
55
|
+
"haskell",
|
|
56
|
+
"clojure",
|
|
57
|
+
"r",
|
|
58
|
+
// Frontend frameworks / libs
|
|
59
|
+
"react",
|
|
60
|
+
"nextjs",
|
|
61
|
+
"vue",
|
|
62
|
+
"nuxt",
|
|
63
|
+
"svelte",
|
|
64
|
+
"angular",
|
|
65
|
+
"solidjs",
|
|
66
|
+
"tailwind",
|
|
67
|
+
"css",
|
|
68
|
+
"html",
|
|
69
|
+
"graphql",
|
|
70
|
+
"trpc",
|
|
71
|
+
// Backend frameworks
|
|
72
|
+
"nodejs",
|
|
73
|
+
"express",
|
|
74
|
+
"fastify",
|
|
75
|
+
"nestjs",
|
|
76
|
+
"django",
|
|
77
|
+
"fastapi",
|
|
78
|
+
"flask",
|
|
79
|
+
"rails",
|
|
80
|
+
"spring",
|
|
81
|
+
"actix",
|
|
82
|
+
"gin",
|
|
83
|
+
"phoenix",
|
|
84
|
+
"laravel",
|
|
85
|
+
"dotnet",
|
|
86
|
+
// Infrastructure & DevOps
|
|
87
|
+
"kubernetes",
|
|
88
|
+
"docker",
|
|
89
|
+
"terraform",
|
|
90
|
+
"aws",
|
|
91
|
+
"gcp",
|
|
92
|
+
"azure",
|
|
93
|
+
"ci-cd",
|
|
94
|
+
"github-actions",
|
|
95
|
+
"linux",
|
|
96
|
+
"nginx",
|
|
97
|
+
"pulumi",
|
|
98
|
+
"ansible",
|
|
99
|
+
"prometheus",
|
|
100
|
+
"grafana",
|
|
101
|
+
"datadog",
|
|
102
|
+
"opentelemetry",
|
|
103
|
+
// Data & ML
|
|
104
|
+
"postgresql",
|
|
105
|
+
"mysql",
|
|
106
|
+
"sqlite",
|
|
107
|
+
"mongodb",
|
|
108
|
+
"redis",
|
|
109
|
+
"elasticsearch",
|
|
110
|
+
"kafka",
|
|
111
|
+
"rabbitmq",
|
|
112
|
+
"data-engineering",
|
|
113
|
+
"spark",
|
|
114
|
+
"airflow",
|
|
115
|
+
"dbt",
|
|
116
|
+
"ml",
|
|
117
|
+
"llm",
|
|
118
|
+
"pytorch",
|
|
119
|
+
"tensorflow",
|
|
120
|
+
"pandas",
|
|
121
|
+
"numpy",
|
|
122
|
+
// Domains / capabilities
|
|
123
|
+
"oauth",
|
|
124
|
+
"authentication",
|
|
125
|
+
"security",
|
|
126
|
+
"payments",
|
|
127
|
+
"billing",
|
|
128
|
+
"frontend",
|
|
129
|
+
"backend",
|
|
130
|
+
"devops",
|
|
131
|
+
"mobile",
|
|
132
|
+
"ios",
|
|
133
|
+
"android",
|
|
134
|
+
"api-design",
|
|
135
|
+
"microservices",
|
|
136
|
+
"websockets",
|
|
137
|
+
"testing",
|
|
138
|
+
"accessibility",
|
|
139
|
+
"seo",
|
|
140
|
+
"performance",
|
|
141
|
+
"observability",
|
|
142
|
+
"search",
|
|
143
|
+
"realtime"
|
|
144
|
+
];
|
|
145
|
+
SYNONYMS = {
|
|
146
|
+
// Kubernetes aliases
|
|
147
|
+
"k8s": "kubernetes",
|
|
148
|
+
"kube": "kubernetes",
|
|
149
|
+
// Auth / identity
|
|
150
|
+
"passport": "authentication",
|
|
151
|
+
"oauth2": "oauth",
|
|
152
|
+
"oidc": "oauth",
|
|
153
|
+
"jwt": "authentication",
|
|
154
|
+
"saml": "authentication",
|
|
155
|
+
"auth0": "authentication",
|
|
156
|
+
"clerk": "authentication",
|
|
157
|
+
"nextauth": "authentication",
|
|
158
|
+
// Payments
|
|
159
|
+
"@stripe/stripe-js": "payments",
|
|
160
|
+
"stripe": "payments",
|
|
161
|
+
"braintree": "payments",
|
|
162
|
+
"paddle": "payments",
|
|
163
|
+
"lemonsqueezy": "payments",
|
|
164
|
+
"recurly": "billing",
|
|
165
|
+
"chargebee": "billing",
|
|
166
|
+
// Framework / lib aliases
|
|
167
|
+
"next": "nextjs",
|
|
168
|
+
"next.js": "nextjs",
|
|
169
|
+
"nuxt.js": "nuxt",
|
|
170
|
+
"vue.js": "vue",
|
|
171
|
+
"angular.js": "angular",
|
|
172
|
+
"angularjs": "angular",
|
|
173
|
+
"express.js": "express",
|
|
174
|
+
"expressjs": "express",
|
|
175
|
+
"fastapi": "fastapi",
|
|
176
|
+
"nest": "nestjs",
|
|
177
|
+
"nest.js": "nestjs",
|
|
178
|
+
"sveltekit": "svelte",
|
|
179
|
+
// Language aliases
|
|
180
|
+
"ts": "typescript",
|
|
181
|
+
"js": "javascript",
|
|
182
|
+
"py": "python",
|
|
183
|
+
"golang": "go",
|
|
184
|
+
"c++": "cpp",
|
|
185
|
+
"c#": "csharp",
|
|
186
|
+
".net": "dotnet",
|
|
187
|
+
"asp.net": "dotnet",
|
|
188
|
+
// DB aliases
|
|
189
|
+
"postgres": "postgresql",
|
|
190
|
+
"pg": "postgresql",
|
|
191
|
+
"mongo": "mongodb",
|
|
192
|
+
"elastic": "elasticsearch",
|
|
193
|
+
// Cloud aliases
|
|
194
|
+
"amazon web services": "aws",
|
|
195
|
+
"google cloud": "gcp",
|
|
196
|
+
"google cloud platform": "gcp",
|
|
197
|
+
"microsoft azure": "azure",
|
|
198
|
+
// CI/CD aliases
|
|
199
|
+
"github actions": "github-actions",
|
|
200
|
+
"circle ci": "ci-cd",
|
|
201
|
+
"circleci": "ci-cd",
|
|
202
|
+
"jenkins": "ci-cd",
|
|
203
|
+
"gitlab ci": "ci-cd",
|
|
204
|
+
"travis": "ci-cd",
|
|
205
|
+
// Mobile
|
|
206
|
+
"react native": "mobile",
|
|
207
|
+
"flutter": "mobile",
|
|
208
|
+
"expo": "mobile",
|
|
209
|
+
// AI / ML
|
|
210
|
+
"openai": "llm",
|
|
211
|
+
"anthropic": "llm",
|
|
212
|
+
"langchain": "llm",
|
|
213
|
+
"llamaindex": "llm",
|
|
214
|
+
"hugging face": "ml",
|
|
215
|
+
"huggingface": "ml",
|
|
216
|
+
"scikit-learn": "ml",
|
|
217
|
+
"sklearn": "ml",
|
|
218
|
+
// Data pipeline
|
|
219
|
+
"apache kafka": "kafka",
|
|
220
|
+
"apache spark": "spark",
|
|
221
|
+
"apache airflow": "airflow",
|
|
222
|
+
// Misc
|
|
223
|
+
"tailwindcss": "tailwind",
|
|
224
|
+
"tw": "tailwind",
|
|
225
|
+
"gql": "graphql",
|
|
226
|
+
"ws": "websockets",
|
|
227
|
+
"socket.io": "websockets",
|
|
228
|
+
"jest": "testing",
|
|
229
|
+
"vitest": "testing",
|
|
230
|
+
"playwright": "testing",
|
|
231
|
+
"cypress": "testing"
|
|
232
|
+
};
|
|
233
|
+
VOCAB_SET = new Set(VOCABULARY);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ../../packages/core/src/matcher.ts
|
|
238
|
+
function computeIdf(jobs) {
|
|
239
|
+
const docFreq = /* @__PURE__ */ new Map();
|
|
240
|
+
const N = jobs.length;
|
|
241
|
+
for (const job of jobs) {
|
|
242
|
+
const unique = new Set(job.tags);
|
|
243
|
+
for (const tag of unique) {
|
|
244
|
+
docFreq.set(tag, (docFreq.get(tag) ?? 0) + 1);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const idf = /* @__PURE__ */ new Map();
|
|
248
|
+
for (const [tag, df] of docFreq) {
|
|
249
|
+
idf.set(tag, Math.log((N + 1) / (df + 1)) + 1);
|
|
250
|
+
}
|
|
251
|
+
return idf;
|
|
252
|
+
}
|
|
253
|
+
function inferSeniority(title) {
|
|
254
|
+
for (const [re, level] of SENIORITY_PATTERNS) {
|
|
255
|
+
if (re.test(title)) return level;
|
|
256
|
+
}
|
|
257
|
+
return void 0;
|
|
258
|
+
}
|
|
259
|
+
function seniorityScore(fp, job) {
|
|
260
|
+
if (!fp.seniorityBand) return 1;
|
|
261
|
+
const jobLevel = inferSeniority(job.title);
|
|
262
|
+
if (!jobLevel) return 0.85;
|
|
263
|
+
const wanted = SENIORITY_RANK[fp.seniorityBand] ?? 1;
|
|
264
|
+
const got = SENIORITY_RANK[jobLevel] ?? 1;
|
|
265
|
+
const delta = Math.abs(wanted - got);
|
|
266
|
+
if (delta === 0) return 1;
|
|
267
|
+
if (delta === 1) return 0.5;
|
|
268
|
+
return 0.2;
|
|
269
|
+
}
|
|
270
|
+
function recencyScore(postedAt) {
|
|
271
|
+
if (!postedAt) return 0.75;
|
|
272
|
+
const ageDays = (Date.now() - new Date(postedAt).getTime()) / 864e5;
|
|
273
|
+
if (ageDays < 7) return 1;
|
|
274
|
+
if (ageDays < 30) return 0.9;
|
|
275
|
+
if (ageDays < 90) return 0.75;
|
|
276
|
+
return 0.6;
|
|
277
|
+
}
|
|
278
|
+
function passesFilters(fp, job) {
|
|
279
|
+
const prefs = fp.prefs;
|
|
280
|
+
if (!prefs) return true;
|
|
281
|
+
if (prefs.remoteOnly && !job.remote) return false;
|
|
282
|
+
if (prefs.roleTypes && prefs.roleTypes.length > 0 && !prefs.roleTypes.includes(job.roleType)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
if (prefs.compFloorUsd !== void 0) {
|
|
286
|
+
if (job.compMax !== void 0 && job.compMax < prefs.compFloorUsd) return false;
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
function buildReason(matchedTags) {
|
|
291
|
+
if (matchedTags.length === 0) return "No direct skill overlap found.";
|
|
292
|
+
const top = matchedTags.slice(0, 3);
|
|
293
|
+
const rest = matchedTags.length - top.length;
|
|
294
|
+
const listed = top.join(", ");
|
|
295
|
+
if (rest === 0) return `Matched on ${listed}.`;
|
|
296
|
+
return `Matched on ${listed} + ${rest} more skill${rest > 1 ? "s" : ""}.`;
|
|
297
|
+
}
|
|
298
|
+
function match(fp, jobs, limit = 5) {
|
|
299
|
+
const idf = computeIdf(jobs);
|
|
300
|
+
const fpTagSet = new Set(fp.skillTags);
|
|
301
|
+
const maxTagScore = fp.skillTags.reduce((acc, t) => acc + (idf.get(t) ?? 1), 0);
|
|
302
|
+
const candidates = jobs.filter((j) => passesFilters(fp, j));
|
|
303
|
+
const scored = candidates.map((job) => {
|
|
304
|
+
const jobTagSet = new Set(job.tags);
|
|
305
|
+
const matched = [];
|
|
306
|
+
let tagScore2 = 0;
|
|
307
|
+
for (const tag of fpTagSet) {
|
|
308
|
+
if (jobTagSet.has(tag)) {
|
|
309
|
+
const w = idf.get(tag) ?? 1;
|
|
310
|
+
tagScore2 += w;
|
|
311
|
+
matched.push(tag);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const normTagScore = maxTagScore > 0 ? tagScore2 / maxTagScore : 0;
|
|
315
|
+
matched.sort((a, b) => (idf.get(b) ?? 1) - (idf.get(a) ?? 1));
|
|
316
|
+
const sScore = seniorityScore(fp, job);
|
|
317
|
+
const rScore = recencyScore(job.postedAt);
|
|
318
|
+
const score = normTagScore * 0.6 + sScore * 0.25 + rScore * 0.15;
|
|
319
|
+
return {
|
|
320
|
+
job,
|
|
321
|
+
score: Math.round(score * 1e3) / 1e3,
|
|
322
|
+
matchedTags: matched,
|
|
323
|
+
reason: buildReason(matched)
|
|
324
|
+
};
|
|
325
|
+
});
|
|
326
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
327
|
+
}
|
|
328
|
+
function matchOne(fp, job) {
|
|
329
|
+
const results = match(fp, [job], 1);
|
|
330
|
+
return results.length > 0 ? results[0] : null;
|
|
331
|
+
}
|
|
332
|
+
var SENIORITY_RANK, SENIORITY_PATTERNS;
|
|
333
|
+
var init_matcher = __esm({
|
|
334
|
+
"../../packages/core/src/matcher.ts"() {
|
|
335
|
+
"use strict";
|
|
336
|
+
SENIORITY_RANK = {
|
|
337
|
+
junior: 0,
|
|
338
|
+
mid: 1,
|
|
339
|
+
senior: 2,
|
|
340
|
+
staff: 3
|
|
341
|
+
};
|
|
342
|
+
SENIORITY_PATTERNS = [
|
|
343
|
+
[/\bstaff\b|\bprincipal\b|\bdistinguished\b/i, "staff"],
|
|
344
|
+
[/\bsenior\b|\bsr\.?\b/i, "senior"],
|
|
345
|
+
[/\bjunior\b|\bjr\.?\b|\bentry[\s-]?level\b/i, "junior"],
|
|
346
|
+
[/\bmid[\s-]?level\b|\bmid\b/i, "mid"]
|
|
347
|
+
];
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// ../../packages/core/src/feeds/greenhouse.ts
|
|
352
|
+
function tokenize(text) {
|
|
353
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
354
|
+
}
|
|
355
|
+
function extractTags(job) {
|
|
356
|
+
const texts = [
|
|
357
|
+
job.title,
|
|
358
|
+
...(job.departments ?? []).map((d) => d.name),
|
|
359
|
+
job.location?.name ?? "",
|
|
360
|
+
...(job.offices ?? []).map((o) => o.name),
|
|
361
|
+
// mine the full HTML description for additional signal when present
|
|
362
|
+
...job.content ? [job.content.replace(/<[^>]*>/g, " ")] : []
|
|
363
|
+
].filter(Boolean);
|
|
364
|
+
const tokens = texts.flatMap(tokenize);
|
|
365
|
+
return normalize(tokens);
|
|
366
|
+
}
|
|
367
|
+
function inferRemote(location) {
|
|
368
|
+
const l = location.toLowerCase();
|
|
369
|
+
return l.includes("remote") || l.includes("anywhere") || l.includes("worldwide");
|
|
370
|
+
}
|
|
371
|
+
async function fetchSlug(slug) {
|
|
372
|
+
const url = `https://boards-api.greenhouse.io/v1/boards/${slug}/jobs?content=true`;
|
|
373
|
+
let res;
|
|
374
|
+
try {
|
|
375
|
+
res = await fetch(url, { headers: { Accept: "application/json" } });
|
|
376
|
+
} catch (err) {
|
|
377
|
+
console.warn(`[greenhouse] ${slug}: network error \u2014`, err);
|
|
378
|
+
return [];
|
|
379
|
+
}
|
|
380
|
+
if (!res.ok) {
|
|
381
|
+
console.warn(`[greenhouse] ${slug}: HTTP ${res.status} ${res.statusText}`);
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
let data;
|
|
385
|
+
try {
|
|
386
|
+
data = await res.json();
|
|
387
|
+
} catch (err) {
|
|
388
|
+
console.warn(`[greenhouse] ${slug}: JSON parse error \u2014`, err);
|
|
389
|
+
return [];
|
|
390
|
+
}
|
|
391
|
+
const jobs = data.jobs ?? [];
|
|
392
|
+
if (jobs.length === 0) {
|
|
393
|
+
console.warn(`[greenhouse] ${slug}: 0 jobs returned (board may be private or slug invalid)`);
|
|
394
|
+
} else {
|
|
395
|
+
console.info(`[greenhouse] ${slug}: ${jobs.length} jobs`);
|
|
396
|
+
}
|
|
397
|
+
return jobs.map((j) => ({
|
|
398
|
+
id: `greenhouse:${j.id}`,
|
|
399
|
+
source: "greenhouse",
|
|
400
|
+
title: j.title,
|
|
401
|
+
company: slug,
|
|
402
|
+
url: j.absolute_url,
|
|
403
|
+
remote: inferRemote(j.location?.name ?? ""),
|
|
404
|
+
location: j.location?.name,
|
|
405
|
+
tags: extractTags(j),
|
|
406
|
+
roleType: "full_time",
|
|
407
|
+
postedAt: j.updated_at,
|
|
408
|
+
applyMode: "direct",
|
|
409
|
+
raw: j
|
|
410
|
+
}));
|
|
411
|
+
}
|
|
412
|
+
var FALLBACK_SLUGS, greenhouse;
|
|
413
|
+
var init_greenhouse = __esm({
|
|
414
|
+
"../../packages/core/src/feeds/greenhouse.ts"() {
|
|
415
|
+
"use strict";
|
|
416
|
+
init_vocabulary();
|
|
417
|
+
FALLBACK_SLUGS = [
|
|
418
|
+
"stripe",
|
|
419
|
+
"linear",
|
|
420
|
+
"vercel",
|
|
421
|
+
"ramp",
|
|
422
|
+
"notion",
|
|
423
|
+
"airbnb",
|
|
424
|
+
"anthropic",
|
|
425
|
+
"figma",
|
|
426
|
+
"discord",
|
|
427
|
+
"brex",
|
|
428
|
+
"mercury",
|
|
429
|
+
"retool",
|
|
430
|
+
"vanta",
|
|
431
|
+
"plaid",
|
|
432
|
+
"gusto",
|
|
433
|
+
"scale",
|
|
434
|
+
"databricks",
|
|
435
|
+
"coinbase",
|
|
436
|
+
"robinhood",
|
|
437
|
+
"doordash"
|
|
438
|
+
];
|
|
439
|
+
greenhouse = {
|
|
440
|
+
source: "greenhouse",
|
|
441
|
+
async fetch(opts) {
|
|
442
|
+
const slugs = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : FALLBACK_SLUGS;
|
|
443
|
+
console.info(`[greenhouse] fetching ${slugs.length} slugs: ${slugs.join(", ")}`);
|
|
444
|
+
const results = await Promise.allSettled(slugs.map(fetchSlug));
|
|
445
|
+
const jobs = [];
|
|
446
|
+
let failures = 0;
|
|
447
|
+
for (const r of results) {
|
|
448
|
+
if (r.status === "fulfilled") {
|
|
449
|
+
jobs.push(...r.value);
|
|
450
|
+
} else {
|
|
451
|
+
failures++;
|
|
452
|
+
console.warn(`[greenhouse] slug fetch rejected:`, r.reason);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
console.info(`[greenhouse] total: ${jobs.length} jobs, ${failures} slug failures`);
|
|
456
|
+
return jobs;
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// ../../packages/core/src/feeds/ashby.ts
|
|
463
|
+
function tokenize2(text) {
|
|
464
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
465
|
+
}
|
|
466
|
+
function extractTags2(job) {
|
|
467
|
+
const texts = [
|
|
468
|
+
job.title,
|
|
469
|
+
job.teamName ?? "",
|
|
470
|
+
job.locationName ?? "",
|
|
471
|
+
...(job.secondaryLocations ?? []).map((l) => l.locationName ?? "")
|
|
472
|
+
];
|
|
473
|
+
return normalize(texts.flatMap(tokenize2));
|
|
474
|
+
}
|
|
475
|
+
function mapEmploymentType(raw) {
|
|
476
|
+
if (!raw) return "full_time";
|
|
477
|
+
const lower = raw.toLowerCase();
|
|
478
|
+
if (lower.includes("contract") || lower.includes("contractor")) return "contract";
|
|
479
|
+
if (lower.includes("freelance")) return "freelance";
|
|
480
|
+
return "full_time";
|
|
481
|
+
}
|
|
482
|
+
function inferRemote2(job) {
|
|
483
|
+
if (job.isRemote === true) return true;
|
|
484
|
+
const loc = (job.locationName ?? "").toLowerCase();
|
|
485
|
+
return loc.includes("remote") || loc.includes("anywhere");
|
|
486
|
+
}
|
|
487
|
+
async function fetchSlug2(slug) {
|
|
488
|
+
const url = `https://api.ashbyhq.com/posting-api/job-board/${slug}`;
|
|
489
|
+
const res = await fetch(url, {
|
|
490
|
+
headers: { Accept: "application/json" }
|
|
491
|
+
});
|
|
492
|
+
if (!res.ok) {
|
|
493
|
+
throw new Error(`Ashby ${slug}: HTTP ${res.status}`);
|
|
494
|
+
}
|
|
495
|
+
const data = await res.json();
|
|
496
|
+
return (data.jobs ?? []).map((j) => {
|
|
497
|
+
const comp = j.compensation;
|
|
498
|
+
return {
|
|
499
|
+
id: `ashby:${j.id}`,
|
|
500
|
+
source: "ashby",
|
|
501
|
+
title: j.title,
|
|
502
|
+
company: slug,
|
|
503
|
+
url: j.applyUrl ?? `https://jobs.ashbyhq.com/${slug}/${j.id}`,
|
|
504
|
+
remote: inferRemote2(j),
|
|
505
|
+
location: j.locationName,
|
|
506
|
+
compMin: comp?.minValue,
|
|
507
|
+
compMax: comp?.maxValue,
|
|
508
|
+
tags: extractTags2(j),
|
|
509
|
+
roleType: mapEmploymentType(j.employmentType),
|
|
510
|
+
postedAt: j.publishedDate,
|
|
511
|
+
applyMode: "direct",
|
|
512
|
+
raw: j
|
|
513
|
+
};
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
var ashby;
|
|
517
|
+
var init_ashby = __esm({
|
|
518
|
+
"../../packages/core/src/feeds/ashby.ts"() {
|
|
519
|
+
"use strict";
|
|
520
|
+
init_vocabulary();
|
|
521
|
+
ashby = {
|
|
522
|
+
source: "ashby",
|
|
523
|
+
async fetch(opts) {
|
|
524
|
+
const slugs = opts?.slugs ?? [];
|
|
525
|
+
const results = await Promise.allSettled(slugs.map(fetchSlug2));
|
|
526
|
+
const jobs = [];
|
|
527
|
+
for (const r of results) {
|
|
528
|
+
if (r.status === "fulfilled") jobs.push(...r.value);
|
|
529
|
+
}
|
|
530
|
+
return jobs;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// ../../packages/core/src/feeds/himalayas.ts
|
|
537
|
+
function tokenize3(text) {
|
|
538
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
539
|
+
}
|
|
540
|
+
function extractTags3(job) {
|
|
541
|
+
const texts = [
|
|
542
|
+
job.title,
|
|
543
|
+
...job.tags ?? []
|
|
544
|
+
];
|
|
545
|
+
return normalize(texts.flatMap(tokenize3));
|
|
546
|
+
}
|
|
547
|
+
function mapJobType(raw) {
|
|
548
|
+
if (!raw) return "full_time";
|
|
549
|
+
const lower = raw.toLowerCase();
|
|
550
|
+
if (lower.includes("contract")) return "contract";
|
|
551
|
+
if (lower.includes("freelance")) return "freelance";
|
|
552
|
+
return "full_time";
|
|
553
|
+
}
|
|
554
|
+
function buildUrl(job) {
|
|
555
|
+
if (job.applicationUrl) return job.applicationUrl;
|
|
556
|
+
if (job.url) return job.url;
|
|
557
|
+
const slug = job.slug ?? job.id ?? "unknown";
|
|
558
|
+
return `https://himalayas.app/jobs/${slug}`;
|
|
559
|
+
}
|
|
560
|
+
function buildId(job) {
|
|
561
|
+
return `himalayas:${job.id ?? job.slug ?? job.title}`;
|
|
562
|
+
}
|
|
563
|
+
var himalayas;
|
|
564
|
+
var init_himalayas = __esm({
|
|
565
|
+
"../../packages/core/src/feeds/himalayas.ts"() {
|
|
566
|
+
"use strict";
|
|
567
|
+
init_vocabulary();
|
|
568
|
+
himalayas = {
|
|
569
|
+
source: "himalayas",
|
|
570
|
+
async fetch(opts) {
|
|
571
|
+
const limit = opts?.limit ?? 100;
|
|
572
|
+
const url = `https://himalayas.app/jobs/api?limit=${limit}`;
|
|
573
|
+
const res = await fetch(url, {
|
|
574
|
+
headers: { Accept: "application/json" }
|
|
575
|
+
});
|
|
576
|
+
if (!res.ok) {
|
|
577
|
+
throw new Error(`Himalayas: HTTP ${res.status}`);
|
|
578
|
+
}
|
|
579
|
+
const data = await res.json();
|
|
580
|
+
return (data.jobs ?? []).map((j) => ({
|
|
581
|
+
id: buildId(j),
|
|
582
|
+
source: "himalayas",
|
|
583
|
+
title: j.title,
|
|
584
|
+
company: j.companyName ?? j.companySlug ?? "unknown",
|
|
585
|
+
url: buildUrl(j),
|
|
586
|
+
// Himalayas is a remote-only board
|
|
587
|
+
remote: true,
|
|
588
|
+
location: (j.locationRestrictions ?? []).join(", ") || "Remote",
|
|
589
|
+
compMin: j.salaryMin,
|
|
590
|
+
compMax: j.salaryMax,
|
|
591
|
+
tags: extractTags3(j),
|
|
592
|
+
roleType: mapJobType(j.jobType),
|
|
593
|
+
postedAt: j.pubDate ?? j.createdAt,
|
|
594
|
+
applyMode: "direct",
|
|
595
|
+
raw: j
|
|
596
|
+
}));
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// ../../packages/core/src/feeds/wwr.ts
|
|
603
|
+
function tokenize4(text) {
|
|
604
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
605
|
+
}
|
|
606
|
+
function stripHtml(html) {
|
|
607
|
+
return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
608
|
+
}
|
|
609
|
+
function inferRoleType(category) {
|
|
610
|
+
const lower = category.toLowerCase();
|
|
611
|
+
if (lower.includes("contract")) return "contract";
|
|
612
|
+
if (lower.includes("freelance")) return "freelance";
|
|
613
|
+
return "full_time";
|
|
614
|
+
}
|
|
615
|
+
function extractId(link) {
|
|
616
|
+
const match2 = link.match(/\/opening\/([^/\s]+)/);
|
|
617
|
+
return `wwr:${match2?.[1] ?? encodeURIComponent(link)}`;
|
|
618
|
+
}
|
|
619
|
+
function parseRss(xml) {
|
|
620
|
+
const items = [];
|
|
621
|
+
const itemBlocks = xml.match(/<item>([\s\S]*?)<\/item>/g) ?? [];
|
|
622
|
+
for (const block of itemBlocks) {
|
|
623
|
+
const get = (tag) => {
|
|
624
|
+
const cdataMatch = block.match(new RegExp(`<${tag}[^>]*><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/${tag}>`, "i"));
|
|
625
|
+
if (cdataMatch) return cdataMatch[1].trim();
|
|
626
|
+
const plainMatch = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i"));
|
|
627
|
+
return plainMatch?.[1].trim() ?? "";
|
|
628
|
+
};
|
|
629
|
+
const rawTitle = get("title");
|
|
630
|
+
const colonIdx = rawTitle.indexOf(":");
|
|
631
|
+
const company = colonIdx !== -1 ? rawTitle.slice(0, colonIdx).trim() : "Unknown";
|
|
632
|
+
const titleAfterColon = colonIdx !== -1 ? rawTitle.slice(colonIdx + 1).trim() : rawTitle;
|
|
633
|
+
const title = titleAfterColon.replace(/\s*\([^)]*\)\s*$/, "").trim();
|
|
634
|
+
items.push({
|
|
635
|
+
title,
|
|
636
|
+
link: get("link") || get("guid"),
|
|
637
|
+
pubDate: get("pubDate"),
|
|
638
|
+
category: get("category"),
|
|
639
|
+
description: get("description"),
|
|
640
|
+
company
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
return items;
|
|
644
|
+
}
|
|
645
|
+
function extractTags4(item) {
|
|
646
|
+
const text = [item.title, item.category, stripHtml(item.description)].join(" ");
|
|
647
|
+
return normalize(tokenize4(text));
|
|
648
|
+
}
|
|
649
|
+
var WWR_RSS_URL, wwr;
|
|
650
|
+
var init_wwr = __esm({
|
|
651
|
+
"../../packages/core/src/feeds/wwr.ts"() {
|
|
652
|
+
"use strict";
|
|
653
|
+
init_vocabulary();
|
|
654
|
+
WWR_RSS_URL = "https://weworkremotely.com/remote-jobs.rss";
|
|
655
|
+
wwr = {
|
|
656
|
+
source: "wwr",
|
|
657
|
+
async fetch(opts) {
|
|
658
|
+
const limit = opts?.limit ?? 200;
|
|
659
|
+
const res = await fetch(WWR_RSS_URL, {
|
|
660
|
+
headers: { Accept: "application/rss+xml, application/xml, text/xml" }
|
|
661
|
+
});
|
|
662
|
+
if (!res.ok) {
|
|
663
|
+
throw new Error(`WWR RSS: HTTP ${res.status}`);
|
|
664
|
+
}
|
|
665
|
+
const xml = await res.text();
|
|
666
|
+
const items = parseRss(xml).slice(0, limit);
|
|
667
|
+
return items.map((item) => ({
|
|
668
|
+
id: extractId(item.link),
|
|
669
|
+
source: "wwr",
|
|
670
|
+
title: item.title,
|
|
671
|
+
company: item.company,
|
|
672
|
+
url: item.link,
|
|
673
|
+
// WWR is a remote-only board
|
|
674
|
+
remote: true,
|
|
675
|
+
location: "Remote",
|
|
676
|
+
tags: extractTags4(item),
|
|
677
|
+
roleType: inferRoleType(item.category),
|
|
678
|
+
postedAt: item.pubDate ? new Date(item.pubDate).toISOString() : void 0,
|
|
679
|
+
applyMode: "direct",
|
|
680
|
+
raw: item
|
|
681
|
+
}));
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// ../../packages/core/src/feeds/hn.ts
|
|
688
|
+
function tokenize5(text) {
|
|
689
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
690
|
+
}
|
|
691
|
+
function stripHtml2(html) {
|
|
692
|
+
return html.replace(/<p>/gi, " ").replace(/<[^>]*>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/\s+/g, " ").trim();
|
|
693
|
+
}
|
|
694
|
+
function extractUrl(text) {
|
|
695
|
+
const match2 = text.match(/https?:\/\/[^\s<>"']+/);
|
|
696
|
+
return match2?.[0] ?? "";
|
|
697
|
+
}
|
|
698
|
+
function inferRemote3(text) {
|
|
699
|
+
const lower = text.toLowerCase();
|
|
700
|
+
return lower.includes("remote") || lower.includes("anywhere") || lower.includes("distributed");
|
|
701
|
+
}
|
|
702
|
+
function inferRoleType2(text) {
|
|
703
|
+
const lower = text.toLowerCase();
|
|
704
|
+
if (lower.includes("contract") || lower.includes("contractor")) return "contract";
|
|
705
|
+
if (lower.includes("freelance")) return "freelance";
|
|
706
|
+
return "full_time";
|
|
707
|
+
}
|
|
708
|
+
function parseComment(item) {
|
|
709
|
+
if (!item.text || item.text.trim().length < 20) return null;
|
|
710
|
+
const raw = stripHtml2(item.text);
|
|
711
|
+
if (!raw) return null;
|
|
712
|
+
const firstLine = raw.split(/\n/)[0];
|
|
713
|
+
const parts = firstLine.split("|").map((s) => s.trim());
|
|
714
|
+
const company = parts[0] ?? "Unknown";
|
|
715
|
+
const title = parts[1] ?? firstLine.slice(0, 80).trim();
|
|
716
|
+
const location = parts[2] ?? "";
|
|
717
|
+
if (company.toLowerCase().startsWith("note:") || company.toLowerCase().startsWith("ps:") || title.length < 3) {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
const url = extractUrl(raw) || `https://news.ycombinator.com/item?id=${item.id}`;
|
|
721
|
+
const tags = extractTags5(raw);
|
|
722
|
+
if (tags.length === 0) return null;
|
|
723
|
+
return {
|
|
724
|
+
id: `hn:${item.id}`,
|
|
725
|
+
source: "hn",
|
|
726
|
+
title: title.slice(0, 120),
|
|
727
|
+
company: company.slice(0, 80),
|
|
728
|
+
url,
|
|
729
|
+
remote: inferRemote3(raw),
|
|
730
|
+
location: location || void 0,
|
|
731
|
+
tags,
|
|
732
|
+
roleType: inferRoleType2(raw),
|
|
733
|
+
postedAt: item.created_at,
|
|
734
|
+
applyMode: "direct",
|
|
735
|
+
raw: item
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function extractTags5(text) {
|
|
739
|
+
return normalize(tokenize5(text));
|
|
740
|
+
}
|
|
741
|
+
var ALGOLIA_SEARCH, ALGOLIA_ITEMS, hn;
|
|
742
|
+
var init_hn = __esm({
|
|
743
|
+
"../../packages/core/src/feeds/hn.ts"() {
|
|
744
|
+
"use strict";
|
|
745
|
+
init_vocabulary();
|
|
746
|
+
ALGOLIA_SEARCH = "https://hn.algolia.com/api/v1/search?query=Ask+HN%3A+Who+is+Hiring%3F&tags=story,ask_hn&hitsPerPage=1";
|
|
747
|
+
ALGOLIA_ITEMS = "https://hn.algolia.com/api/v1/items/";
|
|
748
|
+
hn = {
|
|
749
|
+
source: "hn",
|
|
750
|
+
async fetch(opts) {
|
|
751
|
+
const limit = opts?.limit ?? 150;
|
|
752
|
+
const searchRes = await fetch(ALGOLIA_SEARCH, {
|
|
753
|
+
headers: { Accept: "application/json" }
|
|
754
|
+
});
|
|
755
|
+
if (!searchRes.ok) {
|
|
756
|
+
throw new Error(`HN Algolia search: HTTP ${searchRes.status}`);
|
|
757
|
+
}
|
|
758
|
+
const searchData = await searchRes.json();
|
|
759
|
+
const story = searchData.hits[0];
|
|
760
|
+
if (!story) {
|
|
761
|
+
throw new Error('HN: No "Who is Hiring" story found');
|
|
762
|
+
}
|
|
763
|
+
const itemRes = await fetch(`${ALGOLIA_ITEMS}${story.objectID}`, {
|
|
764
|
+
headers: { Accept: "application/json" }
|
|
765
|
+
});
|
|
766
|
+
if (!itemRes.ok) {
|
|
767
|
+
throw new Error(`HN Algolia item ${story.objectID}: HTTP ${itemRes.status}`);
|
|
768
|
+
}
|
|
769
|
+
const storyItem = await itemRes.json();
|
|
770
|
+
const comments = storyItem.children ?? [];
|
|
771
|
+
const jobs = [];
|
|
772
|
+
for (const comment of comments.slice(0, limit)) {
|
|
773
|
+
const job = parseComment(comment);
|
|
774
|
+
if (job) jobs.push(job);
|
|
775
|
+
}
|
|
776
|
+
return jobs;
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
// ../../packages/core/src/feeds/index.ts
|
|
783
|
+
async function aggregate(opts) {
|
|
784
|
+
const ghSlugs = opts?.slugs?.["greenhouse"] ?? DEFAULT_GREENHOUSE_SLUGS;
|
|
785
|
+
const ashbySlugs = opts?.slugs?.["ashby"] ?? DEFAULT_ASHBY_SLUGS;
|
|
786
|
+
const limit = opts?.limit ?? 150;
|
|
787
|
+
const settled = await Promise.allSettled([
|
|
788
|
+
greenhouse.fetch({ slugs: ghSlugs, limit }),
|
|
789
|
+
ashby.fetch({ slugs: ashbySlugs, limit }),
|
|
790
|
+
himalayas.fetch({ limit }),
|
|
791
|
+
wwr.fetch({ limit }),
|
|
792
|
+
hn.fetch({ limit })
|
|
793
|
+
]);
|
|
794
|
+
const seen = /* @__PURE__ */ new Set();
|
|
795
|
+
const jobs = [];
|
|
796
|
+
const sourceNames = ["greenhouse", "ashby", "himalayas", "wwr", "hn"];
|
|
797
|
+
for (let i = 0; i < settled.length; i++) {
|
|
798
|
+
const result = settled[i];
|
|
799
|
+
if (result.status === "rejected") {
|
|
800
|
+
console.warn(`[feeds] ${sourceNames[i]} failed:`, result.reason);
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
for (const job of result.value) {
|
|
804
|
+
if (!seen.has(job.id)) {
|
|
805
|
+
seen.add(job.id);
|
|
806
|
+
jobs.push(job);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return jobs;
|
|
811
|
+
}
|
|
812
|
+
var FEEDS, DEFAULT_GREENHOUSE_SLUGS, DEFAULT_ASHBY_SLUGS;
|
|
813
|
+
var init_feeds = __esm({
|
|
814
|
+
"../../packages/core/src/feeds/index.ts"() {
|
|
815
|
+
"use strict";
|
|
816
|
+
init_greenhouse();
|
|
817
|
+
init_ashby();
|
|
818
|
+
init_himalayas();
|
|
819
|
+
init_wwr();
|
|
820
|
+
init_hn();
|
|
821
|
+
FEEDS = [greenhouse, ashby, himalayas, wwr, hn];
|
|
822
|
+
DEFAULT_GREENHOUSE_SLUGS = [
|
|
823
|
+
"stripe",
|
|
824
|
+
"linear",
|
|
825
|
+
"vercel",
|
|
826
|
+
"ramp",
|
|
827
|
+
"notion",
|
|
828
|
+
"airbnb",
|
|
829
|
+
"anthropic",
|
|
830
|
+
"figma",
|
|
831
|
+
"discord",
|
|
832
|
+
"brex",
|
|
833
|
+
"mercury",
|
|
834
|
+
"retool",
|
|
835
|
+
"vanta",
|
|
836
|
+
"plaid",
|
|
837
|
+
"gusto",
|
|
838
|
+
"scale",
|
|
839
|
+
"databricks",
|
|
840
|
+
"coinbase",
|
|
841
|
+
"robinhood",
|
|
842
|
+
"doordash"
|
|
843
|
+
];
|
|
844
|
+
DEFAULT_ASHBY_SLUGS = [
|
|
845
|
+
"ramp",
|
|
846
|
+
"notion",
|
|
847
|
+
"linear",
|
|
848
|
+
"vercel",
|
|
849
|
+
"replit",
|
|
850
|
+
"posthog"
|
|
851
|
+
];
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
// ../../packages/core/src/coastal.ts
|
|
856
|
+
import { readFileSync } from "fs";
|
|
857
|
+
import { join } from "path";
|
|
858
|
+
import { fileURLToPath } from "url";
|
|
859
|
+
function resolveDataPath() {
|
|
860
|
+
try {
|
|
861
|
+
const dir = fileURLToPath(new URL("../../../data", import.meta.url));
|
|
862
|
+
return join(dir, "coastal-roles.json");
|
|
863
|
+
} catch {
|
|
864
|
+
return join(process.cwd(), "data", "coastal-roles.json");
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
function loadCoastalRoles() {
|
|
868
|
+
const filePath = resolveDataPath();
|
|
869
|
+
try {
|
|
870
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
871
|
+
const parsed = JSON.parse(raw);
|
|
872
|
+
if (!Array.isArray(parsed)) {
|
|
873
|
+
console.warn("[coastal] coastal-roles.json is not an array \u2014 skipping");
|
|
874
|
+
return [];
|
|
875
|
+
}
|
|
876
|
+
const valid = [];
|
|
877
|
+
for (const entry of parsed) {
|
|
878
|
+
if (typeof entry === "object" && entry !== null && typeof entry.id === "string" && entry.applyMode === "buyer-lead" && entry.buyer === "coastal") {
|
|
879
|
+
valid.push(entry);
|
|
880
|
+
} else {
|
|
881
|
+
console.warn("[coastal] Skipping malformed role entry:", entry);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return valid;
|
|
885
|
+
} catch (err) {
|
|
886
|
+
if (err.code === "ENOENT") {
|
|
887
|
+
console.warn(`[coastal] data/coastal-roles.json not found at ${filePath} \u2014 no Coastal roles loaded`);
|
|
888
|
+
} else {
|
|
889
|
+
console.warn("[coastal] Failed to load coastal-roles.json:", err);
|
|
890
|
+
}
|
|
891
|
+
return [];
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
var COASTAL_BUYER;
|
|
895
|
+
var init_coastal = __esm({
|
|
896
|
+
"../../packages/core/src/coastal.ts"() {
|
|
897
|
+
"use strict";
|
|
898
|
+
COASTAL_BUYER = {
|
|
899
|
+
id: "coastal",
|
|
900
|
+
legalName: "Coastal Recruiting LLC",
|
|
901
|
+
matchCriteria: {
|
|
902
|
+
roleTypes: ["full_time"]
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
// ../../packages/core/src/indexer.ts
|
|
909
|
+
async function buildIndex(opts) {
|
|
910
|
+
const includeCoastal = opts?.includeCoastal ?? true;
|
|
911
|
+
const publicJobs = await aggregate(opts);
|
|
912
|
+
const allJobs = [...publicJobs];
|
|
913
|
+
if (includeCoastal) {
|
|
914
|
+
const coastalJobs = loadCoastalRoles();
|
|
915
|
+
const seen = new Set(publicJobs.map((j) => j.id));
|
|
916
|
+
for (const job of coastalJobs) {
|
|
917
|
+
if (!seen.has(job.id)) {
|
|
918
|
+
seen.add(job.id);
|
|
919
|
+
allJobs.push(job);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
const jobs = allJobs.map(({ raw: _raw, ...rest }) => rest);
|
|
924
|
+
return {
|
|
925
|
+
builtAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
926
|
+
jobs
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
var init_indexer = __esm({
|
|
930
|
+
"../../packages/core/src/indexer.ts"() {
|
|
931
|
+
"use strict";
|
|
932
|
+
init_feeds();
|
|
933
|
+
init_coastal();
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
// ../../packages/core/src/github.ts
|
|
938
|
+
function ghHeaders(token) {
|
|
939
|
+
const headers = {
|
|
940
|
+
Accept: "application/vnd.github+json",
|
|
941
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
942
|
+
};
|
|
943
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
944
|
+
return headers;
|
|
945
|
+
}
|
|
946
|
+
async function ghFetch(path, token) {
|
|
947
|
+
const url = `https://api.github.com${path}`;
|
|
948
|
+
const res = await fetch(url, { headers: ghHeaders(token) });
|
|
949
|
+
if (!res.ok) {
|
|
950
|
+
throw new Error(`GitHub API ${path}: HTTP ${res.status} ${res.statusText}`);
|
|
951
|
+
}
|
|
952
|
+
return res.json();
|
|
953
|
+
}
|
|
954
|
+
async function fetchGitHubProfile(login, token) {
|
|
955
|
+
const user = await ghFetch(`/users/${login}`, token);
|
|
956
|
+
let repos = [];
|
|
957
|
+
try {
|
|
958
|
+
repos = await ghFetch(
|
|
959
|
+
`/users/${login}/repos?sort=pushed&per_page=100`,
|
|
960
|
+
token
|
|
961
|
+
);
|
|
962
|
+
} catch (err) {
|
|
963
|
+
console.warn(`[github] ${login}: repos fetch failed, continuing \u2014`, err);
|
|
964
|
+
}
|
|
965
|
+
const langCount = {};
|
|
966
|
+
for (const repo of repos) {
|
|
967
|
+
if (repo.fork) continue;
|
|
968
|
+
if (repo.language) {
|
|
969
|
+
langCount[repo.language.toLowerCase()] = (langCount[repo.language.toLowerCase()] ?? 0) + 1;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
const topLanguages = Object.entries(langCount).sort(([, a], [, b]) => b - a).slice(0, 10).map(([lang]) => lang);
|
|
973
|
+
const topicSet = /* @__PURE__ */ new Set();
|
|
974
|
+
for (const repo of repos) {
|
|
975
|
+
if (repo.fork) continue;
|
|
976
|
+
for (const t of repo.topics ?? []) topicSet.add(t.toLowerCase());
|
|
977
|
+
}
|
|
978
|
+
const topics = Array.from(topicSet).slice(0, 30);
|
|
979
|
+
let recentPRorgs;
|
|
980
|
+
try {
|
|
981
|
+
const q = encodeURIComponent(
|
|
982
|
+
`type:pr is:merged author:${login} sort:updated`
|
|
983
|
+
);
|
|
984
|
+
const result = await ghFetch(
|
|
985
|
+
`/search/issues?q=${q}&per_page=30`,
|
|
986
|
+
token
|
|
987
|
+
);
|
|
988
|
+
const orgs = /* @__PURE__ */ new Set();
|
|
989
|
+
for (const item of result.items ?? []) {
|
|
990
|
+
const orgLogin = item.repository?.owner?.login;
|
|
991
|
+
if (orgLogin && orgLogin !== login) orgs.add(orgLogin);
|
|
992
|
+
}
|
|
993
|
+
if (orgs.size > 0) recentPRorgs = Array.from(orgs);
|
|
994
|
+
} catch {
|
|
995
|
+
}
|
|
996
|
+
return {
|
|
997
|
+
login: user.login,
|
|
998
|
+
name: user.name ?? void 0,
|
|
999
|
+
publicEmail: user.email ?? void 0,
|
|
1000
|
+
avatarUrl: user.avatar_url,
|
|
1001
|
+
accountCreatedAt: user.created_at,
|
|
1002
|
+
publicRepos: user.public_repos,
|
|
1003
|
+
followers: user.followers,
|
|
1004
|
+
topLanguages,
|
|
1005
|
+
topics,
|
|
1006
|
+
recentPRorgs
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
function inferSeniority2(p) {
|
|
1010
|
+
const ageMs = Date.now() - new Date(p.accountCreatedAt).getTime();
|
|
1011
|
+
const ageYears = ageMs / (1e3 * 60 * 60 * 24 * 365.25);
|
|
1012
|
+
if (ageYears >= 9 && (p.publicRepos >= 40 || p.followers >= 500)) return "staff";
|
|
1013
|
+
if (ageYears >= 5 && (p.publicRepos >= 20 || p.followers >= 100)) return "senior";
|
|
1014
|
+
if (ageYears >= 2 && p.publicRepos >= 5) return "mid";
|
|
1015
|
+
if (ageYears < 2 || p.publicRepos < 5) return "junior";
|
|
1016
|
+
return void 0;
|
|
1017
|
+
}
|
|
1018
|
+
function githubToFingerprint(p) {
|
|
1019
|
+
const rawTokens = [
|
|
1020
|
+
...p.topLanguages,
|
|
1021
|
+
...p.topics
|
|
1022
|
+
// recentPRorgs intentionally excluded — org names are not skill tags
|
|
1023
|
+
];
|
|
1024
|
+
const skillTags = normalize(rawTokens);
|
|
1025
|
+
const seniorityBand = inferSeniority2(p);
|
|
1026
|
+
return { skillTags, seniorityBand };
|
|
1027
|
+
}
|
|
1028
|
+
var init_github = __esm({
|
|
1029
|
+
"../../packages/core/src/github.ts"() {
|
|
1030
|
+
"use strict";
|
|
1031
|
+
init_vocabulary();
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
// ../../packages/core/src/index.ts
|
|
1036
|
+
var src_exports = {};
|
|
1037
|
+
__export(src_exports, {
|
|
1038
|
+
COASTAL_BUYER: () => COASTAL_BUYER,
|
|
1039
|
+
DEFAULT_ASHBY_SLUGS: () => DEFAULT_ASHBY_SLUGS,
|
|
1040
|
+
DEFAULT_GREENHOUSE_SLUGS: () => DEFAULT_GREENHOUSE_SLUGS,
|
|
1041
|
+
FEEDS: () => FEEDS,
|
|
1042
|
+
SYNONYMS: () => SYNONYMS,
|
|
1043
|
+
VOCABULARY: () => VOCABULARY,
|
|
1044
|
+
aggregate: () => aggregate,
|
|
1045
|
+
ashby: () => ashby,
|
|
1046
|
+
buildIndex: () => buildIndex,
|
|
1047
|
+
buildReason: () => buildReason,
|
|
1048
|
+
fetchGitHubProfile: () => fetchGitHubProfile,
|
|
1049
|
+
githubToFingerprint: () => githubToFingerprint,
|
|
1050
|
+
greenhouse: () => greenhouse,
|
|
1051
|
+
himalayas: () => himalayas,
|
|
1052
|
+
hn: () => hn,
|
|
1053
|
+
loadCoastalRoles: () => loadCoastalRoles,
|
|
1054
|
+
match: () => match,
|
|
1055
|
+
matchOne: () => matchOne,
|
|
1056
|
+
normalize: () => normalize,
|
|
1057
|
+
wwr: () => wwr
|
|
1058
|
+
});
|
|
1059
|
+
var init_src = __esm({
|
|
1060
|
+
"../../packages/core/src/index.ts"() {
|
|
1061
|
+
"use strict";
|
|
1062
|
+
init_types();
|
|
1063
|
+
init_vocabulary();
|
|
1064
|
+
init_matcher();
|
|
1065
|
+
init_feeds();
|
|
1066
|
+
init_indexer();
|
|
1067
|
+
init_coastal();
|
|
1068
|
+
init_github();
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
// src/profile.ts
|
|
1073
|
+
var profile_exports = {};
|
|
1074
|
+
__export(profile_exports, {
|
|
1075
|
+
accumulateGitHubTags: () => accumulateGitHubTags,
|
|
1076
|
+
accumulateSession: () => accumulateSession,
|
|
1077
|
+
accumulateTags: () => accumulateTags,
|
|
1078
|
+
addSavedJob: () => addSavedJob,
|
|
1079
|
+
deleteProfile: () => deleteProfile,
|
|
1080
|
+
listSavedJobs: () => listSavedJobs,
|
|
1081
|
+
profileToFingerprint: () => profileToFingerprint,
|
|
1082
|
+
readProfile: () => readProfile,
|
|
1083
|
+
removeSavedJob: () => removeSavedJob,
|
|
1084
|
+
writeProfile: () => writeProfile
|
|
1085
|
+
});
|
|
1086
|
+
import {
|
|
1087
|
+
createCipheriv,
|
|
1088
|
+
createDecipheriv,
|
|
1089
|
+
randomBytes
|
|
1090
|
+
} from "crypto";
|
|
1091
|
+
import {
|
|
1092
|
+
readFileSync as readFileSync2,
|
|
1093
|
+
writeFileSync,
|
|
1094
|
+
mkdirSync,
|
|
1095
|
+
existsSync
|
|
1096
|
+
} from "fs";
|
|
1097
|
+
import { join as join2 } from "path";
|
|
1098
|
+
import { homedir } from "os";
|
|
1099
|
+
async function loadKey() {
|
|
1100
|
+
try {
|
|
1101
|
+
const kt = await import("keytar");
|
|
1102
|
+
const stored = await kt.getPassword("terminalhire", "profile-key");
|
|
1103
|
+
if (stored) {
|
|
1104
|
+
return Buffer.from(stored, "hex");
|
|
1105
|
+
}
|
|
1106
|
+
const key2 = randomBytes(KEY_BYTES);
|
|
1107
|
+
await kt.setPassword("terminalhire", "profile-key", key2.toString("hex"));
|
|
1108
|
+
return key2;
|
|
1109
|
+
} catch {
|
|
1110
|
+
}
|
|
1111
|
+
mkdirSync(TERMINALHIRE_DIR, { recursive: true });
|
|
1112
|
+
if (existsSync(KEY_FILE)) {
|
|
1113
|
+
return Buffer.from(readFileSync2(KEY_FILE, "utf8").trim(), "hex");
|
|
1114
|
+
}
|
|
1115
|
+
const key = randomBytes(KEY_BYTES);
|
|
1116
|
+
writeFileSync(KEY_FILE, key.toString("hex"), { mode: 384, encoding: "utf8" });
|
|
1117
|
+
return key;
|
|
1118
|
+
}
|
|
1119
|
+
function encrypt(plaintext, key) {
|
|
1120
|
+
const iv = randomBytes(IV_BYTES);
|
|
1121
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
1122
|
+
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
1123
|
+
const tag = cipher.getAuthTag();
|
|
1124
|
+
return {
|
|
1125
|
+
iv: iv.toString("hex"),
|
|
1126
|
+
tag: tag.toString("hex"),
|
|
1127
|
+
ciphertext: ct.toString("hex")
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
function decrypt(blob, key) {
|
|
1131
|
+
const decipher = createDecipheriv(
|
|
1132
|
+
ALGO,
|
|
1133
|
+
key,
|
|
1134
|
+
Buffer.from(blob.iv, "hex")
|
|
1135
|
+
);
|
|
1136
|
+
decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
|
|
1137
|
+
const plain = Buffer.concat([
|
|
1138
|
+
decipher.update(Buffer.from(blob.ciphertext, "hex")),
|
|
1139
|
+
decipher.final()
|
|
1140
|
+
]);
|
|
1141
|
+
return plain.toString("utf8");
|
|
1142
|
+
}
|
|
1143
|
+
function blankProfile() {
|
|
1144
|
+
return {
|
|
1145
|
+
version: 3,
|
|
1146
|
+
skillTags: [],
|
|
1147
|
+
tagWeights: {},
|
|
1148
|
+
hasEmployerSessions: false,
|
|
1149
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
function recencyDecay(lastSeen) {
|
|
1153
|
+
const ageMs = Date.now() - new Date(lastSeen).getTime();
|
|
1154
|
+
return Math.pow(0.5, ageMs / DECAY_HALF_LIFE_MS);
|
|
1155
|
+
}
|
|
1156
|
+
function tagScore(w) {
|
|
1157
|
+
return w.count * recencyDecay(w.lastSeen);
|
|
1158
|
+
}
|
|
1159
|
+
function deriveSkillTags(tagWeights) {
|
|
1160
|
+
return Object.entries(tagWeights).filter(([, w]) => w.count >= 1).sort(([, a], [, b]) => tagScore(b) - tagScore(a)).map(([tag]) => tag);
|
|
1161
|
+
}
|
|
1162
|
+
function migrateTagWeights(profile) {
|
|
1163
|
+
if (!profile.tagWeights) {
|
|
1164
|
+
profile.tagWeights = {};
|
|
1165
|
+
}
|
|
1166
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1167
|
+
for (const tag of profile.skillTags) {
|
|
1168
|
+
if (!profile.tagWeights[tag]) {
|
|
1169
|
+
profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
async function readProfile() {
|
|
1174
|
+
if (!existsSync(PROFILE_FILE)) return blankProfile();
|
|
1175
|
+
try {
|
|
1176
|
+
const key = await loadKey();
|
|
1177
|
+
const raw = readFileSync2(PROFILE_FILE, "utf8");
|
|
1178
|
+
const blob = JSON.parse(raw);
|
|
1179
|
+
const plaintext = decrypt(blob, key);
|
|
1180
|
+
const parsed = JSON.parse(plaintext);
|
|
1181
|
+
migrateTagWeights(parsed);
|
|
1182
|
+
return parsed;
|
|
1183
|
+
} catch {
|
|
1184
|
+
return blankProfile();
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
async function writeProfile(profile) {
|
|
1188
|
+
mkdirSync(TERMINALHIRE_DIR, { recursive: true });
|
|
1189
|
+
const key = await loadKey();
|
|
1190
|
+
profile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1191
|
+
profile.skillTags = deriveSkillTags(profile.tagWeights);
|
|
1192
|
+
const blob = encrypt(JSON.stringify(profile), key);
|
|
1193
|
+
writeFileSync(PROFILE_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
|
|
1194
|
+
}
|
|
1195
|
+
function accumulateSession(profile, tags, isEmployerContext2, inferredSeniority) {
|
|
1196
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1197
|
+
let filtered = normalize(tags);
|
|
1198
|
+
if (isEmployerContext2) {
|
|
1199
|
+
filtered = filtered.filter((t) => LANGUAGE_TAGS.has(t));
|
|
1200
|
+
profile.hasEmployerSessions = true;
|
|
1201
|
+
}
|
|
1202
|
+
for (const tag of filtered) {
|
|
1203
|
+
const existing = profile.tagWeights[tag];
|
|
1204
|
+
if (existing) {
|
|
1205
|
+
existing.count += 1;
|
|
1206
|
+
existing.sessions += 1;
|
|
1207
|
+
existing.lastSeen = now;
|
|
1208
|
+
} else {
|
|
1209
|
+
profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
if (inferredSeniority && !isEmployerContext2) {
|
|
1213
|
+
profile.seniority = inferredSeniority;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
async function accumulateTags(rawTokens, isEmployerContext2, inferredSeniority) {
|
|
1217
|
+
const profile = await readProfile();
|
|
1218
|
+
accumulateSession(profile, rawTokens, isEmployerContext2, inferredSeniority);
|
|
1219
|
+
await writeProfile(profile);
|
|
1220
|
+
}
|
|
1221
|
+
function accumulateGitHubTags(profile, tags) {
|
|
1222
|
+
accumulateSession(
|
|
1223
|
+
profile,
|
|
1224
|
+
tags,
|
|
1225
|
+
/* isEmployerContext */
|
|
1226
|
+
false
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
async function listSavedJobs() {
|
|
1230
|
+
const profile = await readProfile();
|
|
1231
|
+
return profile.savedJobs ?? [];
|
|
1232
|
+
}
|
|
1233
|
+
async function addSavedJob(job) {
|
|
1234
|
+
const profile = await readProfile();
|
|
1235
|
+
const existing = profile.savedJobs ?? [];
|
|
1236
|
+
const filtered = existing.filter((j) => j.id !== job.id);
|
|
1237
|
+
profile.savedJobs = [...filtered, { ...job, savedAt: (/* @__PURE__ */ new Date()).toISOString() }];
|
|
1238
|
+
await writeProfile(profile);
|
|
1239
|
+
}
|
|
1240
|
+
async function removeSavedJob(id) {
|
|
1241
|
+
const profile = await readProfile();
|
|
1242
|
+
const existing = profile.savedJobs ?? [];
|
|
1243
|
+
const filtered = existing.filter((j) => j.id !== id);
|
|
1244
|
+
if (filtered.length === existing.length) return false;
|
|
1245
|
+
profile.savedJobs = filtered;
|
|
1246
|
+
await writeProfile(profile);
|
|
1247
|
+
return true;
|
|
1248
|
+
}
|
|
1249
|
+
async function deleteProfile() {
|
|
1250
|
+
const { rmSync } = await import("fs");
|
|
1251
|
+
try {
|
|
1252
|
+
rmSync(PROFILE_FILE);
|
|
1253
|
+
} catch {
|
|
1254
|
+
}
|
|
1255
|
+
try {
|
|
1256
|
+
rmSync(KEY_FILE);
|
|
1257
|
+
} catch {
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
function profileToFingerprint(profile) {
|
|
1261
|
+
const rankedTags = Object.entries(profile.tagWeights).map(([tag, w]) => ({ tag, score: tagScore(w) })).filter(({ score }) => score >= MIN_FINGERPRINT_SCORE).sort((a, b) => b.score - a.score).map(({ tag }) => tag);
|
|
1262
|
+
const skillTags = rankedTags.length > 0 ? rankedTags : profile.skillTags;
|
|
1263
|
+
return {
|
|
1264
|
+
skillTags,
|
|
1265
|
+
seniorityBand: profile.seniority,
|
|
1266
|
+
prefs: {
|
|
1267
|
+
roleTypes: profile.roleTypes,
|
|
1268
|
+
remoteOnly: profile.remoteOnly,
|
|
1269
|
+
compFloorUsd: profile.compFloorUsd
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
var TERMINALHIRE_DIR, PROFILE_FILE, KEY_FILE, ALGO, KEY_BYTES, IV_BYTES, DECAY_HALF_LIFE_MS, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
|
|
1274
|
+
var init_profile = __esm({
|
|
1275
|
+
"src/profile.ts"() {
|
|
1276
|
+
"use strict";
|
|
1277
|
+
init_src();
|
|
1278
|
+
TERMINALHIRE_DIR = join2(homedir(), ".terminalhire");
|
|
1279
|
+
PROFILE_FILE = join2(TERMINALHIRE_DIR, "profile.enc");
|
|
1280
|
+
KEY_FILE = join2(TERMINALHIRE_DIR, "key");
|
|
1281
|
+
ALGO = "aes-256-gcm";
|
|
1282
|
+
KEY_BYTES = 32;
|
|
1283
|
+
IV_BYTES = 12;
|
|
1284
|
+
DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
1285
|
+
LANGUAGE_TAGS = /* @__PURE__ */ new Set([
|
|
1286
|
+
"typescript",
|
|
1287
|
+
"javascript",
|
|
1288
|
+
"python",
|
|
1289
|
+
"go",
|
|
1290
|
+
"rust",
|
|
1291
|
+
"java",
|
|
1292
|
+
"ruby",
|
|
1293
|
+
"elixir",
|
|
1294
|
+
"scala",
|
|
1295
|
+
"kotlin",
|
|
1296
|
+
"swift",
|
|
1297
|
+
"cpp",
|
|
1298
|
+
"csharp",
|
|
1299
|
+
"php",
|
|
1300
|
+
"haskell",
|
|
1301
|
+
"clojure",
|
|
1302
|
+
"r"
|
|
1303
|
+
]);
|
|
1304
|
+
MIN_FINGERPRINT_SCORE = 0.05;
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
// bin/spinner.js
|
|
1309
|
+
var spinner_exports = {};
|
|
1310
|
+
__export(spinner_exports, {
|
|
1311
|
+
SPINNER_DEFAULTS: () => SPINNER_DEFAULTS,
|
|
1312
|
+
applySpinnerTips: () => applySpinnerTips,
|
|
1313
|
+
applySpinnerVerbs: () => applySpinnerVerbs,
|
|
1314
|
+
buildContextVerbs: () => buildContextVerbs,
|
|
1315
|
+
buildSpinnerPool: () => buildSpinnerPool,
|
|
1316
|
+
buildTips: () => buildTips,
|
|
1317
|
+
clearSpinnerTips: () => clearSpinnerTips,
|
|
1318
|
+
clearSpinnerVerbs: () => clearSpinnerVerbs,
|
|
1319
|
+
ctaVerb: () => ctaVerb,
|
|
1320
|
+
formatVerbs: () => formatVerbs,
|
|
1321
|
+
rankBySessionTags: () => rankBySessionTags,
|
|
1322
|
+
readSpinnerConfig: () => readSpinnerConfig
|
|
1323
|
+
});
|
|
1324
|
+
import {
|
|
1325
|
+
readFileSync as readFileSync3,
|
|
1326
|
+
writeFileSync as writeFileSync2,
|
|
1327
|
+
existsSync as existsSync2,
|
|
1328
|
+
mkdirSync as mkdirSync2,
|
|
1329
|
+
renameSync
|
|
1330
|
+
} from "fs";
|
|
1331
|
+
import { join as join3, dirname } from "path";
|
|
1332
|
+
import { homedir as homedir2 } from "os";
|
|
1333
|
+
function readJson(path, fallback) {
|
|
1334
|
+
try {
|
|
1335
|
+
return existsSync2(path) ? JSON.parse(readFileSync3(path, "utf8")) : fallback;
|
|
1336
|
+
} catch {
|
|
1337
|
+
return fallback;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
function atomicWriteJson(path, obj) {
|
|
1341
|
+
mkdirSync2(dirname(path), { recursive: true });
|
|
1342
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
1343
|
+
writeFileSync2(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
|
|
1344
|
+
renameSync(tmp, path);
|
|
1345
|
+
}
|
|
1346
|
+
function titleCase(s) {
|
|
1347
|
+
return String(s || "").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1348
|
+
}
|
|
1349
|
+
function readSpinnerConfig() {
|
|
1350
|
+
const cfg = readJson(CONFIG_FILE, {});
|
|
1351
|
+
const spinner = cfg && typeof cfg.spinner === "object" ? cfg.spinner : {};
|
|
1352
|
+
const merged = { ...SPINNER_DEFAULTS, ...spinner };
|
|
1353
|
+
if (merged.mode !== "append" && merged.mode !== "replace") merged.mode = SPINNER_DEFAULTS.mode;
|
|
1354
|
+
merged.max = Math.max(1, Math.min(12, Number(merged.max) || SPINNER_DEFAULTS.max));
|
|
1355
|
+
merged.enabled = merged.enabled === true;
|
|
1356
|
+
if (!["always", "sometimes", "rare"].includes(merged.frequency)) {
|
|
1357
|
+
merged.frequency = SPINNER_DEFAULTS.frequency;
|
|
1358
|
+
}
|
|
1359
|
+
return merged;
|
|
1360
|
+
}
|
|
1361
|
+
function ctaVerb() {
|
|
1362
|
+
return "\u2605 jobs that fit you \xB7 run: terminalhire jobs";
|
|
1363
|
+
}
|
|
1364
|
+
function formatVerbs(topMatches, max = 6) {
|
|
1365
|
+
const out = [];
|
|
1366
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1367
|
+
for (const m of Array.isArray(topMatches) ? topMatches : []) {
|
|
1368
|
+
if (!m || !m.title || !m.company) continue;
|
|
1369
|
+
let title = String(m.title).trim().replace(/\s+/g, " ");
|
|
1370
|
+
if (title.length > 32) title = title.slice(0, 31).trimEnd() + "\u2026";
|
|
1371
|
+
const company = titleCase(String(m.company).trim().replace(/\s+/g, " "));
|
|
1372
|
+
const pct = Math.max(1, Math.min(99, Math.round((Number(m.score) || 0) * 100)));
|
|
1373
|
+
const key = `${title.toLowerCase()}@${company.toLowerCase()}`;
|
|
1374
|
+
if (seen.has(key)) continue;
|
|
1375
|
+
seen.add(key);
|
|
1376
|
+
const intro = VERB_INTROS[out.length % VERB_INTROS.length];
|
|
1377
|
+
out.push(`${intro} ${title} @ ${company} \xB7 ${pct}% match`);
|
|
1378
|
+
if (out.length >= max) break;
|
|
1379
|
+
}
|
|
1380
|
+
return out;
|
|
1381
|
+
}
|
|
1382
|
+
function rankBySessionTags(topMatches, sessionTags) {
|
|
1383
|
+
const tags = Array.isArray(sessionTags) ? sessionTags.filter(Boolean) : [];
|
|
1384
|
+
if (tags.length === 0 || !Array.isArray(topMatches)) return topMatches;
|
|
1385
|
+
const normalized = tags.map((t) => String(t).toLowerCase().trim());
|
|
1386
|
+
return topMatches.map((m, originalIndex) => {
|
|
1387
|
+
const haystack = `${String(m.title || "").toLowerCase()} ${String(m.company || "").toLowerCase()}`;
|
|
1388
|
+
const hits = normalized.reduce((n, tag) => n + (haystack.includes(tag) ? 1 : 0), 0);
|
|
1389
|
+
return { m, hits, originalIndex };
|
|
1390
|
+
}).sort((a, b) => b.hits - a.hits || a.originalIndex - b.originalIndex).map(({ m }) => m);
|
|
1391
|
+
}
|
|
1392
|
+
function verbCountForFrequency(frequency, max) {
|
|
1393
|
+
switch (frequency) {
|
|
1394
|
+
case "always":
|
|
1395
|
+
return max;
|
|
1396
|
+
case "rare":
|
|
1397
|
+
return 1;
|
|
1398
|
+
case "sometimes":
|
|
1399
|
+
default:
|
|
1400
|
+
return 2;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
function buildContextVerbs(topMatches, sessionTags) {
|
|
1404
|
+
const sess = (Array.isArray(sessionTags) ? sessionTags : []).map((t) => String(t).toLowerCase().trim()).filter(Boolean);
|
|
1405
|
+
const roleTags = /* @__PURE__ */ new Set();
|
|
1406
|
+
for (const m of Array.isArray(topMatches) ? topMatches : []) {
|
|
1407
|
+
const mt = m && Array.isArray(m.matchedTags) ? m.matchedTags : [];
|
|
1408
|
+
for (const t of mt) roleTags.add(String(t).toLowerCase().trim());
|
|
1409
|
+
}
|
|
1410
|
+
const overlap = [];
|
|
1411
|
+
for (const t of sess) {
|
|
1412
|
+
if (roleTags.has(t) && !overlap.includes(t)) overlap.push(t);
|
|
1413
|
+
}
|
|
1414
|
+
if (overlap.length >= 2) {
|
|
1415
|
+
const a = titleCase(overlap[0]);
|
|
1416
|
+
const b = titleCase(overlap[1]);
|
|
1417
|
+
return [`\u2726 Fits your ${a} + ${b} work`, `\u2726 A role matching what you're building`];
|
|
1418
|
+
}
|
|
1419
|
+
if (overlap.length === 1) {
|
|
1420
|
+
const a = titleCase(overlap[0]);
|
|
1421
|
+
return [`\u2726 A role matching your ${a} work`, `\u2726 Your ${a} work \u2014 link in the tip below`];
|
|
1422
|
+
}
|
|
1423
|
+
return [`\u2726 A role that fits your work`, `\u2726 Job match for you \u2014 link in the tip below`];
|
|
1424
|
+
}
|
|
1425
|
+
function buildSpinnerPool(topMatches, max = 6, opts = {}) {
|
|
1426
|
+
const { sessionTags, frequency = "always" } = opts;
|
|
1427
|
+
const ranked = rankBySessionTags(topMatches, sessionTags);
|
|
1428
|
+
if (!Array.isArray(ranked) || ranked.length === 0) return [];
|
|
1429
|
+
const headers = buildContextVerbs(ranked, sessionTags);
|
|
1430
|
+
const cap = Math.max(1, verbCountForFrequency(frequency, headers.length));
|
|
1431
|
+
return [...headers.slice(0, cap), ctaVerb()];
|
|
1432
|
+
}
|
|
1433
|
+
function readState() {
|
|
1434
|
+
return readJson(SPINNER_STATE_FILE, { verbs: [], mode: "replace" });
|
|
1435
|
+
}
|
|
1436
|
+
function applySpinnerVerbs(ourVerbs, mode = "replace") {
|
|
1437
|
+
const verbs = (Array.isArray(ourVerbs) ? ourVerbs : []).filter(Boolean);
|
|
1438
|
+
if (verbs.length === 0) return clearSpinnerVerbs();
|
|
1439
|
+
const settings = readJson(CLAUDE_SETTINGS, {}) || {};
|
|
1440
|
+
const existing = settings.spinnerVerbs && typeof settings.spinnerVerbs === "object" ? settings.spinnerVerbs : null;
|
|
1441
|
+
const prevOurs = new Set(readState().verbs || []);
|
|
1442
|
+
const userVerbs = existing && Array.isArray(existing.verbs) ? existing.verbs.filter((v) => !prevOurs.has(v)) : [];
|
|
1443
|
+
const newVerbs = [...verbs, ...userVerbs];
|
|
1444
|
+
settings.spinnerVerbs = { mode: mode === "append" ? "append" : "replace", verbs: newVerbs };
|
|
1445
|
+
atomicWriteJson(CLAUDE_SETTINGS, settings);
|
|
1446
|
+
const st = readState();
|
|
1447
|
+
atomicWriteJson(SPINNER_STATE_FILE, { ...st, verbs, mode, ts: Date.now() });
|
|
1448
|
+
return { applied: verbs.length, total: newVerbs.length };
|
|
1449
|
+
}
|
|
1450
|
+
function clearSpinnerVerbs() {
|
|
1451
|
+
const settings = readJson(CLAUDE_SETTINGS, null);
|
|
1452
|
+
const prevOurs = new Set(readState().verbs || []);
|
|
1453
|
+
let keptUserVerbs = 0;
|
|
1454
|
+
if (settings && settings.spinnerVerbs && Array.isArray(settings.spinnerVerbs.verbs)) {
|
|
1455
|
+
const userVerbs = settings.spinnerVerbs.verbs.filter((v) => !prevOurs.has(v));
|
|
1456
|
+
keptUserVerbs = userVerbs.length;
|
|
1457
|
+
if (userVerbs.length > 0) {
|
|
1458
|
+
settings.spinnerVerbs = {
|
|
1459
|
+
mode: settings.spinnerVerbs.mode === "append" ? "append" : "replace",
|
|
1460
|
+
verbs: userVerbs
|
|
1461
|
+
};
|
|
1462
|
+
} else {
|
|
1463
|
+
delete settings.spinnerVerbs;
|
|
1464
|
+
}
|
|
1465
|
+
atomicWriteJson(CLAUDE_SETTINGS, settings);
|
|
1466
|
+
}
|
|
1467
|
+
try {
|
|
1468
|
+
const st = readState();
|
|
1469
|
+
atomicWriteJson(SPINNER_STATE_FILE, { ...st, verbs: [], mode: st.mode || "replace", ts: Date.now() });
|
|
1470
|
+
} catch {
|
|
1471
|
+
}
|
|
1472
|
+
return { cleared: true, keptUserVerbs };
|
|
1473
|
+
}
|
|
1474
|
+
function buildTips(topMatches, baseUrl, max = 8) {
|
|
1475
|
+
const base = String(baseUrl || "https://terminalhire.com").replace(/\/+$/, "");
|
|
1476
|
+
const out = [];
|
|
1477
|
+
const seenRole = /* @__PURE__ */ new Set();
|
|
1478
|
+
const perCompany = /* @__PURE__ */ new Map();
|
|
1479
|
+
const COMPANY_CAP = 2;
|
|
1480
|
+
for (const m of Array.isArray(topMatches) ? topMatches : []) {
|
|
1481
|
+
if (!m || !m.title || !m.company || !m.id) continue;
|
|
1482
|
+
const idx = String(m.id).indexOf(":");
|
|
1483
|
+
if (idx <= 0) continue;
|
|
1484
|
+
const source = String(m.id).slice(0, idx);
|
|
1485
|
+
const ext = String(m.id).slice(idx + 1);
|
|
1486
|
+
if (!source || !ext) continue;
|
|
1487
|
+
const companyRaw = String(m.company).trim().replace(/\s+/g, " ");
|
|
1488
|
+
const titleRaw = String(m.title).trim().replace(/\s+/g, " ");
|
|
1489
|
+
const roleKey = `${titleRaw.toLowerCase()}@${companyRaw.toLowerCase()}`;
|
|
1490
|
+
const coKey = companyRaw.toLowerCase();
|
|
1491
|
+
if (seenRole.has(roleKey)) continue;
|
|
1492
|
+
if ((perCompany.get(coKey) || 0) >= COMPANY_CAP) continue;
|
|
1493
|
+
seenRole.add(roleKey);
|
|
1494
|
+
perCompany.set(coKey, (perCompany.get(coKey) || 0) + 1);
|
|
1495
|
+
let title = titleRaw;
|
|
1496
|
+
if (title.length > 34) title = title.slice(0, 33).trimEnd() + "\u2026";
|
|
1497
|
+
const company = titleCase(companyRaw);
|
|
1498
|
+
const pct = Math.max(1, Math.min(99, Math.round((Number(m.score) || 0) * 100)));
|
|
1499
|
+
const token = Buffer.from(String(m.id)).toString("base64url");
|
|
1500
|
+
const url = `${base}/j/${token}`;
|
|
1501
|
+
out.push(`\u2197 ${title} @ ${company} \xB7 ${pct}% \u2014 ${url}`);
|
|
1502
|
+
if (out.length >= max) break;
|
|
1503
|
+
}
|
|
1504
|
+
return out;
|
|
1505
|
+
}
|
|
1506
|
+
function applySpinnerTips(ourTips) {
|
|
1507
|
+
const tips = (Array.isArray(ourTips) ? ourTips : []).filter(Boolean);
|
|
1508
|
+
if (tips.length === 0) return clearSpinnerTips();
|
|
1509
|
+
const settings = readJson(CLAUDE_SETTINGS, {}) || {};
|
|
1510
|
+
const existing = settings.spinnerTipsOverride && Array.isArray(settings.spinnerTipsOverride.tips) ? settings.spinnerTipsOverride.tips : [];
|
|
1511
|
+
const prevOurs = new Set(readState().tips || []);
|
|
1512
|
+
const userTips = existing.filter((t) => !prevOurs.has(t));
|
|
1513
|
+
settings.spinnerTipsEnabled = true;
|
|
1514
|
+
settings.spinnerTipsOverride = { excludeDefault: true, tips: [...tips, ...userTips] };
|
|
1515
|
+
atomicWriteJson(CLAUDE_SETTINGS, settings);
|
|
1516
|
+
const st = readState();
|
|
1517
|
+
atomicWriteJson(SPINNER_STATE_FILE, { ...st, tips, ts: Date.now() });
|
|
1518
|
+
return { applied: tips.length };
|
|
1519
|
+
}
|
|
1520
|
+
function clearSpinnerTips() {
|
|
1521
|
+
const settings = readJson(CLAUDE_SETTINGS, null);
|
|
1522
|
+
const prevOurs = new Set(readState().tips || []);
|
|
1523
|
+
if (settings && settings.spinnerTipsOverride && Array.isArray(settings.spinnerTipsOverride.tips)) {
|
|
1524
|
+
const userTips = settings.spinnerTipsOverride.tips.filter((t) => !prevOurs.has(t));
|
|
1525
|
+
if (userTips.length > 0) {
|
|
1526
|
+
settings.spinnerTipsOverride = {
|
|
1527
|
+
excludeDefault: settings.spinnerTipsOverride.excludeDefault === true,
|
|
1528
|
+
tips: userTips
|
|
1529
|
+
};
|
|
1530
|
+
} else {
|
|
1531
|
+
delete settings.spinnerTipsOverride;
|
|
1532
|
+
delete settings.spinnerTipsEnabled;
|
|
1533
|
+
}
|
|
1534
|
+
atomicWriteJson(CLAUDE_SETTINGS, settings);
|
|
1535
|
+
}
|
|
1536
|
+
try {
|
|
1537
|
+
const st = readState();
|
|
1538
|
+
atomicWriteJson(SPINNER_STATE_FILE, { ...st, tips: [], ts: Date.now() });
|
|
1539
|
+
} catch {
|
|
1540
|
+
}
|
|
1541
|
+
return { cleared: true };
|
|
1542
|
+
}
|
|
1543
|
+
var TH_DIR, CLAUDE_SETTINGS, CONFIG_FILE, SPINNER_STATE_FILE, SPINNER_DEFAULTS, VERB_INTROS;
|
|
1544
|
+
var init_spinner = __esm({
|
|
1545
|
+
"bin/spinner.js"() {
|
|
1546
|
+
"use strict";
|
|
1547
|
+
TH_DIR = process.env["TERMINALHIRE_DIR"] || join3(homedir2(), ".terminalhire");
|
|
1548
|
+
CLAUDE_SETTINGS = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join3(homedir2(), ".claude", "settings.json");
|
|
1549
|
+
CONFIG_FILE = join3(TH_DIR, "config.json");
|
|
1550
|
+
SPINNER_STATE_FILE = join3(TH_DIR, "spinner-state.json");
|
|
1551
|
+
SPINNER_DEFAULTS = { enabled: false, mode: "append", max: 6, frequency: "sometimes" };
|
|
1552
|
+
VERB_INTROS = ["Matched:", "You\u2019d fit:", "Worth a look:", "On your radar:", "Fits your stack:"];
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
// src/signal.ts
|
|
1557
|
+
var signal_exports = {};
|
|
1558
|
+
__export(signal_exports, {
|
|
1559
|
+
extractFingerprint: () => extractFingerprint
|
|
1560
|
+
});
|
|
1561
|
+
import { readFileSync as readFileSync4, readdirSync } from "fs";
|
|
1562
|
+
import { execSync } from "child_process";
|
|
1563
|
+
import { join as join4 } from "path";
|
|
1564
|
+
function safeExec(cmd) {
|
|
1565
|
+
try {
|
|
1566
|
+
return execSync(cmd, { timeout: 2e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
1567
|
+
} catch {
|
|
1568
|
+
return "";
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
function isEmployerContext(cwd) {
|
|
1572
|
+
const remote = safeExec('git -C "' + cwd + '" remote get-url origin 2>/dev/null');
|
|
1573
|
+
if (remote) {
|
|
1574
|
+
try {
|
|
1575
|
+
const sshMatch = remote.match(/^git@([^:]+):/);
|
|
1576
|
+
const httpsMatch = remote.match(/^https?:\/\/([^/]+)/);
|
|
1577
|
+
const host = (sshMatch?.[1] ?? httpsMatch?.[1] ?? "").toLowerCase();
|
|
1578
|
+
if (host && !PERSONAL_GIT_HOSTS.has(host)) {
|
|
1579
|
+
return true;
|
|
1580
|
+
}
|
|
1581
|
+
} catch {
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
const email = safeExec('git -C "' + cwd + '" config user.email 2>/dev/null');
|
|
1585
|
+
if (email) {
|
|
1586
|
+
const domain = email.split("@")[1]?.toLowerCase() ?? "";
|
|
1587
|
+
if (domain && !PERSONAL_EMAIL_DOMAINS.has(domain)) {
|
|
1588
|
+
return true;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
return false;
|
|
1592
|
+
}
|
|
1593
|
+
function readJsonSafe(path) {
|
|
1594
|
+
try {
|
|
1595
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
1596
|
+
} catch {
|
|
1597
|
+
return null;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
function readFileSafe(path) {
|
|
1601
|
+
try {
|
|
1602
|
+
return readFileSync4(path, "utf8");
|
|
1603
|
+
} catch {
|
|
1604
|
+
return "";
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
function tokensFromPackageJson(cwd) {
|
|
1608
|
+
const pkg = readJsonSafe(join4(cwd, "package.json"));
|
|
1609
|
+
if (!pkg || typeof pkg !== "object") return [];
|
|
1610
|
+
const p = pkg;
|
|
1611
|
+
const deps = {
|
|
1612
|
+
...typeof p["dependencies"] === "object" ? p["dependencies"] : {},
|
|
1613
|
+
...typeof p["devDependencies"] === "object" ? p["devDependencies"] : {}
|
|
1614
|
+
};
|
|
1615
|
+
return Object.keys(deps);
|
|
1616
|
+
}
|
|
1617
|
+
function tokensFromRequirementsTxt(cwd) {
|
|
1618
|
+
const content = readFileSafe(join4(cwd, "requirements.txt"));
|
|
1619
|
+
if (!content) return [];
|
|
1620
|
+
return content.split("\n").map((l) => l.trim().split(/[>=<!\[;]/)[0].trim().toLowerCase()).filter(Boolean);
|
|
1621
|
+
}
|
|
1622
|
+
function tokensFromGoMod(cwd) {
|
|
1623
|
+
const content = readFileSafe(join4(cwd, "go.mod"));
|
|
1624
|
+
if (!content) return ["go"];
|
|
1625
|
+
const requires = Array.from(content.matchAll(/^\s+([^\s]+)\s+v/gm)).map((m) => m[1].split("/").pop() ?? "").filter(Boolean);
|
|
1626
|
+
return ["go", ...requires];
|
|
1627
|
+
}
|
|
1628
|
+
function tokensFromCargoToml(cwd) {
|
|
1629
|
+
const content = readFileSafe(join4(cwd, "Cargo.toml"));
|
|
1630
|
+
if (!content) return [];
|
|
1631
|
+
const deps = Array.from(content.matchAll(/^([a-zA-Z0-9_-]+)\s*=/gm)).map((m) => m[1].toLowerCase());
|
|
1632
|
+
return ["rust", ...deps];
|
|
1633
|
+
}
|
|
1634
|
+
function tokensFromFileExtensions(cwd) {
|
|
1635
|
+
const tokens = [];
|
|
1636
|
+
const scanDirs = [cwd];
|
|
1637
|
+
try {
|
|
1638
|
+
const srcDir = join4(cwd, "src");
|
|
1639
|
+
readdirSync(srcDir);
|
|
1640
|
+
scanDirs.push(srcDir);
|
|
1641
|
+
} catch {
|
|
1642
|
+
}
|
|
1643
|
+
for (const dir of scanDirs) {
|
|
1644
|
+
try {
|
|
1645
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
1646
|
+
for (const e of entries) {
|
|
1647
|
+
if (!e.isFile()) continue;
|
|
1648
|
+
const dotIdx = e.name.lastIndexOf(".");
|
|
1649
|
+
if (dotIdx === -1) continue;
|
|
1650
|
+
const ext = e.name.slice(dotIdx).toLowerCase();
|
|
1651
|
+
const tag = EXT_MAP[ext];
|
|
1652
|
+
if (tag) tokens.push(tag);
|
|
1653
|
+
}
|
|
1654
|
+
} catch {
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
return tokens;
|
|
1658
|
+
}
|
|
1659
|
+
function inferSeniority3(rawTokens) {
|
|
1660
|
+
const seniorSignals = /* @__PURE__ */ new Set([
|
|
1661
|
+
"kubernetes",
|
|
1662
|
+
"terraform",
|
|
1663
|
+
"pulumi",
|
|
1664
|
+
"kafka",
|
|
1665
|
+
"spark",
|
|
1666
|
+
"airflow",
|
|
1667
|
+
"dbt",
|
|
1668
|
+
"opentelemetry",
|
|
1669
|
+
"prometheus",
|
|
1670
|
+
"grafana",
|
|
1671
|
+
"microservices",
|
|
1672
|
+
"api-design",
|
|
1673
|
+
"security",
|
|
1674
|
+
"oauth",
|
|
1675
|
+
"payments"
|
|
1676
|
+
]);
|
|
1677
|
+
const midSignals = /* @__PURE__ */ new Set([
|
|
1678
|
+
"docker",
|
|
1679
|
+
"ci-cd",
|
|
1680
|
+
"github-actions",
|
|
1681
|
+
"testing",
|
|
1682
|
+
"postgresql",
|
|
1683
|
+
"redis",
|
|
1684
|
+
"graphql",
|
|
1685
|
+
"trpc"
|
|
1686
|
+
]);
|
|
1687
|
+
const normalized = new Set(normalize(rawTokens));
|
|
1688
|
+
const seniorHits = [...normalized].filter((t) => seniorSignals.has(t)).length;
|
|
1689
|
+
const midHits = [...normalized].filter((t) => midSignals.has(t)).length;
|
|
1690
|
+
if (seniorHits >= 2) return "senior";
|
|
1691
|
+
if (seniorHits >= 1 || midHits >= 2) return "mid";
|
|
1692
|
+
return void 0;
|
|
1693
|
+
}
|
|
1694
|
+
function extractFingerprint(cwd) {
|
|
1695
|
+
const employer = isEmployerContext(cwd);
|
|
1696
|
+
const rawTokens = [
|
|
1697
|
+
...tokensFromPackageJson(cwd),
|
|
1698
|
+
...tokensFromRequirementsTxt(cwd),
|
|
1699
|
+
...tokensFromGoMod(cwd),
|
|
1700
|
+
...tokensFromCargoToml(cwd),
|
|
1701
|
+
...tokensFromFileExtensions(cwd)
|
|
1702
|
+
];
|
|
1703
|
+
let skillTags = normalize(rawTokens);
|
|
1704
|
+
if (employer) {
|
|
1705
|
+
skillTags = skillTags.filter((t) => LANGUAGE_TAGS2.has(t));
|
|
1706
|
+
}
|
|
1707
|
+
const seniorityBand = employer ? void 0 : inferSeniority3(rawTokens);
|
|
1708
|
+
return {
|
|
1709
|
+
skillTags,
|
|
1710
|
+
seniorityBand,
|
|
1711
|
+
employerContext: employer
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
var LANGUAGE_TAGS2, EXT_MAP, PERSONAL_GIT_HOSTS, PERSONAL_EMAIL_DOMAINS;
|
|
1715
|
+
var init_signal = __esm({
|
|
1716
|
+
"src/signal.ts"() {
|
|
1717
|
+
"use strict";
|
|
1718
|
+
init_src();
|
|
1719
|
+
LANGUAGE_TAGS2 = /* @__PURE__ */ new Set([
|
|
1720
|
+
"typescript",
|
|
1721
|
+
"javascript",
|
|
1722
|
+
"python",
|
|
1723
|
+
"go",
|
|
1724
|
+
"rust",
|
|
1725
|
+
"java",
|
|
1726
|
+
"ruby",
|
|
1727
|
+
"elixir",
|
|
1728
|
+
"scala",
|
|
1729
|
+
"kotlin",
|
|
1730
|
+
"swift",
|
|
1731
|
+
"cpp",
|
|
1732
|
+
"csharp",
|
|
1733
|
+
"php",
|
|
1734
|
+
"haskell",
|
|
1735
|
+
"clojure",
|
|
1736
|
+
"r"
|
|
1737
|
+
]);
|
|
1738
|
+
EXT_MAP = {
|
|
1739
|
+
".ts": "typescript",
|
|
1740
|
+
".tsx": "typescript",
|
|
1741
|
+
".js": "javascript",
|
|
1742
|
+
".mjs": "javascript",
|
|
1743
|
+
".cjs": "javascript",
|
|
1744
|
+
".jsx": "javascript",
|
|
1745
|
+
".py": "python",
|
|
1746
|
+
".go": "go",
|
|
1747
|
+
".rs": "rust",
|
|
1748
|
+
".java": "java",
|
|
1749
|
+
".rb": "ruby",
|
|
1750
|
+
".ex": "elixir",
|
|
1751
|
+
".exs": "elixir",
|
|
1752
|
+
".scala": "scala",
|
|
1753
|
+
".kt": "kotlin",
|
|
1754
|
+
".swift": "swift",
|
|
1755
|
+
".cpp": "cpp",
|
|
1756
|
+
".cc": "cpp",
|
|
1757
|
+
".cxx": "cpp",
|
|
1758
|
+
".hpp": "cpp",
|
|
1759
|
+
".cs": "csharp",
|
|
1760
|
+
".php": "php",
|
|
1761
|
+
".hs": "haskell",
|
|
1762
|
+
".clj": "clojure",
|
|
1763
|
+
".cljs": "clojure",
|
|
1764
|
+
".r": "r",
|
|
1765
|
+
".vue": "vue",
|
|
1766
|
+
".svelte": "svelte"
|
|
1767
|
+
};
|
|
1768
|
+
PERSONAL_GIT_HOSTS = /* @__PURE__ */ new Set([
|
|
1769
|
+
"github.com",
|
|
1770
|
+
"gitlab.com",
|
|
1771
|
+
"bitbucket.org",
|
|
1772
|
+
"codeberg.org",
|
|
1773
|
+
"sr.ht"
|
|
1774
|
+
]);
|
|
1775
|
+
PERSONAL_EMAIL_DOMAINS = /* @__PURE__ */ new Set([
|
|
1776
|
+
"gmail.com",
|
|
1777
|
+
"googlemail.com",
|
|
1778
|
+
"yahoo.com",
|
|
1779
|
+
"outlook.com",
|
|
1780
|
+
"hotmail.com",
|
|
1781
|
+
"icloud.com",
|
|
1782
|
+
"me.com",
|
|
1783
|
+
"mac.com",
|
|
1784
|
+
"proton.me",
|
|
1785
|
+
"protonmail.com",
|
|
1786
|
+
"fastmail.com",
|
|
1787
|
+
"hey.com",
|
|
1788
|
+
"duck.com"
|
|
1789
|
+
]);
|
|
1790
|
+
}
|
|
1791
|
+
});
|
|
1792
|
+
|
|
1793
|
+
// bin/jpi-refresh.js
|
|
1794
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
1795
|
+
import { join as join5 } from "path";
|
|
1796
|
+
import { homedir as homedir3 } from "os";
|
|
1797
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1798
|
+
var __dirname = fileURLToPath2(new URL(".", import.meta.url));
|
|
1799
|
+
var TERMINALHIRE_DIR2 = join5(homedir3(), ".terminalhire");
|
|
1800
|
+
var INDEX_CACHE_FILE = join5(TERMINALHIRE_DIR2, "index-cache.json");
|
|
1801
|
+
var API_URL = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
|
|
1802
|
+
async function run() {
|
|
1803
|
+
try {
|
|
1804
|
+
let index;
|
|
1805
|
+
try {
|
|
1806
|
+
const res = await fetch(`${API_URL}/api/index`, {
|
|
1807
|
+
signal: AbortSignal.timeout(15e3),
|
|
1808
|
+
headers: { "Accept": "application/json" }
|
|
1809
|
+
});
|
|
1810
|
+
if (!res.ok) {
|
|
1811
|
+
process.stderr.write(`terminalhire refresh: index fetch failed (HTTP ${res.status})
|
|
1812
|
+
`);
|
|
1813
|
+
process.exit(1);
|
|
1814
|
+
}
|
|
1815
|
+
index = await res.json();
|
|
1816
|
+
} catch (err) {
|
|
1817
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1818
|
+
process.stderr.write(`terminalhire refresh: fetch error \u2014 ${msg}
|
|
1819
|
+
`);
|
|
1820
|
+
process.exit(1);
|
|
1821
|
+
}
|
|
1822
|
+
const jobs = index?.jobs ?? [];
|
|
1823
|
+
let matchCount = 0;
|
|
1824
|
+
let topMatches = [];
|
|
1825
|
+
try {
|
|
1826
|
+
const { readProfile: readProfile2, profileToFingerprint: profileToFingerprint2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
1827
|
+
const { match: match2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
1828
|
+
const profile = await readProfile2();
|
|
1829
|
+
if (profile.skillTags.length > 0 && jobs.length > 0) {
|
|
1830
|
+
const fp = profileToFingerprint2(profile);
|
|
1831
|
+
const results = match2(fp, jobs, jobs.length);
|
|
1832
|
+
matchCount = results.length;
|
|
1833
|
+
topMatches = results.slice(0, 25).map((r) => ({
|
|
1834
|
+
id: r.job.id,
|
|
1835
|
+
title: r.job.title,
|
|
1836
|
+
company: r.job.company,
|
|
1837
|
+
score: r.score,
|
|
1838
|
+
remote: r.job.remote,
|
|
1839
|
+
matchedTags: r.matchedTags
|
|
1840
|
+
}));
|
|
1841
|
+
}
|
|
1842
|
+
} catch {
|
|
1843
|
+
}
|
|
1844
|
+
mkdirSync3(TERMINALHIRE_DIR2, { recursive: true });
|
|
1845
|
+
const cacheEntry = {
|
|
1846
|
+
ts: Date.now(),
|
|
1847
|
+
index,
|
|
1848
|
+
matchCount,
|
|
1849
|
+
topMatches
|
|
1850
|
+
};
|
|
1851
|
+
writeFileSync3(INDEX_CACHE_FILE, JSON.stringify(cacheEntry), "utf8");
|
|
1852
|
+
try {
|
|
1853
|
+
const {
|
|
1854
|
+
readSpinnerConfig: readSpinnerConfig2,
|
|
1855
|
+
buildSpinnerPool: buildSpinnerPool2,
|
|
1856
|
+
applySpinnerVerbs: applySpinnerVerbs2,
|
|
1857
|
+
clearSpinnerVerbs: clearSpinnerVerbs2,
|
|
1858
|
+
buildTips: buildTips2,
|
|
1859
|
+
applySpinnerTips: applySpinnerTips2,
|
|
1860
|
+
clearSpinnerTips: clearSpinnerTips2,
|
|
1861
|
+
rankBySessionTags: rankBySessionTags2
|
|
1862
|
+
} = await Promise.resolve().then(() => (init_spinner(), spinner_exports));
|
|
1863
|
+
const sc = readSpinnerConfig2();
|
|
1864
|
+
if (sc.enabled) {
|
|
1865
|
+
let sessionTags;
|
|
1866
|
+
try {
|
|
1867
|
+
const { extractFingerprint: extractFingerprint2 } = await Promise.resolve().then(() => (init_signal(), signal_exports));
|
|
1868
|
+
const fp = extractFingerprint2(process.cwd());
|
|
1869
|
+
if (Array.isArray(fp.skillTags) && fp.skillTags.length > 0) {
|
|
1870
|
+
sessionTags = fp.skillTags;
|
|
1871
|
+
}
|
|
1872
|
+
} catch {
|
|
1873
|
+
}
|
|
1874
|
+
const ranked = rankBySessionTags2(topMatches, sessionTags);
|
|
1875
|
+
const verbs = buildSpinnerPool2(ranked, sc.max, { sessionTags, frequency: sc.frequency });
|
|
1876
|
+
if (verbs.length > 0) applySpinnerVerbs2(verbs, sc.mode);
|
|
1877
|
+
else clearSpinnerVerbs2();
|
|
1878
|
+
const tips = buildTips2(ranked, API_URL, 8);
|
|
1879
|
+
if (tips.length > 0) applySpinnerTips2(tips);
|
|
1880
|
+
else clearSpinnerTips2();
|
|
1881
|
+
} else {
|
|
1882
|
+
clearSpinnerVerbs2();
|
|
1883
|
+
clearSpinnerTips2();
|
|
1884
|
+
}
|
|
1885
|
+
} catch {
|
|
1886
|
+
}
|
|
1887
|
+
process.exit(0);
|
|
1888
|
+
} catch (err) {
|
|
1889
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1890
|
+
process.stderr.write(`terminalhire refresh: unexpected error \u2014 ${msg}
|
|
1891
|
+
`);
|
|
1892
|
+
process.exit(1);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
export {
|
|
1896
|
+
run
|
|
1897
|
+
};
|