terminalhire 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+
3
+ // bin/jpi-init.js
4
+ import { existsSync } from "fs";
5
+ import { join, resolve } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { createInterface } from "readline";
8
+ import { spawnSync, spawn } from "child_process";
9
+ import { homedir } from "os";
10
+ var __dirname = fileURLToPath(new URL(".", import.meta.url));
11
+ function ask(question) {
12
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
13
+ return new Promise((resolve2) => {
14
+ rl.question(question, (answer) => {
15
+ rl.close();
16
+ resolve2(answer.trim().toLowerCase());
17
+ });
18
+ });
19
+ }
20
+ function resolveScript(name) {
21
+ const distPath = resolve(join(__dirname, "..", "..", "dist", "bin", `${name}.js`));
22
+ const legacyPath = resolve(join(__dirname, `${name}.js`));
23
+ return existsSync(distPath) ? distPath : legacyPath;
24
+ }
25
+ function resolveInstallJs() {
26
+ const fromDist = resolve(join(__dirname, "..", "..", "install.js"));
27
+ const fromBin = resolve(join(__dirname, "..", "install.js"));
28
+ if (existsSync(fromDist)) return fromDist;
29
+ if (existsSync(fromBin)) return fromBin;
30
+ return fromBin;
31
+ }
32
+ async function run() {
33
+ console.log("");
34
+ 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");
35
+ console.log("\u2502 terminalhire init \u2014 one-command onboarding \u2502");
36
+ console.log("\u2502 Local-first job matching for developers in Claude Code \u2502");
37
+ 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\u2500\u2500\u2500\u2500\u2518");
38
+ console.log("");
39
+ console.log("This will:");
40
+ console.log(" 1. Optionally sign you in with GitHub (public profile only, read:user)");
41
+ console.log(" 2. Seed your local job cache (anonymous index download)");
42
+ console.log(" 3. Install the statusLine hook in ~/.claude/settings.json");
43
+ console.log(" (with backup + your explicit consent before any file is touched)");
44
+ console.log("");
45
+ console.log('You can stop at any step. Nothing is changed until you say "yes".');
46
+ console.log("");
47
+ console.log("Step 1/3 \u2014 GitHub sign-in (optional but recommended)");
48
+ console.log("");
49
+ console.log(" Scope: read:user \u2014 public profile + public repos only.");
50
+ console.log(" Your token is encrypted at ~/.terminalhire/github-token.enc.");
51
+ console.log(" GitHub data enriches your local profile. Nothing leaves your machine");
52
+ console.log(" until you explicitly consent to a specific lead.");
53
+ console.log("");
54
+ const githubAnswer = await ask("Sign in with GitHub now? [Y/n] (Enter = yes, n = stay local): ");
55
+ const doGitHub = githubAnswer === "" || githubAnswer === "y" || githubAnswer === "yes";
56
+ if (doGitHub) {
57
+ console.log("");
58
+ console.log(" Starting GitHub device flow...");
59
+ const loginScript = resolveScript("jpi-login");
60
+ const child = spawnSync(process.execPath, [loginScript, "login"], {
61
+ stdio: ["inherit", "inherit", "inherit"],
62
+ env: process.env
63
+ });
64
+ if (child.status !== 0) {
65
+ console.log("");
66
+ console.log(" GitHub sign-in did not complete. Continuing without GitHub.");
67
+ console.log(" You can sign in any time with: terminalhire login");
68
+ }
69
+ } else {
70
+ console.log("");
71
+ console.log(" Staying local-only. Tags accumulate from your personal project sessions.");
72
+ console.log(" Sign in any time with: terminalhire login");
73
+ }
74
+ console.log("");
75
+ console.log("Step 2/3 \u2014 Seeding local job cache");
76
+ console.log("");
77
+ console.log(" Fetching anonymous job index (no dev data sent)...");
78
+ const jobsScript = resolveScript("jpi-jobs");
79
+ const seedChild = spawnSync(
80
+ process.execPath,
81
+ [jobsScript, "--limit", "0"],
82
+ {
83
+ stdio: ["ignore", "pipe", "pipe"],
84
+ env: { ...process.env, TERMINALHIRE_SEED_ONLY: "1" },
85
+ timeout: 15e3
86
+ }
87
+ );
88
+ if (seedChild.status === 0) {
89
+ console.log(" Job cache seeded successfully.");
90
+ } else {
91
+ console.log(" Could not seed job cache right now (no profile tags yet, or offline).");
92
+ console.log(" Run `terminalhire jobs` after a few Claude Code sessions to populate it.");
93
+ }
94
+ console.log("");
95
+ console.log("Step 3/3 \u2014 Install statusLine hook in ~/.claude/settings.json");
96
+ console.log("");
97
+ console.log(" This is the only step that modifies a system file.");
98
+ console.log(" A timestamped backup is created before any change.");
99
+ console.log(" Uninstall at any time: node install.js --uninstall");
100
+ console.log("");
101
+ const installJs = resolveInstallJs();
102
+ const installChild = spawnSync(process.execPath, [installJs], {
103
+ stdio: ["inherit", "inherit", "inherit"],
104
+ env: process.env
105
+ });
106
+ if (installChild.status !== 0) {
107
+ console.log("");
108
+ console.log(" Hook installation did not complete. Run manually: node install.js");
109
+ }
110
+ console.log("");
111
+ 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");
112
+ console.log("\u2502 terminalhire init complete! \u2502");
113
+ console.log("\u2502 \u2502");
114
+ console.log("\u2502 Restart Claude Code to see the statusLine nudge. \u2502");
115
+ console.log("\u2502 \u2502");
116
+ console.log("\u2502 Quick reference: \u2502");
117
+ console.log("\u2502 terminalhire jobs \u2014 browse matching roles \u2502");
118
+ console.log("\u2502 terminalhire config --show \u2014 view nudge settings \u2502");
119
+ console.log("\u2502 terminalhire login \u2014 sign in with GitHub \u2502");
120
+ console.log("\u2502 terminalhire profile --show \u2014 inspect your local profile \u2502");
121
+ 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\u2500\u2500\u2500\u2500\u2518");
122
+ console.log("");
123
+ }
124
+ export {
125
+ run
126
+ };
@@ -1075,9 +1075,12 @@ __export(profile_exports, {
1075
1075
  accumulateGitHubTags: () => accumulateGitHubTags,
1076
1076
  accumulateSession: () => accumulateSession,
1077
1077
  accumulateTags: () => accumulateTags,
1078
+ addSavedJob: () => addSavedJob,
1078
1079
  deleteProfile: () => deleteProfile,
1080
+ listSavedJobs: () => listSavedJobs,
1079
1081
  profileToFingerprint: () => profileToFingerprint,
1080
1082
  readProfile: () => readProfile,
1083
+ removeSavedJob: () => removeSavedJob,
1081
1084
  writeProfile: () => writeProfile
1082
1085
  });
1083
1086
  import {
@@ -1223,6 +1226,26 @@ function accumulateGitHubTags(profile, tags) {
1223
1226
  false
1224
1227
  );
1225
1228
  }
1229
+ async function listSavedJobs() {
1230
+ const profile = await readProfile();
1231
+ return profile.savedJobs ?? [];
1232
+ }
1233
+ async function addSavedJob(job) {
1234
+ const profile = await readProfile();
1235
+ const existing = profile.savedJobs ?? [];
1236
+ const filtered = existing.filter((j) => j.id !== job.id);
1237
+ profile.savedJobs = [...filtered, { ...job, savedAt: (/* @__PURE__ */ new Date()).toISOString() }];
1238
+ await writeProfile(profile);
1239
+ }
1240
+ async function removeSavedJob(id) {
1241
+ const profile = await readProfile();
1242
+ const existing = profile.savedJobs ?? [];
1243
+ const filtered = existing.filter((j) => j.id !== id);
1244
+ if (filtered.length === existing.length) return false;
1245
+ profile.savedJobs = filtered;
1246
+ await writeProfile(profile);
1247
+ return true;
1248
+ }
1226
1249
  async function deleteProfile() {
1227
1250
  const { rmSync } = await import("fs");
1228
1251
  try {
@@ -1391,14 +1414,24 @@ function formatComp(job) {
1391
1414
  if (job.compMin) return `$${Math.round(job.compMin / 1e3)}k+`;
1392
1415
  return "";
1393
1416
  }
1417
+ function linkTitle(title, url) {
1418
+ const isTTY = process.stdout.isTTY;
1419
+ const noColor = process.env["NO_COLOR"] !== void 0;
1420
+ if (isTTY && !noColor && url) {
1421
+ return `\x1B]8;;${url}\x1B\\${title}\x1B]8;;\x1B\\`;
1422
+ }
1423
+ return url ? `${title} (${url})` : title;
1424
+ }
1394
1425
  function printResult(i, result) {
1395
1426
  const { job, score, matchedTags, reason } = result;
1396
1427
  const comp = formatComp(job);
1397
1428
  const remote = job.remote ? "remote" : job.location ?? "onsite";
1398
1429
  const compStr = comp ? ` \xB7 ${comp}` : "";
1399
1430
  const mode = job.applyMode === "buyer-lead" ? " [COASTAL LEAD]" : "";
1431
+ const titleStr = linkTitle(job.title, job.url);
1400
1432
  console.log(`
1401
- ${i + 1}. ${job.title} \u2014 ${job.company}${mode}`);
1433
+ ${i + 1}. ${titleStr} \u2014 ${job.company}${mode}`);
1434
+ console.log(` id: ${job.id}`);
1402
1435
  console.log(` ${remote}${compStr} \xB7 ${job.roleType} \xB7 score: ${formatScore(score)}`);
1403
1436
  console.log(` ${reason}`);
1404
1437
  console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
@@ -577,9 +577,12 @@ __export(profile_exports, {
577
577
  accumulateGitHubTags: () => accumulateGitHubTags,
578
578
  accumulateSession: () => accumulateSession,
579
579
  accumulateTags: () => accumulateTags,
580
+ addSavedJob: () => addSavedJob,
580
581
  deleteProfile: () => deleteProfile,
582
+ listSavedJobs: () => listSavedJobs,
581
583
  profileToFingerprint: () => profileToFingerprint,
582
584
  readProfile: () => readProfile,
585
+ removeSavedJob: () => removeSavedJob,
583
586
  writeProfile: () => writeProfile
584
587
  });
585
588
  import {
@@ -725,6 +728,26 @@ function accumulateGitHubTags(profile, tags) {
725
728
  false
726
729
  );
727
730
  }
731
+ async function listSavedJobs() {
732
+ const profile = await readProfile();
733
+ return profile.savedJobs ?? [];
734
+ }
735
+ async function addSavedJob(job) {
736
+ const profile = await readProfile();
737
+ const existing = profile.savedJobs ?? [];
738
+ const filtered = existing.filter((j) => j.id !== job.id);
739
+ profile.savedJobs = [...filtered, { ...job, savedAt: (/* @__PURE__ */ new Date()).toISOString() }];
740
+ await writeProfile(profile);
741
+ }
742
+ async function removeSavedJob(id) {
743
+ const profile = await readProfile();
744
+ const existing = profile.savedJobs ?? [];
745
+ const filtered = existing.filter((j) => j.id !== id);
746
+ if (filtered.length === existing.length) return false;
747
+ profile.savedJobs = filtered;
748
+ await writeProfile(profile);
749
+ return true;
750
+ }
728
751
  async function deleteProfile() {
729
752
  const { rmSync } = await import("fs");
730
753
  try {
@@ -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,6 +1440,26 @@ 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
1464
  const { rmSync: rmSync2 } = await import("fs");
1442
1465
  try {
@@ -340,9 +340,12 @@ __export(profile_exports, {
340
340
  accumulateGitHubTags: () => accumulateGitHubTags,
341
341
  accumulateSession: () => accumulateSession,
342
342
  accumulateTags: () => accumulateTags,
343
+ addSavedJob: () => addSavedJob,
343
344
  deleteProfile: () => deleteProfile,
345
+ listSavedJobs: () => listSavedJobs,
344
346
  profileToFingerprint: () => profileToFingerprint,
345
347
  readProfile: () => readProfile,
348
+ removeSavedJob: () => removeSavedJob,
346
349
  writeProfile: () => writeProfile
347
350
  });
348
351
  import {
@@ -488,6 +491,26 @@ function accumulateGitHubTags(profile, tags) {
488
491
  false
489
492
  );
490
493
  }
494
+ async function listSavedJobs() {
495
+ const profile = await readProfile();
496
+ return profile.savedJobs ?? [];
497
+ }
498
+ async function addSavedJob(job) {
499
+ const profile = await readProfile();
500
+ const existing = profile.savedJobs ?? [];
501
+ const filtered = existing.filter((j) => j.id !== job.id);
502
+ profile.savedJobs = [...filtered, { ...job, savedAt: (/* @__PURE__ */ new Date()).toISOString() }];
503
+ await writeProfile(profile);
504
+ }
505
+ async function removeSavedJob(id) {
506
+ const profile = await readProfile();
507
+ const existing = profile.savedJobs ?? [];
508
+ const filtered = existing.filter((j) => j.id !== id);
509
+ if (filtered.length === existing.length) return false;
510
+ profile.savedJobs = filtered;
511
+ await writeProfile(profile);
512
+ return true;
513
+ }
491
514
  async function deleteProfile() {
492
515
  const { rmSync } = await import("fs");
493
516
  try {