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,625 @@
|
|
|
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/profile.ts
|
|
338
|
+
var profile_exports = {};
|
|
339
|
+
__export(profile_exports, {
|
|
340
|
+
accumulateGitHubTags: () => accumulateGitHubTags,
|
|
341
|
+
accumulateSession: () => accumulateSession,
|
|
342
|
+
accumulateTags: () => accumulateTags,
|
|
343
|
+
deleteProfile: () => deleteProfile,
|
|
344
|
+
profileToFingerprint: () => profileToFingerprint,
|
|
345
|
+
readProfile: () => readProfile,
|
|
346
|
+
writeProfile: () => writeProfile
|
|
347
|
+
});
|
|
348
|
+
import {
|
|
349
|
+
createCipheriv,
|
|
350
|
+
createDecipheriv,
|
|
351
|
+
randomBytes
|
|
352
|
+
} from "crypto";
|
|
353
|
+
import {
|
|
354
|
+
readFileSync as readFileSync2,
|
|
355
|
+
writeFileSync,
|
|
356
|
+
mkdirSync,
|
|
357
|
+
existsSync
|
|
358
|
+
} from "fs";
|
|
359
|
+
import { join as join2 } from "path";
|
|
360
|
+
import { homedir } from "os";
|
|
361
|
+
async function loadKey() {
|
|
362
|
+
try {
|
|
363
|
+
const kt = await import("keytar");
|
|
364
|
+
const stored = await kt.getPassword("terminalhire", "profile-key");
|
|
365
|
+
if (stored) {
|
|
366
|
+
return Buffer.from(stored, "hex");
|
|
367
|
+
}
|
|
368
|
+
const key2 = randomBytes(KEY_BYTES);
|
|
369
|
+
await kt.setPassword("terminalhire", "profile-key", key2.toString("hex"));
|
|
370
|
+
return key2;
|
|
371
|
+
} catch {
|
|
372
|
+
}
|
|
373
|
+
mkdirSync(TERMINALHIRE_DIR, { recursive: true });
|
|
374
|
+
if (existsSync(KEY_FILE)) {
|
|
375
|
+
return Buffer.from(readFileSync2(KEY_FILE, "utf8").trim(), "hex");
|
|
376
|
+
}
|
|
377
|
+
const key = randomBytes(KEY_BYTES);
|
|
378
|
+
writeFileSync(KEY_FILE, key.toString("hex"), { mode: 384, encoding: "utf8" });
|
|
379
|
+
return key;
|
|
380
|
+
}
|
|
381
|
+
function encrypt(plaintext, key) {
|
|
382
|
+
const iv = randomBytes(IV_BYTES);
|
|
383
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
384
|
+
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
385
|
+
const tag = cipher.getAuthTag();
|
|
386
|
+
return {
|
|
387
|
+
iv: iv.toString("hex"),
|
|
388
|
+
tag: tag.toString("hex"),
|
|
389
|
+
ciphertext: ct.toString("hex")
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function decrypt(blob, key) {
|
|
393
|
+
const decipher = createDecipheriv(
|
|
394
|
+
ALGO,
|
|
395
|
+
key,
|
|
396
|
+
Buffer.from(blob.iv, "hex")
|
|
397
|
+
);
|
|
398
|
+
decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
|
|
399
|
+
const plain = Buffer.concat([
|
|
400
|
+
decipher.update(Buffer.from(blob.ciphertext, "hex")),
|
|
401
|
+
decipher.final()
|
|
402
|
+
]);
|
|
403
|
+
return plain.toString("utf8");
|
|
404
|
+
}
|
|
405
|
+
function blankProfile() {
|
|
406
|
+
return {
|
|
407
|
+
version: 3,
|
|
408
|
+
skillTags: [],
|
|
409
|
+
tagWeights: {},
|
|
410
|
+
hasEmployerSessions: false,
|
|
411
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function recencyDecay(lastSeen) {
|
|
415
|
+
const ageMs = Date.now() - new Date(lastSeen).getTime();
|
|
416
|
+
return Math.pow(0.5, ageMs / DECAY_HALF_LIFE_MS);
|
|
417
|
+
}
|
|
418
|
+
function tagScore(w) {
|
|
419
|
+
return w.count * recencyDecay(w.lastSeen);
|
|
420
|
+
}
|
|
421
|
+
function deriveSkillTags(tagWeights) {
|
|
422
|
+
return Object.entries(tagWeights).filter(([, w]) => w.count >= 1).sort(([, a], [, b]) => tagScore(b) - tagScore(a)).map(([tag]) => tag);
|
|
423
|
+
}
|
|
424
|
+
function migrateTagWeights(profile) {
|
|
425
|
+
if (!profile.tagWeights) {
|
|
426
|
+
profile.tagWeights = {};
|
|
427
|
+
}
|
|
428
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
429
|
+
for (const tag of profile.skillTags) {
|
|
430
|
+
if (!profile.tagWeights[tag]) {
|
|
431
|
+
profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async function readProfile() {
|
|
436
|
+
if (!existsSync(PROFILE_FILE)) return blankProfile();
|
|
437
|
+
try {
|
|
438
|
+
const key = await loadKey();
|
|
439
|
+
const raw = readFileSync2(PROFILE_FILE, "utf8");
|
|
440
|
+
const blob = JSON.parse(raw);
|
|
441
|
+
const plaintext = decrypt(blob, key);
|
|
442
|
+
const parsed = JSON.parse(plaintext);
|
|
443
|
+
migrateTagWeights(parsed);
|
|
444
|
+
return parsed;
|
|
445
|
+
} catch {
|
|
446
|
+
return blankProfile();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function writeProfile(profile) {
|
|
450
|
+
mkdirSync(TERMINALHIRE_DIR, { recursive: true });
|
|
451
|
+
const key = await loadKey();
|
|
452
|
+
profile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
453
|
+
profile.skillTags = deriveSkillTags(profile.tagWeights);
|
|
454
|
+
const blob = encrypt(JSON.stringify(profile), key);
|
|
455
|
+
writeFileSync(PROFILE_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
|
|
456
|
+
}
|
|
457
|
+
function accumulateSession(profile, tags, isEmployerContext, inferredSeniority) {
|
|
458
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
459
|
+
let filtered = normalize(tags);
|
|
460
|
+
if (isEmployerContext) {
|
|
461
|
+
filtered = filtered.filter((t) => LANGUAGE_TAGS.has(t));
|
|
462
|
+
profile.hasEmployerSessions = true;
|
|
463
|
+
}
|
|
464
|
+
for (const tag of filtered) {
|
|
465
|
+
const existing = profile.tagWeights[tag];
|
|
466
|
+
if (existing) {
|
|
467
|
+
existing.count += 1;
|
|
468
|
+
existing.sessions += 1;
|
|
469
|
+
existing.lastSeen = now;
|
|
470
|
+
} else {
|
|
471
|
+
profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (inferredSeniority && !isEmployerContext) {
|
|
475
|
+
profile.seniority = inferredSeniority;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async function accumulateTags(rawTokens, isEmployerContext, inferredSeniority) {
|
|
479
|
+
const profile = await readProfile();
|
|
480
|
+
accumulateSession(profile, rawTokens, isEmployerContext, inferredSeniority);
|
|
481
|
+
await writeProfile(profile);
|
|
482
|
+
}
|
|
483
|
+
function accumulateGitHubTags(profile, tags) {
|
|
484
|
+
accumulateSession(
|
|
485
|
+
profile,
|
|
486
|
+
tags,
|
|
487
|
+
/* isEmployerContext */
|
|
488
|
+
false
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
async function deleteProfile() {
|
|
492
|
+
const { rmSync } = await import("fs");
|
|
493
|
+
try {
|
|
494
|
+
rmSync(PROFILE_FILE);
|
|
495
|
+
} catch {
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
rmSync(KEY_FILE);
|
|
499
|
+
} catch {
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function profileToFingerprint(profile) {
|
|
503
|
+
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);
|
|
504
|
+
const skillTags = rankedTags.length > 0 ? rankedTags : profile.skillTags;
|
|
505
|
+
return {
|
|
506
|
+
skillTags,
|
|
507
|
+
seniorityBand: profile.seniority,
|
|
508
|
+
prefs: {
|
|
509
|
+
roleTypes: profile.roleTypes,
|
|
510
|
+
remoteOnly: profile.remoteOnly,
|
|
511
|
+
compFloorUsd: profile.compFloorUsd
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
var TERMINALHIRE_DIR, PROFILE_FILE, KEY_FILE, ALGO, KEY_BYTES, IV_BYTES, DECAY_HALF_LIFE_MS, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
|
|
516
|
+
var init_profile = __esm({
|
|
517
|
+
"src/profile.ts"() {
|
|
518
|
+
"use strict";
|
|
519
|
+
init_src();
|
|
520
|
+
TERMINALHIRE_DIR = join2(homedir(), ".terminalhire");
|
|
521
|
+
PROFILE_FILE = join2(TERMINALHIRE_DIR, "profile.enc");
|
|
522
|
+
KEY_FILE = join2(TERMINALHIRE_DIR, "key");
|
|
523
|
+
ALGO = "aes-256-gcm";
|
|
524
|
+
KEY_BYTES = 32;
|
|
525
|
+
IV_BYTES = 12;
|
|
526
|
+
DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
527
|
+
LANGUAGE_TAGS = /* @__PURE__ */ new Set([
|
|
528
|
+
"typescript",
|
|
529
|
+
"javascript",
|
|
530
|
+
"python",
|
|
531
|
+
"go",
|
|
532
|
+
"rust",
|
|
533
|
+
"java",
|
|
534
|
+
"ruby",
|
|
535
|
+
"elixir",
|
|
536
|
+
"scala",
|
|
537
|
+
"kotlin",
|
|
538
|
+
"swift",
|
|
539
|
+
"cpp",
|
|
540
|
+
"csharp",
|
|
541
|
+
"php",
|
|
542
|
+
"haskell",
|
|
543
|
+
"clojure",
|
|
544
|
+
"r"
|
|
545
|
+
]);
|
|
546
|
+
MIN_FINGERPRINT_SCORE = 0.05;
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// bin/jpi-profile.js
|
|
551
|
+
import { createInterface } from "readline";
|
|
552
|
+
function prompt(question) {
|
|
553
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
554
|
+
return new Promise((resolve) => {
|
|
555
|
+
rl.question(question, (answer) => {
|
|
556
|
+
rl.close();
|
|
557
|
+
resolve(answer.trim());
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
async function run() {
|
|
562
|
+
const { readProfile: readProfile2, writeProfile: writeProfile2, deleteProfile: deleteProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
563
|
+
const args = process.argv.slice(2);
|
|
564
|
+
if (args.includes("--show")) {
|
|
565
|
+
const profile = await readProfile2();
|
|
566
|
+
console.log("\n\u2726 terminalhire local profile (encrypted at rest \u2014 shown here for your review only)\n");
|
|
567
|
+
console.log(" Skill tags: " + (profile.skillTags.length > 0 ? profile.skillTags.join(", ") : "(none yet)"));
|
|
568
|
+
console.log(" Seniority: " + (profile.seniority ?? "(not set)"));
|
|
569
|
+
if (profile.displayName) console.log(" Display name: " + profile.displayName);
|
|
570
|
+
if (profile.contactEmail) console.log(" Contact email: " + profile.contactEmail);
|
|
571
|
+
if (profile.remoteOnly !== void 0) console.log(" Remote only: " + profile.remoteOnly);
|
|
572
|
+
if (profile.compFloorUsd !== void 0) console.log(" Comp floor USD: $" + profile.compFloorUsd);
|
|
573
|
+
console.log(" Employer sessions contributed: " + profile.hasEmployerSessions);
|
|
574
|
+
if (profile.github) {
|
|
575
|
+
console.log("");
|
|
576
|
+
console.log(" GitHub (public data only, scope: read:user):");
|
|
577
|
+
console.log(" Login: @" + profile.github.login);
|
|
578
|
+
console.log(" Profile URL: " + profile.github.profileUrl);
|
|
579
|
+
console.log(" Top languages: " + profile.github.topLanguages.join(", "));
|
|
580
|
+
console.log(" Public repos: " + profile.github.publicRepos);
|
|
581
|
+
console.log("");
|
|
582
|
+
console.log(' GitHub fields are included in a lead ONLY when you consent "yes".');
|
|
583
|
+
console.log(" To disconnect GitHub: terminalhire logout");
|
|
584
|
+
} else {
|
|
585
|
+
console.log("");
|
|
586
|
+
console.log(" GitHub: not connected (run: terminalhire login for instant enrichment)");
|
|
587
|
+
}
|
|
588
|
+
console.log("");
|
|
589
|
+
console.log(" Raw profile JSON:");
|
|
590
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
591
|
+
console.log("\nThis profile NEVER leaves your machine except in a consented lead payload.");
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (args.includes("--delete")) {
|
|
595
|
+
console.log("\nThis will permanently delete your local terminalhire profile and encryption key.");
|
|
596
|
+
const answer = await prompt('Type "yes" to confirm: ');
|
|
597
|
+
if (answer !== "yes") {
|
|
598
|
+
console.log("Aborted.");
|
|
599
|
+
process.exit(0);
|
|
600
|
+
}
|
|
601
|
+
await deleteProfile2();
|
|
602
|
+
console.log("Profile and key deleted from ~/.terminalhire/");
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (args.includes("--edit")) {
|
|
606
|
+
const profile = await readProfile2();
|
|
607
|
+
console.log("\n\u2726 terminalhire profile editor (press Enter to keep current value)\n");
|
|
608
|
+
const name = await prompt(`Display name [${profile.displayName ?? "not set"}]: `);
|
|
609
|
+
if (name) profile.displayName = name;
|
|
610
|
+
const email = await prompt(`Contact email [${profile.contactEmail ?? "not set"}]: `);
|
|
611
|
+
if (email) profile.contactEmail = email;
|
|
612
|
+
const remote = await prompt(`Remote only? (y/n) [${profile.remoteOnly ? "y" : "n"}]: `);
|
|
613
|
+
if (remote === "y") profile.remoteOnly = true;
|
|
614
|
+
if (remote === "n") profile.remoteOnly = false;
|
|
615
|
+
const floor = await prompt(`Comp floor USD [${profile.compFloorUsd ?? "not set"}]: `);
|
|
616
|
+
if (floor && !isNaN(parseInt(floor, 10))) profile.compFloorUsd = parseInt(floor, 10);
|
|
617
|
+
await writeProfile2(profile);
|
|
618
|
+
console.log("\nProfile updated (encrypted at ~/.terminalhire/profile.enc)");
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
console.log("Usage: terminalhire profile --show | --edit | --delete");
|
|
622
|
+
}
|
|
623
|
+
export {
|
|
624
|
+
run
|
|
625
|
+
};
|