terminalhire 0.2.4 → 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.
@@ -101,7 +101,7 @@ async function runDeviceFlow() {
101
101
  await writeGitHubToken(MOCK_TOKEN);
102
102
  return MOCK_LOGIN;
103
103
  }
104
- const clientId = process.env["GITHUB_DEVICE_CLIENT_ID"] ?? process.env["GITHUB_CLIENT_ID"] ?? DEV_PLACEHOLDER_CLIENT_ID;
104
+ const clientId = process.env["GITHUB_DEVICE_CLIENT_ID"] ?? process.env["GITHUB_CLIENT_ID"] ?? BAKED_IN_CLIENT_ID;
105
105
  if (clientId === "Iv1.PLACEHOLDER_REGISTER_YOUR_APP") {
106
106
  console.warn("\nWarning: GITHUB_CLIENT_ID env var looks like a placeholder.");
107
107
  console.warn("Remove it to use the baked-in client ID, or set it to your own OAuth App Client ID.\n");
@@ -124,7 +124,7 @@ async function runDeviceFlow() {
124
124
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
125
125
  console.log(` 1. Open: ${deviceData.verification_uri}`);
126
126
  console.log(` 2. Enter code: ${deviceData.user_code}`);
127
- console.log(' 3. Authorize "jpi" (scope: read:user \u2014 public data only)');
127
+ console.log(' 3. Authorize "Terminalhire" (scope: read:user \u2014 public data only)');
128
128
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
129
129
  console.log(" Waiting for authorization...");
130
130
  console.log("");
@@ -204,7 +204,7 @@ async function resolveStoredLogin() {
204
204
  function sleep(ms) {
205
205
  return new Promise((resolve) => setTimeout(resolve, ms));
206
206
  }
207
- var TERMINALHIRE_DIR, TOKEN_FILE, KEY_FILE, ALGO, KEY_BYTES, IV_BYTES, GITHUB_SCOPE, DEVICE_CODE_URL, ACCESS_TOKEN_URL, DEV_PLACEHOLDER_CLIENT_ID, MOCK_TOKEN, MOCK_LOGIN;
207
+ var TERMINALHIRE_DIR, TOKEN_FILE, KEY_FILE, ALGO, KEY_BYTES, IV_BYTES, GITHUB_SCOPE, DEVICE_CODE_URL, ACCESS_TOKEN_URL, BAKED_IN_CLIENT_ID, MOCK_TOKEN, MOCK_LOGIN;
208
208
  var init_github_auth = __esm({
209
209
  "src/github-auth.ts"() {
210
210
  "use strict";
@@ -217,234 +217,376 @@ var init_github_auth = __esm({
217
217
  GITHUB_SCOPE = "read:user";
218
218
  DEVICE_CODE_URL = "https://github.com/login/device/code";
219
219
  ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
220
- DEV_PLACEHOLDER_CLIENT_ID = "Ov23lignE2ZSBe0J3a6B";
220
+ BAKED_IN_CLIENT_ID = "Ov23lignE2ZSBe0J3a6B";
221
221
  MOCK_TOKEN = "mock-github-token-jpi-dev";
222
222
  MOCK_LOGIN = "janedev";
223
223
  }
224
224
  });
225
225
 
226
226
  // ../../packages/core/src/types.ts
227
+ function isBounty(job) {
228
+ return job.source === "bounty" && job.bounty != null;
229
+ }
227
230
  var init_types = __esm({
228
231
  "../../packages/core/src/types.ts"() {
229
232
  "use strict";
230
233
  }
231
234
  });
232
235
 
233
- // ../../packages/core/src/vocabulary.ts
236
+ // ../../packages/core/src/vocab/graph.data.ts
237
+ var VOCAB_NODES;
238
+ var init_graph_data = __esm({
239
+ "../../packages/core/src/vocab/graph.data.ts"() {
240
+ "use strict";
241
+ VOCAB_NODES = [
242
+ // ── Languages ─────────────────────────────────────────────────────────────
243
+ { id: "javascript", synonyms: ["js"], related: [{ to: "typescript", w: 0.6 }] },
244
+ { id: "typescript", parents: ["javascript"], synonyms: ["ts"] },
245
+ { id: "python", synonyms: ["py"] },
246
+ { id: "go", synonyms: ["golang"] },
247
+ { id: "rust" },
248
+ { id: "java", related: [{ to: "kotlin", w: 0.45 }, { to: "scala", w: 0.4 }] },
249
+ { id: "ruby" },
250
+ { id: "elixir" },
251
+ { id: "scala", related: [{ to: "java", w: 0.4 }] },
252
+ { id: "kotlin", related: [{ to: "java", w: 0.45 }] },
253
+ { id: "swift" },
254
+ { id: "cpp", synonyms: ["c++"] },
255
+ { id: "csharp", synonyms: ["c#"] },
256
+ { id: "php" },
257
+ { id: "haskell" },
258
+ { id: "clojure" },
259
+ { id: "r" },
260
+ { id: "dart" },
261
+ // ── Frontend ──────────────────────────────────────────────────────────────
262
+ {
263
+ id: "react",
264
+ parents: ["javascript"],
265
+ synonyms: ["reactjs"],
266
+ related: [{ to: "nextjs", w: 0.55 }, { to: "vue", w: 0.4 }, { to: "svelte", w: 0.4 }, { to: "solidjs", w: 0.5 }, { to: "angular", w: 0.35 }]
267
+ },
268
+ { id: "nextjs", parents: ["react"], synonyms: ["next", "next.js"], related: [{ to: "remix", w: 0.5 }] },
269
+ { id: "vue", parents: ["javascript"], synonyms: ["vue.js"], related: [{ to: "nuxt", w: 0.6 }] },
270
+ { id: "nuxt", parents: ["vue"], synonyms: ["nuxt.js"] },
271
+ { id: "svelte", parents: ["javascript"], related: [{ to: "sveltekit", w: 0.65 }] },
272
+ { id: "sveltekit", parents: ["svelte"] },
273
+ { id: "angular", parents: ["typescript"], synonyms: ["angular.js", "angularjs"] },
274
+ { id: "solidjs", parents: ["javascript"] },
275
+ { id: "remix", parents: ["react"], synonyms: ["remix.run"] },
276
+ { id: "astro", parents: ["javascript"], related: [{ to: "nextjs", w: 0.4 }] },
277
+ { id: "qwik", parents: ["javascript"] },
278
+ { id: "tailwind", parents: ["css"], synonyms: ["tailwindcss", "tw"] },
279
+ { id: "css" },
280
+ { id: "html" },
281
+ { id: "redux", parents: ["react"] },
282
+ { id: "vite", parents: ["frontend"] },
283
+ { id: "webpack", parents: ["frontend"] },
284
+ { id: "storybook", parents: ["frontend"] },
285
+ // ── Backend frameworks / runtimes ───────────────────────────────────────────
286
+ {
287
+ id: "nodejs",
288
+ parents: ["javascript"],
289
+ synonyms: ["node", "node.js"],
290
+ related: [{ to: "express", w: 0.5 }, { to: "fastify", w: 0.45 }, { to: "nestjs", w: 0.45 }]
291
+ },
292
+ { id: "express", parents: ["nodejs"], synonyms: ["express.js", "expressjs"], related: [{ to: "fastify", w: 0.5 }] },
293
+ { id: "fastify", parents: ["nodejs"] },
294
+ { id: "nestjs", parents: ["nodejs"], synonyms: ["nest", "nest.js"] },
295
+ { id: "hono", parents: ["nodejs"] },
296
+ { id: "deno", parents: ["javascript"], related: [{ to: "nodejs", w: 0.5 }, { to: "bun", w: 0.5 }] },
297
+ { id: "bun", parents: ["javascript"], related: [{ to: "nodejs", w: 0.5 }] },
298
+ { id: "django", parents: ["python"], related: [{ to: "flask", w: 0.5 }, { to: "fastapi", w: 0.45 }] },
299
+ { id: "fastapi", parents: ["python"], related: [{ to: "flask", w: 0.55 }, { to: "django", w: 0.45 }] },
300
+ { id: "flask", parents: ["python"] },
301
+ { id: "rails", parents: ["ruby"], synonyms: ["ruby-on-rails", "ror"] },
302
+ { id: "spring", parents: ["java"], synonyms: ["spring-boot", "springboot"] },
303
+ { id: "actix", parents: ["rust"] },
304
+ { id: "gin", parents: ["go"] },
305
+ { id: "phoenix", parents: ["elixir"] },
306
+ { id: "laravel", parents: ["php"] },
307
+ { id: "dotnet", parents: ["csharp"], synonyms: [".net", "asp.net", "dotnet-core"] },
308
+ // ── Infrastructure & DevOps ─────────────────────────────────────────────────
309
+ { id: "kubernetes", synonyms: ["k8s", "kube"], related: [{ to: "docker", w: 0.5 }, { to: "helm", w: 0.55 }, { to: "terraform", w: 0.4 }, { to: "argocd", w: 0.45 }] },
310
+ { id: "docker", parents: ["devops"], related: [{ to: "kubernetes", w: 0.5 }] },
311
+ { id: "terraform", synonyms: ["tf"], related: [{ to: "pulumi", w: 0.55 }, { to: "ansible", w: 0.4 }, { to: "aws", w: 0.4 }] },
312
+ { id: "pulumi", related: [{ to: "terraform", w: 0.55 }] },
313
+ { id: "ansible" },
314
+ { id: "aws", synonyms: ["amazon-web-services"], related: [{ to: "gcp", w: 0.4 }, { to: "azure", w: 0.4 }] },
315
+ { id: "gcp", synonyms: ["google-cloud", "google-cloud-platform"], related: [{ to: "aws", w: 0.4 }, { to: "azure", w: 0.4 }] },
316
+ { id: "azure", synonyms: ["microsoft-azure"], related: [{ to: "aws", w: 0.4 }] },
317
+ { id: "ci-cd", synonyms: ["cicd", "jenkins", "circleci", "circle-ci", "travis"], related: [{ to: "github-actions", w: 0.6 }, { to: "gitlab-ci", w: 0.6 }] },
318
+ { id: "github-actions", parents: ["ci-cd"], synonyms: ["github-action"] },
319
+ { id: "gitlab-ci", parents: ["ci-cd"], synonyms: ["gitlab"] },
320
+ { id: "linux" },
321
+ { id: "nginx" },
322
+ { id: "prometheus", parents: ["observability"], related: [{ to: "grafana", w: 0.6 }] },
323
+ { id: "grafana", parents: ["observability"] },
324
+ { id: "datadog", parents: ["observability"] },
325
+ { id: "opentelemetry", parents: ["observability"], synonyms: ["otel"] },
326
+ { id: "vercel", related: [{ to: "netlify", w: 0.5 }, { to: "nextjs", w: 0.4 }] },
327
+ { id: "netlify" },
328
+ { id: "fly", synonyms: ["fly.io"], related: [{ to: "railway", w: 0.5 }, { to: "render", w: 0.5 }] },
329
+ { id: "railway", related: [{ to: "render", w: 0.5 }] },
330
+ { id: "render" },
331
+ { id: "cloudflare", synonyms: ["cloudflare-workers"] },
332
+ { id: "helm", parents: ["kubernetes"] },
333
+ { id: "argocd", parents: ["kubernetes"] },
334
+ { id: "serverless", parents: ["devops"] },
335
+ // ── Databases & storage ─────────────────────────────────────────────────────
336
+ { id: "postgresql", synonyms: ["postgres", "pg"], related: [{ to: "mysql", w: 0.45 }, { to: "sqlite", w: 0.4 }] },
337
+ { id: "mysql", related: [{ to: "postgresql", w: 0.45 }] },
338
+ { id: "sqlite" },
339
+ { id: "mongodb", synonyms: ["mongo"] },
340
+ { id: "redis", related: [{ to: "caching", w: 0.5 }] },
341
+ { id: "elasticsearch", synonyms: ["elastic"], related: [{ to: "search", w: 0.55 }] },
342
+ { id: "kafka", synonyms: ["apache-kafka"], related: [{ to: "rabbitmq", w: 0.5 }, { to: "message-queue", w: 0.55 }] },
343
+ { id: "rabbitmq", related: [{ to: "message-queue", w: 0.55 }] },
344
+ { id: "cassandra" },
345
+ { id: "dynamodb", parents: ["aws"] },
346
+ { id: "snowflake", parents: ["data-engineering"], related: [{ to: "clickhouse", w: 0.4 }] },
347
+ { id: "clickhouse", parents: ["data-engineering"], related: [{ to: "duckdb", w: 0.35 }] },
348
+ { id: "duckdb", parents: ["data-engineering"] },
349
+ { id: "supabase", related: [{ to: "postgresql", w: 0.5 }, { to: "neon", w: 0.4 }] },
350
+ { id: "planetscale", related: [{ to: "mysql", w: 0.5 }] },
351
+ { id: "neon", related: [{ to: "postgresql", w: 0.5 }] },
352
+ { id: "turso", related: [{ to: "sqlite", w: 0.5 }] },
353
+ { id: "cockroachdb", related: [{ to: "postgresql", w: 0.45 }] },
354
+ { id: "prisma", parents: ["backend"], synonyms: ["@prisma/client"], related: [{ to: "drizzle", w: 0.5 }, { to: "typeorm", w: 0.45 }, { to: "sequelize", w: 0.4 }] },
355
+ { id: "drizzle", synonyms: ["drizzle-orm"], related: [{ to: "prisma", w: 0.5 }] },
356
+ { id: "sequelize", related: [{ to: "typeorm", w: 0.4 }] },
357
+ { id: "typeorm", related: [{ to: "prisma", w: 0.45 }] },
358
+ { id: "sqlalchemy", parents: ["python"] },
359
+ // ── Data engineering & ML ───────────────────────────────────────────────────
360
+ { id: "data-engineering", synonyms: ["data-eng"], related: [{ to: "spark", w: 0.5 }, { to: "airflow", w: 0.5 }, { to: "dbt", w: 0.45 }] },
361
+ { id: "spark", parents: ["data-engineering"], synonyms: ["apache-spark"] },
362
+ { id: "airflow", parents: ["data-engineering"], synonyms: ["apache-airflow"] },
363
+ { id: "dbt", parents: ["data-engineering"] },
364
+ { id: "ml", synonyms: ["machine-learning"], related: [{ to: "pytorch", w: 0.5 }, { to: "tensorflow", w: 0.5 }, { to: "scikit-learn", w: 0.5 }] },
365
+ { id: "llm", parents: ["ml"], synonyms: ["llms", "genai", "generative-ai"], related: [{ to: "langchain", w: 0.5 }, { to: "rag", w: 0.55 }, { to: "openai", w: 0.45 }, { to: "anthropic", w: 0.45 }] },
366
+ { id: "pytorch", parents: ["ml"], synonyms: ["torch"], related: [{ to: "tensorflow", w: 0.5 }] },
367
+ { id: "tensorflow", parents: ["ml"], synonyms: ["keras", "tf-keras"] },
368
+ { id: "pandas", parents: ["python"], related: [{ to: "numpy", w: 0.6 }] },
369
+ { id: "numpy", parents: ["python"] },
370
+ { id: "scikit-learn", parents: ["ml"], synonyms: ["sklearn"] },
371
+ { id: "jupyter", parents: ["python"] },
372
+ { id: "langchain", parents: ["llm"], synonyms: ["llamaindex"] },
373
+ { id: "huggingface", parents: ["ml"], synonyms: ["hugging-face"] },
374
+ { id: "openai", parents: ["llm"] },
375
+ { id: "anthropic", parents: ["llm"], synonyms: ["claude"] },
376
+ { id: "rag", parents: ["llm"], synonyms: ["retrieval-augmented-generation"] },
377
+ { id: "mlops", parents: ["ml"], related: [{ to: "devops", w: 0.4 }] },
378
+ // ── Mobile ──────────────────────────────────────────────────────────────────
379
+ { id: "mobile", related: [{ to: "ios", w: 0.5 }, { to: "android", w: 0.5 }] },
380
+ { id: "ios", parents: ["mobile", "swift"], related: [{ to: "android", w: 0.4 }] },
381
+ { id: "android", parents: ["mobile"], related: [{ to: "kotlin", w: 0.4 }] },
382
+ { id: "swiftui", parents: ["ios", "swift"] },
383
+ { id: "react-native", parents: ["mobile", "react"], synonyms: ["reactnative"], related: [{ to: "flutter", w: 0.4 }, { to: "expo", w: 0.6 }] },
384
+ { id: "flutter", parents: ["mobile", "dart"] },
385
+ { id: "expo", parents: ["react-native"] },
386
+ { id: "kotlin-multiplatform", parents: ["mobile", "kotlin"], synonyms: ["kmp"] },
387
+ // ── Domains / capabilities ──────────────────────────────────────────────────
388
+ { id: "frontend", related: [{ to: "react", w: 0.4 }, { to: "css", w: 0.3 }] },
389
+ { id: "backend", related: [{ to: "api-design", w: 0.4 }, { to: "microservices", w: 0.4 }] },
390
+ { id: "devops", related: [{ to: "kubernetes", w: 0.4 }, { to: "ci-cd", w: 0.4 }, { to: "docker", w: 0.4 }] },
391
+ { id: "authentication", synonyms: ["auth", "jwt", "saml", "passport", "auth0", "clerk", "nextauth"], related: [{ to: "oauth", w: 0.6 }, { to: "security", w: 0.5 }] },
392
+ { id: "oauth", parents: ["authentication"], synonyms: ["oauth2", "oidc"], related: [{ to: "security", w: 0.4 }] },
393
+ { id: "security", related: [{ to: "authentication", w: 0.5 }] },
394
+ { id: "payments", synonyms: ["stripe", "braintree", "paddle", "lemonsqueezy", "@stripe/stripe-js"], related: [{ to: "billing", w: 0.6 }] },
395
+ { id: "billing", synonyms: ["recurly", "chargebee"] },
396
+ { id: "api-design", synonyms: ["rest", "restful", "rest-api"], related: [{ to: "graphql", w: 0.4 }, { to: "grpc", w: 0.4 }, { to: "backend", w: 0.4 }] },
397
+ { id: "graphql", synonyms: ["gql"], related: [{ to: "trpc", w: 0.4 }] },
398
+ { id: "trpc", related: [{ to: "graphql", w: 0.4 }] },
399
+ { id: "grpc", synonyms: ["grpc-web"], related: [{ to: "microservices", w: 0.3 }] },
400
+ { id: "microservices" },
401
+ { id: "websockets", synonyms: ["ws", "socket.io"], related: [{ to: "realtime", w: 0.6 }] },
402
+ { id: "realtime", synonyms: ["real-time"] },
403
+ { id: "message-queue", synonyms: ["mq"] },
404
+ { id: "caching", synonyms: ["cache"] },
405
+ { id: "search", synonyms: ["full-text-search"] },
406
+ { id: "observability", synonyms: ["o11y"], related: [{ to: "monitoring", w: 0.6 }] },
407
+ { id: "monitoring", related: [{ to: "prometheus", w: 0.4 }] },
408
+ { id: "testing", related: [{ to: "unit-testing", w: 0.5 }, { to: "e2e-testing", w: 0.5 }] },
409
+ { id: "unit-testing", parents: ["testing"] },
410
+ { id: "e2e-testing", parents: ["testing"], synonyms: ["e2e", "end-to-end-testing"] },
411
+ { id: "jest", parents: ["testing"], related: [{ to: "vitest", w: 0.6 }, { to: "mocha", w: 0.5 }] },
412
+ { id: "vitest", parents: ["testing"], related: [{ to: "jest", w: 0.6 }] },
413
+ { id: "playwright", parents: ["e2e-testing"], related: [{ to: "cypress", w: 0.6 }] },
414
+ { id: "cypress", parents: ["e2e-testing"] },
415
+ { id: "mocha", parents: ["testing"] },
416
+ { id: "pytest", parents: ["testing", "python"] },
417
+ { id: "accessibility", synonyms: ["a11y"] },
418
+ { id: "seo" },
419
+ { id: "performance", synonyms: ["perf", "web-performance"] }
420
+ ];
421
+ }
422
+ });
423
+
424
+ // ../../packages/core/src/vocab/closure.ts
425
+ function round3(n) {
426
+ return Math.round(n * 1e3) / 1e3;
427
+ }
428
+ function validateGraph(nodes) {
429
+ const ids = /* @__PURE__ */ new Set();
430
+ for (const n of nodes) {
431
+ if (ids.has(n.id)) throw new Error(`vocab: duplicate id "${n.id}"`);
432
+ ids.add(n.id);
433
+ }
434
+ const seenAlias = /* @__PURE__ */ new Map();
435
+ for (const n of nodes) {
436
+ for (const p of n.parents ?? []) {
437
+ if (p === n.id) throw new Error(`vocab: "${n.id}" lists itself as a parent`);
438
+ if (!ids.has(p)) throw new Error(`vocab: "${n.id}" parent "${p}" is not a defined id`);
439
+ }
440
+ for (const e of n.related ?? []) {
441
+ if (e.to === n.id) throw new Error(`vocab: "${n.id}" relates to itself`);
442
+ if (!ids.has(e.to)) throw new Error(`vocab: "${n.id}" related "${e.to}" is not a defined id`);
443
+ if (!(e.w > 0 && e.w <= 1)) throw new Error(`vocab: "${n.id}"\u2192"${e.to}" weight ${e.w} out of (0,1]`);
444
+ }
445
+ for (const s of n.synonyms ?? []) {
446
+ const alias = s.toLowerCase();
447
+ if (ids.has(alias)) throw new Error(`vocab: synonym "${alias}" collides with a canonical id`);
448
+ const prev = seenAlias.get(alias);
449
+ if (prev && prev !== n.id) throw new Error(`vocab: synonym "${alias}" maps to both "${prev}" and "${n.id}"`);
450
+ seenAlias.set(alias, n.id);
451
+ }
452
+ }
453
+ const visiting = /* @__PURE__ */ new Set();
454
+ const done = /* @__PURE__ */ new Set();
455
+ const parentMap = new Map(nodes.map((n) => [n.id, n.parents ?? []]));
456
+ const walk = (id, path) => {
457
+ if (done.has(id)) return;
458
+ if (visiting.has(id)) throw new Error(`vocab: parent cycle ${[...path, id].join(" \u2192 ")}`);
459
+ visiting.add(id);
460
+ for (const p of parentMap.get(id) ?? []) walk(p, [...path, id]);
461
+ visiting.delete(id);
462
+ done.add(id);
463
+ };
464
+ for (const n of nodes) walk(n.id, []);
465
+ }
466
+ function buildAdjacency(nodes) {
467
+ const adj = /* @__PURE__ */ new Map();
468
+ const add = (from, to, w) => {
469
+ let m = adj.get(from);
470
+ if (!m) adj.set(from, m = /* @__PURE__ */ new Map());
471
+ if (w > (m.get(to) ?? 0)) m.set(to, w);
472
+ };
473
+ for (const n of nodes) {
474
+ for (const p of n.parents ?? []) {
475
+ add(n.id, p, PARENT_UP);
476
+ add(p, n.id, PARENT_DOWN);
477
+ }
478
+ for (const e of n.related ?? []) {
479
+ add(n.id, e.to, e.w);
480
+ add(e.to, n.id, e.w);
481
+ }
482
+ }
483
+ return adj;
484
+ }
485
+ function closureFrom(source, adj) {
486
+ const best = /* @__PURE__ */ new Map();
487
+ for (const [t, w] of adj.get(source) ?? []) {
488
+ if (w >= DECAY_FLOOR) best.set(t, { w: round3(w), via: t });
489
+ }
490
+ const settled = /* @__PURE__ */ new Set([source]);
491
+ while (true) {
492
+ let u;
493
+ let uw = 0;
494
+ for (const [t, e] of best) {
495
+ if (!settled.has(t) && e.w > uw) {
496
+ u = t;
497
+ uw = e.w;
498
+ }
499
+ }
500
+ if (!u) break;
501
+ settled.add(u);
502
+ const via = best.get(u).via;
503
+ for (const [t, we] of adj.get(u) ?? []) {
504
+ if (settled.has(t)) continue;
505
+ const cand = round3(uw * we);
506
+ if (cand >= DECAY_FLOOR && cand > (best.get(t)?.w ?? 0)) {
507
+ best.set(t, { w: cand, via });
508
+ }
509
+ }
510
+ }
511
+ best.delete(source);
512
+ return best;
513
+ }
514
+ function buildGraph(nodes) {
515
+ validateGraph(nodes);
516
+ const ids = new Set(nodes.map((n) => n.id));
517
+ const synonyms = /* @__PURE__ */ new Map();
518
+ for (const n of nodes) {
519
+ for (const s of n.synonyms ?? []) synonyms.set(s.toLowerCase(), n.id);
520
+ }
521
+ const adj = buildAdjacency(nodes);
522
+ const closure = /* @__PURE__ */ new Map();
523
+ for (const n of nodes) closure.set(n.id, closureFrom(n.id, adj));
524
+ return { ids, synonyms, closure };
525
+ }
526
+ var PARENT_UP, PARENT_DOWN, DECAY_FLOOR;
527
+ var init_closure = __esm({
528
+ "../../packages/core/src/vocab/closure.ts"() {
529
+ "use strict";
530
+ PARENT_UP = 0.6;
531
+ PARENT_DOWN = 0.35;
532
+ DECAY_FLOOR = 0.25;
533
+ }
534
+ });
535
+
536
+ // ../../packages/core/src/vocab/types.ts
537
+ var init_types2 = __esm({
538
+ "../../packages/core/src/vocab/types.ts"() {
539
+ "use strict";
540
+ }
541
+ });
542
+
543
+ // ../../packages/core/src/vocab/index.ts
234
544
  function normalize(tokens) {
235
545
  const result = /* @__PURE__ */ new Set();
236
546
  for (const raw of tokens) {
237
547
  const lower = raw.toLowerCase().trim();
238
- if (VOCAB_SET.has(lower)) {
548
+ if (GRAPH.ids.has(lower)) {
239
549
  result.add(lower);
240
550
  continue;
241
551
  }
242
- const mapped = SYNONYMS[lower];
243
- if (mapped && VOCAB_SET.has(mapped)) {
244
- result.add(mapped);
245
- }
552
+ const mapped = GRAPH.synonyms.get(lower);
553
+ if (mapped) result.add(mapped);
246
554
  }
247
555
  return Array.from(result);
248
556
  }
249
- var VOCABULARY, SYNONYMS, VOCAB_SET;
557
+ function expandWeighted(tags, graph = GRAPH) {
558
+ const out = /* @__PURE__ */ new Map();
559
+ const put = (tag, weight, via) => {
560
+ const ex = out.get(tag);
561
+ if (!ex || weight > ex.weight) out.set(tag, { tag, weight, via });
562
+ };
563
+ for (const t of tags) {
564
+ put(t, 1, t);
565
+ const near = graph.closure.get(t);
566
+ if (near) for (const [n, edge] of near) put(n, edge.w, t);
567
+ }
568
+ return out;
569
+ }
570
+ var GRAPH, VOCABULARY, SYNONYMS;
571
+ var init_vocab = __esm({
572
+ "../../packages/core/src/vocab/index.ts"() {
573
+ "use strict";
574
+ init_graph_data();
575
+ init_closure();
576
+ init_types2();
577
+ init_closure();
578
+ init_graph_data();
579
+ GRAPH = buildGraph(VOCAB_NODES);
580
+ VOCABULARY = [...GRAPH.ids];
581
+ SYNONYMS = Object.fromEntries(GRAPH.synonyms);
582
+ }
583
+ });
584
+
585
+ // ../../packages/core/src/vocabulary.ts
250
586
  var init_vocabulary = __esm({
251
587
  "../../packages/core/src/vocabulary.ts"() {
252
588
  "use strict";
253
- VOCABULARY = [
254
- // Languages
255
- "typescript",
256
- "javascript",
257
- "python",
258
- "go",
259
- "rust",
260
- "java",
261
- "ruby",
262
- "elixir",
263
- "scala",
264
- "kotlin",
265
- "swift",
266
- "cpp",
267
- "csharp",
268
- "php",
269
- "haskell",
270
- "clojure",
271
- "r",
272
- // Frontend frameworks / libs
273
- "react",
274
- "nextjs",
275
- "vue",
276
- "nuxt",
277
- "svelte",
278
- "angular",
279
- "solidjs",
280
- "tailwind",
281
- "css",
282
- "html",
283
- "graphql",
284
- "trpc",
285
- // Backend frameworks
286
- "nodejs",
287
- "express",
288
- "fastify",
289
- "nestjs",
290
- "django",
291
- "fastapi",
292
- "flask",
293
- "rails",
294
- "spring",
295
- "actix",
296
- "gin",
297
- "phoenix",
298
- "laravel",
299
- "dotnet",
300
- // Infrastructure & DevOps
301
- "kubernetes",
302
- "docker",
303
- "terraform",
304
- "aws",
305
- "gcp",
306
- "azure",
307
- "ci-cd",
308
- "github-actions",
309
- "linux",
310
- "nginx",
311
- "pulumi",
312
- "ansible",
313
- "prometheus",
314
- "grafana",
315
- "datadog",
316
- "opentelemetry",
317
- // Data & ML
318
- "postgresql",
319
- "mysql",
320
- "sqlite",
321
- "mongodb",
322
- "redis",
323
- "elasticsearch",
324
- "kafka",
325
- "rabbitmq",
326
- "data-engineering",
327
- "spark",
328
- "airflow",
329
- "dbt",
330
- "ml",
331
- "llm",
332
- "pytorch",
333
- "tensorflow",
334
- "pandas",
335
- "numpy",
336
- // Domains / capabilities
337
- "oauth",
338
- "authentication",
339
- "security",
340
- "payments",
341
- "billing",
342
- "frontend",
343
- "backend",
344
- "devops",
345
- "mobile",
346
- "ios",
347
- "android",
348
- "api-design",
349
- "microservices",
350
- "websockets",
351
- "testing",
352
- "accessibility",
353
- "seo",
354
- "performance",
355
- "observability",
356
- "search",
357
- "realtime"
358
- ];
359
- SYNONYMS = {
360
- // Kubernetes aliases
361
- "k8s": "kubernetes",
362
- "kube": "kubernetes",
363
- // Auth / identity
364
- "passport": "authentication",
365
- "oauth2": "oauth",
366
- "oidc": "oauth",
367
- "jwt": "authentication",
368
- "saml": "authentication",
369
- "auth0": "authentication",
370
- "clerk": "authentication",
371
- "nextauth": "authentication",
372
- // Payments
373
- "@stripe/stripe-js": "payments",
374
- "stripe": "payments",
375
- "braintree": "payments",
376
- "paddle": "payments",
377
- "lemonsqueezy": "payments",
378
- "recurly": "billing",
379
- "chargebee": "billing",
380
- // Framework / lib aliases
381
- "next": "nextjs",
382
- "next.js": "nextjs",
383
- "nuxt.js": "nuxt",
384
- "vue.js": "vue",
385
- "angular.js": "angular",
386
- "angularjs": "angular",
387
- "express.js": "express",
388
- "expressjs": "express",
389
- "fastapi": "fastapi",
390
- "nest": "nestjs",
391
- "nest.js": "nestjs",
392
- "sveltekit": "svelte",
393
- // Language aliases
394
- "ts": "typescript",
395
- "js": "javascript",
396
- "py": "python",
397
- "golang": "go",
398
- "c++": "cpp",
399
- "c#": "csharp",
400
- ".net": "dotnet",
401
- "asp.net": "dotnet",
402
- // DB aliases
403
- "postgres": "postgresql",
404
- "pg": "postgresql",
405
- "mongo": "mongodb",
406
- "elastic": "elasticsearch",
407
- // Cloud aliases
408
- "amazon web services": "aws",
409
- "google cloud": "gcp",
410
- "google cloud platform": "gcp",
411
- "microsoft azure": "azure",
412
- // CI/CD aliases
413
- "github actions": "github-actions",
414
- "circle ci": "ci-cd",
415
- "circleci": "ci-cd",
416
- "jenkins": "ci-cd",
417
- "gitlab ci": "ci-cd",
418
- "travis": "ci-cd",
419
- // Mobile
420
- "react native": "mobile",
421
- "flutter": "mobile",
422
- "expo": "mobile",
423
- // AI / ML
424
- "openai": "llm",
425
- "anthropic": "llm",
426
- "langchain": "llm",
427
- "llamaindex": "llm",
428
- "hugging face": "ml",
429
- "huggingface": "ml",
430
- "scikit-learn": "ml",
431
- "sklearn": "ml",
432
- // Data pipeline
433
- "apache kafka": "kafka",
434
- "apache spark": "spark",
435
- "apache airflow": "airflow",
436
- // Misc
437
- "tailwindcss": "tailwind",
438
- "tw": "tailwind",
439
- "gql": "graphql",
440
- "ws": "websockets",
441
- "socket.io": "websockets",
442
- "jest": "testing",
443
- "vitest": "testing",
444
- "playwright": "testing",
445
- "cypress": "testing"
446
- };
447
- VOCAB_SET = new Set(VOCABULARY);
589
+ init_vocab();
448
590
  }
449
591
  });
450
592
 
@@ -465,6 +607,7 @@ function computeIdf(jobs) {
465
607
  return idf;
466
608
  }
467
609
  function inferSeniority(title) {
610
+ if (!ENG_TITLE.test(title)) return void 0;
468
611
  for (const [re, level] of SENIORITY_PATTERNS) {
469
612
  if (re.test(title)) return level;
470
613
  }
@@ -478,15 +621,15 @@ function seniorityScore(fp, job) {
478
621
  const got = SENIORITY_RANK[jobLevel] ?? 1;
479
622
  const delta = Math.abs(wanted - got);
480
623
  if (delta === 0) return 1;
481
- if (delta === 1) return 0.5;
482
- return 0.2;
624
+ if (delta === 1) return 0.7;
625
+ return 0.4;
483
626
  }
484
- function recencyScore(postedAt) {
627
+ function recencyScore(postedAt, now) {
485
628
  if (!postedAt) return 0.75;
486
- const ageDays = (Date.now() - new Date(postedAt).getTime()) / 864e5;
487
- if (ageDays < 7) return 1;
488
- if (ageDays < 30) return 0.9;
489
- if (ageDays < 90) return 0.75;
629
+ const ageDays2 = (now - new Date(postedAt).getTime()) / 864e5;
630
+ if (ageDays2 < 7) return 1;
631
+ if (ageDays2 < 30) return 0.9;
632
+ if (ageDays2 < 90) return 0.75;
490
633
  return 0.6;
491
634
  }
492
635
  function passesFilters(fp, job) {
@@ -501,52 +644,73 @@ function passesFilters(fp, job) {
501
644
  }
502
645
  return true;
503
646
  }
504
- function buildReason(matchedTags) {
505
- if (matchedTags.length === 0) return "No direct skill overlap found.";
506
- const top = matchedTags.slice(0, 3);
507
- const rest = matchedTags.length - top.length;
647
+ function buildReason(details) {
648
+ if (details.length === 0) return "No direct skill overlap found.";
649
+ const render = (d) => !d.via || d.via === d.tag ? d.tag : `${d.via}\u2192${d.tag} (${d.weight})`;
650
+ const top = details.slice(0, 3).map(render);
651
+ const rest = details.length - top.length;
508
652
  const listed = top.join(", ");
509
653
  if (rest === 0) return `Matched on ${listed}.`;
510
654
  return `Matched on ${listed} + ${rest} more skill${rest > 1 ? "s" : ""}.`;
511
655
  }
512
- function match(fp, jobs, limit = 5) {
656
+ function harmonicMean(a, b) {
657
+ if (a <= 0 || b <= 0) return 0;
658
+ return 2 * a * b / (a + b);
659
+ }
660
+ function match(fp, jobs, limit = 5, now = Date.now()) {
513
661
  const idf = computeIdf(jobs);
514
- const fpTagSet = new Set(fp.skillTags);
515
- const maxTagScore = fp.skillTags.reduce((acc, t) => acc + (idf.get(t) ?? 1), 0);
662
+ const idfOf = (t) => idf.get(t) ?? 0;
663
+ const expanded = expandWeighted(fp.skillTags);
664
+ const maxDevScore = fp.skillTags.reduce((acc, t) => acc + idfOf(t), 0);
516
665
  const candidates = jobs.filter((j) => passesFilters(fp, j));
517
666
  const scored = candidates.map((job) => {
518
- const jobTagSet = new Set(job.tags);
519
- const matched = [];
520
- let tagScore2 = 0;
521
- for (const tag of fpTagSet) {
522
- if (jobTagSet.has(tag)) {
523
- const w = idf.get(tag) ?? 1;
524
- tagScore2 += w;
525
- matched.push(tag);
667
+ const details = [];
668
+ let jobMatchScore = 0;
669
+ let jobMaxScore = 0;
670
+ const devCovByTag = /* @__PURE__ */ new Map();
671
+ for (const tag of job.tags) {
672
+ const w = idfOf(tag);
673
+ jobMaxScore += w;
674
+ const hit = expanded.get(tag);
675
+ if (hit) {
676
+ const credit = Math.pow(hit.weight, SHARPEN);
677
+ jobMatchScore += w * credit;
678
+ details.push({ tag, weight: hit.weight, via: hit.via });
679
+ if (credit > (devCovByTag.get(hit.via) ?? 0)) devCovByTag.set(hit.via, credit);
526
680
  }
527
681
  }
528
- const normTagScore = maxTagScore > 0 ? tagScore2 / maxTagScore : 0;
529
- matched.sort((a, b) => (idf.get(b) ?? 1) - (idf.get(a) ?? 1));
682
+ let devScore = 0;
683
+ for (const t of fp.skillTags) devScore += idfOf(t) * (devCovByTag.get(t) ?? 0);
684
+ const devCov = maxDevScore > 0 ? Math.min(1, devScore / maxDevScore) : 0;
685
+ const jobCov = jobMaxScore > 0 ? Math.min(1, jobMatchScore / jobMaxScore) : 0;
686
+ const tagComponent = harmonicMean(devCov, jobCov);
687
+ if (tagComponent === 0) return null;
688
+ details.sort((a, b) => idfOf(b.tag) * b.weight - idfOf(a.tag) * a.weight);
530
689
  const sScore = seniorityScore(fp, job);
531
- const rScore = recencyScore(job.postedAt);
532
- const score = normTagScore * 0.6 + sScore * 0.25 + rScore * 0.15;
690
+ const rScore = recencyScore(job.postedAt, now);
691
+ const score = tagComponent * 0.6 + sScore * 0.25 + rScore * 0.15;
692
+ const matchedTags = [...new Set(details.map((d) => d.via ?? d.tag))];
533
693
  return {
534
694
  job,
535
695
  score: Math.round(score * 1e3) / 1e3,
536
- matchedTags: matched,
537
- reason: buildReason(matched)
696
+ matchedTags,
697
+ matchDetails: details,
698
+ reason: buildReason(details)
538
699
  };
539
700
  });
540
- return scored.sort((a, b) => b.score - a.score).slice(0, limit);
701
+ return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => b.score - a.score).slice(0, limit);
541
702
  }
542
703
  function matchOne(fp, job) {
543
704
  const results = match(fp, [job], 1);
544
705
  return results.length > 0 ? results[0] : null;
545
706
  }
546
- var SENIORITY_RANK, SENIORITY_PATTERNS;
707
+ var MIN_SCORE, SHARPEN, SENIORITY_RANK, SENIORITY_PATTERNS, ENG_TITLE;
547
708
  var init_matcher = __esm({
548
709
  "../../packages/core/src/matcher.ts"() {
549
710
  "use strict";
711
+ init_vocabulary();
712
+ MIN_SCORE = 0.15;
713
+ SHARPEN = 1.6;
550
714
  SENIORITY_RANK = {
551
715
  junior: 0,
552
716
  mid: 1,
@@ -559,6 +723,7 @@ var init_matcher = __esm({
559
723
  [/\bjunior\b|\bjr\.?\b|\bentry[\s-]?level\b/i, "junior"],
560
724
  [/\bmid[\s-]?level\b|\bmid\b/i, "mid"]
561
725
  ];
726
+ ENG_TITLE = /\b(engineer|engineering|developer|dev|swe|sde|programmer|architect)\b/i;
562
727
  }
563
728
  });
564
729
 
@@ -899,6 +1064,33 @@ var init_himalayas = __esm({
899
1064
  }
900
1065
  });
901
1066
 
1067
+ // ../../packages/core/src/feeds/entities.ts
1068
+ function fromCodePoint(cp) {
1069
+ if (!Number.isFinite(cp) || cp < 0 || cp > 1114111) return "";
1070
+ try {
1071
+ return String.fromCodePoint(cp);
1072
+ } catch {
1073
+ return "";
1074
+ }
1075
+ }
1076
+ function decodeEntities(input) {
1077
+ if (!input || !input.includes("&")) return input;
1078
+ return input.replace(/&#(\d+);/g, (_, n) => fromCodePoint(parseInt(n, 10))).replace(/&#[xX]([0-9a-fA-F]+);/g, (_, h) => fromCodePoint(parseInt(h, 16))).replace(/&(lt|gt|quot|apos|nbsp);/g, (_, name) => NAMED[name] ?? `&${name};`).replace(/&amp;/g, "&");
1079
+ }
1080
+ var NAMED;
1081
+ var init_entities = __esm({
1082
+ "../../packages/core/src/feeds/entities.ts"() {
1083
+ "use strict";
1084
+ NAMED = {
1085
+ lt: "<",
1086
+ gt: ">",
1087
+ quot: '"',
1088
+ apos: "'",
1089
+ nbsp: " "
1090
+ };
1091
+ }
1092
+ });
1093
+
902
1094
  // ../../packages/core/src/feeds/wwr.ts
903
1095
  function tokenize5(text) {
904
1096
  return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
@@ -922,9 +1114,9 @@ function parseRss(xml) {
922
1114
  for (const block of itemBlocks) {
923
1115
  const get = (tag) => {
924
1116
  const cdataMatch = block.match(new RegExp(`<${tag}[^>]*><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/${tag}>`, "i"));
925
- if (cdataMatch) return cdataMatch[1].trim();
1117
+ if (cdataMatch) return decodeEntities(cdataMatch[1].trim());
926
1118
  const plainMatch = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i"));
927
- return plainMatch?.[1].trim() ?? "";
1119
+ return decodeEntities(plainMatch?.[1].trim() ?? "");
928
1120
  };
929
1121
  const rawTitle = get("title");
930
1122
  const colonIdx = rawTitle.indexOf(":");
@@ -951,6 +1143,7 @@ var init_wwr = __esm({
951
1143
  "../../packages/core/src/feeds/wwr.ts"() {
952
1144
  "use strict";
953
1145
  init_vocabulary();
1146
+ init_entities();
954
1147
  WWR_RSS_URL = "https://weworkremotely.com/remote-jobs.rss";
955
1148
  wwr = {
956
1149
  source: "wwr",
@@ -989,7 +1182,7 @@ function tokenize6(text) {
989
1182
  return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
990
1183
  }
991
1184
  function stripHtml2(html) {
992
- return html.replace(/<p>/gi, " ").replace(/<[^>]*>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;/g, "'").replace(/\s+/g, " ").trim();
1185
+ return decodeEntities(html.replace(/<p>/gi, " ").replace(/<[^>]*>/g, "")).replace(/\s+/g, " ").trim();
993
1186
  }
994
1187
  function extractUrl(text) {
995
1188
  const match2 = text.match(/https?:\/\/[^\s<>"']+/);
@@ -1043,6 +1236,7 @@ var init_hn = __esm({
1043
1236
  "../../packages/core/src/feeds/hn.ts"() {
1044
1237
  "use strict";
1045
1238
  init_vocabulary();
1239
+ init_entities();
1046
1240
  ALGOLIA_SEARCH = "https://hn.algolia.com/api/v1/search?query=Ask+HN%3A+Who+is+Hiring%3F&tags=story,ask_hn&hitsPerPage=1";
1047
1241
  ALGOLIA_ITEMS = "https://hn.algolia.com/api/v1/items/";
1048
1242
  hn = {
@@ -1079,7 +1273,198 @@ var init_hn = __esm({
1079
1273
  }
1080
1274
  });
1081
1275
 
1276
+ // ../../packages/core/src/feeds/bounty-gate.ts
1277
+ function ageDays(createdAtIso) {
1278
+ const created = Date.parse(createdAtIso);
1279
+ if (!Number.isFinite(created)) return 0;
1280
+ return (Date.now() - created) / (1e3 * 60 * 60 * 24);
1281
+ }
1282
+ function passesMaturityGate(repo) {
1283
+ if (repo.archived || repo.disabled) return false;
1284
+ if (repo.stargazers < MIN_REPO_STARS) return false;
1285
+ if (ageDays(repo.createdAt) < MIN_REPO_AGE_DAYS) return false;
1286
+ return true;
1287
+ }
1288
+ var DEFAULT_BOUNTY_REPOS, MAX_BOUNTIES_PER_REPO, MIN_REPO_STARS, MIN_REPO_AGE_DAYS;
1289
+ var init_bounty_gate = __esm({
1290
+ "../../packages/core/src/feeds/bounty-gate.ts"() {
1291
+ "use strict";
1292
+ DEFAULT_BOUNTY_REPOS = [
1293
+ "tenstorrent/tt-metal",
1294
+ "sequelize/sequelize",
1295
+ "commaai/opendbc",
1296
+ "aragon/hack",
1297
+ "spacemeshos/app",
1298
+ "archestra-ai/archestra",
1299
+ "boundlessfi/boundless",
1300
+ "ucfopen/Obojobo",
1301
+ "widgetti/ipyvolume",
1302
+ "moorcheh-ai/memanto",
1303
+ "PrismarineJS/mineflayer"
1304
+ ];
1305
+ MAX_BOUNTIES_PER_REPO = 10;
1306
+ MIN_REPO_STARS = 5;
1307
+ MIN_REPO_AGE_DAYS = 30;
1308
+ }
1309
+ });
1310
+
1311
+ // ../../packages/core/src/feeds/github-bounties.ts
1312
+ function authHeaders() {
1313
+ const token = process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"];
1314
+ const h = {
1315
+ Accept: "application/vnd.github+json",
1316
+ "User-Agent": "terminalhire",
1317
+ "X-GitHub-Api-Version": "2022-11-28"
1318
+ };
1319
+ if (token) h["Authorization"] = `Bearer ${token}`;
1320
+ return h;
1321
+ }
1322
+ function tokenize7(text) {
1323
+ return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
1324
+ }
1325
+ function parseAmountUSD(text) {
1326
+ const m = text.match(/\$\s?([0-9][0-9,]*(?:\.[0-9]+)?)\s?([kK])?/);
1327
+ if (!m) return void 0;
1328
+ let n = parseFloat(m[1].replace(/,/g, ""));
1329
+ if (m[2]) n *= 1e3;
1330
+ if (!Number.isFinite(n) || n <= 0 || n > 1e6) return void 0;
1331
+ return Math.round(n);
1332
+ }
1333
+ function effortFromAmount(amount) {
1334
+ if (amount == null) return void 0;
1335
+ if (amount <= 500) return "small";
1336
+ if (amount <= 2e3) return "medium";
1337
+ return "large";
1338
+ }
1339
+ function labelNames(issue) {
1340
+ return (issue.labels ?? []).map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean);
1341
+ }
1342
+ function isBountyIssue(issue) {
1343
+ if (issue.pull_request) return false;
1344
+ const labels = labelNames(issue);
1345
+ if (labels.some((n) => BOUNTY_LABEL_RE.test(n))) return true;
1346
+ return /bounty/i.test(issue.title) && parseAmountUSD(issue.title) != null;
1347
+ }
1348
+ async function ghJson(path) {
1349
+ let res;
1350
+ try {
1351
+ res = await fetch(`${GITHUB_API}${path}`, { headers: authHeaders() });
1352
+ } catch (err) {
1353
+ console.warn(`[github-bounties] network error ${path} \u2014`, err);
1354
+ return null;
1355
+ }
1356
+ if (res.status === 403 && res.headers.get("x-ratelimit-remaining") === "0") {
1357
+ console.warn("[github-bounties] rate-limited (set GITHUB_TOKEN for 5000/hr)");
1358
+ return null;
1359
+ }
1360
+ if (!res.ok) {
1361
+ console.warn(`[github-bounties] HTTP ${res.status} ${path}`);
1362
+ return null;
1363
+ }
1364
+ try {
1365
+ return await res.json();
1366
+ } catch {
1367
+ return null;
1368
+ }
1369
+ }
1370
+ async function fetchCommentAmount(repoFullName, issueNumber) {
1371
+ const comments = await ghJson(
1372
+ `/repos/${repoFullName}/issues/${issueNumber}/comments?per_page=30`
1373
+ );
1374
+ if (!comments) return void 0;
1375
+ for (const c of comments) {
1376
+ const body = c.body ?? "";
1377
+ if (BOUNTY_LABEL_RE.test(body)) {
1378
+ const amt = parseAmountUSD(body);
1379
+ if (amt != null) return amt;
1380
+ }
1381
+ }
1382
+ return void 0;
1383
+ }
1384
+ async function fetchRepoBounties(repoFullName) {
1385
+ const repo = await ghJson(`/repos/${repoFullName}`);
1386
+ if (!repo) return [];
1387
+ const meta = {
1388
+ fullName: repo.full_name,
1389
+ stargazers: repo.stargazers_count,
1390
+ createdAt: repo.created_at,
1391
+ archived: repo.archived,
1392
+ disabled: repo.disabled
1393
+ };
1394
+ if (!passesMaturityGate(meta)) {
1395
+ console.info(`[github-bounties] ${repoFullName}: failed maturity gate, skipping`);
1396
+ return [];
1397
+ }
1398
+ const issues = await ghJson(`/repos/${repoFullName}/issues?state=open&per_page=100`);
1399
+ if (!issues) return [];
1400
+ const bounties = issues.filter(isBountyIssue).slice(0, MAX_BOUNTIES_PER_REPO);
1401
+ const owner = repo.owner.login;
1402
+ return Promise.all(bounties.map(async (issue) => {
1403
+ const title = decodeEntities(issue.title).trim();
1404
+ const body = issue.body ? decodeEntities(issue.body) : "";
1405
+ const amountUSD = parseAmountUSD(title) ?? parseAmountUSD(body) ?? await fetchCommentAmount(repoFullName, issue.number);
1406
+ const labels = labelNames(issue);
1407
+ const tags = normalize(tokenize7([title, labels.join(" "), body.slice(0, 2e3)].join(" ")));
1408
+ return {
1409
+ id: `bounty:${repoFullName}#${issue.number}`,
1410
+ source: "bounty",
1411
+ title,
1412
+ company: owner,
1413
+ url: issue.html_url,
1414
+ remote: true,
1415
+ location: "Remote",
1416
+ tags,
1417
+ roleType: "freelance",
1418
+ postedAt: issue.created_at,
1419
+ applyMode: "direct",
1420
+ bounty: {
1421
+ amountUSD,
1422
+ estimatedEffort: effortFromAmount(amountUSD),
1423
+ bountySource: "github",
1424
+ claimUrl: issue.html_url,
1425
+ repoFullName,
1426
+ repoStars: repo.stargazers_count,
1427
+ issueBody: body.slice(0, 1e3) || void 0
1428
+ },
1429
+ raw: issue
1430
+ };
1431
+ }));
1432
+ }
1433
+ var GITHUB_API, BOUNTY_LABEL_RE, githubBounties;
1434
+ var init_github_bounties = __esm({
1435
+ "../../packages/core/src/feeds/github-bounties.ts"() {
1436
+ "use strict";
1437
+ init_vocabulary();
1438
+ init_entities();
1439
+ init_bounty_gate();
1440
+ GITHUB_API = "https://api.github.com";
1441
+ BOUNTY_LABEL_RE = /bounty|reward|funded|💎|💰/i;
1442
+ githubBounties = {
1443
+ source: "bounty",
1444
+ async fetch(opts) {
1445
+ const repos = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : DEFAULT_BOUNTY_REPOS;
1446
+ console.info(`[github-bounties] scanning ${repos.length} repos`);
1447
+ const settled = await Promise.allSettled(repos.map(fetchRepoBounties));
1448
+ const jobs = [];
1449
+ let failures = 0;
1450
+ for (const r of settled) {
1451
+ if (r.status === "fulfilled") jobs.push(...r.value);
1452
+ else {
1453
+ failures++;
1454
+ console.warn("[github-bounties] repo fetch rejected:", r.reason);
1455
+ }
1456
+ }
1457
+ console.info(`[github-bounties] total: ${jobs.length} bounties, ${failures} repo failures`);
1458
+ return jobs;
1459
+ }
1460
+ };
1461
+ }
1462
+ });
1463
+
1082
1464
  // ../../packages/core/src/feeds/index.ts
1465
+ async function aggregateBounties(opts) {
1466
+ return githubBounties.fetch({ slugs: opts?.repos });
1467
+ }
1083
1468
  function flattenTiers(t) {
1084
1469
  return [.../* @__PURE__ */ new Set([...t.bigco, ...t.scaleup, ...t.startup])];
1085
1470
  }
@@ -1112,6 +1497,19 @@ async function aggregate(opts) {
1112
1497
  }
1113
1498
  }
1114
1499
  }
1500
+ if (opts?.includeBounties !== false) {
1501
+ try {
1502
+ const bounties = await githubBounties.fetch({ slugs: opts?.slugs?.["bounty"], limit });
1503
+ for (const b of bounties) {
1504
+ if (!seen.has(b.id)) {
1505
+ seen.add(b.id);
1506
+ jobs.push(b);
1507
+ }
1508
+ }
1509
+ } catch (err) {
1510
+ console.warn("[feeds] bounties failed:", err);
1511
+ }
1512
+ }
1115
1513
  return jobs;
1116
1514
  }
1117
1515
  var FEEDS, GREENHOUSE_SLUGS_BY_TIER, ASHBY_SLUGS_BY_TIER, LEVER_SLUGS_BY_TIER, DEFAULT_GREENHOUSE_SLUGS, DEFAULT_ASHBY_SLUGS, DEFAULT_LEVER_SLUGS;
@@ -1124,6 +1522,8 @@ var init_feeds = __esm({
1124
1522
  init_himalayas();
1125
1523
  init_wwr();
1126
1524
  init_hn();
1525
+ init_github_bounties();
1526
+ init_bounty_gate();
1127
1527
  FEEDS = [greenhouse, ashby, lever, himalayas, wwr, hn];
1128
1528
  GREENHOUSE_SLUGS_BY_TIER = {
1129
1529
  bigco: [
@@ -1233,72 +1633,78 @@ var init_feeds = __esm({
1233
1633
  }
1234
1634
  });
1235
1635
 
1236
- // ../../packages/core/src/coastal.ts
1636
+ // ../../packages/core/src/partners.ts
1237
1637
  import { readFileSync as readFileSync2 } from "fs";
1238
1638
  import { join as join2 } from "path";
1239
1639
  import { fileURLToPath } from "url";
1240
1640
  function resolveDataPath() {
1241
1641
  try {
1242
1642
  const dir = fileURLToPath(new URL("../../../data", import.meta.url));
1243
- return join2(dir, "coastal-roles.json");
1643
+ return join2(dir, "partner-roles.json");
1244
1644
  } catch {
1245
- return join2(process.cwd(), "data", "coastal-roles.json");
1645
+ return join2(process.cwd(), "data", "partner-roles.json");
1246
1646
  }
1247
1647
  }
1248
- function loadCoastalRoles() {
1648
+ function loadPartnerRoles() {
1249
1649
  const filePath = resolveDataPath();
1250
1650
  try {
1251
1651
  const raw = readFileSync2(filePath, "utf-8");
1252
1652
  const parsed = JSON.parse(raw);
1253
1653
  if (!Array.isArray(parsed)) {
1254
- console.warn("[coastal] coastal-roles.json is not an array \u2014 skipping");
1654
+ console.warn("[partners] partner-roles.json is not an array \u2014 skipping");
1255
1655
  return [];
1256
1656
  }
1257
1657
  const valid = [];
1258
1658
  for (const entry of parsed) {
1259
- if (typeof entry === "object" && entry !== null && typeof entry.id === "string" && entry.applyMode === "buyer-lead" && entry.buyer === "coastal") {
1659
+ const e = entry;
1660
+ if (typeof entry === "object" && entry !== null && typeof e.id === "string" && e.applyMode === "buyer-lead" && typeof e.buyer === "string" && e.buyer.length > 0) {
1260
1661
  valid.push(entry);
1261
1662
  } else {
1262
- console.warn("[coastal] Skipping malformed role entry:", entry);
1663
+ console.warn("[partners] Skipping malformed role entry:", entry);
1263
1664
  }
1264
1665
  }
1265
1666
  return valid;
1266
1667
  } catch (err) {
1267
1668
  if (err.code === "ENOENT") {
1268
- console.warn(`[coastal] data/coastal-roles.json not found at ${filePath} \u2014 no Coastal roles loaded`);
1669
+ console.warn(`[partners] data/partner-roles.json not found at ${filePath} \u2014 no partner roles loaded`);
1269
1670
  } else {
1270
- console.warn("[coastal] Failed to load coastal-roles.json:", err);
1671
+ console.warn("[partners] Failed to load partner-roles.json:", err);
1271
1672
  }
1272
1673
  return [];
1273
1674
  }
1274
1675
  }
1275
- var COASTAL_BUYER;
1276
- var init_coastal = __esm({
1277
- "../../packages/core/src/coastal.ts"() {
1676
+ function getBuyer(id) {
1677
+ return BUYER_REGISTRY[id];
1678
+ }
1679
+ var EXAMPLE_BUYER, BUYER_REGISTRY;
1680
+ var init_partners = __esm({
1681
+ "../../packages/core/src/partners.ts"() {
1278
1682
  "use strict";
1279
- COASTAL_BUYER = {
1280
- id: "coastal",
1281
- legalName: "Coastal Recruiting LLC",
1282
- matchCriteria: {
1283
- roleTypes: ["full_time"]
1284
- }
1683
+ EXAMPLE_BUYER = {
1684
+ id: "northstar",
1685
+ legalName: "Northstar Talent Partners",
1686
+ matchCriteria: { roleTypes: ["full_time"] }
1687
+ };
1688
+ BUYER_REGISTRY = {
1689
+ [EXAMPLE_BUYER.id]: EXAMPLE_BUYER
1285
1690
  };
1286
1691
  }
1287
1692
  });
1288
1693
 
1289
1694
  // ../../packages/core/src/indexer.ts
1290
1695
  async function buildIndex(opts) {
1291
- const includeCoastal = opts?.includeCoastal ?? true;
1696
+ const includePartners = opts?.includePartners ?? true;
1292
1697
  const publicJobs = await aggregate(opts);
1293
1698
  const allJobs = [...publicJobs];
1294
- if (includeCoastal) {
1295
- const coastalJobs = loadCoastalRoles();
1296
- const seen = new Set(publicJobs.map((j) => j.id));
1297
- for (const job of coastalJobs) {
1298
- if (!seen.has(job.id)) {
1299
- seen.add(job.id);
1300
- allJobs.push(job);
1301
- }
1699
+ const seen = new Set(publicJobs.map((j) => j.id));
1700
+ const partnerJobs = [
1701
+ ...includePartners ? loadPartnerRoles() : [],
1702
+ ...opts?.partnerRoles ?? []
1703
+ ];
1704
+ for (const job of partnerJobs) {
1705
+ if (!seen.has(job.id)) {
1706
+ seen.add(job.id);
1707
+ allJobs.push(job);
1302
1708
  }
1303
1709
  }
1304
1710
  const jobs = allJobs.map(({ raw: _raw, ...rest }) => rest);
@@ -1311,7 +1717,7 @@ var init_indexer = __esm({
1311
1717
  "../../packages/core/src/indexer.ts"() {
1312
1718
  "use strict";
1313
1719
  init_feeds();
1314
- init_coastal();
1720
+ init_partners();
1315
1721
  }
1316
1722
  });
1317
1723
 
@@ -1393,8 +1799,7 @@ function inferSeniority2(p) {
1393
1799
  if (ageYears >= 9 && (p.publicRepos >= 40 || p.followers >= 500)) return "staff";
1394
1800
  if (ageYears >= 5 && (p.publicRepos >= 20 || p.followers >= 100)) return "senior";
1395
1801
  if (ageYears >= 2 && p.publicRepos >= 5) return "mid";
1396
- if (ageYears < 2 || p.publicRepos < 5) return "junior";
1397
- return void 0;
1802
+ return "junior";
1398
1803
  }
1399
1804
  function githubToFingerprint(p) {
1400
1805
  const rawTokens = [
@@ -1417,30 +1822,42 @@ var init_github = __esm({
1417
1822
  var src_exports = {};
1418
1823
  __export(src_exports, {
1419
1824
  ASHBY_SLUGS_BY_TIER: () => ASHBY_SLUGS_BY_TIER,
1420
- COASTAL_BUYER: () => COASTAL_BUYER,
1825
+ DECAY_FLOOR: () => DECAY_FLOOR,
1421
1826
  DEFAULT_ASHBY_SLUGS: () => DEFAULT_ASHBY_SLUGS,
1827
+ DEFAULT_BOUNTY_REPOS: () => DEFAULT_BOUNTY_REPOS,
1422
1828
  DEFAULT_GREENHOUSE_SLUGS: () => DEFAULT_GREENHOUSE_SLUGS,
1423
1829
  DEFAULT_LEVER_SLUGS: () => DEFAULT_LEVER_SLUGS,
1830
+ EXAMPLE_BUYER: () => EXAMPLE_BUYER,
1424
1831
  FEEDS: () => FEEDS,
1832
+ GRAPH: () => GRAPH,
1425
1833
  GREENHOUSE_SLUGS_BY_TIER: () => GREENHOUSE_SLUGS_BY_TIER,
1426
1834
  LEVER_SLUGS_BY_TIER: () => LEVER_SLUGS_BY_TIER,
1427
1835
  SYNONYMS: () => SYNONYMS,
1428
1836
  VOCABULARY: () => VOCABULARY,
1837
+ VOCAB_NODES: () => VOCAB_NODES,
1429
1838
  aggregate: () => aggregate,
1839
+ aggregateBounties: () => aggregateBounties,
1430
1840
  ashby: () => ashby,
1841
+ buildGraph: () => buildGraph,
1431
1842
  buildIndex: () => buildIndex,
1432
1843
  buildReason: () => buildReason,
1844
+ expandWeighted: () => expandWeighted,
1433
1845
  fetchGitHubProfile: () => fetchGitHubProfile,
1434
1846
  flattenTiers: () => flattenTiers,
1847
+ getBuyer: () => getBuyer,
1848
+ githubBounties: () => githubBounties,
1435
1849
  githubToFingerprint: () => githubToFingerprint,
1436
1850
  greenhouse: () => greenhouse,
1437
1851
  himalayas: () => himalayas,
1438
1852
  hn: () => hn,
1853
+ isBounty: () => isBounty,
1439
1854
  lever: () => lever,
1440
- loadCoastalRoles: () => loadCoastalRoles,
1855
+ loadPartnerRoles: () => loadPartnerRoles,
1441
1856
  match: () => match,
1442
1857
  matchOne: () => matchOne,
1443
1858
  normalize: () => normalize,
1859
+ passesMaturityGate: () => passesMaturityGate,
1860
+ validateGraph: () => validateGraph,
1444
1861
  wwr: () => wwr
1445
1862
  });
1446
1863
  var init_src = __esm({
@@ -1451,7 +1868,7 @@ var init_src = __esm({
1451
1868
  init_matcher();
1452
1869
  init_feeds();
1453
1870
  init_indexer();
1454
- init_coastal();
1871
+ init_partners();
1455
1872
  init_github();
1456
1873
  }
1457
1874
  });
@@ -1550,10 +1967,10 @@ function migrateTagWeights(profile) {
1550
1967
  if (!profile.tagWeights) {
1551
1968
  profile.tagWeights = {};
1552
1969
  }
1553
- const now = (/* @__PURE__ */ new Date()).toISOString();
1970
+ const seed = profile.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
1554
1971
  for (const tag of profile.skillTags) {
1555
1972
  if (!profile.tagWeights[tag]) {
1556
- profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
1973
+ profile.tagWeights[tag] = { count: 1, firstSeen: seed, lastSeen: seed, sessions: 1 };
1557
1974
  }
1558
1975
  }
1559
1976
  }
@@ -1579,7 +1996,7 @@ async function writeProfile(profile) {
1579
1996
  const blob = encrypt2(JSON.stringify(profile), key);
1580
1997
  writeFileSync2(PROFILE_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
1581
1998
  }
1582
- function accumulateSession(profile, tags, isEmployerContext, inferredSeniority) {
1999
+ function accumulateSession(profile, tags, isEmployerContext, inferredSeniority, seniorityIsAuthoritative = false) {
1583
2000
  const now = (/* @__PURE__ */ new Date()).toISOString();
1584
2001
  let filtered = normalize(tags);
1585
2002
  if (isEmployerContext) {
@@ -1597,7 +2014,9 @@ function accumulateSession(profile, tags, isEmployerContext, inferredSeniority)
1597
2014
  }
1598
2015
  }
1599
2016
  if (inferredSeniority && !isEmployerContext) {
1600
- profile.seniority = inferredSeniority;
2017
+ if (seniorityIsAuthoritative || !profile.github) {
2018
+ profile.seniority = inferredSeniority;
2019
+ }
1601
2020
  }
1602
2021
  }
1603
2022
  async function accumulateTags(rawTokens, isEmployerContext, inferredSeniority) {
@@ -1605,12 +2024,14 @@ async function accumulateTags(rawTokens, isEmployerContext, inferredSeniority) {
1605
2024
  accumulateSession(profile, rawTokens, isEmployerContext, inferredSeniority);
1606
2025
  await writeProfile(profile);
1607
2026
  }
1608
- function accumulateGitHubTags(profile, tags) {
2027
+ function accumulateGitHubTags(profile, tags, inferredSeniority) {
1609
2028
  accumulateSession(
1610
2029
  profile,
1611
2030
  tags,
1612
2031
  /* isEmployerContext */
1613
- false
2032
+ false,
2033
+ inferredSeniority,
2034
+ true
1614
2035
  );
1615
2036
  }
1616
2037
  async function listSavedJobs() {
@@ -1732,10 +2153,7 @@ async function runLogin() {
1732
2153
  }
1733
2154
  const fragment = githubToFingerprint2(ghProfile);
1734
2155
  const profile = await readProfile2();
1735
- accumulateGitHubTags2(profile, fragment.skillTags);
1736
- if (fragment.seniorityBand && !profile.seniority) {
1737
- profile.seniority = fragment.seniorityBand;
1738
- }
2156
+ accumulateGitHubTags2(profile, fragment.skillTags, fragment.seniorityBand);
1739
2157
  if (!profile.displayName && ghProfile.name) {
1740
2158
  profile.displayName = ghProfile.name;
1741
2159
  }