hiregraph 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3282 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/init.ts
7
+ import inquirer from "inquirer";
8
+ import { existsSync as existsSync2 } from "fs";
9
+ import { resolve } from "path";
10
+
11
+ // src/storage/store.ts
12
+ import { homedir } from "os";
13
+ import { join } from "path";
14
+ import { mkdir, readFile, writeFile } from "fs/promises";
15
+ import { existsSync } from "fs";
16
+ var HIREGRAPH_DIR = join(homedir(), ".hiregraph");
17
+ async function ensureDir() {
18
+ if (!existsSync(HIREGRAPH_DIR)) {
19
+ await mkdir(HIREGRAPH_DIR, { recursive: true });
20
+ }
21
+ }
22
+ function getPath(filename) {
23
+ return join(HIREGRAPH_DIR, filename);
24
+ }
25
+ async function loadJson(filename) {
26
+ const filepath = getPath(filename);
27
+ if (!existsSync(filepath)) return null;
28
+ const raw = await readFile(filepath, "utf-8");
29
+ return JSON.parse(raw);
30
+ }
31
+ async function saveJson(filename, data) {
32
+ await ensureDir();
33
+ const filepath = getPath(filename);
34
+ await writeFile(filepath, JSON.stringify(data, null, 2), "utf-8");
35
+ }
36
+ async function ensureSubDir(subdir) {
37
+ const dirPath = join(HIREGRAPH_DIR, subdir);
38
+ if (!existsSync(dirPath)) {
39
+ await mkdir(dirPath, { recursive: true });
40
+ }
41
+ }
42
+ async function loadSubJson(subdir, filename) {
43
+ await ensureSubDir(subdir);
44
+ const filepath = join(HIREGRAPH_DIR, subdir, filename);
45
+ if (!existsSync(filepath)) return null;
46
+ const raw = await readFile(filepath, "utf-8");
47
+ return JSON.parse(raw);
48
+ }
49
+ async function saveSubJson(subdir, filename, data) {
50
+ await ensureSubDir(subdir);
51
+ const filepath = join(HIREGRAPH_DIR, subdir, filename);
52
+ await writeFile(filepath, JSON.stringify(data, null, 2), "utf-8");
53
+ }
54
+
55
+ // src/resume/parser.ts
56
+ import { readFile as readFile2 } from "fs/promises";
57
+ import { extname } from "path";
58
+
59
+ // src/llm/client.ts
60
+ import Anthropic from "@anthropic-ai/sdk";
61
+ var client = null;
62
+ function isApiKeyConfigured() {
63
+ return !!process.env.ANTHROPIC_API_KEY;
64
+ }
65
+ function getClient() {
66
+ if (!client) {
67
+ client = new Anthropic();
68
+ }
69
+ return client;
70
+ }
71
+ async function callHaiku(systemPrompt, userPrompt) {
72
+ const anthropic = getClient();
73
+ const message = await anthropic.messages.create({
74
+ model: "claude-haiku-4-5-20251001",
75
+ max_tokens: 1024,
76
+ system: systemPrompt,
77
+ messages: [{ role: "user", content: userPrompt }]
78
+ });
79
+ const block = message.content[0];
80
+ if (block.type === "text") {
81
+ return block.text;
82
+ }
83
+ throw new Error("Unexpected response format from Haiku");
84
+ }
85
+ async function callHaikuJson(systemPrompt, userPrompt, maxTokens = 1024) {
86
+ const anthropic = getClient();
87
+ const message = await anthropic.messages.create({
88
+ model: "claude-haiku-4-5-20251001",
89
+ max_tokens: maxTokens,
90
+ system: systemPrompt,
91
+ messages: [{ role: "user", content: userPrompt }]
92
+ });
93
+ const block = message.content[0];
94
+ if (block.type !== "text") throw new Error("Unexpected response format from Haiku");
95
+ const cleaned = block.text.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
96
+ return JSON.parse(cleaned);
97
+ }
98
+
99
+ // src/resume/parser.ts
100
+ var SYSTEM_PROMPT = `You extract structured data from resume text.
101
+ Return JSON only, no markdown fences. Schema:
102
+ {
103
+ "name": "string",
104
+ "email": "string",
105
+ "phone": "string or null",
106
+ "links": { "github": "url", "linkedin": "url", "portfolio": "url" },
107
+ "work_history": [{ "company": "string", "role": "string", "start_year": number, "end_year": number|null }],
108
+ "skills": ["string array"],
109
+ "education": [{ "institution": "string", "degree": "string", "field": "string", "year": number }]
110
+ }`;
111
+ async function parseResume(filePath) {
112
+ const ext = extname(filePath).toLowerCase();
113
+ let text;
114
+ if (ext === ".pdf") {
115
+ const pdfParse = (await import("pdf-parse")).default;
116
+ const buffer = await readFile2(filePath);
117
+ const data = await pdfParse(buffer);
118
+ text = data.text;
119
+ } else if (ext === ".txt" || ext === ".md") {
120
+ text = await readFile2(filePath, "utf-8");
121
+ } else {
122
+ throw new Error(`Unsupported resume format: ${ext}. Supported: .pdf, .txt, .md`);
123
+ }
124
+ if (!isApiKeyConfigured()) {
125
+ throw new Error("No API key detected. Run hiregraph inside Claude Code or Cursor for resume parsing.");
126
+ }
127
+ const response = await callHaiku(SYSTEM_PROMPT, `Extract structured data from this resume:
128
+
129
+ ${text}`);
130
+ const cleaned = response.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
131
+ const parsed = JSON.parse(cleaned);
132
+ return {
133
+ name: parsed.name || "",
134
+ email: parsed.email || "",
135
+ phone: parsed.phone || void 0,
136
+ links: parsed.links || {},
137
+ work_history: (parsed.work_history || []).map((w) => ({
138
+ company: w.company || "",
139
+ role: w.role || "",
140
+ start_year: w.start_year || 0,
141
+ end_year: w.end_year || void 0
142
+ })),
143
+ skills: parsed.skills || [],
144
+ education: (parsed.education || []).map((e) => ({
145
+ institution: e.institution || "",
146
+ degree: e.degree || "",
147
+ field: e.field || "",
148
+ year: e.year || 0
149
+ }))
150
+ };
151
+ }
152
+ function resumeToIdentity(parsed, role, targetRoles, remotePref, minComp) {
153
+ return {
154
+ name: parsed.name,
155
+ email: parsed.email,
156
+ phone: parsed.phone,
157
+ primary_role: role,
158
+ target_roles: targetRoles,
159
+ remote_preference: remotePref,
160
+ min_compensation: minComp,
161
+ previous_companies: parsed.work_history,
162
+ education: parsed.education,
163
+ links: parsed.links,
164
+ source: "resume-upload"
165
+ };
166
+ }
167
+
168
+ // src/graph/schema.ts
169
+ function createEmptySkillGraph(identity) {
170
+ return {
171
+ builder_identity: identity,
172
+ tech_stack: {},
173
+ architecture: {},
174
+ quality: {
175
+ test_ratio: 0,
176
+ complexity_avg: 0,
177
+ type_safety: false,
178
+ secrets_clean: true
179
+ },
180
+ projects: [],
181
+ builder_profile: {
182
+ is_end_to_end: false,
183
+ role_signals: []
184
+ },
185
+ last_updated: (/* @__PURE__ */ new Date()).toISOString()
186
+ };
187
+ }
188
+
189
+ // src/graph/skill-graph.ts
190
+ var GRAPH_FILE = "skill-graph.json";
191
+ async function loadGraph() {
192
+ return loadJson(GRAPH_FILE);
193
+ }
194
+ async function saveGraph(graph) {
195
+ graph.last_updated = (/* @__PURE__ */ new Date()).toISOString();
196
+ await saveJson(GRAPH_FILE, graph);
197
+ }
198
+ function buildProjectEntry(scan) {
199
+ return {
200
+ name: scan.project_name,
201
+ path: scan.project_path,
202
+ domain: scan.llm_classification?.domain || "unknown",
203
+ stack: [
204
+ ...scan.dependencies.frameworks,
205
+ ...Object.keys(scan.file_discovery.languages)
206
+ ],
207
+ languages: scan.file_discovery.languages,
208
+ commits: scan.git_forensics.commits,
209
+ active_days: scan.git_forensics.active_days,
210
+ contributors: scan.git_forensics.contributors,
211
+ test_ratio: scan.quality_signals.test_ratio,
212
+ complexity_avg: scan.quality_signals.complexity_avg,
213
+ patterns: scan.architecture_patterns.patterns,
214
+ description: scan.llm_classification?.description || "",
215
+ scanned_at: (/* @__PURE__ */ new Date()).toISOString()
216
+ };
217
+ }
218
+ function mergeIntoGraph(graph, project, classification) {
219
+ const existingIdx = graph.projects.findIndex((p) => p.path === project.path);
220
+ if (existingIdx >= 0) {
221
+ graph.projects[existingIdx] = project;
222
+ } else {
223
+ graph.projects.push(project);
224
+ }
225
+ graph.tech_stack = {};
226
+ for (const proj of graph.projects) {
227
+ for (const [lang, stats] of Object.entries(proj.languages)) {
228
+ if (!graph.tech_stack[lang]) {
229
+ graph.tech_stack[lang] = {
230
+ proficiency: 0,
231
+ source: "code-verified",
232
+ loc: 0,
233
+ projects: 0,
234
+ advanced_features: [],
235
+ last_seen: proj.scanned_at
236
+ };
237
+ }
238
+ const skill = graph.tech_stack[lang];
239
+ skill.loc += stats.loc;
240
+ skill.projects += 1;
241
+ if (proj.scanned_at > skill.last_seen) {
242
+ skill.last_seen = proj.scanned_at;
243
+ }
244
+ }
245
+ for (const fw of proj.stack) {
246
+ if (!graph.tech_stack[fw]) {
247
+ graph.tech_stack[fw] = {
248
+ proficiency: 0,
249
+ source: "code-verified",
250
+ loc: 0,
251
+ projects: 0,
252
+ advanced_features: [],
253
+ last_seen: proj.scanned_at
254
+ };
255
+ }
256
+ if (!proj.languages[fw]) {
257
+ graph.tech_stack[fw].projects += 1;
258
+ }
259
+ }
260
+ }
261
+ for (const skill of Object.values(graph.tech_stack)) {
262
+ skill.proficiency = calculateProficiency(skill);
263
+ }
264
+ graph.architecture = {};
265
+ for (const proj of graph.projects) {
266
+ for (const [pattern, confidence] of Object.entries(proj.patterns)) {
267
+ if (!graph.architecture[pattern] || confidence > graph.architecture[pattern].confidence) {
268
+ graph.architecture[pattern] = { confidence };
269
+ }
270
+ }
271
+ }
272
+ graph.quality = averageQuality(graph.projects);
273
+ if (classification) {
274
+ graph.builder_profile = {
275
+ is_end_to_end: classification.is_end_to_end || graph.builder_profile.is_end_to_end,
276
+ role_signals: [.../* @__PURE__ */ new Set([
277
+ ...graph.builder_profile.role_signals,
278
+ ...classification.role_signals
279
+ ])]
280
+ };
281
+ }
282
+ return graph;
283
+ }
284
+ function calculateProficiency(skill) {
285
+ const locScore = Math.min(1, skill.loc / 5e4) * 0.4;
286
+ const projectScore = Math.min(1, skill.projects / 10) * 0.3;
287
+ const featureScore = Math.min(1, skill.advanced_features.length / 5) * 0.3;
288
+ return Math.round(Math.min(1, locScore + projectScore + featureScore) * 100) / 100;
289
+ }
290
+ function averageQuality(projects) {
291
+ if (projects.length === 0) {
292
+ return { test_ratio: 0, complexity_avg: 0, type_safety: false, secrets_clean: true };
293
+ }
294
+ const testRatio = projects.reduce((sum, p) => sum + p.test_ratio, 0) / projects.length;
295
+ const complexity = projects.reduce((sum, p) => sum + p.complexity_avg, 0) / projects.length;
296
+ return {
297
+ test_ratio: Math.round(testRatio * 100) / 100,
298
+ complexity_avg: Math.round(complexity * 10) / 10,
299
+ type_safety: false,
300
+ // Will be updated per-project
301
+ secrets_clean: true
302
+ };
303
+ }
304
+
305
+ // src/utils/logger.ts
306
+ import chalk from "chalk";
307
+ function info(msg) {
308
+ console.log(chalk.cyan(msg));
309
+ }
310
+ function success(msg) {
311
+ console.log(chalk.green(msg));
312
+ }
313
+ function warn(msg) {
314
+ console.log(chalk.yellow(msg));
315
+ }
316
+ function error(msg) {
317
+ console.log(chalk.red(msg));
318
+ }
319
+ function header(msg) {
320
+ console.log(chalk.bold.white(msg));
321
+ }
322
+ function dim(msg) {
323
+ console.log(chalk.dim(msg));
324
+ }
325
+ function layerOutput(label, detail) {
326
+ console.log(` ${chalk.cyan(label.padEnd(28))} ${detail}`);
327
+ }
328
+
329
+ // src/utils/spinner.ts
330
+ import ora from "ora";
331
+ var current = null;
332
+ function start(text) {
333
+ if (current) current.stop();
334
+ current = ora(text).start();
335
+ return current;
336
+ }
337
+ function succeed(text) {
338
+ if (current) {
339
+ current.succeed(text);
340
+ current = null;
341
+ }
342
+ }
343
+ function fail(text) {
344
+ if (current) {
345
+ current.fail(text);
346
+ current = null;
347
+ }
348
+ }
349
+
350
+ // src/commands/init.ts
351
+ var ROLE_CHOICES = [
352
+ { name: "Engineer", value: "engineer" },
353
+ { name: "PM", value: "pm" },
354
+ { name: "Designer", value: "designer" },
355
+ { name: "Founder", value: "founder" },
356
+ { name: "Builder", value: "builder" }
357
+ ];
358
+ async function initCommand() {
359
+ header("\n HireGraph Init\n");
360
+ const existing = await loadJson("identity.json");
361
+ if (existing) {
362
+ const { overwrite } = await inquirer.prompt([{
363
+ type: "confirm",
364
+ name: "overwrite",
365
+ message: "Profile already exists. Overwrite?",
366
+ default: false
367
+ }]);
368
+ if (!overwrite) {
369
+ info("Keeping existing profile.");
370
+ return;
371
+ }
372
+ }
373
+ const { name, email } = await inquirer.prompt([
374
+ { type: "input", name: "name", message: "Name:" },
375
+ { type: "input", name: "email", message: "Email:" }
376
+ ]);
377
+ let resumeData = null;
378
+ const { hasResume } = await inquirer.prompt([{
379
+ type: "confirm",
380
+ name: "hasResume",
381
+ message: "Do you have an existing resume? (PDF/TXT)",
382
+ default: true
383
+ }]);
384
+ if (hasResume) {
385
+ if (!isApiKeyConfigured()) {
386
+ warn("No API key detected. Run inside Claude Code for resume parsing. Using manual flow.");
387
+ } else {
388
+ const { resumePath } = await inquirer.prompt([{
389
+ type: "input",
390
+ name: "resumePath",
391
+ message: "Path to resume:"
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
+ }
420
+ } catch (err) {
421
+ fail("Failed to parse resume");
422
+ error(err.message);
423
+ resumeData = null;
424
+ }
425
+ } else {
426
+ warn("File not found. Continuing without resume.");
427
+ }
428
+ }
429
+ }
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
+ const targets = targetRoles.split(",").map((r) => r.trim()).filter(Boolean);
455
+ const identity = resumeData ? resumeToIdentity(resumeData, role, targets, remotePref, minComp) : {
456
+ name,
457
+ email,
458
+ primary_role: role,
459
+ target_roles: targets,
460
+ remote_preference: remotePref,
461
+ min_compensation: minComp,
462
+ previous_companies: [],
463
+ education: [],
464
+ links: {},
465
+ source: "manual"
466
+ };
467
+ if (name) identity.name = name;
468
+ if (email) identity.email = email;
469
+ await saveJson("identity.json", identity);
470
+ await saveJson("config.json", { excluded_companies: [], auto_apply_threshold: 8 });
471
+ const graph = createEmptySkillGraph(identity);
472
+ await saveGraph(graph);
473
+ success("\nProfile saved to ~/.hiregraph/identity.json");
474
+ info("Run `hiregraph scan <path>` to analyze your first project.");
475
+ }
476
+
477
+ // src/commands/scan.ts
478
+ import { resolve as resolve2, basename } from "path";
479
+ import { existsSync as existsSync8 } from "fs";
480
+
481
+ // src/layers/file-discovery.ts
482
+ import { readdir, readFile as readFile4 } from "fs/promises";
483
+ import { join as join3, extname as extname2, relative } from "path";
484
+
485
+ // src/utils/gitignore.ts
486
+ import ignore from "ignore";
487
+ import { readFile as readFile3 } from "fs/promises";
488
+ import { join as join2 } from "path";
489
+ import { existsSync as existsSync3 } from "fs";
490
+ var DEFAULT_IGNORES = [
491
+ "node_modules",
492
+ ".git",
493
+ "dist",
494
+ "build",
495
+ "out",
496
+ ".next",
497
+ ".nuxt",
498
+ "__pycache__",
499
+ ".pytest_cache",
500
+ "target",
501
+ "vendor",
502
+ ".venv",
503
+ "venv",
504
+ "env",
505
+ ".tox",
506
+ ".mypy_cache",
507
+ "coverage",
508
+ ".nyc_output",
509
+ ".turbo",
510
+ ".vercel",
511
+ ".expo",
512
+ ".cache"
513
+ ];
514
+ async function createFilter(projectPath) {
515
+ const ig = ignore();
516
+ ig.add(DEFAULT_IGNORES);
517
+ const gitignorePath = join2(projectPath, ".gitignore");
518
+ if (existsSync3(gitignorePath)) {
519
+ const content = await readFile3(gitignorePath, "utf-8");
520
+ ig.add(content);
521
+ }
522
+ return ig;
523
+ }
524
+
525
+ // src/utils/language-map.ts
526
+ var LANGUAGE_MAP = {
527
+ ".ts": "TypeScript",
528
+ ".tsx": "TypeScript",
529
+ ".js": "JavaScript",
530
+ ".jsx": "JavaScript",
531
+ ".mjs": "JavaScript",
532
+ ".cjs": "JavaScript",
533
+ ".py": "Python",
534
+ ".rs": "Rust",
535
+ ".go": "Go",
536
+ ".java": "Java",
537
+ ".kt": "Kotlin",
538
+ ".kts": "Kotlin",
539
+ ".swift": "Swift",
540
+ ".rb": "Ruby",
541
+ ".php": "PHP",
542
+ ".c": "C",
543
+ ".h": "C",
544
+ ".cpp": "C++",
545
+ ".cc": "C++",
546
+ ".cxx": "C++",
547
+ ".hpp": "C++",
548
+ ".cs": "C#",
549
+ ".dart": "Dart",
550
+ ".lua": "Lua",
551
+ ".r": "R",
552
+ ".R": "R",
553
+ ".scala": "Scala",
554
+ ".ex": "Elixir",
555
+ ".exs": "Elixir",
556
+ ".erl": "Erlang",
557
+ ".hs": "Haskell",
558
+ ".clj": "Clojure",
559
+ ".vue": "Vue",
560
+ ".svelte": "Svelte",
561
+ ".sol": "Solidity",
562
+ ".zig": "Zig",
563
+ ".nim": "Nim",
564
+ ".ml": "OCaml",
565
+ ".sh": "Shell",
566
+ ".bash": "Shell",
567
+ ".zsh": "Shell",
568
+ ".sql": "SQL"
569
+ };
570
+ var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
571
+ ".png",
572
+ ".jpg",
573
+ ".jpeg",
574
+ ".gif",
575
+ ".svg",
576
+ ".ico",
577
+ ".webp",
578
+ ".bmp",
579
+ ".woff",
580
+ ".woff2",
581
+ ".ttf",
582
+ ".eot",
583
+ ".otf",
584
+ ".mp3",
585
+ ".mp4",
586
+ ".avi",
587
+ ".mov",
588
+ ".wav",
589
+ ".ogg",
590
+ ".zip",
591
+ ".tar",
592
+ ".gz",
593
+ ".rar",
594
+ ".7z",
595
+ ".pdf",
596
+ ".doc",
597
+ ".docx",
598
+ ".xls",
599
+ ".xlsx",
600
+ ".pptx",
601
+ ".exe",
602
+ ".dll",
603
+ ".so",
604
+ ".dylib",
605
+ ".bin",
606
+ ".lock",
607
+ ".map"
608
+ ]);
609
+ function getLanguage(ext) {
610
+ return LANGUAGE_MAP[ext] ?? null;
611
+ }
612
+ function shouldSkip(ext) {
613
+ return SKIP_EXTENSIONS.has(ext);
614
+ }
615
+
616
+ // src/layers/file-discovery.ts
617
+ var CONFIG_PATTERNS = [
618
+ "Dockerfile",
619
+ "docker-compose.yml",
620
+ "docker-compose.yaml",
621
+ ".github",
622
+ ".gitlab-ci.yml",
623
+ ".circleci",
624
+ "tsconfig.json",
625
+ "jest.config",
626
+ "vitest.config",
627
+ "vite.config",
628
+ ".eslintrc",
629
+ ".prettierrc",
630
+ "prettier.config",
631
+ "biome.json",
632
+ "ruff.toml",
633
+ ".flake8",
634
+ "Makefile",
635
+ "Procfile",
636
+ "vercel.json",
637
+ "netlify.toml",
638
+ "turbo.json",
639
+ "lerna.json"
640
+ ];
641
+ async function analyzeFileDiscovery(projectPath) {
642
+ const ig = await createFilter(projectPath);
643
+ const files = [];
644
+ const configFiles = [];
645
+ await walkDir(projectPath, projectPath, ig, files, configFiles);
646
+ const languages = {};
647
+ let totalLoc = 0;
648
+ for (const file of files) {
649
+ if (!languages[file.language]) {
650
+ languages[file.language] = { files: 0, loc: 0 };
651
+ }
652
+ languages[file.language].files++;
653
+ languages[file.language].loc += file.loc;
654
+ totalLoc += file.loc;
655
+ }
656
+ let primaryLanguage = "";
657
+ let maxLoc = 0;
658
+ for (const [lang, stats] of Object.entries(languages)) {
659
+ if (stats.loc > maxLoc) {
660
+ maxLoc = stats.loc;
661
+ primaryLanguage = lang;
662
+ }
663
+ }
664
+ return {
665
+ total_files: files.length,
666
+ total_loc: totalLoc,
667
+ languages,
668
+ config_files: configFiles,
669
+ primary_language: primaryLanguage
670
+ };
671
+ }
672
+ async function walkDir(basePath, currentPath, ig, files, configFiles) {
673
+ const entries = await readdir(currentPath, { withFileTypes: true });
674
+ for (const entry of entries) {
675
+ const fullPath = join3(currentPath, entry.name);
676
+ const relPath = relative(basePath, fullPath).replace(/\\/g, "/");
677
+ if (ig.ignores(relPath)) continue;
678
+ if (entry.isDirectory()) {
679
+ if (CONFIG_PATTERNS.some((p) => entry.name === p || entry.name.startsWith(p))) {
680
+ configFiles.push(entry.name);
681
+ }
682
+ await walkDir(basePath, fullPath, ig, files, configFiles);
683
+ } else if (entry.isFile()) {
684
+ const ext = extname2(entry.name).toLowerCase();
685
+ if (CONFIG_PATTERNS.some((p) => entry.name === p || entry.name.startsWith(p))) {
686
+ configFiles.push(entry.name);
687
+ }
688
+ if (shouldSkip(ext)) continue;
689
+ const language = getLanguage(ext);
690
+ if (!language) continue;
691
+ try {
692
+ const content = await readFile4(fullPath, "utf-8");
693
+ const loc = countLines(content);
694
+ files.push({ relativePath: relPath, language, loc });
695
+ } catch {
696
+ }
697
+ }
698
+ }
699
+ }
700
+ function countLines(content) {
701
+ const lines = content.split("\n");
702
+ let count = 0;
703
+ for (const line of lines) {
704
+ if (line.trim().length > 0) count++;
705
+ }
706
+ return count;
707
+ }
708
+
709
+ // src/layers/dependency-extraction.ts
710
+ import { readFile as readFile5 } from "fs/promises";
711
+ import { join as join4 } from "path";
712
+ import { existsSync as existsSync4 } from "fs";
713
+ var FRAMEWORK_MAP = {
714
+ // Frontend
715
+ "react": "React",
716
+ "react-dom": "React",
717
+ "next": "Next.js",
718
+ "vue": "Vue",
719
+ "nuxt": "Nuxt",
720
+ "svelte": "Svelte",
721
+ "@sveltejs/kit": "SvelteKit",
722
+ "angular": "Angular",
723
+ "@angular/core": "Angular",
724
+ "solid-js": "Solid",
725
+ "astro": "Astro",
726
+ "gatsby": "Gatsby",
727
+ "remix": "Remix",
728
+ // Mobile
729
+ "react-native": "React Native",
730
+ "expo": "Expo",
731
+ "flutter": "Flutter",
732
+ // Backend
733
+ "express": "Express",
734
+ "fastify": "Fastify",
735
+ "koa": "Koa",
736
+ "hono": "Hono",
737
+ "nestjs": "NestJS",
738
+ "@nestjs/core": "NestJS",
739
+ // Database
740
+ "prisma": "Prisma",
741
+ "@prisma/client": "Prisma",
742
+ "drizzle-orm": "Drizzle",
743
+ "typeorm": "TypeORM",
744
+ "sequelize": "Sequelize",
745
+ "mongoose": "Mongoose",
746
+ "@supabase/supabase-js": "Supabase",
747
+ "firebase": "Firebase",
748
+ // Python frameworks (from requirements.txt)
749
+ "django": "Django",
750
+ "flask": "Flask",
751
+ "fastapi": "FastAPI",
752
+ "tornado": "Tornado",
753
+ "starlette": "Starlette",
754
+ "sqlalchemy": "SQLAlchemy",
755
+ "pandas": "Pandas",
756
+ "numpy": "NumPy",
757
+ "tensorflow": "TensorFlow",
758
+ "torch": "PyTorch",
759
+ "pytorch": "PyTorch",
760
+ // Rust frameworks
761
+ "actix-web": "Actix",
762
+ "axum": "Axum",
763
+ "rocket": "Rocket",
764
+ "warp": "Warp",
765
+ "tokio": "Tokio",
766
+ "serde": "Serde",
767
+ // Go frameworks
768
+ "github.com/gin-gonic/gin": "Gin",
769
+ "github.com/gofiber/fiber": "Fiber",
770
+ "github.com/labstack/echo": "Echo",
771
+ // AI/ML
772
+ "@anthropic-ai/sdk": "Anthropic SDK",
773
+ "openai": "OpenAI",
774
+ "langchain": "LangChain",
775
+ "@langchain/core": "LangChain"
776
+ };
777
+ async function analyzeDependencies(projectPath) {
778
+ const deps = [];
779
+ const devDeps = [];
780
+ const frameworks = /* @__PURE__ */ new Set();
781
+ let ecosystem = "unknown";
782
+ let hasLockfile = false;
783
+ const pkgPath = join4(projectPath, "package.json");
784
+ if (existsSync4(pkgPath)) {
785
+ ecosystem = "node";
786
+ hasLockfile = existsSync4(join4(projectPath, "package-lock.json")) || existsSync4(join4(projectPath, "yarn.lock")) || existsSync4(join4(projectPath, "pnpm-lock.yaml")) || existsSync4(join4(projectPath, "bun.lockb"));
787
+ try {
788
+ const pkg = JSON.parse(await readFile5(pkgPath, "utf-8"));
789
+ if (pkg.dependencies) {
790
+ for (const dep of Object.keys(pkg.dependencies)) {
791
+ deps.push(dep);
792
+ const fw = FRAMEWORK_MAP[dep];
793
+ if (fw) frameworks.add(fw);
794
+ }
795
+ }
796
+ if (pkg.devDependencies) {
797
+ for (const dep of Object.keys(pkg.devDependencies)) {
798
+ devDeps.push(dep);
799
+ const fw = FRAMEWORK_MAP[dep];
800
+ if (fw) frameworks.add(fw);
801
+ }
802
+ }
803
+ } catch {
804
+ }
805
+ }
806
+ const reqPath = join4(projectPath, "requirements.txt");
807
+ if (existsSync4(reqPath)) {
808
+ ecosystem = ecosystem === "node" ? "multi" : "python";
809
+ hasLockfile = hasLockfile || existsSync4(join4(projectPath, "Pipfile.lock")) || existsSync4(join4(projectPath, "poetry.lock"));
810
+ try {
811
+ const content = await readFile5(reqPath, "utf-8");
812
+ for (const line of content.split("\n")) {
813
+ const trimmed = line.trim();
814
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
815
+ const name = trimmed.split(/[=<>!~]/)[0].trim().toLowerCase();
816
+ if (name) {
817
+ deps.push(name);
818
+ const fw = FRAMEWORK_MAP[name];
819
+ if (fw) frameworks.add(fw);
820
+ }
821
+ }
822
+ } catch {
823
+ }
824
+ }
825
+ const pyprojectPath = join4(projectPath, "pyproject.toml");
826
+ if (existsSync4(pyprojectPath)) {
827
+ ecosystem = ecosystem === "unknown" ? "python" : ecosystem === "python" ? "python" : "multi";
828
+ try {
829
+ const content = await readFile5(pyprojectPath, "utf-8");
830
+ const depMatches = content.match(/dependencies\s*=\s*\[([\s\S]*?)\]/);
831
+ if (depMatches) {
832
+ const items = depMatches[1].match(/"([^"]+)"/g);
833
+ if (items) {
834
+ for (const item of items) {
835
+ const name = item.replace(/"/g, "").split(/[=<>!~]/)[0].trim().toLowerCase();
836
+ if (name && !deps.includes(name)) {
837
+ deps.push(name);
838
+ const fw = FRAMEWORK_MAP[name];
839
+ if (fw) frameworks.add(fw);
840
+ }
841
+ }
842
+ }
843
+ }
844
+ } catch {
845
+ }
846
+ }
847
+ const cargoPath = join4(projectPath, "Cargo.toml");
848
+ if (existsSync4(cargoPath)) {
849
+ ecosystem = ecosystem === "unknown" ? "rust" : "multi";
850
+ hasLockfile = hasLockfile || existsSync4(join4(projectPath, "Cargo.lock"));
851
+ try {
852
+ const content = await readFile5(cargoPath, "utf-8");
853
+ const depSection = content.match(/\[dependencies\]([\s\S]*?)(?=\[|$)/);
854
+ if (depSection) {
855
+ for (const line of depSection[1].split("\n")) {
856
+ const match = line.match(/^(\S+)\s*=/);
857
+ if (match) {
858
+ const name = match[1].trim();
859
+ deps.push(name);
860
+ const fw = FRAMEWORK_MAP[name];
861
+ if (fw) frameworks.add(fw);
862
+ }
863
+ }
864
+ }
865
+ const devSection = content.match(/\[dev-dependencies\]([\s\S]*?)(?=\[|$)/);
866
+ if (devSection) {
867
+ for (const line of devSection[1].split("\n")) {
868
+ const match = line.match(/^(\S+)\s*=/);
869
+ if (match) devDeps.push(match[1].trim());
870
+ }
871
+ }
872
+ } catch {
873
+ }
874
+ }
875
+ const goModPath = join4(projectPath, "go.mod");
876
+ if (existsSync4(goModPath)) {
877
+ ecosystem = ecosystem === "unknown" ? "go" : "multi";
878
+ hasLockfile = hasLockfile || existsSync4(join4(projectPath, "go.sum"));
879
+ try {
880
+ const content = await readFile5(goModPath, "utf-8");
881
+ const requireBlock = content.match(/require\s*\(([\s\S]*?)\)/);
882
+ if (requireBlock) {
883
+ for (const line of requireBlock[1].split("\n")) {
884
+ const trimmed = line.trim();
885
+ if (!trimmed || trimmed.startsWith("//")) continue;
886
+ const parts = trimmed.split(/\s+/);
887
+ if (parts[0]) {
888
+ deps.push(parts[0]);
889
+ const fw = FRAMEWORK_MAP[parts[0]];
890
+ if (fw) frameworks.add(fw);
891
+ }
892
+ }
893
+ }
894
+ } catch {
895
+ }
896
+ }
897
+ return {
898
+ ecosystem,
899
+ dependencies: deps,
900
+ dev_dependencies: devDeps,
901
+ frameworks: [...frameworks],
902
+ has_lockfile: hasLockfile
903
+ };
904
+ }
905
+
906
+ // src/layers/ast-analysis.ts
907
+ import { readFile as readFile6, readdir as readdir2 } from "fs/promises";
908
+ import { join as join5, extname as extname3, relative as relative2 } from "path";
909
+ var MAX_FILES = 100;
910
+ async function analyzeAst(projectPath, languages) {
911
+ const ig = await createFilter(projectPath);
912
+ const files = await collectSourceFiles(projectPath, projectPath, ig);
913
+ const sampled = files.length > MAX_FILES ? files.sort(() => Math.random() - 0.5).slice(0, MAX_FILES) : files;
914
+ const totals = {
915
+ functions: 0,
916
+ classes: 0,
917
+ interfaces: 0,
918
+ components: 0,
919
+ hooks: 0,
920
+ services: 0,
921
+ nestingDepth: 0,
922
+ paramCounts: [],
923
+ imports: [],
924
+ features: []
925
+ };
926
+ for (const filePath of sampled) {
927
+ try {
928
+ const content = await readFile6(filePath, "utf-8");
929
+ const ext = extname3(filePath).toLowerCase();
930
+ const analysis = analyzeFile(content, ext);
931
+ totals.functions += analysis.functions;
932
+ totals.classes += analysis.classes;
933
+ totals.interfaces += analysis.interfaces;
934
+ totals.components += analysis.components;
935
+ totals.hooks += analysis.hooks;
936
+ totals.services += analysis.services;
937
+ totals.nestingDepth = Math.max(totals.nestingDepth, analysis.nestingDepth);
938
+ totals.paramCounts.push(...analysis.paramCounts);
939
+ totals.imports.push(...analysis.imports);
940
+ totals.features.push(...analysis.features);
941
+ } catch {
942
+ }
943
+ }
944
+ const uniqueImports = [...new Set(totals.imports)];
945
+ const uniqueFeatures = [...new Set(totals.features)];
946
+ const avgParams = totals.paramCounts.length > 0 ? Math.round(totals.paramCounts.reduce((a, b) => a + b, 0) / totals.paramCounts.length * 100) / 100 : 0;
947
+ return {
948
+ functions: totals.functions,
949
+ classes: totals.classes,
950
+ interfaces: totals.interfaces,
951
+ components: totals.components,
952
+ hooks: totals.hooks,
953
+ services: totals.services,
954
+ max_nesting_depth: totals.nestingDepth,
955
+ avg_params_per_function: avgParams,
956
+ imports_used: uniqueImports,
957
+ advanced_features: uniqueFeatures
958
+ };
959
+ }
960
+ function analyzeFile(content, ext) {
961
+ const result = {
962
+ functions: 0,
963
+ classes: 0,
964
+ interfaces: 0,
965
+ components: 0,
966
+ hooks: 0,
967
+ services: 0,
968
+ nestingDepth: 0,
969
+ paramCounts: [],
970
+ imports: [],
971
+ features: []
972
+ };
973
+ if ([".ts", ".tsx", ".js", ".jsx", ".mjs"].includes(ext)) {
974
+ analyzeTypeScript(content, ext, result);
975
+ } else if (ext === ".py") {
976
+ analyzePython(content, result);
977
+ } else if (ext === ".rs") {
978
+ analyzeRust(content, result);
979
+ } else if (ext === ".go") {
980
+ analyzeGo(content, result);
981
+ }
982
+ return result;
983
+ }
984
+ function analyzeTypeScript(content, ext, result) {
985
+ const funcMatches = content.match(/(?:function\s+\w+|(?:const|let|var)\s+\w+\s*=\s*(?:async\s*)?\(|(?:async\s+)?(?:export\s+)?(?:default\s+)?function)/g);
986
+ if (funcMatches) {
987
+ result.functions += funcMatches.length;
988
+ for (const m of funcMatches) {
989
+ const params = m.match(/\(([^)]*)\)/);
990
+ if (params && params[1]) {
991
+ result.paramCounts.push(params[1].split(",").filter((p) => p.trim()).length);
992
+ }
993
+ }
994
+ }
995
+ const arrowFuncs = content.match(/=>\s*{/g);
996
+ if (arrowFuncs) result.functions += arrowFuncs.length;
997
+ const classMatches = content.match(/\bclass\s+\w+/g);
998
+ if (classMatches) result.classes += classMatches.length;
999
+ const ifaceMatches = content.match(/\b(?:interface|type)\s+\w+/g);
1000
+ if (ifaceMatches) result.interfaces += ifaceMatches.length;
1001
+ if ([".tsx", ".jsx"].includes(ext)) {
1002
+ const componentMatches = content.match(/(?:export\s+)?(?:default\s+)?function\s+[A-Z]\w*/g);
1003
+ if (componentMatches) result.components += componentMatches.length;
1004
+ const arrowComponents = content.match(/(?:const|export\s+const)\s+[A-Z]\w+\s*[=:]/g);
1005
+ if (arrowComponents) result.components += arrowComponents.length;
1006
+ }
1007
+ const hookMatches = content.match(/(?:function|const)\s+use[A-Z]\w*/g);
1008
+ if (hookMatches) result.hooks += hookMatches.length;
1009
+ const serviceMatches = content.match(/class\s+\w*(?:Service|Repository|Controller)\b/g);
1010
+ if (serviceMatches) result.services += serviceMatches.length;
1011
+ const importMatches = content.matchAll(/import\s+.*?from\s+['"]([^'"]+)['"]/g);
1012
+ for (const m of importMatches) {
1013
+ const pkg = m[1];
1014
+ if (!pkg.startsWith(".") && !pkg.startsWith("/")) {
1015
+ result.imports.push(pkg.split("/").slice(0, pkg.startsWith("@") ? 2 : 1).join("/"));
1016
+ }
1017
+ }
1018
+ if (content.match(/<\w+(?:\s+extends|\s*,)/)) result.features.push("generics");
1019
+ if (content.match(/\bkeyof\b|\bin\s+keyof\b/)) result.features.push("mapped-types");
1020
+ if (content.match(/\binfer\b/)) result.features.push("conditional-types");
1021
+ if (content.match(/@\w+/)) result.features.push("decorators");
1022
+ if (content.match(/\basync\s+/)) result.features.push("async-await");
1023
+ result.nestingDepth = estimateNesting(content);
1024
+ }
1025
+ function analyzePython(content, result) {
1026
+ const funcMatches = content.match(/\bdef\s+\w+/g);
1027
+ if (funcMatches) result.functions += funcMatches.length;
1028
+ const classMatches = content.match(/\bclass\s+\w+/g);
1029
+ if (classMatches) result.classes += classMatches.length;
1030
+ const importMatches = content.matchAll(/(?:from\s+(\S+)\s+import|import\s+(\S+))/g);
1031
+ for (const m of importMatches) {
1032
+ const pkg = (m[1] || m[2]).split(".")[0];
1033
+ if (pkg && !pkg.startsWith(".")) result.imports.push(pkg);
1034
+ }
1035
+ if (content.match(/@\w+/)) result.features.push("decorators");
1036
+ if (content.match(/@dataclass/)) result.features.push("dataclasses");
1037
+ if (content.match(/->\s*\w+|:\s*\w+\s*[=,)]/)) result.features.push("type-hints");
1038
+ if (content.match(/\basync\s+def\b/)) result.features.push("async-await");
1039
+ const serviceMatches = content.match(/class\s+\w*(?:Service|Repository|Controller)\b/g);
1040
+ if (serviceMatches) result.services += serviceMatches.length;
1041
+ result.nestingDepth = estimateIndentNesting(content);
1042
+ }
1043
+ function analyzeRust(content, result) {
1044
+ const fnMatches = content.match(/\bfn\s+\w+/g);
1045
+ if (fnMatches) result.functions += fnMatches.length;
1046
+ const structMatches = content.match(/\bstruct\s+\w+/g);
1047
+ if (structMatches) result.classes += structMatches.length;
1048
+ const traitMatches = content.match(/\btrait\s+\w+/g);
1049
+ if (traitMatches) result.interfaces += traitMatches.length;
1050
+ const implMatches = content.match(/\bimpl\b/g);
1051
+ if (implMatches) result.features.push("impl-blocks");
1052
+ const useMatches = content.matchAll(/use\s+(\w+)/g);
1053
+ for (const m of useMatches) {
1054
+ if (m[1] !== "std" && m[1] !== "self" && m[1] !== "super" && m[1] !== "crate") {
1055
+ result.imports.push(m[1]);
1056
+ }
1057
+ }
1058
+ if (content.match(/\basync\s+fn\b/)) result.features.push("async-await");
1059
+ if (content.match(/macro_rules!/)) result.features.push("macros");
1060
+ if (content.match(/<[^>]+>/)) result.features.push("generics");
1061
+ if (content.match(/\bunsafe\b/)) result.features.push("unsafe");
1062
+ result.nestingDepth = estimateNesting(content);
1063
+ }
1064
+ function analyzeGo(content, result) {
1065
+ const funcMatches = content.match(/\bfunc\s+(?:\([^)]*\)\s*)?\w+/g);
1066
+ if (funcMatches) result.functions += funcMatches.length;
1067
+ const structMatches = content.match(/\btype\s+\w+\s+struct\b/g);
1068
+ if (structMatches) result.classes += structMatches.length;
1069
+ const ifaceMatches = content.match(/\btype\s+\w+\s+interface\b/g);
1070
+ if (ifaceMatches) result.interfaces += ifaceMatches.length;
1071
+ const importBlock = content.match(/import\s*\(([\s\S]*?)\)/);
1072
+ if (importBlock) {
1073
+ const imports = importBlock[1].matchAll(/"([^"]+)"/g);
1074
+ for (const m of imports) {
1075
+ const parts = m[1].split("/");
1076
+ result.imports.push(parts[parts.length - 1]);
1077
+ }
1078
+ }
1079
+ if (content.match(/\bgo\s+\w+/)) result.features.push("goroutines");
1080
+ if (content.match(/\bchan\b/)) result.features.push("channels");
1081
+ if (content.match(/\binterface\s*\{/)) result.features.push("interfaces");
1082
+ result.nestingDepth = estimateNesting(content);
1083
+ }
1084
+ function estimateNesting(content) {
1085
+ let max = 0;
1086
+ let current2 = 0;
1087
+ for (const char of content) {
1088
+ if (char === "{") {
1089
+ current2++;
1090
+ max = Math.max(max, current2);
1091
+ } else if (char === "}") {
1092
+ current2 = Math.max(0, current2 - 1);
1093
+ }
1094
+ }
1095
+ return max;
1096
+ }
1097
+ function estimateIndentNesting(content) {
1098
+ let max = 0;
1099
+ for (const line of content.split("\n")) {
1100
+ if (line.trim().length === 0) continue;
1101
+ const indent = line.match(/^(\s*)/);
1102
+ if (indent) {
1103
+ const level = Math.floor(indent[1].length / 4);
1104
+ max = Math.max(max, level);
1105
+ }
1106
+ }
1107
+ return max;
1108
+ }
1109
+ async function collectSourceFiles(basePath, currentPath, ig) {
1110
+ const files = [];
1111
+ const codeExts = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".rs", ".go"]);
1112
+ const entries = await readdir2(currentPath, { withFileTypes: true });
1113
+ for (const entry of entries) {
1114
+ const fullPath = join5(currentPath, entry.name);
1115
+ const relPath = relative2(basePath, fullPath).replace(/\\/g, "/");
1116
+ if (ig.ignores(relPath)) continue;
1117
+ if (entry.isDirectory()) {
1118
+ files.push(...await collectSourceFiles(basePath, fullPath, ig));
1119
+ } else if (entry.isFile()) {
1120
+ const ext = extname3(entry.name).toLowerCase();
1121
+ if (codeExts.has(ext)) files.push(fullPath);
1122
+ }
1123
+ }
1124
+ return files;
1125
+ }
1126
+
1127
+ // src/layers/git-forensics.ts
1128
+ import { execFile } from "child_process";
1129
+ import { promisify } from "util";
1130
+ import { existsSync as existsSync5 } from "fs";
1131
+ import { join as join6 } from "path";
1132
+ var execFileAsync = promisify(execFile);
1133
+ var EMPTY_RESULT = {
1134
+ is_git_repo: false,
1135
+ commits: 0,
1136
+ active_days: 0,
1137
+ contributors: 0,
1138
+ commits_per_active_day: 0,
1139
+ active_days_per_week: 0,
1140
+ first_commit: "",
1141
+ last_commit: "",
1142
+ conventional_commit_ratio: 0,
1143
+ branches: 0,
1144
+ primary_author: ""
1145
+ };
1146
+ var CONVENTIONAL_PREFIXES = [
1147
+ "feat:",
1148
+ "fix:",
1149
+ "refactor:",
1150
+ "docs:",
1151
+ "test:",
1152
+ "chore:",
1153
+ "style:",
1154
+ "perf:",
1155
+ "ci:",
1156
+ "build:",
1157
+ "revert:",
1158
+ "feat(",
1159
+ "fix(",
1160
+ "refactor(",
1161
+ "docs(",
1162
+ "test(",
1163
+ "chore("
1164
+ ];
1165
+ async function analyzeGitForensics(projectPath) {
1166
+ if (!existsSync5(join6(projectPath, ".git"))) {
1167
+ return { ...EMPTY_RESULT };
1168
+ }
1169
+ try {
1170
+ const opts = { cwd: projectPath, maxBuffer: 10 * 1024 * 1024 };
1171
+ const [logResult, branchResult] = await Promise.all([
1172
+ execFileAsync("git", ["log", "--format=%an|%aI|%s", "--all"], opts),
1173
+ execFileAsync("git", ["branch", "-a", "--format=%(refname:short)"], opts)
1174
+ ]);
1175
+ const lines = logResult.stdout.trim().split("\n").filter(Boolean);
1176
+ if (lines.length === 0) return { ...EMPTY_RESULT, is_git_repo: true };
1177
+ const authors = {};
1178
+ const dates = /* @__PURE__ */ new Set();
1179
+ let conventionalCount = 0;
1180
+ let firstDate = "";
1181
+ let lastDate = "";
1182
+ for (const line of lines) {
1183
+ const parts = line.split("|");
1184
+ if (parts.length < 3) continue;
1185
+ const author = parts[0];
1186
+ const date = parts[1];
1187
+ const subject = parts.slice(2).join("|");
1188
+ authors[author] = (authors[author] || 0) + 1;
1189
+ const day = date.slice(0, 10);
1190
+ dates.add(day);
1191
+ if (!lastDate) lastDate = date;
1192
+ firstDate = date;
1193
+ const lowerSubject = subject.toLowerCase().trim();
1194
+ if (CONVENTIONAL_PREFIXES.some((p) => lowerSubject.startsWith(p))) {
1195
+ conventionalCount++;
1196
+ }
1197
+ }
1198
+ const activeDays = dates.size;
1199
+ const commits = lines.length;
1200
+ const firstDateObj = new Date(firstDate);
1201
+ const lastDateObj = new Date(lastDate);
1202
+ const weeksSpan = Math.max(1, (lastDateObj.getTime() - firstDateObj.getTime()) / (7 * 24 * 60 * 60 * 1e3));
1203
+ let primaryAuthor = "";
1204
+ let maxCommits = 0;
1205
+ for (const [author, count] of Object.entries(authors)) {
1206
+ if (count > maxCommits) {
1207
+ maxCommits = count;
1208
+ primaryAuthor = author;
1209
+ }
1210
+ }
1211
+ const branches = branchResult.stdout.trim().split("\n").filter(Boolean).length;
1212
+ return {
1213
+ is_git_repo: true,
1214
+ commits,
1215
+ active_days: activeDays,
1216
+ contributors: Object.keys(authors).length,
1217
+ commits_per_active_day: Math.round(commits / activeDays * 100) / 100,
1218
+ active_days_per_week: Math.round(activeDays / weeksSpan * 100) / 100,
1219
+ first_commit: firstDate,
1220
+ last_commit: lastDate,
1221
+ conventional_commit_ratio: Math.round(conventionalCount / commits * 100) / 100,
1222
+ branches,
1223
+ primary_author: primaryAuthor
1224
+ };
1225
+ } catch {
1226
+ return { ...EMPTY_RESULT, is_git_repo: true };
1227
+ }
1228
+ }
1229
+
1230
+ // src/layers/quality-signals.ts
1231
+ import { readFile as readFile7 } from "fs/promises";
1232
+ import { join as join7 } from "path";
1233
+ import { existsSync as existsSync6 } from "fs";
1234
+ var SECRET_PATTERNS = [
1235
+ /(?:api[_-]?key|apikey)\s*[:=]\s*["'][^"']{8,}["']/i,
1236
+ /AKIA[0-9A-Z]{16}/,
1237
+ /(?:secret|password|credential|token)\s*[:=]\s*["'][^"']{8,}["']/i,
1238
+ /ghp_[a-zA-Z0-9]{36}/,
1239
+ /sk-[a-zA-Z0-9]{20,}/
1240
+ ];
1241
+ var LINT_CONFIGS = [
1242
+ ".eslintrc",
1243
+ ".eslintrc.js",
1244
+ ".eslintrc.json",
1245
+ ".eslintrc.yml",
1246
+ ".eslintrc.yaml",
1247
+ "eslint.config.js",
1248
+ "eslint.config.mjs",
1249
+ "eslint.config.ts",
1250
+ ".prettierrc",
1251
+ ".prettierrc.js",
1252
+ ".prettierrc.json",
1253
+ "prettier.config.js",
1254
+ "biome.json",
1255
+ "biome.jsonc",
1256
+ "ruff.toml",
1257
+ ".flake8",
1258
+ "mypy.ini",
1259
+ "clippy.toml",
1260
+ ".clippy.toml",
1261
+ ".golangci.yml",
1262
+ ".golangci.yaml"
1263
+ ];
1264
+ async function analyzeQualitySignals(projectPath, fileDiscovery) {
1265
+ let testLoc = 0;
1266
+ let sourceLoc = 0;
1267
+ for (const [, stats] of Object.entries(fileDiscovery.languages)) {
1268
+ sourceLoc += stats.loc;
1269
+ }
1270
+ testLoc = await estimateTestLoc(projectPath, fileDiscovery);
1271
+ const testRatio = sourceLoc > 0 ? Math.round(testLoc / sourceLoc * 100) / 100 : 0;
1272
+ const { typeSafety, typeSafetyDetails } = await checkTypeSafety(projectPath);
1273
+ const { secretsClean, secretsFound } = await scanSecrets(projectPath);
1274
+ const lintTools = [];
1275
+ for (const config of LINT_CONFIGS) {
1276
+ if (existsSync6(join7(projectPath, config))) {
1277
+ const tool = config.includes("eslint") ? "ESLint" : config.includes("prettier") ? "Prettier" : config.includes("biome") ? "Biome" : config.includes("ruff") ? "Ruff" : config.includes("flake8") ? "Flake8" : config.includes("mypy") ? "mypy" : config.includes("clippy") ? "Clippy" : config.includes("golangci") ? "golangci-lint" : config;
1278
+ if (!lintTools.includes(tool)) lintTools.push(tool);
1279
+ }
1280
+ }
1281
+ const complexityAvg = await estimateComplexity(projectPath, fileDiscovery);
1282
+ return {
1283
+ test_ratio: testRatio,
1284
+ complexity_avg: complexityAvg,
1285
+ type_safety: typeSafety,
1286
+ type_safety_details: typeSafetyDetails,
1287
+ secrets_clean: secretsClean,
1288
+ secrets_found: secretsFound,
1289
+ lint_tools: lintTools
1290
+ };
1291
+ }
1292
+ async function estimateTestLoc(projectPath, fileDiscovery) {
1293
+ let testLoc = 0;
1294
+ const testDirs = ["__tests__", "tests", "test", "spec"];
1295
+ for (const dir of testDirs) {
1296
+ if (existsSync6(join7(projectPath, dir))) {
1297
+ testLoc += Math.round(fileDiscovery.total_loc * 0.05);
1298
+ }
1299
+ }
1300
+ if (fileDiscovery.config_files.some((f) => f.includes("jest") || f.includes("vitest"))) {
1301
+ testLoc += Math.round(fileDiscovery.total_loc * 0.1);
1302
+ }
1303
+ return Math.min(testLoc, Math.round(fileDiscovery.total_loc * 0.5));
1304
+ }
1305
+ async function checkTypeSafety(projectPath) {
1306
+ const tsconfigPath = join7(projectPath, "tsconfig.json");
1307
+ if (existsSync6(tsconfigPath)) {
1308
+ try {
1309
+ const content = await readFile7(tsconfigPath, "utf-8");
1310
+ const tsconfig = JSON.parse(content);
1311
+ if (tsconfig.compilerOptions?.strict === true) {
1312
+ return { typeSafety: true, typeSafetyDetails: "TypeScript strict mode" };
1313
+ }
1314
+ return { typeSafety: false, typeSafetyDetails: "TypeScript without strict mode" };
1315
+ } catch {
1316
+ }
1317
+ }
1318
+ if (existsSync6(join7(projectPath, "mypy.ini")) || existsSync6(join7(projectPath, "pyrightconfig.json"))) {
1319
+ return { typeSafety: true, typeSafetyDetails: "Python type checker configured" };
1320
+ }
1321
+ const pyprojectPath = join7(projectPath, "pyproject.toml");
1322
+ if (existsSync6(pyprojectPath)) {
1323
+ try {
1324
+ const content = await readFile7(pyprojectPath, "utf-8");
1325
+ if (content.includes("[tool.mypy]") || content.includes("[tool.pyright]")) {
1326
+ return { typeSafety: true, typeSafetyDetails: "Python type checker configured" };
1327
+ }
1328
+ } catch {
1329
+ }
1330
+ }
1331
+ return { typeSafety: false, typeSafetyDetails: "No type checking configured" };
1332
+ }
1333
+ async function scanSecrets(projectPath) {
1334
+ let found = 0;
1335
+ const filesToCheck = [".env", ".env.local", ".env.production"];
1336
+ for (const file of filesToCheck) {
1337
+ const filePath = join7(projectPath, file);
1338
+ if (existsSync6(filePath)) {
1339
+ try {
1340
+ const content = await readFile7(filePath, "utf-8");
1341
+ for (const pattern of SECRET_PATTERNS) {
1342
+ const matches = content.match(pattern);
1343
+ if (matches) found += matches.length;
1344
+ }
1345
+ } catch {
1346
+ }
1347
+ }
1348
+ }
1349
+ return { secretsClean: found === 0, secretsFound: found };
1350
+ }
1351
+ async function estimateComplexity(projectPath, fileDiscovery) {
1352
+ const branchKeywords = /\b(if|else|elif|switch|case|for|while|catch|except|&&|\|\||\?)\b/g;
1353
+ const tsconfigExists = existsSync6(join7(projectPath, "tsconfig.json"));
1354
+ const mainFile = tsconfigExists ? join7(projectPath, "src", "index.ts") : join7(projectPath, "src", "index.js");
1355
+ if (!existsSync6(mainFile)) {
1356
+ return 3.5;
1357
+ }
1358
+ try {
1359
+ const content = await readFile7(mainFile, "utf-8");
1360
+ const branches = (content.match(branchKeywords) || []).length;
1361
+ const functions = (content.match(/\b(function|def|fn|func)\b/g) || []).length || 1;
1362
+ return Math.round(branches / functions * 10) / 10;
1363
+ } catch {
1364
+ return 3.5;
1365
+ }
1366
+ }
1367
+
1368
+ // src/layers/architecture-patterns.ts
1369
+ import { existsSync as existsSync7 } from "fs";
1370
+ import { join as join8 } from "path";
1371
+ async function analyzeArchitecturePatterns(projectPath, fileDiscovery, astAnalysis) {
1372
+ const patterns = [
1373
+ detectServiceLayer(projectPath, astAnalysis),
1374
+ detectMvc(projectPath),
1375
+ detectEventDriven(astAnalysis),
1376
+ detectRepositoryPattern(astAnalysis),
1377
+ detectMicroservices(projectPath),
1378
+ detectMonorepo(projectPath),
1379
+ detectApiFirst(projectPath, fileDiscovery)
1380
+ ];
1381
+ const result = {};
1382
+ let primaryPattern = null;
1383
+ let highestScore = 0;
1384
+ for (const p of patterns) {
1385
+ const confidence = p.maxSignals > 0 ? Math.round(p.score / p.maxSignals * 100) / 100 : 0;
1386
+ if (confidence >= 0.2) {
1387
+ result[p.name] = confidence;
1388
+ if (confidence > highestScore) {
1389
+ highestScore = confidence;
1390
+ primaryPattern = p.name;
1391
+ }
1392
+ }
1393
+ }
1394
+ return { patterns: result, primary_pattern: primaryPattern };
1395
+ }
1396
+ function detectServiceLayer(projectPath, ast) {
1397
+ let score = 0;
1398
+ const max = 5;
1399
+ if (ast.services > 0) score += 2;
1400
+ if (ast.services >= 3) score += 1;
1401
+ if (existsSync7(join8(projectPath, "src", "services")) || existsSync7(join8(projectPath, "services"))) score += 1;
1402
+ if (existsSync7(join8(projectPath, "src", "controllers")) || existsSync7(join8(projectPath, "controllers"))) score += 1;
1403
+ return { name: "Service Layer", score, maxSignals: max };
1404
+ }
1405
+ function detectMvc(projectPath) {
1406
+ let score = 0;
1407
+ const max = 4;
1408
+ const mvcDirs = ["models", "views", "controllers", "routes", "handlers"];
1409
+ for (const dir of mvcDirs) {
1410
+ if (existsSync7(join8(projectPath, "src", dir)) || existsSync7(join8(projectPath, dir))) {
1411
+ score += 1;
1412
+ }
1413
+ }
1414
+ return { name: "MVC", score: Math.min(score, max), maxSignals: max };
1415
+ }
1416
+ function detectEventDriven(ast) {
1417
+ let score = 0;
1418
+ const max = 4;
1419
+ const eventPackages = ["events", "eventemitter", "amqplib", "kafkajs", "bullmq", "ioredis", "socket.io"];
1420
+ for (const pkg of eventPackages) {
1421
+ if (ast.imports_used.some((i) => i.includes(pkg))) score += 1;
1422
+ }
1423
+ if (ast.advanced_features.includes("async-await")) score += 0.5;
1424
+ return { name: "Event-Driven", score: Math.min(score, max), maxSignals: max };
1425
+ }
1426
+ function detectRepositoryPattern(ast) {
1427
+ let score = 0;
1428
+ const max = 4;
1429
+ if (ast.services > 0) score += 1;
1430
+ if (ast.imports_used.some((i) => ["prisma", "@prisma/client", "typeorm", "sequelize", "drizzle-orm", "mongoose"].includes(i))) {
1431
+ score += 2;
1432
+ }
1433
+ if (ast.imports_used.some((i) => ["@supabase/supabase-js", "firebase"].includes(i))) {
1434
+ score += 1;
1435
+ }
1436
+ return { name: "Repository", score, maxSignals: max };
1437
+ }
1438
+ function detectMicroservices(projectPath) {
1439
+ let score = 0;
1440
+ const max = 4;
1441
+ if (existsSync7(join8(projectPath, "docker-compose.yml")) || existsSync7(join8(projectPath, "docker-compose.yaml"))) {
1442
+ score += 2;
1443
+ }
1444
+ if (existsSync7(join8(projectPath, "Dockerfile"))) score += 1;
1445
+ if (existsSync7(join8(projectPath, "k8s")) || existsSync7(join8(projectPath, "kubernetes"))) score += 1;
1446
+ return { name: "Microservices", score, maxSignals: max };
1447
+ }
1448
+ function detectMonorepo(projectPath) {
1449
+ let score = 0;
1450
+ const max = 4;
1451
+ if (existsSync7(join8(projectPath, "turbo.json"))) score += 2;
1452
+ if (existsSync7(join8(projectPath, "lerna.json"))) score += 2;
1453
+ if (existsSync7(join8(projectPath, "pnpm-workspace.yaml"))) score += 2;
1454
+ if (existsSync7(join8(projectPath, "packages"))) score += 1;
1455
+ if (existsSync7(join8(projectPath, "apps"))) score += 1;
1456
+ return { name: "Monorepo", score: Math.min(score, max), maxSignals: max };
1457
+ }
1458
+ function detectApiFirst(projectPath, fileDiscovery) {
1459
+ let score = 0;
1460
+ const max = 4;
1461
+ const apiFiles = ["openapi.yml", "openapi.yaml", "openapi.json", "swagger.yml", "swagger.yaml", "swagger.json"];
1462
+ for (const file of apiFiles) {
1463
+ if (existsSync7(join8(projectPath, file))) {
1464
+ score += 2;
1465
+ break;
1466
+ }
1467
+ }
1468
+ if (existsSync7(join8(projectPath, "src", "routes")) || existsSync7(join8(projectPath, "routes"))) score += 1;
1469
+ if (existsSync7(join8(projectPath, "src", "api")) || existsSync7(join8(projectPath, "api"))) score += 1;
1470
+ return { name: "API-First", score, maxSignals: max };
1471
+ }
1472
+
1473
+ // src/layers/llm-classification.ts
1474
+ var SYSTEM_PROMPT2 = `You classify software projects based on analysis data.
1475
+ Return JSON only, no markdown fences. Schema:
1476
+ {
1477
+ "domain": "string (e.g., travel-tech, fintech, e-commerce, dev-tools)",
1478
+ "builder_profile": "string (e.g., engineer, PM-who-codes, builder, designer-who-ships)",
1479
+ "role_signals": ["string array of matching roles"],
1480
+ "is_end_to_end": boolean,
1481
+ "description": "1-2 sentence project description"
1482
+ }`;
1483
+ async function classifyWithLlm(fileDiscovery, dependencies, astAnalysis, gitForensics, qualitySignals, architecturePatterns) {
1484
+ if (!isApiKeyConfigured()) return null;
1485
+ const topLanguages = Object.entries(fileDiscovery.languages).sort((a, b) => b[1].loc - a[1].loc).slice(0, 5).map(([lang, stats]) => `${lang} (${stats.loc.toLocaleString()} LOC)`).join(", ");
1486
+ const patternStr = Object.entries(architecturePatterns.patterns).map(([name, conf]) => `${name} (${conf})`).join(", ") || "none detected";
1487
+ const prompt = `Given this project analysis summary:
1488
+ Languages: ${topLanguages}
1489
+ Frameworks: ${dependencies.frameworks.join(", ") || "none"}
1490
+ Structure: ${astAnalysis.functions} functions, ${astAnalysis.classes} classes, ${astAnalysis.components} components, ${astAnalysis.hooks} custom hooks
1491
+ Git: ${gitForensics.commits} commits, ${gitForensics.active_days} active days, ${gitForensics.contributors} contributors
1492
+ Quality: test ratio ${qualitySignals.test_ratio}, complexity ${qualitySignals.complexity_avg}, type safety: ${qualitySignals.type_safety}
1493
+ Patterns: ${patternStr}
1494
+ Imports: ${astAnalysis.imports_used.slice(0, 20).join(", ")}
1495
+
1496
+ Classify this project:
1497
+ 1. Project domain
1498
+ 2. Builder profile
1499
+ 3. Role signals (what roles does this work suggest?)
1500
+ 4. End-to-end ownership? (single person across frontend+backend+infra?)
1501
+ 5. Brief project description (1-2 sentences)`;
1502
+ try {
1503
+ const response = await callHaiku(SYSTEM_PROMPT2, prompt);
1504
+ const cleaned = response.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
1505
+ const parsed = JSON.parse(cleaned);
1506
+ return {
1507
+ domain: parsed.domain || "general",
1508
+ builder_profile: parsed.builder_profile || "engineer",
1509
+ role_signals: parsed.role_signals || [],
1510
+ is_end_to_end: parsed.is_end_to_end || false,
1511
+ description: parsed.description || ""
1512
+ };
1513
+ } catch {
1514
+ return null;
1515
+ }
1516
+ }
1517
+
1518
+ // src/commands/scan.ts
1519
+ async function scanCommand(path) {
1520
+ const projectPath = resolve2(path);
1521
+ const projectName = basename(projectPath);
1522
+ if (!existsSync8(projectPath)) {
1523
+ error(`Directory not found: ${projectPath}`);
1524
+ process.exit(1);
1525
+ }
1526
+ header(`
1527
+ Scanning ${projectName}...
1528
+ `);
1529
+ start("Layer 1: File discovery...");
1530
+ const fileDiscovery = await analyzeFileDiscovery(projectPath);
1531
+ succeed("Layer 1: File discovery");
1532
+ layerOutput(
1533
+ "Layer 1: File discovery",
1534
+ `${fileDiscovery.total_files} files, ${fileDiscovery.total_loc.toLocaleString()} LOC`
1535
+ );
1536
+ start("Layer 2: Dependencies...");
1537
+ const dependencies = await analyzeDependencies(projectPath);
1538
+ succeed("Layer 2: Dependencies");
1539
+ layerOutput(
1540
+ "Layer 2: Dependencies",
1541
+ dependencies.frameworks.length > 0 ? dependencies.frameworks.join(", ") : dependencies.dependencies.slice(0, 5).join(", ") || "none detected"
1542
+ );
1543
+ start("Layer 3: AST analysis...");
1544
+ const astAnalysis = await analyzeAst(projectPath, fileDiscovery.languages);
1545
+ succeed("Layer 3: AST analysis");
1546
+ layerOutput(
1547
+ "Layer 3: AST analysis",
1548
+ `${astAnalysis.functions} functions, ${astAnalysis.classes} classes, ${astAnalysis.components} components, ${astAnalysis.hooks} hooks`
1549
+ );
1550
+ start("Layer 4: Git forensics...");
1551
+ const gitForensics = await analyzeGitForensics(projectPath);
1552
+ succeed("Layer 4: Git forensics");
1553
+ if (gitForensics.is_git_repo) {
1554
+ layerOutput(
1555
+ "Layer 4: Git forensics",
1556
+ `${gitForensics.commits} commits, ${gitForensics.active_days} active days, ${gitForensics.contributors} contributors`
1557
+ );
1558
+ } else {
1559
+ layerOutput("Layer 4: Git forensics", "Not a git repo");
1560
+ }
1561
+ start("Layer 5: Quality signals...");
1562
+ const qualitySignals = await analyzeQualitySignals(projectPath, fileDiscovery);
1563
+ succeed("Layer 5: Quality signals");
1564
+ layerOutput(
1565
+ "Layer 5: Quality signals",
1566
+ `test ratio ${qualitySignals.test_ratio}, complexity ${qualitySignals.complexity_avg}, ${qualitySignals.type_safety ? "strict TS" : "no strict types"}`
1567
+ );
1568
+ start("Layer 6: Architecture patterns...");
1569
+ const architecturePatterns = await analyzeArchitecturePatterns(projectPath, fileDiscovery, astAnalysis);
1570
+ succeed("Layer 6: Architecture patterns");
1571
+ const patternStr = Object.entries(architecturePatterns.patterns).map(([name, conf]) => `${name} (${conf})`).join(", ") || "none detected";
1572
+ layerOutput("Layer 6: Architecture patterns", patternStr);
1573
+ let llmClassification = null;
1574
+ if (isApiKeyConfigured()) {
1575
+ start("Layer 7: LLM classification (API call)...");
1576
+ llmClassification = await classifyWithLlm(
1577
+ fileDiscovery,
1578
+ dependencies,
1579
+ astAnalysis,
1580
+ gitForensics,
1581
+ qualitySignals,
1582
+ architecturePatterns
1583
+ );
1584
+ if (llmClassification) {
1585
+ succeed("Layer 7: LLM classification");
1586
+ layerOutput(
1587
+ "Layer 7: LLM classification",
1588
+ `${llmClassification.domain}, ${llmClassification.builder_profile}`
1589
+ );
1590
+ } else {
1591
+ fail("Layer 7: LLM classification failed");
1592
+ }
1593
+ } else {
1594
+ dim(" Layer 7: Skipped (no API key \u2014 run inside Claude Code for full analysis)");
1595
+ }
1596
+ const scanResult = {
1597
+ project_name: projectName,
1598
+ project_path: projectPath,
1599
+ file_discovery: fileDiscovery,
1600
+ dependencies,
1601
+ ast_analysis: astAnalysis,
1602
+ git_forensics: gitForensics,
1603
+ quality_signals: qualitySignals,
1604
+ architecture_patterns: architecturePatterns,
1605
+ llm_classification: llmClassification
1606
+ };
1607
+ start("Updating skill graph...");
1608
+ let graph = await loadGraph();
1609
+ if (!graph) {
1610
+ graph = createEmptySkillGraph({
1611
+ name: "",
1612
+ email: "",
1613
+ primary_role: "engineer",
1614
+ target_roles: [],
1615
+ previous_companies: [],
1616
+ education: [],
1617
+ links: {},
1618
+ source: "manual"
1619
+ });
1620
+ }
1621
+ const projectEntry = buildProjectEntry(scanResult);
1622
+ graph = mergeIntoGraph(graph, projectEntry, llmClassification);
1623
+ await saveGraph(graph);
1624
+ succeed("Skill graph updated");
1625
+ success(`
1626
+ Scan complete. ${graph.projects.length} project(s) in skill graph.`);
1627
+ info("Run `hiregraph status` to see your full profile.");
1628
+ }
1629
+
1630
+ // src/commands/status.ts
1631
+ import chalk2 from "chalk";
1632
+ async function statusCommand() {
1633
+ const graph = await loadGraph();
1634
+ if (!graph || graph.projects.length === 0) {
1635
+ warn("No skill graph found.");
1636
+ info("Run `hiregraph init` to set up your profile.");
1637
+ info("Run `hiregraph scan <path>` to analyze a project.");
1638
+ return;
1639
+ }
1640
+ const identity = graph.builder_identity;
1641
+ header("\n HireGraph Status\n");
1642
+ if (identity.name) {
1643
+ console.log(` ${chalk2.bold("Builder:")} ${identity.name} (${identity.primary_role})`);
1644
+ }
1645
+ console.log(` ${chalk2.bold("Projects scanned:")} ${graph.projects.length}`);
1646
+ console.log();
1647
+ const skills = Object.entries(graph.tech_stack).sort((a, b) => b[1].proficiency - a[1].proficiency).slice(0, 10);
1648
+ if (skills.length > 0) {
1649
+ console.log(` ${chalk2.bold("Tech Stack:")}`);
1650
+ const maxNameLen = Math.max(...skills.map(([name]) => name.length));
1651
+ for (const [name, skill] of skills) {
1652
+ const bar = renderBar(skill.proficiency, 20);
1653
+ const locStr = skill.loc > 0 ? `${skill.loc.toLocaleString()} LOC` : "";
1654
+ const projStr = skill.projects > 0 ? `${skill.projects} projects` : "";
1655
+ const details = [locStr, projStr].filter(Boolean).join(", ");
1656
+ console.log(` ${name.padEnd(maxNameLen + 2)} ${bar} ${skill.proficiency.toFixed(2)}${details ? ` (${details})` : ""}`);
1657
+ }
1658
+ console.log();
1659
+ }
1660
+ const patterns = Object.entries(graph.architecture).sort((a, b) => b[1].confidence - a[1].confidence);
1661
+ if (patterns.length > 0) {
1662
+ const patternStr = patterns.map(([name, { confidence }]) => `${name} (${confidence})`).join(", ");
1663
+ console.log(` ${chalk2.bold("Architecture:")} ${patternStr}`);
1664
+ }
1665
+ console.log(` ${chalk2.bold("Quality:")} test ratio ${graph.quality.test_ratio}, complexity ${graph.quality.complexity_avg}${graph.quality.type_safety ? ", strict types" : ""}`);
1666
+ if (graph.builder_profile.role_signals.length > 0) {
1667
+ console.log(` ${chalk2.bold("Role Signals:")} ${graph.builder_profile.role_signals.join(", ")}`);
1668
+ }
1669
+ if (graph.builder_profile.is_end_to_end) {
1670
+ console.log(` ${chalk2.bold("Builder Profile:")} End-to-end ownership detected`);
1671
+ }
1672
+ console.log();
1673
+ console.log(` ${chalk2.bold("Projects:")}`);
1674
+ for (const project of graph.projects) {
1675
+ const age = getTimeAgo(project.scanned_at);
1676
+ console.log(` ${chalk2.cyan(project.name)} \u2014 ${project.domain || "unknown domain"} (scanned ${age})`);
1677
+ const stackStr = project.stack.slice(0, 5).join(", ");
1678
+ if (stackStr) console.log(` ${chalk2.dim(stackStr)}`);
1679
+ }
1680
+ console.log();
1681
+ dim(` Last updated: ${getTimeAgo(graph.last_updated)}`);
1682
+ console.log();
1683
+ }
1684
+ function renderBar(value, width) {
1685
+ const filled = Math.round(value * width);
1686
+ const empty = width - filled;
1687
+ return chalk2.green("\u2588".repeat(filled)) + chalk2.dim("\u2591".repeat(empty));
1688
+ }
1689
+ function getTimeAgo(isoDate) {
1690
+ const diff = Date.now() - new Date(isoDate).getTime();
1691
+ const minutes = Math.floor(diff / 6e4);
1692
+ if (minutes < 1) return "just now";
1693
+ if (minutes < 60) return `${minutes}m ago`;
1694
+ const hours = Math.floor(minutes / 60);
1695
+ if (hours < 24) return `${hours}h ago`;
1696
+ const days = Math.floor(hours / 24);
1697
+ return `${days}d ago`;
1698
+ }
1699
+
1700
+ // src/commands/jobs.ts
1701
+ import chalk3 from "chalk";
1702
+
1703
+ // src/ats/registry.ts
1704
+ import { readFile as readFile8 } from "fs/promises";
1705
+ import { join as join9, dirname } from "path";
1706
+ import { fileURLToPath } from "url";
1707
+ import { existsSync as existsSync9 } from "fs";
1708
+ var __dirname = dirname(fileURLToPath(import.meta.url));
1709
+ async function loadRegistry() {
1710
+ const candidates = [
1711
+ join9(__dirname, "..", "data", "companies.json"),
1712
+ join9(__dirname, "..", "..", "src", "data", "companies.json"),
1713
+ join9(__dirname, "data", "companies.json")
1714
+ ];
1715
+ let companies = [];
1716
+ for (const seedPath of candidates) {
1717
+ if (existsSync9(seedPath)) {
1718
+ const raw = await readFile8(seedPath, "utf-8");
1719
+ companies = JSON.parse(raw);
1720
+ break;
1721
+ }
1722
+ }
1723
+ const userCompanies = await loadJson("companies.json");
1724
+ if (userCompanies) {
1725
+ const bySlug = new Map(companies.map((c) => [c.slug, c]));
1726
+ for (const uc of userCompanies) {
1727
+ bySlug.set(uc.slug, uc);
1728
+ }
1729
+ companies = [...bySlug.values()];
1730
+ }
1731
+ return companies;
1732
+ }
1733
+ function filterByAts(companies, ats) {
1734
+ return companies.filter((c) => c.ats === ats);
1735
+ }
1736
+ function excludeCompanies(companies, slugs) {
1737
+ const excluded = new Set(slugs);
1738
+ return companies.filter((c) => !excluded.has(c.slug));
1739
+ }
1740
+
1741
+ // src/ats/greenhouse.ts
1742
+ async function fetchGreenhouseJobs(boardToken) {
1743
+ const url = `https://boards-api.greenhouse.io/v1/boards/${boardToken}/jobs?content=true`;
1744
+ const response = await fetch(url, {
1745
+ headers: { "Accept": "application/json" },
1746
+ signal: AbortSignal.timeout(15e3)
1747
+ });
1748
+ if (!response.ok) {
1749
+ if (response.status === 429) throw new Error("Rate limited");
1750
+ throw new Error(`HTTP ${response.status}`);
1751
+ }
1752
+ const data = await response.json();
1753
+ return data.jobs || [];
1754
+ }
1755
+
1756
+ // src/ats/lever.ts
1757
+ async function fetchLeverPostings(company) {
1758
+ const url = `https://api.lever.co/v0/postings/${company}`;
1759
+ const response = await fetch(url, {
1760
+ headers: { "Accept": "application/json" },
1761
+ signal: AbortSignal.timeout(15e3)
1762
+ });
1763
+ if (!response.ok) {
1764
+ if (response.status === 429) throw new Error("Rate limited");
1765
+ throw new Error(`HTTP ${response.status}`);
1766
+ }
1767
+ const data = await response.json();
1768
+ return Array.isArray(data) ? data : [];
1769
+ }
1770
+
1771
+ // src/ats/ashby.ts
1772
+ async function fetchAshbyJobs(boardId) {
1773
+ const url = `https://api.ashbyhq.com/posting-api/job-board/${boardId}`;
1774
+ const response = await fetch(url, {
1775
+ headers: { "Accept": "application/json" },
1776
+ signal: AbortSignal.timeout(15e3)
1777
+ });
1778
+ if (!response.ok) {
1779
+ if (response.status === 429) throw new Error("Rate limited");
1780
+ throw new Error(`HTTP ${response.status}`);
1781
+ }
1782
+ const data = await response.json();
1783
+ return data.jobs || [];
1784
+ }
1785
+
1786
+ // src/ats/normalizer.ts
1787
+ function stripHtml(html) {
1788
+ return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<\/li>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ").replace(/\n{3,}/g, "\n\n").trim();
1789
+ }
1790
+ function normalizeGreenhouseJob(raw, company) {
1791
+ return {
1792
+ id: `gh_${raw.id}`,
1793
+ source: "greenhouse",
1794
+ company: company.name,
1795
+ company_slug: company.slug,
1796
+ title: raw.title,
1797
+ url: raw.absolute_url,
1798
+ location: raw.location?.name || "Unknown",
1799
+ department: raw.departments?.[0]?.name,
1800
+ description_raw: stripHtml(raw.content || ""),
1801
+ updated_at: raw.updated_at,
1802
+ fetched_at: (/* @__PURE__ */ new Date()).toISOString()
1803
+ };
1804
+ }
1805
+ function normalizeLeverPosting(raw, company) {
1806
+ const descParts = [raw.descriptionPlain || stripHtml(raw.description || "")];
1807
+ if (raw.lists) {
1808
+ for (const list of raw.lists) {
1809
+ descParts.push(`${list.text}
1810
+ ${stripHtml(list.content)}`);
1811
+ }
1812
+ }
1813
+ return {
1814
+ id: `lv_${raw.id}`,
1815
+ source: "lever",
1816
+ company: company.name,
1817
+ company_slug: company.slug,
1818
+ title: raw.text,
1819
+ url: raw.hostedUrl,
1820
+ location: raw.categories?.location || "Unknown",
1821
+ department: raw.categories?.department || raw.categories?.team,
1822
+ description_raw: descParts.join("\n\n"),
1823
+ posted_at: raw.createdAt ? new Date(raw.createdAt).toISOString() : void 0,
1824
+ fetched_at: (/* @__PURE__ */ new Date()).toISOString()
1825
+ };
1826
+ }
1827
+ function normalizeAshbyJob(raw, company) {
1828
+ return {
1829
+ id: `ab_${raw.id}`,
1830
+ source: "ashby",
1831
+ company: company.name,
1832
+ company_slug: company.slug,
1833
+ title: raw.title,
1834
+ url: raw.jobUrl || `https://jobs.ashbyhq.com/${company.board_token}/${raw.id}`,
1835
+ location: raw.location || "Unknown",
1836
+ department: raw.department,
1837
+ description_raw: stripHtml(raw.descriptionHtml || ""),
1838
+ posted_at: raw.publishedAt,
1839
+ fetched_at: (/* @__PURE__ */ new Date()).toISOString()
1840
+ };
1841
+ }
1842
+
1843
+ // src/ats/fetcher.ts
1844
+ var CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
1845
+ var BATCH_SIZE = 5;
1846
+ var BATCH_DELAY_MS = 500;
1847
+ function isCacheStale(fetchedAt) {
1848
+ return Date.now() - new Date(fetchedAt).getTime() > CACHE_MAX_AGE_MS;
1849
+ }
1850
+ async function delay(ms) {
1851
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1852
+ }
1853
+ async function fetchInBatches(items, fn, getName, errors) {
1854
+ const allJobs = [];
1855
+ for (let i = 0; i < items.length; i += BATCH_SIZE) {
1856
+ const batch = items.slice(i, i + BATCH_SIZE);
1857
+ const results = await Promise.allSettled(batch.map((item) => fn(item)));
1858
+ for (let j = 0; j < results.length; j++) {
1859
+ const result = results[j];
1860
+ if (result.status === "fulfilled") {
1861
+ allJobs.push(...result.value);
1862
+ } else {
1863
+ errors.push({ company: getName(batch[j]), error: result.reason?.message || "Unknown error" });
1864
+ }
1865
+ }
1866
+ if (i + BATCH_SIZE < items.length) {
1867
+ await delay(BATCH_DELAY_MS);
1868
+ }
1869
+ }
1870
+ return allJobs;
1871
+ }
1872
+ async function fetchAllJobs(options) {
1873
+ const config = await loadJson("config.json");
1874
+ let companies = await loadRegistry();
1875
+ companies = excludeCompanies(companies, config?.excluded_companies || []);
1876
+ if (options?.ats) {
1877
+ companies = filterByAts(companies, options.ats);
1878
+ }
1879
+ const errors = [];
1880
+ const allJobs = [];
1881
+ const stats = { greenhouse: 0, lever: 0, ashby: 0, total: 0, failed: 0 };
1882
+ const ghCompanies = companies.filter((c) => c.ats === "greenhouse");
1883
+ if (ghCompanies.length > 0) {
1884
+ const cached = await loadSubJson("jobs", "greenhouse.json");
1885
+ if (cached && !isCacheStale(cached.fetched_at) && !options?.refresh) {
1886
+ allJobs.push(...cached.jobs);
1887
+ stats.greenhouse = cached.total_jobs;
1888
+ } else {
1889
+ start(`Fetching from Greenhouse (${ghCompanies.length} companies)...`);
1890
+ const ghJobs = await fetchInBatches(
1891
+ ghCompanies,
1892
+ async (c) => {
1893
+ const raw = await fetchGreenhouseJobs(c.board_token);
1894
+ return raw.map((j) => normalizeGreenhouseJob(j, c));
1895
+ },
1896
+ (c) => c.name,
1897
+ errors
1898
+ );
1899
+ allJobs.push(...ghJobs);
1900
+ stats.greenhouse = ghJobs.length;
1901
+ succeed(`Greenhouse: ${ghJobs.length} jobs from ${ghCompanies.length} companies`);
1902
+ await saveSubJson("jobs", "greenhouse.json", {
1903
+ source: "greenhouse",
1904
+ fetched_at: (/* @__PURE__ */ new Date()).toISOString(),
1905
+ companies_fetched: ghCompanies.length,
1906
+ total_jobs: ghJobs.length,
1907
+ jobs: ghJobs
1908
+ });
1909
+ }
1910
+ }
1911
+ const lvCompanies = companies.filter((c) => c.ats === "lever");
1912
+ if (lvCompanies.length > 0) {
1913
+ const cached = await loadSubJson("jobs", "lever.json");
1914
+ if (cached && !isCacheStale(cached.fetched_at) && !options?.refresh) {
1915
+ allJobs.push(...cached.jobs);
1916
+ stats.lever = cached.total_jobs;
1917
+ } else {
1918
+ start(`Fetching from Lever (${lvCompanies.length} companies)...`);
1919
+ const lvJobs = await fetchInBatches(
1920
+ lvCompanies,
1921
+ async (c) => {
1922
+ const raw = await fetchLeverPostings(c.board_token);
1923
+ return raw.map((j) => normalizeLeverPosting(j, c));
1924
+ },
1925
+ (c) => c.name,
1926
+ errors
1927
+ );
1928
+ allJobs.push(...lvJobs);
1929
+ stats.lever = lvJobs.length;
1930
+ succeed(`Lever: ${lvJobs.length} jobs from ${lvCompanies.length} companies`);
1931
+ await saveSubJson("jobs", "lever.json", {
1932
+ source: "lever",
1933
+ fetched_at: (/* @__PURE__ */ new Date()).toISOString(),
1934
+ companies_fetched: lvCompanies.length,
1935
+ total_jobs: lvJobs.length,
1936
+ jobs: lvJobs
1937
+ });
1938
+ }
1939
+ }
1940
+ const abCompanies = companies.filter((c) => c.ats === "ashby");
1941
+ if (abCompanies.length > 0) {
1942
+ const cached = await loadSubJson("jobs", "ashby.json");
1943
+ if (cached && !isCacheStale(cached.fetched_at) && !options?.refresh) {
1944
+ allJobs.push(...cached.jobs);
1945
+ stats.ashby = cached.total_jobs;
1946
+ } else {
1947
+ start(`Fetching from Ashby (${abCompanies.length} companies)...`);
1948
+ const abJobs = await fetchInBatches(
1949
+ abCompanies,
1950
+ async (c) => {
1951
+ const raw = await fetchAshbyJobs(c.board_token);
1952
+ return raw.map((j) => normalizeAshbyJob(j, c));
1953
+ },
1954
+ (c) => c.name,
1955
+ errors
1956
+ );
1957
+ allJobs.push(...abJobs);
1958
+ stats.ashby = abJobs.length;
1959
+ succeed(`Ashby: ${abJobs.length} jobs from ${abCompanies.length} companies`);
1960
+ await saveSubJson("jobs", "ashby.json", {
1961
+ source: "ashby",
1962
+ fetched_at: (/* @__PURE__ */ new Date()).toISOString(),
1963
+ companies_fetched: abCompanies.length,
1964
+ total_jobs: abJobs.length,
1965
+ jobs: abJobs
1966
+ });
1967
+ }
1968
+ }
1969
+ stats.total = allJobs.length;
1970
+ stats.failed = errors.length;
1971
+ return { jobs: allJobs, errors, stats };
1972
+ }
1973
+
1974
+ // src/commands/jobs.ts
1975
+ async function jobsCommand(options) {
1976
+ header("\n HireGraph Jobs\n");
1977
+ const companies = await loadRegistry();
1978
+ const ghCount = companies.filter((c) => c.ats === "greenhouse").length;
1979
+ const lvCount = companies.filter((c) => c.ats === "lever").length;
1980
+ const abCount = companies.filter((c) => c.ats === "ashby").length;
1981
+ info(` Registry: ${companies.length} companies (${ghCount} Greenhouse, ${lvCount} Lever, ${abCount} Ashby)
1982
+ `);
1983
+ const result = await fetchAllJobs({ refresh: options.refresh, ats: options.ats });
1984
+ console.log();
1985
+ console.log(` ${chalk3.bold("Jobs Fetched:")}`);
1986
+ if (result.stats.greenhouse > 0) {
1987
+ console.log(` Greenhouse ${result.stats.greenhouse.toLocaleString().padStart(6)} jobs`);
1988
+ }
1989
+ if (result.stats.lever > 0) {
1990
+ console.log(` Lever ${result.stats.lever.toLocaleString().padStart(6)} jobs`);
1991
+ }
1992
+ if (result.stats.ashby > 0) {
1993
+ console.log(` Ashby ${result.stats.ashby.toLocaleString().padStart(6)} jobs`);
1994
+ }
1995
+ console.log(` ${chalk3.bold("Total")} ${chalk3.bold(result.stats.total.toLocaleString().padStart(6))} jobs`);
1996
+ if (result.stats.failed > 0) {
1997
+ console.log();
1998
+ warn(` ${result.stats.failed} companies unreachable:`);
1999
+ for (const err of result.errors.slice(0, 10)) {
2000
+ dim(` ${err.company}: ${err.error}`);
2001
+ }
2002
+ if (result.errors.length > 10) {
2003
+ dim(` ... and ${result.errors.length - 10} more`);
2004
+ }
2005
+ }
2006
+ if (options.limit && options.limit > 0) {
2007
+ console.log();
2008
+ console.log(` ${chalk3.bold("Sample jobs:")}`);
2009
+ const sample = result.jobs.slice(0, options.limit);
2010
+ for (const job of sample) {
2011
+ console.log(` ${chalk3.cyan(job.title)} @ ${job.company} (${job.location})`);
2012
+ }
2013
+ }
2014
+ console.log();
2015
+ info(" Cached to ~/.hiregraph/jobs/");
2016
+ info(" Run `hiregraph matches` to find your best matches.");
2017
+ console.log();
2018
+ }
2019
+
2020
+ // src/commands/matches.ts
2021
+ import chalk4 from "chalk";
2022
+
2023
+ // src/matching/job-parser.ts
2024
+ var SYSTEM_PROMPT3 = `You extract structured job requirements from job descriptions.
2025
+ Return JSON only, no markdown fences. Schema:
2026
+ {
2027
+ "must_have_skills": ["string array"],
2028
+ "nice_to_have_skills": ["string array"],
2029
+ "seniority_level": "junior|mid|senior|staff|principal|lead|manager",
2030
+ "tech_stack": ["specific technologies mentioned"],
2031
+ "domain": "string (e.g., fintech, dev-tools, e-commerce)",
2032
+ "remote_policy": "remote|hybrid|onsite|unknown",
2033
+ "compensation_range": { "min": number|null, "max": number|null, "currency": "USD" } | null
2034
+ }`;
2035
+ var BATCH_SIZE2 = 10;
2036
+ var BATCH_DELAY_MS2 = 1e3;
2037
+ async function parseJobsBatch(jobs) {
2038
+ if (!isApiKeyConfigured()) {
2039
+ throw new Error("No API key detected. Run hiregraph inside Claude Code or Cursor.");
2040
+ }
2041
+ const cached = await loadSubJson("jobs", "parsed.json");
2042
+ const requirements = cached?.requirements || {};
2043
+ const unparsed = jobs.filter((j) => !requirements[j.id]);
2044
+ if (unparsed.length === 0) return requirements;
2045
+ start(`Parsing ${unparsed.length} new job descriptions...`);
2046
+ let parsed = 0;
2047
+ for (let i = 0; i < unparsed.length; i += BATCH_SIZE2) {
2048
+ const batch = unparsed.slice(i, i + BATCH_SIZE2);
2049
+ const results = await Promise.allSettled(
2050
+ batch.map((job) => parseOneJob(job))
2051
+ );
2052
+ for (let j = 0; j < results.length; j++) {
2053
+ if (results[j].status === "fulfilled") {
2054
+ const req = results[j].value;
2055
+ requirements[batch[j].id] = req;
2056
+ parsed++;
2057
+ }
2058
+ }
2059
+ if (i + BATCH_SIZE2 < unparsed.length) {
2060
+ await new Promise((r) => setTimeout(r, BATCH_DELAY_MS2));
2061
+ }
2062
+ }
2063
+ succeed(`Parsed ${parsed} job descriptions (${Object.keys(requirements).length} total cached)`);
2064
+ await saveSubJson("jobs", "parsed.json", {
2065
+ parsed_at: (/* @__PURE__ */ new Date()).toISOString(),
2066
+ requirements
2067
+ });
2068
+ return requirements;
2069
+ }
2070
+ async function parseOneJob(job) {
2071
+ const description = job.description_raw.slice(0, 4e3);
2072
+ const result = await callHaikuJson(
2073
+ SYSTEM_PROMPT3,
2074
+ `Job: ${job.title} at ${job.company}
2075
+ Location: ${job.location}
2076
+
2077
+ ${description}`
2078
+ );
2079
+ return {
2080
+ job_id: job.id,
2081
+ must_have_skills: result.must_have_skills || [],
2082
+ nice_to_have_skills: result.nice_to_have_skills || [],
2083
+ seniority_level: result.seniority_level || "mid",
2084
+ tech_stack: result.tech_stack || [],
2085
+ domain: result.domain || "unknown",
2086
+ remote_policy: result.remote_policy || "unknown",
2087
+ compensation_range: result.compensation_range || void 0,
2088
+ parsed_at: (/* @__PURE__ */ new Date()).toISOString()
2089
+ };
2090
+ }
2091
+
2092
+ // src/matching/vectorizer.ts
2093
+ var STOP_WORDS = /* @__PURE__ */ new Set([
2094
+ "the",
2095
+ "a",
2096
+ "an",
2097
+ "and",
2098
+ "or",
2099
+ "but",
2100
+ "in",
2101
+ "on",
2102
+ "at",
2103
+ "to",
2104
+ "for",
2105
+ "of",
2106
+ "with",
2107
+ "by",
2108
+ "from",
2109
+ "is",
2110
+ "are",
2111
+ "was",
2112
+ "were",
2113
+ "be",
2114
+ "been",
2115
+ "being",
2116
+ "have",
2117
+ "has",
2118
+ "had",
2119
+ "do",
2120
+ "does",
2121
+ "did",
2122
+ "will",
2123
+ "would",
2124
+ "could",
2125
+ "should",
2126
+ "may",
2127
+ "might",
2128
+ "can",
2129
+ "shall",
2130
+ "not",
2131
+ "no",
2132
+ "nor",
2133
+ "so",
2134
+ "if",
2135
+ "then",
2136
+ "than",
2137
+ "that",
2138
+ "this",
2139
+ "these",
2140
+ "those",
2141
+ "it",
2142
+ "its",
2143
+ "we",
2144
+ "you",
2145
+ "he",
2146
+ "she",
2147
+ "they",
2148
+ "our",
2149
+ "your",
2150
+ "their",
2151
+ "who",
2152
+ "which",
2153
+ "what",
2154
+ "where",
2155
+ "when",
2156
+ "how",
2157
+ "all",
2158
+ "each",
2159
+ "every",
2160
+ "both",
2161
+ "few",
2162
+ "more",
2163
+ "most",
2164
+ "other",
2165
+ "some",
2166
+ "such",
2167
+ "only",
2168
+ "very",
2169
+ "also",
2170
+ "just",
2171
+ "about",
2172
+ "up",
2173
+ "out",
2174
+ "as",
2175
+ "into",
2176
+ "through",
2177
+ "over",
2178
+ "after",
2179
+ "before",
2180
+ "between",
2181
+ "under",
2182
+ "above",
2183
+ "while",
2184
+ "during",
2185
+ "experience",
2186
+ "work",
2187
+ "working",
2188
+ "ability",
2189
+ "strong",
2190
+ "team",
2191
+ "role",
2192
+ "position",
2193
+ "company",
2194
+ "using",
2195
+ "used",
2196
+ "use",
2197
+ "years",
2198
+ "year"
2199
+ ]);
2200
+ function tokenize(text) {
2201
+ return text.toLowerCase().replace(/[^a-z0-9.#+\-_/]/g, " ").split(/\s+/).filter((t) => t.length > 1 && !STOP_WORDS.has(t));
2202
+ }
2203
+ function buildVocabulary(documents) {
2204
+ const docFreq = /* @__PURE__ */ new Map();
2205
+ const allTerms = /* @__PURE__ */ new Set();
2206
+ for (const doc of documents) {
2207
+ const seen = /* @__PURE__ */ new Set();
2208
+ for (const term of doc) {
2209
+ allTerms.add(term);
2210
+ if (!seen.has(term)) {
2211
+ seen.add(term);
2212
+ docFreq.set(term, (docFreq.get(term) || 0) + 1);
2213
+ }
2214
+ }
2215
+ }
2216
+ const sorted = [...allTerms].filter((t) => (docFreq.get(t) || 0) >= 2).sort((a, b) => (docFreq.get(b) || 0) - (docFreq.get(a) || 0)).slice(0, 2e3);
2217
+ const termToIndex = /* @__PURE__ */ new Map();
2218
+ const idf = new Float32Array(sorted.length);
2219
+ const N = documents.length;
2220
+ for (let i = 0; i < sorted.length; i++) {
2221
+ termToIndex.set(sorted[i], i);
2222
+ const df = docFreq.get(sorted[i]) || 1;
2223
+ idf[i] = Math.log(N / df) + 1;
2224
+ }
2225
+ return { termToIndex, idf, size: sorted.length };
2226
+ }
2227
+ function vectorize(tokens, vocab) {
2228
+ const tf = /* @__PURE__ */ new Map();
2229
+ for (const token of tokens) {
2230
+ const idx = vocab.termToIndex.get(token);
2231
+ if (idx !== void 0) {
2232
+ tf.set(idx, (tf.get(idx) || 0) + 1);
2233
+ }
2234
+ }
2235
+ const terms = /* @__PURE__ */ new Map();
2236
+ let magnitude = 0;
2237
+ for (const [idx, count] of tf) {
2238
+ const weight = count * vocab.idf[idx];
2239
+ terms.set(idx, weight);
2240
+ magnitude += weight * weight;
2241
+ }
2242
+ magnitude = Math.sqrt(magnitude);
2243
+ return { terms, magnitude };
2244
+ }
2245
+ function buildSkillVector(graph, vocab) {
2246
+ const tokens = [];
2247
+ for (const [skill, data] of Object.entries(graph.tech_stack)) {
2248
+ const name = skill.toLowerCase();
2249
+ const repeat = data.proficiency > 0.5 ? 3 : data.proficiency > 0.3 ? 2 : 1;
2250
+ for (let i = 0; i < repeat; i++) {
2251
+ tokens.push(...tokenize(name));
2252
+ }
2253
+ }
2254
+ for (const [pattern] of Object.entries(graph.architecture)) {
2255
+ tokens.push(...tokenize(pattern));
2256
+ }
2257
+ for (const signal of graph.builder_profile.role_signals) {
2258
+ tokens.push(...tokenize(signal));
2259
+ }
2260
+ for (const proj of graph.projects) {
2261
+ if (proj.domain) tokens.push(...tokenize(proj.domain));
2262
+ for (const s of proj.stack) tokens.push(...tokenize(s));
2263
+ }
2264
+ return vectorize(tokens, vocab);
2265
+ }
2266
+ function buildJobVector(req, vocab) {
2267
+ const tokens = [];
2268
+ for (const skill of req.must_have_skills) {
2269
+ const t = tokenize(skill);
2270
+ tokens.push(...t, ...t);
2271
+ }
2272
+ for (const skill of req.nice_to_have_skills) {
2273
+ tokens.push(...tokenize(skill));
2274
+ }
2275
+ for (const tech of req.tech_stack) {
2276
+ tokens.push(...tokenize(tech));
2277
+ }
2278
+ if (req.domain) tokens.push(...tokenize(req.domain));
2279
+ return vectorize(tokens, vocab);
2280
+ }
2281
+ function cosineSimilarity(a, b) {
2282
+ if (a.magnitude === 0 || b.magnitude === 0) return 0;
2283
+ let dotProduct = 0;
2284
+ const [smaller, larger] = a.terms.size <= b.terms.size ? [a, b] : [b, a];
2285
+ for (const [idx, weight] of smaller.terms) {
2286
+ const otherWeight = larger.terms.get(idx);
2287
+ if (otherWeight !== void 0) {
2288
+ dotProduct += weight * otherWeight;
2289
+ }
2290
+ }
2291
+ return dotProduct / (a.magnitude * b.magnitude);
2292
+ }
2293
+ function buildAllDocuments(graph, requirements) {
2294
+ const docs = [];
2295
+ const skillTokens = [];
2296
+ for (const skill of Object.keys(graph.tech_stack)) {
2297
+ skillTokens.push(...tokenize(skill));
2298
+ }
2299
+ for (const proj of graph.projects) {
2300
+ for (const s of proj.stack) skillTokens.push(...tokenize(s));
2301
+ if (proj.domain) skillTokens.push(...tokenize(proj.domain));
2302
+ }
2303
+ docs.push(skillTokens);
2304
+ for (const req of Object.values(requirements)) {
2305
+ const tokens = [];
2306
+ for (const s of req.must_have_skills) tokens.push(...tokenize(s));
2307
+ for (const s of req.nice_to_have_skills) tokens.push(...tokenize(s));
2308
+ for (const s of req.tech_stack) tokens.push(...tokenize(s));
2309
+ if (req.domain) tokens.push(...tokenize(req.domain));
2310
+ docs.push(tokens);
2311
+ }
2312
+ return docs;
2313
+ }
2314
+
2315
+ // src/matching/similarity.ts
2316
+ function findTopCandidates(skillVector, jobVectors, k) {
2317
+ const scored = [];
2318
+ for (const [jobId, jobVector] of jobVectors) {
2319
+ const sim = cosineSimilarity(skillVector, jobVector);
2320
+ if (sim > 0) {
2321
+ scored.push({ jobId, similarity: sim });
2322
+ }
2323
+ }
2324
+ scored.sort((a, b) => b.similarity - a.similarity);
2325
+ return scored.slice(0, k);
2326
+ }
2327
+
2328
+ // src/matching/filters.ts
2329
+ function applyHardFilters(jobs, config) {
2330
+ const excluded = new Set(config.excluded_companies.map((s) => s.toLowerCase()));
2331
+ return jobs.filter(({ job, requirements }) => {
2332
+ if (excluded.has(job.company_slug.toLowerCase())) return false;
2333
+ if (config.remote_preference === "Remote" && requirements.remote_policy === "onsite") {
2334
+ return false;
2335
+ }
2336
+ if (config.min_compensation && requirements.compensation_range?.max) {
2337
+ const min = parseCompensation(config.min_compensation);
2338
+ if (min > 0 && requirements.compensation_range.max < min) {
2339
+ return false;
2340
+ }
2341
+ }
2342
+ return true;
2343
+ });
2344
+ }
2345
+ function parseCompensation(value) {
2346
+ const cleaned = value.replace(/[$,]/g, "").trim();
2347
+ if (cleaned.toLowerCase().endsWith("k")) {
2348
+ return parseFloat(cleaned) * 1e3;
2349
+ }
2350
+ return parseFloat(cleaned) || 0;
2351
+ }
2352
+
2353
+ // src/matching/evaluator.ts
2354
+ var SYSTEM_PROMPT4 = `You evaluate job-candidate match quality. Given a candidate's verified skill profile and a job's requirements, score the match.
2355
+
2356
+ Rules:
2357
+ - code-verified skills are confirmed facts. Weight heavily.
2358
+ - self-reported skills are unverified claims. Weight lower.
2359
+ - Proficiency scores: 0.8+ is strong, 0.5+ is moderate, below 0.3 is beginner.
2360
+ - For 'builder' profiles, value end-to-end ownership over single-area depth.
2361
+ - Be strict. 7/10 = genuinely strong match. 8+ = exceptional fit.
2362
+
2363
+ Return JSON only, no markdown fences. Schema:
2364
+ {
2365
+ "score": number (1-10),
2366
+ "confidence": number (0.0-1.0),
2367
+ "reasoning": "2-3 sentences explaining the match",
2368
+ "strengths": ["key matching strengths"],
2369
+ "gaps": ["notable gaps or missing skills"]
2370
+ }`;
2371
+ var BATCH_SIZE3 = 5;
2372
+ var BATCH_DELAY_MS3 = 1500;
2373
+ async function evaluateMatchesBatch(candidates, graph) {
2374
+ if (!isApiKeyConfigured()) {
2375
+ throw new Error("No API key detected. Run hiregraph inside Claude Code or Cursor.");
2376
+ }
2377
+ const skillSummary = buildSkillSummary(graph);
2378
+ const results = [];
2379
+ start(`Evaluating ${candidates.length} matches with LLM...`);
2380
+ for (let i = 0; i < candidates.length; i += BATCH_SIZE3) {
2381
+ const batch = candidates.slice(i, i + BATCH_SIZE3);
2382
+ const batchResults = await Promise.allSettled(
2383
+ batch.map(({ job, requirements }) => evaluateOne(skillSummary, job, requirements))
2384
+ );
2385
+ for (let j = 0; j < batchResults.length; j++) {
2386
+ if (batchResults[j].status === "fulfilled") {
2387
+ results.push(batchResults[j].value);
2388
+ }
2389
+ }
2390
+ if (i + BATCH_SIZE3 < candidates.length) {
2391
+ await new Promise((r) => setTimeout(r, BATCH_DELAY_MS3));
2392
+ }
2393
+ }
2394
+ succeed(`Evaluated ${results.length} matches`);
2395
+ return results;
2396
+ }
2397
+ async function evaluateOne(skillSummary, job, requirements) {
2398
+ const jobSummary = buildJobSummary(job, requirements);
2399
+ const prompt = `CANDIDATE PROFILE:
2400
+ ${skillSummary}
2401
+
2402
+ JOB:
2403
+ ${jobSummary}`;
2404
+ const result = await callHaikuJson(SYSTEM_PROMPT4, prompt, 2048);
2405
+ const score = Math.min(10, Math.max(1, result.score || 1));
2406
+ const tier = score >= 8 ? "strong" : score >= 6 ? "suggested" : "filtered";
2407
+ return {
2408
+ job_id: job.id,
2409
+ job_title: job.title,
2410
+ company: job.company,
2411
+ company_slug: job.company_slug,
2412
+ url: job.url,
2413
+ score,
2414
+ confidence: result.confidence || 0.5,
2415
+ tier,
2416
+ reasoning: result.reasoning || "",
2417
+ strengths: result.strengths || [],
2418
+ gaps: result.gaps || [],
2419
+ matched_at: (/* @__PURE__ */ new Date()).toISOString()
2420
+ };
2421
+ }
2422
+ function buildSkillSummary(graph) {
2423
+ const lines = [];
2424
+ if (graph.builder_identity.name) {
2425
+ lines.push(`Name: ${graph.builder_identity.name} (${graph.builder_identity.primary_role})`);
2426
+ }
2427
+ const skills = Object.entries(graph.tech_stack).sort((a, b) => b[1].proficiency - a[1].proficiency).slice(0, 15);
2428
+ if (skills.length > 0) {
2429
+ lines.push("Tech Stack (code-verified):");
2430
+ for (const [name, data] of skills) {
2431
+ lines.push(` ${name}: proficiency ${data.proficiency}, ${data.loc.toLocaleString()} LOC, ${data.projects} projects`);
2432
+ }
2433
+ }
2434
+ const patterns = Object.entries(graph.architecture).sort((a, b) => b[1].confidence - a[1].confidence);
2435
+ if (patterns.length > 0) {
2436
+ lines.push(`Architecture: ${patterns.map(([n, { confidence }]) => `${n} (${confidence})`).join(", ")}`);
2437
+ }
2438
+ lines.push(`Quality: test ratio ${graph.quality.test_ratio}, complexity ${graph.quality.complexity_avg}`);
2439
+ if (graph.projects.length > 0) {
2440
+ lines.push(`Projects (${graph.projects.length}):`);
2441
+ for (const proj of graph.projects.slice(0, 5)) {
2442
+ lines.push(` ${proj.name}: ${proj.domain || "unknown"} \u2014 ${proj.stack.slice(0, 5).join(", ")}`);
2443
+ }
2444
+ }
2445
+ if (graph.builder_profile.role_signals.length > 0) {
2446
+ lines.push(`Role signals: ${graph.builder_profile.role_signals.join(", ")}`);
2447
+ }
2448
+ if (graph.builder_profile.is_end_to_end) {
2449
+ lines.push("End-to-end builder: yes");
2450
+ }
2451
+ if (graph.builder_identity.previous_companies.length > 0) {
2452
+ lines.push("Work history:");
2453
+ for (const w of graph.builder_identity.previous_companies) {
2454
+ lines.push(` ${w.role} @ ${w.company} (${w.start_year}-${w.end_year || "present"})`);
2455
+ }
2456
+ }
2457
+ return lines.join("\n");
2458
+ }
2459
+ function buildJobSummary(job, req) {
2460
+ const lines = [
2461
+ `Title: ${job.title}`,
2462
+ `Company: ${job.company}`,
2463
+ `Location: ${job.location}`,
2464
+ `Seniority: ${req.seniority_level}`,
2465
+ `Domain: ${req.domain}`,
2466
+ `Remote: ${req.remote_policy}`
2467
+ ];
2468
+ if (req.must_have_skills.length > 0) {
2469
+ lines.push(`Must have: ${req.must_have_skills.join(", ")}`);
2470
+ }
2471
+ if (req.nice_to_have_skills.length > 0) {
2472
+ lines.push(`Nice to have: ${req.nice_to_have_skills.join(", ")}`);
2473
+ }
2474
+ if (req.tech_stack.length > 0) {
2475
+ lines.push(`Tech stack: ${req.tech_stack.join(", ")}`);
2476
+ }
2477
+ return lines.join("\n");
2478
+ }
2479
+
2480
+ // src/matching/matcher.ts
2481
+ async function runMatchPipeline(options) {
2482
+ const topK = options?.topK || 50;
2483
+ const maxEval = options?.maxEval || 50;
2484
+ const graph = await loadGraph();
2485
+ if (!graph || graph.projects.length === 0) {
2486
+ throw new Error("No skill graph found. Run `hiregraph scan <path>` first.");
2487
+ }
2488
+ const identity = graph.builder_identity;
2489
+ const config = await loadJson("config.json");
2490
+ const filterConfig = {
2491
+ excluded_companies: config?.excluded_companies || [],
2492
+ remote_preference: identity.remote_preference,
2493
+ min_compensation: identity.min_compensation
2494
+ };
2495
+ const fetchResult = await fetchAllJobs({ refresh: options?.refresh });
2496
+ const allJobs = fetchResult.jobs;
2497
+ if (allJobs.length === 0) {
2498
+ throw new Error("No jobs found. Run `hiregraph jobs` first.");
2499
+ }
2500
+ const requirements = await parseJobsBatch(allJobs);
2501
+ const jobMap = /* @__PURE__ */ new Map();
2502
+ for (const job of allJobs) jobMap.set(job.id, job);
2503
+ const filterableJobs = [];
2504
+ for (const [jobId, req] of Object.entries(requirements)) {
2505
+ const job = jobMap.get(jobId);
2506
+ if (job) filterableJobs.push({ job, requirements: req });
2507
+ }
2508
+ start("Applying filters...");
2509
+ const filtered = applyHardFilters(filterableJobs, filterConfig);
2510
+ succeed(`After filtering: ${filtered.length} jobs (from ${filterableJobs.length})`);
2511
+ if (filtered.length === 0) {
2512
+ throw new Error("All jobs filtered out. Try adjusting your preferences.");
2513
+ }
2514
+ start("Building vectors for matching...");
2515
+ const filteredReqs = {};
2516
+ for (const f of filtered) filteredReqs[f.job.id] = f.requirements;
2517
+ const documents = buildAllDocuments(graph, filteredReqs);
2518
+ const vocab = buildVocabulary(documents);
2519
+ const skillVector = buildSkillVector(graph, vocab);
2520
+ const jobVectors = /* @__PURE__ */ new Map();
2521
+ for (const f of filtered) {
2522
+ jobVectors.set(f.job.id, buildJobVector(f.requirements, vocab));
2523
+ }
2524
+ succeed(`Vectorized ${filtered.length} jobs`);
2525
+ start(`Pre-filtering top ${topK} candidates...`);
2526
+ const candidates = findTopCandidates(skillVector, jobVectors, topK);
2527
+ succeed(`Top ${candidates.length} candidates selected`);
2528
+ const evalInputs = candidates.slice(0, maxEval).map((c) => {
2529
+ const job = jobMap.get(c.jobId);
2530
+ const req = requirements[c.jobId];
2531
+ return { job, requirements: req };
2532
+ });
2533
+ const matchResults = await evaluateMatchesBatch(evalInputs, graph);
2534
+ matchResults.sort((a, b) => b.score - a.score);
2535
+ const strong = matchResults.filter((m) => m.tier === "strong");
2536
+ const suggested = matchResults.filter((m) => m.tier === "suggested");
2537
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2538
+ const matchRun = {
2539
+ date: today,
2540
+ total_jobs_fetched: allJobs.length,
2541
+ total_jobs_parsed: Object.keys(requirements).length,
2542
+ total_candidates_evaluated: matchResults.length,
2543
+ strong_matches: strong,
2544
+ suggested_matches: suggested,
2545
+ run_at: (/* @__PURE__ */ new Date()).toISOString(),
2546
+ cost_estimate: {
2547
+ jobs_parsed: Object.keys(requirements).length,
2548
+ pairs_evaluated: matchResults.length,
2549
+ estimated_usd: Math.round((Object.keys(requirements).length * 3e-4 + matchResults.length * 3e-3) * 100) / 100
2550
+ }
2551
+ };
2552
+ await saveSubJson("matches", `${today}.json`, matchRun);
2553
+ return matchRun;
2554
+ }
2555
+
2556
+ // src/commands/matches.ts
2557
+ async function matchesCommand(options) {
2558
+ header("\n HireGraph Matches\n");
2559
+ if (!isApiKeyConfigured()) {
2560
+ warn("No API key detected. Run hiregraph inside Claude Code or Cursor for automatic access.");
2561
+ return;
2562
+ }
2563
+ try {
2564
+ const result = await runMatchPipeline({
2565
+ topK: options.top || 50,
2566
+ refresh: options.refresh
2567
+ });
2568
+ console.log();
2569
+ if (result.strong_matches.length > 0) {
2570
+ console.log(` ${chalk4.bold.green("Strong Matches (score 8-10):")}`);
2571
+ for (let i = 0; i < result.strong_matches.length; i++) {
2572
+ printMatch(result.strong_matches[i], i + 1, true);
2573
+ }
2574
+ console.log();
2575
+ }
2576
+ if (result.suggested_matches.length > 0) {
2577
+ console.log(` ${chalk4.bold.yellow("Suggested Matches (score 6-7):")}`);
2578
+ for (let i = 0; i < result.suggested_matches.length; i++) {
2579
+ const idx = result.strong_matches.length + i + 1;
2580
+ printMatch(result.suggested_matches[i], idx, !!options.verbose);
2581
+ }
2582
+ console.log();
2583
+ }
2584
+ if (result.strong_matches.length === 0 && result.suggested_matches.length === 0) {
2585
+ warn(" No matches above threshold. Try scanning more projects or adjusting preferences.");
2586
+ console.log();
2587
+ }
2588
+ console.log(` ${chalk4.bold("Summary:")}`);
2589
+ console.log(` Jobs analyzed: ${result.total_jobs_fetched.toLocaleString()}`);
2590
+ console.log(` Jobs parsed: ${result.total_jobs_parsed.toLocaleString()}`);
2591
+ console.log(` LLM evaluated: ${result.total_candidates_evaluated}`);
2592
+ console.log(` Strong matches: ${chalk4.green(String(result.strong_matches.length))}`);
2593
+ console.log(` Suggested: ${chalk4.yellow(String(result.suggested_matches.length))}`);
2594
+ console.log(` Cost estimate: ~$${result.cost_estimate.estimated_usd.toFixed(2)}`);
2595
+ console.log();
2596
+ info(` Results saved to ~/.hiregraph/matches/${result.date}.json`);
2597
+ console.log();
2598
+ } catch (err) {
2599
+ error(err.message);
2600
+ process.exit(1);
2601
+ }
2602
+ }
2603
+ function printMatch(match, rank, showDetails) {
2604
+ const scoreColor = match.score >= 8 ? chalk4.green : chalk4.yellow;
2605
+ console.log(` ${chalk4.dim(`#${rank}`)} ${scoreColor(match.score.toFixed(1))} ${chalk4.bold(match.job_title)} @ ${match.company}`);
2606
+ if (showDetails) {
2607
+ if (match.strengths.length > 0) {
2608
+ console.log(` ${chalk4.green("+")} ${match.strengths.join(", ")}`);
2609
+ }
2610
+ if (match.gaps.length > 0) {
2611
+ console.log(` ${chalk4.red("-")} ${match.gaps.join(", ")}`);
2612
+ }
2613
+ console.log(` ${chalk4.dim(match.url)}`);
2614
+ }
2615
+ }
2616
+
2617
+ // src/commands/apply.ts
2618
+ import chalk5 from "chalk";
2619
+ import inquirer2 from "inquirer";
2620
+
2621
+ // src/history/tracker.ts
2622
+ var HISTORY_FILE = "history.json";
2623
+ async function loadHistory() {
2624
+ const data = await loadJson(HISTORY_FILE);
2625
+ return data || { applications: [], last_updated: (/* @__PURE__ */ new Date()).toISOString() };
2626
+ }
2627
+ async function saveHistory(history) {
2628
+ history.last_updated = (/* @__PURE__ */ new Date()).toISOString();
2629
+ await saveJson(HISTORY_FILE, history);
2630
+ }
2631
+ async function addApplication(record) {
2632
+ const history = await loadHistory();
2633
+ history.applications.push(record);
2634
+ await saveHistory(history);
2635
+ }
2636
+ async function updateApplicationStatus(appId, status, notes) {
2637
+ const history = await loadHistory();
2638
+ const app = history.applications.find(
2639
+ (a) => a.id === appId || a.id.startsWith(appId)
2640
+ );
2641
+ if (!app) return null;
2642
+ app.status = status;
2643
+ app.updated_at = (/* @__PURE__ */ new Date()).toISOString();
2644
+ if (notes) app.notes = notes;
2645
+ await saveHistory(history);
2646
+ return app;
2647
+ }
2648
+ function findByJobId(history, jobId) {
2649
+ return history.applications.find((a) => a.job_id === jobId);
2650
+ }
2651
+ function generateAppId() {
2652
+ const ts = Date.now().toString(36).slice(-4);
2653
+ const rand = Math.random().toString(36).slice(2, 6);
2654
+ return `app_${ts}${rand}`;
2655
+ }
2656
+
2657
+ // src/resume/tailorer.ts
2658
+ var SYSTEM_PROMPT5 = `You tailor a resume for a specific job. Given the candidate's skill data and target job, produce a tailored resume configuration.
2659
+
2660
+ Rules:
2661
+ - professional_summary: Write 3-4 sentences as if the candidate wrote them. No mention of scores, tools, analysis systems, or automation. Sound natural and specific to this role.
2662
+ - project_order: Rank the candidate's projects by relevance to this job. Most relevant first. Use exact project names.
2663
+ - bullet_emphasis: For each project, write 2-4 resume bullet points that best align with the job. Start with action verbs, include metrics where possible (commits, LOC, active days).
2664
+ - skills_order: List the candidate's tech skills reordered by relevance to this job. Most relevant first. Use exact skill names from the profile.
2665
+
2666
+ Return JSON only, no markdown fences. Schema:
2667
+ {
2668
+ "professional_summary": "string",
2669
+ "project_order": ["project names"],
2670
+ "bullet_emphasis": { "ProjectName": ["bullet1", "bullet2"] },
2671
+ "skills_order": ["skill names"]
2672
+ }`;
2673
+ async function tailorResume(graph, job, requirements, match) {
2674
+ const identity = graph.builder_identity;
2675
+ const profileLines = [
2676
+ `Name: ${identity.name}`,
2677
+ `Role: ${identity.primary_role}`
2678
+ ];
2679
+ const skills = Object.entries(graph.tech_stack).sort((a, b) => b[1].proficiency - a[1].proficiency).slice(0, 15);
2680
+ if (skills.length > 0) {
2681
+ profileLines.push("Skills (code-verified):");
2682
+ for (const [name, data] of skills) {
2683
+ profileLines.push(` ${name}: ${data.loc.toLocaleString()} LOC, ${data.projects} projects`);
2684
+ }
2685
+ }
2686
+ if (graph.projects.length > 0) {
2687
+ profileLines.push("Projects:");
2688
+ for (const proj of graph.projects) {
2689
+ profileLines.push(` ${proj.name}: ${proj.domain} \u2014 ${proj.stack.slice(0, 5).join(", ")} \u2014 ${proj.commits} commits, ${proj.active_days} active days`);
2690
+ if (proj.description) profileLines.push(` ${proj.description}`);
2691
+ }
2692
+ }
2693
+ if (identity.previous_companies.length > 0) {
2694
+ profileLines.push("Work History:");
2695
+ for (const w of identity.previous_companies) {
2696
+ profileLines.push(` ${w.role} @ ${w.company} (${w.start_year}-${w.end_year || "present"})`);
2697
+ }
2698
+ }
2699
+ const jobLines = [
2700
+ `Title: ${job.title}`,
2701
+ `Company: ${job.company}`,
2702
+ `Must have: ${requirements.must_have_skills.join(", ")}`,
2703
+ `Nice to have: ${requirements.nice_to_have_skills.join(", ")}`,
2704
+ `Tech stack: ${requirements.tech_stack.join(", ")}`,
2705
+ `Domain: ${requirements.domain}`,
2706
+ `Match strengths: ${match.strengths.join(", ")}`
2707
+ ];
2708
+ const prompt = `CANDIDATE PROFILE:
2709
+ ${profileLines.join("\n")}
2710
+
2711
+ TARGET JOB:
2712
+ ${jobLines.join("\n")}`;
2713
+ const result = await callHaikuJson(SYSTEM_PROMPT5, prompt, 2048);
2714
+ return {
2715
+ job_id: job.id,
2716
+ professional_summary: result.professional_summary || "",
2717
+ project_order: result.project_order || graph.projects.map((p) => p.name),
2718
+ bullet_emphasis: result.bullet_emphasis || {},
2719
+ skills_order: result.skills_order || Object.keys(graph.tech_stack),
2720
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
2721
+ };
2722
+ }
2723
+
2724
+ // src/resume/pdf-builder.ts
2725
+ import PDFDocument from "pdfkit";
2726
+ import { writeFile as writeFile2 } from "fs/promises";
2727
+ import { join as join10 } from "path";
2728
+ var MARGIN = 54;
2729
+ var PAGE_WIDTH = 612;
2730
+ var CONTENT_WIDTH = PAGE_WIDTH - MARGIN * 2;
2731
+ var FONT_BODY = "Helvetica";
2732
+ var FONT_BOLD = "Helvetica-Bold";
2733
+ var SIZE_NAME = 18;
2734
+ var SIZE_SECTION = 12;
2735
+ var SIZE_BODY = 10.5;
2736
+ var SIZE_SMALL = 9.5;
2737
+ var COLOR_BLACK = "#000000";
2738
+ var COLOR_GRAY = "#444444";
2739
+ var SKILL_CATEGORIES = {
2740
+ Languages: ["TypeScript", "JavaScript", "Python", "Rust", "Go", "Java", "C", "C++", "C#", "Ruby", "PHP", "Swift", "Kotlin", "Dart", "Scala", "Elixir", "Haskell", "SQL", "Shell", "Lua", "R", "Zig"],
2741
+ Frameworks: ["React", "React Native", "Next.js", "Vue", "Nuxt", "Svelte", "SvelteKit", "Angular", "Express", "Fastify", "NestJS", "Django", "Flask", "FastAPI", "Actix", "Axum", "Gin", "Fiber", "Expo", "Hono", "Remix", "Astro", "Gatsby", "Solid"],
2742
+ "Data & AI": ["Prisma", "Drizzle", "TypeORM", "Sequelize", "Mongoose", "SQLAlchemy", "Pandas", "NumPy", "TensorFlow", "PyTorch", "LangChain", "Anthropic SDK", "OpenAI"],
2743
+ Infrastructure: ["Supabase", "Firebase", "Docker", "Kubernetes", "AWS", "GCP", "Azure", "Vercel", "Tokio", "Serde"]
2744
+ };
2745
+ async function generateResumePdf(graph, tailoring) {
2746
+ return new Promise((resolve3, reject) => {
2747
+ const doc = new PDFDocument({
2748
+ size: "LETTER",
2749
+ margins: { top: MARGIN, bottom: MARGIN, left: MARGIN, right: MARGIN }
2750
+ });
2751
+ const chunks = [];
2752
+ doc.on("data", (chunk) => chunks.push(chunk));
2753
+ doc.on("end", () => resolve3(Buffer.concat(chunks)));
2754
+ doc.on("error", reject);
2755
+ const identity = graph.builder_identity;
2756
+ doc.font(FONT_BOLD).fontSize(SIZE_NAME).fillColor(COLOR_BLACK);
2757
+ doc.text(identity.name || "Builder", { align: "center" });
2758
+ doc.moveDown(0.3);
2759
+ const contactParts = [];
2760
+ if (identity.email) contactParts.push(identity.email);
2761
+ if (identity.phone) contactParts.push(identity.phone);
2762
+ const linkParts = [];
2763
+ for (const [, url] of Object.entries(identity.links || {})) {
2764
+ if (url) linkParts.push(url);
2765
+ }
2766
+ doc.font(FONT_BODY).fontSize(SIZE_SMALL).fillColor(COLOR_GRAY);
2767
+ if (contactParts.length > 0) {
2768
+ doc.text(contactParts.join(" | "), { align: "center" });
2769
+ }
2770
+ if (linkParts.length > 0) {
2771
+ doc.text(linkParts.join(" | "), { align: "center" });
2772
+ }
2773
+ doc.moveDown(0.8);
2774
+ renderSectionHeader(doc, "PROFESSIONAL SUMMARY");
2775
+ doc.font(FONT_BODY).fontSize(SIZE_BODY).fillColor(COLOR_BLACK);
2776
+ doc.text(tailoring.professional_summary, { lineGap: 2 });
2777
+ doc.moveDown(0.6);
2778
+ renderSectionHeader(doc, "TECHNICAL SKILLS");
2779
+ const categorized = categorizeSkills(tailoring.skills_order, graph);
2780
+ doc.font(FONT_BODY).fontSize(SIZE_BODY).fillColor(COLOR_BLACK);
2781
+ for (const [category, skills] of Object.entries(categorized)) {
2782
+ if (skills.length > 0) {
2783
+ doc.font(FONT_BOLD).text(`${category}: `, { continued: true });
2784
+ doc.font(FONT_BODY).text(skills.join(", "));
2785
+ }
2786
+ }
2787
+ doc.moveDown(0.6);
2788
+ if (identity.previous_companies && identity.previous_companies.length > 0) {
2789
+ renderSectionHeader(doc, "WORK EXPERIENCE");
2790
+ for (const work of identity.previous_companies) {
2791
+ renderWorkEntry(doc, work);
2792
+ }
2793
+ doc.moveDown(0.3);
2794
+ }
2795
+ if (graph.projects.length > 0) {
2796
+ renderSectionHeader(doc, "PROJECTS");
2797
+ const orderedProjects = orderProjects(graph.projects, tailoring.project_order);
2798
+ for (const proj of orderedProjects.slice(0, 4)) {
2799
+ renderProject(doc, proj, tailoring.bullet_emphasis[proj.name] || []);
2800
+ }
2801
+ doc.moveDown(0.3);
2802
+ }
2803
+ if (identity.education && identity.education.length > 0) {
2804
+ renderSectionHeader(doc, "EDUCATION");
2805
+ for (const edu of identity.education) {
2806
+ renderEducation(doc, edu);
2807
+ }
2808
+ }
2809
+ doc.end();
2810
+ });
2811
+ }
2812
+ async function saveResumePdf(pdfBuffer, jobId) {
2813
+ await ensureSubDir("resumes");
2814
+ const filename = `${jobId.replace(/[^a-zA-Z0-9_-]/g, "_")}.pdf`;
2815
+ const filepath = join10(getPath("resumes"), filename);
2816
+ await writeFile2(filepath, pdfBuffer);
2817
+ return filepath;
2818
+ }
2819
+ function renderSectionHeader(doc, title) {
2820
+ doc.font(FONT_BOLD).fontSize(SIZE_SECTION).fillColor(COLOR_BLACK);
2821
+ doc.text(title);
2822
+ doc.moveTo(MARGIN, doc.y + 2).lineTo(PAGE_WIDTH - MARGIN, doc.y + 2).strokeColor(COLOR_GRAY).lineWidth(0.5).stroke();
2823
+ doc.moveDown(0.4);
2824
+ }
2825
+ function renderWorkEntry(doc, work) {
2826
+ const dateRange = `${work.start_year} - ${work.end_year || "Present"}`;
2827
+ doc.font(FONT_BOLD).fontSize(SIZE_BODY).fillColor(COLOR_BLACK);
2828
+ doc.text(`${work.role}`, { continued: true });
2829
+ doc.font(FONT_BODY).text(` | ${work.company}`);
2830
+ doc.font(FONT_BODY).fontSize(SIZE_SMALL).fillColor(COLOR_GRAY);
2831
+ doc.text(dateRange);
2832
+ doc.fillColor(COLOR_BLACK).fontSize(SIZE_BODY);
2833
+ if (work.bullets && work.bullets.length > 0) {
2834
+ for (const bullet of work.bullets) {
2835
+ doc.text(` \u2022 ${bullet}`, { indent: 10, lineGap: 1 });
2836
+ }
2837
+ }
2838
+ doc.moveDown(0.4);
2839
+ }
2840
+ function renderProject(doc, proj, bullets) {
2841
+ const stackStr = proj.stack.slice(0, 5).join(", ");
2842
+ doc.font(FONT_BOLD).fontSize(SIZE_BODY).fillColor(COLOR_BLACK);
2843
+ doc.text(proj.name, { continued: true });
2844
+ doc.font(FONT_BODY).fillColor(COLOR_GRAY).text(` | ${proj.domain || "project"} | ${stackStr}`);
2845
+ doc.fillColor(COLOR_BLACK);
2846
+ if (proj.description) {
2847
+ doc.font(FONT_BODY).fontSize(SIZE_BODY).text(proj.description, { lineGap: 1 });
2848
+ }
2849
+ const displayBullets = bullets.length > 0 ? bullets : generateDefaultBullets(proj);
2850
+ for (const bullet of displayBullets.slice(0, 3)) {
2851
+ doc.text(` \u2022 ${bullet}`, { indent: 10, lineGap: 1 });
2852
+ }
2853
+ doc.moveDown(0.4);
2854
+ }
2855
+ function renderEducation(doc, edu) {
2856
+ doc.font(FONT_BOLD).fontSize(SIZE_BODY).fillColor(COLOR_BLACK);
2857
+ doc.text(`${edu.degree} in ${edu.field}`, { continued: true });
2858
+ doc.font(FONT_BODY).text(` | ${edu.institution} | ${edu.year}`);
2859
+ }
2860
+ function orderProjects(projects, order) {
2861
+ const byName = new Map(projects.map((p) => [p.name, p]));
2862
+ const ordered = [];
2863
+ for (const name of order) {
2864
+ const proj = byName.get(name);
2865
+ if (proj) {
2866
+ ordered.push(proj);
2867
+ byName.delete(name);
2868
+ }
2869
+ }
2870
+ for (const proj of byName.values()) {
2871
+ ordered.push(proj);
2872
+ }
2873
+ return ordered;
2874
+ }
2875
+ function categorizeSkills(skillsOrder, graph) {
2876
+ const allSkills = skillsOrder.length > 0 ? skillsOrder : Object.keys(graph.tech_stack);
2877
+ const result = {};
2878
+ const used = /* @__PURE__ */ new Set();
2879
+ for (const [category, known] of Object.entries(SKILL_CATEGORIES)) {
2880
+ const knownLower = new Set(known.map((k) => k.toLowerCase()));
2881
+ const matched = allSkills.filter((s) => knownLower.has(s.toLowerCase()) && !used.has(s.toLowerCase()));
2882
+ if (matched.length > 0) {
2883
+ result[category] = matched;
2884
+ matched.forEach((s) => used.add(s.toLowerCase()));
2885
+ }
2886
+ }
2887
+ const remaining = allSkills.filter((s) => !used.has(s.toLowerCase()));
2888
+ if (remaining.length > 0) {
2889
+ result["Other"] = remaining;
2890
+ }
2891
+ return result;
2892
+ }
2893
+ function generateDefaultBullets(proj) {
2894
+ const bullets = [];
2895
+ if (proj.commits > 0) {
2896
+ bullets.push(`${proj.commits} commits over ${proj.active_days} active days${proj.contributors > 1 ? ` with ${proj.contributors} contributors` : ""}`);
2897
+ }
2898
+ const langStr = Object.entries(proj.languages).sort((a, b) => b[1].loc - a[1].loc).slice(0, 3).map(([lang, { loc }]) => `${lang} (${loc.toLocaleString()} LOC)`).join(", ");
2899
+ if (langStr) bullets.push(`Built with ${langStr}`);
2900
+ return bullets;
2901
+ }
2902
+
2903
+ // src/ats/submitter.ts
2904
+ import FormData from "form-data";
2905
+ async function submitApplication(job, pdfBuffer, identity) {
2906
+ const registry = await loadRegistry();
2907
+ const company = registry.find((c) => c.slug === job.company_slug);
2908
+ if (!company) {
2909
+ return { success: false, message: `Company not found in registry: ${job.company_slug}` };
2910
+ }
2911
+ const { source, rawId } = extractRawJobId(job.id);
2912
+ switch (source) {
2913
+ case "greenhouse":
2914
+ return submitToGreenhouse(company.board_token, rawId, pdfBuffer, identity);
2915
+ case "lever":
2916
+ return submitToLever(company.board_token, rawId, pdfBuffer, identity);
2917
+ case "ashby":
2918
+ return submitToAshby(rawId, pdfBuffer, identity);
2919
+ default:
2920
+ return { success: false, message: `Unknown ATS source: ${source}` };
2921
+ }
2922
+ }
2923
+ function extractRawJobId(normalizedId) {
2924
+ if (normalizedId.startsWith("gh_")) return { source: "greenhouse", rawId: normalizedId.slice(3) };
2925
+ if (normalizedId.startsWith("lv_")) return { source: "lever", rawId: normalizedId.slice(3) };
2926
+ if (normalizedId.startsWith("ab_")) return { source: "ashby", rawId: normalizedId.slice(3) };
2927
+ return { source: "unknown", rawId: normalizedId };
2928
+ }
2929
+ async function submitToGreenhouse(boardToken, rawJobId, pdfBuffer, identity) {
2930
+ const url = `https://boards-api.greenhouse.io/v1/boards/${boardToken}/jobs/${rawJobId}`;
2931
+ const nameParts = (identity.name || "").split(" ");
2932
+ const firstName = nameParts[0] || "";
2933
+ const lastName = nameParts.slice(1).join(" ") || "";
2934
+ const form = new FormData();
2935
+ form.append("first_name", firstName);
2936
+ form.append("last_name", lastName);
2937
+ form.append("email", identity.email);
2938
+ if (identity.phone) form.append("phone", identity.phone);
2939
+ form.append("resume", pdfBuffer, { filename: "resume.pdf", contentType: "application/pdf" });
2940
+ if (identity.links?.github) form.append("urls[GitHub]", identity.links.github);
2941
+ if (identity.links?.linkedin) form.append("urls[LinkedIn]", identity.links.linkedin);
2942
+ if (identity.links?.portfolio) form.append("urls[Portfolio]", identity.links.portfolio);
2943
+ try {
2944
+ const response = await fetch(url, {
2945
+ method: "POST",
2946
+ headers: form.getHeaders(),
2947
+ body: form.getBuffer(),
2948
+ signal: AbortSignal.timeout(3e4)
2949
+ });
2950
+ if (response.ok) {
2951
+ return { success: true, message: "Application submitted via Greenhouse", status: response.status };
2952
+ }
2953
+ const body = await response.text().catch(() => "");
2954
+ return { success: false, message: `Greenhouse HTTP ${response.status}: ${body.slice(0, 200)}`, status: response.status };
2955
+ } catch (err) {
2956
+ return { success: false, message: `Greenhouse error: ${err.message}` };
2957
+ }
2958
+ }
2959
+ async function submitToLever(boardToken, rawPostingId, pdfBuffer, identity) {
2960
+ const url = `https://api.lever.co/v0/postings/${boardToken}/${rawPostingId}`;
2961
+ const form = new FormData();
2962
+ form.append("name", identity.name || "");
2963
+ form.append("email", identity.email);
2964
+ if (identity.phone) form.append("phone", identity.phone);
2965
+ form.append("resume", pdfBuffer, { filename: "resume.pdf", contentType: "application/pdf" });
2966
+ if (identity.links?.github) form.append("urls[GitHub]", identity.links.github);
2967
+ if (identity.links?.linkedin) form.append("urls[LinkedIn]", identity.links.linkedin);
2968
+ try {
2969
+ const response = await fetch(url, {
2970
+ method: "POST",
2971
+ headers: form.getHeaders(),
2972
+ body: form.getBuffer(),
2973
+ signal: AbortSignal.timeout(3e4)
2974
+ });
2975
+ if (response.ok) {
2976
+ return { success: true, message: "Application submitted via Lever", status: response.status };
2977
+ }
2978
+ const body = await response.text().catch(() => "");
2979
+ return { success: false, message: `Lever HTTP ${response.status}: ${body.slice(0, 200)}`, status: response.status };
2980
+ } catch (err) {
2981
+ return { success: false, message: `Lever error: ${err.message}` };
2982
+ }
2983
+ }
2984
+ async function submitToAshby(rawJobId, pdfBuffer, identity) {
2985
+ const url = "https://api.ashbyhq.com/posting-api/applicationForm.submit";
2986
+ const nameParts = (identity.name || "").split(" ");
2987
+ const body = {
2988
+ jobPostingId: rawJobId,
2989
+ applicationForm: {
2990
+ firstName: nameParts[0] || "",
2991
+ lastName: nameParts.slice(1).join(" ") || "",
2992
+ email: identity.email,
2993
+ phone: identity.phone || "",
2994
+ resume: {
2995
+ filename: "resume.pdf",
2996
+ mimeType: "application/pdf",
2997
+ data: pdfBuffer.toString("base64")
2998
+ }
2999
+ }
3000
+ };
3001
+ try {
3002
+ const response = await fetch(url, {
3003
+ method: "POST",
3004
+ headers: { "Content-Type": "application/json" },
3005
+ body: JSON.stringify(body),
3006
+ signal: AbortSignal.timeout(3e4)
3007
+ });
3008
+ if (response.ok) {
3009
+ return { success: true, message: "Application submitted via Ashby", status: response.status };
3010
+ }
3011
+ const respBody = await response.text().catch(() => "");
3012
+ return { success: false, message: `Ashby HTTP ${response.status}: ${respBody.slice(0, 200)}`, status: response.status };
3013
+ } catch (err) {
3014
+ return { success: false, message: `Ashby error: ${err.message}` };
3015
+ }
3016
+ }
3017
+
3018
+ // src/commands/apply.ts
3019
+ async function applyCommand(jobId, options) {
3020
+ header("\n HireGraph Apply\n");
3021
+ if (!isApiKeyConfigured()) {
3022
+ warn("No API key detected. Run hiregraph inside Claude Code or Cursor.");
3023
+ return;
3024
+ }
3025
+ const graph = await loadGraph();
3026
+ if (!graph || graph.projects.length === 0) {
3027
+ error("No skill graph found. Run `hiregraph scan <path>` first.");
3028
+ return;
3029
+ }
3030
+ if (!graph.builder_identity.name || !graph.builder_identity.email) {
3031
+ error("Name and email required. Run `hiregraph init` first.");
3032
+ return;
3033
+ }
3034
+ const matchRun = await loadLatestMatches();
3035
+ if (!matchRun) {
3036
+ error("No match results found. Run `hiregraph matches` first.");
3037
+ return;
3038
+ }
3039
+ const jobsMap = await loadJobsMap();
3040
+ const requirementsMap = await loadRequirementsMap();
3041
+ const history = await loadHistory();
3042
+ let targets;
3043
+ if (options?.allAbove !== void 0) {
3044
+ const threshold = options.allAbove;
3045
+ const allMatches = [...matchRun.strong_matches, ...matchRun.suggested_matches];
3046
+ targets = allMatches.filter((m) => m.score >= threshold);
3047
+ info(` Found ${targets.length} matches with score >= ${threshold}
3048
+ `);
3049
+ } else if (jobId) {
3050
+ const allMatches = [...matchRun.strong_matches, ...matchRun.suggested_matches];
3051
+ const match = allMatches.find((m) => m.job_id === jobId);
3052
+ if (!match) {
3053
+ error(`Match not found for job ID: ${jobId}`);
3054
+ info("Available matches:");
3055
+ for (const m of allMatches.slice(0, 10)) {
3056
+ console.log(` ${chalk5.dim(m.job_id)} ${m.job_title} @ ${m.company} (${m.score})`);
3057
+ }
3058
+ return;
3059
+ }
3060
+ targets = [match];
3061
+ } else {
3062
+ error("Provide a job-id or use --all-above <score>");
3063
+ info("Usage: hiregraph apply <job-id> [--review] [--dry-run]");
3064
+ info(" hiregraph apply --all-above 8");
3065
+ return;
3066
+ }
3067
+ let applied = 0;
3068
+ let skipped = 0;
3069
+ let failed = 0;
3070
+ for (const match of targets) {
3071
+ if (findByJobId(history, match.job_id)) {
3072
+ dim(` Skipped (already applied): ${match.job_title} @ ${match.company}`);
3073
+ skipped++;
3074
+ continue;
3075
+ }
3076
+ const job = jobsMap.get(match.job_id);
3077
+ const requirements = requirementsMap[match.job_id];
3078
+ if (!job || !requirements) {
3079
+ warn(` Skipped (missing data): ${match.job_title} @ ${match.company}`);
3080
+ failed++;
3081
+ continue;
3082
+ }
3083
+ console.log(`
3084
+ ${chalk5.bold(match.job_title)} @ ${match.company} (score: ${chalk5.green(String(match.score))})`);
3085
+ start("Tailoring resume...");
3086
+ let tailoring;
3087
+ try {
3088
+ tailoring = await tailorResume(graph, job, requirements, match);
3089
+ succeed("Resume tailored");
3090
+ } catch (err) {
3091
+ fail(`Tailoring failed: ${err.message}`);
3092
+ failed++;
3093
+ continue;
3094
+ }
3095
+ if (options?.review) {
3096
+ console.log();
3097
+ console.log(` ${chalk5.bold("Summary:")} ${tailoring.professional_summary}`);
3098
+ console.log(` ${chalk5.bold("Projects:")} ${tailoring.project_order.join(" > ")}`);
3099
+ console.log(` ${chalk5.bold("Skills:")} ${tailoring.skills_order.slice(0, 8).join(", ")}`);
3100
+ console.log(` ${chalk5.bold("Strengths:")} ${match.strengths.join(", ")}`);
3101
+ console.log(` ${chalk5.bold("Gaps:")} ${match.gaps.join(", ")}`);
3102
+ console.log();
3103
+ const { proceed } = await inquirer2.prompt([{
3104
+ type: "confirm",
3105
+ name: "proceed",
3106
+ message: "Apply to this job?",
3107
+ default: true
3108
+ }]);
3109
+ if (!proceed) {
3110
+ dim(" Skipped by user");
3111
+ skipped++;
3112
+ continue;
3113
+ }
3114
+ }
3115
+ start("Generating resume PDF...");
3116
+ let pdfBuffer;
3117
+ let resumePath;
3118
+ try {
3119
+ pdfBuffer = await generateResumePdf(graph, tailoring);
3120
+ resumePath = await saveResumePdf(pdfBuffer, match.job_id);
3121
+ succeed(`Resume saved: ${resumePath}`);
3122
+ } catch (err) {
3123
+ fail(`PDF generation failed: ${err.message}`);
3124
+ failed++;
3125
+ continue;
3126
+ }
3127
+ if (options?.dryRun) {
3128
+ success(` [dry-run] Resume generated but not submitted`);
3129
+ applied++;
3130
+ continue;
3131
+ }
3132
+ start("Submitting application...");
3133
+ const result = await submitApplication(job, pdfBuffer, graph.builder_identity);
3134
+ if (result.success) {
3135
+ succeed(result.message);
3136
+ const appId = generateAppId();
3137
+ await addApplication({
3138
+ id: appId,
3139
+ job_id: match.job_id,
3140
+ job_title: match.job_title,
3141
+ company: match.company,
3142
+ company_slug: match.company_slug,
3143
+ url: match.url,
3144
+ ats_source: job.source,
3145
+ match_score: match.score,
3146
+ resume_path: resumePath,
3147
+ status: "applied",
3148
+ applied_at: (/* @__PURE__ */ new Date()).toISOString(),
3149
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
3150
+ });
3151
+ success(` Applied! (${appId})`);
3152
+ applied++;
3153
+ } else {
3154
+ fail(result.message);
3155
+ failed++;
3156
+ }
3157
+ if (targets.length > 1) {
3158
+ await new Promise((r) => setTimeout(r, 2e3));
3159
+ }
3160
+ }
3161
+ console.log();
3162
+ console.log(` ${chalk5.bold("Summary:")}`);
3163
+ console.log(` Applied: ${chalk5.green(String(applied))}`);
3164
+ if (skipped > 0) console.log(` Skipped: ${chalk5.yellow(String(skipped))}`);
3165
+ if (failed > 0) console.log(` Failed: ${chalk5.red(String(failed))}`);
3166
+ console.log();
3167
+ }
3168
+ async function loadLatestMatches() {
3169
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3170
+ let run = await loadSubJson("matches", `${today}.json`);
3171
+ if (run) return run;
3172
+ const yesterday = new Date(Date.now() - 864e5).toISOString().slice(0, 10);
3173
+ run = await loadSubJson("matches", `${yesterday}.json`);
3174
+ return run;
3175
+ }
3176
+ async function loadJobsMap() {
3177
+ const map = /* @__PURE__ */ new Map();
3178
+ for (const source of ["greenhouse", "lever", "ashby"]) {
3179
+ const cache = await loadSubJson("jobs", `${source}.json`);
3180
+ if (cache) {
3181
+ for (const job of cache.jobs) {
3182
+ map.set(job.id, job);
3183
+ }
3184
+ }
3185
+ }
3186
+ return map;
3187
+ }
3188
+ async function loadRequirementsMap() {
3189
+ const cache = await loadSubJson("jobs", "parsed.json");
3190
+ return cache?.requirements || {};
3191
+ }
3192
+
3193
+ // src/commands/history.ts
3194
+ import chalk6 from "chalk";
3195
+ var VALID_STATUSES = [
3196
+ "applied",
3197
+ "screening",
3198
+ "interview",
3199
+ "offer",
3200
+ "rejected",
3201
+ "withdrawn",
3202
+ "no-response"
3203
+ ];
3204
+ var STATUS_COLORS = {
3205
+ applied: chalk6.cyan,
3206
+ screening: chalk6.blue,
3207
+ interview: chalk6.green,
3208
+ offer: chalk6.bold.green,
3209
+ rejected: chalk6.red,
3210
+ withdrawn: chalk6.dim,
3211
+ "no-response": chalk6.yellow
3212
+ };
3213
+ async function historyCommand(action, id, options) {
3214
+ if (action === "update" && id) {
3215
+ await handleUpdate(id, options?.status, options?.notes);
3216
+ return;
3217
+ }
3218
+ await handleList();
3219
+ }
3220
+ async function handleList() {
3221
+ const history = await loadHistory();
3222
+ if (history.applications.length === 0) {
3223
+ info("\n No applications yet. Run `hiregraph apply <job-id>` to apply.\n");
3224
+ return;
3225
+ }
3226
+ header(`
3227
+ HireGraph Application History (${history.applications.length})
3228
+ `);
3229
+ const sorted = [...history.applications].sort(
3230
+ (a, b) => new Date(b.applied_at).getTime() - new Date(a.applied_at).getTime()
3231
+ );
3232
+ for (const app of sorted) {
3233
+ const colorFn = STATUS_COLORS[app.status] || chalk6.white;
3234
+ const age = getTimeAgo2(app.applied_at);
3235
+ console.log(
3236
+ ` ${chalk6.dim(app.id.padEnd(14))} ${app.job_title.padEnd(35)} @ ${app.company.padEnd(18)} ${colorFn(app.status.padEnd(12))} ${chalk6.dim(age)}`
3237
+ );
3238
+ if (app.notes) {
3239
+ console.log(` ${" ".repeat(14)} ${chalk6.dim(`Notes: ${app.notes}`)}`);
3240
+ }
3241
+ }
3242
+ console.log();
3243
+ }
3244
+ async function handleUpdate(id, status, notes) {
3245
+ if (!status) {
3246
+ error(`Status required. Valid: ${VALID_STATUSES.join(", ")}`);
3247
+ return;
3248
+ }
3249
+ if (!VALID_STATUSES.includes(status)) {
3250
+ error(`Invalid status "${status}". Valid: ${VALID_STATUSES.join(", ")}`);
3251
+ return;
3252
+ }
3253
+ const updated = await updateApplicationStatus(id, status, notes);
3254
+ if (!updated) {
3255
+ error(`Application not found: ${id}`);
3256
+ return;
3257
+ }
3258
+ const colorFn = STATUS_COLORS[updated.status] || chalk6.white;
3259
+ success(`Updated ${updated.id}: ${updated.job_title} @ ${updated.company} -> ${colorFn(updated.status)}`);
3260
+ }
3261
+ function getTimeAgo2(isoDate) {
3262
+ const diff = Date.now() - new Date(isoDate).getTime();
3263
+ const minutes = Math.floor(diff / 6e4);
3264
+ if (minutes < 60) return `${minutes}m ago`;
3265
+ const hours = Math.floor(minutes / 60);
3266
+ if (hours < 24) return `${hours}h ago`;
3267
+ const days = Math.floor(hours / 24);
3268
+ return `${days}d ago`;
3269
+ }
3270
+
3271
+ // src/index.ts
3272
+ 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.0");
3274
+ program.command("init").description("Set up your builder profile (resume upload + preferences)").action(initCommand);
3275
+ program.command("scan").description("Scan a project and update your skill graph").argument("[path]", "Path to the project directory", ".").action(scanCommand);
3276
+ program.command("status").description("Show your current skill graph summary").action(statusCommand);
3277
+ 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);
3278
+ program.command("matches").description("Match your skill graph against fetched jobs").option("--refresh", "Re-fetch jobs before matching").option("--top <n>", "Number of candidates for LLM evaluation (default 50)", parseInt).option("--verbose", "Show detailed reasoning for all matches").action(matchesCommand);
3279
+ program.command("apply").description("Generate a tailored resume and submit to ATS").argument("[job-id]", "Job ID to apply to").option("--all-above <score>", "Apply to all matches above this score", parseFloat).option("--review", "Review each resume before submitting").option("--dry-run", "Generate resume PDF without submitting").action(applyCommand);
3280
+ program.command("history").description("View and manage your application history").argument("[action]", 'Action: "update"').argument("[id]", "Application ID").option("--status <status>", "New status (applied, screening, interview, offer, rejected, withdrawn, no-response)").option("--notes <notes>", "Optional notes").action(historyCommand);
3281
+ program.parse();
3282
+ //# sourceMappingURL=index.js.map