terminalhire 0.18.2 → 0.19.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.
@@ -1,13 +1,954 @@
1
1
  #!/usr/bin/env node
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
9
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
10
+ }) : x)(function(x) {
11
+ if (typeof require !== "undefined") return require.apply(this, arguments);
12
+ throw Error('Dynamic require of "' + x + '" is not supported');
13
+ });
4
14
  var __esm = (fn, res) => function __init() {
5
15
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
16
  };
17
+ var __commonJS = (cb, mod) => function __require2() {
18
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
19
+ };
7
20
  var __export = (target, all) => {
8
21
  for (var name in all)
9
22
  __defProp(target, name, { get: all[name], enumerable: true });
10
23
  };
24
+ var __copyProps = (to, from, except, desc) => {
25
+ if (from && typeof from === "object" || typeof from === "function") {
26
+ for (let key of __getOwnPropNames(from))
27
+ if (!__hasOwnProp.call(to, key) && key !== except)
28
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
29
+ }
30
+ return to;
31
+ };
32
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
33
+ // If the importer is in node compatibility mode or this is not an ESM
34
+ // file that has been converted to a CommonJS file using a Babel-
35
+ // compatible transform (i.e. "__esModule" has not been set), then set
36
+ // "default" to the CommonJS "module.exports" for node compatibility.
37
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
38
+ mod
39
+ ));
40
+
41
+ // ../../packages/core/src/types.ts
42
+ var init_types = __esm({
43
+ "../../packages/core/src/types.ts"() {
44
+ "use strict";
45
+ }
46
+ });
47
+
48
+ // ../../packages/core/src/vocab/graph.data.ts
49
+ var VOCAB_NODES;
50
+ var init_graph_data = __esm({
51
+ "../../packages/core/src/vocab/graph.data.ts"() {
52
+ "use strict";
53
+ VOCAB_NODES = [
54
+ // ── Languages ─────────────────────────────────────────────────────────────
55
+ { id: "javascript", synonyms: ["js"], related: [{ to: "typescript", w: 0.6 }] },
56
+ { id: "typescript", parents: ["javascript"], synonyms: ["ts"] },
57
+ { id: "python", synonyms: ["py"] },
58
+ { id: "go", synonyms: ["golang"] },
59
+ { id: "rust" },
60
+ { id: "java", related: [{ to: "kotlin", w: 0.45 }, { to: "scala", w: 0.4 }] },
61
+ { id: "ruby" },
62
+ { id: "elixir" },
63
+ { id: "scala", related: [{ to: "java", w: 0.4 }] },
64
+ { id: "kotlin", related: [{ to: "java", w: 0.45 }] },
65
+ { id: "swift" },
66
+ { id: "cpp", synonyms: ["c++"] },
67
+ { id: "csharp", synonyms: ["c#"] },
68
+ { id: "php" },
69
+ { id: "haskell" },
70
+ { id: "clojure" },
71
+ { id: "r" },
72
+ { id: "dart" },
73
+ // ── Frontend ──────────────────────────────────────────────────────────────
74
+ {
75
+ id: "react",
76
+ parents: ["javascript"],
77
+ synonyms: ["reactjs"],
78
+ 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 }]
79
+ },
80
+ { id: "nextjs", parents: ["react"], synonyms: ["next", "next.js"], related: [{ to: "remix", w: 0.5 }] },
81
+ { id: "vue", parents: ["javascript"], synonyms: ["vue.js"], related: [{ to: "nuxt", w: 0.6 }] },
82
+ { id: "nuxt", parents: ["vue"], synonyms: ["nuxt.js"] },
83
+ { id: "svelte", parents: ["javascript"], related: [{ to: "sveltekit", w: 0.65 }] },
84
+ { id: "sveltekit", parents: ["svelte"] },
85
+ { id: "angular", parents: ["typescript"], synonyms: ["angular.js", "angularjs"] },
86
+ { id: "solidjs", parents: ["javascript"] },
87
+ { id: "remix", parents: ["react"], synonyms: ["remix.run"] },
88
+ { id: "astro", parents: ["javascript"], related: [{ to: "nextjs", w: 0.4 }] },
89
+ { id: "qwik", parents: ["javascript"] },
90
+ { id: "tailwind", parents: ["css"], synonyms: ["tailwindcss", "tw"] },
91
+ { id: "css" },
92
+ { id: "html" },
93
+ { id: "redux", parents: ["react"] },
94
+ { id: "vite", parents: ["frontend"] },
95
+ { id: "webpack", parents: ["frontend"] },
96
+ { id: "storybook", parents: ["frontend"] },
97
+ // ── Backend frameworks / runtimes ───────────────────────────────────────────
98
+ {
99
+ id: "nodejs",
100
+ parents: ["javascript"],
101
+ synonyms: ["node", "node.js"],
102
+ related: [{ to: "express", w: 0.5 }, { to: "fastify", w: 0.45 }, { to: "nestjs", w: 0.45 }]
103
+ },
104
+ { id: "express", parents: ["nodejs"], synonyms: ["express.js", "expressjs"], related: [{ to: "fastify", w: 0.5 }] },
105
+ { id: "fastify", parents: ["nodejs"] },
106
+ { id: "nestjs", parents: ["nodejs"], synonyms: ["nest", "nest.js"] },
107
+ { id: "hono", parents: ["nodejs"] },
108
+ { id: "deno", parents: ["javascript"], related: [{ to: "nodejs", w: 0.5 }, { to: "bun", w: 0.5 }] },
109
+ { id: "bun", parents: ["javascript"], related: [{ to: "nodejs", w: 0.5 }] },
110
+ { id: "django", parents: ["python"], related: [{ to: "flask", w: 0.5 }, { to: "fastapi", w: 0.45 }] },
111
+ { id: "fastapi", parents: ["python"], related: [{ to: "flask", w: 0.55 }, { to: "django", w: 0.45 }] },
112
+ { id: "flask", parents: ["python"] },
113
+ { id: "rails", parents: ["ruby"], synonyms: ["ruby-on-rails", "ror"] },
114
+ { id: "spring", parents: ["java"], synonyms: ["spring-boot", "springboot"] },
115
+ { id: "actix", parents: ["rust"] },
116
+ { id: "gin", parents: ["go"] },
117
+ { id: "phoenix", parents: ["elixir"] },
118
+ { id: "laravel", parents: ["php"] },
119
+ { id: "dotnet", parents: ["csharp"], synonyms: [".net", "asp.net", "dotnet-core"] },
120
+ // ── Infrastructure & DevOps ─────────────────────────────────────────────────
121
+ { 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 }] },
122
+ { id: "docker", parents: ["devops"], related: [{ to: "kubernetes", w: 0.5 }] },
123
+ { id: "terraform", synonyms: ["tf"], related: [{ to: "pulumi", w: 0.55 }, { to: "ansible", w: 0.4 }, { to: "aws", w: 0.4 }] },
124
+ { id: "pulumi", related: [{ to: "terraform", w: 0.55 }] },
125
+ { id: "ansible" },
126
+ { id: "aws", synonyms: ["amazon-web-services"], related: [{ to: "gcp", w: 0.4 }, { to: "azure", w: 0.4 }] },
127
+ { id: "gcp", synonyms: ["google-cloud", "google-cloud-platform"], related: [{ to: "aws", w: 0.4 }, { to: "azure", w: 0.4 }] },
128
+ { id: "azure", synonyms: ["microsoft-azure"], related: [{ to: "aws", w: 0.4 }] },
129
+ { id: "ci-cd", synonyms: ["cicd", "jenkins", "circleci", "circle-ci", "travis"], related: [{ to: "github-actions", w: 0.6 }, { to: "gitlab-ci", w: 0.6 }] },
130
+ { id: "github-actions", parents: ["ci-cd"], synonyms: ["github-action"] },
131
+ { id: "gitlab-ci", parents: ["ci-cd"], synonyms: ["gitlab"] },
132
+ { id: "linux" },
133
+ { id: "nginx" },
134
+ { id: "prometheus", parents: ["observability"], related: [{ to: "grafana", w: 0.6 }] },
135
+ { id: "grafana", parents: ["observability"] },
136
+ { id: "datadog", parents: ["observability"] },
137
+ { id: "opentelemetry", parents: ["observability"], synonyms: ["otel"] },
138
+ { id: "vercel", related: [{ to: "netlify", w: 0.5 }, { to: "nextjs", w: 0.4 }] },
139
+ { id: "netlify" },
140
+ { id: "fly", synonyms: ["fly.io"], related: [{ to: "railway", w: 0.5 }, { to: "render", w: 0.5 }] },
141
+ { id: "railway", related: [{ to: "render", w: 0.5 }] },
142
+ { id: "render" },
143
+ { id: "cloudflare", synonyms: ["cloudflare-workers"] },
144
+ { id: "helm", parents: ["kubernetes"] },
145
+ { id: "argocd", parents: ["kubernetes"] },
146
+ { id: "serverless", parents: ["devops"] },
147
+ // ── Databases & storage ─────────────────────────────────────────────────────
148
+ { id: "postgresql", synonyms: ["postgres", "pg"], related: [{ to: "mysql", w: 0.45 }, { to: "sqlite", w: 0.4 }] },
149
+ { id: "mysql", related: [{ to: "postgresql", w: 0.45 }] },
150
+ { id: "sqlite" },
151
+ { id: "mongodb", synonyms: ["mongo"] },
152
+ { id: "redis", related: [{ to: "caching", w: 0.5 }] },
153
+ { id: "elasticsearch", synonyms: ["elastic"], related: [{ to: "search", w: 0.55 }] },
154
+ { id: "kafka", synonyms: ["apache-kafka"], related: [{ to: "rabbitmq", w: 0.5 }, { to: "message-queue", w: 0.55 }] },
155
+ { id: "rabbitmq", related: [{ to: "message-queue", w: 0.55 }] },
156
+ { id: "cassandra" },
157
+ { id: "dynamodb", parents: ["aws"] },
158
+ { id: "snowflake", parents: ["data-engineering"], related: [{ to: "clickhouse", w: 0.4 }] },
159
+ { id: "clickhouse", parents: ["data-engineering"], related: [{ to: "duckdb", w: 0.35 }] },
160
+ { id: "duckdb", parents: ["data-engineering"] },
161
+ { id: "supabase", related: [{ to: "postgresql", w: 0.5 }, { to: "neon", w: 0.4 }] },
162
+ { id: "planetscale", related: [{ to: "mysql", w: 0.5 }] },
163
+ { id: "neon", related: [{ to: "postgresql", w: 0.5 }] },
164
+ { id: "turso", related: [{ to: "sqlite", w: 0.5 }] },
165
+ { id: "cockroachdb", related: [{ to: "postgresql", w: 0.45 }] },
166
+ { id: "prisma", parents: ["backend"], synonyms: ["@prisma/client"], related: [{ to: "drizzle", w: 0.5 }, { to: "typeorm", w: 0.45 }, { to: "sequelize", w: 0.4 }] },
167
+ { id: "drizzle", synonyms: ["drizzle-orm"], related: [{ to: "prisma", w: 0.5 }] },
168
+ { id: "sequelize", related: [{ to: "typeorm", w: 0.4 }] },
169
+ { id: "typeorm", related: [{ to: "prisma", w: 0.45 }] },
170
+ { id: "sqlalchemy", parents: ["python"] },
171
+ // ── Data engineering & ML ───────────────────────────────────────────────────
172
+ { id: "data-engineering", synonyms: ["data-eng"], related: [{ to: "spark", w: 0.5 }, { to: "airflow", w: 0.5 }, { to: "dbt", w: 0.45 }] },
173
+ { id: "spark", parents: ["data-engineering"], synonyms: ["apache-spark"] },
174
+ { id: "airflow", parents: ["data-engineering"], synonyms: ["apache-airflow"] },
175
+ { id: "dbt", parents: ["data-engineering"] },
176
+ { 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 }] },
177
+ { 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 }] },
178
+ { id: "pytorch", parents: ["ml"], synonyms: ["torch"], related: [{ to: "tensorflow", w: 0.5 }] },
179
+ { id: "tensorflow", parents: ["ml"], synonyms: ["keras", "tf-keras"] },
180
+ { id: "pandas", parents: ["python"], related: [{ to: "numpy", w: 0.6 }, { to: "data-engineering", w: 0.45 }, { to: "spark", w: 0.4 }] },
181
+ { id: "numpy", parents: ["python"] },
182
+ { id: "scikit-learn", parents: ["ml"], synonyms: ["sklearn"] },
183
+ { id: "jupyter", parents: ["python"] },
184
+ { id: "langchain", parents: ["llm"], synonyms: ["llamaindex"] },
185
+ { id: "huggingface", parents: ["ml"], synonyms: ["hugging-face"] },
186
+ { id: "openai", parents: ["llm"] },
187
+ { id: "anthropic", parents: ["llm"], synonyms: ["claude"] },
188
+ { id: "rag", parents: ["llm"], synonyms: ["retrieval-augmented-generation"] },
189
+ { id: "mlops", parents: ["ml"], related: [{ to: "devops", w: 0.4 }] },
190
+ { id: "agents", parents: ["llm"], synonyms: ["agentic", "ai-agents", "multi-agent"], related: [{ to: "rag", w: 0.4 }] },
191
+ { id: "mcp", parents: ["agents"], synonyms: ["model-context-protocol"], related: [{ to: "llm", w: 0.45 }] },
192
+ { id: "inference", parents: ["ml"], synonyms: ["model-inference", "llm-inference", "model-serving"], related: [{ to: "mlops", w: 0.5 }, { to: "llm", w: 0.4 }] },
193
+ { id: "embeddings", parents: ["ml"], synonyms: ["embedding", "vector-embeddings"], related: [{ to: "rag", w: 0.55 }, { to: "llm", w: 0.45 }] },
194
+ { id: "prompt-engineering", parents: ["llm"], synonyms: ["prompting", "prompt"] },
195
+ { id: "fine-tuning", parents: ["ml"], synonyms: ["finetuning", "fine-tune", "rlhf"], related: [{ to: "llm", w: 0.5 }] },
196
+ { id: "computer-vision", parents: ["ml"], synonyms: ["image-recognition", "object-detection"] },
197
+ { id: "recsys", parents: ["ml"], synonyms: ["recommender-systems", "recommendation-systems", "recommendation"] },
198
+ // ── Mobile ──────────────────────────────────────────────────────────────────
199
+ { id: "mobile", related: [{ to: "ios", w: 0.5 }, { to: "android", w: 0.5 }] },
200
+ { id: "ios", parents: ["mobile", "swift"], related: [{ to: "android", w: 0.4 }] },
201
+ { id: "android", parents: ["mobile"], related: [{ to: "kotlin", w: 0.4 }] },
202
+ { id: "swiftui", parents: ["ios", "swift"] },
203
+ { id: "react-native", parents: ["mobile", "react"], synonyms: ["reactnative"], related: [{ to: "flutter", w: 0.4 }, { to: "expo", w: 0.6 }] },
204
+ { id: "flutter", parents: ["mobile", "dart"] },
205
+ { id: "expo", parents: ["react-native"] },
206
+ { id: "kotlin-multiplatform", parents: ["mobile", "kotlin"], synonyms: ["kmp"] },
207
+ // ── Domains / capabilities ──────────────────────────────────────────────────
208
+ { id: "frontend", related: [{ to: "react", w: 0.4 }, { to: "css", w: 0.3 }] },
209
+ { id: "backend", related: [{ to: "api-design", w: 0.4 }, { to: "microservices", w: 0.4 }] },
210
+ { id: "devops", related: [{ to: "kubernetes", w: 0.4 }, { to: "ci-cd", w: 0.4 }, { to: "docker", w: 0.4 }] },
211
+ { id: "authentication", synonyms: ["auth", "jwt", "saml", "passport", "auth0", "clerk", "nextauth"], related: [{ to: "oauth", w: 0.6 }, { to: "security", w: 0.5 }] },
212
+ { id: "oauth", parents: ["authentication"], synonyms: ["oauth2", "oidc"], related: [{ to: "security", w: 0.4 }] },
213
+ { id: "security", related: [{ to: "authentication", w: 0.5 }] },
214
+ { id: "payments", synonyms: ["stripe", "braintree", "paddle", "lemonsqueezy", "@stripe/stripe-js"], related: [{ to: "billing", w: 0.6 }] },
215
+ { id: "billing", synonyms: ["recurly", "chargebee"] },
216
+ { id: "api-design", synonyms: ["rest", "restful", "rest-api"], related: [{ to: "graphql", w: 0.4 }, { to: "grpc", w: 0.4 }, { to: "backend", w: 0.4 }] },
217
+ { id: "graphql", synonyms: ["gql"], related: [{ to: "trpc", w: 0.4 }] },
218
+ { id: "trpc", related: [{ to: "graphql", w: 0.4 }] },
219
+ { id: "grpc", synonyms: ["grpc-web"], related: [{ to: "microservices", w: 0.3 }] },
220
+ { id: "microservices" },
221
+ { id: "websockets", synonyms: ["ws", "socket.io"], related: [{ to: "realtime", w: 0.6 }] },
222
+ { id: "realtime", synonyms: ["real-time"] },
223
+ { id: "message-queue", synonyms: ["mq"] },
224
+ { id: "caching", synonyms: ["cache"] },
225
+ { id: "search", synonyms: ["full-text-search"] },
226
+ { id: "observability", synonyms: ["o11y"], related: [{ to: "monitoring", w: 0.6 }] },
227
+ { id: "monitoring", related: [{ to: "prometheus", w: 0.4 }] },
228
+ { id: "testing", related: [{ to: "unit-testing", w: 0.5 }, { to: "e2e-testing", w: 0.5 }] },
229
+ { id: "unit-testing", parents: ["testing"] },
230
+ { id: "e2e-testing", parents: ["testing"], synonyms: ["e2e", "end-to-end-testing"] },
231
+ { id: "jest", parents: ["testing"], related: [{ to: "vitest", w: 0.6 }, { to: "mocha", w: 0.5 }] },
232
+ { id: "vitest", parents: ["testing"], related: [{ to: "jest", w: 0.6 }] },
233
+ { id: "playwright", parents: ["e2e-testing"], related: [{ to: "cypress", w: 0.6 }] },
234
+ { id: "cypress", parents: ["e2e-testing"] },
235
+ { id: "mocha", parents: ["testing"] },
236
+ { id: "pytest", parents: ["testing", "python"] },
237
+ { id: "accessibility", synonyms: ["a11y"] },
238
+ { id: "seo" },
239
+ { id: "performance", synonyms: ["perf", "web-performance"] }
240
+ ];
241
+ }
242
+ });
243
+
244
+ // ../../packages/core/src/vocab/closure.ts
245
+ function round3(n) {
246
+ return Math.round(n * 1e3) / 1e3;
247
+ }
248
+ function validateGraph(nodes) {
249
+ const ids = /* @__PURE__ */ new Set();
250
+ for (const n of nodes) {
251
+ if (ids.has(n.id)) throw new Error(`vocab: duplicate id "${n.id}"`);
252
+ ids.add(n.id);
253
+ }
254
+ const seenAlias = /* @__PURE__ */ new Map();
255
+ for (const n of nodes) {
256
+ for (const p of n.parents ?? []) {
257
+ if (p === n.id) throw new Error(`vocab: "${n.id}" lists itself as a parent`);
258
+ if (!ids.has(p)) throw new Error(`vocab: "${n.id}" parent "${p}" is not a defined id`);
259
+ }
260
+ for (const e of n.related ?? []) {
261
+ if (e.to === n.id) throw new Error(`vocab: "${n.id}" relates to itself`);
262
+ if (!ids.has(e.to)) throw new Error(`vocab: "${n.id}" related "${e.to}" is not a defined id`);
263
+ if (!(e.w > 0 && e.w <= 1)) throw new Error(`vocab: "${n.id}"\u2192"${e.to}" weight ${e.w} out of (0,1]`);
264
+ }
265
+ for (const s of n.synonyms ?? []) {
266
+ const alias = s.toLowerCase();
267
+ if (ids.has(alias)) throw new Error(`vocab: synonym "${alias}" collides with a canonical id`);
268
+ const prev = seenAlias.get(alias);
269
+ if (prev && prev !== n.id) throw new Error(`vocab: synonym "${alias}" maps to both "${prev}" and "${n.id}"`);
270
+ seenAlias.set(alias, n.id);
271
+ }
272
+ }
273
+ const visiting = /* @__PURE__ */ new Set();
274
+ const done = /* @__PURE__ */ new Set();
275
+ const parentMap = new Map(nodes.map((n) => [n.id, n.parents ?? []]));
276
+ const walk = (id, path) => {
277
+ if (done.has(id)) return;
278
+ if (visiting.has(id)) throw new Error(`vocab: parent cycle ${[...path, id].join(" \u2192 ")}`);
279
+ visiting.add(id);
280
+ for (const p of parentMap.get(id) ?? []) walk(p, [...path, id]);
281
+ visiting.delete(id);
282
+ done.add(id);
283
+ };
284
+ for (const n of nodes) walk(n.id, []);
285
+ }
286
+ function buildAdjacency(nodes) {
287
+ const adj = /* @__PURE__ */ new Map();
288
+ const add = (from, to, w) => {
289
+ let m = adj.get(from);
290
+ if (!m) adj.set(from, m = /* @__PURE__ */ new Map());
291
+ if (w > (m.get(to) ?? 0)) m.set(to, w);
292
+ };
293
+ for (const n of nodes) {
294
+ for (const p of n.parents ?? []) {
295
+ add(n.id, p, PARENT_UP);
296
+ add(p, n.id, PARENT_DOWN);
297
+ }
298
+ for (const e of n.related ?? []) {
299
+ add(n.id, e.to, e.w);
300
+ add(e.to, n.id, e.w);
301
+ }
302
+ }
303
+ return adj;
304
+ }
305
+ function closureFrom(source, adj) {
306
+ const best = /* @__PURE__ */ new Map();
307
+ for (const [t, w] of adj.get(source) ?? []) {
308
+ if (w >= DECAY_FLOOR) best.set(t, { w: round3(w), via: t });
309
+ }
310
+ const settled = /* @__PURE__ */ new Set([source]);
311
+ while (true) {
312
+ let u;
313
+ let uw = 0;
314
+ for (const [t, e] of best) {
315
+ if (!settled.has(t) && e.w > uw) {
316
+ u = t;
317
+ uw = e.w;
318
+ }
319
+ }
320
+ if (!u) break;
321
+ settled.add(u);
322
+ const via = best.get(u).via;
323
+ for (const [t, we] of adj.get(u) ?? []) {
324
+ if (settled.has(t)) continue;
325
+ const cand = round3(uw * we);
326
+ if (cand >= DECAY_FLOOR && cand > (best.get(t)?.w ?? 0)) {
327
+ best.set(t, { w: cand, via });
328
+ }
329
+ }
330
+ }
331
+ best.delete(source);
332
+ return best;
333
+ }
334
+ function buildGraph(nodes) {
335
+ validateGraph(nodes);
336
+ const ids = new Set(nodes.map((n) => n.id));
337
+ const synonyms = /* @__PURE__ */ new Map();
338
+ for (const n of nodes) {
339
+ for (const s of n.synonyms ?? []) synonyms.set(s.toLowerCase(), n.id);
340
+ }
341
+ const adj = buildAdjacency(nodes);
342
+ const closure = /* @__PURE__ */ new Map();
343
+ for (const n of nodes) closure.set(n.id, closureFrom(n.id, adj));
344
+ return { ids, synonyms, closure };
345
+ }
346
+ var PARENT_UP, PARENT_DOWN, DECAY_FLOOR;
347
+ var init_closure = __esm({
348
+ "../../packages/core/src/vocab/closure.ts"() {
349
+ "use strict";
350
+ PARENT_UP = 0.6;
351
+ PARENT_DOWN = 0.35;
352
+ DECAY_FLOOR = 0.25;
353
+ }
354
+ });
355
+
356
+ // ../../packages/core/src/vocab/types.ts
357
+ var init_types2 = __esm({
358
+ "../../packages/core/src/vocab/types.ts"() {
359
+ "use strict";
360
+ }
361
+ });
362
+
363
+ // ../../packages/core/src/vocab/extract.ts
364
+ var SOFT_DOMAIN, SYNONYM_ONLY;
365
+ var init_extract = __esm({
366
+ "../../packages/core/src/vocab/extract.ts"() {
367
+ "use strict";
368
+ init_vocab();
369
+ SOFT_DOMAIN = /* @__PURE__ */ new Set([
370
+ "frontend",
371
+ "backend",
372
+ "devops",
373
+ "security",
374
+ "payments",
375
+ "billing",
376
+ "microservices",
377
+ "caching",
378
+ "search",
379
+ "observability",
380
+ "monitoring",
381
+ "testing",
382
+ "accessibility",
383
+ "seo",
384
+ "performance",
385
+ "realtime",
386
+ "authentication",
387
+ "api-design"
388
+ ]);
389
+ SYNONYM_ONLY = /* @__PURE__ */ new Set(["performance", "security", "seo"]);
390
+ for (const id of SYNONYM_ONLY) {
391
+ if (!SOFT_DOMAIN.has(id)) throw new Error(`extract: SYNONYM_ONLY "${id}" not in SOFT_DOMAIN`);
392
+ }
393
+ }
394
+ });
395
+
396
+ // ../../packages/core/src/vocab/idf-background.ts
397
+ var init_idf_background = __esm({
398
+ "../../packages/core/src/vocab/idf-background.ts"() {
399
+ "use strict";
400
+ }
401
+ });
402
+
403
+ // ../../packages/core/src/vocab/index.ts
404
+ function normalize(tokens) {
405
+ const result = /* @__PURE__ */ new Set();
406
+ for (const raw of tokens) {
407
+ const lower = raw.toLowerCase().trim();
408
+ if (GRAPH.ids.has(lower)) {
409
+ result.add(lower);
410
+ continue;
411
+ }
412
+ const mapped = GRAPH.synonyms.get(lower);
413
+ if (mapped) result.add(mapped);
414
+ }
415
+ return Array.from(result);
416
+ }
417
+ var GRAPH, VOCABULARY, SYNONYMS;
418
+ var init_vocab = __esm({
419
+ "../../packages/core/src/vocab/index.ts"() {
420
+ "use strict";
421
+ init_graph_data();
422
+ init_closure();
423
+ init_types2();
424
+ init_closure();
425
+ init_graph_data();
426
+ init_extract();
427
+ init_idf_background();
428
+ GRAPH = buildGraph(VOCAB_NODES);
429
+ VOCABULARY = [...GRAPH.ids];
430
+ SYNONYMS = Object.fromEntries(GRAPH.synonyms);
431
+ }
432
+ });
433
+
434
+ // ../../packages/core/src/vocabulary.ts
435
+ var init_vocabulary = __esm({
436
+ "../../packages/core/src/vocabulary.ts"() {
437
+ "use strict";
438
+ init_vocab();
439
+ }
440
+ });
441
+
442
+ // ../../packages/core/src/feeds/bounty-gate.ts
443
+ var BOUNTY_REPO_DENYLIST, DENYLIST_LC, AI_BAN_DENYLIST, AI_BAN_LC;
444
+ var init_bounty_gate = __esm({
445
+ "../../packages/core/src/feeds/bounty-gate.ts"() {
446
+ "use strict";
447
+ BOUNTY_REPO_DENYLIST = [
448
+ "SecureBananaLabs/bug-bounty",
449
+ // Meta-farm: a bounty PLATFORM whose own issues are an assignment-gated
450
+ // contributor queue ("please assign me, my chief") — an unsolicited PR won't
451
+ // merge, so it's not a real claimable bounty. Not structurally derivable from
452
+ // any fetched field, so it's a manual entry (also dropped from the allowlist).
453
+ "boundlessfi/boundless"
454
+ ];
455
+ DENYLIST_LC = new Set(BOUNTY_REPO_DENYLIST.map((r) => r.toLowerCase()));
456
+ AI_BAN_DENYLIST = [
457
+ // Gentoo Council voted 6-0 (2024-04-14) to ban AI/ML-generated contributions
458
+ // project-wide. https://wiki.gentoo.org/wiki/Project:Council/AI_policy
459
+ "gentoo",
460
+ // NetBSD Commit Guidelines: code generated by an LLM/similar technology is
461
+ // "presumed to be tainted code, and must not be committed without prior
462
+ // written approval by core". https://www.netbsd.org/developers/commit-guidelines.html
463
+ "NetBSD"
464
+ // NOT listed (checked, deliberately excluded): QEMU's blanket AI-contribution
465
+ // ban is IN FLUX as of 2026-05 — a patch replacing it with a disclosure-based
466
+ // policy ("AI-used-for:" tag) was posted to qemu-devel and appears to have
467
+ // developer buy-in, so it is no longer a clean/current ban to key a hard
468
+ // supply-side drop on. https://www.theregister.com/ai-and-ml/2026/05/29/qemu-mulls-relaxing-ai-contribution-ban/
469
+ ];
470
+ AI_BAN_LC = new Set(AI_BAN_DENYLIST.map((g) => g.toLowerCase()));
471
+ }
472
+ });
473
+
474
+ // ../../packages/core/src/feeds/contribution-gate.ts
475
+ var init_contribution_gate = __esm({
476
+ "../../packages/core/src/feeds/contribution-gate.ts"() {
477
+ "use strict";
478
+ init_bounty_gate();
479
+ }
480
+ });
481
+
482
+ // ../../packages/core/src/github.ts
483
+ var RESUME_DECAY_HALF_LIFE_MS;
484
+ var init_github = __esm({
485
+ "../../packages/core/src/github.ts"() {
486
+ "use strict";
487
+ init_vocabulary();
488
+ init_contribution_gate();
489
+ RESUME_DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
490
+ }
491
+ });
492
+
493
+ // ../../packages/core/src/matcher.ts
494
+ var init_matcher = __esm({
495
+ "../../packages/core/src/matcher.ts"() {
496
+ "use strict";
497
+ init_vocabulary();
498
+ init_github();
499
+ }
500
+ });
501
+
502
+ // ../../packages/core/src/rerank.ts
503
+ var init_rerank = __esm({
504
+ "../../packages/core/src/rerank.ts"() {
505
+ "use strict";
506
+ }
507
+ });
508
+
509
+ // ../../packages/core/src/feeds/http.ts
510
+ var init_http = __esm({
511
+ "../../packages/core/src/feeds/http.ts"() {
512
+ "use strict";
513
+ }
514
+ });
515
+
516
+ // ../../packages/core/src/feeds/greenhouse.ts
517
+ var init_greenhouse = __esm({
518
+ "../../packages/core/src/feeds/greenhouse.ts"() {
519
+ "use strict";
520
+ init_vocabulary();
521
+ init_http();
522
+ }
523
+ });
524
+
525
+ // ../../packages/core/src/feeds/ashby.ts
526
+ var init_ashby = __esm({
527
+ "../../packages/core/src/feeds/ashby.ts"() {
528
+ "use strict";
529
+ init_vocabulary();
530
+ init_http();
531
+ }
532
+ });
533
+
534
+ // ../../packages/core/src/feeds/lever.ts
535
+ var init_lever = __esm({
536
+ "../../packages/core/src/feeds/lever.ts"() {
537
+ "use strict";
538
+ init_vocabulary();
539
+ init_http();
540
+ }
541
+ });
542
+
543
+ // ../../packages/core/src/feeds/himalayas.ts
544
+ var init_himalayas = __esm({
545
+ "../../packages/core/src/feeds/himalayas.ts"() {
546
+ "use strict";
547
+ init_vocabulary();
548
+ init_http();
549
+ }
550
+ });
551
+
552
+ // ../../packages/core/src/feeds/entities.ts
553
+ var init_entities = __esm({
554
+ "../../packages/core/src/feeds/entities.ts"() {
555
+ "use strict";
556
+ }
557
+ });
558
+
559
+ // ../../packages/core/src/feeds/wwr.ts
560
+ var init_wwr = __esm({
561
+ "../../packages/core/src/feeds/wwr.ts"() {
562
+ "use strict";
563
+ init_vocabulary();
564
+ init_entities();
565
+ init_http();
566
+ }
567
+ });
568
+
569
+ // ../../packages/core/src/feeds/hn.ts
570
+ var init_hn = __esm({
571
+ "../../packages/core/src/feeds/hn.ts"() {
572
+ "use strict";
573
+ init_vocabulary();
574
+ init_entities();
575
+ init_http();
576
+ }
577
+ });
578
+
579
+ // ../../packages/core/src/concurrency.ts
580
+ var init_concurrency = __esm({
581
+ "../../packages/core/src/concurrency.ts"() {
582
+ "use strict";
583
+ }
584
+ });
585
+
586
+ // ../../packages/core/src/feeds/effort.ts
587
+ var init_effort = __esm({
588
+ "../../packages/core/src/feeds/effort.ts"() {
589
+ "use strict";
590
+ }
591
+ });
592
+
593
+ // ../../packages/core/src/feeds/github-bounties.ts
594
+ var init_github_bounties = __esm({
595
+ "../../packages/core/src/feeds/github-bounties.ts"() {
596
+ "use strict";
597
+ init_vocabulary();
598
+ init_entities();
599
+ init_bounty_gate();
600
+ init_http();
601
+ init_concurrency();
602
+ init_effort();
603
+ }
604
+ });
605
+
606
+ // ../../packages/core/src/feeds/opire.ts
607
+ var init_opire = __esm({
608
+ "../../packages/core/src/feeds/opire.ts"() {
609
+ "use strict";
610
+ init_vocabulary();
611
+ init_bounty_gate();
612
+ init_github_bounties();
613
+ init_http();
614
+ init_effort();
615
+ }
616
+ });
617
+
618
+ // ../../packages/core/src/feeds/workable.ts
619
+ var init_workable = __esm({
620
+ "../../packages/core/src/feeds/workable.ts"() {
621
+ "use strict";
622
+ init_vocabulary();
623
+ init_http();
624
+ }
625
+ });
626
+
627
+ // ../../packages/core/src/feeds/directory.ts
628
+ var init_directory = __esm({
629
+ "../../packages/core/src/feeds/directory.ts"() {
630
+ "use strict";
631
+ }
632
+ });
633
+
634
+ // ../../packages/core/src/feeds/index.ts
635
+ function flattenTiers(t) {
636
+ return [.../* @__PURE__ */ new Set([...t.bigco, ...t.scaleup, ...t.startup])];
637
+ }
638
+ var GREENHOUSE_SLUGS_BY_TIER, ASHBY_SLUGS_BY_TIER, LEVER_SLUGS_BY_TIER, DEFAULT_GREENHOUSE_SLUGS, DEFAULT_ASHBY_SLUGS, DEFAULT_LEVER_SLUGS;
639
+ var init_feeds = __esm({
640
+ "../../packages/core/src/feeds/index.ts"() {
641
+ "use strict";
642
+ init_greenhouse();
643
+ init_ashby();
644
+ init_lever();
645
+ init_himalayas();
646
+ init_wwr();
647
+ init_hn();
648
+ init_github_bounties();
649
+ init_opire();
650
+ init_workable();
651
+ init_directory();
652
+ init_bounty_gate();
653
+ init_bounty_gate();
654
+ init_contribution_gate();
655
+ GREENHOUSE_SLUGS_BY_TIER = {
656
+ bigco: [
657
+ "stripe",
658
+ "anthropic",
659
+ "figma",
660
+ "discord",
661
+ "brex",
662
+ "mercury",
663
+ "plaid",
664
+ "gusto",
665
+ "scale",
666
+ "databricks",
667
+ "coinbase",
668
+ "robinhood",
669
+ "doordash",
670
+ "airbnb",
671
+ "dropbox",
672
+ "datadog",
673
+ "cloudflare",
674
+ "reddit",
675
+ "lyft",
676
+ "instacart"
677
+ ],
678
+ scaleup: [
679
+ "samsara",
680
+ "verkada",
681
+ "affirm",
682
+ "gitlab",
683
+ "asana",
684
+ "flexport",
685
+ "faire",
686
+ "twitch",
687
+ "airtable",
688
+ "retool"
689
+ ],
690
+ startup: [
691
+ "watershed"
692
+ ]
693
+ };
694
+ ASHBY_SLUGS_BY_TIER = {
695
+ bigco: [
696
+ "openai"
697
+ ],
698
+ scaleup: [
699
+ "harvey",
700
+ "elevenlabs",
701
+ "notion",
702
+ "sierra",
703
+ "cohere",
704
+ "ramp",
705
+ "vanta",
706
+ "decagon",
707
+ "cursor",
708
+ "replit",
709
+ "perplexity",
710
+ "baseten",
711
+ "drata",
712
+ "writer",
713
+ "temporal",
714
+ "supabase"
715
+ ],
716
+ startup: [
717
+ "suno",
718
+ "attio",
719
+ "modal",
720
+ "workos",
721
+ "linear",
722
+ "render",
723
+ "warp",
724
+ "plain",
725
+ "posthog",
726
+ "pylon",
727
+ "resend",
728
+ "langfuse",
729
+ "railway",
730
+ "mintlify",
731
+ "neon",
732
+ "browserbase",
733
+ "knock",
734
+ "speakeasy",
735
+ "stytch",
736
+ "runway",
737
+ "doppler",
738
+ "inngest",
739
+ "hightouch",
740
+ "zed"
741
+ ]
742
+ };
743
+ LEVER_SLUGS_BY_TIER = {
744
+ bigco: [
745
+ "palantir",
746
+ "spotify"
747
+ ],
748
+ scaleup: [
749
+ "mistral",
750
+ "ro",
751
+ "secureframe"
752
+ ],
753
+ startup: [
754
+ "anyscale"
755
+ ]
756
+ };
757
+ DEFAULT_GREENHOUSE_SLUGS = flattenTiers(GREENHOUSE_SLUGS_BY_TIER);
758
+ DEFAULT_ASHBY_SLUGS = flattenTiers(ASHBY_SLUGS_BY_TIER);
759
+ DEFAULT_LEVER_SLUGS = flattenTiers(LEVER_SLUGS_BY_TIER);
760
+ }
761
+ });
762
+
763
+ // ../../packages/core/src/feeds/contribution-classify.ts
764
+ var init_contribution_classify = __esm({
765
+ "../../packages/core/src/feeds/contribution-classify.ts"() {
766
+ "use strict";
767
+ }
768
+ });
769
+
770
+ // ../../packages/core/src/feeds/contributions.ts
771
+ var init_contributions = __esm({
772
+ "../../packages/core/src/feeds/contributions.ts"() {
773
+ "use strict";
774
+ init_vocabulary();
775
+ init_entities();
776
+ init_bounty_gate();
777
+ init_contribution_gate();
778
+ init_contribution_classify();
779
+ init_github_bounties();
780
+ init_http();
781
+ }
782
+ });
783
+
784
+ // ../../packages/core/src/partners.ts
785
+ import { readFileSync } from "fs";
786
+ import { join } from "path";
787
+ import { fileURLToPath } from "url";
788
+ var EXAMPLE_BUYER, BUYER_REGISTRY;
789
+ var init_partners = __esm({
790
+ "../../packages/core/src/partners.ts"() {
791
+ "use strict";
792
+ EXAMPLE_BUYER = {
793
+ id: "northstar",
794
+ legalName: "Northstar Talent Partners",
795
+ matchCriteria: { roleTypes: ["full_time"] }
796
+ };
797
+ BUYER_REGISTRY = {
798
+ [EXAMPLE_BUYER.id]: EXAMPLE_BUYER
799
+ };
800
+ }
801
+ });
802
+
803
+ // ../../packages/core/src/indexer.ts
804
+ var init_indexer = __esm({
805
+ "../../packages/core/src/indexer.ts"() {
806
+ "use strict";
807
+ init_feeds();
808
+ init_contributions();
809
+ init_partners();
810
+ }
811
+ });
812
+
813
+ // ../../packages/core/src/intro.ts
814
+ var INTRO_ALLOWED_FIELDS, INTRO_ALLOWED_SET, INTRO_PENDING_TTL_MS, INTRO_ACCEPTED_TTL_MS;
815
+ var init_intro = __esm({
816
+ "../../packages/core/src/intro.ts"() {
817
+ "use strict";
818
+ INTRO_ALLOWED_FIELDS = [
819
+ "requesterLogin",
820
+ "requesterDisplayName",
821
+ "requesterContact",
822
+ "note",
823
+ "targetLogin"
824
+ ];
825
+ INTRO_ALLOWED_SET = new Set(INTRO_ALLOWED_FIELDS);
826
+ INTRO_PENDING_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
827
+ INTRO_ACCEPTED_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
828
+ }
829
+ });
830
+
831
+ // ../../packages/core/src/directoryThreshold.ts
832
+ var init_directoryThreshold = __esm({
833
+ "../../packages/core/src/directoryThreshold.ts"() {
834
+ "use strict";
835
+ }
836
+ });
837
+
838
+ // ../../packages/core/src/chatCrypto.ts
839
+ import { hkdfSync, createHash, randomBytes } from "crypto";
840
+ var KDF_INFO;
841
+ var init_chatCrypto = __esm({
842
+ "../../packages/core/src/chatCrypto.ts"() {
843
+ "use strict";
844
+ KDF_INFO = Buffer.from("terminalhire-chat-v1");
845
+ }
846
+ });
847
+
848
+ // ../../packages/core/src/job-status.ts
849
+ function recordClick(map, id) {
850
+ const prev = map[id];
851
+ if (prev?.clicked === true) return map;
852
+ return { ...map, [id]: { ...prev, clicked: true } };
853
+ }
854
+ function setStatus(map, id, s, markedAt = (/* @__PURE__ */ new Date()).toISOString()) {
855
+ const prev = map[id];
856
+ return { ...map, [id]: { ...prev, status: s, markedAt } };
857
+ }
858
+ var init_job_status = __esm({
859
+ "../../packages/core/src/job-status.ts"() {
860
+ "use strict";
861
+ }
862
+ });
863
+
864
+ // ../../packages/core/src/episodes/schema.ts
865
+ var init_schema = __esm({
866
+ "../../packages/core/src/episodes/schema.ts"() {
867
+ "use strict";
868
+ }
869
+ });
870
+
871
+ // ../../packages/core/src/episodes/doors.ts
872
+ var init_doors = __esm({
873
+ "../../packages/core/src/episodes/doors.ts"() {
874
+ "use strict";
875
+ init_schema();
876
+ }
877
+ });
878
+
879
+ // ../../packages/core/src/episodes/node-model.ts
880
+ var init_node_model = __esm({
881
+ "../../packages/core/src/episodes/node-model.ts"() {
882
+ "use strict";
883
+ }
884
+ });
885
+
886
+ // ../../packages/core/src/episodes/derivers/signals.ts
887
+ var init_signals = __esm({
888
+ "../../packages/core/src/episodes/derivers/signals.ts"() {
889
+ "use strict";
890
+ init_node_model();
891
+ }
892
+ });
893
+
894
+ // ../../packages/core/src/episodes/derivers/recency-split.ts
895
+ var init_recency_split = __esm({
896
+ "../../packages/core/src/episodes/derivers/recency-split.ts"() {
897
+ "use strict";
898
+ init_doors();
899
+ init_signals();
900
+ }
901
+ });
902
+
903
+ // ../../packages/core/src/credential/legible.ts
904
+ var init_legible = __esm({
905
+ "../../packages/core/src/credential/legible.ts"() {
906
+ "use strict";
907
+ init_contribution_gate();
908
+ init_vocabulary();
909
+ init_recency_split();
910
+ }
911
+ });
912
+
913
+ // ../../packages/core/src/credential/legible-trajectory.ts
914
+ var init_legible_trajectory = __esm({
915
+ "../../packages/core/src/credential/legible-trajectory.ts"() {
916
+ "use strict";
917
+ }
918
+ });
919
+
920
+ // ../../packages/core/src/short-token.ts
921
+ import { createHash as createHash2 } from "crypto";
922
+ function opportunityShortToken(id) {
923
+ return createHash2("sha256").update(id, "utf8").digest("base64url").slice(0, 8);
924
+ }
925
+ var init_short_token = __esm({
926
+ "../../packages/core/src/short-token.ts"() {
927
+ "use strict";
928
+ }
929
+ });
930
+
931
+ // ../../packages/core/src/index.ts
932
+ var init_src = __esm({
933
+ "../../packages/core/src/index.ts"() {
934
+ "use strict";
935
+ init_types();
936
+ init_vocabulary();
937
+ init_matcher();
938
+ init_rerank();
939
+ init_feeds();
940
+ init_indexer();
941
+ init_partners();
942
+ init_github();
943
+ init_intro();
944
+ init_directoryThreshold();
945
+ init_chatCrypto();
946
+ init_job_status();
947
+ init_legible();
948
+ init_legible_trajectory();
949
+ init_short_token();
950
+ }
951
+ });
11
952
 
12
953
  // src/claims.ts
13
954
  var claims_exports = {};
@@ -18,11 +959,23 @@ __export(claims_exports, {
18
959
  readClaims: () => readClaims,
19
960
  recordClaim: () => recordClaim,
20
961
  removeClaim: () => removeClaim,
962
+ toPushedClaim: () => toPushedClaim,
21
963
  updateClaim: () => updateClaim
22
964
  });
23
965
  import { readFileSync as readFileSync2, writeFileSync, mkdirSync, renameSync, existsSync } from "fs";
24
966
  import { join as join2 } from "path";
25
967
  import { homedir } from "os";
968
+ function toPushedClaim(claim) {
969
+ return {
970
+ kind: claim.kind,
971
+ repoFullName: claim.repoFullName,
972
+ state: claim.state,
973
+ prUrl: claim.prUrl,
974
+ merged: claim.state === "merged",
975
+ claimedAt: claim.claimedAt,
976
+ updatedAt: claim.updatedAt
977
+ };
978
+ }
26
979
  function nowISO() {
27
980
  return (/* @__PURE__ */ new Date()).toISOString();
28
981
  }
@@ -195,518 +1148,469 @@ var init_repo_policy = __esm({
195
1148
  }
196
1149
  });
197
1150
 
198
- // bin/jpi-claim.js
199
- import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
200
- import { join as join3 } from "path";
1151
+ // bin/job-status-store.js
1152
+ var job_status_store_exports = {};
1153
+ __export(job_status_store_exports, {
1154
+ markClicked: () => markClicked,
1155
+ markStatus: () => markStatus,
1156
+ readStatusMap: () => readStatusMap,
1157
+ statusFilePath: () => statusFilePath
1158
+ });
1159
+ import {
1160
+ readFileSync as readFileSync3,
1161
+ writeFileSync as writeFileSync2,
1162
+ renameSync as renameSync2,
1163
+ mkdirSync as mkdirSync2,
1164
+ existsSync as existsSync2,
1165
+ copyFileSync,
1166
+ openSync,
1167
+ closeSync,
1168
+ unlinkSync
1169
+ } from "fs";
1170
+ import { join as join3, dirname } from "path";
201
1171
  import { homedir as homedir2 } from "os";
202
- import { execFile } from "child_process";
203
- import { promisify } from "util";
204
- import { createInterface } from "readline";
205
-
206
- // ../../packages/core/src/vocab/graph.data.ts
207
- var VOCAB_NODES = [
208
- // ── Languages ─────────────────────────────────────────────────────────────
209
- { id: "javascript", synonyms: ["js"], related: [{ to: "typescript", w: 0.6 }] },
210
- { id: "typescript", parents: ["javascript"], synonyms: ["ts"] },
211
- { id: "python", synonyms: ["py"] },
212
- { id: "go", synonyms: ["golang"] },
213
- { id: "rust" },
214
- { id: "java", related: [{ to: "kotlin", w: 0.45 }, { to: "scala", w: 0.4 }] },
215
- { id: "ruby" },
216
- { id: "elixir" },
217
- { id: "scala", related: [{ to: "java", w: 0.4 }] },
218
- { id: "kotlin", related: [{ to: "java", w: 0.45 }] },
219
- { id: "swift" },
220
- { id: "cpp", synonyms: ["c++"] },
221
- { id: "csharp", synonyms: ["c#"] },
222
- { id: "php" },
223
- { id: "haskell" },
224
- { id: "clojure" },
225
- { id: "r" },
226
- { id: "dart" },
227
- // ── Frontend ──────────────────────────────────────────────────────────────
228
- {
229
- id: "react",
230
- parents: ["javascript"],
231
- synonyms: ["reactjs"],
232
- 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 }]
233
- },
234
- { id: "nextjs", parents: ["react"], synonyms: ["next", "next.js"], related: [{ to: "remix", w: 0.5 }] },
235
- { id: "vue", parents: ["javascript"], synonyms: ["vue.js"], related: [{ to: "nuxt", w: 0.6 }] },
236
- { id: "nuxt", parents: ["vue"], synonyms: ["nuxt.js"] },
237
- { id: "svelte", parents: ["javascript"], related: [{ to: "sveltekit", w: 0.65 }] },
238
- { id: "sveltekit", parents: ["svelte"] },
239
- { id: "angular", parents: ["typescript"], synonyms: ["angular.js", "angularjs"] },
240
- { id: "solidjs", parents: ["javascript"] },
241
- { id: "remix", parents: ["react"], synonyms: ["remix.run"] },
242
- { id: "astro", parents: ["javascript"], related: [{ to: "nextjs", w: 0.4 }] },
243
- { id: "qwik", parents: ["javascript"] },
244
- { id: "tailwind", parents: ["css"], synonyms: ["tailwindcss", "tw"] },
245
- { id: "css" },
246
- { id: "html" },
247
- { id: "redux", parents: ["react"] },
248
- { id: "vite", parents: ["frontend"] },
249
- { id: "webpack", parents: ["frontend"] },
250
- { id: "storybook", parents: ["frontend"] },
251
- // ── Backend frameworks / runtimes ───────────────────────────────────────────
252
- {
253
- id: "nodejs",
254
- parents: ["javascript"],
255
- synonyms: ["node", "node.js"],
256
- related: [{ to: "express", w: 0.5 }, { to: "fastify", w: 0.45 }, { to: "nestjs", w: 0.45 }]
257
- },
258
- { id: "express", parents: ["nodejs"], synonyms: ["express.js", "expressjs"], related: [{ to: "fastify", w: 0.5 }] },
259
- { id: "fastify", parents: ["nodejs"] },
260
- { id: "nestjs", parents: ["nodejs"], synonyms: ["nest", "nest.js"] },
261
- { id: "hono", parents: ["nodejs"] },
262
- { id: "deno", parents: ["javascript"], related: [{ to: "nodejs", w: 0.5 }, { to: "bun", w: 0.5 }] },
263
- { id: "bun", parents: ["javascript"], related: [{ to: "nodejs", w: 0.5 }] },
264
- { id: "django", parents: ["python"], related: [{ to: "flask", w: 0.5 }, { to: "fastapi", w: 0.45 }] },
265
- { id: "fastapi", parents: ["python"], related: [{ to: "flask", w: 0.55 }, { to: "django", w: 0.45 }] },
266
- { id: "flask", parents: ["python"] },
267
- { id: "rails", parents: ["ruby"], synonyms: ["ruby-on-rails", "ror"] },
268
- { id: "spring", parents: ["java"], synonyms: ["spring-boot", "springboot"] },
269
- { id: "actix", parents: ["rust"] },
270
- { id: "gin", parents: ["go"] },
271
- { id: "phoenix", parents: ["elixir"] },
272
- { id: "laravel", parents: ["php"] },
273
- { id: "dotnet", parents: ["csharp"], synonyms: [".net", "asp.net", "dotnet-core"] },
274
- // ── Infrastructure & DevOps ─────────────────────────────────────────────────
275
- { 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 }] },
276
- { id: "docker", parents: ["devops"], related: [{ to: "kubernetes", w: 0.5 }] },
277
- { id: "terraform", synonyms: ["tf"], related: [{ to: "pulumi", w: 0.55 }, { to: "ansible", w: 0.4 }, { to: "aws", w: 0.4 }] },
278
- { id: "pulumi", related: [{ to: "terraform", w: 0.55 }] },
279
- { id: "ansible" },
280
- { id: "aws", synonyms: ["amazon-web-services"], related: [{ to: "gcp", w: 0.4 }, { to: "azure", w: 0.4 }] },
281
- { id: "gcp", synonyms: ["google-cloud", "google-cloud-platform"], related: [{ to: "aws", w: 0.4 }, { to: "azure", w: 0.4 }] },
282
- { id: "azure", synonyms: ["microsoft-azure"], related: [{ to: "aws", w: 0.4 }] },
283
- { id: "ci-cd", synonyms: ["cicd", "jenkins", "circleci", "circle-ci", "travis"], related: [{ to: "github-actions", w: 0.6 }, { to: "gitlab-ci", w: 0.6 }] },
284
- { id: "github-actions", parents: ["ci-cd"], synonyms: ["github-action"] },
285
- { id: "gitlab-ci", parents: ["ci-cd"], synonyms: ["gitlab"] },
286
- { id: "linux" },
287
- { id: "nginx" },
288
- { id: "prometheus", parents: ["observability"], related: [{ to: "grafana", w: 0.6 }] },
289
- { id: "grafana", parents: ["observability"] },
290
- { id: "datadog", parents: ["observability"] },
291
- { id: "opentelemetry", parents: ["observability"], synonyms: ["otel"] },
292
- { id: "vercel", related: [{ to: "netlify", w: 0.5 }, { to: "nextjs", w: 0.4 }] },
293
- { id: "netlify" },
294
- { id: "fly", synonyms: ["fly.io"], related: [{ to: "railway", w: 0.5 }, { to: "render", w: 0.5 }] },
295
- { id: "railway", related: [{ to: "render", w: 0.5 }] },
296
- { id: "render" },
297
- { id: "cloudflare", synonyms: ["cloudflare-workers"] },
298
- { id: "helm", parents: ["kubernetes"] },
299
- { id: "argocd", parents: ["kubernetes"] },
300
- { id: "serverless", parents: ["devops"] },
301
- // ── Databases & storage ─────────────────────────────────────────────────────
302
- { id: "postgresql", synonyms: ["postgres", "pg"], related: [{ to: "mysql", w: 0.45 }, { to: "sqlite", w: 0.4 }] },
303
- { id: "mysql", related: [{ to: "postgresql", w: 0.45 }] },
304
- { id: "sqlite" },
305
- { id: "mongodb", synonyms: ["mongo"] },
306
- { id: "redis", related: [{ to: "caching", w: 0.5 }] },
307
- { id: "elasticsearch", synonyms: ["elastic"], related: [{ to: "search", w: 0.55 }] },
308
- { id: "kafka", synonyms: ["apache-kafka"], related: [{ to: "rabbitmq", w: 0.5 }, { to: "message-queue", w: 0.55 }] },
309
- { id: "rabbitmq", related: [{ to: "message-queue", w: 0.55 }] },
310
- { id: "cassandra" },
311
- { id: "dynamodb", parents: ["aws"] },
312
- { id: "snowflake", parents: ["data-engineering"], related: [{ to: "clickhouse", w: 0.4 }] },
313
- { id: "clickhouse", parents: ["data-engineering"], related: [{ to: "duckdb", w: 0.35 }] },
314
- { id: "duckdb", parents: ["data-engineering"] },
315
- { id: "supabase", related: [{ to: "postgresql", w: 0.5 }, { to: "neon", w: 0.4 }] },
316
- { id: "planetscale", related: [{ to: "mysql", w: 0.5 }] },
317
- { id: "neon", related: [{ to: "postgresql", w: 0.5 }] },
318
- { id: "turso", related: [{ to: "sqlite", w: 0.5 }] },
319
- { id: "cockroachdb", related: [{ to: "postgresql", w: 0.45 }] },
320
- { id: "prisma", parents: ["backend"], synonyms: ["@prisma/client"], related: [{ to: "drizzle", w: 0.5 }, { to: "typeorm", w: 0.45 }, { to: "sequelize", w: 0.4 }] },
321
- { id: "drizzle", synonyms: ["drizzle-orm"], related: [{ to: "prisma", w: 0.5 }] },
322
- { id: "sequelize", related: [{ to: "typeorm", w: 0.4 }] },
323
- { id: "typeorm", related: [{ to: "prisma", w: 0.45 }] },
324
- { id: "sqlalchemy", parents: ["python"] },
325
- // ── Data engineering & ML ───────────────────────────────────────────────────
326
- { id: "data-engineering", synonyms: ["data-eng"], related: [{ to: "spark", w: 0.5 }, { to: "airflow", w: 0.5 }, { to: "dbt", w: 0.45 }] },
327
- { id: "spark", parents: ["data-engineering"], synonyms: ["apache-spark"] },
328
- { id: "airflow", parents: ["data-engineering"], synonyms: ["apache-airflow"] },
329
- { id: "dbt", parents: ["data-engineering"] },
330
- { 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 }] },
331
- { 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 }] },
332
- { id: "pytorch", parents: ["ml"], synonyms: ["torch"], related: [{ to: "tensorflow", w: 0.5 }] },
333
- { id: "tensorflow", parents: ["ml"], synonyms: ["keras", "tf-keras"] },
334
- { id: "pandas", parents: ["python"], related: [{ to: "numpy", w: 0.6 }, { to: "data-engineering", w: 0.45 }, { to: "spark", w: 0.4 }] },
335
- { id: "numpy", parents: ["python"] },
336
- { id: "scikit-learn", parents: ["ml"], synonyms: ["sklearn"] },
337
- { id: "jupyter", parents: ["python"] },
338
- { id: "langchain", parents: ["llm"], synonyms: ["llamaindex"] },
339
- { id: "huggingface", parents: ["ml"], synonyms: ["hugging-face"] },
340
- { id: "openai", parents: ["llm"] },
341
- { id: "anthropic", parents: ["llm"], synonyms: ["claude"] },
342
- { id: "rag", parents: ["llm"], synonyms: ["retrieval-augmented-generation"] },
343
- { id: "mlops", parents: ["ml"], related: [{ to: "devops", w: 0.4 }] },
344
- { id: "agents", parents: ["llm"], synonyms: ["agentic", "ai-agents", "multi-agent"], related: [{ to: "rag", w: 0.4 }] },
345
- { id: "mcp", parents: ["agents"], synonyms: ["model-context-protocol"], related: [{ to: "llm", w: 0.45 }] },
346
- { id: "inference", parents: ["ml"], synonyms: ["model-inference", "llm-inference", "model-serving"], related: [{ to: "mlops", w: 0.5 }, { to: "llm", w: 0.4 }] },
347
- { id: "embeddings", parents: ["ml"], synonyms: ["embedding", "vector-embeddings"], related: [{ to: "rag", w: 0.55 }, { to: "llm", w: 0.45 }] },
348
- { id: "prompt-engineering", parents: ["llm"], synonyms: ["prompting", "prompt"] },
349
- { id: "fine-tuning", parents: ["ml"], synonyms: ["finetuning", "fine-tune", "rlhf"], related: [{ to: "llm", w: 0.5 }] },
350
- { id: "computer-vision", parents: ["ml"], synonyms: ["image-recognition", "object-detection"] },
351
- { id: "recsys", parents: ["ml"], synonyms: ["recommender-systems", "recommendation-systems", "recommendation"] },
352
- // ── Mobile ──────────────────────────────────────────────────────────────────
353
- { id: "mobile", related: [{ to: "ios", w: 0.5 }, { to: "android", w: 0.5 }] },
354
- { id: "ios", parents: ["mobile", "swift"], related: [{ to: "android", w: 0.4 }] },
355
- { id: "android", parents: ["mobile"], related: [{ to: "kotlin", w: 0.4 }] },
356
- { id: "swiftui", parents: ["ios", "swift"] },
357
- { id: "react-native", parents: ["mobile", "react"], synonyms: ["reactnative"], related: [{ to: "flutter", w: 0.4 }, { to: "expo", w: 0.6 }] },
358
- { id: "flutter", parents: ["mobile", "dart"] },
359
- { id: "expo", parents: ["react-native"] },
360
- { id: "kotlin-multiplatform", parents: ["mobile", "kotlin"], synonyms: ["kmp"] },
361
- // ── Domains / capabilities ──────────────────────────────────────────────────
362
- { id: "frontend", related: [{ to: "react", w: 0.4 }, { to: "css", w: 0.3 }] },
363
- { id: "backend", related: [{ to: "api-design", w: 0.4 }, { to: "microservices", w: 0.4 }] },
364
- { id: "devops", related: [{ to: "kubernetes", w: 0.4 }, { to: "ci-cd", w: 0.4 }, { to: "docker", w: 0.4 }] },
365
- { id: "authentication", synonyms: ["auth", "jwt", "saml", "passport", "auth0", "clerk", "nextauth"], related: [{ to: "oauth", w: 0.6 }, { to: "security", w: 0.5 }] },
366
- { id: "oauth", parents: ["authentication"], synonyms: ["oauth2", "oidc"], related: [{ to: "security", w: 0.4 }] },
367
- { id: "security", related: [{ to: "authentication", w: 0.5 }] },
368
- { id: "payments", synonyms: ["stripe", "braintree", "paddle", "lemonsqueezy", "@stripe/stripe-js"], related: [{ to: "billing", w: 0.6 }] },
369
- { id: "billing", synonyms: ["recurly", "chargebee"] },
370
- { id: "api-design", synonyms: ["rest", "restful", "rest-api"], related: [{ to: "graphql", w: 0.4 }, { to: "grpc", w: 0.4 }, { to: "backend", w: 0.4 }] },
371
- { id: "graphql", synonyms: ["gql"], related: [{ to: "trpc", w: 0.4 }] },
372
- { id: "trpc", related: [{ to: "graphql", w: 0.4 }] },
373
- { id: "grpc", synonyms: ["grpc-web"], related: [{ to: "microservices", w: 0.3 }] },
374
- { id: "microservices" },
375
- { id: "websockets", synonyms: ["ws", "socket.io"], related: [{ to: "realtime", w: 0.6 }] },
376
- { id: "realtime", synonyms: ["real-time"] },
377
- { id: "message-queue", synonyms: ["mq"] },
378
- { id: "caching", synonyms: ["cache"] },
379
- { id: "search", synonyms: ["full-text-search"] },
380
- { id: "observability", synonyms: ["o11y"], related: [{ to: "monitoring", w: 0.6 }] },
381
- { id: "monitoring", related: [{ to: "prometheus", w: 0.4 }] },
382
- { id: "testing", related: [{ to: "unit-testing", w: 0.5 }, { to: "e2e-testing", w: 0.5 }] },
383
- { id: "unit-testing", parents: ["testing"] },
384
- { id: "e2e-testing", parents: ["testing"], synonyms: ["e2e", "end-to-end-testing"] },
385
- { id: "jest", parents: ["testing"], related: [{ to: "vitest", w: 0.6 }, { to: "mocha", w: 0.5 }] },
386
- { id: "vitest", parents: ["testing"], related: [{ to: "jest", w: 0.6 }] },
387
- { id: "playwright", parents: ["e2e-testing"], related: [{ to: "cypress", w: 0.6 }] },
388
- { id: "cypress", parents: ["e2e-testing"] },
389
- { id: "mocha", parents: ["testing"] },
390
- { id: "pytest", parents: ["testing", "python"] },
391
- { id: "accessibility", synonyms: ["a11y"] },
392
- { id: "seo" },
393
- { id: "performance", synonyms: ["perf", "web-performance"] }
394
- ];
395
-
396
- // ../../packages/core/src/vocab/closure.ts
397
- var PARENT_UP = 0.6;
398
- var PARENT_DOWN = 0.35;
399
- var DECAY_FLOOR = 0.25;
400
- function round3(n) {
401
- return Math.round(n * 1e3) / 1e3;
1172
+ function statusFilePath() {
1173
+ return STATUS_FILE;
402
1174
  }
403
- function validateGraph(nodes) {
404
- const ids = /* @__PURE__ */ new Set();
405
- for (const n of nodes) {
406
- if (ids.has(n.id)) throw new Error(`vocab: duplicate id "${n.id}"`);
407
- ids.add(n.id);
1175
+ function atomicWriteJson(path, obj) {
1176
+ mkdirSync2(dirname(path), { recursive: true });
1177
+ const tmp = `${path}.tmp-${process.pid}`;
1178
+ writeFileSync2(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
1179
+ renameSync2(tmp, path);
1180
+ }
1181
+ function sleepMs(ms) {
1182
+ try {
1183
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1184
+ } catch {
1185
+ const end = Date.now() + ms;
1186
+ while (Date.now() < end) {
1187
+ }
408
1188
  }
409
- const seenAlias = /* @__PURE__ */ new Map();
410
- for (const n of nodes) {
411
- for (const p of n.parents ?? []) {
412
- if (p === n.id) throw new Error(`vocab: "${n.id}" lists itself as a parent`);
413
- if (!ids.has(p)) throw new Error(`vocab: "${n.id}" parent "${p}" is not a defined id`);
1189
+ }
1190
+ function withLock(fn) {
1191
+ const deadline = Date.now() + 2e3;
1192
+ for (; ; ) {
1193
+ let fd;
1194
+ try {
1195
+ mkdirSync2(dirname(LOCK_FILE), { recursive: true });
1196
+ fd = openSync(LOCK_FILE, "wx");
1197
+ } catch (err) {
1198
+ if (err && err.code === "EEXIST") {
1199
+ if (Date.now() > deadline) {
1200
+ try {
1201
+ unlinkSync(LOCK_FILE);
1202
+ } catch {
1203
+ }
1204
+ continue;
1205
+ }
1206
+ sleepMs(5);
1207
+ continue;
1208
+ }
1209
+ throw err;
414
1210
  }
415
- for (const e of n.related ?? []) {
416
- if (e.to === n.id) throw new Error(`vocab: "${n.id}" relates to itself`);
417
- if (!ids.has(e.to)) throw new Error(`vocab: "${n.id}" related "${e.to}" is not a defined id`);
418
- if (!(e.w > 0 && e.w <= 1)) throw new Error(`vocab: "${n.id}"\u2192"${e.to}" weight ${e.w} out of (0,1]`);
1211
+ try {
1212
+ return fn();
1213
+ } finally {
1214
+ try {
1215
+ closeSync(fd);
1216
+ } catch {
1217
+ }
1218
+ try {
1219
+ unlinkSync(LOCK_FILE);
1220
+ } catch {
1221
+ }
419
1222
  }
420
- for (const s of n.synonyms ?? []) {
421
- const alias = s.toLowerCase();
422
- if (ids.has(alias)) throw new Error(`vocab: synonym "${alias}" collides with a canonical id`);
423
- const prev = seenAlias.get(alias);
424
- if (prev && prev !== n.id) throw new Error(`vocab: synonym "${alias}" maps to both "${prev}" and "${n.id}"`);
425
- seenAlias.set(alias, n.id);
1223
+ }
1224
+ }
1225
+ function readStatusMap() {
1226
+ if (!existsSync2(STATUS_FILE)) return {};
1227
+ let raw;
1228
+ try {
1229
+ raw = readFileSync3(STATUS_FILE, "utf8");
1230
+ } catch {
1231
+ return {};
1232
+ }
1233
+ try {
1234
+ const parsed = JSON.parse(raw);
1235
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
1236
+ throw new Error("status store is not an object");
1237
+ } catch {
1238
+ try {
1239
+ copyFileSync(STATUS_FILE, BAK_FILE);
1240
+ } catch {
1241
+ }
1242
+ return {};
1243
+ }
1244
+ }
1245
+ function markStatus(id, status) {
1246
+ return withLock(() => {
1247
+ const current = readStatusMap();
1248
+ const next = status === "claimed" ? { ...current, [id]: { ...current[id], status: "claimed", markedAt: (/* @__PURE__ */ new Date()).toISOString() } } : setStatus(current, id, status);
1249
+ atomicWriteJson(STATUS_FILE, next);
1250
+ return next[id];
1251
+ });
1252
+ }
1253
+ function markClicked(id) {
1254
+ return withLock(() => {
1255
+ const current = readStatusMap();
1256
+ const next = recordClick(current, id);
1257
+ atomicWriteJson(STATUS_FILE, next);
1258
+ return next[id];
1259
+ });
1260
+ }
1261
+ var TERMINALHIRE_DIR2, STATUS_FILE, LOCK_FILE, BAK_FILE;
1262
+ var init_job_status_store = __esm({
1263
+ "bin/job-status-store.js"() {
1264
+ "use strict";
1265
+ init_src();
1266
+ TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join3(homedir2(), ".terminalhire");
1267
+ STATUS_FILE = join3(TERMINALHIRE_DIR2, "job-status.json");
1268
+ LOCK_FILE = `${STATUS_FILE}.lock`;
1269
+ BAK_FILE = `${STATUS_FILE}.bak`;
1270
+ }
1271
+ });
1272
+
1273
+ // ../../node_modules/keytar/build/Release/keytar.node
1274
+ var keytar_default;
1275
+ var init_keytar = __esm({
1276
+ "../../node_modules/keytar/build/Release/keytar.node"() {
1277
+ keytar_default = "../keytar-KOAAH267.node";
1278
+ }
1279
+ });
1280
+
1281
+ // node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node
1282
+ var require_keytar = __commonJS({
1283
+ "node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node"(exports, module) {
1284
+ "use strict";
1285
+ init_keytar();
1286
+ try {
1287
+ module.exports = __require(keytar_default);
1288
+ } catch {
1289
+ }
1290
+ }
1291
+ });
1292
+
1293
+ // ../../node_modules/keytar/lib/keytar.js
1294
+ var require_keytar2 = __commonJS({
1295
+ "../../node_modules/keytar/lib/keytar.js"(exports, module) {
1296
+ "use strict";
1297
+ var keytar = require_keytar();
1298
+ function checkRequired(val, name) {
1299
+ if (!val || val.length <= 0) {
1300
+ throw new Error(name + " is required.");
1301
+ }
1302
+ }
1303
+ module.exports = {
1304
+ getPassword: function(service, account) {
1305
+ checkRequired(service, "Service");
1306
+ checkRequired(account, "Account");
1307
+ return keytar.getPassword(service, account);
1308
+ },
1309
+ setPassword: function(service, account, password) {
1310
+ checkRequired(service, "Service");
1311
+ checkRequired(account, "Account");
1312
+ checkRequired(password, "Password");
1313
+ return keytar.setPassword(service, account, password);
1314
+ },
1315
+ deletePassword: function(service, account) {
1316
+ checkRequired(service, "Service");
1317
+ checkRequired(account, "Account");
1318
+ return keytar.deletePassword(service, account);
1319
+ },
1320
+ findPassword: function(service) {
1321
+ checkRequired(service, "Service");
1322
+ return keytar.findPassword(service);
1323
+ },
1324
+ findCredentials: function(service) {
1325
+ checkRequired(service, "Service");
1326
+ return keytar.findCredentials(service);
1327
+ }
1328
+ };
1329
+ }
1330
+ });
1331
+
1332
+ // src/profile.ts
1333
+ var profile_exports = {};
1334
+ __export(profile_exports, {
1335
+ accumulateGitHubTags: () => accumulateGitHubTags,
1336
+ accumulateSession: () => accumulateSession,
1337
+ accumulateTags: () => accumulateTags,
1338
+ addSavedJob: () => addSavedJob,
1339
+ deleteProfile: () => deleteProfile,
1340
+ listSavedJobs: () => listSavedJobs,
1341
+ profileToFingerprint: () => profileToFingerprint,
1342
+ readProfile: () => readProfile,
1343
+ removeSavedJob: () => removeSavedJob,
1344
+ writeProfile: () => writeProfile
1345
+ });
1346
+ import {
1347
+ createCipheriv,
1348
+ createDecipheriv,
1349
+ randomBytes as randomBytes2
1350
+ } from "crypto";
1351
+ import {
1352
+ readFileSync as readFileSync4,
1353
+ writeFileSync as writeFileSync3,
1354
+ mkdirSync as mkdirSync3,
1355
+ existsSync as existsSync3
1356
+ } from "fs";
1357
+ import { join as join4 } from "path";
1358
+ import { homedir as homedir3 } from "os";
1359
+ async function loadKey() {
1360
+ try {
1361
+ const kt = await Promise.resolve().then(() => __toESM(require_keytar2(), 1));
1362
+ const stored = await kt.getPassword("terminalhire", "profile-key");
1363
+ if (stored) {
1364
+ return Buffer.from(stored, "hex");
426
1365
  }
1366
+ const key2 = randomBytes2(KEY_BYTES);
1367
+ await kt.setPassword("terminalhire", "profile-key", key2.toString("hex"));
1368
+ return key2;
1369
+ } catch {
427
1370
  }
428
- const visiting = /* @__PURE__ */ new Set();
429
- const done = /* @__PURE__ */ new Set();
430
- const parentMap = new Map(nodes.map((n) => [n.id, n.parents ?? []]));
431
- const walk = (id, path) => {
432
- if (done.has(id)) return;
433
- if (visiting.has(id)) throw new Error(`vocab: parent cycle ${[...path, id].join(" \u2192 ")}`);
434
- visiting.add(id);
435
- for (const p of parentMap.get(id) ?? []) walk(p, [...path, id]);
436
- visiting.delete(id);
437
- done.add(id);
1371
+ mkdirSync3(TERMINALHIRE_DIR3, { recursive: true });
1372
+ if (existsSync3(KEY_FILE)) {
1373
+ return Buffer.from(readFileSync4(KEY_FILE, "utf8").trim(), "hex");
1374
+ }
1375
+ const key = randomBytes2(KEY_BYTES);
1376
+ writeFileSync3(KEY_FILE, key.toString("hex"), { mode: 384, encoding: "utf8" });
1377
+ return key;
1378
+ }
1379
+ function encrypt(plaintext, key) {
1380
+ const iv = randomBytes2(IV_BYTES);
1381
+ const cipher = createCipheriv(ALGO, key, iv);
1382
+ const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
1383
+ const tag = cipher.getAuthTag();
1384
+ return {
1385
+ iv: iv.toString("hex"),
1386
+ tag: tag.toString("hex"),
1387
+ ciphertext: ct.toString("hex")
438
1388
  };
439
- for (const n of nodes) walk(n.id, []);
440
1389
  }
441
- function buildAdjacency(nodes) {
442
- const adj = /* @__PURE__ */ new Map();
443
- const add = (from, to, w) => {
444
- let m = adj.get(from);
445
- if (!m) adj.set(from, m = /* @__PURE__ */ new Map());
446
- if (w > (m.get(to) ?? 0)) m.set(to, w);
1390
+ function decrypt(blob, key) {
1391
+ const decipher = createDecipheriv(
1392
+ ALGO,
1393
+ key,
1394
+ Buffer.from(blob.iv, "hex")
1395
+ );
1396
+ decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
1397
+ const plain = Buffer.concat([
1398
+ decipher.update(Buffer.from(blob.ciphertext, "hex")),
1399
+ decipher.final()
1400
+ ]);
1401
+ return plain.toString("utf8");
1402
+ }
1403
+ function blankProfile() {
1404
+ return {
1405
+ version: 3,
1406
+ skillTags: [],
1407
+ tagWeights: {},
1408
+ hasEmployerSessions: false,
1409
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
447
1410
  };
448
- for (const n of nodes) {
449
- for (const p of n.parents ?? []) {
450
- add(n.id, p, PARENT_UP);
451
- add(p, n.id, PARENT_DOWN);
452
- }
453
- for (const e of n.related ?? []) {
454
- add(n.id, e.to, e.w);
455
- add(e.to, n.id, e.w);
1411
+ }
1412
+ function recencyDecay(lastSeen) {
1413
+ const ageMs = Date.now() - new Date(lastSeen).getTime();
1414
+ return Math.pow(0.5, ageMs / DECAY_HALF_LIFE_MS);
1415
+ }
1416
+ function tagScore(w) {
1417
+ return w.count * recencyDecay(w.lastSeen);
1418
+ }
1419
+ function deriveSkillTags(tagWeights) {
1420
+ return Object.entries(tagWeights).filter(([, w]) => w.count >= 1).sort(([, a], [, b]) => tagScore(b) - tagScore(a)).map(([tag]) => tag);
1421
+ }
1422
+ function migrateTagWeights(profile) {
1423
+ if (!profile.tagWeights) {
1424
+ profile.tagWeights = {};
1425
+ }
1426
+ const seed = profile.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
1427
+ for (const tag of profile.skillTags) {
1428
+ if (!profile.tagWeights[tag]) {
1429
+ profile.tagWeights[tag] = { count: 1, firstSeen: seed, lastSeen: seed, sessions: 1 };
456
1430
  }
457
1431
  }
458
- return adj;
459
1432
  }
460
- function closureFrom(source, adj) {
461
- const best = /* @__PURE__ */ new Map();
462
- for (const [t, w] of adj.get(source) ?? []) {
463
- if (w >= DECAY_FLOOR) best.set(t, { w: round3(w), via: t });
1433
+ async function readProfile() {
1434
+ if (!existsSync3(PROFILE_FILE)) return blankProfile();
1435
+ try {
1436
+ const key = await loadKey();
1437
+ const raw = readFileSync4(PROFILE_FILE, "utf8");
1438
+ const blob = JSON.parse(raw);
1439
+ const plaintext = decrypt(blob, key);
1440
+ const parsed = JSON.parse(plaintext);
1441
+ migrateTagWeights(parsed);
1442
+ return parsed;
1443
+ } catch {
1444
+ return blankProfile();
464
1445
  }
465
- const settled = /* @__PURE__ */ new Set([source]);
466
- while (true) {
467
- let u;
468
- let uw = 0;
469
- for (const [t, e] of best) {
470
- if (!settled.has(t) && e.w > uw) {
471
- u = t;
472
- uw = e.w;
473
- }
1446
+ }
1447
+ async function writeProfile(profile) {
1448
+ mkdirSync3(TERMINALHIRE_DIR3, { recursive: true });
1449
+ const key = await loadKey();
1450
+ profile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1451
+ profile.skillTags = deriveSkillTags(profile.tagWeights);
1452
+ const blob = encrypt(JSON.stringify(profile), key);
1453
+ writeFileSync3(PROFILE_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
1454
+ }
1455
+ function accumulateSession(profile, tags, isEmployerContext, inferredSeniority, seniorityIsAuthoritative = false) {
1456
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1457
+ let filtered = normalize(tags);
1458
+ if (isEmployerContext) {
1459
+ filtered = filtered.filter((t) => LANGUAGE_TAGS.has(t));
1460
+ profile.hasEmployerSessions = true;
1461
+ }
1462
+ for (const tag of filtered) {
1463
+ const existing = profile.tagWeights[tag];
1464
+ if (existing) {
1465
+ existing.count += 1;
1466
+ existing.sessions += 1;
1467
+ existing.lastSeen = now;
1468
+ } else {
1469
+ profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
474
1470
  }
475
- if (!u) break;
476
- settled.add(u);
477
- const via = best.get(u).via;
478
- for (const [t, we] of adj.get(u) ?? []) {
479
- if (settled.has(t)) continue;
480
- const cand = round3(uw * we);
481
- if (cand >= DECAY_FLOOR && cand > (best.get(t)?.w ?? 0)) {
482
- best.set(t, { w: cand, via });
483
- }
1471
+ }
1472
+ if (inferredSeniority && !isEmployerContext) {
1473
+ if (seniorityIsAuthoritative || !profile.github) {
1474
+ profile.seniority = inferredSeniority;
484
1475
  }
485
1476
  }
486
- best.delete(source);
487
- return best;
488
1477
  }
489
- function buildGraph(nodes) {
490
- validateGraph(nodes);
491
- const ids = new Set(nodes.map((n) => n.id));
492
- const synonyms = /* @__PURE__ */ new Map();
493
- for (const n of nodes) {
494
- for (const s of n.synonyms ?? []) synonyms.set(s.toLowerCase(), n.id);
495
- }
496
- const adj = buildAdjacency(nodes);
497
- const closure = /* @__PURE__ */ new Map();
498
- for (const n of nodes) closure.set(n.id, closureFrom(n.id, adj));
499
- return { ids, synonyms, closure };
1478
+ async function accumulateTags(rawTokens, isEmployerContext, inferredSeniority) {
1479
+ const profile = await readProfile();
1480
+ accumulateSession(profile, rawTokens, isEmployerContext, inferredSeniority);
1481
+ await writeProfile(profile);
500
1482
  }
501
-
502
- // ../../packages/core/src/vocab/extract.ts
503
- var SOFT_DOMAIN = /* @__PURE__ */ new Set([
504
- "frontend",
505
- "backend",
506
- "devops",
507
- "security",
508
- "payments",
509
- "billing",
510
- "microservices",
511
- "caching",
512
- "search",
513
- "observability",
514
- "monitoring",
515
- "testing",
516
- "accessibility",
517
- "seo",
518
- "performance",
519
- "realtime",
520
- "authentication",
521
- "api-design"
522
- ]);
523
- var SYNONYM_ONLY = /* @__PURE__ */ new Set(["performance", "security", "seo"]);
524
- for (const id of SYNONYM_ONLY) {
525
- if (!SOFT_DOMAIN.has(id)) throw new Error(`extract: SYNONYM_ONLY "${id}" not in SOFT_DOMAIN`);
1483
+ function accumulateGitHubTags(profile, tags, inferredSeniority) {
1484
+ accumulateSession(
1485
+ profile,
1486
+ tags,
1487
+ /* isEmployerContext */
1488
+ false,
1489
+ inferredSeniority,
1490
+ true
1491
+ );
526
1492
  }
527
-
528
- // ../../packages/core/src/vocab/index.ts
529
- var GRAPH = buildGraph(VOCAB_NODES);
530
- var VOCABULARY = [...GRAPH.ids];
531
- var SYNONYMS = Object.fromEntries(GRAPH.synonyms);
532
-
533
- // ../../packages/core/src/feeds/bounty-gate.ts
534
- var BOUNTY_REPO_DENYLIST = [
535
- "SecureBananaLabs/bug-bounty",
536
- // Meta-farm: a bounty PLATFORM whose own issues are an assignment-gated
537
- // contributor queue ("please assign me, my chief") — an unsolicited PR won't
538
- // merge, so it's not a real claimable bounty. Not structurally derivable from
539
- // any fetched field, so it's a manual entry (also dropped from the allowlist).
540
- "boundlessfi/boundless"
541
- ];
542
- var DENYLIST_LC = new Set(BOUNTY_REPO_DENYLIST.map((r) => r.toLowerCase()));
543
- var AI_BAN_DENYLIST = [
544
- // Gentoo Council voted 6-0 (2024-04-14) to ban AI/ML-generated contributions
545
- // project-wide. https://wiki.gentoo.org/wiki/Project:Council/AI_policy
546
- "gentoo",
547
- // NetBSD Commit Guidelines: code generated by an LLM/similar technology is
548
- // "presumed to be tainted code, and must not be committed without prior
549
- // written approval by core". https://www.netbsd.org/developers/commit-guidelines.html
550
- "NetBSD"
551
- // NOT listed (checked, deliberately excluded): QEMU's blanket AI-contribution
552
- // ban is IN FLUX as of 2026-05 — a patch replacing it with a disclosure-based
553
- // policy ("AI-used-for:" tag) was posted to qemu-devel and appears to have
554
- // developer buy-in, so it is no longer a clean/current ban to key a hard
555
- // supply-side drop on. https://www.theregister.com/ai-and-ml/2026/05/29/qemu-mulls-relaxing-ai-contribution-ban/
556
- ];
557
- var AI_BAN_LC = new Set(AI_BAN_DENYLIST.map((g) => g.toLowerCase()));
558
-
559
- // ../../packages/core/src/github.ts
560
- var RESUME_DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
561
-
562
- // ../../packages/core/src/feeds/index.ts
563
- var GREENHOUSE_SLUGS_BY_TIER = {
564
- bigco: [
565
- "stripe",
566
- "anthropic",
567
- "figma",
568
- "discord",
569
- "brex",
570
- "mercury",
571
- "plaid",
572
- "gusto",
573
- "scale",
574
- "databricks",
575
- "coinbase",
576
- "robinhood",
577
- "doordash",
578
- "airbnb",
579
- "dropbox",
580
- "datadog",
581
- "cloudflare",
582
- "reddit",
583
- "lyft",
584
- "instacart"
585
- ],
586
- scaleup: [
587
- "samsara",
588
- "verkada",
589
- "affirm",
590
- "gitlab",
591
- "asana",
592
- "flexport",
593
- "faire",
594
- "twitch",
595
- "airtable",
596
- "retool"
597
- ],
598
- startup: [
599
- "watershed"
600
- ]
601
- };
602
- var ASHBY_SLUGS_BY_TIER = {
603
- bigco: [
604
- "openai"
605
- ],
606
- scaleup: [
607
- "harvey",
608
- "elevenlabs",
609
- "notion",
610
- "sierra",
611
- "cohere",
612
- "ramp",
613
- "vanta",
614
- "decagon",
615
- "cursor",
616
- "replit",
617
- "perplexity",
618
- "baseten",
619
- "drata",
620
- "writer",
621
- "temporal",
622
- "supabase"
623
- ],
624
- startup: [
625
- "suno",
626
- "attio",
627
- "modal",
628
- "workos",
629
- "linear",
630
- "render",
631
- "warp",
632
- "plain",
633
- "posthog",
634
- "pylon",
635
- "resend",
636
- "langfuse",
637
- "railway",
638
- "mintlify",
639
- "neon",
640
- "browserbase",
641
- "knock",
642
- "speakeasy",
643
- "stytch",
644
- "runway",
645
- "doppler",
646
- "inngest",
647
- "hightouch",
648
- "zed"
649
- ]
650
- };
651
- var LEVER_SLUGS_BY_TIER = {
652
- bigco: [
653
- "palantir",
654
- "spotify"
655
- ],
656
- scaleup: [
657
- "mistral",
658
- "ro",
659
- "secureframe"
660
- ],
661
- startup: [
662
- "anyscale"
663
- ]
664
- };
665
- function flattenTiers(t) {
666
- return [.../* @__PURE__ */ new Set([...t.bigco, ...t.scaleup, ...t.startup])];
1493
+ async function listSavedJobs() {
1494
+ const profile = await readProfile();
1495
+ return profile.savedJobs ?? [];
667
1496
  }
668
- var DEFAULT_GREENHOUSE_SLUGS = flattenTiers(GREENHOUSE_SLUGS_BY_TIER);
669
- var DEFAULT_ASHBY_SLUGS = flattenTiers(ASHBY_SLUGS_BY_TIER);
670
- var DEFAULT_LEVER_SLUGS = flattenTiers(LEVER_SLUGS_BY_TIER);
671
-
672
- // ../../packages/core/src/partners.ts
673
- import { readFileSync } from "fs";
674
- import { join } from "path";
675
- import { fileURLToPath } from "url";
676
- var EXAMPLE_BUYER = {
677
- id: "northstar",
678
- legalName: "Northstar Talent Partners",
679
- matchCriteria: { roleTypes: ["full_time"] }
680
- };
681
- var BUYER_REGISTRY = {
682
- [EXAMPLE_BUYER.id]: EXAMPLE_BUYER
683
- };
684
-
685
- // ../../packages/core/src/intro.ts
686
- var INTRO_ALLOWED_FIELDS = [
687
- "requesterLogin",
688
- "requesterDisplayName",
689
- "requesterContact",
690
- "note",
691
- "targetLogin"
692
- ];
693
- var INTRO_ALLOWED_SET = new Set(INTRO_ALLOWED_FIELDS);
694
- var INTRO_PENDING_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
695
- var INTRO_ACCEPTED_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
1497
+ async function addSavedJob(job) {
1498
+ const profile = await readProfile();
1499
+ const existing = profile.savedJobs ?? [];
1500
+ const filtered = existing.filter((j) => j.id !== job.id);
1501
+ profile.savedJobs = [...filtered, { ...job, savedAt: (/* @__PURE__ */ new Date()).toISOString() }];
1502
+ await writeProfile(profile);
1503
+ }
1504
+ async function removeSavedJob(id) {
1505
+ const profile = await readProfile();
1506
+ const existing = profile.savedJobs ?? [];
1507
+ const filtered = existing.filter((j) => j.id !== id);
1508
+ if (filtered.length === existing.length) return false;
1509
+ profile.savedJobs = filtered;
1510
+ await writeProfile(profile);
1511
+ return true;
1512
+ }
1513
+ async function deleteProfile() {
1514
+ const { rmSync: rmSync2 } = await import("fs");
1515
+ try {
1516
+ rmSync2(PROFILE_FILE);
1517
+ } catch {
1518
+ }
1519
+ try {
1520
+ rmSync2(KEY_FILE);
1521
+ } catch {
1522
+ }
1523
+ }
1524
+ function profileToFingerprint(profile) {
1525
+ 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);
1526
+ const skillTags = rankedTags.length > 0 ? rankedTags : profile.skillTags;
1527
+ return {
1528
+ skillTags,
1529
+ seniorityBand: profile.seniority,
1530
+ prefs: {
1531
+ roleTypes: profile.roleTypes,
1532
+ remoteOnly: profile.remoteOnly,
1533
+ compFloorUsd: profile.compFloorUsd
1534
+ }
1535
+ };
1536
+ }
1537
+ var TERMINALHIRE_DIR3, PROFILE_FILE, KEY_FILE, ALGO, KEY_BYTES, IV_BYTES, DECAY_HALF_LIFE_MS, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
1538
+ var init_profile = __esm({
1539
+ "src/profile.ts"() {
1540
+ "use strict";
1541
+ init_src();
1542
+ TERMINALHIRE_DIR3 = join4(homedir3(), ".terminalhire");
1543
+ PROFILE_FILE = join4(TERMINALHIRE_DIR3, "profile.enc");
1544
+ KEY_FILE = join4(TERMINALHIRE_DIR3, "key");
1545
+ ALGO = "aes-256-gcm";
1546
+ KEY_BYTES = 32;
1547
+ IV_BYTES = 12;
1548
+ DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
1549
+ LANGUAGE_TAGS = /* @__PURE__ */ new Set([
1550
+ "typescript",
1551
+ "javascript",
1552
+ "python",
1553
+ "go",
1554
+ "rust",
1555
+ "java",
1556
+ "ruby",
1557
+ "elixir",
1558
+ "scala",
1559
+ "kotlin",
1560
+ "swift",
1561
+ "cpp",
1562
+ "csharp",
1563
+ "php",
1564
+ "haskell",
1565
+ "clojure",
1566
+ "r"
1567
+ ]);
1568
+ MIN_FINGERPRINT_SCORE = 0.05;
1569
+ }
1570
+ });
696
1571
 
697
- // ../../packages/core/src/chatCrypto.ts
698
- import { hkdfSync, createHash, randomBytes } from "crypto";
699
- var KDF_INFO = Buffer.from("terminalhire-chat-v1");
1572
+ // bin/jpi-claim.js
1573
+ init_src();
1574
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync4, rmSync } from "fs";
1575
+ import { join as join5 } from "path";
1576
+ import { homedir as homedir4, hostname as osHostname } from "os";
1577
+ import { execFile } from "child_process";
1578
+ import { promisify } from "util";
1579
+ import { createInterface } from "readline";
700
1580
 
701
- // ../../packages/core/src/short-token.ts
702
- import { createHash as createHash2 } from "crypto";
703
- function opportunityShortToken(id) {
704
- return createHash2("sha256").update(id, "utf8").digest("base64url").slice(0, 8);
1581
+ // src/open-url.js
1582
+ import { spawn } from "child_process";
1583
+ function openInBrowser(url) {
1584
+ let cmd;
1585
+ let args;
1586
+ if (process.platform === "darwin") {
1587
+ cmd = "open";
1588
+ args = [url];
1589
+ } else if (process.platform === "win32") {
1590
+ cmd = "cmd";
1591
+ args = ["/c", "start", "", url];
1592
+ } else {
1593
+ cmd = "xdg-open";
1594
+ args = [url];
1595
+ }
1596
+ try {
1597
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
1598
+ child.on("error", () => {
1599
+ });
1600
+ child.unref();
1601
+ } catch {
1602
+ }
705
1603
  }
706
1604
 
707
1605
  // bin/jpi-claim.js
708
- var TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join3(homedir2(), ".terminalhire");
709
- var INDEX_CACHE_FILE = join3(TERMINALHIRE_DIR2, "index-cache.json");
1606
+ var TERMINALHIRE_DIR4 = process.env.TERMINALHIRE_DIR || join5(homedir4(), ".terminalhire");
1607
+ var INDEX_CACHE_FILE = join5(TERMINALHIRE_DIR4, "index-cache.json");
1608
+ var CLAIM_PUSH_MARKER = join5(TERMINALHIRE_DIR4, "claim-push.json");
1609
+ var API_URL = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
1610
+ var CLAIM_SYNC_BASE = "https://terminalhire.com";
1611
+ var CLAIM_CONSENT_VERSION = 1;
1612
+ var CLAIM_POLL_INTERVAL_MS = 2e3;
1613
+ var CLAIM_POLL_TIMEOUT_MS = 10 * 60 * 1e3;
710
1614
  var GH_API2 = "https://api.github.com";
711
1615
  var GH_HEADERS2 = { "User-Agent": "terminalhire-claim", Accept: "application/vnd.github+json" };
712
1616
  var AI_DISCLOSURE_NOTE = "---\nThis contribution was developed with AI assistance via [terminalhire](https://terminalhire.com). The author has reviewed the change and takes responsibility for its content.";
@@ -773,8 +1677,8 @@ function parseRepoFromRemote(url) {
773
1677
  return m ? `${m[1]}/${m[2]}` : null;
774
1678
  }
775
1679
  function readClaimablePool() {
776
- if (!existsSync2(INDEX_CACHE_FILE)) return [];
777
- const entry = JSON.parse(readFileSync3(INDEX_CACHE_FILE, "utf8"));
1680
+ if (!existsSync4(INDEX_CACHE_FILE)) return [];
1681
+ const entry = JSON.parse(readFileSync5(INDEX_CACHE_FILE, "utf8"));
778
1682
  const bounties = (entry?.index?.jobs ?? []).filter((j) => j.source === "bounty");
779
1683
  const contributions = (entry?.index?.contribute ?? []).filter((j) => j.source === "contribute");
780
1684
  return [...bounties, ...contributions];
@@ -791,11 +1695,33 @@ function looksLikeShortRef(arg) {
791
1695
  }
792
1696
  function findClaimableByShortRef(ref) {
793
1697
  try {
794
- return readClaimablePool().find((j) => opportunityShortToken(j.id) === ref) ?? null;
1698
+ return findByShortRefInPool(readClaimablePool(), ref);
795
1699
  } catch {
796
1700
  return null;
797
1701
  }
798
1702
  }
1703
+ function findByShortRefInPool(pool, ref) {
1704
+ return pool.find((j) => opportunityShortToken(j.id) === ref) ?? null;
1705
+ }
1706
+ async function fetchFreshClaimablePool() {
1707
+ let index;
1708
+ for (let attempt = 1; ; attempt++) {
1709
+ try {
1710
+ const res = await fetch(`${API_URL}/api/index`, {
1711
+ signal: AbortSignal.timeout(15e3),
1712
+ headers: { Accept: "application/json" }
1713
+ });
1714
+ if (!res.ok) return null;
1715
+ index = await res.json();
1716
+ break;
1717
+ } catch {
1718
+ if (attempt >= 2) return null;
1719
+ }
1720
+ }
1721
+ const bounties = (index?.jobs ?? []).filter((j) => j.source === "bounty");
1722
+ const contributions = (index?.contribute ?? []).filter((j) => j.source === "contribute");
1723
+ return [...bounties, ...contributions];
1724
+ }
799
1725
  function extractClaimableFields(job) {
800
1726
  if (job.source === "contribute") {
801
1727
  const c = job.contribution ?? {};
@@ -805,7 +1731,11 @@ function extractClaimableFields(job) {
805
1731
  repoFullName: c.repoFullName ?? job.company ?? "",
806
1732
  issueUrl: c.issueUrl ?? job.url ?? "",
807
1733
  amountUSD: null,
808
- source: "contribute"
1734
+ source: "contribute",
1735
+ // Fix 2: the aggregator proved 0 open PRs at discovery (it only admits an
1736
+ // uncontested issue). Carry it through so resolveBounty can prefer it over
1737
+ // a live re-count that degrades to null under a rate limit / >100-PR page.
1738
+ openPRsAtDiscovery: c.openPRsAtDiscovery
809
1739
  };
810
1740
  }
811
1741
  const b = job.bounty ?? {};
@@ -815,7 +1745,9 @@ function extractClaimableFields(job) {
815
1745
  repoFullName: b.repoFullName ?? job.company ?? "",
816
1746
  issueUrl: b.claimUrl ?? job.url ?? "",
817
1747
  amountUSD: b.amountUSD ?? null,
818
- source: "bounty"
1748
+ source: "bounty",
1749
+ // Bounties carry no discovery-time PR proof — always live-count (unchanged).
1750
+ openPRsAtDiscovery: void 0
819
1751
  };
820
1752
  }
821
1753
  function parseGitHubUrl(url) {
@@ -849,7 +1781,18 @@ async function fetchIssue(repoFullName, issueNumber) {
849
1781
  if (!res.ok) return null;
850
1782
  const issue = await res.json();
851
1783
  const state = issue.state === "open" ? "open" : issue.state === "closed" ? "closed" : null;
852
- return { state, title: typeof issue.title === "string" ? issue.title : null };
1784
+ const logins = /* @__PURE__ */ new Set();
1785
+ if (issue.assignee && typeof issue.assignee.login === "string") logins.add(issue.assignee.login);
1786
+ if (Array.isArray(issue.assignees)) {
1787
+ for (const a of issue.assignees) {
1788
+ if (a && typeof a.login === "string") logins.add(a.login);
1789
+ }
1790
+ }
1791
+ return {
1792
+ state,
1793
+ title: typeof issue.title === "string" ? issue.title : null,
1794
+ assignees: [...logins]
1795
+ };
853
1796
  } catch {
854
1797
  return null;
855
1798
  }
@@ -881,10 +1824,16 @@ function printMetric(rate) {
881
1824
  \u{1F4CA} Accepted-PR rate: ${rate.merged}/${rate.total} claims merged (${pct}%)`);
882
1825
  }
883
1826
  async function resolveBounty(arg) {
884
- let bountyId, title, repoFullName, issueUrl, amountUSD, source;
885
- const job = findClaimableInCache(arg) ?? (looksLikeShortRef(arg) ? findClaimableByShortRef(arg) : null);
1827
+ let bountyId, title, repoFullName, issueUrl, amountUSD, source, openPRsAtDiscovery, indexNativeId;
1828
+ let job = findClaimableInCache(arg) ?? (looksLikeShortRef(arg) ? findClaimableByShortRef(arg) : null);
1829
+ let freshPool;
1830
+ if (!job && looksLikeShortRef(arg)) {
1831
+ freshPool = await fetchFreshClaimablePool();
1832
+ if (freshPool) job = findByShortRefInPool(freshPool, arg);
1833
+ }
886
1834
  if (job) {
887
- ({ bountyId, title, repoFullName, issueUrl, amountUSD, source } = extractClaimableFields(job));
1835
+ ({ bountyId, title, repoFullName, issueUrl, amountUSD, source, openPRsAtDiscovery } = extractClaimableFields(job));
1836
+ indexNativeId = bountyId;
888
1837
  } else {
889
1838
  const parsed = parseGitHubUrl(arg);
890
1839
  if (!parsed) return null;
@@ -894,30 +1843,84 @@ async function resolveBounty(arg) {
894
1843
  issueUrl = arg;
895
1844
  amountUSD = null;
896
1845
  source = "bounty";
1846
+ indexNativeId = `bounty:${parsed.repoFullName}#${parsed.number}`;
1847
+ if (freshPool === void 0) freshPool = await fetchFreshClaimablePool();
1848
+ const pooled = (freshPool ?? []).find((j) => {
1849
+ const p = parseGitHubUrl(extractClaimableFields(j).issueUrl);
1850
+ return p && p.repoFullName.toLowerCase() === parsed.repoFullName.toLowerCase() && p.number === parsed.number;
1851
+ });
1852
+ if (pooled) {
1853
+ const f = extractClaimableFields(pooled);
1854
+ indexNativeId = f.bountyId;
1855
+ source = f.source;
1856
+ }
897
1857
  }
898
1858
  const ghIssue = parseGitHubUrl(issueUrl);
899
- const [issue, openPRs] = ghIssue ? await Promise.all([
900
- fetchIssue(repoFullName, ghIssue.number),
901
- countOpenPRsReferencingIssue(repoFullName, ghIssue.number)
902
- ]) : [null, null];
1859
+ const issue = ghIssue ? await fetchIssue(repoFullName, ghIssue.number) : null;
1860
+ let openPRs;
1861
+ let openPRsFromDiscovery = false;
1862
+ if (openPRsAtDiscovery != null) {
1863
+ openPRs = openPRsAtDiscovery;
1864
+ openPRsFromDiscovery = true;
1865
+ } else {
1866
+ openPRs = ghIssue ? await countOpenPRsReferencingIssue(repoFullName, ghIssue.number) : null;
1867
+ }
1868
+ let openPRsLive;
1869
+ if (openPRsFromDiscovery) {
1870
+ openPRsLive = ghIssue ? await countOpenPRsReferencingIssue(repoFullName, ghIssue.number) : null;
1871
+ } else {
1872
+ openPRsLive = openPRs;
1873
+ }
903
1874
  const issueState = issue ? issue.state : null;
1875
+ const assignees = issue ? issue.assignees : null;
904
1876
  if (!job && issue && issue.title) title = issue.title;
905
1877
  return {
906
1878
  bountyId,
1879
+ indexNativeId,
907
1880
  title,
908
1881
  repoFullName,
909
1882
  issueUrl,
910
1883
  amountUSD,
911
1884
  source,
912
1885
  issueState,
1886
+ assignees,
913
1887
  openPRs,
1888
+ openPRsFromDiscovery,
1889
+ // Live open-PR count for the contention decision only (Fix 2). Distinct from
1890
+ // `openPRs`, which may be the discovery-time snapshot used for display.
1891
+ openPRsLive,
914
1892
  issueNumber: ghIssue ? ghIssue.number : null
915
1893
  };
916
1894
  }
1895
+ function fmtOpenPRsLine(b) {
1896
+ const atDiscovery = b.openPRsFromDiscovery ? " (at discovery)" : "";
1897
+ if (b.openPRs == null) {
1898
+ return " open PRs: unknown (GitHub read unavailable \u2014 check the issue manually before working)";
1899
+ } else if (b.openPRs > 0) {
1900
+ return ` \u26A0 open PRs referencing this issue${atDiscovery}: ${b.openPRs} \u2014 someone may already be on it. Check before working.`;
1901
+ }
1902
+ return ` open PRs referencing this issue${atDiscovery}: 0`;
1903
+ }
1904
+ function isContested(b) {
1905
+ const assignedToSomeoneElse = Array.isArray(b.assignees) && b.assignees.length > 0;
1906
+ const hasOpenPR = b.openPRsLive != null && b.openPRsLive > 0;
1907
+ return assignedToSomeoneElse || hasOpenPR;
1908
+ }
1909
+ function fmtContestedWarning(b) {
1910
+ if (!isContested(b)) return null;
1911
+ const parts = [];
1912
+ if (Array.isArray(b.assignees) && b.assignees.length > 0) {
1913
+ parts.push(`assigned to ${b.assignees.map((l) => `@${l}`).join(", ")}`);
1914
+ }
1915
+ if (b.openPRsLive != null && b.openPRsLive > 0) {
1916
+ parts.push(`${b.openPRsLive} open PR${b.openPRsLive === 1 ? "" : "s"} referencing it`);
1917
+ }
1918
+ return ` \u26A0 This issue looks taken: ${parts.join(" / ")}. A merged PR here is unlikely.`;
1919
+ }
917
1920
  async function cmdRecord(arg, flags = {}) {
918
1921
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
919
1922
  if (!arg) {
920
- console.error("Usage: terminalhire claim record <bountyId|issueUrl> [--ack-policy]");
1923
+ console.error("Usage: terminalhire claim record <bountyId|issueUrl> [--ack-policy] [--ack-contested]");
921
1924
  console.error(" Run `terminalhire bounties` first to populate the local index cache,");
922
1925
  console.error(" then pass the id shown in its output \u2014 or pass a GitHub issue URL directly.");
923
1926
  process.exit(1);
@@ -936,6 +1939,21 @@ async function cmdRecord(arg, flags = {}) {
936
1939
  );
937
1940
  process.exit(1);
938
1941
  }
1942
+ const contestedWarning = fmtContestedWarning(b);
1943
+ if (contestedWarning) {
1944
+ console.log(`
1945
+ ${contestedWarning}`);
1946
+ let acked = Boolean(flags["ack-contested"]);
1947
+ if (!acked && process.stdin.isTTY) {
1948
+ acked = await confirm("\n Claim it anyway? (y/N) ");
1949
+ }
1950
+ if (!acked) {
1951
+ console.error(
1952
+ "\nterminalhire claim: refusing to record \u2014 this issue looks taken.\n Re-run with --ack-contested to claim it anyway (or confirm interactively)."
1953
+ );
1954
+ process.exit(1);
1955
+ }
1956
+ }
939
1957
  const { checkRepoPolicy: checkRepoPolicy2 } = await Promise.resolve().then(() => (init_repo_policy(), repo_policy_exports));
940
1958
  const policy = await checkRepoPolicy2(b.repoFullName);
941
1959
  printPolicySection(policy);
@@ -977,19 +1995,18 @@ terminalhire claim: refusing to record \u2014 read ${b.repoFullName}'s contribut
977
1995
  console.error(`terminalhire claim: ${err.message ?? err}`);
978
1996
  process.exit(1);
979
1997
  }
1998
+ try {
1999
+ const { markStatus: markStatus2 } = await Promise.resolve().then(() => (init_job_status_store(), job_status_store_exports));
2000
+ markStatus2(b.indexNativeId, "claimed");
2001
+ } catch {
2002
+ }
980
2003
  console.log(`
981
2004
  \u2713 Claimed: ${claim.title}`);
982
2005
  console.log(` id: ${claim.id}`);
983
2006
  console.log(` repo: ${claim.repoFullName}`);
984
2007
  console.log(` amount: ${fmtAmount(claim.amountUSD)}`);
985
2008
  console.log(` issue: ${claim.issueUrl}`);
986
- if (b.openPRs == null) {
987
- console.log(" open PRs: unknown (GitHub read unavailable \u2014 check the issue manually before working)");
988
- } else if (b.openPRs > 0) {
989
- console.log(` \u26A0 open PRs referencing this issue: ${b.openPRs} \u2014 someone may already be on it. Check before working.`);
990
- } else {
991
- console.log(" open PRs referencing this issue: 0");
992
- }
2009
+ console.log(fmtOpenPRsLine(b));
993
2010
  console.log("\n Executor constraints (enforce when spawning the background agent):");
994
2011
  console.log(" \u2022 work in an ISOLATED git worktree; scrub the subprocess env (no token/profile inheritance)");
995
2012
  console.log(" \u2022 MUST NOT `git push` or `gh pr` \u2014 pushing happens only via `terminalhire submit`");
@@ -1023,6 +2040,8 @@ async function cmdPreview(arg, { json } = {}) {
1023
2040
  issueUrl: b.issueUrl,
1024
2041
  issueState: b.issueState,
1025
2042
  openPRs: b.openPRs,
2043
+ assignees: b.assignees,
2044
+ contested: isContested(b),
1026
2045
  policy: { status: policy.status, hits: policy.hits }
1027
2046
  }) + "\n"
1028
2047
  );
@@ -1037,13 +2056,9 @@ async function cmdPreview(arg, { json } = {}) {
1037
2056
  if (b.issueState === "closed") {
1038
2057
  console.log(" \u2717 CLOSED \u2014 not claimable (the pool drops closed issues; likely a stale cache entry)");
1039
2058
  }
1040
- if (b.openPRs == null) {
1041
- console.log(" open PRs: unknown (GitHub read unavailable \u2014 check the issue manually before working)");
1042
- } else if (b.openPRs > 0) {
1043
- console.log(` \u26A0 open PRs referencing this issue: ${b.openPRs} \u2014 someone may already be on it. Check before working.`);
1044
- } else {
1045
- console.log(" open PRs referencing this issue: 0");
1046
- }
2059
+ console.log(fmtOpenPRsLine(b));
2060
+ const previewContested = fmtContestedWarning(b);
2061
+ if (previewContested) console.log(previewContested);
1047
2062
  printPolicySection(policy);
1048
2063
  console.log("\n Preview only \u2014 NOT claimed. Run `terminalhire claim record " + arg + "` to claim it.");
1049
2064
  }
@@ -1312,11 +2327,223 @@ async function cmdSubmit(id, worktreeOverride) {
1312
2327
  \u2713 Submitted ${id} \u2192 ${prUrl}`);
1313
2328
  console.log(` Run 'terminalhire claim status ${id}' after the maintainer acts to fold the merge into your accepted-PR rate.`);
1314
2329
  }
2330
+ var CLAIM_PUSH_FIELDS = ["kind", "repoFullName", "state", "prUrl", "merged", "claimedAt", "updatedAt"];
2331
+ function askYes(question) {
2332
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2333
+ return new Promise((res) => rl.question(question, (a) => {
2334
+ rl.close();
2335
+ res(String(a).trim().toLowerCase());
2336
+ }));
2337
+ }
2338
+ function claimSleep(ms) {
2339
+ return new Promise((res) => setTimeout(res, ms));
2340
+ }
2341
+ function readClaimPushMarker() {
2342
+ try {
2343
+ return existsSync4(CLAIM_PUSH_MARKER) ? JSON.parse(readFileSync5(CLAIM_PUSH_MARKER, "utf8")) : null;
2344
+ } catch {
2345
+ return null;
2346
+ }
2347
+ }
2348
+ function writeClaimPushMarker(marker) {
2349
+ mkdirSync4(TERMINALHIRE_DIR4, { recursive: true });
2350
+ writeFileSync4(CLAIM_PUSH_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
2351
+ }
2352
+ function clearClaimPushMarker() {
2353
+ try {
2354
+ rmSync(CLAIM_PUSH_MARKER);
2355
+ } catch {
2356
+ }
2357
+ }
2358
+ function renderClaimConsent(pushed, login) {
2359
+ console.log("");
2360
+ console.log(" terminalhire \u2014 show your claims on your dashboard (opt-in)");
2361
+ console.log("");
2362
+ console.log(` As @${login}, the following SCORE-FREE fields of your ${pushed.length} claim${pushed.length === 1 ? "" : "s"}`);
2363
+ console.log(" will be shared with staqs (terminalhire.com) after you confirm in the browser:");
2364
+ console.log("");
2365
+ for (const f of CLAIM_PUSH_FIELDS) console.log(` - ${f}`);
2366
+ console.log("");
2367
+ console.log(" What is NEVER sent: your diff-acceptance score, review blockers,");
2368
+ console.log(" branch names, worktree paths, repo policy, amounts, or any private data.");
2369
+ console.log("");
2370
+ console.log(" Revoke any time: terminalhire claim --push --revoke");
2371
+ console.log(" This is NOT required to use terminalhire.");
2372
+ console.log("");
2373
+ }
2374
+ async function cmdPush() {
2375
+ const claimsMod = await Promise.resolve().then(() => (init_claims(), claims_exports));
2376
+ const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
2377
+ const profile = await readProfile2();
2378
+ const login = profile.github && profile.github.login ? profile.github.login : null;
2379
+ if (!login) {
2380
+ console.log("\n No GitHub data in your local profile yet.");
2381
+ console.log(" Run `terminalhire login` first, then re-run `terminalhire claim --push`.\n");
2382
+ process.exit(1);
2383
+ }
2384
+ const all = claimsMod.listClaims();
2385
+ if (all.length === 0) {
2386
+ console.log("\n No claims recorded yet \u2014 nothing to push.");
2387
+ console.log(" Record one first: terminalhire claim record <bountyId|issueUrl>\n");
2388
+ process.exit(0);
2389
+ }
2390
+ const pushed = all.map((c) => claimsMod.toPushedClaim(c));
2391
+ renderClaimConsent(pushed, login);
2392
+ const answer = await askYes(' Share these with your dashboard? Type "yes" to confirm: ');
2393
+ if (answer !== "yes") {
2394
+ console.log("\n Aborted \u2014 nothing was shared.\n");
2395
+ process.exit(0);
2396
+ }
2397
+ const consentedAt = (/* @__PURE__ */ new Date()).toISOString();
2398
+ console.log("\n Starting browser verification...");
2399
+ let begin;
2400
+ try {
2401
+ const r = await fetch(`${CLAIM_SYNC_BASE}/api/claim-sync/begin`, {
2402
+ method: "POST",
2403
+ headers: { "Content-Type": "application/json" },
2404
+ body: JSON.stringify({ hostname: osHostname() }),
2405
+ signal: AbortSignal.timeout(1e4)
2406
+ });
2407
+ if (!r.ok) {
2408
+ let detail = "";
2409
+ try {
2410
+ detail = (await r.json())?.message || "";
2411
+ } catch {
2412
+ }
2413
+ console.error(`
2414
+ Could not start claim sync: /api/claim-sync/begin returned ${r.status}. ${detail}`);
2415
+ process.exit(1);
2416
+ }
2417
+ begin = await r.json();
2418
+ } catch (err) {
2419
+ console.error(`
2420
+ Could not start claim sync: ${err instanceof Error ? err.message : String(err)}`);
2421
+ process.exit(1);
2422
+ }
2423
+ const { challenge, verifyUrl } = begin || {};
2424
+ if (!challenge || !verifyUrl) {
2425
+ console.error("\n Could not start claim sync: malformed begin response.");
2426
+ process.exit(1);
2427
+ }
2428
+ console.log("\n Open this URL to authorize (sign in with GitHub, then Confirm):");
2429
+ console.log(` ${verifyUrl}`);
2430
+ console.log("\n (Attempting to open it automatically...)");
2431
+ openInBrowser(verifyUrl);
2432
+ console.log(" Waiting for browser verification...");
2433
+ const deadline = Date.now() + CLAIM_POLL_TIMEOUT_MS;
2434
+ let proofToken = null;
2435
+ while (Date.now() < deadline) {
2436
+ await claimSleep(CLAIM_POLL_INTERVAL_MS);
2437
+ let statusRes;
2438
+ try {
2439
+ statusRes = await fetch(
2440
+ `${CLAIM_SYNC_BASE}/api/claim-sync/status?challenge=${encodeURIComponent(challenge)}`,
2441
+ { signal: AbortSignal.timeout(1e4) }
2442
+ );
2443
+ } catch {
2444
+ continue;
2445
+ }
2446
+ if (!statusRes.ok) continue;
2447
+ let body;
2448
+ try {
2449
+ body = await statusRes.json();
2450
+ } catch {
2451
+ continue;
2452
+ }
2453
+ if (body && body.status === "verified" && body.proofToken) {
2454
+ proofToken = body.proofToken;
2455
+ break;
2456
+ }
2457
+ }
2458
+ if (!proofToken) {
2459
+ console.error("\n Timed out waiting for browser verification (10 min).");
2460
+ console.error(" Re-run `terminalhire claim --push` to try again.\n");
2461
+ process.exit(1);
2462
+ }
2463
+ const consentToken = { consentedAt, version: CLAIM_CONSENT_VERSION, fields: CLAIM_PUSH_FIELDS };
2464
+ console.log("\n Verified. Sharing your claims...");
2465
+ let res;
2466
+ try {
2467
+ res = await fetch(`${CLAIM_SYNC_BASE}/api/claim-sync`, {
2468
+ method: "POST",
2469
+ headers: { "Content-Type": "application/json" },
2470
+ body: JSON.stringify({ consentToken, claims: pushed, proofToken }),
2471
+ signal: AbortSignal.timeout(1e4)
2472
+ });
2473
+ } catch (err) {
2474
+ console.error(`
2475
+ Claim sync failed: ${err instanceof Error ? err.message : String(err)}`);
2476
+ process.exit(1);
2477
+ }
2478
+ if (!res.ok) {
2479
+ let detail = "";
2480
+ try {
2481
+ detail = (await res.json())?.message || "";
2482
+ } catch {
2483
+ }
2484
+ console.error(`
2485
+ Claim sync failed: /api/claim-sync returned ${res.status}. ${detail}`);
2486
+ process.exit(1);
2487
+ }
2488
+ let deleteToken = null;
2489
+ try {
2490
+ deleteToken = (await res.json())?.deleteToken || null;
2491
+ } catch {
2492
+ }
2493
+ writeClaimPushMarker({ consentedAt, login, deleteToken });
2494
+ console.log("\n \u2713 Your claims now show on your dashboard: https://terminalhire.com/dashboard");
2495
+ console.log(" Delete them any time: terminalhire claim --push --revoke\n");
2496
+ }
2497
+ async function cmdRevoke() {
2498
+ const marker = readClaimPushMarker();
2499
+ console.log("\n This hard-deletes your pushed claims from staqs (terminalhire.com)");
2500
+ console.log(" and removes the local marker. There is no soft-delete.\n");
2501
+ const answer = await askYes(' Delete your pushed claims? Type "yes" to confirm: ');
2502
+ if (answer !== "yes") {
2503
+ console.log("\n Aborted \u2014 nothing was deleted.\n");
2504
+ process.exit(0);
2505
+ }
2506
+ const login = marker && marker.login ? marker.login : null;
2507
+ const deleteToken = marker && marker.deleteToken ? marker.deleteToken : null;
2508
+ if (!login || !deleteToken) {
2509
+ console.log("\n No claim-push marker found on this machine.");
2510
+ console.log(" Deletion must run from the machine that pushed (the delete token is stored there),");
2511
+ console.log(' or use the "delete my pushed claims" button on your dashboard.\n');
2512
+ process.exit(1);
2513
+ }
2514
+ console.log("\n Requesting deletion...");
2515
+ let res;
2516
+ try {
2517
+ res = await fetch(`${CLAIM_SYNC_BASE}/api/claim-sync`, {
2518
+ method: "DELETE",
2519
+ headers: { "Content-Type": "application/json" },
2520
+ body: JSON.stringify({ login, deleteToken }),
2521
+ signal: AbortSignal.timeout(1e4)
2522
+ });
2523
+ } catch (err) {
2524
+ console.error(`
2525
+ Delete failed: ${err instanceof Error ? err.message : String(err)}`);
2526
+ console.error(" Local marker NOT cleared (server state unknown). Re-run to retry.\n");
2527
+ process.exit(1);
2528
+ }
2529
+ if (!res.ok) {
2530
+ console.error(`
2531
+ Delete failed: /api/claim-sync returned ${res.status}.`);
2532
+ process.exit(1);
2533
+ }
2534
+ clearClaimPushMarker();
2535
+ console.log("\n \u2713 Pushed claims deleted and local marker cleared.\n");
2536
+ }
1315
2537
  async function run() {
1316
2538
  const verb = process.argv[2];
1317
2539
  const { flags, positional } = parseArgs(process.argv.slice(3));
1318
2540
  const active = Boolean(flags.active);
1319
2541
  const json = Boolean(flags.json);
2542
+ if (verb === "--push") {
2543
+ if (flags.revoke) await cmdRevoke();
2544
+ else await cmdPush();
2545
+ return;
2546
+ }
1320
2547
  try {
1321
2548
  switch (verb) {
1322
2549
  case "preview":
@@ -1358,6 +2585,8 @@ export {
1358
2585
  cmdRecord,
1359
2586
  findClaimableByShortRef,
1360
2587
  findClaimableInCache,
2588
+ fmtContestedWarning,
2589
+ isContested,
1361
2590
  resolveBounty,
1362
2591
  run
1363
2592
  };