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,423 @@
|
|
|
1
|
+
// src/profile.ts
|
|
2
|
+
import {
|
|
3
|
+
createCipheriv,
|
|
4
|
+
createDecipheriv,
|
|
5
|
+
randomBytes
|
|
6
|
+
} from "crypto";
|
|
7
|
+
import {
|
|
8
|
+
readFileSync as readFileSync2,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
existsSync
|
|
12
|
+
} from "fs";
|
|
13
|
+
import { join as join2 } from "path";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
|
|
16
|
+
// ../../packages/core/src/vocabulary.ts
|
|
17
|
+
var VOCABULARY = [
|
|
18
|
+
// Languages
|
|
19
|
+
"typescript",
|
|
20
|
+
"javascript",
|
|
21
|
+
"python",
|
|
22
|
+
"go",
|
|
23
|
+
"rust",
|
|
24
|
+
"java",
|
|
25
|
+
"ruby",
|
|
26
|
+
"elixir",
|
|
27
|
+
"scala",
|
|
28
|
+
"kotlin",
|
|
29
|
+
"swift",
|
|
30
|
+
"cpp",
|
|
31
|
+
"csharp",
|
|
32
|
+
"php",
|
|
33
|
+
"haskell",
|
|
34
|
+
"clojure",
|
|
35
|
+
"r",
|
|
36
|
+
// Frontend frameworks / libs
|
|
37
|
+
"react",
|
|
38
|
+
"nextjs",
|
|
39
|
+
"vue",
|
|
40
|
+
"nuxt",
|
|
41
|
+
"svelte",
|
|
42
|
+
"angular",
|
|
43
|
+
"solidjs",
|
|
44
|
+
"tailwind",
|
|
45
|
+
"css",
|
|
46
|
+
"html",
|
|
47
|
+
"graphql",
|
|
48
|
+
"trpc",
|
|
49
|
+
// Backend frameworks
|
|
50
|
+
"nodejs",
|
|
51
|
+
"express",
|
|
52
|
+
"fastify",
|
|
53
|
+
"nestjs",
|
|
54
|
+
"django",
|
|
55
|
+
"fastapi",
|
|
56
|
+
"flask",
|
|
57
|
+
"rails",
|
|
58
|
+
"spring",
|
|
59
|
+
"actix",
|
|
60
|
+
"gin",
|
|
61
|
+
"phoenix",
|
|
62
|
+
"laravel",
|
|
63
|
+
"dotnet",
|
|
64
|
+
// Infrastructure & DevOps
|
|
65
|
+
"kubernetes",
|
|
66
|
+
"docker",
|
|
67
|
+
"terraform",
|
|
68
|
+
"aws",
|
|
69
|
+
"gcp",
|
|
70
|
+
"azure",
|
|
71
|
+
"ci-cd",
|
|
72
|
+
"github-actions",
|
|
73
|
+
"linux",
|
|
74
|
+
"nginx",
|
|
75
|
+
"pulumi",
|
|
76
|
+
"ansible",
|
|
77
|
+
"prometheus",
|
|
78
|
+
"grafana",
|
|
79
|
+
"datadog",
|
|
80
|
+
"opentelemetry",
|
|
81
|
+
// Data & ML
|
|
82
|
+
"postgresql",
|
|
83
|
+
"mysql",
|
|
84
|
+
"sqlite",
|
|
85
|
+
"mongodb",
|
|
86
|
+
"redis",
|
|
87
|
+
"elasticsearch",
|
|
88
|
+
"kafka",
|
|
89
|
+
"rabbitmq",
|
|
90
|
+
"data-engineering",
|
|
91
|
+
"spark",
|
|
92
|
+
"airflow",
|
|
93
|
+
"dbt",
|
|
94
|
+
"ml",
|
|
95
|
+
"llm",
|
|
96
|
+
"pytorch",
|
|
97
|
+
"tensorflow",
|
|
98
|
+
"pandas",
|
|
99
|
+
"numpy",
|
|
100
|
+
// Domains / capabilities
|
|
101
|
+
"oauth",
|
|
102
|
+
"authentication",
|
|
103
|
+
"security",
|
|
104
|
+
"payments",
|
|
105
|
+
"billing",
|
|
106
|
+
"frontend",
|
|
107
|
+
"backend",
|
|
108
|
+
"devops",
|
|
109
|
+
"mobile",
|
|
110
|
+
"ios",
|
|
111
|
+
"android",
|
|
112
|
+
"api-design",
|
|
113
|
+
"microservices",
|
|
114
|
+
"websockets",
|
|
115
|
+
"testing",
|
|
116
|
+
"accessibility",
|
|
117
|
+
"seo",
|
|
118
|
+
"performance",
|
|
119
|
+
"observability",
|
|
120
|
+
"search",
|
|
121
|
+
"realtime"
|
|
122
|
+
];
|
|
123
|
+
var SYNONYMS = {
|
|
124
|
+
// Kubernetes aliases
|
|
125
|
+
"k8s": "kubernetes",
|
|
126
|
+
"kube": "kubernetes",
|
|
127
|
+
// Auth / identity
|
|
128
|
+
"passport": "authentication",
|
|
129
|
+
"oauth2": "oauth",
|
|
130
|
+
"oidc": "oauth",
|
|
131
|
+
"jwt": "authentication",
|
|
132
|
+
"saml": "authentication",
|
|
133
|
+
"auth0": "authentication",
|
|
134
|
+
"clerk": "authentication",
|
|
135
|
+
"nextauth": "authentication",
|
|
136
|
+
// Payments
|
|
137
|
+
"@stripe/stripe-js": "payments",
|
|
138
|
+
"stripe": "payments",
|
|
139
|
+
"braintree": "payments",
|
|
140
|
+
"paddle": "payments",
|
|
141
|
+
"lemonsqueezy": "payments",
|
|
142
|
+
"recurly": "billing",
|
|
143
|
+
"chargebee": "billing",
|
|
144
|
+
// Framework / lib aliases
|
|
145
|
+
"next": "nextjs",
|
|
146
|
+
"next.js": "nextjs",
|
|
147
|
+
"nuxt.js": "nuxt",
|
|
148
|
+
"vue.js": "vue",
|
|
149
|
+
"angular.js": "angular",
|
|
150
|
+
"angularjs": "angular",
|
|
151
|
+
"express.js": "express",
|
|
152
|
+
"expressjs": "express",
|
|
153
|
+
"fastapi": "fastapi",
|
|
154
|
+
"nest": "nestjs",
|
|
155
|
+
"nest.js": "nestjs",
|
|
156
|
+
"sveltekit": "svelte",
|
|
157
|
+
// Language aliases
|
|
158
|
+
"ts": "typescript",
|
|
159
|
+
"js": "javascript",
|
|
160
|
+
"py": "python",
|
|
161
|
+
"golang": "go",
|
|
162
|
+
"c++": "cpp",
|
|
163
|
+
"c#": "csharp",
|
|
164
|
+
".net": "dotnet",
|
|
165
|
+
"asp.net": "dotnet",
|
|
166
|
+
// DB aliases
|
|
167
|
+
"postgres": "postgresql",
|
|
168
|
+
"pg": "postgresql",
|
|
169
|
+
"mongo": "mongodb",
|
|
170
|
+
"elastic": "elasticsearch",
|
|
171
|
+
// Cloud aliases
|
|
172
|
+
"amazon web services": "aws",
|
|
173
|
+
"google cloud": "gcp",
|
|
174
|
+
"google cloud platform": "gcp",
|
|
175
|
+
"microsoft azure": "azure",
|
|
176
|
+
// CI/CD aliases
|
|
177
|
+
"github actions": "github-actions",
|
|
178
|
+
"circle ci": "ci-cd",
|
|
179
|
+
"circleci": "ci-cd",
|
|
180
|
+
"jenkins": "ci-cd",
|
|
181
|
+
"gitlab ci": "ci-cd",
|
|
182
|
+
"travis": "ci-cd",
|
|
183
|
+
// Mobile
|
|
184
|
+
"react native": "mobile",
|
|
185
|
+
"flutter": "mobile",
|
|
186
|
+
"expo": "mobile",
|
|
187
|
+
// AI / ML
|
|
188
|
+
"openai": "llm",
|
|
189
|
+
"anthropic": "llm",
|
|
190
|
+
"langchain": "llm",
|
|
191
|
+
"llamaindex": "llm",
|
|
192
|
+
"hugging face": "ml",
|
|
193
|
+
"huggingface": "ml",
|
|
194
|
+
"scikit-learn": "ml",
|
|
195
|
+
"sklearn": "ml",
|
|
196
|
+
// Data pipeline
|
|
197
|
+
"apache kafka": "kafka",
|
|
198
|
+
"apache spark": "spark",
|
|
199
|
+
"apache airflow": "airflow",
|
|
200
|
+
// Misc
|
|
201
|
+
"tailwindcss": "tailwind",
|
|
202
|
+
"tw": "tailwind",
|
|
203
|
+
"gql": "graphql",
|
|
204
|
+
"ws": "websockets",
|
|
205
|
+
"socket.io": "websockets",
|
|
206
|
+
"jest": "testing",
|
|
207
|
+
"vitest": "testing",
|
|
208
|
+
"playwright": "testing",
|
|
209
|
+
"cypress": "testing"
|
|
210
|
+
};
|
|
211
|
+
var VOCAB_SET = new Set(VOCABULARY);
|
|
212
|
+
function normalize(tokens) {
|
|
213
|
+
const result = /* @__PURE__ */ new Set();
|
|
214
|
+
for (const raw of tokens) {
|
|
215
|
+
const lower = raw.toLowerCase().trim();
|
|
216
|
+
if (VOCAB_SET.has(lower)) {
|
|
217
|
+
result.add(lower);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const mapped = SYNONYMS[lower];
|
|
221
|
+
if (mapped && VOCAB_SET.has(mapped)) {
|
|
222
|
+
result.add(mapped);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return Array.from(result);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ../../packages/core/src/coastal.ts
|
|
229
|
+
import { readFileSync } from "fs";
|
|
230
|
+
import { join } from "path";
|
|
231
|
+
import { fileURLToPath } from "url";
|
|
232
|
+
|
|
233
|
+
// src/profile.ts
|
|
234
|
+
var TERMINALHIRE_DIR = join2(homedir(), ".terminalhire");
|
|
235
|
+
var PROFILE_FILE = join2(TERMINALHIRE_DIR, "profile.enc");
|
|
236
|
+
var KEY_FILE = join2(TERMINALHIRE_DIR, "key");
|
|
237
|
+
var ALGO = "aes-256-gcm";
|
|
238
|
+
var KEY_BYTES = 32;
|
|
239
|
+
var IV_BYTES = 12;
|
|
240
|
+
async function loadKey() {
|
|
241
|
+
try {
|
|
242
|
+
const kt = await import("keytar");
|
|
243
|
+
const stored = await kt.getPassword("terminalhire", "profile-key");
|
|
244
|
+
if (stored) {
|
|
245
|
+
return Buffer.from(stored, "hex");
|
|
246
|
+
}
|
|
247
|
+
const key2 = randomBytes(KEY_BYTES);
|
|
248
|
+
await kt.setPassword("terminalhire", "profile-key", key2.toString("hex"));
|
|
249
|
+
return key2;
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
mkdirSync(TERMINALHIRE_DIR, { recursive: true });
|
|
253
|
+
if (existsSync(KEY_FILE)) {
|
|
254
|
+
return Buffer.from(readFileSync2(KEY_FILE, "utf8").trim(), "hex");
|
|
255
|
+
}
|
|
256
|
+
const key = randomBytes(KEY_BYTES);
|
|
257
|
+
writeFileSync(KEY_FILE, key.toString("hex"), { mode: 384, encoding: "utf8" });
|
|
258
|
+
return key;
|
|
259
|
+
}
|
|
260
|
+
function encrypt(plaintext, key) {
|
|
261
|
+
const iv = randomBytes(IV_BYTES);
|
|
262
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
263
|
+
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
264
|
+
const tag = cipher.getAuthTag();
|
|
265
|
+
return {
|
|
266
|
+
iv: iv.toString("hex"),
|
|
267
|
+
tag: tag.toString("hex"),
|
|
268
|
+
ciphertext: ct.toString("hex")
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function decrypt(blob, key) {
|
|
272
|
+
const decipher = createDecipheriv(
|
|
273
|
+
ALGO,
|
|
274
|
+
key,
|
|
275
|
+
Buffer.from(blob.iv, "hex")
|
|
276
|
+
);
|
|
277
|
+
decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
|
|
278
|
+
const plain = Buffer.concat([
|
|
279
|
+
decipher.update(Buffer.from(blob.ciphertext, "hex")),
|
|
280
|
+
decipher.final()
|
|
281
|
+
]);
|
|
282
|
+
return plain.toString("utf8");
|
|
283
|
+
}
|
|
284
|
+
function blankProfile() {
|
|
285
|
+
return {
|
|
286
|
+
version: 3,
|
|
287
|
+
skillTags: [],
|
|
288
|
+
tagWeights: {},
|
|
289
|
+
hasEmployerSessions: false,
|
|
290
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
var DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
294
|
+
function recencyDecay(lastSeen) {
|
|
295
|
+
const ageMs = Date.now() - new Date(lastSeen).getTime();
|
|
296
|
+
return Math.pow(0.5, ageMs / DECAY_HALF_LIFE_MS);
|
|
297
|
+
}
|
|
298
|
+
function tagScore(w) {
|
|
299
|
+
return w.count * recencyDecay(w.lastSeen);
|
|
300
|
+
}
|
|
301
|
+
function deriveSkillTags(tagWeights) {
|
|
302
|
+
return Object.entries(tagWeights).filter(([, w]) => w.count >= 1).sort(([, a], [, b]) => tagScore(b) - tagScore(a)).map(([tag]) => tag);
|
|
303
|
+
}
|
|
304
|
+
function migrateTagWeights(profile) {
|
|
305
|
+
if (!profile.tagWeights) {
|
|
306
|
+
profile.tagWeights = {};
|
|
307
|
+
}
|
|
308
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
309
|
+
for (const tag of profile.skillTags) {
|
|
310
|
+
if (!profile.tagWeights[tag]) {
|
|
311
|
+
profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
async function readProfile() {
|
|
316
|
+
if (!existsSync(PROFILE_FILE)) return blankProfile();
|
|
317
|
+
try {
|
|
318
|
+
const key = await loadKey();
|
|
319
|
+
const raw = readFileSync2(PROFILE_FILE, "utf8");
|
|
320
|
+
const blob = JSON.parse(raw);
|
|
321
|
+
const plaintext = decrypt(blob, key);
|
|
322
|
+
const parsed = JSON.parse(plaintext);
|
|
323
|
+
migrateTagWeights(parsed);
|
|
324
|
+
return parsed;
|
|
325
|
+
} catch {
|
|
326
|
+
return blankProfile();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
async function writeProfile(profile) {
|
|
330
|
+
mkdirSync(TERMINALHIRE_DIR, { recursive: true });
|
|
331
|
+
const key = await loadKey();
|
|
332
|
+
profile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
333
|
+
profile.skillTags = deriveSkillTags(profile.tagWeights);
|
|
334
|
+
const blob = encrypt(JSON.stringify(profile), key);
|
|
335
|
+
writeFileSync(PROFILE_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
|
|
336
|
+
}
|
|
337
|
+
var LANGUAGE_TAGS = /* @__PURE__ */ new Set([
|
|
338
|
+
"typescript",
|
|
339
|
+
"javascript",
|
|
340
|
+
"python",
|
|
341
|
+
"go",
|
|
342
|
+
"rust",
|
|
343
|
+
"java",
|
|
344
|
+
"ruby",
|
|
345
|
+
"elixir",
|
|
346
|
+
"scala",
|
|
347
|
+
"kotlin",
|
|
348
|
+
"swift",
|
|
349
|
+
"cpp",
|
|
350
|
+
"csharp",
|
|
351
|
+
"php",
|
|
352
|
+
"haskell",
|
|
353
|
+
"clojure",
|
|
354
|
+
"r"
|
|
355
|
+
]);
|
|
356
|
+
function accumulateSession(profile, tags, isEmployerContext, inferredSeniority) {
|
|
357
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
358
|
+
let filtered = normalize(tags);
|
|
359
|
+
if (isEmployerContext) {
|
|
360
|
+
filtered = filtered.filter((t) => LANGUAGE_TAGS.has(t));
|
|
361
|
+
profile.hasEmployerSessions = true;
|
|
362
|
+
}
|
|
363
|
+
for (const tag of filtered) {
|
|
364
|
+
const existing = profile.tagWeights[tag];
|
|
365
|
+
if (existing) {
|
|
366
|
+
existing.count += 1;
|
|
367
|
+
existing.sessions += 1;
|
|
368
|
+
existing.lastSeen = now;
|
|
369
|
+
} else {
|
|
370
|
+
profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (inferredSeniority && !isEmployerContext) {
|
|
374
|
+
profile.seniority = inferredSeniority;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function accumulateTags(rawTokens, isEmployerContext, inferredSeniority) {
|
|
378
|
+
const profile = await readProfile();
|
|
379
|
+
accumulateSession(profile, rawTokens, isEmployerContext, inferredSeniority);
|
|
380
|
+
await writeProfile(profile);
|
|
381
|
+
}
|
|
382
|
+
function accumulateGitHubTags(profile, tags) {
|
|
383
|
+
accumulateSession(
|
|
384
|
+
profile,
|
|
385
|
+
tags,
|
|
386
|
+
/* isEmployerContext */
|
|
387
|
+
false
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
async function deleteProfile() {
|
|
391
|
+
const { rmSync } = await import("fs");
|
|
392
|
+
try {
|
|
393
|
+
rmSync(PROFILE_FILE);
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
rmSync(KEY_FILE);
|
|
398
|
+
} catch {
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
var MIN_FINGERPRINT_SCORE = 0.05;
|
|
402
|
+
function profileToFingerprint(profile) {
|
|
403
|
+
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);
|
|
404
|
+
const skillTags = rankedTags.length > 0 ? rankedTags : profile.skillTags;
|
|
405
|
+
return {
|
|
406
|
+
skillTags,
|
|
407
|
+
seniorityBand: profile.seniority,
|
|
408
|
+
prefs: {
|
|
409
|
+
roleTypes: profile.roleTypes,
|
|
410
|
+
remoteOnly: profile.remoteOnly,
|
|
411
|
+
compFloorUsd: profile.compFloorUsd
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
export {
|
|
416
|
+
accumulateGitHubTags,
|
|
417
|
+
accumulateSession,
|
|
418
|
+
accumulateTags,
|
|
419
|
+
deleteProfile,
|
|
420
|
+
profileToFingerprint,
|
|
421
|
+
readProfile,
|
|
422
|
+
writeProfile
|
|
423
|
+
};
|