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-jobs.js
CHANGED
|
@@ -10,227 +10,369 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// ../../packages/core/src/types.ts
|
|
13
|
+
function isBounty(job) {
|
|
14
|
+
return job.source === "bounty" && job.bounty != null;
|
|
15
|
+
}
|
|
13
16
|
var init_types = __esm({
|
|
14
17
|
"../../packages/core/src/types.ts"() {
|
|
15
18
|
"use strict";
|
|
16
19
|
}
|
|
17
20
|
});
|
|
18
21
|
|
|
19
|
-
// ../../packages/core/src/
|
|
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
|
|
20
330
|
function normalize(tokens) {
|
|
21
331
|
const result = /* @__PURE__ */ new Set();
|
|
22
332
|
for (const raw of tokens) {
|
|
23
333
|
const lower = raw.toLowerCase().trim();
|
|
24
|
-
if (
|
|
334
|
+
if (GRAPH.ids.has(lower)) {
|
|
25
335
|
result.add(lower);
|
|
26
336
|
continue;
|
|
27
337
|
}
|
|
28
|
-
const mapped =
|
|
29
|
-
if (mapped
|
|
30
|
-
result.add(mapped);
|
|
31
|
-
}
|
|
338
|
+
const mapped = GRAPH.synonyms.get(lower);
|
|
339
|
+
if (mapped) result.add(mapped);
|
|
32
340
|
}
|
|
33
341
|
return Array.from(result);
|
|
34
342
|
}
|
|
35
|
-
|
|
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
|
|
36
372
|
var init_vocabulary = __esm({
|
|
37
373
|
"../../packages/core/src/vocabulary.ts"() {
|
|
38
374
|
"use strict";
|
|
39
|
-
|
|
40
|
-
// Languages
|
|
41
|
-
"typescript",
|
|
42
|
-
"javascript",
|
|
43
|
-
"python",
|
|
44
|
-
"go",
|
|
45
|
-
"rust",
|
|
46
|
-
"java",
|
|
47
|
-
"ruby",
|
|
48
|
-
"elixir",
|
|
49
|
-
"scala",
|
|
50
|
-
"kotlin",
|
|
51
|
-
"swift",
|
|
52
|
-
"cpp",
|
|
53
|
-
"csharp",
|
|
54
|
-
"php",
|
|
55
|
-
"haskell",
|
|
56
|
-
"clojure",
|
|
57
|
-
"r",
|
|
58
|
-
// Frontend frameworks / libs
|
|
59
|
-
"react",
|
|
60
|
-
"nextjs",
|
|
61
|
-
"vue",
|
|
62
|
-
"nuxt",
|
|
63
|
-
"svelte",
|
|
64
|
-
"angular",
|
|
65
|
-
"solidjs",
|
|
66
|
-
"tailwind",
|
|
67
|
-
"css",
|
|
68
|
-
"html",
|
|
69
|
-
"graphql",
|
|
70
|
-
"trpc",
|
|
71
|
-
// Backend frameworks
|
|
72
|
-
"nodejs",
|
|
73
|
-
"express",
|
|
74
|
-
"fastify",
|
|
75
|
-
"nestjs",
|
|
76
|
-
"django",
|
|
77
|
-
"fastapi",
|
|
78
|
-
"flask",
|
|
79
|
-
"rails",
|
|
80
|
-
"spring",
|
|
81
|
-
"actix",
|
|
82
|
-
"gin",
|
|
83
|
-
"phoenix",
|
|
84
|
-
"laravel",
|
|
85
|
-
"dotnet",
|
|
86
|
-
// Infrastructure & DevOps
|
|
87
|
-
"kubernetes",
|
|
88
|
-
"docker",
|
|
89
|
-
"terraform",
|
|
90
|
-
"aws",
|
|
91
|
-
"gcp",
|
|
92
|
-
"azure",
|
|
93
|
-
"ci-cd",
|
|
94
|
-
"github-actions",
|
|
95
|
-
"linux",
|
|
96
|
-
"nginx",
|
|
97
|
-
"pulumi",
|
|
98
|
-
"ansible",
|
|
99
|
-
"prometheus",
|
|
100
|
-
"grafana",
|
|
101
|
-
"datadog",
|
|
102
|
-
"opentelemetry",
|
|
103
|
-
// Data & ML
|
|
104
|
-
"postgresql",
|
|
105
|
-
"mysql",
|
|
106
|
-
"sqlite",
|
|
107
|
-
"mongodb",
|
|
108
|
-
"redis",
|
|
109
|
-
"elasticsearch",
|
|
110
|
-
"kafka",
|
|
111
|
-
"rabbitmq",
|
|
112
|
-
"data-engineering",
|
|
113
|
-
"spark",
|
|
114
|
-
"airflow",
|
|
115
|
-
"dbt",
|
|
116
|
-
"ml",
|
|
117
|
-
"llm",
|
|
118
|
-
"pytorch",
|
|
119
|
-
"tensorflow",
|
|
120
|
-
"pandas",
|
|
121
|
-
"numpy",
|
|
122
|
-
// Domains / capabilities
|
|
123
|
-
"oauth",
|
|
124
|
-
"authentication",
|
|
125
|
-
"security",
|
|
126
|
-
"payments",
|
|
127
|
-
"billing",
|
|
128
|
-
"frontend",
|
|
129
|
-
"backend",
|
|
130
|
-
"devops",
|
|
131
|
-
"mobile",
|
|
132
|
-
"ios",
|
|
133
|
-
"android",
|
|
134
|
-
"api-design",
|
|
135
|
-
"microservices",
|
|
136
|
-
"websockets",
|
|
137
|
-
"testing",
|
|
138
|
-
"accessibility",
|
|
139
|
-
"seo",
|
|
140
|
-
"performance",
|
|
141
|
-
"observability",
|
|
142
|
-
"search",
|
|
143
|
-
"realtime"
|
|
144
|
-
];
|
|
145
|
-
SYNONYMS = {
|
|
146
|
-
// Kubernetes aliases
|
|
147
|
-
"k8s": "kubernetes",
|
|
148
|
-
"kube": "kubernetes",
|
|
149
|
-
// Auth / identity
|
|
150
|
-
"passport": "authentication",
|
|
151
|
-
"oauth2": "oauth",
|
|
152
|
-
"oidc": "oauth",
|
|
153
|
-
"jwt": "authentication",
|
|
154
|
-
"saml": "authentication",
|
|
155
|
-
"auth0": "authentication",
|
|
156
|
-
"clerk": "authentication",
|
|
157
|
-
"nextauth": "authentication",
|
|
158
|
-
// Payments
|
|
159
|
-
"@stripe/stripe-js": "payments",
|
|
160
|
-
"stripe": "payments",
|
|
161
|
-
"braintree": "payments",
|
|
162
|
-
"paddle": "payments",
|
|
163
|
-
"lemonsqueezy": "payments",
|
|
164
|
-
"recurly": "billing",
|
|
165
|
-
"chargebee": "billing",
|
|
166
|
-
// Framework / lib aliases
|
|
167
|
-
"next": "nextjs",
|
|
168
|
-
"next.js": "nextjs",
|
|
169
|
-
"nuxt.js": "nuxt",
|
|
170
|
-
"vue.js": "vue",
|
|
171
|
-
"angular.js": "angular",
|
|
172
|
-
"angularjs": "angular",
|
|
173
|
-
"express.js": "express",
|
|
174
|
-
"expressjs": "express",
|
|
175
|
-
"fastapi": "fastapi",
|
|
176
|
-
"nest": "nestjs",
|
|
177
|
-
"nest.js": "nestjs",
|
|
178
|
-
"sveltekit": "svelte",
|
|
179
|
-
// Language aliases
|
|
180
|
-
"ts": "typescript",
|
|
181
|
-
"js": "javascript",
|
|
182
|
-
"py": "python",
|
|
183
|
-
"golang": "go",
|
|
184
|
-
"c++": "cpp",
|
|
185
|
-
"c#": "csharp",
|
|
186
|
-
".net": "dotnet",
|
|
187
|
-
"asp.net": "dotnet",
|
|
188
|
-
// DB aliases
|
|
189
|
-
"postgres": "postgresql",
|
|
190
|
-
"pg": "postgresql",
|
|
191
|
-
"mongo": "mongodb",
|
|
192
|
-
"elastic": "elasticsearch",
|
|
193
|
-
// Cloud aliases
|
|
194
|
-
"amazon web services": "aws",
|
|
195
|
-
"google cloud": "gcp",
|
|
196
|
-
"google cloud platform": "gcp",
|
|
197
|
-
"microsoft azure": "azure",
|
|
198
|
-
// CI/CD aliases
|
|
199
|
-
"github actions": "github-actions",
|
|
200
|
-
"circle ci": "ci-cd",
|
|
201
|
-
"circleci": "ci-cd",
|
|
202
|
-
"jenkins": "ci-cd",
|
|
203
|
-
"gitlab ci": "ci-cd",
|
|
204
|
-
"travis": "ci-cd",
|
|
205
|
-
// Mobile
|
|
206
|
-
"react native": "mobile",
|
|
207
|
-
"flutter": "mobile",
|
|
208
|
-
"expo": "mobile",
|
|
209
|
-
// AI / ML
|
|
210
|
-
"openai": "llm",
|
|
211
|
-
"anthropic": "llm",
|
|
212
|
-
"langchain": "llm",
|
|
213
|
-
"llamaindex": "llm",
|
|
214
|
-
"hugging face": "ml",
|
|
215
|
-
"huggingface": "ml",
|
|
216
|
-
"scikit-learn": "ml",
|
|
217
|
-
"sklearn": "ml",
|
|
218
|
-
// Data pipeline
|
|
219
|
-
"apache kafka": "kafka",
|
|
220
|
-
"apache spark": "spark",
|
|
221
|
-
"apache airflow": "airflow",
|
|
222
|
-
// Misc
|
|
223
|
-
"tailwindcss": "tailwind",
|
|
224
|
-
"tw": "tailwind",
|
|
225
|
-
"gql": "graphql",
|
|
226
|
-
"ws": "websockets",
|
|
227
|
-
"socket.io": "websockets",
|
|
228
|
-
"jest": "testing",
|
|
229
|
-
"vitest": "testing",
|
|
230
|
-
"playwright": "testing",
|
|
231
|
-
"cypress": "testing"
|
|
232
|
-
};
|
|
233
|
-
VOCAB_SET = new Set(VOCABULARY);
|
|
375
|
+
init_vocab();
|
|
234
376
|
}
|
|
235
377
|
});
|
|
236
378
|
|
|
@@ -251,6 +393,7 @@ function computeIdf(jobs) {
|
|
|
251
393
|
return idf;
|
|
252
394
|
}
|
|
253
395
|
function inferSeniority(title) {
|
|
396
|
+
if (!ENG_TITLE.test(title)) return void 0;
|
|
254
397
|
for (const [re, level] of SENIORITY_PATTERNS) {
|
|
255
398
|
if (re.test(title)) return level;
|
|
256
399
|
}
|
|
@@ -264,15 +407,15 @@ function seniorityScore(fp, job) {
|
|
|
264
407
|
const got = SENIORITY_RANK[jobLevel] ?? 1;
|
|
265
408
|
const delta = Math.abs(wanted - got);
|
|
266
409
|
if (delta === 0) return 1;
|
|
267
|
-
if (delta === 1) return 0.
|
|
268
|
-
return 0.
|
|
410
|
+
if (delta === 1) return 0.7;
|
|
411
|
+
return 0.4;
|
|
269
412
|
}
|
|
270
|
-
function recencyScore(postedAt) {
|
|
413
|
+
function recencyScore(postedAt, now) {
|
|
271
414
|
if (!postedAt) return 0.75;
|
|
272
|
-
const
|
|
273
|
-
if (
|
|
274
|
-
if (
|
|
275
|
-
if (
|
|
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;
|
|
276
419
|
return 0.6;
|
|
277
420
|
}
|
|
278
421
|
function passesFilters(fp, job) {
|
|
@@ -287,52 +430,73 @@ function passesFilters(fp, job) {
|
|
|
287
430
|
}
|
|
288
431
|
return true;
|
|
289
432
|
}
|
|
290
|
-
function buildReason(
|
|
291
|
-
if (
|
|
292
|
-
const
|
|
293
|
-
const
|
|
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;
|
|
294
438
|
const listed = top.join(", ");
|
|
295
439
|
if (rest === 0) return `Matched on ${listed}.`;
|
|
296
440
|
return `Matched on ${listed} + ${rest} more skill${rest > 1 ? "s" : ""}.`;
|
|
297
441
|
}
|
|
298
|
-
function
|
|
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()) {
|
|
299
447
|
const idf = computeIdf(jobs);
|
|
300
|
-
const
|
|
301
|
-
const
|
|
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);
|
|
302
451
|
const candidates = jobs.filter((j) => passesFilters(fp, j));
|
|
303
452
|
const scored = candidates.map((job) => {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
let
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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);
|
|
312
466
|
}
|
|
313
467
|
}
|
|
314
|
-
|
|
315
|
-
|
|
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);
|
|
316
475
|
const sScore = seniorityScore(fp, job);
|
|
317
|
-
const rScore = recencyScore(job.postedAt);
|
|
318
|
-
const score =
|
|
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))];
|
|
319
479
|
return {
|
|
320
480
|
job,
|
|
321
481
|
score: Math.round(score * 1e3) / 1e3,
|
|
322
|
-
matchedTags
|
|
323
|
-
|
|
482
|
+
matchedTags,
|
|
483
|
+
matchDetails: details,
|
|
484
|
+
reason: buildReason(details)
|
|
324
485
|
};
|
|
325
486
|
});
|
|
326
|
-
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
487
|
+
return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
327
488
|
}
|
|
328
489
|
function matchOne(fp, job) {
|
|
329
490
|
const results = match(fp, [job], 1);
|
|
330
491
|
return results.length > 0 ? results[0] : null;
|
|
331
492
|
}
|
|
332
|
-
var SENIORITY_RANK, SENIORITY_PATTERNS;
|
|
493
|
+
var MIN_SCORE, SHARPEN, SENIORITY_RANK, SENIORITY_PATTERNS, ENG_TITLE;
|
|
333
494
|
var init_matcher = __esm({
|
|
334
495
|
"../../packages/core/src/matcher.ts"() {
|
|
335
496
|
"use strict";
|
|
497
|
+
init_vocabulary();
|
|
498
|
+
MIN_SCORE = 0.15;
|
|
499
|
+
SHARPEN = 1.6;
|
|
336
500
|
SENIORITY_RANK = {
|
|
337
501
|
junior: 0,
|
|
338
502
|
mid: 1,
|
|
@@ -345,6 +509,7 @@ var init_matcher = __esm({
|
|
|
345
509
|
[/\bjunior\b|\bjr\.?\b|\bentry[\s-]?level\b/i, "junior"],
|
|
346
510
|
[/\bmid[\s-]?level\b|\bmid\b/i, "mid"]
|
|
347
511
|
];
|
|
512
|
+
ENG_TITLE = /\b(engineer|engineering|developer|dev|swe|sde|programmer|architect)\b/i;
|
|
348
513
|
}
|
|
349
514
|
});
|
|
350
515
|
|
|
@@ -685,6 +850,33 @@ var init_himalayas = __esm({
|
|
|
685
850
|
}
|
|
686
851
|
});
|
|
687
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
|
+
|
|
688
880
|
// ../../packages/core/src/feeds/wwr.ts
|
|
689
881
|
function tokenize5(text) {
|
|
690
882
|
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
@@ -708,9 +900,9 @@ function parseRss(xml) {
|
|
|
708
900
|
for (const block of itemBlocks) {
|
|
709
901
|
const get = (tag) => {
|
|
710
902
|
const cdataMatch = block.match(new RegExp(`<${tag}[^>]*><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/${tag}>`, "i"));
|
|
711
|
-
if (cdataMatch) return cdataMatch[1].trim();
|
|
903
|
+
if (cdataMatch) return decodeEntities(cdataMatch[1].trim());
|
|
712
904
|
const plainMatch = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i"));
|
|
713
|
-
return plainMatch?.[1].trim() ?? "";
|
|
905
|
+
return decodeEntities(plainMatch?.[1].trim() ?? "");
|
|
714
906
|
};
|
|
715
907
|
const rawTitle = get("title");
|
|
716
908
|
const colonIdx = rawTitle.indexOf(":");
|
|
@@ -737,6 +929,7 @@ var init_wwr = __esm({
|
|
|
737
929
|
"../../packages/core/src/feeds/wwr.ts"() {
|
|
738
930
|
"use strict";
|
|
739
931
|
init_vocabulary();
|
|
932
|
+
init_entities();
|
|
740
933
|
WWR_RSS_URL = "https://weworkremotely.com/remote-jobs.rss";
|
|
741
934
|
wwr = {
|
|
742
935
|
source: "wwr",
|
|
@@ -775,7 +968,7 @@ function tokenize6(text) {
|
|
|
775
968
|
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
776
969
|
}
|
|
777
970
|
function stripHtml2(html) {
|
|
778
|
-
return html.replace(/<p>/gi, " ").replace(/<[^>]*>/g, "")
|
|
971
|
+
return decodeEntities(html.replace(/<p>/gi, " ").replace(/<[^>]*>/g, "")).replace(/\s+/g, " ").trim();
|
|
779
972
|
}
|
|
780
973
|
function extractUrl(text) {
|
|
781
974
|
const match2 = text.match(/https?:\/\/[^\s<>"']+/);
|
|
@@ -829,6 +1022,7 @@ var init_hn = __esm({
|
|
|
829
1022
|
"../../packages/core/src/feeds/hn.ts"() {
|
|
830
1023
|
"use strict";
|
|
831
1024
|
init_vocabulary();
|
|
1025
|
+
init_entities();
|
|
832
1026
|
ALGOLIA_SEARCH = "https://hn.algolia.com/api/v1/search?query=Ask+HN%3A+Who+is+Hiring%3F&tags=story,ask_hn&hitsPerPage=1";
|
|
833
1027
|
ALGOLIA_ITEMS = "https://hn.algolia.com/api/v1/items/";
|
|
834
1028
|
hn = {
|
|
@@ -865,7 +1059,198 @@ var init_hn = __esm({
|
|
|
865
1059
|
}
|
|
866
1060
|
});
|
|
867
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
|
+
|
|
868
1250
|
// ../../packages/core/src/feeds/index.ts
|
|
1251
|
+
async function aggregateBounties(opts) {
|
|
1252
|
+
return githubBounties.fetch({ slugs: opts?.repos });
|
|
1253
|
+
}
|
|
869
1254
|
function flattenTiers(t) {
|
|
870
1255
|
return [.../* @__PURE__ */ new Set([...t.bigco, ...t.scaleup, ...t.startup])];
|
|
871
1256
|
}
|
|
@@ -898,6 +1283,19 @@ async function aggregate(opts) {
|
|
|
898
1283
|
}
|
|
899
1284
|
}
|
|
900
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
|
+
}
|
|
901
1299
|
return jobs;
|
|
902
1300
|
}
|
|
903
1301
|
var FEEDS, GREENHOUSE_SLUGS_BY_TIER, ASHBY_SLUGS_BY_TIER, LEVER_SLUGS_BY_TIER, DEFAULT_GREENHOUSE_SLUGS, DEFAULT_ASHBY_SLUGS, DEFAULT_LEVER_SLUGS;
|
|
@@ -910,6 +1308,8 @@ var init_feeds = __esm({
|
|
|
910
1308
|
init_himalayas();
|
|
911
1309
|
init_wwr();
|
|
912
1310
|
init_hn();
|
|
1311
|
+
init_github_bounties();
|
|
1312
|
+
init_bounty_gate();
|
|
913
1313
|
FEEDS = [greenhouse, ashby, lever, himalayas, wwr, hn];
|
|
914
1314
|
GREENHOUSE_SLUGS_BY_TIER = {
|
|
915
1315
|
bigco: [
|
|
@@ -1019,72 +1419,78 @@ var init_feeds = __esm({
|
|
|
1019
1419
|
}
|
|
1020
1420
|
});
|
|
1021
1421
|
|
|
1022
|
-
// ../../packages/core/src/
|
|
1422
|
+
// ../../packages/core/src/partners.ts
|
|
1023
1423
|
import { readFileSync } from "fs";
|
|
1024
1424
|
import { join } from "path";
|
|
1025
1425
|
import { fileURLToPath } from "url";
|
|
1026
1426
|
function resolveDataPath() {
|
|
1027
1427
|
try {
|
|
1028
1428
|
const dir = fileURLToPath(new URL("../../../data", import.meta.url));
|
|
1029
|
-
return join(dir, "
|
|
1429
|
+
return join(dir, "partner-roles.json");
|
|
1030
1430
|
} catch {
|
|
1031
|
-
return join(process.cwd(), "data", "
|
|
1431
|
+
return join(process.cwd(), "data", "partner-roles.json");
|
|
1032
1432
|
}
|
|
1033
1433
|
}
|
|
1034
|
-
function
|
|
1434
|
+
function loadPartnerRoles() {
|
|
1035
1435
|
const filePath = resolveDataPath();
|
|
1036
1436
|
try {
|
|
1037
1437
|
const raw = readFileSync(filePath, "utf-8");
|
|
1038
1438
|
const parsed = JSON.parse(raw);
|
|
1039
1439
|
if (!Array.isArray(parsed)) {
|
|
1040
|
-
console.warn("[
|
|
1440
|
+
console.warn("[partners] partner-roles.json is not an array \u2014 skipping");
|
|
1041
1441
|
return [];
|
|
1042
1442
|
}
|
|
1043
1443
|
const valid = [];
|
|
1044
1444
|
for (const entry of parsed) {
|
|
1045
|
-
|
|
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) {
|
|
1046
1447
|
valid.push(entry);
|
|
1047
1448
|
} else {
|
|
1048
|
-
console.warn("[
|
|
1449
|
+
console.warn("[partners] Skipping malformed role entry:", entry);
|
|
1049
1450
|
}
|
|
1050
1451
|
}
|
|
1051
1452
|
return valid;
|
|
1052
1453
|
} catch (err) {
|
|
1053
1454
|
if (err.code === "ENOENT") {
|
|
1054
|
-
console.warn(`[
|
|
1455
|
+
console.warn(`[partners] data/partner-roles.json not found at ${filePath} \u2014 no partner roles loaded`);
|
|
1055
1456
|
} else {
|
|
1056
|
-
console.warn("[
|
|
1457
|
+
console.warn("[partners] Failed to load partner-roles.json:", err);
|
|
1057
1458
|
}
|
|
1058
1459
|
return [];
|
|
1059
1460
|
}
|
|
1060
1461
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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"() {
|
|
1064
1468
|
"use strict";
|
|
1065
|
-
|
|
1066
|
-
id: "
|
|
1067
|
-
legalName: "
|
|
1068
|
-
matchCriteria: {
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
|
1071
1476
|
};
|
|
1072
1477
|
}
|
|
1073
1478
|
});
|
|
1074
1479
|
|
|
1075
1480
|
// ../../packages/core/src/indexer.ts
|
|
1076
1481
|
async function buildIndex(opts) {
|
|
1077
|
-
const
|
|
1482
|
+
const includePartners = opts?.includePartners ?? true;
|
|
1078
1483
|
const publicJobs = await aggregate(opts);
|
|
1079
1484
|
const allJobs = [...publicJobs];
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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);
|
|
1088
1494
|
}
|
|
1089
1495
|
}
|
|
1090
1496
|
const jobs = allJobs.map(({ raw: _raw, ...rest }) => rest);
|
|
@@ -1097,7 +1503,7 @@ var init_indexer = __esm({
|
|
|
1097
1503
|
"../../packages/core/src/indexer.ts"() {
|
|
1098
1504
|
"use strict";
|
|
1099
1505
|
init_feeds();
|
|
1100
|
-
|
|
1506
|
+
init_partners();
|
|
1101
1507
|
}
|
|
1102
1508
|
});
|
|
1103
1509
|
|
|
@@ -1179,8 +1585,7 @@ function inferSeniority2(p) {
|
|
|
1179
1585
|
if (ageYears >= 9 && (p.publicRepos >= 40 || p.followers >= 500)) return "staff";
|
|
1180
1586
|
if (ageYears >= 5 && (p.publicRepos >= 20 || p.followers >= 100)) return "senior";
|
|
1181
1587
|
if (ageYears >= 2 && p.publicRepos >= 5) return "mid";
|
|
1182
|
-
|
|
1183
|
-
return void 0;
|
|
1588
|
+
return "junior";
|
|
1184
1589
|
}
|
|
1185
1590
|
function githubToFingerprint(p) {
|
|
1186
1591
|
const rawTokens = [
|
|
@@ -1203,30 +1608,42 @@ var init_github = __esm({
|
|
|
1203
1608
|
var src_exports = {};
|
|
1204
1609
|
__export(src_exports, {
|
|
1205
1610
|
ASHBY_SLUGS_BY_TIER: () => ASHBY_SLUGS_BY_TIER,
|
|
1206
|
-
|
|
1611
|
+
DECAY_FLOOR: () => DECAY_FLOOR,
|
|
1207
1612
|
DEFAULT_ASHBY_SLUGS: () => DEFAULT_ASHBY_SLUGS,
|
|
1613
|
+
DEFAULT_BOUNTY_REPOS: () => DEFAULT_BOUNTY_REPOS,
|
|
1208
1614
|
DEFAULT_GREENHOUSE_SLUGS: () => DEFAULT_GREENHOUSE_SLUGS,
|
|
1209
1615
|
DEFAULT_LEVER_SLUGS: () => DEFAULT_LEVER_SLUGS,
|
|
1616
|
+
EXAMPLE_BUYER: () => EXAMPLE_BUYER,
|
|
1210
1617
|
FEEDS: () => FEEDS,
|
|
1618
|
+
GRAPH: () => GRAPH,
|
|
1211
1619
|
GREENHOUSE_SLUGS_BY_TIER: () => GREENHOUSE_SLUGS_BY_TIER,
|
|
1212
1620
|
LEVER_SLUGS_BY_TIER: () => LEVER_SLUGS_BY_TIER,
|
|
1213
1621
|
SYNONYMS: () => SYNONYMS,
|
|
1214
1622
|
VOCABULARY: () => VOCABULARY,
|
|
1623
|
+
VOCAB_NODES: () => VOCAB_NODES,
|
|
1215
1624
|
aggregate: () => aggregate,
|
|
1625
|
+
aggregateBounties: () => aggregateBounties,
|
|
1216
1626
|
ashby: () => ashby,
|
|
1627
|
+
buildGraph: () => buildGraph,
|
|
1217
1628
|
buildIndex: () => buildIndex,
|
|
1218
1629
|
buildReason: () => buildReason,
|
|
1630
|
+
expandWeighted: () => expandWeighted,
|
|
1219
1631
|
fetchGitHubProfile: () => fetchGitHubProfile,
|
|
1220
1632
|
flattenTiers: () => flattenTiers,
|
|
1633
|
+
getBuyer: () => getBuyer,
|
|
1634
|
+
githubBounties: () => githubBounties,
|
|
1221
1635
|
githubToFingerprint: () => githubToFingerprint,
|
|
1222
1636
|
greenhouse: () => greenhouse,
|
|
1223
1637
|
himalayas: () => himalayas,
|
|
1224
1638
|
hn: () => hn,
|
|
1639
|
+
isBounty: () => isBounty,
|
|
1225
1640
|
lever: () => lever,
|
|
1226
|
-
|
|
1641
|
+
loadPartnerRoles: () => loadPartnerRoles,
|
|
1227
1642
|
match: () => match,
|
|
1228
1643
|
matchOne: () => matchOne,
|
|
1229
1644
|
normalize: () => normalize,
|
|
1645
|
+
passesMaturityGate: () => passesMaturityGate,
|
|
1646
|
+
validateGraph: () => validateGraph,
|
|
1230
1647
|
wwr: () => wwr
|
|
1231
1648
|
});
|
|
1232
1649
|
var init_src = __esm({
|
|
@@ -1237,7 +1654,7 @@ var init_src = __esm({
|
|
|
1237
1654
|
init_matcher();
|
|
1238
1655
|
init_feeds();
|
|
1239
1656
|
init_indexer();
|
|
1240
|
-
|
|
1657
|
+
init_partners();
|
|
1241
1658
|
init_github();
|
|
1242
1659
|
}
|
|
1243
1660
|
});
|
|
@@ -1336,10 +1753,10 @@ function migrateTagWeights(profile) {
|
|
|
1336
1753
|
if (!profile.tagWeights) {
|
|
1337
1754
|
profile.tagWeights = {};
|
|
1338
1755
|
}
|
|
1339
|
-
const
|
|
1756
|
+
const seed = profile.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1340
1757
|
for (const tag of profile.skillTags) {
|
|
1341
1758
|
if (!profile.tagWeights[tag]) {
|
|
1342
|
-
profile.tagWeights[tag] = { count: 1, firstSeen:
|
|
1759
|
+
profile.tagWeights[tag] = { count: 1, firstSeen: seed, lastSeen: seed, sessions: 1 };
|
|
1343
1760
|
}
|
|
1344
1761
|
}
|
|
1345
1762
|
}
|
|
@@ -1365,7 +1782,7 @@ async function writeProfile(profile) {
|
|
|
1365
1782
|
const blob = encrypt(JSON.stringify(profile), key);
|
|
1366
1783
|
writeFileSync(PROFILE_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
|
|
1367
1784
|
}
|
|
1368
|
-
function accumulateSession(profile, tags, isEmployerContext, inferredSeniority) {
|
|
1785
|
+
function accumulateSession(profile, tags, isEmployerContext, inferredSeniority, seniorityIsAuthoritative = false) {
|
|
1369
1786
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1370
1787
|
let filtered = normalize(tags);
|
|
1371
1788
|
if (isEmployerContext) {
|
|
@@ -1383,7 +1800,9 @@ function accumulateSession(profile, tags, isEmployerContext, inferredSeniority)
|
|
|
1383
1800
|
}
|
|
1384
1801
|
}
|
|
1385
1802
|
if (inferredSeniority && !isEmployerContext) {
|
|
1386
|
-
profile.
|
|
1803
|
+
if (seniorityIsAuthoritative || !profile.github) {
|
|
1804
|
+
profile.seniority = inferredSeniority;
|
|
1805
|
+
}
|
|
1387
1806
|
}
|
|
1388
1807
|
}
|
|
1389
1808
|
async function accumulateTags(rawTokens, isEmployerContext, inferredSeniority) {
|
|
@@ -1391,12 +1810,14 @@ async function accumulateTags(rawTokens, isEmployerContext, inferredSeniority) {
|
|
|
1391
1810
|
accumulateSession(profile, rawTokens, isEmployerContext, inferredSeniority);
|
|
1392
1811
|
await writeProfile(profile);
|
|
1393
1812
|
}
|
|
1394
|
-
function accumulateGitHubTags(profile, tags) {
|
|
1813
|
+
function accumulateGitHubTags(profile, tags, inferredSeniority) {
|
|
1395
1814
|
accumulateSession(
|
|
1396
1815
|
profile,
|
|
1397
1816
|
tags,
|
|
1398
1817
|
/* isEmployerContext */
|
|
1399
|
-
false
|
|
1818
|
+
false,
|
|
1819
|
+
inferredSeniority,
|
|
1820
|
+
true
|
|
1400
1821
|
);
|
|
1401
1822
|
}
|
|
1402
1823
|
async function listSavedJobs() {
|
|
@@ -1488,7 +1909,7 @@ var __dirname = fileURLToPath2(new URL(".", import.meta.url));
|
|
|
1488
1909
|
var TERMINALHIRE_DIR2 = join3(homedir2(), ".terminalhire");
|
|
1489
1910
|
var INDEX_CACHE_FILE = join3(TERMINALHIRE_DIR2, "index-cache.json");
|
|
1490
1911
|
var INDEX_TTL_MS = 15 * 60 * 1e3;
|
|
1491
|
-
var API_URL = process.env["TERMINALHIRE_API_URL"] ?? process.env["
|
|
1912
|
+
var API_URL = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
|
|
1492
1913
|
var DEFAULT_LIMIT = 10;
|
|
1493
1914
|
var args = process.argv.slice(2);
|
|
1494
1915
|
var limitArg = args.indexOf("--limit");
|
|
@@ -1656,7 +2077,9 @@ async function run() {
|
|
|
1656
2077
|
}
|
|
1657
2078
|
console.log(`Fetching job index from ${API_URL}/api/index...`);
|
|
1658
2079
|
const index = await fetchIndex();
|
|
1659
|
-
const
|
|
2080
|
+
const allListings = index.jobs ?? [];
|
|
2081
|
+
const jobs = allListings.filter((j) => j.source !== "bounty");
|
|
2082
|
+
const bountyCount = allListings.length - jobs.length;
|
|
1660
2083
|
if (jobs.length === 0) {
|
|
1661
2084
|
console.log("No jobs in index. Try again later.");
|
|
1662
2085
|
return;
|
|
@@ -1682,6 +2105,12 @@ async function run() {
|
|
|
1682
2105
|
for (let i = 0; i < results.length; i++) {
|
|
1683
2106
|
printResult(i, results[i]);
|
|
1684
2107
|
}
|
|
2108
|
+
if (bountyCount > 0) {
|
|
2109
|
+
console.log(
|
|
2110
|
+
`
|
|
2111
|
+
\u26A1 ${bountyCount} bount${bountyCount === 1 ? "y" : "ies"} you could knock out today \u2014 run: terminalhire bounties`
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
1685
2114
|
if (!process.stdin.isTTY) {
|
|
1686
2115
|
return;
|
|
1687
2116
|
}
|