@linkegringo/mcp 1.0.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.
@@ -0,0 +1,57 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+
3
+ declare function createLinkeGringoMcpServer(): McpServer;
4
+
5
+ interface ChromeVersionResponse {
6
+ Browser: string;
7
+ 'Protocol-Version': string;
8
+ 'User-Agent': string;
9
+ 'V8-Version': string;
10
+ 'WebKit-Version': string;
11
+ webSocketDebuggerUrl?: string;
12
+ }
13
+ interface ChromeTabInfo {
14
+ id: string;
15
+ title: string;
16
+ type: string;
17
+ url: string;
18
+ description?: string;
19
+ webSocketDebuggerUrl?: string;
20
+ devtoolsFrontendUrl?: string;
21
+ }
22
+ interface CdpStatus {
23
+ isRunning: boolean;
24
+ port: number;
25
+ host: string;
26
+ browser?: string;
27
+ protocolVersion?: string;
28
+ activeTabs: Array<{
29
+ id: string;
30
+ title: string;
31
+ url: string;
32
+ webSocketDebuggerUrl?: string;
33
+ }>;
34
+ linkeGringoTabFound: boolean;
35
+ linkeGringoTabUrl?: string;
36
+ error?: string;
37
+ }
38
+
39
+ declare function checkChromeCdp(port?: number, host?: string, timeoutMs?: number): Promise<CdpStatus>;
40
+
41
+ interface InstallResult {
42
+ client: string;
43
+ configPath: string;
44
+ status: 'created' | 'updated' | 'skipped' | 'error';
45
+ message?: string;
46
+ }
47
+ declare function getMcpConfigsForSystem(): Array<{
48
+ client: string;
49
+ configPath: string;
50
+ }>;
51
+ declare function installMcpServerConfig(configPath: string): {
52
+ status: 'created' | 'updated';
53
+ path: string;
54
+ };
55
+ declare function runInstaller(): InstallResult[];
56
+
57
+ export { type CdpStatus, type ChromeTabInfo, type ChromeVersionResponse, type InstallResult, checkChromeCdp, createLinkeGringoMcpServer, getMcpConfigsForSystem, installMcpServerConfig, runInstaller };
package/dist/index.js ADDED
@@ -0,0 +1,1297 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // src/server.ts
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+
9
+ // src/tools/audit-profile.ts
10
+ import { z } from "zod";
11
+ function getExperienceBulletCount(exp) {
12
+ if (Array.isArray(exp.bullets) && exp.bullets.length > 0) {
13
+ return exp.bullets.length;
14
+ }
15
+ const desc = exp.description;
16
+ if (!desc || !desc.trim()) return 0;
17
+ const lines = desc.split(/\n+/).map((l) => l.trim()).filter(Boolean);
18
+ const bulletLines = lines.filter((l) => /^[-*•\u2022\u25E6\u25AA\d+.]/.test(l));
19
+ if (bulletLines.length > 0) return bulletLines.length;
20
+ const sentences = desc.split(/(?<=[.!?])\s+/).filter((s) => s.trim().length > 10);
21
+ return Math.max(lines.length, sentences.length);
22
+ }
23
+ function detectSparseExperiences(experiences) {
24
+ if (!experiences || !Array.isArray(experiences)) return [];
25
+ const sparse = [];
26
+ for (const exp of experiences) {
27
+ const company = exp.companyName || exp.company || "Company";
28
+ const title = exp.title || "Software Engineer";
29
+ const count = getExperienceBulletCount(exp);
30
+ if (count <= 3) {
31
+ sparse.push({
32
+ company,
33
+ title,
34
+ estimatedBullets: count
35
+ });
36
+ }
37
+ }
38
+ return sparse;
39
+ }
40
+ var auditProfileInputSchema = z.object({
41
+ profileText: z.string().optional().describe("Texto bruto extra\xEDdo do perfil do LinkedIn ou curr\xEDculo do candidato"),
42
+ headline: z.string().optional().describe("Headline / T\xEDtulo atual do LinkedIn (se fornecido isoladamente)"),
43
+ summary: z.string().optional().describe("Se\xE7\xE3o Sobre / About atual (se fornecida isoladamente)"),
44
+ experiences: z.array(
45
+ z.object({
46
+ company: z.string(),
47
+ title: z.string(),
48
+ bullets: z.array(z.string()).optional(),
49
+ description: z.string().optional()
50
+ })
51
+ ).optional().describe("Lista estruturada de experi\xEAncias profissionais"),
52
+ skills: z.array(z.string()).optional().describe("Lista de compet\xEAncias / skills declaradas"),
53
+ targetRole: z.string().default("Senior Software Engineer").describe("Cargo almejado no mercado norte-americano (ex: Senior Backend Engineer)"),
54
+ targetMarket: z.string().default("United States Remote").describe("Mercado e regime de trabalho pretendido")
55
+ });
56
+ async function handleAuditProfile(input) {
57
+ let headline = input.headline || "";
58
+ let summary = input.summary || "";
59
+ let skills = input.skills || [];
60
+ const rawExperiences = input.experiences || [];
61
+ if (input.profileText && !headline && rawExperiences.length === 0) {
62
+ const lines = input.profileText.split("\n").map((l) => l.trim()).filter(Boolean);
63
+ if (lines.length > 0) {
64
+ headline = lines[1] || lines[0];
65
+ const potentialSkills = lines.filter(
66
+ (l) => l.includes(",") && (l.toLowerCase().includes("react") || l.toLowerCase().includes("typescript") || l.toLowerCase().includes("python") || l.toLowerCase().includes("go") || l.toLowerCase().includes("aws") || l.toLowerCase().includes("docker"))
67
+ );
68
+ if (potentialSkills.length > 0) {
69
+ skills = potentialSkills[0].split(",").map((s) => s.trim());
70
+ }
71
+ }
72
+ }
73
+ const domainExperiences = rawExperiences.map((e) => ({
74
+ companyName: e.company,
75
+ title: e.title,
76
+ bullets: e.bullets,
77
+ description: e.description,
78
+ current: false,
79
+ location: ""
80
+ }));
81
+ let score = 100;
82
+ const issues = [];
83
+ const triageBottlenecks = [];
84
+ if (!headline || headline.length < 5) {
85
+ score -= 25;
86
+ triageBottlenecks.push("Headline ausente ou muito curta: perfil invis\xEDvel no LinkedIn Recruiter.");
87
+ issues.push({
88
+ severity: "critical",
89
+ category: "headline",
90
+ message: "Headline vazia ou insuficiente.",
91
+ recommendation: `Adicione uma headline no formato: "${input.targetRole} | [3-4 Core Techs] | [Escala/Impacto] | US Remote".`
92
+ });
93
+ } else {
94
+ if (headline.length > 160) {
95
+ score -= 10;
96
+ issues.push({
97
+ severity: "warning",
98
+ category: "headline",
99
+ message: `Headline possui ${headline.length} caracteres, excedendo o limite recomendado de 160.`,
100
+ recommendation: "Reduza para menos de 160 caracteres para evitar que o LinkedIn corte informa\xE7\xF5es cruciais no app mobile."
101
+ });
102
+ }
103
+ const lowerHeadline = headline.toLowerCase();
104
+ const fluffWords = ["passionate", "aspiring", "open to opportunities", "looking for", "rockstar", "ninja", "entusiasta"];
105
+ const detectedFluff = fluffWords.filter((w) => lowerHeadline.includes(w));
106
+ if (detectedFluff.length > 0) {
107
+ score -= 10;
108
+ triageBottlenecks.push(`Termos vagos na headline (${detectedFluff.join(", ")}): enfraquece o posicionamento s\xEAnior.`);
109
+ issues.push({
110
+ severity: "warning",
111
+ category: "headline",
112
+ message: `Headline cont\xE9m palavras-chave fracas: ${detectedFluff.join(", ")}.`,
113
+ recommendation: "Substitua termos gen\xE9ricos por especializa\xE7\xF5es t\xE9cnicas concretas e m\xE9tricas de sistema."
114
+ });
115
+ }
116
+ if (!lowerHeadline.includes("remote") && !lowerHeadline.includes("global") && !lowerHeadline.includes("us")) {
117
+ issues.push({
118
+ severity: "info",
119
+ category: "headline",
120
+ message: "Nenhuma men\xE7\xE3o expl\xEDcita a trabalho remoto internacional na headline.",
121
+ recommendation: 'Inclua "US Remote" ou "Global Teams" para facilitar a triagem de recrutadores americanos.'
122
+ });
123
+ }
124
+ }
125
+ const sparseExps = detectSparseExperiences(domainExperiences);
126
+ let totalBullets = 0;
127
+ let bulletsWithMetrics = 0;
128
+ const metricRegex = /\b(\d+|%|\$|ms|s|k|m|rps|tps|x)\b/i;
129
+ for (const exp of rawExperiences) {
130
+ const bullets = exp.bullets || (exp.description ? exp.description.split("\n") : []);
131
+ for (const b of bullets) {
132
+ if (b.trim().length > 10) {
133
+ totalBullets++;
134
+ if (metricRegex.test(b)) {
135
+ bulletsWithMetrics++;
136
+ }
137
+ }
138
+ }
139
+ }
140
+ if (rawExperiences.length > 0) {
141
+ if (sparseExps.length > 0) {
142
+ const deduction = Math.min(25, sparseExps.length * 10);
143
+ score -= deduction;
144
+ triageBottlenecks.push(
145
+ `${sparseExps.length} experi\xEAncia(s) com poucos detalhes (\u2264 3 bullets): faltam evid\xEAncias de escopo t\xE9cnico.`
146
+ );
147
+ issues.push({
148
+ severity: "critical",
149
+ category: "experience",
150
+ message: `Experi\xEAncias esparsas detectadas em: ${sparseExps.map((s) => s.company).join(", ")}.`,
151
+ recommendation: "Expanda cada experi\xEAncia com 3 a 5 bullets densos e fundamentados."
152
+ });
153
+ }
154
+ if (totalBullets > 0) {
155
+ const metricRatio = bulletsWithMetrics / totalBullets;
156
+ if (metricRatio < 0.4) {
157
+ score -= 20;
158
+ triageBottlenecks.push("Falta de mensura\xE7\xE3o quantitativa (apenas " + Math.round(metricRatio * 100) + "% dos bullets possuem m\xE9tricas).");
159
+ issues.push({
160
+ severity: "critical",
161
+ category: "experience",
162
+ message: "Baixa densidade da f\xF3rmula Google XYZ.",
163
+ recommendation: 'Converta os bullets para "Accomplished [X], measured by [Y], by doing [Z]" ancorando lat\xEAncia, throughput, custo ou SLA.'
164
+ });
165
+ }
166
+ }
167
+ } else if (input.profileText) {
168
+ const hasNumbers = /\b\d+(%|k|m|ms)?\b/i.test(input.profileText);
169
+ if (!hasNumbers) {
170
+ score -= 15;
171
+ triageBottlenecks.push("Raras evid\xEAncias num\xE9ricas encontradas no texto do curr\xEDculo.");
172
+ }
173
+ }
174
+ if (summary) {
175
+ if (summary.length < 100) {
176
+ score -= 10;
177
+ issues.push({
178
+ severity: "warning",
179
+ category: "about",
180
+ message: "Resumo Sobre (About) muito conciso ou incompleto.",
181
+ recommendation: "Crie um resumo estruturado com um gancho forte nos primeiros 250 caracteres e escopo arquitetural."
182
+ });
183
+ }
184
+ }
185
+ const finalScore = Math.max(15, Math.min(100, score));
186
+ const markdownSummary = `
187
+ # Relat\xF3rio de Diagn\xF3stico Inbound (LinkeGringo)
188
+
189
+ **Cargo-Alvo**: ${input.targetRole} (${input.targetMarket})
190
+ **Nota Inbound**: **${finalScore} / 100** ${finalScore >= 80 ? "\u{1F7E2} Recruiter-Ready" : finalScore >= 50 ? "\u{1F7E1} Competitivo M\xE9dio" : "\u{1F534} Cr\xEDtico / Baixa Indexa\xE7\xE3o"}
191
+ **Modo**: 100% Local (Executado pelo seu Agente de IA sem necessidade de Chaves de API)
192
+
193
+ ---
194
+
195
+ ## \u{1F6A6} Gargalos Cr\xEDticos de Triagem (Triage Bottlenecks)
196
+ ${triageBottlenecks.length > 0 ? triageBottlenecks.map((b) => `- \u274C ${b}`).join("\n") : "- \u2713 Nenhum gargalo impeditivo encontrado para triagem inicial."}
197
+
198
+ ## \u{1F4CB} Auditoria Se\xE7\xE3o a Se\xE7\xE3o
199
+ ${issues.length > 0 ? issues.map(
200
+ (iss) => `- **[${iss.category.toUpperCase()}]** (${iss.severity}): ${iss.message}
201
+ *A\xE7\xE3o recomendada*: ${iss.recommendation}`
202
+ ).join("\n") : "- Todas as se\xE7\xF5es atendem \xE0s diretrizes de triagem dos EUA."}
203
+
204
+ ## \u{1F4A1} Pr\xF3ximos Passos para o Agente de IA:
205
+ 1. Usar a ferramenta \`generate_headline_proposals\` para calibrar a headline em at\xE9 160 caracteres.
206
+ 2. Usar a ferramenta \`convert_to_xyz_bullet\` para reescrever as conquistas passivas no formato do Google (*Accomplished [X], measured by [Y], by doing [Z]*).
207
+ 3. Conduzir uma breve entrevista t\xE9cnica sobre as experi\xEAncias esparsas detectadas para extrair m\xE9tricas de escala realistas.
208
+ `.trim();
209
+ return {
210
+ content: [
211
+ {
212
+ type: "text",
213
+ text: markdownSummary
214
+ }
215
+ ],
216
+ structuredData: {
217
+ score: finalScore,
218
+ targetRole: input.targetRole,
219
+ headline,
220
+ totalBullets,
221
+ bulletsWithMetrics,
222
+ sparseExperiences: sparseExps,
223
+ triageBottlenecks,
224
+ issues
225
+ }
226
+ };
227
+ }
228
+
229
+ // src/tools/recruiter-simulator.ts
230
+ import { z as z8 } from "zod";
231
+
232
+ // ../core/src/domain/date-range.ts
233
+ import { z as z2 } from "zod";
234
+ var yearMonthSchema = z2.object({
235
+ year: z2.number().int(),
236
+ month: z2.number().int().min(1).max(12).optional()
237
+ });
238
+ var dateRangeSchema = z2.object({
239
+ start: yearMonthSchema.optional(),
240
+ end: yearMonthSchema.optional()
241
+ });
242
+
243
+ // ../core/src/domain/profile.ts
244
+ import { z as z3 } from "zod";
245
+ var workplaceTypeSchema = z3.preprocess((val) => {
246
+ if (typeof val === "string") {
247
+ const lower = val.toLowerCase().trim();
248
+ if (lower.includes("remote") || lower.includes("remoto")) return "remote";
249
+ if (lower.includes("hybrid") || lower.includes("h\xEDbrido") || lower.includes("hibrido")) return "hybrid";
250
+ if (lower.includes("onsite") || lower.includes("presencial")) return "onsite";
251
+ }
252
+ return val;
253
+ }, z3.enum(["remote", "hybrid", "onsite"]).optional());
254
+ var experienceSchema = z3.object({
255
+ title: z3.string().default("Software Engineer"),
256
+ companyName: z3.string().default("Company"),
257
+ employmentType: z3.string().optional(),
258
+ workplaceType: workplaceTypeSchema,
259
+ location: z3.string().optional(),
260
+ startDate: yearMonthSchema.optional(),
261
+ endDate: yearMonthSchema.optional(),
262
+ current: z3.preprocess((val) => {
263
+ if (typeof val === "boolean") return val;
264
+ if (typeof val === "string") {
265
+ const lower = val.toLowerCase().trim();
266
+ return lower === "true" || lower === "present" || lower === "presente" || lower === "atual";
267
+ }
268
+ return false;
269
+ }, z3.boolean().default(false)),
270
+ dateRangeText: z3.string().optional(),
271
+ durationText: z3.string().optional(),
272
+ description: z3.string().optional()
273
+ });
274
+ var educationSchema = z3.object({
275
+ schoolName: z3.string(),
276
+ degreeName: z3.string().optional(),
277
+ fieldOfStudy: z3.string().optional(),
278
+ grade: z3.string().optional(),
279
+ description: z3.string().optional(),
280
+ dateRange: dateRangeSchema.optional()
281
+ });
282
+ var skillSchema = z3.union([
283
+ z3.string().transform((name) => ({ name })),
284
+ z3.object({
285
+ name: z3.string(),
286
+ endorsementCount: z3.number().int().nonnegative().optional()
287
+ })
288
+ ]);
289
+ var certificationSchema = z3.union([
290
+ z3.string().transform((name) => ({ name })),
291
+ z3.object({
292
+ name: z3.string(),
293
+ issuer: z3.string().optional(),
294
+ credentialId: z3.string().optional(),
295
+ url: z3.string().optional(),
296
+ issuedDate: dateRangeSchema.shape.start.optional(),
297
+ expirationDate: dateRangeSchema.shape.end.optional()
298
+ })
299
+ ]);
300
+ var projectSchema = z3.object({
301
+ name: z3.string(),
302
+ description: z3.string().optional(),
303
+ url: z3.string().optional(),
304
+ dateRange: dateRangeSchema.optional()
305
+ });
306
+ var languageSchema = z3.union([
307
+ z3.string().transform((name) => ({ name })),
308
+ z3.object({
309
+ name: z3.string(),
310
+ proficiency: z3.string().optional()
311
+ })
312
+ ]);
313
+ var honorSchema = z3.object({
314
+ title: z3.string(),
315
+ issuer: z3.string().optional(),
316
+ description: z3.string().optional(),
317
+ date: dateRangeSchema.shape.start.optional()
318
+ });
319
+ var profileSchema = z3.object({
320
+ publicId: z3.string().default("user"),
321
+ firstName: z3.string().default(""),
322
+ lastName: z3.string().default(""),
323
+ headline: z3.string().optional().default(""),
324
+ location: z3.string().optional().default(""),
325
+ summary: z3.string().optional().default(""),
326
+ experiences: z3.array(experienceSchema).default([]),
327
+ education: z3.array(educationSchema).default([]),
328
+ skills: z3.array(skillSchema).default([]),
329
+ certifications: z3.array(certificationSchema).default([]),
330
+ projects: z3.array(projectSchema).default([]),
331
+ languages: z3.array(languageSchema).default([]),
332
+ honors: z3.array(honorSchema).default([])
333
+ });
334
+
335
+ // ../core/src/domain/scraped.ts
336
+ import { z as z4 } from "zod";
337
+ var scrapedIdentitySchema = z4.object({
338
+ publicId: z4.string().optional(),
339
+ fullName: z4.string(),
340
+ headline: z4.string().optional(),
341
+ location: z4.string().optional(),
342
+ summary: z4.string().optional()
343
+ });
344
+ var scrapedProfileSchema = z4.object({
345
+ identity: scrapedIdentitySchema.optional(),
346
+ experiences: z4.array(experienceSchema).optional(),
347
+ skills: z4.array(z4.string()).optional(),
348
+ education: z4.array(educationSchema).optional(),
349
+ certifications: z4.array(certificationSchema).optional(),
350
+ projects: z4.array(projectSchema).optional(),
351
+ languages: z4.array(languageSchema).optional(),
352
+ updatedAt: z4.number().optional()
353
+ });
354
+
355
+ // ../core/src/domain/analysis.ts
356
+ import { z as z5 } from "zod";
357
+ var critiqueSeveritySchema = z5.preprocess((val) => {
358
+ if (typeof val === "string") {
359
+ const lower = val.toLowerCase().trim();
360
+ if (lower === "critical" || lower === "grave") return "high";
361
+ if (lower === "moderate" || lower === "moderado") return "medium";
362
+ if (lower === "minor" || lower === "leve") return "low";
363
+ return lower;
364
+ }
365
+ return val;
366
+ }, z5.enum(["high", "medium", "low"]).catch("medium"));
367
+ var sectionCritiqueSchema = z5.object({
368
+ section: z5.string(),
369
+ assessment: z5.string(),
370
+ strengths: z5.preprocess((val) => {
371
+ if (typeof val === "string") return [val];
372
+ if (Array.isArray(val)) return val.map(String);
373
+ return [];
374
+ }, z5.array(z5.string()).default([])),
375
+ issues: z5.preprocess((val) => {
376
+ if (typeof val === "string") return [val];
377
+ if (Array.isArray(val)) return val.map(String);
378
+ return [];
379
+ }, z5.array(z5.string()).default([])),
380
+ severity: critiqueSeveritySchema
381
+ });
382
+ var scoreNumber = z5.coerce.number().transform((n) => Math.round(n)).pipe(z5.number().min(0).max(100));
383
+ var profileScoresSchema = z5.object({
384
+ searchRelevance: scoreNumber,
385
+ humanVoice: scoreNumber,
386
+ credibility: scoreNumber,
387
+ positioningClarity: scoreNumber,
388
+ evidenceCoverage: scoreNumber
389
+ });
390
+ var scoreExplanationsSchema = z5.object({
391
+ searchRelevance: z5.string().default(""),
392
+ humanVoice: z5.string().default(""),
393
+ credibility: z5.string().default(""),
394
+ positioningClarity: z5.string().default(""),
395
+ evidenceCoverage: z5.string().default("")
396
+ });
397
+ var profileDirectionSchema = z5.object({
398
+ positioning: z5.string(),
399
+ primaryRole: z5.string(),
400
+ alternativeRoles: z5.preprocess((val) => {
401
+ if (Array.isArray(val)) return val.map(String);
402
+ if (typeof val === "string" && val.trim()) return [val.trim()];
403
+ return [];
404
+ }, z5.array(z5.string()).default([])),
405
+ openToWorkTitles: z5.preprocess((val) => {
406
+ if (Array.isArray(val)) return val.map(String);
407
+ if (typeof val === "string" && val.trim()) return [val.trim()];
408
+ return void 0;
409
+ }, z5.array(z5.string()).optional()),
410
+ rationale: z5.string()
411
+ });
412
+ var profileGapTargetSectionSchema = z5.enum(["headline", "about", "experience", "skills"]);
413
+ var profileGapSchema = z5.object({
414
+ id: z5.string(),
415
+ label: z5.string(),
416
+ targetSection: profileGapTargetSectionSchema,
417
+ targetExperienceId: z5.string().optional(),
418
+ targetBulletIndex: z5.number().optional(),
419
+ targetBlockId: z5.string().optional(),
420
+ suggestedUnlock: z5.string().optional()
421
+ });
422
+ var profileReviewSchema = z5.object({
423
+ targetMarket: z5.string().default("United States"),
424
+ language: z5.string().default("en"),
425
+ overallScore: scoreNumber,
426
+ scores: profileScoresSchema,
427
+ scoreExplanations: scoreExplanationsSchema.optional(),
428
+ executiveSummary: z5.string(),
429
+ profileDirection: profileDirectionSchema,
430
+ critique: z5.array(sectionCritiqueSchema).default([]),
431
+ triageBottlenecks: z5.preprocess((val) => {
432
+ if (Array.isArray(val)) return val;
433
+ if (typeof val === "string" && val.trim()) return [val.trim()];
434
+ return [];
435
+ }, z5.array(z5.string()).default([])),
436
+ primaryGaps: z5.array(profileGapSchema).optional(),
437
+ inboundReadiness: z5.object({
438
+ score: scoreNumber,
439
+ primaryGapId: z5.string().optional()
440
+ }).optional()
441
+ });
442
+ var rewrittenExperienceSchema = z5.object({
443
+ id: z5.string().optional(),
444
+ title: z5.string(),
445
+ companyName: z5.string(),
446
+ bullets: z5.preprocess((val) => {
447
+ if (typeof val === "string") return [val];
448
+ if (Array.isArray(val)) return val.map(String);
449
+ return [];
450
+ }, z5.array(z5.string()).default([]))
451
+ });
452
+ var rewrittenProfileSchema = z5.object({
453
+ headline: z5.string(),
454
+ summary: z5.string(),
455
+ experiences: z5.array(rewrittenExperienceSchema).default([]),
456
+ skills: z5.preprocess((val) => {
457
+ if (typeof val === "string") return val.split(/,\s*/);
458
+ if (Array.isArray(val)) {
459
+ return val.map((v) => typeof v === "object" && v && "name" in v ? String(v.name) : String(v));
460
+ }
461
+ return [];
462
+ }, z5.array(z5.string()).default([])),
463
+ openToWorkTitles: z5.preprocess((val) => {
464
+ if (Array.isArray(val)) return val.map(String);
465
+ if (typeof val === "string" && val.trim()) return [val.trim()];
466
+ return void 0;
467
+ }, z5.array(z5.string()).optional()),
468
+ cardConversionBadges: z5.preprocess((val) => {
469
+ if (Array.isArray(val)) return val.map(String);
470
+ if (typeof val === "string" && val.trim()) return [val.trim()];
471
+ return void 0;
472
+ }, z5.array(z5.string()).optional()),
473
+ cardConversionReasons: z5.preprocess((val) => {
474
+ if (Array.isArray(val)) return val.map(String);
475
+ if (typeof val === "string" && val.trim()) return [val.trim()];
476
+ return void 0;
477
+ }, z5.array(z5.string()).optional())
478
+ });
479
+ var profileAnalysisSchema = z5.object({
480
+ targetMarket: z5.string().default("United States"),
481
+ language: z5.string().default("en"),
482
+ initialScore: scoreNumber.optional(),
483
+ overallScore: scoreNumber,
484
+ scores: profileScoresSchema,
485
+ scoreExplanations: scoreExplanationsSchema.optional(),
486
+ executiveSummary: z5.string(),
487
+ profileDirection: profileDirectionSchema,
488
+ critique: z5.array(sectionCritiqueSchema).default([]),
489
+ triageBottlenecks: z5.preprocess((val) => {
490
+ if (Array.isArray(val)) return val;
491
+ if (typeof val === "string" && val.trim()) return [val.trim()];
492
+ return [];
493
+ }, z5.array(z5.string()).default([])),
494
+ primaryGaps: z5.array(profileGapSchema).optional(),
495
+ inboundReadiness: z5.object({
496
+ score: scoreNumber,
497
+ primaryGapId: z5.string().optional()
498
+ }).optional(),
499
+ rewritten: rewrittenProfileSchema
500
+ });
501
+ var inboundStageStatusSchema = z5.enum(["ready", "needs_attention", "blocked", "not_measurable"]);
502
+ var inboundStageSchema = z5.object({
503
+ id: z5.enum(["search", "card", "profile", "inmail"]),
504
+ name: z5.string(),
505
+ score: z5.number().optional(),
506
+ status: inboundStageStatusSchema,
507
+ primaryGapId: z5.string().optional(),
508
+ primaryGap: profileGapSchema.optional(),
509
+ description: z5.string().optional()
510
+ });
511
+ var inboundJourneySchema = z5.object({
512
+ search: inboundStageSchema,
513
+ card: inboundStageSchema,
514
+ profile: inboundStageSchema,
515
+ inmail: inboundStageSchema,
516
+ stages: z5.array(inboundStageSchema)
517
+ });
518
+
519
+ // ../core/src/domain/interview.ts
520
+ import { z as z6 } from "zod";
521
+ var interviewCategorySchema = z6.preprocess((val) => {
522
+ if (typeof val === "string") {
523
+ const lower = val.toLowerCase().trim();
524
+ if (lower.includes("tech") || lower.includes("arch") || lower.includes("deep")) return "technical-depth";
525
+ if (lower.includes("scale") || lower.includes("volume") || lower.includes("concurr")) return "scale";
526
+ if (lower.includes("impact") || lower.includes("result") || lower.includes("metric")) return "impact";
527
+ if (lower.includes("lead") || lower.includes("mentor")) return "leadership";
528
+ if (lower.includes("responsib") || lower.includes("invisib")) return "responsibility";
529
+ if (lower.includes("pref") || lower.includes("stack")) return "preference";
530
+ if (lower.includes("diff") || lower.includes("unique")) return "differentiation";
531
+ if (lower.includes("credib") || lower.includes("evid")) return "credibility";
532
+ if (lower.includes("market") || lower.includes("align")) return "market";
533
+ if (lower.includes("direct") || lower.includes("goal")) return "direction";
534
+ return lower;
535
+ }
536
+ return val;
537
+ }, z6.enum([
538
+ "direction",
539
+ "responsibility",
540
+ "technical-depth",
541
+ "impact",
542
+ "scale",
543
+ "leadership",
544
+ "preference",
545
+ "market",
546
+ "credibility",
547
+ "differentiation"
548
+ ]).catch("technical-depth"));
549
+ var careerObjectiveSchema = z6.object({
550
+ targetMarket: z6.string().default("United States"),
551
+ primaryRole: z6.string().min(1),
552
+ seniority: z6.string().optional(),
553
+ workPreference: z6.enum(["remote", "hybrid", "onsite", "flexible"]).default("remote"),
554
+ excludedTechnologies: z6.array(z6.string()).default([])
555
+ });
556
+ var interviewAnswerTypeSchema = z6.preprocess((val) => {
557
+ if (typeof val === "string") {
558
+ const lower = val.toLowerCase().trim();
559
+ if (lower === "text") return "long-text";
560
+ if (lower.includes("choice") || lower.includes("select")) return "single-choice";
561
+ if (lower.includes("yes") || lower.includes("bool")) return "yes-no";
562
+ if (lower.includes("short")) return "short-text";
563
+ if (lower.includes("long")) return "long-text";
564
+ }
565
+ return val;
566
+ }, z6.enum(["short-text", "long-text", "single-choice", "yes-no"]).catch("long-text"));
567
+ var interviewQuestionSchema = z6.object({
568
+ id: z6.string().min(1),
569
+ category: interviewCategorySchema,
570
+ question: z6.string().min(1),
571
+ reason: z6.string().min(1),
572
+ placeholderExample: z6.string().optional(),
573
+ relatedExperience: z6.string().optional(),
574
+ answerType: interviewAnswerTypeSchema,
575
+ options: z6.array(z6.string()).optional(),
576
+ required: z6.boolean().default(false)
577
+ });
578
+ var interviewAnswerSchema = z6.object({
579
+ questionId: z6.string().min(1),
580
+ value: z6.string().default(""),
581
+ skipped: z6.boolean().default(false)
582
+ });
583
+ var confirmedFactSourceSchema = z6.preprocess((val) => {
584
+ if (typeof val === "string") {
585
+ const lower = val.toLowerCase().trim();
586
+ if (lower.includes("link") || lower.includes("pdf") || lower.includes("resume") || lower.includes("cv") || lower.includes("profile")) {
587
+ return "linkedin-profile";
588
+ }
589
+ return "interview";
590
+ }
591
+ return val;
592
+ }, z6.enum(["linkedin-profile", "interview"]).catch("interview"));
593
+ var confirmedFactSchema = z6.object({
594
+ id: z6.string().min(1),
595
+ statement: z6.string().min(1),
596
+ source: confirmedFactSourceSchema,
597
+ sourceReference: z6.string().default("Experi\xEAncia"),
598
+ confirmed: z6.boolean().default(false)
599
+ });
600
+ var interviewPlanSchema = z6.object({
601
+ questions: z6.array(interviewQuestionSchema).default([])
602
+ });
603
+ var interviewProgressSchema = z6.object({
604
+ readyForGeneration: z6.boolean(),
605
+ rationale: z6.string().min(1),
606
+ questions: z6.array(interviewQuestionSchema).default([]),
607
+ facts: z6.array(confirmedFactSchema).default([])
608
+ });
609
+
610
+ // ../core/src/domain/micro-integration.ts
611
+ import { z as z7 } from "zod";
612
+ var gapTermKindSchema = z7.enum([
613
+ "role",
614
+ "technology",
615
+ "concept",
616
+ "scale",
617
+ "tool",
618
+ "domain"
619
+ ]);
620
+ var gapEvidenceStatusSchema = z7.enum([
621
+ "unverified",
622
+ "confirmed",
623
+ "denied",
624
+ "ambiguous"
625
+ ]);
626
+ var gapEvidenceSchema = z7.object({
627
+ status: gapEvidenceStatusSchema,
628
+ experienceId: z7.string().optional(),
629
+ evidenceText: z7.string().optional(),
630
+ source: z7.enum(["candidate", "profile"])
631
+ });
632
+ var searchGapSchema = z7.object({
633
+ id: z7.string(),
634
+ term: z7.string(),
635
+ status: z7.enum(["weak", "missing"]),
636
+ kind: gapTermKindSchema,
637
+ targetSection: z7.enum(["headline", "about", "experience", "skills"]),
638
+ targetExperienceId: z7.string().optional(),
639
+ evidence: gapEvidenceSchema.optional()
640
+ });
641
+ var microPatchKindSchema = z7.enum([
642
+ "headline_replace",
643
+ "about_insert",
644
+ "experience_rewrite",
645
+ "skill_add",
646
+ "no_safe_change"
647
+ ]);
648
+ var microIntegrationInputSchema = z7.object({
649
+ targetRole: z7.string(),
650
+ gap: searchGapSchema,
651
+ currentText: z7.string(),
652
+ context: z7.object({
653
+ headline: z7.string().optional(),
654
+ summary: z7.string().optional(),
655
+ skills: z7.array(z7.string()).optional(),
656
+ experience: z7.any().optional()
657
+ }).optional(),
658
+ evidence: gapEvidenceSchema,
659
+ style: z7.object({
660
+ language: z7.literal("en").default("en"),
661
+ tone: z7.literal("executive").default("executive")
662
+ }).optional()
663
+ });
664
+ var microIntegrationProposalSchema = z7.object({
665
+ status: z7.enum(["ready", "blocked"]),
666
+ patchKind: microPatchKindSchema,
667
+ term: z7.string(),
668
+ target: z7.object({
669
+ section: z7.enum(["headline", "about", "experience", "skills"]),
670
+ experienceId: z7.string().optional(),
671
+ bulletIndex: z7.number().optional()
672
+ }),
673
+ before: z7.string(),
674
+ after: z7.string().optional(),
675
+ rationale: z7.string(),
676
+ matchedEvidence: z7.array(z7.string()).default([]),
677
+ warnings: z7.array(z7.string()).default([])
678
+ });
679
+ function termMatchesText(content, term) {
680
+ if (!content || !term) return false;
681
+ const cleanTerm = term.trim().toLowerCase().replace(/^["']|["']$/g, "");
682
+ if (cleanTerm.length === 0) return false;
683
+ const cleanContent = content.toLowerCase();
684
+ const words = cleanTerm.split(/[\s\-_/]+/).filter(Boolean);
685
+ if (words.length === 0) return false;
686
+ if (words.length > 1) {
687
+ const escapedWords = words.map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
688
+ const lastWord = words[words.length - 1];
689
+ const escapedLast = escapedWords[escapedWords.length - 1];
690
+ let stemPattern = `${escapedLast}(?:s|es|d|ed|ing)?`;
691
+ if (lastWord.length > 3 && lastWord.endsWith("e")) {
692
+ const root = lastWord.slice(0, -1);
693
+ const escapedRoot = root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
694
+ stemPattern = `(?:${escapedLast}(?:s|d)?|${escapedRoot}(?:ing|ed)?)`;
695
+ }
696
+ const flexiblePattern = "(?:^|[^a-z0-9])" + escapedWords.slice(0, -1).join("[\\s\\-_/]+") + "[\\s\\-_/]+" + stemPattern + "(?:[^a-z0-9]|$)";
697
+ try {
698
+ const rx = new RegExp(flexiblePattern, "i");
699
+ if (rx.test(cleanContent)) return true;
700
+ } catch {
701
+ }
702
+ const joined = words.join("");
703
+ const joinedRx = new RegExp(`(?:^|[^a-z0-9])${joined}(?:[^a-z0-9]|$)`, "i");
704
+ if (joinedRx.test(cleanContent)) return true;
705
+ } else if (words.length === 1) {
706
+ const word = words[0];
707
+ const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
708
+ const rx = new RegExp(`(?:^|[^a-z0-9])${escaped}(?:s|es|d|ed|ing)?(?:[^a-z0-9]|$)`, "i");
709
+ if (rx.test(cleanContent)) return true;
710
+ if (word.length > 6) {
711
+ const prefixMatch = word.match(/^(micro|multi|cross|inter|sub|meta)(.*)$/);
712
+ if (prefixMatch) {
713
+ const hyphenated = `${prefixMatch[1]}-${prefixMatch[2]}`;
714
+ if (cleanContent.includes(hyphenated)) return true;
715
+ }
716
+ }
717
+ }
718
+ return false;
719
+ }
720
+
721
+ // src/tools/recruiter-simulator.ts
722
+ var simulateRecruiterSearchInputSchema = z8.object({
723
+ headline: z8.string().describe("Headline atual ou proposta do candidato"),
724
+ summary: z8.string().default("").describe("Resumo ou se\xE7\xE3o About do perfil"),
725
+ skills: z8.array(z8.string()).default([]).describe("Lista de compet\xEAncias t\xE9cnicas registradas"),
726
+ experienceBullets: z8.array(z8.string()).default([]).describe("Bullets das experi\xEAncias profissionais"),
727
+ targetRole: z8.string().default("Senior Software Engineer").describe("Cargo-alvo da busca (ex: Senior Backend Engineer)"),
728
+ requiredKeywords: z8.array(z8.string()).optional().describe('Termos t\xE9cnicos ou palavras-chave obrigat\xF3rias a testar (ex: ["Go", "Kubernetes", "Microservices"])')
729
+ });
730
+ async function handleSimulateRecruiterSearch(input) {
731
+ const defaultKeywords = input.requiredKeywords?.length ? input.requiredKeywords : [input.targetRole, "Senior", "Remote", "Architecture", "Scale"];
732
+ const highPriorityText = `${input.headline} ${input.skills.join(" ")}`;
733
+ const bodyText = `${input.summary} ${input.experienceBullets.join(" ")}`;
734
+ const evaluations = defaultKeywords.map((term) => {
735
+ const inHighPriority = termMatchesText(highPriorityText, term);
736
+ const inBody = termMatchesText(bodyText, term);
737
+ let status = "missing";
738
+ let detail = "Termo n\xE3o encontrado no perfil.";
739
+ if (inHighPriority) {
740
+ status = "match";
741
+ detail = "Posicionado em local de peso 3x (Headline ou Top Skills).";
742
+ } else if (inBody) {
743
+ status = "weak";
744
+ detail = "Presente apenas no corpo do perfil ou bullets secund\xE1rios.";
745
+ }
746
+ return {
747
+ term,
748
+ status,
749
+ detail
750
+ };
751
+ });
752
+ const matchCount = evaluations.filter((e) => e.status === "match").length;
753
+ const weakCount = evaluations.filter((e) => e.status === "weak").length;
754
+ const missingCount = evaluations.filter((e) => e.status === "missing").length;
755
+ const total = evaluations.length || 1;
756
+ const matchPercentage = Math.round(
757
+ (matchCount * 1 + weakCount * 0.5) / total * 100
758
+ );
759
+ const overallStatus = matchPercentage >= 80 ? "match" : matchPercentage >= 40 ? "weak" : "missing";
760
+ const markdownSummary = `
761
+ # Simula\xE7\xE3o de Busca do Recrutador (LinkedIn Recruiter ATS)
762
+
763
+ **Cargo Buscado**: ${input.targetRole}
764
+ **Compatibilidade com a Busca**: **${matchPercentage}%** (${overallStatus === "match" ? "\u{1F7E2} Alta Visibilidade" : overallStatus === "weak" ? "\u{1F7E1} Visibilidade Parcial" : "\u{1F534} Fora do Radar"})
765
+ **Resumo de Termos**: ${matchCount} com Match Direto (3x), ${weakCount} Fracos, ${missingCount} Faltantes.
766
+
767
+ ## \u{1F50D} An\xE1lise de Termos-Chave
768
+ ${evaluations.map(
769
+ (e) => `- ${e.status === "match" ? "\u2713 \u{1F7E2}" : e.status === "weak" ? "\u26A0\uFE0F \u{1F7E1}" : "\u274C \u{1F534}"} **${e.term}**: ${e.detail}`
770
+ ).join("\n")}
771
+
772
+ ${missingCount > 0 ? `
773
+ > \u{1F4A1} **Dica do Recrutador**: Adicione os termos faltantes diretamente na sua Headline ou na se\xE7\xE3o de Compet\xEAncias para triplicar a chance de indexa\xE7\xE3o.` : "\n> \u{1F3C6} **Excelente**: Seu perfil tem densidade perfeita para capturar filtros booleanos de recrutadores dos EUA."}
774
+ `.trim();
775
+ return {
776
+ content: [
777
+ {
778
+ type: "text",
779
+ text: markdownSummary
780
+ }
781
+ ],
782
+ structuredData: {
783
+ overallStatus,
784
+ matchPercentage,
785
+ evaluations,
786
+ matchCount,
787
+ weakCount,
788
+ missingCount
789
+ }
790
+ };
791
+ }
792
+
793
+ // src/tools/xyz-bullet-converter.ts
794
+ import { z as z9 } from "zod";
795
+ var convertToXyzBulletInputSchema = z9.object({
796
+ rawBullet: z9.string().describe('Bullet original descritivo ou passivo (ex: "Desenvolvi microsservi\xE7os em Go para pagamentos")'),
797
+ roleContext: z9.string().default("Senior Software Engineer").describe('Contexto da empresa, cargo ou projeto (ex: "Fintech de pagamentos, alta escala")'),
798
+ action: z9.string().optional().describe('A\xE7\xE3o de impacto com verbo no passado (ex: "Architected and deployed distributed payment services")'),
799
+ metric: z9.string().optional().describe('M\xE9trica quantitativa [Y] (ex: "reducing p99 latency by 35% and scaling to 12,000 RPS")'),
800
+ method: z9.string().optional().describe('Como foi feito [Z] (ex: "by migrating monolith endpoints to Go microservices on AWS EKS")')
801
+ });
802
+ function formatGoogleXyzBullet(parts) {
803
+ const cleanAction = parts.action.trim().replace(/[.,;]+$/, "");
804
+ const cleanMetric = parts.metric.trim().replace(/[.,;]+$/, "");
805
+ const cleanMethod = parts.method.trim().replace(/[.,;]+$/, "");
806
+ const methodPrefix = /^by\s+/i.test(cleanMethod) ? "" : "by ";
807
+ const metricPrefix = /^measured by\s+/i.test(cleanMetric) ? "" : /^resulting in\s+/i.test(cleanMetric) ? "" : "measured by ";
808
+ return `${cleanAction}, ${metricPrefix}${cleanMetric}, ${methodPrefix}${cleanMethod}.`;
809
+ }
810
+ async function handleConvertToXyzBullet(input) {
811
+ const raw = input.rawBullet.trim();
812
+ if (input.action && input.metric && input.method) {
813
+ const formatted = formatGoogleXyzBullet({
814
+ action: input.action,
815
+ metric: input.metric,
816
+ method: input.method
817
+ });
818
+ return {
819
+ content: [
820
+ {
821
+ type: "text",
822
+ text: `
823
+ # Bullet Google XYZ Formatado
824
+
825
+ \u2728 **${formatted}**
826
+
827
+ - **[X] Accomplished**: ${input.action}
828
+ - **[Y] Measured by**: ${input.metric}
829
+ - **[Z] By doing**: ${input.method}
830
+ `.trim()
831
+ }
832
+ ],
833
+ structuredData: {
834
+ formattedBullet: formatted,
835
+ parts: { action: input.action, metric: input.metric, method: input.method }
836
+ }
837
+ };
838
+ }
839
+ const metricRegex = /\b(\d+|%|\$|ms|s|k|m|rps|tps|x)\b/i;
840
+ const hasMetrics = metricRegex.test(raw);
841
+ const activeVerbs = [
842
+ "Architected",
843
+ "Engineered",
844
+ "Designed",
845
+ "Spearheaded",
846
+ "Optimized",
847
+ "Scaled",
848
+ "Automated",
849
+ "Refactored",
850
+ "Implemented",
851
+ "Streamlined",
852
+ "Accelerated"
853
+ ];
854
+ const cleanBullet = raw.replace(/^[-*•\s]+/, "");
855
+ const optionScale = `Architected and scaled core ${input.roleContext} workflows, measured by reducing p99 latency by 35% and handling 10k+ peak RPS, by redesigning synchronous bottlenecks with distributed message queues.`;
856
+ const optionCost = `Optimized cloud resource utilization and data processing pipelines, measured by reducing AWS infrastructure costs by 28% ($45k/year savings), by implementing intelligent autoscaling and caching layers.`;
857
+ const optionReliability = `Engineered robust automated CI/CD and deployment pipelines for ${input.roleContext}, measured by increasing deployment frequency by 3x and achieving 99.99% system availability, by establishing automated regression testing and canary releases.`;
858
+ const report = `
859
+ # An\xE1lise de F\xF3rmula Google XYZ
860
+
861
+ **Bullet Analisado**:
862
+ > "${cleanBullet}"
863
+
864
+ **Status de Auditoria**:
865
+ - **Verbo de A\xE7\xE3o no Passado**: ${activeVerbs.some((v) => cleanBullet.toLowerCase().startsWith(v.toLowerCase())) ? "\u2705 Presente" : "\u26A0\uFE0F Recomendado substituir o in\xEDcio por um verbo de forte lideran\xE7a t\xE9cnica (ex: Architected, Engineered, Optimized)"}
866
+ - **M\xE9trica Quantitativa [Y]**: ${hasMetrics ? "\u2705 Detectada" : "\u274C N\xE3o detectada \u2014 faltam n\xFAmeros, porcentagens, lat\xEAncia ou volume"}
867
+
868
+ ---
869
+
870
+ ## 3 Propostas Prontas de Google XYZ para este Contexto:
871
+ 1. **Foco em Escala & Performance (Lat\xEAncia/Throughput)**:
872
+ > \u2728 "${optionScale}"
873
+ 2. **Foco em Otimiza\xE7\xE3o de Custo & Recursos**:
874
+ > \u2728 "${optionCost}"
875
+ 3. **Foco em Confiabilidade & Qualidade de Engenharia**:
876
+ > \u2728 "${optionReliability}"
877
+
878
+ ## \u{1F4A1} Instru\xE7\xF5es para o Agente de IA:
879
+ Pergunte ao candidato qual m\xE9trica real mais se aproxima da sua entrega (${hasMetrics ? "ou valide os n\xFAmeros detectados" : "ex: % de redu\xE7\xE3o de tempo, volume de requisi\xE7\xF5es ou economia"}). Em seguida, finalize o bullet usando a estrutura:
880
+ \`Accomplished [X], measured by [Y], by doing [Z]\`.
881
+ `.trim();
882
+ return {
883
+ content: [
884
+ {
885
+ type: "text",
886
+ text: report
887
+ }
888
+ ],
889
+ structuredData: {
890
+ hasMetrics,
891
+ rawBullet: cleanBullet,
892
+ proposals: [optionScale, optionCost, optionReliability]
893
+ }
894
+ };
895
+ }
896
+
897
+ // src/tools/headline-generator.ts
898
+ import { z as z10 } from "zod";
899
+ var generateHeadlineInputSchema = z10.object({
900
+ targetRole: z10.string().default("Senior Software Engineer").describe("Cargo pretendido em ingl\xEAs (ex: Staff Distributed Systems Engineer)"),
901
+ coreTechnologies: z10.array(z10.string()).min(1).max(5).default(["TypeScript", "React", "Node.js"]).describe("3 a 4 tecnologias centrais e mais procuradas da sua stack"),
902
+ keyDifferentiator: z10.string().optional().describe("Diferencial ou escopo t\xE9cnico (ex: High Scale, Fintech, Cloud Architecture)"),
903
+ seniorityOrScope: z10.string().default("US Remote").describe("Senioridade ou disponibilidade (ex: US Remote, Global Teams, Staff)")
904
+ });
905
+ async function handleGenerateHeadline(input) {
906
+ const techsStr = input.coreTechnologies.slice(0, 4).join(" \u2022 ");
907
+ const diffStr = input.keyDifferentiator || "Distributed Systems";
908
+ const scopeStr = input.seniorityOrScope || "US Remote";
909
+ const option1 = `${input.targetRole} | ${techsStr} | ${diffStr} | ${scopeStr}`;
910
+ const option2 = `${input.targetRole} | Scaling ${diffStr} with ${techsStr} | ${scopeStr}`;
911
+ const option3 = `${input.targetRole} | ${input.coreTechnologies.slice(0, 3).join(", ")} Specialist | ${scopeStr}`;
912
+ const proposals = [
913
+ {
914
+ type: "Niche Specialist (Recomendada)",
915
+ headline: option1.length > 160 ? option1.slice(0, 157) + "..." : option1,
916
+ charCount: option1.length,
917
+ focus: "Densidade m\xE1xima de palavras-chave para o algoritmo do LinkedIn Recruiter (peso 3x)."
918
+ },
919
+ {
920
+ type: "Scale & Impact Oriented",
921
+ headline: option2.length > 160 ? option2.slice(0, 157) + "..." : option2,
922
+ charCount: option2.length,
923
+ focus: "Comunica maturidade arquitetural e foco em resolu\xE7\xE3o de problemas de neg\xF3cio."
924
+ },
925
+ {
926
+ type: "Direct & Concise",
927
+ headline: option3.length > 160 ? option3.slice(0, 157) + "..." : option3,
928
+ charCount: option3.length,
929
+ focus: "F\xF3rmula limpa ideal para visualiza\xE7\xE3o completa no app mobile do LinkedIn."
930
+ }
931
+ ];
932
+ const markdownSummary = `
933
+ # Propostas de Headline de Alta Convers\xE3o
934
+
935
+ **Cargo-Alvo**: ${input.targetRole}
936
+
937
+ ${proposals.map(
938
+ (p, i) => `
939
+ ### Op\xE7\xE3o ${i + 1}: ${p.type}
940
+ > \u2728 **"${p.headline}"**
941
+ - **Caracteres**: ${p.charCount} / 160 ${p.charCount <= 160 ? "\u2713 (Dentro do limite recomendado)" : "\u26A0\uFE0F (Excedeu 160)"}
942
+ - **Vantagem**: ${p.focus}
943
+ `
944
+ ).join("\n")}
945
+
946
+ > \u{1F4A1} **Regra de Ouro**: Mantenha sempre abaixo de 160 caracteres para garantir que os recrutadores leiam o cargo e as tecnologias inteiras tanto no Desktop quanto no Mobile.
947
+ `.trim();
948
+ return {
949
+ content: [
950
+ {
951
+ type: "text",
952
+ text: markdownSummary
953
+ }
954
+ ],
955
+ structuredData: {
956
+ proposals
957
+ }
958
+ };
959
+ }
960
+
961
+ // src/tools/cdp-check.ts
962
+ import { z as z11 } from "zod";
963
+
964
+ // src/cdp/probe.ts
965
+ async function checkChromeCdp(port = 9222, host = "127.0.0.1", timeoutMs = 2e3) {
966
+ const controller = new AbortController();
967
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
968
+ try {
969
+ const versionRes = await fetch(`http://${host}:${port}/json/version`, {
970
+ signal: controller.signal
971
+ });
972
+ if (!versionRes.ok) {
973
+ throw new Error(`HTTP ${versionRes.status}: ${versionRes.statusText}`);
974
+ }
975
+ const versionData = await versionRes.json();
976
+ let pageTabs = [];
977
+ try {
978
+ const listRes = await fetch(`http://${host}:${port}/json/list`, {
979
+ signal: controller.signal
980
+ });
981
+ if (listRes.ok) {
982
+ const rawTabs = await listRes.json();
983
+ if (Array.isArray(rawTabs)) {
984
+ pageTabs = rawTabs.filter((t) => t.type === "page");
985
+ }
986
+ }
987
+ } catch {
988
+ }
989
+ const linkeGringoTab = pageTabs.find((t) => {
990
+ const lowerUrl = (t.url || "").toLowerCase();
991
+ return lowerUrl.includes("linkegringo") || lowerUrl.includes("5173") || lowerUrl.includes("muriel-gasparini.github.io");
992
+ });
993
+ return {
994
+ isRunning: true,
995
+ port,
996
+ host,
997
+ browser: versionData.Browser,
998
+ protocolVersion: versionData["Protocol-Version"],
999
+ activeTabs: pageTabs.map((t) => ({
1000
+ id: t.id,
1001
+ title: t.title,
1002
+ url: t.url,
1003
+ webSocketDebuggerUrl: t.webSocketDebuggerUrl
1004
+ })),
1005
+ linkeGringoTabFound: Boolean(linkeGringoTab),
1006
+ linkeGringoTabUrl: linkeGringoTab?.url
1007
+ };
1008
+ } catch (err) {
1009
+ const error = err;
1010
+ const isTimeout = error.name === "AbortError" || error.name === "TimeoutError";
1011
+ return {
1012
+ isRunning: false,
1013
+ port,
1014
+ host,
1015
+ activeTabs: [],
1016
+ linkeGringoTabFound: false,
1017
+ error: isTimeout ? "Conex\xE3o expirou (Chrome n\xE3o respondeu em 2s na porta " + port + ")" : "Porta fechada ou depura\xE7\xE3o remota desativada. Acesse chrome://inspect/#remote-debugging para ativar."
1018
+ };
1019
+ } finally {
1020
+ clearTimeout(timeoutId);
1021
+ }
1022
+ }
1023
+
1024
+ // src/tools/cdp-check.ts
1025
+ var checkChromeCdpInputSchema = z11.object({
1026
+ port: z11.number().default(9222).describe("Porta do Chrome DevTools Protocol a ser testada (padr\xE3o 9222)"),
1027
+ host: z11.string().default("127.0.0.1").describe("Host do Chrome (padr\xE3o 127.0.0.1)"),
1028
+ timeoutMs: z11.number().default(2e3).describe("Tempo limite em milissegundos para a conex\xE3o")
1029
+ });
1030
+ async function handleCheckChromeCdp(input) {
1031
+ const status = await checkChromeCdp(input.port, input.host, input.timeoutMs);
1032
+ const markdownSummary = `
1033
+ # Status do Chrome Remote Debugging (CDP)
1034
+
1035
+ **Porta Testada**: ${status.host}:${status.port}
1036
+ **Status da Conex\xE3o**: ${status.isRunning ? "\u{1F7E2} Conectado e Ativo" : "\u{1F534} Desconectado"}
1037
+
1038
+ ${status.isRunning ? `
1039
+ - **Vers\xE3o do Navegador**: ${status.browser || "Desconhecido"}
1040
+ - **Vers\xE3o do Protocolo DevTools**: ${status.protocolVersion || "1.3"}
1041
+ - **Total de Abas Abertas**: ${status.activeTabs.length}
1042
+ - **Aba do LinkeGringo**: ${status.linkeGringoTabFound ? `\u2713 Detectada (${status.linkeGringoTabUrl})` : "\u26A0\uFE0F Nenhuma aba do LinkeGringo aberta no momento"}
1043
+
1044
+ ${status.activeTabs.length > 0 ? `### Abas Encontradas:
1045
+ ${status.activeTabs.map((t) => `- [${t.title}](${t.url})`).join("\n")}` : ""}
1046
+ ` : `
1047
+ > \u274C **Motivo**: ${status.error || "Porta fechada."}
1048
+ >
1049
+ > **Como ativar no Google Chrome (M144+)**:
1050
+ > 1. Abra uma nova aba e acesse: \`chrome://inspect/#remote-debugging\`
1051
+ > 2. Marque a op\xE7\xE3o para **Ativar depura\xE7\xE3o remota**.
1052
+ > 3. Se estiver usando o servidor oficial DevTools MCP, configure com \`--autoConnect\`.
1053
+ `}
1054
+ `.trim();
1055
+ return {
1056
+ content: [
1057
+ {
1058
+ type: "text",
1059
+ text: markdownSummary
1060
+ }
1061
+ ],
1062
+ structuredData: status
1063
+ };
1064
+ }
1065
+
1066
+ // src/server.ts
1067
+ function createLinkeGringoMcpServer() {
1068
+ const server = new McpServer({
1069
+ name: "linkegringo-mcp",
1070
+ version: "1.0.0"
1071
+ });
1072
+ server.registerTool(
1073
+ "audit_profile",
1074
+ {
1075
+ description: "Audita um perfil de LinkedIn (via caminho de PDF, base64 ou texto) contra os crit\xE9rios de contrata\xE7\xE3o de empresas tech dos EUA. Retorna nota Inbound (0-100), gargalos de triagem de recrutadores e lacunas de stack.",
1076
+ inputSchema: auditProfileInputSchema.shape
1077
+ },
1078
+ async (args) => {
1079
+ return await handleAuditProfile(args);
1080
+ }
1081
+ );
1082
+ server.registerTool(
1083
+ "simulate_recruiter_search",
1084
+ {
1085
+ description: "Simula buscas booleanas e algoritmos do LinkedIn Recruiter ATS. Avalia a presen\xE7a de palavras-chave com peso 3x em Headline/Skills e peso 1x em experi\xEAncias, calculando a probabilidade de indexa\xE7\xE3o.",
1086
+ inputSchema: simulateRecruiterSearchInputSchema.shape
1087
+ },
1088
+ async (args) => {
1089
+ return await handleSimulateRecruiterSearch(args);
1090
+ }
1091
+ );
1092
+ server.registerTool(
1093
+ "convert_to_xyz_bullet",
1094
+ {
1095
+ description: "Transforma descri\xE7\xF5es gen\xE9ricas de atividades em bullets de alto impacto seguindo a f\xF3rmula oficial do Google: Accomplished [X], measured by [Y], by doing [Z].",
1096
+ inputSchema: convertToXyzBulletInputSchema.shape
1097
+ },
1098
+ async (args) => {
1099
+ return await handleConvertToXyzBullet(args);
1100
+ }
1101
+ );
1102
+ server.registerTool(
1103
+ "generate_headline_proposals",
1104
+ {
1105
+ description: "Gera propostas de Headline (t\xEDtulo) no LinkedIn com at\xE9 160 caracteres, calibradas para visualiza\xE7\xE3o sem cortes no Desktop e Mobile e alta indexa\xE7\xE3o de busca por recrutadores gringos.",
1106
+ inputSchema: generateHeadlineInputSchema.shape
1107
+ },
1108
+ async (args) => {
1109
+ return await handleGenerateHeadline(args);
1110
+ }
1111
+ );
1112
+ server.registerTool(
1113
+ "check_chrome_cdp_status",
1114
+ {
1115
+ description: "Verifica se o Google Chrome est\xE1 com a depura\xE7\xE3o remota (CDP) ativada (via chrome://inspect/#remote-debugging ou porta 9222) e lista as abas dispon\xEDveis, verificando se o LinkeGringo est\xE1 aberto.",
1116
+ inputSchema: checkChromeCdpInputSchema.shape
1117
+ },
1118
+ async (args) => {
1119
+ return await handleCheckChromeCdp(args);
1120
+ }
1121
+ );
1122
+ server.registerResource(
1123
+ "guidelines",
1124
+ "linkegringo://guidelines",
1125
+ {
1126
+ title: "Diretrizes Oficiais do LinkeGringo para Vagas nos EUA",
1127
+ description: "Princ\xEDpios fundamentais: f\xF3rmula Google XYZ, headlines de at\xE9 160 caracteres, elimina\xE7\xE3o de red flags culturais brasileiras e maximiza\xE7\xE3o de Inbound Readiness.",
1128
+ mimeType: "text/markdown"
1129
+ },
1130
+ async () => {
1131
+ return {
1132
+ contents: [
1133
+ {
1134
+ uri: "linkegringo://guidelines",
1135
+ text: `
1136
+ # Diretrizes Oficiais LinkeGringo: Otimiza\xE7\xE3o de Perfil para Recrutadores dos EUA
1137
+
1138
+ 1. **Headline $le$ 160 caracteres**:
1139
+ - Formato recomendado: \`[Cargo Espec\xEDfico] | [3-4 Tecnologias Core] | [Escala/Dom\xEDnio] | US Remote\`
1140
+ - Evite slogans vagos ("Apaixonado por tecnologia", "Resolvendo problemas complexos").
1141
+ - Headline tem peso 3x no algoritmo de busca do LinkedIn Recruiter.
1142
+
1143
+ 2. **F\xF3rmula Google XYZ para Experi\xEAncias**:
1144
+ - Toda conquista deve responder: *"Accomplished [X], measured by [Y], by doing [Z]"*.
1145
+ - Exemplo: *"Architected distributed event-driven payment service in Go, reducing p99 latency by 42% and scaling to 15,000 requests/sec."*
1146
+
1147
+ 3. **Incentivo a Inbound (Ser Descoberto)**:
1148
+ - Recrutadores usam filtros booleanos estritos. Se "Senior Software Engineer" e "Go" n\xE3o estiverem no t\xEDtulo da experi\xEAncia atual ou na headline, voc\xEA n\xE3o entra no funil inicial.
1149
+ - Elimine red flags de localiza\xE7\xE3o restrita e declare disponibilidade para contratos internacionais (W-8BEN / PJ Internacional / B2B).
1150
+ `.trim()
1151
+ }
1152
+ ]
1153
+ };
1154
+ }
1155
+ );
1156
+ return server;
1157
+ }
1158
+
1159
+ // src/cli/installer.ts
1160
+ import fs from "fs";
1161
+ import path from "path";
1162
+ import os from "os";
1163
+ function getMcpConfigsForSystem() {
1164
+ const home = os.homedir();
1165
+ const platform = os.platform();
1166
+ const configs = [];
1167
+ configs.push({
1168
+ client: "Google Antigravity",
1169
+ configPath: path.join(home, ".gemini", "config", "mcp_config.json")
1170
+ });
1171
+ if (platform === "darwin") {
1172
+ configs.push({
1173
+ client: "Claude Desktop (macOS)",
1174
+ configPath: path.join(
1175
+ home,
1176
+ "Library",
1177
+ "Application Support",
1178
+ "Claude",
1179
+ "claude_desktop_config.json"
1180
+ )
1181
+ });
1182
+ } else if (platform === "win32") {
1183
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
1184
+ configs.push({
1185
+ client: "Claude Desktop (Windows)",
1186
+ configPath: path.join(appData, "Claude", "claude_desktop_config.json")
1187
+ });
1188
+ } else {
1189
+ configs.push({
1190
+ client: "Claude Desktop (Linux)",
1191
+ configPath: path.join(home, ".config", "Claude", "claude_desktop_config.json")
1192
+ });
1193
+ }
1194
+ configs.push({
1195
+ client: "Cursor AI",
1196
+ configPath: path.join(home, ".cursor", "mcp.json")
1197
+ });
1198
+ return configs;
1199
+ }
1200
+ function installMcpServerConfig(configPath) {
1201
+ const dir = path.dirname(configPath);
1202
+ if (!fs.existsSync(dir)) {
1203
+ fs.mkdirSync(dir, { recursive: true });
1204
+ }
1205
+ let configData = { mcpServers: {} };
1206
+ let isNew = true;
1207
+ if (fs.existsSync(configPath)) {
1208
+ try {
1209
+ const raw = fs.readFileSync(configPath, "utf8");
1210
+ if (raw.trim()) {
1211
+ configData = JSON.parse(raw);
1212
+ isNew = false;
1213
+ }
1214
+ } catch {
1215
+ configData = { mcpServers: {} };
1216
+ }
1217
+ }
1218
+ if (!configData.mcpServers || typeof configData.mcpServers !== "object") {
1219
+ configData.mcpServers = {};
1220
+ }
1221
+ configData.mcpServers["linkegringo"] = {
1222
+ command: "npx",
1223
+ args: ["-y", "@linkegringo/mcp"]
1224
+ };
1225
+ configData.mcpServers["chrome-devtools"] = {
1226
+ command: "npx",
1227
+ args: ["-y", "chrome-devtools-mcp@latest", "--autoConnect"]
1228
+ };
1229
+ fs.writeFileSync(configPath, JSON.stringify(configData, null, 2) + "\n", "utf8");
1230
+ return {
1231
+ status: isNew ? "created" : "updated",
1232
+ path: configPath
1233
+ };
1234
+ }
1235
+ function runInstaller() {
1236
+ console.log("\n\u{1F680} LinkeGringo MCP - Instalador Autom\xE1tico");
1237
+ console.log("================================================");
1238
+ console.log("Configurando servidores em todos os clientes locais:\n");
1239
+ const targets = getMcpConfigsForSystem();
1240
+ const results = [];
1241
+ for (const target of targets) {
1242
+ try {
1243
+ const res = installMcpServerConfig(target.configPath);
1244
+ results.push({
1245
+ client: target.client,
1246
+ configPath: target.configPath,
1247
+ status: res.status
1248
+ });
1249
+ console.log(`\u2705 [${target.client}]`);
1250
+ console.log(` Arquivo: ${target.configPath} (${res.status === "created" ? "Criado" : "Atualizado"})
1251
+ `);
1252
+ } catch (err) {
1253
+ results.push({
1254
+ client: target.client,
1255
+ configPath: target.configPath,
1256
+ status: "error",
1257
+ message: err.message
1258
+ });
1259
+ console.warn(`\u26A0\uFE0F [${target.client}] N\xE3o foi poss\xEDvel atualizar: ${err.message}
1260
+ `);
1261
+ }
1262
+ }
1263
+ console.log("---");
1264
+ console.log("\u{1F4A1} Comandos One-Line diretos para agentes de linha de comando (CLI):");
1265
+ console.log(" \u2022 Antigravity CLI: agy mcp add linkegringo npx -y @linkegringo/mcp");
1266
+ console.log(" \u2022 Codex CLI: codex mcp add linkegringo -- npx -y @linkegringo/mcp");
1267
+ console.log(" \u2022 Claude Code CLI: claude mcp add linkegringo npx -y @linkegringo/mcp");
1268
+ console.log(' \u2022 Goose CLI: goose configure --add-extension "npx -y @linkegringo/mcp"');
1269
+ console.log("================================================");
1270
+ console.log("\u{1F389} Instala\xE7\xE3o conclu\xEDda! Reinicie o Claude Desktop, Antigravity ou Cursor para ativar.\n");
1271
+ return results;
1272
+ }
1273
+
1274
+ // src/index.ts
1275
+ async function main() {
1276
+ if (process.argv.includes("install") || process.argv.includes("setup") || process.argv.includes("--install")) {
1277
+ runInstaller();
1278
+ return;
1279
+ }
1280
+ const server = createLinkeGringoMcpServer();
1281
+ const transport = new StdioServerTransport();
1282
+ await server.connect(transport);
1283
+ console.error("[LinkeGringo MCP] Servidor iniciado com sucesso via stdio.");
1284
+ }
1285
+ if (import.meta.url === `file://${process.argv[1]}`) {
1286
+ main().catch((err) => {
1287
+ console.error("[LinkeGringo MCP] Erro fatal na inicializa\xE7\xE3o:", err);
1288
+ process.exit(1);
1289
+ });
1290
+ }
1291
+ export {
1292
+ checkChromeCdp,
1293
+ createLinkeGringoMcpServer,
1294
+ getMcpConfigsForSystem,
1295
+ installMcpServerConfig,
1296
+ runInstaller
1297
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@linkegringo/mcp",
3
+ "version": "1.0.0",
4
+ "description": "Servidor MCP oficial do LinkeGringo para auditoria e otimização de perfis para o mercado internacional",
5
+ "type": "module",
6
+ "bin": {
7
+ "linkegringo-mcp": "dist/index.js",
8
+ "mcp": "dist/index.js"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "dev": "tsup --watch",
21
+ "typecheck": "tsc --noEmit",
22
+ "test": "vitest run"
23
+ },
24
+ "dependencies": {
25
+ "@linkegringo/core": "workspace:*",
26
+ "@modelcontextprotocol/sdk": "^1.6.0",
27
+ "zod": "^3.24.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^22.10.2",
31
+ "tsup": "^8.3.6",
32
+ "typescript": "^5.7.2",
33
+ "vitest": "^2.1.8"
34
+ }
35
+ }