@linkegringo/mcp 1.0.5 → 1.0.7

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 +176 -82
  3. package/dist/index.js +818 -367
  4. package/package.json +10 -10
package/dist/index.js CHANGED
@@ -9,229 +9,234 @@ 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
- }
41
- }
42
- } catch {
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;
43
47
  }
48
+ this.pendingQueue.shift();
44
49
  }
50
+ return void 0;
45
51
  }
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 {
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
+ // Watcher tracking
121
+ activeWatchers = /* @__PURE__ */ new Set();
122
+ registerWatcher(id) {
123
+ this.activeWatchers.add(id);
124
+ this.emit("watcher:connected", id);
125
+ }
126
+ unregisterWatcher(id) {
127
+ this.activeWatchers.delete(id);
128
+ this.emit("watcher:disconnected", id);
129
+ }
130
+ getWatcherCount() {
131
+ return this.activeWatchers.size;
132
+ }
133
+ hasActiveWatcher() {
134
+ return this.activeWatchers.size > 0;
135
+ }
136
+ cancelAllPending(reason = "Cancelado pelo usu\xE1rio via interface web") {
137
+ let count = 0;
138
+ while (this.pendingQueue.length > 0) {
139
+ const id = this.pendingQueue.shift();
140
+ const job = this.jobs.get(id);
141
+ if (job && (job.status === "pending" || job.status === "processing")) {
142
+ job.status = "failed";
143
+ job.error = reason;
144
+ job.updatedAt = Date.now();
145
+ count++;
146
+ this.emit(`job:${id}`, job);
147
+ this.emit("job:failed", job);
57
148
  }
58
- resolve(null);
59
- }, timeoutMs);
149
+ }
150
+ this.emit("queue:cleared", count);
151
+ return count;
152
+ }
153
+ clear() {
154
+ this.jobs.clear();
155
+ this.pendingQueue = [];
156
+ this.activeWatchers.clear();
157
+ this.removeAllListeners();
158
+ }
159
+ };
160
+ var bridgeJobStore = new BridgeJobStore();
161
+
162
+ // src/bridge/client.ts
163
+ var DEFAULT_BRIDGE_URL = process.env.LINKEGRINGO_BRIDGE_URL || (process.env.NODE_ENV === "test" ? "" : "http://127.0.0.1:5174");
164
+ async function getRemoteOrLocalPendingJob(jobId, bridgeUrl = DEFAULT_BRIDGE_URL) {
165
+ if (bridgeUrl) {
60
166
  try {
61
- ws = new globalThis.WebSocket(wsUrl);
62
- } catch {
167
+ const url = jobId ? `${bridgeUrl}/api/jobs/pending?id=${encodeURIComponent(jobId)}` : `${bridgeUrl}/api/jobs/pending`;
168
+ const controller = new AbortController();
169
+ const timer = setTimeout(() => controller.abort(), 2e3);
170
+ const res = await fetch(url, { signal: controller.signal });
63
171
  clearTimeout(timer);
64
- resolve(null);
65
- return;
172
+ if (res.ok) {
173
+ const data = await res.json();
174
+ if (data.ok && data.job) {
175
+ return data.job;
176
+ }
177
+ }
178
+ } catch {
66
179
  }
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;
180
+ }
181
+ return bridgeJobStore.getPendingJob(jobId);
182
+ }
183
+ async function completeRemoteOrLocalJob(jobId, result, bridgeUrl = DEFAULT_BRIDGE_URL) {
184
+ if (bridgeUrl) {
185
+ try {
186
+ const controller = new AbortController();
187
+ const timer = setTimeout(() => controller.abort(), 5e3);
188
+ const res = await fetch(`${bridgeUrl}/api/jobs/${encodeURIComponent(jobId)}/complete`, {
189
+ method: "POST",
190
+ headers: { "Content-Type": "application/json" },
191
+ body: JSON.stringify({ result }),
192
+ signal: controller.signal
193
+ });
194
+ clearTimeout(timer);
195
+ if (res.ok) {
196
+ const data = await res.json();
197
+ if (data.ok && data.job) {
118
198
  try {
119
- if (raw) session = JSON.parse(raw);
199
+ if (bridgeJobStore.getJob(jobId)) {
200
+ bridgeJobStore.completeJob(jobId, result);
201
+ }
120
202
  } catch {
121
203
  }
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
- });
204
+ return data.job;
137
205
  }
138
- } catch {
139
- clearTimeout(timer);
140
- try {
141
- ws?.close();
142
- } catch {
143
- }
144
- resolve(null);
145
206
  }
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
- };
207
+ } catch {
172
208
  }
173
209
  }
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 = [];
210
+ return bridgeJobStore.completeJob(jobId, result);
211
+ }
212
+ async function failRemoteOrLocalJob(jobId, error, bridgeUrl = DEFAULT_BRIDGE_URL) {
213
+ if (bridgeUrl) {
185
214
  try {
186
- const listRes = await fetch(`http://${host}:${port}/json/list`, {
215
+ const controller = new AbortController();
216
+ const timer = setTimeout(() => controller.abort(), 5e3);
217
+ const res = await fetch(`${bridgeUrl}/api/jobs/${encodeURIComponent(jobId)}/fail`, {
218
+ method: "POST",
219
+ headers: { "Content-Type": "application/json" },
220
+ body: JSON.stringify({ error }),
187
221
  signal: controller.signal
188
222
  });
189
- if (listRes.ok) {
190
- const rawTabs = await listRes.json();
191
- if (Array.isArray(rawTabs)) {
192
- pageTabs = rawTabs.filter((t) => t.type === "page");
223
+ clearTimeout(timer);
224
+ if (res.ok) {
225
+ const data = await res.json();
226
+ if (data.ok && data.job) {
227
+ try {
228
+ if (bridgeJobStore.getJob(jobId)) {
229
+ bridgeJobStore.failJob(jobId, error);
230
+ }
231
+ } catch {
232
+ }
233
+ return data.job;
193
234
  }
194
235
  }
195
236
  } catch {
196
237
  }
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
238
  }
239
+ return bridgeJobStore.failJob(jobId, error);
235
240
  }
236
241
 
237
242
  // src/tools/audit-profile.ts
@@ -286,74 +291,59 @@ async function handleAuditProfile(input) {
286
291
  let skills = input.skills || [];
287
292
  const rawExperiences = [...input.experiences || []];
288
293
  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
294
+ const pendingJob = await getRemoteOrLocalPendingJob();
295
+ if (pendingJob) {
296
+ const p = pendingJob.payload || {};
297
+ return {
298
+ content: [
299
+ {
300
+ type: "text",
301
+ text: `
302
+ # \u{1F4C4} Job Pendente da Interface Web Detectado!
313
303
 
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**.
304
+ O front-end em \`localhost:5173\` submeteu o seguinte job para o Agente de IA:
305
+ - **Job ID**: \`${pendingJob.id}\`
306
+ - **Tipo**: \`${pendingJob.type}\`
307
+ - **Cargo Almejado**: ${p.targetRole || input.targetRole || "Senior Software Engineer"}
308
+ - **Possui PDF Base64**: ${Boolean(p.pdfBase64)}
315
309
 
316
310
  ---
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!
311
+ \u{1F4A1} **A\xE7\xE3o para o Agente de IA**:
312
+ Processe a auditoria deste perfil e chame a ferramenta \`submit_job_result\` passando \`jobId: "${pendingJob.id}"\` e o resultado formatado (\`{ profile, review }\`).
313
+ A tela do usu\xE1rio avan\xE7ar\xE1 instantaneamente para o Diagn\xF3stico!
322
314
  `.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
315
  }
331
- };
332
- } else {
333
- return {
334
- content: [
335
- {
336
- type: "text",
337
- text: `
316
+ ],
317
+ structuredData: {
318
+ status: "pending_bridge_job",
319
+ jobId: pendingJob.id,
320
+ jobType: pendingJob.type,
321
+ payload: pendingJob.payload,
322
+ actionRequired: "submit_job_result"
323
+ }
324
+ };
325
+ }
326
+ return {
327
+ content: [
328
+ {
329
+ type: "text",
330
+ text: `
338
331
  # \u26A0\uFE0F Nenhum Perfil Fornecido para Auditoria
339
332
 
340
- Nenhum dado de perfil foi informado e a aplica\xE7\xE3o LinkeGringo n\xE3o foi detectada no navegador.
333
+ Nenhum dado de perfil foi informado e n\xE3o h\xE1 nenhum job pendente na interface web (porta 5174).
341
334
 
342
335
  ### 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\`.
336
+ 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
337
+ 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
338
  `.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
- };
339
+ }
340
+ ],
341
+ structuredData: {
342
+ status: "missing_profile",
343
+ message: "Nenhum perfil fornecido e nenhum job pendente no Bridge HTTP.",
344
+ actionRequired: "provide_input_or_open_web"
354
345
  }
355
- } catch {
356
- }
346
+ };
357
347
  }
358
348
  if (input.profileText && !headline && rawExperiences.length === 0) {
359
349
  const lines = input.profileText.split("\n").map((l) => l.trim()).filter(Boolean);
@@ -1026,8 +1016,8 @@ var simulateRecruiterSearchInputSchema = z8.object({
1026
1016
  });
1027
1017
  async function handleSimulateRecruiterSearch(input) {
1028
1018
  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(" ")}`;
1019
+ const highPriorityText = `${input.headline || ""} ${(input.skills || []).join(" ")}`;
1020
+ const bodyText = `${input.summary || ""} ${(input.experienceBullets || []).join(" ")}`;
1031
1021
  const evaluations = defaultKeywords.map((term) => {
1032
1022
  const inHighPriority = termMatchesText(highPriorityText, term);
1033
1023
  const inBody = termMatchesText(bodyText, term);
@@ -1200,12 +1190,13 @@ var generateHeadlineInputSchema = z10.object({
1200
1190
  seniorityOrScope: z10.string().default("US Remote").describe("Senioridade ou disponibilidade (ex: US Remote, Global Teams, Staff)")
1201
1191
  });
1202
1192
  async function handleGenerateHeadline(input) {
1203
- const techsStr = input.coreTechnologies.slice(0, 4).join(" \u2022 ");
1193
+ const coreTechs = Array.isArray(input.coreTechnologies) ? input.coreTechnologies : ["TypeScript", "React", "Node.js"];
1194
+ const techsStr = coreTechs.slice(0, 4).join(" \u2022 ");
1204
1195
  const diffStr = input.keyDifferentiator || "Distributed Systems";
1205
1196
  const scopeStr = input.seniorityOrScope || "US Remote";
1206
1197
  const option1 = `${input.targetRole} | ${techsStr} | ${diffStr} | ${scopeStr}`;
1207
1198
  const option2 = `${input.targetRole} | Scaling ${diffStr} with ${techsStr} | ${scopeStr}`;
1208
- const option3 = `${input.targetRole} | ${input.coreTechnologies.slice(0, 3).join(", ")} Specialist | ${scopeStr}`;
1199
+ const option3 = `${input.targetRole} | ${coreTechs.slice(0, 3).join(", ")} Specialist | ${scopeStr}`;
1209
1200
  const proposals = [
1210
1201
  {
1211
1202
  type: "Niche Specialist (Recomendada)",
@@ -1255,79 +1246,148 @@ ${proposals.map(
1255
1246
  };
1256
1247
  }
1257
1248
 
1258
- // src/tools/cdp-check.ts
1249
+ // src/tools/get-pending-job.ts
1259
1250
  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(5e3).describe("Tempo limite em milissegundos para a conex\xE3o")
1251
+ var getPendingJobInputSchema = z11.object({
1252
+ jobId: z11.string().optional().describe("ID espec\xEDfico do job a ser buscado (opcional. Se omitido, pega o pr\xF3ximo da fila)")
1264
1253
  });
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
- `;
1254
+ async function handleGetPendingJob(input = {}) {
1255
+ const job = await getRemoteOrLocalPendingJob(input.jobId);
1256
+ if (!job) {
1257
+ return {
1258
+ content: [
1259
+ {
1260
+ type: "text",
1261
+ text: "Nenhum job pendente no momento na interface web do LinkeGringo."
1262
+ }
1263
+ ],
1264
+ structuredData: {
1265
+ jobFound: false,
1266
+ pendingCount: bridgeJobStore.getPendingCount()
1267
+ }
1268
+ };
1295
1269
  }
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
- `}
1270
+ bridgeJobStore.markProcessing(job.id);
1271
+ let details = "";
1272
+ if (job.type === "parse_and_diagnose") {
1273
+ const p = job.payload || {};
1274
+ details = `
1275
+ - **Tipo de A\xE7\xE3o**: Extra\xE7\xE3o Multimodal e Diagn\xF3stico de Perfil (parseAndDiagnose)
1276
+ - **Cargo Almejado**: ${p.targetRole || "N\xE3o especificado"}
1277
+ - **Possui PDF Base64**: ${Boolean(p.pdfBase64)} (${p.pdfBase64 ? Math.round(p.pdfBase64.length / 1024) + " KB base64" : "N/A"})
1278
+ - **Possui Texto**: ${Boolean(p.pdfText)}
1279
+ `.trim();
1280
+ } else if (job.type === "generate_interview") {
1281
+ details = `
1282
+ - **Tipo de A\xE7\xE3o**: Gera\xE7\xE3o de Perguntas de Entrevista T\xE9cnica
1283
+ - **Candidato**: ${job.payload?.profile?.name || "N\xE3o informado"}
1284
+ - **Cargo Almejado**: ${job.payload?.objective?.primaryRole || "N\xE3o informado"}
1285
+ `.trim();
1286
+ } else if (job.type === "generate_rewritten_profile") {
1287
+ details = `
1288
+ - **Tipo de A\xE7\xE3o**: Reescrita do Perfil com Bullets Google XYZ
1289
+ - **Candidato**: ${job.payload?.profile?.name || "N\xE3o informado"}
1290
+ - **Fatos Confirmados**: ${job.payload?.confirmedFacts?.length || 0} fatos
1319
1291
  `.trim();
1292
+ }
1320
1293
  return {
1321
1294
  content: [
1322
1295
  {
1323
1296
  type: "text",
1324
- text: markdownSummary
1297
+ text: `
1298
+ # \u{1F4E5} Job da Interface Web Encontrado!
1299
+
1300
+ - **Job ID**: \`${job.id}\`
1301
+ - **Tipo**: \`${job.type}\`
1302
+ - **Criado em**: ${new Date(job.createdAt).toLocaleTimeString()}
1303
+
1304
+ ${details}
1305
+
1306
+ ---
1307
+ \u{1F4A1} **Instru\xE7\xF5es para o Agente de IA**:
1308
+ 1. Processe a intelig\xEAncia requerida para este job com base nas diretrizes do LinkeGringo.
1309
+ 2. Ao concluir, chame a ferramenta \`submit_job_result\` passando \`jobId: "${job.id}"\` e o JSON estruturado no campo \`result\`.
1310
+ 3. A interface web em \`localhost:5173\` atualizar\xE1 a tela no mesmo instante!
1311
+ `.trim()
1325
1312
  }
1326
1313
  ],
1327
- structuredData: status
1314
+ structuredData: {
1315
+ jobFound: true,
1316
+ job: {
1317
+ id: job.id,
1318
+ type: job.type,
1319
+ payload: job.payload,
1320
+ createdAt: job.createdAt
1321
+ }
1322
+ }
1328
1323
  };
1329
1324
  }
1330
1325
 
1326
+ // src/tools/submit-job-result.ts
1327
+ import { z as z12 } from "zod";
1328
+ var submitJobResultInputSchema = z12.object({
1329
+ jobId: z12.string().describe("ID do job a ser conclu\xEDdo"),
1330
+ result: z12.any().describe("JSON com os dados estruturados exigidos pelo front-end (ex: ParseAndDiagnoseResult, InterviewPlan, ProfileAnalysis)"),
1331
+ status: z12.enum(["completed", "failed"]).default("completed").describe("Status final do job"),
1332
+ error: z12.string().optional().describe('Mensagem de erro caso o status seja "failed"')
1333
+ });
1334
+ async function handleSubmitJobResult(input) {
1335
+ try {
1336
+ if (input.status === "failed") {
1337
+ const job2 = await failRemoteOrLocalJob(input.jobId, input.error || "Falha no processamento pelo agente");
1338
+ return {
1339
+ content: [
1340
+ {
1341
+ type: "text",
1342
+ text: `\u26A0\uFE0F Job ${job2.id} marcado como com falha. A interface web exibir\xE1 a mensagem de erro.`
1343
+ }
1344
+ ],
1345
+ structuredData: {
1346
+ success: false,
1347
+ jobId: job2.id,
1348
+ status: "failed"
1349
+ }
1350
+ };
1351
+ }
1352
+ const job = await completeRemoteOrLocalJob(input.jobId, input.result);
1353
+ return {
1354
+ content: [
1355
+ {
1356
+ type: "text",
1357
+ text: `
1358
+ \u2705 **Resultado Enviado com Sucesso para a Interface Web!**
1359
+
1360
+ - **Job ID**: \`${job.id}\`
1361
+ - **Tipo**: \`${job.type}\`
1362
+ - **Status**: \`completed\`
1363
+
1364
+ O front-end em \`localhost:5173\` acabou de receber a resposta formatada e atualizou a tela automaticamente!
1365
+ `.trim()
1366
+ }
1367
+ ],
1368
+ structuredData: {
1369
+ success: true,
1370
+ jobId: job.id,
1371
+ status: "completed",
1372
+ type: job.type
1373
+ }
1374
+ };
1375
+ } catch (err) {
1376
+ return {
1377
+ content: [
1378
+ {
1379
+ type: "text",
1380
+ text: `\u274C Erro ao submeter resultado para o job "${input.jobId}": ${err.message}`
1381
+ }
1382
+ ],
1383
+ structuredData: {
1384
+ success: false,
1385
+ error: err.message
1386
+ }
1387
+ };
1388
+ }
1389
+ }
1390
+
1331
1391
  // src/server.ts
1332
1392
  function createLinkeGringoMcpServer() {
1333
1393
  const server = new McpServer({
@@ -1375,13 +1435,23 @@ function createLinkeGringoMcpServer() {
1375
1435
  }
1376
1436
  );
1377
1437
  server.registerTool(
1378
- "check_chrome_cdp_status",
1438
+ "get_pending_job",
1379
1439
  {
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
1440
+ 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).",
1441
+ inputSchema: getPendingJobInputSchema.shape
1382
1442
  },
1383
1443
  async (args) => {
1384
- return await handleCheckChromeCdp(args);
1444
+ return await handleGetPendingJob(args);
1445
+ }
1446
+ );
1447
+ server.registerTool(
1448
+ "submit_job_result",
1449
+ {
1450
+ 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.",
1451
+ inputSchema: submitJobResultInputSchema.shape
1452
+ },
1453
+ async (args) => {
1454
+ return await handleSubmitJobResult(args);
1385
1455
  }
1386
1456
  );
1387
1457
  server.registerResource(
@@ -1422,49 +1492,49 @@ function createLinkeGringoMcpServer() {
1422
1492
  }
1423
1493
 
1424
1494
  // src/cli/installer.ts
1425
- import fs2 from "fs";
1426
- import path2 from "path";
1427
- import os2 from "os";
1495
+ import fs from "fs";
1496
+ import path from "path";
1497
+ import os from "os";
1428
1498
  function getMcpConfigsForSystem() {
1429
- const home = os2.homedir();
1430
- const platform = os2.platform();
1499
+ const home = os.homedir();
1500
+ const platform = os.platform();
1431
1501
  const configs = [];
1432
- const antigravityPath = path2.join(home, ".gemini", "config", "mcp_config.json");
1502
+ const antigravityPath = path.join(home, ".gemini", "config", "mcp_config.json");
1433
1503
  configs.push({
1434
1504
  id: "antigravity",
1435
1505
  client: "Google Antigravity",
1436
1506
  configPath: antigravityPath,
1437
- detected: fs2.existsSync(path2.join(home, ".gemini")) || fs2.existsSync(antigravityPath)
1507
+ detected: fs.existsSync(path.join(home, ".gemini")) || fs.existsSync(antigravityPath)
1438
1508
  });
1439
1509
  let claudePath;
1440
1510
  let claudeDir;
1441
1511
  if (platform === "darwin") {
1442
- claudeDir = path2.join(home, "Library", "Application Support", "Claude");
1443
- claudePath = path2.join(claudeDir, "claude_desktop_config.json");
1512
+ claudeDir = path.join(home, "Library", "Application Support", "Claude");
1513
+ claudePath = path.join(claudeDir, "claude_desktop_config.json");
1444
1514
  } 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");
1515
+ claudeDir = path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "Claude");
1516
+ claudePath = path.join(claudeDir, "claude_desktop_config.json");
1447
1517
  } else {
1448
- claudeDir = path2.join(home, ".config", "Claude");
1449
- claudePath = path2.join(claudeDir, "claude_desktop_config.json");
1518
+ claudeDir = path.join(home, ".config", "Claude");
1519
+ claudePath = path.join(claudeDir, "claude_desktop_config.json");
1450
1520
  }
1451
1521
  configs.push({
1452
1522
  id: "claude",
1453
1523
  client: `Claude Desktop (${platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : "Linux"})`,
1454
1524
  configPath: claudePath,
1455
- detected: fs2.existsSync(claudeDir) || fs2.existsSync(claudePath)
1525
+ detected: fs.existsSync(claudeDir) || fs.existsSync(claudePath)
1456
1526
  });
1457
- const cursorDir = path2.join(home, ".cursor");
1458
- const cursorPath = path2.join(cursorDir, "mcp.json");
1527
+ const cursorDir = path.join(home, ".cursor");
1528
+ const cursorPath = path.join(cursorDir, "mcp.json");
1459
1529
  configs.push({
1460
1530
  id: "cursor",
1461
1531
  client: "Cursor AI",
1462
1532
  configPath: cursorPath,
1463
- detected: fs2.existsSync(cursorDir) || fs2.existsSync(cursorPath)
1533
+ detected: fs.existsSync(cursorDir) || fs.existsSync(cursorPath)
1464
1534
  });
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)) {
1535
+ const windsurfDir = path.join(home, ".codeium", "windsurf");
1536
+ const windsurfPath = path.join(windsurfDir, "mcp_config.json");
1537
+ if (fs.existsSync(windsurfDir) || fs.existsSync(windsurfPath)) {
1468
1538
  configs.push({
1469
1539
  id: "windsurf",
1470
1540
  client: "Windsurf",
@@ -1475,15 +1545,15 @@ function getMcpConfigsForSystem() {
1475
1545
  return configs;
1476
1546
  }
1477
1547
  function installMcpServerConfig(configPath) {
1478
- const dir = path2.dirname(configPath);
1479
- if (!fs2.existsSync(dir)) {
1480
- fs2.mkdirSync(dir, { recursive: true });
1548
+ const dir = path.dirname(configPath);
1549
+ if (!fs.existsSync(dir)) {
1550
+ fs.mkdirSync(dir, { recursive: true });
1481
1551
  }
1482
1552
  let configData = { mcpServers: {} };
1483
1553
  let isNew = true;
1484
- if (fs2.existsSync(configPath)) {
1554
+ if (fs.existsSync(configPath)) {
1485
1555
  try {
1486
- const raw = fs2.readFileSync(configPath, "utf8");
1556
+ const raw = fs.readFileSync(configPath, "utf8");
1487
1557
  if (raw.trim()) {
1488
1558
  configData = JSON.parse(raw);
1489
1559
  isNew = false;
@@ -1499,11 +1569,7 @@ function installMcpServerConfig(configPath) {
1499
1569
  command: "npx",
1500
1570
  args: ["-y", "@linkegringo/mcp"]
1501
1571
  };
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");
1572
+ fs.writeFileSync(configPath, JSON.stringify(configData, null, 2) + "\n", "utf8");
1507
1573
  return {
1508
1574
  status: isNew ? "created" : "updated",
1509
1575
  path: configPath
@@ -1531,8 +1597,8 @@ function runInstaller(args = process.argv) {
1531
1597
  const results = [];
1532
1598
  if (options.local) {
1533
1599
  const cwd = process.cwd();
1534
- const localCursorDir = path2.join(cwd, ".cursor");
1535
- const localPath = path2.join(localCursorDir, "mcp.json");
1600
+ const localCursorDir = path.join(cwd, ".cursor");
1601
+ const localPath = path.join(localCursorDir, "mcp.json");
1536
1602
  try {
1537
1603
  const res = installMcpServerConfig(localPath);
1538
1604
  results.push({
@@ -1604,14 +1670,393 @@ function runInstaller(args = process.argv) {
1604
1670
  return results;
1605
1671
  }
1606
1672
 
1673
+ // src/bridge/server.ts
1674
+ import http from "http";
1675
+ function setCorsHeaders(res) {
1676
+ res.setHeader("Access-Control-Allow-Origin", "*");
1677
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1678
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
1679
+ res.setHeader("Access-Control-Max-Age", "86400");
1680
+ }
1681
+ function sendJson(res, statusCode, data) {
1682
+ setCorsHeaders(res);
1683
+ res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
1684
+ res.end(JSON.stringify(data));
1685
+ }
1686
+ async function parseBody(req) {
1687
+ return new Promise((resolve, reject) => {
1688
+ let body = "";
1689
+ req.on("data", (chunk) => {
1690
+ body += chunk;
1691
+ if (body.length > 50 * 1024 * 1024) {
1692
+ req.destroy();
1693
+ reject(new Error("Payload too large"));
1694
+ }
1695
+ });
1696
+ req.on("end", () => {
1697
+ try {
1698
+ resolve(body ? JSON.parse(body) : {});
1699
+ } catch (err) {
1700
+ reject(err);
1701
+ }
1702
+ });
1703
+ req.on("error", reject);
1704
+ });
1705
+ }
1706
+ function createBridgeHttpServer(options = {}) {
1707
+ const server = http.createServer(async (req, res) => {
1708
+ setCorsHeaders(res);
1709
+ if (req.method === "OPTIONS") {
1710
+ res.writeHead(204);
1711
+ res.end();
1712
+ return;
1713
+ }
1714
+ const parsedUrl = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
1715
+ const pathname = parsedUrl.pathname;
1716
+ try {
1717
+ if (req.method === "GET" && (pathname === "/health" || pathname === "/api/health")) {
1718
+ sendJson(res, 200, {
1719
+ status: "ok",
1720
+ server: "linkegringo-mcp-bridge",
1721
+ watcherConnected: bridgeJobStore.hasActiveWatcher(),
1722
+ watcherCount: bridgeJobStore.getWatcherCount(),
1723
+ pendingCount: bridgeJobStore.getPendingCount(),
1724
+ totalJobs: bridgeJobStore.getAllJobs().length
1725
+ });
1726
+ return;
1727
+ }
1728
+ if (req.method === "GET" && pathname === "/api/bridge/status") {
1729
+ const pendingJob = bridgeJobStore.getPendingJob();
1730
+ sendJson(res, 200, {
1731
+ ok: true,
1732
+ status: "ready",
1733
+ server: "linkegringo-mcp-bridge",
1734
+ watcherConnected: bridgeJobStore.hasActiveWatcher(),
1735
+ watcherCount: bridgeJobStore.getWatcherCount(),
1736
+ pendingCount: bridgeJobStore.getPendingCount(),
1737
+ activeJobId: pendingJob ? pendingJob.id : null,
1738
+ totalJobs: bridgeJobStore.getAllJobs().length
1739
+ });
1740
+ return;
1741
+ }
1742
+ if (req.method === "POST" && pathname === "/api/bridge/disconnect") {
1743
+ const canceledCount = bridgeJobStore.cancelAllPending("Cancelado pelo usu\xE1rio na interface web.");
1744
+ sendJson(res, 200, {
1745
+ ok: true,
1746
+ message: "Conex\xE3o cancelada e fila de jobs limpa com sucesso.",
1747
+ canceledCount
1748
+ });
1749
+ return;
1750
+ }
1751
+ if (req.method === "GET" && pathname === "/api/jobs/stream") {
1752
+ res.writeHead(200, {
1753
+ "Content-Type": "text/event-stream",
1754
+ "Cache-Control": "no-cache, no-transform",
1755
+ "Connection": "keep-alive",
1756
+ "Access-Control-Allow-Origin": "*"
1757
+ });
1758
+ res.flushHeaders?.();
1759
+ const watcherId = `watcher_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
1760
+ bridgeJobStore.registerWatcher(watcherId);
1761
+ res.write(`data: ${JSON.stringify({ type: "connected", watcherId, pendingCount: bridgeJobStore.getPendingCount() })}
1762
+
1763
+ `);
1764
+ const onJobCreated = (job) => {
1765
+ res.write(`data: ${JSON.stringify({ type: "job_created", job: { id: job.id, type: job.type, createdAt: job.createdAt } })}
1766
+
1767
+ `);
1768
+ };
1769
+ const onJobCompleted = (job) => {
1770
+ res.write(`data: ${JSON.stringify({ type: "job_completed", job: { id: job.id, type: job.type } })}
1771
+
1772
+ `);
1773
+ };
1774
+ const onJobFailed = (job) => {
1775
+ res.write(`data: ${JSON.stringify({ type: "job_failed", job: { id: job.id, type: job.type, error: job.error } })}
1776
+
1777
+ `);
1778
+ };
1779
+ bridgeJobStore.on("job:created", onJobCreated);
1780
+ bridgeJobStore.on("job:completed", onJobCompleted);
1781
+ bridgeJobStore.on("job:failed", onJobFailed);
1782
+ const heartbeat = setInterval(() => {
1783
+ res.write(`: heartbeat
1784
+
1785
+ `);
1786
+ }, 15e3);
1787
+ req.on("close", () => {
1788
+ clearInterval(heartbeat);
1789
+ bridgeJobStore.unregisterWatcher(watcherId);
1790
+ bridgeJobStore.off("job:created", onJobCreated);
1791
+ bridgeJobStore.off("job:completed", onJobCompleted);
1792
+ bridgeJobStore.off("job:failed", onJobFailed);
1793
+ });
1794
+ return;
1795
+ }
1796
+ if (req.method === "POST" && pathname === "/api/jobs") {
1797
+ const body = await parseBody(req);
1798
+ if (!body.type) {
1799
+ sendJson(res, 400, { ok: false, error: 'Campo "type" \xE9 obrigat\xF3rio.' });
1800
+ return;
1801
+ }
1802
+ const job = bridgeJobStore.createJob(body.type, body.payload || {});
1803
+ options.onJobCreated?.(job);
1804
+ sendJson(res, 201, { ok: true, job });
1805
+ return;
1806
+ }
1807
+ if (req.method === "GET" && pathname === "/api/jobs") {
1808
+ sendJson(res, 200, { ok: true, jobs: bridgeJobStore.getAllJobs() });
1809
+ return;
1810
+ }
1811
+ if (req.method === "GET" && pathname === "/api/jobs/pending") {
1812
+ const id = parsedUrl.searchParams.get("id") || void 0;
1813
+ const job = bridgeJobStore.getPendingJob(id);
1814
+ sendJson(res, 200, { ok: true, job: job || null });
1815
+ return;
1816
+ }
1817
+ const jobMatch = pathname.match(/^\/api\/jobs\/([a-zA-Z0-9_-]+)$/);
1818
+ if (req.method === "GET" && jobMatch) {
1819
+ const jobId = jobMatch[1];
1820
+ const wait = parsedUrl.searchParams.get("wait") === "true";
1821
+ const timeout = parseInt(parsedUrl.searchParams.get("timeout") || "30000", 10);
1822
+ let job = bridgeJobStore.getJob(jobId);
1823
+ if (!job) {
1824
+ sendJson(res, 404, { ok: false, error: `Job n\xE3o encontrado: ${jobId}` });
1825
+ return;
1826
+ }
1827
+ if (wait && (job.status === "pending" || job.status === "processing")) {
1828
+ try {
1829
+ job = await bridgeJobStore.waitForJob(jobId, timeout);
1830
+ } catch {
1831
+ job = bridgeJobStore.getJob(jobId) || job;
1832
+ }
1833
+ }
1834
+ sendJson(res, 200, { ok: true, job });
1835
+ return;
1836
+ }
1837
+ const completeMatch = pathname.match(/^\/api\/jobs\/([a-zA-Z0-9_-]+)\/complete$/);
1838
+ if (req.method === "POST" && completeMatch) {
1839
+ const jobId = completeMatch[1];
1840
+ const body = await parseBody(req);
1841
+ try {
1842
+ const job = bridgeJobStore.completeJob(jobId, body.result);
1843
+ sendJson(res, 200, { ok: true, job });
1844
+ } catch (err) {
1845
+ sendJson(res, 404, { ok: false, error: err.message });
1846
+ }
1847
+ return;
1848
+ }
1849
+ const failMatch = pathname.match(/^\/api\/jobs\/([a-zA-Z0-9_-]+)\/fail$/);
1850
+ if (req.method === "POST" && failMatch) {
1851
+ const jobId = failMatch[1];
1852
+ const body = await parseBody(req);
1853
+ try {
1854
+ const job = bridgeJobStore.failJob(jobId, body.error || "Erro desconhecido");
1855
+ sendJson(res, 200, { ok: true, job });
1856
+ } catch (err) {
1857
+ sendJson(res, 404, { ok: false, error: err.message });
1858
+ }
1859
+ return;
1860
+ }
1861
+ if (req.method === "POST" && pathname === "/api/jobs/clear") {
1862
+ bridgeJobStore.clear();
1863
+ sendJson(res, 200, { ok: true, message: "Fila de jobs limpa com sucesso." });
1864
+ return;
1865
+ }
1866
+ sendJson(res, 404, { ok: false, error: "Rota n\xE3o encontrada" });
1867
+ } catch (err) {
1868
+ sendJson(res, 500, { ok: false, error: err?.message || "Erro interno no servidor bridge" });
1869
+ }
1870
+ });
1871
+ return server;
1872
+ }
1873
+ var activeBridgeServer = null;
1874
+ var activePort = 5174;
1875
+ async function startBridgeServer(options = {}) {
1876
+ if (activeBridgeServer) {
1877
+ return {
1878
+ server: activeBridgeServer,
1879
+ port: activePort,
1880
+ close: stopBridgeServer
1881
+ };
1882
+ }
1883
+ const port = options.port || parseInt(process.env.LINKEGRINGO_BRIDGE_PORT || "5174", 10);
1884
+ const host = options.host || "127.0.0.1";
1885
+ const server = createBridgeHttpServer(options);
1886
+ return new Promise((resolve, reject) => {
1887
+ server.on("error", (err) => {
1888
+ if (err.code === "EADDRINUSE") {
1889
+ console.error(`[LinkeGringo Bridge] Porta ${port} j\xE1 est\xE1 em uso. Reutilizando porta existente.`);
1890
+ resolve({
1891
+ server,
1892
+ port,
1893
+ close: stopBridgeServer
1894
+ });
1895
+ } else {
1896
+ reject(err);
1897
+ }
1898
+ });
1899
+ server.listen(port, host, () => {
1900
+ activeBridgeServer = server;
1901
+ activePort = port;
1902
+ console.error(`[LinkeGringo Bridge] Servidor HTTP local ativo em http://${host}:${port}`);
1903
+ resolve({
1904
+ server,
1905
+ port,
1906
+ close: stopBridgeServer
1907
+ });
1908
+ });
1909
+ });
1910
+ }
1911
+ async function stopBridgeServer() {
1912
+ if (!activeBridgeServer) return;
1913
+ return new Promise((resolve) => {
1914
+ activeBridgeServer?.close(() => {
1915
+ activeBridgeServer = null;
1916
+ resolve();
1917
+ });
1918
+ });
1919
+ }
1920
+
1921
+ // src/bridge/watcher.ts
1922
+ import http2 from "http";
1923
+ function startBridgeWatcher(options = {}) {
1924
+ const bridgeUrl = options.bridgeUrl || process.env.LINKEGRINGO_BRIDGE_URL || "http://127.0.0.1:5174";
1925
+ let isClosed = false;
1926
+ let currentReq = null;
1927
+ let retryTimer = null;
1928
+ let retryDelay = 1e3;
1929
+ function connect() {
1930
+ if (isClosed) return;
1931
+ try {
1932
+ const url = new URL("/api/jobs/stream", bridgeUrl);
1933
+ const req = http2.get(
1934
+ url.toString(),
1935
+ {
1936
+ headers: {
1937
+ Accept: "text/event-stream",
1938
+ "Cache-Control": "no-cache"
1939
+ }
1940
+ },
1941
+ (res) => {
1942
+ if (res.statusCode !== 200) {
1943
+ options.onError?.(new Error(`Falha ao conectar no stream SSE: HTTP ${res.statusCode}`));
1944
+ scheduleRetry();
1945
+ return;
1946
+ }
1947
+ retryDelay = 1e3;
1948
+ let buffer = "";
1949
+ res.on("data", (chunk) => {
1950
+ buffer += chunk.toString("utf-8");
1951
+ const lines = buffer.split("\n\n");
1952
+ buffer = lines.pop() || "";
1953
+ for (const block of lines) {
1954
+ for (const line of block.split("\n")) {
1955
+ if (line.startsWith("data: ")) {
1956
+ try {
1957
+ const payload = JSON.parse(line.slice(6));
1958
+ if (payload.type === "connected") {
1959
+ options.onConnect?.(payload);
1960
+ console.log(
1961
+ `[LinkeGringo Watcher] \u{1F7E2} Conectado ao bridge (${bridgeUrl}). Watcher ativo: ${payload.watcherId} (Jobs pendentes: ${payload.pendingCount})`
1962
+ );
1963
+ if (options.once && payload.pendingCount > 0) {
1964
+ console.log(
1965
+ `[LinkeGringo Watcher] \u{1F680} JOB DETECTADO NA FILA (${payload.pendingCount} pendente(s)). Finalizando para ativar agente...`
1966
+ );
1967
+ setTimeout(() => process.exit(0), 50);
1968
+ }
1969
+ } else if (payload.type === "job_created") {
1970
+ console.log(
1971
+ `
1972
+ [LinkeGringo Watcher] \u{1F680} NOVO JOB DETECTADO: ${payload.job.id} | Tipo: ${payload.job.type}`
1973
+ );
1974
+ options.onJob?.(payload.job);
1975
+ if (options.once) {
1976
+ setTimeout(() => process.exit(0), 50);
1977
+ }
1978
+ } else if (payload.type === "job_completed") {
1979
+ console.log(`[LinkeGringo Watcher] \u2705 Job finalizado: ${payload.job.id}`);
1980
+ } else if (payload.type === "job_failed") {
1981
+ console.log(
1982
+ `[LinkeGringo Watcher] \u26A0\uFE0F Job cancelado ou com erro: ${payload.job.id} (${payload.job.error})`
1983
+ );
1984
+ }
1985
+ } catch {
1986
+ }
1987
+ }
1988
+ }
1989
+ }
1990
+ });
1991
+ res.on("end", () => {
1992
+ if (!isClosed) {
1993
+ scheduleRetry();
1994
+ }
1995
+ });
1996
+ res.on("error", (err) => {
1997
+ options.onError?.(err);
1998
+ if (!isClosed) {
1999
+ scheduleRetry();
2000
+ }
2001
+ });
2002
+ }
2003
+ );
2004
+ req.on("error", (err) => {
2005
+ options.onError?.(err);
2006
+ if (!isClosed) {
2007
+ scheduleRetry();
2008
+ }
2009
+ });
2010
+ currentReq = req;
2011
+ } catch (err) {
2012
+ options.onError?.(err);
2013
+ scheduleRetry();
2014
+ }
2015
+ }
2016
+ function scheduleRetry() {
2017
+ if (isClosed || retryTimer) return;
2018
+ retryTimer = setTimeout(() => {
2019
+ retryTimer = null;
2020
+ retryDelay = Math.min(retryDelay * 1.5, 1e4);
2021
+ connect();
2022
+ }, retryDelay);
2023
+ }
2024
+ connect();
2025
+ return {
2026
+ close() {
2027
+ isClosed = true;
2028
+ if (retryTimer) {
2029
+ clearTimeout(retryTimer);
2030
+ retryTimer = null;
2031
+ }
2032
+ if (currentReq) {
2033
+ currentReq.destroy();
2034
+ currentReq = null;
2035
+ }
2036
+ console.log("[LinkeGringo Watcher] Modo watch desconectado.");
2037
+ }
2038
+ };
2039
+ }
2040
+
1607
2041
  // src/index.ts
1608
- import fs3 from "fs";
2042
+ import fs2 from "fs";
1609
2043
  import { fileURLToPath } from "url";
1610
2044
  async function main() {
1611
2045
  if (process.argv.includes("install") || process.argv.includes("setup") || process.argv.includes("--install")) {
1612
2046
  runInstaller(process.argv);
1613
2047
  return;
1614
2048
  }
2049
+ if (process.argv.includes("watch") || process.argv.includes("--watch") || process.argv.includes("listen")) {
2050
+ const once = process.argv.includes("--once") || process.argv.includes("-1");
2051
+ console.log(`[LinkeGringo Watcher] Iniciando modo escuta ultraleve${once ? " (once)" : ""}...`);
2052
+ startBridgeWatcher({ once });
2053
+ return;
2054
+ }
2055
+ try {
2056
+ await startBridgeServer();
2057
+ } catch (err) {
2058
+ console.error("[LinkeGringo Bridge] N\xE3o foi poss\xEDvel iniciar bridge HTTP:", err);
2059
+ }
1615
2060
  const server = createLinkeGringoMcpServer();
1616
2061
  const transport = new StdioServerTransport();
1617
2062
  await server.connect(transport);
@@ -1621,7 +2066,7 @@ function isDirectExecution() {
1621
2066
  if (!process.argv[1]) return false;
1622
2067
  try {
1623
2068
  const currentFilePath = fileURLToPath(import.meta.url);
1624
- const scriptPath = fs3.existsSync(process.argv[1]) ? fs3.realpathSync(process.argv[1]) : process.argv[1];
2069
+ const scriptPath = fs2.existsSync(process.argv[1]) ? fs2.realpathSync(process.argv[1]) : process.argv[1];
1625
2070
  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
2071
  } catch {
1627
2072
  return true;
@@ -1635,25 +2080,31 @@ if (isDirectExecution()) {
1635
2080
  }
1636
2081
  export {
1637
2082
  auditProfileInputSchema,
1638
- checkChromeCdp,
1639
- checkChromeCdpInputSchema,
2083
+ bridgeJobStore,
2084
+ completeRemoteOrLocalJob,
1640
2085
  convertToXyzBulletInputSchema,
2086
+ createBridgeHttpServer,
1641
2087
  createLinkeGringoMcpServer,
1642
2088
  detectSparseExperiences,
1643
- findDevToolsActivePort,
2089
+ failRemoteOrLocalJob,
1644
2090
  formatGoogleXyzBullet,
1645
2091
  generateHeadlineInputSchema,
1646
2092
  getExperienceBulletCount,
1647
2093
  getMcpConfigsForSystem,
2094
+ getPendingJobInputSchema,
2095
+ getRemoteOrLocalPendingJob,
1648
2096
  handleAuditProfile,
1649
- handleCheckChromeCdp,
1650
2097
  handleConvertToXyzBullet,
1651
2098
  handleGenerateHeadline,
2099
+ handleGetPendingJob,
1652
2100
  handleSimulateRecruiterSearch,
2101
+ handleSubmitJobResult,
1653
2102
  installMcpServerConfig,
1654
- isLinkeGringoUrl,
1655
2103
  parseArgs,
1656
- probeViaWebSocket,
1657
2104
  runInstaller,
1658
- simulateRecruiterSearchInputSchema
2105
+ simulateRecruiterSearchInputSchema,
2106
+ startBridgeServer,
2107
+ startBridgeWatcher,
2108
+ stopBridgeServer,
2109
+ submitJobResultInputSchema
1659
2110
  };