terminalhire 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/jpi-bounties.js +2031 -0
- package/dist/bin/jpi-dispatch.js +1059 -462
- 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 +381 -213
- 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
|
@@ -0,0 +1,2031 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ../../packages/core/src/types.ts
|
|
13
|
+
function isBounty(job) {
|
|
14
|
+
return job.source === "bounty" && job.bounty != null;
|
|
15
|
+
}
|
|
16
|
+
var init_types = __esm({
|
|
17
|
+
"../../packages/core/src/types.ts"() {
|
|
18
|
+
"use strict";
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ../../packages/core/src/vocab/graph.data.ts
|
|
23
|
+
var VOCAB_NODES;
|
|
24
|
+
var init_graph_data = __esm({
|
|
25
|
+
"../../packages/core/src/vocab/graph.data.ts"() {
|
|
26
|
+
"use strict";
|
|
27
|
+
VOCAB_NODES = [
|
|
28
|
+
// ── Languages ─────────────────────────────────────────────────────────────
|
|
29
|
+
{ id: "javascript", synonyms: ["js"], related: [{ to: "typescript", w: 0.6 }] },
|
|
30
|
+
{ id: "typescript", parents: ["javascript"], synonyms: ["ts"] },
|
|
31
|
+
{ id: "python", synonyms: ["py"] },
|
|
32
|
+
{ id: "go", synonyms: ["golang"] },
|
|
33
|
+
{ id: "rust" },
|
|
34
|
+
{ id: "java", related: [{ to: "kotlin", w: 0.45 }, { to: "scala", w: 0.4 }] },
|
|
35
|
+
{ id: "ruby" },
|
|
36
|
+
{ id: "elixir" },
|
|
37
|
+
{ id: "scala", related: [{ to: "java", w: 0.4 }] },
|
|
38
|
+
{ id: "kotlin", related: [{ to: "java", w: 0.45 }] },
|
|
39
|
+
{ id: "swift" },
|
|
40
|
+
{ id: "cpp", synonyms: ["c++"] },
|
|
41
|
+
{ id: "csharp", synonyms: ["c#"] },
|
|
42
|
+
{ id: "php" },
|
|
43
|
+
{ id: "haskell" },
|
|
44
|
+
{ id: "clojure" },
|
|
45
|
+
{ id: "r" },
|
|
46
|
+
{ id: "dart" },
|
|
47
|
+
// ── Frontend ──────────────────────────────────────────────────────────────
|
|
48
|
+
{
|
|
49
|
+
id: "react",
|
|
50
|
+
parents: ["javascript"],
|
|
51
|
+
synonyms: ["reactjs"],
|
|
52
|
+
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 }]
|
|
53
|
+
},
|
|
54
|
+
{ id: "nextjs", parents: ["react"], synonyms: ["next", "next.js"], related: [{ to: "remix", w: 0.5 }] },
|
|
55
|
+
{ id: "vue", parents: ["javascript"], synonyms: ["vue.js"], related: [{ to: "nuxt", w: 0.6 }] },
|
|
56
|
+
{ id: "nuxt", parents: ["vue"], synonyms: ["nuxt.js"] },
|
|
57
|
+
{ id: "svelte", parents: ["javascript"], related: [{ to: "sveltekit", w: 0.65 }] },
|
|
58
|
+
{ id: "sveltekit", parents: ["svelte"] },
|
|
59
|
+
{ id: "angular", parents: ["typescript"], synonyms: ["angular.js", "angularjs"] },
|
|
60
|
+
{ id: "solidjs", parents: ["javascript"] },
|
|
61
|
+
{ id: "remix", parents: ["react"], synonyms: ["remix.run"] },
|
|
62
|
+
{ id: "astro", parents: ["javascript"], related: [{ to: "nextjs", w: 0.4 }] },
|
|
63
|
+
{ id: "qwik", parents: ["javascript"] },
|
|
64
|
+
{ id: "tailwind", parents: ["css"], synonyms: ["tailwindcss", "tw"] },
|
|
65
|
+
{ id: "css" },
|
|
66
|
+
{ id: "html" },
|
|
67
|
+
{ id: "redux", parents: ["react"] },
|
|
68
|
+
{ id: "vite", parents: ["frontend"] },
|
|
69
|
+
{ id: "webpack", parents: ["frontend"] },
|
|
70
|
+
{ id: "storybook", parents: ["frontend"] },
|
|
71
|
+
// ── Backend frameworks / runtimes ───────────────────────────────────────────
|
|
72
|
+
{
|
|
73
|
+
id: "nodejs",
|
|
74
|
+
parents: ["javascript"],
|
|
75
|
+
synonyms: ["node", "node.js"],
|
|
76
|
+
related: [{ to: "express", w: 0.5 }, { to: "fastify", w: 0.45 }, { to: "nestjs", w: 0.45 }]
|
|
77
|
+
},
|
|
78
|
+
{ id: "express", parents: ["nodejs"], synonyms: ["express.js", "expressjs"], related: [{ to: "fastify", w: 0.5 }] },
|
|
79
|
+
{ id: "fastify", parents: ["nodejs"] },
|
|
80
|
+
{ id: "nestjs", parents: ["nodejs"], synonyms: ["nest", "nest.js"] },
|
|
81
|
+
{ id: "hono", parents: ["nodejs"] },
|
|
82
|
+
{ id: "deno", parents: ["javascript"], related: [{ to: "nodejs", w: 0.5 }, { to: "bun", w: 0.5 }] },
|
|
83
|
+
{ id: "bun", parents: ["javascript"], related: [{ to: "nodejs", w: 0.5 }] },
|
|
84
|
+
{ id: "django", parents: ["python"], related: [{ to: "flask", w: 0.5 }, { to: "fastapi", w: 0.45 }] },
|
|
85
|
+
{ id: "fastapi", parents: ["python"], related: [{ to: "flask", w: 0.55 }, { to: "django", w: 0.45 }] },
|
|
86
|
+
{ id: "flask", parents: ["python"] },
|
|
87
|
+
{ id: "rails", parents: ["ruby"], synonyms: ["ruby-on-rails", "ror"] },
|
|
88
|
+
{ id: "spring", parents: ["java"], synonyms: ["spring-boot", "springboot"] },
|
|
89
|
+
{ id: "actix", parents: ["rust"] },
|
|
90
|
+
{ id: "gin", parents: ["go"] },
|
|
91
|
+
{ id: "phoenix", parents: ["elixir"] },
|
|
92
|
+
{ id: "laravel", parents: ["php"] },
|
|
93
|
+
{ id: "dotnet", parents: ["csharp"], synonyms: [".net", "asp.net", "dotnet-core"] },
|
|
94
|
+
// ── Infrastructure & DevOps ─────────────────────────────────────────────────
|
|
95
|
+
{ 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 }] },
|
|
96
|
+
{ id: "docker", parents: ["devops"], related: [{ to: "kubernetes", w: 0.5 }] },
|
|
97
|
+
{ id: "terraform", synonyms: ["tf"], related: [{ to: "pulumi", w: 0.55 }, { to: "ansible", w: 0.4 }, { to: "aws", w: 0.4 }] },
|
|
98
|
+
{ id: "pulumi", related: [{ to: "terraform", w: 0.55 }] },
|
|
99
|
+
{ id: "ansible" },
|
|
100
|
+
{ id: "aws", synonyms: ["amazon-web-services"], related: [{ to: "gcp", w: 0.4 }, { to: "azure", w: 0.4 }] },
|
|
101
|
+
{ id: "gcp", synonyms: ["google-cloud", "google-cloud-platform"], related: [{ to: "aws", w: 0.4 }, { to: "azure", w: 0.4 }] },
|
|
102
|
+
{ id: "azure", synonyms: ["microsoft-azure"], related: [{ to: "aws", w: 0.4 }] },
|
|
103
|
+
{ id: "ci-cd", synonyms: ["cicd", "jenkins", "circleci", "circle-ci", "travis"], related: [{ to: "github-actions", w: 0.6 }, { to: "gitlab-ci", w: 0.6 }] },
|
|
104
|
+
{ id: "github-actions", parents: ["ci-cd"], synonyms: ["github-action"] },
|
|
105
|
+
{ id: "gitlab-ci", parents: ["ci-cd"], synonyms: ["gitlab"] },
|
|
106
|
+
{ id: "linux" },
|
|
107
|
+
{ id: "nginx" },
|
|
108
|
+
{ id: "prometheus", parents: ["observability"], related: [{ to: "grafana", w: 0.6 }] },
|
|
109
|
+
{ id: "grafana", parents: ["observability"] },
|
|
110
|
+
{ id: "datadog", parents: ["observability"] },
|
|
111
|
+
{ id: "opentelemetry", parents: ["observability"], synonyms: ["otel"] },
|
|
112
|
+
{ id: "vercel", related: [{ to: "netlify", w: 0.5 }, { to: "nextjs", w: 0.4 }] },
|
|
113
|
+
{ id: "netlify" },
|
|
114
|
+
{ id: "fly", synonyms: ["fly.io"], related: [{ to: "railway", w: 0.5 }, { to: "render", w: 0.5 }] },
|
|
115
|
+
{ id: "railway", related: [{ to: "render", w: 0.5 }] },
|
|
116
|
+
{ id: "render" },
|
|
117
|
+
{ id: "cloudflare", synonyms: ["cloudflare-workers"] },
|
|
118
|
+
{ id: "helm", parents: ["kubernetes"] },
|
|
119
|
+
{ id: "argocd", parents: ["kubernetes"] },
|
|
120
|
+
{ id: "serverless", parents: ["devops"] },
|
|
121
|
+
// ── Databases & storage ─────────────────────────────────────────────────────
|
|
122
|
+
{ id: "postgresql", synonyms: ["postgres", "pg"], related: [{ to: "mysql", w: 0.45 }, { to: "sqlite", w: 0.4 }] },
|
|
123
|
+
{ id: "mysql", related: [{ to: "postgresql", w: 0.45 }] },
|
|
124
|
+
{ id: "sqlite" },
|
|
125
|
+
{ id: "mongodb", synonyms: ["mongo"] },
|
|
126
|
+
{ id: "redis", related: [{ to: "caching", w: 0.5 }] },
|
|
127
|
+
{ id: "elasticsearch", synonyms: ["elastic"], related: [{ to: "search", w: 0.55 }] },
|
|
128
|
+
{ id: "kafka", synonyms: ["apache-kafka"], related: [{ to: "rabbitmq", w: 0.5 }, { to: "message-queue", w: 0.55 }] },
|
|
129
|
+
{ id: "rabbitmq", related: [{ to: "message-queue", w: 0.55 }] },
|
|
130
|
+
{ id: "cassandra" },
|
|
131
|
+
{ id: "dynamodb", parents: ["aws"] },
|
|
132
|
+
{ id: "snowflake", parents: ["data-engineering"], related: [{ to: "clickhouse", w: 0.4 }] },
|
|
133
|
+
{ id: "clickhouse", parents: ["data-engineering"], related: [{ to: "duckdb", w: 0.35 }] },
|
|
134
|
+
{ id: "duckdb", parents: ["data-engineering"] },
|
|
135
|
+
{ id: "supabase", related: [{ to: "postgresql", w: 0.5 }, { to: "neon", w: 0.4 }] },
|
|
136
|
+
{ id: "planetscale", related: [{ to: "mysql", w: 0.5 }] },
|
|
137
|
+
{ id: "neon", related: [{ to: "postgresql", w: 0.5 }] },
|
|
138
|
+
{ id: "turso", related: [{ to: "sqlite", w: 0.5 }] },
|
|
139
|
+
{ id: "cockroachdb", related: [{ to: "postgresql", w: 0.45 }] },
|
|
140
|
+
{ id: "prisma", parents: ["backend"], synonyms: ["@prisma/client"], related: [{ to: "drizzle", w: 0.5 }, { to: "typeorm", w: 0.45 }, { to: "sequelize", w: 0.4 }] },
|
|
141
|
+
{ id: "drizzle", synonyms: ["drizzle-orm"], related: [{ to: "prisma", w: 0.5 }] },
|
|
142
|
+
{ id: "sequelize", related: [{ to: "typeorm", w: 0.4 }] },
|
|
143
|
+
{ id: "typeorm", related: [{ to: "prisma", w: 0.45 }] },
|
|
144
|
+
{ id: "sqlalchemy", parents: ["python"] },
|
|
145
|
+
// ── Data engineering & ML ───────────────────────────────────────────────────
|
|
146
|
+
{ id: "data-engineering", synonyms: ["data-eng"], related: [{ to: "spark", w: 0.5 }, { to: "airflow", w: 0.5 }, { to: "dbt", w: 0.45 }] },
|
|
147
|
+
{ id: "spark", parents: ["data-engineering"], synonyms: ["apache-spark"] },
|
|
148
|
+
{ id: "airflow", parents: ["data-engineering"], synonyms: ["apache-airflow"] },
|
|
149
|
+
{ id: "dbt", parents: ["data-engineering"] },
|
|
150
|
+
{ id: "ml", synonyms: ["machine-learning"], related: [{ to: "pytorch", w: 0.5 }, { to: "tensorflow", w: 0.5 }, { to: "scikit-learn", w: 0.5 }] },
|
|
151
|
+
{ 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 }] },
|
|
152
|
+
{ id: "pytorch", parents: ["ml"], synonyms: ["torch"], related: [{ to: "tensorflow", w: 0.5 }] },
|
|
153
|
+
{ id: "tensorflow", parents: ["ml"], synonyms: ["keras", "tf-keras"] },
|
|
154
|
+
{ id: "pandas", parents: ["python"], related: [{ to: "numpy", w: 0.6 }] },
|
|
155
|
+
{ id: "numpy", parents: ["python"] },
|
|
156
|
+
{ id: "scikit-learn", parents: ["ml"], synonyms: ["sklearn"] },
|
|
157
|
+
{ id: "jupyter", parents: ["python"] },
|
|
158
|
+
{ id: "langchain", parents: ["llm"], synonyms: ["llamaindex"] },
|
|
159
|
+
{ id: "huggingface", parents: ["ml"], synonyms: ["hugging-face"] },
|
|
160
|
+
{ id: "openai", parents: ["llm"] },
|
|
161
|
+
{ id: "anthropic", parents: ["llm"], synonyms: ["claude"] },
|
|
162
|
+
{ id: "rag", parents: ["llm"], synonyms: ["retrieval-augmented-generation"] },
|
|
163
|
+
{ id: "mlops", parents: ["ml"], related: [{ to: "devops", w: 0.4 }] },
|
|
164
|
+
// ── Mobile ──────────────────────────────────────────────────────────────────
|
|
165
|
+
{ id: "mobile", related: [{ to: "ios", w: 0.5 }, { to: "android", w: 0.5 }] },
|
|
166
|
+
{ id: "ios", parents: ["mobile", "swift"], related: [{ to: "android", w: 0.4 }] },
|
|
167
|
+
{ id: "android", parents: ["mobile"], related: [{ to: "kotlin", w: 0.4 }] },
|
|
168
|
+
{ id: "swiftui", parents: ["ios", "swift"] },
|
|
169
|
+
{ id: "react-native", parents: ["mobile", "react"], synonyms: ["reactnative"], related: [{ to: "flutter", w: 0.4 }, { to: "expo", w: 0.6 }] },
|
|
170
|
+
{ id: "flutter", parents: ["mobile", "dart"] },
|
|
171
|
+
{ id: "expo", parents: ["react-native"] },
|
|
172
|
+
{ id: "kotlin-multiplatform", parents: ["mobile", "kotlin"], synonyms: ["kmp"] },
|
|
173
|
+
// ── Domains / capabilities ──────────────────────────────────────────────────
|
|
174
|
+
{ id: "frontend", related: [{ to: "react", w: 0.4 }, { to: "css", w: 0.3 }] },
|
|
175
|
+
{ id: "backend", related: [{ to: "api-design", w: 0.4 }, { to: "microservices", w: 0.4 }] },
|
|
176
|
+
{ id: "devops", related: [{ to: "kubernetes", w: 0.4 }, { to: "ci-cd", w: 0.4 }, { to: "docker", w: 0.4 }] },
|
|
177
|
+
{ id: "authentication", synonyms: ["auth", "jwt", "saml", "passport", "auth0", "clerk", "nextauth"], related: [{ to: "oauth", w: 0.6 }, { to: "security", w: 0.5 }] },
|
|
178
|
+
{ id: "oauth", parents: ["authentication"], synonyms: ["oauth2", "oidc"], related: [{ to: "security", w: 0.4 }] },
|
|
179
|
+
{ id: "security", related: [{ to: "authentication", w: 0.5 }] },
|
|
180
|
+
{ id: "payments", synonyms: ["stripe", "braintree", "paddle", "lemonsqueezy", "@stripe/stripe-js"], related: [{ to: "billing", w: 0.6 }] },
|
|
181
|
+
{ id: "billing", synonyms: ["recurly", "chargebee"] },
|
|
182
|
+
{ id: "api-design", synonyms: ["rest", "restful", "rest-api"], related: [{ to: "graphql", w: 0.4 }, { to: "grpc", w: 0.4 }, { to: "backend", w: 0.4 }] },
|
|
183
|
+
{ id: "graphql", synonyms: ["gql"], related: [{ to: "trpc", w: 0.4 }] },
|
|
184
|
+
{ id: "trpc", related: [{ to: "graphql", w: 0.4 }] },
|
|
185
|
+
{ id: "grpc", synonyms: ["grpc-web"], related: [{ to: "microservices", w: 0.3 }] },
|
|
186
|
+
{ id: "microservices" },
|
|
187
|
+
{ id: "websockets", synonyms: ["ws", "socket.io"], related: [{ to: "realtime", w: 0.6 }] },
|
|
188
|
+
{ id: "realtime", synonyms: ["real-time"] },
|
|
189
|
+
{ id: "message-queue", synonyms: ["mq"] },
|
|
190
|
+
{ id: "caching", synonyms: ["cache"] },
|
|
191
|
+
{ id: "search", synonyms: ["full-text-search"] },
|
|
192
|
+
{ id: "observability", synonyms: ["o11y"], related: [{ to: "monitoring", w: 0.6 }] },
|
|
193
|
+
{ id: "monitoring", related: [{ to: "prometheus", w: 0.4 }] },
|
|
194
|
+
{ id: "testing", related: [{ to: "unit-testing", w: 0.5 }, { to: "e2e-testing", w: 0.5 }] },
|
|
195
|
+
{ id: "unit-testing", parents: ["testing"] },
|
|
196
|
+
{ id: "e2e-testing", parents: ["testing"], synonyms: ["e2e", "end-to-end-testing"] },
|
|
197
|
+
{ id: "jest", parents: ["testing"], related: [{ to: "vitest", w: 0.6 }, { to: "mocha", w: 0.5 }] },
|
|
198
|
+
{ id: "vitest", parents: ["testing"], related: [{ to: "jest", w: 0.6 }] },
|
|
199
|
+
{ id: "playwright", parents: ["e2e-testing"], related: [{ to: "cypress", w: 0.6 }] },
|
|
200
|
+
{ id: "cypress", parents: ["e2e-testing"] },
|
|
201
|
+
{ id: "mocha", parents: ["testing"] },
|
|
202
|
+
{ id: "pytest", parents: ["testing", "python"] },
|
|
203
|
+
{ id: "accessibility", synonyms: ["a11y"] },
|
|
204
|
+
{ id: "seo" },
|
|
205
|
+
{ id: "performance", synonyms: ["perf", "web-performance"] }
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ../../packages/core/src/vocab/closure.ts
|
|
211
|
+
function round3(n) {
|
|
212
|
+
return Math.round(n * 1e3) / 1e3;
|
|
213
|
+
}
|
|
214
|
+
function validateGraph(nodes) {
|
|
215
|
+
const ids = /* @__PURE__ */ new Set();
|
|
216
|
+
for (const n of nodes) {
|
|
217
|
+
if (ids.has(n.id)) throw new Error(`vocab: duplicate id "${n.id}"`);
|
|
218
|
+
ids.add(n.id);
|
|
219
|
+
}
|
|
220
|
+
const seenAlias = /* @__PURE__ */ new Map();
|
|
221
|
+
for (const n of nodes) {
|
|
222
|
+
for (const p of n.parents ?? []) {
|
|
223
|
+
if (p === n.id) throw new Error(`vocab: "${n.id}" lists itself as a parent`);
|
|
224
|
+
if (!ids.has(p)) throw new Error(`vocab: "${n.id}" parent "${p}" is not a defined id`);
|
|
225
|
+
}
|
|
226
|
+
for (const e of n.related ?? []) {
|
|
227
|
+
if (e.to === n.id) throw new Error(`vocab: "${n.id}" relates to itself`);
|
|
228
|
+
if (!ids.has(e.to)) throw new Error(`vocab: "${n.id}" related "${e.to}" is not a defined id`);
|
|
229
|
+
if (!(e.w > 0 && e.w <= 1)) throw new Error(`vocab: "${n.id}"\u2192"${e.to}" weight ${e.w} out of (0,1]`);
|
|
230
|
+
}
|
|
231
|
+
for (const s of n.synonyms ?? []) {
|
|
232
|
+
const alias = s.toLowerCase();
|
|
233
|
+
if (ids.has(alias)) throw new Error(`vocab: synonym "${alias}" collides with a canonical id`);
|
|
234
|
+
const prev = seenAlias.get(alias);
|
|
235
|
+
if (prev && prev !== n.id) throw new Error(`vocab: synonym "${alias}" maps to both "${prev}" and "${n.id}"`);
|
|
236
|
+
seenAlias.set(alias, n.id);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
240
|
+
const done = /* @__PURE__ */ new Set();
|
|
241
|
+
const parentMap = new Map(nodes.map((n) => [n.id, n.parents ?? []]));
|
|
242
|
+
const walk = (id, path) => {
|
|
243
|
+
if (done.has(id)) return;
|
|
244
|
+
if (visiting.has(id)) throw new Error(`vocab: parent cycle ${[...path, id].join(" \u2192 ")}`);
|
|
245
|
+
visiting.add(id);
|
|
246
|
+
for (const p of parentMap.get(id) ?? []) walk(p, [...path, id]);
|
|
247
|
+
visiting.delete(id);
|
|
248
|
+
done.add(id);
|
|
249
|
+
};
|
|
250
|
+
for (const n of nodes) walk(n.id, []);
|
|
251
|
+
}
|
|
252
|
+
function buildAdjacency(nodes) {
|
|
253
|
+
const adj = /* @__PURE__ */ new Map();
|
|
254
|
+
const add = (from, to, w) => {
|
|
255
|
+
let m = adj.get(from);
|
|
256
|
+
if (!m) adj.set(from, m = /* @__PURE__ */ new Map());
|
|
257
|
+
if (w > (m.get(to) ?? 0)) m.set(to, w);
|
|
258
|
+
};
|
|
259
|
+
for (const n of nodes) {
|
|
260
|
+
for (const p of n.parents ?? []) {
|
|
261
|
+
add(n.id, p, PARENT_UP);
|
|
262
|
+
add(p, n.id, PARENT_DOWN);
|
|
263
|
+
}
|
|
264
|
+
for (const e of n.related ?? []) {
|
|
265
|
+
add(n.id, e.to, e.w);
|
|
266
|
+
add(e.to, n.id, e.w);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return adj;
|
|
270
|
+
}
|
|
271
|
+
function closureFrom(source, adj) {
|
|
272
|
+
const best = /* @__PURE__ */ new Map();
|
|
273
|
+
for (const [t, w] of adj.get(source) ?? []) {
|
|
274
|
+
if (w >= DECAY_FLOOR) best.set(t, { w: round3(w), via: t });
|
|
275
|
+
}
|
|
276
|
+
const settled = /* @__PURE__ */ new Set([source]);
|
|
277
|
+
while (true) {
|
|
278
|
+
let u;
|
|
279
|
+
let uw = 0;
|
|
280
|
+
for (const [t, e] of best) {
|
|
281
|
+
if (!settled.has(t) && e.w > uw) {
|
|
282
|
+
u = t;
|
|
283
|
+
uw = e.w;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (!u) break;
|
|
287
|
+
settled.add(u);
|
|
288
|
+
const via = best.get(u).via;
|
|
289
|
+
for (const [t, we] of adj.get(u) ?? []) {
|
|
290
|
+
if (settled.has(t)) continue;
|
|
291
|
+
const cand = round3(uw * we);
|
|
292
|
+
if (cand >= DECAY_FLOOR && cand > (best.get(t)?.w ?? 0)) {
|
|
293
|
+
best.set(t, { w: cand, via });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
best.delete(source);
|
|
298
|
+
return best;
|
|
299
|
+
}
|
|
300
|
+
function buildGraph(nodes) {
|
|
301
|
+
validateGraph(nodes);
|
|
302
|
+
const ids = new Set(nodes.map((n) => n.id));
|
|
303
|
+
const synonyms = /* @__PURE__ */ new Map();
|
|
304
|
+
for (const n of nodes) {
|
|
305
|
+
for (const s of n.synonyms ?? []) synonyms.set(s.toLowerCase(), n.id);
|
|
306
|
+
}
|
|
307
|
+
const adj = buildAdjacency(nodes);
|
|
308
|
+
const closure = /* @__PURE__ */ new Map();
|
|
309
|
+
for (const n of nodes) closure.set(n.id, closureFrom(n.id, adj));
|
|
310
|
+
return { ids, synonyms, closure };
|
|
311
|
+
}
|
|
312
|
+
var PARENT_UP, PARENT_DOWN, DECAY_FLOOR;
|
|
313
|
+
var init_closure = __esm({
|
|
314
|
+
"../../packages/core/src/vocab/closure.ts"() {
|
|
315
|
+
"use strict";
|
|
316
|
+
PARENT_UP = 0.6;
|
|
317
|
+
PARENT_DOWN = 0.35;
|
|
318
|
+
DECAY_FLOOR = 0.25;
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// ../../packages/core/src/vocab/types.ts
|
|
323
|
+
var init_types2 = __esm({
|
|
324
|
+
"../../packages/core/src/vocab/types.ts"() {
|
|
325
|
+
"use strict";
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// ../../packages/core/src/vocab/index.ts
|
|
330
|
+
function normalize(tokens) {
|
|
331
|
+
const result = /* @__PURE__ */ new Set();
|
|
332
|
+
for (const raw of tokens) {
|
|
333
|
+
const lower = raw.toLowerCase().trim();
|
|
334
|
+
if (GRAPH.ids.has(lower)) {
|
|
335
|
+
result.add(lower);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const mapped = GRAPH.synonyms.get(lower);
|
|
339
|
+
if (mapped) result.add(mapped);
|
|
340
|
+
}
|
|
341
|
+
return Array.from(result);
|
|
342
|
+
}
|
|
343
|
+
function expandWeighted(tags, graph = GRAPH) {
|
|
344
|
+
const out = /* @__PURE__ */ new Map();
|
|
345
|
+
const put = (tag, weight, via) => {
|
|
346
|
+
const ex = out.get(tag);
|
|
347
|
+
if (!ex || weight > ex.weight) out.set(tag, { tag, weight, via });
|
|
348
|
+
};
|
|
349
|
+
for (const t of tags) {
|
|
350
|
+
put(t, 1, t);
|
|
351
|
+
const near = graph.closure.get(t);
|
|
352
|
+
if (near) for (const [n, edge] of near) put(n, edge.w, t);
|
|
353
|
+
}
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
var GRAPH, VOCABULARY, SYNONYMS;
|
|
357
|
+
var init_vocab = __esm({
|
|
358
|
+
"../../packages/core/src/vocab/index.ts"() {
|
|
359
|
+
"use strict";
|
|
360
|
+
init_graph_data();
|
|
361
|
+
init_closure();
|
|
362
|
+
init_types2();
|
|
363
|
+
init_closure();
|
|
364
|
+
init_graph_data();
|
|
365
|
+
GRAPH = buildGraph(VOCAB_NODES);
|
|
366
|
+
VOCABULARY = [...GRAPH.ids];
|
|
367
|
+
SYNONYMS = Object.fromEntries(GRAPH.synonyms);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// ../../packages/core/src/vocabulary.ts
|
|
372
|
+
var init_vocabulary = __esm({
|
|
373
|
+
"../../packages/core/src/vocabulary.ts"() {
|
|
374
|
+
"use strict";
|
|
375
|
+
init_vocab();
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// ../../packages/core/src/matcher.ts
|
|
380
|
+
function computeIdf(jobs) {
|
|
381
|
+
const docFreq = /* @__PURE__ */ new Map();
|
|
382
|
+
const N = jobs.length;
|
|
383
|
+
for (const job of jobs) {
|
|
384
|
+
const unique = new Set(job.tags);
|
|
385
|
+
for (const tag of unique) {
|
|
386
|
+
docFreq.set(tag, (docFreq.get(tag) ?? 0) + 1);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const idf = /* @__PURE__ */ new Map();
|
|
390
|
+
for (const [tag, df] of docFreq) {
|
|
391
|
+
idf.set(tag, Math.log((N + 1) / (df + 1)) + 1);
|
|
392
|
+
}
|
|
393
|
+
return idf;
|
|
394
|
+
}
|
|
395
|
+
function inferSeniority(title) {
|
|
396
|
+
if (!ENG_TITLE.test(title)) return void 0;
|
|
397
|
+
for (const [re, level] of SENIORITY_PATTERNS) {
|
|
398
|
+
if (re.test(title)) return level;
|
|
399
|
+
}
|
|
400
|
+
return void 0;
|
|
401
|
+
}
|
|
402
|
+
function seniorityScore(fp, job) {
|
|
403
|
+
if (!fp.seniorityBand) return 1;
|
|
404
|
+
const jobLevel = inferSeniority(job.title);
|
|
405
|
+
if (!jobLevel) return 0.85;
|
|
406
|
+
const wanted = SENIORITY_RANK[fp.seniorityBand] ?? 1;
|
|
407
|
+
const got = SENIORITY_RANK[jobLevel] ?? 1;
|
|
408
|
+
const delta = Math.abs(wanted - got);
|
|
409
|
+
if (delta === 0) return 1;
|
|
410
|
+
if (delta === 1) return 0.7;
|
|
411
|
+
return 0.4;
|
|
412
|
+
}
|
|
413
|
+
function recencyScore(postedAt, now) {
|
|
414
|
+
if (!postedAt) return 0.75;
|
|
415
|
+
const ageDays2 = (now - new Date(postedAt).getTime()) / 864e5;
|
|
416
|
+
if (ageDays2 < 7) return 1;
|
|
417
|
+
if (ageDays2 < 30) return 0.9;
|
|
418
|
+
if (ageDays2 < 90) return 0.75;
|
|
419
|
+
return 0.6;
|
|
420
|
+
}
|
|
421
|
+
function passesFilters(fp, job) {
|
|
422
|
+
const prefs = fp.prefs;
|
|
423
|
+
if (!prefs) return true;
|
|
424
|
+
if (prefs.remoteOnly && !job.remote) return false;
|
|
425
|
+
if (prefs.roleTypes && prefs.roleTypes.length > 0 && !prefs.roleTypes.includes(job.roleType)) {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
if (prefs.compFloorUsd !== void 0) {
|
|
429
|
+
if (job.compMax !== void 0 && job.compMax < prefs.compFloorUsd) return false;
|
|
430
|
+
}
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
function buildReason(details) {
|
|
434
|
+
if (details.length === 0) return "No direct skill overlap found.";
|
|
435
|
+
const render = (d) => !d.via || d.via === d.tag ? d.tag : `${d.via}\u2192${d.tag} (${d.weight})`;
|
|
436
|
+
const top = details.slice(0, 3).map(render);
|
|
437
|
+
const rest = details.length - top.length;
|
|
438
|
+
const listed = top.join(", ");
|
|
439
|
+
if (rest === 0) return `Matched on ${listed}.`;
|
|
440
|
+
return `Matched on ${listed} + ${rest} more skill${rest > 1 ? "s" : ""}.`;
|
|
441
|
+
}
|
|
442
|
+
function harmonicMean(a, b) {
|
|
443
|
+
if (a <= 0 || b <= 0) return 0;
|
|
444
|
+
return 2 * a * b / (a + b);
|
|
445
|
+
}
|
|
446
|
+
function match(fp, jobs, limit = 5, now = Date.now()) {
|
|
447
|
+
const idf = computeIdf(jobs);
|
|
448
|
+
const idfOf = (t) => idf.get(t) ?? 0;
|
|
449
|
+
const expanded = expandWeighted(fp.skillTags);
|
|
450
|
+
const maxDevScore = fp.skillTags.reduce((acc, t) => acc + idfOf(t), 0);
|
|
451
|
+
const candidates = jobs.filter((j) => passesFilters(fp, j));
|
|
452
|
+
const scored = candidates.map((job) => {
|
|
453
|
+
const details = [];
|
|
454
|
+
let jobMatchScore = 0;
|
|
455
|
+
let jobMaxScore = 0;
|
|
456
|
+
const devCovByTag = /* @__PURE__ */ new Map();
|
|
457
|
+
for (const tag of job.tags) {
|
|
458
|
+
const w = idfOf(tag);
|
|
459
|
+
jobMaxScore += w;
|
|
460
|
+
const hit = expanded.get(tag);
|
|
461
|
+
if (hit) {
|
|
462
|
+
const credit = Math.pow(hit.weight, SHARPEN);
|
|
463
|
+
jobMatchScore += w * credit;
|
|
464
|
+
details.push({ tag, weight: hit.weight, via: hit.via });
|
|
465
|
+
if (credit > (devCovByTag.get(hit.via) ?? 0)) devCovByTag.set(hit.via, credit);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
let devScore = 0;
|
|
469
|
+
for (const t of fp.skillTags) devScore += idfOf(t) * (devCovByTag.get(t) ?? 0);
|
|
470
|
+
const devCov = maxDevScore > 0 ? Math.min(1, devScore / maxDevScore) : 0;
|
|
471
|
+
const jobCov = jobMaxScore > 0 ? Math.min(1, jobMatchScore / jobMaxScore) : 0;
|
|
472
|
+
const tagComponent = harmonicMean(devCov, jobCov);
|
|
473
|
+
if (tagComponent === 0) return null;
|
|
474
|
+
details.sort((a, b) => idfOf(b.tag) * b.weight - idfOf(a.tag) * a.weight);
|
|
475
|
+
const sScore = seniorityScore(fp, job);
|
|
476
|
+
const rScore = recencyScore(job.postedAt, now);
|
|
477
|
+
const score = tagComponent * 0.6 + sScore * 0.25 + rScore * 0.15;
|
|
478
|
+
const matchedTags = [...new Set(details.map((d) => d.via ?? d.tag))];
|
|
479
|
+
return {
|
|
480
|
+
job,
|
|
481
|
+
score: Math.round(score * 1e3) / 1e3,
|
|
482
|
+
matchedTags,
|
|
483
|
+
matchDetails: details,
|
|
484
|
+
reason: buildReason(details)
|
|
485
|
+
};
|
|
486
|
+
});
|
|
487
|
+
return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
488
|
+
}
|
|
489
|
+
function matchOne(fp, job) {
|
|
490
|
+
const results = match(fp, [job], 1);
|
|
491
|
+
return results.length > 0 ? results[0] : null;
|
|
492
|
+
}
|
|
493
|
+
var MIN_SCORE, SHARPEN, SENIORITY_RANK, SENIORITY_PATTERNS, ENG_TITLE;
|
|
494
|
+
var init_matcher = __esm({
|
|
495
|
+
"../../packages/core/src/matcher.ts"() {
|
|
496
|
+
"use strict";
|
|
497
|
+
init_vocabulary();
|
|
498
|
+
MIN_SCORE = 0.15;
|
|
499
|
+
SHARPEN = 1.6;
|
|
500
|
+
SENIORITY_RANK = {
|
|
501
|
+
junior: 0,
|
|
502
|
+
mid: 1,
|
|
503
|
+
senior: 2,
|
|
504
|
+
staff: 3
|
|
505
|
+
};
|
|
506
|
+
SENIORITY_PATTERNS = [
|
|
507
|
+
[/\bstaff\b|\bprincipal\b|\bdistinguished\b/i, "staff"],
|
|
508
|
+
[/\bsenior\b|\bsr\.?\b/i, "senior"],
|
|
509
|
+
[/\bjunior\b|\bjr\.?\b|\bentry[\s-]?level\b/i, "junior"],
|
|
510
|
+
[/\bmid[\s-]?level\b|\bmid\b/i, "mid"]
|
|
511
|
+
];
|
|
512
|
+
ENG_TITLE = /\b(engineer|engineering|developer|dev|swe|sde|programmer|architect)\b/i;
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// ../../packages/core/src/feeds/greenhouse.ts
|
|
517
|
+
function tokenize(text) {
|
|
518
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
519
|
+
}
|
|
520
|
+
function extractTags(job) {
|
|
521
|
+
const texts = [
|
|
522
|
+
job.title,
|
|
523
|
+
...(job.departments ?? []).map((d) => d.name),
|
|
524
|
+
job.location?.name ?? "",
|
|
525
|
+
...(job.offices ?? []).map((o) => o.name),
|
|
526
|
+
// mine the full HTML description for additional signal when present
|
|
527
|
+
...job.content ? [job.content.replace(/<[^>]*>/g, " ")] : []
|
|
528
|
+
].filter(Boolean);
|
|
529
|
+
const tokens = texts.flatMap(tokenize);
|
|
530
|
+
return normalize(tokens);
|
|
531
|
+
}
|
|
532
|
+
function inferRemote(location) {
|
|
533
|
+
const l = location.toLowerCase();
|
|
534
|
+
return l.includes("remote") || l.includes("anywhere") || l.includes("worldwide");
|
|
535
|
+
}
|
|
536
|
+
async function fetchSlug(slug) {
|
|
537
|
+
const url = `https://boards-api.greenhouse.io/v1/boards/${slug}/jobs?content=true`;
|
|
538
|
+
let res;
|
|
539
|
+
try {
|
|
540
|
+
res = await fetch(url, { headers: { Accept: "application/json" } });
|
|
541
|
+
} catch (err) {
|
|
542
|
+
console.warn(`[greenhouse] ${slug}: network error \u2014`, err);
|
|
543
|
+
return [];
|
|
544
|
+
}
|
|
545
|
+
if (!res.ok) {
|
|
546
|
+
console.warn(`[greenhouse] ${slug}: HTTP ${res.status} ${res.statusText}`);
|
|
547
|
+
return [];
|
|
548
|
+
}
|
|
549
|
+
let data;
|
|
550
|
+
try {
|
|
551
|
+
data = await res.json();
|
|
552
|
+
} catch (err) {
|
|
553
|
+
console.warn(`[greenhouse] ${slug}: JSON parse error \u2014`, err);
|
|
554
|
+
return [];
|
|
555
|
+
}
|
|
556
|
+
const jobs = data.jobs ?? [];
|
|
557
|
+
if (jobs.length === 0) {
|
|
558
|
+
console.warn(`[greenhouse] ${slug}: 0 jobs returned (board may be private or slug invalid)`);
|
|
559
|
+
} else {
|
|
560
|
+
console.info(`[greenhouse] ${slug}: ${jobs.length} jobs`);
|
|
561
|
+
}
|
|
562
|
+
return jobs.map((j) => ({
|
|
563
|
+
id: `greenhouse:${j.id}`,
|
|
564
|
+
source: "greenhouse",
|
|
565
|
+
title: j.title,
|
|
566
|
+
company: slug,
|
|
567
|
+
url: j.absolute_url,
|
|
568
|
+
remote: inferRemote(j.location?.name ?? ""),
|
|
569
|
+
location: j.location?.name,
|
|
570
|
+
tags: extractTags(j),
|
|
571
|
+
roleType: "full_time",
|
|
572
|
+
postedAt: j.updated_at,
|
|
573
|
+
applyMode: "direct",
|
|
574
|
+
raw: j
|
|
575
|
+
}));
|
|
576
|
+
}
|
|
577
|
+
var FALLBACK_SLUGS, greenhouse;
|
|
578
|
+
var init_greenhouse = __esm({
|
|
579
|
+
"../../packages/core/src/feeds/greenhouse.ts"() {
|
|
580
|
+
"use strict";
|
|
581
|
+
init_vocabulary();
|
|
582
|
+
FALLBACK_SLUGS = [
|
|
583
|
+
"stripe",
|
|
584
|
+
"linear",
|
|
585
|
+
"vercel",
|
|
586
|
+
"ramp",
|
|
587
|
+
"notion",
|
|
588
|
+
"airbnb",
|
|
589
|
+
"anthropic",
|
|
590
|
+
"figma",
|
|
591
|
+
"discord",
|
|
592
|
+
"brex",
|
|
593
|
+
"mercury",
|
|
594
|
+
"retool",
|
|
595
|
+
"vanta",
|
|
596
|
+
"plaid",
|
|
597
|
+
"gusto",
|
|
598
|
+
"scale",
|
|
599
|
+
"databricks",
|
|
600
|
+
"coinbase",
|
|
601
|
+
"robinhood",
|
|
602
|
+
"doordash"
|
|
603
|
+
];
|
|
604
|
+
greenhouse = {
|
|
605
|
+
source: "greenhouse",
|
|
606
|
+
async fetch(opts) {
|
|
607
|
+
const slugs = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : FALLBACK_SLUGS;
|
|
608
|
+
console.info(`[greenhouse] fetching ${slugs.length} slugs: ${slugs.join(", ")}`);
|
|
609
|
+
const results = await Promise.allSettled(slugs.map(fetchSlug));
|
|
610
|
+
const jobs = [];
|
|
611
|
+
let failures = 0;
|
|
612
|
+
for (const r of results) {
|
|
613
|
+
if (r.status === "fulfilled") {
|
|
614
|
+
jobs.push(...r.value);
|
|
615
|
+
} else {
|
|
616
|
+
failures++;
|
|
617
|
+
console.warn(`[greenhouse] slug fetch rejected:`, r.reason);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
console.info(`[greenhouse] total: ${jobs.length} jobs, ${failures} slug failures`);
|
|
621
|
+
return jobs;
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// ../../packages/core/src/feeds/ashby.ts
|
|
628
|
+
function tokenize2(text) {
|
|
629
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
630
|
+
}
|
|
631
|
+
function extractTags2(job) {
|
|
632
|
+
const texts = [
|
|
633
|
+
job.title,
|
|
634
|
+
job.teamName ?? "",
|
|
635
|
+
job.locationName ?? "",
|
|
636
|
+
...(job.secondaryLocations ?? []).map((l) => l.locationName ?? "")
|
|
637
|
+
];
|
|
638
|
+
return normalize(texts.flatMap(tokenize2));
|
|
639
|
+
}
|
|
640
|
+
function mapEmploymentType(raw) {
|
|
641
|
+
if (!raw) return "full_time";
|
|
642
|
+
const lower = raw.toLowerCase();
|
|
643
|
+
if (lower.includes("contract") || lower.includes("contractor")) return "contract";
|
|
644
|
+
if (lower.includes("freelance")) return "freelance";
|
|
645
|
+
return "full_time";
|
|
646
|
+
}
|
|
647
|
+
function inferRemote2(job) {
|
|
648
|
+
if (job.isRemote === true) return true;
|
|
649
|
+
const loc = (job.locationName ?? "").toLowerCase();
|
|
650
|
+
return loc.includes("remote") || loc.includes("anywhere");
|
|
651
|
+
}
|
|
652
|
+
async function fetchSlug2(slug) {
|
|
653
|
+
const url = `https://api.ashbyhq.com/posting-api/job-board/${slug}`;
|
|
654
|
+
const res = await fetch(url, {
|
|
655
|
+
headers: { Accept: "application/json" }
|
|
656
|
+
});
|
|
657
|
+
if (!res.ok) {
|
|
658
|
+
throw new Error(`Ashby ${slug}: HTTP ${res.status}`);
|
|
659
|
+
}
|
|
660
|
+
const data = await res.json();
|
|
661
|
+
return (data.jobs ?? []).map((j) => {
|
|
662
|
+
const comp = j.compensation;
|
|
663
|
+
return {
|
|
664
|
+
id: `ashby:${j.id}`,
|
|
665
|
+
source: "ashby",
|
|
666
|
+
title: j.title,
|
|
667
|
+
company: slug,
|
|
668
|
+
url: j.applyUrl ?? `https://jobs.ashbyhq.com/${slug}/${j.id}`,
|
|
669
|
+
remote: inferRemote2(j),
|
|
670
|
+
location: j.locationName,
|
|
671
|
+
compMin: comp?.minValue,
|
|
672
|
+
compMax: comp?.maxValue,
|
|
673
|
+
tags: extractTags2(j),
|
|
674
|
+
roleType: mapEmploymentType(j.employmentType),
|
|
675
|
+
postedAt: j.publishedDate,
|
|
676
|
+
applyMode: "direct",
|
|
677
|
+
raw: j
|
|
678
|
+
};
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
var ashby;
|
|
682
|
+
var init_ashby = __esm({
|
|
683
|
+
"../../packages/core/src/feeds/ashby.ts"() {
|
|
684
|
+
"use strict";
|
|
685
|
+
init_vocabulary();
|
|
686
|
+
ashby = {
|
|
687
|
+
source: "ashby",
|
|
688
|
+
async fetch(opts) {
|
|
689
|
+
const slugs = opts?.slugs ?? [];
|
|
690
|
+
const results = await Promise.allSettled(slugs.map(fetchSlug2));
|
|
691
|
+
const jobs = [];
|
|
692
|
+
for (const r of results) {
|
|
693
|
+
if (r.status === "fulfilled") jobs.push(...r.value);
|
|
694
|
+
}
|
|
695
|
+
return jobs;
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// ../../packages/core/src/feeds/lever.ts
|
|
702
|
+
function tokenize3(text) {
|
|
703
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
704
|
+
}
|
|
705
|
+
function extractTags3(p) {
|
|
706
|
+
const cat = p.categories ?? {};
|
|
707
|
+
const texts = [
|
|
708
|
+
p.text,
|
|
709
|
+
cat.team ?? "",
|
|
710
|
+
cat.department ?? "",
|
|
711
|
+
cat.location ?? "",
|
|
712
|
+
...cat.allLocations ?? [],
|
|
713
|
+
p.descriptionPlain ?? ""
|
|
714
|
+
];
|
|
715
|
+
return normalize(texts.flatMap(tokenize3));
|
|
716
|
+
}
|
|
717
|
+
function mapCommitment(raw) {
|
|
718
|
+
if (!raw) return "full_time";
|
|
719
|
+
const lower = raw.toLowerCase();
|
|
720
|
+
if (lower.includes("contract") || lower.includes("contractor")) return "contract";
|
|
721
|
+
if (lower.includes("freelance")) return "freelance";
|
|
722
|
+
return "full_time";
|
|
723
|
+
}
|
|
724
|
+
function inferRemote3(p) {
|
|
725
|
+
if ((p.workplaceType ?? "").toLowerCase() === "remote") return true;
|
|
726
|
+
const cat = p.categories ?? {};
|
|
727
|
+
const haystack = [cat.location ?? "", ...cat.allLocations ?? []].join(" ").toLowerCase();
|
|
728
|
+
return haystack.includes("remote") || haystack.includes("anywhere");
|
|
729
|
+
}
|
|
730
|
+
function toIso(ms) {
|
|
731
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return void 0;
|
|
732
|
+
try {
|
|
733
|
+
return new Date(ms).toISOString();
|
|
734
|
+
} catch {
|
|
735
|
+
return void 0;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
async function fetchSlug3(slug) {
|
|
739
|
+
const url = `https://api.lever.co/v0/postings/${slug}?mode=json`;
|
|
740
|
+
const res = await fetch(url, { headers: { Accept: "application/json" } });
|
|
741
|
+
if (!res.ok) {
|
|
742
|
+
throw new Error(`Lever ${slug}: HTTP ${res.status}`);
|
|
743
|
+
}
|
|
744
|
+
const data = await res.json();
|
|
745
|
+
const postings = Array.isArray(data) ? data : [];
|
|
746
|
+
if (postings.length === 0) {
|
|
747
|
+
console.warn(`[lever] ${slug}: 0 jobs returned (board may be private or slug invalid)`);
|
|
748
|
+
} else {
|
|
749
|
+
console.info(`[lever] ${slug}: ${postings.length} jobs`);
|
|
750
|
+
}
|
|
751
|
+
return postings.filter((p) => p && p.id && p.text).map((p) => ({
|
|
752
|
+
id: `lever:${p.id}`,
|
|
753
|
+
source: "lever",
|
|
754
|
+
title: p.text,
|
|
755
|
+
company: slug,
|
|
756
|
+
url: p.hostedUrl ?? p.applyUrl ?? `https://jobs.lever.co/${slug}/${p.id}`,
|
|
757
|
+
remote: inferRemote3(p),
|
|
758
|
+
location: p.categories?.location,
|
|
759
|
+
tags: extractTags3(p),
|
|
760
|
+
roleType: mapCommitment(p.categories?.commitment),
|
|
761
|
+
postedAt: toIso(p.createdAt),
|
|
762
|
+
applyMode: "direct",
|
|
763
|
+
raw: p
|
|
764
|
+
}));
|
|
765
|
+
}
|
|
766
|
+
var lever;
|
|
767
|
+
var init_lever = __esm({
|
|
768
|
+
"../../packages/core/src/feeds/lever.ts"() {
|
|
769
|
+
"use strict";
|
|
770
|
+
init_vocabulary();
|
|
771
|
+
lever = {
|
|
772
|
+
source: "lever",
|
|
773
|
+
async fetch(opts) {
|
|
774
|
+
const slugs = opts?.slugs ?? [];
|
|
775
|
+
const results = await Promise.allSettled(slugs.map(fetchSlug3));
|
|
776
|
+
const jobs = [];
|
|
777
|
+
for (const r of results) {
|
|
778
|
+
if (r.status === "fulfilled") jobs.push(...r.value);
|
|
779
|
+
else console.warn("[lever] slug fetch rejected:", r.reason);
|
|
780
|
+
}
|
|
781
|
+
return jobs;
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// ../../packages/core/src/feeds/himalayas.ts
|
|
788
|
+
function tokenize4(text) {
|
|
789
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
790
|
+
}
|
|
791
|
+
function extractTags4(job) {
|
|
792
|
+
const texts = [
|
|
793
|
+
job.title,
|
|
794
|
+
...job.tags ?? []
|
|
795
|
+
];
|
|
796
|
+
return normalize(texts.flatMap(tokenize4));
|
|
797
|
+
}
|
|
798
|
+
function mapJobType(raw) {
|
|
799
|
+
if (!raw) return "full_time";
|
|
800
|
+
const lower = raw.toLowerCase();
|
|
801
|
+
if (lower.includes("contract")) return "contract";
|
|
802
|
+
if (lower.includes("freelance")) return "freelance";
|
|
803
|
+
return "full_time";
|
|
804
|
+
}
|
|
805
|
+
function buildUrl(job) {
|
|
806
|
+
if (job.applicationUrl) return job.applicationUrl;
|
|
807
|
+
if (job.url) return job.url;
|
|
808
|
+
const slug = job.slug ?? job.id ?? "unknown";
|
|
809
|
+
return `https://himalayas.app/jobs/${slug}`;
|
|
810
|
+
}
|
|
811
|
+
function buildId(job) {
|
|
812
|
+
return `himalayas:${job.id ?? job.slug ?? job.title}`;
|
|
813
|
+
}
|
|
814
|
+
var himalayas;
|
|
815
|
+
var init_himalayas = __esm({
|
|
816
|
+
"../../packages/core/src/feeds/himalayas.ts"() {
|
|
817
|
+
"use strict";
|
|
818
|
+
init_vocabulary();
|
|
819
|
+
himalayas = {
|
|
820
|
+
source: "himalayas",
|
|
821
|
+
async fetch(opts) {
|
|
822
|
+
const limit = opts?.limit ?? 100;
|
|
823
|
+
const url = `https://himalayas.app/jobs/api?limit=${limit}`;
|
|
824
|
+
const res = await fetch(url, {
|
|
825
|
+
headers: { Accept: "application/json" }
|
|
826
|
+
});
|
|
827
|
+
if (!res.ok) {
|
|
828
|
+
throw new Error(`Himalayas: HTTP ${res.status}`);
|
|
829
|
+
}
|
|
830
|
+
const data = await res.json();
|
|
831
|
+
return (data.jobs ?? []).map((j) => ({
|
|
832
|
+
id: buildId(j),
|
|
833
|
+
source: "himalayas",
|
|
834
|
+
title: j.title,
|
|
835
|
+
company: j.companyName ?? j.companySlug ?? "unknown",
|
|
836
|
+
url: buildUrl(j),
|
|
837
|
+
// Himalayas is a remote-only board
|
|
838
|
+
remote: true,
|
|
839
|
+
location: (j.locationRestrictions ?? []).join(", ") || "Remote",
|
|
840
|
+
compMin: j.salaryMin,
|
|
841
|
+
compMax: j.salaryMax,
|
|
842
|
+
tags: extractTags4(j),
|
|
843
|
+
roleType: mapJobType(j.jobType),
|
|
844
|
+
postedAt: j.pubDate ?? j.createdAt,
|
|
845
|
+
applyMode: "direct",
|
|
846
|
+
raw: j
|
|
847
|
+
}));
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
// ../../packages/core/src/feeds/entities.ts
|
|
854
|
+
function fromCodePoint(cp) {
|
|
855
|
+
if (!Number.isFinite(cp) || cp < 0 || cp > 1114111) return "";
|
|
856
|
+
try {
|
|
857
|
+
return String.fromCodePoint(cp);
|
|
858
|
+
} catch {
|
|
859
|
+
return "";
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function decodeEntities(input) {
|
|
863
|
+
if (!input || !input.includes("&")) return input;
|
|
864
|
+
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, "&");
|
|
865
|
+
}
|
|
866
|
+
var NAMED;
|
|
867
|
+
var init_entities = __esm({
|
|
868
|
+
"../../packages/core/src/feeds/entities.ts"() {
|
|
869
|
+
"use strict";
|
|
870
|
+
NAMED = {
|
|
871
|
+
lt: "<",
|
|
872
|
+
gt: ">",
|
|
873
|
+
quot: '"',
|
|
874
|
+
apos: "'",
|
|
875
|
+
nbsp: " "
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
// ../../packages/core/src/feeds/wwr.ts
|
|
881
|
+
function tokenize5(text) {
|
|
882
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
883
|
+
}
|
|
884
|
+
function stripHtml(html) {
|
|
885
|
+
return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
886
|
+
}
|
|
887
|
+
function inferRoleType(category) {
|
|
888
|
+
const lower = category.toLowerCase();
|
|
889
|
+
if (lower.includes("contract")) return "contract";
|
|
890
|
+
if (lower.includes("freelance")) return "freelance";
|
|
891
|
+
return "full_time";
|
|
892
|
+
}
|
|
893
|
+
function extractId(link) {
|
|
894
|
+
const match2 = link.match(/\/opening\/([^/\s]+)/);
|
|
895
|
+
return `wwr:${match2?.[1] ?? encodeURIComponent(link)}`;
|
|
896
|
+
}
|
|
897
|
+
function parseRss(xml) {
|
|
898
|
+
const items = [];
|
|
899
|
+
const itemBlocks = xml.match(/<item>([\s\S]*?)<\/item>/g) ?? [];
|
|
900
|
+
for (const block of itemBlocks) {
|
|
901
|
+
const get = (tag) => {
|
|
902
|
+
const cdataMatch = block.match(new RegExp(`<${tag}[^>]*><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/${tag}>`, "i"));
|
|
903
|
+
if (cdataMatch) return decodeEntities(cdataMatch[1].trim());
|
|
904
|
+
const plainMatch = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i"));
|
|
905
|
+
return decodeEntities(plainMatch?.[1].trim() ?? "");
|
|
906
|
+
};
|
|
907
|
+
const rawTitle = get("title");
|
|
908
|
+
const colonIdx = rawTitle.indexOf(":");
|
|
909
|
+
const company = colonIdx !== -1 ? rawTitle.slice(0, colonIdx).trim() : "Unknown";
|
|
910
|
+
const titleAfterColon = colonIdx !== -1 ? rawTitle.slice(colonIdx + 1).trim() : rawTitle;
|
|
911
|
+
const title = titleAfterColon.replace(/\s*\([^)]*\)\s*$/, "").trim();
|
|
912
|
+
items.push({
|
|
913
|
+
title,
|
|
914
|
+
link: get("link") || get("guid"),
|
|
915
|
+
pubDate: get("pubDate"),
|
|
916
|
+
category: get("category"),
|
|
917
|
+
description: get("description"),
|
|
918
|
+
company
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
return items;
|
|
922
|
+
}
|
|
923
|
+
function extractTags5(item) {
|
|
924
|
+
const text = [item.title, item.category, stripHtml(item.description)].join(" ");
|
|
925
|
+
return normalize(tokenize5(text));
|
|
926
|
+
}
|
|
927
|
+
var WWR_RSS_URL, wwr;
|
|
928
|
+
var init_wwr = __esm({
|
|
929
|
+
"../../packages/core/src/feeds/wwr.ts"() {
|
|
930
|
+
"use strict";
|
|
931
|
+
init_vocabulary();
|
|
932
|
+
init_entities();
|
|
933
|
+
WWR_RSS_URL = "https://weworkremotely.com/remote-jobs.rss";
|
|
934
|
+
wwr = {
|
|
935
|
+
source: "wwr",
|
|
936
|
+
async fetch(opts) {
|
|
937
|
+
const limit = opts?.limit ?? 200;
|
|
938
|
+
const res = await fetch(WWR_RSS_URL, {
|
|
939
|
+
headers: { Accept: "application/rss+xml, application/xml, text/xml" }
|
|
940
|
+
});
|
|
941
|
+
if (!res.ok) {
|
|
942
|
+
throw new Error(`WWR RSS: HTTP ${res.status}`);
|
|
943
|
+
}
|
|
944
|
+
const xml = await res.text();
|
|
945
|
+
const items = parseRss(xml).slice(0, limit);
|
|
946
|
+
return items.map((item) => ({
|
|
947
|
+
id: extractId(item.link),
|
|
948
|
+
source: "wwr",
|
|
949
|
+
title: item.title,
|
|
950
|
+
company: item.company,
|
|
951
|
+
url: item.link,
|
|
952
|
+
// WWR is a remote-only board
|
|
953
|
+
remote: true,
|
|
954
|
+
location: "Remote",
|
|
955
|
+
tags: extractTags5(item),
|
|
956
|
+
roleType: inferRoleType(item.category),
|
|
957
|
+
postedAt: item.pubDate ? new Date(item.pubDate).toISOString() : void 0,
|
|
958
|
+
applyMode: "direct",
|
|
959
|
+
raw: item
|
|
960
|
+
}));
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
// ../../packages/core/src/feeds/hn.ts
|
|
967
|
+
function tokenize6(text) {
|
|
968
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
969
|
+
}
|
|
970
|
+
function stripHtml2(html) {
|
|
971
|
+
return decodeEntities(html.replace(/<p>/gi, " ").replace(/<[^>]*>/g, "")).replace(/\s+/g, " ").trim();
|
|
972
|
+
}
|
|
973
|
+
function extractUrl(text) {
|
|
974
|
+
const match2 = text.match(/https?:\/\/[^\s<>"']+/);
|
|
975
|
+
return match2?.[0] ?? "";
|
|
976
|
+
}
|
|
977
|
+
function inferRemote4(text) {
|
|
978
|
+
const lower = text.toLowerCase();
|
|
979
|
+
return lower.includes("remote") || lower.includes("anywhere") || lower.includes("distributed");
|
|
980
|
+
}
|
|
981
|
+
function inferRoleType2(text) {
|
|
982
|
+
const lower = text.toLowerCase();
|
|
983
|
+
if (lower.includes("contract") || lower.includes("contractor")) return "contract";
|
|
984
|
+
if (lower.includes("freelance")) return "freelance";
|
|
985
|
+
return "full_time";
|
|
986
|
+
}
|
|
987
|
+
function parseComment(item) {
|
|
988
|
+
if (!item.text || item.text.trim().length < 20) return null;
|
|
989
|
+
const raw = stripHtml2(item.text);
|
|
990
|
+
if (!raw) return null;
|
|
991
|
+
const firstLine = raw.split(/\n/)[0];
|
|
992
|
+
const parts = firstLine.split("|").map((s) => s.trim());
|
|
993
|
+
const company = parts[0] ?? "Unknown";
|
|
994
|
+
const title = parts[1] ?? firstLine.slice(0, 80).trim();
|
|
995
|
+
const location = parts[2] ?? "";
|
|
996
|
+
if (company.toLowerCase().startsWith("note:") || company.toLowerCase().startsWith("ps:") || title.length < 3) {
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
const url = extractUrl(raw) || `https://news.ycombinator.com/item?id=${item.id}`;
|
|
1000
|
+
const tags = extractTags6(raw);
|
|
1001
|
+
if (tags.length === 0) return null;
|
|
1002
|
+
return {
|
|
1003
|
+
id: `hn:${item.id}`,
|
|
1004
|
+
source: "hn",
|
|
1005
|
+
title: title.slice(0, 120),
|
|
1006
|
+
company: company.slice(0, 80),
|
|
1007
|
+
url,
|
|
1008
|
+
remote: inferRemote4(raw),
|
|
1009
|
+
location: location || void 0,
|
|
1010
|
+
tags,
|
|
1011
|
+
roleType: inferRoleType2(raw),
|
|
1012
|
+
postedAt: item.created_at,
|
|
1013
|
+
applyMode: "direct",
|
|
1014
|
+
raw: item
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
function extractTags6(text) {
|
|
1018
|
+
return normalize(tokenize6(text));
|
|
1019
|
+
}
|
|
1020
|
+
var ALGOLIA_SEARCH, ALGOLIA_ITEMS, hn;
|
|
1021
|
+
var init_hn = __esm({
|
|
1022
|
+
"../../packages/core/src/feeds/hn.ts"() {
|
|
1023
|
+
"use strict";
|
|
1024
|
+
init_vocabulary();
|
|
1025
|
+
init_entities();
|
|
1026
|
+
ALGOLIA_SEARCH = "https://hn.algolia.com/api/v1/search?query=Ask+HN%3A+Who+is+Hiring%3F&tags=story,ask_hn&hitsPerPage=1";
|
|
1027
|
+
ALGOLIA_ITEMS = "https://hn.algolia.com/api/v1/items/";
|
|
1028
|
+
hn = {
|
|
1029
|
+
source: "hn",
|
|
1030
|
+
async fetch(opts) {
|
|
1031
|
+
const limit = opts?.limit ?? 150;
|
|
1032
|
+
const searchRes = await fetch(ALGOLIA_SEARCH, {
|
|
1033
|
+
headers: { Accept: "application/json" }
|
|
1034
|
+
});
|
|
1035
|
+
if (!searchRes.ok) {
|
|
1036
|
+
throw new Error(`HN Algolia search: HTTP ${searchRes.status}`);
|
|
1037
|
+
}
|
|
1038
|
+
const searchData = await searchRes.json();
|
|
1039
|
+
const story = searchData.hits[0];
|
|
1040
|
+
if (!story) {
|
|
1041
|
+
throw new Error('HN: No "Who is Hiring" story found');
|
|
1042
|
+
}
|
|
1043
|
+
const itemRes = await fetch(`${ALGOLIA_ITEMS}${story.objectID}`, {
|
|
1044
|
+
headers: { Accept: "application/json" }
|
|
1045
|
+
});
|
|
1046
|
+
if (!itemRes.ok) {
|
|
1047
|
+
throw new Error(`HN Algolia item ${story.objectID}: HTTP ${itemRes.status}`);
|
|
1048
|
+
}
|
|
1049
|
+
const storyItem = await itemRes.json();
|
|
1050
|
+
const comments = storyItem.children ?? [];
|
|
1051
|
+
const jobs = [];
|
|
1052
|
+
for (const comment of comments.slice(0, limit)) {
|
|
1053
|
+
const job = parseComment(comment);
|
|
1054
|
+
if (job) jobs.push(job);
|
|
1055
|
+
}
|
|
1056
|
+
return jobs;
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
// ../../packages/core/src/feeds/bounty-gate.ts
|
|
1063
|
+
function ageDays(createdAtIso) {
|
|
1064
|
+
const created = Date.parse(createdAtIso);
|
|
1065
|
+
if (!Number.isFinite(created)) return 0;
|
|
1066
|
+
return (Date.now() - created) / (1e3 * 60 * 60 * 24);
|
|
1067
|
+
}
|
|
1068
|
+
function passesMaturityGate(repo) {
|
|
1069
|
+
if (repo.archived || repo.disabled) return false;
|
|
1070
|
+
if (repo.stargazers < MIN_REPO_STARS) return false;
|
|
1071
|
+
if (ageDays(repo.createdAt) < MIN_REPO_AGE_DAYS) return false;
|
|
1072
|
+
return true;
|
|
1073
|
+
}
|
|
1074
|
+
var DEFAULT_BOUNTY_REPOS, MAX_BOUNTIES_PER_REPO, MIN_REPO_STARS, MIN_REPO_AGE_DAYS;
|
|
1075
|
+
var init_bounty_gate = __esm({
|
|
1076
|
+
"../../packages/core/src/feeds/bounty-gate.ts"() {
|
|
1077
|
+
"use strict";
|
|
1078
|
+
DEFAULT_BOUNTY_REPOS = [
|
|
1079
|
+
"tenstorrent/tt-metal",
|
|
1080
|
+
"sequelize/sequelize",
|
|
1081
|
+
"commaai/opendbc",
|
|
1082
|
+
"aragon/hack",
|
|
1083
|
+
"spacemeshos/app",
|
|
1084
|
+
"archestra-ai/archestra",
|
|
1085
|
+
"boundlessfi/boundless",
|
|
1086
|
+
"ucfopen/Obojobo",
|
|
1087
|
+
"widgetti/ipyvolume",
|
|
1088
|
+
"moorcheh-ai/memanto",
|
|
1089
|
+
"PrismarineJS/mineflayer"
|
|
1090
|
+
];
|
|
1091
|
+
MAX_BOUNTIES_PER_REPO = 10;
|
|
1092
|
+
MIN_REPO_STARS = 5;
|
|
1093
|
+
MIN_REPO_AGE_DAYS = 30;
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
// ../../packages/core/src/feeds/github-bounties.ts
|
|
1098
|
+
function authHeaders() {
|
|
1099
|
+
const token = process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"];
|
|
1100
|
+
const h = {
|
|
1101
|
+
Accept: "application/vnd.github+json",
|
|
1102
|
+
"User-Agent": "terminalhire",
|
|
1103
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
1104
|
+
};
|
|
1105
|
+
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
1106
|
+
return h;
|
|
1107
|
+
}
|
|
1108
|
+
function tokenize7(text) {
|
|
1109
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
1110
|
+
}
|
|
1111
|
+
function parseAmountUSD(text) {
|
|
1112
|
+
const m = text.match(/\$\s?([0-9][0-9,]*(?:\.[0-9]+)?)\s?([kK])?/);
|
|
1113
|
+
if (!m) return void 0;
|
|
1114
|
+
let n = parseFloat(m[1].replace(/,/g, ""));
|
|
1115
|
+
if (m[2]) n *= 1e3;
|
|
1116
|
+
if (!Number.isFinite(n) || n <= 0 || n > 1e6) return void 0;
|
|
1117
|
+
return Math.round(n);
|
|
1118
|
+
}
|
|
1119
|
+
function effortFromAmount(amount) {
|
|
1120
|
+
if (amount == null) return void 0;
|
|
1121
|
+
if (amount <= 500) return "small";
|
|
1122
|
+
if (amount <= 2e3) return "medium";
|
|
1123
|
+
return "large";
|
|
1124
|
+
}
|
|
1125
|
+
function labelNames(issue) {
|
|
1126
|
+
return (issue.labels ?? []).map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean);
|
|
1127
|
+
}
|
|
1128
|
+
function isBountyIssue(issue) {
|
|
1129
|
+
if (issue.pull_request) return false;
|
|
1130
|
+
const labels = labelNames(issue);
|
|
1131
|
+
if (labels.some((n) => BOUNTY_LABEL_RE.test(n))) return true;
|
|
1132
|
+
return /bounty/i.test(issue.title) && parseAmountUSD(issue.title) != null;
|
|
1133
|
+
}
|
|
1134
|
+
async function ghJson(path) {
|
|
1135
|
+
let res;
|
|
1136
|
+
try {
|
|
1137
|
+
res = await fetch(`${GITHUB_API}${path}`, { headers: authHeaders() });
|
|
1138
|
+
} catch (err) {
|
|
1139
|
+
console.warn(`[github-bounties] network error ${path} \u2014`, err);
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
if (res.status === 403 && res.headers.get("x-ratelimit-remaining") === "0") {
|
|
1143
|
+
console.warn("[github-bounties] rate-limited (set GITHUB_TOKEN for 5000/hr)");
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
if (!res.ok) {
|
|
1147
|
+
console.warn(`[github-bounties] HTTP ${res.status} ${path}`);
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
try {
|
|
1151
|
+
return await res.json();
|
|
1152
|
+
} catch {
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
async function fetchCommentAmount(repoFullName, issueNumber) {
|
|
1157
|
+
const comments = await ghJson(
|
|
1158
|
+
`/repos/${repoFullName}/issues/${issueNumber}/comments?per_page=30`
|
|
1159
|
+
);
|
|
1160
|
+
if (!comments) return void 0;
|
|
1161
|
+
for (const c of comments) {
|
|
1162
|
+
const body = c.body ?? "";
|
|
1163
|
+
if (BOUNTY_LABEL_RE.test(body)) {
|
|
1164
|
+
const amt = parseAmountUSD(body);
|
|
1165
|
+
if (amt != null) return amt;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return void 0;
|
|
1169
|
+
}
|
|
1170
|
+
async function fetchRepoBounties(repoFullName) {
|
|
1171
|
+
const repo = await ghJson(`/repos/${repoFullName}`);
|
|
1172
|
+
if (!repo) return [];
|
|
1173
|
+
const meta = {
|
|
1174
|
+
fullName: repo.full_name,
|
|
1175
|
+
stargazers: repo.stargazers_count,
|
|
1176
|
+
createdAt: repo.created_at,
|
|
1177
|
+
archived: repo.archived,
|
|
1178
|
+
disabled: repo.disabled
|
|
1179
|
+
};
|
|
1180
|
+
if (!passesMaturityGate(meta)) {
|
|
1181
|
+
console.info(`[github-bounties] ${repoFullName}: failed maturity gate, skipping`);
|
|
1182
|
+
return [];
|
|
1183
|
+
}
|
|
1184
|
+
const issues = await ghJson(`/repos/${repoFullName}/issues?state=open&per_page=100`);
|
|
1185
|
+
if (!issues) return [];
|
|
1186
|
+
const bounties = issues.filter(isBountyIssue).slice(0, MAX_BOUNTIES_PER_REPO);
|
|
1187
|
+
const owner = repo.owner.login;
|
|
1188
|
+
return Promise.all(bounties.map(async (issue) => {
|
|
1189
|
+
const title = decodeEntities(issue.title).trim();
|
|
1190
|
+
const body = issue.body ? decodeEntities(issue.body) : "";
|
|
1191
|
+
const amountUSD = parseAmountUSD(title) ?? parseAmountUSD(body) ?? await fetchCommentAmount(repoFullName, issue.number);
|
|
1192
|
+
const labels = labelNames(issue);
|
|
1193
|
+
const tags = normalize(tokenize7([title, labels.join(" "), body.slice(0, 2e3)].join(" ")));
|
|
1194
|
+
return {
|
|
1195
|
+
id: `bounty:${repoFullName}#${issue.number}`,
|
|
1196
|
+
source: "bounty",
|
|
1197
|
+
title,
|
|
1198
|
+
company: owner,
|
|
1199
|
+
url: issue.html_url,
|
|
1200
|
+
remote: true,
|
|
1201
|
+
location: "Remote",
|
|
1202
|
+
tags,
|
|
1203
|
+
roleType: "freelance",
|
|
1204
|
+
postedAt: issue.created_at,
|
|
1205
|
+
applyMode: "direct",
|
|
1206
|
+
bounty: {
|
|
1207
|
+
amountUSD,
|
|
1208
|
+
estimatedEffort: effortFromAmount(amountUSD),
|
|
1209
|
+
bountySource: "github",
|
|
1210
|
+
claimUrl: issue.html_url,
|
|
1211
|
+
repoFullName,
|
|
1212
|
+
repoStars: repo.stargazers_count,
|
|
1213
|
+
issueBody: body.slice(0, 1e3) || void 0
|
|
1214
|
+
},
|
|
1215
|
+
raw: issue
|
|
1216
|
+
};
|
|
1217
|
+
}));
|
|
1218
|
+
}
|
|
1219
|
+
var GITHUB_API, BOUNTY_LABEL_RE, githubBounties;
|
|
1220
|
+
var init_github_bounties = __esm({
|
|
1221
|
+
"../../packages/core/src/feeds/github-bounties.ts"() {
|
|
1222
|
+
"use strict";
|
|
1223
|
+
init_vocabulary();
|
|
1224
|
+
init_entities();
|
|
1225
|
+
init_bounty_gate();
|
|
1226
|
+
GITHUB_API = "https://api.github.com";
|
|
1227
|
+
BOUNTY_LABEL_RE = /bounty|reward|funded|💎|💰/i;
|
|
1228
|
+
githubBounties = {
|
|
1229
|
+
source: "bounty",
|
|
1230
|
+
async fetch(opts) {
|
|
1231
|
+
const repos = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : DEFAULT_BOUNTY_REPOS;
|
|
1232
|
+
console.info(`[github-bounties] scanning ${repos.length} repos`);
|
|
1233
|
+
const settled = await Promise.allSettled(repos.map(fetchRepoBounties));
|
|
1234
|
+
const jobs = [];
|
|
1235
|
+
let failures = 0;
|
|
1236
|
+
for (const r of settled) {
|
|
1237
|
+
if (r.status === "fulfilled") jobs.push(...r.value);
|
|
1238
|
+
else {
|
|
1239
|
+
failures++;
|
|
1240
|
+
console.warn("[github-bounties] repo fetch rejected:", r.reason);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
console.info(`[github-bounties] total: ${jobs.length} bounties, ${failures} repo failures`);
|
|
1244
|
+
return jobs;
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
// ../../packages/core/src/feeds/index.ts
|
|
1251
|
+
async function aggregateBounties(opts) {
|
|
1252
|
+
return githubBounties.fetch({ slugs: opts?.repos });
|
|
1253
|
+
}
|
|
1254
|
+
function flattenTiers(t) {
|
|
1255
|
+
return [.../* @__PURE__ */ new Set([...t.bigco, ...t.scaleup, ...t.startup])];
|
|
1256
|
+
}
|
|
1257
|
+
async function aggregate(opts) {
|
|
1258
|
+
const ghSlugs = opts?.slugs?.["greenhouse"] ?? DEFAULT_GREENHOUSE_SLUGS;
|
|
1259
|
+
const ashbySlugs = opts?.slugs?.["ashby"] ?? DEFAULT_ASHBY_SLUGS;
|
|
1260
|
+
const leverSlugs = opts?.slugs?.["lever"] ?? DEFAULT_LEVER_SLUGS;
|
|
1261
|
+
const limit = opts?.limit ?? 150;
|
|
1262
|
+
const settled = await Promise.allSettled([
|
|
1263
|
+
greenhouse.fetch({ slugs: ghSlugs, limit }),
|
|
1264
|
+
ashby.fetch({ slugs: ashbySlugs, limit }),
|
|
1265
|
+
lever.fetch({ slugs: leverSlugs, limit }),
|
|
1266
|
+
himalayas.fetch({ limit }),
|
|
1267
|
+
wwr.fetch({ limit }),
|
|
1268
|
+
hn.fetch({ limit })
|
|
1269
|
+
]);
|
|
1270
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1271
|
+
const jobs = [];
|
|
1272
|
+
const sourceNames = ["greenhouse", "ashby", "lever", "himalayas", "wwr", "hn"];
|
|
1273
|
+
for (let i = 0; i < settled.length; i++) {
|
|
1274
|
+
const result = settled[i];
|
|
1275
|
+
if (result.status === "rejected") {
|
|
1276
|
+
console.warn(`[feeds] ${sourceNames[i]} failed:`, result.reason);
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
for (const job of result.value) {
|
|
1280
|
+
if (!seen.has(job.id)) {
|
|
1281
|
+
seen.add(job.id);
|
|
1282
|
+
jobs.push(job);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
if (opts?.includeBounties !== false) {
|
|
1287
|
+
try {
|
|
1288
|
+
const bounties = await githubBounties.fetch({ slugs: opts?.slugs?.["bounty"], limit });
|
|
1289
|
+
for (const b of bounties) {
|
|
1290
|
+
if (!seen.has(b.id)) {
|
|
1291
|
+
seen.add(b.id);
|
|
1292
|
+
jobs.push(b);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
} catch (err) {
|
|
1296
|
+
console.warn("[feeds] bounties failed:", err);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return jobs;
|
|
1300
|
+
}
|
|
1301
|
+
var FEEDS, GREENHOUSE_SLUGS_BY_TIER, ASHBY_SLUGS_BY_TIER, LEVER_SLUGS_BY_TIER, DEFAULT_GREENHOUSE_SLUGS, DEFAULT_ASHBY_SLUGS, DEFAULT_LEVER_SLUGS;
|
|
1302
|
+
var init_feeds = __esm({
|
|
1303
|
+
"../../packages/core/src/feeds/index.ts"() {
|
|
1304
|
+
"use strict";
|
|
1305
|
+
init_greenhouse();
|
|
1306
|
+
init_ashby();
|
|
1307
|
+
init_lever();
|
|
1308
|
+
init_himalayas();
|
|
1309
|
+
init_wwr();
|
|
1310
|
+
init_hn();
|
|
1311
|
+
init_github_bounties();
|
|
1312
|
+
init_bounty_gate();
|
|
1313
|
+
FEEDS = [greenhouse, ashby, lever, himalayas, wwr, hn];
|
|
1314
|
+
GREENHOUSE_SLUGS_BY_TIER = {
|
|
1315
|
+
bigco: [
|
|
1316
|
+
"stripe",
|
|
1317
|
+
"anthropic",
|
|
1318
|
+
"figma",
|
|
1319
|
+
"discord",
|
|
1320
|
+
"brex",
|
|
1321
|
+
"mercury",
|
|
1322
|
+
"plaid",
|
|
1323
|
+
"gusto",
|
|
1324
|
+
"scale",
|
|
1325
|
+
"databricks",
|
|
1326
|
+
"coinbase",
|
|
1327
|
+
"robinhood",
|
|
1328
|
+
"doordash",
|
|
1329
|
+
"airbnb",
|
|
1330
|
+
"dropbox",
|
|
1331
|
+
"datadog",
|
|
1332
|
+
"cloudflare",
|
|
1333
|
+
"reddit",
|
|
1334
|
+
"lyft",
|
|
1335
|
+
"instacart"
|
|
1336
|
+
],
|
|
1337
|
+
scaleup: [
|
|
1338
|
+
"samsara",
|
|
1339
|
+
"verkada",
|
|
1340
|
+
"affirm",
|
|
1341
|
+
"gitlab",
|
|
1342
|
+
"asana",
|
|
1343
|
+
"flexport",
|
|
1344
|
+
"faire",
|
|
1345
|
+
"twitch",
|
|
1346
|
+
"airtable",
|
|
1347
|
+
"retool"
|
|
1348
|
+
],
|
|
1349
|
+
startup: [
|
|
1350
|
+
"watershed"
|
|
1351
|
+
]
|
|
1352
|
+
};
|
|
1353
|
+
ASHBY_SLUGS_BY_TIER = {
|
|
1354
|
+
bigco: [
|
|
1355
|
+
"openai"
|
|
1356
|
+
],
|
|
1357
|
+
scaleup: [
|
|
1358
|
+
"harvey",
|
|
1359
|
+
"elevenlabs",
|
|
1360
|
+
"notion",
|
|
1361
|
+
"sierra",
|
|
1362
|
+
"cohere",
|
|
1363
|
+
"ramp",
|
|
1364
|
+
"vanta",
|
|
1365
|
+
"decagon",
|
|
1366
|
+
"cursor",
|
|
1367
|
+
"replit",
|
|
1368
|
+
"perplexity",
|
|
1369
|
+
"baseten",
|
|
1370
|
+
"drata",
|
|
1371
|
+
"writer",
|
|
1372
|
+
"temporal",
|
|
1373
|
+
"supabase"
|
|
1374
|
+
],
|
|
1375
|
+
startup: [
|
|
1376
|
+
"suno",
|
|
1377
|
+
"attio",
|
|
1378
|
+
"modal",
|
|
1379
|
+
"workos",
|
|
1380
|
+
"linear",
|
|
1381
|
+
"render",
|
|
1382
|
+
"warp",
|
|
1383
|
+
"plain",
|
|
1384
|
+
"posthog",
|
|
1385
|
+
"pylon",
|
|
1386
|
+
"resend",
|
|
1387
|
+
"langfuse",
|
|
1388
|
+
"railway",
|
|
1389
|
+
"mintlify",
|
|
1390
|
+
"neon",
|
|
1391
|
+
"browserbase",
|
|
1392
|
+
"knock",
|
|
1393
|
+
"speakeasy",
|
|
1394
|
+
"stytch",
|
|
1395
|
+
"runway",
|
|
1396
|
+
"doppler",
|
|
1397
|
+
"inngest",
|
|
1398
|
+
"hightouch",
|
|
1399
|
+
"zed"
|
|
1400
|
+
]
|
|
1401
|
+
};
|
|
1402
|
+
LEVER_SLUGS_BY_TIER = {
|
|
1403
|
+
bigco: [
|
|
1404
|
+
"palantir",
|
|
1405
|
+
"spotify"
|
|
1406
|
+
],
|
|
1407
|
+
scaleup: [
|
|
1408
|
+
"mistral",
|
|
1409
|
+
"ro",
|
|
1410
|
+
"secureframe"
|
|
1411
|
+
],
|
|
1412
|
+
startup: [
|
|
1413
|
+
"anyscale"
|
|
1414
|
+
]
|
|
1415
|
+
};
|
|
1416
|
+
DEFAULT_GREENHOUSE_SLUGS = flattenTiers(GREENHOUSE_SLUGS_BY_TIER);
|
|
1417
|
+
DEFAULT_ASHBY_SLUGS = flattenTiers(ASHBY_SLUGS_BY_TIER);
|
|
1418
|
+
DEFAULT_LEVER_SLUGS = flattenTiers(LEVER_SLUGS_BY_TIER);
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
// ../../packages/core/src/partners.ts
|
|
1423
|
+
import { readFileSync } from "fs";
|
|
1424
|
+
import { join } from "path";
|
|
1425
|
+
import { fileURLToPath } from "url";
|
|
1426
|
+
function resolveDataPath() {
|
|
1427
|
+
try {
|
|
1428
|
+
const dir = fileURLToPath(new URL("../../../data", import.meta.url));
|
|
1429
|
+
return join(dir, "partner-roles.json");
|
|
1430
|
+
} catch {
|
|
1431
|
+
return join(process.cwd(), "data", "partner-roles.json");
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
function loadPartnerRoles() {
|
|
1435
|
+
const filePath = resolveDataPath();
|
|
1436
|
+
try {
|
|
1437
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
1438
|
+
const parsed = JSON.parse(raw);
|
|
1439
|
+
if (!Array.isArray(parsed)) {
|
|
1440
|
+
console.warn("[partners] partner-roles.json is not an array \u2014 skipping");
|
|
1441
|
+
return [];
|
|
1442
|
+
}
|
|
1443
|
+
const valid = [];
|
|
1444
|
+
for (const entry of parsed) {
|
|
1445
|
+
const e = entry;
|
|
1446
|
+
if (typeof entry === "object" && entry !== null && typeof e.id === "string" && e.applyMode === "buyer-lead" && typeof e.buyer === "string" && e.buyer.length > 0) {
|
|
1447
|
+
valid.push(entry);
|
|
1448
|
+
} else {
|
|
1449
|
+
console.warn("[partners] Skipping malformed role entry:", entry);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
return valid;
|
|
1453
|
+
} catch (err) {
|
|
1454
|
+
if (err.code === "ENOENT") {
|
|
1455
|
+
console.warn(`[partners] data/partner-roles.json not found at ${filePath} \u2014 no partner roles loaded`);
|
|
1456
|
+
} else {
|
|
1457
|
+
console.warn("[partners] Failed to load partner-roles.json:", err);
|
|
1458
|
+
}
|
|
1459
|
+
return [];
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
function getBuyer(id) {
|
|
1463
|
+
return BUYER_REGISTRY[id];
|
|
1464
|
+
}
|
|
1465
|
+
var EXAMPLE_BUYER, BUYER_REGISTRY;
|
|
1466
|
+
var init_partners = __esm({
|
|
1467
|
+
"../../packages/core/src/partners.ts"() {
|
|
1468
|
+
"use strict";
|
|
1469
|
+
EXAMPLE_BUYER = {
|
|
1470
|
+
id: "northstar",
|
|
1471
|
+
legalName: "Northstar Talent Partners",
|
|
1472
|
+
matchCriteria: { roleTypes: ["full_time"] }
|
|
1473
|
+
};
|
|
1474
|
+
BUYER_REGISTRY = {
|
|
1475
|
+
[EXAMPLE_BUYER.id]: EXAMPLE_BUYER
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
// ../../packages/core/src/indexer.ts
|
|
1481
|
+
async function buildIndex(opts) {
|
|
1482
|
+
const includePartners = opts?.includePartners ?? true;
|
|
1483
|
+
const publicJobs = await aggregate(opts);
|
|
1484
|
+
const allJobs = [...publicJobs];
|
|
1485
|
+
const seen = new Set(publicJobs.map((j) => j.id));
|
|
1486
|
+
const partnerJobs = [
|
|
1487
|
+
...includePartners ? loadPartnerRoles() : [],
|
|
1488
|
+
...opts?.partnerRoles ?? []
|
|
1489
|
+
];
|
|
1490
|
+
for (const job of partnerJobs) {
|
|
1491
|
+
if (!seen.has(job.id)) {
|
|
1492
|
+
seen.add(job.id);
|
|
1493
|
+
allJobs.push(job);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
const jobs = allJobs.map(({ raw: _raw, ...rest }) => rest);
|
|
1497
|
+
return {
|
|
1498
|
+
builtAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1499
|
+
jobs
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
var init_indexer = __esm({
|
|
1503
|
+
"../../packages/core/src/indexer.ts"() {
|
|
1504
|
+
"use strict";
|
|
1505
|
+
init_feeds();
|
|
1506
|
+
init_partners();
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
// ../../packages/core/src/github.ts
|
|
1511
|
+
function ghHeaders(token) {
|
|
1512
|
+
const headers = {
|
|
1513
|
+
Accept: "application/vnd.github+json",
|
|
1514
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
1515
|
+
};
|
|
1516
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
1517
|
+
return headers;
|
|
1518
|
+
}
|
|
1519
|
+
async function ghFetch(path, token) {
|
|
1520
|
+
const url = `https://api.github.com${path}`;
|
|
1521
|
+
const res = await fetch(url, { headers: ghHeaders(token) });
|
|
1522
|
+
if (!res.ok) {
|
|
1523
|
+
throw new Error(`GitHub API ${path}: HTTP ${res.status} ${res.statusText}`);
|
|
1524
|
+
}
|
|
1525
|
+
return res.json();
|
|
1526
|
+
}
|
|
1527
|
+
async function fetchGitHubProfile(login, token) {
|
|
1528
|
+
const user = await ghFetch(`/users/${login}`, token);
|
|
1529
|
+
let repos = [];
|
|
1530
|
+
try {
|
|
1531
|
+
repos = await ghFetch(
|
|
1532
|
+
`/users/${login}/repos?sort=pushed&per_page=100`,
|
|
1533
|
+
token
|
|
1534
|
+
);
|
|
1535
|
+
} catch (err) {
|
|
1536
|
+
console.warn(`[github] ${login}: repos fetch failed, continuing \u2014`, err);
|
|
1537
|
+
}
|
|
1538
|
+
const langCount = {};
|
|
1539
|
+
for (const repo of repos) {
|
|
1540
|
+
if (repo.fork) continue;
|
|
1541
|
+
if (repo.language) {
|
|
1542
|
+
langCount[repo.language.toLowerCase()] = (langCount[repo.language.toLowerCase()] ?? 0) + 1;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
const topLanguages = Object.entries(langCount).sort(([, a], [, b]) => b - a).slice(0, 10).map(([lang]) => lang);
|
|
1546
|
+
const topicSet = /* @__PURE__ */ new Set();
|
|
1547
|
+
for (const repo of repos) {
|
|
1548
|
+
if (repo.fork) continue;
|
|
1549
|
+
for (const t of repo.topics ?? []) topicSet.add(t.toLowerCase());
|
|
1550
|
+
}
|
|
1551
|
+
const topics = Array.from(topicSet).slice(0, 30);
|
|
1552
|
+
let recentPRorgs;
|
|
1553
|
+
try {
|
|
1554
|
+
const q = encodeURIComponent(
|
|
1555
|
+
`type:pr is:merged author:${login} sort:updated`
|
|
1556
|
+
);
|
|
1557
|
+
const result = await ghFetch(
|
|
1558
|
+
`/search/issues?q=${q}&per_page=30`,
|
|
1559
|
+
token
|
|
1560
|
+
);
|
|
1561
|
+
const orgs = /* @__PURE__ */ new Set();
|
|
1562
|
+
for (const item of result.items ?? []) {
|
|
1563
|
+
const orgLogin = item.repository?.owner?.login;
|
|
1564
|
+
if (orgLogin && orgLogin !== login) orgs.add(orgLogin);
|
|
1565
|
+
}
|
|
1566
|
+
if (orgs.size > 0) recentPRorgs = Array.from(orgs);
|
|
1567
|
+
} catch {
|
|
1568
|
+
}
|
|
1569
|
+
return {
|
|
1570
|
+
login: user.login,
|
|
1571
|
+
name: user.name ?? void 0,
|
|
1572
|
+
publicEmail: user.email ?? void 0,
|
|
1573
|
+
avatarUrl: user.avatar_url,
|
|
1574
|
+
accountCreatedAt: user.created_at,
|
|
1575
|
+
publicRepos: user.public_repos,
|
|
1576
|
+
followers: user.followers,
|
|
1577
|
+
topLanguages,
|
|
1578
|
+
topics,
|
|
1579
|
+
recentPRorgs
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
function inferSeniority2(p) {
|
|
1583
|
+
const ageMs = Date.now() - new Date(p.accountCreatedAt).getTime();
|
|
1584
|
+
const ageYears = ageMs / (1e3 * 60 * 60 * 24 * 365.25);
|
|
1585
|
+
if (ageYears >= 9 && (p.publicRepos >= 40 || p.followers >= 500)) return "staff";
|
|
1586
|
+
if (ageYears >= 5 && (p.publicRepos >= 20 || p.followers >= 100)) return "senior";
|
|
1587
|
+
if (ageYears >= 2 && p.publicRepos >= 5) return "mid";
|
|
1588
|
+
return "junior";
|
|
1589
|
+
}
|
|
1590
|
+
function githubToFingerprint(p) {
|
|
1591
|
+
const rawTokens = [
|
|
1592
|
+
...p.topLanguages,
|
|
1593
|
+
...p.topics
|
|
1594
|
+
// recentPRorgs intentionally excluded — org names are not skill tags
|
|
1595
|
+
];
|
|
1596
|
+
const skillTags = normalize(rawTokens);
|
|
1597
|
+
const seniorityBand = inferSeniority2(p);
|
|
1598
|
+
return { skillTags, seniorityBand };
|
|
1599
|
+
}
|
|
1600
|
+
var init_github = __esm({
|
|
1601
|
+
"../../packages/core/src/github.ts"() {
|
|
1602
|
+
"use strict";
|
|
1603
|
+
init_vocabulary();
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
// ../../packages/core/src/index.ts
|
|
1608
|
+
var src_exports = {};
|
|
1609
|
+
__export(src_exports, {
|
|
1610
|
+
ASHBY_SLUGS_BY_TIER: () => ASHBY_SLUGS_BY_TIER,
|
|
1611
|
+
DECAY_FLOOR: () => DECAY_FLOOR,
|
|
1612
|
+
DEFAULT_ASHBY_SLUGS: () => DEFAULT_ASHBY_SLUGS,
|
|
1613
|
+
DEFAULT_BOUNTY_REPOS: () => DEFAULT_BOUNTY_REPOS,
|
|
1614
|
+
DEFAULT_GREENHOUSE_SLUGS: () => DEFAULT_GREENHOUSE_SLUGS,
|
|
1615
|
+
DEFAULT_LEVER_SLUGS: () => DEFAULT_LEVER_SLUGS,
|
|
1616
|
+
EXAMPLE_BUYER: () => EXAMPLE_BUYER,
|
|
1617
|
+
FEEDS: () => FEEDS,
|
|
1618
|
+
GRAPH: () => GRAPH,
|
|
1619
|
+
GREENHOUSE_SLUGS_BY_TIER: () => GREENHOUSE_SLUGS_BY_TIER,
|
|
1620
|
+
LEVER_SLUGS_BY_TIER: () => LEVER_SLUGS_BY_TIER,
|
|
1621
|
+
SYNONYMS: () => SYNONYMS,
|
|
1622
|
+
VOCABULARY: () => VOCABULARY,
|
|
1623
|
+
VOCAB_NODES: () => VOCAB_NODES,
|
|
1624
|
+
aggregate: () => aggregate,
|
|
1625
|
+
aggregateBounties: () => aggregateBounties,
|
|
1626
|
+
ashby: () => ashby,
|
|
1627
|
+
buildGraph: () => buildGraph,
|
|
1628
|
+
buildIndex: () => buildIndex,
|
|
1629
|
+
buildReason: () => buildReason,
|
|
1630
|
+
expandWeighted: () => expandWeighted,
|
|
1631
|
+
fetchGitHubProfile: () => fetchGitHubProfile,
|
|
1632
|
+
flattenTiers: () => flattenTiers,
|
|
1633
|
+
getBuyer: () => getBuyer,
|
|
1634
|
+
githubBounties: () => githubBounties,
|
|
1635
|
+
githubToFingerprint: () => githubToFingerprint,
|
|
1636
|
+
greenhouse: () => greenhouse,
|
|
1637
|
+
himalayas: () => himalayas,
|
|
1638
|
+
hn: () => hn,
|
|
1639
|
+
isBounty: () => isBounty,
|
|
1640
|
+
lever: () => lever,
|
|
1641
|
+
loadPartnerRoles: () => loadPartnerRoles,
|
|
1642
|
+
match: () => match,
|
|
1643
|
+
matchOne: () => matchOne,
|
|
1644
|
+
normalize: () => normalize,
|
|
1645
|
+
passesMaturityGate: () => passesMaturityGate,
|
|
1646
|
+
validateGraph: () => validateGraph,
|
|
1647
|
+
wwr: () => wwr
|
|
1648
|
+
});
|
|
1649
|
+
var init_src = __esm({
|
|
1650
|
+
"../../packages/core/src/index.ts"() {
|
|
1651
|
+
"use strict";
|
|
1652
|
+
init_types();
|
|
1653
|
+
init_vocabulary();
|
|
1654
|
+
init_matcher();
|
|
1655
|
+
init_feeds();
|
|
1656
|
+
init_indexer();
|
|
1657
|
+
init_partners();
|
|
1658
|
+
init_github();
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
// src/profile.ts
|
|
1663
|
+
var profile_exports = {};
|
|
1664
|
+
__export(profile_exports, {
|
|
1665
|
+
accumulateGitHubTags: () => accumulateGitHubTags,
|
|
1666
|
+
accumulateSession: () => accumulateSession,
|
|
1667
|
+
accumulateTags: () => accumulateTags,
|
|
1668
|
+
addSavedJob: () => addSavedJob,
|
|
1669
|
+
deleteProfile: () => deleteProfile,
|
|
1670
|
+
listSavedJobs: () => listSavedJobs,
|
|
1671
|
+
profileToFingerprint: () => profileToFingerprint,
|
|
1672
|
+
readProfile: () => readProfile,
|
|
1673
|
+
removeSavedJob: () => removeSavedJob,
|
|
1674
|
+
writeProfile: () => writeProfile
|
|
1675
|
+
});
|
|
1676
|
+
import {
|
|
1677
|
+
createCipheriv,
|
|
1678
|
+
createDecipheriv,
|
|
1679
|
+
randomBytes
|
|
1680
|
+
} from "crypto";
|
|
1681
|
+
import {
|
|
1682
|
+
readFileSync as readFileSync2,
|
|
1683
|
+
writeFileSync,
|
|
1684
|
+
mkdirSync,
|
|
1685
|
+
existsSync
|
|
1686
|
+
} from "fs";
|
|
1687
|
+
import { join as join2 } from "path";
|
|
1688
|
+
import { homedir } from "os";
|
|
1689
|
+
async function loadKey() {
|
|
1690
|
+
try {
|
|
1691
|
+
const kt = await import("keytar");
|
|
1692
|
+
const stored = await kt.getPassword("terminalhire", "profile-key");
|
|
1693
|
+
if (stored) {
|
|
1694
|
+
return Buffer.from(stored, "hex");
|
|
1695
|
+
}
|
|
1696
|
+
const key2 = randomBytes(KEY_BYTES);
|
|
1697
|
+
await kt.setPassword("terminalhire", "profile-key", key2.toString("hex"));
|
|
1698
|
+
return key2;
|
|
1699
|
+
} catch {
|
|
1700
|
+
}
|
|
1701
|
+
mkdirSync(TERMINALHIRE_DIR, { recursive: true });
|
|
1702
|
+
if (existsSync(KEY_FILE)) {
|
|
1703
|
+
return Buffer.from(readFileSync2(KEY_FILE, "utf8").trim(), "hex");
|
|
1704
|
+
}
|
|
1705
|
+
const key = randomBytes(KEY_BYTES);
|
|
1706
|
+
writeFileSync(KEY_FILE, key.toString("hex"), { mode: 384, encoding: "utf8" });
|
|
1707
|
+
return key;
|
|
1708
|
+
}
|
|
1709
|
+
function encrypt(plaintext, key) {
|
|
1710
|
+
const iv = randomBytes(IV_BYTES);
|
|
1711
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
1712
|
+
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
1713
|
+
const tag = cipher.getAuthTag();
|
|
1714
|
+
return {
|
|
1715
|
+
iv: iv.toString("hex"),
|
|
1716
|
+
tag: tag.toString("hex"),
|
|
1717
|
+
ciphertext: ct.toString("hex")
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
function decrypt(blob, key) {
|
|
1721
|
+
const decipher = createDecipheriv(
|
|
1722
|
+
ALGO,
|
|
1723
|
+
key,
|
|
1724
|
+
Buffer.from(blob.iv, "hex")
|
|
1725
|
+
);
|
|
1726
|
+
decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
|
|
1727
|
+
const plain = Buffer.concat([
|
|
1728
|
+
decipher.update(Buffer.from(blob.ciphertext, "hex")),
|
|
1729
|
+
decipher.final()
|
|
1730
|
+
]);
|
|
1731
|
+
return plain.toString("utf8");
|
|
1732
|
+
}
|
|
1733
|
+
function blankProfile() {
|
|
1734
|
+
return {
|
|
1735
|
+
version: 3,
|
|
1736
|
+
skillTags: [],
|
|
1737
|
+
tagWeights: {},
|
|
1738
|
+
hasEmployerSessions: false,
|
|
1739
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
function recencyDecay(lastSeen) {
|
|
1743
|
+
const ageMs = Date.now() - new Date(lastSeen).getTime();
|
|
1744
|
+
return Math.pow(0.5, ageMs / DECAY_HALF_LIFE_MS);
|
|
1745
|
+
}
|
|
1746
|
+
function tagScore(w) {
|
|
1747
|
+
return w.count * recencyDecay(w.lastSeen);
|
|
1748
|
+
}
|
|
1749
|
+
function deriveSkillTags(tagWeights) {
|
|
1750
|
+
return Object.entries(tagWeights).filter(([, w]) => w.count >= 1).sort(([, a], [, b]) => tagScore(b) - tagScore(a)).map(([tag]) => tag);
|
|
1751
|
+
}
|
|
1752
|
+
function migrateTagWeights(profile) {
|
|
1753
|
+
if (!profile.tagWeights) {
|
|
1754
|
+
profile.tagWeights = {};
|
|
1755
|
+
}
|
|
1756
|
+
const seed = profile.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1757
|
+
for (const tag of profile.skillTags) {
|
|
1758
|
+
if (!profile.tagWeights[tag]) {
|
|
1759
|
+
profile.tagWeights[tag] = { count: 1, firstSeen: seed, lastSeen: seed, sessions: 1 };
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
async function readProfile() {
|
|
1764
|
+
if (!existsSync(PROFILE_FILE)) return blankProfile();
|
|
1765
|
+
try {
|
|
1766
|
+
const key = await loadKey();
|
|
1767
|
+
const raw = readFileSync2(PROFILE_FILE, "utf8");
|
|
1768
|
+
const blob = JSON.parse(raw);
|
|
1769
|
+
const plaintext = decrypt(blob, key);
|
|
1770
|
+
const parsed = JSON.parse(plaintext);
|
|
1771
|
+
migrateTagWeights(parsed);
|
|
1772
|
+
return parsed;
|
|
1773
|
+
} catch {
|
|
1774
|
+
return blankProfile();
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
async function writeProfile(profile) {
|
|
1778
|
+
mkdirSync(TERMINALHIRE_DIR, { recursive: true });
|
|
1779
|
+
const key = await loadKey();
|
|
1780
|
+
profile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1781
|
+
profile.skillTags = deriveSkillTags(profile.tagWeights);
|
|
1782
|
+
const blob = encrypt(JSON.stringify(profile), key);
|
|
1783
|
+
writeFileSync(PROFILE_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
|
|
1784
|
+
}
|
|
1785
|
+
function accumulateSession(profile, tags, isEmployerContext, inferredSeniority, seniorityIsAuthoritative = false) {
|
|
1786
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1787
|
+
let filtered = normalize(tags);
|
|
1788
|
+
if (isEmployerContext) {
|
|
1789
|
+
filtered = filtered.filter((t) => LANGUAGE_TAGS.has(t));
|
|
1790
|
+
profile.hasEmployerSessions = true;
|
|
1791
|
+
}
|
|
1792
|
+
for (const tag of filtered) {
|
|
1793
|
+
const existing = profile.tagWeights[tag];
|
|
1794
|
+
if (existing) {
|
|
1795
|
+
existing.count += 1;
|
|
1796
|
+
existing.sessions += 1;
|
|
1797
|
+
existing.lastSeen = now;
|
|
1798
|
+
} else {
|
|
1799
|
+
profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
if (inferredSeniority && !isEmployerContext) {
|
|
1803
|
+
if (seniorityIsAuthoritative || !profile.github) {
|
|
1804
|
+
profile.seniority = inferredSeniority;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
async function accumulateTags(rawTokens, isEmployerContext, inferredSeniority) {
|
|
1809
|
+
const profile = await readProfile();
|
|
1810
|
+
accumulateSession(profile, rawTokens, isEmployerContext, inferredSeniority);
|
|
1811
|
+
await writeProfile(profile);
|
|
1812
|
+
}
|
|
1813
|
+
function accumulateGitHubTags(profile, tags, inferredSeniority) {
|
|
1814
|
+
accumulateSession(
|
|
1815
|
+
profile,
|
|
1816
|
+
tags,
|
|
1817
|
+
/* isEmployerContext */
|
|
1818
|
+
false,
|
|
1819
|
+
inferredSeniority,
|
|
1820
|
+
true
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
async function listSavedJobs() {
|
|
1824
|
+
const profile = await readProfile();
|
|
1825
|
+
return profile.savedJobs ?? [];
|
|
1826
|
+
}
|
|
1827
|
+
async function addSavedJob(job) {
|
|
1828
|
+
const profile = await readProfile();
|
|
1829
|
+
const existing = profile.savedJobs ?? [];
|
|
1830
|
+
const filtered = existing.filter((j) => j.id !== job.id);
|
|
1831
|
+
profile.savedJobs = [...filtered, { ...job, savedAt: (/* @__PURE__ */ new Date()).toISOString() }];
|
|
1832
|
+
await writeProfile(profile);
|
|
1833
|
+
}
|
|
1834
|
+
async function removeSavedJob(id) {
|
|
1835
|
+
const profile = await readProfile();
|
|
1836
|
+
const existing = profile.savedJobs ?? [];
|
|
1837
|
+
const filtered = existing.filter((j) => j.id !== id);
|
|
1838
|
+
if (filtered.length === existing.length) return false;
|
|
1839
|
+
profile.savedJobs = filtered;
|
|
1840
|
+
await writeProfile(profile);
|
|
1841
|
+
return true;
|
|
1842
|
+
}
|
|
1843
|
+
async function deleteProfile() {
|
|
1844
|
+
const { rmSync } = await import("fs");
|
|
1845
|
+
try {
|
|
1846
|
+
rmSync(PROFILE_FILE);
|
|
1847
|
+
} catch {
|
|
1848
|
+
}
|
|
1849
|
+
try {
|
|
1850
|
+
rmSync(KEY_FILE);
|
|
1851
|
+
} catch {
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
function profileToFingerprint(profile) {
|
|
1855
|
+
const rankedTags = Object.entries(profile.tagWeights).map(([tag, w]) => ({ tag, score: tagScore(w) })).filter(({ score }) => score >= MIN_FINGERPRINT_SCORE).sort((a, b) => b.score - a.score).map(({ tag }) => tag);
|
|
1856
|
+
const skillTags = rankedTags.length > 0 ? rankedTags : profile.skillTags;
|
|
1857
|
+
return {
|
|
1858
|
+
skillTags,
|
|
1859
|
+
seniorityBand: profile.seniority,
|
|
1860
|
+
prefs: {
|
|
1861
|
+
roleTypes: profile.roleTypes,
|
|
1862
|
+
remoteOnly: profile.remoteOnly,
|
|
1863
|
+
compFloorUsd: profile.compFloorUsd
|
|
1864
|
+
}
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
var TERMINALHIRE_DIR, PROFILE_FILE, KEY_FILE, ALGO, KEY_BYTES, IV_BYTES, DECAY_HALF_LIFE_MS, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
|
|
1868
|
+
var init_profile = __esm({
|
|
1869
|
+
"src/profile.ts"() {
|
|
1870
|
+
"use strict";
|
|
1871
|
+
init_src();
|
|
1872
|
+
TERMINALHIRE_DIR = join2(homedir(), ".terminalhire");
|
|
1873
|
+
PROFILE_FILE = join2(TERMINALHIRE_DIR, "profile.enc");
|
|
1874
|
+
KEY_FILE = join2(TERMINALHIRE_DIR, "key");
|
|
1875
|
+
ALGO = "aes-256-gcm";
|
|
1876
|
+
KEY_BYTES = 32;
|
|
1877
|
+
IV_BYTES = 12;
|
|
1878
|
+
DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
1879
|
+
LANGUAGE_TAGS = /* @__PURE__ */ new Set([
|
|
1880
|
+
"typescript",
|
|
1881
|
+
"javascript",
|
|
1882
|
+
"python",
|
|
1883
|
+
"go",
|
|
1884
|
+
"rust",
|
|
1885
|
+
"java",
|
|
1886
|
+
"ruby",
|
|
1887
|
+
"elixir",
|
|
1888
|
+
"scala",
|
|
1889
|
+
"kotlin",
|
|
1890
|
+
"swift",
|
|
1891
|
+
"cpp",
|
|
1892
|
+
"csharp",
|
|
1893
|
+
"php",
|
|
1894
|
+
"haskell",
|
|
1895
|
+
"clojure",
|
|
1896
|
+
"r"
|
|
1897
|
+
]);
|
|
1898
|
+
MIN_FINGERPRINT_SCORE = 0.05;
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
// bin/jpi-bounties.js
|
|
1903
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
1904
|
+
import { join as join3 } from "path";
|
|
1905
|
+
import { homedir as homedir2 } from "os";
|
|
1906
|
+
import { createInterface } from "readline";
|
|
1907
|
+
var TERMINALHIRE_DIR2 = join3(homedir2(), ".terminalhire");
|
|
1908
|
+
var INDEX_CACHE_FILE = join3(TERMINALHIRE_DIR2, "index-cache.json");
|
|
1909
|
+
var INDEX_TTL_MS = 15 * 60 * 1e3;
|
|
1910
|
+
var API_URL = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
|
|
1911
|
+
var DEFAULT_LIMIT = 15;
|
|
1912
|
+
var args = process.argv.slice(2);
|
|
1913
|
+
var limitArg = args.indexOf("--limit");
|
|
1914
|
+
var LIMIT = limitArg !== -1 ? parseInt(args[limitArg + 1] ?? "15", 10) : DEFAULT_LIMIT;
|
|
1915
|
+
var PRICED_ONLY = args.includes("--priced");
|
|
1916
|
+
var SHOW_ALL = args.includes("--all");
|
|
1917
|
+
function readIndexCache() {
|
|
1918
|
+
try {
|
|
1919
|
+
const entry = JSON.parse(readFileSync3(INDEX_CACHE_FILE, "utf8"));
|
|
1920
|
+
if (Date.now() - entry.ts < INDEX_TTL_MS) return entry.index;
|
|
1921
|
+
return null;
|
|
1922
|
+
} catch {
|
|
1923
|
+
return null;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
function writeIndexCache(index) {
|
|
1927
|
+
mkdirSync2(TERMINALHIRE_DIR2, { recursive: true });
|
|
1928
|
+
writeFileSync2(INDEX_CACHE_FILE, JSON.stringify({ ts: Date.now(), index }), "utf8");
|
|
1929
|
+
}
|
|
1930
|
+
async function fetchIndex() {
|
|
1931
|
+
const cached = readIndexCache();
|
|
1932
|
+
if (cached) return cached;
|
|
1933
|
+
const res = await fetch(`${API_URL}/api/index`, { signal: AbortSignal.timeout(1e4) });
|
|
1934
|
+
if (!res.ok) throw new Error(`/api/index returned ${res.status}`);
|
|
1935
|
+
const index = await res.json();
|
|
1936
|
+
writeIndexCache(index);
|
|
1937
|
+
return index;
|
|
1938
|
+
}
|
|
1939
|
+
function prompt(question) {
|
|
1940
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1941
|
+
return new Promise((resolve) => {
|
|
1942
|
+
rl.question(question, (answer) => {
|
|
1943
|
+
rl.close();
|
|
1944
|
+
resolve(answer.trim().toLowerCase());
|
|
1945
|
+
});
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
function formatAmount(b) {
|
|
1949
|
+
return b.amountUSD != null ? "$" + b.amountUSD.toLocaleString() : "$\u2014";
|
|
1950
|
+
}
|
|
1951
|
+
var EFFORT_LABEL = { small: "small (~\xBD day)", medium: "medium (~1 day)", large: "large (multi-day)" };
|
|
1952
|
+
function linkTitle(title, url) {
|
|
1953
|
+
const isTTY = process.stdout.isTTY;
|
|
1954
|
+
const noColor = process.env["NO_COLOR"] !== void 0;
|
|
1955
|
+
if (isTTY && !noColor && url) return `\x1B]8;;${url}\x1B\\${title}\x1B]8;;\x1B\\`;
|
|
1956
|
+
return url ? `${title} (${url})` : title;
|
|
1957
|
+
}
|
|
1958
|
+
function printBounty(i, job, score, reason, matchedTags) {
|
|
1959
|
+
const b = job.bounty ?? {};
|
|
1960
|
+
const stars = b.repoStars != null ? ` \xB7 ${b.repoStars}\u2605` : "";
|
|
1961
|
+
const effort = b.estimatedEffort ? ` \xB7 ${EFFORT_LABEL[b.estimatedEffort]}` : "";
|
|
1962
|
+
const scoreStr = score > 0 ? ` \xB7 match ${Math.round(score * 100)}%` : "";
|
|
1963
|
+
console.log(`
|
|
1964
|
+
${i + 1}. ${linkTitle(job.title, job.url)}`);
|
|
1965
|
+
console.log(` ${formatAmount(b)}${effort} \xB7 ${b.repoFullName ?? job.company}${stars}${scoreStr}`);
|
|
1966
|
+
if (reason) console.log(` ${reason}`);
|
|
1967
|
+
if (matchedTags && matchedTags.length) console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
|
|
1968
|
+
console.log(` id: ${job.id}`);
|
|
1969
|
+
console.log(` Claim: ${b.claimUrl ?? job.url}`);
|
|
1970
|
+
}
|
|
1971
|
+
async function run() {
|
|
1972
|
+
try {
|
|
1973
|
+
console.log(`Fetching bounty index from ${API_URL}/api/index...`);
|
|
1974
|
+
const index = await fetchIndex();
|
|
1975
|
+
let bounties = (index.jobs ?? []).filter((j) => j.source === "bounty");
|
|
1976
|
+
if (PRICED_ONLY) bounties = bounties.filter((j) => j.bounty?.amountUSD != null);
|
|
1977
|
+
if (bounties.length === 0) {
|
|
1978
|
+
console.log("\nNo bounties available right now. Try again later \u2014 supply refreshes through the day.");
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
const ranked = /* @__PURE__ */ new Map();
|
|
1982
|
+
try {
|
|
1983
|
+
const { readProfile: readProfile2, profileToFingerprint: profileToFingerprint2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
1984
|
+
const { match: match2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
1985
|
+
const profile = await readProfile2();
|
|
1986
|
+
if (profile.skillTags.length > 0) {
|
|
1987
|
+
const fp = profileToFingerprint2(profile);
|
|
1988
|
+
for (const r of match2(fp, bounties, bounties.length)) {
|
|
1989
|
+
ranked.set(r.job.id, { score: r.score, reason: r.reason, matchedTags: r.matchedTags });
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
} catch {
|
|
1993
|
+
}
|
|
1994
|
+
const score = (j) => ranked.get(j.id)?.score ?? 0;
|
|
1995
|
+
const amt = (j) => j.bounty?.amountUSD ?? -1;
|
|
1996
|
+
bounties.sort((a, b) => score(b) - score(a) || amt(b) - amt(a));
|
|
1997
|
+
const shown = SHOW_ALL ? bounties : bounties.slice(0, LIMIT);
|
|
1998
|
+
const matchedCount = bounties.filter((j) => score(j) > 0).length;
|
|
1999
|
+
console.log(
|
|
2000
|
+
`
|
|
2001
|
+
\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)
|
|
2002
|
+
`
|
|
2003
|
+
);
|
|
2004
|
+
for (let i = 0; i < shown.length; i++) {
|
|
2005
|
+
const r = ranked.get(shown[i].id);
|
|
2006
|
+
printBounty(i, shown[i], r?.score ?? 0, r?.reason, r?.matchedTags);
|
|
2007
|
+
}
|
|
2008
|
+
if (!SHOW_ALL && bounties.length > shown.length) {
|
|
2009
|
+
console.log(`
|
|
2010
|
+
\u2026and ${bounties.length - shown.length} more \u2014 run with --all to see every bounty.`);
|
|
2011
|
+
}
|
|
2012
|
+
if (!process.stdin.isTTY) return;
|
|
2013
|
+
console.log("\n" + "\u2500".repeat(70));
|
|
2014
|
+
const pick = await prompt(`
|
|
2015
|
+
Enter a number to open a bounty's claim page, or press Enter to exit: `);
|
|
2016
|
+
const idx = parseInt(pick, 10) - 1;
|
|
2017
|
+
if (Number.isNaN(idx) || idx < 0 || idx >= shown.length) return;
|
|
2018
|
+
const chosen = shown[idx];
|
|
2019
|
+
console.log(
|
|
2020
|
+
`
|
|
2021
|
+
Open this to claim/work the bounty (you go straight to the source \u2014 we never touch payment):
|
|
2022
|
+
${chosen.bounty?.claimUrl ?? chosen.url}`
|
|
2023
|
+
);
|
|
2024
|
+
} catch (err) {
|
|
2025
|
+
console.error("terminalhire bounties error:", err.message ?? err);
|
|
2026
|
+
process.exit(1);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
export {
|
|
2030
|
+
run
|
|
2031
|
+
};
|