hiregraph 0.1.0 → 0.1.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/dist/index.js +110 -84
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -122,7 +122,7 @@ async function parseResume(filePath) {
|
|
|
122
122
|
throw new Error(`Unsupported resume format: ${ext}. Supported: .pdf, .txt, .md`);
|
|
123
123
|
}
|
|
124
124
|
if (!isApiKeyConfigured()) {
|
|
125
|
-
throw new Error("
|
|
125
|
+
throw new Error("ANTHROPIC_API_KEY not set. Set it in your environment variables.");
|
|
126
126
|
}
|
|
127
127
|
const response = await callHaiku(SYSTEM_PROMPT, `Extract structured data from this resume:
|
|
128
128
|
|
|
@@ -355,10 +355,12 @@ var ROLE_CHOICES = [
|
|
|
355
355
|
{ name: "Founder", value: "founder" },
|
|
356
356
|
{ name: "Builder", value: "builder" }
|
|
357
357
|
];
|
|
358
|
-
|
|
358
|
+
var VALID_ROLES = ROLE_CHOICES.map((c) => c.value);
|
|
359
|
+
async function initCommand(options) {
|
|
359
360
|
header("\n HireGraph Init\n");
|
|
361
|
+
const isNonInteractive = !!(options.name && options.email);
|
|
360
362
|
const existing = await loadJson("identity.json");
|
|
361
|
-
if (existing) {
|
|
363
|
+
if (existing && !isNonInteractive) {
|
|
362
364
|
const { overwrite } = await inquirer.prompt([{
|
|
363
365
|
type: "confirm",
|
|
364
366
|
name: "overwrite",
|
|
@@ -370,87 +372,111 @@ async function initCommand() {
|
|
|
370
372
|
return;
|
|
371
373
|
}
|
|
372
374
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
375
|
+
let name;
|
|
376
|
+
let email;
|
|
377
|
+
let role;
|
|
378
|
+
let targetRoles;
|
|
379
|
+
let remotePref;
|
|
380
|
+
let minComp;
|
|
381
|
+
if (isNonInteractive) {
|
|
382
|
+
name = options.name;
|
|
383
|
+
email = options.email;
|
|
384
|
+
role = VALID_ROLES.includes(options.role || "") ? options.role : "engineer";
|
|
385
|
+
targetRoles = options.targets || "Founding Engineer, Full-Stack Engineer";
|
|
386
|
+
remotePref = options.remote || "Remote";
|
|
387
|
+
minComp = options.compensation || "";
|
|
388
|
+
} else {
|
|
389
|
+
({ name, email } = await inquirer.prompt([
|
|
390
|
+
{ type: "input", name: "name", message: "Name:" },
|
|
391
|
+
{ type: "input", name: "email", message: "Email:" }
|
|
392
|
+
]));
|
|
393
|
+
({ role } = await inquirer.prompt([{
|
|
394
|
+
type: "list",
|
|
395
|
+
name: "role",
|
|
396
|
+
message: "What describes you best?",
|
|
397
|
+
choices: ROLE_CHOICES
|
|
398
|
+
}]));
|
|
399
|
+
({ targetRoles } = await inquirer.prompt([{
|
|
400
|
+
type: "input",
|
|
401
|
+
name: "targetRoles",
|
|
402
|
+
message: "Target roles (comma separated):",
|
|
403
|
+
default: "Founding Engineer, Full-Stack Engineer"
|
|
404
|
+
}]));
|
|
405
|
+
({ remotePref } = await inquirer.prompt([{
|
|
406
|
+
type: "list",
|
|
407
|
+
name: "remotePref",
|
|
408
|
+
message: "Remote preference?",
|
|
409
|
+
choices: ["Remote", "Hybrid", "Onsite", "No preference"]
|
|
410
|
+
}]));
|
|
411
|
+
({ minComp } = await inquirer.prompt([{
|
|
412
|
+
type: "input",
|
|
413
|
+
name: "minComp",
|
|
414
|
+
message: "Min compensation (optional):",
|
|
415
|
+
default: ""
|
|
416
|
+
}]));
|
|
417
|
+
}
|
|
377
418
|
let resumeData = null;
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}]);
|
|
393
|
-
const resolved = resolve(resumePath.trim().replace(/^["']|["']$/g, ""));
|
|
394
|
-
if (existsSync2(resolved)) {
|
|
395
|
-
start("Parsing resume...");
|
|
396
|
-
try {
|
|
397
|
-
resumeData = await parseResume(resolved);
|
|
398
|
-
succeed("Resume parsed");
|
|
399
|
-
info(` Name: ${resumeData.name}`);
|
|
400
|
-
info(` Email: ${resumeData.email}`);
|
|
401
|
-
if (resumeData.work_history.length > 0) {
|
|
402
|
-
info(" Work History:");
|
|
403
|
-
for (const w of resumeData.work_history) {
|
|
404
|
-
info(` ${w.role} @ ${w.company} (${w.start_year}-${w.end_year || "present"})`);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
if (resumeData.skills.length > 0) {
|
|
408
|
-
info(` Skills: ${resumeData.skills.join(", ")}`);
|
|
409
|
-
}
|
|
410
|
-
const { looksRight } = await inquirer.prompt([{
|
|
411
|
-
type: "confirm",
|
|
412
|
-
name: "looksRight",
|
|
413
|
-
message: "Look right?",
|
|
414
|
-
default: true
|
|
415
|
-
}]);
|
|
416
|
-
if (!looksRight) {
|
|
417
|
-
info("Resume data discarded. Using manual input.");
|
|
418
|
-
resumeData = null;
|
|
419
|
+
const resumePath = options.resume;
|
|
420
|
+
if (resumePath && isApiKeyConfigured()) {
|
|
421
|
+
const resolved = resolve(resumePath.trim().replace(/^["']|["']$/g, ""));
|
|
422
|
+
if (existsSync2(resolved)) {
|
|
423
|
+
start("Parsing resume...");
|
|
424
|
+
try {
|
|
425
|
+
resumeData = await parseResume(resolved);
|
|
426
|
+
succeed("Resume parsed");
|
|
427
|
+
info(` Name: ${resumeData.name}`);
|
|
428
|
+
info(` Email: ${resumeData.email}`);
|
|
429
|
+
if (resumeData.work_history.length > 0) {
|
|
430
|
+
info(" Work History:");
|
|
431
|
+
for (const w of resumeData.work_history) {
|
|
432
|
+
info(` ${w.role} @ ${w.company} (${w.start_year}-${w.end_year || "present"})`);
|
|
419
433
|
}
|
|
420
|
-
} catch (err) {
|
|
421
|
-
fail("Failed to parse resume");
|
|
422
|
-
error(err.message);
|
|
423
|
-
resumeData = null;
|
|
424
434
|
}
|
|
435
|
+
} catch (err) {
|
|
436
|
+
fail("Failed to parse resume");
|
|
437
|
+
error(err.message);
|
|
438
|
+
resumeData = null;
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
warn(`Resume file not found: ${resolved}`);
|
|
442
|
+
}
|
|
443
|
+
} else if (!isNonInteractive && !resumePath) {
|
|
444
|
+
const { hasResume } = await inquirer.prompt([{
|
|
445
|
+
type: "confirm",
|
|
446
|
+
name: "hasResume",
|
|
447
|
+
message: "Do you have an existing resume? (PDF/TXT)",
|
|
448
|
+
default: true
|
|
449
|
+
}]);
|
|
450
|
+
if (hasResume) {
|
|
451
|
+
if (!isApiKeyConfigured()) {
|
|
452
|
+
warn("ANTHROPIC_API_KEY not set \u2014 skipping resume parsing. Set it in your environment for full features.");
|
|
425
453
|
} else {
|
|
426
|
-
|
|
454
|
+
const { rPath } = await inquirer.prompt([{
|
|
455
|
+
type: "input",
|
|
456
|
+
name: "rPath",
|
|
457
|
+
message: "Path to resume:"
|
|
458
|
+
}]);
|
|
459
|
+
const resolved = resolve(rPath.trim().replace(/^["']|["']$/g, ""));
|
|
460
|
+
if (existsSync2(resolved)) {
|
|
461
|
+
start("Parsing resume...");
|
|
462
|
+
try {
|
|
463
|
+
resumeData = await parseResume(resolved);
|
|
464
|
+
succeed("Resume parsed");
|
|
465
|
+
const { looksRight } = await inquirer.prompt([{
|
|
466
|
+
type: "confirm",
|
|
467
|
+
name: "looksRight",
|
|
468
|
+
message: "Look right?",
|
|
469
|
+
default: true
|
|
470
|
+
}]);
|
|
471
|
+
if (!looksRight) resumeData = null;
|
|
472
|
+
} catch (err) {
|
|
473
|
+
fail("Failed to parse resume");
|
|
474
|
+
error(err.message);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
427
477
|
}
|
|
428
478
|
}
|
|
429
479
|
}
|
|
430
|
-
const { role } = await inquirer.prompt([{
|
|
431
|
-
type: "list",
|
|
432
|
-
name: "role",
|
|
433
|
-
message: "What describes you best?",
|
|
434
|
-
choices: ROLE_CHOICES
|
|
435
|
-
}]);
|
|
436
|
-
const { targetRoles } = await inquirer.prompt([{
|
|
437
|
-
type: "input",
|
|
438
|
-
name: "targetRoles",
|
|
439
|
-
message: "Target roles (comma separated):",
|
|
440
|
-
default: "Founding Engineer, Full-Stack Engineer"
|
|
441
|
-
}]);
|
|
442
|
-
const { remotePref } = await inquirer.prompt([{
|
|
443
|
-
type: "list",
|
|
444
|
-
name: "remotePref",
|
|
445
|
-
message: "Remote preference?",
|
|
446
|
-
choices: ["Remote", "Hybrid", "Onsite", "No preference"]
|
|
447
|
-
}]);
|
|
448
|
-
const { minComp } = await inquirer.prompt([{
|
|
449
|
-
type: "input",
|
|
450
|
-
name: "minComp",
|
|
451
|
-
message: "Min compensation (optional):",
|
|
452
|
-
default: ""
|
|
453
|
-
}]);
|
|
454
480
|
const targets = targetRoles.split(",").map((r) => r.trim()).filter(Boolean);
|
|
455
481
|
const identity = resumeData ? resumeToIdentity(resumeData, role, targets, remotePref, minComp) : {
|
|
456
482
|
name,
|
|
@@ -1591,7 +1617,7 @@ async function scanCommand(path) {
|
|
|
1591
1617
|
fail("Layer 7: LLM classification failed");
|
|
1592
1618
|
}
|
|
1593
1619
|
} else {
|
|
1594
|
-
dim(" Layer 7: Skipped (
|
|
1620
|
+
dim(" Layer 7: Skipped (set ANTHROPIC_API_KEY for LLM classification)");
|
|
1595
1621
|
}
|
|
1596
1622
|
const scanResult = {
|
|
1597
1623
|
project_name: projectName,
|
|
@@ -2036,7 +2062,7 @@ var BATCH_SIZE2 = 10;
|
|
|
2036
2062
|
var BATCH_DELAY_MS2 = 1e3;
|
|
2037
2063
|
async function parseJobsBatch(jobs) {
|
|
2038
2064
|
if (!isApiKeyConfigured()) {
|
|
2039
|
-
throw new Error("
|
|
2065
|
+
throw new Error("ANTHROPIC_API_KEY not set. Set it in your environment variables.");
|
|
2040
2066
|
}
|
|
2041
2067
|
const cached = await loadSubJson("jobs", "parsed.json");
|
|
2042
2068
|
const requirements = cached?.requirements || {};
|
|
@@ -2372,7 +2398,7 @@ var BATCH_SIZE3 = 5;
|
|
|
2372
2398
|
var BATCH_DELAY_MS3 = 1500;
|
|
2373
2399
|
async function evaluateMatchesBatch(candidates, graph) {
|
|
2374
2400
|
if (!isApiKeyConfigured()) {
|
|
2375
|
-
throw new Error("
|
|
2401
|
+
throw new Error("ANTHROPIC_API_KEY not set. Set it in your environment variables.");
|
|
2376
2402
|
}
|
|
2377
2403
|
const skillSummary = buildSkillSummary(graph);
|
|
2378
2404
|
const results = [];
|
|
@@ -2557,7 +2583,7 @@ async function runMatchPipeline(options) {
|
|
|
2557
2583
|
async function matchesCommand(options) {
|
|
2558
2584
|
header("\n HireGraph Matches\n");
|
|
2559
2585
|
if (!isApiKeyConfigured()) {
|
|
2560
|
-
warn(
|
|
2586
|
+
warn('ANTHROPIC_API_KEY not set. Add it to your environment variables or run: $env:ANTHROPIC_API_KEY = "your-key"');
|
|
2561
2587
|
return;
|
|
2562
2588
|
}
|
|
2563
2589
|
try {
|
|
@@ -3019,7 +3045,7 @@ async function submitToAshby(rawJobId, pdfBuffer, identity) {
|
|
|
3019
3045
|
async function applyCommand(jobId, options) {
|
|
3020
3046
|
header("\n HireGraph Apply\n");
|
|
3021
3047
|
if (!isApiKeyConfigured()) {
|
|
3022
|
-
warn(
|
|
3048
|
+
warn('ANTHROPIC_API_KEY not set. Add it to your environment variables or run: $env:ANTHROPIC_API_KEY = "your-key"');
|
|
3023
3049
|
return;
|
|
3024
3050
|
}
|
|
3025
3051
|
const graph = await loadGraph();
|
|
@@ -3270,8 +3296,8 @@ function getTimeAgo2(isoDate) {
|
|
|
3270
3296
|
|
|
3271
3297
|
// src/index.ts
|
|
3272
3298
|
var program = new Command();
|
|
3273
|
-
program.name("hiregraph").description("Turn your code into job applications. Local-first CLI that scans codebases and builds skill graphs.").version("0.1.
|
|
3274
|
-
program.command("init").description("Set up your builder profile (resume upload + preferences)").action(initCommand);
|
|
3299
|
+
program.name("hiregraph").description("Turn your code into job applications. Local-first CLI that scans codebases and builds skill graphs.").version("0.1.2");
|
|
3300
|
+
program.command("init").description("Set up your builder profile (resume upload + preferences)").option("--name <name>", "Your full name").option("--email <email>", "Your email address").option("--role <role>", "Your role (engineer, pm, designer, founder, builder)").option("--targets <roles>", "Target roles, comma separated").option("--remote <pref>", "Remote preference (Remote, Hybrid, Onsite)").option("--resume <path>", "Path to resume PDF/TXT").option("--compensation <amount>", "Minimum compensation").action(initCommand);
|
|
3275
3301
|
program.command("scan").description("Scan a project and update your skill graph").argument("[path]", "Path to the project directory", ".").action(scanCommand);
|
|
3276
3302
|
program.command("status").description("Show your current skill graph summary").action(statusCommand);
|
|
3277
3303
|
program.command("jobs").description("Fetch job listings from Greenhouse, Lever, and Ashby boards").option("--refresh", "Force refresh cached jobs").option("--ats <type>", "Filter by ATS type (greenhouse, lever, ashby)").option("--limit <n>", "Show sample of N job titles", parseInt).action(jobsCommand);
|