terminalhire 0.2.5 → 0.3.1

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((resolve2) => setTimeout(resolve2, 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, isEmployerContext2, inferredSeniority) {
1999
+ function accumulateSession(profile, tags, isEmployerContext2, inferredSeniority, seniorityIsAuthoritative = false) {
1583
2000
  const now = (/* @__PURE__ */ new Date()).toISOString();
1584
2001
  let filtered = normalize(tags);
1585
2002
  if (isEmployerContext2) {
@@ -1597,7 +2014,9 @@ function accumulateSession(profile, tags, isEmployerContext2, inferredSeniority)
1597
2014
  }
1598
2015
  }
1599
2016
  if (inferredSeniority && !isEmployerContext2) {
1600
- profile.seniority = inferredSeniority;
2017
+ if (seniorityIsAuthoritative || !profile.github) {
2018
+ profile.seniority = inferredSeniority;
2019
+ }
1601
2020
  }
1602
2021
  }
1603
2022
  async function accumulateTags(rawTokens, isEmployerContext2, inferredSeniority) {
@@ -1605,12 +2024,14 @@ async function accumulateTags(rawTokens, isEmployerContext2, inferredSeniority)
1605
2024
  accumulateSession(profile, rawTokens, isEmployerContext2, 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() {
@@ -1726,20 +2147,17 @@ async function runLogin() {
1726
2147
  if (process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1") {
1727
2148
  const { createRequire: createRequire2 } = await import("module");
1728
2149
  const { fileURLToPath: fileURLToPath7 } = await import("url");
1729
- const { join: join14, dirname: dirname3 } = await import("path");
2150
+ const { join: join15, dirname: dirname3 } = await import("path");
1730
2151
  const __dirname6 = fileURLToPath7(new URL(".", import.meta.url));
1731
- const fixturePath = join14(__dirname6, "../../fixtures/github-sample.json");
1732
- const { readFileSync: readFileSync13 } = await import("fs");
1733
- ghProfile = JSON.parse(readFileSync13(fixturePath, "utf8"));
2152
+ const fixturePath = join15(__dirname6, "../../fixtures/github-sample.json");
2153
+ const { readFileSync: readFileSync14 } = await import("fs");
2154
+ ghProfile = JSON.parse(readFileSync14(fixturePath, "utf8"));
1734
2155
  } else {
1735
2156
  ghProfile = await fetchGitHubProfile2(login, token);
1736
2157
  }
1737
2158
  const fragment = githubToFingerprint2(ghProfile);
1738
2159
  const profile = await readProfile2();
1739
- accumulateGitHubTags2(profile, fragment.skillTags);
1740
- if (fragment.seniorityBand && !profile.seniority) {
1741
- profile.seniority = fragment.seniorityBand;
1742
- }
2160
+ accumulateGitHubTags2(profile, fragment.skillTags, fragment.seniorityBand);
1743
2161
  if (!profile.displayName && ghProfile.name) {
1744
2162
  profile.displayName = ghProfile.name;
1745
2163
  }
@@ -1975,7 +2393,9 @@ async function run2() {
1975
2393
  }
1976
2394
  console.log(`Fetching job index from ${API_URL}/api/index...`);
1977
2395
  const index = await fetchIndex();
1978
- const jobs = index.jobs ?? [];
2396
+ const allListings = index.jobs ?? [];
2397
+ const jobs = allListings.filter((j) => j.source !== "bounty");
2398
+ const bountyCount = allListings.length - jobs.length;
1979
2399
  if (jobs.length === 0) {
1980
2400
  console.log("No jobs in index. Try again later.");
1981
2401
  return;
@@ -2001,6 +2421,12 @@ async function run2() {
2001
2421
  for (let i = 0; i < results.length; i++) {
2002
2422
  printResult(i, results[i]);
2003
2423
  }
2424
+ if (bountyCount > 0) {
2425
+ console.log(
2426
+ `
2427
+ \u26A1 ${bountyCount} bount${bountyCount === 1 ? "y" : "ies"} you could knock out today \u2014 run: terminalhire bounties`
2428
+ );
2429
+ }
2004
2430
  if (!process.stdin.isTTY) {
2005
2431
  return;
2006
2432
  }
@@ -2034,7 +2460,7 @@ var init_jpi_jobs = __esm({
2034
2460
  TERMINALHIRE_DIR3 = join4(homedir3(), ".terminalhire");
2035
2461
  INDEX_CACHE_FILE = join4(TERMINALHIRE_DIR3, "index-cache.json");
2036
2462
  INDEX_TTL_MS = 15 * 60 * 1e3;
2037
- API_URL = process.env["TERMINALHIRE_API_URL"] ?? process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
2463
+ API_URL = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
2038
2464
  DEFAULT_LIMIT = 10;
2039
2465
  args = process.argv.slice(2);
2040
2466
  limitArg = args.indexOf("--limit");
@@ -2044,25 +2470,163 @@ var init_jpi_jobs = __esm({
2044
2470
  }
2045
2471
  });
2046
2472
 
2047
- // bin/jpi-profile.js
2048
- var jpi_profile_exports = {};
2049
- __export(jpi_profile_exports, {
2473
+ // bin/jpi-bounties.js
2474
+ var jpi_bounties_exports = {};
2475
+ __export(jpi_bounties_exports, {
2050
2476
  run: () => run3
2051
2477
  });
2478
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
2479
+ import { join as join5 } from "path";
2480
+ import { homedir as homedir4 } from "os";
2052
2481
  import { createInterface as createInterface2 } from "readline";
2482
+ function readIndexCache2() {
2483
+ try {
2484
+ const entry = JSON.parse(readFileSync5(INDEX_CACHE_FILE2, "utf8"));
2485
+ if (Date.now() - entry.ts < INDEX_TTL_MS2) return entry.index;
2486
+ return null;
2487
+ } catch {
2488
+ return null;
2489
+ }
2490
+ }
2491
+ function writeIndexCache2(index) {
2492
+ mkdirSync4(TERMINALHIRE_DIR4, { recursive: true });
2493
+ writeFileSync4(INDEX_CACHE_FILE2, JSON.stringify({ ts: Date.now(), index }), "utf8");
2494
+ }
2495
+ async function fetchIndex2() {
2496
+ const cached = readIndexCache2();
2497
+ if (cached) return cached;
2498
+ const res = await fetch(`${API_URL2}/api/index`, { signal: AbortSignal.timeout(1e4) });
2499
+ if (!res.ok) throw new Error(`/api/index returned ${res.status}`);
2500
+ const index = await res.json();
2501
+ writeIndexCache2(index);
2502
+ return index;
2503
+ }
2053
2504
  function prompt2(question) {
2054
2505
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
2055
2506
  return new Promise((resolve2) => {
2056
2507
  rl.question(question, (answer) => {
2057
2508
  rl.close();
2058
- resolve2(answer.trim());
2509
+ resolve2(answer.trim().toLowerCase());
2059
2510
  });
2060
2511
  });
2061
2512
  }
2513
+ function formatAmount(b) {
2514
+ return b.amountUSD != null ? "$" + b.amountUSD.toLocaleString() : "$\u2014";
2515
+ }
2516
+ function linkTitle2(title, url) {
2517
+ const isTTY = process.stdout.isTTY;
2518
+ const noColor = process.env["NO_COLOR"] !== void 0;
2519
+ if (isTTY && !noColor && url) return `\x1B]8;;${url}\x1B\\${title}\x1B]8;;\x1B\\`;
2520
+ return url ? `${title} (${url})` : title;
2521
+ }
2522
+ function printBounty(i, job, score, reason, matchedTags) {
2523
+ const b = job.bounty ?? {};
2524
+ const stars = b.repoStars != null ? ` \xB7 ${b.repoStars}\u2605` : "";
2525
+ const effort = b.estimatedEffort ? ` \xB7 ${EFFORT_LABEL[b.estimatedEffort]}` : "";
2526
+ const scoreStr = score > 0 ? ` \xB7 match ${Math.round(score * 100)}%` : "";
2527
+ console.log(`
2528
+ ${i + 1}. ${linkTitle2(job.title, job.url)}`);
2529
+ console.log(` ${formatAmount(b)}${effort} \xB7 ${b.repoFullName ?? job.company}${stars}${scoreStr}`);
2530
+ if (reason) console.log(` ${reason}`);
2531
+ if (matchedTags && matchedTags.length) console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
2532
+ console.log(` id: ${job.id}`);
2533
+ console.log(` Claim: ${b.claimUrl ?? job.url}`);
2534
+ }
2062
2535
  async function run3() {
2536
+ try {
2537
+ console.log(`Fetching bounty index from ${API_URL2}/api/index...`);
2538
+ const index = await fetchIndex2();
2539
+ let bounties = (index.jobs ?? []).filter((j) => j.source === "bounty");
2540
+ if (PRICED_ONLY) bounties = bounties.filter((j) => j.bounty?.amountUSD != null);
2541
+ if (bounties.length === 0) {
2542
+ console.log("\nNo bounties available right now. Try again later \u2014 supply refreshes through the day.");
2543
+ return;
2544
+ }
2545
+ const ranked = /* @__PURE__ */ new Map();
2546
+ try {
2547
+ const { readProfile: readProfile2, profileToFingerprint: profileToFingerprint2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
2548
+ const { match: match2 } = await Promise.resolve().then(() => (init_src(), src_exports));
2549
+ const profile = await readProfile2();
2550
+ if (profile.skillTags.length > 0) {
2551
+ const fp = profileToFingerprint2(profile);
2552
+ for (const r of match2(fp, bounties, bounties.length)) {
2553
+ ranked.set(r.job.id, { score: r.score, reason: r.reason, matchedTags: r.matchedTags });
2554
+ }
2555
+ }
2556
+ } catch {
2557
+ }
2558
+ const score = (j) => ranked.get(j.id)?.score ?? 0;
2559
+ const amt = (j) => j.bounty?.amountUSD ?? -1;
2560
+ bounties.sort((a, b) => score(b) - score(a) || amt(b) - amt(a));
2561
+ const shown = SHOW_ALL2 ? bounties : bounties.slice(0, LIMIT2);
2562
+ const matchedCount = bounties.filter((j) => score(j) > 0).length;
2563
+ console.log(
2564
+ `
2565
+ \u26A1 ${bounties.length} bount${bounties.length === 1 ? "y" : "ies"} you could knock out` + (matchedCount ? ` \u2014 ${matchedCount} matched to your profile` : "") + ` (local rank \u2014 no data sent)
2566
+ `
2567
+ );
2568
+ for (let i = 0; i < shown.length; i++) {
2569
+ const r = ranked.get(shown[i].id);
2570
+ printBounty(i, shown[i], r?.score ?? 0, r?.reason, r?.matchedTags);
2571
+ }
2572
+ if (!SHOW_ALL2 && bounties.length > shown.length) {
2573
+ console.log(`
2574
+ \u2026and ${bounties.length - shown.length} more \u2014 run with --all to see every bounty.`);
2575
+ }
2576
+ if (!process.stdin.isTTY) return;
2577
+ console.log("\n" + "\u2500".repeat(70));
2578
+ const pick = await prompt2(`
2579
+ Enter a number to open a bounty's claim page, or press Enter to exit: `);
2580
+ const idx = parseInt(pick, 10) - 1;
2581
+ if (Number.isNaN(idx) || idx < 0 || idx >= shown.length) return;
2582
+ const chosen = shown[idx];
2583
+ console.log(
2584
+ `
2585
+ Open this to claim/work the bounty (you go straight to the source \u2014 we never touch payment):
2586
+ ${chosen.bounty?.claimUrl ?? chosen.url}`
2587
+ );
2588
+ } catch (err) {
2589
+ console.error("terminalhire bounties error:", err.message ?? err);
2590
+ process.exit(1);
2591
+ }
2592
+ }
2593
+ var TERMINALHIRE_DIR4, INDEX_CACHE_FILE2, INDEX_TTL_MS2, API_URL2, DEFAULT_LIMIT2, args2, limitArg2, LIMIT2, PRICED_ONLY, SHOW_ALL2, EFFORT_LABEL;
2594
+ var init_jpi_bounties = __esm({
2595
+ "bin/jpi-bounties.js"() {
2596
+ "use strict";
2597
+ TERMINALHIRE_DIR4 = join5(homedir4(), ".terminalhire");
2598
+ INDEX_CACHE_FILE2 = join5(TERMINALHIRE_DIR4, "index-cache.json");
2599
+ INDEX_TTL_MS2 = 15 * 60 * 1e3;
2600
+ API_URL2 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
2601
+ DEFAULT_LIMIT2 = 15;
2602
+ args2 = process.argv.slice(2);
2603
+ limitArg2 = args2.indexOf("--limit");
2604
+ LIMIT2 = limitArg2 !== -1 ? parseInt(args2[limitArg2 + 1] ?? "15", 10) : DEFAULT_LIMIT2;
2605
+ PRICED_ONLY = args2.includes("--priced");
2606
+ SHOW_ALL2 = args2.includes("--all");
2607
+ EFFORT_LABEL = { small: "small (~\xBD day)", medium: "medium (~1 day)", large: "large (multi-day)" };
2608
+ }
2609
+ });
2610
+
2611
+ // bin/jpi-profile.js
2612
+ var jpi_profile_exports = {};
2613
+ __export(jpi_profile_exports, {
2614
+ run: () => run4
2615
+ });
2616
+ import { createInterface as createInterface3 } from "readline";
2617
+ function prompt3(question) {
2618
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
2619
+ return new Promise((resolve2) => {
2620
+ rl.question(question, (answer) => {
2621
+ rl.close();
2622
+ resolve2(answer.trim());
2623
+ });
2624
+ });
2625
+ }
2626
+ async function run4() {
2063
2627
  const { readProfile: readProfile2, writeProfile: writeProfile2, deleteProfile: deleteProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
2064
- const args2 = process.argv.slice(2);
2065
- if (args2.includes("--show")) {
2628
+ const args3 = process.argv.slice(2);
2629
+ if (args3.includes("--show")) {
2066
2630
  const profile = await readProfile2();
2067
2631
  console.log("\n\u2726 terminalhire local profile (encrypted at rest \u2014 shown here for your review only)\n");
2068
2632
  console.log(" Skill tags: " + (profile.skillTags.length > 0 ? profile.skillTags.join(", ") : "(none yet)"));
@@ -2092,9 +2656,9 @@ async function run3() {
2092
2656
  console.log("\nThis profile NEVER leaves your machine except in a consented lead payload.");
2093
2657
  return;
2094
2658
  }
2095
- if (args2.includes("--delete")) {
2659
+ if (args3.includes("--delete")) {
2096
2660
  console.log("\nThis will permanently delete your local terminalhire profile and encryption key.");
2097
- const answer = await prompt2('Type "yes" to confirm: ');
2661
+ const answer = await prompt3('Type "yes" to confirm: ');
2098
2662
  if (answer !== "yes") {
2099
2663
  console.log("Aborted.");
2100
2664
  process.exit(0);
@@ -2103,17 +2667,17 @@ async function run3() {
2103
2667
  console.log("Profile and key deleted from ~/.terminalhire/");
2104
2668
  return;
2105
2669
  }
2106
- if (args2.includes("--edit")) {
2670
+ if (args3.includes("--edit")) {
2107
2671
  const profile = await readProfile2();
2108
2672
  console.log("\n\u2726 terminalhire profile editor (press Enter to keep current value)\n");
2109
- const name = await prompt2(`Display name [${profile.displayName ?? "not set"}]: `);
2673
+ const name = await prompt3(`Display name [${profile.displayName ?? "not set"}]: `);
2110
2674
  if (name) profile.displayName = name;
2111
- const email = await prompt2(`Contact email [${profile.contactEmail ?? "not set"}]: `);
2675
+ const email = await prompt3(`Contact email [${profile.contactEmail ?? "not set"}]: `);
2112
2676
  if (email) profile.contactEmail = email;
2113
- const remote = await prompt2(`Remote only? (y/n) [${profile.remoteOnly ? "y" : "n"}]: `);
2677
+ const remote = await prompt3(`Remote only? (y/n) [${profile.remoteOnly ? "y" : "n"}]: `);
2114
2678
  if (remote === "y") profile.remoteOnly = true;
2115
2679
  if (remote === "n") profile.remoteOnly = false;
2116
- const floor = await prompt2(`Comp floor USD [${profile.compFloorUsd ?? "not set"}]: `);
2680
+ const floor = await prompt3(`Comp floor USD [${profile.compFloorUsd ?? "not set"}]: `);
2117
2681
  if (floor && !isNaN(parseInt(floor, 10))) profile.compFloorUsd = parseInt(floor, 10);
2118
2682
  await writeProfile2(profile);
2119
2683
  console.log("\nProfile updated (encrypted at ~/.terminalhire/profile.enc)");
@@ -2132,84 +2696,106 @@ var signal_exports = {};
2132
2696
  __export(signal_exports, {
2133
2697
  extractFingerprint: () => extractFingerprint
2134
2698
  });
2135
- import { readFileSync as readFileSync5, readdirSync } from "fs";
2136
- import { execSync } from "child_process";
2137
- import { join as join5 } from "path";
2138
- function safeExec(cmd) {
2699
+ import { readFileSync as readFileSync6, readdirSync } from "fs";
2700
+ import { execFileSync } from "child_process";
2701
+ import { join as join6 } from "path";
2702
+ function safeGit(args3, cwd) {
2139
2703
  try {
2140
- return execSync(cmd, { timeout: 2e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
2704
+ return execFileSync("git", ["-C", cwd, ...args3], {
2705
+ timeout: 2e3,
2706
+ stdio: ["ignore", "pipe", "ignore"]
2707
+ }).toString().trim();
2141
2708
  } catch {
2142
2709
  return "";
2143
2710
  }
2144
2711
  }
2145
2712
  function isEmployerContext(cwd) {
2146
- const remote = safeExec('git -C "' + cwd + '" remote get-url origin 2>/dev/null');
2713
+ const inRepo = safeGit(["rev-parse", "--is-inside-work-tree"], cwd);
2714
+ if (inRepo !== "true") return false;
2715
+ const remote = safeGit(["remote", "get-url", "origin"], cwd);
2147
2716
  if (remote) {
2148
- try {
2149
- const sshMatch = remote.match(/^git@([^:]+):/);
2150
- const httpsMatch = remote.match(/^https?:\/\/([^/]+)/);
2151
- const host = (sshMatch?.[1] ?? httpsMatch?.[1] ?? "").toLowerCase();
2152
- if (host && !PERSONAL_GIT_HOSTS.has(host)) {
2153
- return true;
2154
- }
2155
- } catch {
2156
- }
2157
- }
2158
- const email = safeExec('git -C "' + cwd + '" config user.email 2>/dev/null');
2159
- if (email) {
2160
- const domain = email.split("@")[1]?.toLowerCase() ?? "";
2161
- if (domain && !PERSONAL_EMAIL_DOMAINS.has(domain)) {
2162
- return true;
2163
- }
2164
- }
2717
+ const sshMatch = remote.match(/^git@([^:]+):/);
2718
+ const httpsMatch = remote.match(/^https?:\/\/([^/]+)/);
2719
+ const host = (sshMatch?.[1] ?? httpsMatch?.[1] ?? "").toLowerCase();
2720
+ if (host) return !PERSONAL_GIT_HOSTS.has(host);
2721
+ }
2722
+ const email = safeGit(["config", "user.email"], cwd);
2723
+ const domain = email.split("@")[1]?.toLowerCase() ?? "";
2724
+ if (domain) return !PERSONAL_EMAIL_DOMAINS.has(domain);
2165
2725
  return false;
2166
2726
  }
2167
2727
  function readJsonSafe(path) {
2168
2728
  try {
2169
- return JSON.parse(readFileSync5(path, "utf8"));
2729
+ return JSON.parse(readFileSync6(path, "utf8"));
2170
2730
  } catch {
2171
2731
  return null;
2172
2732
  }
2173
2733
  }
2174
2734
  function readFileSafe(path) {
2175
2735
  try {
2176
- return readFileSync5(path, "utf8");
2736
+ return readFileSync6(path, "utf8");
2177
2737
  } catch {
2178
2738
  return "";
2179
2739
  }
2180
2740
  }
2181
2741
  function tokensFromPackageJson(cwd) {
2182
- const pkg = readJsonSafe(join5(cwd, "package.json"));
2742
+ const pkg = readJsonSafe(join6(cwd, "package.json"));
2183
2743
  if (!pkg || typeof pkg !== "object") return [];
2184
2744
  const p = pkg;
2185
2745
  const deps = {
2186
2746
  ...typeof p["dependencies"] === "object" ? p["dependencies"] : {},
2187
- ...typeof p["devDependencies"] === "object" ? p["devDependencies"] : {}
2747
+ ...typeof p["devDependencies"] === "object" ? p["devDependencies"] : {},
2748
+ ...typeof p["peerDependencies"] === "object" ? p["peerDependencies"] : {}
2188
2749
  };
2189
2750
  return Object.keys(deps);
2190
2751
  }
2752
+ function workspaceMemberDirs(cwd) {
2753
+ const dirs = [cwd];
2754
+ for (const group of ["apps", "packages"]) {
2755
+ try {
2756
+ const groupDir = join6(cwd, group);
2757
+ for (const e of readdirSync(groupDir, { withFileTypes: true })) {
2758
+ if (e.isDirectory() && !e.isSymbolicLink()) dirs.push(join6(groupDir, e.name));
2759
+ }
2760
+ } catch {
2761
+ }
2762
+ }
2763
+ return dirs;
2764
+ }
2191
2765
  function tokensFromRequirementsTxt(cwd) {
2192
- const content = readFileSafe(join5(cwd, "requirements.txt"));
2766
+ const content = readFileSafe(join6(cwd, "requirements.txt"));
2193
2767
  if (!content) return [];
2194
2768
  return content.split("\n").map((l) => l.trim().split(/[>=<!\[;]/)[0].trim().toLowerCase()).filter(Boolean);
2195
2769
  }
2196
2770
  function tokensFromGoMod(cwd) {
2197
- const content = readFileSafe(join5(cwd, "go.mod"));
2198
- if (!content) return ["go"];
2771
+ const content = readFileSafe(join6(cwd, "go.mod"));
2772
+ if (!content) return [];
2199
2773
  const requires = Array.from(content.matchAll(/^\s+([^\s]+)\s+v/gm)).map((m) => m[1].split("/").pop() ?? "").filter(Boolean);
2200
2774
  return ["go", ...requires];
2201
2775
  }
2202
2776
  function tokensFromCargoToml(cwd) {
2203
- const content = readFileSafe(join5(cwd, "Cargo.toml"));
2777
+ const content = readFileSafe(join6(cwd, "Cargo.toml"));
2204
2778
  if (!content) return [];
2205
- const deps = Array.from(content.matchAll(/^([a-zA-Z0-9_-]+)\s*=/gm)).map((m) => m[1].toLowerCase());
2779
+ const deps = [];
2780
+ let inDeps = false;
2781
+ for (const line of content.split("\n")) {
2782
+ const trimmed = line.trim();
2783
+ const section = trimmed.match(/^\[([^\]]+)\]/);
2784
+ if (section) {
2785
+ inDeps = /(^|\.)(dependencies|dev-dependencies|build-dependencies)$/.test(section[1].trim());
2786
+ continue;
2787
+ }
2788
+ if (!inDeps) continue;
2789
+ const key = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=/);
2790
+ if (key) deps.push(key[1].toLowerCase());
2791
+ }
2206
2792
  return ["rust", ...deps];
2207
2793
  }
2208
2794
  function tokensFromFileExtensions(cwd) {
2209
2795
  const tokens = [];
2210
2796
  const scanDirs = [cwd];
2211
2797
  try {
2212
- const srcDir = join5(cwd, "src");
2798
+ const srcDir = join6(cwd, "src");
2213
2799
  readdirSync(srcDir);
2214
2800
  scanDirs.push(srcDir);
2215
2801
  } catch {
@@ -2242,11 +2828,7 @@ function inferSeniority3(rawTokens) {
2242
2828
  "opentelemetry",
2243
2829
  "prometheus",
2244
2830
  "grafana",
2245
- "microservices",
2246
- "api-design",
2247
- "security",
2248
- "oauth",
2249
- "payments"
2831
+ "microservices"
2250
2832
  ]);
2251
2833
  const midSignals = /* @__PURE__ */ new Set([
2252
2834
  "docker",
@@ -2256,7 +2838,11 @@ function inferSeniority3(rawTokens) {
2256
2838
  "postgresql",
2257
2839
  "redis",
2258
2840
  "graphql",
2259
- "trpc"
2841
+ "trpc",
2842
+ "api-design",
2843
+ "security",
2844
+ "oauth",
2845
+ "payments"
2260
2846
  ]);
2261
2847
  const normalized = new Set(normalize(rawTokens));
2262
2848
  const seniorHits = [...normalized].filter((t) => seniorSignals.has(t)).length;
@@ -2267,13 +2853,16 @@ function inferSeniority3(rawTokens) {
2267
2853
  }
2268
2854
  function extractFingerprint(cwd) {
2269
2855
  const employer = isEmployerContext(cwd);
2270
- const rawTokens = [
2271
- ...tokensFromPackageJson(cwd),
2272
- ...tokensFromRequirementsTxt(cwd),
2273
- ...tokensFromGoMod(cwd),
2274
- ...tokensFromCargoToml(cwd),
2275
- ...tokensFromFileExtensions(cwd)
2276
- ];
2856
+ const rawTokens = [];
2857
+ for (const dir of workspaceMemberDirs(cwd)) {
2858
+ rawTokens.push(
2859
+ ...tokensFromPackageJson(dir),
2860
+ ...tokensFromRequirementsTxt(dir),
2861
+ ...tokensFromGoMod(dir),
2862
+ ...tokensFromCargoToml(dir),
2863
+ ...tokensFromFileExtensions(dir)
2864
+ );
2865
+ }
2277
2866
  let skillTags = normalize(rawTokens);
2278
2867
  if (employer) {
2279
2868
  skillTags = skillTags.filter((t) => LANGUAGE_TAGS2.has(t));
@@ -2367,13 +2956,13 @@ var init_signal = __esm({
2367
2956
  // bin/jpi-learn.js
2368
2957
  var jpi_learn_exports = {};
2369
2958
  __export(jpi_learn_exports, {
2370
- run: () => run4
2959
+ run: () => run5
2371
2960
  });
2372
- async function run4() {
2961
+ async function run5() {
2373
2962
  try {
2374
- const args2 = process.argv.slice(2);
2375
- const cwdIdx = args2.indexOf("--cwd");
2376
- const cwd = cwdIdx !== -1 && args2[cwdIdx + 1] ? args2[cwdIdx + 1] : process.cwd();
2963
+ const args3 = process.argv.slice(2);
2964
+ const cwdIdx = args3.indexOf("--cwd");
2965
+ const cwd = cwdIdx !== -1 && args3[cwdIdx + 1] ? args3[cwdIdx + 1] : process.cwd();
2377
2966
  const { extractFingerprint: extractFingerprint2 } = await Promise.resolve().then(() => (init_signal(), signal_exports));
2378
2967
  const { readProfile: readProfile2, writeProfile: writeProfile2, accumulateSession: accumulateSession2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
2379
2968
  const fingerprint = extractFingerprint2(cwd);
@@ -2396,7 +2985,7 @@ var init_jpi_learn = __esm({
2396
2985
  "use strict";
2397
2986
  isMain = process.argv[1]?.endsWith("jpi-learn.js") || process.argv[1]?.endsWith("jpi-learn");
2398
2987
  if (isMain) {
2399
- run4();
2988
+ run5();
2400
2989
  }
2401
2990
  }
2402
2991
  });
@@ -2404,23 +2993,23 @@ var init_jpi_learn = __esm({
2404
2993
  // bin/jpi-config.js
2405
2994
  var jpi_config_exports = {};
2406
2995
  __export(jpi_config_exports, {
2407
- run: () => run5
2996
+ run: () => run6
2408
2997
  });
2409
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync4 } from "fs";
2410
- import { join as join6 } from "path";
2411
- import { homedir as homedir4 } from "os";
2998
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync4 } from "fs";
2999
+ import { join as join7 } from "path";
3000
+ import { homedir as homedir5 } from "os";
2412
3001
  function readConfig() {
2413
3002
  try {
2414
3003
  if (!existsSync4(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
2415
- return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync6(CONFIG_FILE, "utf8")) };
3004
+ return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync7(CONFIG_FILE, "utf8")) };
2416
3005
  } catch {
2417
3006
  return { ...DEFAULT_CONFIG };
2418
3007
  }
2419
3008
  }
2420
3009
  function writeConfig(patch) {
2421
- mkdirSync4(TERMINALHIRE_DIR4, { recursive: true });
3010
+ mkdirSync5(TERMINALHIRE_DIR5, { recursive: true });
2422
3011
  const merged = { ...readConfig(), ...patch };
2423
- writeFileSync4(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
3012
+ writeFileSync5(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
2424
3013
  }
2425
3014
  function parseNudgeMode(raw) {
2426
3015
  if (raw === "session" || raw === "always") return raw;
@@ -2428,9 +3017,9 @@ function parseNudgeMode(raw) {
2428
3017
  if (m && parseInt(m[1], 10) >= 1) return raw;
2429
3018
  return null;
2430
3019
  }
2431
- async function run5() {
2432
- const args2 = process.argv.slice(2);
2433
- const filtered = args2[0] === "config" ? args2.slice(1) : args2;
3020
+ async function run6() {
3021
+ const args3 = process.argv.slice(2);
3022
+ const filtered = args3[0] === "config" ? args3.slice(1) : args3;
2434
3023
  if (filtered.includes("--show") || filtered.length === 0) {
2435
3024
  const cfg = readConfig();
2436
3025
  const envOverride = process.env["TERMINALHIRE_NUDGE"];
@@ -2471,12 +3060,12 @@ async function run5() {
2471
3060
  console.error(" terminalhire config --show");
2472
3061
  process.exit(1);
2473
3062
  }
2474
- var TERMINALHIRE_DIR4, CONFIG_FILE, DEFAULT_CONFIG;
3063
+ var TERMINALHIRE_DIR5, CONFIG_FILE, DEFAULT_CONFIG;
2475
3064
  var init_jpi_config = __esm({
2476
3065
  "bin/jpi-config.js"() {
2477
3066
  "use strict";
2478
- TERMINALHIRE_DIR4 = join6(homedir4(), ".terminalhire");
2479
- CONFIG_FILE = join6(TERMINALHIRE_DIR4, "config.json");
3067
+ TERMINALHIRE_DIR5 = join7(homedir5(), ".terminalhire");
3068
+ CONFIG_FILE = join7(TERMINALHIRE_DIR5, "config.json");
2480
3069
  DEFAULT_CONFIG = { nudge: "session" };
2481
3070
  }
2482
3071
  });
@@ -2499,25 +3088,25 @@ __export(spinner_exports, {
2499
3088
  readSpinnerConfig: () => readSpinnerConfig
2500
3089
  });
2501
3090
  import {
2502
- readFileSync as readFileSync7,
2503
- writeFileSync as writeFileSync5,
3091
+ readFileSync as readFileSync8,
3092
+ writeFileSync as writeFileSync6,
2504
3093
  existsSync as existsSync5,
2505
- mkdirSync as mkdirSync5,
3094
+ mkdirSync as mkdirSync6,
2506
3095
  renameSync
2507
3096
  } from "fs";
2508
- import { join as join7, dirname } from "path";
2509
- import { homedir as homedir5 } from "os";
3097
+ import { join as join8, dirname } from "path";
3098
+ import { homedir as homedir6 } from "os";
2510
3099
  function readJson(path, fallback) {
2511
3100
  try {
2512
- return existsSync5(path) ? JSON.parse(readFileSync7(path, "utf8")) : fallback;
3101
+ return existsSync5(path) ? JSON.parse(readFileSync8(path, "utf8")) : fallback;
2513
3102
  } catch {
2514
3103
  return fallback;
2515
3104
  }
2516
3105
  }
2517
3106
  function atomicWriteJson(path, obj) {
2518
- mkdirSync5(dirname(path), { recursive: true });
3107
+ mkdirSync6(dirname(path), { recursive: true });
2519
3108
  const tmp = `${path}.tmp-${process.pid}`;
2520
- writeFileSync5(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
3109
+ writeFileSync6(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
2521
3110
  renameSync(tmp, path);
2522
3111
  }
2523
3112
  function titleCase(s) {
@@ -2588,16 +3177,26 @@ function buildContextVerbs(topMatches, sessionTags) {
2588
3177
  for (const t of sess) {
2589
3178
  if (roleTags.has(t) && !overlap.includes(t)) overlap.push(t);
2590
3179
  }
3180
+ let headers;
2591
3181
  if (overlap.length >= 2) {
2592
3182
  const a = titleCase(overlap[0]);
2593
3183
  const b = titleCase(overlap[1]);
2594
- return [`\u2726 Fits your ${a} + ${b} work`, `\u2726 A role matching what you're building`];
2595
- }
2596
- if (overlap.length === 1) {
3184
+ headers = [`\u2726 Fits your ${a} + ${b} work`, `\u2726 A role matching what you're building`];
3185
+ } else if (overlap.length === 1) {
2597
3186
  const a = titleCase(overlap[0]);
2598
- return [`\u2726 A role matching your ${a} work`, `\u2726 Your ${a} work \u2014 link in the tip below`];
3187
+ headers = [`\u2726 A role matching your ${a} work`, `\u2726 Your ${a} work \u2014 link in the tip below`];
3188
+ } else {
3189
+ headers = [`\u2726 A role that fits your work`, `\u2726 Job match for you \u2014 link in the tip below`];
3190
+ }
3191
+ const list = Array.isArray(topMatches) ? topMatches : [];
3192
+ const bounty = list.find((m) => m && m.source === "bounty" && m.amountUSD != null) || list.find((m) => m && m.source === "bounty");
3193
+ if (bounty) {
3194
+ const money = bounty.amountUSD != null ? `$${bounty.amountUSD.toLocaleString()} ` : "";
3195
+ const bountyHeader = `\u2726 \u{1F48E} A ${money}bounty in your stack \u2014 link below`;
3196
+ if (list[0] && list[0].source === "bounty") headers.unshift(bountyHeader);
3197
+ else headers.push(bountyHeader);
2599
3198
  }
2600
- return [`\u2726 A role that fits your work`, `\u2726 Job match for you \u2014 link in the tip below`];
3199
+ return headers;
2601
3200
  }
2602
3201
  function buildSpinnerPool(topMatches, max = 6, opts = {}) {
2603
3202
  const { sessionTags, frequency = "always" } = opts;
@@ -2702,7 +3301,13 @@ function buildTips(topMatches, baseUrl, max = 8) {
2702
3301
  const pct = Math.max(1, Math.min(99, Math.round((Number(m.score) || 0) * 100)));
2703
3302
  const token = Buffer.from(String(m.id)).toString("base64url");
2704
3303
  const url = `${base}/j/${token}`;
2705
- out.push(`\u2197 ${title} @ ${company} \xB7 ${pct}% \u2014 ${url}`);
3304
+ if (source === "bounty") {
3305
+ const money = m.amountUSD != null ? `$${Number(m.amountUSD).toLocaleString()}` : "$\u2014";
3306
+ const repo = m.repo || companyRaw;
3307
+ out.push(`\u{1F48E} ${money} \xB7 ${title} \xB7 ${repo} \xB7 ${pct}% \u2014 ${url}`);
3308
+ } else {
3309
+ out.push(`\u2197 ${title} @ ${company} \xB7 ${pct}% \u2014 ${url}`);
3310
+ }
2706
3311
  if (out.length >= max) break;
2707
3312
  }
2708
3313
  return out;
@@ -2748,10 +3353,10 @@ var TH_DIR, CLAUDE_SETTINGS, CONFIG_FILE2, SPINNER_STATE_FILE, SPINNER_DEFAULTS,
2748
3353
  var init_spinner = __esm({
2749
3354
  "bin/spinner.js"() {
2750
3355
  "use strict";
2751
- TH_DIR = process.env["TERMINALHIRE_DIR"] || join7(homedir5(), ".terminalhire");
2752
- CLAUDE_SETTINGS = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join7(homedir5(), ".claude", "settings.json");
2753
- CONFIG_FILE2 = join7(TH_DIR, "config.json");
2754
- SPINNER_STATE_FILE = join7(TH_DIR, "spinner-state.json");
3356
+ TH_DIR = process.env["TERMINALHIRE_DIR"] || join8(homedir6(), ".terminalhire");
3357
+ CLAUDE_SETTINGS = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join8(homedir6(), ".claude", "settings.json");
3358
+ CONFIG_FILE2 = join8(TH_DIR, "config.json");
3359
+ SPINNER_STATE_FILE = join8(TH_DIR, "spinner-state.json");
2755
3360
  SPINNER_DEFAULTS = { enabled: false, mode: "append", max: 6, frequency: "sometimes" };
2756
3361
  VERB_INTROS = ["Matched:", "You\u2019d fit:", "Worth a look:", "On your radar:", "Fits your stack:"];
2757
3362
  }
@@ -2760,29 +3365,29 @@ var init_spinner = __esm({
2760
3365
  // bin/jpi-spinner.js
2761
3366
  var jpi_spinner_exports = {};
2762
3367
  __export(jpi_spinner_exports, {
2763
- run: () => run6
3368
+ run: () => run7
2764
3369
  });
2765
3370
  import {
2766
- readFileSync as readFileSync8,
2767
- writeFileSync as writeFileSync6,
3371
+ readFileSync as readFileSync9,
3372
+ writeFileSync as writeFileSync7,
2768
3373
  copyFileSync,
2769
3374
  existsSync as existsSync6,
2770
- mkdirSync as mkdirSync6
3375
+ mkdirSync as mkdirSync7
2771
3376
  } from "fs";
2772
- import { join as join8 } from "path";
2773
- import { homedir as homedir6 } from "os";
2774
- import { createInterface as createInterface3 } from "readline";
3377
+ import { join as join9 } from "path";
3378
+ import { homedir as homedir7 } from "os";
3379
+ import { createInterface as createInterface4 } from "readline";
2775
3380
  function readConfig2() {
2776
3381
  try {
2777
- return existsSync6(CONFIG_FILE3) ? JSON.parse(readFileSync8(CONFIG_FILE3, "utf8")) : {};
3382
+ return existsSync6(CONFIG_FILE3) ? JSON.parse(readFileSync9(CONFIG_FILE3, "utf8")) : {};
2778
3383
  } catch {
2779
3384
  return {};
2780
3385
  }
2781
3386
  }
2782
3387
  function writeConfig2(patch) {
2783
- mkdirSync6(TH_DIR2, { recursive: true });
3388
+ mkdirSync7(TH_DIR2, { recursive: true });
2784
3389
  const merged = { ...readConfig2(), ...patch };
2785
- writeFileSync6(CONFIG_FILE3, JSON.stringify(merged, null, 2) + "\n", "utf8");
3390
+ writeFileSync7(CONFIG_FILE3, JSON.stringify(merged, null, 2) + "\n", "utf8");
2786
3391
  }
2787
3392
  function backupSettings() {
2788
3393
  if (!existsSync6(SETTINGS_PATH)) return null;
@@ -2792,7 +3397,7 @@ function backupSettings() {
2792
3397
  return backupPath;
2793
3398
  }
2794
3399
  function ask(question) {
2795
- const rl = createInterface3({ input: process.stdin, output: process.stdout });
3400
+ const rl = createInterface4({ input: process.stdin, output: process.stdout });
2796
3401
  return new Promise((res) => {
2797
3402
  rl.question(question, (answer) => {
2798
3403
  rl.close();
@@ -2802,20 +3407,20 @@ function ask(question) {
2802
3407
  }
2803
3408
  function readTopMatches() {
2804
3409
  try {
2805
- const c = JSON.parse(readFileSync8(CACHE_FILE, "utf8"));
3410
+ const c = JSON.parse(readFileSync9(CACHE_FILE, "utf8"));
2806
3411
  return Array.isArray(c.topMatches) ? c.topMatches : [];
2807
3412
  } catch {
2808
3413
  return [];
2809
3414
  }
2810
3415
  }
2811
- async function run6() {
2812
- const args2 = process.argv.slice(2).filter((a) => a !== "spinner");
2813
- const has = (f) => args2.includes(f);
3416
+ async function run7() {
3417
+ const args3 = process.argv.slice(2).filter((a) => a !== "spinner");
3418
+ const has = (f) => args3.includes(f);
2814
3419
  const val = (f) => {
2815
- const i = args2.indexOf(f);
2816
- return i >= 0 ? args2[i + 1] : void 0;
3420
+ const i = args3.indexOf(f);
3421
+ return i >= 0 ? args3[i + 1] : void 0;
2817
3422
  };
2818
- if (has("--show") || args2.length === 0) {
3423
+ if (has("--show") || args3.length === 0) {
2819
3424
  const sc = readSpinnerConfig();
2820
3425
  console.log("");
2821
3426
  console.log("terminalhire spinner \u2014 job matches in the Claude Code spinner line");
@@ -2945,25 +3550,25 @@ var init_jpi_spinner = __esm({
2945
3550
  "bin/jpi-spinner.js"() {
2946
3551
  "use strict";
2947
3552
  init_spinner();
2948
- TH_DIR2 = process.env["TERMINALHIRE_DIR"] || join8(homedir6(), ".terminalhire");
2949
- CONFIG_FILE3 = join8(TH_DIR2, "config.json");
2950
- SETTINGS_PATH = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join8(homedir6(), ".claude", "settings.json");
2951
- CACHE_FILE = join8(TH_DIR2, "index-cache.json");
3553
+ TH_DIR2 = process.env["TERMINALHIRE_DIR"] || join9(homedir7(), ".terminalhire");
3554
+ CONFIG_FILE3 = join9(TH_DIR2, "config.json");
3555
+ SETTINGS_PATH = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join9(homedir7(), ".claude", "settings.json");
3556
+ CACHE_FILE = join9(TH_DIR2, "index-cache.json");
2952
3557
  }
2953
3558
  });
2954
3559
 
2955
3560
  // bin/jpi-sync.js
2956
3561
  var jpi_sync_exports = {};
2957
3562
  __export(jpi_sync_exports, {
2958
- run: () => run7
3563
+ run: () => run8
2959
3564
  });
2960
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, existsSync as existsSync7, rmSync as rmSync2 } from "fs";
2961
- import { join as join9 } from "path";
2962
- import { homedir as homedir7, hostname as osHostname } from "os";
2963
- import { createInterface as createInterface4 } from "readline";
3565
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8, existsSync as existsSync7, rmSync as rmSync2 } from "fs";
3566
+ import { join as join10 } from "path";
3567
+ import { homedir as homedir8, hostname as osHostname } from "os";
3568
+ import { createInterface as createInterface5 } from "readline";
2964
3569
  import { spawn } from "child_process";
2965
3570
  function ask2(question) {
2966
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
3571
+ const rl = createInterface5({ input: process.stdin, output: process.stdout });
2967
3572
  return new Promise((res) => {
2968
3573
  rl.question(question, (answer) => {
2969
3574
  rl.close();
@@ -2973,14 +3578,14 @@ function ask2(question) {
2973
3578
  }
2974
3579
  function readMarker() {
2975
3580
  try {
2976
- return existsSync7(TIER1_MARKER) ? JSON.parse(readFileSync9(TIER1_MARKER, "utf8")) : null;
3581
+ return existsSync7(TIER1_MARKER) ? JSON.parse(readFileSync10(TIER1_MARKER, "utf8")) : null;
2977
3582
  } catch {
2978
3583
  return null;
2979
3584
  }
2980
3585
  }
2981
3586
  function writeMarker(marker) {
2982
- mkdirSync7(TH_DIR3, { recursive: true });
2983
- writeFileSync7(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
3587
+ mkdirSync8(TH_DIR3, { recursive: true });
3588
+ writeFileSync8(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
2984
3589
  }
2985
3590
  function clearMarker() {
2986
3591
  try {
@@ -3032,19 +3637,19 @@ function renderPreview(fields) {
3032
3637
  }
3033
3638
  function openInBrowser(url) {
3034
3639
  let cmd;
3035
- let args2;
3640
+ let args3;
3036
3641
  if (process.platform === "darwin") {
3037
3642
  cmd = "open";
3038
- args2 = [url];
3643
+ args3 = [url];
3039
3644
  } else if (process.platform === "win32") {
3040
3645
  cmd = "cmd";
3041
- args2 = ["/c", "start", "", url];
3646
+ args3 = ["/c", "start", "", url];
3042
3647
  } else {
3043
3648
  cmd = "xdg-open";
3044
- args2 = [url];
3649
+ args3 = [url];
3045
3650
  }
3046
3651
  try {
3047
- const child = spawn(cmd, args2, { stdio: "ignore", detached: true });
3652
+ const child = spawn(cmd, args3, { stdio: "ignore", detached: true });
3048
3653
  child.on("error", () => {
3049
3654
  });
3050
3655
  child.unref();
@@ -3067,7 +3672,7 @@ async function runPush() {
3067
3672
  const fields = buildConsentFields(profile);
3068
3673
  renderPreview(fields);
3069
3674
  await new Promise((resolve2) => {
3070
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
3675
+ const rl = createInterface5({ input: process.stdin, output: process.stdout });
3071
3676
  rl.question(
3072
3677
  " Press Enter to open your browser to authorize + consent (or Ctrl-C to cancel)... ",
3073
3678
  () => {
@@ -3265,7 +3870,7 @@ async function runDelete() {
3265
3870
  console.log("\n Requesting deletion...");
3266
3871
  let res;
3267
3872
  try {
3268
- res = await fetch(`${API_URL2}/api/profile-sync`, {
3873
+ res = await fetch(`${API_URL3}/api/profile-sync`, {
3269
3874
  method: "DELETE",
3270
3875
  headers: { "Content-Type": "application/json" },
3271
3876
  body: JSON.stringify({ consentToken, login, deleteToken }),
@@ -3289,9 +3894,9 @@ async function runDelete() {
3289
3894
  clearMarker();
3290
3895
  console.log("\n Synced profile deleted and local marker cleared.\n");
3291
3896
  }
3292
- async function run7() {
3293
- const args2 = process.argv.slice(2).filter((a) => a !== "sync");
3294
- const has = (f) => args2.includes(f);
3897
+ async function run8() {
3898
+ const args3 = process.argv.slice(2).filter((a) => a !== "sync");
3899
+ const has = (f) => args3.includes(f);
3295
3900
  if (has("--push") || has("--enable")) {
3296
3901
  await runPush();
3297
3902
  return;
@@ -3315,13 +3920,13 @@ async function run7() {
3315
3920
  console.log(" This is NOT required to use terminalhire.");
3316
3921
  console.log("");
3317
3922
  }
3318
- var TH_DIR3, TIER1_MARKER, API_URL2, SYNC_BASE, POLL_INTERVAL_MS, POLL_TIMEOUT_MS, CONSENT_VERSION;
3923
+ var TH_DIR3, TIER1_MARKER, API_URL3, SYNC_BASE, POLL_INTERVAL_MS, POLL_TIMEOUT_MS, CONSENT_VERSION;
3319
3924
  var init_jpi_sync = __esm({
3320
3925
  "bin/jpi-sync.js"() {
3321
3926
  "use strict";
3322
- TH_DIR3 = process.env["TERMINALHIRE_DIR"] || join9(homedir7(), ".terminalhire");
3323
- TIER1_MARKER = join9(TH_DIR3, "tier1.json");
3324
- API_URL2 = process.env["TERMINALHIRE_API_URL"] || process.env["JPI_API_URL"] || "https://terminalhire.com";
3927
+ TH_DIR3 = process.env["TERMINALHIRE_DIR"] || join10(homedir8(), ".terminalhire");
3928
+ TIER1_MARKER = join10(TH_DIR3, "tier1.json");
3929
+ API_URL3 = process.env["TERMINALHIRE_API_URL"] || process.env["JPI_API_URL"] || "https://terminalhire.com";
3325
3930
  SYNC_BASE = "https://www.terminalhire.com";
3326
3931
  POLL_INTERVAL_MS = 2e3;
3327
3932
  POLL_TIMEOUT_MS = 10 * 60 * 1e3;
@@ -3332,16 +3937,16 @@ var init_jpi_sync = __esm({
3332
3937
  // bin/jpi-init.js
3333
3938
  var jpi_init_exports = {};
3334
3939
  __export(jpi_init_exports, {
3335
- run: () => run8
3940
+ run: () => run9
3336
3941
  });
3337
3942
  import { existsSync as existsSync8 } from "fs";
3338
- import { join as join10, resolve } from "path";
3943
+ import { join as join11, resolve } from "path";
3339
3944
  import { fileURLToPath as fileURLToPath3 } from "url";
3340
- import { createInterface as createInterface5 } from "readline";
3945
+ import { createInterface as createInterface6 } from "readline";
3341
3946
  import { spawnSync, spawn as spawn2 } from "child_process";
3342
- import { homedir as homedir8 } from "os";
3947
+ import { homedir as homedir9 } from "os";
3343
3948
  function ask3(question) {
3344
- const rl = createInterface5({ input: process.stdin, output: process.stdout });
3949
+ const rl = createInterface6({ input: process.stdin, output: process.stdout });
3345
3950
  return new Promise((resolve2) => {
3346
3951
  rl.question(question, (answer) => {
3347
3952
  rl.close();
@@ -3350,18 +3955,18 @@ function ask3(question) {
3350
3955
  });
3351
3956
  }
3352
3957
  function resolveScript(name) {
3353
- const distPath = resolve(join10(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
3354
- const legacyPath = resolve(join10(__dirname2, `${name}.js`));
3958
+ const distPath = resolve(join11(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
3959
+ const legacyPath = resolve(join11(__dirname2, `${name}.js`));
3355
3960
  return existsSync8(distPath) ? distPath : legacyPath;
3356
3961
  }
3357
3962
  function resolveInstallJs() {
3358
- const fromDist = resolve(join10(__dirname2, "..", "..", "install.js"));
3359
- const fromBin = resolve(join10(__dirname2, "..", "install.js"));
3963
+ const fromDist = resolve(join11(__dirname2, "..", "..", "install.js"));
3964
+ const fromBin = resolve(join11(__dirname2, "..", "install.js"));
3360
3965
  if (existsSync8(fromDist)) return fromDist;
3361
3966
  if (existsSync8(fromBin)) return fromBin;
3362
3967
  return fromBin;
3363
3968
  }
3364
- async function run8() {
3969
+ async function run9() {
3365
3970
  console.log("");
3366
3971
  console.log("\u250C\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\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\u2510");
3367
3972
  console.log("\u2502 terminalhire init \u2014 one-command onboarding \u2502");
@@ -3464,17 +4069,17 @@ var init_jpi_init = __esm({
3464
4069
  // bin/jpi-refresh.js
3465
4070
  var jpi_refresh_exports = {};
3466
4071
  __export(jpi_refresh_exports, {
3467
- run: () => run9
4072
+ run: () => run10
3468
4073
  });
3469
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, existsSync as existsSync9, mkdirSync as mkdirSync8 } from "fs";
3470
- import { join as join11 } from "path";
3471
- import { homedir as homedir9 } from "os";
4074
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync9 } from "fs";
4075
+ import { join as join12 } from "path";
4076
+ import { homedir as homedir10 } from "os";
3472
4077
  import { fileURLToPath as fileURLToPath4 } from "url";
3473
- async function run9() {
4078
+ async function run10() {
3474
4079
  try {
3475
4080
  let index;
3476
4081
  try {
3477
- const res = await fetch(`${API_URL3}/api/index`, {
4082
+ const res = await fetch(`${API_URL4}/api/index`, {
3478
4083
  signal: AbortSignal.timeout(15e3),
3479
4084
  headers: { "Accept": "application/json" }
3480
4085
  });
@@ -3507,19 +4112,24 @@ async function run9() {
3507
4112
  company: r.job.company,
3508
4113
  score: r.score,
3509
4114
  remote: r.job.remote,
3510
- matchedTags: r.matchedTags
4115
+ matchedTags: r.matchedTags,
4116
+ // Bounty fields so the spinner can render bounty framing ($ + 💎).
4117
+ // Public job text, stays LOCAL (same as the rest of topMatches).
4118
+ source: r.job.source,
4119
+ amountUSD: r.job.bounty?.amountUSD,
4120
+ repo: r.job.bounty?.repoFullName
3511
4121
  }));
3512
4122
  }
3513
4123
  } catch {
3514
4124
  }
3515
- mkdirSync8(TERMINALHIRE_DIR5, { recursive: true });
4125
+ mkdirSync9(TERMINALHIRE_DIR6, { recursive: true });
3516
4126
  const cacheEntry = {
3517
4127
  ts: Date.now(),
3518
4128
  index,
3519
4129
  matchCount,
3520
4130
  topMatches
3521
4131
  };
3522
- writeFileSync8(INDEX_CACHE_FILE2, JSON.stringify(cacheEntry), "utf8");
4132
+ writeFileSync9(INDEX_CACHE_FILE3, JSON.stringify(cacheEntry), "utf8");
3523
4133
  try {
3524
4134
  const {
3525
4135
  readSpinnerConfig: readSpinnerConfig2,
@@ -3546,7 +4156,7 @@ async function run9() {
3546
4156
  const verbs = buildSpinnerPool2(ranked, sc.max, { sessionTags, frequency: sc.frequency });
3547
4157
  if (verbs.length > 0) applySpinnerVerbs2(verbs, sc.mode);
3548
4158
  else clearSpinnerVerbs2();
3549
- const tips = buildTips2(ranked, API_URL3, 8);
4159
+ const tips = buildTips2(ranked, API_URL4, 8);
3550
4160
  if (tips.length > 0) applySpinnerTips2(tips);
3551
4161
  else clearSpinnerTips2();
3552
4162
  } else {
@@ -3563,30 +4173,30 @@ async function run9() {
3563
4173
  process.exit(1);
3564
4174
  }
3565
4175
  }
3566
- var __dirname3, TERMINALHIRE_DIR5, INDEX_CACHE_FILE2, API_URL3;
4176
+ var __dirname3, TERMINALHIRE_DIR6, INDEX_CACHE_FILE3, API_URL4;
3567
4177
  var init_jpi_refresh = __esm({
3568
4178
  "bin/jpi-refresh.js"() {
3569
4179
  "use strict";
3570
4180
  __dirname3 = fileURLToPath4(new URL(".", import.meta.url));
3571
- TERMINALHIRE_DIR5 = join11(homedir9(), ".terminalhire");
3572
- INDEX_CACHE_FILE2 = join11(TERMINALHIRE_DIR5, "index-cache.json");
3573
- API_URL3 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
4181
+ TERMINALHIRE_DIR6 = join12(homedir10(), ".terminalhire");
4182
+ INDEX_CACHE_FILE3 = join12(TERMINALHIRE_DIR6, "index-cache.json");
4183
+ API_URL4 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
3574
4184
  }
3575
4185
  });
3576
4186
 
3577
4187
  // bin/jpi-save.js
3578
4188
  var jpi_save_exports = {};
3579
4189
  __export(jpi_save_exports, {
3580
- run: () => run10
4190
+ run: () => run11
3581
4191
  });
3582
- import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
3583
- import { join as join12 } from "path";
3584
- import { homedir as homedir10 } from "os";
4192
+ import { readFileSync as readFileSync12, existsSync as existsSync10 } from "fs";
4193
+ import { join as join13 } from "path";
4194
+ import { homedir as homedir11 } from "os";
3585
4195
  import { fileURLToPath as fileURLToPath5 } from "url";
3586
4196
  function findJobInCache(jobId) {
3587
4197
  try {
3588
- if (!existsSync10(INDEX_CACHE_FILE3)) return null;
3589
- const raw = readFileSync11(INDEX_CACHE_FILE3, "utf8");
4198
+ if (!existsSync10(INDEX_CACHE_FILE4)) return null;
4199
+ const raw = readFileSync12(INDEX_CACHE_FILE4, "utf8");
3590
4200
  const entry = JSON.parse(raw);
3591
4201
  const jobs = entry?.index?.jobs ?? [];
3592
4202
  return jobs.find((j) => j.id === jobId) ?? null;
@@ -3655,7 +4265,7 @@ async function cmdUnsave(jobId) {
3655
4265
  process.exit(1);
3656
4266
  }
3657
4267
  }
3658
- async function run10() {
4268
+ async function run11() {
3659
4269
  const verb = process.argv[2];
3660
4270
  const jobId = process.argv[3];
3661
4271
  try {
@@ -3674,31 +4284,31 @@ async function run10() {
3674
4284
  process.exit(1);
3675
4285
  }
3676
4286
  }
3677
- var __dirname4, TERMINALHIRE_DIR6, INDEX_CACHE_FILE3;
4287
+ var __dirname4, TERMINALHIRE_DIR7, INDEX_CACHE_FILE4;
3678
4288
  var init_jpi_save = __esm({
3679
4289
  "bin/jpi-save.js"() {
3680
4290
  "use strict";
3681
4291
  __dirname4 = fileURLToPath5(new URL(".", import.meta.url));
3682
- TERMINALHIRE_DIR6 = join12(homedir10(), ".terminalhire");
3683
- INDEX_CACHE_FILE3 = join12(TERMINALHIRE_DIR6, "index-cache.json");
4292
+ TERMINALHIRE_DIR7 = join13(homedir11(), ".terminalhire");
4293
+ INDEX_CACHE_FILE4 = join13(TERMINALHIRE_DIR7, "index-cache.json");
3684
4294
  }
3685
4295
  });
3686
4296
 
3687
4297
  // bin/jpi-dispatch.js
3688
4298
  import { fileURLToPath as fileURLToPath6 } from "url";
3689
- import { join as join13, dirname as dirname2 } from "path";
3690
- import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
4299
+ import { join as join14, dirname as dirname2 } from "path";
4300
+ import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
3691
4301
  import { createRequire } from "module";
3692
4302
  var __dirname5 = fileURLToPath6(new URL(".", import.meta.url));
3693
4303
  function readPackageVersion() {
3694
4304
  try {
3695
4305
  const candidates = [
3696
- join13(__dirname5, "..", "..", "package.json"),
3697
- join13(__dirname5, "..", "package.json")
4306
+ join14(__dirname5, "..", "..", "package.json"),
4307
+ join14(__dirname5, "..", "package.json")
3698
4308
  ];
3699
4309
  for (const p of candidates) {
3700
4310
  if (existsSync11(p)) {
3701
- const pkg = JSON.parse(readFileSync12(p, "utf8"));
4311
+ const pkg = JSON.parse(readFileSync13(p, "utf8"));
3702
4312
  if (pkg.version) return pkg.version;
3703
4313
  }
3704
4314
  }
@@ -3709,7 +4319,7 @@ function readPackageVersion() {
3709
4319
  var firstArg = process.argv[2];
3710
4320
  if (!firstArg && !process.stdin.isTTY) {
3711
4321
  const { default: childProcess } = await import("child_process");
3712
- const nudgeScript = join13(__dirname5, "jpi.js");
4322
+ const nudgeScript = join14(__dirname5, "jpi.js");
3713
4323
  const child = childProcess.spawnSync(process.execPath, [nudgeScript], {
3714
4324
  stdio: ["inherit", "inherit", "inherit"]
3715
4325
  });
@@ -3726,6 +4336,8 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
3726
4336
  console.log(" terminalhire jobs Fetch job index, match locally, browse roles");
3727
4337
  console.log(" terminalhire jobs --limit N Show top N results (default: 10)");
3728
4338
  console.log(" terminalhire jobs --remote-only Filter to remote roles only");
4339
+ console.log(" terminalhire bounties Day-sized paid tasks you can knock out today");
4340
+ console.log(" terminalhire bounties --priced Only bounties with a known $ amount");
3729
4341
  console.log(" terminalhire profile --show Display your encrypted local profile");
3730
4342
  console.log(" terminalhire profile --edit Set displayName, contactEmail, prefs");
3731
4343
  console.log(" terminalhire profile --delete Wipe profile and encryption key from disk");
@@ -3772,6 +4384,12 @@ if (firstArg === "jobs") {
3772
4384
  await mod.run();
3773
4385
  process.exit(0);
3774
4386
  }
4387
+ if (firstArg === "bounties") {
4388
+ process.argv.splice(2, 1);
4389
+ const mod = await Promise.resolve().then(() => (init_jpi_bounties(), jpi_bounties_exports));
4390
+ await mod.run();
4391
+ process.exit(0);
4392
+ }
3775
4393
  if (firstArg === "profile") {
3776
4394
  const mod = await Promise.resolve().then(() => (init_jpi_profile(), jpi_profile_exports));
3777
4395
  await mod.run();