terminalhire 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +294 -0
- package/dist/bin/jpi-dispatch.js +2264 -0
- package/dist/bin/jpi-jobs.js +1506 -0
- package/dist/bin/jpi-learn.js +815 -0
- package/dist/bin/jpi-login.js +1603 -0
- package/dist/bin/jpi-profile.js +625 -0
- package/dist/bin/jpi.js +106 -0
- package/dist/src/github-auth.js +206 -0
- package/dist/src/profile.js +423 -0
- package/dist/src/signal.js +447 -0
- package/fixtures/github-sample.json +33 -0
- package/install.js +275 -0
- package/package.json +43 -0
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ../../packages/core/src/types.ts
|
|
13
|
+
var init_types = __esm({
|
|
14
|
+
"../../packages/core/src/types.ts"() {
|
|
15
|
+
"use strict";
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// ../../packages/core/src/vocabulary.ts
|
|
20
|
+
function normalize(tokens) {
|
|
21
|
+
const result = /* @__PURE__ */ new Set();
|
|
22
|
+
for (const raw of tokens) {
|
|
23
|
+
const lower = raw.toLowerCase().trim();
|
|
24
|
+
if (VOCAB_SET.has(lower)) {
|
|
25
|
+
result.add(lower);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const mapped = SYNONYMS[lower];
|
|
29
|
+
if (mapped && VOCAB_SET.has(mapped)) {
|
|
30
|
+
result.add(mapped);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return Array.from(result);
|
|
34
|
+
}
|
|
35
|
+
var VOCABULARY, SYNONYMS, VOCAB_SET;
|
|
36
|
+
var init_vocabulary = __esm({
|
|
37
|
+
"../../packages/core/src/vocabulary.ts"() {
|
|
38
|
+
"use strict";
|
|
39
|
+
VOCABULARY = [
|
|
40
|
+
// Languages
|
|
41
|
+
"typescript",
|
|
42
|
+
"javascript",
|
|
43
|
+
"python",
|
|
44
|
+
"go",
|
|
45
|
+
"rust",
|
|
46
|
+
"java",
|
|
47
|
+
"ruby",
|
|
48
|
+
"elixir",
|
|
49
|
+
"scala",
|
|
50
|
+
"kotlin",
|
|
51
|
+
"swift",
|
|
52
|
+
"cpp",
|
|
53
|
+
"csharp",
|
|
54
|
+
"php",
|
|
55
|
+
"haskell",
|
|
56
|
+
"clojure",
|
|
57
|
+
"r",
|
|
58
|
+
// Frontend frameworks / libs
|
|
59
|
+
"react",
|
|
60
|
+
"nextjs",
|
|
61
|
+
"vue",
|
|
62
|
+
"nuxt",
|
|
63
|
+
"svelte",
|
|
64
|
+
"angular",
|
|
65
|
+
"solidjs",
|
|
66
|
+
"tailwind",
|
|
67
|
+
"css",
|
|
68
|
+
"html",
|
|
69
|
+
"graphql",
|
|
70
|
+
"trpc",
|
|
71
|
+
// Backend frameworks
|
|
72
|
+
"nodejs",
|
|
73
|
+
"express",
|
|
74
|
+
"fastify",
|
|
75
|
+
"nestjs",
|
|
76
|
+
"django",
|
|
77
|
+
"fastapi",
|
|
78
|
+
"flask",
|
|
79
|
+
"rails",
|
|
80
|
+
"spring",
|
|
81
|
+
"actix",
|
|
82
|
+
"gin",
|
|
83
|
+
"phoenix",
|
|
84
|
+
"laravel",
|
|
85
|
+
"dotnet",
|
|
86
|
+
// Infrastructure & DevOps
|
|
87
|
+
"kubernetes",
|
|
88
|
+
"docker",
|
|
89
|
+
"terraform",
|
|
90
|
+
"aws",
|
|
91
|
+
"gcp",
|
|
92
|
+
"azure",
|
|
93
|
+
"ci-cd",
|
|
94
|
+
"github-actions",
|
|
95
|
+
"linux",
|
|
96
|
+
"nginx",
|
|
97
|
+
"pulumi",
|
|
98
|
+
"ansible",
|
|
99
|
+
"prometheus",
|
|
100
|
+
"grafana",
|
|
101
|
+
"datadog",
|
|
102
|
+
"opentelemetry",
|
|
103
|
+
// Data & ML
|
|
104
|
+
"postgresql",
|
|
105
|
+
"mysql",
|
|
106
|
+
"sqlite",
|
|
107
|
+
"mongodb",
|
|
108
|
+
"redis",
|
|
109
|
+
"elasticsearch",
|
|
110
|
+
"kafka",
|
|
111
|
+
"rabbitmq",
|
|
112
|
+
"data-engineering",
|
|
113
|
+
"spark",
|
|
114
|
+
"airflow",
|
|
115
|
+
"dbt",
|
|
116
|
+
"ml",
|
|
117
|
+
"llm",
|
|
118
|
+
"pytorch",
|
|
119
|
+
"tensorflow",
|
|
120
|
+
"pandas",
|
|
121
|
+
"numpy",
|
|
122
|
+
// Domains / capabilities
|
|
123
|
+
"oauth",
|
|
124
|
+
"authentication",
|
|
125
|
+
"security",
|
|
126
|
+
"payments",
|
|
127
|
+
"billing",
|
|
128
|
+
"frontend",
|
|
129
|
+
"backend",
|
|
130
|
+
"devops",
|
|
131
|
+
"mobile",
|
|
132
|
+
"ios",
|
|
133
|
+
"android",
|
|
134
|
+
"api-design",
|
|
135
|
+
"microservices",
|
|
136
|
+
"websockets",
|
|
137
|
+
"testing",
|
|
138
|
+
"accessibility",
|
|
139
|
+
"seo",
|
|
140
|
+
"performance",
|
|
141
|
+
"observability",
|
|
142
|
+
"search",
|
|
143
|
+
"realtime"
|
|
144
|
+
];
|
|
145
|
+
SYNONYMS = {
|
|
146
|
+
// Kubernetes aliases
|
|
147
|
+
"k8s": "kubernetes",
|
|
148
|
+
"kube": "kubernetes",
|
|
149
|
+
// Auth / identity
|
|
150
|
+
"passport": "authentication",
|
|
151
|
+
"oauth2": "oauth",
|
|
152
|
+
"oidc": "oauth",
|
|
153
|
+
"jwt": "authentication",
|
|
154
|
+
"saml": "authentication",
|
|
155
|
+
"auth0": "authentication",
|
|
156
|
+
"clerk": "authentication",
|
|
157
|
+
"nextauth": "authentication",
|
|
158
|
+
// Payments
|
|
159
|
+
"@stripe/stripe-js": "payments",
|
|
160
|
+
"stripe": "payments",
|
|
161
|
+
"braintree": "payments",
|
|
162
|
+
"paddle": "payments",
|
|
163
|
+
"lemonsqueezy": "payments",
|
|
164
|
+
"recurly": "billing",
|
|
165
|
+
"chargebee": "billing",
|
|
166
|
+
// Framework / lib aliases
|
|
167
|
+
"next": "nextjs",
|
|
168
|
+
"next.js": "nextjs",
|
|
169
|
+
"nuxt.js": "nuxt",
|
|
170
|
+
"vue.js": "vue",
|
|
171
|
+
"angular.js": "angular",
|
|
172
|
+
"angularjs": "angular",
|
|
173
|
+
"express.js": "express",
|
|
174
|
+
"expressjs": "express",
|
|
175
|
+
"fastapi": "fastapi",
|
|
176
|
+
"nest": "nestjs",
|
|
177
|
+
"nest.js": "nestjs",
|
|
178
|
+
"sveltekit": "svelte",
|
|
179
|
+
// Language aliases
|
|
180
|
+
"ts": "typescript",
|
|
181
|
+
"js": "javascript",
|
|
182
|
+
"py": "python",
|
|
183
|
+
"golang": "go",
|
|
184
|
+
"c++": "cpp",
|
|
185
|
+
"c#": "csharp",
|
|
186
|
+
".net": "dotnet",
|
|
187
|
+
"asp.net": "dotnet",
|
|
188
|
+
// DB aliases
|
|
189
|
+
"postgres": "postgresql",
|
|
190
|
+
"pg": "postgresql",
|
|
191
|
+
"mongo": "mongodb",
|
|
192
|
+
"elastic": "elasticsearch",
|
|
193
|
+
// Cloud aliases
|
|
194
|
+
"amazon web services": "aws",
|
|
195
|
+
"google cloud": "gcp",
|
|
196
|
+
"google cloud platform": "gcp",
|
|
197
|
+
"microsoft azure": "azure",
|
|
198
|
+
// CI/CD aliases
|
|
199
|
+
"github actions": "github-actions",
|
|
200
|
+
"circle ci": "ci-cd",
|
|
201
|
+
"circleci": "ci-cd",
|
|
202
|
+
"jenkins": "ci-cd",
|
|
203
|
+
"gitlab ci": "ci-cd",
|
|
204
|
+
"travis": "ci-cd",
|
|
205
|
+
// Mobile
|
|
206
|
+
"react native": "mobile",
|
|
207
|
+
"flutter": "mobile",
|
|
208
|
+
"expo": "mobile",
|
|
209
|
+
// AI / ML
|
|
210
|
+
"openai": "llm",
|
|
211
|
+
"anthropic": "llm",
|
|
212
|
+
"langchain": "llm",
|
|
213
|
+
"llamaindex": "llm",
|
|
214
|
+
"hugging face": "ml",
|
|
215
|
+
"huggingface": "ml",
|
|
216
|
+
"scikit-learn": "ml",
|
|
217
|
+
"sklearn": "ml",
|
|
218
|
+
// Data pipeline
|
|
219
|
+
"apache kafka": "kafka",
|
|
220
|
+
"apache spark": "spark",
|
|
221
|
+
"apache airflow": "airflow",
|
|
222
|
+
// Misc
|
|
223
|
+
"tailwindcss": "tailwind",
|
|
224
|
+
"tw": "tailwind",
|
|
225
|
+
"gql": "graphql",
|
|
226
|
+
"ws": "websockets",
|
|
227
|
+
"socket.io": "websockets",
|
|
228
|
+
"jest": "testing",
|
|
229
|
+
"vitest": "testing",
|
|
230
|
+
"playwright": "testing",
|
|
231
|
+
"cypress": "testing"
|
|
232
|
+
};
|
|
233
|
+
VOCAB_SET = new Set(VOCABULARY);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ../../packages/core/src/matcher.ts
|
|
238
|
+
var init_matcher = __esm({
|
|
239
|
+
"../../packages/core/src/matcher.ts"() {
|
|
240
|
+
"use strict";
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ../../packages/core/src/feeds/greenhouse.ts
|
|
245
|
+
var init_greenhouse = __esm({
|
|
246
|
+
"../../packages/core/src/feeds/greenhouse.ts"() {
|
|
247
|
+
"use strict";
|
|
248
|
+
init_vocabulary();
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ../../packages/core/src/feeds/ashby.ts
|
|
253
|
+
var init_ashby = __esm({
|
|
254
|
+
"../../packages/core/src/feeds/ashby.ts"() {
|
|
255
|
+
"use strict";
|
|
256
|
+
init_vocabulary();
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// ../../packages/core/src/feeds/himalayas.ts
|
|
261
|
+
var init_himalayas = __esm({
|
|
262
|
+
"../../packages/core/src/feeds/himalayas.ts"() {
|
|
263
|
+
"use strict";
|
|
264
|
+
init_vocabulary();
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ../../packages/core/src/feeds/wwr.ts
|
|
269
|
+
var init_wwr = __esm({
|
|
270
|
+
"../../packages/core/src/feeds/wwr.ts"() {
|
|
271
|
+
"use strict";
|
|
272
|
+
init_vocabulary();
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ../../packages/core/src/feeds/hn.ts
|
|
277
|
+
var init_hn = __esm({
|
|
278
|
+
"../../packages/core/src/feeds/hn.ts"() {
|
|
279
|
+
"use strict";
|
|
280
|
+
init_vocabulary();
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ../../packages/core/src/feeds/index.ts
|
|
285
|
+
var init_feeds = __esm({
|
|
286
|
+
"../../packages/core/src/feeds/index.ts"() {
|
|
287
|
+
"use strict";
|
|
288
|
+
init_greenhouse();
|
|
289
|
+
init_ashby();
|
|
290
|
+
init_himalayas();
|
|
291
|
+
init_wwr();
|
|
292
|
+
init_hn();
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// ../../packages/core/src/coastal.ts
|
|
297
|
+
import { readFileSync } from "fs";
|
|
298
|
+
import { join } from "path";
|
|
299
|
+
import { fileURLToPath } from "url";
|
|
300
|
+
var init_coastal = __esm({
|
|
301
|
+
"../../packages/core/src/coastal.ts"() {
|
|
302
|
+
"use strict";
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// ../../packages/core/src/indexer.ts
|
|
307
|
+
var init_indexer = __esm({
|
|
308
|
+
"../../packages/core/src/indexer.ts"() {
|
|
309
|
+
"use strict";
|
|
310
|
+
init_feeds();
|
|
311
|
+
init_coastal();
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// ../../packages/core/src/github.ts
|
|
316
|
+
var init_github = __esm({
|
|
317
|
+
"../../packages/core/src/github.ts"() {
|
|
318
|
+
"use strict";
|
|
319
|
+
init_vocabulary();
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// ../../packages/core/src/index.ts
|
|
324
|
+
var init_src = __esm({
|
|
325
|
+
"../../packages/core/src/index.ts"() {
|
|
326
|
+
"use strict";
|
|
327
|
+
init_types();
|
|
328
|
+
init_vocabulary();
|
|
329
|
+
init_matcher();
|
|
330
|
+
init_feeds();
|
|
331
|
+
init_indexer();
|
|
332
|
+
init_coastal();
|
|
333
|
+
init_github();
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// src/signal.ts
|
|
338
|
+
var signal_exports = {};
|
|
339
|
+
__export(signal_exports, {
|
|
340
|
+
extractFingerprint: () => extractFingerprint
|
|
341
|
+
});
|
|
342
|
+
import { readFileSync as readFileSync2, readdirSync } from "fs";
|
|
343
|
+
import { execSync } from "child_process";
|
|
344
|
+
import { join as join2 } from "path";
|
|
345
|
+
function safeExec(cmd) {
|
|
346
|
+
try {
|
|
347
|
+
return execSync(cmd, { timeout: 2e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
348
|
+
} catch {
|
|
349
|
+
return "";
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function isEmployerContext(cwd) {
|
|
353
|
+
const remote = safeExec('git -C "' + cwd + '" remote get-url origin 2>/dev/null');
|
|
354
|
+
if (remote) {
|
|
355
|
+
try {
|
|
356
|
+
const sshMatch = remote.match(/^git@([^:]+):/);
|
|
357
|
+
const httpsMatch = remote.match(/^https?:\/\/([^/]+)/);
|
|
358
|
+
const host = (sshMatch?.[1] ?? httpsMatch?.[1] ?? "").toLowerCase();
|
|
359
|
+
if (host && !PERSONAL_GIT_HOSTS.has(host)) {
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
} catch {
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const email = safeExec('git -C "' + cwd + '" config user.email 2>/dev/null');
|
|
366
|
+
if (email) {
|
|
367
|
+
const domain = email.split("@")[1]?.toLowerCase() ?? "";
|
|
368
|
+
if (domain && !PERSONAL_EMAIL_DOMAINS.has(domain)) {
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
function readJsonSafe(path) {
|
|
375
|
+
try {
|
|
376
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
377
|
+
} catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function readFileSafe(path) {
|
|
382
|
+
try {
|
|
383
|
+
return readFileSync2(path, "utf8");
|
|
384
|
+
} catch {
|
|
385
|
+
return "";
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function tokensFromPackageJson(cwd) {
|
|
389
|
+
const pkg = readJsonSafe(join2(cwd, "package.json"));
|
|
390
|
+
if (!pkg || typeof pkg !== "object") return [];
|
|
391
|
+
const p = pkg;
|
|
392
|
+
const deps = {
|
|
393
|
+
...typeof p["dependencies"] === "object" ? p["dependencies"] : {},
|
|
394
|
+
...typeof p["devDependencies"] === "object" ? p["devDependencies"] : {}
|
|
395
|
+
};
|
|
396
|
+
return Object.keys(deps);
|
|
397
|
+
}
|
|
398
|
+
function tokensFromRequirementsTxt(cwd) {
|
|
399
|
+
const content = readFileSafe(join2(cwd, "requirements.txt"));
|
|
400
|
+
if (!content) return [];
|
|
401
|
+
return content.split("\n").map((l) => l.trim().split(/[>=<!\[;]/)[0].trim().toLowerCase()).filter(Boolean);
|
|
402
|
+
}
|
|
403
|
+
function tokensFromGoMod(cwd) {
|
|
404
|
+
const content = readFileSafe(join2(cwd, "go.mod"));
|
|
405
|
+
if (!content) return ["go"];
|
|
406
|
+
const requires = Array.from(content.matchAll(/^\s+([^\s]+)\s+v/gm)).map((m) => m[1].split("/").pop() ?? "").filter(Boolean);
|
|
407
|
+
return ["go", ...requires];
|
|
408
|
+
}
|
|
409
|
+
function tokensFromCargoToml(cwd) {
|
|
410
|
+
const content = readFileSafe(join2(cwd, "Cargo.toml"));
|
|
411
|
+
if (!content) return [];
|
|
412
|
+
const deps = Array.from(content.matchAll(/^([a-zA-Z0-9_-]+)\s*=/gm)).map((m) => m[1].toLowerCase());
|
|
413
|
+
return ["rust", ...deps];
|
|
414
|
+
}
|
|
415
|
+
function tokensFromFileExtensions(cwd) {
|
|
416
|
+
const tokens = [];
|
|
417
|
+
const scanDirs = [cwd];
|
|
418
|
+
try {
|
|
419
|
+
const srcDir = join2(cwd, "src");
|
|
420
|
+
readdirSync(srcDir);
|
|
421
|
+
scanDirs.push(srcDir);
|
|
422
|
+
} catch {
|
|
423
|
+
}
|
|
424
|
+
for (const dir of scanDirs) {
|
|
425
|
+
try {
|
|
426
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
427
|
+
for (const e of entries) {
|
|
428
|
+
if (!e.isFile()) continue;
|
|
429
|
+
const dotIdx = e.name.lastIndexOf(".");
|
|
430
|
+
if (dotIdx === -1) continue;
|
|
431
|
+
const ext = e.name.slice(dotIdx).toLowerCase();
|
|
432
|
+
const tag = EXT_MAP[ext];
|
|
433
|
+
if (tag) tokens.push(tag);
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return tokens;
|
|
439
|
+
}
|
|
440
|
+
function inferSeniority(rawTokens) {
|
|
441
|
+
const seniorSignals = /* @__PURE__ */ new Set([
|
|
442
|
+
"kubernetes",
|
|
443
|
+
"terraform",
|
|
444
|
+
"pulumi",
|
|
445
|
+
"kafka",
|
|
446
|
+
"spark",
|
|
447
|
+
"airflow",
|
|
448
|
+
"dbt",
|
|
449
|
+
"opentelemetry",
|
|
450
|
+
"prometheus",
|
|
451
|
+
"grafana",
|
|
452
|
+
"microservices",
|
|
453
|
+
"api-design",
|
|
454
|
+
"security",
|
|
455
|
+
"oauth",
|
|
456
|
+
"payments"
|
|
457
|
+
]);
|
|
458
|
+
const midSignals = /* @__PURE__ */ new Set([
|
|
459
|
+
"docker",
|
|
460
|
+
"ci-cd",
|
|
461
|
+
"github-actions",
|
|
462
|
+
"testing",
|
|
463
|
+
"postgresql",
|
|
464
|
+
"redis",
|
|
465
|
+
"graphql",
|
|
466
|
+
"trpc"
|
|
467
|
+
]);
|
|
468
|
+
const normalized = new Set(normalize(rawTokens));
|
|
469
|
+
const seniorHits = [...normalized].filter((t) => seniorSignals.has(t)).length;
|
|
470
|
+
const midHits = [...normalized].filter((t) => midSignals.has(t)).length;
|
|
471
|
+
if (seniorHits >= 2) return "senior";
|
|
472
|
+
if (seniorHits >= 1 || midHits >= 2) return "mid";
|
|
473
|
+
return void 0;
|
|
474
|
+
}
|
|
475
|
+
function extractFingerprint(cwd) {
|
|
476
|
+
const employer = isEmployerContext(cwd);
|
|
477
|
+
const rawTokens = [
|
|
478
|
+
...tokensFromPackageJson(cwd),
|
|
479
|
+
...tokensFromRequirementsTxt(cwd),
|
|
480
|
+
...tokensFromGoMod(cwd),
|
|
481
|
+
...tokensFromCargoToml(cwd),
|
|
482
|
+
...tokensFromFileExtensions(cwd)
|
|
483
|
+
];
|
|
484
|
+
let skillTags = normalize(rawTokens);
|
|
485
|
+
if (employer) {
|
|
486
|
+
skillTags = skillTags.filter((t) => LANGUAGE_TAGS.has(t));
|
|
487
|
+
}
|
|
488
|
+
const seniorityBand = employer ? void 0 : inferSeniority(rawTokens);
|
|
489
|
+
return {
|
|
490
|
+
skillTags,
|
|
491
|
+
seniorityBand,
|
|
492
|
+
employerContext: employer
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
var LANGUAGE_TAGS, EXT_MAP, PERSONAL_GIT_HOSTS, PERSONAL_EMAIL_DOMAINS;
|
|
496
|
+
var init_signal = __esm({
|
|
497
|
+
"src/signal.ts"() {
|
|
498
|
+
"use strict";
|
|
499
|
+
init_src();
|
|
500
|
+
LANGUAGE_TAGS = /* @__PURE__ */ new Set([
|
|
501
|
+
"typescript",
|
|
502
|
+
"javascript",
|
|
503
|
+
"python",
|
|
504
|
+
"go",
|
|
505
|
+
"rust",
|
|
506
|
+
"java",
|
|
507
|
+
"ruby",
|
|
508
|
+
"elixir",
|
|
509
|
+
"scala",
|
|
510
|
+
"kotlin",
|
|
511
|
+
"swift",
|
|
512
|
+
"cpp",
|
|
513
|
+
"csharp",
|
|
514
|
+
"php",
|
|
515
|
+
"haskell",
|
|
516
|
+
"clojure",
|
|
517
|
+
"r"
|
|
518
|
+
]);
|
|
519
|
+
EXT_MAP = {
|
|
520
|
+
".ts": "typescript",
|
|
521
|
+
".tsx": "typescript",
|
|
522
|
+
".js": "javascript",
|
|
523
|
+
".mjs": "javascript",
|
|
524
|
+
".cjs": "javascript",
|
|
525
|
+
".jsx": "javascript",
|
|
526
|
+
".py": "python",
|
|
527
|
+
".go": "go",
|
|
528
|
+
".rs": "rust",
|
|
529
|
+
".java": "java",
|
|
530
|
+
".rb": "ruby",
|
|
531
|
+
".ex": "elixir",
|
|
532
|
+
".exs": "elixir",
|
|
533
|
+
".scala": "scala",
|
|
534
|
+
".kt": "kotlin",
|
|
535
|
+
".swift": "swift",
|
|
536
|
+
".cpp": "cpp",
|
|
537
|
+
".cc": "cpp",
|
|
538
|
+
".cxx": "cpp",
|
|
539
|
+
".hpp": "cpp",
|
|
540
|
+
".cs": "csharp",
|
|
541
|
+
".php": "php",
|
|
542
|
+
".hs": "haskell",
|
|
543
|
+
".clj": "clojure",
|
|
544
|
+
".cljs": "clojure",
|
|
545
|
+
".r": "r",
|
|
546
|
+
".vue": "vue",
|
|
547
|
+
".svelte": "svelte"
|
|
548
|
+
};
|
|
549
|
+
PERSONAL_GIT_HOSTS = /* @__PURE__ */ new Set([
|
|
550
|
+
"github.com",
|
|
551
|
+
"gitlab.com",
|
|
552
|
+
"bitbucket.org",
|
|
553
|
+
"codeberg.org",
|
|
554
|
+
"sr.ht"
|
|
555
|
+
]);
|
|
556
|
+
PERSONAL_EMAIL_DOMAINS = /* @__PURE__ */ new Set([
|
|
557
|
+
"gmail.com",
|
|
558
|
+
"googlemail.com",
|
|
559
|
+
"yahoo.com",
|
|
560
|
+
"outlook.com",
|
|
561
|
+
"hotmail.com",
|
|
562
|
+
"icloud.com",
|
|
563
|
+
"me.com",
|
|
564
|
+
"mac.com",
|
|
565
|
+
"proton.me",
|
|
566
|
+
"protonmail.com",
|
|
567
|
+
"fastmail.com",
|
|
568
|
+
"hey.com",
|
|
569
|
+
"duck.com"
|
|
570
|
+
]);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// src/profile.ts
|
|
575
|
+
var profile_exports = {};
|
|
576
|
+
__export(profile_exports, {
|
|
577
|
+
accumulateGitHubTags: () => accumulateGitHubTags,
|
|
578
|
+
accumulateSession: () => accumulateSession,
|
|
579
|
+
accumulateTags: () => accumulateTags,
|
|
580
|
+
deleteProfile: () => deleteProfile,
|
|
581
|
+
profileToFingerprint: () => profileToFingerprint,
|
|
582
|
+
readProfile: () => readProfile,
|
|
583
|
+
writeProfile: () => writeProfile
|
|
584
|
+
});
|
|
585
|
+
import {
|
|
586
|
+
createCipheriv,
|
|
587
|
+
createDecipheriv,
|
|
588
|
+
randomBytes
|
|
589
|
+
} from "crypto";
|
|
590
|
+
import {
|
|
591
|
+
readFileSync as readFileSync3,
|
|
592
|
+
writeFileSync,
|
|
593
|
+
mkdirSync,
|
|
594
|
+
existsSync
|
|
595
|
+
} from "fs";
|
|
596
|
+
import { join as join3 } from "path";
|
|
597
|
+
import { homedir } from "os";
|
|
598
|
+
async function loadKey() {
|
|
599
|
+
try {
|
|
600
|
+
const kt = await import("keytar");
|
|
601
|
+
const stored = await kt.getPassword("terminalhire", "profile-key");
|
|
602
|
+
if (stored) {
|
|
603
|
+
return Buffer.from(stored, "hex");
|
|
604
|
+
}
|
|
605
|
+
const key2 = randomBytes(KEY_BYTES);
|
|
606
|
+
await kt.setPassword("terminalhire", "profile-key", key2.toString("hex"));
|
|
607
|
+
return key2;
|
|
608
|
+
} catch {
|
|
609
|
+
}
|
|
610
|
+
mkdirSync(TERMINALHIRE_DIR, { recursive: true });
|
|
611
|
+
if (existsSync(KEY_FILE)) {
|
|
612
|
+
return Buffer.from(readFileSync3(KEY_FILE, "utf8").trim(), "hex");
|
|
613
|
+
}
|
|
614
|
+
const key = randomBytes(KEY_BYTES);
|
|
615
|
+
writeFileSync(KEY_FILE, key.toString("hex"), { mode: 384, encoding: "utf8" });
|
|
616
|
+
return key;
|
|
617
|
+
}
|
|
618
|
+
function encrypt(plaintext, key) {
|
|
619
|
+
const iv = randomBytes(IV_BYTES);
|
|
620
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
621
|
+
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
622
|
+
const tag = cipher.getAuthTag();
|
|
623
|
+
return {
|
|
624
|
+
iv: iv.toString("hex"),
|
|
625
|
+
tag: tag.toString("hex"),
|
|
626
|
+
ciphertext: ct.toString("hex")
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function decrypt(blob, key) {
|
|
630
|
+
const decipher = createDecipheriv(
|
|
631
|
+
ALGO,
|
|
632
|
+
key,
|
|
633
|
+
Buffer.from(blob.iv, "hex")
|
|
634
|
+
);
|
|
635
|
+
decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
|
|
636
|
+
const plain = Buffer.concat([
|
|
637
|
+
decipher.update(Buffer.from(blob.ciphertext, "hex")),
|
|
638
|
+
decipher.final()
|
|
639
|
+
]);
|
|
640
|
+
return plain.toString("utf8");
|
|
641
|
+
}
|
|
642
|
+
function blankProfile() {
|
|
643
|
+
return {
|
|
644
|
+
version: 3,
|
|
645
|
+
skillTags: [],
|
|
646
|
+
tagWeights: {},
|
|
647
|
+
hasEmployerSessions: false,
|
|
648
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function recencyDecay(lastSeen) {
|
|
652
|
+
const ageMs = Date.now() - new Date(lastSeen).getTime();
|
|
653
|
+
return Math.pow(0.5, ageMs / DECAY_HALF_LIFE_MS);
|
|
654
|
+
}
|
|
655
|
+
function tagScore(w) {
|
|
656
|
+
return w.count * recencyDecay(w.lastSeen);
|
|
657
|
+
}
|
|
658
|
+
function deriveSkillTags(tagWeights) {
|
|
659
|
+
return Object.entries(tagWeights).filter(([, w]) => w.count >= 1).sort(([, a], [, b]) => tagScore(b) - tagScore(a)).map(([tag]) => tag);
|
|
660
|
+
}
|
|
661
|
+
function migrateTagWeights(profile) {
|
|
662
|
+
if (!profile.tagWeights) {
|
|
663
|
+
profile.tagWeights = {};
|
|
664
|
+
}
|
|
665
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
666
|
+
for (const tag of profile.skillTags) {
|
|
667
|
+
if (!profile.tagWeights[tag]) {
|
|
668
|
+
profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
async function readProfile() {
|
|
673
|
+
if (!existsSync(PROFILE_FILE)) return blankProfile();
|
|
674
|
+
try {
|
|
675
|
+
const key = await loadKey();
|
|
676
|
+
const raw = readFileSync3(PROFILE_FILE, "utf8");
|
|
677
|
+
const blob = JSON.parse(raw);
|
|
678
|
+
const plaintext = decrypt(blob, key);
|
|
679
|
+
const parsed = JSON.parse(plaintext);
|
|
680
|
+
migrateTagWeights(parsed);
|
|
681
|
+
return parsed;
|
|
682
|
+
} catch {
|
|
683
|
+
return blankProfile();
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
async function writeProfile(profile) {
|
|
687
|
+
mkdirSync(TERMINALHIRE_DIR, { recursive: true });
|
|
688
|
+
const key = await loadKey();
|
|
689
|
+
profile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
690
|
+
profile.skillTags = deriveSkillTags(profile.tagWeights);
|
|
691
|
+
const blob = encrypt(JSON.stringify(profile), key);
|
|
692
|
+
writeFileSync(PROFILE_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
|
|
693
|
+
}
|
|
694
|
+
function accumulateSession(profile, tags, isEmployerContext2, inferredSeniority) {
|
|
695
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
696
|
+
let filtered = normalize(tags);
|
|
697
|
+
if (isEmployerContext2) {
|
|
698
|
+
filtered = filtered.filter((t) => LANGUAGE_TAGS2.has(t));
|
|
699
|
+
profile.hasEmployerSessions = true;
|
|
700
|
+
}
|
|
701
|
+
for (const tag of filtered) {
|
|
702
|
+
const existing = profile.tagWeights[tag];
|
|
703
|
+
if (existing) {
|
|
704
|
+
existing.count += 1;
|
|
705
|
+
existing.sessions += 1;
|
|
706
|
+
existing.lastSeen = now;
|
|
707
|
+
} else {
|
|
708
|
+
profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (inferredSeniority && !isEmployerContext2) {
|
|
712
|
+
profile.seniority = inferredSeniority;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
async function accumulateTags(rawTokens, isEmployerContext2, inferredSeniority) {
|
|
716
|
+
const profile = await readProfile();
|
|
717
|
+
accumulateSession(profile, rawTokens, isEmployerContext2, inferredSeniority);
|
|
718
|
+
await writeProfile(profile);
|
|
719
|
+
}
|
|
720
|
+
function accumulateGitHubTags(profile, tags) {
|
|
721
|
+
accumulateSession(
|
|
722
|
+
profile,
|
|
723
|
+
tags,
|
|
724
|
+
/* isEmployerContext */
|
|
725
|
+
false
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
async function deleteProfile() {
|
|
729
|
+
const { rmSync } = await import("fs");
|
|
730
|
+
try {
|
|
731
|
+
rmSync(PROFILE_FILE);
|
|
732
|
+
} catch {
|
|
733
|
+
}
|
|
734
|
+
try {
|
|
735
|
+
rmSync(KEY_FILE);
|
|
736
|
+
} catch {
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function profileToFingerprint(profile) {
|
|
740
|
+
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);
|
|
741
|
+
const skillTags = rankedTags.length > 0 ? rankedTags : profile.skillTags;
|
|
742
|
+
return {
|
|
743
|
+
skillTags,
|
|
744
|
+
seniorityBand: profile.seniority,
|
|
745
|
+
prefs: {
|
|
746
|
+
roleTypes: profile.roleTypes,
|
|
747
|
+
remoteOnly: profile.remoteOnly,
|
|
748
|
+
compFloorUsd: profile.compFloorUsd
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
var TERMINALHIRE_DIR, PROFILE_FILE, KEY_FILE, ALGO, KEY_BYTES, IV_BYTES, DECAY_HALF_LIFE_MS, LANGUAGE_TAGS2, MIN_FINGERPRINT_SCORE;
|
|
753
|
+
var init_profile = __esm({
|
|
754
|
+
"src/profile.ts"() {
|
|
755
|
+
"use strict";
|
|
756
|
+
init_src();
|
|
757
|
+
TERMINALHIRE_DIR = join3(homedir(), ".terminalhire");
|
|
758
|
+
PROFILE_FILE = join3(TERMINALHIRE_DIR, "profile.enc");
|
|
759
|
+
KEY_FILE = join3(TERMINALHIRE_DIR, "key");
|
|
760
|
+
ALGO = "aes-256-gcm";
|
|
761
|
+
KEY_BYTES = 32;
|
|
762
|
+
IV_BYTES = 12;
|
|
763
|
+
DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
764
|
+
LANGUAGE_TAGS2 = /* @__PURE__ */ new Set([
|
|
765
|
+
"typescript",
|
|
766
|
+
"javascript",
|
|
767
|
+
"python",
|
|
768
|
+
"go",
|
|
769
|
+
"rust",
|
|
770
|
+
"java",
|
|
771
|
+
"ruby",
|
|
772
|
+
"elixir",
|
|
773
|
+
"scala",
|
|
774
|
+
"kotlin",
|
|
775
|
+
"swift",
|
|
776
|
+
"cpp",
|
|
777
|
+
"csharp",
|
|
778
|
+
"php",
|
|
779
|
+
"haskell",
|
|
780
|
+
"clojure",
|
|
781
|
+
"r"
|
|
782
|
+
]);
|
|
783
|
+
MIN_FINGERPRINT_SCORE = 0.05;
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// bin/jpi-learn.js
|
|
788
|
+
async function run() {
|
|
789
|
+
try {
|
|
790
|
+
const args = process.argv.slice(2);
|
|
791
|
+
const cwdIdx = args.indexOf("--cwd");
|
|
792
|
+
const cwd = cwdIdx !== -1 && args[cwdIdx + 1] ? args[cwdIdx + 1] : process.cwd();
|
|
793
|
+
const { extractFingerprint: extractFingerprint2 } = await Promise.resolve().then(() => (init_signal(), signal_exports));
|
|
794
|
+
const { readProfile: readProfile2, writeProfile: writeProfile2, accumulateSession: accumulateSession2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
795
|
+
const fingerprint = extractFingerprint2(cwd);
|
|
796
|
+
const profile = await readProfile2();
|
|
797
|
+
accumulateSession2(
|
|
798
|
+
profile,
|
|
799
|
+
fingerprint.skillTags,
|
|
800
|
+
fingerprint.employerContext,
|
|
801
|
+
fingerprint.seniorityBand
|
|
802
|
+
);
|
|
803
|
+
await writeProfile2(profile);
|
|
804
|
+
process.exit(0);
|
|
805
|
+
} catch {
|
|
806
|
+
process.exit(0);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
var isMain = process.argv[1]?.endsWith("jpi-learn.js") || process.argv[1]?.endsWith("jpi-learn");
|
|
810
|
+
if (isMain) {
|
|
811
|
+
run();
|
|
812
|
+
}
|
|
813
|
+
export {
|
|
814
|
+
run
|
|
815
|
+
};
|