terminalhire 0.14.0 → 0.15.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/dist/bin/jpi-bounties.js +8 -4
- package/dist/bin/jpi-chat-read.js +3 -1
- package/dist/bin/jpi-chat.js +282 -256
- package/dist/bin/jpi-claim.js +1 -1
- package/dist/bin/jpi-config.js +1 -1
- package/dist/bin/jpi-contribute.js +1 -1
- package/dist/bin/jpi-devs.js +8 -4
- package/dist/bin/jpi-dispatch.js +448 -324
- package/dist/bin/jpi-init.js +1 -2
- package/dist/bin/jpi-jobs.js +10 -6
- package/dist/bin/jpi-login.js +8 -5
- package/dist/bin/jpi-project.js +8 -4
- package/dist/bin/jpi-refresh.js +298 -197
- package/dist/bin/jpi-save.js +1 -1
- package/dist/bin/jpi-spinner.js +148 -125
- package/dist/bin/jpi-statusline-launch.js +1 -1
- package/dist/bin/jpi-statusline.js +1 -1
- package/dist/bin/jpi.js +1 -1
- package/dist/bin/spinner.js +220 -185
- package/dist/bin/version-nudge.js +1 -1
- package/install.js +1 -2
- package/package.json +1 -1
package/dist/bin/jpi-bounties.js
CHANGED
|
@@ -2953,11 +2953,14 @@ You control whether this connects \u2014 no contact details are shared unless yo
|
|
|
2953
2953
|
return { subject, text };
|
|
2954
2954
|
}
|
|
2955
2955
|
function introActorRole(intro, actorLogin) {
|
|
2956
|
-
|
|
2957
|
-
if (
|
|
2958
|
-
if (a && a === intro.requesterLogin.trim().toLowerCase()) return "requester";
|
|
2956
|
+
if (sameLogin(actorLogin, intro.targetLogin)) return "target";
|
|
2957
|
+
if (sameLogin(actorLogin, intro.requesterLogin)) return "requester";
|
|
2959
2958
|
return "other";
|
|
2960
2959
|
}
|
|
2960
|
+
function sameLogin(a, b) {
|
|
2961
|
+
const an = a.trim().toLowerCase();
|
|
2962
|
+
return an.length > 0 && an === b.trim().toLowerCase();
|
|
2963
|
+
}
|
|
2961
2964
|
function authorizeIntroDecision(intro, actorLogin) {
|
|
2962
2965
|
const role = introActorRole(intro, actorLogin);
|
|
2963
2966
|
if (role === "target") return { ok: true };
|
|
@@ -6399,6 +6402,7 @@ __export(src_exports, {
|
|
|
6399
6402
|
rejectExtraIntroFields: () => rejectExtraIntroFields,
|
|
6400
6403
|
revealIntroContacts: () => revealIntroContacts,
|
|
6401
6404
|
safetyNumber: () => safetyNumber,
|
|
6405
|
+
sameLogin: () => sameLogin,
|
|
6402
6406
|
setStatus: () => setStatus,
|
|
6403
6407
|
tokenize: () => tokenize,
|
|
6404
6408
|
validateGraph: () => validateGraph,
|
|
@@ -6670,7 +6674,7 @@ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSy
|
|
|
6670
6674
|
import { join as join3 } from "path";
|
|
6671
6675
|
import { homedir as homedir2 } from "os";
|
|
6672
6676
|
import { createInterface } from "readline";
|
|
6673
|
-
var TERMINALHIRE_DIR2 = join3(homedir2(), ".terminalhire");
|
|
6677
|
+
var TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join3(homedir2(), ".terminalhire");
|
|
6674
6678
|
var INDEX_CACHE_FILE = join3(TERMINALHIRE_DIR2, "index-cache.json");
|
|
6675
6679
|
var INDEX_TTL_MS = 15 * 60 * 1e3;
|
|
6676
6680
|
var API_URL = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
|
|
@@ -4456,6 +4456,8 @@ function readReadCursors() {
|
|
|
4456
4456
|
function writeReadCursor(login, iso, deps = {}) {
|
|
4457
4457
|
const read = deps.readReadCursors ?? readReadCursors;
|
|
4458
4458
|
const cursors = read();
|
|
4459
|
+
const prev = cursors[login];
|
|
4460
|
+
if (prev && iso <= prev) return;
|
|
4459
4461
|
cursors[login] = iso;
|
|
4460
4462
|
mkdirSync6(TERMINALHIRE_DIR5, { recursive: true });
|
|
4461
4463
|
writeFileSync6(READS_FILE, JSON.stringify(cursors, null, 2), { mode: 384, encoding: "utf8" });
|
|
@@ -4771,7 +4773,7 @@ var init_jpi_chat_read = __esm({
|
|
|
4771
4773
|
init_jpi_chat();
|
|
4772
4774
|
CHAT_BASE3 = process.env["TERMINALHIRE_API_URL"] || "https://www.terminalhire.com";
|
|
4773
4775
|
GH_SESSION_COOKIE3 = "__jpi_gh_session";
|
|
4774
|
-
TERMINALHIRE_DIR5 = join8(homedir7(), ".terminalhire");
|
|
4776
|
+
TERMINALHIRE_DIR5 = process.env.TERMINALHIRE_DIR || join8(homedir7(), ".terminalhire");
|
|
4775
4777
|
READS_FILE = join8(TERMINALHIRE_DIR5, "chat-reads.json");
|
|
4776
4778
|
INDEX_CACHE_FILE = join8(TERMINALHIRE_DIR5, "index-cache.json");
|
|
4777
4779
|
REACHABLE_DISPLAY = { shareActivity: false, optin: false, lastSeen: null };
|
package/dist/bin/jpi-chat.js
CHANGED
|
@@ -4281,246 +4281,6 @@ var init_config = __esm({
|
|
|
4281
4281
|
}
|
|
4282
4282
|
});
|
|
4283
4283
|
|
|
4284
|
-
// src/profile.ts
|
|
4285
|
-
var profile_exports = {};
|
|
4286
|
-
__export(profile_exports, {
|
|
4287
|
-
accumulateGitHubTags: () => accumulateGitHubTags,
|
|
4288
|
-
accumulateSession: () => accumulateSession,
|
|
4289
|
-
accumulateTags: () => accumulateTags,
|
|
4290
|
-
addSavedJob: () => addSavedJob,
|
|
4291
|
-
deleteProfile: () => deleteProfile,
|
|
4292
|
-
listSavedJobs: () => listSavedJobs,
|
|
4293
|
-
profileToFingerprint: () => profileToFingerprint,
|
|
4294
|
-
readProfile: () => readProfile,
|
|
4295
|
-
removeSavedJob: () => removeSavedJob,
|
|
4296
|
-
writeProfile: () => writeProfile
|
|
4297
|
-
});
|
|
4298
|
-
import {
|
|
4299
|
-
createCipheriv as createCipheriv2,
|
|
4300
|
-
createDecipheriv as createDecipheriv2,
|
|
4301
|
-
randomBytes as randomBytes4
|
|
4302
|
-
} from "crypto";
|
|
4303
|
-
import {
|
|
4304
|
-
readFileSync as readFileSync7,
|
|
4305
|
-
writeFileSync as writeFileSync6,
|
|
4306
|
-
mkdirSync as mkdirSync6,
|
|
4307
|
-
existsSync as existsSync6
|
|
4308
|
-
} from "fs";
|
|
4309
|
-
import { join as join7 } from "path";
|
|
4310
|
-
import { homedir as homedir6 } from "os";
|
|
4311
|
-
async function loadKey2() {
|
|
4312
|
-
try {
|
|
4313
|
-
const kt = await import("keytar");
|
|
4314
|
-
const stored = await kt.getPassword("terminalhire", "profile-key");
|
|
4315
|
-
if (stored) {
|
|
4316
|
-
return Buffer.from(stored, "hex");
|
|
4317
|
-
}
|
|
4318
|
-
const key2 = randomBytes4(KEY_BYTES2);
|
|
4319
|
-
await kt.setPassword("terminalhire", "profile-key", key2.toString("hex"));
|
|
4320
|
-
return key2;
|
|
4321
|
-
} catch {
|
|
4322
|
-
}
|
|
4323
|
-
mkdirSync6(TERMINALHIRE_DIR5, { recursive: true });
|
|
4324
|
-
if (existsSync6(KEY_FILE2)) {
|
|
4325
|
-
return Buffer.from(readFileSync7(KEY_FILE2, "utf8").trim(), "hex");
|
|
4326
|
-
}
|
|
4327
|
-
const key = randomBytes4(KEY_BYTES2);
|
|
4328
|
-
writeFileSync6(KEY_FILE2, key.toString("hex"), { mode: 384, encoding: "utf8" });
|
|
4329
|
-
return key;
|
|
4330
|
-
}
|
|
4331
|
-
function encrypt2(plaintext, key) {
|
|
4332
|
-
const iv = randomBytes4(IV_BYTES2);
|
|
4333
|
-
const cipher = createCipheriv2(ALGO2, key, iv);
|
|
4334
|
-
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
4335
|
-
const tag = cipher.getAuthTag();
|
|
4336
|
-
return {
|
|
4337
|
-
iv: iv.toString("hex"),
|
|
4338
|
-
tag: tag.toString("hex"),
|
|
4339
|
-
ciphertext: ct.toString("hex")
|
|
4340
|
-
};
|
|
4341
|
-
}
|
|
4342
|
-
function decrypt2(blob, key) {
|
|
4343
|
-
const decipher = createDecipheriv2(
|
|
4344
|
-
ALGO2,
|
|
4345
|
-
key,
|
|
4346
|
-
Buffer.from(blob.iv, "hex")
|
|
4347
|
-
);
|
|
4348
|
-
decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
|
|
4349
|
-
const plain = Buffer.concat([
|
|
4350
|
-
decipher.update(Buffer.from(blob.ciphertext, "hex")),
|
|
4351
|
-
decipher.final()
|
|
4352
|
-
]);
|
|
4353
|
-
return plain.toString("utf8");
|
|
4354
|
-
}
|
|
4355
|
-
function blankProfile() {
|
|
4356
|
-
return {
|
|
4357
|
-
version: 3,
|
|
4358
|
-
skillTags: [],
|
|
4359
|
-
tagWeights: {},
|
|
4360
|
-
hasEmployerSessions: false,
|
|
4361
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4362
|
-
};
|
|
4363
|
-
}
|
|
4364
|
-
function recencyDecay(lastSeen) {
|
|
4365
|
-
const ageMs = Date.now() - new Date(lastSeen).getTime();
|
|
4366
|
-
return Math.pow(0.5, ageMs / DECAY_HALF_LIFE_MS);
|
|
4367
|
-
}
|
|
4368
|
-
function tagScore(w) {
|
|
4369
|
-
return w.count * recencyDecay(w.lastSeen);
|
|
4370
|
-
}
|
|
4371
|
-
function deriveSkillTags(tagWeights) {
|
|
4372
|
-
return Object.entries(tagWeights).filter(([, w]) => w.count >= 1).sort(([, a], [, b]) => tagScore(b) - tagScore(a)).map(([tag]) => tag);
|
|
4373
|
-
}
|
|
4374
|
-
function migrateTagWeights(profile) {
|
|
4375
|
-
if (!profile.tagWeights) {
|
|
4376
|
-
profile.tagWeights = {};
|
|
4377
|
-
}
|
|
4378
|
-
const seed = profile.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4379
|
-
for (const tag of profile.skillTags) {
|
|
4380
|
-
if (!profile.tagWeights[tag]) {
|
|
4381
|
-
profile.tagWeights[tag] = { count: 1, firstSeen: seed, lastSeen: seed, sessions: 1 };
|
|
4382
|
-
}
|
|
4383
|
-
}
|
|
4384
|
-
}
|
|
4385
|
-
async function readProfile() {
|
|
4386
|
-
if (!existsSync6(PROFILE_FILE)) return blankProfile();
|
|
4387
|
-
try {
|
|
4388
|
-
const key = await loadKey2();
|
|
4389
|
-
const raw = readFileSync7(PROFILE_FILE, "utf8");
|
|
4390
|
-
const blob = JSON.parse(raw);
|
|
4391
|
-
const plaintext = decrypt2(blob, key);
|
|
4392
|
-
const parsed = JSON.parse(plaintext);
|
|
4393
|
-
migrateTagWeights(parsed);
|
|
4394
|
-
return parsed;
|
|
4395
|
-
} catch {
|
|
4396
|
-
return blankProfile();
|
|
4397
|
-
}
|
|
4398
|
-
}
|
|
4399
|
-
async function writeProfile(profile) {
|
|
4400
|
-
mkdirSync6(TERMINALHIRE_DIR5, { recursive: true });
|
|
4401
|
-
const key = await loadKey2();
|
|
4402
|
-
profile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4403
|
-
profile.skillTags = deriveSkillTags(profile.tagWeights);
|
|
4404
|
-
const blob = encrypt2(JSON.stringify(profile), key);
|
|
4405
|
-
writeFileSync6(PROFILE_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
|
|
4406
|
-
}
|
|
4407
|
-
function accumulateSession(profile, tags, isEmployerContext, inferredSeniority, seniorityIsAuthoritative = false) {
|
|
4408
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4409
|
-
let filtered = normalize(tags);
|
|
4410
|
-
if (isEmployerContext) {
|
|
4411
|
-
filtered = filtered.filter((t) => LANGUAGE_TAGS.has(t));
|
|
4412
|
-
profile.hasEmployerSessions = true;
|
|
4413
|
-
}
|
|
4414
|
-
for (const tag of filtered) {
|
|
4415
|
-
const existing = profile.tagWeights[tag];
|
|
4416
|
-
if (existing) {
|
|
4417
|
-
existing.count += 1;
|
|
4418
|
-
existing.sessions += 1;
|
|
4419
|
-
existing.lastSeen = now;
|
|
4420
|
-
} else {
|
|
4421
|
-
profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
|
|
4422
|
-
}
|
|
4423
|
-
}
|
|
4424
|
-
if (inferredSeniority && !isEmployerContext) {
|
|
4425
|
-
if (seniorityIsAuthoritative || !profile.github) {
|
|
4426
|
-
profile.seniority = inferredSeniority;
|
|
4427
|
-
}
|
|
4428
|
-
}
|
|
4429
|
-
}
|
|
4430
|
-
async function accumulateTags(rawTokens, isEmployerContext, inferredSeniority) {
|
|
4431
|
-
const profile = await readProfile();
|
|
4432
|
-
accumulateSession(profile, rawTokens, isEmployerContext, inferredSeniority);
|
|
4433
|
-
await writeProfile(profile);
|
|
4434
|
-
}
|
|
4435
|
-
function accumulateGitHubTags(profile, tags, inferredSeniority) {
|
|
4436
|
-
accumulateSession(
|
|
4437
|
-
profile,
|
|
4438
|
-
tags,
|
|
4439
|
-
/* isEmployerContext */
|
|
4440
|
-
false,
|
|
4441
|
-
inferredSeniority,
|
|
4442
|
-
true
|
|
4443
|
-
);
|
|
4444
|
-
}
|
|
4445
|
-
async function listSavedJobs() {
|
|
4446
|
-
const profile = await readProfile();
|
|
4447
|
-
return profile.savedJobs ?? [];
|
|
4448
|
-
}
|
|
4449
|
-
async function addSavedJob(job) {
|
|
4450
|
-
const profile = await readProfile();
|
|
4451
|
-
const existing = profile.savedJobs ?? [];
|
|
4452
|
-
const filtered = existing.filter((j) => j.id !== job.id);
|
|
4453
|
-
profile.savedJobs = [...filtered, { ...job, savedAt: (/* @__PURE__ */ new Date()).toISOString() }];
|
|
4454
|
-
await writeProfile(profile);
|
|
4455
|
-
}
|
|
4456
|
-
async function removeSavedJob(id) {
|
|
4457
|
-
const profile = await readProfile();
|
|
4458
|
-
const existing = profile.savedJobs ?? [];
|
|
4459
|
-
const filtered = existing.filter((j) => j.id !== id);
|
|
4460
|
-
if (filtered.length === existing.length) return false;
|
|
4461
|
-
profile.savedJobs = filtered;
|
|
4462
|
-
await writeProfile(profile);
|
|
4463
|
-
return true;
|
|
4464
|
-
}
|
|
4465
|
-
async function deleteProfile() {
|
|
4466
|
-
const { rmSync: rmSync4 } = await import("fs");
|
|
4467
|
-
try {
|
|
4468
|
-
rmSync4(PROFILE_FILE);
|
|
4469
|
-
} catch {
|
|
4470
|
-
}
|
|
4471
|
-
try {
|
|
4472
|
-
rmSync4(KEY_FILE2);
|
|
4473
|
-
} catch {
|
|
4474
|
-
}
|
|
4475
|
-
}
|
|
4476
|
-
function profileToFingerprint(profile) {
|
|
4477
|
-
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);
|
|
4478
|
-
const skillTags = rankedTags.length > 0 ? rankedTags : profile.skillTags;
|
|
4479
|
-
return {
|
|
4480
|
-
skillTags,
|
|
4481
|
-
seniorityBand: profile.seniority,
|
|
4482
|
-
prefs: {
|
|
4483
|
-
roleTypes: profile.roleTypes,
|
|
4484
|
-
remoteOnly: profile.remoteOnly,
|
|
4485
|
-
compFloorUsd: profile.compFloorUsd
|
|
4486
|
-
}
|
|
4487
|
-
};
|
|
4488
|
-
}
|
|
4489
|
-
var TERMINALHIRE_DIR5, PROFILE_FILE, KEY_FILE2, ALGO2, KEY_BYTES2, IV_BYTES2, DECAY_HALF_LIFE_MS, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
|
|
4490
|
-
var init_profile = __esm({
|
|
4491
|
-
"src/profile.ts"() {
|
|
4492
|
-
"use strict";
|
|
4493
|
-
init_src();
|
|
4494
|
-
TERMINALHIRE_DIR5 = join7(homedir6(), ".terminalhire");
|
|
4495
|
-
PROFILE_FILE = join7(TERMINALHIRE_DIR5, "profile.enc");
|
|
4496
|
-
KEY_FILE2 = join7(TERMINALHIRE_DIR5, "key");
|
|
4497
|
-
ALGO2 = "aes-256-gcm";
|
|
4498
|
-
KEY_BYTES2 = 32;
|
|
4499
|
-
IV_BYTES2 = 12;
|
|
4500
|
-
DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
4501
|
-
LANGUAGE_TAGS = /* @__PURE__ */ new Set([
|
|
4502
|
-
"typescript",
|
|
4503
|
-
"javascript",
|
|
4504
|
-
"python",
|
|
4505
|
-
"go",
|
|
4506
|
-
"rust",
|
|
4507
|
-
"java",
|
|
4508
|
-
"ruby",
|
|
4509
|
-
"elixir",
|
|
4510
|
-
"scala",
|
|
4511
|
-
"kotlin",
|
|
4512
|
-
"swift",
|
|
4513
|
-
"cpp",
|
|
4514
|
-
"csharp",
|
|
4515
|
-
"php",
|
|
4516
|
-
"haskell",
|
|
4517
|
-
"clojure",
|
|
4518
|
-
"r"
|
|
4519
|
-
]);
|
|
4520
|
-
MIN_FINGERPRINT_SCORE = 0.05;
|
|
4521
|
-
}
|
|
4522
|
-
});
|
|
4523
|
-
|
|
4524
4284
|
// bin/jpi-chat-read.js
|
|
4525
4285
|
var jpi_chat_read_exports = {};
|
|
4526
4286
|
__export(jpi_chat_read_exports, {
|
|
@@ -4536,16 +4296,16 @@ __export(jpi_chat_read_exports, {
|
|
|
4536
4296
|
syncUnreadBadge: () => syncUnreadBadge,
|
|
4537
4297
|
writeReadCursor: () => writeReadCursor
|
|
4538
4298
|
});
|
|
4539
|
-
import { existsSync as
|
|
4540
|
-
import { homedir as
|
|
4541
|
-
import { join as
|
|
4299
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
4300
|
+
import { homedir as homedir6 } from "os";
|
|
4301
|
+
import { join as join7 } from "path";
|
|
4542
4302
|
async function syncUnreadBadge(deps = {}) {
|
|
4543
4303
|
const readCookie = deps.readCookie ?? readWebSessionCookie;
|
|
4544
4304
|
const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
|
|
4545
4305
|
const cacheFile = deps.cacheFile ?? INDEX_CACHE_FILE;
|
|
4546
4306
|
try {
|
|
4547
4307
|
const cookie = readCookie();
|
|
4548
|
-
if (!cookie || !
|
|
4308
|
+
if (!cookie || !existsSync6(cacheFile)) return;
|
|
4549
4309
|
const res = await fetchImpl(`${CHAT_BASE2}/api/chat/inbox`, {
|
|
4550
4310
|
method: "GET",
|
|
4551
4311
|
headers: { Cookie: `${GH_SESSION_COOKIE2}=${cookie}` },
|
|
@@ -4558,16 +4318,16 @@ async function syncUnreadBadge(deps = {}) {
|
|
|
4558
4318
|
(sum, it) => sum + (it && typeof it.unreadCount === "number" && it.unreadCount > 0 ? it.unreadCount : 0),
|
|
4559
4319
|
0
|
|
4560
4320
|
);
|
|
4561
|
-
const entry = JSON.parse(
|
|
4321
|
+
const entry = JSON.parse(readFileSync7(cacheFile, "utf8"));
|
|
4562
4322
|
entry.unreadChat = { count: total };
|
|
4563
|
-
|
|
4323
|
+
writeFileSync6(cacheFile, JSON.stringify(entry), "utf8");
|
|
4564
4324
|
} catch {
|
|
4565
4325
|
}
|
|
4566
4326
|
}
|
|
4567
4327
|
function readReadCursors() {
|
|
4568
4328
|
try {
|
|
4569
|
-
if (!
|
|
4570
|
-
const parsed = JSON.parse(
|
|
4329
|
+
if (!existsSync6(READS_FILE)) return {};
|
|
4330
|
+
const parsed = JSON.parse(readFileSync7(READS_FILE, "utf8"));
|
|
4571
4331
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
4572
4332
|
const out = {};
|
|
4573
4333
|
for (const [login, iso] of Object.entries(parsed)) {
|
|
@@ -4581,9 +4341,11 @@ function readReadCursors() {
|
|
|
4581
4341
|
function writeReadCursor(login, iso, deps = {}) {
|
|
4582
4342
|
const read = deps.readReadCursors ?? readReadCursors;
|
|
4583
4343
|
const cursors = read();
|
|
4344
|
+
const prev = cursors[login];
|
|
4345
|
+
if (prev && iso <= prev) return;
|
|
4584
4346
|
cursors[login] = iso;
|
|
4585
|
-
|
|
4586
|
-
|
|
4347
|
+
mkdirSync6(TERMINALHIRE_DIR5, { recursive: true });
|
|
4348
|
+
writeFileSync6(READS_FILE, JSON.stringify(cursors, null, 2), { mode: 384, encoding: "utf8" });
|
|
4587
4349
|
}
|
|
4588
4350
|
async function postReadCursor(peerLogin, lastReadAt, deps = {}) {
|
|
4589
4351
|
const readCookie = deps.readCookie ?? readWebSessionCookie;
|
|
@@ -4888,7 +4650,7 @@ async function runSend(opts = {}) {
|
|
|
4888
4650
|
);
|
|
4889
4651
|
return { ok: true };
|
|
4890
4652
|
}
|
|
4891
|
-
var CHAT_BASE2, GH_SESSION_COOKIE2,
|
|
4653
|
+
var CHAT_BASE2, GH_SESSION_COOKIE2, TERMINALHIRE_DIR5, READS_FILE, INDEX_CACHE_FILE, REACHABLE_DISPLAY;
|
|
4892
4654
|
var init_jpi_chat_read = __esm({
|
|
4893
4655
|
"bin/jpi-chat-read.js"() {
|
|
4894
4656
|
"use strict";
|
|
@@ -4897,13 +4659,253 @@ var init_jpi_chat_read = __esm({
|
|
|
4897
4659
|
init_jpi_chat();
|
|
4898
4660
|
CHAT_BASE2 = process.env["TERMINALHIRE_API_URL"] || "https://www.terminalhire.com";
|
|
4899
4661
|
GH_SESSION_COOKIE2 = "__jpi_gh_session";
|
|
4900
|
-
|
|
4901
|
-
READS_FILE =
|
|
4902
|
-
INDEX_CACHE_FILE =
|
|
4662
|
+
TERMINALHIRE_DIR5 = process.env.TERMINALHIRE_DIR || join7(homedir6(), ".terminalhire");
|
|
4663
|
+
READS_FILE = join7(TERMINALHIRE_DIR5, "chat-reads.json");
|
|
4664
|
+
INDEX_CACHE_FILE = join7(TERMINALHIRE_DIR5, "index-cache.json");
|
|
4903
4665
|
REACHABLE_DISPLAY = { shareActivity: false, optin: false, lastSeen: null };
|
|
4904
4666
|
}
|
|
4905
4667
|
});
|
|
4906
4668
|
|
|
4669
|
+
// src/profile.ts
|
|
4670
|
+
var profile_exports = {};
|
|
4671
|
+
__export(profile_exports, {
|
|
4672
|
+
accumulateGitHubTags: () => accumulateGitHubTags,
|
|
4673
|
+
accumulateSession: () => accumulateSession,
|
|
4674
|
+
accumulateTags: () => accumulateTags,
|
|
4675
|
+
addSavedJob: () => addSavedJob,
|
|
4676
|
+
deleteProfile: () => deleteProfile,
|
|
4677
|
+
listSavedJobs: () => listSavedJobs,
|
|
4678
|
+
profileToFingerprint: () => profileToFingerprint,
|
|
4679
|
+
readProfile: () => readProfile,
|
|
4680
|
+
removeSavedJob: () => removeSavedJob,
|
|
4681
|
+
writeProfile: () => writeProfile
|
|
4682
|
+
});
|
|
4683
|
+
import {
|
|
4684
|
+
createCipheriv as createCipheriv2,
|
|
4685
|
+
createDecipheriv as createDecipheriv2,
|
|
4686
|
+
randomBytes as randomBytes4
|
|
4687
|
+
} from "crypto";
|
|
4688
|
+
import {
|
|
4689
|
+
readFileSync as readFileSync8,
|
|
4690
|
+
writeFileSync as writeFileSync7,
|
|
4691
|
+
mkdirSync as mkdirSync7,
|
|
4692
|
+
existsSync as existsSync7
|
|
4693
|
+
} from "fs";
|
|
4694
|
+
import { join as join8 } from "path";
|
|
4695
|
+
import { homedir as homedir7 } from "os";
|
|
4696
|
+
async function loadKey2() {
|
|
4697
|
+
try {
|
|
4698
|
+
const kt = await import("keytar");
|
|
4699
|
+
const stored = await kt.getPassword("terminalhire", "profile-key");
|
|
4700
|
+
if (stored) {
|
|
4701
|
+
return Buffer.from(stored, "hex");
|
|
4702
|
+
}
|
|
4703
|
+
const key2 = randomBytes4(KEY_BYTES2);
|
|
4704
|
+
await kt.setPassword("terminalhire", "profile-key", key2.toString("hex"));
|
|
4705
|
+
return key2;
|
|
4706
|
+
} catch {
|
|
4707
|
+
}
|
|
4708
|
+
mkdirSync7(TERMINALHIRE_DIR6, { recursive: true });
|
|
4709
|
+
if (existsSync7(KEY_FILE2)) {
|
|
4710
|
+
return Buffer.from(readFileSync8(KEY_FILE2, "utf8").trim(), "hex");
|
|
4711
|
+
}
|
|
4712
|
+
const key = randomBytes4(KEY_BYTES2);
|
|
4713
|
+
writeFileSync7(KEY_FILE2, key.toString("hex"), { mode: 384, encoding: "utf8" });
|
|
4714
|
+
return key;
|
|
4715
|
+
}
|
|
4716
|
+
function encrypt2(plaintext, key) {
|
|
4717
|
+
const iv = randomBytes4(IV_BYTES2);
|
|
4718
|
+
const cipher = createCipheriv2(ALGO2, key, iv);
|
|
4719
|
+
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
4720
|
+
const tag = cipher.getAuthTag();
|
|
4721
|
+
return {
|
|
4722
|
+
iv: iv.toString("hex"),
|
|
4723
|
+
tag: tag.toString("hex"),
|
|
4724
|
+
ciphertext: ct.toString("hex")
|
|
4725
|
+
};
|
|
4726
|
+
}
|
|
4727
|
+
function decrypt2(blob, key) {
|
|
4728
|
+
const decipher = createDecipheriv2(
|
|
4729
|
+
ALGO2,
|
|
4730
|
+
key,
|
|
4731
|
+
Buffer.from(blob.iv, "hex")
|
|
4732
|
+
);
|
|
4733
|
+
decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
|
|
4734
|
+
const plain = Buffer.concat([
|
|
4735
|
+
decipher.update(Buffer.from(blob.ciphertext, "hex")),
|
|
4736
|
+
decipher.final()
|
|
4737
|
+
]);
|
|
4738
|
+
return plain.toString("utf8");
|
|
4739
|
+
}
|
|
4740
|
+
function blankProfile() {
|
|
4741
|
+
return {
|
|
4742
|
+
version: 3,
|
|
4743
|
+
skillTags: [],
|
|
4744
|
+
tagWeights: {},
|
|
4745
|
+
hasEmployerSessions: false,
|
|
4746
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4747
|
+
};
|
|
4748
|
+
}
|
|
4749
|
+
function recencyDecay(lastSeen) {
|
|
4750
|
+
const ageMs = Date.now() - new Date(lastSeen).getTime();
|
|
4751
|
+
return Math.pow(0.5, ageMs / DECAY_HALF_LIFE_MS);
|
|
4752
|
+
}
|
|
4753
|
+
function tagScore(w) {
|
|
4754
|
+
return w.count * recencyDecay(w.lastSeen);
|
|
4755
|
+
}
|
|
4756
|
+
function deriveSkillTags(tagWeights) {
|
|
4757
|
+
return Object.entries(tagWeights).filter(([, w]) => w.count >= 1).sort(([, a], [, b]) => tagScore(b) - tagScore(a)).map(([tag]) => tag);
|
|
4758
|
+
}
|
|
4759
|
+
function migrateTagWeights(profile) {
|
|
4760
|
+
if (!profile.tagWeights) {
|
|
4761
|
+
profile.tagWeights = {};
|
|
4762
|
+
}
|
|
4763
|
+
const seed = profile.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4764
|
+
for (const tag of profile.skillTags) {
|
|
4765
|
+
if (!profile.tagWeights[tag]) {
|
|
4766
|
+
profile.tagWeights[tag] = { count: 1, firstSeen: seed, lastSeen: seed, sessions: 1 };
|
|
4767
|
+
}
|
|
4768
|
+
}
|
|
4769
|
+
}
|
|
4770
|
+
async function readProfile() {
|
|
4771
|
+
if (!existsSync7(PROFILE_FILE)) return blankProfile();
|
|
4772
|
+
try {
|
|
4773
|
+
const key = await loadKey2();
|
|
4774
|
+
const raw = readFileSync8(PROFILE_FILE, "utf8");
|
|
4775
|
+
const blob = JSON.parse(raw);
|
|
4776
|
+
const plaintext = decrypt2(blob, key);
|
|
4777
|
+
const parsed = JSON.parse(plaintext);
|
|
4778
|
+
migrateTagWeights(parsed);
|
|
4779
|
+
return parsed;
|
|
4780
|
+
} catch {
|
|
4781
|
+
return blankProfile();
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4784
|
+
async function writeProfile(profile) {
|
|
4785
|
+
mkdirSync7(TERMINALHIRE_DIR6, { recursive: true });
|
|
4786
|
+
const key = await loadKey2();
|
|
4787
|
+
profile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4788
|
+
profile.skillTags = deriveSkillTags(profile.tagWeights);
|
|
4789
|
+
const blob = encrypt2(JSON.stringify(profile), key);
|
|
4790
|
+
writeFileSync7(PROFILE_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
|
|
4791
|
+
}
|
|
4792
|
+
function accumulateSession(profile, tags, isEmployerContext, inferredSeniority, seniorityIsAuthoritative = false) {
|
|
4793
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4794
|
+
let filtered = normalize(tags);
|
|
4795
|
+
if (isEmployerContext) {
|
|
4796
|
+
filtered = filtered.filter((t) => LANGUAGE_TAGS.has(t));
|
|
4797
|
+
profile.hasEmployerSessions = true;
|
|
4798
|
+
}
|
|
4799
|
+
for (const tag of filtered) {
|
|
4800
|
+
const existing = profile.tagWeights[tag];
|
|
4801
|
+
if (existing) {
|
|
4802
|
+
existing.count += 1;
|
|
4803
|
+
existing.sessions += 1;
|
|
4804
|
+
existing.lastSeen = now;
|
|
4805
|
+
} else {
|
|
4806
|
+
profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
|
|
4807
|
+
}
|
|
4808
|
+
}
|
|
4809
|
+
if (inferredSeniority && !isEmployerContext) {
|
|
4810
|
+
if (seniorityIsAuthoritative || !profile.github) {
|
|
4811
|
+
profile.seniority = inferredSeniority;
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
async function accumulateTags(rawTokens, isEmployerContext, inferredSeniority) {
|
|
4816
|
+
const profile = await readProfile();
|
|
4817
|
+
accumulateSession(profile, rawTokens, isEmployerContext, inferredSeniority);
|
|
4818
|
+
await writeProfile(profile);
|
|
4819
|
+
}
|
|
4820
|
+
function accumulateGitHubTags(profile, tags, inferredSeniority) {
|
|
4821
|
+
accumulateSession(
|
|
4822
|
+
profile,
|
|
4823
|
+
tags,
|
|
4824
|
+
/* isEmployerContext */
|
|
4825
|
+
false,
|
|
4826
|
+
inferredSeniority,
|
|
4827
|
+
true
|
|
4828
|
+
);
|
|
4829
|
+
}
|
|
4830
|
+
async function listSavedJobs() {
|
|
4831
|
+
const profile = await readProfile();
|
|
4832
|
+
return profile.savedJobs ?? [];
|
|
4833
|
+
}
|
|
4834
|
+
async function addSavedJob(job) {
|
|
4835
|
+
const profile = await readProfile();
|
|
4836
|
+
const existing = profile.savedJobs ?? [];
|
|
4837
|
+
const filtered = existing.filter((j) => j.id !== job.id);
|
|
4838
|
+
profile.savedJobs = [...filtered, { ...job, savedAt: (/* @__PURE__ */ new Date()).toISOString() }];
|
|
4839
|
+
await writeProfile(profile);
|
|
4840
|
+
}
|
|
4841
|
+
async function removeSavedJob(id) {
|
|
4842
|
+
const profile = await readProfile();
|
|
4843
|
+
const existing = profile.savedJobs ?? [];
|
|
4844
|
+
const filtered = existing.filter((j) => j.id !== id);
|
|
4845
|
+
if (filtered.length === existing.length) return false;
|
|
4846
|
+
profile.savedJobs = filtered;
|
|
4847
|
+
await writeProfile(profile);
|
|
4848
|
+
return true;
|
|
4849
|
+
}
|
|
4850
|
+
async function deleteProfile() {
|
|
4851
|
+
const { rmSync: rmSync4 } = await import("fs");
|
|
4852
|
+
try {
|
|
4853
|
+
rmSync4(PROFILE_FILE);
|
|
4854
|
+
} catch {
|
|
4855
|
+
}
|
|
4856
|
+
try {
|
|
4857
|
+
rmSync4(KEY_FILE2);
|
|
4858
|
+
} catch {
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
function profileToFingerprint(profile) {
|
|
4862
|
+
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);
|
|
4863
|
+
const skillTags = rankedTags.length > 0 ? rankedTags : profile.skillTags;
|
|
4864
|
+
return {
|
|
4865
|
+
skillTags,
|
|
4866
|
+
seniorityBand: profile.seniority,
|
|
4867
|
+
prefs: {
|
|
4868
|
+
roleTypes: profile.roleTypes,
|
|
4869
|
+
remoteOnly: profile.remoteOnly,
|
|
4870
|
+
compFloorUsd: profile.compFloorUsd
|
|
4871
|
+
}
|
|
4872
|
+
};
|
|
4873
|
+
}
|
|
4874
|
+
var TERMINALHIRE_DIR6, PROFILE_FILE, KEY_FILE2, ALGO2, KEY_BYTES2, IV_BYTES2, DECAY_HALF_LIFE_MS, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
|
|
4875
|
+
var init_profile = __esm({
|
|
4876
|
+
"src/profile.ts"() {
|
|
4877
|
+
"use strict";
|
|
4878
|
+
init_src();
|
|
4879
|
+
TERMINALHIRE_DIR6 = join8(homedir7(), ".terminalhire");
|
|
4880
|
+
PROFILE_FILE = join8(TERMINALHIRE_DIR6, "profile.enc");
|
|
4881
|
+
KEY_FILE2 = join8(TERMINALHIRE_DIR6, "key");
|
|
4882
|
+
ALGO2 = "aes-256-gcm";
|
|
4883
|
+
KEY_BYTES2 = 32;
|
|
4884
|
+
IV_BYTES2 = 12;
|
|
4885
|
+
DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
4886
|
+
LANGUAGE_TAGS = /* @__PURE__ */ new Set([
|
|
4887
|
+
"typescript",
|
|
4888
|
+
"javascript",
|
|
4889
|
+
"python",
|
|
4890
|
+
"go",
|
|
4891
|
+
"rust",
|
|
4892
|
+
"java",
|
|
4893
|
+
"ruby",
|
|
4894
|
+
"elixir",
|
|
4895
|
+
"scala",
|
|
4896
|
+
"kotlin",
|
|
4897
|
+
"swift",
|
|
4898
|
+
"cpp",
|
|
4899
|
+
"csharp",
|
|
4900
|
+
"php",
|
|
4901
|
+
"haskell",
|
|
4902
|
+
"clojure",
|
|
4903
|
+
"r"
|
|
4904
|
+
]);
|
|
4905
|
+
MIN_FINGERPRINT_SCORE = 0.05;
|
|
4906
|
+
}
|
|
4907
|
+
});
|
|
4908
|
+
|
|
4907
4909
|
// bin/jpi-chat.js
|
|
4908
4910
|
import { createInterface } from "readline";
|
|
4909
4911
|
import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
|
|
@@ -5075,7 +5077,7 @@ function mergeMessages(existing, incoming) {
|
|
|
5075
5077
|
}
|
|
5076
5078
|
function readCachedSessionStale() {
|
|
5077
5079
|
try {
|
|
5078
|
-
const p = join9(homedir8(), ".terminalhire", "index-cache.json");
|
|
5080
|
+
const p = join9(process.env.TERMINALHIRE_DIR || join9(homedir8(), ".terminalhire"), "index-cache.json");
|
|
5079
5081
|
if (!existsSync8(p)) return false;
|
|
5080
5082
|
const cache = JSON.parse(readFileSync9(p, "utf8"));
|
|
5081
5083
|
return cache?.sessionStale === true;
|
|
@@ -5083,6 +5085,14 @@ function readCachedSessionStale() {
|
|
|
5083
5085
|
return false;
|
|
5084
5086
|
}
|
|
5085
5087
|
}
|
|
5088
|
+
async function defaultMarkThreadRead(peerLogin, iso) {
|
|
5089
|
+
const { writeReadCursor: writeReadCursor2, postReadCursor: postReadCursor2, syncUnreadBadge: syncUnreadBadge2 } = await Promise.resolve().then(() => (init_jpi_chat_read(), jpi_chat_read_exports));
|
|
5090
|
+
writeReadCursor2(peerLogin, iso);
|
|
5091
|
+
await syncUnreadBadge2().catch(() => {
|
|
5092
|
+
});
|
|
5093
|
+
await postReadCursor2(peerLogin, iso).catch(() => {
|
|
5094
|
+
});
|
|
5095
|
+
}
|
|
5086
5096
|
async function runChatPane(opts = {}) {
|
|
5087
5097
|
const {
|
|
5088
5098
|
login,
|
|
@@ -5093,7 +5103,8 @@ async function runChatPane(opts = {}) {
|
|
|
5093
5103
|
signals = process,
|
|
5094
5104
|
pollIntervalMs = 1500,
|
|
5095
5105
|
setTimer = (fn, ms) => setInterval(fn, ms),
|
|
5096
|
-
clearTimer = (t) => clearInterval(t)
|
|
5106
|
+
clearTimer = (t) => clearInterval(t),
|
|
5107
|
+
markRead = defaultMarkThreadRead
|
|
5097
5108
|
} = opts;
|
|
5098
5109
|
const target = String(login ?? "").replace(/^@/, "").trim();
|
|
5099
5110
|
if (!target) {
|
|
@@ -5210,6 +5221,7 @@ async function runChatPane(opts = {}) {
|
|
|
5210
5221
|
let presence = null;
|
|
5211
5222
|
let banner = "";
|
|
5212
5223
|
let lastSeen;
|
|
5224
|
+
let lastReadMarked;
|
|
5213
5225
|
let timer = null;
|
|
5214
5226
|
let polling = false;
|
|
5215
5227
|
let cleaned = false;
|
|
@@ -5281,6 +5293,20 @@ async function runChatPane(opts = {}) {
|
|
|
5281
5293
|
messages = mergeMessages(messages, fresh);
|
|
5282
5294
|
const newest = fresh[fresh.length - 1];
|
|
5283
5295
|
if (newest && newest.createdAt) lastSeen = newest.createdAt;
|
|
5296
|
+
let newestIncoming;
|
|
5297
|
+
for (const m of fresh) {
|
|
5298
|
+
if (m && m.senderLogin === peerLogin && m.createdAt && (!newestIncoming || m.createdAt > newestIncoming)) {
|
|
5299
|
+
newestIncoming = m.createdAt;
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
5302
|
+
if (newestIncoming && (!lastReadMarked || newestIncoming > lastReadMarked)) {
|
|
5303
|
+
lastReadMarked = newestIncoming;
|
|
5304
|
+
try {
|
|
5305
|
+
void Promise.resolve(markRead(peerLogin, newestIncoming)).catch(() => {
|
|
5306
|
+
});
|
|
5307
|
+
} catch {
|
|
5308
|
+
}
|
|
5309
|
+
}
|
|
5284
5310
|
if (banner && !banner.startsWith("\u26A0")) banner = "";
|
|
5285
5311
|
}
|
|
5286
5312
|
try {
|
package/dist/bin/jpi-claim.js
CHANGED
|
@@ -109,7 +109,7 @@ import { homedir as homedir2 } from "os";
|
|
|
109
109
|
import { execFile } from "child_process";
|
|
110
110
|
import { promisify } from "util";
|
|
111
111
|
import { createInterface } from "readline";
|
|
112
|
-
var TERMINALHIRE_DIR2 = join2(homedir2(), ".terminalhire");
|
|
112
|
+
var TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join2(homedir2(), ".terminalhire");
|
|
113
113
|
var INDEX_CACHE_FILE = join2(TERMINALHIRE_DIR2, "index-cache.json");
|
|
114
114
|
var GH_API = "https://api.github.com";
|
|
115
115
|
var GH_HEADERS = { "User-Agent": "terminalhire-claim", Accept: "application/vnd.github+json" };
|