@hasna/microservices 0.0.4 → 0.0.6

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.
Files changed (57) hide show
  1. package/bin/index.js +9 -1
  2. package/bin/mcp.js +9 -1
  3. package/dist/index.js +9 -1
  4. package/microservices/microservice-ads/src/cli/index.ts +198 -0
  5. package/microservices/microservice-ads/src/db/campaigns.ts +304 -0
  6. package/microservices/microservice-ads/src/mcp/index.ts +160 -0
  7. package/microservices/microservice-company/package.json +27 -0
  8. package/microservices/microservice-company/src/cli/index.ts +1126 -0
  9. package/microservices/microservice-company/src/db/company.ts +854 -0
  10. package/microservices/microservice-company/src/db/database.ts +93 -0
  11. package/microservices/microservice-company/src/db/migrations.ts +214 -0
  12. package/microservices/microservice-company/src/db/workflow-migrations.ts +44 -0
  13. package/microservices/microservice-company/src/index.ts +60 -0
  14. package/microservices/microservice-company/src/lib/audit.ts +168 -0
  15. package/microservices/microservice-company/src/lib/finance.ts +299 -0
  16. package/microservices/microservice-company/src/lib/settings.ts +85 -0
  17. package/microservices/microservice-company/src/lib/workflows.ts +698 -0
  18. package/microservices/microservice-company/src/mcp/index.ts +991 -0
  19. package/microservices/microservice-contracts/src/cli/index.ts +410 -23
  20. package/microservices/microservice-contracts/src/db/contracts.ts +430 -1
  21. package/microservices/microservice-contracts/src/db/migrations.ts +83 -0
  22. package/microservices/microservice-contracts/src/mcp/index.ts +312 -3
  23. package/microservices/microservice-domains/src/cli/index.ts +673 -0
  24. package/microservices/microservice-domains/src/db/domains.ts +613 -0
  25. package/microservices/microservice-domains/src/index.ts +21 -0
  26. package/microservices/microservice-domains/src/lib/brandsight.ts +285 -0
  27. package/microservices/microservice-domains/src/lib/godaddy.ts +328 -0
  28. package/microservices/microservice-domains/src/lib/namecheap.ts +474 -0
  29. package/microservices/microservice-domains/src/lib/registrar.ts +355 -0
  30. package/microservices/microservice-domains/src/mcp/index.ts +413 -0
  31. package/microservices/microservice-hiring/src/cli/index.ts +318 -8
  32. package/microservices/microservice-hiring/src/db/hiring.ts +503 -0
  33. package/microservices/microservice-hiring/src/db/migrations.ts +21 -0
  34. package/microservices/microservice-hiring/src/index.ts +29 -0
  35. package/microservices/microservice-hiring/src/lib/scoring.ts +206 -0
  36. package/microservices/microservice-hiring/src/mcp/index.ts +245 -0
  37. package/microservices/microservice-payments/src/cli/index.ts +255 -3
  38. package/microservices/microservice-payments/src/db/migrations.ts +18 -0
  39. package/microservices/microservice-payments/src/db/payments.ts +552 -0
  40. package/microservices/microservice-payments/src/mcp/index.ts +223 -0
  41. package/microservices/microservice-payroll/src/cli/index.ts +269 -0
  42. package/microservices/microservice-payroll/src/db/migrations.ts +26 -0
  43. package/microservices/microservice-payroll/src/db/payroll.ts +636 -0
  44. package/microservices/microservice-payroll/src/mcp/index.ts +246 -0
  45. package/microservices/microservice-shipping/src/cli/index.ts +211 -3
  46. package/microservices/microservice-shipping/src/db/migrations.ts +8 -0
  47. package/microservices/microservice-shipping/src/db/shipping.ts +453 -3
  48. package/microservices/microservice-shipping/src/mcp/index.ts +149 -1
  49. package/microservices/microservice-social/src/cli/index.ts +244 -2
  50. package/microservices/microservice-social/src/db/migrations.ts +33 -0
  51. package/microservices/microservice-social/src/db/social.ts +378 -4
  52. package/microservices/microservice-social/src/mcp/index.ts +221 -1
  53. package/microservices/microservice-subscriptions/src/cli/index.ts +315 -0
  54. package/microservices/microservice-subscriptions/src/db/migrations.ts +68 -0
  55. package/microservices/microservice-subscriptions/src/db/subscriptions.ts +567 -3
  56. package/microservices/microservice-subscriptions/src/mcp/index.ts +267 -1
  57. package/package.json +1 -1
@@ -0,0 +1,206 @@
1
+ /**
2
+ * AI resume scoring — uses OpenAI or Anthropic to evaluate applicant fit
3
+ */
4
+
5
+ import { getApplicant, getJob, updateApplicant } from "../db/hiring.js";
6
+ import type { Applicant, Job } from "../db/hiring.js";
7
+
8
+ export interface ScoreResult {
9
+ match_pct: number;
10
+ strengths: string[];
11
+ gaps: string[];
12
+ recommendation: string;
13
+ }
14
+
15
+ export interface RankEntry {
16
+ applicant: Applicant;
17
+ score: ScoreResult;
18
+ }
19
+
20
+ // ---- Provider abstraction ----
21
+
22
+ async function callAI(prompt: string): Promise<string> {
23
+ // Try Anthropic first, then OpenAI
24
+ const anthropicKey = process.env["ANTHROPIC_API_KEY"];
25
+ const openaiKey = process.env["OPENAI_API_KEY"];
26
+
27
+ if (anthropicKey) {
28
+ return callAnthropic(anthropicKey, prompt);
29
+ }
30
+ if (openaiKey) {
31
+ return callOpenAI(openaiKey, prompt);
32
+ }
33
+
34
+ throw new Error(
35
+ "No AI API key found. Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable."
36
+ );
37
+ }
38
+
39
+ async function callAnthropic(apiKey: string, prompt: string): Promise<string> {
40
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
41
+ method: "POST",
42
+ headers: {
43
+ "Content-Type": "application/json",
44
+ "x-api-key": apiKey,
45
+ "anthropic-version": "2023-06-01",
46
+ },
47
+ body: JSON.stringify({
48
+ model: "claude-sonnet-4-20250514",
49
+ max_tokens: 1024,
50
+ messages: [{ role: "user", content: prompt }],
51
+ }),
52
+ });
53
+
54
+ if (!response.ok) {
55
+ const text = await response.text();
56
+ throw new Error(`Anthropic API error ${response.status}: ${text}`);
57
+ }
58
+
59
+ const data = (await response.json()) as {
60
+ content: Array<{ type: string; text: string }>;
61
+ };
62
+ return data.content[0]?.text || "";
63
+ }
64
+
65
+ async function callOpenAI(apiKey: string, prompt: string): Promise<string> {
66
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
67
+ method: "POST",
68
+ headers: {
69
+ "Content-Type": "application/json",
70
+ Authorization: `Bearer ${apiKey}`,
71
+ },
72
+ body: JSON.stringify({
73
+ model: "gpt-4o-mini",
74
+ messages: [{ role: "user", content: prompt }],
75
+ max_tokens: 1024,
76
+ }),
77
+ });
78
+
79
+ if (!response.ok) {
80
+ const text = await response.text();
81
+ throw new Error(`OpenAI API error ${response.status}: ${text}`);
82
+ }
83
+
84
+ const data = (await response.json()) as {
85
+ choices: Array<{ message: { content: string } }>;
86
+ };
87
+ return data.choices[0]?.message?.content || "";
88
+ }
89
+
90
+ // ---- Scoring ----
91
+
92
+ function buildScoringPrompt(job: Job, applicant: Applicant): string {
93
+ const requirements = job.requirements.length
94
+ ? job.requirements.join(", ")
95
+ : "No specific requirements listed";
96
+
97
+ const applicantInfo = [
98
+ `Name: ${applicant.name}`,
99
+ applicant.resume_url ? `Resume: ${applicant.resume_url}` : null,
100
+ applicant.notes ? `Notes/Summary: ${applicant.notes}` : null,
101
+ applicant.source ? `Source: ${applicant.source}` : null,
102
+ applicant.stage ? `Current stage: ${applicant.stage}` : null,
103
+ Object.keys(applicant.metadata).length > 0
104
+ ? `Additional info: ${JSON.stringify(applicant.metadata)}`
105
+ : null,
106
+ ]
107
+ .filter(Boolean)
108
+ .join("\n");
109
+
110
+ return `You are an expert hiring evaluator. Analyze this applicant against the job requirements and return a JSON assessment.
111
+
112
+ JOB:
113
+ Title: ${job.title}
114
+ Department: ${job.department || "N/A"}
115
+ Description: ${job.description || "N/A"}
116
+ Requirements: ${requirements}
117
+ Salary Range: ${job.salary_range || "N/A"}
118
+
119
+ APPLICANT:
120
+ ${applicantInfo}
121
+
122
+ Return ONLY a valid JSON object (no markdown, no code fences) with this exact structure:
123
+ {
124
+ "match_pct": <number 0-100>,
125
+ "strengths": ["strength1", "strength2"],
126
+ "gaps": ["gap1", "gap2"],
127
+ "recommendation": "<hire/strong_hire/no_hire/maybe> — brief explanation"
128
+ }`;
129
+ }
130
+
131
+ function parseScoreResponse(text: string): ScoreResult {
132
+ // Strip markdown code fences if present
133
+ let cleaned = text.trim();
134
+ if (cleaned.startsWith("```")) {
135
+ cleaned = cleaned.replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "");
136
+ }
137
+
138
+ try {
139
+ const parsed = JSON.parse(cleaned);
140
+ return {
141
+ match_pct: Math.max(0, Math.min(100, Number(parsed.match_pct) || 0)),
142
+ strengths: Array.isArray(parsed.strengths) ? parsed.strengths : [],
143
+ gaps: Array.isArray(parsed.gaps) ? parsed.gaps : [],
144
+ recommendation: String(parsed.recommendation || "Unable to determine"),
145
+ };
146
+ } catch {
147
+ return {
148
+ match_pct: 0,
149
+ strengths: [],
150
+ gaps: [],
151
+ recommendation: `AI response could not be parsed: ${text.slice(0, 200)}`,
152
+ };
153
+ }
154
+ }
155
+
156
+ export async function scoreApplicant(applicantId: string): Promise<ScoreResult> {
157
+ const applicant = getApplicant(applicantId);
158
+ if (!applicant) throw new Error(`Applicant '${applicantId}' not found`);
159
+
160
+ const job = getJob(applicant.job_id);
161
+ if (!job) throw new Error(`Job '${applicant.job_id}' not found`);
162
+
163
+ const prompt = buildScoringPrompt(job, applicant);
164
+ const response = await callAI(prompt);
165
+ const score = parseScoreResponse(response);
166
+
167
+ // Store in applicant metadata
168
+ const metadata = { ...applicant.metadata, ai_score: score, scored_at: new Date().toISOString() };
169
+ updateApplicant(applicantId, { metadata });
170
+
171
+ return score;
172
+ }
173
+
174
+ export async function rankApplicants(jobId: string): Promise<RankEntry[]> {
175
+ const job = getJob(jobId);
176
+ if (!job) throw new Error(`Job '${jobId}' not found`);
177
+
178
+ const applicants = (await import("../db/hiring.js")).listApplicants({ job_id: jobId });
179
+
180
+ const results: RankEntry[] = [];
181
+
182
+ for (const applicant of applicants) {
183
+ // Use cached score if available and recent (less than 24h old)
184
+ const cached = applicant.metadata?.ai_score as ScoreResult | undefined;
185
+ const cachedAt = applicant.metadata?.scored_at as string | undefined;
186
+ const isFresh =
187
+ cachedAt && Date.now() - new Date(cachedAt).getTime() < 24 * 60 * 60 * 1000;
188
+
189
+ if (cached && isFresh) {
190
+ results.push({ applicant, score: cached });
191
+ } else {
192
+ const score = await scoreApplicant(applicant.id);
193
+ // Re-fetch to get updated metadata
194
+ const updated = getApplicant(applicant.id)!;
195
+ results.push({ applicant: updated, score });
196
+ }
197
+ }
198
+
199
+ // Sort by match_pct descending
200
+ results.sort((a, b) => b.score.match_pct - a.score.match_pct);
201
+
202
+ return results;
203
+ }
204
+
205
+ // Exported for testing
206
+ export { buildScoringPrompt, parseScoreResponse, callAI };
@@ -26,7 +26,18 @@ import {
26
26
  updateInterview,
27
27
  addInterviewFeedback,
28
28
  deleteInterview,
29
+ bulkImportApplicants,
30
+ generateOffer,
31
+ getHiringForecast,
32
+ submitStructuredFeedback,
33
+ bulkReject,
34
+ getReferralStats,
35
+ saveJobAsTemplate,
36
+ createJobFromTemplate,
37
+ listJobTemplates,
38
+ deleteJobTemplate,
29
39
  } from "../db/hiring.js";
40
+ import { scoreApplicant, rankApplicants } from "../lib/scoring.js";
30
41
 
31
42
  const server = new McpServer({
32
43
  name: "microservice-hiring",
@@ -451,6 +462,240 @@ server.registerTool(
451
462
  }
452
463
  );
453
464
 
465
+ // --- Bulk Import ---
466
+
467
+ server.registerTool(
468
+ "bulk_import_applicants",
469
+ {
470
+ title: "Bulk Import Applicants",
471
+ description: "Import applicants from CSV data (columns: name,email,phone,job_id,source,resume_url).",
472
+ inputSchema: {
473
+ csv_data: z.string().describe("CSV string with header row"),
474
+ },
475
+ },
476
+ async ({ csv_data }) => {
477
+ const result = bulkImportApplicants(csv_data);
478
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
479
+ }
480
+ );
481
+
482
+ // --- AI Scoring ---
483
+
484
+ server.registerTool(
485
+ "score_applicant",
486
+ {
487
+ title: "Score Applicant",
488
+ description: "AI-score an applicant against job requirements. Returns match percentage, strengths, gaps, and recommendation.",
489
+ inputSchema: { id: z.string() },
490
+ },
491
+ async ({ id }) => {
492
+ try {
493
+ const score = await scoreApplicant(id);
494
+ return { content: [{ type: "text", text: JSON.stringify(score, null, 2) }] };
495
+ } catch (err) {
496
+ return { content: [{ type: "text", text: String(err instanceof Error ? err.message : err) }], isError: true };
497
+ }
498
+ }
499
+ );
500
+
501
+ server.registerTool(
502
+ "rank_applicants",
503
+ {
504
+ title: "Rank Applicants",
505
+ description: "AI-rank all applicants for a job by fit score, sorted best-first.",
506
+ inputSchema: { job_id: z.string() },
507
+ },
508
+ async ({ job_id }) => {
509
+ try {
510
+ const ranked = await rankApplicants(job_id);
511
+ return { content: [{ type: "text", text: JSON.stringify(ranked, null, 2) }] };
512
+ } catch (err) {
513
+ return { content: [{ type: "text", text: String(err instanceof Error ? err.message : err) }], isError: true };
514
+ }
515
+ }
516
+ );
517
+
518
+ // --- Offer Letter ---
519
+
520
+ server.registerTool(
521
+ "generate_offer",
522
+ {
523
+ title: "Generate Offer Letter",
524
+ description: "Generate a Markdown offer letter for an applicant.",
525
+ inputSchema: {
526
+ id: z.string().describe("Applicant ID"),
527
+ salary: z.number().describe("Annual salary"),
528
+ start_date: z.string().describe("Start date (YYYY-MM-DD)"),
529
+ position_title: z.string().optional(),
530
+ department: z.string().optional(),
531
+ benefits: z.string().optional(),
532
+ equity: z.string().optional(),
533
+ signing_bonus: z.number().optional(),
534
+ },
535
+ },
536
+ async ({ id, salary, start_date, ...rest }) => {
537
+ try {
538
+ const letter = generateOffer(id, { salary, start_date, ...rest });
539
+ return { content: [{ type: "text", text: letter }] };
540
+ } catch (err) {
541
+ return { content: [{ type: "text", text: String(err instanceof Error ? err.message : err) }], isError: true };
542
+ }
543
+ }
544
+ );
545
+
546
+ // --- Pipeline Velocity / Forecast ---
547
+
548
+ server.registerTool(
549
+ "hiring_forecast",
550
+ {
551
+ title: "Hiring Forecast",
552
+ description: "Estimate days-to-fill based on average time between pipeline stages.",
553
+ inputSchema: { job_id: z.string() },
554
+ },
555
+ async ({ job_id }) => {
556
+ try {
557
+ const forecast = getHiringForecast(job_id);
558
+ return { content: [{ type: "text", text: JSON.stringify(forecast, null, 2) }] };
559
+ } catch (err) {
560
+ return { content: [{ type: "text", text: String(err instanceof Error ? err.message : err) }], isError: true };
561
+ }
562
+ }
563
+ );
564
+
565
+ // --- Structured Feedback ---
566
+
567
+ server.registerTool(
568
+ "submit_structured_feedback",
569
+ {
570
+ title: "Submit Structured Interview Feedback",
571
+ description: "Submit scored interview feedback with dimensions (technical, communication, culture_fit, etc.).",
572
+ inputSchema: {
573
+ id: z.string().describe("Interview ID"),
574
+ feedback_text: z.string().optional(),
575
+ technical: z.number().min(1).max(5).optional(),
576
+ communication: z.number().min(1).max(5).optional(),
577
+ culture_fit: z.number().min(1).max(5).optional(),
578
+ problem_solving: z.number().min(1).max(5).optional(),
579
+ leadership: z.number().min(1).max(5).optional(),
580
+ overall: z.number().min(1).max(5).optional(),
581
+ },
582
+ },
583
+ async ({ id, feedback_text, ...scores }) => {
584
+ const interview = submitStructuredFeedback(id, scores, feedback_text);
585
+ if (!interview) {
586
+ return { content: [{ type: "text", text: `Interview '${id}' not found.` }], isError: true };
587
+ }
588
+ return { content: [{ type: "text", text: JSON.stringify(interview, null, 2) }] };
589
+ }
590
+ );
591
+
592
+ // --- Bulk Rejection ---
593
+
594
+ server.registerTool(
595
+ "bulk_reject",
596
+ {
597
+ title: "Bulk Reject Applicants",
598
+ description: "Bulk reject all applicants for a job matching a specific status.",
599
+ inputSchema: {
600
+ job_id: z.string(),
601
+ status: z.enum(["applied", "screening", "interviewing", "offered"]),
602
+ reason: z.string().optional(),
603
+ },
604
+ },
605
+ async ({ job_id, status, reason }) => {
606
+ const result = bulkReject(job_id, status, reason);
607
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
608
+ }
609
+ );
610
+
611
+ // --- Referral Stats ---
612
+
613
+ server.registerTool(
614
+ "referral_stats",
615
+ {
616
+ title: "Referral Stats",
617
+ description: "Show conversion rates by applicant source/referral channel.",
618
+ inputSchema: {},
619
+ },
620
+ async () => {
621
+ const stats = getReferralStats();
622
+ return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] };
623
+ }
624
+ );
625
+
626
+ // --- Job Templates ---
627
+
628
+ server.registerTool(
629
+ "save_job_template",
630
+ {
631
+ title: "Save Job as Template",
632
+ description: "Save an existing job posting as a reusable template.",
633
+ inputSchema: {
634
+ job_id: z.string(),
635
+ name: z.string().describe("Unique template name"),
636
+ },
637
+ },
638
+ async ({ job_id, name }) => {
639
+ try {
640
+ const template = saveJobAsTemplate(job_id, name);
641
+ return { content: [{ type: "text", text: JSON.stringify(template, null, 2) }] };
642
+ } catch (err) {
643
+ return { content: [{ type: "text", text: String(err instanceof Error ? err.message : err) }], isError: true };
644
+ }
645
+ }
646
+ );
647
+
648
+ server.registerTool(
649
+ "create_job_from_template",
650
+ {
651
+ title: "Create Job from Template",
652
+ description: "Create a new job posting from an existing template.",
653
+ inputSchema: {
654
+ template_name: z.string(),
655
+ title: z.string().optional(),
656
+ department: z.string().optional(),
657
+ location: z.string().optional(),
658
+ salary_range: z.string().optional(),
659
+ },
660
+ },
661
+ async ({ template_name, ...overrides }) => {
662
+ try {
663
+ const job = createJobFromTemplate(template_name, overrides);
664
+ return { content: [{ type: "text", text: JSON.stringify(job, null, 2) }] };
665
+ } catch (err) {
666
+ return { content: [{ type: "text", text: String(err instanceof Error ? err.message : err) }], isError: true };
667
+ }
668
+ }
669
+ );
670
+
671
+ server.registerTool(
672
+ "list_job_templates",
673
+ {
674
+ title: "List Job Templates",
675
+ description: "List all saved job templates.",
676
+ inputSchema: {},
677
+ },
678
+ async () => {
679
+ const templates = listJobTemplates();
680
+ return {
681
+ content: [{ type: "text", text: JSON.stringify({ templates, count: templates.length }, null, 2) }],
682
+ };
683
+ }
684
+ );
685
+
686
+ server.registerTool(
687
+ "delete_job_template",
688
+ {
689
+ title: "Delete Job Template",
690
+ description: "Delete a job template by ID.",
691
+ inputSchema: { id: z.string() },
692
+ },
693
+ async ({ id }) => {
694
+ const deleted = deleteJobTemplate(id);
695
+ return { content: [{ type: "text", text: JSON.stringify({ id, deleted }) }] };
696
+ }
697
+ );
698
+
454
699
  // --- Start ---
455
700
  async function main() {
456
701
  const transport = new StdioServerTransport();