terminalhire 0.5.0 → 0.7.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 +286 -1
- package/dist/bin/jpi-claim.js +249 -10
- package/dist/bin/jpi-devs.js +3309 -0
- package/dist/bin/jpi-dispatch.js +1778 -296
- package/dist/bin/jpi-intro.js +1400 -0
- package/dist/bin/jpi-jobs.js +286 -1
- package/dist/bin/jpi-learn.js +27 -0
- package/dist/bin/jpi-login.js +286 -1
- package/dist/bin/jpi-profile.js +27 -0
- package/dist/bin/jpi-project.js +2995 -0
- package/dist/bin/jpi-refresh.js +286 -1
- package/dist/bin/jpi-save.js +27 -0
- package/dist/bin/jpi-sync.js +27 -0
- package/dist/bin/jpi-trajectory.js +1339 -75
- package/dist/src/intro.js +1348 -0
- package/dist/src/profile.js +12 -0
- package/dist/src/signal.js +12 -0
- package/dist/src/trajectory.js +1341 -74
- package/package.json +1 -1
|
@@ -0,0 +1,3309 @@
|
|
|
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 }, { to: "data-engineering", w: 0.4 }] },
|
|
151
|
+
{ id: "llm", parents: ["ml"], synonyms: ["llms", "genai", "generative-ai", "gpt"], 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 }, { to: "data-engineering", w: 0.45 }, { to: "spark", w: 0.4 }] },
|
|
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
|
+
{ id: "agents", parents: ["llm"], synonyms: ["agentic", "ai-agents", "multi-agent"], related: [{ to: "rag", w: 0.4 }] },
|
|
165
|
+
{ id: "mcp", parents: ["agents"], synonyms: ["model-context-protocol"], related: [{ to: "llm", w: 0.45 }] },
|
|
166
|
+
{ id: "inference", parents: ["ml"], synonyms: ["model-inference", "llm-inference", "model-serving"], related: [{ to: "mlops", w: 0.5 }, { to: "llm", w: 0.4 }] },
|
|
167
|
+
{ id: "embeddings", parents: ["ml"], synonyms: ["embedding", "vector-embeddings"], related: [{ to: "rag", w: 0.55 }, { to: "llm", w: 0.45 }] },
|
|
168
|
+
{ id: "prompt-engineering", parents: ["llm"], synonyms: ["prompting", "prompt"] },
|
|
169
|
+
{ id: "fine-tuning", parents: ["ml"], synonyms: ["finetuning", "fine-tune", "rlhf"], related: [{ to: "llm", w: 0.5 }] },
|
|
170
|
+
{ id: "computer-vision", parents: ["ml"], synonyms: ["image-recognition", "object-detection"] },
|
|
171
|
+
{ id: "recsys", parents: ["ml"], synonyms: ["recommender-systems", "recommendation-systems", "recommendation"] },
|
|
172
|
+
// ── Mobile ──────────────────────────────────────────────────────────────────
|
|
173
|
+
{ id: "mobile", related: [{ to: "ios", w: 0.5 }, { to: "android", w: 0.5 }] },
|
|
174
|
+
{ id: "ios", parents: ["mobile", "swift"], related: [{ to: "android", w: 0.4 }] },
|
|
175
|
+
{ id: "android", parents: ["mobile"], related: [{ to: "kotlin", w: 0.4 }] },
|
|
176
|
+
{ id: "swiftui", parents: ["ios", "swift"] },
|
|
177
|
+
{ id: "react-native", parents: ["mobile", "react"], synonyms: ["reactnative"], related: [{ to: "flutter", w: 0.4 }, { to: "expo", w: 0.6 }] },
|
|
178
|
+
{ id: "flutter", parents: ["mobile", "dart"] },
|
|
179
|
+
{ id: "expo", parents: ["react-native"] },
|
|
180
|
+
{ id: "kotlin-multiplatform", parents: ["mobile", "kotlin"], synonyms: ["kmp"] },
|
|
181
|
+
// ── Domains / capabilities ──────────────────────────────────────────────────
|
|
182
|
+
{ id: "frontend", related: [{ to: "react", w: 0.4 }, { to: "css", w: 0.3 }] },
|
|
183
|
+
{ id: "backend", related: [{ to: "api-design", w: 0.4 }, { to: "microservices", w: 0.4 }] },
|
|
184
|
+
{ id: "devops", related: [{ to: "kubernetes", w: 0.4 }, { to: "ci-cd", w: 0.4 }, { to: "docker", w: 0.4 }] },
|
|
185
|
+
{ id: "authentication", synonyms: ["auth", "jwt", "saml", "passport", "auth0", "clerk", "nextauth"], related: [{ to: "oauth", w: 0.6 }, { to: "security", w: 0.5 }] },
|
|
186
|
+
{ id: "oauth", parents: ["authentication"], synonyms: ["oauth2", "oidc"], related: [{ to: "security", w: 0.4 }] },
|
|
187
|
+
{ id: "security", related: [{ to: "authentication", w: 0.5 }] },
|
|
188
|
+
{ id: "payments", synonyms: ["stripe", "braintree", "paddle", "lemonsqueezy", "@stripe/stripe-js"], related: [{ to: "billing", w: 0.6 }] },
|
|
189
|
+
{ id: "billing", synonyms: ["recurly", "chargebee"] },
|
|
190
|
+
{ id: "api-design", synonyms: ["rest", "restful", "rest-api"], related: [{ to: "graphql", w: 0.4 }, { to: "grpc", w: 0.4 }, { to: "backend", w: 0.4 }] },
|
|
191
|
+
{ id: "graphql", synonyms: ["gql"], related: [{ to: "trpc", w: 0.4 }] },
|
|
192
|
+
{ id: "trpc", related: [{ to: "graphql", w: 0.4 }] },
|
|
193
|
+
{ id: "grpc", synonyms: ["grpc-web"], related: [{ to: "microservices", w: 0.3 }] },
|
|
194
|
+
{ id: "microservices" },
|
|
195
|
+
{ id: "websockets", synonyms: ["ws", "socket.io"], related: [{ to: "realtime", w: 0.6 }] },
|
|
196
|
+
{ id: "realtime", synonyms: ["real-time"] },
|
|
197
|
+
{ id: "message-queue", synonyms: ["mq"] },
|
|
198
|
+
{ id: "caching", synonyms: ["cache"] },
|
|
199
|
+
{ id: "search", synonyms: ["full-text-search"] },
|
|
200
|
+
{ id: "observability", synonyms: ["o11y"], related: [{ to: "monitoring", w: 0.6 }] },
|
|
201
|
+
{ id: "monitoring", related: [{ to: "prometheus", w: 0.4 }] },
|
|
202
|
+
{ id: "testing", related: [{ to: "unit-testing", w: 0.5 }, { to: "e2e-testing", w: 0.5 }] },
|
|
203
|
+
{ id: "unit-testing", parents: ["testing"] },
|
|
204
|
+
{ id: "e2e-testing", parents: ["testing"], synonyms: ["e2e", "end-to-end-testing"] },
|
|
205
|
+
{ id: "jest", parents: ["testing"], related: [{ to: "vitest", w: 0.6 }, { to: "mocha", w: 0.5 }] },
|
|
206
|
+
{ id: "vitest", parents: ["testing"], related: [{ to: "jest", w: 0.6 }] },
|
|
207
|
+
{ id: "playwright", parents: ["e2e-testing"], related: [{ to: "cypress", w: 0.6 }] },
|
|
208
|
+
{ id: "cypress", parents: ["e2e-testing"] },
|
|
209
|
+
{ id: "mocha", parents: ["testing"] },
|
|
210
|
+
{ id: "pytest", parents: ["testing", "python"] },
|
|
211
|
+
{ id: "accessibility", synonyms: ["a11y"] },
|
|
212
|
+
{ id: "seo" },
|
|
213
|
+
{ id: "performance", synonyms: ["perf", "web-performance"] }
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ../../packages/core/src/vocab/closure.ts
|
|
219
|
+
function round3(n) {
|
|
220
|
+
return Math.round(n * 1e3) / 1e3;
|
|
221
|
+
}
|
|
222
|
+
function validateGraph(nodes) {
|
|
223
|
+
const ids = /* @__PURE__ */ new Set();
|
|
224
|
+
for (const n of nodes) {
|
|
225
|
+
if (ids.has(n.id)) throw new Error(`vocab: duplicate id "${n.id}"`);
|
|
226
|
+
ids.add(n.id);
|
|
227
|
+
}
|
|
228
|
+
const seenAlias = /* @__PURE__ */ new Map();
|
|
229
|
+
for (const n of nodes) {
|
|
230
|
+
for (const p of n.parents ?? []) {
|
|
231
|
+
if (p === n.id) throw new Error(`vocab: "${n.id}" lists itself as a parent`);
|
|
232
|
+
if (!ids.has(p)) throw new Error(`vocab: "${n.id}" parent "${p}" is not a defined id`);
|
|
233
|
+
}
|
|
234
|
+
for (const e of n.related ?? []) {
|
|
235
|
+
if (e.to === n.id) throw new Error(`vocab: "${n.id}" relates to itself`);
|
|
236
|
+
if (!ids.has(e.to)) throw new Error(`vocab: "${n.id}" related "${e.to}" is not a defined id`);
|
|
237
|
+
if (!(e.w > 0 && e.w <= 1)) throw new Error(`vocab: "${n.id}"\u2192"${e.to}" weight ${e.w} out of (0,1]`);
|
|
238
|
+
}
|
|
239
|
+
for (const s of n.synonyms ?? []) {
|
|
240
|
+
const alias = s.toLowerCase();
|
|
241
|
+
if (ids.has(alias)) throw new Error(`vocab: synonym "${alias}" collides with a canonical id`);
|
|
242
|
+
const prev = seenAlias.get(alias);
|
|
243
|
+
if (prev && prev !== n.id) throw new Error(`vocab: synonym "${alias}" maps to both "${prev}" and "${n.id}"`);
|
|
244
|
+
seenAlias.set(alias, n.id);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
248
|
+
const done = /* @__PURE__ */ new Set();
|
|
249
|
+
const parentMap = new Map(nodes.map((n) => [n.id, n.parents ?? []]));
|
|
250
|
+
const walk = (id, path) => {
|
|
251
|
+
if (done.has(id)) return;
|
|
252
|
+
if (visiting.has(id)) throw new Error(`vocab: parent cycle ${[...path, id].join(" \u2192 ")}`);
|
|
253
|
+
visiting.add(id);
|
|
254
|
+
for (const p of parentMap.get(id) ?? []) walk(p, [...path, id]);
|
|
255
|
+
visiting.delete(id);
|
|
256
|
+
done.add(id);
|
|
257
|
+
};
|
|
258
|
+
for (const n of nodes) walk(n.id, []);
|
|
259
|
+
}
|
|
260
|
+
function buildAdjacency(nodes) {
|
|
261
|
+
const adj = /* @__PURE__ */ new Map();
|
|
262
|
+
const add = (from, to, w) => {
|
|
263
|
+
let m = adj.get(from);
|
|
264
|
+
if (!m) adj.set(from, m = /* @__PURE__ */ new Map());
|
|
265
|
+
if (w > (m.get(to) ?? 0)) m.set(to, w);
|
|
266
|
+
};
|
|
267
|
+
for (const n of nodes) {
|
|
268
|
+
for (const p of n.parents ?? []) {
|
|
269
|
+
add(n.id, p, PARENT_UP);
|
|
270
|
+
add(p, n.id, PARENT_DOWN);
|
|
271
|
+
}
|
|
272
|
+
for (const e of n.related ?? []) {
|
|
273
|
+
add(n.id, e.to, e.w);
|
|
274
|
+
add(e.to, n.id, e.w);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return adj;
|
|
278
|
+
}
|
|
279
|
+
function closureFrom(source, adj) {
|
|
280
|
+
const best = /* @__PURE__ */ new Map();
|
|
281
|
+
for (const [t, w] of adj.get(source) ?? []) {
|
|
282
|
+
if (w >= DECAY_FLOOR) best.set(t, { w: round3(w), via: t });
|
|
283
|
+
}
|
|
284
|
+
const settled = /* @__PURE__ */ new Set([source]);
|
|
285
|
+
while (true) {
|
|
286
|
+
let u;
|
|
287
|
+
let uw = 0;
|
|
288
|
+
for (const [t, e] of best) {
|
|
289
|
+
if (!settled.has(t) && e.w > uw) {
|
|
290
|
+
u = t;
|
|
291
|
+
uw = e.w;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (!u) break;
|
|
295
|
+
settled.add(u);
|
|
296
|
+
const via = best.get(u).via;
|
|
297
|
+
for (const [t, we] of adj.get(u) ?? []) {
|
|
298
|
+
if (settled.has(t)) continue;
|
|
299
|
+
const cand = round3(uw * we);
|
|
300
|
+
if (cand >= DECAY_FLOOR && cand > (best.get(t)?.w ?? 0)) {
|
|
301
|
+
best.set(t, { w: cand, via });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
best.delete(source);
|
|
306
|
+
return best;
|
|
307
|
+
}
|
|
308
|
+
function buildGraph(nodes) {
|
|
309
|
+
validateGraph(nodes);
|
|
310
|
+
const ids = new Set(nodes.map((n) => n.id));
|
|
311
|
+
const synonyms = /* @__PURE__ */ new Map();
|
|
312
|
+
for (const n of nodes) {
|
|
313
|
+
for (const s of n.synonyms ?? []) synonyms.set(s.toLowerCase(), n.id);
|
|
314
|
+
}
|
|
315
|
+
const adj = buildAdjacency(nodes);
|
|
316
|
+
const closure = /* @__PURE__ */ new Map();
|
|
317
|
+
for (const n of nodes) closure.set(n.id, closureFrom(n.id, adj));
|
|
318
|
+
return { ids, synonyms, closure };
|
|
319
|
+
}
|
|
320
|
+
var PARENT_UP, PARENT_DOWN, DECAY_FLOOR;
|
|
321
|
+
var init_closure = __esm({
|
|
322
|
+
"../../packages/core/src/vocab/closure.ts"() {
|
|
323
|
+
"use strict";
|
|
324
|
+
PARENT_UP = 0.6;
|
|
325
|
+
PARENT_DOWN = 0.35;
|
|
326
|
+
DECAY_FLOOR = 0.25;
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// ../../packages/core/src/vocab/types.ts
|
|
331
|
+
var init_types2 = __esm({
|
|
332
|
+
"../../packages/core/src/vocab/types.ts"() {
|
|
333
|
+
"use strict";
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ../../packages/core/src/vocab/extract.ts
|
|
338
|
+
function tokenize(text) {
|
|
339
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
340
|
+
}
|
|
341
|
+
function looksLikeEngRole(title) {
|
|
342
|
+
return !NON_ENG_TITLE.test(title) && ENG_INTENT.test(title);
|
|
343
|
+
}
|
|
344
|
+
function resolveToken(token) {
|
|
345
|
+
const tryOne = (t) => {
|
|
346
|
+
if (GRAPH.ids.has(t)) return { id: t, viaSynonym: false };
|
|
347
|
+
const mapped = GRAPH.synonyms.get(t);
|
|
348
|
+
return mapped ? { id: mapped, viaSynonym: true } : null;
|
|
349
|
+
};
|
|
350
|
+
return tryOne(token) ?? tryOne(token.replace(/^[.\-+#]+|[.\-+#]+$/g, ""));
|
|
351
|
+
}
|
|
352
|
+
function extractSkillTags(title, body = "") {
|
|
353
|
+
if (!looksLikeEngRole(title)) return [];
|
|
354
|
+
const text = `${title}
|
|
355
|
+
${body}`;
|
|
356
|
+
const tokens = tokenize(text);
|
|
357
|
+
const ids = /* @__PURE__ */ new Set();
|
|
358
|
+
const ambiguousPending = /* @__PURE__ */ new Set();
|
|
359
|
+
for (const tok of tokens) {
|
|
360
|
+
const r = resolveToken(tok);
|
|
361
|
+
if (!r) continue;
|
|
362
|
+
if (NON_EXTRACTABLE.has(r.id)) continue;
|
|
363
|
+
if (SYNONYM_ONLY.has(r.id) && !r.viaSynonym) continue;
|
|
364
|
+
const cue = AMBIGUOUS[r.id];
|
|
365
|
+
if (cue) {
|
|
366
|
+
if (cue.test(text)) ids.add(r.id);
|
|
367
|
+
else ambiguousPending.add(r.id);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
ids.add(r.id);
|
|
371
|
+
}
|
|
372
|
+
const hardCount = [...ids].filter((id) => !SOFT_DOMAIN.has(id)).length;
|
|
373
|
+
if (hardCount >= 2) for (const id of ambiguousPending) ids.add(id);
|
|
374
|
+
return [...ids];
|
|
375
|
+
}
|
|
376
|
+
function coreTagsFromTitle(title) {
|
|
377
|
+
return extractSkillTags(title, "").filter((t) => !SOFT_DOMAIN.has(t));
|
|
378
|
+
}
|
|
379
|
+
var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, ENG_INTENT, NON_ENG_TITLE;
|
|
380
|
+
var init_extract = __esm({
|
|
381
|
+
"../../packages/core/src/vocab/extract.ts"() {
|
|
382
|
+
"use strict";
|
|
383
|
+
init_vocab();
|
|
384
|
+
SOFT_DOMAIN = /* @__PURE__ */ new Set([
|
|
385
|
+
"frontend",
|
|
386
|
+
"backend",
|
|
387
|
+
"devops",
|
|
388
|
+
"security",
|
|
389
|
+
"payments",
|
|
390
|
+
"billing",
|
|
391
|
+
"microservices",
|
|
392
|
+
"caching",
|
|
393
|
+
"search",
|
|
394
|
+
"observability",
|
|
395
|
+
"monitoring",
|
|
396
|
+
"testing",
|
|
397
|
+
"accessibility",
|
|
398
|
+
"seo",
|
|
399
|
+
"performance",
|
|
400
|
+
"realtime",
|
|
401
|
+
"authentication",
|
|
402
|
+
"api-design"
|
|
403
|
+
]);
|
|
404
|
+
SYNONYM_ONLY = /* @__PURE__ */ new Set(["performance", "security", "seo"]);
|
|
405
|
+
NON_EXTRACTABLE = /* @__PURE__ */ new Set(["payments", "billing"]);
|
|
406
|
+
for (const id of SYNONYM_ONLY) {
|
|
407
|
+
if (!SOFT_DOMAIN.has(id)) throw new Error(`extract: SYNONYM_ONLY "${id}" not in SOFT_DOMAIN`);
|
|
408
|
+
}
|
|
409
|
+
AMBIGUOUS = {
|
|
410
|
+
// Accept "go" with an ecosystem cue OR an explicit-skill phrasing ("Go developer",
|
|
411
|
+
// "in Go", "experience with Go"). Rejects prose: "ready to go", "go above", "go live".
|
|
412
|
+
go: /\b(golang|goroutines?|go\.mod|gin framework|gorm)\b|\bgo\b\s+(developer|engineer|programmer|microservices?|backend|services?|lang)|\b(in|with|using|written in|built in|experience (?:in|with)|proficient in|fluent in)\s+go\b/i,
|
|
413
|
+
r: /\b(rstudio|tidyverse|ggplot|shiny|dplyr|cran|r-lang|rlang)\b/i,
|
|
414
|
+
ml: /\b(machine[\s-]?learning|pytorch|tensorflow|scikit|sklearn|keras|neural|model training|deep[\s-]?learning|numpy|pandas|ml\s+(?:engineer|platform|researcher|infrastructure)|(?:ml|ai)\s+research)\b/i
|
|
415
|
+
};
|
|
416
|
+
ENG_INTENT = /\b(engineer|engineering|developer|dev\b|swe|sde|programmer|architect|full[\s-]?stack|front[\s-]?end|back[\s-]?end|devops|sre|software|coding|codebase|technical staff|tech(?:nical)? lead)\b/i;
|
|
417
|
+
NON_ENG_TITLE = /\b(account executive|account manager|sales (?:rep|representative|development|manager|lead)|sdr|bdr|recruiter|recruiting|talent|marketing|administrative|business partner|billing coordinator|operations (?:administrator|coordinator)|customer success|project finance|controller|bookkeeper|graphic|brand)\b/i;
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// ../../packages/core/src/vocab/idf-background.ts
|
|
422
|
+
var IDF_BACKGROUND;
|
|
423
|
+
var init_idf_background = __esm({
|
|
424
|
+
"../../packages/core/src/vocab/idf-background.ts"() {
|
|
425
|
+
"use strict";
|
|
426
|
+
IDF_BACKGROUND = {
|
|
427
|
+
N: 244,
|
|
428
|
+
df: {
|
|
429
|
+
"backend": 71,
|
|
430
|
+
"python": 57,
|
|
431
|
+
"monitoring": 44,
|
|
432
|
+
"nextjs": 40,
|
|
433
|
+
"testing": 40,
|
|
434
|
+
"observability": 38,
|
|
435
|
+
"llm": 38,
|
|
436
|
+
"go": 36,
|
|
437
|
+
"aws": 36,
|
|
438
|
+
"react": 33,
|
|
439
|
+
"frontend": 30,
|
|
440
|
+
"ml": 28,
|
|
441
|
+
"mobile": 24,
|
|
442
|
+
"realtime": 24,
|
|
443
|
+
"typescript": 23,
|
|
444
|
+
"devops": 22,
|
|
445
|
+
"kubernetes": 22,
|
|
446
|
+
"javascript": 21,
|
|
447
|
+
"java": 20,
|
|
448
|
+
"rag": 20,
|
|
449
|
+
"api-design": 20,
|
|
450
|
+
"linux": 19,
|
|
451
|
+
"postgresql": 19,
|
|
452
|
+
"search": 17,
|
|
453
|
+
"azure": 16,
|
|
454
|
+
"snowflake": 15,
|
|
455
|
+
"spark": 15,
|
|
456
|
+
"kotlin": 14,
|
|
457
|
+
"gcp": 14,
|
|
458
|
+
"accessibility": 14,
|
|
459
|
+
"nodejs": 14,
|
|
460
|
+
"graphql": 14,
|
|
461
|
+
"airflow": 14,
|
|
462
|
+
"docker": 14,
|
|
463
|
+
"ci-cd": 13,
|
|
464
|
+
"android": 12,
|
|
465
|
+
"cpp": 12,
|
|
466
|
+
"gitlab-ci": 11,
|
|
467
|
+
"anthropic": 11,
|
|
468
|
+
"terraform": 11,
|
|
469
|
+
"mysql": 11,
|
|
470
|
+
"r": 10,
|
|
471
|
+
"dbt": 9,
|
|
472
|
+
"langchain": 9,
|
|
473
|
+
"pytorch": 9,
|
|
474
|
+
"ruby": 9,
|
|
475
|
+
"rails": 9,
|
|
476
|
+
"cloudflare": 7,
|
|
477
|
+
"datadog": 7,
|
|
478
|
+
"css": 7,
|
|
479
|
+
"ansible": 7,
|
|
480
|
+
"openai": 6,
|
|
481
|
+
"kafka": 6,
|
|
482
|
+
"rust": 5,
|
|
483
|
+
"grpc": 5,
|
|
484
|
+
"microservices": 5,
|
|
485
|
+
"serverless": 5,
|
|
486
|
+
"scala": 5,
|
|
487
|
+
"prometheus": 5,
|
|
488
|
+
"grafana": 5,
|
|
489
|
+
"php": 5,
|
|
490
|
+
"redis": 5,
|
|
491
|
+
"huggingface": 4,
|
|
492
|
+
"pandas": 4,
|
|
493
|
+
"scikit-learn": 4,
|
|
494
|
+
"html": 4,
|
|
495
|
+
"ios": 4,
|
|
496
|
+
"authentication": 4,
|
|
497
|
+
"vue": 4,
|
|
498
|
+
"mlops": 3,
|
|
499
|
+
"spring": 3,
|
|
500
|
+
"mongodb": 3,
|
|
501
|
+
"csharp": 3,
|
|
502
|
+
"swift": 2,
|
|
503
|
+
"caching": 2,
|
|
504
|
+
"haskell": 2,
|
|
505
|
+
"pulumi": 2,
|
|
506
|
+
"argocd": 2,
|
|
507
|
+
"tensorflow": 2,
|
|
508
|
+
"express": 2,
|
|
509
|
+
"elasticsearch": 2,
|
|
510
|
+
"clickhouse": 2,
|
|
511
|
+
"nestjs": 2,
|
|
512
|
+
"vite": 2,
|
|
513
|
+
"svelte": 2,
|
|
514
|
+
"phoenix": 2,
|
|
515
|
+
"angular": 2,
|
|
516
|
+
"django": 2,
|
|
517
|
+
"dotnet": 2,
|
|
518
|
+
"elixir": 2,
|
|
519
|
+
"bun": 1,
|
|
520
|
+
"oauth": 1,
|
|
521
|
+
"dynamodb": 1,
|
|
522
|
+
"helm": 1,
|
|
523
|
+
"playwright": 1,
|
|
524
|
+
"cypress": 1,
|
|
525
|
+
"jest": 1,
|
|
526
|
+
"mocha": 1,
|
|
527
|
+
"typeorm": 1,
|
|
528
|
+
"tailwind": 1,
|
|
529
|
+
"prisma": 1,
|
|
530
|
+
"expo": 1,
|
|
531
|
+
"rabbitmq": 1,
|
|
532
|
+
"redux": 1
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// ../../packages/core/src/vocab/index.ts
|
|
539
|
+
function normalize(tokens) {
|
|
540
|
+
const result = /* @__PURE__ */ new Set();
|
|
541
|
+
for (const raw of tokens) {
|
|
542
|
+
const lower = raw.toLowerCase().trim();
|
|
543
|
+
if (GRAPH.ids.has(lower)) {
|
|
544
|
+
result.add(lower);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
const mapped = GRAPH.synonyms.get(lower);
|
|
548
|
+
if (mapped) result.add(mapped);
|
|
549
|
+
}
|
|
550
|
+
return Array.from(result);
|
|
551
|
+
}
|
|
552
|
+
function expandWeighted(tags, graph = GRAPH) {
|
|
553
|
+
const out = /* @__PURE__ */ new Map();
|
|
554
|
+
const put = (tag, weight, via) => {
|
|
555
|
+
const ex = out.get(tag);
|
|
556
|
+
if (!ex || weight > ex.weight) out.set(tag, { tag, weight, via });
|
|
557
|
+
};
|
|
558
|
+
for (const t of tags) {
|
|
559
|
+
put(t, 1, t);
|
|
560
|
+
const near = graph.closure.get(t);
|
|
561
|
+
if (near) for (const [n, edge] of near) put(n, edge.w, t);
|
|
562
|
+
}
|
|
563
|
+
return out;
|
|
564
|
+
}
|
|
565
|
+
var GRAPH, VOCABULARY, SYNONYMS;
|
|
566
|
+
var init_vocab = __esm({
|
|
567
|
+
"../../packages/core/src/vocab/index.ts"() {
|
|
568
|
+
"use strict";
|
|
569
|
+
init_graph_data();
|
|
570
|
+
init_closure();
|
|
571
|
+
init_types2();
|
|
572
|
+
init_closure();
|
|
573
|
+
init_graph_data();
|
|
574
|
+
init_extract();
|
|
575
|
+
init_idf_background();
|
|
576
|
+
GRAPH = buildGraph(VOCAB_NODES);
|
|
577
|
+
VOCABULARY = [...GRAPH.ids];
|
|
578
|
+
SYNONYMS = Object.fromEntries(GRAPH.synonyms);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// ../../packages/core/src/vocabulary.ts
|
|
583
|
+
var init_vocabulary = __esm({
|
|
584
|
+
"../../packages/core/src/vocabulary.ts"() {
|
|
585
|
+
"use strict";
|
|
586
|
+
init_vocab();
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// ../../packages/core/src/github.ts
|
|
591
|
+
function ghHeaders(token) {
|
|
592
|
+
const headers = {
|
|
593
|
+
Accept: "application/vnd.github+json",
|
|
594
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
595
|
+
// GitHub's REST API REQUIRES a User-Agent; serverless runtimes don't always
|
|
596
|
+
// send a default (omitting it yields a 403 "administrative rules" error).
|
|
597
|
+
"User-Agent": "terminalhire"
|
|
598
|
+
};
|
|
599
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
600
|
+
return headers;
|
|
601
|
+
}
|
|
602
|
+
async function ghFetch(path, token) {
|
|
603
|
+
const url = `https://api.github.com${path}`;
|
|
604
|
+
const res = await fetch(url, { headers: ghHeaders(token) });
|
|
605
|
+
if (!res.ok) {
|
|
606
|
+
throw new Error(`GitHub API ${path}: HTTP ${res.status} ${res.statusText}`);
|
|
607
|
+
}
|
|
608
|
+
return res.json();
|
|
609
|
+
}
|
|
610
|
+
async function fetchGitHubProfile(login, token) {
|
|
611
|
+
const user = await ghFetch(`/users/${login}`, token);
|
|
612
|
+
let repos = [];
|
|
613
|
+
try {
|
|
614
|
+
repos = await ghFetch(
|
|
615
|
+
`/users/${login}/repos?sort=pushed&per_page=100`,
|
|
616
|
+
token
|
|
617
|
+
);
|
|
618
|
+
} catch (err) {
|
|
619
|
+
console.warn(`[github] ${login}: repos fetch failed, continuing \u2014`, err);
|
|
620
|
+
}
|
|
621
|
+
const langCount = {};
|
|
622
|
+
for (const repo of repos) {
|
|
623
|
+
if (repo.fork) continue;
|
|
624
|
+
if (repo.language) {
|
|
625
|
+
langCount[repo.language.toLowerCase()] = (langCount[repo.language.toLowerCase()] ?? 0) + 1;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const topLanguages = Object.entries(langCount).sort(([, a], [, b]) => b - a).slice(0, 10).map(([lang]) => lang);
|
|
629
|
+
const topicSet = /* @__PURE__ */ new Set();
|
|
630
|
+
for (const repo of repos) {
|
|
631
|
+
if (repo.fork) continue;
|
|
632
|
+
for (const t of repo.topics ?? []) topicSet.add(t.toLowerCase());
|
|
633
|
+
}
|
|
634
|
+
const topics = Array.from(topicSet).slice(0, 30);
|
|
635
|
+
let recentPRorgs;
|
|
636
|
+
try {
|
|
637
|
+
const q = encodeURIComponent(
|
|
638
|
+
`type:pr is:merged author:${login} sort:updated`
|
|
639
|
+
);
|
|
640
|
+
const result = await ghFetch(
|
|
641
|
+
`/search/issues?q=${q}&per_page=30`,
|
|
642
|
+
token
|
|
643
|
+
);
|
|
644
|
+
const orgs = /* @__PURE__ */ new Set();
|
|
645
|
+
for (const item of result.items ?? []) {
|
|
646
|
+
const orgLogin = item.repository?.owner?.login;
|
|
647
|
+
if (orgLogin && orgLogin !== login) orgs.add(orgLogin);
|
|
648
|
+
}
|
|
649
|
+
if (orgs.size > 0) recentPRorgs = Array.from(orgs);
|
|
650
|
+
} catch {
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
login: user.login,
|
|
654
|
+
name: user.name ?? void 0,
|
|
655
|
+
publicEmail: user.email ?? void 0,
|
|
656
|
+
avatarUrl: user.avatar_url,
|
|
657
|
+
accountCreatedAt: user.created_at,
|
|
658
|
+
publicRepos: user.public_repos,
|
|
659
|
+
followers: user.followers,
|
|
660
|
+
topLanguages,
|
|
661
|
+
topics,
|
|
662
|
+
recentPRorgs
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
function inferSeniority(p) {
|
|
666
|
+
const ageMs = Date.now() - new Date(p.accountCreatedAt).getTime();
|
|
667
|
+
const ageYears = ageMs / (1e3 * 60 * 60 * 24 * 365.25);
|
|
668
|
+
if (ageYears >= 9 && (p.publicRepos >= 40 || p.followers >= 500)) return "staff";
|
|
669
|
+
if (ageYears >= 5 && (p.publicRepos >= 20 || p.followers >= 100)) return "senior";
|
|
670
|
+
if (ageYears >= 2 && p.publicRepos >= 5) return "mid";
|
|
671
|
+
return "junior";
|
|
672
|
+
}
|
|
673
|
+
function githubToFingerprint(p) {
|
|
674
|
+
const rawTokens = [
|
|
675
|
+
...p.topLanguages,
|
|
676
|
+
...p.topics
|
|
677
|
+
// recentPRorgs intentionally excluded — org names are not skill tags
|
|
678
|
+
];
|
|
679
|
+
const skillTags = normalize(rawTokens);
|
|
680
|
+
const seniorityBand = inferSeniority(p);
|
|
681
|
+
return { skillTags, seniorityBand };
|
|
682
|
+
}
|
|
683
|
+
async function fetchOwnedRepoTraction(login, token) {
|
|
684
|
+
const computedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
685
|
+
const empty = (status) => ({
|
|
686
|
+
status,
|
|
687
|
+
totalStars: 0,
|
|
688
|
+
totalForks: 0,
|
|
689
|
+
reposWithStars: 0,
|
|
690
|
+
top: [],
|
|
691
|
+
computedAt
|
|
692
|
+
});
|
|
693
|
+
let repos;
|
|
694
|
+
try {
|
|
695
|
+
repos = await ghFetch(
|
|
696
|
+
`/users/${login}/repos?sort=pushed&per_page=100`,
|
|
697
|
+
token
|
|
698
|
+
);
|
|
699
|
+
} catch (err) {
|
|
700
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
701
|
+
const status = /HTTP 403|HTTP 429|rate limit/i.test(msg) ? "rate-limited" : "failed";
|
|
702
|
+
console.warn(`[github] ${login}: traction fetch failed (${status}) \u2014`, msg);
|
|
703
|
+
return empty(status);
|
|
704
|
+
}
|
|
705
|
+
if (!Array.isArray(repos)) return empty("failed");
|
|
706
|
+
const owned = repos.filter((r) => r && !r.fork && !r.archived);
|
|
707
|
+
let totalStars = 0;
|
|
708
|
+
let totalForks = 0;
|
|
709
|
+
let reposWithStars = 0;
|
|
710
|
+
const ranked = [];
|
|
711
|
+
for (const r of owned) {
|
|
712
|
+
const stars = typeof r.stargazers_count === "number" ? r.stargazers_count : 0;
|
|
713
|
+
const forks = typeof r.forks_count === "number" ? r.forks_count : 0;
|
|
714
|
+
totalStars += stars;
|
|
715
|
+
totalForks += forks;
|
|
716
|
+
if (stars >= 1) reposWithStars++;
|
|
717
|
+
ranked.push({ name: r.name, stars, forks });
|
|
718
|
+
}
|
|
719
|
+
ranked.sort((a, b) => b.stars - a.stars || b.forks - a.forks);
|
|
720
|
+
return {
|
|
721
|
+
status: "ok",
|
|
722
|
+
totalStars,
|
|
723
|
+
totalForks,
|
|
724
|
+
reposWithStars,
|
|
725
|
+
top: ranked.slice(0, TRACTION_TOP_N),
|
|
726
|
+
computedAt
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
async function ghFetchRaw(path, token) {
|
|
730
|
+
return fetch(`https://api.github.com${path}`, { headers: ghHeaders(token) });
|
|
731
|
+
}
|
|
732
|
+
function parseRepoUrl(repoUrl) {
|
|
733
|
+
const m = repoUrl.match(/\/repos\/([^/]+)\/([^/]+)\/?$/);
|
|
734
|
+
return m ? { owner: m[1], name: m[2] } : null;
|
|
735
|
+
}
|
|
736
|
+
function isTrivialPRTitle(title) {
|
|
737
|
+
return TRIVIAL_PR_TITLE.test(title);
|
|
738
|
+
}
|
|
739
|
+
async function fetchOwnedOrgs(token) {
|
|
740
|
+
try {
|
|
741
|
+
const memberships = await ghFetch(`/user/memberships/orgs?per_page=100`, token);
|
|
742
|
+
return new Set(
|
|
743
|
+
memberships.filter((m) => m.role === "admin").map((m) => m.organization.login.toLowerCase())
|
|
744
|
+
);
|
|
745
|
+
} catch {
|
|
746
|
+
return /* @__PURE__ */ new Set();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
async function repoContributorCount(owner, name, token) {
|
|
750
|
+
try {
|
|
751
|
+
const res = await ghFetchRaw(
|
|
752
|
+
`/repos/${owner}/${name}/contributors?per_page=1&anon=false`,
|
|
753
|
+
token
|
|
754
|
+
);
|
|
755
|
+
if (!res.ok) return void 0;
|
|
756
|
+
const link = res.headers.get("link");
|
|
757
|
+
const m = link?.match(/[?&]page=(\d+)>;\s*rel="last"/);
|
|
758
|
+
if (m) return Number(m[1]);
|
|
759
|
+
const body = await res.json();
|
|
760
|
+
return Array.isArray(body) ? body.length : 0;
|
|
761
|
+
} catch {
|
|
762
|
+
return void 0;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
async function fetchRepoMeta(owner, name, token, cache) {
|
|
766
|
+
const key = `${owner}/${name}`.toLowerCase();
|
|
767
|
+
const cached = cache.get(key);
|
|
768
|
+
if (cached !== void 0) return cached;
|
|
769
|
+
let meta = null;
|
|
770
|
+
try {
|
|
771
|
+
const r = await ghFetch(`/repos/${owner}/${name}`, token);
|
|
772
|
+
const contributors = await repoContributorCount(owner, name, token);
|
|
773
|
+
meta = {
|
|
774
|
+
stars: r.stargazers_count ?? 0,
|
|
775
|
+
archived: !!r.archived,
|
|
776
|
+
fork: !!r.fork,
|
|
777
|
+
language: r.language ?? null,
|
|
778
|
+
topics: r.topics ?? [],
|
|
779
|
+
contributors
|
|
780
|
+
};
|
|
781
|
+
} catch {
|
|
782
|
+
meta = null;
|
|
783
|
+
}
|
|
784
|
+
cache.set(key, meta);
|
|
785
|
+
return meta;
|
|
786
|
+
}
|
|
787
|
+
function emptyCredential(status) {
|
|
788
|
+
return { status, byDomain: {}, qualifyingTotal: 0, computedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
789
|
+
}
|
|
790
|
+
async function fetchPublicOrgs(login, token) {
|
|
791
|
+
try {
|
|
792
|
+
const orgs = await ghFetch(
|
|
793
|
+
`/users/${login}/orgs?per_page=100`,
|
|
794
|
+
token
|
|
795
|
+
);
|
|
796
|
+
return new Set(orgs.map((o) => o.login.toLowerCase()));
|
|
797
|
+
} catch {
|
|
798
|
+
return /* @__PURE__ */ new Set();
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
async function computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates = {
|
|
802
|
+
minStars: MIN_STARS,
|
|
803
|
+
minContributors: MIN_CONTRIBUTORS
|
|
804
|
+
}) {
|
|
805
|
+
const computedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
806
|
+
const loginLc = login.toLowerCase();
|
|
807
|
+
let items;
|
|
808
|
+
try {
|
|
809
|
+
const q = encodeURIComponent(`type:pr is:merged author:${login} -user:${login} sort:updated`);
|
|
810
|
+
const res = await ghFetch(
|
|
811
|
+
`/search/issues?q=${q}&per_page=${CANDIDATE_PR_PAGE}`,
|
|
812
|
+
token
|
|
813
|
+
);
|
|
814
|
+
items = res.items ?? [];
|
|
815
|
+
} catch (err) {
|
|
816
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
817
|
+
console.warn("[acceptance] search failed:", msg);
|
|
818
|
+
return emptyCredential(/HTTP 403|HTTP 429|rate limit/i.test(msg) ? "rate-limited" : "failed");
|
|
819
|
+
}
|
|
820
|
+
const byDomain = {};
|
|
821
|
+
let qualifyingTotal = 0;
|
|
822
|
+
for (const item of items) {
|
|
823
|
+
const repo = parseRepoUrl(item.repository_url);
|
|
824
|
+
if (!repo) continue;
|
|
825
|
+
const ownerLc = repo.owner.toLowerCase();
|
|
826
|
+
if (ownerLc === loginLc) continue;
|
|
827
|
+
if (ownedOrgs.has(ownerLc)) continue;
|
|
828
|
+
if (isTrivialPRTitle(item.title)) continue;
|
|
829
|
+
const meta = await fetchRepoMeta(repo.owner, repo.name, token, cache);
|
|
830
|
+
if (!meta) continue;
|
|
831
|
+
if (meta.archived || meta.fork) continue;
|
|
832
|
+
if (meta.stars < gates.minStars) continue;
|
|
833
|
+
if (meta.contributors !== void 0 && meta.contributors < gates.minContributors) continue;
|
|
834
|
+
qualifyingTotal += 1;
|
|
835
|
+
const mergedAt = item.pull_request?.merged_at ?? item.closed_at ?? item.created_at;
|
|
836
|
+
const rawDomains = [meta.language ?? "", ...meta.topics].filter(Boolean);
|
|
837
|
+
for (const d of new Set(normalize(rawDomains))) {
|
|
838
|
+
const b = byDomain[d] ?? (byDomain[d] = { mergedPRs: 0, distinctOrgs: 0, lastMergedAt: mergedAt, orgs: /* @__PURE__ */ new Set() });
|
|
839
|
+
b.mergedPRs += 1;
|
|
840
|
+
b.orgs.add(ownerLc);
|
|
841
|
+
if (mergedAt > b.lastMergedAt) b.lastMergedAt = mergedAt;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
const finalDomains = {};
|
|
845
|
+
for (const [d, b] of Object.entries(byDomain)) {
|
|
846
|
+
finalDomains[d] = {
|
|
847
|
+
mergedPRs: b.mergedPRs,
|
|
848
|
+
distinctOrgs: b.orgs.size,
|
|
849
|
+
lastMergedAt: b.lastMergedAt
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
return { status: "ok", byDomain: finalDomains, qualifyingTotal, computedAt };
|
|
853
|
+
}
|
|
854
|
+
async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */ new Map()) {
|
|
855
|
+
if (!token) return emptyCredential("no-token");
|
|
856
|
+
const ownedOrgs = await fetchOwnedOrgs(token);
|
|
857
|
+
return computeAcceptanceFromSearch(login, token, ownedOrgs, cache);
|
|
858
|
+
}
|
|
859
|
+
async function computeAcceptanceCredentialPublic(login, token, cache = /* @__PURE__ */ new Map(), opts) {
|
|
860
|
+
if (!token) return emptyCredential("no-token");
|
|
861
|
+
const ownedOrgs = await fetchPublicOrgs(login, token);
|
|
862
|
+
for (const org of opts?.includeOrgs ?? []) ownedOrgs.delete(org.toLowerCase());
|
|
863
|
+
const gates = opts?.relaxGates ? { minStars: 0, minContributors: 0 } : void 0;
|
|
864
|
+
return computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates);
|
|
865
|
+
}
|
|
866
|
+
function acceptanceCountForDomains(cred, domains) {
|
|
867
|
+
if (cred.status !== "ok") return 0;
|
|
868
|
+
let max = 0;
|
|
869
|
+
for (const d of domains) {
|
|
870
|
+
const c = cred.byDomain[d]?.mergedPRs ?? 0;
|
|
871
|
+
if (c > max) max = c;
|
|
872
|
+
}
|
|
873
|
+
return max;
|
|
874
|
+
}
|
|
875
|
+
function bestAcceptanceDomain(cred, domains) {
|
|
876
|
+
if (cred.status !== "ok") return null;
|
|
877
|
+
let best = null;
|
|
878
|
+
for (const d of domains) {
|
|
879
|
+
const count = cred.byDomain[d]?.mergedPRs ?? 0;
|
|
880
|
+
if (count > 0 && (best === null || count > best.count)) best = { domain: d, count };
|
|
881
|
+
}
|
|
882
|
+
return best;
|
|
883
|
+
}
|
|
884
|
+
function resumeRecencyDecay(lastSeenIso, now) {
|
|
885
|
+
const ageMs = now - new Date(lastSeenIso).getTime();
|
|
886
|
+
if (!Number.isFinite(ageMs)) return 0;
|
|
887
|
+
return Math.pow(0.5, ageMs / RESUME_DECAY_HALF_LIFE_MS);
|
|
888
|
+
}
|
|
889
|
+
async function fetchRepoRecency(login, token) {
|
|
890
|
+
try {
|
|
891
|
+
const repos = await ghFetch(`/users/${login}/repos?sort=pushed&per_page=100`, token);
|
|
892
|
+
return repos.filter((r) => !r.fork && !!r.pushed_at).map((r) => ({ pushedAt: r.pushed_at, language: r.language ?? null, topics: r.topics ?? [] }));
|
|
893
|
+
} catch {
|
|
894
|
+
return [];
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
function deriveResumeTrend(cred, repoRecency, now = Date.now()) {
|
|
898
|
+
const agg = /* @__PURE__ */ new Map();
|
|
899
|
+
const bump = (domain, when, count, mergedPRs) => {
|
|
900
|
+
const e = agg.get(domain);
|
|
901
|
+
if (!e) {
|
|
902
|
+
agg.set(domain, { count, last: when, earliest: when, mergedPRs });
|
|
903
|
+
} else {
|
|
904
|
+
e.count += count;
|
|
905
|
+
e.mergedPRs += mergedPRs;
|
|
906
|
+
if (when > e.last) e.last = when;
|
|
907
|
+
if (when < e.earliest) e.earliest = when;
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
if (cred.status === "ok") {
|
|
911
|
+
for (const [domain, d] of Object.entries(cred.byDomain)) {
|
|
912
|
+
bump(domain, d.lastMergedAt, d.mergedPRs, d.mergedPRs);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
for (const r of repoRecency) {
|
|
916
|
+
for (const domain of new Set(normalize([r.language ?? "", ...r.topics].filter(Boolean)))) {
|
|
917
|
+
bump(domain, r.pushedAt, 1, 0);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
const oneHalfLifeAgoIso = new Date(now - RESUME_DECAY_HALF_LIFE_MS).toISOString();
|
|
921
|
+
const scored = [];
|
|
922
|
+
for (const [domain, e] of agg.entries()) {
|
|
923
|
+
const recencyScore2 = resumeRecencyDecay(e.last, now);
|
|
924
|
+
const weight = e.count * recencyScore2;
|
|
925
|
+
if (weight < RESUME_MIN_SCORE) continue;
|
|
926
|
+
let direction;
|
|
927
|
+
if (e.earliest > oneHalfLifeAgoIso) direction = "new";
|
|
928
|
+
else if (recencyScore2 >= 0.5) direction = "up";
|
|
929
|
+
else direction = "down";
|
|
930
|
+
scored.push({
|
|
931
|
+
t: { domain, direction, recencyScore: Math.round(recencyScore2 * 1e3) / 1e3, mergedPRs: e.mergedPRs },
|
|
932
|
+
weight
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
return scored.sort((a, b) => b.weight - a.weight).slice(0, 12).map((s) => s.t);
|
|
936
|
+
}
|
|
937
|
+
var TRACTION_TOP_N, MIN_STARS, MIN_CONTRIBUTORS, CANDIDATE_PR_PAGE, TRIVIAL_PR_TITLE, RESUME_DECAY_HALF_LIFE_MS, RESUME_MIN_SCORE;
|
|
938
|
+
var init_github = __esm({
|
|
939
|
+
"../../packages/core/src/github.ts"() {
|
|
940
|
+
"use strict";
|
|
941
|
+
init_vocabulary();
|
|
942
|
+
TRACTION_TOP_N = 6;
|
|
943
|
+
MIN_STARS = 50;
|
|
944
|
+
MIN_CONTRIBUTORS = 10;
|
|
945
|
+
CANDIDATE_PR_PAGE = 50;
|
|
946
|
+
TRIVIAL_PR_TITLE = /^\s*(fix\s+typo|typo\b|update\s+readme|readme\b|docs?:|docs?\(|chore:|chore\(|style:|ci:|build:|bump\b|update\s+dependenc)/i;
|
|
947
|
+
RESUME_DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
948
|
+
RESUME_MIN_SCORE = 0.05;
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
// ../../packages/core/src/matcher.ts
|
|
953
|
+
function acceptanceDomainsOf(job) {
|
|
954
|
+
return job.coreTags && job.coreTags.length > 0 ? job.coreTags : job.tags;
|
|
955
|
+
}
|
|
956
|
+
function backgroundIdf(tag) {
|
|
957
|
+
const df = IDF_BACKGROUND.df[tag] ?? 0;
|
|
958
|
+
return Math.log((IDF_BACKGROUND.N + 1) / (df + 1)) + 1;
|
|
959
|
+
}
|
|
960
|
+
function inferSeniority2(title) {
|
|
961
|
+
if (!ENG_TITLE.test(title)) return void 0;
|
|
962
|
+
for (const [re, level] of SENIORITY_PATTERNS) {
|
|
963
|
+
if (re.test(title)) return level;
|
|
964
|
+
}
|
|
965
|
+
return void 0;
|
|
966
|
+
}
|
|
967
|
+
function seniorityScore(fp, job) {
|
|
968
|
+
if (!fp.seniorityBand) return 1;
|
|
969
|
+
const jobLevel = inferSeniority2(job.title);
|
|
970
|
+
if (!jobLevel) return 0.85;
|
|
971
|
+
const wanted = SENIORITY_RANK[fp.seniorityBand] ?? 1;
|
|
972
|
+
const got = SENIORITY_RANK[jobLevel] ?? 1;
|
|
973
|
+
const delta = Math.abs(wanted - got);
|
|
974
|
+
if (delta === 0) return 1;
|
|
975
|
+
if (delta === 1) return 0.7;
|
|
976
|
+
return 0.4;
|
|
977
|
+
}
|
|
978
|
+
function recencyScore(postedAt, now) {
|
|
979
|
+
if (!postedAt) return UNKNOWN_RECENCY;
|
|
980
|
+
const ms = new Date(postedAt).getTime();
|
|
981
|
+
if (Number.isNaN(ms)) return UNKNOWN_RECENCY;
|
|
982
|
+
const ageDays2 = (now - ms) / 864e5;
|
|
983
|
+
if (ageDays2 < 7) return 1;
|
|
984
|
+
if (ageDays2 < 30) return 0.9;
|
|
985
|
+
if (ageDays2 < 90) return 0.75;
|
|
986
|
+
return 0.6;
|
|
987
|
+
}
|
|
988
|
+
function passesFilters(fp, job) {
|
|
989
|
+
const prefs = fp.prefs;
|
|
990
|
+
if (!prefs) return true;
|
|
991
|
+
if (prefs.remoteOnly && !job.remote) return false;
|
|
992
|
+
if (prefs.roleTypes && prefs.roleTypes.length > 0 && !prefs.roleTypes.includes(job.roleType)) {
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
if (prefs.compFloorUsd !== void 0) {
|
|
996
|
+
if (job.compMax !== void 0 && job.compMax < prefs.compFloorUsd) return false;
|
|
997
|
+
}
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
function buildReason(details) {
|
|
1001
|
+
if (details.length === 0) return "No direct skill overlap found.";
|
|
1002
|
+
const render = (d) => !d.via || d.via === d.tag ? d.tag : `${d.via}\u2192${d.tag} (${d.weight})`;
|
|
1003
|
+
const top = details.slice(0, 3).map(render);
|
|
1004
|
+
const rest = details.length - top.length;
|
|
1005
|
+
const listed = top.join(", ");
|
|
1006
|
+
if (rest === 0) return `Matched on ${listed}.`;
|
|
1007
|
+
return `Matched on ${listed} + ${rest} more skill${rest > 1 ? "s" : ""}.`;
|
|
1008
|
+
}
|
|
1009
|
+
function harmonicMean(a, b) {
|
|
1010
|
+
if (a <= 0 || b <= 0) return 0;
|
|
1011
|
+
return 2 * a * b / (a + b);
|
|
1012
|
+
}
|
|
1013
|
+
function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
|
|
1014
|
+
const idfOf = backgroundIdf;
|
|
1015
|
+
const expanded = expandWeighted(fp.skillTags);
|
|
1016
|
+
const maxDevScore = fp.skillTags.reduce((acc, t) => acc + idfOf(t), 0);
|
|
1017
|
+
const candidates = jobs.filter((j) => passesFilters(fp, j));
|
|
1018
|
+
const scored = candidates.map((job) => {
|
|
1019
|
+
const details = [];
|
|
1020
|
+
let jobMatchScore = 0;
|
|
1021
|
+
let jobMaxScore = 0;
|
|
1022
|
+
const devCovByTag = /* @__PURE__ */ new Map();
|
|
1023
|
+
for (const tag of job.tags) {
|
|
1024
|
+
const w = idfOf(tag);
|
|
1025
|
+
jobMaxScore += w;
|
|
1026
|
+
const hit = expanded.get(tag);
|
|
1027
|
+
if (hit) {
|
|
1028
|
+
const credit = Math.pow(hit.weight, SHARPEN);
|
|
1029
|
+
jobMatchScore += w * credit;
|
|
1030
|
+
details.push({ tag, weight: hit.weight, via: hit.via });
|
|
1031
|
+
if (credit > (devCovByTag.get(hit.via) ?? 0)) devCovByTag.set(hit.via, credit);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
let devScore = 0;
|
|
1035
|
+
for (const t of fp.skillTags) devScore += idfOf(t) * (devCovByTag.get(t) ?? 0);
|
|
1036
|
+
const devCov = maxDevScore > 0 ? Math.min(1, devScore / maxDevScore) : 0;
|
|
1037
|
+
const jobCov = jobMaxScore > 0 ? Math.min(1, jobMatchScore / jobMaxScore) : 0;
|
|
1038
|
+
const tagComponent = harmonicMean(devCov, jobCov);
|
|
1039
|
+
if (tagComponent === 0) return null;
|
|
1040
|
+
const coreTags = job.coreTags ?? coreTagsFromTitle(job.title);
|
|
1041
|
+
let coreComponent = tagComponent;
|
|
1042
|
+
if (coreTags.length > 0) {
|
|
1043
|
+
const coreCov = Math.max(0, ...coreTags.map((ct) => expanded.get(ct)?.weight ?? 0));
|
|
1044
|
+
if (coreCov === 0) coreComponent = tagComponent * CORE_MISS_PENALTY;
|
|
1045
|
+
}
|
|
1046
|
+
details.sort((a, b) => idfOf(b.tag) * b.weight - idfOf(a.tag) * a.weight);
|
|
1047
|
+
const sScore = seniorityScore(fp, job);
|
|
1048
|
+
const rScore = recencyScore(job.postedAt, now);
|
|
1049
|
+
const score = coreComponent * 0.6 + sScore * 0.25 + rScore * 0.15;
|
|
1050
|
+
const matchedTags = [...new Set(details.map((d) => d.via ?? d.tag))];
|
|
1051
|
+
const badge = opts.acceptance ? bestAcceptanceDomain(opts.acceptance, acceptanceDomainsOf(job)) : null;
|
|
1052
|
+
return {
|
|
1053
|
+
job,
|
|
1054
|
+
score: Math.round(score * 1e3) / 1e3,
|
|
1055
|
+
matchedTags,
|
|
1056
|
+
matchDetails: details,
|
|
1057
|
+
...badge ? { acceptance: { status: "ok", domain: badge.domain, count: badge.count } } : {},
|
|
1058
|
+
reason: buildReason(details)
|
|
1059
|
+
};
|
|
1060
|
+
});
|
|
1061
|
+
return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => {
|
|
1062
|
+
const byScore = b.score - a.score;
|
|
1063
|
+
if (Math.abs(byScore) > TIEBREAK_EPS) return byScore;
|
|
1064
|
+
const byAcceptance = (b.acceptance?.count ?? 0) - (a.acceptance?.count ?? 0);
|
|
1065
|
+
if (byAcceptance !== 0) return byAcceptance;
|
|
1066
|
+
return byScore;
|
|
1067
|
+
}).slice(0, limit);
|
|
1068
|
+
}
|
|
1069
|
+
var MIN_SCORE, TIEBREAK_EPS, SHARPEN, CORE_MISS_PENALTY, SENIORITY_RANK, SENIORITY_PATTERNS, ENG_TITLE, UNKNOWN_RECENCY;
|
|
1070
|
+
var init_matcher = __esm({
|
|
1071
|
+
"../../packages/core/src/matcher.ts"() {
|
|
1072
|
+
"use strict";
|
|
1073
|
+
init_vocabulary();
|
|
1074
|
+
init_github();
|
|
1075
|
+
MIN_SCORE = 0.15;
|
|
1076
|
+
TIEBREAK_EPS = 5e-3;
|
|
1077
|
+
SHARPEN = 1.6;
|
|
1078
|
+
CORE_MISS_PENALTY = 0.4;
|
|
1079
|
+
SENIORITY_RANK = {
|
|
1080
|
+
junior: 0,
|
|
1081
|
+
mid: 1,
|
|
1082
|
+
senior: 2,
|
|
1083
|
+
staff: 3
|
|
1084
|
+
};
|
|
1085
|
+
SENIORITY_PATTERNS = [
|
|
1086
|
+
[/\bstaff\b|\bprincipal\b|\bdistinguished\b/i, "staff"],
|
|
1087
|
+
[/\bsenior\b|\bsr\.?\b/i, "senior"],
|
|
1088
|
+
[/\bjunior\b|\bjr\.?\b|\bentry[\s-]?level\b/i, "junior"],
|
|
1089
|
+
[/\bmid[\s-]?level\b|\bmid\b/i, "mid"]
|
|
1090
|
+
];
|
|
1091
|
+
ENG_TITLE = /\b(engineer|engineering|developer|dev|swe|sde|programmer|architect)\b/i;
|
|
1092
|
+
UNKNOWN_RECENCY = 0.75;
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
// ../../packages/core/src/feeds/http.ts
|
|
1097
|
+
function fetchWithTimeout(input, init, timeoutMs = FEED_FETCH_TIMEOUT_MS) {
|
|
1098
|
+
return fetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
1099
|
+
}
|
|
1100
|
+
var FEED_FETCH_TIMEOUT_MS;
|
|
1101
|
+
var init_http = __esm({
|
|
1102
|
+
"../../packages/core/src/feeds/http.ts"() {
|
|
1103
|
+
"use strict";
|
|
1104
|
+
FEED_FETCH_TIMEOUT_MS = 1e4;
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
// ../../packages/core/src/feeds/greenhouse.ts
|
|
1109
|
+
function extractTags(job) {
|
|
1110
|
+
const body = [
|
|
1111
|
+
...(job.departments ?? []).map((d) => d.name),
|
|
1112
|
+
job.location?.name ?? "",
|
|
1113
|
+
...(job.offices ?? []).map((o) => o.name),
|
|
1114
|
+
...job.content ? [job.content.replace(/<[^>]*>/g, " ")] : []
|
|
1115
|
+
].filter(Boolean).join(" ");
|
|
1116
|
+
return extractSkillTags(job.title, body);
|
|
1117
|
+
}
|
|
1118
|
+
function inferRemote(location) {
|
|
1119
|
+
const l = location.toLowerCase();
|
|
1120
|
+
return l.includes("remote") || l.includes("anywhere") || l.includes("worldwide");
|
|
1121
|
+
}
|
|
1122
|
+
async function fetchSlug(slug) {
|
|
1123
|
+
const url = `https://boards-api.greenhouse.io/v1/boards/${slug}/jobs?content=true`;
|
|
1124
|
+
let res;
|
|
1125
|
+
try {
|
|
1126
|
+
res = await fetchWithTimeout(url, { headers: { Accept: "application/json" } });
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
console.warn(`[greenhouse] ${slug}: network error \u2014`, err);
|
|
1129
|
+
return [];
|
|
1130
|
+
}
|
|
1131
|
+
if (!res.ok) {
|
|
1132
|
+
console.warn(`[greenhouse] ${slug}: HTTP ${res.status} ${res.statusText}`);
|
|
1133
|
+
return [];
|
|
1134
|
+
}
|
|
1135
|
+
let data;
|
|
1136
|
+
try {
|
|
1137
|
+
data = await res.json();
|
|
1138
|
+
} catch (err) {
|
|
1139
|
+
console.warn(`[greenhouse] ${slug}: JSON parse error \u2014`, err);
|
|
1140
|
+
return [];
|
|
1141
|
+
}
|
|
1142
|
+
const jobs = data.jobs ?? [];
|
|
1143
|
+
if (jobs.length === 0) {
|
|
1144
|
+
console.warn(`[greenhouse] ${slug}: 0 jobs returned (board may be private or slug invalid)`);
|
|
1145
|
+
} else {
|
|
1146
|
+
console.info(`[greenhouse] ${slug}: ${jobs.length} jobs`);
|
|
1147
|
+
}
|
|
1148
|
+
return jobs.map((j) => ({
|
|
1149
|
+
id: `greenhouse:${j.id}`,
|
|
1150
|
+
source: "greenhouse",
|
|
1151
|
+
title: j.title,
|
|
1152
|
+
company: slug,
|
|
1153
|
+
url: j.absolute_url,
|
|
1154
|
+
remote: inferRemote(j.location?.name ?? ""),
|
|
1155
|
+
location: j.location?.name,
|
|
1156
|
+
tags: extractTags(j),
|
|
1157
|
+
roleType: "full_time",
|
|
1158
|
+
postedAt: j.updated_at,
|
|
1159
|
+
applyMode: "direct",
|
|
1160
|
+
raw: j
|
|
1161
|
+
}));
|
|
1162
|
+
}
|
|
1163
|
+
var FALLBACK_SLUGS, greenhouse;
|
|
1164
|
+
var init_greenhouse = __esm({
|
|
1165
|
+
"../../packages/core/src/feeds/greenhouse.ts"() {
|
|
1166
|
+
"use strict";
|
|
1167
|
+
init_vocabulary();
|
|
1168
|
+
init_http();
|
|
1169
|
+
FALLBACK_SLUGS = [
|
|
1170
|
+
"stripe",
|
|
1171
|
+
"linear",
|
|
1172
|
+
"vercel",
|
|
1173
|
+
"ramp",
|
|
1174
|
+
"notion",
|
|
1175
|
+
"airbnb",
|
|
1176
|
+
"anthropic",
|
|
1177
|
+
"figma",
|
|
1178
|
+
"discord",
|
|
1179
|
+
"brex",
|
|
1180
|
+
"mercury",
|
|
1181
|
+
"retool",
|
|
1182
|
+
"vanta",
|
|
1183
|
+
"plaid",
|
|
1184
|
+
"gusto",
|
|
1185
|
+
"scale",
|
|
1186
|
+
"databricks",
|
|
1187
|
+
"coinbase",
|
|
1188
|
+
"robinhood",
|
|
1189
|
+
"doordash"
|
|
1190
|
+
];
|
|
1191
|
+
greenhouse = {
|
|
1192
|
+
source: "greenhouse",
|
|
1193
|
+
async fetch(opts) {
|
|
1194
|
+
const slugs = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : FALLBACK_SLUGS;
|
|
1195
|
+
console.info(`[greenhouse] fetching ${slugs.length} slugs: ${slugs.join(", ")}`);
|
|
1196
|
+
const results = await Promise.allSettled(slugs.map(fetchSlug));
|
|
1197
|
+
const jobs = [];
|
|
1198
|
+
let failures = 0;
|
|
1199
|
+
for (const r of results) {
|
|
1200
|
+
if (r.status === "fulfilled") {
|
|
1201
|
+
jobs.push(...r.value);
|
|
1202
|
+
} else {
|
|
1203
|
+
failures++;
|
|
1204
|
+
console.warn(`[greenhouse] slug fetch rejected:`, r.reason);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
console.info(`[greenhouse] total: ${jobs.length} jobs, ${failures} slug failures`);
|
|
1208
|
+
return jobs;
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
// ../../packages/core/src/feeds/ashby.ts
|
|
1215
|
+
function extractTags2(job) {
|
|
1216
|
+
const body = [
|
|
1217
|
+
job.team ?? "",
|
|
1218
|
+
job.department ?? "",
|
|
1219
|
+
job.location ?? "",
|
|
1220
|
+
...(job.secondaryLocations ?? []).map((l) => l.location ?? ""),
|
|
1221
|
+
job.descriptionPlain ?? ""
|
|
1222
|
+
].join(" ");
|
|
1223
|
+
return extractSkillTags(job.title, body);
|
|
1224
|
+
}
|
|
1225
|
+
function mapEmploymentType(raw) {
|
|
1226
|
+
if (!raw) return "full_time";
|
|
1227
|
+
const lower = raw.toLowerCase();
|
|
1228
|
+
if (lower.includes("contract") || lower.includes("contractor")) return "contract";
|
|
1229
|
+
if (lower.includes("freelance")) return "freelance";
|
|
1230
|
+
return "full_time";
|
|
1231
|
+
}
|
|
1232
|
+
function inferRemote2(job) {
|
|
1233
|
+
if (job.isRemote === true) return true;
|
|
1234
|
+
const loc = (job.location ?? "").toLowerCase();
|
|
1235
|
+
return loc.includes("remote") || loc.includes("anywhere");
|
|
1236
|
+
}
|
|
1237
|
+
async function fetchSlug2(slug) {
|
|
1238
|
+
const url = `https://api.ashbyhq.com/posting-api/job-board/${slug}`;
|
|
1239
|
+
const res = await fetchWithTimeout(url, {
|
|
1240
|
+
headers: { Accept: "application/json" }
|
|
1241
|
+
});
|
|
1242
|
+
if (!res.ok) {
|
|
1243
|
+
throw new Error(`Ashby ${slug}: HTTP ${res.status}`);
|
|
1244
|
+
}
|
|
1245
|
+
const data = await res.json();
|
|
1246
|
+
return (data.jobs ?? []).map((j) => {
|
|
1247
|
+
const comp = j.compensation;
|
|
1248
|
+
return {
|
|
1249
|
+
id: `ashby:${j.id}`,
|
|
1250
|
+
source: "ashby",
|
|
1251
|
+
title: j.title,
|
|
1252
|
+
company: slug,
|
|
1253
|
+
url: j.jobUrl ?? j.applyUrl ?? `https://jobs.ashbyhq.com/${slug}/${j.id}`,
|
|
1254
|
+
remote: inferRemote2(j),
|
|
1255
|
+
location: j.location,
|
|
1256
|
+
compMin: comp?.minValue,
|
|
1257
|
+
compMax: comp?.maxValue,
|
|
1258
|
+
tags: extractTags2(j),
|
|
1259
|
+
roleType: mapEmploymentType(j.employmentType),
|
|
1260
|
+
postedAt: j.publishedAt,
|
|
1261
|
+
applyMode: "direct",
|
|
1262
|
+
raw: j
|
|
1263
|
+
};
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
var ashby;
|
|
1267
|
+
var init_ashby = __esm({
|
|
1268
|
+
"../../packages/core/src/feeds/ashby.ts"() {
|
|
1269
|
+
"use strict";
|
|
1270
|
+
init_vocabulary();
|
|
1271
|
+
init_http();
|
|
1272
|
+
ashby = {
|
|
1273
|
+
source: "ashby",
|
|
1274
|
+
async fetch(opts) {
|
|
1275
|
+
const slugs = opts?.slugs ?? [];
|
|
1276
|
+
const results = await Promise.allSettled(slugs.map(fetchSlug2));
|
|
1277
|
+
const jobs = [];
|
|
1278
|
+
for (const r of results) {
|
|
1279
|
+
if (r.status === "fulfilled") jobs.push(...r.value);
|
|
1280
|
+
}
|
|
1281
|
+
return jobs;
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
// ../../packages/core/src/feeds/lever.ts
|
|
1288
|
+
function extractTags3(p) {
|
|
1289
|
+
const cat = p.categories ?? {};
|
|
1290
|
+
const body = [
|
|
1291
|
+
cat.team ?? "",
|
|
1292
|
+
cat.department ?? "",
|
|
1293
|
+
cat.location ?? "",
|
|
1294
|
+
...cat.allLocations ?? [],
|
|
1295
|
+
p.descriptionPlain ?? ""
|
|
1296
|
+
].join(" ");
|
|
1297
|
+
return extractSkillTags(p.text, body);
|
|
1298
|
+
}
|
|
1299
|
+
function mapCommitment(raw) {
|
|
1300
|
+
if (!raw) return "full_time";
|
|
1301
|
+
const lower = raw.toLowerCase();
|
|
1302
|
+
if (lower.includes("contract") || lower.includes("contractor")) return "contract";
|
|
1303
|
+
if (lower.includes("freelance")) return "freelance";
|
|
1304
|
+
return "full_time";
|
|
1305
|
+
}
|
|
1306
|
+
function inferRemote3(p) {
|
|
1307
|
+
if ((p.workplaceType ?? "").toLowerCase() === "remote") return true;
|
|
1308
|
+
const cat = p.categories ?? {};
|
|
1309
|
+
const haystack = [cat.location ?? "", ...cat.allLocations ?? []].join(" ").toLowerCase();
|
|
1310
|
+
return haystack.includes("remote") || haystack.includes("anywhere");
|
|
1311
|
+
}
|
|
1312
|
+
function toIso(ms) {
|
|
1313
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return void 0;
|
|
1314
|
+
try {
|
|
1315
|
+
return new Date(ms).toISOString();
|
|
1316
|
+
} catch {
|
|
1317
|
+
return void 0;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
async function fetchSlug3(slug) {
|
|
1321
|
+
const url = `https://api.lever.co/v0/postings/${slug}?mode=json`;
|
|
1322
|
+
const res = await fetchWithTimeout(url, { headers: { Accept: "application/json" } });
|
|
1323
|
+
if (!res.ok) {
|
|
1324
|
+
throw new Error(`Lever ${slug}: HTTP ${res.status}`);
|
|
1325
|
+
}
|
|
1326
|
+
const data = await res.json();
|
|
1327
|
+
const postings = Array.isArray(data) ? data : [];
|
|
1328
|
+
if (postings.length === 0) {
|
|
1329
|
+
console.warn(`[lever] ${slug}: 0 jobs returned (board may be private or slug invalid)`);
|
|
1330
|
+
} else {
|
|
1331
|
+
console.info(`[lever] ${slug}: ${postings.length} jobs`);
|
|
1332
|
+
}
|
|
1333
|
+
return postings.filter((p) => p && p.id && p.text).map((p) => ({
|
|
1334
|
+
id: `lever:${p.id}`,
|
|
1335
|
+
source: "lever",
|
|
1336
|
+
title: p.text,
|
|
1337
|
+
company: slug,
|
|
1338
|
+
url: p.hostedUrl ?? p.applyUrl ?? `https://jobs.lever.co/${slug}/${p.id}`,
|
|
1339
|
+
remote: inferRemote3(p),
|
|
1340
|
+
location: p.categories?.location,
|
|
1341
|
+
tags: extractTags3(p),
|
|
1342
|
+
roleType: mapCommitment(p.categories?.commitment),
|
|
1343
|
+
postedAt: toIso(p.createdAt),
|
|
1344
|
+
applyMode: "direct",
|
|
1345
|
+
raw: p
|
|
1346
|
+
}));
|
|
1347
|
+
}
|
|
1348
|
+
var lever;
|
|
1349
|
+
var init_lever = __esm({
|
|
1350
|
+
"../../packages/core/src/feeds/lever.ts"() {
|
|
1351
|
+
"use strict";
|
|
1352
|
+
init_vocabulary();
|
|
1353
|
+
init_http();
|
|
1354
|
+
lever = {
|
|
1355
|
+
source: "lever",
|
|
1356
|
+
async fetch(opts) {
|
|
1357
|
+
const slugs = opts?.slugs ?? [];
|
|
1358
|
+
const results = await Promise.allSettled(slugs.map(fetchSlug3));
|
|
1359
|
+
const jobs = [];
|
|
1360
|
+
for (const r of results) {
|
|
1361
|
+
if (r.status === "fulfilled") jobs.push(...r.value);
|
|
1362
|
+
else console.warn("[lever] slug fetch rejected:", r.reason);
|
|
1363
|
+
}
|
|
1364
|
+
return jobs;
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
// ../../packages/core/src/feeds/himalayas.ts
|
|
1371
|
+
function extractTags4(job) {
|
|
1372
|
+
return extractSkillTags(job.title, (job.tags ?? []).join(" "));
|
|
1373
|
+
}
|
|
1374
|
+
function mapJobType(raw) {
|
|
1375
|
+
if (!raw) return "full_time";
|
|
1376
|
+
const lower = raw.toLowerCase();
|
|
1377
|
+
if (lower.includes("contract")) return "contract";
|
|
1378
|
+
if (lower.includes("freelance")) return "freelance";
|
|
1379
|
+
return "full_time";
|
|
1380
|
+
}
|
|
1381
|
+
function buildUrl(job) {
|
|
1382
|
+
if (job.applicationUrl) return job.applicationUrl;
|
|
1383
|
+
if (job.url) return job.url;
|
|
1384
|
+
const slug = job.slug ?? job.id ?? "unknown";
|
|
1385
|
+
return `https://himalayas.app/jobs/${slug}`;
|
|
1386
|
+
}
|
|
1387
|
+
function buildId(job) {
|
|
1388
|
+
return `himalayas:${job.id ?? job.slug ?? job.title}`;
|
|
1389
|
+
}
|
|
1390
|
+
var himalayas;
|
|
1391
|
+
var init_himalayas = __esm({
|
|
1392
|
+
"../../packages/core/src/feeds/himalayas.ts"() {
|
|
1393
|
+
"use strict";
|
|
1394
|
+
init_vocabulary();
|
|
1395
|
+
init_http();
|
|
1396
|
+
himalayas = {
|
|
1397
|
+
source: "himalayas",
|
|
1398
|
+
async fetch(opts) {
|
|
1399
|
+
const limit = opts?.limit ?? 100;
|
|
1400
|
+
const url = `https://himalayas.app/jobs/api?limit=${limit}`;
|
|
1401
|
+
const res = await fetchWithTimeout(url, {
|
|
1402
|
+
headers: { Accept: "application/json" }
|
|
1403
|
+
});
|
|
1404
|
+
if (!res.ok) {
|
|
1405
|
+
throw new Error(`Himalayas: HTTP ${res.status}`);
|
|
1406
|
+
}
|
|
1407
|
+
const data = await res.json();
|
|
1408
|
+
return (data.jobs ?? []).map((j) => ({
|
|
1409
|
+
id: buildId(j),
|
|
1410
|
+
source: "himalayas",
|
|
1411
|
+
title: j.title,
|
|
1412
|
+
company: j.companyName ?? j.companySlug ?? "unknown",
|
|
1413
|
+
url: buildUrl(j),
|
|
1414
|
+
// Himalayas is a remote-only board
|
|
1415
|
+
remote: true,
|
|
1416
|
+
location: (j.locationRestrictions ?? []).join(", ") || "Remote",
|
|
1417
|
+
compMin: j.salaryMin,
|
|
1418
|
+
compMax: j.salaryMax,
|
|
1419
|
+
tags: extractTags4(j),
|
|
1420
|
+
roleType: mapJobType(j.jobType),
|
|
1421
|
+
postedAt: j.pubDate ?? j.createdAt,
|
|
1422
|
+
applyMode: "direct",
|
|
1423
|
+
raw: j
|
|
1424
|
+
}));
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
// ../../packages/core/src/feeds/entities.ts
|
|
1431
|
+
function fromCodePoint(cp) {
|
|
1432
|
+
if (!Number.isFinite(cp) || cp < 0 || cp > 1114111) return "";
|
|
1433
|
+
try {
|
|
1434
|
+
return String.fromCodePoint(cp);
|
|
1435
|
+
} catch {
|
|
1436
|
+
return "";
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
function decodeEntities(input) {
|
|
1440
|
+
if (!input || !input.includes("&")) return input;
|
|
1441
|
+
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, "&");
|
|
1442
|
+
}
|
|
1443
|
+
var NAMED;
|
|
1444
|
+
var init_entities = __esm({
|
|
1445
|
+
"../../packages/core/src/feeds/entities.ts"() {
|
|
1446
|
+
"use strict";
|
|
1447
|
+
NAMED = {
|
|
1448
|
+
lt: "<",
|
|
1449
|
+
gt: ">",
|
|
1450
|
+
quot: '"',
|
|
1451
|
+
apos: "'",
|
|
1452
|
+
nbsp: " "
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
// ../../packages/core/src/feeds/wwr.ts
|
|
1458
|
+
function stripHtml(html) {
|
|
1459
|
+
return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
1460
|
+
}
|
|
1461
|
+
function inferRoleType(category) {
|
|
1462
|
+
const lower = category.toLowerCase();
|
|
1463
|
+
if (lower.includes("contract")) return "contract";
|
|
1464
|
+
if (lower.includes("freelance")) return "freelance";
|
|
1465
|
+
return "full_time";
|
|
1466
|
+
}
|
|
1467
|
+
function extractId(link) {
|
|
1468
|
+
const match2 = link.match(/\/opening\/([^/\s]+)/);
|
|
1469
|
+
return `wwr:${match2?.[1] ?? encodeURIComponent(link)}`;
|
|
1470
|
+
}
|
|
1471
|
+
function parseRss(xml) {
|
|
1472
|
+
const items = [];
|
|
1473
|
+
const itemBlocks = xml.match(/<item>([\s\S]*?)<\/item>/g) ?? [];
|
|
1474
|
+
for (const block of itemBlocks) {
|
|
1475
|
+
const get = (tag) => {
|
|
1476
|
+
const cdataMatch = block.match(new RegExp(`<${tag}[^>]*><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/${tag}>`, "i"));
|
|
1477
|
+
if (cdataMatch) return decodeEntities(cdataMatch[1].trim());
|
|
1478
|
+
const plainMatch = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i"));
|
|
1479
|
+
return decodeEntities(plainMatch?.[1].trim() ?? "");
|
|
1480
|
+
};
|
|
1481
|
+
const rawTitle = get("title");
|
|
1482
|
+
const m = rawTitle.match(/^(.*?):\s+(.*)$/);
|
|
1483
|
+
let company = m ? m[1].trim() : "Unknown";
|
|
1484
|
+
const titleAfterColon = m ? m[2].trim() : rawTitle;
|
|
1485
|
+
const title = titleAfterColon.replace(/\s*\([^)]*\)\s*$/, "").trim();
|
|
1486
|
+
if (/^https?:\/\//i.test(company)) {
|
|
1487
|
+
company = company.replace(/^https?:\/\//i, "").replace(/\/.*$/, "").trim() || "Unknown";
|
|
1488
|
+
}
|
|
1489
|
+
items.push({
|
|
1490
|
+
title,
|
|
1491
|
+
link: get("link") || get("guid"),
|
|
1492
|
+
pubDate: get("pubDate"),
|
|
1493
|
+
category: get("category"),
|
|
1494
|
+
description: get("description"),
|
|
1495
|
+
company
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
return items;
|
|
1499
|
+
}
|
|
1500
|
+
function extractTags5(item) {
|
|
1501
|
+
const body = [item.category, stripHtml(item.description)].join(" ");
|
|
1502
|
+
return extractSkillTags(item.title, body);
|
|
1503
|
+
}
|
|
1504
|
+
var WWR_RSS_URL, wwr;
|
|
1505
|
+
var init_wwr = __esm({
|
|
1506
|
+
"../../packages/core/src/feeds/wwr.ts"() {
|
|
1507
|
+
"use strict";
|
|
1508
|
+
init_vocabulary();
|
|
1509
|
+
init_entities();
|
|
1510
|
+
init_http();
|
|
1511
|
+
WWR_RSS_URL = "https://weworkremotely.com/remote-jobs.rss";
|
|
1512
|
+
wwr = {
|
|
1513
|
+
source: "wwr",
|
|
1514
|
+
async fetch(opts) {
|
|
1515
|
+
const limit = opts?.limit ?? 200;
|
|
1516
|
+
const res = await fetchWithTimeout(WWR_RSS_URL, {
|
|
1517
|
+
headers: { Accept: "application/rss+xml, application/xml, text/xml" }
|
|
1518
|
+
});
|
|
1519
|
+
if (!res.ok) {
|
|
1520
|
+
throw new Error(`WWR RSS: HTTP ${res.status}`);
|
|
1521
|
+
}
|
|
1522
|
+
const xml = await res.text();
|
|
1523
|
+
const items = parseRss(xml).slice(0, limit);
|
|
1524
|
+
function safeIso(s) {
|
|
1525
|
+
if (!s) return void 0;
|
|
1526
|
+
const d = new Date(s);
|
|
1527
|
+
return Number.isNaN(d.getTime()) ? void 0 : d.toISOString();
|
|
1528
|
+
}
|
|
1529
|
+
return items.map((item) => ({
|
|
1530
|
+
id: extractId(item.link),
|
|
1531
|
+
source: "wwr",
|
|
1532
|
+
title: item.title,
|
|
1533
|
+
company: item.company,
|
|
1534
|
+
url: item.link,
|
|
1535
|
+
// WWR is a remote-only board
|
|
1536
|
+
remote: true,
|
|
1537
|
+
location: "Remote",
|
|
1538
|
+
tags: extractTags5(item),
|
|
1539
|
+
roleType: inferRoleType(item.category),
|
|
1540
|
+
postedAt: safeIso(item.pubDate),
|
|
1541
|
+
applyMode: "direct",
|
|
1542
|
+
raw: item
|
|
1543
|
+
}));
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
// ../../packages/core/src/feeds/hn.ts
|
|
1550
|
+
function stripHtml2(html) {
|
|
1551
|
+
return decodeEntities(html.replace(/<p>/gi, " ").replace(/<[^>]*>/g, "")).replace(/\s+/g, " ").trim();
|
|
1552
|
+
}
|
|
1553
|
+
function extractUrl(text) {
|
|
1554
|
+
const match2 = text.match(/https?:\/\/[^\s<>"']+/);
|
|
1555
|
+
return match2?.[0] ?? "";
|
|
1556
|
+
}
|
|
1557
|
+
function inferRemote4(text) {
|
|
1558
|
+
const lower = text.toLowerCase();
|
|
1559
|
+
return lower.includes("remote") || lower.includes("anywhere") || lower.includes("distributed");
|
|
1560
|
+
}
|
|
1561
|
+
function inferRoleType2(text) {
|
|
1562
|
+
const lower = text.toLowerCase();
|
|
1563
|
+
if (lower.includes("contract") || lower.includes("contractor")) return "contract";
|
|
1564
|
+
if (lower.includes("freelance")) return "freelance";
|
|
1565
|
+
return "full_time";
|
|
1566
|
+
}
|
|
1567
|
+
function parseComment(item) {
|
|
1568
|
+
if (!item.text || item.text.trim().length < 20) return null;
|
|
1569
|
+
const raw = stripHtml2(item.text);
|
|
1570
|
+
if (!raw) return null;
|
|
1571
|
+
const firstLine = raw.split(/\n/)[0];
|
|
1572
|
+
const parts = firstLine.split("|").map((s) => s.trim());
|
|
1573
|
+
const company = parts[0] ?? "Unknown";
|
|
1574
|
+
const title = parts[1] ?? firstLine.slice(0, 80).trim();
|
|
1575
|
+
const location = parts[2] ?? "";
|
|
1576
|
+
if (company.toLowerCase().startsWith("note:") || company.toLowerCase().startsWith("ps:") || title.length < 3) {
|
|
1577
|
+
return null;
|
|
1578
|
+
}
|
|
1579
|
+
const url = extractUrl(raw) || `https://news.ycombinator.com/item?id=${item.id}`;
|
|
1580
|
+
const tags = extractTags6(title, raw);
|
|
1581
|
+
if (tags.length === 0) return null;
|
|
1582
|
+
return {
|
|
1583
|
+
id: `hn:${item.id}`,
|
|
1584
|
+
source: "hn",
|
|
1585
|
+
title: title.slice(0, 120),
|
|
1586
|
+
company: company.slice(0, 80),
|
|
1587
|
+
url,
|
|
1588
|
+
remote: inferRemote4(raw),
|
|
1589
|
+
location: location || void 0,
|
|
1590
|
+
tags,
|
|
1591
|
+
roleType: inferRoleType2(raw),
|
|
1592
|
+
postedAt: item.created_at,
|
|
1593
|
+
applyMode: "direct",
|
|
1594
|
+
raw: item
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
function extractTags6(title, text) {
|
|
1598
|
+
return extractSkillTags(title, text);
|
|
1599
|
+
}
|
|
1600
|
+
var ALGOLIA_SEARCH, ALGOLIA_ITEMS, hn;
|
|
1601
|
+
var init_hn = __esm({
|
|
1602
|
+
"../../packages/core/src/feeds/hn.ts"() {
|
|
1603
|
+
"use strict";
|
|
1604
|
+
init_vocabulary();
|
|
1605
|
+
init_entities();
|
|
1606
|
+
init_http();
|
|
1607
|
+
ALGOLIA_SEARCH = "https://hn.algolia.com/api/v1/search?query=Ask+HN%3A+Who+is+Hiring%3F&tags=story,ask_hn&hitsPerPage=1";
|
|
1608
|
+
ALGOLIA_ITEMS = "https://hn.algolia.com/api/v1/items/";
|
|
1609
|
+
hn = {
|
|
1610
|
+
source: "hn",
|
|
1611
|
+
async fetch(opts) {
|
|
1612
|
+
const limit = opts?.limit ?? 150;
|
|
1613
|
+
const searchRes = await fetchWithTimeout(ALGOLIA_SEARCH, {
|
|
1614
|
+
headers: { Accept: "application/json" }
|
|
1615
|
+
});
|
|
1616
|
+
if (!searchRes.ok) {
|
|
1617
|
+
throw new Error(`HN Algolia search: HTTP ${searchRes.status}`);
|
|
1618
|
+
}
|
|
1619
|
+
const searchData = await searchRes.json();
|
|
1620
|
+
const story = searchData.hits[0];
|
|
1621
|
+
if (!story) {
|
|
1622
|
+
throw new Error('HN: No "Who is Hiring" story found');
|
|
1623
|
+
}
|
|
1624
|
+
const itemRes = await fetchWithTimeout(`${ALGOLIA_ITEMS}${story.objectID}`, {
|
|
1625
|
+
headers: { Accept: "application/json" }
|
|
1626
|
+
});
|
|
1627
|
+
if (!itemRes.ok) {
|
|
1628
|
+
throw new Error(`HN Algolia item ${story.objectID}: HTTP ${itemRes.status}`);
|
|
1629
|
+
}
|
|
1630
|
+
const storyItem = await itemRes.json();
|
|
1631
|
+
const comments = storyItem.children ?? [];
|
|
1632
|
+
const jobs = [];
|
|
1633
|
+
for (const comment of comments.slice(0, limit)) {
|
|
1634
|
+
const job = parseComment(comment);
|
|
1635
|
+
if (job) jobs.push(job);
|
|
1636
|
+
}
|
|
1637
|
+
return jobs;
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
|
|
1643
|
+
// ../../packages/core/src/feeds/bounty-gate.ts
|
|
1644
|
+
function isDenylistedRepo(fullName) {
|
|
1645
|
+
return DENYLIST_LC.has(fullName.toLowerCase());
|
|
1646
|
+
}
|
|
1647
|
+
function passesAntiFarm(amountUSD, stargazers) {
|
|
1648
|
+
return !(amountUSD > HIGH_VALUE_USD && stargazers < HIGH_VALUE_MIN_STARS);
|
|
1649
|
+
}
|
|
1650
|
+
function ageDays(createdAtIso) {
|
|
1651
|
+
const created = Date.parse(createdAtIso);
|
|
1652
|
+
if (!Number.isFinite(created)) return 0;
|
|
1653
|
+
return (Date.now() - created) / (1e3 * 60 * 60 * 24);
|
|
1654
|
+
}
|
|
1655
|
+
function passesMaturityGate(repo) {
|
|
1656
|
+
if (isDenylistedRepo(repo.fullName)) return false;
|
|
1657
|
+
if (repo.archived || repo.disabled) return false;
|
|
1658
|
+
if (repo.stargazers < MIN_REPO_STARS) return false;
|
|
1659
|
+
if (ageDays(repo.createdAt) < MIN_REPO_AGE_DAYS) return false;
|
|
1660
|
+
return true;
|
|
1661
|
+
}
|
|
1662
|
+
var DEFAULT_BOUNTY_REPOS, BOUNTY_REPO_DENYLIST, DENYLIST_LC, MAX_BOUNTIES_PER_REPO, MAX_BOUNTIES_PER_DISCOVERED_REPO, MIN_REPO_STARS, HIGH_VALUE_USD, HIGH_VALUE_MIN_STARS, MIN_REPO_AGE_DAYS;
|
|
1663
|
+
var init_bounty_gate = __esm({
|
|
1664
|
+
"../../packages/core/src/feeds/bounty-gate.ts"() {
|
|
1665
|
+
"use strict";
|
|
1666
|
+
DEFAULT_BOUNTY_REPOS = [
|
|
1667
|
+
"tenstorrent/tt-metal",
|
|
1668
|
+
"sequelize/sequelize",
|
|
1669
|
+
"commaai/opendbc",
|
|
1670
|
+
"aragon/hack",
|
|
1671
|
+
"spacemeshos/app",
|
|
1672
|
+
"archestra-ai/archestra",
|
|
1673
|
+
"boundlessfi/boundless",
|
|
1674
|
+
"ucfopen/Obojobo",
|
|
1675
|
+
"widgetti/ipyvolume",
|
|
1676
|
+
"moorcheh-ai/memanto",
|
|
1677
|
+
"PrismarineJS/mineflayer"
|
|
1678
|
+
];
|
|
1679
|
+
BOUNTY_REPO_DENYLIST = ["SecureBananaLabs/bug-bounty"];
|
|
1680
|
+
DENYLIST_LC = new Set(BOUNTY_REPO_DENYLIST.map((r) => r.toLowerCase()));
|
|
1681
|
+
MAX_BOUNTIES_PER_REPO = 10;
|
|
1682
|
+
MAX_BOUNTIES_PER_DISCOVERED_REPO = 3;
|
|
1683
|
+
MIN_REPO_STARS = 5;
|
|
1684
|
+
HIGH_VALUE_USD = 500;
|
|
1685
|
+
HIGH_VALUE_MIN_STARS = 50;
|
|
1686
|
+
MIN_REPO_AGE_DAYS = 30;
|
|
1687
|
+
}
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
// ../../packages/core/src/concurrency.ts
|
|
1691
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
1692
|
+
const results = new Array(items.length);
|
|
1693
|
+
if (items.length === 0) return results;
|
|
1694
|
+
const workers = Math.max(1, Math.min(Math.floor(limit) || 1, items.length));
|
|
1695
|
+
let next = 0;
|
|
1696
|
+
async function run2() {
|
|
1697
|
+
for (; ; ) {
|
|
1698
|
+
const i = next++;
|
|
1699
|
+
if (i >= items.length) return;
|
|
1700
|
+
results[i] = await fn(items[i], i);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
await Promise.all(Array.from({ length: workers }, run2));
|
|
1704
|
+
return results;
|
|
1705
|
+
}
|
|
1706
|
+
var init_concurrency = __esm({
|
|
1707
|
+
"../../packages/core/src/concurrency.ts"() {
|
|
1708
|
+
"use strict";
|
|
1709
|
+
}
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
// ../../packages/core/src/feeds/github-bounties.ts
|
|
1713
|
+
function authHeaders() {
|
|
1714
|
+
const token = process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"];
|
|
1715
|
+
const h = {
|
|
1716
|
+
Accept: "application/vnd.github+json",
|
|
1717
|
+
"User-Agent": "terminalhire",
|
|
1718
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
1719
|
+
};
|
|
1720
|
+
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
1721
|
+
return h;
|
|
1722
|
+
}
|
|
1723
|
+
function tokenize2(text) {
|
|
1724
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
1725
|
+
}
|
|
1726
|
+
function parseAmountUSD(text) {
|
|
1727
|
+
const m = text.match(/\$\s?([0-9][0-9,]*(?:\.[0-9]+)?)\s?([kK])?/);
|
|
1728
|
+
if (!m) return void 0;
|
|
1729
|
+
let n = parseFloat(m[1].replace(/,/g, ""));
|
|
1730
|
+
if (m[2]) n *= 1e3;
|
|
1731
|
+
if (!Number.isFinite(n) || n <= 0 || n > 1e6) return void 0;
|
|
1732
|
+
return Math.round(n);
|
|
1733
|
+
}
|
|
1734
|
+
function effortFromAmount(amount) {
|
|
1735
|
+
if (amount == null) return void 0;
|
|
1736
|
+
if (amount <= 500) return "small";
|
|
1737
|
+
if (amount <= 2e3) return "medium";
|
|
1738
|
+
return "large";
|
|
1739
|
+
}
|
|
1740
|
+
function labelNames(issue) {
|
|
1741
|
+
return (issue.labels ?? []).map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean);
|
|
1742
|
+
}
|
|
1743
|
+
function isBountyIssue(issue) {
|
|
1744
|
+
if (issue.pull_request) return false;
|
|
1745
|
+
const labels = labelNames(issue);
|
|
1746
|
+
if (labels.some((n) => BOUNTY_LABEL_RE.test(n))) return true;
|
|
1747
|
+
return /bounty/i.test(issue.title) && parseAmountUSD(issue.title) != null;
|
|
1748
|
+
}
|
|
1749
|
+
async function ghJson(path) {
|
|
1750
|
+
let res;
|
|
1751
|
+
try {
|
|
1752
|
+
res = await fetchWithTimeout(`${GITHUB_API}${path}`, { headers: authHeaders() });
|
|
1753
|
+
} catch (err) {
|
|
1754
|
+
console.warn(`[github-bounties] network error ${path} \u2014`, err);
|
|
1755
|
+
return null;
|
|
1756
|
+
}
|
|
1757
|
+
if (res.status === 403 && res.headers.get("x-ratelimit-remaining") === "0") {
|
|
1758
|
+
console.warn("[github-bounties] rate-limited (set GITHUB_TOKEN for 5000/hr)");
|
|
1759
|
+
return null;
|
|
1760
|
+
}
|
|
1761
|
+
if (!res.ok) {
|
|
1762
|
+
console.warn(`[github-bounties] HTTP ${res.status} ${path}`);
|
|
1763
|
+
return null;
|
|
1764
|
+
}
|
|
1765
|
+
try {
|
|
1766
|
+
return await res.json();
|
|
1767
|
+
} catch {
|
|
1768
|
+
return null;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
async function fetchCommentAmount(repoFullName, issueNumber) {
|
|
1772
|
+
const comments = await ghJson(
|
|
1773
|
+
`/repos/${repoFullName}/issues/${issueNumber}/comments?per_page=30`
|
|
1774
|
+
);
|
|
1775
|
+
if (!comments) return void 0;
|
|
1776
|
+
for (const c of comments) {
|
|
1777
|
+
const body = c.body ?? "";
|
|
1778
|
+
if (BOUNTY_LABEL_RE.test(body)) {
|
|
1779
|
+
const amt = parseAmountUSD(body);
|
|
1780
|
+
if (amt != null) return amt;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
return void 0;
|
|
1784
|
+
}
|
|
1785
|
+
async function fetchRepoBounties(repoFullName) {
|
|
1786
|
+
const repo = await ghJson(`/repos/${repoFullName}`);
|
|
1787
|
+
if (!repo) return [];
|
|
1788
|
+
const meta = {
|
|
1789
|
+
fullName: repo.full_name,
|
|
1790
|
+
stargazers: repo.stargazers_count,
|
|
1791
|
+
createdAt: repo.created_at,
|
|
1792
|
+
archived: repo.archived,
|
|
1793
|
+
disabled: repo.disabled
|
|
1794
|
+
};
|
|
1795
|
+
if (!passesMaturityGate(meta)) {
|
|
1796
|
+
console.info(`[github-bounties] ${repoFullName}: failed maturity gate, skipping`);
|
|
1797
|
+
return [];
|
|
1798
|
+
}
|
|
1799
|
+
const issues = await ghJson(`/repos/${repoFullName}/issues?state=open&per_page=100`);
|
|
1800
|
+
if (!issues) return [];
|
|
1801
|
+
const bounties = issues.filter(isBountyIssue).slice(0, MAX_BOUNTIES_PER_REPO);
|
|
1802
|
+
const owner = repo.owner.login;
|
|
1803
|
+
return mapWithConcurrency(bounties, BOUNTY_FETCH_CONCURRENCY, async (issue) => {
|
|
1804
|
+
const title = decodeEntities(issue.title).trim();
|
|
1805
|
+
const body = issue.body ? decodeEntities(issue.body) : "";
|
|
1806
|
+
const amountUSD = parseAmountUSD(title) ?? parseAmountUSD(body) ?? await fetchCommentAmount(repoFullName, issue.number);
|
|
1807
|
+
const labels = labelNames(issue);
|
|
1808
|
+
const tags = normalize(tokenize2([title, labels.join(" "), body.slice(0, 2e3)].join(" ")));
|
|
1809
|
+
return {
|
|
1810
|
+
id: `bounty:${repoFullName}#${issue.number}`,
|
|
1811
|
+
source: "bounty",
|
|
1812
|
+
title,
|
|
1813
|
+
company: owner,
|
|
1814
|
+
url: issue.html_url,
|
|
1815
|
+
remote: true,
|
|
1816
|
+
location: "Remote",
|
|
1817
|
+
tags,
|
|
1818
|
+
roleType: "freelance",
|
|
1819
|
+
postedAt: issue.created_at,
|
|
1820
|
+
applyMode: "direct",
|
|
1821
|
+
bounty: {
|
|
1822
|
+
amountUSD,
|
|
1823
|
+
estimatedEffort: effortFromAmount(amountUSD),
|
|
1824
|
+
bountySource: "github",
|
|
1825
|
+
claimUrl: issue.html_url,
|
|
1826
|
+
repoFullName,
|
|
1827
|
+
repoStars: repo.stargazers_count,
|
|
1828
|
+
issueBody: body.slice(0, 1e3) || void 0
|
|
1829
|
+
},
|
|
1830
|
+
raw: issue
|
|
1831
|
+
};
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
function repoFullNameFromApiUrl(url) {
|
|
1835
|
+
const m = url.match(/\/repos\/([^/]+)\/([^/]+)\/?$/);
|
|
1836
|
+
return m ? `${m[1]}/${m[2]}` : null;
|
|
1837
|
+
}
|
|
1838
|
+
async function searchBountyIssues() {
|
|
1839
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
1840
|
+
for (const q of SEARCH_QUERIES) {
|
|
1841
|
+
const res = await ghJson(
|
|
1842
|
+
`/search/issues?q=${encodeURIComponent(q)}&sort=created&order=desc&per_page=${SEARCH_PER_PAGE}`
|
|
1843
|
+
);
|
|
1844
|
+
for (const it of res?.items ?? []) {
|
|
1845
|
+
if (it.pull_request) continue;
|
|
1846
|
+
if (!byUrl.has(it.html_url)) byUrl.set(it.html_url, it);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
return [...byUrl.values()].sort(
|
|
1850
|
+
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
async function repoMetaCached(fullName) {
|
|
1854
|
+
const hit = repoMetaCache.get(fullName);
|
|
1855
|
+
if (hit !== void 0) return hit;
|
|
1856
|
+
const r = await ghJson(`/repos/${fullName}`) ?? null;
|
|
1857
|
+
repoMetaCache.set(fullName, r);
|
|
1858
|
+
return r;
|
|
1859
|
+
}
|
|
1860
|
+
async function fetchRepoMeta2(fullName) {
|
|
1861
|
+
const repo = await repoMetaCached(fullName);
|
|
1862
|
+
if (!repo) return null;
|
|
1863
|
+
return {
|
|
1864
|
+
fullName: repo.full_name,
|
|
1865
|
+
stargazers: repo.stargazers_count,
|
|
1866
|
+
createdAt: repo.created_at,
|
|
1867
|
+
archived: repo.archived,
|
|
1868
|
+
disabled: repo.disabled
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
async function fetchRepoOpenPRRefs(fullName) {
|
|
1872
|
+
const hit = repoOpenPRRefsCache.get(fullName);
|
|
1873
|
+
if (hit !== void 0) return hit;
|
|
1874
|
+
const refs = /* @__PURE__ */ new Map();
|
|
1875
|
+
let scannedAny = false;
|
|
1876
|
+
for (let page = 1; page <= MAX_PR_PAGES; page++) {
|
|
1877
|
+
const prs = await ghJson(
|
|
1878
|
+
`/repos/${fullName}/pulls?state=open&per_page=100&page=${page}`
|
|
1879
|
+
);
|
|
1880
|
+
if (!Array.isArray(prs)) break;
|
|
1881
|
+
scannedAny = true;
|
|
1882
|
+
for (const pr of prs) {
|
|
1883
|
+
const counted = /* @__PURE__ */ new Set();
|
|
1884
|
+
for (const m of `${pr.title ?? ""}
|
|
1885
|
+
${pr.body ?? ""}`.matchAll(/#(\d+)\b/g)) {
|
|
1886
|
+
const n = Number(m[1]);
|
|
1887
|
+
if (!counted.has(n)) {
|
|
1888
|
+
counted.add(n);
|
|
1889
|
+
refs.set(n, (refs.get(n) ?? 0) + 1);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
if (prs.length < 100) break;
|
|
1894
|
+
}
|
|
1895
|
+
const result = scannedAny ? refs : null;
|
|
1896
|
+
repoOpenPRRefsCache.set(fullName, result);
|
|
1897
|
+
return result;
|
|
1898
|
+
}
|
|
1899
|
+
async function fetchIssueState(fullName, issueNumber) {
|
|
1900
|
+
const key = `${fullName}#${issueNumber}`;
|
|
1901
|
+
const hit = issueStateCache.get(key);
|
|
1902
|
+
if (hit !== void 0) return hit;
|
|
1903
|
+
const issue = await ghJson(`/repos/${fullName}/issues/${issueNumber}`);
|
|
1904
|
+
const state = issue?.state === "open" ? "open" : issue?.state === "closed" ? "closed" : null;
|
|
1905
|
+
issueStateCache.set(key, state);
|
|
1906
|
+
return state;
|
|
1907
|
+
}
|
|
1908
|
+
async function fetchSearchBounties() {
|
|
1909
|
+
const issues = (await searchBountyIssues()).slice(0, MAX_SEARCH_ISSUES_SCANNED);
|
|
1910
|
+
const distinctRepos = [
|
|
1911
|
+
...new Set(
|
|
1912
|
+
issues.map((i) => repoFullNameFromApiUrl(i.repository_url)).filter((x) => !!x)
|
|
1913
|
+
)
|
|
1914
|
+
];
|
|
1915
|
+
for (let i = 0; i < distinctRepos.length; i += REPO_META_CONCURRENCY) {
|
|
1916
|
+
await Promise.all(distinctRepos.slice(i, i + REPO_META_CONCURRENCY).map(repoMetaCached));
|
|
1917
|
+
}
|
|
1918
|
+
const jobs = [];
|
|
1919
|
+
const perRepo = /* @__PURE__ */ new Map();
|
|
1920
|
+
for (const issue of issues) {
|
|
1921
|
+
if (jobs.length >= MAX_SEARCH_BOUNTIES) break;
|
|
1922
|
+
const fullName = repoFullNameFromApiUrl(issue.repository_url);
|
|
1923
|
+
if (!fullName) continue;
|
|
1924
|
+
if ((perRepo.get(fullName) ?? 0) >= MAX_BOUNTIES_PER_REPO) continue;
|
|
1925
|
+
const repo = await repoMetaCached(fullName);
|
|
1926
|
+
if (!repo) continue;
|
|
1927
|
+
const passes = passesMaturityGate({
|
|
1928
|
+
fullName: repo.full_name,
|
|
1929
|
+
stargazers: repo.stargazers_count,
|
|
1930
|
+
createdAt: repo.created_at,
|
|
1931
|
+
archived: repo.archived,
|
|
1932
|
+
disabled: repo.disabled
|
|
1933
|
+
});
|
|
1934
|
+
if (!passes) continue;
|
|
1935
|
+
const title = decodeEntities(issue.title).trim();
|
|
1936
|
+
const body = issue.body ? decodeEntities(issue.body) : "";
|
|
1937
|
+
const labels = labelNames(issue);
|
|
1938
|
+
let amountUSD = parseAmountUSD(title) ?? parseAmountUSD(labels.join(" ")) ?? parseAmountUSD(body);
|
|
1939
|
+
if (amountUSD == null && labels.some((n) => /💎|💰/.test(n))) {
|
|
1940
|
+
amountUSD = await fetchCommentAmount(fullName, issue.number);
|
|
1941
|
+
}
|
|
1942
|
+
if (amountUSD == null) continue;
|
|
1943
|
+
if (!passesAntiFarm(amountUSD, repo.stargazers_count)) continue;
|
|
1944
|
+
const tags = normalize(
|
|
1945
|
+
tokenize2([title, labels.join(" "), body.slice(0, 2e3)].join(" "))
|
|
1946
|
+
);
|
|
1947
|
+
perRepo.set(fullName, (perRepo.get(fullName) ?? 0) + 1);
|
|
1948
|
+
jobs.push({
|
|
1949
|
+
id: `bounty:${fullName}#${issue.number}`,
|
|
1950
|
+
source: "bounty",
|
|
1951
|
+
title,
|
|
1952
|
+
company: repo.owner.login,
|
|
1953
|
+
url: issue.html_url,
|
|
1954
|
+
remote: true,
|
|
1955
|
+
location: "Remote",
|
|
1956
|
+
tags,
|
|
1957
|
+
roleType: "freelance",
|
|
1958
|
+
postedAt: issue.created_at,
|
|
1959
|
+
applyMode: "direct",
|
|
1960
|
+
bounty: {
|
|
1961
|
+
amountUSD,
|
|
1962
|
+
estimatedEffort: effortFromAmount(amountUSD),
|
|
1963
|
+
bountySource: "github",
|
|
1964
|
+
claimUrl: issue.html_url,
|
|
1965
|
+
repoFullName: fullName,
|
|
1966
|
+
repoStars: repo.stargazers_count,
|
|
1967
|
+
issueBody: body.slice(0, 1e3) || void 0
|
|
1968
|
+
},
|
|
1969
|
+
raw: issue
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
return jobs;
|
|
1973
|
+
}
|
|
1974
|
+
var GITHUB_API, BOUNTY_LABEL_RE, SEARCH_QUERIES, SEARCH_PER_PAGE, MAX_SEARCH_BOUNTIES, MAX_SEARCH_ISSUES_SCANNED, REPO_META_CONCURRENCY, BOUNTY_FETCH_CONCURRENCY, repoMetaCache, MAX_PR_PAGES, repoOpenPRRefsCache, issueStateCache, githubBounties;
|
|
1975
|
+
var init_github_bounties = __esm({
|
|
1976
|
+
"../../packages/core/src/feeds/github-bounties.ts"() {
|
|
1977
|
+
"use strict";
|
|
1978
|
+
init_vocabulary();
|
|
1979
|
+
init_entities();
|
|
1980
|
+
init_bounty_gate();
|
|
1981
|
+
init_http();
|
|
1982
|
+
init_concurrency();
|
|
1983
|
+
GITHUB_API = "https://api.github.com";
|
|
1984
|
+
BOUNTY_LABEL_RE = /bounty|reward|funded|💎|💰/i;
|
|
1985
|
+
SEARCH_QUERIES = [
|
|
1986
|
+
'label:"\u{1F48E} Bounty" type:issue state:open',
|
|
1987
|
+
// Algora-applied — highest signal
|
|
1988
|
+
"label:bounty type:issue state:open",
|
|
1989
|
+
'label:"\u{1F4B0} Bounty" type:issue state:open'
|
|
1990
|
+
];
|
|
1991
|
+
SEARCH_PER_PAGE = 100;
|
|
1992
|
+
MAX_SEARCH_BOUNTIES = 150;
|
|
1993
|
+
MAX_SEARCH_ISSUES_SCANNED = 300;
|
|
1994
|
+
REPO_META_CONCURRENCY = 15;
|
|
1995
|
+
BOUNTY_FETCH_CONCURRENCY = 6;
|
|
1996
|
+
repoMetaCache = /* @__PURE__ */ new Map();
|
|
1997
|
+
MAX_PR_PAGES = 3;
|
|
1998
|
+
repoOpenPRRefsCache = /* @__PURE__ */ new Map();
|
|
1999
|
+
issueStateCache = /* @__PURE__ */ new Map();
|
|
2000
|
+
githubBounties = {
|
|
2001
|
+
source: "bounty",
|
|
2002
|
+
async fetch(opts) {
|
|
2003
|
+
const allowlist = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : DEFAULT_BOUNTY_REPOS;
|
|
2004
|
+
const [searched, listed] = await Promise.all([
|
|
2005
|
+
fetchSearchBounties().catch((e) => {
|
|
2006
|
+
console.warn("[github-bounties] search discovery failed:", e);
|
|
2007
|
+
return [];
|
|
2008
|
+
}),
|
|
2009
|
+
Promise.allSettled(allowlist.map(fetchRepoBounties)).then(
|
|
2010
|
+
(settled) => settled.flatMap((r) => r.status === "fulfilled" ? r.value : [])
|
|
2011
|
+
)
|
|
2012
|
+
]);
|
|
2013
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2014
|
+
const out = [];
|
|
2015
|
+
for (const j of [...searched, ...listed]) {
|
|
2016
|
+
if (!seen.has(j.id)) {
|
|
2017
|
+
seen.add(j.id);
|
|
2018
|
+
out.push(j);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
console.info(
|
|
2022
|
+
`[github-bounties] total: ${out.length} bounties (${searched.length} search + ${listed.length} allowlist, deduped)`
|
|
2023
|
+
);
|
|
2024
|
+
return out;
|
|
2025
|
+
}
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
});
|
|
2029
|
+
|
|
2030
|
+
// ../../packages/core/src/feeds/opire.ts
|
|
2031
|
+
function tokenize3(text) {
|
|
2032
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter((w) => w.length > 1);
|
|
2033
|
+
}
|
|
2034
|
+
function effortFromAmount2(usd) {
|
|
2035
|
+
if (usd == null) return void 0;
|
|
2036
|
+
if (usd < 150) return "small";
|
|
2037
|
+
if (usd < 750) return "medium";
|
|
2038
|
+
return "large";
|
|
2039
|
+
}
|
|
2040
|
+
function priceToUSD(p) {
|
|
2041
|
+
if (!p || typeof p.value !== "number") return void 0;
|
|
2042
|
+
if (p.unit === "USD_CENT") return Math.round(p.value) / 100;
|
|
2043
|
+
if (p.unit === "USD") return p.value;
|
|
2044
|
+
return void 0;
|
|
2045
|
+
}
|
|
2046
|
+
function repoFullNameFromUrl(url) {
|
|
2047
|
+
const m = url?.match(/github\.com\/([^/]+)\/([^/]+)/i);
|
|
2048
|
+
return m ? `${m[1]}/${m[2].replace(/\.git$/, "")}` : void 0;
|
|
2049
|
+
}
|
|
2050
|
+
function issueNumberFromUrl(url) {
|
|
2051
|
+
const m = url?.match(/\/issues\/(\d+)/);
|
|
2052
|
+
return m ? parseInt(m[1], 10) : void 0;
|
|
2053
|
+
}
|
|
2054
|
+
var OPIRE_REWARDS_URL, MIN_USD, MAX_USD, MAX_OPIRE_BOUNTIES, REPO_META_CONCURRENCY2, opire;
|
|
2055
|
+
var init_opire = __esm({
|
|
2056
|
+
"../../packages/core/src/feeds/opire.ts"() {
|
|
2057
|
+
"use strict";
|
|
2058
|
+
init_vocabulary();
|
|
2059
|
+
init_bounty_gate();
|
|
2060
|
+
init_github_bounties();
|
|
2061
|
+
init_http();
|
|
2062
|
+
OPIRE_REWARDS_URL = "https://api.opire.dev/rewards";
|
|
2063
|
+
MIN_USD = 25;
|
|
2064
|
+
MAX_USD = 25e3;
|
|
2065
|
+
MAX_OPIRE_BOUNTIES = 100;
|
|
2066
|
+
REPO_META_CONCURRENCY2 = 15;
|
|
2067
|
+
opire = {
|
|
2068
|
+
source: "bounty",
|
|
2069
|
+
async fetch() {
|
|
2070
|
+
let rewards;
|
|
2071
|
+
try {
|
|
2072
|
+
const res = await fetchWithTimeout(OPIRE_REWARDS_URL, {
|
|
2073
|
+
headers: { Accept: "application/json", "User-Agent": "terminalhire" }
|
|
2074
|
+
});
|
|
2075
|
+
if (!res.ok) {
|
|
2076
|
+
console.warn(`[opire] HTTP ${res.status}`);
|
|
2077
|
+
return [];
|
|
2078
|
+
}
|
|
2079
|
+
const json = await res.json();
|
|
2080
|
+
rewards = Array.isArray(json) ? json : json?.data ?? json?.items ?? [];
|
|
2081
|
+
} catch (err) {
|
|
2082
|
+
console.warn("[opire] fetch failed \u2014", err);
|
|
2083
|
+
return [];
|
|
2084
|
+
}
|
|
2085
|
+
const candidates = [];
|
|
2086
|
+
for (const r of rewards) {
|
|
2087
|
+
if (r.platform !== "GitHub") continue;
|
|
2088
|
+
if (r.project && r.project.isPublic === false) continue;
|
|
2089
|
+
const repoFullName = repoFullNameFromUrl(r.project?.url ?? r.url);
|
|
2090
|
+
if (!repoFullName) continue;
|
|
2091
|
+
const amountUSD = priceToUSD(r.pendingPrice);
|
|
2092
|
+
if (amountUSD == null || amountUSD < MIN_USD || amountUSD > MAX_USD) continue;
|
|
2093
|
+
const title = (r.title ?? "").trim();
|
|
2094
|
+
if (title.length < 4) continue;
|
|
2095
|
+
const tags = normalize([...r.programmingLanguages ?? [], ...tokenize3(title)]);
|
|
2096
|
+
const bounty = {
|
|
2097
|
+
amountUSD,
|
|
2098
|
+
estimatedEffort: effortFromAmount2(amountUSD),
|
|
2099
|
+
bountySource: "opire",
|
|
2100
|
+
claimUrl: r.url,
|
|
2101
|
+
repoFullName
|
|
2102
|
+
};
|
|
2103
|
+
candidates.push({
|
|
2104
|
+
repoFullName,
|
|
2105
|
+
amountUSD,
|
|
2106
|
+
issueNumber: issueNumberFromUrl(r.url),
|
|
2107
|
+
bounty,
|
|
2108
|
+
job: {
|
|
2109
|
+
id: `bounty:opire:${r.id}`,
|
|
2110
|
+
source: "bounty",
|
|
2111
|
+
title,
|
|
2112
|
+
company: r.organization?.name ?? repoFullName.split("/")[0],
|
|
2113
|
+
url: r.url,
|
|
2114
|
+
remote: true,
|
|
2115
|
+
location: "Remote",
|
|
2116
|
+
tags,
|
|
2117
|
+
roleType: "freelance",
|
|
2118
|
+
postedAt: Number.isFinite(r.createdAt) ? new Date(r.createdAt).toISOString() : void 0,
|
|
2119
|
+
applyMode: "direct",
|
|
2120
|
+
bounty,
|
|
2121
|
+
raw: r
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
const distinctRepos = [...new Set(candidates.map((c) => c.repoFullName))];
|
|
2126
|
+
const meta = /* @__PURE__ */ new Map();
|
|
2127
|
+
for (let i = 0; i < distinctRepos.length; i += REPO_META_CONCURRENCY2) {
|
|
2128
|
+
const batch = distinctRepos.slice(i, i + REPO_META_CONCURRENCY2);
|
|
2129
|
+
const metas = await Promise.all(batch.map((name) => fetchRepoMeta2(name)));
|
|
2130
|
+
batch.forEach((name, k) => meta.set(name, metas[k]));
|
|
2131
|
+
}
|
|
2132
|
+
const gated = [];
|
|
2133
|
+
let dropped = 0;
|
|
2134
|
+
let ungated = 0;
|
|
2135
|
+
for (const c of candidates) {
|
|
2136
|
+
const m = meta.get(c.repoFullName);
|
|
2137
|
+
if (m) {
|
|
2138
|
+
if (!passesMaturityGate(m) || !passesAntiFarm(c.amountUSD, m.stargazers)) {
|
|
2139
|
+
dropped++;
|
|
2140
|
+
continue;
|
|
2141
|
+
}
|
|
2142
|
+
c.bounty.repoStars = m.stargazers;
|
|
2143
|
+
} else {
|
|
2144
|
+
ungated++;
|
|
2145
|
+
}
|
|
2146
|
+
gated.push(c);
|
|
2147
|
+
}
|
|
2148
|
+
const issueState = /* @__PURE__ */ new Map();
|
|
2149
|
+
for (let i = 0; i < gated.length; i += REPO_META_CONCURRENCY2) {
|
|
2150
|
+
const batch = gated.slice(i, i + REPO_META_CONCURRENCY2);
|
|
2151
|
+
const states = await Promise.all(
|
|
2152
|
+
batch.map(
|
|
2153
|
+
(c) => c.issueNumber != null ? fetchIssueState(c.repoFullName, c.issueNumber) : Promise.resolve(null)
|
|
2154
|
+
)
|
|
2155
|
+
);
|
|
2156
|
+
batch.forEach((c, k) => issueState.set(c.job.id, states[k]));
|
|
2157
|
+
}
|
|
2158
|
+
const jobs = [];
|
|
2159
|
+
let closed = 0;
|
|
2160
|
+
for (const c of gated) {
|
|
2161
|
+
if (jobs.length >= MAX_OPIRE_BOUNTIES) break;
|
|
2162
|
+
if (issueState.get(c.job.id) === "closed") {
|
|
2163
|
+
closed++;
|
|
2164
|
+
continue;
|
|
2165
|
+
}
|
|
2166
|
+
jobs.push(c.job);
|
|
2167
|
+
}
|
|
2168
|
+
console.info(
|
|
2169
|
+
`[opire] ${jobs.length} bounties (from ${rewards.length} rewards; ${dropped} repo-gated, ${closed} closed-issue, ${ungated} kept ungated)`
|
|
2170
|
+
);
|
|
2171
|
+
return jobs;
|
|
2172
|
+
}
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
});
|
|
2176
|
+
|
|
2177
|
+
// ../../packages/core/src/feeds/workable.ts
|
|
2178
|
+
function locationStr(loc) {
|
|
2179
|
+
if (!loc) return "";
|
|
2180
|
+
return [loc.city, loc.country].filter(Boolean).join(", ");
|
|
2181
|
+
}
|
|
2182
|
+
function isRemote(j) {
|
|
2183
|
+
return j.remote === true || (j.workplace ?? "").toLowerCase() === "remote";
|
|
2184
|
+
}
|
|
2185
|
+
function extractTags7(j) {
|
|
2186
|
+
const body = [...j.department ?? [], locationStr(j.location)].filter(Boolean).join(" ");
|
|
2187
|
+
return extractSkillTags(j.title, body);
|
|
2188
|
+
}
|
|
2189
|
+
async function fetchAccount(account) {
|
|
2190
|
+
const url = `https://apply.workable.com/api/v3/accounts/${account}/jobs`;
|
|
2191
|
+
const out = [];
|
|
2192
|
+
let token;
|
|
2193
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
2194
|
+
let res;
|
|
2195
|
+
try {
|
|
2196
|
+
res = await fetchWithTimeout(url, {
|
|
2197
|
+
method: "POST",
|
|
2198
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
2199
|
+
body: JSON.stringify(token ? { token } : {})
|
|
2200
|
+
});
|
|
2201
|
+
} catch (err) {
|
|
2202
|
+
console.warn(`[workable] ${account}: network error \u2014`, err);
|
|
2203
|
+
break;
|
|
2204
|
+
}
|
|
2205
|
+
if (!res.ok) {
|
|
2206
|
+
console.warn(`[workable] ${account}: HTTP ${res.status}`);
|
|
2207
|
+
break;
|
|
2208
|
+
}
|
|
2209
|
+
let data;
|
|
2210
|
+
try {
|
|
2211
|
+
data = await res.json();
|
|
2212
|
+
} catch (err) {
|
|
2213
|
+
console.warn(`[workable] ${account}: JSON parse error \u2014`, err);
|
|
2214
|
+
break;
|
|
2215
|
+
}
|
|
2216
|
+
const results = data.results ?? [];
|
|
2217
|
+
for (const j of results) {
|
|
2218
|
+
if (j.state && j.state !== "published") continue;
|
|
2219
|
+
out.push({
|
|
2220
|
+
id: `workable:${j.id}`,
|
|
2221
|
+
source: "workable",
|
|
2222
|
+
title: j.title,
|
|
2223
|
+
company: account,
|
|
2224
|
+
url: `https://apply.workable.com/${account}/j/${j.shortcode}/`,
|
|
2225
|
+
remote: isRemote(j),
|
|
2226
|
+
location: locationStr(j.location) || void 0,
|
|
2227
|
+
tags: extractTags7(j),
|
|
2228
|
+
roleType: "full_time",
|
|
2229
|
+
postedAt: j.published,
|
|
2230
|
+
applyMode: "direct",
|
|
2231
|
+
raw: j
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
token = data.token;
|
|
2235
|
+
if (!token || results.length === 0) break;
|
|
2236
|
+
}
|
|
2237
|
+
if (out.length > 0) console.info(`[workable] ${account}: ${out.length} jobs`);
|
|
2238
|
+
return out;
|
|
2239
|
+
}
|
|
2240
|
+
var FALLBACK_ACCOUNTS, MAX_PAGES, workable;
|
|
2241
|
+
var init_workable = __esm({
|
|
2242
|
+
"../../packages/core/src/feeds/workable.ts"() {
|
|
2243
|
+
"use strict";
|
|
2244
|
+
init_vocabulary();
|
|
2245
|
+
init_http();
|
|
2246
|
+
FALLBACK_ACCOUNTS = ["zego", "workmotion"];
|
|
2247
|
+
MAX_PAGES = 5;
|
|
2248
|
+
workable = {
|
|
2249
|
+
source: "workable",
|
|
2250
|
+
async fetch(opts) {
|
|
2251
|
+
const accounts = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : FALLBACK_ACCOUNTS;
|
|
2252
|
+
console.info(`[workable] fetching ${accounts.length} accounts: ${accounts.join(", ")}`);
|
|
2253
|
+
const results = await Promise.allSettled(accounts.map(fetchAccount));
|
|
2254
|
+
const jobs = [];
|
|
2255
|
+
let failures = 0;
|
|
2256
|
+
for (const r of results) {
|
|
2257
|
+
if (r.status === "fulfilled") {
|
|
2258
|
+
jobs.push(...r.value);
|
|
2259
|
+
} else {
|
|
2260
|
+
failures++;
|
|
2261
|
+
console.warn("[workable] account fetch rejected:", r.reason);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
console.info(`[workable] total: ${jobs.length} jobs, ${failures} account failures`);
|
|
2265
|
+
return jobs;
|
|
2266
|
+
}
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
});
|
|
2270
|
+
|
|
2271
|
+
// ../../packages/core/src/feeds/directory.ts
|
|
2272
|
+
function personCardToJob(row) {
|
|
2273
|
+
const tags = [...row.skill_tags];
|
|
2274
|
+
return {
|
|
2275
|
+
id: `dev:${row.login}`,
|
|
2276
|
+
source: "person",
|
|
2277
|
+
title: row.name ?? row.login,
|
|
2278
|
+
company: row.login,
|
|
2279
|
+
url: `/r/${row.login}`,
|
|
2280
|
+
remote: true,
|
|
2281
|
+
tags,
|
|
2282
|
+
coreTags: tags.slice(0, TOP_CORE_TAGS),
|
|
2283
|
+
roleType: "full_time",
|
|
2284
|
+
applyMode: "direct"
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
function buildDirectoryIndex(people, opts) {
|
|
2288
|
+
return {
|
|
2289
|
+
builtAt: opts?.builtAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2290
|
+
cards: people.map(personCardToJob)
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2293
|
+
function projectCardToJob(row) {
|
|
2294
|
+
const tags = [...row.needed_skills];
|
|
2295
|
+
return {
|
|
2296
|
+
id: `proj:${row.id}`,
|
|
2297
|
+
source: "project",
|
|
2298
|
+
title: row.title,
|
|
2299
|
+
company: row.owner_login,
|
|
2300
|
+
url: `/r/${row.owner_login}`,
|
|
2301
|
+
remote: true,
|
|
2302
|
+
tags,
|
|
2303
|
+
coreTags: tags.slice(0, TOP_CORE_TAGS),
|
|
2304
|
+
roleType: "full_time",
|
|
2305
|
+
applyMode: "direct"
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
var TOP_CORE_TAGS;
|
|
2309
|
+
var init_directory = __esm({
|
|
2310
|
+
"../../packages/core/src/feeds/directory.ts"() {
|
|
2311
|
+
"use strict";
|
|
2312
|
+
TOP_CORE_TAGS = 4;
|
|
2313
|
+
}
|
|
2314
|
+
});
|
|
2315
|
+
|
|
2316
|
+
// ../../packages/core/src/feeds/index.ts
|
|
2317
|
+
async function aggregateBounties(opts) {
|
|
2318
|
+
const [gh, op] = await Promise.all([
|
|
2319
|
+
githubBounties.fetch({ slugs: opts?.repos }),
|
|
2320
|
+
opire.fetch()
|
|
2321
|
+
]);
|
|
2322
|
+
const allowlist = new Set(
|
|
2323
|
+
(opts?.repos && opts.repos.length > 0 ? opts.repos : DEFAULT_BOUNTY_REPOS).map(
|
|
2324
|
+
(r) => r.toLowerCase()
|
|
2325
|
+
)
|
|
2326
|
+
);
|
|
2327
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2328
|
+
const perRepo = /* @__PURE__ */ new Map();
|
|
2329
|
+
const seenRepoTitles = /* @__PURE__ */ new Set();
|
|
2330
|
+
const out = [];
|
|
2331
|
+
for (const j of [...gh, ...op]) {
|
|
2332
|
+
const key = j.bounty?.claimUrl ?? j.url;
|
|
2333
|
+
if (seen.has(key)) continue;
|
|
2334
|
+
const repo = j.bounty?.repoFullName?.toLowerCase();
|
|
2335
|
+
if (repo) {
|
|
2336
|
+
if (isDenylistedRepo(repo)) continue;
|
|
2337
|
+
const titleKey = `${repo} ${normalizeBountyTitle(j.title)}`;
|
|
2338
|
+
if (seenRepoTitles.has(titleKey)) continue;
|
|
2339
|
+
const cap = allowlist.has(repo) ? MAX_BOUNTIES_PER_REPO : MAX_BOUNTIES_PER_DISCOVERED_REPO;
|
|
2340
|
+
const n = perRepo.get(repo) ?? 0;
|
|
2341
|
+
if (n >= cap) continue;
|
|
2342
|
+
perRepo.set(repo, n + 1);
|
|
2343
|
+
seenRepoTitles.add(titleKey);
|
|
2344
|
+
}
|
|
2345
|
+
seen.add(key);
|
|
2346
|
+
out.push(j);
|
|
2347
|
+
}
|
|
2348
|
+
const repos = [...new Set(out.map((j) => j.bounty?.repoFullName).filter((r) => !!r))];
|
|
2349
|
+
const refsByRepo = /* @__PURE__ */ new Map();
|
|
2350
|
+
const PR_REFS_CONCURRENCY = 15;
|
|
2351
|
+
for (let i = 0; i < repos.length; i += PR_REFS_CONCURRENCY) {
|
|
2352
|
+
const batch = repos.slice(i, i + PR_REFS_CONCURRENCY);
|
|
2353
|
+
const results = await Promise.all(batch.map((r) => fetchRepoOpenPRRefs(r)));
|
|
2354
|
+
batch.forEach((r, k) => refsByRepo.set(r, results[k]));
|
|
2355
|
+
}
|
|
2356
|
+
for (const j of out) {
|
|
2357
|
+
const num = bountyIssueNumber(j.bounty?.claimUrl);
|
|
2358
|
+
const refs = j.bounty?.repoFullName ? refsByRepo.get(j.bounty.repoFullName) : void 0;
|
|
2359
|
+
if (j.bounty && refs && num != null) j.bounty.competingOpenPRs = refs.get(num) ?? 0;
|
|
2360
|
+
}
|
|
2361
|
+
return out;
|
|
2362
|
+
}
|
|
2363
|
+
function bountyIssueNumber(url) {
|
|
2364
|
+
const m = url?.match(/\/issues\/(\d+)/);
|
|
2365
|
+
return m ? Number(m[1]) : void 0;
|
|
2366
|
+
}
|
|
2367
|
+
function normalizeBountyTitle(title) {
|
|
2368
|
+
return title.toLowerCase().replace(/#\d+\s*$/, "").replace(/[^a-z0-9]+/g, " ").trim();
|
|
2369
|
+
}
|
|
2370
|
+
function flattenTiers(t) {
|
|
2371
|
+
return [.../* @__PURE__ */ new Set([...t.bigco, ...t.scaleup, ...t.startup])];
|
|
2372
|
+
}
|
|
2373
|
+
async function aggregate(opts) {
|
|
2374
|
+
const ghSlugs = opts?.slugs?.["greenhouse"] ?? DEFAULT_GREENHOUSE_SLUGS;
|
|
2375
|
+
const ashbySlugs = opts?.slugs?.["ashby"] ?? DEFAULT_ASHBY_SLUGS;
|
|
2376
|
+
const leverSlugs = opts?.slugs?.["lever"] ?? DEFAULT_LEVER_SLUGS;
|
|
2377
|
+
const workableSlugs = opts?.slugs?.["workable"] ?? DEFAULT_WORKABLE_SLUGS;
|
|
2378
|
+
const limit = opts?.limit ?? 150;
|
|
2379
|
+
const settled = await Promise.allSettled([
|
|
2380
|
+
greenhouse.fetch({ slugs: ghSlugs, limit }),
|
|
2381
|
+
ashby.fetch({ slugs: ashbySlugs, limit }),
|
|
2382
|
+
lever.fetch({ slugs: leverSlugs, limit }),
|
|
2383
|
+
workable.fetch({ slugs: workableSlugs, limit }),
|
|
2384
|
+
himalayas.fetch({ limit }),
|
|
2385
|
+
wwr.fetch({ limit }),
|
|
2386
|
+
hn.fetch({ limit })
|
|
2387
|
+
]);
|
|
2388
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2389
|
+
const jobs = [];
|
|
2390
|
+
const sourceNames = ["greenhouse", "ashby", "lever", "workable", "himalayas", "wwr", "hn"];
|
|
2391
|
+
for (let i = 0; i < settled.length; i++) {
|
|
2392
|
+
const result = settled[i];
|
|
2393
|
+
if (result.status === "rejected") {
|
|
2394
|
+
console.warn(`[feeds] ${sourceNames[i]} failed:`, result.reason);
|
|
2395
|
+
continue;
|
|
2396
|
+
}
|
|
2397
|
+
for (const job of result.value) {
|
|
2398
|
+
if (!seen.has(job.id)) {
|
|
2399
|
+
seen.add(job.id);
|
|
2400
|
+
jobs.push(job);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
if (opts?.includeBounties !== false) {
|
|
2405
|
+
try {
|
|
2406
|
+
const bounties = await aggregateBounties({ repos: opts?.slugs?.["bounty"] });
|
|
2407
|
+
for (const b of bounties) {
|
|
2408
|
+
if (!seen.has(b.id)) {
|
|
2409
|
+
seen.add(b.id);
|
|
2410
|
+
jobs.push(b);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
} catch (err) {
|
|
2414
|
+
console.warn("[feeds] bounties failed:", err);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
return jobs;
|
|
2418
|
+
}
|
|
2419
|
+
var FEEDS, GREENHOUSE_SLUGS_BY_TIER, ASHBY_SLUGS_BY_TIER, LEVER_SLUGS_BY_TIER, DEFAULT_GREENHOUSE_SLUGS, DEFAULT_ASHBY_SLUGS, DEFAULT_LEVER_SLUGS, DEFAULT_WORKABLE_SLUGS;
|
|
2420
|
+
var init_feeds = __esm({
|
|
2421
|
+
"../../packages/core/src/feeds/index.ts"() {
|
|
2422
|
+
"use strict";
|
|
2423
|
+
init_greenhouse();
|
|
2424
|
+
init_ashby();
|
|
2425
|
+
init_lever();
|
|
2426
|
+
init_himalayas();
|
|
2427
|
+
init_wwr();
|
|
2428
|
+
init_hn();
|
|
2429
|
+
init_github_bounties();
|
|
2430
|
+
init_opire();
|
|
2431
|
+
init_workable();
|
|
2432
|
+
init_directory();
|
|
2433
|
+
init_bounty_gate();
|
|
2434
|
+
init_bounty_gate();
|
|
2435
|
+
FEEDS = [greenhouse, ashby, lever, workable, himalayas, wwr, hn];
|
|
2436
|
+
GREENHOUSE_SLUGS_BY_TIER = {
|
|
2437
|
+
bigco: [
|
|
2438
|
+
"stripe",
|
|
2439
|
+
"anthropic",
|
|
2440
|
+
"figma",
|
|
2441
|
+
"discord",
|
|
2442
|
+
"brex",
|
|
2443
|
+
"mercury",
|
|
2444
|
+
"plaid",
|
|
2445
|
+
"gusto",
|
|
2446
|
+
"scale",
|
|
2447
|
+
"databricks",
|
|
2448
|
+
"coinbase",
|
|
2449
|
+
"robinhood",
|
|
2450
|
+
"doordash",
|
|
2451
|
+
"airbnb",
|
|
2452
|
+
"dropbox",
|
|
2453
|
+
"datadog",
|
|
2454
|
+
"cloudflare",
|
|
2455
|
+
"reddit",
|
|
2456
|
+
"lyft",
|
|
2457
|
+
"instacart"
|
|
2458
|
+
],
|
|
2459
|
+
scaleup: [
|
|
2460
|
+
"samsara",
|
|
2461
|
+
"verkada",
|
|
2462
|
+
"affirm",
|
|
2463
|
+
"gitlab",
|
|
2464
|
+
"asana",
|
|
2465
|
+
"flexport",
|
|
2466
|
+
"faire",
|
|
2467
|
+
"twitch",
|
|
2468
|
+
"airtable",
|
|
2469
|
+
"retool"
|
|
2470
|
+
],
|
|
2471
|
+
startup: [
|
|
2472
|
+
"watershed"
|
|
2473
|
+
]
|
|
2474
|
+
};
|
|
2475
|
+
ASHBY_SLUGS_BY_TIER = {
|
|
2476
|
+
bigco: [
|
|
2477
|
+
"openai"
|
|
2478
|
+
],
|
|
2479
|
+
scaleup: [
|
|
2480
|
+
"harvey",
|
|
2481
|
+
"elevenlabs",
|
|
2482
|
+
"notion",
|
|
2483
|
+
"sierra",
|
|
2484
|
+
"cohere",
|
|
2485
|
+
"ramp",
|
|
2486
|
+
"vanta",
|
|
2487
|
+
"decagon",
|
|
2488
|
+
"cursor",
|
|
2489
|
+
"replit",
|
|
2490
|
+
"perplexity",
|
|
2491
|
+
"baseten",
|
|
2492
|
+
"drata",
|
|
2493
|
+
"writer",
|
|
2494
|
+
"temporal",
|
|
2495
|
+
"supabase"
|
|
2496
|
+
],
|
|
2497
|
+
startup: [
|
|
2498
|
+
"suno",
|
|
2499
|
+
"attio",
|
|
2500
|
+
"modal",
|
|
2501
|
+
"workos",
|
|
2502
|
+
"linear",
|
|
2503
|
+
"render",
|
|
2504
|
+
"warp",
|
|
2505
|
+
"plain",
|
|
2506
|
+
"posthog",
|
|
2507
|
+
"pylon",
|
|
2508
|
+
"resend",
|
|
2509
|
+
"langfuse",
|
|
2510
|
+
"railway",
|
|
2511
|
+
"mintlify",
|
|
2512
|
+
"neon",
|
|
2513
|
+
"browserbase",
|
|
2514
|
+
"knock",
|
|
2515
|
+
"speakeasy",
|
|
2516
|
+
"stytch",
|
|
2517
|
+
"runway",
|
|
2518
|
+
"doppler",
|
|
2519
|
+
"inngest",
|
|
2520
|
+
"hightouch",
|
|
2521
|
+
"zed"
|
|
2522
|
+
]
|
|
2523
|
+
};
|
|
2524
|
+
LEVER_SLUGS_BY_TIER = {
|
|
2525
|
+
bigco: [
|
|
2526
|
+
"palantir",
|
|
2527
|
+
"spotify"
|
|
2528
|
+
],
|
|
2529
|
+
scaleup: [
|
|
2530
|
+
"mistral",
|
|
2531
|
+
"ro",
|
|
2532
|
+
"secureframe"
|
|
2533
|
+
],
|
|
2534
|
+
startup: [
|
|
2535
|
+
"anyscale"
|
|
2536
|
+
]
|
|
2537
|
+
};
|
|
2538
|
+
DEFAULT_GREENHOUSE_SLUGS = flattenTiers(GREENHOUSE_SLUGS_BY_TIER);
|
|
2539
|
+
DEFAULT_ASHBY_SLUGS = flattenTiers(ASHBY_SLUGS_BY_TIER);
|
|
2540
|
+
DEFAULT_LEVER_SLUGS = flattenTiers(LEVER_SLUGS_BY_TIER);
|
|
2541
|
+
DEFAULT_WORKABLE_SLUGS = ["zego", "workmotion"];
|
|
2542
|
+
}
|
|
2543
|
+
});
|
|
2544
|
+
|
|
2545
|
+
// ../../packages/core/src/partners.ts
|
|
2546
|
+
import { readFileSync } from "fs";
|
|
2547
|
+
import { join } from "path";
|
|
2548
|
+
import { fileURLToPath } from "url";
|
|
2549
|
+
function resolveDataPath() {
|
|
2550
|
+
try {
|
|
2551
|
+
const dir = fileURLToPath(new URL("../../../data", import.meta.url));
|
|
2552
|
+
return join(dir, "partner-roles.json");
|
|
2553
|
+
} catch {
|
|
2554
|
+
return join(process.cwd(), "data", "partner-roles.json");
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
function loadPartnerRoles() {
|
|
2558
|
+
const filePath = resolveDataPath();
|
|
2559
|
+
try {
|
|
2560
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
2561
|
+
const parsed = JSON.parse(raw);
|
|
2562
|
+
if (!Array.isArray(parsed)) {
|
|
2563
|
+
console.warn("[partners] partner-roles.json is not an array \u2014 skipping");
|
|
2564
|
+
return [];
|
|
2565
|
+
}
|
|
2566
|
+
const valid = [];
|
|
2567
|
+
for (const entry of parsed) {
|
|
2568
|
+
const e = entry;
|
|
2569
|
+
if (typeof entry === "object" && entry !== null && typeof e.id === "string" && e.applyMode === "buyer-lead" && typeof e.buyer === "string" && e.buyer.length > 0) {
|
|
2570
|
+
valid.push(entry);
|
|
2571
|
+
} else {
|
|
2572
|
+
console.warn("[partners] Skipping malformed role entry:", entry);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
return valid;
|
|
2576
|
+
} catch (err) {
|
|
2577
|
+
if (err.code === "ENOENT") {
|
|
2578
|
+
console.warn(`[partners] data/partner-roles.json not found at ${filePath} \u2014 no partner roles loaded`);
|
|
2579
|
+
} else {
|
|
2580
|
+
console.warn("[partners] Failed to load partner-roles.json:", err);
|
|
2581
|
+
}
|
|
2582
|
+
return [];
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
function getBuyer(id) {
|
|
2586
|
+
return BUYER_REGISTRY[id];
|
|
2587
|
+
}
|
|
2588
|
+
var EXAMPLE_BUYER, BUYER_REGISTRY;
|
|
2589
|
+
var init_partners = __esm({
|
|
2590
|
+
"../../packages/core/src/partners.ts"() {
|
|
2591
|
+
"use strict";
|
|
2592
|
+
EXAMPLE_BUYER = {
|
|
2593
|
+
id: "northstar",
|
|
2594
|
+
legalName: "Northstar Talent Partners",
|
|
2595
|
+
matchCriteria: { roleTypes: ["full_time"] }
|
|
2596
|
+
};
|
|
2597
|
+
BUYER_REGISTRY = {
|
|
2598
|
+
[EXAMPLE_BUYER.id]: EXAMPLE_BUYER
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2601
|
+
});
|
|
2602
|
+
|
|
2603
|
+
// ../../packages/core/src/indexer.ts
|
|
2604
|
+
async function buildIndex(opts) {
|
|
2605
|
+
const includePartners = opts?.includePartners ?? true;
|
|
2606
|
+
const publicJobs = await aggregate(opts);
|
|
2607
|
+
const allJobs = [...publicJobs];
|
|
2608
|
+
const seen = new Set(publicJobs.map((j) => j.id));
|
|
2609
|
+
const partnerJobs = [
|
|
2610
|
+
...includePartners ? loadPartnerRoles() : [],
|
|
2611
|
+
...opts?.partnerRoles ?? []
|
|
2612
|
+
];
|
|
2613
|
+
for (const job of partnerJobs) {
|
|
2614
|
+
if (!seen.has(job.id)) {
|
|
2615
|
+
seen.add(job.id);
|
|
2616
|
+
allJobs.push(job);
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
const jobs = allJobs.map(({ raw: _raw, ...rest }) => rest);
|
|
2620
|
+
return {
|
|
2621
|
+
builtAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2622
|
+
jobs
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
var init_indexer = __esm({
|
|
2626
|
+
"../../packages/core/src/indexer.ts"() {
|
|
2627
|
+
"use strict";
|
|
2628
|
+
init_feeds();
|
|
2629
|
+
init_partners();
|
|
2630
|
+
}
|
|
2631
|
+
});
|
|
2632
|
+
|
|
2633
|
+
// ../../packages/core/src/intro.ts
|
|
2634
|
+
function buildIntroPayload(input) {
|
|
2635
|
+
const payload = {
|
|
2636
|
+
requesterLogin: input.requesterLogin,
|
|
2637
|
+
requesterDisplayName: input.requesterDisplayName,
|
|
2638
|
+
requesterContact: input.requesterContact,
|
|
2639
|
+
targetLogin: input.targetLogin
|
|
2640
|
+
};
|
|
2641
|
+
const note = input.note?.trim();
|
|
2642
|
+
if (note) payload.note = note;
|
|
2643
|
+
return payload;
|
|
2644
|
+
}
|
|
2645
|
+
function rejectExtraIntroFields(body) {
|
|
2646
|
+
for (const key of Object.keys(body)) {
|
|
2647
|
+
if (!INTRO_ALLOWED_SET.has(key)) {
|
|
2648
|
+
return `intro payload contains disallowed field: "${key}"`;
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
return null;
|
|
2652
|
+
}
|
|
2653
|
+
function validateIntroPayload(body) {
|
|
2654
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
2655
|
+
return { ok: false, reason: "intro payload must be a JSON object" };
|
|
2656
|
+
}
|
|
2657
|
+
const b = body;
|
|
2658
|
+
const extra = rejectExtraIntroFields(b);
|
|
2659
|
+
if (extra) return { ok: false, reason: extra };
|
|
2660
|
+
const ok = (v, max) => typeof v === "string" && v.trim().length > 0 && v.length <= max;
|
|
2661
|
+
if (!ok(b.requesterLogin, MAX_SHORT)) return { ok: false, reason: "requesterLogin is required" };
|
|
2662
|
+
if (!ok(b.requesterDisplayName, MAX_SHORT)) return { ok: false, reason: "requesterDisplayName is required" };
|
|
2663
|
+
if (!ok(b.requesterContact, MAX_SHORT)) return { ok: false, reason: "requesterContact is required" };
|
|
2664
|
+
if (!ok(b.targetLogin, MAX_SHORT)) return { ok: false, reason: "targetLogin is required" };
|
|
2665
|
+
if (b.note !== void 0 && (typeof b.note !== "string" || b.note.length > MAX_NOTE)) {
|
|
2666
|
+
return { ok: false, reason: "note must be a string of at most 500 chars" };
|
|
2667
|
+
}
|
|
2668
|
+
const value = {
|
|
2669
|
+
requesterLogin: b.requesterLogin.trim(),
|
|
2670
|
+
requesterDisplayName: b.requesterDisplayName.trim(),
|
|
2671
|
+
requesterContact: b.requesterContact.trim(),
|
|
2672
|
+
targetLogin: b.targetLogin.trim()
|
|
2673
|
+
};
|
|
2674
|
+
const note = typeof b.note === "string" ? b.note.trim() : "";
|
|
2675
|
+
if (note) value.note = note;
|
|
2676
|
+
return { ok: true, value };
|
|
2677
|
+
}
|
|
2678
|
+
function introRateLimitCheck(history, now, opts) {
|
|
2679
|
+
const cutoff = now - opts.windowMs;
|
|
2680
|
+
const recent = history.filter((t) => t > cutoff);
|
|
2681
|
+
if (recent.length >= opts.max) {
|
|
2682
|
+
const oldest = recent[0] ?? now;
|
|
2683
|
+
return { allowed: false, retained: recent, retryAfterMs: Math.max(0, oldest + opts.windowMs - now) };
|
|
2684
|
+
}
|
|
2685
|
+
return { allowed: true, retained: [...recent, now], retryAfterMs: 0 };
|
|
2686
|
+
}
|
|
2687
|
+
function isOverIntroLimit(recentCount, max) {
|
|
2688
|
+
return recentCount >= max;
|
|
2689
|
+
}
|
|
2690
|
+
function composeIntroEmail(args2) {
|
|
2691
|
+
const subject = `New intro request from @${args2.requesterLogin} \xB7 terminalhire`;
|
|
2692
|
+
const text = `@${args2.requesterLogin} wants an intro to you on terminalhire.
|
|
2693
|
+
|
|
2694
|
+
Sign in to view the request and choose whether to share your contact back:
|
|
2695
|
+
${args2.dashboardUrl}
|
|
2696
|
+
|
|
2697
|
+
You control whether this connects \u2014 no contact details are shared unless you accept.
|
|
2698
|
+
|
|
2699
|
+
\u2014 Terminalhire`;
|
|
2700
|
+
return { subject, text };
|
|
2701
|
+
}
|
|
2702
|
+
function introActorRole(intro, actorLogin) {
|
|
2703
|
+
const a = actorLogin.trim().toLowerCase();
|
|
2704
|
+
if (a && a === intro.targetLogin.trim().toLowerCase()) return "target";
|
|
2705
|
+
if (a && a === intro.requesterLogin.trim().toLowerCase()) return "requester";
|
|
2706
|
+
return "other";
|
|
2707
|
+
}
|
|
2708
|
+
function authorizeIntroDecision(intro, actorLogin) {
|
|
2709
|
+
const role = introActorRole(intro, actorLogin);
|
|
2710
|
+
if (role === "target") return { ok: true };
|
|
2711
|
+
if (role === "requester") {
|
|
2712
|
+
return { ok: false, status: 403, reason: "the requester cannot accept or decline their own intro request" };
|
|
2713
|
+
}
|
|
2714
|
+
return { ok: false, status: 404, reason: "intro not found" };
|
|
2715
|
+
}
|
|
2716
|
+
function authorizeIntroDeletion(intro, actorLogin) {
|
|
2717
|
+
const role = introActorRole(intro, actorLogin);
|
|
2718
|
+
if (role === "other") return { ok: false, status: 404, reason: "intro not found" };
|
|
2719
|
+
return { ok: true };
|
|
2720
|
+
}
|
|
2721
|
+
function revealIntroContacts(intro) {
|
|
2722
|
+
if (intro.status !== "accepted") return { toRequester: null, toTarget: null };
|
|
2723
|
+
return { toRequester: intro.targetContact ?? null, toTarget: intro.requesterContact };
|
|
2724
|
+
}
|
|
2725
|
+
function validateTargetContact(v) {
|
|
2726
|
+
if (typeof v !== "string" || v.trim().length === 0) return { ok: false, reason: "targetContact is required" };
|
|
2727
|
+
if (v.length > MAX_SHORT) return { ok: false, reason: `targetContact must be at most ${MAX_SHORT} chars` };
|
|
2728
|
+
return { ok: true, value: v.trim() };
|
|
2729
|
+
}
|
|
2730
|
+
function buildIntroListItem(intro, viewerLogin) {
|
|
2731
|
+
const role = introActorRole(intro, viewerLogin);
|
|
2732
|
+
if (role === "other") return null;
|
|
2733
|
+
const reveal = revealIntroContacts(intro);
|
|
2734
|
+
if (role === "target") {
|
|
2735
|
+
return {
|
|
2736
|
+
id: intro.id,
|
|
2737
|
+
role: "incoming",
|
|
2738
|
+
counterpartyLogin: intro.requesterLogin,
|
|
2739
|
+
status: intro.status,
|
|
2740
|
+
note: intro.note ?? null,
|
|
2741
|
+
contact: reveal.toTarget
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
return {
|
|
2745
|
+
id: intro.id,
|
|
2746
|
+
role: "outgoing",
|
|
2747
|
+
counterpartyLogin: intro.targetLogin,
|
|
2748
|
+
status: intro.status,
|
|
2749
|
+
note: intro.note ?? null,
|
|
2750
|
+
contact: reveal.toRequester
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
function composeIntroAcceptedEmail(args2) {
|
|
2754
|
+
const subject = `Intro connected with @${args2.counterpartyLogin} \xB7 terminalhire`;
|
|
2755
|
+
const lead = args2.recipientRole === "requester" ? `@${args2.counterpartyLogin} accepted your intro request on terminalhire.` : `You accepted @${args2.counterpartyLogin}'s intro request on terminalhire.`;
|
|
2756
|
+
const text = `${lead}
|
|
2757
|
+
|
|
2758
|
+
You can now reach them directly:
|
|
2759
|
+
@${args2.counterpartyLogin} \u2014 ${args2.counterpartyContact}
|
|
2760
|
+
|
|
2761
|
+
Take it from here.
|
|
2762
|
+
|
|
2763
|
+
\u2014 Terminalhire`;
|
|
2764
|
+
return { subject, text };
|
|
2765
|
+
}
|
|
2766
|
+
function introRetentionAction(row, now) {
|
|
2767
|
+
if (row.status === "pending") {
|
|
2768
|
+
const created = Date.parse(row.createdAt);
|
|
2769
|
+
if (Number.isFinite(created) && now - created > INTRO_PENDING_TTL_MS) return "purge";
|
|
2770
|
+
return "keep";
|
|
2771
|
+
}
|
|
2772
|
+
if (row.status === "declined") {
|
|
2773
|
+
return row.hasContact ? "scrub-declined" : "keep";
|
|
2774
|
+
}
|
|
2775
|
+
if (row.status === "accepted") {
|
|
2776
|
+
const updated = Date.parse(row.updatedAt);
|
|
2777
|
+
if (row.hasContact && Number.isFinite(updated) && now - updated > INTRO_ACCEPTED_TTL_MS) {
|
|
2778
|
+
return "expire-accepted";
|
|
2779
|
+
}
|
|
2780
|
+
return "keep";
|
|
2781
|
+
}
|
|
2782
|
+
return "keep";
|
|
2783
|
+
}
|
|
2784
|
+
var INTRO_ALLOWED_FIELDS, INTRO_ALLOWED_SET, MAX_SHORT, MAX_NOTE, INTRO_PENDING_TTL_MS, INTRO_ACCEPTED_TTL_MS;
|
|
2785
|
+
var init_intro = __esm({
|
|
2786
|
+
"../../packages/core/src/intro.ts"() {
|
|
2787
|
+
"use strict";
|
|
2788
|
+
INTRO_ALLOWED_FIELDS = [
|
|
2789
|
+
"requesterLogin",
|
|
2790
|
+
"requesterDisplayName",
|
|
2791
|
+
"requesterContact",
|
|
2792
|
+
"note",
|
|
2793
|
+
"targetLogin"
|
|
2794
|
+
];
|
|
2795
|
+
INTRO_ALLOWED_SET = new Set(INTRO_ALLOWED_FIELDS);
|
|
2796
|
+
MAX_SHORT = 200;
|
|
2797
|
+
MAX_NOTE = 500;
|
|
2798
|
+
INTRO_PENDING_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
2799
|
+
INTRO_ACCEPTED_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
2800
|
+
}
|
|
2801
|
+
});
|
|
2802
|
+
|
|
2803
|
+
// ../../packages/core/src/index.ts
|
|
2804
|
+
var src_exports = {};
|
|
2805
|
+
__export(src_exports, {
|
|
2806
|
+
ASHBY_SLUGS_BY_TIER: () => ASHBY_SLUGS_BY_TIER,
|
|
2807
|
+
DECAY_FLOOR: () => DECAY_FLOOR,
|
|
2808
|
+
DEFAULT_ASHBY_SLUGS: () => DEFAULT_ASHBY_SLUGS,
|
|
2809
|
+
DEFAULT_BOUNTY_REPOS: () => DEFAULT_BOUNTY_REPOS,
|
|
2810
|
+
DEFAULT_GREENHOUSE_SLUGS: () => DEFAULT_GREENHOUSE_SLUGS,
|
|
2811
|
+
DEFAULT_LEVER_SLUGS: () => DEFAULT_LEVER_SLUGS,
|
|
2812
|
+
DEFAULT_WORKABLE_SLUGS: () => DEFAULT_WORKABLE_SLUGS,
|
|
2813
|
+
EXAMPLE_BUYER: () => EXAMPLE_BUYER,
|
|
2814
|
+
FEEDS: () => FEEDS,
|
|
2815
|
+
GRAPH: () => GRAPH,
|
|
2816
|
+
GREENHOUSE_SLUGS_BY_TIER: () => GREENHOUSE_SLUGS_BY_TIER,
|
|
2817
|
+
IDF_BACKGROUND: () => IDF_BACKGROUND,
|
|
2818
|
+
INTRO_ACCEPTED_TTL_MS: () => INTRO_ACCEPTED_TTL_MS,
|
|
2819
|
+
INTRO_ALLOWED_FIELDS: () => INTRO_ALLOWED_FIELDS,
|
|
2820
|
+
INTRO_PENDING_TTL_MS: () => INTRO_PENDING_TTL_MS,
|
|
2821
|
+
LEVER_SLUGS_BY_TIER: () => LEVER_SLUGS_BY_TIER,
|
|
2822
|
+
SYNONYMS: () => SYNONYMS,
|
|
2823
|
+
VOCABULARY: () => VOCABULARY,
|
|
2824
|
+
VOCAB_NODES: () => VOCAB_NODES,
|
|
2825
|
+
acceptanceCountForDomains: () => acceptanceCountForDomains,
|
|
2826
|
+
aggregate: () => aggregate,
|
|
2827
|
+
aggregateBounties: () => aggregateBounties,
|
|
2828
|
+
ashby: () => ashby,
|
|
2829
|
+
authorizeIntroDecision: () => authorizeIntroDecision,
|
|
2830
|
+
authorizeIntroDeletion: () => authorizeIntroDeletion,
|
|
2831
|
+
bestAcceptanceDomain: () => bestAcceptanceDomain,
|
|
2832
|
+
buildDirectoryIndex: () => buildDirectoryIndex,
|
|
2833
|
+
buildGraph: () => buildGraph,
|
|
2834
|
+
buildIndex: () => buildIndex,
|
|
2835
|
+
buildIntroListItem: () => buildIntroListItem,
|
|
2836
|
+
buildIntroPayload: () => buildIntroPayload,
|
|
2837
|
+
buildReason: () => buildReason,
|
|
2838
|
+
composeIntroAcceptedEmail: () => composeIntroAcceptedEmail,
|
|
2839
|
+
composeIntroEmail: () => composeIntroEmail,
|
|
2840
|
+
computeAcceptanceCredential: () => computeAcceptanceCredential,
|
|
2841
|
+
computeAcceptanceCredentialPublic: () => computeAcceptanceCredentialPublic,
|
|
2842
|
+
coreTagsFromTitle: () => coreTagsFromTitle,
|
|
2843
|
+
deriveResumeTrend: () => deriveResumeTrend,
|
|
2844
|
+
expandWeighted: () => expandWeighted,
|
|
2845
|
+
extractSkillTags: () => extractSkillTags,
|
|
2846
|
+
fetchGitHubProfile: () => fetchGitHubProfile,
|
|
2847
|
+
fetchOwnedRepoTraction: () => fetchOwnedRepoTraction,
|
|
2848
|
+
fetchRepoRecency: () => fetchRepoRecency,
|
|
2849
|
+
flattenTiers: () => flattenTiers,
|
|
2850
|
+
getBuyer: () => getBuyer,
|
|
2851
|
+
githubBounties: () => githubBounties,
|
|
2852
|
+
githubToFingerprint: () => githubToFingerprint,
|
|
2853
|
+
greenhouse: () => greenhouse,
|
|
2854
|
+
himalayas: () => himalayas,
|
|
2855
|
+
hn: () => hn,
|
|
2856
|
+
introActorRole: () => introActorRole,
|
|
2857
|
+
introRateLimitCheck: () => introRateLimitCheck,
|
|
2858
|
+
introRetentionAction: () => introRetentionAction,
|
|
2859
|
+
isBounty: () => isBounty,
|
|
2860
|
+
isOverIntroLimit: () => isOverIntroLimit,
|
|
2861
|
+
lever: () => lever,
|
|
2862
|
+
loadPartnerRoles: () => loadPartnerRoles,
|
|
2863
|
+
looksLikeEngRole: () => looksLikeEngRole,
|
|
2864
|
+
match: () => match,
|
|
2865
|
+
normalize: () => normalize,
|
|
2866
|
+
opire: () => opire,
|
|
2867
|
+
passesMaturityGate: () => passesMaturityGate,
|
|
2868
|
+
personCardToJob: () => personCardToJob,
|
|
2869
|
+
projectCardToJob: () => projectCardToJob,
|
|
2870
|
+
rejectExtraIntroFields: () => rejectExtraIntroFields,
|
|
2871
|
+
revealIntroContacts: () => revealIntroContacts,
|
|
2872
|
+
tokenize: () => tokenize,
|
|
2873
|
+
validateGraph: () => validateGraph,
|
|
2874
|
+
validateIntroPayload: () => validateIntroPayload,
|
|
2875
|
+
validateTargetContact: () => validateTargetContact,
|
|
2876
|
+
workable: () => workable,
|
|
2877
|
+
wwr: () => wwr
|
|
2878
|
+
});
|
|
2879
|
+
var init_src = __esm({
|
|
2880
|
+
"../../packages/core/src/index.ts"() {
|
|
2881
|
+
"use strict";
|
|
2882
|
+
init_types();
|
|
2883
|
+
init_vocabulary();
|
|
2884
|
+
init_matcher();
|
|
2885
|
+
init_feeds();
|
|
2886
|
+
init_indexer();
|
|
2887
|
+
init_partners();
|
|
2888
|
+
init_github();
|
|
2889
|
+
init_intro();
|
|
2890
|
+
}
|
|
2891
|
+
});
|
|
2892
|
+
|
|
2893
|
+
// src/profile.ts
|
|
2894
|
+
var profile_exports = {};
|
|
2895
|
+
__export(profile_exports, {
|
|
2896
|
+
accumulateGitHubTags: () => accumulateGitHubTags,
|
|
2897
|
+
accumulateSession: () => accumulateSession,
|
|
2898
|
+
accumulateTags: () => accumulateTags,
|
|
2899
|
+
addSavedJob: () => addSavedJob,
|
|
2900
|
+
deleteProfile: () => deleteProfile,
|
|
2901
|
+
listSavedJobs: () => listSavedJobs,
|
|
2902
|
+
profileToFingerprint: () => profileToFingerprint,
|
|
2903
|
+
readProfile: () => readProfile,
|
|
2904
|
+
removeSavedJob: () => removeSavedJob,
|
|
2905
|
+
writeProfile: () => writeProfile
|
|
2906
|
+
});
|
|
2907
|
+
import {
|
|
2908
|
+
createCipheriv,
|
|
2909
|
+
createDecipheriv,
|
|
2910
|
+
randomBytes
|
|
2911
|
+
} from "crypto";
|
|
2912
|
+
import {
|
|
2913
|
+
readFileSync as readFileSync2,
|
|
2914
|
+
writeFileSync,
|
|
2915
|
+
mkdirSync,
|
|
2916
|
+
existsSync
|
|
2917
|
+
} from "fs";
|
|
2918
|
+
import { join as join2 } from "path";
|
|
2919
|
+
import { homedir } from "os";
|
|
2920
|
+
async function loadKey() {
|
|
2921
|
+
try {
|
|
2922
|
+
const kt = await import("keytar");
|
|
2923
|
+
const stored = await kt.getPassword("terminalhire", "profile-key");
|
|
2924
|
+
if (stored) {
|
|
2925
|
+
return Buffer.from(stored, "hex");
|
|
2926
|
+
}
|
|
2927
|
+
const key2 = randomBytes(KEY_BYTES);
|
|
2928
|
+
await kt.setPassword("terminalhire", "profile-key", key2.toString("hex"));
|
|
2929
|
+
return key2;
|
|
2930
|
+
} catch {
|
|
2931
|
+
}
|
|
2932
|
+
mkdirSync(TERMINALHIRE_DIR, { recursive: true });
|
|
2933
|
+
if (existsSync(KEY_FILE)) {
|
|
2934
|
+
return Buffer.from(readFileSync2(KEY_FILE, "utf8").trim(), "hex");
|
|
2935
|
+
}
|
|
2936
|
+
const key = randomBytes(KEY_BYTES);
|
|
2937
|
+
writeFileSync(KEY_FILE, key.toString("hex"), { mode: 384, encoding: "utf8" });
|
|
2938
|
+
return key;
|
|
2939
|
+
}
|
|
2940
|
+
function encrypt(plaintext, key) {
|
|
2941
|
+
const iv = randomBytes(IV_BYTES);
|
|
2942
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
2943
|
+
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
2944
|
+
const tag = cipher.getAuthTag();
|
|
2945
|
+
return {
|
|
2946
|
+
iv: iv.toString("hex"),
|
|
2947
|
+
tag: tag.toString("hex"),
|
|
2948
|
+
ciphertext: ct.toString("hex")
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
function decrypt(blob, key) {
|
|
2952
|
+
const decipher = createDecipheriv(
|
|
2953
|
+
ALGO,
|
|
2954
|
+
key,
|
|
2955
|
+
Buffer.from(blob.iv, "hex")
|
|
2956
|
+
);
|
|
2957
|
+
decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
|
|
2958
|
+
const plain = Buffer.concat([
|
|
2959
|
+
decipher.update(Buffer.from(blob.ciphertext, "hex")),
|
|
2960
|
+
decipher.final()
|
|
2961
|
+
]);
|
|
2962
|
+
return plain.toString("utf8");
|
|
2963
|
+
}
|
|
2964
|
+
function blankProfile() {
|
|
2965
|
+
return {
|
|
2966
|
+
version: 3,
|
|
2967
|
+
skillTags: [],
|
|
2968
|
+
tagWeights: {},
|
|
2969
|
+
hasEmployerSessions: false,
|
|
2970
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
function recencyDecay(lastSeen) {
|
|
2974
|
+
const ageMs = Date.now() - new Date(lastSeen).getTime();
|
|
2975
|
+
return Math.pow(0.5, ageMs / DECAY_HALF_LIFE_MS);
|
|
2976
|
+
}
|
|
2977
|
+
function tagScore(w) {
|
|
2978
|
+
return w.count * recencyDecay(w.lastSeen);
|
|
2979
|
+
}
|
|
2980
|
+
function deriveSkillTags(tagWeights) {
|
|
2981
|
+
return Object.entries(tagWeights).filter(([, w]) => w.count >= 1).sort(([, a], [, b]) => tagScore(b) - tagScore(a)).map(([tag]) => tag);
|
|
2982
|
+
}
|
|
2983
|
+
function migrateTagWeights(profile) {
|
|
2984
|
+
if (!profile.tagWeights) {
|
|
2985
|
+
profile.tagWeights = {};
|
|
2986
|
+
}
|
|
2987
|
+
const seed = profile.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2988
|
+
for (const tag of profile.skillTags) {
|
|
2989
|
+
if (!profile.tagWeights[tag]) {
|
|
2990
|
+
profile.tagWeights[tag] = { count: 1, firstSeen: seed, lastSeen: seed, sessions: 1 };
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
async function readProfile() {
|
|
2995
|
+
if (!existsSync(PROFILE_FILE)) return blankProfile();
|
|
2996
|
+
try {
|
|
2997
|
+
const key = await loadKey();
|
|
2998
|
+
const raw = readFileSync2(PROFILE_FILE, "utf8");
|
|
2999
|
+
const blob = JSON.parse(raw);
|
|
3000
|
+
const plaintext = decrypt(blob, key);
|
|
3001
|
+
const parsed = JSON.parse(plaintext);
|
|
3002
|
+
migrateTagWeights(parsed);
|
|
3003
|
+
return parsed;
|
|
3004
|
+
} catch {
|
|
3005
|
+
return blankProfile();
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
async function writeProfile(profile) {
|
|
3009
|
+
mkdirSync(TERMINALHIRE_DIR, { recursive: true });
|
|
3010
|
+
const key = await loadKey();
|
|
3011
|
+
profile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3012
|
+
profile.skillTags = deriveSkillTags(profile.tagWeights);
|
|
3013
|
+
const blob = encrypt(JSON.stringify(profile), key);
|
|
3014
|
+
writeFileSync(PROFILE_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
|
|
3015
|
+
}
|
|
3016
|
+
function accumulateSession(profile, tags, isEmployerContext, inferredSeniority, seniorityIsAuthoritative = false) {
|
|
3017
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3018
|
+
let filtered = normalize(tags);
|
|
3019
|
+
if (isEmployerContext) {
|
|
3020
|
+
filtered = filtered.filter((t) => LANGUAGE_TAGS.has(t));
|
|
3021
|
+
profile.hasEmployerSessions = true;
|
|
3022
|
+
}
|
|
3023
|
+
for (const tag of filtered) {
|
|
3024
|
+
const existing = profile.tagWeights[tag];
|
|
3025
|
+
if (existing) {
|
|
3026
|
+
existing.count += 1;
|
|
3027
|
+
existing.sessions += 1;
|
|
3028
|
+
existing.lastSeen = now;
|
|
3029
|
+
} else {
|
|
3030
|
+
profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
if (inferredSeniority && !isEmployerContext) {
|
|
3034
|
+
if (seniorityIsAuthoritative || !profile.github) {
|
|
3035
|
+
profile.seniority = inferredSeniority;
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
async function accumulateTags(rawTokens, isEmployerContext, inferredSeniority) {
|
|
3040
|
+
const profile = await readProfile();
|
|
3041
|
+
accumulateSession(profile, rawTokens, isEmployerContext, inferredSeniority);
|
|
3042
|
+
await writeProfile(profile);
|
|
3043
|
+
}
|
|
3044
|
+
function accumulateGitHubTags(profile, tags, inferredSeniority) {
|
|
3045
|
+
accumulateSession(
|
|
3046
|
+
profile,
|
|
3047
|
+
tags,
|
|
3048
|
+
/* isEmployerContext */
|
|
3049
|
+
false,
|
|
3050
|
+
inferredSeniority,
|
|
3051
|
+
true
|
|
3052
|
+
);
|
|
3053
|
+
}
|
|
3054
|
+
async function listSavedJobs() {
|
|
3055
|
+
const profile = await readProfile();
|
|
3056
|
+
return profile.savedJobs ?? [];
|
|
3057
|
+
}
|
|
3058
|
+
async function addSavedJob(job) {
|
|
3059
|
+
const profile = await readProfile();
|
|
3060
|
+
const existing = profile.savedJobs ?? [];
|
|
3061
|
+
const filtered = existing.filter((j) => j.id !== job.id);
|
|
3062
|
+
profile.savedJobs = [...filtered, { ...job, savedAt: (/* @__PURE__ */ new Date()).toISOString() }];
|
|
3063
|
+
await writeProfile(profile);
|
|
3064
|
+
}
|
|
3065
|
+
async function removeSavedJob(id) {
|
|
3066
|
+
const profile = await readProfile();
|
|
3067
|
+
const existing = profile.savedJobs ?? [];
|
|
3068
|
+
const filtered = existing.filter((j) => j.id !== id);
|
|
3069
|
+
if (filtered.length === existing.length) return false;
|
|
3070
|
+
profile.savedJobs = filtered;
|
|
3071
|
+
await writeProfile(profile);
|
|
3072
|
+
return true;
|
|
3073
|
+
}
|
|
3074
|
+
async function deleteProfile() {
|
|
3075
|
+
const { rmSync } = await import("fs");
|
|
3076
|
+
try {
|
|
3077
|
+
rmSync(PROFILE_FILE);
|
|
3078
|
+
} catch {
|
|
3079
|
+
}
|
|
3080
|
+
try {
|
|
3081
|
+
rmSync(KEY_FILE);
|
|
3082
|
+
} catch {
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
function profileToFingerprint(profile) {
|
|
3086
|
+
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);
|
|
3087
|
+
const skillTags = rankedTags.length > 0 ? rankedTags : profile.skillTags;
|
|
3088
|
+
return {
|
|
3089
|
+
skillTags,
|
|
3090
|
+
seniorityBand: profile.seniority,
|
|
3091
|
+
prefs: {
|
|
3092
|
+
roleTypes: profile.roleTypes,
|
|
3093
|
+
remoteOnly: profile.remoteOnly,
|
|
3094
|
+
compFloorUsd: profile.compFloorUsd
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
3097
|
+
}
|
|
3098
|
+
var TERMINALHIRE_DIR, PROFILE_FILE, KEY_FILE, ALGO, KEY_BYTES, IV_BYTES, DECAY_HALF_LIFE_MS, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
|
|
3099
|
+
var init_profile = __esm({
|
|
3100
|
+
"src/profile.ts"() {
|
|
3101
|
+
"use strict";
|
|
3102
|
+
init_src();
|
|
3103
|
+
TERMINALHIRE_DIR = join2(homedir(), ".terminalhire");
|
|
3104
|
+
PROFILE_FILE = join2(TERMINALHIRE_DIR, "profile.enc");
|
|
3105
|
+
KEY_FILE = join2(TERMINALHIRE_DIR, "key");
|
|
3106
|
+
ALGO = "aes-256-gcm";
|
|
3107
|
+
KEY_BYTES = 32;
|
|
3108
|
+
IV_BYTES = 12;
|
|
3109
|
+
DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
3110
|
+
LANGUAGE_TAGS = /* @__PURE__ */ new Set([
|
|
3111
|
+
"typescript",
|
|
3112
|
+
"javascript",
|
|
3113
|
+
"python",
|
|
3114
|
+
"go",
|
|
3115
|
+
"rust",
|
|
3116
|
+
"java",
|
|
3117
|
+
"ruby",
|
|
3118
|
+
"elixir",
|
|
3119
|
+
"scala",
|
|
3120
|
+
"kotlin",
|
|
3121
|
+
"swift",
|
|
3122
|
+
"cpp",
|
|
3123
|
+
"csharp",
|
|
3124
|
+
"php",
|
|
3125
|
+
"haskell",
|
|
3126
|
+
"clojure",
|
|
3127
|
+
"r"
|
|
3128
|
+
]);
|
|
3129
|
+
MIN_FINGERPRINT_SCORE = 0.05;
|
|
3130
|
+
}
|
|
3131
|
+
});
|
|
3132
|
+
|
|
3133
|
+
// bin/jpi-devs.js
|
|
3134
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
3135
|
+
import { join as join3 } from "path";
|
|
3136
|
+
import { homedir as homedir2 } from "os";
|
|
3137
|
+
import { createInterface } from "readline";
|
|
3138
|
+
var TERMINALHIRE_DIR2 = join3(homedir2(), ".terminalhire");
|
|
3139
|
+
var DIRECTORY_CACHE_FILE = join3(TERMINALHIRE_DIR2, "directory-cache.json");
|
|
3140
|
+
var PROJECT_FILE = join3(TERMINALHIRE_DIR2, "project.json");
|
|
3141
|
+
var INDEX_TTL_MS = 15 * 60 * 1e3;
|
|
3142
|
+
var API_URL = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
|
|
3143
|
+
var DEFAULT_LIMIT = 10;
|
|
3144
|
+
var args = process.argv.slice(2);
|
|
3145
|
+
var limitArg = args.indexOf("--limit");
|
|
3146
|
+
var LIMIT = limitArg !== -1 ? parseInt(args[limitArg + 1] ?? "10", 10) : DEFAULT_LIMIT;
|
|
3147
|
+
var SHOW_ALL = args.includes("--all");
|
|
3148
|
+
var AS_PROJECT = args.includes("--as-project");
|
|
3149
|
+
function readDirectoryCache() {
|
|
3150
|
+
try {
|
|
3151
|
+
const entry = JSON.parse(readFileSync3(DIRECTORY_CACHE_FILE, "utf8"));
|
|
3152
|
+
if (typeof entry.ts === "number" && Number.isFinite(entry.ts) && Date.now() - entry.ts < INDEX_TTL_MS) {
|
|
3153
|
+
return { index: entry.index, ts: entry.ts };
|
|
3154
|
+
}
|
|
3155
|
+
return null;
|
|
3156
|
+
} catch {
|
|
3157
|
+
return null;
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
function writeDirectoryCache(index) {
|
|
3161
|
+
mkdirSync2(TERMINALHIRE_DIR2, { recursive: true });
|
|
3162
|
+
writeFileSync2(DIRECTORY_CACHE_FILE, JSON.stringify({ ts: Date.now(), index }), "utf8");
|
|
3163
|
+
}
|
|
3164
|
+
function readProject() {
|
|
3165
|
+
try {
|
|
3166
|
+
return JSON.parse(readFileSync3(PROJECT_FILE, "utf8"));
|
|
3167
|
+
} catch {
|
|
3168
|
+
return null;
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
function relativeTime(ts) {
|
|
3172
|
+
const secs = Math.max(0, Math.round((Date.now() - ts) / 1e3));
|
|
3173
|
+
if (secs < 60) return `${secs}s ago`;
|
|
3174
|
+
const mins = Math.round(secs / 60);
|
|
3175
|
+
return mins < 60 ? `${mins}m ago` : `${Math.round(mins / 60)}h ago`;
|
|
3176
|
+
}
|
|
3177
|
+
async function fetchDirectory() {
|
|
3178
|
+
const cached = readDirectoryCache();
|
|
3179
|
+
if (cached) {
|
|
3180
|
+
console.log(`\u2713 Using cached directory (updated ${relativeTime(cached.ts)})`);
|
|
3181
|
+
return cached.index;
|
|
3182
|
+
}
|
|
3183
|
+
console.log(`\u21BB Refreshing builder directory from ${API_URL}/api/directory...`);
|
|
3184
|
+
const res = await fetch(`${API_URL}/api/directory`, { signal: AbortSignal.timeout(1e4) });
|
|
3185
|
+
if (!res.ok) throw new Error(`/api/directory returned ${res.status}`);
|
|
3186
|
+
const index = await res.json();
|
|
3187
|
+
writeDirectoryCache(index);
|
|
3188
|
+
return index;
|
|
3189
|
+
}
|
|
3190
|
+
function reportMatched(results, fetchImpl = fetch) {
|
|
3191
|
+
try {
|
|
3192
|
+
const logins = [
|
|
3193
|
+
...new Set(
|
|
3194
|
+
results.map((r) => r?.job?.company).filter((login) => typeof login === "string" && login.length > 0)
|
|
3195
|
+
)
|
|
3196
|
+
];
|
|
3197
|
+
if (logins.length === 0) return;
|
|
3198
|
+
Promise.resolve(
|
|
3199
|
+
fetchImpl(`${API_URL}/api/directory/matched`, {
|
|
3200
|
+
method: "POST",
|
|
3201
|
+
headers: { "Content-Type": "application/json" },
|
|
3202
|
+
body: JSON.stringify({ matched: logins }),
|
|
3203
|
+
signal: AbortSignal.timeout(3e3)
|
|
3204
|
+
})
|
|
3205
|
+
).catch(() => {
|
|
3206
|
+
});
|
|
3207
|
+
} catch {
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
function prompt(question) {
|
|
3211
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3212
|
+
return new Promise((resolve) => {
|
|
3213
|
+
rl.question(question, (answer) => {
|
|
3214
|
+
rl.close();
|
|
3215
|
+
resolve(answer.trim().toLowerCase());
|
|
3216
|
+
});
|
|
3217
|
+
});
|
|
3218
|
+
}
|
|
3219
|
+
function credentialUrl(job) {
|
|
3220
|
+
const u = job.url ?? "";
|
|
3221
|
+
return u.startsWith("http") ? u : `${API_URL}${u}`;
|
|
3222
|
+
}
|
|
3223
|
+
function linkTitle(title, url) {
|
|
3224
|
+
const isTTY = process.stdout.isTTY;
|
|
3225
|
+
const noColor = process.env["NO_COLOR"] !== void 0;
|
|
3226
|
+
if (isTTY && !noColor && url) return `\x1B]8;;${url}\x1B\\${title}\x1B]8;;\x1B\\`;
|
|
3227
|
+
return url ? `${title} (${url})` : title;
|
|
3228
|
+
}
|
|
3229
|
+
function printCard(i, result) {
|
|
3230
|
+
const { job, score, matchedTags, reason } = result;
|
|
3231
|
+
const url = credentialUrl(job);
|
|
3232
|
+
const kind = job.source === "project" ? "project" : "developer";
|
|
3233
|
+
const byline = job.source === "project" ? ` \xB7 by @${job.company}` : "";
|
|
3234
|
+
const scoreStr = score > 0 ? ` \xB7 match ${Math.round(score * 100)}%` : "";
|
|
3235
|
+
console.log(`
|
|
3236
|
+
${i + 1}. ${linkTitle(job.title, url)} \u2014 ${kind}${byline}${scoreStr}`);
|
|
3237
|
+
console.log(` id: ${job.id}`);
|
|
3238
|
+
if (reason) console.log(` ${reason}`);
|
|
3239
|
+
if (matchedTags && matchedTags.length) console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
|
|
3240
|
+
console.log(` Profile: ${url}`);
|
|
3241
|
+
}
|
|
3242
|
+
async function run() {
|
|
3243
|
+
try {
|
|
3244
|
+
const { match: match2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
3245
|
+
let fp;
|
|
3246
|
+
if (AS_PROJECT) {
|
|
3247
|
+
const project = readProject();
|
|
3248
|
+
if (!project) {
|
|
3249
|
+
console.log("\u2726 terminalhire devs --as-project: no project declared yet.");
|
|
3250
|
+
console.log(' Run `terminalhire project "<title>: <skills it needs>"` first, then come back.');
|
|
3251
|
+
return;
|
|
3252
|
+
}
|
|
3253
|
+
if (!project.skillTags || project.skillTags.length === 0) {
|
|
3254
|
+
console.log("\u2726 Your declared project has no recognized skills yet.");
|
|
3255
|
+
console.log(" Re-run `terminalhire project` with known stack names (react, go, postgres, ...).");
|
|
3256
|
+
return;
|
|
3257
|
+
}
|
|
3258
|
+
fp = { skillTags: project.skillTags, prefs: project.prefs ?? {} };
|
|
3259
|
+
console.log(`Ranking builders for your project: ${project.title}`);
|
|
3260
|
+
} else {
|
|
3261
|
+
const { readProfile: readProfile2, profileToFingerprint: profileToFingerprint2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
3262
|
+
const profile = await readProfile2();
|
|
3263
|
+
if (profile.skillTags.length === 0) {
|
|
3264
|
+
console.log("\u2726 terminalhire devs: no skill tags in your local profile yet.");
|
|
3265
|
+
console.log(" Run `terminalhire init` first to set up your profile, then come back.");
|
|
3266
|
+
console.log(" (Tags also accumulate as you work in Claude Code sessions.)");
|
|
3267
|
+
return;
|
|
3268
|
+
}
|
|
3269
|
+
fp = profileToFingerprint2(profile);
|
|
3270
|
+
}
|
|
3271
|
+
const index = await fetchDirectory();
|
|
3272
|
+
const cards = index.cards ?? [];
|
|
3273
|
+
if (cards.length === 0) {
|
|
3274
|
+
console.log("\nNo builders or projects published yet. Check back soon \u2014 the directory fills as devs publish.");
|
|
3275
|
+
return;
|
|
3276
|
+
}
|
|
3277
|
+
const results = match2(fp, cards, SHOW_ALL ? cards.length : LIMIT);
|
|
3278
|
+
if (results.length === 0) {
|
|
3279
|
+
console.log(`No matching builders or projects for your current ${AS_PROJECT ? "project" : "profile"}.`);
|
|
3280
|
+
console.log(" Your tags: " + fp.skillTags.join(", "));
|
|
3281
|
+
return;
|
|
3282
|
+
}
|
|
3283
|
+
reportMatched(results);
|
|
3284
|
+
console.log(
|
|
3285
|
+
`
|
|
3286
|
+
\u2726 ${results.length} match${results.length === 1 ? "" : "es"} in the builder directory (local match \u2014 no data sent)
|
|
3287
|
+
`
|
|
3288
|
+
);
|
|
3289
|
+
for (let i = 0; i < results.length; i++) {
|
|
3290
|
+
printCard(i, results[i]);
|
|
3291
|
+
}
|
|
3292
|
+
if (!process.stdin.isTTY) return;
|
|
3293
|
+
console.log("\n" + "\u2500".repeat(70));
|
|
3294
|
+
const pick = await prompt(`
|
|
3295
|
+
Enter a number to open a profile, or press Enter to exit: `);
|
|
3296
|
+
const idx = parseInt(pick, 10) - 1;
|
|
3297
|
+
if (Number.isNaN(idx) || idx < 0 || idx >= results.length) return;
|
|
3298
|
+
console.log(`
|
|
3299
|
+
Open this public credential (no data shared):
|
|
3300
|
+
${credentialUrl(results[idx].job)}`);
|
|
3301
|
+
} catch (err) {
|
|
3302
|
+
console.error("terminalhire devs error:", err.message ?? err);
|
|
3303
|
+
process.exit(1);
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
export {
|
|
3307
|
+
reportMatched,
|
|
3308
|
+
run
|
|
3309
|
+
};
|