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.
@@ -202,7 +202,7 @@ async function resolveStoredLogin() {
202
202
  }
203
203
  }
204
204
  function sleep(ms) {
205
- return new Promise((resolve) => setTimeout(resolve, ms));
205
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
206
206
  }
207
207
  var TERMINALHIRE_DIR, TOKEN_FILE, KEY_FILE, ALGO, KEY_BYTES, IV_BYTES, GITHUB_SCOPE, DEVICE_CODE_URL, ACCESS_TOKEN_URL, DEV_PLACEHOLDER_CLIENT_ID, MOCK_TOKEN, MOCK_LOGIN;
208
208
  var init_github_auth = __esm({
@@ -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 {
@@ -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: fileURLToPath4 } = await import("url");
1533
- const { join: join7, dirname } = await import("path");
1534
- const __dirname3 = fileURLToPath4(new URL(".", import.meta.url));
1535
- const fixturePath = join7(__dirname3, "../../fixtures/github-sample.json");
1536
- const { readFileSync: readFileSync7 } = await import("fs");
1537
- ghProfile = JSON.parse(readFileSync7(fixturePath, "utf8"));
1555
+ const { fileURLToPath: fileURLToPath7 } = await import("url");
1556
+ const { join: join13, dirname: dirname3 } = await import("path");
1557
+ const __dirname6 = fileURLToPath7(new URL(".", import.meta.url));
1558
+ const fixturePath = join13(__dirname6, "../../fixtures/github-sample.json");
1559
+ const { readFileSync: readFileSync12 } = await import("fs");
1560
+ ghProfile = JSON.parse(readFileSync12(fixturePath, "utf8"));
1538
1561
  } else {
1539
1562
  ghProfile = await fetchGitHubProfile2(login, token);
1540
1563
  }
@@ -1645,10 +1668,10 @@ async function fetchIndex() {
1645
1668
  }
1646
1669
  function prompt(question) {
1647
1670
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1648
- return new Promise((resolve) => {
1671
+ return new Promise((resolve2) => {
1649
1672
  rl.question(question, (answer) => {
1650
1673
  rl.close();
1651
- resolve(answer.trim().toLowerCase());
1674
+ resolve2(answer.trim().toLowerCase());
1652
1675
  });
1653
1676
  });
1654
1677
  }
@@ -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}. ${job.title} \u2014 ${job.company}${mode}`);
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(", ")}`);
@@ -1846,10 +1879,10 @@ __export(jpi_profile_exports, {
1846
1879
  import { createInterface as createInterface2 } from "readline";
1847
1880
  function prompt2(question) {
1848
1881
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
1849
- return new Promise((resolve) => {
1882
+ return new Promise((resolve2) => {
1850
1883
  rl.question(question, (answer) => {
1851
1884
  rl.close();
1852
- resolve(answer.trim());
1885
+ resolve2(answer.trim());
1853
1886
  });
1854
1887
  });
1855
1888
  }
@@ -2195,16 +2228,902 @@ var init_jpi_learn = __esm({
2195
2228
  }
2196
2229
  });
2197
2230
 
2198
- // bin/jpi-dispatch.js
2199
- import { fileURLToPath as fileURLToPath3 } from "url";
2231
+ // bin/jpi-config.js
2232
+ var jpi_config_exports = {};
2233
+ __export(jpi_config_exports, {
2234
+ run: () => run5
2235
+ });
2236
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync4 } from "fs";
2200
2237
  import { join as join6 } from "path";
2201
- import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
2238
+ import { homedir as homedir4 } from "os";
2239
+ function readConfig() {
2240
+ try {
2241
+ if (!existsSync4(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
2242
+ return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync6(CONFIG_FILE, "utf8")) };
2243
+ } catch {
2244
+ return { ...DEFAULT_CONFIG };
2245
+ }
2246
+ }
2247
+ function writeConfig(patch) {
2248
+ mkdirSync4(TERMINALHIRE_DIR4, { recursive: true });
2249
+ const merged = { ...readConfig(), ...patch };
2250
+ writeFileSync4(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
2251
+ }
2252
+ function parseNudgeMode(raw) {
2253
+ if (raw === "session" || raw === "always") return raw;
2254
+ const m = /^every:(\d+)$/.exec(raw);
2255
+ if (m && parseInt(m[1], 10) >= 1) return raw;
2256
+ return null;
2257
+ }
2258
+ async function run5() {
2259
+ const args2 = process.argv.slice(2);
2260
+ const filtered = args2[0] === "config" ? args2.slice(1) : args2;
2261
+ if (filtered.includes("--show") || filtered.length === 0) {
2262
+ const cfg = readConfig();
2263
+ const envOverride = process.env["TERMINALHIRE_NUDGE"];
2264
+ console.log("");
2265
+ console.log("terminalhire config");
2266
+ console.log("");
2267
+ console.log(` nudge: ${cfg.nudge}`);
2268
+ if (envOverride) {
2269
+ console.log(` (overridden by TERMINALHIRE_NUDGE=${envOverride} at runtime)`);
2270
+ }
2271
+ console.log(` config file: ${CONFIG_FILE}`);
2272
+ console.log("");
2273
+ console.log(" Valid nudge values:");
2274
+ console.log(" session \u2014 print at most once per Claude Code session (default)");
2275
+ console.log(" always \u2014 print every statusLine render when matches exist");
2276
+ console.log(" every:N \u2014 print every Nth render (e.g. every:3)");
2277
+ console.log("");
2278
+ return;
2279
+ }
2280
+ const nudgeIdx = filtered.indexOf("--nudge");
2281
+ if (nudgeIdx !== -1) {
2282
+ const value = filtered[nudgeIdx + 1];
2283
+ if (!value) {
2284
+ console.error("Error: --nudge requires a value: session | always | every:N");
2285
+ process.exit(1);
2286
+ }
2287
+ const parsed = parseNudgeMode(value);
2288
+ if (!parsed) {
2289
+ console.error(`Error: invalid nudge value "${value}". Valid: session | always | every:N`);
2290
+ process.exit(1);
2291
+ }
2292
+ writeConfig({ nudge: parsed });
2293
+ console.log(` nudge set to: ${parsed}`);
2294
+ console.log(` (saved to ${CONFIG_FILE})`);
2295
+ return;
2296
+ }
2297
+ console.error("Usage: terminalhire config --nudge <session|always|every:N>");
2298
+ console.error(" terminalhire config --show");
2299
+ process.exit(1);
2300
+ }
2301
+ var TERMINALHIRE_DIR4, CONFIG_FILE, DEFAULT_CONFIG;
2302
+ var init_jpi_config = __esm({
2303
+ "bin/jpi-config.js"() {
2304
+ "use strict";
2305
+ TERMINALHIRE_DIR4 = join6(homedir4(), ".terminalhire");
2306
+ CONFIG_FILE = join6(TERMINALHIRE_DIR4, "config.json");
2307
+ DEFAULT_CONFIG = { nudge: "session" };
2308
+ }
2309
+ });
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 = 3) {
2478
+ const base = String(baseUrl || "https://terminalhire.com").replace(/\/+$/, "");
2479
+ const out = [];
2480
+ const seen = /* @__PURE__ */ new Set();
2481
+ for (const m of Array.isArray(topMatches) ? topMatches : []) {
2482
+ if (!m || !m.title || !m.company || !m.id) continue;
2483
+ const idx = String(m.id).indexOf(":");
2484
+ if (idx <= 0) continue;
2485
+ const source = String(m.id).slice(0, idx);
2486
+ const ext = String(m.id).slice(idx + 1);
2487
+ if (!source || !ext) continue;
2488
+ const key = `${source}/${ext}`;
2489
+ if (seen.has(key)) continue;
2490
+ seen.add(key);
2491
+ let title = String(m.title).trim().replace(/\s+/g, " ");
2492
+ if (title.length > 34) title = title.slice(0, 33).trimEnd() + "\u2026";
2493
+ const company = titleCase(String(m.company).trim().replace(/\s+/g, " "));
2494
+ const pct = Math.max(1, Math.min(99, Math.round((Number(m.score) || 0) * 100)));
2495
+ const url = `${base}/j/${source}/${encodeURIComponent(ext)}`;
2496
+ out.push(`\u2197 ${title} @ ${company} \xB7 ${pct}% \u2014 ${url}`);
2497
+ if (out.length >= max) break;
2498
+ }
2499
+ return out;
2500
+ }
2501
+ function applySpinnerTips(ourTips) {
2502
+ const tips = (Array.isArray(ourTips) ? ourTips : []).filter(Boolean);
2503
+ if (tips.length === 0) return clearSpinnerTips();
2504
+ const settings = readJson(CLAUDE_SETTINGS, {}) || {};
2505
+ const existing = settings.spinnerTipsOverride && Array.isArray(settings.spinnerTipsOverride.tips) ? settings.spinnerTipsOverride.tips : [];
2506
+ const prevOurs = new Set(readState().tips || []);
2507
+ const userTips = existing.filter((t) => !prevOurs.has(t));
2508
+ settings.spinnerTipsEnabled = true;
2509
+ settings.spinnerTipsOverride = { excludeDefault: true, tips: [...tips, ...userTips] };
2510
+ atomicWriteJson(CLAUDE_SETTINGS, settings);
2511
+ const st = readState();
2512
+ atomicWriteJson(SPINNER_STATE_FILE, { ...st, tips, ts: Date.now() });
2513
+ return { applied: tips.length };
2514
+ }
2515
+ function clearSpinnerTips() {
2516
+ const settings = readJson(CLAUDE_SETTINGS, null);
2517
+ const prevOurs = new Set(readState().tips || []);
2518
+ if (settings && settings.spinnerTipsOverride && Array.isArray(settings.spinnerTipsOverride.tips)) {
2519
+ const userTips = settings.spinnerTipsOverride.tips.filter((t) => !prevOurs.has(t));
2520
+ if (userTips.length > 0) {
2521
+ settings.spinnerTipsOverride = {
2522
+ excludeDefault: settings.spinnerTipsOverride.excludeDefault === true,
2523
+ tips: userTips
2524
+ };
2525
+ } else {
2526
+ delete settings.spinnerTipsOverride;
2527
+ delete settings.spinnerTipsEnabled;
2528
+ }
2529
+ atomicWriteJson(CLAUDE_SETTINGS, settings);
2530
+ }
2531
+ try {
2532
+ const st = readState();
2533
+ atomicWriteJson(SPINNER_STATE_FILE, { ...st, tips: [], ts: Date.now() });
2534
+ } catch {
2535
+ }
2536
+ return { cleared: true };
2537
+ }
2538
+ var TH_DIR, CLAUDE_SETTINGS, CONFIG_FILE2, SPINNER_STATE_FILE, SPINNER_DEFAULTS, VERB_INTROS;
2539
+ var init_spinner = __esm({
2540
+ "bin/spinner.js"() {
2541
+ "use strict";
2542
+ TH_DIR = process.env["TERMINALHIRE_DIR"] || join7(homedir5(), ".terminalhire");
2543
+ CLAUDE_SETTINGS = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join7(homedir5(), ".claude", "settings.json");
2544
+ CONFIG_FILE2 = join7(TH_DIR, "config.json");
2545
+ SPINNER_STATE_FILE = join7(TH_DIR, "spinner-state.json");
2546
+ SPINNER_DEFAULTS = { enabled: false, mode: "append", max: 6, frequency: "sometimes" };
2547
+ VERB_INTROS = ["Matched:", "You\u2019d fit:", "Worth a look:", "On your radar:", "Fits your stack:"];
2548
+ }
2549
+ });
2550
+
2551
+ // bin/jpi-spinner.js
2552
+ var jpi_spinner_exports = {};
2553
+ __export(jpi_spinner_exports, {
2554
+ run: () => run6
2555
+ });
2556
+ import {
2557
+ readFileSync as readFileSync8,
2558
+ writeFileSync as writeFileSync6,
2559
+ copyFileSync,
2560
+ existsSync as existsSync6,
2561
+ mkdirSync as mkdirSync6
2562
+ } from "fs";
2563
+ import { join as join8 } from "path";
2564
+ import { homedir as homedir6 } from "os";
2565
+ import { createInterface as createInterface3 } from "readline";
2566
+ function readConfig2() {
2567
+ try {
2568
+ return existsSync6(CONFIG_FILE3) ? JSON.parse(readFileSync8(CONFIG_FILE3, "utf8")) : {};
2569
+ } catch {
2570
+ return {};
2571
+ }
2572
+ }
2573
+ function writeConfig2(patch) {
2574
+ mkdirSync6(TH_DIR2, { recursive: true });
2575
+ const merged = { ...readConfig2(), ...patch };
2576
+ writeFileSync6(CONFIG_FILE3, JSON.stringify(merged, null, 2) + "\n", "utf8");
2577
+ }
2578
+ function backupSettings() {
2579
+ if (!existsSync6(SETTINGS_PATH)) return null;
2580
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2581
+ const backupPath = `${SETTINGS_PATH}.terminalhire-backup-${ts}`;
2582
+ copyFileSync(SETTINGS_PATH, backupPath);
2583
+ return backupPath;
2584
+ }
2585
+ function ask(question) {
2586
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
2587
+ return new Promise((res) => {
2588
+ rl.question(question, (answer) => {
2589
+ rl.close();
2590
+ res(answer.trim().toLowerCase());
2591
+ });
2592
+ });
2593
+ }
2594
+ function readTopMatches() {
2595
+ try {
2596
+ const c = JSON.parse(readFileSync8(CACHE_FILE, "utf8"));
2597
+ return Array.isArray(c.topMatches) ? c.topMatches : [];
2598
+ } catch {
2599
+ return [];
2600
+ }
2601
+ }
2602
+ async function run6() {
2603
+ const args2 = process.argv.slice(2).filter((a) => a !== "spinner");
2604
+ const has = (f) => args2.includes(f);
2605
+ const val = (f) => {
2606
+ const i = args2.indexOf(f);
2607
+ return i >= 0 ? args2[i + 1] : void 0;
2608
+ };
2609
+ if (has("--show") || args2.length === 0) {
2610
+ const sc = readSpinnerConfig();
2611
+ console.log("");
2612
+ console.log("terminalhire spinner \u2014 job matches in the Claude Code spinner line");
2613
+ console.log("");
2614
+ console.log(` enabled: ${sc.enabled}`);
2615
+ console.log(` mode: ${sc.mode} (replace = only job matches; append = mixed with Claude defaults)`);
2616
+ console.log(` max: ${sc.max} (max job verbs that rotate)`);
2617
+ console.log(` frequency: ${sc.frequency} (always = up to max; sometimes = up to 2; rare = 1 per cycle)`);
2618
+ console.log("");
2619
+ console.log(" terminalhire spinner --on enable (asks consent, backs up settings.json)");
2620
+ console.log(" terminalhire spinner --off disable + restore your original spinner");
2621
+ console.log(" terminalhire spinner --mode append mix job verbs with Claude's defaults");
2622
+ console.log(" terminalhire spinner --mode replace show only job matches");
2623
+ console.log(" terminalhire spinner --max N cap how many job verbs rotate (1\u201312)");
2624
+ console.log(" terminalhire spinner --frequency always surface up to max role verbs every cycle");
2625
+ console.log(" terminalhire spinner --frequency sometimes surface up to 2 role verbs (default)");
2626
+ console.log(" terminalhire spinner --frequency rare surface 1 role verb per cycle (quietest)");
2627
+ console.log("");
2628
+ return;
2629
+ }
2630
+ if (has("--off")) {
2631
+ const res = clearSpinnerVerbs();
2632
+ clearSpinnerTips();
2633
+ writeConfig2({ spinner: { ...readSpinnerConfig(), enabled: false } });
2634
+ console.log("");
2635
+ console.log(" Spinner job verbs removed.");
2636
+ if (res.keptUserVerbs > 0) {
2637
+ console.log(` Preserved ${res.keptUserVerbs} spinner verb(s) you set yourself.`);
2638
+ } else {
2639
+ console.log(" Your original spinner is restored.");
2640
+ }
2641
+ console.log(" Re-enable any time: terminalhire spinner --on");
2642
+ console.log("");
2643
+ return;
2644
+ }
2645
+ if ((has("--mode") || has("--max") || has("--frequency")) && !has("--on")) {
2646
+ const cur = readSpinnerConfig();
2647
+ const next = { ...cur };
2648
+ const m = val("--mode");
2649
+ if (m) {
2650
+ if (m !== "append" && m !== "replace") {
2651
+ console.error('Error: --mode must be "append" or "replace".');
2652
+ process.exit(1);
2653
+ }
2654
+ next.mode = m;
2655
+ }
2656
+ const mx = val("--max");
2657
+ if (mx) {
2658
+ const n = parseInt(mx, 10);
2659
+ if (!(n >= 1 && n <= 12)) {
2660
+ console.error("Error: --max must be a number 1\u201312.");
2661
+ process.exit(1);
2662
+ }
2663
+ next.max = n;
2664
+ }
2665
+ const freq = val("--frequency");
2666
+ if (freq) {
2667
+ if (!["always", "sometimes", "rare"].includes(freq)) {
2668
+ console.error('Error: --frequency must be "always", "sometimes", or "rare".');
2669
+ process.exit(1);
2670
+ }
2671
+ next.frequency = freq;
2672
+ }
2673
+ writeConfig2({ spinner: next });
2674
+ console.log(` spinner config updated: mode=${next.mode} max=${next.max} frequency=${next.frequency} enabled=${next.enabled}`);
2675
+ if (next.enabled) {
2676
+ const verbs = buildSpinnerPool(readTopMatches(), next.max, { frequency: next.frequency });
2677
+ if (verbs.length) applySpinnerVerbs(verbs, next.mode);
2678
+ else clearSpinnerVerbs();
2679
+ }
2680
+ return;
2681
+ }
2682
+ if (has("--on")) {
2683
+ const mode = val("--mode") === "replace" ? "replace" : "append";
2684
+ const maxRaw = parseInt(val("--max"), 10);
2685
+ const max = maxRaw >= 1 && maxRaw <= 12 ? maxRaw : 6;
2686
+ const freqRaw = val("--frequency");
2687
+ const frequency = ["always", "sometimes", "rare"].includes(freqRaw) ? freqRaw : "sometimes";
2688
+ console.log("");
2689
+ 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");
2690
+ console.log("\u2502 terminalhire \u2014 enable the spinner job surface \u2502");
2691
+ 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");
2692
+ console.log("");
2693
+ console.log("WHAT THIS CHANGES:");
2694
+ console.log(' \u2022 Adds a "spinnerVerbs" key to ~/.claude/settings.json \u2014 the official,');
2695
+ console.log(" documented Claude Code setting. No patching, no binary changes (Rule 7).");
2696
+ console.log(" \u2022 While Claude works, the spinner line shows your TOP LOCAL JOB MATCHES,");
2697
+ console.log(" e.g. Senior Backend Engineer @ Stripe \xB7 82% \u2026");
2698
+ console.log(" \u2022 The tip line below shows a \u2318-clickable terminalhire.com/j/\u2026 link to open");
2699
+ console.log(" the listing (clicks logged anonymously, no profile data).");
2700
+ console.log(` \u2022 mode=${mode} (replace = only job matches; append = mixed with defaults)`);
2701
+ console.log(` \u2022 frequency=${frequency} (always = every cycle; sometimes = up to 2 verbs; rare = 1 verb)`);
2702
+ console.log(" \u2022 Matches are computed LOCALLY and refreshed in the background.");
2703
+ console.log(" ZERO egress \u2014 your profile never leaves the machine; only public job");
2704
+ console.log(" text appears on YOUR screen.");
2705
+ console.log(" \u2022 Any spinner verbs you already set are preserved, never clobbered.");
2706
+ console.log("");
2707
+ console.log("FULLY REVERSIBLE:");
2708
+ console.log(" terminalhire spinner --off removes job verbs, restores your spinner");
2709
+ console.log(" (a timestamped backup of settings.json is taken now)");
2710
+ console.log("");
2711
+ const answer = await ask('Enable the spinner job surface? Type "yes" to continue: ');
2712
+ if (answer !== "yes") {
2713
+ console.log("\nAborted \u2014 nothing changed.");
2714
+ process.exit(0);
2715
+ }
2716
+ const backup = backupSettings();
2717
+ writeConfig2({ spinner: { enabled: true, mode, max, frequency } });
2718
+ const verbs = buildSpinnerPool(readTopMatches(), max, { frequency });
2719
+ if (verbs.length) applySpinnerVerbs(verbs, mode);
2720
+ console.log("");
2721
+ if (backup) console.log(` Backed up settings to: ${backup}`);
2722
+ console.log(` Enabled. ${verbs.length} job verb(s) live now; refreshes in the background.`);
2723
+ if (verbs.length === 0) {
2724
+ console.log(" (No matches cached yet \u2014 run `terminalhire refresh` or wait for the monitor.)");
2725
+ }
2726
+ console.log(" Claude Code picks up settings.json changes automatically.");
2727
+ console.log(" Turn off any time: terminalhire spinner --off");
2728
+ console.log("");
2729
+ return;
2730
+ }
2731
+ console.error("Usage: terminalhire spinner --on | --off | --show | --mode <append|replace> | --max N | --frequency <always|sometimes|rare>");
2732
+ process.exit(1);
2733
+ }
2734
+ var TH_DIR2, CONFIG_FILE3, SETTINGS_PATH, CACHE_FILE;
2735
+ var init_jpi_spinner = __esm({
2736
+ "bin/jpi-spinner.js"() {
2737
+ "use strict";
2738
+ init_spinner();
2739
+ TH_DIR2 = process.env["TERMINALHIRE_DIR"] || join8(homedir6(), ".terminalhire");
2740
+ CONFIG_FILE3 = join8(TH_DIR2, "config.json");
2741
+ SETTINGS_PATH = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join8(homedir6(), ".claude", "settings.json");
2742
+ CACHE_FILE = join8(TH_DIR2, "index-cache.json");
2743
+ }
2744
+ });
2745
+
2746
+ // bin/jpi-init.js
2747
+ var jpi_init_exports = {};
2748
+ __export(jpi_init_exports, {
2749
+ run: () => run7
2750
+ });
2751
+ import { existsSync as existsSync7 } from "fs";
2752
+ import { join as join9, resolve } from "path";
2753
+ import { fileURLToPath as fileURLToPath3 } from "url";
2754
+ import { createInterface as createInterface4 } from "readline";
2755
+ import { spawnSync, spawn } from "child_process";
2756
+ import { homedir as homedir7 } from "os";
2757
+ function ask2(question) {
2758
+ const rl = createInterface4({ input: process.stdin, output: process.stdout });
2759
+ return new Promise((resolve2) => {
2760
+ rl.question(question, (answer) => {
2761
+ rl.close();
2762
+ resolve2(answer.trim().toLowerCase());
2763
+ });
2764
+ });
2765
+ }
2766
+ function resolveScript(name) {
2767
+ const distPath = resolve(join9(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
2768
+ const legacyPath = resolve(join9(__dirname2, `${name}.js`));
2769
+ return existsSync7(distPath) ? distPath : legacyPath;
2770
+ }
2771
+ function resolveInstallJs() {
2772
+ const fromDist = resolve(join9(__dirname2, "..", "..", "install.js"));
2773
+ const fromBin = resolve(join9(__dirname2, "..", "install.js"));
2774
+ if (existsSync7(fromDist)) return fromDist;
2775
+ if (existsSync7(fromBin)) return fromBin;
2776
+ return fromBin;
2777
+ }
2778
+ async function run7() {
2779
+ console.log("");
2780
+ 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");
2781
+ console.log("\u2502 terminalhire init \u2014 one-command onboarding \u2502");
2782
+ console.log("\u2502 Local-first job matching for developers in Claude Code \u2502");
2783
+ 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");
2784
+ console.log("");
2785
+ console.log("This will:");
2786
+ console.log(" 1. Optionally sign you in with GitHub (public profile only, read:user)");
2787
+ console.log(" 2. Seed your local job cache (anonymous index download)");
2788
+ console.log(" 3. Install the statusLine hook in ~/.claude/settings.json");
2789
+ console.log(" (with backup + your explicit consent before any file is touched)");
2790
+ console.log("");
2791
+ console.log('You can stop at any step. Nothing is changed until you say "yes".');
2792
+ console.log("");
2793
+ console.log("Step 1/3 \u2014 GitHub sign-in (optional but recommended)");
2794
+ console.log("");
2795
+ console.log(" Scope: read:user \u2014 public profile + public repos only.");
2796
+ console.log(" Your token is encrypted at ~/.terminalhire/github-token.enc.");
2797
+ console.log(" GitHub data enriches your local profile. Nothing leaves your machine");
2798
+ console.log(" until you explicitly consent to a specific lead.");
2799
+ console.log("");
2800
+ const githubAnswer = await ask2("Sign in with GitHub now? [Y/n] (Enter = yes, n = stay local): ");
2801
+ const doGitHub = githubAnswer === "" || githubAnswer === "y" || githubAnswer === "yes";
2802
+ if (doGitHub) {
2803
+ console.log("");
2804
+ console.log(" Starting GitHub device flow...");
2805
+ const loginScript = resolveScript("jpi-login");
2806
+ const child = spawnSync(process.execPath, [loginScript, "login"], {
2807
+ stdio: ["inherit", "inherit", "inherit"],
2808
+ env: process.env
2809
+ });
2810
+ if (child.status !== 0) {
2811
+ console.log("");
2812
+ console.log(" GitHub sign-in did not complete. Continuing without GitHub.");
2813
+ console.log(" You can sign in any time with: terminalhire login");
2814
+ }
2815
+ } else {
2816
+ console.log("");
2817
+ console.log(" Staying local-only. Tags accumulate from your personal project sessions.");
2818
+ console.log(" Sign in any time with: terminalhire login");
2819
+ }
2820
+ console.log("");
2821
+ console.log("Step 2/3 \u2014 Seeding local job cache");
2822
+ console.log("");
2823
+ console.log(" Fetching anonymous job index (no dev data sent)...");
2824
+ const jobsScript = resolveScript("jpi-jobs");
2825
+ const seedChild = spawnSync(
2826
+ process.execPath,
2827
+ [jobsScript, "--limit", "0"],
2828
+ {
2829
+ stdio: ["ignore", "pipe", "pipe"],
2830
+ env: { ...process.env, TERMINALHIRE_SEED_ONLY: "1" },
2831
+ timeout: 15e3
2832
+ }
2833
+ );
2834
+ if (seedChild.status === 0) {
2835
+ console.log(" Job cache seeded successfully.");
2836
+ } else {
2837
+ console.log(" Could not seed job cache right now (no profile tags yet, or offline).");
2838
+ console.log(" Run `terminalhire jobs` after a few Claude Code sessions to populate it.");
2839
+ }
2840
+ console.log("");
2841
+ console.log("Step 3/3 \u2014 Install statusLine hook in ~/.claude/settings.json");
2842
+ console.log("");
2843
+ console.log(" This is the only step that modifies a system file.");
2844
+ console.log(" A timestamped backup is created before any change.");
2845
+ console.log(" Uninstall at any time: node install.js --uninstall");
2846
+ console.log("");
2847
+ const installJs = resolveInstallJs();
2848
+ const installChild = spawnSync(process.execPath, [installJs], {
2849
+ stdio: ["inherit", "inherit", "inherit"],
2850
+ env: process.env
2851
+ });
2852
+ if (installChild.status !== 0) {
2853
+ console.log("");
2854
+ console.log(" Hook installation did not complete. Run manually: node install.js");
2855
+ }
2856
+ console.log("");
2857
+ 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");
2858
+ console.log("\u2502 terminalhire init complete! \u2502");
2859
+ console.log("\u2502 \u2502");
2860
+ console.log("\u2502 Restart Claude Code to see the statusLine nudge. \u2502");
2861
+ console.log("\u2502 \u2502");
2862
+ console.log("\u2502 Quick reference: \u2502");
2863
+ console.log("\u2502 terminalhire jobs \u2014 browse matching roles \u2502");
2864
+ console.log("\u2502 terminalhire config --show \u2014 view nudge settings \u2502");
2865
+ console.log("\u2502 terminalhire login \u2014 sign in with GitHub \u2502");
2866
+ console.log("\u2502 terminalhire profile --show \u2014 inspect your local profile \u2502");
2867
+ 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");
2868
+ console.log("");
2869
+ }
2870
+ var __dirname2;
2871
+ var init_jpi_init = __esm({
2872
+ "bin/jpi-init.js"() {
2873
+ "use strict";
2874
+ __dirname2 = fileURLToPath3(new URL(".", import.meta.url));
2875
+ }
2876
+ });
2877
+
2878
+ // bin/jpi-refresh.js
2879
+ var jpi_refresh_exports = {};
2880
+ __export(jpi_refresh_exports, {
2881
+ run: () => run8
2882
+ });
2883
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync8, mkdirSync as mkdirSync7 } from "fs";
2884
+ import { join as join10 } from "path";
2885
+ import { homedir as homedir8 } from "os";
2886
+ import { fileURLToPath as fileURLToPath4 } from "url";
2887
+ async function run8() {
2888
+ try {
2889
+ let index;
2890
+ try {
2891
+ const res = await fetch(`${API_URL2}/api/index`, {
2892
+ signal: AbortSignal.timeout(15e3),
2893
+ headers: { "Accept": "application/json" }
2894
+ });
2895
+ if (!res.ok) {
2896
+ process.stderr.write(`terminalhire refresh: index fetch failed (HTTP ${res.status})
2897
+ `);
2898
+ process.exit(1);
2899
+ }
2900
+ index = await res.json();
2901
+ } catch (err) {
2902
+ const msg = err instanceof Error ? err.message : String(err);
2903
+ process.stderr.write(`terminalhire refresh: fetch error \u2014 ${msg}
2904
+ `);
2905
+ process.exit(1);
2906
+ }
2907
+ const jobs = index?.jobs ?? [];
2908
+ let matchCount = 0;
2909
+ let topMatches = [];
2910
+ try {
2911
+ const { readProfile: readProfile2, profileToFingerprint: profileToFingerprint2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
2912
+ const { match: match2 } = await Promise.resolve().then(() => (init_src(), src_exports));
2913
+ const profile = await readProfile2();
2914
+ if (profile.skillTags.length > 0 && jobs.length > 0) {
2915
+ const fp = profileToFingerprint2(profile);
2916
+ const results = match2(fp, jobs, jobs.length);
2917
+ matchCount = results.length;
2918
+ topMatches = results.slice(0, 8).map((r) => ({
2919
+ id: r.job.id,
2920
+ title: r.job.title,
2921
+ company: r.job.company,
2922
+ score: r.score,
2923
+ remote: r.job.remote,
2924
+ matchedTags: r.matchedTags
2925
+ }));
2926
+ }
2927
+ } catch {
2928
+ }
2929
+ mkdirSync7(TERMINALHIRE_DIR5, { recursive: true });
2930
+ const cacheEntry = {
2931
+ ts: Date.now(),
2932
+ index,
2933
+ matchCount,
2934
+ topMatches
2935
+ };
2936
+ writeFileSync7(INDEX_CACHE_FILE2, JSON.stringify(cacheEntry), "utf8");
2937
+ try {
2938
+ const {
2939
+ readSpinnerConfig: readSpinnerConfig2,
2940
+ buildSpinnerPool: buildSpinnerPool2,
2941
+ applySpinnerVerbs: applySpinnerVerbs2,
2942
+ clearSpinnerVerbs: clearSpinnerVerbs2,
2943
+ buildTips: buildTips2,
2944
+ applySpinnerTips: applySpinnerTips2,
2945
+ clearSpinnerTips: clearSpinnerTips2,
2946
+ rankBySessionTags: rankBySessionTags2
2947
+ } = await Promise.resolve().then(() => (init_spinner(), spinner_exports));
2948
+ const sc = readSpinnerConfig2();
2949
+ if (sc.enabled) {
2950
+ let sessionTags;
2951
+ try {
2952
+ const { extractFingerprint: extractFingerprint2 } = await Promise.resolve().then(() => (init_signal(), signal_exports));
2953
+ const fp = extractFingerprint2(process.cwd());
2954
+ if (Array.isArray(fp.skillTags) && fp.skillTags.length > 0) {
2955
+ sessionTags = fp.skillTags;
2956
+ }
2957
+ } catch {
2958
+ }
2959
+ const ranked = rankBySessionTags2(topMatches, sessionTags);
2960
+ const verbs = buildSpinnerPool2(ranked, sc.max, { sessionTags, frequency: sc.frequency });
2961
+ if (verbs.length > 0) applySpinnerVerbs2(verbs, sc.mode);
2962
+ else clearSpinnerVerbs2();
2963
+ const tips = buildTips2(ranked, API_URL2, 3);
2964
+ if (tips.length > 0) applySpinnerTips2(tips);
2965
+ else clearSpinnerTips2();
2966
+ } else {
2967
+ clearSpinnerVerbs2();
2968
+ clearSpinnerTips2();
2969
+ }
2970
+ } catch {
2971
+ }
2972
+ process.exit(0);
2973
+ } catch (err) {
2974
+ const msg = err instanceof Error ? err.message : String(err);
2975
+ process.stderr.write(`terminalhire refresh: unexpected error \u2014 ${msg}
2976
+ `);
2977
+ process.exit(1);
2978
+ }
2979
+ }
2980
+ var __dirname3, TERMINALHIRE_DIR5, INDEX_CACHE_FILE2, API_URL2;
2981
+ var init_jpi_refresh = __esm({
2982
+ "bin/jpi-refresh.js"() {
2983
+ "use strict";
2984
+ __dirname3 = fileURLToPath4(new URL(".", import.meta.url));
2985
+ TERMINALHIRE_DIR5 = join10(homedir8(), ".terminalhire");
2986
+ INDEX_CACHE_FILE2 = join10(TERMINALHIRE_DIR5, "index-cache.json");
2987
+ API_URL2 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
2988
+ }
2989
+ });
2990
+
2991
+ // bin/jpi-save.js
2992
+ var jpi_save_exports = {};
2993
+ __export(jpi_save_exports, {
2994
+ run: () => run9
2995
+ });
2996
+ import { readFileSync as readFileSync10, existsSync as existsSync9 } from "fs";
2997
+ import { join as join11 } from "path";
2998
+ import { homedir as homedir9 } from "os";
2999
+ import { fileURLToPath as fileURLToPath5 } from "url";
3000
+ function findJobInCache(jobId) {
3001
+ try {
3002
+ if (!existsSync9(INDEX_CACHE_FILE3)) return null;
3003
+ const raw = readFileSync10(INDEX_CACHE_FILE3, "utf8");
3004
+ const entry = JSON.parse(raw);
3005
+ const jobs = entry?.index?.jobs ?? [];
3006
+ return jobs.find((j) => j.id === jobId) ?? null;
3007
+ } catch {
3008
+ return null;
3009
+ }
3010
+ }
3011
+ async function cmdSave(jobId) {
3012
+ if (!jobId) {
3013
+ console.error("Usage: terminalhire save <jobId>");
3014
+ console.error(" jobId is shown in `terminalhire jobs` output (e.g. greenhouse:abc123)");
3015
+ process.exit(1);
3016
+ }
3017
+ const { addSavedJob: addSavedJob2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
3018
+ const job = findJobInCache(jobId);
3019
+ if (!job) {
3020
+ console.error(`terminalhire save: job '${jobId}' not found in local index cache.`);
3021
+ console.error(" Run `terminalhire jobs` first to populate the cache.");
3022
+ process.exit(1);
3023
+ }
3024
+ await addSavedJob2({
3025
+ id: job.id,
3026
+ title: job.title,
3027
+ company: job.company,
3028
+ url: job.url,
3029
+ source: job.source,
3030
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
3031
+ // overwritten by addSavedJob, but required by type
3032
+ });
3033
+ console.log(`Saved: ${job.title} \u2014 ${job.company}`);
3034
+ console.log(` id: ${job.id}`);
3035
+ console.log(` url: ${job.url}`);
3036
+ console.log(" Stored locally in encrypted profile. Run `terminalhire saved` to list.");
3037
+ }
3038
+ async function cmdSaved() {
3039
+ const { listSavedJobs: listSavedJobs2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
3040
+ const jobs = await listSavedJobs2();
3041
+ if (jobs.length === 0) {
3042
+ console.log("No saved jobs. Use `terminalhire save <jobId>` to save a role.");
3043
+ return;
3044
+ }
3045
+ console.log(`
3046
+ ${jobs.length} saved job${jobs.length === 1 ? "" : "s"}:
3047
+ `);
3048
+ for (const j of jobs) {
3049
+ const date = new Date(j.savedAt).toLocaleDateString();
3050
+ console.log(` ${j.id}`);
3051
+ console.log(` ${j.title} \u2014 ${j.company}`);
3052
+ console.log(` ${j.url}`);
3053
+ console.log(` Saved: ${date}`);
3054
+ console.log("");
3055
+ }
3056
+ console.log("To remove: terminalhire unsave <jobId>");
3057
+ }
3058
+ async function cmdUnsave(jobId) {
3059
+ if (!jobId) {
3060
+ console.error("Usage: terminalhire unsave <jobId>");
3061
+ process.exit(1);
3062
+ }
3063
+ const { removeSavedJob: removeSavedJob2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
3064
+ const removed = await removeSavedJob2(jobId);
3065
+ if (removed) {
3066
+ console.log(`Removed saved job: ${jobId}`);
3067
+ } else {
3068
+ console.error(`terminalhire unsave: job '${jobId}' was not in your saved list.`);
3069
+ process.exit(1);
3070
+ }
3071
+ }
3072
+ async function run9() {
3073
+ const verb = process.argv[2];
3074
+ const jobId = process.argv[3];
3075
+ try {
3076
+ if (verb === "save") {
3077
+ await cmdSave(jobId);
3078
+ } else if (verb === "saved") {
3079
+ await cmdSaved();
3080
+ } else if (verb === "unsave") {
3081
+ await cmdUnsave(jobId);
3082
+ } else {
3083
+ console.error(`terminalhire: unknown save verb '${verb}'. Expected: save | saved | unsave`);
3084
+ process.exit(1);
3085
+ }
3086
+ } catch (err) {
3087
+ console.error("terminalhire save error:", err.message ?? err);
3088
+ process.exit(1);
3089
+ }
3090
+ }
3091
+ var __dirname4, TERMINALHIRE_DIR6, INDEX_CACHE_FILE3;
3092
+ var init_jpi_save = __esm({
3093
+ "bin/jpi-save.js"() {
3094
+ "use strict";
3095
+ __dirname4 = fileURLToPath5(new URL(".", import.meta.url));
3096
+ TERMINALHIRE_DIR6 = join11(homedir9(), ".terminalhire");
3097
+ INDEX_CACHE_FILE3 = join11(TERMINALHIRE_DIR6, "index-cache.json");
3098
+ }
3099
+ });
3100
+
3101
+ // bin/jpi-dispatch.js
3102
+ import { fileURLToPath as fileURLToPath6 } from "url";
3103
+ import { join as join12, dirname as dirname2 } from "path";
3104
+ import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
2202
3105
  import { createRequire } from "module";
2203
- var __dirname2 = fileURLToPath3(new URL(".", import.meta.url));
3106
+ var __dirname5 = fileURLToPath6(new URL(".", import.meta.url));
3107
+ function readPackageVersion() {
3108
+ try {
3109
+ const candidates = [
3110
+ join12(__dirname5, "..", "..", "package.json"),
3111
+ join12(__dirname5, "..", "package.json")
3112
+ ];
3113
+ for (const p of candidates) {
3114
+ if (existsSync10(p)) {
3115
+ const pkg = JSON.parse(readFileSync11(p, "utf8"));
3116
+ if (pkg.version) return pkg.version;
3117
+ }
3118
+ }
3119
+ } catch {
3120
+ }
3121
+ return "0.1.1";
3122
+ }
2204
3123
  var firstArg = process.argv[2];
2205
3124
  if (!firstArg && !process.stdin.isTTY) {
2206
3125
  const { default: childProcess } = await import("child_process");
2207
- const nudgeScript = join6(__dirname2, "jpi.js");
3126
+ const nudgeScript = join12(__dirname5, "jpi.js");
2208
3127
  const child = childProcess.spawnSync(process.execPath, [nudgeScript], {
2209
3128
  stdio: ["inherit", "inherit", "inherit"]
2210
3129
  });
@@ -2212,21 +3131,34 @@ if (!firstArg && !process.stdin.isTTY) {
2212
3131
  }
2213
3132
  if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-h") {
2214
3133
  console.log("");
2215
- console.log("terminalhire v3.1 \u2014 local-first job matching for developers");
3134
+ console.log(`terminalhire v${readPackageVersion()} \u2014 local-first job matching for developers`);
2216
3135
  console.log("");
2217
3136
  console.log("Commands:");
2218
- console.log(" terminalhire login Sign in with GitHub (recommended \u2014 enriches profile instantly)");
2219
- console.log(" terminalhire logout Clear stored GitHub token");
2220
- console.log(" terminalhire jobs Fetch job index, match locally, browse roles");
2221
- console.log(" terminalhire jobs --limit N Show top N results (default: 10)");
2222
- console.log(" terminalhire jobs --remote-only Filter to remote roles only");
2223
- console.log(" terminalhire profile --show Display your encrypted local profile");
2224
- console.log(" terminalhire profile --edit Set displayName, contactEmail, prefs");
2225
- console.log(" terminalhire profile --delete Wipe profile and encryption key from disk");
3137
+ console.log(" terminalhire init One-command onboarding (start here)");
3138
+ console.log(" terminalhire login Sign in with GitHub (enriches profile instantly)");
3139
+ console.log(" terminalhire logout Clear stored GitHub token");
3140
+ console.log(" terminalhire jobs Fetch job index, match locally, browse roles");
3141
+ console.log(" terminalhire jobs --limit N Show top N results (default: 10)");
3142
+ console.log(" terminalhire jobs --remote-only Filter to remote roles only");
3143
+ console.log(" terminalhire profile --show Display your encrypted local profile");
3144
+ console.log(" terminalhire profile --edit Set displayName, contactEmail, prefs");
3145
+ console.log(" terminalhire profile --delete Wipe profile and encryption key from disk");
3146
+ console.log(" terminalhire config --nudge session Nudge at most once per session (default)");
3147
+ console.log(" terminalhire config --nudge always Nudge every statusLine render");
3148
+ console.log(" terminalhire config --nudge every:N Nudge every Nth render");
3149
+ console.log(" terminalhire config --show Print current config");
3150
+ console.log(" terminalhire spinner --show Job matches in the spinner line while Claude works");
3151
+ console.log(" terminalhire spinner --off Turn the spinner job surface off (restores your spinner)");
3152
+ console.log(" terminalhire spinner --mode append|replace Mix with Claude defaults, or show only matches");
3153
+ console.log(" terminalhire refresh Fetch index + match locally, update cache (non-interactive)");
3154
+ console.log(" terminalhire save <jobId> Save a job locally (id shown in `jobs` output)");
3155
+ console.log(" terminalhire saved List all locally-saved jobs");
3156
+ console.log(" terminalhire unsave <jobId> Remove a saved job");
2226
3157
  console.log("");
2227
3158
  console.log("Install / uninstall the Claude Code statusLine nudge:");
2228
- console.log(" node install.js");
2229
- console.log(" node install.js --uninstall");
3159
+ console.log(" terminalhire init (preferred \u2014 full guided onboarding)");
3160
+ console.log(" node install.js (manual install)");
3161
+ console.log(" node install.js --uninstall (manual uninstall)");
2230
3162
  console.log("");
2231
3163
  console.log("Privacy: your profile never leaves your device.");
2232
3164
  console.log(" GET /api/index \u2014 anonymous index download (no dev data)");
@@ -2236,7 +3168,7 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
2236
3168
  process.exit(0);
2237
3169
  }
2238
3170
  if (firstArg === "--version" || firstArg === "-v") {
2239
- console.log("terminalhire v3.1.0");
3171
+ console.log(`terminalhire v${readPackageVersion()}`);
2240
3172
  process.exit(0);
2241
3173
  }
2242
3174
  if (firstArg === "login" || firstArg === "logout") {
@@ -2260,5 +3192,30 @@ if (firstArg === "learn") {
2260
3192
  await mod.run();
2261
3193
  process.exit(0);
2262
3194
  }
3195
+ if (firstArg === "config") {
3196
+ const mod = await Promise.resolve().then(() => (init_jpi_config(), jpi_config_exports));
3197
+ await mod.run();
3198
+ process.exit(0);
3199
+ }
3200
+ if (firstArg === "spinner") {
3201
+ const mod = await Promise.resolve().then(() => (init_jpi_spinner(), jpi_spinner_exports));
3202
+ await mod.run();
3203
+ process.exit(0);
3204
+ }
3205
+ if (firstArg === "init") {
3206
+ const mod = await Promise.resolve().then(() => (init_jpi_init(), jpi_init_exports));
3207
+ await mod.run();
3208
+ process.exit(0);
3209
+ }
3210
+ if (firstArg === "refresh") {
3211
+ const mod = await Promise.resolve().then(() => (init_jpi_refresh(), jpi_refresh_exports));
3212
+ await mod.run();
3213
+ process.exit(0);
3214
+ }
3215
+ if (firstArg === "save" || firstArg === "saved" || firstArg === "unsave") {
3216
+ const mod = await Promise.resolve().then(() => (init_jpi_save(), jpi_save_exports));
3217
+ await mod.run();
3218
+ process.exit(0);
3219
+ }
2263
3220
  console.error(`terminalhire: unknown command '${firstArg}'. Run 'terminalhire help' for usage.`);
2264
3221
  process.exit(1);