terminalhire 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -101,7 +101,7 @@ async function runDeviceFlow() {
101
101
  await writeGitHubToken(MOCK_TOKEN);
102
102
  return MOCK_LOGIN;
103
103
  }
104
- const clientId = process.env["GITHUB_DEVICE_CLIENT_ID"] ?? process.env["GITHUB_CLIENT_ID"] ?? DEV_PLACEHOLDER_CLIENT_ID;
104
+ const clientId = process.env["GITHUB_DEVICE_CLIENT_ID"] ?? process.env["GITHUB_CLIENT_ID"] ?? BAKED_IN_CLIENT_ID;
105
105
  if (clientId === "Iv1.PLACEHOLDER_REGISTER_YOUR_APP") {
106
106
  console.warn("\nWarning: GITHUB_CLIENT_ID env var looks like a placeholder.");
107
107
  console.warn("Remove it to use the baked-in client ID, or set it to your own OAuth App Client ID.\n");
@@ -124,7 +124,7 @@ async function runDeviceFlow() {
124
124
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
125
125
  console.log(` 1. Open: ${deviceData.verification_uri}`);
126
126
  console.log(` 2. Enter code: ${deviceData.user_code}`);
127
- console.log(' 3. Authorize "jpi" (scope: read:user \u2014 public data only)');
127
+ console.log(' 3. Authorize "Terminalhire" (scope: read:user \u2014 public data only)');
128
128
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
129
129
  console.log(" Waiting for authorization...");
130
130
  console.log("");
@@ -204,7 +204,7 @@ async function resolveStoredLogin() {
204
204
  function sleep(ms) {
205
205
  return new Promise((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) {
@@ -2748,10 +3337,10 @@ var TH_DIR, CLAUDE_SETTINGS, CONFIG_FILE2, SPINNER_STATE_FILE, SPINNER_DEFAULTS,
2748
3337
  var init_spinner = __esm({
2749
3338
  "bin/spinner.js"() {
2750
3339
  "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");
3340
+ TH_DIR = process.env["TERMINALHIRE_DIR"] || join8(homedir6(), ".terminalhire");
3341
+ CLAUDE_SETTINGS = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join8(homedir6(), ".claude", "settings.json");
3342
+ CONFIG_FILE2 = join8(TH_DIR, "config.json");
3343
+ SPINNER_STATE_FILE = join8(TH_DIR, "spinner-state.json");
2755
3344
  SPINNER_DEFAULTS = { enabled: false, mode: "append", max: 6, frequency: "sometimes" };
2756
3345
  VERB_INTROS = ["Matched:", "You\u2019d fit:", "Worth a look:", "On your radar:", "Fits your stack:"];
2757
3346
  }
@@ -2760,29 +3349,29 @@ var init_spinner = __esm({
2760
3349
  // bin/jpi-spinner.js
2761
3350
  var jpi_spinner_exports = {};
2762
3351
  __export(jpi_spinner_exports, {
2763
- run: () => run6
3352
+ run: () => run7
2764
3353
  });
2765
3354
  import {
2766
- readFileSync as readFileSync8,
2767
- writeFileSync as writeFileSync6,
3355
+ readFileSync as readFileSync9,
3356
+ writeFileSync as writeFileSync7,
2768
3357
  copyFileSync,
2769
3358
  existsSync as existsSync6,
2770
- mkdirSync as mkdirSync6
3359
+ mkdirSync as mkdirSync7
2771
3360
  } from "fs";
2772
- import { join as join8 } from "path";
2773
- import { homedir as homedir6 } from "os";
2774
- import { createInterface as createInterface3 } from "readline";
3361
+ import { join as join9 } from "path";
3362
+ import { homedir as homedir7 } from "os";
3363
+ import { createInterface as createInterface4 } from "readline";
2775
3364
  function readConfig2() {
2776
3365
  try {
2777
- return existsSync6(CONFIG_FILE3) ? JSON.parse(readFileSync8(CONFIG_FILE3, "utf8")) : {};
3366
+ return existsSync6(CONFIG_FILE3) ? JSON.parse(readFileSync9(CONFIG_FILE3, "utf8")) : {};
2778
3367
  } catch {
2779
3368
  return {};
2780
3369
  }
2781
3370
  }
2782
3371
  function writeConfig2(patch) {
2783
- mkdirSync6(TH_DIR2, { recursive: true });
3372
+ mkdirSync7(TH_DIR2, { recursive: true });
2784
3373
  const merged = { ...readConfig2(), ...patch };
2785
- writeFileSync6(CONFIG_FILE3, JSON.stringify(merged, null, 2) + "\n", "utf8");
3374
+ writeFileSync7(CONFIG_FILE3, JSON.stringify(merged, null, 2) + "\n", "utf8");
2786
3375
  }
2787
3376
  function backupSettings() {
2788
3377
  if (!existsSync6(SETTINGS_PATH)) return null;
@@ -2792,7 +3381,7 @@ function backupSettings() {
2792
3381
  return backupPath;
2793
3382
  }
2794
3383
  function ask(question) {
2795
- const rl = createInterface3({ input: process.stdin, output: process.stdout });
3384
+ const rl = createInterface4({ input: process.stdin, output: process.stdout });
2796
3385
  return new Promise((res) => {
2797
3386
  rl.question(question, (answer) => {
2798
3387
  rl.close();
@@ -2802,20 +3391,20 @@ function ask(question) {
2802
3391
  }
2803
3392
  function readTopMatches() {
2804
3393
  try {
2805
- const c = JSON.parse(readFileSync8(CACHE_FILE, "utf8"));
3394
+ const c = JSON.parse(readFileSync9(CACHE_FILE, "utf8"));
2806
3395
  return Array.isArray(c.topMatches) ? c.topMatches : [];
2807
3396
  } catch {
2808
3397
  return [];
2809
3398
  }
2810
3399
  }
2811
- async function run6() {
2812
- const args2 = process.argv.slice(2).filter((a) => a !== "spinner");
2813
- const has = (f) => args2.includes(f);
3400
+ async function run7() {
3401
+ const args3 = process.argv.slice(2).filter((a) => a !== "spinner");
3402
+ const has = (f) => args3.includes(f);
2814
3403
  const val = (f) => {
2815
- const i = args2.indexOf(f);
2816
- return i >= 0 ? args2[i + 1] : void 0;
3404
+ const i = args3.indexOf(f);
3405
+ return i >= 0 ? args3[i + 1] : void 0;
2817
3406
  };
2818
- if (has("--show") || args2.length === 0) {
3407
+ if (has("--show") || args3.length === 0) {
2819
3408
  const sc = readSpinnerConfig();
2820
3409
  console.log("");
2821
3410
  console.log("terminalhire spinner \u2014 job matches in the Claude Code spinner line");
@@ -2945,25 +3534,25 @@ var init_jpi_spinner = __esm({
2945
3534
  "bin/jpi-spinner.js"() {
2946
3535
  "use strict";
2947
3536
  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");
3537
+ TH_DIR2 = process.env["TERMINALHIRE_DIR"] || join9(homedir7(), ".terminalhire");
3538
+ CONFIG_FILE3 = join9(TH_DIR2, "config.json");
3539
+ SETTINGS_PATH = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join9(homedir7(), ".claude", "settings.json");
3540
+ CACHE_FILE = join9(TH_DIR2, "index-cache.json");
2952
3541
  }
2953
3542
  });
2954
3543
 
2955
3544
  // bin/jpi-sync.js
2956
3545
  var jpi_sync_exports = {};
2957
3546
  __export(jpi_sync_exports, {
2958
- run: () => run7
3547
+ run: () => run8
2959
3548
  });
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";
3549
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8, existsSync as existsSync7, rmSync as rmSync2 } from "fs";
3550
+ import { join as join10 } from "path";
3551
+ import { homedir as homedir8, hostname as osHostname } from "os";
3552
+ import { createInterface as createInterface5 } from "readline";
2964
3553
  import { spawn } from "child_process";
2965
3554
  function ask2(question) {
2966
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
3555
+ const rl = createInterface5({ input: process.stdin, output: process.stdout });
2967
3556
  return new Promise((res) => {
2968
3557
  rl.question(question, (answer) => {
2969
3558
  rl.close();
@@ -2973,14 +3562,14 @@ function ask2(question) {
2973
3562
  }
2974
3563
  function readMarker() {
2975
3564
  try {
2976
- return existsSync7(TIER1_MARKER) ? JSON.parse(readFileSync9(TIER1_MARKER, "utf8")) : null;
3565
+ return existsSync7(TIER1_MARKER) ? JSON.parse(readFileSync10(TIER1_MARKER, "utf8")) : null;
2977
3566
  } catch {
2978
3567
  return null;
2979
3568
  }
2980
3569
  }
2981
3570
  function writeMarker(marker) {
2982
- mkdirSync7(TH_DIR3, { recursive: true });
2983
- writeFileSync7(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
3571
+ mkdirSync8(TH_DIR3, { recursive: true });
3572
+ writeFileSync8(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
2984
3573
  }
2985
3574
  function clearMarker() {
2986
3575
  try {
@@ -3032,19 +3621,19 @@ function renderPreview(fields) {
3032
3621
  }
3033
3622
  function openInBrowser(url) {
3034
3623
  let cmd;
3035
- let args2;
3624
+ let args3;
3036
3625
  if (process.platform === "darwin") {
3037
3626
  cmd = "open";
3038
- args2 = [url];
3627
+ args3 = [url];
3039
3628
  } else if (process.platform === "win32") {
3040
3629
  cmd = "cmd";
3041
- args2 = ["/c", "start", "", url];
3630
+ args3 = ["/c", "start", "", url];
3042
3631
  } else {
3043
3632
  cmd = "xdg-open";
3044
- args2 = [url];
3633
+ args3 = [url];
3045
3634
  }
3046
3635
  try {
3047
- const child = spawn(cmd, args2, { stdio: "ignore", detached: true });
3636
+ const child = spawn(cmd, args3, { stdio: "ignore", detached: true });
3048
3637
  child.on("error", () => {
3049
3638
  });
3050
3639
  child.unref();
@@ -3067,7 +3656,7 @@ async function runPush() {
3067
3656
  const fields = buildConsentFields(profile);
3068
3657
  renderPreview(fields);
3069
3658
  await new Promise((resolve2) => {
3070
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
3659
+ const rl = createInterface5({ input: process.stdin, output: process.stdout });
3071
3660
  rl.question(
3072
3661
  " Press Enter to open your browser to authorize + consent (or Ctrl-C to cancel)... ",
3073
3662
  () => {
@@ -3265,7 +3854,7 @@ async function runDelete() {
3265
3854
  console.log("\n Requesting deletion...");
3266
3855
  let res;
3267
3856
  try {
3268
- res = await fetch(`${API_URL2}/api/profile-sync`, {
3857
+ res = await fetch(`${API_URL3}/api/profile-sync`, {
3269
3858
  method: "DELETE",
3270
3859
  headers: { "Content-Type": "application/json" },
3271
3860
  body: JSON.stringify({ consentToken, login, deleteToken }),
@@ -3289,9 +3878,9 @@ async function runDelete() {
3289
3878
  clearMarker();
3290
3879
  console.log("\n Synced profile deleted and local marker cleared.\n");
3291
3880
  }
3292
- async function run7() {
3293
- const args2 = process.argv.slice(2).filter((a) => a !== "sync");
3294
- const has = (f) => args2.includes(f);
3881
+ async function run8() {
3882
+ const args3 = process.argv.slice(2).filter((a) => a !== "sync");
3883
+ const has = (f) => args3.includes(f);
3295
3884
  if (has("--push") || has("--enable")) {
3296
3885
  await runPush();
3297
3886
  return;
@@ -3315,13 +3904,13 @@ async function run7() {
3315
3904
  console.log(" This is NOT required to use terminalhire.");
3316
3905
  console.log("");
3317
3906
  }
3318
- var TH_DIR3, TIER1_MARKER, API_URL2, SYNC_BASE, POLL_INTERVAL_MS, POLL_TIMEOUT_MS, CONSENT_VERSION;
3907
+ var TH_DIR3, TIER1_MARKER, API_URL3, SYNC_BASE, POLL_INTERVAL_MS, POLL_TIMEOUT_MS, CONSENT_VERSION;
3319
3908
  var init_jpi_sync = __esm({
3320
3909
  "bin/jpi-sync.js"() {
3321
3910
  "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";
3911
+ TH_DIR3 = process.env["TERMINALHIRE_DIR"] || join10(homedir8(), ".terminalhire");
3912
+ TIER1_MARKER = join10(TH_DIR3, "tier1.json");
3913
+ API_URL3 = process.env["TERMINALHIRE_API_URL"] || process.env["JPI_API_URL"] || "https://terminalhire.com";
3325
3914
  SYNC_BASE = "https://www.terminalhire.com";
3326
3915
  POLL_INTERVAL_MS = 2e3;
3327
3916
  POLL_TIMEOUT_MS = 10 * 60 * 1e3;
@@ -3332,16 +3921,16 @@ var init_jpi_sync = __esm({
3332
3921
  // bin/jpi-init.js
3333
3922
  var jpi_init_exports = {};
3334
3923
  __export(jpi_init_exports, {
3335
- run: () => run8
3924
+ run: () => run9
3336
3925
  });
3337
3926
  import { existsSync as existsSync8 } from "fs";
3338
- import { join as join10, resolve } from "path";
3927
+ import { join as join11, resolve } from "path";
3339
3928
  import { fileURLToPath as fileURLToPath3 } from "url";
3340
- import { createInterface as createInterface5 } from "readline";
3929
+ import { createInterface as createInterface6 } from "readline";
3341
3930
  import { spawnSync, spawn as spawn2 } from "child_process";
3342
- import { homedir as homedir8 } from "os";
3931
+ import { homedir as homedir9 } from "os";
3343
3932
  function ask3(question) {
3344
- const rl = createInterface5({ input: process.stdin, output: process.stdout });
3933
+ const rl = createInterface6({ input: process.stdin, output: process.stdout });
3345
3934
  return new Promise((resolve2) => {
3346
3935
  rl.question(question, (answer) => {
3347
3936
  rl.close();
@@ -3350,18 +3939,18 @@ function ask3(question) {
3350
3939
  });
3351
3940
  }
3352
3941
  function resolveScript(name) {
3353
- const distPath = resolve(join10(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
3354
- const legacyPath = resolve(join10(__dirname2, `${name}.js`));
3942
+ const distPath = resolve(join11(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
3943
+ const legacyPath = resolve(join11(__dirname2, `${name}.js`));
3355
3944
  return existsSync8(distPath) ? distPath : legacyPath;
3356
3945
  }
3357
3946
  function resolveInstallJs() {
3358
- const fromDist = resolve(join10(__dirname2, "..", "..", "install.js"));
3359
- const fromBin = resolve(join10(__dirname2, "..", "install.js"));
3947
+ const fromDist = resolve(join11(__dirname2, "..", "..", "install.js"));
3948
+ const fromBin = resolve(join11(__dirname2, "..", "install.js"));
3360
3949
  if (existsSync8(fromDist)) return fromDist;
3361
3950
  if (existsSync8(fromBin)) return fromBin;
3362
3951
  return fromBin;
3363
3952
  }
3364
- async function run8() {
3953
+ async function run9() {
3365
3954
  console.log("");
3366
3955
  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
3956
  console.log("\u2502 terminalhire init \u2014 one-command onboarding \u2502");
@@ -3464,17 +4053,17 @@ var init_jpi_init = __esm({
3464
4053
  // bin/jpi-refresh.js
3465
4054
  var jpi_refresh_exports = {};
3466
4055
  __export(jpi_refresh_exports, {
3467
- run: () => run9
4056
+ run: () => run10
3468
4057
  });
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";
4058
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync9 } from "fs";
4059
+ import { join as join12 } from "path";
4060
+ import { homedir as homedir10 } from "os";
3472
4061
  import { fileURLToPath as fileURLToPath4 } from "url";
3473
- async function run9() {
4062
+ async function run10() {
3474
4063
  try {
3475
4064
  let index;
3476
4065
  try {
3477
- const res = await fetch(`${API_URL3}/api/index`, {
4066
+ const res = await fetch(`${API_URL4}/api/index`, {
3478
4067
  signal: AbortSignal.timeout(15e3),
3479
4068
  headers: { "Accept": "application/json" }
3480
4069
  });
@@ -3512,14 +4101,14 @@ async function run9() {
3512
4101
  }
3513
4102
  } catch {
3514
4103
  }
3515
- mkdirSync8(TERMINALHIRE_DIR5, { recursive: true });
4104
+ mkdirSync9(TERMINALHIRE_DIR6, { recursive: true });
3516
4105
  const cacheEntry = {
3517
4106
  ts: Date.now(),
3518
4107
  index,
3519
4108
  matchCount,
3520
4109
  topMatches
3521
4110
  };
3522
- writeFileSync8(INDEX_CACHE_FILE2, JSON.stringify(cacheEntry), "utf8");
4111
+ writeFileSync9(INDEX_CACHE_FILE3, JSON.stringify(cacheEntry), "utf8");
3523
4112
  try {
3524
4113
  const {
3525
4114
  readSpinnerConfig: readSpinnerConfig2,
@@ -3546,7 +4135,7 @@ async function run9() {
3546
4135
  const verbs = buildSpinnerPool2(ranked, sc.max, { sessionTags, frequency: sc.frequency });
3547
4136
  if (verbs.length > 0) applySpinnerVerbs2(verbs, sc.mode);
3548
4137
  else clearSpinnerVerbs2();
3549
- const tips = buildTips2(ranked, API_URL3, 8);
4138
+ const tips = buildTips2(ranked, API_URL4, 8);
3550
4139
  if (tips.length > 0) applySpinnerTips2(tips);
3551
4140
  else clearSpinnerTips2();
3552
4141
  } else {
@@ -3563,30 +4152,30 @@ async function run9() {
3563
4152
  process.exit(1);
3564
4153
  }
3565
4154
  }
3566
- var __dirname3, TERMINALHIRE_DIR5, INDEX_CACHE_FILE2, API_URL3;
4155
+ var __dirname3, TERMINALHIRE_DIR6, INDEX_CACHE_FILE3, API_URL4;
3567
4156
  var init_jpi_refresh = __esm({
3568
4157
  "bin/jpi-refresh.js"() {
3569
4158
  "use strict";
3570
4159
  __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";
4160
+ TERMINALHIRE_DIR6 = join12(homedir10(), ".terminalhire");
4161
+ INDEX_CACHE_FILE3 = join12(TERMINALHIRE_DIR6, "index-cache.json");
4162
+ API_URL4 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
3574
4163
  }
3575
4164
  });
3576
4165
 
3577
4166
  // bin/jpi-save.js
3578
4167
  var jpi_save_exports = {};
3579
4168
  __export(jpi_save_exports, {
3580
- run: () => run10
4169
+ run: () => run11
3581
4170
  });
3582
- import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
3583
- import { join as join12 } from "path";
3584
- import { homedir as homedir10 } from "os";
4171
+ import { readFileSync as readFileSync12, existsSync as existsSync10 } from "fs";
4172
+ import { join as join13 } from "path";
4173
+ import { homedir as homedir11 } from "os";
3585
4174
  import { fileURLToPath as fileURLToPath5 } from "url";
3586
4175
  function findJobInCache(jobId) {
3587
4176
  try {
3588
- if (!existsSync10(INDEX_CACHE_FILE3)) return null;
3589
- const raw = readFileSync11(INDEX_CACHE_FILE3, "utf8");
4177
+ if (!existsSync10(INDEX_CACHE_FILE4)) return null;
4178
+ const raw = readFileSync12(INDEX_CACHE_FILE4, "utf8");
3590
4179
  const entry = JSON.parse(raw);
3591
4180
  const jobs = entry?.index?.jobs ?? [];
3592
4181
  return jobs.find((j) => j.id === jobId) ?? null;
@@ -3655,7 +4244,7 @@ async function cmdUnsave(jobId) {
3655
4244
  process.exit(1);
3656
4245
  }
3657
4246
  }
3658
- async function run10() {
4247
+ async function run11() {
3659
4248
  const verb = process.argv[2];
3660
4249
  const jobId = process.argv[3];
3661
4250
  try {
@@ -3674,31 +4263,31 @@ async function run10() {
3674
4263
  process.exit(1);
3675
4264
  }
3676
4265
  }
3677
- var __dirname4, TERMINALHIRE_DIR6, INDEX_CACHE_FILE3;
4266
+ var __dirname4, TERMINALHIRE_DIR7, INDEX_CACHE_FILE4;
3678
4267
  var init_jpi_save = __esm({
3679
4268
  "bin/jpi-save.js"() {
3680
4269
  "use strict";
3681
4270
  __dirname4 = fileURLToPath5(new URL(".", import.meta.url));
3682
- TERMINALHIRE_DIR6 = join12(homedir10(), ".terminalhire");
3683
- INDEX_CACHE_FILE3 = join12(TERMINALHIRE_DIR6, "index-cache.json");
4271
+ TERMINALHIRE_DIR7 = join13(homedir11(), ".terminalhire");
4272
+ INDEX_CACHE_FILE4 = join13(TERMINALHIRE_DIR7, "index-cache.json");
3684
4273
  }
3685
4274
  });
3686
4275
 
3687
4276
  // bin/jpi-dispatch.js
3688
4277
  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";
4278
+ import { join as join14, dirname as dirname2 } from "path";
4279
+ import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
3691
4280
  import { createRequire } from "module";
3692
4281
  var __dirname5 = fileURLToPath6(new URL(".", import.meta.url));
3693
4282
  function readPackageVersion() {
3694
4283
  try {
3695
4284
  const candidates = [
3696
- join13(__dirname5, "..", "..", "package.json"),
3697
- join13(__dirname5, "..", "package.json")
4285
+ join14(__dirname5, "..", "..", "package.json"),
4286
+ join14(__dirname5, "..", "package.json")
3698
4287
  ];
3699
4288
  for (const p of candidates) {
3700
4289
  if (existsSync11(p)) {
3701
- const pkg = JSON.parse(readFileSync12(p, "utf8"));
4290
+ const pkg = JSON.parse(readFileSync13(p, "utf8"));
3702
4291
  if (pkg.version) return pkg.version;
3703
4292
  }
3704
4293
  }
@@ -3709,7 +4298,7 @@ function readPackageVersion() {
3709
4298
  var firstArg = process.argv[2];
3710
4299
  if (!firstArg && !process.stdin.isTTY) {
3711
4300
  const { default: childProcess } = await import("child_process");
3712
- const nudgeScript = join13(__dirname5, "jpi.js");
4301
+ const nudgeScript = join14(__dirname5, "jpi.js");
3713
4302
  const child = childProcess.spawnSync(process.execPath, [nudgeScript], {
3714
4303
  stdio: ["inherit", "inherit", "inherit"]
3715
4304
  });
@@ -3726,6 +4315,8 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
3726
4315
  console.log(" terminalhire jobs Fetch job index, match locally, browse roles");
3727
4316
  console.log(" terminalhire jobs --limit N Show top N results (default: 10)");
3728
4317
  console.log(" terminalhire jobs --remote-only Filter to remote roles only");
4318
+ console.log(" terminalhire bounties Day-sized paid tasks you can knock out today");
4319
+ console.log(" terminalhire bounties --priced Only bounties with a known $ amount");
3729
4320
  console.log(" terminalhire profile --show Display your encrypted local profile");
3730
4321
  console.log(" terminalhire profile --edit Set displayName, contactEmail, prefs");
3731
4322
  console.log(" terminalhire profile --delete Wipe profile and encryption key from disk");
@@ -3772,6 +4363,12 @@ if (firstArg === "jobs") {
3772
4363
  await mod.run();
3773
4364
  process.exit(0);
3774
4365
  }
4366
+ if (firstArg === "bounties") {
4367
+ process.argv.splice(2, 1);
4368
+ const mod = await Promise.resolve().then(() => (init_jpi_bounties(), jpi_bounties_exports));
4369
+ await mod.run();
4370
+ process.exit(0);
4371
+ }
3775
4372
  if (firstArg === "profile") {
3776
4373
  const mod = await Promise.resolve().then(() => (init_jpi_profile(), jpi_profile_exports));
3777
4374
  await mod.run();