terminalhire 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/jpi-bounties.js +2031 -0
- package/dist/bin/jpi-dispatch.js +1174 -473
- 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 +757 -311
- package/dist/bin/jpi-save.js +381 -213
- package/dist/bin/jpi-sync.js +502 -230
- 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) {
|
|
@@ -2748,10 +3337,10 @@ var TH_DIR, CLAUDE_SETTINGS, CONFIG_FILE2, SPINNER_STATE_FILE, SPINNER_DEFAULTS,
|
|
|
2748
3337
|
var init_spinner = __esm({
|
|
2749
3338
|
"bin/spinner.js"() {
|
|
2750
3339
|
"use strict";
|
|
2751
|
-
TH_DIR = process.env["TERMINALHIRE_DIR"] ||
|
|
2752
|
-
CLAUDE_SETTINGS = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] ||
|
|
2753
|
-
CONFIG_FILE2 =
|
|
2754
|
-
SPINNER_STATE_FILE =
|
|
3340
|
+
TH_DIR = process.env["TERMINALHIRE_DIR"] || join8(homedir6(), ".terminalhire");
|
|
3341
|
+
CLAUDE_SETTINGS = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join8(homedir6(), ".claude", "settings.json");
|
|
3342
|
+
CONFIG_FILE2 = join8(TH_DIR, "config.json");
|
|
3343
|
+
SPINNER_STATE_FILE = join8(TH_DIR, "spinner-state.json");
|
|
2755
3344
|
SPINNER_DEFAULTS = { enabled: false, mode: "append", max: 6, frequency: "sometimes" };
|
|
2756
3345
|
VERB_INTROS = ["Matched:", "You\u2019d fit:", "Worth a look:", "On your radar:", "Fits your stack:"];
|
|
2757
3346
|
}
|
|
@@ -2760,29 +3349,29 @@ var init_spinner = __esm({
|
|
|
2760
3349
|
// bin/jpi-spinner.js
|
|
2761
3350
|
var jpi_spinner_exports = {};
|
|
2762
3351
|
__export(jpi_spinner_exports, {
|
|
2763
|
-
run: () =>
|
|
3352
|
+
run: () => run7
|
|
2764
3353
|
});
|
|
2765
3354
|
import {
|
|
2766
|
-
readFileSync as
|
|
2767
|
-
writeFileSync as
|
|
3355
|
+
readFileSync as readFileSync9,
|
|
3356
|
+
writeFileSync as writeFileSync7,
|
|
2768
3357
|
copyFileSync,
|
|
2769
3358
|
existsSync as existsSync6,
|
|
2770
|
-
mkdirSync as
|
|
3359
|
+
mkdirSync as mkdirSync7
|
|
2771
3360
|
} from "fs";
|
|
2772
|
-
import { join as
|
|
2773
|
-
import { homedir as
|
|
2774
|
-
import { createInterface as
|
|
3361
|
+
import { join as join9 } from "path";
|
|
3362
|
+
import { homedir as homedir7 } from "os";
|
|
3363
|
+
import { createInterface as createInterface4 } from "readline";
|
|
2775
3364
|
function readConfig2() {
|
|
2776
3365
|
try {
|
|
2777
|
-
return existsSync6(CONFIG_FILE3) ? JSON.parse(
|
|
3366
|
+
return existsSync6(CONFIG_FILE3) ? JSON.parse(readFileSync9(CONFIG_FILE3, "utf8")) : {};
|
|
2778
3367
|
} catch {
|
|
2779
3368
|
return {};
|
|
2780
3369
|
}
|
|
2781
3370
|
}
|
|
2782
3371
|
function writeConfig2(patch) {
|
|
2783
|
-
|
|
3372
|
+
mkdirSync7(TH_DIR2, { recursive: true });
|
|
2784
3373
|
const merged = { ...readConfig2(), ...patch };
|
|
2785
|
-
|
|
3374
|
+
writeFileSync7(CONFIG_FILE3, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
2786
3375
|
}
|
|
2787
3376
|
function backupSettings() {
|
|
2788
3377
|
if (!existsSync6(SETTINGS_PATH)) return null;
|
|
@@ -2792,7 +3381,7 @@ function backupSettings() {
|
|
|
2792
3381
|
return backupPath;
|
|
2793
3382
|
}
|
|
2794
3383
|
function ask(question) {
|
|
2795
|
-
const rl =
|
|
3384
|
+
const rl = createInterface4({ input: process.stdin, output: process.stdout });
|
|
2796
3385
|
return new Promise((res) => {
|
|
2797
3386
|
rl.question(question, (answer) => {
|
|
2798
3387
|
rl.close();
|
|
@@ -2802,20 +3391,20 @@ function ask(question) {
|
|
|
2802
3391
|
}
|
|
2803
3392
|
function readTopMatches() {
|
|
2804
3393
|
try {
|
|
2805
|
-
const c = JSON.parse(
|
|
3394
|
+
const c = JSON.parse(readFileSync9(CACHE_FILE, "utf8"));
|
|
2806
3395
|
return Array.isArray(c.topMatches) ? c.topMatches : [];
|
|
2807
3396
|
} catch {
|
|
2808
3397
|
return [];
|
|
2809
3398
|
}
|
|
2810
3399
|
}
|
|
2811
|
-
async function
|
|
2812
|
-
const
|
|
2813
|
-
const has = (f) =>
|
|
3400
|
+
async function run7() {
|
|
3401
|
+
const args3 = process.argv.slice(2).filter((a) => a !== "spinner");
|
|
3402
|
+
const has = (f) => args3.includes(f);
|
|
2814
3403
|
const val = (f) => {
|
|
2815
|
-
const i =
|
|
2816
|
-
return i >= 0 ?
|
|
3404
|
+
const i = args3.indexOf(f);
|
|
3405
|
+
return i >= 0 ? args3[i + 1] : void 0;
|
|
2817
3406
|
};
|
|
2818
|
-
if (has("--show") ||
|
|
3407
|
+
if (has("--show") || args3.length === 0) {
|
|
2819
3408
|
const sc = readSpinnerConfig();
|
|
2820
3409
|
console.log("");
|
|
2821
3410
|
console.log("terminalhire spinner \u2014 job matches in the Claude Code spinner line");
|
|
@@ -2945,24 +3534,25 @@ var init_jpi_spinner = __esm({
|
|
|
2945
3534
|
"bin/jpi-spinner.js"() {
|
|
2946
3535
|
"use strict";
|
|
2947
3536
|
init_spinner();
|
|
2948
|
-
TH_DIR2 = process.env["TERMINALHIRE_DIR"] ||
|
|
2949
|
-
CONFIG_FILE3 =
|
|
2950
|
-
SETTINGS_PATH = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] ||
|
|
2951
|
-
CACHE_FILE =
|
|
3537
|
+
TH_DIR2 = process.env["TERMINALHIRE_DIR"] || join9(homedir7(), ".terminalhire");
|
|
3538
|
+
CONFIG_FILE3 = join9(TH_DIR2, "config.json");
|
|
3539
|
+
SETTINGS_PATH = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join9(homedir7(), ".claude", "settings.json");
|
|
3540
|
+
CACHE_FILE = join9(TH_DIR2, "index-cache.json");
|
|
2952
3541
|
}
|
|
2953
3542
|
});
|
|
2954
3543
|
|
|
2955
3544
|
// bin/jpi-sync.js
|
|
2956
3545
|
var jpi_sync_exports = {};
|
|
2957
3546
|
__export(jpi_sync_exports, {
|
|
2958
|
-
run: () =>
|
|
3547
|
+
run: () => run8
|
|
2959
3548
|
});
|
|
2960
|
-
import { readFileSync as
|
|
2961
|
-
import { join as
|
|
2962
|
-
import { homedir as
|
|
2963
|
-
import { createInterface as
|
|
3549
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8, existsSync as existsSync7, rmSync as rmSync2 } from "fs";
|
|
3550
|
+
import { join as join10 } from "path";
|
|
3551
|
+
import { homedir as homedir8, hostname as osHostname } from "os";
|
|
3552
|
+
import { createInterface as createInterface5 } from "readline";
|
|
3553
|
+
import { spawn } from "child_process";
|
|
2964
3554
|
function ask2(question) {
|
|
2965
|
-
const rl =
|
|
3555
|
+
const rl = createInterface5({ input: process.stdin, output: process.stdout });
|
|
2966
3556
|
return new Promise((res) => {
|
|
2967
3557
|
rl.question(question, (answer) => {
|
|
2968
3558
|
rl.close();
|
|
@@ -2972,14 +3562,14 @@ function ask2(question) {
|
|
|
2972
3562
|
}
|
|
2973
3563
|
function readMarker() {
|
|
2974
3564
|
try {
|
|
2975
|
-
return existsSync7(TIER1_MARKER) ? JSON.parse(
|
|
3565
|
+
return existsSync7(TIER1_MARKER) ? JSON.parse(readFileSync10(TIER1_MARKER, "utf8")) : null;
|
|
2976
3566
|
} catch {
|
|
2977
3567
|
return null;
|
|
2978
3568
|
}
|
|
2979
3569
|
}
|
|
2980
3570
|
function writeMarker(marker) {
|
|
2981
|
-
|
|
2982
|
-
|
|
3571
|
+
mkdirSync8(TH_DIR3, { recursive: true });
|
|
3572
|
+
writeFileSync8(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
2983
3573
|
}
|
|
2984
3574
|
function clearMarker() {
|
|
2985
3575
|
try {
|
|
@@ -3003,14 +3593,14 @@ function buildConsentFields(profile) {
|
|
|
3003
3593
|
}
|
|
3004
3594
|
return fields;
|
|
3005
3595
|
}
|
|
3006
|
-
function
|
|
3596
|
+
function renderPreview(fields) {
|
|
3007
3597
|
console.log("");
|
|
3008
3598
|
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\u2510");
|
|
3009
3599
|
console.log("\u2502 terminalhire \u2014 sync your profile (Tier-1, opt-in) \u2502");
|
|
3010
3600
|
console.log("\u2514\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\u2518");
|
|
3011
3601
|
console.log("");
|
|
3012
|
-
console.log("
|
|
3013
|
-
console.log("
|
|
3602
|
+
console.log(" The following data will be shared with staqs (terminalhire.com)");
|
|
3603
|
+
console.log(" AFTER you authorize + consent in the browser:");
|
|
3014
3604
|
console.log("");
|
|
3015
3605
|
for (const f of fields) {
|
|
3016
3606
|
const shown = Array.isArray(f.value) ? JSON.stringify(f.value) : String(f.value ?? "(not set)");
|
|
@@ -3029,6 +3619,30 @@ function renderConsentCard(fields) {
|
|
|
3029
3619
|
console.log(" This is NOT required to use terminalhire.");
|
|
3030
3620
|
console.log("");
|
|
3031
3621
|
}
|
|
3622
|
+
function openInBrowser(url) {
|
|
3623
|
+
let cmd;
|
|
3624
|
+
let args3;
|
|
3625
|
+
if (process.platform === "darwin") {
|
|
3626
|
+
cmd = "open";
|
|
3627
|
+
args3 = [url];
|
|
3628
|
+
} else if (process.platform === "win32") {
|
|
3629
|
+
cmd = "cmd";
|
|
3630
|
+
args3 = ["/c", "start", "", url];
|
|
3631
|
+
} else {
|
|
3632
|
+
cmd = "xdg-open";
|
|
3633
|
+
args3 = [url];
|
|
3634
|
+
}
|
|
3635
|
+
try {
|
|
3636
|
+
const child = spawn(cmd, args3, { stdio: "ignore", detached: true });
|
|
3637
|
+
child.on("error", () => {
|
|
3638
|
+
});
|
|
3639
|
+
child.unref();
|
|
3640
|
+
} catch {
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
function sleep2(ms) {
|
|
3644
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
3645
|
+
}
|
|
3032
3646
|
async function runPush() {
|
|
3033
3647
|
const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
3034
3648
|
const profile = await readProfile2();
|
|
@@ -3040,15 +3654,91 @@ async function runPush() {
|
|
|
3040
3654
|
process.exit(1);
|
|
3041
3655
|
}
|
|
3042
3656
|
const fields = buildConsentFields(profile);
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3657
|
+
renderPreview(fields);
|
|
3658
|
+
await new Promise((resolve2) => {
|
|
3659
|
+
const rl = createInterface5({ input: process.stdin, output: process.stdout });
|
|
3660
|
+
rl.question(
|
|
3661
|
+
" Press Enter to open your browser to authorize + consent (or Ctrl-C to cancel)... ",
|
|
3662
|
+
() => {
|
|
3663
|
+
rl.close();
|
|
3664
|
+
resolve2();
|
|
3665
|
+
}
|
|
3666
|
+
);
|
|
3667
|
+
});
|
|
3668
|
+
console.log("");
|
|
3669
|
+
console.log(" Starting browser verification...");
|
|
3670
|
+
let begin;
|
|
3671
|
+
try {
|
|
3672
|
+
const r = await fetch(`${SYNC_BASE}/api/profile-sync/begin`, {
|
|
3673
|
+
method: "POST",
|
|
3674
|
+
headers: { "Content-Type": "application/json" },
|
|
3675
|
+
body: JSON.stringify({ hostname: osHostname() }),
|
|
3676
|
+
signal: AbortSignal.timeout(1e4)
|
|
3677
|
+
});
|
|
3678
|
+
if (!r.ok) {
|
|
3679
|
+
let detail = "";
|
|
3680
|
+
try {
|
|
3681
|
+
detail = (await r.json())?.message || "";
|
|
3682
|
+
} catch {
|
|
3683
|
+
}
|
|
3684
|
+
console.error(`
|
|
3685
|
+
Could not start sync: /api/profile-sync/begin returned ${r.status}. ${detail}`);
|
|
3686
|
+
if (r.status === 503) console.error(" (Tier-1 sync is not enabled on the server yet.)");
|
|
3687
|
+
process.exit(1);
|
|
3688
|
+
}
|
|
3689
|
+
begin = await r.json();
|
|
3690
|
+
} catch (err) {
|
|
3691
|
+
console.error(`
|
|
3692
|
+
Could not start sync: ${err instanceof Error ? err.message : String(err)}`);
|
|
3693
|
+
process.exit(1);
|
|
3694
|
+
}
|
|
3695
|
+
const { challenge, verifyUrl } = begin || {};
|
|
3696
|
+
if (!challenge || !verifyUrl) {
|
|
3697
|
+
console.error("\n Could not start sync: malformed begin response.");
|
|
3698
|
+
process.exit(1);
|
|
3699
|
+
}
|
|
3700
|
+
console.log("");
|
|
3701
|
+
console.log(" Open this URL in your browser to authorize + consent:");
|
|
3702
|
+
console.log(` ${verifyUrl}`);
|
|
3703
|
+
console.log("");
|
|
3704
|
+
console.log(" (Attempting to open it automatically...)");
|
|
3705
|
+
openInBrowser(verifyUrl);
|
|
3706
|
+
console.log(" Waiting for browser verification...");
|
|
3707
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
3708
|
+
let proofToken = null;
|
|
3709
|
+
while (Date.now() < deadline) {
|
|
3710
|
+
await sleep2(POLL_INTERVAL_MS);
|
|
3711
|
+
let statusRes;
|
|
3712
|
+
try {
|
|
3713
|
+
statusRes = await fetch(
|
|
3714
|
+
`${SYNC_BASE}/api/profile-sync/status?challenge=${encodeURIComponent(challenge)}`,
|
|
3715
|
+
{ signal: AbortSignal.timeout(1e4) }
|
|
3716
|
+
);
|
|
3717
|
+
} catch {
|
|
3718
|
+
continue;
|
|
3719
|
+
}
|
|
3720
|
+
if (!statusRes.ok) {
|
|
3721
|
+
if (statusRes.status === 503) {
|
|
3722
|
+
console.error("\n Tier-1 sync is not enabled on the server yet.\n");
|
|
3723
|
+
process.exit(1);
|
|
3724
|
+
}
|
|
3725
|
+
continue;
|
|
3726
|
+
}
|
|
3727
|
+
let body;
|
|
3728
|
+
try {
|
|
3729
|
+
body = await statusRes.json();
|
|
3730
|
+
} catch {
|
|
3731
|
+
continue;
|
|
3732
|
+
}
|
|
3733
|
+
if (body && body.status === "verified" && body.proofToken) {
|
|
3734
|
+
proofToken = body.proofToken;
|
|
3735
|
+
break;
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
if (!proofToken) {
|
|
3739
|
+
console.error("\n Timed out waiting for browser verification (10 min).");
|
|
3740
|
+
console.error(" Re-run `terminalhire sync --push` to try again.\n");
|
|
3741
|
+
process.exit(1);
|
|
3052
3742
|
}
|
|
3053
3743
|
const consentedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3054
3744
|
const consentToken = {
|
|
@@ -3067,14 +3757,14 @@ async function runPush() {
|
|
|
3067
3757
|
};
|
|
3068
3758
|
const priorMarker = readMarker();
|
|
3069
3759
|
const rowToken = priorMarker && priorMarker.deleteToken ? priorMarker.deleteToken : null;
|
|
3070
|
-
const requestBody = { consentToken, profile: payloadProfile };
|
|
3760
|
+
const requestBody = { consentToken, profile: payloadProfile, proofToken };
|
|
3071
3761
|
if (rowToken) {
|
|
3072
3762
|
requestBody.rowToken = rowToken;
|
|
3073
3763
|
}
|
|
3074
|
-
console.log("\n Sending one-time snapshot...");
|
|
3764
|
+
console.log("\n Verified. Sending one-time snapshot...");
|
|
3075
3765
|
let res;
|
|
3076
3766
|
try {
|
|
3077
|
-
res = await fetch(`${
|
|
3767
|
+
res = await fetch(`${SYNC_BASE}/api/profile-sync`, {
|
|
3078
3768
|
method: "POST",
|
|
3079
3769
|
headers: { "Content-Type": "application/json" },
|
|
3080
3770
|
body: JSON.stringify(requestBody),
|
|
@@ -3097,7 +3787,7 @@ async function runPush() {
|
|
|
3097
3787
|
console.error(" (Tier-1 sync is not enabled on the server yet.)");
|
|
3098
3788
|
}
|
|
3099
3789
|
if (res.status === 403) {
|
|
3100
|
-
console.error(" (
|
|
3790
|
+
console.error(" (Ownership proof rejected, expired, or already used \u2014 re-run sync --push.)");
|
|
3101
3791
|
}
|
|
3102
3792
|
process.exit(1);
|
|
3103
3793
|
}
|
|
@@ -3164,7 +3854,7 @@ async function runDelete() {
|
|
|
3164
3854
|
console.log("\n Requesting deletion...");
|
|
3165
3855
|
let res;
|
|
3166
3856
|
try {
|
|
3167
|
-
res = await fetch(`${
|
|
3857
|
+
res = await fetch(`${API_URL3}/api/profile-sync`, {
|
|
3168
3858
|
method: "DELETE",
|
|
3169
3859
|
headers: { "Content-Type": "application/json" },
|
|
3170
3860
|
body: JSON.stringify({ consentToken, login, deleteToken }),
|
|
@@ -3188,9 +3878,9 @@ async function runDelete() {
|
|
|
3188
3878
|
clearMarker();
|
|
3189
3879
|
console.log("\n Synced profile deleted and local marker cleared.\n");
|
|
3190
3880
|
}
|
|
3191
|
-
async function
|
|
3192
|
-
const
|
|
3193
|
-
const has = (f) =>
|
|
3881
|
+
async function run8() {
|
|
3882
|
+
const args3 = process.argv.slice(2).filter((a) => a !== "sync");
|
|
3883
|
+
const has = (f) => args3.includes(f);
|
|
3194
3884
|
if (has("--push") || has("--enable")) {
|
|
3195
3885
|
await runPush();
|
|
3196
3886
|
return;
|
|
@@ -3214,13 +3904,16 @@ async function run7() {
|
|
|
3214
3904
|
console.log(" This is NOT required to use terminalhire.");
|
|
3215
3905
|
console.log("");
|
|
3216
3906
|
}
|
|
3217
|
-
var TH_DIR3, TIER1_MARKER,
|
|
3907
|
+
var TH_DIR3, TIER1_MARKER, API_URL3, SYNC_BASE, POLL_INTERVAL_MS, POLL_TIMEOUT_MS, CONSENT_VERSION;
|
|
3218
3908
|
var init_jpi_sync = __esm({
|
|
3219
3909
|
"bin/jpi-sync.js"() {
|
|
3220
3910
|
"use strict";
|
|
3221
|
-
TH_DIR3 = process.env["TERMINALHIRE_DIR"] ||
|
|
3222
|
-
TIER1_MARKER =
|
|
3223
|
-
|
|
3911
|
+
TH_DIR3 = process.env["TERMINALHIRE_DIR"] || join10(homedir8(), ".terminalhire");
|
|
3912
|
+
TIER1_MARKER = join10(TH_DIR3, "tier1.json");
|
|
3913
|
+
API_URL3 = process.env["TERMINALHIRE_API_URL"] || process.env["JPI_API_URL"] || "https://terminalhire.com";
|
|
3914
|
+
SYNC_BASE = "https://www.terminalhire.com";
|
|
3915
|
+
POLL_INTERVAL_MS = 2e3;
|
|
3916
|
+
POLL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
3224
3917
|
CONSENT_VERSION = 1;
|
|
3225
3918
|
}
|
|
3226
3919
|
});
|
|
@@ -3228,16 +3921,16 @@ var init_jpi_sync = __esm({
|
|
|
3228
3921
|
// bin/jpi-init.js
|
|
3229
3922
|
var jpi_init_exports = {};
|
|
3230
3923
|
__export(jpi_init_exports, {
|
|
3231
|
-
run: () =>
|
|
3924
|
+
run: () => run9
|
|
3232
3925
|
});
|
|
3233
3926
|
import { existsSync as existsSync8 } from "fs";
|
|
3234
|
-
import { join as
|
|
3927
|
+
import { join as join11, resolve } from "path";
|
|
3235
3928
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3236
|
-
import { createInterface as
|
|
3237
|
-
import { spawnSync, spawn } from "child_process";
|
|
3238
|
-
import { homedir as
|
|
3929
|
+
import { createInterface as createInterface6 } from "readline";
|
|
3930
|
+
import { spawnSync, spawn as spawn2 } from "child_process";
|
|
3931
|
+
import { homedir as homedir9 } from "os";
|
|
3239
3932
|
function ask3(question) {
|
|
3240
|
-
const rl =
|
|
3933
|
+
const rl = createInterface6({ input: process.stdin, output: process.stdout });
|
|
3241
3934
|
return new Promise((resolve2) => {
|
|
3242
3935
|
rl.question(question, (answer) => {
|
|
3243
3936
|
rl.close();
|
|
@@ -3246,18 +3939,18 @@ function ask3(question) {
|
|
|
3246
3939
|
});
|
|
3247
3940
|
}
|
|
3248
3941
|
function resolveScript(name) {
|
|
3249
|
-
const distPath = resolve(
|
|
3250
|
-
const legacyPath = resolve(
|
|
3942
|
+
const distPath = resolve(join11(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
|
|
3943
|
+
const legacyPath = resolve(join11(__dirname2, `${name}.js`));
|
|
3251
3944
|
return existsSync8(distPath) ? distPath : legacyPath;
|
|
3252
3945
|
}
|
|
3253
3946
|
function resolveInstallJs() {
|
|
3254
|
-
const fromDist = resolve(
|
|
3255
|
-
const fromBin = resolve(
|
|
3947
|
+
const fromDist = resolve(join11(__dirname2, "..", "..", "install.js"));
|
|
3948
|
+
const fromBin = resolve(join11(__dirname2, "..", "install.js"));
|
|
3256
3949
|
if (existsSync8(fromDist)) return fromDist;
|
|
3257
3950
|
if (existsSync8(fromBin)) return fromBin;
|
|
3258
3951
|
return fromBin;
|
|
3259
3952
|
}
|
|
3260
|
-
async function
|
|
3953
|
+
async function run9() {
|
|
3261
3954
|
console.log("");
|
|
3262
3955
|
console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
3263
3956
|
console.log("\u2502 terminalhire init \u2014 one-command onboarding \u2502");
|
|
@@ -3360,17 +4053,17 @@ var init_jpi_init = __esm({
|
|
|
3360
4053
|
// bin/jpi-refresh.js
|
|
3361
4054
|
var jpi_refresh_exports = {};
|
|
3362
4055
|
__export(jpi_refresh_exports, {
|
|
3363
|
-
run: () =>
|
|
4056
|
+
run: () => run10
|
|
3364
4057
|
});
|
|
3365
|
-
import { readFileSync as
|
|
3366
|
-
import { join as
|
|
3367
|
-
import { homedir as
|
|
4058
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync9 } from "fs";
|
|
4059
|
+
import { join as join12 } from "path";
|
|
4060
|
+
import { homedir as homedir10 } from "os";
|
|
3368
4061
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
3369
|
-
async function
|
|
4062
|
+
async function run10() {
|
|
3370
4063
|
try {
|
|
3371
4064
|
let index;
|
|
3372
4065
|
try {
|
|
3373
|
-
const res = await fetch(`${
|
|
4066
|
+
const res = await fetch(`${API_URL4}/api/index`, {
|
|
3374
4067
|
signal: AbortSignal.timeout(15e3),
|
|
3375
4068
|
headers: { "Accept": "application/json" }
|
|
3376
4069
|
});
|
|
@@ -3408,14 +4101,14 @@ async function run9() {
|
|
|
3408
4101
|
}
|
|
3409
4102
|
} catch {
|
|
3410
4103
|
}
|
|
3411
|
-
|
|
4104
|
+
mkdirSync9(TERMINALHIRE_DIR6, { recursive: true });
|
|
3412
4105
|
const cacheEntry = {
|
|
3413
4106
|
ts: Date.now(),
|
|
3414
4107
|
index,
|
|
3415
4108
|
matchCount,
|
|
3416
4109
|
topMatches
|
|
3417
4110
|
};
|
|
3418
|
-
|
|
4111
|
+
writeFileSync9(INDEX_CACHE_FILE3, JSON.stringify(cacheEntry), "utf8");
|
|
3419
4112
|
try {
|
|
3420
4113
|
const {
|
|
3421
4114
|
readSpinnerConfig: readSpinnerConfig2,
|
|
@@ -3442,7 +4135,7 @@ async function run9() {
|
|
|
3442
4135
|
const verbs = buildSpinnerPool2(ranked, sc.max, { sessionTags, frequency: sc.frequency });
|
|
3443
4136
|
if (verbs.length > 0) applySpinnerVerbs2(verbs, sc.mode);
|
|
3444
4137
|
else clearSpinnerVerbs2();
|
|
3445
|
-
const tips = buildTips2(ranked,
|
|
4138
|
+
const tips = buildTips2(ranked, API_URL4, 8);
|
|
3446
4139
|
if (tips.length > 0) applySpinnerTips2(tips);
|
|
3447
4140
|
else clearSpinnerTips2();
|
|
3448
4141
|
} else {
|
|
@@ -3459,30 +4152,30 @@ async function run9() {
|
|
|
3459
4152
|
process.exit(1);
|
|
3460
4153
|
}
|
|
3461
4154
|
}
|
|
3462
|
-
var __dirname3,
|
|
4155
|
+
var __dirname3, TERMINALHIRE_DIR6, INDEX_CACHE_FILE3, API_URL4;
|
|
3463
4156
|
var init_jpi_refresh = __esm({
|
|
3464
4157
|
"bin/jpi-refresh.js"() {
|
|
3465
4158
|
"use strict";
|
|
3466
4159
|
__dirname3 = fileURLToPath4(new URL(".", import.meta.url));
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
4160
|
+
TERMINALHIRE_DIR6 = join12(homedir10(), ".terminalhire");
|
|
4161
|
+
INDEX_CACHE_FILE3 = join12(TERMINALHIRE_DIR6, "index-cache.json");
|
|
4162
|
+
API_URL4 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
|
|
3470
4163
|
}
|
|
3471
4164
|
});
|
|
3472
4165
|
|
|
3473
4166
|
// bin/jpi-save.js
|
|
3474
4167
|
var jpi_save_exports = {};
|
|
3475
4168
|
__export(jpi_save_exports, {
|
|
3476
|
-
run: () =>
|
|
4169
|
+
run: () => run11
|
|
3477
4170
|
});
|
|
3478
|
-
import { readFileSync as
|
|
3479
|
-
import { join as
|
|
3480
|
-
import { homedir as
|
|
4171
|
+
import { readFileSync as readFileSync12, existsSync as existsSync10 } from "fs";
|
|
4172
|
+
import { join as join13 } from "path";
|
|
4173
|
+
import { homedir as homedir11 } from "os";
|
|
3481
4174
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
3482
4175
|
function findJobInCache(jobId) {
|
|
3483
4176
|
try {
|
|
3484
|
-
if (!existsSync10(
|
|
3485
|
-
const raw =
|
|
4177
|
+
if (!existsSync10(INDEX_CACHE_FILE4)) return null;
|
|
4178
|
+
const raw = readFileSync12(INDEX_CACHE_FILE4, "utf8");
|
|
3486
4179
|
const entry = JSON.parse(raw);
|
|
3487
4180
|
const jobs = entry?.index?.jobs ?? [];
|
|
3488
4181
|
return jobs.find((j) => j.id === jobId) ?? null;
|
|
@@ -3551,7 +4244,7 @@ async function cmdUnsave(jobId) {
|
|
|
3551
4244
|
process.exit(1);
|
|
3552
4245
|
}
|
|
3553
4246
|
}
|
|
3554
|
-
async function
|
|
4247
|
+
async function run11() {
|
|
3555
4248
|
const verb = process.argv[2];
|
|
3556
4249
|
const jobId = process.argv[3];
|
|
3557
4250
|
try {
|
|
@@ -3570,31 +4263,31 @@ async function run10() {
|
|
|
3570
4263
|
process.exit(1);
|
|
3571
4264
|
}
|
|
3572
4265
|
}
|
|
3573
|
-
var __dirname4,
|
|
4266
|
+
var __dirname4, TERMINALHIRE_DIR7, INDEX_CACHE_FILE4;
|
|
3574
4267
|
var init_jpi_save = __esm({
|
|
3575
4268
|
"bin/jpi-save.js"() {
|
|
3576
4269
|
"use strict";
|
|
3577
4270
|
__dirname4 = fileURLToPath5(new URL(".", import.meta.url));
|
|
3578
|
-
|
|
3579
|
-
|
|
4271
|
+
TERMINALHIRE_DIR7 = join13(homedir11(), ".terminalhire");
|
|
4272
|
+
INDEX_CACHE_FILE4 = join13(TERMINALHIRE_DIR7, "index-cache.json");
|
|
3580
4273
|
}
|
|
3581
4274
|
});
|
|
3582
4275
|
|
|
3583
4276
|
// bin/jpi-dispatch.js
|
|
3584
4277
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
3585
|
-
import { join as
|
|
3586
|
-
import { existsSync as existsSync11, readFileSync as
|
|
4278
|
+
import { join as join14, dirname as dirname2 } from "path";
|
|
4279
|
+
import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
|
|
3587
4280
|
import { createRequire } from "module";
|
|
3588
4281
|
var __dirname5 = fileURLToPath6(new URL(".", import.meta.url));
|
|
3589
4282
|
function readPackageVersion() {
|
|
3590
4283
|
try {
|
|
3591
4284
|
const candidates = [
|
|
3592
|
-
|
|
3593
|
-
|
|
4285
|
+
join14(__dirname5, "..", "..", "package.json"),
|
|
4286
|
+
join14(__dirname5, "..", "package.json")
|
|
3594
4287
|
];
|
|
3595
4288
|
for (const p of candidates) {
|
|
3596
4289
|
if (existsSync11(p)) {
|
|
3597
|
-
const pkg = JSON.parse(
|
|
4290
|
+
const pkg = JSON.parse(readFileSync13(p, "utf8"));
|
|
3598
4291
|
if (pkg.version) return pkg.version;
|
|
3599
4292
|
}
|
|
3600
4293
|
}
|
|
@@ -3605,7 +4298,7 @@ function readPackageVersion() {
|
|
|
3605
4298
|
var firstArg = process.argv[2];
|
|
3606
4299
|
if (!firstArg && !process.stdin.isTTY) {
|
|
3607
4300
|
const { default: childProcess } = await import("child_process");
|
|
3608
|
-
const nudgeScript =
|
|
4301
|
+
const nudgeScript = join14(__dirname5, "jpi.js");
|
|
3609
4302
|
const child = childProcess.spawnSync(process.execPath, [nudgeScript], {
|
|
3610
4303
|
stdio: ["inherit", "inherit", "inherit"]
|
|
3611
4304
|
});
|
|
@@ -3622,6 +4315,8 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
|
|
|
3622
4315
|
console.log(" terminalhire jobs Fetch job index, match locally, browse roles");
|
|
3623
4316
|
console.log(" terminalhire jobs --limit N Show top N results (default: 10)");
|
|
3624
4317
|
console.log(" terminalhire jobs --remote-only Filter to remote roles only");
|
|
4318
|
+
console.log(" terminalhire bounties Day-sized paid tasks you can knock out today");
|
|
4319
|
+
console.log(" terminalhire bounties --priced Only bounties with a known $ amount");
|
|
3625
4320
|
console.log(" terminalhire profile --show Display your encrypted local profile");
|
|
3626
4321
|
console.log(" terminalhire profile --edit Set displayName, contactEmail, prefs");
|
|
3627
4322
|
console.log(" terminalhire profile --delete Wipe profile and encryption key from disk");
|
|
@@ -3668,6 +4363,12 @@ if (firstArg === "jobs") {
|
|
|
3668
4363
|
await mod.run();
|
|
3669
4364
|
process.exit(0);
|
|
3670
4365
|
}
|
|
4366
|
+
if (firstArg === "bounties") {
|
|
4367
|
+
process.argv.splice(2, 1);
|
|
4368
|
+
const mod = await Promise.resolve().then(() => (init_jpi_bounties(), jpi_bounties_exports));
|
|
4369
|
+
await mod.run();
|
|
4370
|
+
process.exit(0);
|
|
4371
|
+
}
|
|
3671
4372
|
if (firstArg === "profile") {
|
|
3672
4373
|
const mod = await Promise.resolve().then(() => (init_jpi_profile(), jpi_profile_exports));
|
|
3673
4374
|
await mod.run();
|