@linkegringo/mcp 1.0.3 → 1.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +313 -7
  2. package/dist/index.js +385 -104
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { Experience } from '@linkegringo/core';
2
4
 
3
5
  declare function createLinkeGringoMcpServer(): McpServer;
4
6
 
@@ -19,23 +21,50 @@ interface ChromeTabInfo {
19
21
  webSocketDebuggerUrl?: string;
20
22
  devtoolsFrontendUrl?: string;
21
23
  }
24
+ interface LinkeGringoTabInfo {
25
+ id: string;
26
+ title: string;
27
+ url: string;
28
+ webSocketDebuggerUrl?: string;
29
+ }
30
+ interface LinkeGringoSessionState {
31
+ hasUploadedProfile: boolean;
32
+ candidateName?: string;
33
+ targetRole?: string;
34
+ step?: string;
35
+ inboundScore?: number;
36
+ profile?: any;
37
+ review?: any;
38
+ }
22
39
  interface CdpStatus {
23
40
  isRunning: boolean;
24
41
  port: number;
25
42
  host: string;
26
43
  browser?: string;
27
44
  protocolVersion?: string;
28
- activeTabs: Array<{
29
- id: string;
30
- title: string;
31
- url: string;
32
- webSocketDebuggerUrl?: string;
33
- }>;
45
+ /**
46
+ * Abas ativas restritas estritamente ao escopo do LinkeGringo (Privacy Shield).
47
+ * Abas pessoais (e-mails, mensageiros, bancos) são completamente omitidas.
48
+ */
49
+ activeTabs: LinkeGringoTabInfo[];
50
+ linkeGringoTabs: LinkeGringoTabInfo[];
51
+ otherTabsCount: number;
34
52
  linkeGringoTabFound: boolean;
35
53
  linkeGringoTabUrl?: string;
54
+ sessionState?: LinkeGringoSessionState | null;
36
55
  error?: string;
37
56
  }
38
57
 
58
+ declare function isLinkeGringoUrl(url?: string): boolean;
59
+ declare function findDevToolsActivePort(): {
60
+ port: number;
61
+ wsPath: string;
62
+ } | null;
63
+ declare function probeViaWebSocket(port: number, wsPath: string, timeoutMs?: number): Promise<{
64
+ linkeGringoTabs: LinkeGringoTabInfo[];
65
+ otherTabsCount: number;
66
+ sessionState: LinkeGringoSessionState | null;
67
+ } | null>;
39
68
  declare function checkChromeCdp(port?: number, host?: string, timeoutMs?: number): Promise<CdpStatus>;
40
69
 
41
70
  interface McpTarget {
@@ -63,4 +92,281 @@ declare function installMcpServerConfig(configPath: string): {
63
92
  declare function parseArgs(args?: string[]): InstallerOptions;
64
93
  declare function runInstaller(args?: string[]): InstallResult[];
65
94
 
66
- export { type CdpStatus, type ChromeTabInfo, type ChromeVersionResponse, type InstallResult, type InstallerOptions, type McpTarget, checkChromeCdp, createLinkeGringoMcpServer, getMcpConfigsForSystem, installMcpServerConfig, parseArgs, runInstaller };
95
+ interface SparseExperience {
96
+ company: string;
97
+ title: string;
98
+ estimatedBullets: number;
99
+ }
100
+ declare function getExperienceBulletCount(exp: {
101
+ description?: string;
102
+ bullets?: string[];
103
+ }): number;
104
+ declare function detectSparseExperiences(experiences?: Experience[]): SparseExperience[];
105
+ declare const auditProfileInputSchema: z.ZodObject<{
106
+ profileText: z.ZodOptional<z.ZodString>;
107
+ headline: z.ZodOptional<z.ZodString>;
108
+ summary: z.ZodOptional<z.ZodString>;
109
+ experiences: z.ZodOptional<z.ZodArray<z.ZodObject<{
110
+ company: z.ZodString;
111
+ title: z.ZodString;
112
+ bullets: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
113
+ description: z.ZodOptional<z.ZodString>;
114
+ }, "strip", z.ZodTypeAny, {
115
+ title: string;
116
+ company: string;
117
+ description?: string | undefined;
118
+ bullets?: string[] | undefined;
119
+ }, {
120
+ title: string;
121
+ company: string;
122
+ description?: string | undefined;
123
+ bullets?: string[] | undefined;
124
+ }>, "many">>;
125
+ skills: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
126
+ targetRole: z.ZodDefault<z.ZodString>;
127
+ targetMarket: z.ZodDefault<z.ZodString>;
128
+ }, "strip", z.ZodTypeAny, {
129
+ targetMarket: string;
130
+ targetRole: string;
131
+ headline?: string | undefined;
132
+ summary?: string | undefined;
133
+ experiences?: {
134
+ title: string;
135
+ company: string;
136
+ description?: string | undefined;
137
+ bullets?: string[] | undefined;
138
+ }[] | undefined;
139
+ skills?: string[] | undefined;
140
+ profileText?: string | undefined;
141
+ }, {
142
+ headline?: string | undefined;
143
+ summary?: string | undefined;
144
+ experiences?: {
145
+ title: string;
146
+ company: string;
147
+ description?: string | undefined;
148
+ bullets?: string[] | undefined;
149
+ }[] | undefined;
150
+ skills?: string[] | undefined;
151
+ targetMarket?: string | undefined;
152
+ targetRole?: string | undefined;
153
+ profileText?: string | undefined;
154
+ }>;
155
+ type AuditProfileInput = z.infer<typeof auditProfileInputSchema>;
156
+ interface AuditIssue {
157
+ severity: 'critical' | 'warning' | 'info';
158
+ category: 'headline' | 'experience' | 'about' | 'skills';
159
+ message: string;
160
+ recommendation: string;
161
+ }
162
+ declare function handleAuditProfile(input: AuditProfileInput): Promise<{
163
+ content: {
164
+ type: "text";
165
+ text: string;
166
+ }[];
167
+ structuredData: {
168
+ status: string;
169
+ linkeGringoTabUrl: string | undefined;
170
+ message: string;
171
+ actionRequired: string;
172
+ score?: undefined;
173
+ targetRole?: undefined;
174
+ headline?: undefined;
175
+ totalBullets?: undefined;
176
+ bulletsWithMetrics?: undefined;
177
+ sparseExperiences?: undefined;
178
+ triageBottlenecks?: undefined;
179
+ issues?: undefined;
180
+ };
181
+ } | {
182
+ content: {
183
+ type: "text";
184
+ text: string;
185
+ }[];
186
+ structuredData: {
187
+ status: string;
188
+ message: string;
189
+ actionRequired: string;
190
+ linkeGringoTabUrl?: undefined;
191
+ score?: undefined;
192
+ targetRole?: undefined;
193
+ headline?: undefined;
194
+ totalBullets?: undefined;
195
+ bulletsWithMetrics?: undefined;
196
+ sparseExperiences?: undefined;
197
+ triageBottlenecks?: undefined;
198
+ issues?: undefined;
199
+ };
200
+ } | {
201
+ content: {
202
+ type: "text";
203
+ text: string;
204
+ }[];
205
+ structuredData: {
206
+ score: number;
207
+ targetRole: string;
208
+ headline: string;
209
+ totalBullets: number;
210
+ bulletsWithMetrics: number;
211
+ sparseExperiences: SparseExperience[];
212
+ triageBottlenecks: string[];
213
+ issues: AuditIssue[];
214
+ status?: undefined;
215
+ linkeGringoTabUrl?: undefined;
216
+ message?: undefined;
217
+ actionRequired?: undefined;
218
+ };
219
+ }>;
220
+
221
+ declare const simulateRecruiterSearchInputSchema: z.ZodObject<{
222
+ headline: z.ZodString;
223
+ summary: z.ZodDefault<z.ZodString>;
224
+ skills: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
225
+ experienceBullets: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
226
+ targetRole: z.ZodDefault<z.ZodString>;
227
+ requiredKeywords: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
228
+ }, "strip", z.ZodTypeAny, {
229
+ headline: string;
230
+ summary: string;
231
+ skills: string[];
232
+ targetRole: string;
233
+ experienceBullets: string[];
234
+ requiredKeywords?: string[] | undefined;
235
+ }, {
236
+ headline: string;
237
+ summary?: string | undefined;
238
+ skills?: string[] | undefined;
239
+ targetRole?: string | undefined;
240
+ experienceBullets?: string[] | undefined;
241
+ requiredKeywords?: string[] | undefined;
242
+ }>;
243
+ type SimulateRecruiterSearchInput = z.infer<typeof simulateRecruiterSearchInputSchema>;
244
+ declare function handleSimulateRecruiterSearch(input: SimulateRecruiterSearchInput): Promise<{
245
+ content: {
246
+ type: "text";
247
+ text: string;
248
+ }[];
249
+ structuredData: {
250
+ overallStatus: "weak" | "missing" | "match";
251
+ matchPercentage: number;
252
+ evaluations: {
253
+ term: string;
254
+ status: "weak" | "missing" | "match";
255
+ detail: string;
256
+ }[];
257
+ matchCount: number;
258
+ weakCount: number;
259
+ missingCount: number;
260
+ };
261
+ }>;
262
+
263
+ declare const convertToXyzBulletInputSchema: z.ZodObject<{
264
+ rawBullet: z.ZodString;
265
+ roleContext: z.ZodDefault<z.ZodString>;
266
+ action: z.ZodOptional<z.ZodString>;
267
+ metric: z.ZodOptional<z.ZodString>;
268
+ method: z.ZodOptional<z.ZodString>;
269
+ }, "strip", z.ZodTypeAny, {
270
+ rawBullet: string;
271
+ roleContext: string;
272
+ method?: string | undefined;
273
+ metric?: string | undefined;
274
+ action?: string | undefined;
275
+ }, {
276
+ rawBullet: string;
277
+ method?: string | undefined;
278
+ metric?: string | undefined;
279
+ roleContext?: string | undefined;
280
+ action?: string | undefined;
281
+ }>;
282
+ type ConvertToXyzBulletInput = z.infer<typeof convertToXyzBulletInputSchema>;
283
+ declare function formatGoogleXyzBullet(parts: {
284
+ action: string;
285
+ metric: string;
286
+ method: string;
287
+ }): string;
288
+ declare function handleConvertToXyzBullet(input: ConvertToXyzBulletInput): Promise<{
289
+ content: {
290
+ type: "text";
291
+ text: string;
292
+ }[];
293
+ structuredData: {
294
+ formattedBullet: string;
295
+ parts: {
296
+ action: string;
297
+ metric: string;
298
+ method: string;
299
+ };
300
+ hasMetrics?: undefined;
301
+ rawBullet?: undefined;
302
+ proposals?: undefined;
303
+ };
304
+ } | {
305
+ content: {
306
+ type: "text";
307
+ text: string;
308
+ }[];
309
+ structuredData: {
310
+ hasMetrics: boolean;
311
+ rawBullet: string;
312
+ proposals: string[];
313
+ formattedBullet?: undefined;
314
+ parts?: undefined;
315
+ };
316
+ }>;
317
+
318
+ declare const generateHeadlineInputSchema: z.ZodObject<{
319
+ targetRole: z.ZodDefault<z.ZodString>;
320
+ coreTechnologies: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
321
+ keyDifferentiator: z.ZodOptional<z.ZodString>;
322
+ seniorityOrScope: z.ZodDefault<z.ZodString>;
323
+ }, "strip", z.ZodTypeAny, {
324
+ targetRole: string;
325
+ coreTechnologies: string[];
326
+ seniorityOrScope: string;
327
+ keyDifferentiator?: string | undefined;
328
+ }, {
329
+ targetRole?: string | undefined;
330
+ coreTechnologies?: string[] | undefined;
331
+ keyDifferentiator?: string | undefined;
332
+ seniorityOrScope?: string | undefined;
333
+ }>;
334
+ type GenerateHeadlineInput = z.infer<typeof generateHeadlineInputSchema>;
335
+ declare function handleGenerateHeadline(input: GenerateHeadlineInput): Promise<{
336
+ content: {
337
+ type: "text";
338
+ text: string;
339
+ }[];
340
+ structuredData: {
341
+ proposals: {
342
+ type: string;
343
+ headline: string;
344
+ charCount: number;
345
+ focus: string;
346
+ }[];
347
+ };
348
+ }>;
349
+
350
+ declare const checkChromeCdpInputSchema: z.ZodObject<{
351
+ port: z.ZodDefault<z.ZodNumber>;
352
+ host: z.ZodDefault<z.ZodString>;
353
+ timeoutMs: z.ZodDefault<z.ZodNumber>;
354
+ }, "strip", z.ZodTypeAny, {
355
+ port: number;
356
+ host: string;
357
+ timeoutMs: number;
358
+ }, {
359
+ port?: number | undefined;
360
+ host?: string | undefined;
361
+ timeoutMs?: number | undefined;
362
+ }>;
363
+ type CheckChromeCdpInput = z.infer<typeof checkChromeCdpInputSchema>;
364
+ declare function handleCheckChromeCdp(input: CheckChromeCdpInput): Promise<{
365
+ content: {
366
+ type: "text";
367
+ text: string;
368
+ }[];
369
+ structuredData: CdpStatus;
370
+ }>;
371
+
372
+ export { type AuditIssue, type AuditProfileInput, type CdpStatus, type CheckChromeCdpInput, type ChromeTabInfo, type ChromeVersionResponse, type ConvertToXyzBulletInput, type GenerateHeadlineInput, type InstallResult, type InstallerOptions, type LinkeGringoSessionState, type LinkeGringoTabInfo, type McpTarget, type SimulateRecruiterSearchInput, type SparseExperience, auditProfileInputSchema, checkChromeCdp, checkChromeCdpInputSchema, convertToXyzBulletInputSchema, createLinkeGringoMcpServer, detectSparseExperiences, findDevToolsActivePort, formatGoogleXyzBullet, generateHeadlineInputSchema, getExperienceBulletCount, getMcpConfigsForSystem, handleAuditProfile, handleCheckChromeCdp, handleConvertToXyzBullet, handleGenerateHeadline, handleSimulateRecruiterSearch, installMcpServerConfig, isLinkeGringoUrl, parseArgs, probeViaWebSocket, runInstaller, simulateRecruiterSearchInputSchema };
package/dist/index.js CHANGED
@@ -8,6 +8,233 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
 
9
9
  // src/tools/audit-profile.ts
10
10
  import { z } from "zod";
11
+
12
+ // src/cdp/probe.ts
13
+ import fs from "fs";
14
+ import os from "os";
15
+ import path from "path";
16
+ function isLinkeGringoUrl(url = "") {
17
+ const lower = url.toLowerCase();
18
+ return lower.includes("localhost:5173") || lower.includes("127.0.0.1:5173") || lower.includes("localhost:4173") || lower.includes("127.0.0.1:4173") || lower.includes("linkegringo") || lower.includes("muriel-gasparini.github.io");
19
+ }
20
+ function findDevToolsActivePort() {
21
+ const possiblePaths = [
22
+ // Linux
23
+ path.join(os.homedir(), ".config", "google-chrome", "DevToolsActivePort"),
24
+ path.join(os.homedir(), ".config", "chromium", "DevToolsActivePort"),
25
+ path.join(os.homedir(), ".config", "google-chrome-beta", "DevToolsActivePort"),
26
+ // macOS
27
+ path.join(os.homedir(), "Library", "Application Support", "Google", "Chrome", "DevToolsActivePort"),
28
+ // Windows
29
+ path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "User Data", "DevToolsActivePort")
30
+ ];
31
+ for (const p of possiblePaths) {
32
+ if (p && fs.existsSync(p)) {
33
+ try {
34
+ const lines = fs.readFileSync(p, "utf8").trim().split("\n");
35
+ if (lines.length >= 2) {
36
+ const port = parseInt(lines[0].trim(), 10);
37
+ const wsPath = lines[1].trim();
38
+ if (port > 0 && wsPath) {
39
+ return { port, wsPath };
40
+ }
41
+ }
42
+ } catch {
43
+ }
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ async function probeViaWebSocket(port, wsPath, timeoutMs = 2e3) {
49
+ const wsUrl = `ws://127.0.0.1:${port}${wsPath}`;
50
+ if (typeof globalThis.WebSocket !== "function") return null;
51
+ return new Promise((resolve) => {
52
+ let ws;
53
+ const timer = setTimeout(() => {
54
+ try {
55
+ ws?.close();
56
+ } catch {
57
+ }
58
+ resolve(null);
59
+ }, timeoutMs);
60
+ try {
61
+ ws = new globalThis.WebSocket(wsUrl);
62
+ } catch {
63
+ clearTimeout(timer);
64
+ resolve(null);
65
+ return;
66
+ }
67
+ let linkeTarget = null;
68
+ let linkeGringoTabs = [];
69
+ let otherTabsCount = 0;
70
+ ws.onopen = () => {
71
+ ws.send(JSON.stringify({ id: 1, method: "Target.getTargets" }));
72
+ };
73
+ ws.onmessage = (event) => {
74
+ try {
75
+ const msg = JSON.parse(String(event.data));
76
+ if (msg.id === 1) {
77
+ const targets = msg.result?.targetInfos || [];
78
+ const pageTargets = targets.filter((t) => t.type === "page");
79
+ const matched = pageTargets.filter((t) => isLinkeGringoUrl(t.url));
80
+ linkeGringoTabs = matched.map((t) => ({
81
+ id: t.targetId,
82
+ title: t.title,
83
+ url: t.url
84
+ }));
85
+ otherTabsCount = pageTargets.length - matched.length;
86
+ if (linkeGringoTabs.length === 0) {
87
+ clearTimeout(timer);
88
+ ws.close();
89
+ resolve({ linkeGringoTabs: [], otherTabsCount, sessionState: null });
90
+ return;
91
+ }
92
+ linkeTarget = matched[0];
93
+ ws.send(
94
+ JSON.stringify({
95
+ id: 2,
96
+ method: "Target.attachToTarget",
97
+ params: { targetId: linkeTarget.targetId, flatten: true }
98
+ })
99
+ );
100
+ } else if (msg.id === 2) {
101
+ const sessionId = msg.result?.sessionId;
102
+ ws.send(
103
+ JSON.stringify({
104
+ id: 3,
105
+ sessionId,
106
+ method: "Runtime.evaluate",
107
+ params: {
108
+ expression: 'window.localStorage.getItem("linkegringo_active_session")',
109
+ returnByValue: true
110
+ }
111
+ })
112
+ );
113
+ } else if (msg.id === 3) {
114
+ clearTimeout(timer);
115
+ ws.close();
116
+ const raw = msg.result?.result?.value;
117
+ let session = null;
118
+ try {
119
+ if (raw) session = JSON.parse(raw);
120
+ } catch {
121
+ }
122
+ const hasUploadedProfile = Boolean(session && session.profile);
123
+ const sessionState = {
124
+ hasUploadedProfile,
125
+ candidateName: session?.profile?.name,
126
+ targetRole: session?.profile?.targetRole || session?.objective,
127
+ step: session?.step || (hasUploadedProfile ? "diagnostic" : "upload"),
128
+ inboundScore: session?.review?.inboundReadinessScore,
129
+ profile: session?.profile || null,
130
+ review: session?.review || null
131
+ };
132
+ resolve({
133
+ linkeGringoTabs,
134
+ otherTabsCount,
135
+ sessionState
136
+ });
137
+ }
138
+ } catch {
139
+ clearTimeout(timer);
140
+ try {
141
+ ws?.close();
142
+ } catch {
143
+ }
144
+ resolve(null);
145
+ }
146
+ };
147
+ ws.onerror = () => {
148
+ clearTimeout(timer);
149
+ resolve(null);
150
+ };
151
+ });
152
+ }
153
+ async function checkChromeCdp(port = 9222, host = "127.0.0.1", timeoutMs = 5e3) {
154
+ const activePortData = findDevToolsActivePort();
155
+ if (activePortData) {
156
+ const wsResult = await probeViaWebSocket(activePortData.port, activePortData.wsPath, timeoutMs);
157
+ if (wsResult) {
158
+ const found = wsResult.linkeGringoTabs.length > 0;
159
+ return {
160
+ isRunning: true,
161
+ port: activePortData.port,
162
+ host: "127.0.0.1",
163
+ browser: "Google Chrome (DevTools Protocol)",
164
+ protocolVersion: "1.3",
165
+ activeTabs: wsResult.linkeGringoTabs,
166
+ linkeGringoTabs: wsResult.linkeGringoTabs,
167
+ otherTabsCount: wsResult.otherTabsCount,
168
+ linkeGringoTabFound: found,
169
+ linkeGringoTabUrl: wsResult.linkeGringoTabs[0]?.url,
170
+ sessionState: wsResult.sessionState
171
+ };
172
+ }
173
+ }
174
+ const controller = new AbortController();
175
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
176
+ try {
177
+ const versionRes = await fetch(`http://${host}:${port}/json/version`, {
178
+ signal: controller.signal
179
+ });
180
+ if (!versionRes.ok) {
181
+ throw new Error(`HTTP ${versionRes.status}: ${versionRes.statusText}`);
182
+ }
183
+ const versionData = await versionRes.json();
184
+ let pageTabs = [];
185
+ try {
186
+ const listRes = await fetch(`http://${host}:${port}/json/list`, {
187
+ signal: controller.signal
188
+ });
189
+ if (listRes.ok) {
190
+ const rawTabs = await listRes.json();
191
+ if (Array.isArray(rawTabs)) {
192
+ pageTabs = rawTabs.filter((t) => t.type === "page");
193
+ }
194
+ }
195
+ } catch {
196
+ }
197
+ const linkeGringoTabs = pageTabs.filter((t) => isLinkeGringoUrl(t.url)).map((t) => ({
198
+ id: t.id,
199
+ title: t.title,
200
+ url: t.url,
201
+ webSocketDebuggerUrl: t.webSocketDebuggerUrl
202
+ }));
203
+ const otherTabsCount = pageTabs.length - linkeGringoTabs.length;
204
+ const found = linkeGringoTabs.length > 0;
205
+ return {
206
+ isRunning: true,
207
+ port,
208
+ host,
209
+ browser: versionData.Browser,
210
+ protocolVersion: versionData["Protocol-Version"],
211
+ activeTabs: linkeGringoTabs,
212
+ linkeGringoTabs,
213
+ otherTabsCount,
214
+ linkeGringoTabFound: found,
215
+ linkeGringoTabUrl: linkeGringoTabs[0]?.url,
216
+ sessionState: null
217
+ };
218
+ } catch (err) {
219
+ const error = err;
220
+ const isTimeout = error.name === "AbortError" || error.name === "TimeoutError";
221
+ return {
222
+ isRunning: false,
223
+ port,
224
+ host,
225
+ activeTabs: [],
226
+ linkeGringoTabs: [],
227
+ otherTabsCount: 0,
228
+ linkeGringoTabFound: false,
229
+ sessionState: null,
230
+ 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."
231
+ };
232
+ } finally {
233
+ clearTimeout(timeoutId);
234
+ }
235
+ }
236
+
237
+ // src/tools/audit-profile.ts
11
238
  function getExperienceBulletCount(exp) {
12
239
  if (Array.isArray(exp.bullets) && exp.bullets.length > 0) {
13
240
  return exp.bullets.length;
@@ -57,7 +284,77 @@ async function handleAuditProfile(input) {
57
284
  let headline = input.headline || "";
58
285
  let summary = input.summary || "";
59
286
  let skills = input.skills || [];
60
- const rawExperiences = input.experiences || [];
287
+ const rawExperiences = [...input.experiences || []];
288
+ if (!headline && !input.profileText && rawExperiences.length === 0) {
289
+ try {
290
+ const cdp = await checkChromeCdp();
291
+ if (cdp.sessionState?.hasUploadedProfile && cdp.sessionState.profile) {
292
+ const p = cdp.sessionState.profile;
293
+ headline = p.headline || "";
294
+ summary = p.summary || "";
295
+ skills = p.skills || [];
296
+ if (Array.isArray(p.experiences)) {
297
+ for (const exp of p.experiences) {
298
+ rawExperiences.push({
299
+ company: exp.companyName || exp.company || "Company",
300
+ title: exp.title || "Engineer",
301
+ bullets: exp.bullets,
302
+ description: exp.description
303
+ });
304
+ }
305
+ }
306
+ } else if (cdp.linkeGringoTabFound) {
307
+ return {
308
+ content: [
309
+ {
310
+ type: "text",
311
+ text: `
312
+ # \u26A0\uFE0F Nenhum Perfil Carregado no LinkeGringo
313
+
314
+ A aplica\xE7\xE3o **LinkeGringo est\xE1 aberta no seu navegador** (\`${cdp.linkeGringoTabUrl || "http://localhost:5173"}\`), mas o **upload do PDF do LinkedIn ainda n\xE3o foi realizado**.
315
+
316
+ ---
317
+
318
+ ### \u{1F4A1} Pr\xF3ximo Passo
319
+ Por favor, acesse a aba do LinkeGringo no seu navegador e **arraste o PDF do seu perfil do LinkedIn para o dropzone** (ou clique para selecionar o arquivo).
320
+
321
+ Assim que o upload for processado pela aplica\xE7\xE3o web, chame o \`audit_profile\` novamente para auditar o perfil real automaticamente!
322
+ `.trim()
323
+ }
324
+ ],
325
+ structuredData: {
326
+ status: "waiting_for_upload",
327
+ linkeGringoTabUrl: cdp.linkeGringoTabUrl,
328
+ message: "O usu\xE1rio ainda n\xE3o subiu o PDF do LinkedIn na aplica\xE7\xE3o web.",
329
+ actionRequired: "upload_pdf"
330
+ }
331
+ };
332
+ } else {
333
+ return {
334
+ content: [
335
+ {
336
+ type: "text",
337
+ text: `
338
+ # \u26A0\uFE0F Nenhum Perfil Fornecido para Auditoria
339
+
340
+ Nenhum dado de perfil foi informado e a aplica\xE7\xE3o LinkeGringo n\xE3o foi detectada no navegador.
341
+
342
+ ### Como prosseguir:
343
+ 1. **Pela Web**: Abra o LinkeGringo em \`http://localhost:5173\` no Google Chrome e fa\xE7a o upload do PDF do seu LinkedIn; OU
344
+ 2. **Via Par\xE2metros**: Forne\xE7a o texto bruto do perfil no par\xE2metro \`profileText\` ou informe \`headline\`, \`experiences\` e \`skills\`.
345
+ `.trim()
346
+ }
347
+ ],
348
+ structuredData: {
349
+ status: "missing_profile",
350
+ message: "Nenhum perfil fornecido e aplica\xE7\xE3o LinkeGringo n\xE3o detectada no navegador.",
351
+ actionRequired: "provide_input_or_open_web"
352
+ }
353
+ };
354
+ }
355
+ } catch {
356
+ }
357
+ }
61
358
  if (input.profileText && !headline && rawExperiences.length === 0) {
62
359
  const lines = input.profileText.split("\n").map((l) => l.trim()).filter(Boolean);
63
360
  if (lines.length > 0) {
@@ -960,89 +1257,57 @@ ${proposals.map(
960
1257
 
961
1258
  // src/tools/cdp-check.ts
962
1259
  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
1260
  var checkChromeCdpInputSchema = z11.object({
1026
1261
  port: z11.number().default(9222).describe("Porta do Chrome DevTools Protocol a ser testada (padr\xE3o 9222)"),
1027
1262
  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")
1263
+ timeoutMs: z11.number().default(5e3).describe("Tempo limite em milissegundos para a conex\xE3o")
1029
1264
  });
1030
1265
  async function handleCheckChromeCdp(input) {
1031
1266
  const status = await checkChromeCdp(input.port, input.host, input.timeoutMs);
1267
+ const session = status.sessionState;
1268
+ const isProfileLoaded = Boolean(session?.hasUploadedProfile);
1269
+ let sessionSection = "";
1270
+ if (status.linkeGringoTabFound) {
1271
+ if (isProfileLoaded) {
1272
+ sessionSection = `
1273
+ ### \u{1F464} Sess\xE3o do LinkeGringo: \u{1F7E2} Perfil Carregado
1274
+ - **Candidato**: ${session?.candidateName || "Identificado"}
1275
+ - **Cargo Alvo**: ${session?.targetRole || "N\xE3o especificado"}
1276
+ - **Passo Atual**: \`${session?.step || "diagnostic"}\`
1277
+ - **Inbound Score**: ${session?.inboundScore !== void 0 ? `${session.inboundScore}/100` : "Pronto para c\xE1lculo"}
1278
+
1279
+ > \u{1F4A1} **Instru\xE7\xE3o para a IA**: O perfil do usu\xE1rio j\xE1 est\xE1 carregado. Voc\xEA pode utilizar diretamente as ferramentas \`audit_profile\`, \`simulate_recruiter_search\`, \`generate_headline_proposals\` e \`convert_to_xyz_bullet\` com os dados do candidato.
1280
+ `;
1281
+ } else {
1282
+ sessionSection = `
1283
+ ### \u{1F464} Sess\xE3o do LinkeGringo: \u{1F7E1} Aguardando Upload do PDF
1284
+ - **Passo Atual**: \`upload\` (Tela inicial)
1285
+ - **Status do Arquivo**: Nenhum PDF do LinkedIn foi carregado ainda pelo usu\xE1rio.
1286
+
1287
+ > \u{1F4A1} **Instru\xE7\xE3o para a IA**: O usu\xE1rio est\xE1 com o LinkeGringo aberto, mas ainda n\xE3o subiu o PDF. **Pe\xE7a educadamente para o usu\xE1rio arrastar ou selecionar o PDF do seu perfil do LinkedIn no dropzone da aplica\xE7\xE3o web** (${status.linkeGringoTabUrl || "http://localhost:5173"}). Assim que o usu\xE1rio subir o PDF, voc\xEA ter\xE1 acesso instant\xE2neo aos dados para auditar e otimizar!
1288
+ `;
1289
+ }
1290
+ } else {
1291
+ sessionSection = `
1292
+ ### \u{1F310} Sess\xE3o do LinkeGringo: \u26A0\uFE0F N\xE3o Detectada
1293
+ > O LinkeGringo n\xE3o foi detectado em nenhuma aba aberta. Pe\xE7a ao usu\xE1rio para abrir \`http://localhost:5173\` no navegador ou fornecer o texto do perfil diretamente.
1294
+ `;
1295
+ }
1032
1296
  const markdownSummary = `
1033
- # Status do Chrome Remote Debugging (CDP)
1297
+ # Status do Chrome Remote Debugging (CDP) & LinkeGringo
1034
1298
 
1035
- **Porta Testada**: ${status.host}:${status.port}
1036
- **Status da Conex\xE3o**: ${status.isRunning ? "\u{1F7E2} Conectado e Ativo" : "\u{1F534} Desconectado"}
1299
+ **Porta CDP**: ${status.host}:${status.port}
1300
+ **Status do Chrome**: ${status.isRunning ? "\u{1F7E2} Conectado e Ativo" : "\u{1F534} Desconectado"}
1037
1301
 
1038
1302
  ${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"}
1303
+ - **Vers\xE3o do Navegador**: ${status.browser || "Google Chrome"}
1304
+ - **Aba do LinkeGringo**: ${status.linkeGringoTabFound ? `\u2713 Detectada (\`${status.linkeGringoTabUrl}\`)` : "\u26A0\uFE0F Nenhuma aba do LinkeGringo aberta"}
1043
1305
 
1044
- ${status.activeTabs.length > 0 ? `### Abas Encontradas:
1045
- ${status.activeTabs.map((t) => `- [${t.title}](${t.url})`).join("\n")}` : ""}
1306
+ ### \u{1F6E1}\uFE0F Privacy Shield Ativo
1307
+ - **Abas Pessoais Protegidas**: ${status.otherTabsCount} aba(s) abertas no navegador foram preservadas sem inspe\xE7\xE3o (e-mails, mensageiros, documentos).
1308
+ - **Escopo Restrito**: O LinkeGringo acessa exclusivamente abas pertencentes \xE0 pr\xF3pria aplica\xE7\xE3o LinkeGringo.
1309
+
1310
+ ${sessionSection}
1046
1311
  ` : `
1047
1312
  > \u274C **Motivo**: ${status.error || "Porta fechada."}
1048
1313
  >
@@ -1157,49 +1422,49 @@ function createLinkeGringoMcpServer() {
1157
1422
  }
1158
1423
 
1159
1424
  // src/cli/installer.ts
1160
- import fs from "fs";
1161
- import path from "path";
1162
- import os from "os";
1425
+ import fs2 from "fs";
1426
+ import path2 from "path";
1427
+ import os2 from "os";
1163
1428
  function getMcpConfigsForSystem() {
1164
- const home = os.homedir();
1165
- const platform = os.platform();
1429
+ const home = os2.homedir();
1430
+ const platform = os2.platform();
1166
1431
  const configs = [];
1167
- const antigravityPath = path.join(home, ".gemini", "config", "mcp_config.json");
1432
+ const antigravityPath = path2.join(home, ".gemini", "config", "mcp_config.json");
1168
1433
  configs.push({
1169
1434
  id: "antigravity",
1170
1435
  client: "Google Antigravity",
1171
1436
  configPath: antigravityPath,
1172
- detected: fs.existsSync(path.join(home, ".gemini")) || fs.existsSync(antigravityPath)
1437
+ detected: fs2.existsSync(path2.join(home, ".gemini")) || fs2.existsSync(antigravityPath)
1173
1438
  });
1174
1439
  let claudePath;
1175
1440
  let claudeDir;
1176
1441
  if (platform === "darwin") {
1177
- claudeDir = path.join(home, "Library", "Application Support", "Claude");
1178
- claudePath = path.join(claudeDir, "claude_desktop_config.json");
1442
+ claudeDir = path2.join(home, "Library", "Application Support", "Claude");
1443
+ claudePath = path2.join(claudeDir, "claude_desktop_config.json");
1179
1444
  } else if (platform === "win32") {
1180
- claudeDir = path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "Claude");
1181
- claudePath = path.join(claudeDir, "claude_desktop_config.json");
1445
+ claudeDir = path2.join(process.env.APPDATA || path2.join(home, "AppData", "Roaming"), "Claude");
1446
+ claudePath = path2.join(claudeDir, "claude_desktop_config.json");
1182
1447
  } else {
1183
- claudeDir = path.join(home, ".config", "Claude");
1184
- claudePath = path.join(claudeDir, "claude_desktop_config.json");
1448
+ claudeDir = path2.join(home, ".config", "Claude");
1449
+ claudePath = path2.join(claudeDir, "claude_desktop_config.json");
1185
1450
  }
1186
1451
  configs.push({
1187
1452
  id: "claude",
1188
1453
  client: `Claude Desktop (${platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : "Linux"})`,
1189
1454
  configPath: claudePath,
1190
- detected: fs.existsSync(claudeDir) || fs.existsSync(claudePath)
1455
+ detected: fs2.existsSync(claudeDir) || fs2.existsSync(claudePath)
1191
1456
  });
1192
- const cursorDir = path.join(home, ".cursor");
1193
- const cursorPath = path.join(cursorDir, "mcp.json");
1457
+ const cursorDir = path2.join(home, ".cursor");
1458
+ const cursorPath = path2.join(cursorDir, "mcp.json");
1194
1459
  configs.push({
1195
1460
  id: "cursor",
1196
1461
  client: "Cursor AI",
1197
1462
  configPath: cursorPath,
1198
- detected: fs.existsSync(cursorDir) || fs.existsSync(cursorPath)
1463
+ detected: fs2.existsSync(cursorDir) || fs2.existsSync(cursorPath)
1199
1464
  });
1200
- const windsurfDir = path.join(home, ".codeium", "windsurf");
1201
- const windsurfPath = path.join(windsurfDir, "mcp_config.json");
1202
- if (fs.existsSync(windsurfDir) || fs.existsSync(windsurfPath)) {
1465
+ const windsurfDir = path2.join(home, ".codeium", "windsurf");
1466
+ const windsurfPath = path2.join(windsurfDir, "mcp_config.json");
1467
+ if (fs2.existsSync(windsurfDir) || fs2.existsSync(windsurfPath)) {
1203
1468
  configs.push({
1204
1469
  id: "windsurf",
1205
1470
  client: "Windsurf",
@@ -1210,15 +1475,15 @@ function getMcpConfigsForSystem() {
1210
1475
  return configs;
1211
1476
  }
1212
1477
  function installMcpServerConfig(configPath) {
1213
- const dir = path.dirname(configPath);
1214
- if (!fs.existsSync(dir)) {
1215
- fs.mkdirSync(dir, { recursive: true });
1478
+ const dir = path2.dirname(configPath);
1479
+ if (!fs2.existsSync(dir)) {
1480
+ fs2.mkdirSync(dir, { recursive: true });
1216
1481
  }
1217
1482
  let configData = { mcpServers: {} };
1218
1483
  let isNew = true;
1219
- if (fs.existsSync(configPath)) {
1484
+ if (fs2.existsSync(configPath)) {
1220
1485
  try {
1221
- const raw = fs.readFileSync(configPath, "utf8");
1486
+ const raw = fs2.readFileSync(configPath, "utf8");
1222
1487
  if (raw.trim()) {
1223
1488
  configData = JSON.parse(raw);
1224
1489
  isNew = false;
@@ -1238,7 +1503,7 @@ function installMcpServerConfig(configPath) {
1238
1503
  command: "npx",
1239
1504
  args: ["-y", "chrome-devtools-mcp@latest", "--autoConnect"]
1240
1505
  };
1241
- fs.writeFileSync(configPath, JSON.stringify(configData, null, 2) + "\n", "utf8");
1506
+ fs2.writeFileSync(configPath, JSON.stringify(configData, null, 2) + "\n", "utf8");
1242
1507
  return {
1243
1508
  status: isNew ? "created" : "updated",
1244
1509
  path: configPath
@@ -1266,8 +1531,8 @@ function runInstaller(args = process.argv) {
1266
1531
  const results = [];
1267
1532
  if (options.local) {
1268
1533
  const cwd = process.cwd();
1269
- const localCursorDir = path.join(cwd, ".cursor");
1270
- const localPath = path.join(localCursorDir, "mcp.json");
1534
+ const localCursorDir = path2.join(cwd, ".cursor");
1535
+ const localPath = path2.join(localCursorDir, "mcp.json");
1271
1536
  try {
1272
1537
  const res = installMcpServerConfig(localPath);
1273
1538
  results.push({
@@ -1340,7 +1605,7 @@ function runInstaller(args = process.argv) {
1340
1605
  }
1341
1606
 
1342
1607
  // src/index.ts
1343
- import fs2 from "fs";
1608
+ import fs3 from "fs";
1344
1609
  import { fileURLToPath } from "url";
1345
1610
  async function main() {
1346
1611
  if (process.argv.includes("install") || process.argv.includes("setup") || process.argv.includes("--install")) {
@@ -1356,7 +1621,7 @@ function isDirectExecution() {
1356
1621
  if (!process.argv[1]) return false;
1357
1622
  try {
1358
1623
  const currentFilePath = fileURLToPath(import.meta.url);
1359
- const scriptPath = fs2.existsSync(process.argv[1]) ? fs2.realpathSync(process.argv[1]) : process.argv[1];
1624
+ const scriptPath = fs3.existsSync(process.argv[1]) ? fs3.realpathSync(process.argv[1]) : process.argv[1];
1360
1625
  return currentFilePath === scriptPath || process.argv[1].endsWith("index.js") || process.argv[1].endsWith("linkegringo-mcp") || process.argv[1].endsWith("mcp") || process.argv[1].endsWith("linkegringo");
1361
1626
  } catch {
1362
1627
  return true;
@@ -1369,10 +1634,26 @@ if (isDirectExecution()) {
1369
1634
  });
1370
1635
  }
1371
1636
  export {
1637
+ auditProfileInputSchema,
1372
1638
  checkChromeCdp,
1639
+ checkChromeCdpInputSchema,
1640
+ convertToXyzBulletInputSchema,
1373
1641
  createLinkeGringoMcpServer,
1642
+ detectSparseExperiences,
1643
+ findDevToolsActivePort,
1644
+ formatGoogleXyzBullet,
1645
+ generateHeadlineInputSchema,
1646
+ getExperienceBulletCount,
1374
1647
  getMcpConfigsForSystem,
1648
+ handleAuditProfile,
1649
+ handleCheckChromeCdp,
1650
+ handleConvertToXyzBullet,
1651
+ handleGenerateHeadline,
1652
+ handleSimulateRecruiterSearch,
1375
1653
  installMcpServerConfig,
1654
+ isLinkeGringoUrl,
1376
1655
  parseArgs,
1377
- runInstaller
1656
+ probeViaWebSocket,
1657
+ runInstaller,
1658
+ simulateRecruiterSearchInputSchema
1378
1659
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linkegringo/mcp",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Servidor MCP oficial do LinkeGringo para auditoria e otimização de perfis para o mercado internacional",
5
5
  "type": "module",
6
6
  "bin": {