terminalhire 0.2.5 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/jpi-bounties.js +2031 -0
- package/dist/bin/jpi-dispatch.js +1087 -469
- package/dist/bin/jpi-jobs.js +705 -276
- package/dist/bin/jpi-learn.js +442 -249
- package/dist/bin/jpi-login.js +700 -282
- package/dist/bin/jpi-profile.js +381 -213
- package/dist/bin/jpi-refresh.js +785 -318
- package/dist/bin/jpi-save.js +381 -213
- package/dist/bin/jpi-spinner.js +15 -5
- package/dist/bin/jpi-sync.js +381 -213
- package/dist/bin/spinner.js +22 -6
- package/dist/src/github-auth.js +3 -3
- package/dist/src/profile.js +313 -207
- package/dist/src/signal.js +364 -237
- package/package.json +3 -3
package/dist/bin/jpi-dispatch.js
CHANGED
|
@@ -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"] ??
|
|
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 "
|
|
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,
|
|
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
|
-
|
|
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/
|
|
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 (
|
|
548
|
+
if (GRAPH.ids.has(lower)) {
|
|
239
549
|
result.add(lower);
|
|
240
550
|
continue;
|
|
241
551
|
}
|
|
242
|
-
const mapped =
|
|
243
|
-
if (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
|
-
|
|
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
|
-
|
|
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.
|
|
482
|
-
return 0.
|
|
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
|
|
487
|
-
if (
|
|
488
|
-
if (
|
|
489
|
-
if (
|
|
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(
|
|
505
|
-
if (
|
|
506
|
-
const
|
|
507
|
-
const
|
|
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
|
|
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
|
|
515
|
-
const
|
|
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
|
|
519
|
-
|
|
520
|
-
let
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
529
|
-
|
|
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 =
|
|
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
|
|
537
|
-
|
|
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(/&/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, "")
|
|
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/
|
|
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, "
|
|
1643
|
+
return join2(dir, "partner-roles.json");
|
|
1244
1644
|
} catch {
|
|
1245
|
-
return join2(process.cwd(), "data", "
|
|
1645
|
+
return join2(process.cwd(), "data", "partner-roles.json");
|
|
1246
1646
|
}
|
|
1247
1647
|
}
|
|
1248
|
-
function
|
|
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("[
|
|
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
|
-
|
|
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("[
|
|
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(`[
|
|
1669
|
+
console.warn(`[partners] data/partner-roles.json not found at ${filePath} \u2014 no partner roles loaded`);
|
|
1269
1670
|
} else {
|
|
1270
|
-
console.warn("[
|
|
1671
|
+
console.warn("[partners] Failed to load partner-roles.json:", err);
|
|
1271
1672
|
}
|
|
1272
1673
|
return [];
|
|
1273
1674
|
}
|
|
1274
1675
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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
|
-
|
|
1280
|
-
id: "
|
|
1281
|
-
legalName: "
|
|
1282
|
-
matchCriteria: {
|
|
1283
|
-
|
|
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
|
|
1696
|
+
const includePartners = opts?.includePartners ?? true;
|
|
1292
1697
|
const publicJobs = await aggregate(opts);
|
|
1293
1698
|
const allJobs = [...publicJobs];
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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.
|
|
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:
|
|
2150
|
+
const { join: join15, dirname: dirname3 } = await import("path");
|
|
1730
2151
|
const __dirname6 = fileURLToPath7(new URL(".", import.meta.url));
|
|
1731
|
-
const fixturePath =
|
|
1732
|
-
const { readFileSync:
|
|
1733
|
-
ghProfile = JSON.parse(
|
|
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
|
|
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["
|
|
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-
|
|
2048
|
-
var
|
|
2049
|
-
__export(
|
|
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
|
|
2065
|
-
if (
|
|
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 (
|
|
2659
|
+
if (args3.includes("--delete")) {
|
|
2096
2660
|
console.log("\nThis will permanently delete your local terminalhire profile and encryption key.");
|
|
2097
|
-
const answer = await
|
|
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 (
|
|
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
|
|
2673
|
+
const name = await prompt3(`Display name [${profile.displayName ?? "not set"}]: `);
|
|
2110
2674
|
if (name) profile.displayName = name;
|
|
2111
|
-
const email = await
|
|
2675
|
+
const email = await prompt3(`Contact email [${profile.contactEmail ?? "not set"}]: `);
|
|
2112
2676
|
if (email) profile.contactEmail = email;
|
|
2113
|
-
const remote = await
|
|
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
|
|
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
|
|
2136
|
-
import {
|
|
2137
|
-
import { join as
|
|
2138
|
-
function
|
|
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
|
|
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
|
|
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
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
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(
|
|
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
|
|
2736
|
+
return readFileSync6(path, "utf8");
|
|
2177
2737
|
} catch {
|
|
2178
2738
|
return "";
|
|
2179
2739
|
}
|
|
2180
2740
|
}
|
|
2181
2741
|
function tokensFromPackageJson(cwd) {
|
|
2182
|
-
const pkg = readJsonSafe(
|
|
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(
|
|
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(
|
|
2198
|
-
if (!content) return [
|
|
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(
|
|
2777
|
+
const content = readFileSafe(join6(cwd, "Cargo.toml"));
|
|
2204
2778
|
if (!content) return [];
|
|
2205
|
-
const deps =
|
|
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 =
|
|
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
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
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: () =>
|
|
2959
|
+
run: () => run5
|
|
2371
2960
|
});
|
|
2372
|
-
async function
|
|
2961
|
+
async function run5() {
|
|
2373
2962
|
try {
|
|
2374
|
-
const
|
|
2375
|
-
const cwdIdx =
|
|
2376
|
-
const cwd = cwdIdx !== -1 &&
|
|
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
|
-
|
|
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: () =>
|
|
2996
|
+
run: () => run6
|
|
2408
2997
|
});
|
|
2409
|
-
import { readFileSync as
|
|
2410
|
-
import { join as
|
|
2411
|
-
import { homedir as
|
|
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(
|
|
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
|
-
|
|
3010
|
+
mkdirSync5(TERMINALHIRE_DIR5, { recursive: true });
|
|
2422
3011
|
const merged = { ...readConfig(), ...patch };
|
|
2423
|
-
|
|
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
|
|
2432
|
-
const
|
|
2433
|
-
const filtered =
|
|
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
|
|
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
|
-
|
|
2479
|
-
CONFIG_FILE =
|
|
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
|
|
2503
|
-
writeFileSync as
|
|
3091
|
+
readFileSync as readFileSync8,
|
|
3092
|
+
writeFileSync as writeFileSync6,
|
|
2504
3093
|
existsSync as existsSync5,
|
|
2505
|
-
mkdirSync as
|
|
3094
|
+
mkdirSync as mkdirSync6,
|
|
2506
3095
|
renameSync
|
|
2507
3096
|
} from "fs";
|
|
2508
|
-
import { join as
|
|
2509
|
-
import { homedir as
|
|
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(
|
|
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
|
-
|
|
3107
|
+
mkdirSync6(dirname(path), { recursive: true });
|
|
2519
3108
|
const tmp = `${path}.tmp-${process.pid}`;
|
|
2520
|
-
|
|
3109
|
+
writeFileSync6(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
|
|
2521
3110
|
renameSync(tmp, path);
|
|
2522
3111
|
}
|
|
2523
3112
|
function titleCase(s) {
|
|
@@ -2588,16 +3177,26 @@ function buildContextVerbs(topMatches, sessionTags) {
|
|
|
2588
3177
|
for (const t of sess) {
|
|
2589
3178
|
if (roleTags.has(t) && !overlap.includes(t)) overlap.push(t);
|
|
2590
3179
|
}
|
|
3180
|
+
let headers;
|
|
2591
3181
|
if (overlap.length >= 2) {
|
|
2592
3182
|
const a = titleCase(overlap[0]);
|
|
2593
3183
|
const b = titleCase(overlap[1]);
|
|
2594
|
-
|
|
2595
|
-
}
|
|
2596
|
-
if (overlap.length === 1) {
|
|
3184
|
+
headers = [`\u2726 Fits your ${a} + ${b} work`, `\u2726 A role matching what you're building`];
|
|
3185
|
+
} else if (overlap.length === 1) {
|
|
2597
3186
|
const a = titleCase(overlap[0]);
|
|
2598
|
-
|
|
3187
|
+
headers = [`\u2726 A role matching your ${a} work`, `\u2726 Your ${a} work \u2014 link in the tip below`];
|
|
3188
|
+
} else {
|
|
3189
|
+
headers = [`\u2726 A role that fits your work`, `\u2726 Job match for you \u2014 link in the tip below`];
|
|
3190
|
+
}
|
|
3191
|
+
const list = Array.isArray(topMatches) ? topMatches : [];
|
|
3192
|
+
const bounty = list.find((m) => m && m.source === "bounty" && m.amountUSD != null) || list.find((m) => m && m.source === "bounty");
|
|
3193
|
+
if (bounty) {
|
|
3194
|
+
const money = bounty.amountUSD != null ? `$${bounty.amountUSD.toLocaleString()} ` : "";
|
|
3195
|
+
const bountyHeader = `\u2726 \u{1F48E} A ${money}bounty in your stack \u2014 link below`;
|
|
3196
|
+
if (list[0] && list[0].source === "bounty") headers.unshift(bountyHeader);
|
|
3197
|
+
else headers.push(bountyHeader);
|
|
2599
3198
|
}
|
|
2600
|
-
return
|
|
3199
|
+
return headers;
|
|
2601
3200
|
}
|
|
2602
3201
|
function buildSpinnerPool(topMatches, max = 6, opts = {}) {
|
|
2603
3202
|
const { sessionTags, frequency = "always" } = opts;
|
|
@@ -2702,7 +3301,13 @@ function buildTips(topMatches, baseUrl, max = 8) {
|
|
|
2702
3301
|
const pct = Math.max(1, Math.min(99, Math.round((Number(m.score) || 0) * 100)));
|
|
2703
3302
|
const token = Buffer.from(String(m.id)).toString("base64url");
|
|
2704
3303
|
const url = `${base}/j/${token}`;
|
|
2705
|
-
|
|
3304
|
+
if (source === "bounty") {
|
|
3305
|
+
const money = m.amountUSD != null ? `$${Number(m.amountUSD).toLocaleString()}` : "$\u2014";
|
|
3306
|
+
const repo = m.repo || companyRaw;
|
|
3307
|
+
out.push(`\u{1F48E} ${money} \xB7 ${title} \xB7 ${repo} \xB7 ${pct}% \u2014 ${url}`);
|
|
3308
|
+
} else {
|
|
3309
|
+
out.push(`\u2197 ${title} @ ${company} \xB7 ${pct}% \u2014 ${url}`);
|
|
3310
|
+
}
|
|
2706
3311
|
if (out.length >= max) break;
|
|
2707
3312
|
}
|
|
2708
3313
|
return out;
|
|
@@ -2748,10 +3353,10 @@ var TH_DIR, CLAUDE_SETTINGS, CONFIG_FILE2, SPINNER_STATE_FILE, SPINNER_DEFAULTS,
|
|
|
2748
3353
|
var init_spinner = __esm({
|
|
2749
3354
|
"bin/spinner.js"() {
|
|
2750
3355
|
"use strict";
|
|
2751
|
-
TH_DIR = process.env["TERMINALHIRE_DIR"] ||
|
|
2752
|
-
CLAUDE_SETTINGS = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] ||
|
|
2753
|
-
CONFIG_FILE2 =
|
|
2754
|
-
SPINNER_STATE_FILE =
|
|
3356
|
+
TH_DIR = process.env["TERMINALHIRE_DIR"] || join8(homedir6(), ".terminalhire");
|
|
3357
|
+
CLAUDE_SETTINGS = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join8(homedir6(), ".claude", "settings.json");
|
|
3358
|
+
CONFIG_FILE2 = join8(TH_DIR, "config.json");
|
|
3359
|
+
SPINNER_STATE_FILE = join8(TH_DIR, "spinner-state.json");
|
|
2755
3360
|
SPINNER_DEFAULTS = { enabled: false, mode: "append", max: 6, frequency: "sometimes" };
|
|
2756
3361
|
VERB_INTROS = ["Matched:", "You\u2019d fit:", "Worth a look:", "On your radar:", "Fits your stack:"];
|
|
2757
3362
|
}
|
|
@@ -2760,29 +3365,29 @@ var init_spinner = __esm({
|
|
|
2760
3365
|
// bin/jpi-spinner.js
|
|
2761
3366
|
var jpi_spinner_exports = {};
|
|
2762
3367
|
__export(jpi_spinner_exports, {
|
|
2763
|
-
run: () =>
|
|
3368
|
+
run: () => run7
|
|
2764
3369
|
});
|
|
2765
3370
|
import {
|
|
2766
|
-
readFileSync as
|
|
2767
|
-
writeFileSync as
|
|
3371
|
+
readFileSync as readFileSync9,
|
|
3372
|
+
writeFileSync as writeFileSync7,
|
|
2768
3373
|
copyFileSync,
|
|
2769
3374
|
existsSync as existsSync6,
|
|
2770
|
-
mkdirSync as
|
|
3375
|
+
mkdirSync as mkdirSync7
|
|
2771
3376
|
} from "fs";
|
|
2772
|
-
import { join as
|
|
2773
|
-
import { homedir as
|
|
2774
|
-
import { createInterface as
|
|
3377
|
+
import { join as join9 } from "path";
|
|
3378
|
+
import { homedir as homedir7 } from "os";
|
|
3379
|
+
import { createInterface as createInterface4 } from "readline";
|
|
2775
3380
|
function readConfig2() {
|
|
2776
3381
|
try {
|
|
2777
|
-
return existsSync6(CONFIG_FILE3) ? JSON.parse(
|
|
3382
|
+
return existsSync6(CONFIG_FILE3) ? JSON.parse(readFileSync9(CONFIG_FILE3, "utf8")) : {};
|
|
2778
3383
|
} catch {
|
|
2779
3384
|
return {};
|
|
2780
3385
|
}
|
|
2781
3386
|
}
|
|
2782
3387
|
function writeConfig2(patch) {
|
|
2783
|
-
|
|
3388
|
+
mkdirSync7(TH_DIR2, { recursive: true });
|
|
2784
3389
|
const merged = { ...readConfig2(), ...patch };
|
|
2785
|
-
|
|
3390
|
+
writeFileSync7(CONFIG_FILE3, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
2786
3391
|
}
|
|
2787
3392
|
function backupSettings() {
|
|
2788
3393
|
if (!existsSync6(SETTINGS_PATH)) return null;
|
|
@@ -2792,7 +3397,7 @@ function backupSettings() {
|
|
|
2792
3397
|
return backupPath;
|
|
2793
3398
|
}
|
|
2794
3399
|
function ask(question) {
|
|
2795
|
-
const rl =
|
|
3400
|
+
const rl = createInterface4({ input: process.stdin, output: process.stdout });
|
|
2796
3401
|
return new Promise((res) => {
|
|
2797
3402
|
rl.question(question, (answer) => {
|
|
2798
3403
|
rl.close();
|
|
@@ -2802,20 +3407,20 @@ function ask(question) {
|
|
|
2802
3407
|
}
|
|
2803
3408
|
function readTopMatches() {
|
|
2804
3409
|
try {
|
|
2805
|
-
const c = JSON.parse(
|
|
3410
|
+
const c = JSON.parse(readFileSync9(CACHE_FILE, "utf8"));
|
|
2806
3411
|
return Array.isArray(c.topMatches) ? c.topMatches : [];
|
|
2807
3412
|
} catch {
|
|
2808
3413
|
return [];
|
|
2809
3414
|
}
|
|
2810
3415
|
}
|
|
2811
|
-
async function
|
|
2812
|
-
const
|
|
2813
|
-
const has = (f) =>
|
|
3416
|
+
async function run7() {
|
|
3417
|
+
const args3 = process.argv.slice(2).filter((a) => a !== "spinner");
|
|
3418
|
+
const has = (f) => args3.includes(f);
|
|
2814
3419
|
const val = (f) => {
|
|
2815
|
-
const i =
|
|
2816
|
-
return i >= 0 ?
|
|
3420
|
+
const i = args3.indexOf(f);
|
|
3421
|
+
return i >= 0 ? args3[i + 1] : void 0;
|
|
2817
3422
|
};
|
|
2818
|
-
if (has("--show") ||
|
|
3423
|
+
if (has("--show") || args3.length === 0) {
|
|
2819
3424
|
const sc = readSpinnerConfig();
|
|
2820
3425
|
console.log("");
|
|
2821
3426
|
console.log("terminalhire spinner \u2014 job matches in the Claude Code spinner line");
|
|
@@ -2945,25 +3550,25 @@ var init_jpi_spinner = __esm({
|
|
|
2945
3550
|
"bin/jpi-spinner.js"() {
|
|
2946
3551
|
"use strict";
|
|
2947
3552
|
init_spinner();
|
|
2948
|
-
TH_DIR2 = process.env["TERMINALHIRE_DIR"] ||
|
|
2949
|
-
CONFIG_FILE3 =
|
|
2950
|
-
SETTINGS_PATH = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] ||
|
|
2951
|
-
CACHE_FILE =
|
|
3553
|
+
TH_DIR2 = process.env["TERMINALHIRE_DIR"] || join9(homedir7(), ".terminalhire");
|
|
3554
|
+
CONFIG_FILE3 = join9(TH_DIR2, "config.json");
|
|
3555
|
+
SETTINGS_PATH = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join9(homedir7(), ".claude", "settings.json");
|
|
3556
|
+
CACHE_FILE = join9(TH_DIR2, "index-cache.json");
|
|
2952
3557
|
}
|
|
2953
3558
|
});
|
|
2954
3559
|
|
|
2955
3560
|
// bin/jpi-sync.js
|
|
2956
3561
|
var jpi_sync_exports = {};
|
|
2957
3562
|
__export(jpi_sync_exports, {
|
|
2958
|
-
run: () =>
|
|
3563
|
+
run: () => run8
|
|
2959
3564
|
});
|
|
2960
|
-
import { readFileSync as
|
|
2961
|
-
import { join as
|
|
2962
|
-
import { homedir as
|
|
2963
|
-
import { createInterface as
|
|
3565
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8, existsSync as existsSync7, rmSync as rmSync2 } from "fs";
|
|
3566
|
+
import { join as join10 } from "path";
|
|
3567
|
+
import { homedir as homedir8, hostname as osHostname } from "os";
|
|
3568
|
+
import { createInterface as createInterface5 } from "readline";
|
|
2964
3569
|
import { spawn } from "child_process";
|
|
2965
3570
|
function ask2(question) {
|
|
2966
|
-
const rl =
|
|
3571
|
+
const rl = createInterface5({ input: process.stdin, output: process.stdout });
|
|
2967
3572
|
return new Promise((res) => {
|
|
2968
3573
|
rl.question(question, (answer) => {
|
|
2969
3574
|
rl.close();
|
|
@@ -2973,14 +3578,14 @@ function ask2(question) {
|
|
|
2973
3578
|
}
|
|
2974
3579
|
function readMarker() {
|
|
2975
3580
|
try {
|
|
2976
|
-
return existsSync7(TIER1_MARKER) ? JSON.parse(
|
|
3581
|
+
return existsSync7(TIER1_MARKER) ? JSON.parse(readFileSync10(TIER1_MARKER, "utf8")) : null;
|
|
2977
3582
|
} catch {
|
|
2978
3583
|
return null;
|
|
2979
3584
|
}
|
|
2980
3585
|
}
|
|
2981
3586
|
function writeMarker(marker) {
|
|
2982
|
-
|
|
2983
|
-
|
|
3587
|
+
mkdirSync8(TH_DIR3, { recursive: true });
|
|
3588
|
+
writeFileSync8(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
2984
3589
|
}
|
|
2985
3590
|
function clearMarker() {
|
|
2986
3591
|
try {
|
|
@@ -3032,19 +3637,19 @@ function renderPreview(fields) {
|
|
|
3032
3637
|
}
|
|
3033
3638
|
function openInBrowser(url) {
|
|
3034
3639
|
let cmd;
|
|
3035
|
-
let
|
|
3640
|
+
let args3;
|
|
3036
3641
|
if (process.platform === "darwin") {
|
|
3037
3642
|
cmd = "open";
|
|
3038
|
-
|
|
3643
|
+
args3 = [url];
|
|
3039
3644
|
} else if (process.platform === "win32") {
|
|
3040
3645
|
cmd = "cmd";
|
|
3041
|
-
|
|
3646
|
+
args3 = ["/c", "start", "", url];
|
|
3042
3647
|
} else {
|
|
3043
3648
|
cmd = "xdg-open";
|
|
3044
|
-
|
|
3649
|
+
args3 = [url];
|
|
3045
3650
|
}
|
|
3046
3651
|
try {
|
|
3047
|
-
const child = spawn(cmd,
|
|
3652
|
+
const child = spawn(cmd, args3, { stdio: "ignore", detached: true });
|
|
3048
3653
|
child.on("error", () => {
|
|
3049
3654
|
});
|
|
3050
3655
|
child.unref();
|
|
@@ -3067,7 +3672,7 @@ async function runPush() {
|
|
|
3067
3672
|
const fields = buildConsentFields(profile);
|
|
3068
3673
|
renderPreview(fields);
|
|
3069
3674
|
await new Promise((resolve2) => {
|
|
3070
|
-
const rl =
|
|
3675
|
+
const rl = createInterface5({ input: process.stdin, output: process.stdout });
|
|
3071
3676
|
rl.question(
|
|
3072
3677
|
" Press Enter to open your browser to authorize + consent (or Ctrl-C to cancel)... ",
|
|
3073
3678
|
() => {
|
|
@@ -3265,7 +3870,7 @@ async function runDelete() {
|
|
|
3265
3870
|
console.log("\n Requesting deletion...");
|
|
3266
3871
|
let res;
|
|
3267
3872
|
try {
|
|
3268
|
-
res = await fetch(`${
|
|
3873
|
+
res = await fetch(`${API_URL3}/api/profile-sync`, {
|
|
3269
3874
|
method: "DELETE",
|
|
3270
3875
|
headers: { "Content-Type": "application/json" },
|
|
3271
3876
|
body: JSON.stringify({ consentToken, login, deleteToken }),
|
|
@@ -3289,9 +3894,9 @@ async function runDelete() {
|
|
|
3289
3894
|
clearMarker();
|
|
3290
3895
|
console.log("\n Synced profile deleted and local marker cleared.\n");
|
|
3291
3896
|
}
|
|
3292
|
-
async function
|
|
3293
|
-
const
|
|
3294
|
-
const has = (f) =>
|
|
3897
|
+
async function run8() {
|
|
3898
|
+
const args3 = process.argv.slice(2).filter((a) => a !== "sync");
|
|
3899
|
+
const has = (f) => args3.includes(f);
|
|
3295
3900
|
if (has("--push") || has("--enable")) {
|
|
3296
3901
|
await runPush();
|
|
3297
3902
|
return;
|
|
@@ -3315,13 +3920,13 @@ async function run7() {
|
|
|
3315
3920
|
console.log(" This is NOT required to use terminalhire.");
|
|
3316
3921
|
console.log("");
|
|
3317
3922
|
}
|
|
3318
|
-
var TH_DIR3, TIER1_MARKER,
|
|
3923
|
+
var TH_DIR3, TIER1_MARKER, API_URL3, SYNC_BASE, POLL_INTERVAL_MS, POLL_TIMEOUT_MS, CONSENT_VERSION;
|
|
3319
3924
|
var init_jpi_sync = __esm({
|
|
3320
3925
|
"bin/jpi-sync.js"() {
|
|
3321
3926
|
"use strict";
|
|
3322
|
-
TH_DIR3 = process.env["TERMINALHIRE_DIR"] ||
|
|
3323
|
-
TIER1_MARKER =
|
|
3324
|
-
|
|
3927
|
+
TH_DIR3 = process.env["TERMINALHIRE_DIR"] || join10(homedir8(), ".terminalhire");
|
|
3928
|
+
TIER1_MARKER = join10(TH_DIR3, "tier1.json");
|
|
3929
|
+
API_URL3 = process.env["TERMINALHIRE_API_URL"] || process.env["JPI_API_URL"] || "https://terminalhire.com";
|
|
3325
3930
|
SYNC_BASE = "https://www.terminalhire.com";
|
|
3326
3931
|
POLL_INTERVAL_MS = 2e3;
|
|
3327
3932
|
POLL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
@@ -3332,16 +3937,16 @@ var init_jpi_sync = __esm({
|
|
|
3332
3937
|
// bin/jpi-init.js
|
|
3333
3938
|
var jpi_init_exports = {};
|
|
3334
3939
|
__export(jpi_init_exports, {
|
|
3335
|
-
run: () =>
|
|
3940
|
+
run: () => run9
|
|
3336
3941
|
});
|
|
3337
3942
|
import { existsSync as existsSync8 } from "fs";
|
|
3338
|
-
import { join as
|
|
3943
|
+
import { join as join11, resolve } from "path";
|
|
3339
3944
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3340
|
-
import { createInterface as
|
|
3945
|
+
import { createInterface as createInterface6 } from "readline";
|
|
3341
3946
|
import { spawnSync, spawn as spawn2 } from "child_process";
|
|
3342
|
-
import { homedir as
|
|
3947
|
+
import { homedir as homedir9 } from "os";
|
|
3343
3948
|
function ask3(question) {
|
|
3344
|
-
const rl =
|
|
3949
|
+
const rl = createInterface6({ input: process.stdin, output: process.stdout });
|
|
3345
3950
|
return new Promise((resolve2) => {
|
|
3346
3951
|
rl.question(question, (answer) => {
|
|
3347
3952
|
rl.close();
|
|
@@ -3350,18 +3955,18 @@ function ask3(question) {
|
|
|
3350
3955
|
});
|
|
3351
3956
|
}
|
|
3352
3957
|
function resolveScript(name) {
|
|
3353
|
-
const distPath = resolve(
|
|
3354
|
-
const legacyPath = resolve(
|
|
3958
|
+
const distPath = resolve(join11(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
|
|
3959
|
+
const legacyPath = resolve(join11(__dirname2, `${name}.js`));
|
|
3355
3960
|
return existsSync8(distPath) ? distPath : legacyPath;
|
|
3356
3961
|
}
|
|
3357
3962
|
function resolveInstallJs() {
|
|
3358
|
-
const fromDist = resolve(
|
|
3359
|
-
const fromBin = resolve(
|
|
3963
|
+
const fromDist = resolve(join11(__dirname2, "..", "..", "install.js"));
|
|
3964
|
+
const fromBin = resolve(join11(__dirname2, "..", "install.js"));
|
|
3360
3965
|
if (existsSync8(fromDist)) return fromDist;
|
|
3361
3966
|
if (existsSync8(fromBin)) return fromBin;
|
|
3362
3967
|
return fromBin;
|
|
3363
3968
|
}
|
|
3364
|
-
async function
|
|
3969
|
+
async function run9() {
|
|
3365
3970
|
console.log("");
|
|
3366
3971
|
console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
3367
3972
|
console.log("\u2502 terminalhire init \u2014 one-command onboarding \u2502");
|
|
@@ -3464,17 +4069,17 @@ var init_jpi_init = __esm({
|
|
|
3464
4069
|
// bin/jpi-refresh.js
|
|
3465
4070
|
var jpi_refresh_exports = {};
|
|
3466
4071
|
__export(jpi_refresh_exports, {
|
|
3467
|
-
run: () =>
|
|
4072
|
+
run: () => run10
|
|
3468
4073
|
});
|
|
3469
|
-
import { readFileSync as
|
|
3470
|
-
import { join as
|
|
3471
|
-
import { homedir as
|
|
4074
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync9 } from "fs";
|
|
4075
|
+
import { join as join12 } from "path";
|
|
4076
|
+
import { homedir as homedir10 } from "os";
|
|
3472
4077
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
3473
|
-
async function
|
|
4078
|
+
async function run10() {
|
|
3474
4079
|
try {
|
|
3475
4080
|
let index;
|
|
3476
4081
|
try {
|
|
3477
|
-
const res = await fetch(`${
|
|
4082
|
+
const res = await fetch(`${API_URL4}/api/index`, {
|
|
3478
4083
|
signal: AbortSignal.timeout(15e3),
|
|
3479
4084
|
headers: { "Accept": "application/json" }
|
|
3480
4085
|
});
|
|
@@ -3507,19 +4112,24 @@ async function run9() {
|
|
|
3507
4112
|
company: r.job.company,
|
|
3508
4113
|
score: r.score,
|
|
3509
4114
|
remote: r.job.remote,
|
|
3510
|
-
matchedTags: r.matchedTags
|
|
4115
|
+
matchedTags: r.matchedTags,
|
|
4116
|
+
// Bounty fields so the spinner can render bounty framing ($ + 💎).
|
|
4117
|
+
// Public job text, stays LOCAL (same as the rest of topMatches).
|
|
4118
|
+
source: r.job.source,
|
|
4119
|
+
amountUSD: r.job.bounty?.amountUSD,
|
|
4120
|
+
repo: r.job.bounty?.repoFullName
|
|
3511
4121
|
}));
|
|
3512
4122
|
}
|
|
3513
4123
|
} catch {
|
|
3514
4124
|
}
|
|
3515
|
-
|
|
4125
|
+
mkdirSync9(TERMINALHIRE_DIR6, { recursive: true });
|
|
3516
4126
|
const cacheEntry = {
|
|
3517
4127
|
ts: Date.now(),
|
|
3518
4128
|
index,
|
|
3519
4129
|
matchCount,
|
|
3520
4130
|
topMatches
|
|
3521
4131
|
};
|
|
3522
|
-
|
|
4132
|
+
writeFileSync9(INDEX_CACHE_FILE3, JSON.stringify(cacheEntry), "utf8");
|
|
3523
4133
|
try {
|
|
3524
4134
|
const {
|
|
3525
4135
|
readSpinnerConfig: readSpinnerConfig2,
|
|
@@ -3546,7 +4156,7 @@ async function run9() {
|
|
|
3546
4156
|
const verbs = buildSpinnerPool2(ranked, sc.max, { sessionTags, frequency: sc.frequency });
|
|
3547
4157
|
if (verbs.length > 0) applySpinnerVerbs2(verbs, sc.mode);
|
|
3548
4158
|
else clearSpinnerVerbs2();
|
|
3549
|
-
const tips = buildTips2(ranked,
|
|
4159
|
+
const tips = buildTips2(ranked, API_URL4, 8);
|
|
3550
4160
|
if (tips.length > 0) applySpinnerTips2(tips);
|
|
3551
4161
|
else clearSpinnerTips2();
|
|
3552
4162
|
} else {
|
|
@@ -3563,30 +4173,30 @@ async function run9() {
|
|
|
3563
4173
|
process.exit(1);
|
|
3564
4174
|
}
|
|
3565
4175
|
}
|
|
3566
|
-
var __dirname3,
|
|
4176
|
+
var __dirname3, TERMINALHIRE_DIR6, INDEX_CACHE_FILE3, API_URL4;
|
|
3567
4177
|
var init_jpi_refresh = __esm({
|
|
3568
4178
|
"bin/jpi-refresh.js"() {
|
|
3569
4179
|
"use strict";
|
|
3570
4180
|
__dirname3 = fileURLToPath4(new URL(".", import.meta.url));
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
4181
|
+
TERMINALHIRE_DIR6 = join12(homedir10(), ".terminalhire");
|
|
4182
|
+
INDEX_CACHE_FILE3 = join12(TERMINALHIRE_DIR6, "index-cache.json");
|
|
4183
|
+
API_URL4 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
|
|
3574
4184
|
}
|
|
3575
4185
|
});
|
|
3576
4186
|
|
|
3577
4187
|
// bin/jpi-save.js
|
|
3578
4188
|
var jpi_save_exports = {};
|
|
3579
4189
|
__export(jpi_save_exports, {
|
|
3580
|
-
run: () =>
|
|
4190
|
+
run: () => run11
|
|
3581
4191
|
});
|
|
3582
|
-
import { readFileSync as
|
|
3583
|
-
import { join as
|
|
3584
|
-
import { homedir as
|
|
4192
|
+
import { readFileSync as readFileSync12, existsSync as existsSync10 } from "fs";
|
|
4193
|
+
import { join as join13 } from "path";
|
|
4194
|
+
import { homedir as homedir11 } from "os";
|
|
3585
4195
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
3586
4196
|
function findJobInCache(jobId) {
|
|
3587
4197
|
try {
|
|
3588
|
-
if (!existsSync10(
|
|
3589
|
-
const raw =
|
|
4198
|
+
if (!existsSync10(INDEX_CACHE_FILE4)) return null;
|
|
4199
|
+
const raw = readFileSync12(INDEX_CACHE_FILE4, "utf8");
|
|
3590
4200
|
const entry = JSON.parse(raw);
|
|
3591
4201
|
const jobs = entry?.index?.jobs ?? [];
|
|
3592
4202
|
return jobs.find((j) => j.id === jobId) ?? null;
|
|
@@ -3655,7 +4265,7 @@ async function cmdUnsave(jobId) {
|
|
|
3655
4265
|
process.exit(1);
|
|
3656
4266
|
}
|
|
3657
4267
|
}
|
|
3658
|
-
async function
|
|
4268
|
+
async function run11() {
|
|
3659
4269
|
const verb = process.argv[2];
|
|
3660
4270
|
const jobId = process.argv[3];
|
|
3661
4271
|
try {
|
|
@@ -3674,31 +4284,31 @@ async function run10() {
|
|
|
3674
4284
|
process.exit(1);
|
|
3675
4285
|
}
|
|
3676
4286
|
}
|
|
3677
|
-
var __dirname4,
|
|
4287
|
+
var __dirname4, TERMINALHIRE_DIR7, INDEX_CACHE_FILE4;
|
|
3678
4288
|
var init_jpi_save = __esm({
|
|
3679
4289
|
"bin/jpi-save.js"() {
|
|
3680
4290
|
"use strict";
|
|
3681
4291
|
__dirname4 = fileURLToPath5(new URL(".", import.meta.url));
|
|
3682
|
-
|
|
3683
|
-
|
|
4292
|
+
TERMINALHIRE_DIR7 = join13(homedir11(), ".terminalhire");
|
|
4293
|
+
INDEX_CACHE_FILE4 = join13(TERMINALHIRE_DIR7, "index-cache.json");
|
|
3684
4294
|
}
|
|
3685
4295
|
});
|
|
3686
4296
|
|
|
3687
4297
|
// bin/jpi-dispatch.js
|
|
3688
4298
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
3689
|
-
import { join as
|
|
3690
|
-
import { existsSync as existsSync11, readFileSync as
|
|
4299
|
+
import { join as join14, dirname as dirname2 } from "path";
|
|
4300
|
+
import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
|
|
3691
4301
|
import { createRequire } from "module";
|
|
3692
4302
|
var __dirname5 = fileURLToPath6(new URL(".", import.meta.url));
|
|
3693
4303
|
function readPackageVersion() {
|
|
3694
4304
|
try {
|
|
3695
4305
|
const candidates = [
|
|
3696
|
-
|
|
3697
|
-
|
|
4306
|
+
join14(__dirname5, "..", "..", "package.json"),
|
|
4307
|
+
join14(__dirname5, "..", "package.json")
|
|
3698
4308
|
];
|
|
3699
4309
|
for (const p of candidates) {
|
|
3700
4310
|
if (existsSync11(p)) {
|
|
3701
|
-
const pkg = JSON.parse(
|
|
4311
|
+
const pkg = JSON.parse(readFileSync13(p, "utf8"));
|
|
3702
4312
|
if (pkg.version) return pkg.version;
|
|
3703
4313
|
}
|
|
3704
4314
|
}
|
|
@@ -3709,7 +4319,7 @@ function readPackageVersion() {
|
|
|
3709
4319
|
var firstArg = process.argv[2];
|
|
3710
4320
|
if (!firstArg && !process.stdin.isTTY) {
|
|
3711
4321
|
const { default: childProcess } = await import("child_process");
|
|
3712
|
-
const nudgeScript =
|
|
4322
|
+
const nudgeScript = join14(__dirname5, "jpi.js");
|
|
3713
4323
|
const child = childProcess.spawnSync(process.execPath, [nudgeScript], {
|
|
3714
4324
|
stdio: ["inherit", "inherit", "inherit"]
|
|
3715
4325
|
});
|
|
@@ -3726,6 +4336,8 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
|
|
|
3726
4336
|
console.log(" terminalhire jobs Fetch job index, match locally, browse roles");
|
|
3727
4337
|
console.log(" terminalhire jobs --limit N Show top N results (default: 10)");
|
|
3728
4338
|
console.log(" terminalhire jobs --remote-only Filter to remote roles only");
|
|
4339
|
+
console.log(" terminalhire bounties Day-sized paid tasks you can knock out today");
|
|
4340
|
+
console.log(" terminalhire bounties --priced Only bounties with a known $ amount");
|
|
3729
4341
|
console.log(" terminalhire profile --show Display your encrypted local profile");
|
|
3730
4342
|
console.log(" terminalhire profile --edit Set displayName, contactEmail, prefs");
|
|
3731
4343
|
console.log(" terminalhire profile --delete Wipe profile and encryption key from disk");
|
|
@@ -3772,6 +4384,12 @@ if (firstArg === "jobs") {
|
|
|
3772
4384
|
await mod.run();
|
|
3773
4385
|
process.exit(0);
|
|
3774
4386
|
}
|
|
4387
|
+
if (firstArg === "bounties") {
|
|
4388
|
+
process.argv.splice(2, 1);
|
|
4389
|
+
const mod = await Promise.resolve().then(() => (init_jpi_bounties(), jpi_bounties_exports));
|
|
4390
|
+
await mod.run();
|
|
4391
|
+
process.exit(0);
|
|
4392
|
+
}
|
|
3775
4393
|
if (firstArg === "profile") {
|
|
3776
4394
|
const mod = await Promise.resolve().then(() => (init_jpi_profile(), jpi_profile_exports));
|
|
3777
4395
|
await mod.run();
|