@linkegringo/mcp 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +152 -82
  3. package/dist/index.js +583 -367
  4. package/package.json +10 -10
package/dist/index.js CHANGED
@@ -9,229 +9,200 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
9
  // src/tools/audit-profile.ts
10
10
  import { z } from "zod";
11
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
- }
12
+ // src/bridge/state.ts
13
+ import { EventEmitter } from "events";
14
+ var BridgeJobStore = class extends EventEmitter {
15
+ jobs = /* @__PURE__ */ new Map();
16
+ pendingQueue = [];
17
+ createJob(type, payload) {
18
+ const id = `job_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
19
+ const now = Date.now();
20
+ const job = {
21
+ id,
22
+ type,
23
+ payload,
24
+ status: "pending",
25
+ createdAt: now,
26
+ updatedAt: now
27
+ };
28
+ this.jobs.set(id, job);
29
+ this.pendingQueue.push(id);
30
+ this.emit("job:created", job);
31
+ this.emit(`job:${id}`, job);
32
+ return job;
33
+ }
34
+ getJob(id) {
35
+ return this.jobs.get(id);
36
+ }
37
+ getPendingJob(id) {
38
+ if (id) {
39
+ const job = this.jobs.get(id);
40
+ return job && (job.status === "pending" || job.status === "processing") ? job : void 0;
41
+ }
42
+ while (this.pendingQueue.length > 0) {
43
+ const nextId = this.pendingQueue[0];
44
+ const job = this.jobs.get(nextId);
45
+ if (job && job.status === "pending") {
46
+ return job;
47
+ }
48
+ this.pendingQueue.shift();
49
+ }
50
+ return void 0;
51
+ }
52
+ markProcessing(id) {
53
+ const job = this.jobs.get(id);
54
+ if (!job) return void 0;
55
+ job.status = "processing";
56
+ job.updatedAt = Date.now();
57
+ this.emit(`job:${id}`, job);
58
+ return job;
59
+ }
60
+ completeJob(id, result) {
61
+ const job = this.jobs.get(id);
62
+ if (!job) {
63
+ throw new Error(`Job n\xE3o encontrado: ${id}`);
64
+ }
65
+ job.status = "completed";
66
+ job.result = result;
67
+ job.updatedAt = Date.now();
68
+ const idx = this.pendingQueue.indexOf(id);
69
+ if (idx !== -1) {
70
+ this.pendingQueue.splice(idx, 1);
71
+ }
72
+ this.emit(`job:${id}`, job);
73
+ this.emit("job:completed", job);
74
+ return job;
75
+ }
76
+ failJob(id, error) {
77
+ const job = this.jobs.get(id);
78
+ if (!job) {
79
+ throw new Error(`Job n\xE3o encontrado: ${id}`);
80
+ }
81
+ job.status = "failed";
82
+ job.error = error;
83
+ job.updatedAt = Date.now();
84
+ const idx = this.pendingQueue.indexOf(id);
85
+ if (idx !== -1) {
86
+ this.pendingQueue.splice(idx, 1);
87
+ }
88
+ this.emit(`job:${id}`, job);
89
+ this.emit("job:failed", job);
90
+ return job;
91
+ }
92
+ async waitForJob(id, timeoutMs = 6e4) {
93
+ const existing = this.jobs.get(id);
94
+ if (existing && (existing.status === "completed" || existing.status === "failed")) {
95
+ return existing;
96
+ }
97
+ return new Promise((resolve, reject) => {
98
+ const timer = setTimeout(() => {
99
+ this.off(`job:${id}`, onUpdate);
100
+ const current = this.jobs.get(id);
101
+ if (current) resolve(current);
102
+ else reject(new Error(`Timeout aguardando pelo job ${id}`));
103
+ }, timeoutMs);
104
+ const onUpdate = (job) => {
105
+ if (job.status === "completed" || job.status === "failed") {
106
+ clearTimeout(timer);
107
+ this.off(`job:${id}`, onUpdate);
108
+ resolve(job);
109
+ }
110
+ };
111
+ this.on(`job:${id}`, onUpdate);
112
+ });
113
+ }
114
+ getAllJobs() {
115
+ return Array.from(this.jobs.values());
116
+ }
117
+ getPendingCount() {
118
+ return this.jobs.size > 0 ? Array.from(this.jobs.values()).filter((j) => j.status === "pending" || j.status === "processing").length : 0;
119
+ }
120
+ clear() {
121
+ this.jobs.clear();
122
+ this.pendingQueue = [];
123
+ this.removeAllListeners();
124
+ }
125
+ };
126
+ var bridgeJobStore = new BridgeJobStore();
127
+
128
+ // src/bridge/client.ts
129
+ var DEFAULT_BRIDGE_URL = process.env.LINKEGRINGO_BRIDGE_URL || (process.env.NODE_ENV === "test" ? "" : "http://127.0.0.1:5174");
130
+ async function getRemoteOrLocalPendingJob(jobId, bridgeUrl = DEFAULT_BRIDGE_URL) {
131
+ if (bridgeUrl) {
132
+ try {
133
+ const url = jobId ? `${bridgeUrl}/api/jobs/pending?id=${encodeURIComponent(jobId)}` : `${bridgeUrl}/api/jobs/pending`;
134
+ const controller = new AbortController();
135
+ const timer = setTimeout(() => controller.abort(), 2e3);
136
+ const res = await fetch(url, { signal: controller.signal });
137
+ clearTimeout(timer);
138
+ if (res.ok) {
139
+ const data = await res.json();
140
+ if (data.ok && data.job) {
141
+ return data.job;
41
142
  }
42
- } catch {
43
143
  }
144
+ } catch {
44
145
  }
45
146
  }
46
- return null;
147
+ return bridgeJobStore.getPendingJob(jobId);
47
148
  }
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);
149
+ async function completeRemoteOrLocalJob(jobId, result, bridgeUrl = DEFAULT_BRIDGE_URL) {
150
+ if (bridgeUrl) {
60
151
  try {
61
- ws = new globalThis.WebSocket(wsUrl);
62
- } catch {
152
+ const controller = new AbortController();
153
+ const timer = setTimeout(() => controller.abort(), 5e3);
154
+ const res = await fetch(`${bridgeUrl}/api/jobs/${encodeURIComponent(jobId)}/complete`, {
155
+ method: "POST",
156
+ headers: { "Content-Type": "application/json" },
157
+ body: JSON.stringify({ result }),
158
+ signal: controller.signal
159
+ });
63
160
  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;
161
+ if (res.ok) {
162
+ const data = await res.json();
163
+ if (data.ok && data.job) {
118
164
  try {
119
- if (raw) session = JSON.parse(raw);
165
+ if (bridgeJobStore.getJob(jobId)) {
166
+ bridgeJobStore.completeJob(jobId, result);
167
+ }
120
168
  } catch {
121
169
  }
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 {
170
+ return data.job;
143
171
  }
144
- resolve(null);
145
172
  }
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 = 2e3) {
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
- };
173
+ } catch {
172
174
  }
173
175
  }
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 = [];
176
+ return bridgeJobStore.completeJob(jobId, result);
177
+ }
178
+ async function failRemoteOrLocalJob(jobId, error, bridgeUrl = DEFAULT_BRIDGE_URL) {
179
+ if (bridgeUrl) {
185
180
  try {
186
- const listRes = await fetch(`http://${host}:${port}/json/list`, {
181
+ const controller = new AbortController();
182
+ const timer = setTimeout(() => controller.abort(), 5e3);
183
+ const res = await fetch(`${bridgeUrl}/api/jobs/${encodeURIComponent(jobId)}/fail`, {
184
+ method: "POST",
185
+ headers: { "Content-Type": "application/json" },
186
+ body: JSON.stringify({ error }),
187
187
  signal: controller.signal
188
188
  });
189
- if (listRes.ok) {
190
- const rawTabs = await listRes.json();
191
- if (Array.isArray(rawTabs)) {
192
- pageTabs = rawTabs.filter((t) => t.type === "page");
189
+ clearTimeout(timer);
190
+ if (res.ok) {
191
+ const data = await res.json();
192
+ if (data.ok && data.job) {
193
+ try {
194
+ if (bridgeJobStore.getJob(jobId)) {
195
+ bridgeJobStore.failJob(jobId, error);
196
+ }
197
+ } catch {
198
+ }
199
+ return data.job;
193
200
  }
194
201
  }
195
202
  } catch {
196
203
  }
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
204
  }
205
+ return bridgeJobStore.failJob(jobId, error);
235
206
  }
236
207
 
237
208
  // src/tools/audit-profile.ts
@@ -286,74 +257,59 @@ async function handleAuditProfile(input) {
286
257
  let skills = input.skills || [];
287
258
  const rawExperiences = [...input.experiences || []];
288
259
  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
260
+ const pendingJob = await getRemoteOrLocalPendingJob();
261
+ if (pendingJob) {
262
+ const p = pendingJob.payload || {};
263
+ return {
264
+ content: [
265
+ {
266
+ type: "text",
267
+ text: `
268
+ # \u{1F4C4} Job Pendente da Interface Web Detectado!
313
269
 
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**.
270
+ O front-end em \`localhost:5173\` submeteu o seguinte job para o Agente de IA:
271
+ - **Job ID**: \`${pendingJob.id}\`
272
+ - **Tipo**: \`${pendingJob.type}\`
273
+ - **Cargo Almejado**: ${p.targetRole || input.targetRole || "Senior Software Engineer"}
274
+ - **Possui PDF Base64**: ${Boolean(p.pdfBase64)}
315
275
 
316
276
  ---
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!
277
+ \u{1F4A1} **A\xE7\xE3o para o Agente de IA**:
278
+ Processe a auditoria deste perfil e chame a ferramenta \`submit_job_result\` passando \`jobId: "${pendingJob.id}"\` e o resultado formatado (\`{ profile, review }\`).
279
+ A tela do usu\xE1rio avan\xE7ar\xE1 instantaneamente para o Diagn\xF3stico!
322
280
  `.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
281
  }
331
- };
332
- } else {
333
- return {
334
- content: [
335
- {
336
- type: "text",
337
- text: `
282
+ ],
283
+ structuredData: {
284
+ status: "pending_bridge_job",
285
+ jobId: pendingJob.id,
286
+ jobType: pendingJob.type,
287
+ payload: pendingJob.payload,
288
+ actionRequired: "submit_job_result"
289
+ }
290
+ };
291
+ }
292
+ return {
293
+ content: [
294
+ {
295
+ type: "text",
296
+ text: `
338
297
  # \u26A0\uFE0F Nenhum Perfil Fornecido para Auditoria
339
298
 
340
- Nenhum dado de perfil foi informado e a aplica\xE7\xE3o LinkeGringo n\xE3o foi detectada no navegador.
299
+ Nenhum dado de perfil foi informado e n\xE3o h\xE1 nenhum job pendente na interface web (porta 5174).
341
300
 
342
301
  ### 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\`.
302
+ 1. **Pela Interface Web**: Abra o LinkeGringo (http://localhost:5173), selecione o cargo-alvo e envie o PDF do LinkedIn no Modo Agente MCP; OU
303
+ 2. **Via Par\xE2metros MCP**: Forne\xE7a o texto bruto do perfil no par\xE2metro \`profileText\` ou informe \`headline\`, \`experiences\` e \`skills\` diretamente nesta ferramenta.
345
304
  `.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
- };
305
+ }
306
+ ],
307
+ structuredData: {
308
+ status: "missing_profile",
309
+ message: "Nenhum perfil fornecido e nenhum job pendente no Bridge HTTP.",
310
+ actionRequired: "provide_input_or_open_web"
354
311
  }
355
- } catch {
356
- }
312
+ };
357
313
  }
358
314
  if (input.profileText && !headline && rawExperiences.length === 0) {
359
315
  const lines = input.profileText.split("\n").map((l) => l.trim()).filter(Boolean);
@@ -1026,8 +982,8 @@ var simulateRecruiterSearchInputSchema = z8.object({
1026
982
  });
1027
983
  async function handleSimulateRecruiterSearch(input) {
1028
984
  const defaultKeywords = input.requiredKeywords?.length ? input.requiredKeywords : [input.targetRole, "Senior", "Remote", "Architecture", "Scale"];
1029
- const highPriorityText = `${input.headline} ${input.skills.join(" ")}`;
1030
- const bodyText = `${input.summary} ${input.experienceBullets.join(" ")}`;
985
+ const highPriorityText = `${input.headline || ""} ${(input.skills || []).join(" ")}`;
986
+ const bodyText = `${input.summary || ""} ${(input.experienceBullets || []).join(" ")}`;
1031
987
  const evaluations = defaultKeywords.map((term) => {
1032
988
  const inHighPriority = termMatchesText(highPriorityText, term);
1033
989
  const inBody = termMatchesText(bodyText, term);
@@ -1200,12 +1156,13 @@ var generateHeadlineInputSchema = z10.object({
1200
1156
  seniorityOrScope: z10.string().default("US Remote").describe("Senioridade ou disponibilidade (ex: US Remote, Global Teams, Staff)")
1201
1157
  });
1202
1158
  async function handleGenerateHeadline(input) {
1203
- const techsStr = input.coreTechnologies.slice(0, 4).join(" \u2022 ");
1159
+ const coreTechs = Array.isArray(input.coreTechnologies) ? input.coreTechnologies : ["TypeScript", "React", "Node.js"];
1160
+ const techsStr = coreTechs.slice(0, 4).join(" \u2022 ");
1204
1161
  const diffStr = input.keyDifferentiator || "Distributed Systems";
1205
1162
  const scopeStr = input.seniorityOrScope || "US Remote";
1206
1163
  const option1 = `${input.targetRole} | ${techsStr} | ${diffStr} | ${scopeStr}`;
1207
1164
  const option2 = `${input.targetRole} | Scaling ${diffStr} with ${techsStr} | ${scopeStr}`;
1208
- const option3 = `${input.targetRole} | ${input.coreTechnologies.slice(0, 3).join(", ")} Specialist | ${scopeStr}`;
1165
+ const option3 = `${input.targetRole} | ${coreTechs.slice(0, 3).join(", ")} Specialist | ${scopeStr}`;
1209
1166
  const proposals = [
1210
1167
  {
1211
1168
  type: "Niche Specialist (Recomendada)",
@@ -1255,79 +1212,148 @@ ${proposals.map(
1255
1212
  };
1256
1213
  }
1257
1214
 
1258
- // src/tools/cdp-check.ts
1215
+ // src/tools/get-pending-job.ts
1259
1216
  import { z as z11 } from "zod";
1260
- var checkChromeCdpInputSchema = z11.object({
1261
- port: z11.number().default(9222).describe("Porta do Chrome DevTools Protocol a ser testada (padr\xE3o 9222)"),
1262
- host: z11.string().default("127.0.0.1").describe("Host do Chrome (padr\xE3o 127.0.0.1)"),
1263
- timeoutMs: z11.number().default(2e3).describe("Tempo limite em milissegundos para a conex\xE3o")
1217
+ var getPendingJobInputSchema = z11.object({
1218
+ jobId: z11.string().optional().describe("ID espec\xEDfico do job a ser buscado (opcional. Se omitido, pega o pr\xF3ximo da fila)")
1264
1219
  });
1265
- async function handleCheckChromeCdp(input) {
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
- `;
1220
+ async function handleGetPendingJob(input = {}) {
1221
+ const job = await getRemoteOrLocalPendingJob(input.jobId);
1222
+ if (!job) {
1223
+ return {
1224
+ content: [
1225
+ {
1226
+ type: "text",
1227
+ text: "Nenhum job pendente no momento na interface web do LinkeGringo."
1228
+ }
1229
+ ],
1230
+ structuredData: {
1231
+ jobFound: false,
1232
+ pendingCount: bridgeJobStore.getPendingCount()
1233
+ }
1234
+ };
1295
1235
  }
1296
- const markdownSummary = `
1297
- # Status do Chrome Remote Debugging (CDP) & LinkeGringo
1298
-
1299
- **Porta CDP**: ${status.host}:${status.port}
1300
- **Status do Chrome**: ${status.isRunning ? "\u{1F7E2} Conectado e Ativo" : "\u{1F534} Desconectado"}
1301
-
1302
- ${status.isRunning ? `
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"}
1305
-
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}
1311
- ` : `
1312
- > \u274C **Motivo**: ${status.error || "Porta fechada."}
1313
- >
1314
- > **Como ativar no Google Chrome (M144+)**:
1315
- > 1. Abra uma nova aba e acesse: \`chrome://inspect/#remote-debugging\`
1316
- > 2. Marque a op\xE7\xE3o para **Ativar depura\xE7\xE3o remota**.
1317
- > 3. Se estiver usando o servidor oficial DevTools MCP, configure com \`--autoConnect\`.
1318
- `}
1236
+ bridgeJobStore.markProcessing(job.id);
1237
+ let details = "";
1238
+ if (job.type === "parse_and_diagnose") {
1239
+ const p = job.payload || {};
1240
+ details = `
1241
+ - **Tipo de A\xE7\xE3o**: Extra\xE7\xE3o Multimodal e Diagn\xF3stico de Perfil (parseAndDiagnose)
1242
+ - **Cargo Almejado**: ${p.targetRole || "N\xE3o especificado"}
1243
+ - **Possui PDF Base64**: ${Boolean(p.pdfBase64)} (${p.pdfBase64 ? Math.round(p.pdfBase64.length / 1024) + " KB base64" : "N/A"})
1244
+ - **Possui Texto**: ${Boolean(p.pdfText)}
1245
+ `.trim();
1246
+ } else if (job.type === "generate_interview") {
1247
+ details = `
1248
+ - **Tipo de A\xE7\xE3o**: Gera\xE7\xE3o de Perguntas de Entrevista T\xE9cnica
1249
+ - **Candidato**: ${job.payload?.profile?.name || "N\xE3o informado"}
1250
+ - **Cargo Almejado**: ${job.payload?.objective?.primaryRole || "N\xE3o informado"}
1251
+ `.trim();
1252
+ } else if (job.type === "generate_rewritten_profile") {
1253
+ details = `
1254
+ - **Tipo de A\xE7\xE3o**: Reescrita do Perfil com Bullets Google XYZ
1255
+ - **Candidato**: ${job.payload?.profile?.name || "N\xE3o informado"}
1256
+ - **Fatos Confirmados**: ${job.payload?.confirmedFacts?.length || 0} fatos
1319
1257
  `.trim();
1258
+ }
1320
1259
  return {
1321
1260
  content: [
1322
1261
  {
1323
1262
  type: "text",
1324
- text: markdownSummary
1263
+ text: `
1264
+ # \u{1F4E5} Job da Interface Web Encontrado!
1265
+
1266
+ - **Job ID**: \`${job.id}\`
1267
+ - **Tipo**: \`${job.type}\`
1268
+ - **Criado em**: ${new Date(job.createdAt).toLocaleTimeString()}
1269
+
1270
+ ${details}
1271
+
1272
+ ---
1273
+ \u{1F4A1} **Instru\xE7\xF5es para o Agente de IA**:
1274
+ 1. Processe a intelig\xEAncia requerida para este job com base nas diretrizes do LinkeGringo.
1275
+ 2. Ao concluir, chame a ferramenta \`submit_job_result\` passando \`jobId: "${job.id}"\` e o JSON estruturado no campo \`result\`.
1276
+ 3. A interface web em \`localhost:5173\` atualizar\xE1 a tela no mesmo instante!
1277
+ `.trim()
1325
1278
  }
1326
1279
  ],
1327
- structuredData: status
1280
+ structuredData: {
1281
+ jobFound: true,
1282
+ job: {
1283
+ id: job.id,
1284
+ type: job.type,
1285
+ payload: job.payload,
1286
+ createdAt: job.createdAt
1287
+ }
1288
+ }
1328
1289
  };
1329
1290
  }
1330
1291
 
1292
+ // src/tools/submit-job-result.ts
1293
+ import { z as z12 } from "zod";
1294
+ var submitJobResultInputSchema = z12.object({
1295
+ jobId: z12.string().describe("ID do job a ser conclu\xEDdo"),
1296
+ result: z12.any().describe("JSON com os dados estruturados exigidos pelo front-end (ex: ParseAndDiagnoseResult, InterviewPlan, ProfileAnalysis)"),
1297
+ status: z12.enum(["completed", "failed"]).default("completed").describe("Status final do job"),
1298
+ error: z12.string().optional().describe('Mensagem de erro caso o status seja "failed"')
1299
+ });
1300
+ async function handleSubmitJobResult(input) {
1301
+ try {
1302
+ if (input.status === "failed") {
1303
+ const job2 = await failRemoteOrLocalJob(input.jobId, input.error || "Falha no processamento pelo agente");
1304
+ return {
1305
+ content: [
1306
+ {
1307
+ type: "text",
1308
+ text: `\u26A0\uFE0F Job ${job2.id} marcado como com falha. A interface web exibir\xE1 a mensagem de erro.`
1309
+ }
1310
+ ],
1311
+ structuredData: {
1312
+ success: false,
1313
+ jobId: job2.id,
1314
+ status: "failed"
1315
+ }
1316
+ };
1317
+ }
1318
+ const job = await completeRemoteOrLocalJob(input.jobId, input.result);
1319
+ return {
1320
+ content: [
1321
+ {
1322
+ type: "text",
1323
+ text: `
1324
+ \u2705 **Resultado Enviado com Sucesso para a Interface Web!**
1325
+
1326
+ - **Job ID**: \`${job.id}\`
1327
+ - **Tipo**: \`${job.type}\`
1328
+ - **Status**: \`completed\`
1329
+
1330
+ O front-end em \`localhost:5173\` acabou de receber a resposta formatada e atualizou a tela automaticamente!
1331
+ `.trim()
1332
+ }
1333
+ ],
1334
+ structuredData: {
1335
+ success: true,
1336
+ jobId: job.id,
1337
+ status: "completed",
1338
+ type: job.type
1339
+ }
1340
+ };
1341
+ } catch (err) {
1342
+ return {
1343
+ content: [
1344
+ {
1345
+ type: "text",
1346
+ text: `\u274C Erro ao submeter resultado para o job "${input.jobId}": ${err.message}`
1347
+ }
1348
+ ],
1349
+ structuredData: {
1350
+ success: false,
1351
+ error: err.message
1352
+ }
1353
+ };
1354
+ }
1355
+ }
1356
+
1331
1357
  // src/server.ts
1332
1358
  function createLinkeGringoMcpServer() {
1333
1359
  const server = new McpServer({
@@ -1375,13 +1401,23 @@ function createLinkeGringoMcpServer() {
1375
1401
  }
1376
1402
  );
1377
1403
  server.registerTool(
1378
- "check_chrome_cdp_status",
1404
+ "get_pending_job",
1405
+ {
1406
+ description: "Busca o pr\xF3ximo job pendente enviado pela interface gr\xE1fica web do LinkeGringo (ex: upload de perfil para diagn\xF3stico, gera\xE7\xE3o de entrevista, reescrita de perfil).",
1407
+ inputSchema: getPendingJobInputSchema.shape
1408
+ },
1409
+ async (args) => {
1410
+ return await handleGetPendingJob(args);
1411
+ }
1412
+ );
1413
+ server.registerTool(
1414
+ "submit_job_result",
1379
1415
  {
1380
- 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.",
1381
- inputSchema: checkChromeCdpInputSchema.shape
1416
+ description: "Envia o resultado processado pelo agente de IA de volta para a interface gr\xE1fica web do LinkeGringo, desbloqueando o front-end e atualizando a tela imediatamente.",
1417
+ inputSchema: submitJobResultInputSchema.shape
1382
1418
  },
1383
1419
  async (args) => {
1384
- return await handleCheckChromeCdp(args);
1420
+ return await handleSubmitJobResult(args);
1385
1421
  }
1386
1422
  );
1387
1423
  server.registerResource(
@@ -1422,49 +1458,49 @@ function createLinkeGringoMcpServer() {
1422
1458
  }
1423
1459
 
1424
1460
  // src/cli/installer.ts
1425
- import fs2 from "fs";
1426
- import path2 from "path";
1427
- import os2 from "os";
1461
+ import fs from "fs";
1462
+ import path from "path";
1463
+ import os from "os";
1428
1464
  function getMcpConfigsForSystem() {
1429
- const home = os2.homedir();
1430
- const platform = os2.platform();
1465
+ const home = os.homedir();
1466
+ const platform = os.platform();
1431
1467
  const configs = [];
1432
- const antigravityPath = path2.join(home, ".gemini", "config", "mcp_config.json");
1468
+ const antigravityPath = path.join(home, ".gemini", "config", "mcp_config.json");
1433
1469
  configs.push({
1434
1470
  id: "antigravity",
1435
1471
  client: "Google Antigravity",
1436
1472
  configPath: antigravityPath,
1437
- detected: fs2.existsSync(path2.join(home, ".gemini")) || fs2.existsSync(antigravityPath)
1473
+ detected: fs.existsSync(path.join(home, ".gemini")) || fs.existsSync(antigravityPath)
1438
1474
  });
1439
1475
  let claudePath;
1440
1476
  let claudeDir;
1441
1477
  if (platform === "darwin") {
1442
- claudeDir = path2.join(home, "Library", "Application Support", "Claude");
1443
- claudePath = path2.join(claudeDir, "claude_desktop_config.json");
1478
+ claudeDir = path.join(home, "Library", "Application Support", "Claude");
1479
+ claudePath = path.join(claudeDir, "claude_desktop_config.json");
1444
1480
  } else if (platform === "win32") {
1445
- claudeDir = path2.join(process.env.APPDATA || path2.join(home, "AppData", "Roaming"), "Claude");
1446
- claudePath = path2.join(claudeDir, "claude_desktop_config.json");
1481
+ claudeDir = path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "Claude");
1482
+ claudePath = path.join(claudeDir, "claude_desktop_config.json");
1447
1483
  } else {
1448
- claudeDir = path2.join(home, ".config", "Claude");
1449
- claudePath = path2.join(claudeDir, "claude_desktop_config.json");
1484
+ claudeDir = path.join(home, ".config", "Claude");
1485
+ claudePath = path.join(claudeDir, "claude_desktop_config.json");
1450
1486
  }
1451
1487
  configs.push({
1452
1488
  id: "claude",
1453
1489
  client: `Claude Desktop (${platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : "Linux"})`,
1454
1490
  configPath: claudePath,
1455
- detected: fs2.existsSync(claudeDir) || fs2.existsSync(claudePath)
1491
+ detected: fs.existsSync(claudeDir) || fs.existsSync(claudePath)
1456
1492
  });
1457
- const cursorDir = path2.join(home, ".cursor");
1458
- const cursorPath = path2.join(cursorDir, "mcp.json");
1493
+ const cursorDir = path.join(home, ".cursor");
1494
+ const cursorPath = path.join(cursorDir, "mcp.json");
1459
1495
  configs.push({
1460
1496
  id: "cursor",
1461
1497
  client: "Cursor AI",
1462
1498
  configPath: cursorPath,
1463
- detected: fs2.existsSync(cursorDir) || fs2.existsSync(cursorPath)
1499
+ detected: fs.existsSync(cursorDir) || fs.existsSync(cursorPath)
1464
1500
  });
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)) {
1501
+ const windsurfDir = path.join(home, ".codeium", "windsurf");
1502
+ const windsurfPath = path.join(windsurfDir, "mcp_config.json");
1503
+ if (fs.existsSync(windsurfDir) || fs.existsSync(windsurfPath)) {
1468
1504
  configs.push({
1469
1505
  id: "windsurf",
1470
1506
  client: "Windsurf",
@@ -1475,15 +1511,15 @@ function getMcpConfigsForSystem() {
1475
1511
  return configs;
1476
1512
  }
1477
1513
  function installMcpServerConfig(configPath) {
1478
- const dir = path2.dirname(configPath);
1479
- if (!fs2.existsSync(dir)) {
1480
- fs2.mkdirSync(dir, { recursive: true });
1514
+ const dir = path.dirname(configPath);
1515
+ if (!fs.existsSync(dir)) {
1516
+ fs.mkdirSync(dir, { recursive: true });
1481
1517
  }
1482
1518
  let configData = { mcpServers: {} };
1483
1519
  let isNew = true;
1484
- if (fs2.existsSync(configPath)) {
1520
+ if (fs.existsSync(configPath)) {
1485
1521
  try {
1486
- const raw = fs2.readFileSync(configPath, "utf8");
1522
+ const raw = fs.readFileSync(configPath, "utf8");
1487
1523
  if (raw.trim()) {
1488
1524
  configData = JSON.parse(raw);
1489
1525
  isNew = false;
@@ -1499,11 +1535,7 @@ function installMcpServerConfig(configPath) {
1499
1535
  command: "npx",
1500
1536
  args: ["-y", "@linkegringo/mcp"]
1501
1537
  };
1502
- configData.mcpServers["chrome-devtools"] = {
1503
- command: "npx",
1504
- args: ["-y", "chrome-devtools-mcp@latest", "--autoConnect"]
1505
- };
1506
- fs2.writeFileSync(configPath, JSON.stringify(configData, null, 2) + "\n", "utf8");
1538
+ fs.writeFileSync(configPath, JSON.stringify(configData, null, 2) + "\n", "utf8");
1507
1539
  return {
1508
1540
  status: isNew ? "created" : "updated",
1509
1541
  path: configPath
@@ -1531,8 +1563,8 @@ function runInstaller(args = process.argv) {
1531
1563
  const results = [];
1532
1564
  if (options.local) {
1533
1565
  const cwd = process.cwd();
1534
- const localCursorDir = path2.join(cwd, ".cursor");
1535
- const localPath = path2.join(localCursorDir, "mcp.json");
1566
+ const localCursorDir = path.join(cwd, ".cursor");
1567
+ const localPath = path.join(localCursorDir, "mcp.json");
1536
1568
  try {
1537
1569
  const res = installMcpServerConfig(localPath);
1538
1570
  results.push({
@@ -1604,14 +1636,193 @@ function runInstaller(args = process.argv) {
1604
1636
  return results;
1605
1637
  }
1606
1638
 
1639
+ // src/bridge/server.ts
1640
+ import http from "http";
1641
+ function setCorsHeaders(res) {
1642
+ res.setHeader("Access-Control-Allow-Origin", "*");
1643
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1644
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
1645
+ res.setHeader("Access-Control-Max-Age", "86400");
1646
+ }
1647
+ function sendJson(res, statusCode, data) {
1648
+ setCorsHeaders(res);
1649
+ res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
1650
+ res.end(JSON.stringify(data));
1651
+ }
1652
+ async function parseBody(req) {
1653
+ return new Promise((resolve, reject) => {
1654
+ let body = "";
1655
+ req.on("data", (chunk) => {
1656
+ body += chunk;
1657
+ if (body.length > 50 * 1024 * 1024) {
1658
+ req.destroy();
1659
+ reject(new Error("Payload too large"));
1660
+ }
1661
+ });
1662
+ req.on("end", () => {
1663
+ try {
1664
+ resolve(body ? JSON.parse(body) : {});
1665
+ } catch (err) {
1666
+ reject(err);
1667
+ }
1668
+ });
1669
+ req.on("error", reject);
1670
+ });
1671
+ }
1672
+ function createBridgeHttpServer(options = {}) {
1673
+ const server = http.createServer(async (req, res) => {
1674
+ setCorsHeaders(res);
1675
+ if (req.method === "OPTIONS") {
1676
+ res.writeHead(204);
1677
+ res.end();
1678
+ return;
1679
+ }
1680
+ const parsedUrl = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
1681
+ const pathname = parsedUrl.pathname;
1682
+ try {
1683
+ if (req.method === "GET" && (pathname === "/health" || pathname === "/api/health")) {
1684
+ sendJson(res, 200, {
1685
+ status: "ok",
1686
+ server: "linkegringo-mcp-bridge",
1687
+ pendingCount: bridgeJobStore.getPendingCount(),
1688
+ totalJobs: bridgeJobStore.getAllJobs().length
1689
+ });
1690
+ return;
1691
+ }
1692
+ if (req.method === "POST" && pathname === "/api/jobs") {
1693
+ const body = await parseBody(req);
1694
+ if (!body.type) {
1695
+ sendJson(res, 400, { ok: false, error: 'Campo "type" \xE9 obrigat\xF3rio.' });
1696
+ return;
1697
+ }
1698
+ const job = bridgeJobStore.createJob(body.type, body.payload || {});
1699
+ options.onJobCreated?.(job);
1700
+ sendJson(res, 201, { ok: true, job });
1701
+ return;
1702
+ }
1703
+ if (req.method === "GET" && pathname === "/api/jobs/pending") {
1704
+ const id = parsedUrl.searchParams.get("id") || void 0;
1705
+ const job = bridgeJobStore.getPendingJob(id);
1706
+ sendJson(res, 200, { ok: true, job: job || null });
1707
+ return;
1708
+ }
1709
+ const jobMatch = pathname.match(/^\/api\/jobs\/([a-zA-Z0-9_-]+)$/);
1710
+ if (req.method === "GET" && jobMatch) {
1711
+ const jobId = jobMatch[1];
1712
+ const wait = parsedUrl.searchParams.get("wait") === "true";
1713
+ const timeout = parseInt(parsedUrl.searchParams.get("timeout") || "30000", 10);
1714
+ let job = bridgeJobStore.getJob(jobId);
1715
+ if (!job) {
1716
+ sendJson(res, 404, { ok: false, error: `Job n\xE3o encontrado: ${jobId}` });
1717
+ return;
1718
+ }
1719
+ if (wait && (job.status === "pending" || job.status === "processing")) {
1720
+ try {
1721
+ job = await bridgeJobStore.waitForJob(jobId, timeout);
1722
+ } catch {
1723
+ job = bridgeJobStore.getJob(jobId) || job;
1724
+ }
1725
+ }
1726
+ sendJson(res, 200, { ok: true, job });
1727
+ return;
1728
+ }
1729
+ const completeMatch = pathname.match(/^\/api\/jobs\/([a-zA-Z0-9_-]+)\/complete$/);
1730
+ if (req.method === "POST" && completeMatch) {
1731
+ const jobId = completeMatch[1];
1732
+ const body = await parseBody(req);
1733
+ try {
1734
+ const job = bridgeJobStore.completeJob(jobId, body.result);
1735
+ sendJson(res, 200, { ok: true, job });
1736
+ } catch (err) {
1737
+ sendJson(res, 404, { ok: false, error: err.message });
1738
+ }
1739
+ return;
1740
+ }
1741
+ const failMatch = pathname.match(/^\/api\/jobs\/([a-zA-Z0-9_-]+)\/fail$/);
1742
+ if (req.method === "POST" && failMatch) {
1743
+ const jobId = failMatch[1];
1744
+ const body = await parseBody(req);
1745
+ try {
1746
+ const job = bridgeJobStore.failJob(jobId, body.error || "Erro desconhecido");
1747
+ sendJson(res, 200, { ok: true, job });
1748
+ } catch (err) {
1749
+ sendJson(res, 404, { ok: false, error: err.message });
1750
+ }
1751
+ return;
1752
+ }
1753
+ if (req.method === "POST" && pathname === "/api/jobs/clear") {
1754
+ bridgeJobStore.clear();
1755
+ sendJson(res, 200, { ok: true, message: "Fila de jobs limpa com sucesso." });
1756
+ return;
1757
+ }
1758
+ sendJson(res, 404, { ok: false, error: "Rota n\xE3o encontrada" });
1759
+ } catch (err) {
1760
+ sendJson(res, 500, { ok: false, error: err?.message || "Erro interno no servidor bridge" });
1761
+ }
1762
+ });
1763
+ return server;
1764
+ }
1765
+ var activeBridgeServer = null;
1766
+ var activePort = 5174;
1767
+ async function startBridgeServer(options = {}) {
1768
+ if (activeBridgeServer) {
1769
+ return {
1770
+ server: activeBridgeServer,
1771
+ port: activePort,
1772
+ close: stopBridgeServer
1773
+ };
1774
+ }
1775
+ const port = options.port || parseInt(process.env.LINKEGRINGO_BRIDGE_PORT || "5174", 10);
1776
+ const host = options.host || "127.0.0.1";
1777
+ const server = createBridgeHttpServer(options);
1778
+ return new Promise((resolve, reject) => {
1779
+ server.on("error", (err) => {
1780
+ if (err.code === "EADDRINUSE") {
1781
+ console.error(`[LinkeGringo Bridge] Porta ${port} j\xE1 est\xE1 em uso. Reutilizando porta existente.`);
1782
+ resolve({
1783
+ server,
1784
+ port,
1785
+ close: stopBridgeServer
1786
+ });
1787
+ } else {
1788
+ reject(err);
1789
+ }
1790
+ });
1791
+ server.listen(port, host, () => {
1792
+ activeBridgeServer = server;
1793
+ activePort = port;
1794
+ console.error(`[LinkeGringo Bridge] Servidor HTTP local ativo em http://${host}:${port}`);
1795
+ resolve({
1796
+ server,
1797
+ port,
1798
+ close: stopBridgeServer
1799
+ });
1800
+ });
1801
+ });
1802
+ }
1803
+ async function stopBridgeServer() {
1804
+ if (!activeBridgeServer) return;
1805
+ return new Promise((resolve) => {
1806
+ activeBridgeServer?.close(() => {
1807
+ activeBridgeServer = null;
1808
+ resolve();
1809
+ });
1810
+ });
1811
+ }
1812
+
1607
1813
  // src/index.ts
1608
- import fs3 from "fs";
1814
+ import fs2 from "fs";
1609
1815
  import { fileURLToPath } from "url";
1610
1816
  async function main() {
1611
1817
  if (process.argv.includes("install") || process.argv.includes("setup") || process.argv.includes("--install")) {
1612
1818
  runInstaller(process.argv);
1613
1819
  return;
1614
1820
  }
1821
+ try {
1822
+ await startBridgeServer();
1823
+ } catch (err) {
1824
+ console.error("[LinkeGringo Bridge] N\xE3o foi poss\xEDvel iniciar bridge HTTP:", err);
1825
+ }
1615
1826
  const server = createLinkeGringoMcpServer();
1616
1827
  const transport = new StdioServerTransport();
1617
1828
  await server.connect(transport);
@@ -1621,7 +1832,7 @@ function isDirectExecution() {
1621
1832
  if (!process.argv[1]) return false;
1622
1833
  try {
1623
1834
  const currentFilePath = fileURLToPath(import.meta.url);
1624
- const scriptPath = fs3.existsSync(process.argv[1]) ? fs3.realpathSync(process.argv[1]) : process.argv[1];
1835
+ const scriptPath = fs2.existsSync(process.argv[1]) ? fs2.realpathSync(process.argv[1]) : process.argv[1];
1625
1836
  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");
1626
1837
  } catch {
1627
1838
  return true;
@@ -1635,25 +1846,30 @@ if (isDirectExecution()) {
1635
1846
  }
1636
1847
  export {
1637
1848
  auditProfileInputSchema,
1638
- checkChromeCdp,
1639
- checkChromeCdpInputSchema,
1849
+ bridgeJobStore,
1850
+ completeRemoteOrLocalJob,
1640
1851
  convertToXyzBulletInputSchema,
1852
+ createBridgeHttpServer,
1641
1853
  createLinkeGringoMcpServer,
1642
1854
  detectSparseExperiences,
1643
- findDevToolsActivePort,
1855
+ failRemoteOrLocalJob,
1644
1856
  formatGoogleXyzBullet,
1645
1857
  generateHeadlineInputSchema,
1646
1858
  getExperienceBulletCount,
1647
1859
  getMcpConfigsForSystem,
1860
+ getPendingJobInputSchema,
1861
+ getRemoteOrLocalPendingJob,
1648
1862
  handleAuditProfile,
1649
- handleCheckChromeCdp,
1650
1863
  handleConvertToXyzBullet,
1651
1864
  handleGenerateHeadline,
1865
+ handleGetPendingJob,
1652
1866
  handleSimulateRecruiterSearch,
1867
+ handleSubmitJobResult,
1653
1868
  installMcpServerConfig,
1654
- isLinkeGringoUrl,
1655
1869
  parseArgs,
1656
- probeViaWebSocket,
1657
1870
  runInstaller,
1658
- simulateRecruiterSearchInputSchema
1871
+ simulateRecruiterSearchInputSchema,
1872
+ startBridgeServer,
1873
+ stopBridgeServer,
1874
+ submitJobResultInputSchema
1659
1875
  };