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.
@@ -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
+ };