@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.
- package/LICENSE +21 -0
- package/dist/index.d.ts +152 -82
- package/dist/index.js +583 -367
- 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/
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
147
|
+
return bridgeJobStore.getPendingJob(jobId);
|
|
47
148
|
}
|
|
48
|
-
async function
|
|
49
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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 (
|
|
165
|
+
if (bridgeJobStore.getJob(jobId)) {
|
|
166
|
+
bridgeJobStore.completeJob(jobId, result);
|
|
167
|
+
}
|
|
120
168
|
} catch {
|
|
121
169
|
}
|
|
122
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
}
|
|
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
|
|
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} | ${
|
|
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/
|
|
1215
|
+
// src/tools/get-pending-job.ts
|
|
1259
1216
|
import { z as z11 } from "zod";
|
|
1260
|
-
var
|
|
1261
|
-
|
|
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
|
|
1266
|
-
const
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
${
|
|
1303
|
-
- **
|
|
1304
|
-
- **
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
- **
|
|
1309
|
-
|
|
1310
|
-
${
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
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:
|
|
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:
|
|
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
|
-
"
|
|
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: "
|
|
1381
|
-
inputSchema:
|
|
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
|
|
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
|
|
1426
|
-
import
|
|
1427
|
-
import
|
|
1461
|
+
import fs from "fs";
|
|
1462
|
+
import path from "path";
|
|
1463
|
+
import os from "os";
|
|
1428
1464
|
function getMcpConfigsForSystem() {
|
|
1429
|
-
const home =
|
|
1430
|
-
const platform =
|
|
1465
|
+
const home = os.homedir();
|
|
1466
|
+
const platform = os.platform();
|
|
1431
1467
|
const configs = [];
|
|
1432
|
-
const antigravityPath =
|
|
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:
|
|
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 =
|
|
1443
|
-
claudePath =
|
|
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 =
|
|
1446
|
-
claudePath =
|
|
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 =
|
|
1449
|
-
claudePath =
|
|
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:
|
|
1491
|
+
detected: fs.existsSync(claudeDir) || fs.existsSync(claudePath)
|
|
1456
1492
|
});
|
|
1457
|
-
const cursorDir =
|
|
1458
|
-
const cursorPath =
|
|
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:
|
|
1499
|
+
detected: fs.existsSync(cursorDir) || fs.existsSync(cursorPath)
|
|
1464
1500
|
});
|
|
1465
|
-
const windsurfDir =
|
|
1466
|
-
const windsurfPath =
|
|
1467
|
-
if (
|
|
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 =
|
|
1479
|
-
if (!
|
|
1480
|
-
|
|
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 (
|
|
1520
|
+
if (fs.existsSync(configPath)) {
|
|
1485
1521
|
try {
|
|
1486
|
-
const raw =
|
|
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
|
|
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 =
|
|
1535
|
-
const localPath =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1639
|
-
|
|
1849
|
+
bridgeJobStore,
|
|
1850
|
+
completeRemoteOrLocalJob,
|
|
1640
1851
|
convertToXyzBulletInputSchema,
|
|
1852
|
+
createBridgeHttpServer,
|
|
1641
1853
|
createLinkeGringoMcpServer,
|
|
1642
1854
|
detectSparseExperiences,
|
|
1643
|
-
|
|
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
|
};
|