terminalhire 0.1.1 → 0.2.2
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 +39 -12
- package/dist/bin/jpi-dispatch.js +1038 -35
- package/dist/bin/jpi-jobs.js +34 -1
- package/dist/bin/jpi-learn.js +23 -0
- package/dist/bin/jpi-login.js +23 -0
- package/dist/bin/jpi-profile.js +23 -0
- package/dist/bin/jpi-refresh.js +1897 -0
- package/dist/bin/jpi-save.js +674 -0
- package/dist/bin/jpi-spinner.js +352 -0
- package/dist/bin/jpi-sync.js +837 -0
- package/dist/bin/spinner.js +242 -0
- package/dist/src/profile.js +23 -0
- package/install.js +96 -4
- package/package.json +13 -3
package/dist/bin/jpi-dispatch.js
CHANGED
|
@@ -1289,9 +1289,12 @@ __export(profile_exports, {
|
|
|
1289
1289
|
accumulateGitHubTags: () => accumulateGitHubTags,
|
|
1290
1290
|
accumulateSession: () => accumulateSession,
|
|
1291
1291
|
accumulateTags: () => accumulateTags,
|
|
1292
|
+
addSavedJob: () => addSavedJob,
|
|
1292
1293
|
deleteProfile: () => deleteProfile,
|
|
1294
|
+
listSavedJobs: () => listSavedJobs,
|
|
1293
1295
|
profileToFingerprint: () => profileToFingerprint,
|
|
1294
1296
|
readProfile: () => readProfile,
|
|
1297
|
+
removeSavedJob: () => removeSavedJob,
|
|
1295
1298
|
writeProfile: () => writeProfile
|
|
1296
1299
|
});
|
|
1297
1300
|
import {
|
|
@@ -1437,14 +1440,34 @@ function accumulateGitHubTags(profile, tags) {
|
|
|
1437
1440
|
false
|
|
1438
1441
|
);
|
|
1439
1442
|
}
|
|
1443
|
+
async function listSavedJobs() {
|
|
1444
|
+
const profile = await readProfile();
|
|
1445
|
+
return profile.savedJobs ?? [];
|
|
1446
|
+
}
|
|
1447
|
+
async function addSavedJob(job) {
|
|
1448
|
+
const profile = await readProfile();
|
|
1449
|
+
const existing = profile.savedJobs ?? [];
|
|
1450
|
+
const filtered = existing.filter((j) => j.id !== job.id);
|
|
1451
|
+
profile.savedJobs = [...filtered, { ...job, savedAt: (/* @__PURE__ */ new Date()).toISOString() }];
|
|
1452
|
+
await writeProfile(profile);
|
|
1453
|
+
}
|
|
1454
|
+
async function removeSavedJob(id) {
|
|
1455
|
+
const profile = await readProfile();
|
|
1456
|
+
const existing = profile.savedJobs ?? [];
|
|
1457
|
+
const filtered = existing.filter((j) => j.id !== id);
|
|
1458
|
+
if (filtered.length === existing.length) return false;
|
|
1459
|
+
profile.savedJobs = filtered;
|
|
1460
|
+
await writeProfile(profile);
|
|
1461
|
+
return true;
|
|
1462
|
+
}
|
|
1440
1463
|
async function deleteProfile() {
|
|
1441
|
-
const { rmSync:
|
|
1464
|
+
const { rmSync: rmSync3 } = await import("fs");
|
|
1442
1465
|
try {
|
|
1443
|
-
|
|
1466
|
+
rmSync3(PROFILE_FILE);
|
|
1444
1467
|
} catch {
|
|
1445
1468
|
}
|
|
1446
1469
|
try {
|
|
1447
|
-
|
|
1470
|
+
rmSync3(KEY_FILE2);
|
|
1448
1471
|
} catch {
|
|
1449
1472
|
}
|
|
1450
1473
|
}
|
|
@@ -1529,12 +1552,12 @@ async function runLogin() {
|
|
|
1529
1552
|
let ghProfile;
|
|
1530
1553
|
if (process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1") {
|
|
1531
1554
|
const { createRequire: createRequire2 } = await import("module");
|
|
1532
|
-
const { fileURLToPath:
|
|
1533
|
-
const { join:
|
|
1534
|
-
const
|
|
1535
|
-
const fixturePath =
|
|
1536
|
-
const { readFileSync:
|
|
1537
|
-
ghProfile = JSON.parse(
|
|
1555
|
+
const { fileURLToPath: fileURLToPath7 } = await import("url");
|
|
1556
|
+
const { join: join14, dirname: dirname3 } = await import("path");
|
|
1557
|
+
const __dirname6 = fileURLToPath7(new URL(".", import.meta.url));
|
|
1558
|
+
const fixturePath = join14(__dirname6, "../../fixtures/github-sample.json");
|
|
1559
|
+
const { readFileSync: readFileSync13 } = await import("fs");
|
|
1560
|
+
ghProfile = JSON.parse(readFileSync13(fixturePath, "utf8"));
|
|
1538
1561
|
} else {
|
|
1539
1562
|
ghProfile = await fetchGitHubProfile2(login, token);
|
|
1540
1563
|
}
|
|
@@ -1710,14 +1733,24 @@ function formatComp(job) {
|
|
|
1710
1733
|
if (job.compMin) return `$${Math.round(job.compMin / 1e3)}k+`;
|
|
1711
1734
|
return "";
|
|
1712
1735
|
}
|
|
1736
|
+
function linkTitle(title, url) {
|
|
1737
|
+
const isTTY = process.stdout.isTTY;
|
|
1738
|
+
const noColor = process.env["NO_COLOR"] !== void 0;
|
|
1739
|
+
if (isTTY && !noColor && url) {
|
|
1740
|
+
return `\x1B]8;;${url}\x1B\\${title}\x1B]8;;\x1B\\`;
|
|
1741
|
+
}
|
|
1742
|
+
return url ? `${title} (${url})` : title;
|
|
1743
|
+
}
|
|
1713
1744
|
function printResult(i, result) {
|
|
1714
1745
|
const { job, score, matchedTags, reason } = result;
|
|
1715
1746
|
const comp = formatComp(job);
|
|
1716
1747
|
const remote = job.remote ? "remote" : job.location ?? "onsite";
|
|
1717
1748
|
const compStr = comp ? ` \xB7 ${comp}` : "";
|
|
1718
1749
|
const mode = job.applyMode === "buyer-lead" ? " [COASTAL LEAD]" : "";
|
|
1750
|
+
const titleStr = linkTitle(job.title, job.url);
|
|
1719
1751
|
console.log(`
|
|
1720
|
-
${i + 1}. ${
|
|
1752
|
+
${i + 1}. ${titleStr} \u2014 ${job.company}${mode}`);
|
|
1753
|
+
console.log(` id: ${job.id}`);
|
|
1721
1754
|
console.log(` ${remote}${compStr} \xB7 ${job.roleType} \xB7 score: ${formatScore(score)}`);
|
|
1722
1755
|
console.log(` ${reason}`);
|
|
1723
1756
|
console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
|
|
@@ -2275,19 +2308,735 @@ var init_jpi_config = __esm({
|
|
|
2275
2308
|
}
|
|
2276
2309
|
});
|
|
2277
2310
|
|
|
2311
|
+
// bin/spinner.js
|
|
2312
|
+
var spinner_exports = {};
|
|
2313
|
+
__export(spinner_exports, {
|
|
2314
|
+
SPINNER_DEFAULTS: () => SPINNER_DEFAULTS,
|
|
2315
|
+
applySpinnerTips: () => applySpinnerTips,
|
|
2316
|
+
applySpinnerVerbs: () => applySpinnerVerbs,
|
|
2317
|
+
buildContextVerbs: () => buildContextVerbs,
|
|
2318
|
+
buildSpinnerPool: () => buildSpinnerPool,
|
|
2319
|
+
buildTips: () => buildTips,
|
|
2320
|
+
clearSpinnerTips: () => clearSpinnerTips,
|
|
2321
|
+
clearSpinnerVerbs: () => clearSpinnerVerbs,
|
|
2322
|
+
ctaVerb: () => ctaVerb,
|
|
2323
|
+
formatVerbs: () => formatVerbs,
|
|
2324
|
+
rankBySessionTags: () => rankBySessionTags,
|
|
2325
|
+
readSpinnerConfig: () => readSpinnerConfig
|
|
2326
|
+
});
|
|
2327
|
+
import {
|
|
2328
|
+
readFileSync as readFileSync7,
|
|
2329
|
+
writeFileSync as writeFileSync5,
|
|
2330
|
+
existsSync as existsSync5,
|
|
2331
|
+
mkdirSync as mkdirSync5,
|
|
2332
|
+
renameSync
|
|
2333
|
+
} from "fs";
|
|
2334
|
+
import { join as join7, dirname } from "path";
|
|
2335
|
+
import { homedir as homedir5 } from "os";
|
|
2336
|
+
function readJson(path, fallback) {
|
|
2337
|
+
try {
|
|
2338
|
+
return existsSync5(path) ? JSON.parse(readFileSync7(path, "utf8")) : fallback;
|
|
2339
|
+
} catch {
|
|
2340
|
+
return fallback;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
function atomicWriteJson(path, obj) {
|
|
2344
|
+
mkdirSync5(dirname(path), { recursive: true });
|
|
2345
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
2346
|
+
writeFileSync5(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
|
|
2347
|
+
renameSync(tmp, path);
|
|
2348
|
+
}
|
|
2349
|
+
function titleCase(s) {
|
|
2350
|
+
return String(s || "").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2351
|
+
}
|
|
2352
|
+
function readSpinnerConfig() {
|
|
2353
|
+
const cfg = readJson(CONFIG_FILE2, {});
|
|
2354
|
+
const spinner = cfg && typeof cfg.spinner === "object" ? cfg.spinner : {};
|
|
2355
|
+
const merged = { ...SPINNER_DEFAULTS, ...spinner };
|
|
2356
|
+
if (merged.mode !== "append" && merged.mode !== "replace") merged.mode = SPINNER_DEFAULTS.mode;
|
|
2357
|
+
merged.max = Math.max(1, Math.min(12, Number(merged.max) || SPINNER_DEFAULTS.max));
|
|
2358
|
+
merged.enabled = merged.enabled === true;
|
|
2359
|
+
if (!["always", "sometimes", "rare"].includes(merged.frequency)) {
|
|
2360
|
+
merged.frequency = SPINNER_DEFAULTS.frequency;
|
|
2361
|
+
}
|
|
2362
|
+
return merged;
|
|
2363
|
+
}
|
|
2364
|
+
function ctaVerb() {
|
|
2365
|
+
return "\u2605 jobs that fit you \xB7 run: terminalhire jobs";
|
|
2366
|
+
}
|
|
2367
|
+
function formatVerbs(topMatches, max = 6) {
|
|
2368
|
+
const out = [];
|
|
2369
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2370
|
+
for (const m of Array.isArray(topMatches) ? topMatches : []) {
|
|
2371
|
+
if (!m || !m.title || !m.company) continue;
|
|
2372
|
+
let title = String(m.title).trim().replace(/\s+/g, " ");
|
|
2373
|
+
if (title.length > 32) title = title.slice(0, 31).trimEnd() + "\u2026";
|
|
2374
|
+
const company = titleCase(String(m.company).trim().replace(/\s+/g, " "));
|
|
2375
|
+
const pct = Math.max(1, Math.min(99, Math.round((Number(m.score) || 0) * 100)));
|
|
2376
|
+
const key = `${title.toLowerCase()}@${company.toLowerCase()}`;
|
|
2377
|
+
if (seen.has(key)) continue;
|
|
2378
|
+
seen.add(key);
|
|
2379
|
+
const intro = VERB_INTROS[out.length % VERB_INTROS.length];
|
|
2380
|
+
out.push(`${intro} ${title} @ ${company} \xB7 ${pct}% match`);
|
|
2381
|
+
if (out.length >= max) break;
|
|
2382
|
+
}
|
|
2383
|
+
return out;
|
|
2384
|
+
}
|
|
2385
|
+
function rankBySessionTags(topMatches, sessionTags) {
|
|
2386
|
+
const tags = Array.isArray(sessionTags) ? sessionTags.filter(Boolean) : [];
|
|
2387
|
+
if (tags.length === 0 || !Array.isArray(topMatches)) return topMatches;
|
|
2388
|
+
const normalized = tags.map((t) => String(t).toLowerCase().trim());
|
|
2389
|
+
return topMatches.map((m, originalIndex) => {
|
|
2390
|
+
const haystack = `${String(m.title || "").toLowerCase()} ${String(m.company || "").toLowerCase()}`;
|
|
2391
|
+
const hits = normalized.reduce((n, tag) => n + (haystack.includes(tag) ? 1 : 0), 0);
|
|
2392
|
+
return { m, hits, originalIndex };
|
|
2393
|
+
}).sort((a, b) => b.hits - a.hits || a.originalIndex - b.originalIndex).map(({ m }) => m);
|
|
2394
|
+
}
|
|
2395
|
+
function verbCountForFrequency(frequency, max) {
|
|
2396
|
+
switch (frequency) {
|
|
2397
|
+
case "always":
|
|
2398
|
+
return max;
|
|
2399
|
+
case "rare":
|
|
2400
|
+
return 1;
|
|
2401
|
+
case "sometimes":
|
|
2402
|
+
default:
|
|
2403
|
+
return 2;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
function buildContextVerbs(topMatches, sessionTags) {
|
|
2407
|
+
const sess = (Array.isArray(sessionTags) ? sessionTags : []).map((t) => String(t).toLowerCase().trim()).filter(Boolean);
|
|
2408
|
+
const roleTags = /* @__PURE__ */ new Set();
|
|
2409
|
+
for (const m of Array.isArray(topMatches) ? topMatches : []) {
|
|
2410
|
+
const mt = m && Array.isArray(m.matchedTags) ? m.matchedTags : [];
|
|
2411
|
+
for (const t of mt) roleTags.add(String(t).toLowerCase().trim());
|
|
2412
|
+
}
|
|
2413
|
+
const overlap = [];
|
|
2414
|
+
for (const t of sess) {
|
|
2415
|
+
if (roleTags.has(t) && !overlap.includes(t)) overlap.push(t);
|
|
2416
|
+
}
|
|
2417
|
+
if (overlap.length >= 2) {
|
|
2418
|
+
const a = titleCase(overlap[0]);
|
|
2419
|
+
const b = titleCase(overlap[1]);
|
|
2420
|
+
return [`\u2726 Fits your ${a} + ${b} work`, `\u2726 A role matching what you're building`];
|
|
2421
|
+
}
|
|
2422
|
+
if (overlap.length === 1) {
|
|
2423
|
+
const a = titleCase(overlap[0]);
|
|
2424
|
+
return [`\u2726 A role matching your ${a} work`, `\u2726 Your ${a} work \u2014 link in the tip below`];
|
|
2425
|
+
}
|
|
2426
|
+
return [`\u2726 A role that fits your work`, `\u2726 Job match for you \u2014 link in the tip below`];
|
|
2427
|
+
}
|
|
2428
|
+
function buildSpinnerPool(topMatches, max = 6, opts = {}) {
|
|
2429
|
+
const { sessionTags, frequency = "always" } = opts;
|
|
2430
|
+
const ranked = rankBySessionTags(topMatches, sessionTags);
|
|
2431
|
+
if (!Array.isArray(ranked) || ranked.length === 0) return [];
|
|
2432
|
+
const headers = buildContextVerbs(ranked, sessionTags);
|
|
2433
|
+
const cap = Math.max(1, verbCountForFrequency(frequency, headers.length));
|
|
2434
|
+
return [...headers.slice(0, cap), ctaVerb()];
|
|
2435
|
+
}
|
|
2436
|
+
function readState() {
|
|
2437
|
+
return readJson(SPINNER_STATE_FILE, { verbs: [], mode: "replace" });
|
|
2438
|
+
}
|
|
2439
|
+
function applySpinnerVerbs(ourVerbs, mode = "replace") {
|
|
2440
|
+
const verbs = (Array.isArray(ourVerbs) ? ourVerbs : []).filter(Boolean);
|
|
2441
|
+
if (verbs.length === 0) return clearSpinnerVerbs();
|
|
2442
|
+
const settings = readJson(CLAUDE_SETTINGS, {}) || {};
|
|
2443
|
+
const existing = settings.spinnerVerbs && typeof settings.spinnerVerbs === "object" ? settings.spinnerVerbs : null;
|
|
2444
|
+
const prevOurs = new Set(readState().verbs || []);
|
|
2445
|
+
const userVerbs = existing && Array.isArray(existing.verbs) ? existing.verbs.filter((v) => !prevOurs.has(v)) : [];
|
|
2446
|
+
const newVerbs = [...verbs, ...userVerbs];
|
|
2447
|
+
settings.spinnerVerbs = { mode: mode === "append" ? "append" : "replace", verbs: newVerbs };
|
|
2448
|
+
atomicWriteJson(CLAUDE_SETTINGS, settings);
|
|
2449
|
+
const st = readState();
|
|
2450
|
+
atomicWriteJson(SPINNER_STATE_FILE, { ...st, verbs, mode, ts: Date.now() });
|
|
2451
|
+
return { applied: verbs.length, total: newVerbs.length };
|
|
2452
|
+
}
|
|
2453
|
+
function clearSpinnerVerbs() {
|
|
2454
|
+
const settings = readJson(CLAUDE_SETTINGS, null);
|
|
2455
|
+
const prevOurs = new Set(readState().verbs || []);
|
|
2456
|
+
let keptUserVerbs = 0;
|
|
2457
|
+
if (settings && settings.spinnerVerbs && Array.isArray(settings.spinnerVerbs.verbs)) {
|
|
2458
|
+
const userVerbs = settings.spinnerVerbs.verbs.filter((v) => !prevOurs.has(v));
|
|
2459
|
+
keptUserVerbs = userVerbs.length;
|
|
2460
|
+
if (userVerbs.length > 0) {
|
|
2461
|
+
settings.spinnerVerbs = {
|
|
2462
|
+
mode: settings.spinnerVerbs.mode === "append" ? "append" : "replace",
|
|
2463
|
+
verbs: userVerbs
|
|
2464
|
+
};
|
|
2465
|
+
} else {
|
|
2466
|
+
delete settings.spinnerVerbs;
|
|
2467
|
+
}
|
|
2468
|
+
atomicWriteJson(CLAUDE_SETTINGS, settings);
|
|
2469
|
+
}
|
|
2470
|
+
try {
|
|
2471
|
+
const st = readState();
|
|
2472
|
+
atomicWriteJson(SPINNER_STATE_FILE, { ...st, verbs: [], mode: st.mode || "replace", ts: Date.now() });
|
|
2473
|
+
} catch {
|
|
2474
|
+
}
|
|
2475
|
+
return { cleared: true, keptUserVerbs };
|
|
2476
|
+
}
|
|
2477
|
+
function buildTips(topMatches, baseUrl, max = 8) {
|
|
2478
|
+
const base = String(baseUrl || "https://terminalhire.com").replace(/\/+$/, "");
|
|
2479
|
+
const out = [];
|
|
2480
|
+
const seenRole = /* @__PURE__ */ new Set();
|
|
2481
|
+
const perCompany = /* @__PURE__ */ new Map();
|
|
2482
|
+
const COMPANY_CAP = 2;
|
|
2483
|
+
for (const m of Array.isArray(topMatches) ? topMatches : []) {
|
|
2484
|
+
if (!m || !m.title || !m.company || !m.id) continue;
|
|
2485
|
+
const idx = String(m.id).indexOf(":");
|
|
2486
|
+
if (idx <= 0) continue;
|
|
2487
|
+
const source = String(m.id).slice(0, idx);
|
|
2488
|
+
const ext = String(m.id).slice(idx + 1);
|
|
2489
|
+
if (!source || !ext) continue;
|
|
2490
|
+
const companyRaw = String(m.company).trim().replace(/\s+/g, " ");
|
|
2491
|
+
const titleRaw = String(m.title).trim().replace(/\s+/g, " ");
|
|
2492
|
+
const roleKey = `${titleRaw.toLowerCase()}@${companyRaw.toLowerCase()}`;
|
|
2493
|
+
const coKey = companyRaw.toLowerCase();
|
|
2494
|
+
if (seenRole.has(roleKey)) continue;
|
|
2495
|
+
if ((perCompany.get(coKey) || 0) >= COMPANY_CAP) continue;
|
|
2496
|
+
seenRole.add(roleKey);
|
|
2497
|
+
perCompany.set(coKey, (perCompany.get(coKey) || 0) + 1);
|
|
2498
|
+
let title = titleRaw;
|
|
2499
|
+
if (title.length > 34) title = title.slice(0, 33).trimEnd() + "\u2026";
|
|
2500
|
+
const company = titleCase(companyRaw);
|
|
2501
|
+
const pct = Math.max(1, Math.min(99, Math.round((Number(m.score) || 0) * 100)));
|
|
2502
|
+
const token = Buffer.from(String(m.id)).toString("base64url");
|
|
2503
|
+
const url = `${base}/j/${token}`;
|
|
2504
|
+
out.push(`\u2197 ${title} @ ${company} \xB7 ${pct}% \u2014 ${url}`);
|
|
2505
|
+
if (out.length >= max) break;
|
|
2506
|
+
}
|
|
2507
|
+
return out;
|
|
2508
|
+
}
|
|
2509
|
+
function applySpinnerTips(ourTips) {
|
|
2510
|
+
const tips = (Array.isArray(ourTips) ? ourTips : []).filter(Boolean);
|
|
2511
|
+
if (tips.length === 0) return clearSpinnerTips();
|
|
2512
|
+
const settings = readJson(CLAUDE_SETTINGS, {}) || {};
|
|
2513
|
+
const existing = settings.spinnerTipsOverride && Array.isArray(settings.spinnerTipsOverride.tips) ? settings.spinnerTipsOverride.tips : [];
|
|
2514
|
+
const prevOurs = new Set(readState().tips || []);
|
|
2515
|
+
const userTips = existing.filter((t) => !prevOurs.has(t));
|
|
2516
|
+
settings.spinnerTipsEnabled = true;
|
|
2517
|
+
settings.spinnerTipsOverride = { excludeDefault: true, tips: [...tips, ...userTips] };
|
|
2518
|
+
atomicWriteJson(CLAUDE_SETTINGS, settings);
|
|
2519
|
+
const st = readState();
|
|
2520
|
+
atomicWriteJson(SPINNER_STATE_FILE, { ...st, tips, ts: Date.now() });
|
|
2521
|
+
return { applied: tips.length };
|
|
2522
|
+
}
|
|
2523
|
+
function clearSpinnerTips() {
|
|
2524
|
+
const settings = readJson(CLAUDE_SETTINGS, null);
|
|
2525
|
+
const prevOurs = new Set(readState().tips || []);
|
|
2526
|
+
if (settings && settings.spinnerTipsOverride && Array.isArray(settings.spinnerTipsOverride.tips)) {
|
|
2527
|
+
const userTips = settings.spinnerTipsOverride.tips.filter((t) => !prevOurs.has(t));
|
|
2528
|
+
if (userTips.length > 0) {
|
|
2529
|
+
settings.spinnerTipsOverride = {
|
|
2530
|
+
excludeDefault: settings.spinnerTipsOverride.excludeDefault === true,
|
|
2531
|
+
tips: userTips
|
|
2532
|
+
};
|
|
2533
|
+
} else {
|
|
2534
|
+
delete settings.spinnerTipsOverride;
|
|
2535
|
+
delete settings.spinnerTipsEnabled;
|
|
2536
|
+
}
|
|
2537
|
+
atomicWriteJson(CLAUDE_SETTINGS, settings);
|
|
2538
|
+
}
|
|
2539
|
+
try {
|
|
2540
|
+
const st = readState();
|
|
2541
|
+
atomicWriteJson(SPINNER_STATE_FILE, { ...st, tips: [], ts: Date.now() });
|
|
2542
|
+
} catch {
|
|
2543
|
+
}
|
|
2544
|
+
return { cleared: true };
|
|
2545
|
+
}
|
|
2546
|
+
var TH_DIR, CLAUDE_SETTINGS, CONFIG_FILE2, SPINNER_STATE_FILE, SPINNER_DEFAULTS, VERB_INTROS;
|
|
2547
|
+
var init_spinner = __esm({
|
|
2548
|
+
"bin/spinner.js"() {
|
|
2549
|
+
"use strict";
|
|
2550
|
+
TH_DIR = process.env["TERMINALHIRE_DIR"] || join7(homedir5(), ".terminalhire");
|
|
2551
|
+
CLAUDE_SETTINGS = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join7(homedir5(), ".claude", "settings.json");
|
|
2552
|
+
CONFIG_FILE2 = join7(TH_DIR, "config.json");
|
|
2553
|
+
SPINNER_STATE_FILE = join7(TH_DIR, "spinner-state.json");
|
|
2554
|
+
SPINNER_DEFAULTS = { enabled: false, mode: "append", max: 6, frequency: "sometimes" };
|
|
2555
|
+
VERB_INTROS = ["Matched:", "You\u2019d fit:", "Worth a look:", "On your radar:", "Fits your stack:"];
|
|
2556
|
+
}
|
|
2557
|
+
});
|
|
2558
|
+
|
|
2559
|
+
// bin/jpi-spinner.js
|
|
2560
|
+
var jpi_spinner_exports = {};
|
|
2561
|
+
__export(jpi_spinner_exports, {
|
|
2562
|
+
run: () => run6
|
|
2563
|
+
});
|
|
2564
|
+
import {
|
|
2565
|
+
readFileSync as readFileSync8,
|
|
2566
|
+
writeFileSync as writeFileSync6,
|
|
2567
|
+
copyFileSync,
|
|
2568
|
+
existsSync as existsSync6,
|
|
2569
|
+
mkdirSync as mkdirSync6
|
|
2570
|
+
} from "fs";
|
|
2571
|
+
import { join as join8 } from "path";
|
|
2572
|
+
import { homedir as homedir6 } from "os";
|
|
2573
|
+
import { createInterface as createInterface3 } from "readline";
|
|
2574
|
+
function readConfig2() {
|
|
2575
|
+
try {
|
|
2576
|
+
return existsSync6(CONFIG_FILE3) ? JSON.parse(readFileSync8(CONFIG_FILE3, "utf8")) : {};
|
|
2577
|
+
} catch {
|
|
2578
|
+
return {};
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
function writeConfig2(patch) {
|
|
2582
|
+
mkdirSync6(TH_DIR2, { recursive: true });
|
|
2583
|
+
const merged = { ...readConfig2(), ...patch };
|
|
2584
|
+
writeFileSync6(CONFIG_FILE3, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
2585
|
+
}
|
|
2586
|
+
function backupSettings() {
|
|
2587
|
+
if (!existsSync6(SETTINGS_PATH)) return null;
|
|
2588
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2589
|
+
const backupPath = `${SETTINGS_PATH}.terminalhire-backup-${ts}`;
|
|
2590
|
+
copyFileSync(SETTINGS_PATH, backupPath);
|
|
2591
|
+
return backupPath;
|
|
2592
|
+
}
|
|
2593
|
+
function ask(question) {
|
|
2594
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
2595
|
+
return new Promise((res) => {
|
|
2596
|
+
rl.question(question, (answer) => {
|
|
2597
|
+
rl.close();
|
|
2598
|
+
res(answer.trim().toLowerCase());
|
|
2599
|
+
});
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
function readTopMatches() {
|
|
2603
|
+
try {
|
|
2604
|
+
const c = JSON.parse(readFileSync8(CACHE_FILE, "utf8"));
|
|
2605
|
+
return Array.isArray(c.topMatches) ? c.topMatches : [];
|
|
2606
|
+
} catch {
|
|
2607
|
+
return [];
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
async function run6() {
|
|
2611
|
+
const args2 = process.argv.slice(2).filter((a) => a !== "spinner");
|
|
2612
|
+
const has = (f) => args2.includes(f);
|
|
2613
|
+
const val = (f) => {
|
|
2614
|
+
const i = args2.indexOf(f);
|
|
2615
|
+
return i >= 0 ? args2[i + 1] : void 0;
|
|
2616
|
+
};
|
|
2617
|
+
if (has("--show") || args2.length === 0) {
|
|
2618
|
+
const sc = readSpinnerConfig();
|
|
2619
|
+
console.log("");
|
|
2620
|
+
console.log("terminalhire spinner \u2014 job matches in the Claude Code spinner line");
|
|
2621
|
+
console.log("");
|
|
2622
|
+
console.log(` enabled: ${sc.enabled}`);
|
|
2623
|
+
console.log(` mode: ${sc.mode} (replace = only job matches; append = mixed with Claude defaults)`);
|
|
2624
|
+
console.log(` max: ${sc.max} (max job verbs that rotate)`);
|
|
2625
|
+
console.log(` frequency: ${sc.frequency} (always = up to max; sometimes = up to 2; rare = 1 per cycle)`);
|
|
2626
|
+
console.log("");
|
|
2627
|
+
console.log(" terminalhire spinner --on enable (asks consent, backs up settings.json)");
|
|
2628
|
+
console.log(" terminalhire spinner --off disable + restore your original spinner");
|
|
2629
|
+
console.log(" terminalhire spinner --mode append mix job verbs with Claude's defaults");
|
|
2630
|
+
console.log(" terminalhire spinner --mode replace show only job matches");
|
|
2631
|
+
console.log(" terminalhire spinner --max N cap how many job verbs rotate (1\u201312)");
|
|
2632
|
+
console.log(" terminalhire spinner --frequency always surface up to max role verbs every cycle");
|
|
2633
|
+
console.log(" terminalhire spinner --frequency sometimes surface up to 2 role verbs (default)");
|
|
2634
|
+
console.log(" terminalhire spinner --frequency rare surface 1 role verb per cycle (quietest)");
|
|
2635
|
+
console.log("");
|
|
2636
|
+
return;
|
|
2637
|
+
}
|
|
2638
|
+
if (has("--off")) {
|
|
2639
|
+
const res = clearSpinnerVerbs();
|
|
2640
|
+
clearSpinnerTips();
|
|
2641
|
+
writeConfig2({ spinner: { ...readSpinnerConfig(), enabled: false } });
|
|
2642
|
+
console.log("");
|
|
2643
|
+
console.log(" Spinner job verbs removed.");
|
|
2644
|
+
if (res.keptUserVerbs > 0) {
|
|
2645
|
+
console.log(` Preserved ${res.keptUserVerbs} spinner verb(s) you set yourself.`);
|
|
2646
|
+
} else {
|
|
2647
|
+
console.log(" Your original spinner is restored.");
|
|
2648
|
+
}
|
|
2649
|
+
console.log(" Re-enable any time: terminalhire spinner --on");
|
|
2650
|
+
console.log("");
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
if ((has("--mode") || has("--max") || has("--frequency")) && !has("--on")) {
|
|
2654
|
+
const cur = readSpinnerConfig();
|
|
2655
|
+
const next = { ...cur };
|
|
2656
|
+
const m = val("--mode");
|
|
2657
|
+
if (m) {
|
|
2658
|
+
if (m !== "append" && m !== "replace") {
|
|
2659
|
+
console.error('Error: --mode must be "append" or "replace".');
|
|
2660
|
+
process.exit(1);
|
|
2661
|
+
}
|
|
2662
|
+
next.mode = m;
|
|
2663
|
+
}
|
|
2664
|
+
const mx = val("--max");
|
|
2665
|
+
if (mx) {
|
|
2666
|
+
const n = parseInt(mx, 10);
|
|
2667
|
+
if (!(n >= 1 && n <= 12)) {
|
|
2668
|
+
console.error("Error: --max must be a number 1\u201312.");
|
|
2669
|
+
process.exit(1);
|
|
2670
|
+
}
|
|
2671
|
+
next.max = n;
|
|
2672
|
+
}
|
|
2673
|
+
const freq = val("--frequency");
|
|
2674
|
+
if (freq) {
|
|
2675
|
+
if (!["always", "sometimes", "rare"].includes(freq)) {
|
|
2676
|
+
console.error('Error: --frequency must be "always", "sometimes", or "rare".');
|
|
2677
|
+
process.exit(1);
|
|
2678
|
+
}
|
|
2679
|
+
next.frequency = freq;
|
|
2680
|
+
}
|
|
2681
|
+
writeConfig2({ spinner: next });
|
|
2682
|
+
console.log(` spinner config updated: mode=${next.mode} max=${next.max} frequency=${next.frequency} enabled=${next.enabled}`);
|
|
2683
|
+
if (next.enabled) {
|
|
2684
|
+
const verbs = buildSpinnerPool(readTopMatches(), next.max, { frequency: next.frequency });
|
|
2685
|
+
if (verbs.length) applySpinnerVerbs(verbs, next.mode);
|
|
2686
|
+
else clearSpinnerVerbs();
|
|
2687
|
+
}
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
if (has("--on")) {
|
|
2691
|
+
const mode = val("--mode") === "replace" ? "replace" : "append";
|
|
2692
|
+
const maxRaw = parseInt(val("--max"), 10);
|
|
2693
|
+
const max = maxRaw >= 1 && maxRaw <= 12 ? maxRaw : 6;
|
|
2694
|
+
const freqRaw = val("--frequency");
|
|
2695
|
+
const frequency = ["always", "sometimes", "rare"].includes(freqRaw) ? freqRaw : "sometimes";
|
|
2696
|
+
console.log("");
|
|
2697
|
+
console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
2698
|
+
console.log("\u2502 terminalhire \u2014 enable the spinner job surface \u2502");
|
|
2699
|
+
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
2700
|
+
console.log("");
|
|
2701
|
+
console.log("WHAT THIS CHANGES:");
|
|
2702
|
+
console.log(' \u2022 Adds a "spinnerVerbs" key to ~/.claude/settings.json \u2014 the official,');
|
|
2703
|
+
console.log(" documented Claude Code setting. No patching, no binary changes (Rule 7).");
|
|
2704
|
+
console.log(" \u2022 While Claude works, the spinner line shows your TOP LOCAL JOB MATCHES,");
|
|
2705
|
+
console.log(" e.g. Senior Backend Engineer @ Stripe \xB7 82% \u2026");
|
|
2706
|
+
console.log(" \u2022 The tip line below shows a \u2318-clickable terminalhire.com/j/\u2026 link to open");
|
|
2707
|
+
console.log(" the listing (clicks logged anonymously, no profile data).");
|
|
2708
|
+
console.log(` \u2022 mode=${mode} (replace = only job matches; append = mixed with defaults)`);
|
|
2709
|
+
console.log(` \u2022 frequency=${frequency} (always = every cycle; sometimes = up to 2 verbs; rare = 1 verb)`);
|
|
2710
|
+
console.log(" \u2022 Matches are computed LOCALLY and refreshed in the background.");
|
|
2711
|
+
console.log(" ZERO egress \u2014 your profile never leaves the machine; only public job");
|
|
2712
|
+
console.log(" text appears on YOUR screen.");
|
|
2713
|
+
console.log(" \u2022 Any spinner verbs you already set are preserved, never clobbered.");
|
|
2714
|
+
console.log("");
|
|
2715
|
+
console.log("FULLY REVERSIBLE:");
|
|
2716
|
+
console.log(" terminalhire spinner --off removes job verbs, restores your spinner");
|
|
2717
|
+
console.log(" (a timestamped backup of settings.json is taken now)");
|
|
2718
|
+
console.log("");
|
|
2719
|
+
const answer = await ask('Enable the spinner job surface? Type "yes" to continue: ');
|
|
2720
|
+
if (answer !== "yes") {
|
|
2721
|
+
console.log("\nAborted \u2014 nothing changed.");
|
|
2722
|
+
process.exit(0);
|
|
2723
|
+
}
|
|
2724
|
+
const backup = backupSettings();
|
|
2725
|
+
writeConfig2({ spinner: { enabled: true, mode, max, frequency } });
|
|
2726
|
+
const verbs = buildSpinnerPool(readTopMatches(), max, { frequency });
|
|
2727
|
+
if (verbs.length) applySpinnerVerbs(verbs, mode);
|
|
2728
|
+
console.log("");
|
|
2729
|
+
if (backup) console.log(` Backed up settings to: ${backup}`);
|
|
2730
|
+
console.log(` Enabled. ${verbs.length} job verb(s) live now; refreshes in the background.`);
|
|
2731
|
+
if (verbs.length === 0) {
|
|
2732
|
+
console.log(" (No matches cached yet \u2014 run `terminalhire refresh` or wait for the monitor.)");
|
|
2733
|
+
}
|
|
2734
|
+
console.log(" Claude Code picks up settings.json changes automatically.");
|
|
2735
|
+
console.log(" Turn off any time: terminalhire spinner --off");
|
|
2736
|
+
console.log("");
|
|
2737
|
+
return;
|
|
2738
|
+
}
|
|
2739
|
+
console.error("Usage: terminalhire spinner --on | --off | --show | --mode <append|replace> | --max N | --frequency <always|sometimes|rare>");
|
|
2740
|
+
process.exit(1);
|
|
2741
|
+
}
|
|
2742
|
+
var TH_DIR2, CONFIG_FILE3, SETTINGS_PATH, CACHE_FILE;
|
|
2743
|
+
var init_jpi_spinner = __esm({
|
|
2744
|
+
"bin/jpi-spinner.js"() {
|
|
2745
|
+
"use strict";
|
|
2746
|
+
init_spinner();
|
|
2747
|
+
TH_DIR2 = process.env["TERMINALHIRE_DIR"] || join8(homedir6(), ".terminalhire");
|
|
2748
|
+
CONFIG_FILE3 = join8(TH_DIR2, "config.json");
|
|
2749
|
+
SETTINGS_PATH = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join8(homedir6(), ".claude", "settings.json");
|
|
2750
|
+
CACHE_FILE = join8(TH_DIR2, "index-cache.json");
|
|
2751
|
+
}
|
|
2752
|
+
});
|
|
2753
|
+
|
|
2754
|
+
// bin/jpi-sync.js
|
|
2755
|
+
var jpi_sync_exports = {};
|
|
2756
|
+
__export(jpi_sync_exports, {
|
|
2757
|
+
run: () => run7
|
|
2758
|
+
});
|
|
2759
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, existsSync as existsSync7, rmSync as rmSync2 } from "fs";
|
|
2760
|
+
import { join as join9 } from "path";
|
|
2761
|
+
import { homedir as homedir7 } from "os";
|
|
2762
|
+
import { createInterface as createInterface4 } from "readline";
|
|
2763
|
+
function ask2(question) {
|
|
2764
|
+
const rl = createInterface4({ input: process.stdin, output: process.stdout });
|
|
2765
|
+
return new Promise((res) => {
|
|
2766
|
+
rl.question(question, (answer) => {
|
|
2767
|
+
rl.close();
|
|
2768
|
+
res(answer.trim().toLowerCase());
|
|
2769
|
+
});
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
function readMarker() {
|
|
2773
|
+
try {
|
|
2774
|
+
return existsSync7(TIER1_MARKER) ? JSON.parse(readFileSync9(TIER1_MARKER, "utf8")) : null;
|
|
2775
|
+
} catch {
|
|
2776
|
+
return null;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
function writeMarker(marker) {
|
|
2780
|
+
mkdirSync7(TH_DIR3, { recursive: true });
|
|
2781
|
+
writeFileSync7(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
2782
|
+
}
|
|
2783
|
+
function clearMarker() {
|
|
2784
|
+
try {
|
|
2785
|
+
rmSync2(TIER1_MARKER);
|
|
2786
|
+
} catch {
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
function buildConsentFields(profile) {
|
|
2790
|
+
const gh = profile.github || {};
|
|
2791
|
+
const fields = [
|
|
2792
|
+
{ key: "login", label: "GitHub login", value: gh.login },
|
|
2793
|
+
{ key: "publicEmail", label: "Public email", value: gh.publicEmail || null },
|
|
2794
|
+
{ key: "topLanguages", label: "Top languages", value: gh.topLanguages || [] },
|
|
2795
|
+
{ key: "skillTags", label: "Skill tags", value: profile.skillTags || [] }
|
|
2796
|
+
];
|
|
2797
|
+
if (profile.displayName) {
|
|
2798
|
+
fields.push({ key: "displayName", label: "Display name", value: profile.displayName });
|
|
2799
|
+
}
|
|
2800
|
+
if (profile.contactEmail) {
|
|
2801
|
+
fields.push({ key: "contactEmail", label: "Contact email", value: profile.contactEmail });
|
|
2802
|
+
}
|
|
2803
|
+
return fields;
|
|
2804
|
+
}
|
|
2805
|
+
function renderConsentCard(fields) {
|
|
2806
|
+
console.log("");
|
|
2807
|
+
console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
2808
|
+
console.log("\u2502 terminalhire \u2014 sync your profile (Tier-1, opt-in) \u2502");
|
|
2809
|
+
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
2810
|
+
console.log("");
|
|
2811
|
+
console.log(" You are about to send the following data to");
|
|
2812
|
+
console.log(" staqs (terminalhire.com):");
|
|
2813
|
+
console.log("");
|
|
2814
|
+
for (const f of fields) {
|
|
2815
|
+
const shown = Array.isArray(f.value) ? JSON.stringify(f.value) : String(f.value ?? "(not set)");
|
|
2816
|
+
console.log(` ${f.label.padEnd(16)}: ${shown}`);
|
|
2817
|
+
}
|
|
2818
|
+
console.log("");
|
|
2819
|
+
console.log(" What is NEVER sent:");
|
|
2820
|
+
console.log(" - Private repos, employer repos, raw code");
|
|
2821
|
+
console.log(" - Employer-repo-derived tags (filtered at source)");
|
|
2822
|
+
console.log(" - Session history, file paths, project names");
|
|
2823
|
+
console.log("");
|
|
2824
|
+
console.log(" This is a ONE-TIME snapshot. Nothing is sent automatically;");
|
|
2825
|
+
console.log(" re-run `terminalhire sync --push` to update it later.");
|
|
2826
|
+
console.log(" Delete it any time: terminalhire sync --delete");
|
|
2827
|
+
console.log("");
|
|
2828
|
+
console.log(" This is NOT required to use terminalhire.");
|
|
2829
|
+
console.log("");
|
|
2830
|
+
}
|
|
2831
|
+
async function runPush() {
|
|
2832
|
+
const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
2833
|
+
const profile = await readProfile2();
|
|
2834
|
+
if (!profile.github || !profile.github.login) {
|
|
2835
|
+
console.log("");
|
|
2836
|
+
console.log(" No GitHub data in your local profile yet.");
|
|
2837
|
+
console.log(" Run `terminalhire login` first, then re-run `terminalhire sync --push`.");
|
|
2838
|
+
console.log("");
|
|
2839
|
+
process.exit(1);
|
|
2840
|
+
}
|
|
2841
|
+
const fields = buildConsentFields(profile);
|
|
2842
|
+
renderConsentCard(fields);
|
|
2843
|
+
let consentConfirmed = false;
|
|
2844
|
+
const answer = await ask2(' Sync your profile to staqs (terminalhire.com)? Type "yes" to continue: ');
|
|
2845
|
+
if (answer === "yes") {
|
|
2846
|
+
consentConfirmed = true;
|
|
2847
|
+
}
|
|
2848
|
+
if (!consentConfirmed) {
|
|
2849
|
+
console.log("\n Aborted \u2014 nothing was sent.\n");
|
|
2850
|
+
process.exit(0);
|
|
2851
|
+
}
|
|
2852
|
+
const consentedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2853
|
+
const consentToken = {
|
|
2854
|
+
consentedAt,
|
|
2855
|
+
version: CONSENT_VERSION,
|
|
2856
|
+
fields: fields.map((f) => f.key)
|
|
2857
|
+
};
|
|
2858
|
+
const payloadProfile = {
|
|
2859
|
+
login: profile.github.login,
|
|
2860
|
+
name: profile.displayName || null,
|
|
2861
|
+
publicEmail: profile.github.publicEmail || null,
|
|
2862
|
+
topLanguages: profile.github.topLanguages || [],
|
|
2863
|
+
skillTags: profile.skillTags || [],
|
|
2864
|
+
displayName: profile.displayName || null,
|
|
2865
|
+
contactEmail: profile.contactEmail || null
|
|
2866
|
+
};
|
|
2867
|
+
const priorMarker = readMarker();
|
|
2868
|
+
const rowToken = priorMarker && priorMarker.deleteToken ? priorMarker.deleteToken : null;
|
|
2869
|
+
const requestBody = { consentToken, profile: payloadProfile };
|
|
2870
|
+
if (rowToken) {
|
|
2871
|
+
requestBody.rowToken = rowToken;
|
|
2872
|
+
}
|
|
2873
|
+
console.log("\n Sending one-time snapshot...");
|
|
2874
|
+
let res;
|
|
2875
|
+
try {
|
|
2876
|
+
res = await fetch(`${API_URL2}/api/profile-sync`, {
|
|
2877
|
+
method: "POST",
|
|
2878
|
+
headers: { "Content-Type": "application/json" },
|
|
2879
|
+
body: JSON.stringify(requestBody),
|
|
2880
|
+
signal: AbortSignal.timeout(1e4)
|
|
2881
|
+
});
|
|
2882
|
+
} catch (err) {
|
|
2883
|
+
console.error(`
|
|
2884
|
+
Sync failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2885
|
+
process.exit(1);
|
|
2886
|
+
}
|
|
2887
|
+
if (!res.ok) {
|
|
2888
|
+
let detail = "";
|
|
2889
|
+
try {
|
|
2890
|
+
detail = (await res.json())?.message || "";
|
|
2891
|
+
} catch {
|
|
2892
|
+
}
|
|
2893
|
+
console.error(`
|
|
2894
|
+
Sync failed: /api/profile-sync returned ${res.status}. ${detail}`);
|
|
2895
|
+
if (res.status === 503) {
|
|
2896
|
+
console.error(" (Tier-1 sync is not enabled on the server yet.)");
|
|
2897
|
+
}
|
|
2898
|
+
if (res.status === 403) {
|
|
2899
|
+
console.error(" (This GitHub login was already claimed by a different push.)");
|
|
2900
|
+
}
|
|
2901
|
+
process.exit(1);
|
|
2902
|
+
}
|
|
2903
|
+
let deleteToken = null;
|
|
2904
|
+
try {
|
|
2905
|
+
deleteToken = (await res.json())?.deleteToken || null;
|
|
2906
|
+
} catch {
|
|
2907
|
+
}
|
|
2908
|
+
writeMarker({ consentedAt, login: profile.github.login, deleteToken });
|
|
2909
|
+
console.log("\n Profile synced. A local consent marker was written to ~/.terminalhire/tier1.json");
|
|
2910
|
+
console.log(" Delete your synced profile any time: terminalhire sync --delete\n");
|
|
2911
|
+
}
|
|
2912
|
+
function runStatus() {
|
|
2913
|
+
const marker = readMarker();
|
|
2914
|
+
console.log("");
|
|
2915
|
+
if (marker && marker.consentedAt) {
|
|
2916
|
+
console.log(" Tier-1 sync: CONSENTED (local marker present)");
|
|
2917
|
+
console.log(` login: ${marker.login || "(unknown)"}`);
|
|
2918
|
+
console.log(` consented at: ${marker.consentedAt}`);
|
|
2919
|
+
console.log("");
|
|
2920
|
+
console.log(" This reflects your local marker only (no network call was made).");
|
|
2921
|
+
console.log(" Update: terminalhire sync --push | Delete: terminalhire sync --delete");
|
|
2922
|
+
} else {
|
|
2923
|
+
console.log(" Tier-1 sync: NOT CONSENTED (no local marker).");
|
|
2924
|
+
console.log(" Your profile has not been synced. Enable: terminalhire sync --push");
|
|
2925
|
+
}
|
|
2926
|
+
console.log("");
|
|
2927
|
+
}
|
|
2928
|
+
async function runDelete() {
|
|
2929
|
+
const marker = readMarker();
|
|
2930
|
+
console.log("");
|
|
2931
|
+
console.log(" This will hard-delete your synced profile from staqs (terminalhire.com)");
|
|
2932
|
+
console.log(" and remove the local consent marker. There is no soft-delete.");
|
|
2933
|
+
console.log("");
|
|
2934
|
+
const answer = await ask2(' Delete your synced profile? Type "yes" to confirm: ');
|
|
2935
|
+
if (answer !== "yes") {
|
|
2936
|
+
console.log("\n Aborted \u2014 nothing was deleted.\n");
|
|
2937
|
+
process.exit(0);
|
|
2938
|
+
}
|
|
2939
|
+
let login = marker && marker.login ? marker.login : null;
|
|
2940
|
+
if (!login) {
|
|
2941
|
+
const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
2942
|
+
const profile = await readProfile2();
|
|
2943
|
+
login = profile.github && profile.github.login ? profile.github.login : null;
|
|
2944
|
+
}
|
|
2945
|
+
if (!login) {
|
|
2946
|
+
console.log("\n No github login to delete (no marker, no GitHub profile). Clearing local marker.\n");
|
|
2947
|
+
clearMarker();
|
|
2948
|
+
process.exit(0);
|
|
2949
|
+
}
|
|
2950
|
+
const deleteToken = marker && marker.deleteToken ? marker.deleteToken : null;
|
|
2951
|
+
if (!deleteToken) {
|
|
2952
|
+
console.log("\n No delete token found in ~/.terminalhire/tier1.json.");
|
|
2953
|
+
console.log(" Deletion must be run from the machine that originally pushed your profile");
|
|
2954
|
+
console.log(" (the delete token is stored there), or re-run `terminalhire sync --push`");
|
|
2955
|
+
console.log(" first to obtain a fresh token, then `terminalhire sync --delete`.\n");
|
|
2956
|
+
process.exit(1);
|
|
2957
|
+
}
|
|
2958
|
+
const consentToken = {
|
|
2959
|
+
consentedAt: marker && marker.consentedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
2960
|
+
version: CONSENT_VERSION,
|
|
2961
|
+
fields: ["login"]
|
|
2962
|
+
};
|
|
2963
|
+
console.log("\n Requesting deletion...");
|
|
2964
|
+
let res;
|
|
2965
|
+
try {
|
|
2966
|
+
res = await fetch(`${API_URL2}/api/profile-sync`, {
|
|
2967
|
+
method: "DELETE",
|
|
2968
|
+
headers: { "Content-Type": "application/json" },
|
|
2969
|
+
body: JSON.stringify({ consentToken, login, deleteToken }),
|
|
2970
|
+
signal: AbortSignal.timeout(1e4)
|
|
2971
|
+
});
|
|
2972
|
+
} catch (err) {
|
|
2973
|
+
console.error(`
|
|
2974
|
+
Delete failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2975
|
+
console.error(" Local marker NOT cleared (server state unknown). Re-run to retry.\n");
|
|
2976
|
+
process.exit(1);
|
|
2977
|
+
}
|
|
2978
|
+
if (!res.ok) {
|
|
2979
|
+
console.error(`
|
|
2980
|
+
Delete failed: /api/profile-sync returned ${res.status}.`);
|
|
2981
|
+
if (res.status === 503) {
|
|
2982
|
+
console.error(" (Tier-1 sync is not enabled on the server yet.) Clearing local marker.");
|
|
2983
|
+
clearMarker();
|
|
2984
|
+
}
|
|
2985
|
+
process.exit(1);
|
|
2986
|
+
}
|
|
2987
|
+
clearMarker();
|
|
2988
|
+
console.log("\n Synced profile deleted and local marker cleared.\n");
|
|
2989
|
+
}
|
|
2990
|
+
async function run7() {
|
|
2991
|
+
const args2 = process.argv.slice(2).filter((a) => a !== "sync");
|
|
2992
|
+
const has = (f) => args2.includes(f);
|
|
2993
|
+
if (has("--push") || has("--enable")) {
|
|
2994
|
+
await runPush();
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
if (has("--status")) {
|
|
2998
|
+
runStatus();
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
if (has("--delete")) {
|
|
3002
|
+
await runDelete();
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
3005
|
+
console.log("");
|
|
3006
|
+
console.log(" terminalhire sync \u2014 opt-in Tier-1 profile sync (one-time snapshot)");
|
|
3007
|
+
console.log("");
|
|
3008
|
+
console.log(' terminalhire sync --push Send your profile (shows a consent card, requires typed "yes")');
|
|
3009
|
+
console.log(" terminalhire sync --status Show whether you have consented (local read only)");
|
|
3010
|
+
console.log(" terminalhire sync --delete Hard-delete your synced profile (revocation)");
|
|
3011
|
+
console.log("");
|
|
3012
|
+
console.log(' Your profile is NEVER sent without an explicit typed "yes".');
|
|
3013
|
+
console.log(" This is NOT required to use terminalhire.");
|
|
3014
|
+
console.log("");
|
|
3015
|
+
}
|
|
3016
|
+
var TH_DIR3, TIER1_MARKER, API_URL2, CONSENT_VERSION;
|
|
3017
|
+
var init_jpi_sync = __esm({
|
|
3018
|
+
"bin/jpi-sync.js"() {
|
|
3019
|
+
"use strict";
|
|
3020
|
+
TH_DIR3 = process.env["TERMINALHIRE_DIR"] || join9(homedir7(), ".terminalhire");
|
|
3021
|
+
TIER1_MARKER = join9(TH_DIR3, "tier1.json");
|
|
3022
|
+
API_URL2 = process.env["TERMINALHIRE_API_URL"] || process.env["JPI_API_URL"] || "https://terminalhire.com";
|
|
3023
|
+
CONSENT_VERSION = 1;
|
|
3024
|
+
}
|
|
3025
|
+
});
|
|
3026
|
+
|
|
2278
3027
|
// bin/jpi-init.js
|
|
2279
3028
|
var jpi_init_exports = {};
|
|
2280
3029
|
__export(jpi_init_exports, {
|
|
2281
|
-
run: () =>
|
|
3030
|
+
run: () => run8
|
|
2282
3031
|
});
|
|
2283
|
-
import { existsSync as
|
|
2284
|
-
import { join as
|
|
3032
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3033
|
+
import { join as join10, resolve } from "path";
|
|
2285
3034
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2286
|
-
import { createInterface as
|
|
3035
|
+
import { createInterface as createInterface5 } from "readline";
|
|
2287
3036
|
import { spawnSync, spawn } from "child_process";
|
|
2288
|
-
import { homedir as
|
|
2289
|
-
function
|
|
2290
|
-
const rl =
|
|
3037
|
+
import { homedir as homedir8 } from "os";
|
|
3038
|
+
function ask3(question) {
|
|
3039
|
+
const rl = createInterface5({ input: process.stdin, output: process.stdout });
|
|
2291
3040
|
return new Promise((resolve2) => {
|
|
2292
3041
|
rl.question(question, (answer) => {
|
|
2293
3042
|
rl.close();
|
|
@@ -2296,18 +3045,18 @@ function ask(question) {
|
|
|
2296
3045
|
});
|
|
2297
3046
|
}
|
|
2298
3047
|
function resolveScript(name) {
|
|
2299
|
-
const distPath = resolve(
|
|
2300
|
-
const legacyPath = resolve(
|
|
2301
|
-
return
|
|
3048
|
+
const distPath = resolve(join10(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
|
|
3049
|
+
const legacyPath = resolve(join10(__dirname2, `${name}.js`));
|
|
3050
|
+
return existsSync8(distPath) ? distPath : legacyPath;
|
|
2302
3051
|
}
|
|
2303
3052
|
function resolveInstallJs() {
|
|
2304
|
-
const fromDist = resolve(
|
|
2305
|
-
const fromBin = resolve(
|
|
2306
|
-
if (
|
|
2307
|
-
if (
|
|
3053
|
+
const fromDist = resolve(join10(__dirname2, "..", "..", "install.js"));
|
|
3054
|
+
const fromBin = resolve(join10(__dirname2, "..", "install.js"));
|
|
3055
|
+
if (existsSync8(fromDist)) return fromDist;
|
|
3056
|
+
if (existsSync8(fromBin)) return fromBin;
|
|
2308
3057
|
return fromBin;
|
|
2309
3058
|
}
|
|
2310
|
-
async function
|
|
3059
|
+
async function run8() {
|
|
2311
3060
|
console.log("");
|
|
2312
3061
|
console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
2313
3062
|
console.log("\u2502 terminalhire init \u2014 one-command onboarding \u2502");
|
|
@@ -2329,7 +3078,7 @@ async function run6() {
|
|
|
2329
3078
|
console.log(" GitHub data enriches your local profile. Nothing leaves your machine");
|
|
2330
3079
|
console.log(" until you explicitly consent to a specific lead.");
|
|
2331
3080
|
console.log("");
|
|
2332
|
-
const githubAnswer = await
|
|
3081
|
+
const githubAnswer = await ask3("Sign in with GitHub now? [Y/n] (Enter = yes, n = stay local): ");
|
|
2333
3082
|
const doGitHub = githubAnswer === "" || githubAnswer === "y" || githubAnswer === "yes";
|
|
2334
3083
|
if (doGitHub) {
|
|
2335
3084
|
console.log("");
|
|
@@ -2407,21 +3156,244 @@ var init_jpi_init = __esm({
|
|
|
2407
3156
|
}
|
|
2408
3157
|
});
|
|
2409
3158
|
|
|
2410
|
-
// bin/jpi-
|
|
3159
|
+
// bin/jpi-refresh.js
|
|
3160
|
+
var jpi_refresh_exports = {};
|
|
3161
|
+
__export(jpi_refresh_exports, {
|
|
3162
|
+
run: () => run9
|
|
3163
|
+
});
|
|
3164
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, existsSync as existsSync9, mkdirSync as mkdirSync8 } from "fs";
|
|
3165
|
+
import { join as join11 } from "path";
|
|
3166
|
+
import { homedir as homedir9 } from "os";
|
|
2411
3167
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
2412
|
-
|
|
2413
|
-
|
|
3168
|
+
async function run9() {
|
|
3169
|
+
try {
|
|
3170
|
+
let index;
|
|
3171
|
+
try {
|
|
3172
|
+
const res = await fetch(`${API_URL3}/api/index`, {
|
|
3173
|
+
signal: AbortSignal.timeout(15e3),
|
|
3174
|
+
headers: { "Accept": "application/json" }
|
|
3175
|
+
});
|
|
3176
|
+
if (!res.ok) {
|
|
3177
|
+
process.stderr.write(`terminalhire refresh: index fetch failed (HTTP ${res.status})
|
|
3178
|
+
`);
|
|
3179
|
+
process.exit(1);
|
|
3180
|
+
}
|
|
3181
|
+
index = await res.json();
|
|
3182
|
+
} catch (err) {
|
|
3183
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3184
|
+
process.stderr.write(`terminalhire refresh: fetch error \u2014 ${msg}
|
|
3185
|
+
`);
|
|
3186
|
+
process.exit(1);
|
|
3187
|
+
}
|
|
3188
|
+
const jobs = index?.jobs ?? [];
|
|
3189
|
+
let matchCount = 0;
|
|
3190
|
+
let topMatches = [];
|
|
3191
|
+
try {
|
|
3192
|
+
const { readProfile: readProfile2, profileToFingerprint: profileToFingerprint2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
3193
|
+
const { match: match2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
3194
|
+
const profile = await readProfile2();
|
|
3195
|
+
if (profile.skillTags.length > 0 && jobs.length > 0) {
|
|
3196
|
+
const fp = profileToFingerprint2(profile);
|
|
3197
|
+
const results = match2(fp, jobs, jobs.length);
|
|
3198
|
+
matchCount = results.length;
|
|
3199
|
+
topMatches = results.slice(0, 25).map((r) => ({
|
|
3200
|
+
id: r.job.id,
|
|
3201
|
+
title: r.job.title,
|
|
3202
|
+
company: r.job.company,
|
|
3203
|
+
score: r.score,
|
|
3204
|
+
remote: r.job.remote,
|
|
3205
|
+
matchedTags: r.matchedTags
|
|
3206
|
+
}));
|
|
3207
|
+
}
|
|
3208
|
+
} catch {
|
|
3209
|
+
}
|
|
3210
|
+
mkdirSync8(TERMINALHIRE_DIR5, { recursive: true });
|
|
3211
|
+
const cacheEntry = {
|
|
3212
|
+
ts: Date.now(),
|
|
3213
|
+
index,
|
|
3214
|
+
matchCount,
|
|
3215
|
+
topMatches
|
|
3216
|
+
};
|
|
3217
|
+
writeFileSync8(INDEX_CACHE_FILE2, JSON.stringify(cacheEntry), "utf8");
|
|
3218
|
+
try {
|
|
3219
|
+
const {
|
|
3220
|
+
readSpinnerConfig: readSpinnerConfig2,
|
|
3221
|
+
buildSpinnerPool: buildSpinnerPool2,
|
|
3222
|
+
applySpinnerVerbs: applySpinnerVerbs2,
|
|
3223
|
+
clearSpinnerVerbs: clearSpinnerVerbs2,
|
|
3224
|
+
buildTips: buildTips2,
|
|
3225
|
+
applySpinnerTips: applySpinnerTips2,
|
|
3226
|
+
clearSpinnerTips: clearSpinnerTips2,
|
|
3227
|
+
rankBySessionTags: rankBySessionTags2
|
|
3228
|
+
} = await Promise.resolve().then(() => (init_spinner(), spinner_exports));
|
|
3229
|
+
const sc = readSpinnerConfig2();
|
|
3230
|
+
if (sc.enabled) {
|
|
3231
|
+
let sessionTags;
|
|
3232
|
+
try {
|
|
3233
|
+
const { extractFingerprint: extractFingerprint2 } = await Promise.resolve().then(() => (init_signal(), signal_exports));
|
|
3234
|
+
const fp = extractFingerprint2(process.cwd());
|
|
3235
|
+
if (Array.isArray(fp.skillTags) && fp.skillTags.length > 0) {
|
|
3236
|
+
sessionTags = fp.skillTags;
|
|
3237
|
+
}
|
|
3238
|
+
} catch {
|
|
3239
|
+
}
|
|
3240
|
+
const ranked = rankBySessionTags2(topMatches, sessionTags);
|
|
3241
|
+
const verbs = buildSpinnerPool2(ranked, sc.max, { sessionTags, frequency: sc.frequency });
|
|
3242
|
+
if (verbs.length > 0) applySpinnerVerbs2(verbs, sc.mode);
|
|
3243
|
+
else clearSpinnerVerbs2();
|
|
3244
|
+
const tips = buildTips2(ranked, API_URL3, 8);
|
|
3245
|
+
if (tips.length > 0) applySpinnerTips2(tips);
|
|
3246
|
+
else clearSpinnerTips2();
|
|
3247
|
+
} else {
|
|
3248
|
+
clearSpinnerVerbs2();
|
|
3249
|
+
clearSpinnerTips2();
|
|
3250
|
+
}
|
|
3251
|
+
} catch {
|
|
3252
|
+
}
|
|
3253
|
+
process.exit(0);
|
|
3254
|
+
} catch (err) {
|
|
3255
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3256
|
+
process.stderr.write(`terminalhire refresh: unexpected error \u2014 ${msg}
|
|
3257
|
+
`);
|
|
3258
|
+
process.exit(1);
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
var __dirname3, TERMINALHIRE_DIR5, INDEX_CACHE_FILE2, API_URL3;
|
|
3262
|
+
var init_jpi_refresh = __esm({
|
|
3263
|
+
"bin/jpi-refresh.js"() {
|
|
3264
|
+
"use strict";
|
|
3265
|
+
__dirname3 = fileURLToPath4(new URL(".", import.meta.url));
|
|
3266
|
+
TERMINALHIRE_DIR5 = join11(homedir9(), ".terminalhire");
|
|
3267
|
+
INDEX_CACHE_FILE2 = join11(TERMINALHIRE_DIR5, "index-cache.json");
|
|
3268
|
+
API_URL3 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
|
|
3269
|
+
}
|
|
3270
|
+
});
|
|
3271
|
+
|
|
3272
|
+
// bin/jpi-save.js
|
|
3273
|
+
var jpi_save_exports = {};
|
|
3274
|
+
__export(jpi_save_exports, {
|
|
3275
|
+
run: () => run10
|
|
3276
|
+
});
|
|
3277
|
+
import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
|
|
3278
|
+
import { join as join12 } from "path";
|
|
3279
|
+
import { homedir as homedir10 } from "os";
|
|
3280
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
3281
|
+
function findJobInCache(jobId) {
|
|
3282
|
+
try {
|
|
3283
|
+
if (!existsSync10(INDEX_CACHE_FILE3)) return null;
|
|
3284
|
+
const raw = readFileSync11(INDEX_CACHE_FILE3, "utf8");
|
|
3285
|
+
const entry = JSON.parse(raw);
|
|
3286
|
+
const jobs = entry?.index?.jobs ?? [];
|
|
3287
|
+
return jobs.find((j) => j.id === jobId) ?? null;
|
|
3288
|
+
} catch {
|
|
3289
|
+
return null;
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
async function cmdSave(jobId) {
|
|
3293
|
+
if (!jobId) {
|
|
3294
|
+
console.error("Usage: terminalhire save <jobId>");
|
|
3295
|
+
console.error(" jobId is shown in `terminalhire jobs` output (e.g. greenhouse:abc123)");
|
|
3296
|
+
process.exit(1);
|
|
3297
|
+
}
|
|
3298
|
+
const { addSavedJob: addSavedJob2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
3299
|
+
const job = findJobInCache(jobId);
|
|
3300
|
+
if (!job) {
|
|
3301
|
+
console.error(`terminalhire save: job '${jobId}' not found in local index cache.`);
|
|
3302
|
+
console.error(" Run `terminalhire jobs` first to populate the cache.");
|
|
3303
|
+
process.exit(1);
|
|
3304
|
+
}
|
|
3305
|
+
await addSavedJob2({
|
|
3306
|
+
id: job.id,
|
|
3307
|
+
title: job.title,
|
|
3308
|
+
company: job.company,
|
|
3309
|
+
url: job.url,
|
|
3310
|
+
source: job.source,
|
|
3311
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3312
|
+
// overwritten by addSavedJob, but required by type
|
|
3313
|
+
});
|
|
3314
|
+
console.log(`Saved: ${job.title} \u2014 ${job.company}`);
|
|
3315
|
+
console.log(` id: ${job.id}`);
|
|
3316
|
+
console.log(` url: ${job.url}`);
|
|
3317
|
+
console.log(" Stored locally in encrypted profile. Run `terminalhire saved` to list.");
|
|
3318
|
+
}
|
|
3319
|
+
async function cmdSaved() {
|
|
3320
|
+
const { listSavedJobs: listSavedJobs2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
3321
|
+
const jobs = await listSavedJobs2();
|
|
3322
|
+
if (jobs.length === 0) {
|
|
3323
|
+
console.log("No saved jobs. Use `terminalhire save <jobId>` to save a role.");
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
console.log(`
|
|
3327
|
+
${jobs.length} saved job${jobs.length === 1 ? "" : "s"}:
|
|
3328
|
+
`);
|
|
3329
|
+
for (const j of jobs) {
|
|
3330
|
+
const date = new Date(j.savedAt).toLocaleDateString();
|
|
3331
|
+
console.log(` ${j.id}`);
|
|
3332
|
+
console.log(` ${j.title} \u2014 ${j.company}`);
|
|
3333
|
+
console.log(` ${j.url}`);
|
|
3334
|
+
console.log(` Saved: ${date}`);
|
|
3335
|
+
console.log("");
|
|
3336
|
+
}
|
|
3337
|
+
console.log("To remove: terminalhire unsave <jobId>");
|
|
3338
|
+
}
|
|
3339
|
+
async function cmdUnsave(jobId) {
|
|
3340
|
+
if (!jobId) {
|
|
3341
|
+
console.error("Usage: terminalhire unsave <jobId>");
|
|
3342
|
+
process.exit(1);
|
|
3343
|
+
}
|
|
3344
|
+
const { removeSavedJob: removeSavedJob2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
3345
|
+
const removed = await removeSavedJob2(jobId);
|
|
3346
|
+
if (removed) {
|
|
3347
|
+
console.log(`Removed saved job: ${jobId}`);
|
|
3348
|
+
} else {
|
|
3349
|
+
console.error(`terminalhire unsave: job '${jobId}' was not in your saved list.`);
|
|
3350
|
+
process.exit(1);
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
async function run10() {
|
|
3354
|
+
const verb = process.argv[2];
|
|
3355
|
+
const jobId = process.argv[3];
|
|
3356
|
+
try {
|
|
3357
|
+
if (verb === "save") {
|
|
3358
|
+
await cmdSave(jobId);
|
|
3359
|
+
} else if (verb === "saved") {
|
|
3360
|
+
await cmdSaved();
|
|
3361
|
+
} else if (verb === "unsave") {
|
|
3362
|
+
await cmdUnsave(jobId);
|
|
3363
|
+
} else {
|
|
3364
|
+
console.error(`terminalhire: unknown save verb '${verb}'. Expected: save | saved | unsave`);
|
|
3365
|
+
process.exit(1);
|
|
3366
|
+
}
|
|
3367
|
+
} catch (err) {
|
|
3368
|
+
console.error("terminalhire save error:", err.message ?? err);
|
|
3369
|
+
process.exit(1);
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
var __dirname4, TERMINALHIRE_DIR6, INDEX_CACHE_FILE3;
|
|
3373
|
+
var init_jpi_save = __esm({
|
|
3374
|
+
"bin/jpi-save.js"() {
|
|
3375
|
+
"use strict";
|
|
3376
|
+
__dirname4 = fileURLToPath5(new URL(".", import.meta.url));
|
|
3377
|
+
TERMINALHIRE_DIR6 = join12(homedir10(), ".terminalhire");
|
|
3378
|
+
INDEX_CACHE_FILE3 = join12(TERMINALHIRE_DIR6, "index-cache.json");
|
|
3379
|
+
}
|
|
3380
|
+
});
|
|
3381
|
+
|
|
3382
|
+
// bin/jpi-dispatch.js
|
|
3383
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
3384
|
+
import { join as join13, dirname as dirname2 } from "path";
|
|
3385
|
+
import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
|
|
2414
3386
|
import { createRequire } from "module";
|
|
2415
|
-
var
|
|
3387
|
+
var __dirname5 = fileURLToPath6(new URL(".", import.meta.url));
|
|
2416
3388
|
function readPackageVersion() {
|
|
2417
3389
|
try {
|
|
2418
3390
|
const candidates = [
|
|
2419
|
-
|
|
2420
|
-
|
|
3391
|
+
join13(__dirname5, "..", "..", "package.json"),
|
|
3392
|
+
join13(__dirname5, "..", "package.json")
|
|
2421
3393
|
];
|
|
2422
3394
|
for (const p of candidates) {
|
|
2423
|
-
if (
|
|
2424
|
-
const pkg = JSON.parse(
|
|
3395
|
+
if (existsSync11(p)) {
|
|
3396
|
+
const pkg = JSON.parse(readFileSync12(p, "utf8"));
|
|
2425
3397
|
if (pkg.version) return pkg.version;
|
|
2426
3398
|
}
|
|
2427
3399
|
}
|
|
@@ -2432,7 +3404,7 @@ function readPackageVersion() {
|
|
|
2432
3404
|
var firstArg = process.argv[2];
|
|
2433
3405
|
if (!firstArg && !process.stdin.isTTY) {
|
|
2434
3406
|
const { default: childProcess } = await import("child_process");
|
|
2435
|
-
const nudgeScript =
|
|
3407
|
+
const nudgeScript = join13(__dirname5, "jpi.js");
|
|
2436
3408
|
const child = childProcess.spawnSync(process.execPath, [nudgeScript], {
|
|
2437
3409
|
stdio: ["inherit", "inherit", "inherit"]
|
|
2438
3410
|
});
|
|
@@ -2456,6 +3428,16 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
|
|
|
2456
3428
|
console.log(" terminalhire config --nudge always Nudge every statusLine render");
|
|
2457
3429
|
console.log(" terminalhire config --nudge every:N Nudge every Nth render");
|
|
2458
3430
|
console.log(" terminalhire config --show Print current config");
|
|
3431
|
+
console.log(" terminalhire spinner --show Job matches in the spinner line while Claude works");
|
|
3432
|
+
console.log(" terminalhire spinner --off Turn the spinner job surface off (restores your spinner)");
|
|
3433
|
+
console.log(" terminalhire spinner --mode append|replace Mix with Claude defaults, or show only matches");
|
|
3434
|
+
console.log(" terminalhire refresh Fetch index + match locally, update cache (non-interactive)");
|
|
3435
|
+
console.log(" terminalhire save <jobId> Save a job locally (id shown in `jobs` output)");
|
|
3436
|
+
console.log(" terminalhire saved List all locally-saved jobs");
|
|
3437
|
+
console.log(" terminalhire unsave <jobId> Remove a saved job");
|
|
3438
|
+
console.log(" terminalhire sync --push Opt-in: send your profile to staqs (typed-yes consent)");
|
|
3439
|
+
console.log(" terminalhire sync --status Show whether you have consented (local read only)");
|
|
3440
|
+
console.log(" terminalhire sync --delete Hard-delete your synced profile (revocation)");
|
|
2459
3441
|
console.log("");
|
|
2460
3442
|
console.log("Install / uninstall the Claude Code statusLine nudge:");
|
|
2461
3443
|
console.log(" terminalhire init (preferred \u2014 full guided onboarding)");
|
|
@@ -2465,6 +3447,7 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
|
|
|
2465
3447
|
console.log("Privacy: your profile never leaves your device.");
|
|
2466
3448
|
console.log(" GET /api/index \u2014 anonymous index download (no dev data)");
|
|
2467
3449
|
console.log(" POST /api/lead \u2014 only after explicit per-role named-entity consent");
|
|
3450
|
+
console.log(" POST /api/profile-sync \u2014 only via `terminalhire sync --push` with a typed-yes consent token");
|
|
2468
3451
|
console.log(" GitHub token \u2014 encrypted at ~/.terminalhire/github-token.enc, scope: read:user");
|
|
2469
3452
|
console.log("");
|
|
2470
3453
|
process.exit(0);
|
|
@@ -2499,10 +3482,30 @@ if (firstArg === "config") {
|
|
|
2499
3482
|
await mod.run();
|
|
2500
3483
|
process.exit(0);
|
|
2501
3484
|
}
|
|
3485
|
+
if (firstArg === "spinner") {
|
|
3486
|
+
const mod = await Promise.resolve().then(() => (init_jpi_spinner(), jpi_spinner_exports));
|
|
3487
|
+
await mod.run();
|
|
3488
|
+
process.exit(0);
|
|
3489
|
+
}
|
|
3490
|
+
if (firstArg === "sync") {
|
|
3491
|
+
const mod = await Promise.resolve().then(() => (init_jpi_sync(), jpi_sync_exports));
|
|
3492
|
+
await mod.run();
|
|
3493
|
+
process.exit(0);
|
|
3494
|
+
}
|
|
2502
3495
|
if (firstArg === "init") {
|
|
2503
3496
|
const mod = await Promise.resolve().then(() => (init_jpi_init(), jpi_init_exports));
|
|
2504
3497
|
await mod.run();
|
|
2505
3498
|
process.exit(0);
|
|
2506
3499
|
}
|
|
3500
|
+
if (firstArg === "refresh") {
|
|
3501
|
+
const mod = await Promise.resolve().then(() => (init_jpi_refresh(), jpi_refresh_exports));
|
|
3502
|
+
await mod.run();
|
|
3503
|
+
process.exit(0);
|
|
3504
|
+
}
|
|
3505
|
+
if (firstArg === "save" || firstArg === "saved" || firstArg === "unsave") {
|
|
3506
|
+
const mod = await Promise.resolve().then(() => (init_jpi_save(), jpi_save_exports));
|
|
3507
|
+
await mod.run();
|
|
3508
|
+
process.exit(0);
|
|
3509
|
+
}
|
|
2507
3510
|
console.error(`terminalhire: unknown command '${firstArg}'. Run 'terminalhire help' for usage.`);
|
|
2508
3511
|
process.exit(1);
|