@hasna/microservices 0.0.4 → 0.0.5
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/microservices/microservice-ads/src/cli/index.ts +198 -0
- package/microservices/microservice-ads/src/db/campaigns.ts +304 -0
- package/microservices/microservice-ads/src/mcp/index.ts +160 -0
- package/microservices/microservice-contracts/src/cli/index.ts +410 -23
- package/microservices/microservice-contracts/src/db/contracts.ts +430 -1
- package/microservices/microservice-contracts/src/db/migrations.ts +83 -0
- package/microservices/microservice-contracts/src/mcp/index.ts +312 -3
- package/microservices/microservice-domains/src/cli/index.ts +253 -0
- package/microservices/microservice-domains/src/db/domains.ts +613 -0
- package/microservices/microservice-domains/src/index.ts +21 -0
- package/microservices/microservice-domains/src/mcp/index.ts +168 -0
- package/microservices/microservice-hiring/src/cli/index.ts +318 -8
- package/microservices/microservice-hiring/src/db/hiring.ts +503 -0
- package/microservices/microservice-hiring/src/db/migrations.ts +21 -0
- package/microservices/microservice-hiring/src/index.ts +29 -0
- package/microservices/microservice-hiring/src/lib/scoring.ts +206 -0
- package/microservices/microservice-hiring/src/mcp/index.ts +245 -0
- package/microservices/microservice-payments/src/cli/index.ts +255 -3
- package/microservices/microservice-payments/src/db/migrations.ts +18 -0
- package/microservices/microservice-payments/src/db/payments.ts +552 -0
- package/microservices/microservice-payments/src/mcp/index.ts +223 -0
- package/microservices/microservice-payroll/src/cli/index.ts +269 -0
- package/microservices/microservice-payroll/src/db/migrations.ts +26 -0
- package/microservices/microservice-payroll/src/db/payroll.ts +636 -0
- package/microservices/microservice-payroll/src/mcp/index.ts +246 -0
- package/microservices/microservice-shipping/src/cli/index.ts +211 -3
- package/microservices/microservice-shipping/src/db/migrations.ts +8 -0
- package/microservices/microservice-shipping/src/db/shipping.ts +453 -3
- package/microservices/microservice-shipping/src/mcp/index.ts +149 -1
- package/microservices/microservice-social/src/cli/index.ts +244 -2
- package/microservices/microservice-social/src/db/migrations.ts +33 -0
- package/microservices/microservice-social/src/db/social.ts +378 -4
- package/microservices/microservice-social/src/mcp/index.ts +221 -1
- package/microservices/microservice-subscriptions/src/cli/index.ts +315 -0
- package/microservices/microservice-subscriptions/src/db/migrations.ts +68 -0
- package/microservices/microservice-subscriptions/src/db/subscriptions.ts +567 -3
- package/microservices/microservice-subscriptions/src/mcp/index.ts +267 -1
- package/package.json +1 -1
|
@@ -22,6 +22,15 @@ import {
|
|
|
22
22
|
createAlert,
|
|
23
23
|
listAlerts,
|
|
24
24
|
deleteAlert,
|
|
25
|
+
whoisLookup,
|
|
26
|
+
checkDnsPropagation,
|
|
27
|
+
checkSsl,
|
|
28
|
+
exportZoneFile,
|
|
29
|
+
importZoneFile,
|
|
30
|
+
discoverSubdomains,
|
|
31
|
+
validateDns,
|
|
32
|
+
exportPortfolio,
|
|
33
|
+
checkAllDomains,
|
|
25
34
|
} from "../db/domains.js";
|
|
26
35
|
|
|
27
36
|
const server = new McpServer({
|
|
@@ -355,6 +364,165 @@ server.registerTool(
|
|
|
355
364
|
}
|
|
356
365
|
);
|
|
357
366
|
|
|
367
|
+
// --- WHOIS Lookup ---
|
|
368
|
+
|
|
369
|
+
server.registerTool(
|
|
370
|
+
"whois_lookup",
|
|
371
|
+
{
|
|
372
|
+
title: "WHOIS Lookup",
|
|
373
|
+
description: "Run a WHOIS lookup for a domain. Parses registrar, expiry, nameservers from output. Updates DB record if found.",
|
|
374
|
+
inputSchema: { domain: z.string().describe("Domain name (e.g. example.com)") },
|
|
375
|
+
},
|
|
376
|
+
async ({ domain }) => {
|
|
377
|
+
try {
|
|
378
|
+
const result = whoisLookup(domain);
|
|
379
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
380
|
+
} catch (error: unknown) {
|
|
381
|
+
return {
|
|
382
|
+
content: [{ type: "text", text: `WHOIS lookup failed: ${error instanceof Error ? error.message : String(error)}` }],
|
|
383
|
+
isError: true,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
// --- DNS Propagation Check ---
|
|
390
|
+
|
|
391
|
+
server.registerTool(
|
|
392
|
+
"check_dns_propagation",
|
|
393
|
+
{
|
|
394
|
+
title: "Check DNS Propagation",
|
|
395
|
+
description: "Check DNS propagation by querying multiple DNS servers (Google, Cloudflare, Quad9, OpenDNS).",
|
|
396
|
+
inputSchema: {
|
|
397
|
+
domain: z.string().describe("Domain name to check"),
|
|
398
|
+
record_type: z.string().default("A").describe("DNS record type (A, AAAA, CNAME, MX, TXT, NS)"),
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
async ({ domain, record_type }) => {
|
|
402
|
+
const result = checkDnsPropagation(domain, record_type);
|
|
403
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
404
|
+
}
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
// --- SSL Check ---
|
|
408
|
+
|
|
409
|
+
server.registerTool(
|
|
410
|
+
"check_ssl",
|
|
411
|
+
{
|
|
412
|
+
title: "Check SSL Certificate",
|
|
413
|
+
description: "Check SSL certificate for a domain. Extracts issuer and expiry. Updates DB record if found.",
|
|
414
|
+
inputSchema: { domain: z.string().describe("Domain name (e.g. example.com)") },
|
|
415
|
+
},
|
|
416
|
+
async ({ domain }) => {
|
|
417
|
+
const result = checkSsl(domain);
|
|
418
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
// --- Zone File Export ---
|
|
423
|
+
|
|
424
|
+
server.registerTool(
|
|
425
|
+
"export_zone_file",
|
|
426
|
+
{
|
|
427
|
+
title: "Export Zone File",
|
|
428
|
+
description: "Export DNS records for a domain as a BIND-format zone file.",
|
|
429
|
+
inputSchema: { domain_id: z.string().describe("Domain ID") },
|
|
430
|
+
},
|
|
431
|
+
async ({ domain_id }) => {
|
|
432
|
+
const zone = exportZoneFile(domain_id);
|
|
433
|
+
if (!zone) {
|
|
434
|
+
return { content: [{ type: "text", text: `Domain '${domain_id}' not found.` }], isError: true };
|
|
435
|
+
}
|
|
436
|
+
return { content: [{ type: "text", text: zone }] };
|
|
437
|
+
}
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
// --- Zone File Import ---
|
|
441
|
+
|
|
442
|
+
server.registerTool(
|
|
443
|
+
"import_zone_file",
|
|
444
|
+
{
|
|
445
|
+
title: "Import Zone File",
|
|
446
|
+
description: "Import DNS records from BIND zone file content into a domain.",
|
|
447
|
+
inputSchema: {
|
|
448
|
+
domain_id: z.string().describe("Domain ID"),
|
|
449
|
+
content: z.string().describe("Zone file content"),
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
async ({ domain_id, content }) => {
|
|
453
|
+
const result = importZoneFile(domain_id, content);
|
|
454
|
+
if (!result) {
|
|
455
|
+
return { content: [{ type: "text", text: `Domain '${domain_id}' not found.` }], isError: true };
|
|
456
|
+
}
|
|
457
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
// --- Subdomain Discovery ---
|
|
462
|
+
|
|
463
|
+
server.registerTool(
|
|
464
|
+
"discover_subdomains",
|
|
465
|
+
{
|
|
466
|
+
title: "Discover Subdomains",
|
|
467
|
+
description: "Discover subdomains via certificate transparency logs (crt.sh).",
|
|
468
|
+
inputSchema: { domain: z.string().describe("Domain name") },
|
|
469
|
+
},
|
|
470
|
+
async ({ domain }) => {
|
|
471
|
+
const result = await discoverSubdomains(domain);
|
|
472
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
473
|
+
}
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
// --- DNS Validation ---
|
|
477
|
+
|
|
478
|
+
server.registerTool(
|
|
479
|
+
"validate_dns",
|
|
480
|
+
{
|
|
481
|
+
title: "Validate DNS",
|
|
482
|
+
description: "Validate DNS records for common issues (CNAME conflicts, missing MX, orphan records).",
|
|
483
|
+
inputSchema: { domain_id: z.string().describe("Domain ID") },
|
|
484
|
+
},
|
|
485
|
+
async ({ domain_id }) => {
|
|
486
|
+
const result = validateDns(domain_id);
|
|
487
|
+
if (!result) {
|
|
488
|
+
return { content: [{ type: "text", text: `Domain '${domain_id}' not found.` }], isError: true };
|
|
489
|
+
}
|
|
490
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
// --- Portfolio Export ---
|
|
495
|
+
|
|
496
|
+
server.registerTool(
|
|
497
|
+
"export_portfolio",
|
|
498
|
+
{
|
|
499
|
+
title: "Export Portfolio",
|
|
500
|
+
description: "Export all domains as CSV or JSON with expiry, SSL, registrar, and auto-renew info.",
|
|
501
|
+
inputSchema: {
|
|
502
|
+
format: z.enum(["csv", "json"]).default("json").describe("Export format"),
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
async ({ format }) => {
|
|
506
|
+
const output = exportPortfolio(format);
|
|
507
|
+
return { content: [{ type: "text", text: output }] };
|
|
508
|
+
}
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
// --- Bulk Domain Check ---
|
|
512
|
+
|
|
513
|
+
server.registerTool(
|
|
514
|
+
"check_all_domains",
|
|
515
|
+
{
|
|
516
|
+
title: "Check All Domains",
|
|
517
|
+
description: "Run WHOIS + SSL + DNS validation on all domains. Returns a summary of issues found.",
|
|
518
|
+
inputSchema: {},
|
|
519
|
+
},
|
|
520
|
+
async () => {
|
|
521
|
+
const results = checkAllDomains();
|
|
522
|
+
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
523
|
+
}
|
|
524
|
+
);
|
|
525
|
+
|
|
358
526
|
// --- Start ---
|
|
359
527
|
async function main() {
|
|
360
528
|
const transport = new StdioServerTransport();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
4
5
|
import {
|
|
5
6
|
createJob,
|
|
6
7
|
getJob,
|
|
@@ -17,7 +18,17 @@ import {
|
|
|
17
18
|
createInterview,
|
|
18
19
|
listInterviews,
|
|
19
20
|
addInterviewFeedback,
|
|
21
|
+
bulkImportApplicants,
|
|
22
|
+
generateOffer,
|
|
23
|
+
getHiringForecast,
|
|
24
|
+
submitStructuredFeedback,
|
|
25
|
+
bulkReject,
|
|
26
|
+
getReferralStats,
|
|
27
|
+
saveJobAsTemplate,
|
|
28
|
+
createJobFromTemplate,
|
|
29
|
+
listJobTemplates,
|
|
20
30
|
} from "../db/hiring.js";
|
|
31
|
+
import { scoreApplicant, rankApplicants } from "../lib/scoring.js";
|
|
21
32
|
|
|
22
33
|
const program = new Command();
|
|
23
34
|
|
|
@@ -291,6 +302,142 @@ applicantCmd
|
|
|
291
302
|
}
|
|
292
303
|
});
|
|
293
304
|
|
|
305
|
+
// --- Bulk Import ---
|
|
306
|
+
|
|
307
|
+
applicantCmd
|
|
308
|
+
.command("bulk-import")
|
|
309
|
+
.description("Bulk import applicants from a CSV file")
|
|
310
|
+
.requiredOption("--file <path>", "Path to CSV file (name,email,phone,job_id,source,resume_url)")
|
|
311
|
+
.option("--json", "Output as JSON", false)
|
|
312
|
+
.action((opts) => {
|
|
313
|
+
const csvData = readFileSync(opts.file, "utf-8");
|
|
314
|
+
const result = bulkImportApplicants(csvData);
|
|
315
|
+
|
|
316
|
+
if (opts.json) {
|
|
317
|
+
console.log(JSON.stringify(result, null, 2));
|
|
318
|
+
} else {
|
|
319
|
+
console.log(`Imported: ${result.imported}`);
|
|
320
|
+
console.log(`Skipped: ${result.skipped}`);
|
|
321
|
+
if (result.errors.length > 0) {
|
|
322
|
+
console.log("Errors:");
|
|
323
|
+
for (const e of result.errors) {
|
|
324
|
+
console.log(` - ${e}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// --- AI Scoring ---
|
|
331
|
+
|
|
332
|
+
applicantCmd
|
|
333
|
+
.command("score")
|
|
334
|
+
.description("AI-score an applicant against job requirements")
|
|
335
|
+
.argument("<id>", "Applicant ID")
|
|
336
|
+
.option("--json", "Output as JSON", false)
|
|
337
|
+
.action(async (id, opts) => {
|
|
338
|
+
try {
|
|
339
|
+
const score = await scoreApplicant(id);
|
|
340
|
+
|
|
341
|
+
if (opts.json) {
|
|
342
|
+
console.log(JSON.stringify(score, null, 2));
|
|
343
|
+
} else {
|
|
344
|
+
console.log(`Match: ${score.match_pct}%`);
|
|
345
|
+
console.log(`Recommendation: ${score.recommendation}`);
|
|
346
|
+
if (score.strengths.length) console.log(`Strengths: ${score.strengths.join(", ")}`);
|
|
347
|
+
if (score.gaps.length) console.log(`Gaps: ${score.gaps.join(", ")}`);
|
|
348
|
+
}
|
|
349
|
+
} catch (err) {
|
|
350
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// --- AI Bulk Ranking ---
|
|
356
|
+
|
|
357
|
+
applicantCmd
|
|
358
|
+
.command("rank")
|
|
359
|
+
.description("AI-rank all applicants for a job by fit score")
|
|
360
|
+
.requiredOption("--job <id>", "Job ID")
|
|
361
|
+
.option("--json", "Output as JSON", false)
|
|
362
|
+
.action(async (opts) => {
|
|
363
|
+
try {
|
|
364
|
+
const ranked = await rankApplicants(opts.job);
|
|
365
|
+
|
|
366
|
+
if (opts.json) {
|
|
367
|
+
console.log(JSON.stringify(ranked, null, 2));
|
|
368
|
+
} else {
|
|
369
|
+
if (ranked.length === 0) {
|
|
370
|
+
console.log("No applicants to rank.");
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
console.log("Ranking:");
|
|
374
|
+
for (let i = 0; i < ranked.length; i++) {
|
|
375
|
+
const { applicant, score } = ranked[i];
|
|
376
|
+
console.log(` ${i + 1}. ${applicant.name} — ${score.match_pct}% (${score.recommendation})`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} catch (err) {
|
|
380
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// --- Offer Letter ---
|
|
386
|
+
|
|
387
|
+
applicantCmd
|
|
388
|
+
.command("offer")
|
|
389
|
+
.description("Generate a Markdown offer letter")
|
|
390
|
+
.argument("<id>", "Applicant ID")
|
|
391
|
+
.requiredOption("--salary <amount>", "Annual salary")
|
|
392
|
+
.requiredOption("--start-date <date>", "Start date (YYYY-MM-DD)")
|
|
393
|
+
.option("--title <title>", "Position title override")
|
|
394
|
+
.option("--department <dept>", "Department override")
|
|
395
|
+
.option("--benefits <text>", "Benefits description")
|
|
396
|
+
.option("--equity <text>", "Equity details")
|
|
397
|
+
.option("--signing-bonus <amount>", "Signing bonus")
|
|
398
|
+
.option("--json", "Output as JSON", false)
|
|
399
|
+
.action((id, opts) => {
|
|
400
|
+
try {
|
|
401
|
+
const letter = generateOffer(id, {
|
|
402
|
+
salary: parseInt(opts.salary),
|
|
403
|
+
start_date: opts.startDate,
|
|
404
|
+
position_title: opts.title,
|
|
405
|
+
department: opts.department,
|
|
406
|
+
benefits: opts.benefits,
|
|
407
|
+
equity: opts.equity,
|
|
408
|
+
signing_bonus: opts.signingBonus ? parseInt(opts.signingBonus) : undefined,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
if (opts.json) {
|
|
412
|
+
console.log(JSON.stringify({ offer_letter: letter }, null, 2));
|
|
413
|
+
} else {
|
|
414
|
+
console.log(letter);
|
|
415
|
+
}
|
|
416
|
+
} catch (err) {
|
|
417
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// --- Bulk Rejection ---
|
|
423
|
+
|
|
424
|
+
applicantCmd
|
|
425
|
+
.command("reject-batch")
|
|
426
|
+
.description("Bulk reject applicants for a job by status")
|
|
427
|
+
.requiredOption("--job <id>", "Job ID")
|
|
428
|
+
.requiredOption("--status <status>", "Status to reject (applied/screening/etc.)")
|
|
429
|
+
.option("--reason <reason>", "Rejection reason")
|
|
430
|
+
.option("--json", "Output as JSON", false)
|
|
431
|
+
.action((opts) => {
|
|
432
|
+
const result = bulkReject(opts.job, opts.status, opts.reason);
|
|
433
|
+
|
|
434
|
+
if (opts.json) {
|
|
435
|
+
console.log(JSON.stringify(result, null, 2));
|
|
436
|
+
} else {
|
|
437
|
+
console.log(`Rejected ${result.rejected} applicant(s)`);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
294
441
|
// --- Interviews ---
|
|
295
442
|
|
|
296
443
|
const interviewCmd = program
|
|
@@ -356,17 +503,46 @@ interviewCmd
|
|
|
356
503
|
|
|
357
504
|
interviewCmd
|
|
358
505
|
.command("feedback")
|
|
359
|
-
.description("Add feedback to an interview")
|
|
506
|
+
.description("Add feedback to an interview (supports structured scoring dimensions)")
|
|
360
507
|
.argument("<id>", "Interview ID")
|
|
361
|
-
.
|
|
362
|
-
.option("--rating <n>", "
|
|
508
|
+
.option("--feedback <text>", "Feedback text")
|
|
509
|
+
.option("--rating <n>", "Overall rating (1-5)")
|
|
510
|
+
.option("--technical <n>", "Technical score (1-5)")
|
|
511
|
+
.option("--communication <n>", "Communication score (1-5)")
|
|
512
|
+
.option("--culture-fit <n>", "Culture fit score (1-5)")
|
|
513
|
+
.option("--problem-solving <n>", "Problem solving score (1-5)")
|
|
514
|
+
.option("--leadership <n>", "Leadership score (1-5)")
|
|
363
515
|
.option("--json", "Output as JSON", false)
|
|
364
516
|
.action((id, opts) => {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
)
|
|
517
|
+
const hasStructured = opts.technical || opts.communication || opts.cultureFit ||
|
|
518
|
+
opts.problemSolving || opts.leadership;
|
|
519
|
+
|
|
520
|
+
let interview;
|
|
521
|
+
if (hasStructured) {
|
|
522
|
+
interview = submitStructuredFeedback(
|
|
523
|
+
id,
|
|
524
|
+
{
|
|
525
|
+
technical: opts.technical ? parseInt(opts.technical) : undefined,
|
|
526
|
+
communication: opts.communication ? parseInt(opts.communication) : undefined,
|
|
527
|
+
culture_fit: opts.cultureFit ? parseInt(opts.cultureFit) : undefined,
|
|
528
|
+
problem_solving: opts.problemSolving ? parseInt(opts.problemSolving) : undefined,
|
|
529
|
+
leadership: opts.leadership ? parseInt(opts.leadership) : undefined,
|
|
530
|
+
overall: opts.rating ? parseInt(opts.rating) : undefined,
|
|
531
|
+
},
|
|
532
|
+
opts.feedback
|
|
533
|
+
);
|
|
534
|
+
} else {
|
|
535
|
+
if (!opts.feedback) {
|
|
536
|
+
console.error("Either --feedback or structured scores (--technical, --communication, etc.) are required.");
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
interview = addInterviewFeedback(
|
|
540
|
+
id,
|
|
541
|
+
opts.feedback,
|
|
542
|
+
opts.rating ? parseInt(opts.rating) : undefined
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
370
546
|
if (!interview) {
|
|
371
547
|
console.error(`Interview '${id}' not found.`);
|
|
372
548
|
process.exit(1);
|
|
@@ -428,4 +604,138 @@ program
|
|
|
428
604
|
}
|
|
429
605
|
});
|
|
430
606
|
|
|
607
|
+
// --- Referral Stats ---
|
|
608
|
+
|
|
609
|
+
const statsCmd = program
|
|
610
|
+
.command("stats-referrals")
|
|
611
|
+
.description("Show referral/source conversion rates")
|
|
612
|
+
.option("--json", "Output as JSON", false)
|
|
613
|
+
.action((opts) => {
|
|
614
|
+
const stats = getReferralStats();
|
|
615
|
+
|
|
616
|
+
if (opts.json) {
|
|
617
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
618
|
+
} else {
|
|
619
|
+
if (stats.length === 0) {
|
|
620
|
+
console.log("No applicant source data.");
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
console.log("Referral Stats:");
|
|
624
|
+
for (const s of stats) {
|
|
625
|
+
console.log(` ${s.source}: ${s.total} total, ${s.hired} hired, ${s.conversion_rate}% conversion`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// --- Forecast ---
|
|
631
|
+
|
|
632
|
+
program
|
|
633
|
+
.command("forecast")
|
|
634
|
+
.description("Estimate days-to-fill based on pipeline velocity")
|
|
635
|
+
.argument("<job-id>", "Job ID")
|
|
636
|
+
.option("--json", "Output as JSON", false)
|
|
637
|
+
.action((jobId, opts) => {
|
|
638
|
+
try {
|
|
639
|
+
const forecast = getHiringForecast(jobId);
|
|
640
|
+
|
|
641
|
+
if (opts.json) {
|
|
642
|
+
console.log(JSON.stringify(forecast, null, 2));
|
|
643
|
+
} else {
|
|
644
|
+
console.log(`Forecast for: ${forecast.job_title}`);
|
|
645
|
+
console.log(` Total applicants: ${forecast.total_applicants}`);
|
|
646
|
+
console.log(` Estimated days to fill: ${forecast.estimated_days_to_fill ?? "N/A"}`);
|
|
647
|
+
|
|
648
|
+
if (Object.keys(forecast.avg_days_per_stage).length) {
|
|
649
|
+
console.log(" Avg days per transition:");
|
|
650
|
+
for (const [stage, days] of Object.entries(forecast.avg_days_per_stage)) {
|
|
651
|
+
console.log(` ${stage}: ${days} days`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (Object.keys(forecast.conversion_rates).length) {
|
|
656
|
+
console.log(" Conversion rates:");
|
|
657
|
+
for (const [stage, rate] of Object.entries(forecast.conversion_rates)) {
|
|
658
|
+
console.log(` ${stage}: ${rate}%`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
} catch (err) {
|
|
663
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// --- Job Templates ---
|
|
669
|
+
|
|
670
|
+
jobCmd
|
|
671
|
+
.command("save-template")
|
|
672
|
+
.description("Save a job as a reusable template")
|
|
673
|
+
.argument("<id>", "Job ID")
|
|
674
|
+
.requiredOption("--name <name>", "Template name")
|
|
675
|
+
.option("--json", "Output as JSON", false)
|
|
676
|
+
.action((id, opts) => {
|
|
677
|
+
try {
|
|
678
|
+
const template = saveJobAsTemplate(id, opts.name);
|
|
679
|
+
|
|
680
|
+
if (opts.json) {
|
|
681
|
+
console.log(JSON.stringify(template, null, 2));
|
|
682
|
+
} else {
|
|
683
|
+
console.log(`Saved template: ${template.name} (${template.id})`);
|
|
684
|
+
}
|
|
685
|
+
} catch (err) {
|
|
686
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
jobCmd
|
|
692
|
+
.command("from-template")
|
|
693
|
+
.description("Create a job from a template")
|
|
694
|
+
.requiredOption("--template <name>", "Template name")
|
|
695
|
+
.option("--title <title>", "Override title")
|
|
696
|
+
.option("--department <dept>", "Override department")
|
|
697
|
+
.option("--location <loc>", "Override location")
|
|
698
|
+
.option("--salary-range <range>", "Override salary range")
|
|
699
|
+
.option("--json", "Output as JSON", false)
|
|
700
|
+
.action((opts) => {
|
|
701
|
+
try {
|
|
702
|
+
const job = createJobFromTemplate(opts.template, {
|
|
703
|
+
title: opts.title,
|
|
704
|
+
department: opts.department,
|
|
705
|
+
location: opts.location,
|
|
706
|
+
salary_range: opts.salaryRange,
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
if (opts.json) {
|
|
710
|
+
console.log(JSON.stringify(job, null, 2));
|
|
711
|
+
} else {
|
|
712
|
+
console.log(`Created job from template: ${job.title} (${job.id})`);
|
|
713
|
+
}
|
|
714
|
+
} catch (err) {
|
|
715
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
jobCmd
|
|
721
|
+
.command("templates")
|
|
722
|
+
.description("List all job templates")
|
|
723
|
+
.option("--json", "Output as JSON", false)
|
|
724
|
+
.action((opts) => {
|
|
725
|
+
const templates = listJobTemplates();
|
|
726
|
+
|
|
727
|
+
if (opts.json) {
|
|
728
|
+
console.log(JSON.stringify(templates, null, 2));
|
|
729
|
+
} else {
|
|
730
|
+
if (templates.length === 0) {
|
|
731
|
+
console.log("No templates found.");
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
for (const t of templates) {
|
|
735
|
+
console.log(` ${t.name} — ${t.title} (${t.id})`);
|
|
736
|
+
}
|
|
737
|
+
console.log(`\n${templates.length} template(s)`);
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
|
|
431
741
|
program.parse(process.argv);
|