@linkegringo/mcp 1.0.11 → 2.0.1
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/dist/index.d.ts +2615 -4
- package/dist/index.js +2454 -262
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -169,8 +169,14 @@ var bridgeJobStore = new BridgeJobStore();
|
|
|
169
169
|
|
|
170
170
|
// src/bridge/client.ts
|
|
171
171
|
var DEFAULT_BRIDGE_URL = process.env.LINKEGRINGO_BRIDGE_URL || (process.env.NODE_ENV === "test" ? "" : "http://127.0.0.1:5174");
|
|
172
|
-
|
|
173
|
-
if (
|
|
172
|
+
function getBridgeUrl(overrideUrl) {
|
|
173
|
+
if (overrideUrl) return overrideUrl;
|
|
174
|
+
if (process.env.LINKEGRINGO_BRIDGE_URL) return process.env.LINKEGRINGO_BRIDGE_URL;
|
|
175
|
+
return process.env.NODE_ENV === "test" ? "" : "http://127.0.0.1:5174";
|
|
176
|
+
}
|
|
177
|
+
async function getRemoteOrLocalPendingJob(jobId, bridgeUrl = getBridgeUrl()) {
|
|
178
|
+
const urlBase = getBridgeUrl(bridgeUrl);
|
|
179
|
+
if (urlBase) {
|
|
174
180
|
try {
|
|
175
181
|
const url = jobId ? `${bridgeUrl}/api/jobs/${encodeURIComponent(jobId)}` : `${bridgeUrl}/api/jobs/pending`;
|
|
176
182
|
const controller = new AbortController();
|
|
@@ -246,6 +252,89 @@ async function failRemoteOrLocalJob(jobId, error, bridgeUrl = DEFAULT_BRIDGE_URL
|
|
|
246
252
|
}
|
|
247
253
|
return bridgeJobStore.failJob(jobId, error);
|
|
248
254
|
}
|
|
255
|
+
async function listRemoteJobs(filter, bridgeUrl) {
|
|
256
|
+
const base = getBridgeUrl(bridgeUrl) || "http://127.0.0.1:5174";
|
|
257
|
+
const url = new URL(`${base}/api/jobs`);
|
|
258
|
+
if (filter?.status) url.searchParams.set("status", filter.status);
|
|
259
|
+
if (filter?.type) url.searchParams.set("type", filter.type);
|
|
260
|
+
const res = await fetch(url.toString());
|
|
261
|
+
if (!res.ok) {
|
|
262
|
+
throw new Error(`Falha ao listar jobs: HTTP ${res.status}`);
|
|
263
|
+
}
|
|
264
|
+
const data = await res.json();
|
|
265
|
+
return data.jobs || [];
|
|
266
|
+
}
|
|
267
|
+
async function claimRemoteJob(jobId, agent, leaseDurationMs, bridgeUrl) {
|
|
268
|
+
const base = getBridgeUrl(bridgeUrl) || "http://127.0.0.1:5174";
|
|
269
|
+
const url = `${base}/api/jobs/${encodeURIComponent(jobId)}/claim`;
|
|
270
|
+
const res = await fetch(url, {
|
|
271
|
+
method: "POST",
|
|
272
|
+
headers: { "Content-Type": "application/json" },
|
|
273
|
+
body: JSON.stringify({ agent, leaseDurationMs })
|
|
274
|
+
});
|
|
275
|
+
if (!res.ok) {
|
|
276
|
+
const err = await res.json().catch(() => ({}));
|
|
277
|
+
throw new Error(err.error || `Falha ao dar claim no job ${jobId} (HTTP ${res.status})`);
|
|
278
|
+
}
|
|
279
|
+
const data = await res.json();
|
|
280
|
+
return data.job;
|
|
281
|
+
}
|
|
282
|
+
async function reportRemoteProgress(jobId, agent, update, bridgeUrl) {
|
|
283
|
+
const base = getBridgeUrl(bridgeUrl) || "http://127.0.0.1:5174";
|
|
284
|
+
const url = `${base}/api/jobs/${encodeURIComponent(jobId)}/progress`;
|
|
285
|
+
const res = await fetch(url, {
|
|
286
|
+
method: "POST",
|
|
287
|
+
headers: { "Content-Type": "application/json" },
|
|
288
|
+
body: JSON.stringify({ agent, ...update })
|
|
289
|
+
});
|
|
290
|
+
if (!res.ok) {
|
|
291
|
+
const err = await res.json().catch(() => ({}));
|
|
292
|
+
throw new Error(err.error || `Falha ao reportar progresso no job ${jobId}`);
|
|
293
|
+
}
|
|
294
|
+
const data = await res.json();
|
|
295
|
+
return data.job;
|
|
296
|
+
}
|
|
297
|
+
async function requestRemoteUserAction(jobId, agent, details, bridgeUrl) {
|
|
298
|
+
const base = getBridgeUrl(bridgeUrl) || "http://127.0.0.1:5174";
|
|
299
|
+
const url = `${base}/api/jobs/${encodeURIComponent(jobId)}/user-action`;
|
|
300
|
+
const res = await fetch(url, {
|
|
301
|
+
method: "POST",
|
|
302
|
+
headers: { "Content-Type": "application/json" },
|
|
303
|
+
body: JSON.stringify({ agent, ...details })
|
|
304
|
+
});
|
|
305
|
+
if (!res.ok) {
|
|
306
|
+
const err = await res.json().catch(() => ({}));
|
|
307
|
+
throw new Error(err.error || `Falha ao solicitar a\xE7\xE3o do usu\xE1rio no job ${jobId}`);
|
|
308
|
+
}
|
|
309
|
+
const data = await res.json();
|
|
310
|
+
return data.job;
|
|
311
|
+
}
|
|
312
|
+
async function inspectRemoteDocument(fileId, bridgeUrl) {
|
|
313
|
+
const base = getBridgeUrl(bridgeUrl) || "http://127.0.0.1:5174";
|
|
314
|
+
const url = `${base}/api/documents/${encodeURIComponent(fileId)}`;
|
|
315
|
+
const res = await fetch(url);
|
|
316
|
+
if (!res.ok) {
|
|
317
|
+
const err = await res.json().catch(() => ({}));
|
|
318
|
+
throw new Error(err.error || `Documento n\xE3o encontrado: ${fileId}`);
|
|
319
|
+
}
|
|
320
|
+
const data = await res.json();
|
|
321
|
+
return data.document;
|
|
322
|
+
}
|
|
323
|
+
async function submitRemoteJobResult(jobId, agent, resultData, bridgeUrl) {
|
|
324
|
+
const base = getBridgeUrl(bridgeUrl) || "http://127.0.0.1:5174";
|
|
325
|
+
const url = `${base}/api/jobs/${encodeURIComponent(jobId)}/complete`;
|
|
326
|
+
const res = await fetch(url, {
|
|
327
|
+
method: "POST",
|
|
328
|
+
headers: { "Content-Type": "application/json" },
|
|
329
|
+
body: JSON.stringify({ agent, result: resultData })
|
|
330
|
+
});
|
|
331
|
+
if (!res.ok) {
|
|
332
|
+
const err = await res.json().catch(() => ({}));
|
|
333
|
+
throw new Error(err.error || `Falha ao submeter resultado do job ${jobId}`);
|
|
334
|
+
}
|
|
335
|
+
const data = await res.json();
|
|
336
|
+
return { job: data.job, resultId: data.resultId };
|
|
337
|
+
}
|
|
249
338
|
|
|
250
339
|
// src/tools/audit-profile.ts
|
|
251
340
|
function getExperienceBulletCount(exp) {
|
|
@@ -522,7 +611,7 @@ ${issues.length > 0 ? issues.map(
|
|
|
522
611
|
}
|
|
523
612
|
|
|
524
613
|
// src/tools/recruiter-simulator.ts
|
|
525
|
-
import { z as
|
|
614
|
+
import { z as z10 } from "zod";
|
|
526
615
|
|
|
527
616
|
// ../core/src/domain/date-range.ts
|
|
528
617
|
import { z as z2 } from "zod";
|
|
@@ -1013,14 +1102,404 @@ function termMatchesText(content, term) {
|
|
|
1013
1102
|
return false;
|
|
1014
1103
|
}
|
|
1015
1104
|
|
|
1105
|
+
// ../core/src/domain/job.ts
|
|
1106
|
+
import { z as z8 } from "zod";
|
|
1107
|
+
var jobTypeSchema = z8.enum([
|
|
1108
|
+
"diagnostic",
|
|
1109
|
+
"interview",
|
|
1110
|
+
"fact_confirmation",
|
|
1111
|
+
"rewrite",
|
|
1112
|
+
"action_hub"
|
|
1113
|
+
]);
|
|
1114
|
+
var jobStatusSchema = z8.enum([
|
|
1115
|
+
"created",
|
|
1116
|
+
"waiting_for_agent",
|
|
1117
|
+
"running",
|
|
1118
|
+
"waiting_for_user",
|
|
1119
|
+
"completed",
|
|
1120
|
+
"failed",
|
|
1121
|
+
"cancelled"
|
|
1122
|
+
]);
|
|
1123
|
+
var jobProgressPhaseSchema = z8.enum([
|
|
1124
|
+
"loading_document",
|
|
1125
|
+
"reading_document",
|
|
1126
|
+
"extracting_facts",
|
|
1127
|
+
"diagnosing",
|
|
1128
|
+
"writing_result"
|
|
1129
|
+
]);
|
|
1130
|
+
var agentIdentitySchema = z8.object({
|
|
1131
|
+
agentId: z8.string().min(1),
|
|
1132
|
+
sessionId: z8.string().min(1),
|
|
1133
|
+
clientName: z8.string().optional(),
|
|
1134
|
+
clientVersion: z8.string().optional()
|
|
1135
|
+
});
|
|
1136
|
+
var fileFingerprintSchema = z8.object({
|
|
1137
|
+
name: z8.string().min(1),
|
|
1138
|
+
size: z8.number().int().nonnegative(),
|
|
1139
|
+
lastModified: z8.number().nonnegative()
|
|
1140
|
+
});
|
|
1141
|
+
var documentHandleSchema = z8.object({
|
|
1142
|
+
fileId: z8.string().min(1),
|
|
1143
|
+
jobId: z8.string().min(1),
|
|
1144
|
+
name: z8.string().min(1),
|
|
1145
|
+
mediaType: z8.literal("application/pdf"),
|
|
1146
|
+
size: z8.number().int().nonnegative(),
|
|
1147
|
+
mtime: z8.number().nonnegative(),
|
|
1148
|
+
root: z8.enum(["downloads", "desktop", "documents"]),
|
|
1149
|
+
access: z8.literal("native-local-document"),
|
|
1150
|
+
expiresAt: z8.string()
|
|
1151
|
+
});
|
|
1152
|
+
var jobEventTypeSchema = z8.enum([
|
|
1153
|
+
"job.created",
|
|
1154
|
+
"job.ready_for_agent",
|
|
1155
|
+
"job.claimed",
|
|
1156
|
+
"job.progress",
|
|
1157
|
+
"job.lease_renewed",
|
|
1158
|
+
"job.waiting_for_user",
|
|
1159
|
+
"job.user_resumed",
|
|
1160
|
+
"job.lease_expired",
|
|
1161
|
+
"job.completed",
|
|
1162
|
+
"job.failed",
|
|
1163
|
+
"job.cancelled"
|
|
1164
|
+
]);
|
|
1165
|
+
var jobEventSchema = z8.object({
|
|
1166
|
+
id: z8.string().min(1),
|
|
1167
|
+
jobId: z8.string().min(1),
|
|
1168
|
+
commandId: z8.string().nullish(),
|
|
1169
|
+
type: jobEventTypeSchema,
|
|
1170
|
+
timestamp: z8.string(),
|
|
1171
|
+
payload: z8.unknown().optional()
|
|
1172
|
+
});
|
|
1173
|
+
var linkeGringoJobSchema = z8.object({
|
|
1174
|
+
id: z8.string().min(1),
|
|
1175
|
+
type: jobTypeSchema,
|
|
1176
|
+
status: jobStatusSchema,
|
|
1177
|
+
attempt: z8.number().int().nonnegative().default(0),
|
|
1178
|
+
maxAttempts: z8.number().int().positive().default(3),
|
|
1179
|
+
claimedBy: agentIdentitySchema.nullish(),
|
|
1180
|
+
leaseExpiresAt: z8.string().nullish(),
|
|
1181
|
+
expiresAt: z8.string(),
|
|
1182
|
+
progress: z8.number().min(0).max(100).default(0),
|
|
1183
|
+
currentPhase: jobProgressPhaseSchema.nullish(),
|
|
1184
|
+
progressMessage: z8.string().nullish(),
|
|
1185
|
+
resultId: z8.string().nullish(),
|
|
1186
|
+
fileHandleId: z8.string().nullish(),
|
|
1187
|
+
fileHandle: documentHandleSchema.optional(),
|
|
1188
|
+
targetRole: z8.string().nullish(),
|
|
1189
|
+
result: z8.unknown().optional(),
|
|
1190
|
+
error: z8.string().nullish(),
|
|
1191
|
+
createdAt: z8.string(),
|
|
1192
|
+
updatedAt: z8.string(),
|
|
1193
|
+
metadata: z8.record(z8.unknown()).optional()
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
// ../core/src/domain/job-state-machine.ts
|
|
1197
|
+
var InvalidJobTransitionError = class extends Error {
|
|
1198
|
+
currentStatus;
|
|
1199
|
+
commandType;
|
|
1200
|
+
constructor(currentStatus, commandType, message) {
|
|
1201
|
+
super(`Transi\xE7\xE3o de estado inv\xE1lida para o job [${currentStatus} -> ${commandType}]: ${message}`);
|
|
1202
|
+
this.name = "InvalidJobTransitionError";
|
|
1203
|
+
this.currentStatus = currentStatus;
|
|
1204
|
+
this.commandType = commandType;
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
var DEFAULT_LEASE_DURATION_MS = 6e4;
|
|
1208
|
+
function generateEventId() {
|
|
1209
|
+
return `evt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1210
|
+
}
|
|
1211
|
+
function transitionJob(job, command, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
1212
|
+
const timestamp = command.timestamp || nowIso;
|
|
1213
|
+
const eventId = generateEventId();
|
|
1214
|
+
if (command.type === "cancel") {
|
|
1215
|
+
if (job.status === "completed" || job.status === "failed" || job.status === "cancelled") {
|
|
1216
|
+
throw new InvalidJobTransitionError(
|
|
1217
|
+
job.status,
|
|
1218
|
+
command.type,
|
|
1219
|
+
"N\xE3o \xE9 poss\xEDvel cancelar um job que j\xE1 est\xE1 em estado terminal."
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
const nextJob = {
|
|
1223
|
+
...job,
|
|
1224
|
+
status: "cancelled",
|
|
1225
|
+
claimedBy: null,
|
|
1226
|
+
leaseExpiresAt: null,
|
|
1227
|
+
updatedAt: timestamp
|
|
1228
|
+
};
|
|
1229
|
+
const event = {
|
|
1230
|
+
id: eventId,
|
|
1231
|
+
jobId: job.id,
|
|
1232
|
+
commandId: command.commandId,
|
|
1233
|
+
type: "job.cancelled",
|
|
1234
|
+
timestamp,
|
|
1235
|
+
payload: { actor: command.actor, reason: command.reason }
|
|
1236
|
+
};
|
|
1237
|
+
return { nextJob, event };
|
|
1238
|
+
}
|
|
1239
|
+
if (command.type === "fail") {
|
|
1240
|
+
if (job.status === "completed" || job.status === "failed" || job.status === "cancelled") {
|
|
1241
|
+
throw new InvalidJobTransitionError(
|
|
1242
|
+
job.status,
|
|
1243
|
+
command.type,
|
|
1244
|
+
"N\xE3o \xE9 poss\xEDvel marcar falha em um job que j\xE1 est\xE1 em estado terminal."
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
const nextJob = {
|
|
1248
|
+
...job,
|
|
1249
|
+
status: "failed",
|
|
1250
|
+
claimedBy: null,
|
|
1251
|
+
leaseExpiresAt: null,
|
|
1252
|
+
progressMessage: command.error,
|
|
1253
|
+
updatedAt: timestamp
|
|
1254
|
+
};
|
|
1255
|
+
const event = {
|
|
1256
|
+
id: eventId,
|
|
1257
|
+
jobId: job.id,
|
|
1258
|
+
commandId: command.commandId,
|
|
1259
|
+
type: "job.failed",
|
|
1260
|
+
timestamp,
|
|
1261
|
+
payload: { error: command.error, actor: command.actor }
|
|
1262
|
+
};
|
|
1263
|
+
return { nextJob, event };
|
|
1264
|
+
}
|
|
1265
|
+
switch (job.status) {
|
|
1266
|
+
case "created": {
|
|
1267
|
+
if (command.type !== "ready_for_agent") {
|
|
1268
|
+
throw new InvalidJobTransitionError(
|
|
1269
|
+
job.status,
|
|
1270
|
+
command.type,
|
|
1271
|
+
"Jobs rec\xE9m-criados devem transitar para ready_for_agent."
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
const nextJob = {
|
|
1275
|
+
...job,
|
|
1276
|
+
status: "waiting_for_agent",
|
|
1277
|
+
updatedAt: timestamp
|
|
1278
|
+
};
|
|
1279
|
+
const event = {
|
|
1280
|
+
id: eventId,
|
|
1281
|
+
jobId: job.id,
|
|
1282
|
+
commandId: command.commandId,
|
|
1283
|
+
type: "job.ready_for_agent",
|
|
1284
|
+
timestamp
|
|
1285
|
+
};
|
|
1286
|
+
return { nextJob, event };
|
|
1287
|
+
}
|
|
1288
|
+
case "waiting_for_agent": {
|
|
1289
|
+
if (command.type !== "claim") {
|
|
1290
|
+
throw new InvalidJobTransitionError(
|
|
1291
|
+
job.status,
|
|
1292
|
+
command.type,
|
|
1293
|
+
"Jobs aguardando agente s\xF3 aceitam o comando claim."
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
const leaseMs = command.leaseDurationMs || DEFAULT_LEASE_DURATION_MS;
|
|
1297
|
+
const leaseExpiresAt = new Date(new Date(timestamp).getTime() + leaseMs).toISOString();
|
|
1298
|
+
const nextAttempt = job.attempt + 1;
|
|
1299
|
+
const nextJob = {
|
|
1300
|
+
...job,
|
|
1301
|
+
status: "running",
|
|
1302
|
+
attempt: nextAttempt,
|
|
1303
|
+
claimedBy: command.agent,
|
|
1304
|
+
leaseExpiresAt,
|
|
1305
|
+
updatedAt: timestamp
|
|
1306
|
+
};
|
|
1307
|
+
const event = {
|
|
1308
|
+
id: eventId,
|
|
1309
|
+
jobId: job.id,
|
|
1310
|
+
commandId: command.commandId,
|
|
1311
|
+
type: "job.claimed",
|
|
1312
|
+
timestamp,
|
|
1313
|
+
payload: { agent: command.agent, leaseExpiresAt, attempt: nextAttempt }
|
|
1314
|
+
};
|
|
1315
|
+
return { nextJob, event };
|
|
1316
|
+
}
|
|
1317
|
+
case "running": {
|
|
1318
|
+
if ("agent" in command) {
|
|
1319
|
+
if (!job.claimedBy || job.claimedBy.agentId !== command.agent.agentId) {
|
|
1320
|
+
throw new InvalidJobTransitionError(
|
|
1321
|
+
job.status,
|
|
1322
|
+
command.type,
|
|
1323
|
+
`Comando rejeitado: o job est\xE1 sob lease do agente [${job.claimedBy?.agentId || "desconhecido"}], mas foi chamado por [${command.agent.agentId}].`
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
if (command.type === "report_progress") {
|
|
1328
|
+
const leaseMs = command.leaseDurationMs || DEFAULT_LEASE_DURATION_MS;
|
|
1329
|
+
const leaseExpiresAt = new Date(new Date(timestamp).getTime() + leaseMs).toISOString();
|
|
1330
|
+
const nextProgress = typeof command.progress === "number" ? Math.max(0, Math.min(100, command.progress)) : job.progress;
|
|
1331
|
+
const nextJob = {
|
|
1332
|
+
...job,
|
|
1333
|
+
progress: nextProgress,
|
|
1334
|
+
currentPhase: command.phase ?? job.currentPhase,
|
|
1335
|
+
progressMessage: command.message ?? job.progressMessage,
|
|
1336
|
+
leaseExpiresAt,
|
|
1337
|
+
updatedAt: timestamp
|
|
1338
|
+
};
|
|
1339
|
+
const event = {
|
|
1340
|
+
id: eventId,
|
|
1341
|
+
jobId: job.id,
|
|
1342
|
+
commandId: command.commandId,
|
|
1343
|
+
type: "job.progress",
|
|
1344
|
+
timestamp,
|
|
1345
|
+
payload: {
|
|
1346
|
+
phase: nextJob.currentPhase,
|
|
1347
|
+
progress: nextJob.progress,
|
|
1348
|
+
message: nextJob.progressMessage,
|
|
1349
|
+
leaseExpiresAt
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
return { nextJob, event };
|
|
1353
|
+
}
|
|
1354
|
+
if (command.type === "request_user_action") {
|
|
1355
|
+
const nextJob = {
|
|
1356
|
+
...job,
|
|
1357
|
+
status: "waiting_for_user",
|
|
1358
|
+
leaseExpiresAt: null,
|
|
1359
|
+
progressMessage: command.reason,
|
|
1360
|
+
updatedAt: timestamp
|
|
1361
|
+
};
|
|
1362
|
+
const event = {
|
|
1363
|
+
id: eventId,
|
|
1364
|
+
jobId: job.id,
|
|
1365
|
+
commandId: command.commandId,
|
|
1366
|
+
type: "job.waiting_for_user",
|
|
1367
|
+
timestamp,
|
|
1368
|
+
payload: { reason: command.reason, prompt: command.prompt }
|
|
1369
|
+
};
|
|
1370
|
+
return { nextJob, event };
|
|
1371
|
+
}
|
|
1372
|
+
if (command.type === "complete") {
|
|
1373
|
+
const nextJob = {
|
|
1374
|
+
...job,
|
|
1375
|
+
status: "completed",
|
|
1376
|
+
progress: 100,
|
|
1377
|
+
resultId: command.resultId,
|
|
1378
|
+
claimedBy: null,
|
|
1379
|
+
leaseExpiresAt: null,
|
|
1380
|
+
updatedAt: timestamp
|
|
1381
|
+
};
|
|
1382
|
+
const event = {
|
|
1383
|
+
id: eventId,
|
|
1384
|
+
jobId: job.id,
|
|
1385
|
+
commandId: command.commandId,
|
|
1386
|
+
type: "job.completed",
|
|
1387
|
+
timestamp,
|
|
1388
|
+
payload: { resultId: command.resultId, agent: command.agent }
|
|
1389
|
+
};
|
|
1390
|
+
return { nextJob, event };
|
|
1391
|
+
}
|
|
1392
|
+
if (command.type === "expire_lease") {
|
|
1393
|
+
if (job.attempt >= job.maxAttempts) {
|
|
1394
|
+
const nextJob2 = {
|
|
1395
|
+
...job,
|
|
1396
|
+
status: "failed",
|
|
1397
|
+
claimedBy: null,
|
|
1398
|
+
leaseExpiresAt: null,
|
|
1399
|
+
progressMessage: "Limite m\xE1ximo de tentativas excedido ap\xF3s expira\xE7\xE3o de leases.",
|
|
1400
|
+
updatedAt: timestamp
|
|
1401
|
+
};
|
|
1402
|
+
const event2 = {
|
|
1403
|
+
id: eventId,
|
|
1404
|
+
jobId: job.id,
|
|
1405
|
+
commandId: command.commandId,
|
|
1406
|
+
type: "job.failed",
|
|
1407
|
+
timestamp,
|
|
1408
|
+
payload: { reason: "MAX_ATTEMPTS_EXCEEDED", attempt: job.attempt }
|
|
1409
|
+
};
|
|
1410
|
+
return { nextJob: nextJob2, event: event2 };
|
|
1411
|
+
}
|
|
1412
|
+
const nextJob = {
|
|
1413
|
+
...job,
|
|
1414
|
+
status: "waiting_for_agent",
|
|
1415
|
+
claimedBy: null,
|
|
1416
|
+
leaseExpiresAt: null,
|
|
1417
|
+
updatedAt: timestamp
|
|
1418
|
+
};
|
|
1419
|
+
const event = {
|
|
1420
|
+
id: eventId,
|
|
1421
|
+
jobId: job.id,
|
|
1422
|
+
commandId: command.commandId,
|
|
1423
|
+
type: "job.lease_expired",
|
|
1424
|
+
timestamp,
|
|
1425
|
+
payload: { previousAttempt: job.attempt, maxAttempts: job.maxAttempts, reason: command.reason }
|
|
1426
|
+
};
|
|
1427
|
+
return { nextJob, event };
|
|
1428
|
+
}
|
|
1429
|
+
throw new InvalidJobTransitionError(
|
|
1430
|
+
job.status,
|
|
1431
|
+
command.type,
|
|
1432
|
+
`Comando [${command.type}] n\xE3o permitido enquanto o job est\xE1 running.`
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
case "waiting_for_user": {
|
|
1436
|
+
if (command.type !== "user_resume") {
|
|
1437
|
+
throw new InvalidJobTransitionError(
|
|
1438
|
+
job.status,
|
|
1439
|
+
command.type,
|
|
1440
|
+
"Jobs aguardando usu\xE1rio s\xF3 aceitam o comando user_resume."
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
const nextJob = {
|
|
1444
|
+
...job,
|
|
1445
|
+
status: "running",
|
|
1446
|
+
updatedAt: timestamp
|
|
1447
|
+
};
|
|
1448
|
+
const event = {
|
|
1449
|
+
id: eventId,
|
|
1450
|
+
jobId: job.id,
|
|
1451
|
+
commandId: command.commandId,
|
|
1452
|
+
type: "job.user_resumed",
|
|
1453
|
+
timestamp,
|
|
1454
|
+
payload: { actor: command.actor, reason: command.reason, payload: command.payload }
|
|
1455
|
+
};
|
|
1456
|
+
return { nextJob, event };
|
|
1457
|
+
}
|
|
1458
|
+
case "completed":
|
|
1459
|
+
case "failed":
|
|
1460
|
+
case "cancelled": {
|
|
1461
|
+
throw new InvalidJobTransitionError(
|
|
1462
|
+
job.status,
|
|
1463
|
+
command.type,
|
|
1464
|
+
`O job j\xE1 est\xE1 em estado terminal [${job.status}] e n\xE3o pode receber novas transi\xE7\xF5es.`
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// ../core/src/domain/job-results.ts
|
|
1471
|
+
import { z as z9 } from "zod";
|
|
1472
|
+
var jobResultEnvelopeSchema = z9.object({
|
|
1473
|
+
id: z9.string().min(1),
|
|
1474
|
+
jobId: z9.string().min(1),
|
|
1475
|
+
schemaVersion: z9.string().default("1.0"),
|
|
1476
|
+
submissionId: z9.string().min(1),
|
|
1477
|
+
submittedBy: agentIdentitySchema,
|
|
1478
|
+
createdAt: z9.string(),
|
|
1479
|
+
payload: z9.unknown()
|
|
1480
|
+
});
|
|
1481
|
+
var diagnosticResultPayloadSchema = z9.object({
|
|
1482
|
+
profile: profileSchema,
|
|
1483
|
+
review: profileReviewSchema
|
|
1484
|
+
});
|
|
1485
|
+
var interviewResultPayloadSchema = z9.object({
|
|
1486
|
+
interviewPlan: interviewPlanSchema
|
|
1487
|
+
});
|
|
1488
|
+
var factConfirmationResultPayloadSchema = z9.object({
|
|
1489
|
+
confirmedFacts: z9.array(confirmedFactSchema)
|
|
1490
|
+
});
|
|
1491
|
+
var rewriteResultPayloadSchema = z9.object({
|
|
1492
|
+
profileAnalysis: profileAnalysisSchema
|
|
1493
|
+
});
|
|
1494
|
+
|
|
1016
1495
|
// src/tools/recruiter-simulator.ts
|
|
1017
|
-
var simulateRecruiterSearchInputSchema =
|
|
1018
|
-
headline:
|
|
1019
|
-
summary:
|
|
1020
|
-
skills:
|
|
1021
|
-
experienceBullets:
|
|
1022
|
-
targetRole:
|
|
1023
|
-
requiredKeywords:
|
|
1496
|
+
var simulateRecruiterSearchInputSchema = z10.object({
|
|
1497
|
+
headline: z10.string().describe("Headline atual ou proposta do candidato"),
|
|
1498
|
+
summary: z10.string().default("").describe("Resumo ou se\xE7\xE3o About do perfil"),
|
|
1499
|
+
skills: z10.array(z10.string()).default([]).describe("Lista de compet\xEAncias t\xE9cnicas registradas"),
|
|
1500
|
+
experienceBullets: z10.array(z10.string()).default([]).describe("Bullets das experi\xEAncias profissionais"),
|
|
1501
|
+
targetRole: z10.string().default("Senior Software Engineer").describe("Cargo-alvo da busca (ex: Senior Backend Engineer)"),
|
|
1502
|
+
requiredKeywords: z10.array(z10.string()).optional().describe('Termos t\xE9cnicos ou palavras-chave obrigat\xF3rias a testar (ex: ["Go", "Kubernetes", "Microservices"])')
|
|
1024
1503
|
});
|
|
1025
1504
|
async function handleSimulateRecruiterSearch(input) {
|
|
1026
1505
|
const defaultKeywords = input.requiredKeywords?.length ? input.requiredKeywords : [input.targetRole, "Senior", "Remote", "Architecture", "Scale"];
|
|
@@ -1086,13 +1565,13 @@ ${missingCount > 0 ? `
|
|
|
1086
1565
|
}
|
|
1087
1566
|
|
|
1088
1567
|
// src/tools/xyz-bullet-converter.ts
|
|
1089
|
-
import { z as
|
|
1090
|
-
var convertToXyzBulletInputSchema =
|
|
1091
|
-
rawBullet:
|
|
1092
|
-
roleContext:
|
|
1093
|
-
action:
|
|
1094
|
-
metric:
|
|
1095
|
-
method:
|
|
1568
|
+
import { z as z11 } from "zod";
|
|
1569
|
+
var convertToXyzBulletInputSchema = z11.object({
|
|
1570
|
+
rawBullet: z11.string().describe('Bullet original descritivo ou passivo (ex: "Desenvolvi microsservi\xE7os em Go para pagamentos")'),
|
|
1571
|
+
roleContext: z11.string().default("Senior Software Engineer").describe('Contexto da empresa, cargo ou projeto (ex: "Fintech de pagamentos, alta escala")'),
|
|
1572
|
+
action: z11.string().optional().describe('A\xE7\xE3o de impacto com verbo no passado (ex: "Architected and deployed distributed payment services")'),
|
|
1573
|
+
metric: z11.string().optional().describe('M\xE9trica quantitativa [Y] (ex: "reducing p99 latency by 35% and scaling to 12,000 RPS")'),
|
|
1574
|
+
method: z11.string().optional().describe('Como foi feito [Z] (ex: "by migrating monolith endpoints to Go microservices on AWS EKS")')
|
|
1096
1575
|
});
|
|
1097
1576
|
function formatGoogleXyzBullet(parts) {
|
|
1098
1577
|
const cleanAction = parts.action.trim().replace(/[.,;]+$/, "");
|
|
@@ -1190,12 +1669,12 @@ Pergunte ao candidato qual m\xE9trica real mais se aproxima da sua entrega (${ha
|
|
|
1190
1669
|
}
|
|
1191
1670
|
|
|
1192
1671
|
// src/tools/headline-generator.ts
|
|
1193
|
-
import { z as
|
|
1194
|
-
var generateHeadlineInputSchema =
|
|
1195
|
-
targetRole:
|
|
1196
|
-
coreTechnologies:
|
|
1197
|
-
keyDifferentiator:
|
|
1198
|
-
seniorityOrScope:
|
|
1672
|
+
import { z as z12 } from "zod";
|
|
1673
|
+
var generateHeadlineInputSchema = z12.object({
|
|
1674
|
+
targetRole: z12.string().default("Senior Software Engineer").describe("Cargo pretendido em ingl\xEAs (ex: Staff Distributed Systems Engineer)"),
|
|
1675
|
+
coreTechnologies: z12.array(z12.string()).min(1).max(5).default(["TypeScript", "React", "Node.js"]).describe("3 a 4 tecnologias centrais e mais procuradas da sua stack"),
|
|
1676
|
+
keyDifferentiator: z12.string().optional().describe("Diferencial ou escopo t\xE9cnico (ex: High Scale, Fintech, Cloud Architecture)"),
|
|
1677
|
+
seniorityOrScope: z12.string().default("US Remote").describe("Senioridade ou disponibilidade (ex: US Remote, Global Teams, Staff)")
|
|
1199
1678
|
});
|
|
1200
1679
|
async function handleGenerateHeadline(input) {
|
|
1201
1680
|
const coreTechs = Array.isArray(input.coreTechnologies) ? input.coreTechnologies : ["TypeScript", "React", "Node.js"];
|
|
@@ -1255,7 +1734,7 @@ ${proposals.map(
|
|
|
1255
1734
|
}
|
|
1256
1735
|
|
|
1257
1736
|
// src/tools/get-pending-job.ts
|
|
1258
|
-
import { z as
|
|
1737
|
+
import { z as z13 } from "zod";
|
|
1259
1738
|
|
|
1260
1739
|
// src/tools/format-job.ts
|
|
1261
1740
|
function formatJobResponse(job) {
|
|
@@ -1384,8 +1863,9 @@ function formatJobResponse(job) {
|
|
|
1384
1863
|
"id": "q1",
|
|
1385
1864
|
"category": "scale",
|
|
1386
1865
|
"question": "Texto claro da pergunta para investigar o escopo e impacto...",
|
|
1387
|
-
"
|
|
1388
|
-
"
|
|
1866
|
+
"reason": "Crit\xE9rio dos recrutadores dos EUA: por que esta pergunta \xE9 importante...",
|
|
1867
|
+
"answerType": "long-text",
|
|
1868
|
+
"placeholderExample": "Ex: Processamos 7M de requisi\xE7\xF5es por m\xEAs com lat\xEAncia P99 < 200ms..."
|
|
1389
1869
|
}
|
|
1390
1870
|
]
|
|
1391
1871
|
}
|
|
@@ -1449,8 +1929,8 @@ ${instructions}`
|
|
|
1449
1929
|
}
|
|
1450
1930
|
|
|
1451
1931
|
// src/tools/get-pending-job.ts
|
|
1452
|
-
var getPendingJobInputSchema =
|
|
1453
|
-
jobId:
|
|
1932
|
+
var getPendingJobInputSchema = z13.object({
|
|
1933
|
+
jobId: z13.string().optional().describe("ID espec\xEDfico do job a ser buscado (opcional. Se omitido, pega o pr\xF3ximo da fila)")
|
|
1454
1934
|
});
|
|
1455
1935
|
async function handleGetPendingJob(input = {}) {
|
|
1456
1936
|
const job = await getRemoteOrLocalPendingJob(input.jobId);
|
|
@@ -1473,12 +1953,12 @@ async function handleGetPendingJob(input = {}) {
|
|
|
1473
1953
|
}
|
|
1474
1954
|
|
|
1475
1955
|
// src/tools/submit-job-result.ts
|
|
1476
|
-
import { z as
|
|
1477
|
-
var submitJobResultInputSchema =
|
|
1478
|
-
jobId:
|
|
1479
|
-
result:
|
|
1480
|
-
status:
|
|
1481
|
-
error:
|
|
1956
|
+
import { z as z14 } from "zod";
|
|
1957
|
+
var submitJobResultInputSchema = z14.object({
|
|
1958
|
+
jobId: z14.string().describe("ID do job a ser conclu\xEDdo"),
|
|
1959
|
+
result: z14.any().describe("JSON com os dados estruturados exigidos pelo front-end (ex: ParseAndDiagnoseResult, InterviewPlan, ProfileAnalysis)"),
|
|
1960
|
+
status: z14.enum(["completed", "failed"]).default("completed").describe("Status final do job"),
|
|
1961
|
+
error: z14.string().optional().describe('Mensagem de erro caso o status seja "failed"')
|
|
1482
1962
|
});
|
|
1483
1963
|
async function handleSubmitJobResult(input) {
|
|
1484
1964
|
try {
|
|
@@ -1538,13 +2018,13 @@ O front-end em \`localhost:5173\` acabou de receber a resposta formatada e atual
|
|
|
1538
2018
|
}
|
|
1539
2019
|
|
|
1540
2020
|
// src/tools/watch-linkegringo.ts
|
|
1541
|
-
import { z as
|
|
2021
|
+
import { z as z15 } from "zod";
|
|
1542
2022
|
import http from "http";
|
|
1543
|
-
var watchLinkeGringoInputSchema =
|
|
1544
|
-
timeoutSeconds:
|
|
2023
|
+
var watchLinkeGringoInputSchema = z15.object({
|
|
2024
|
+
timeoutSeconds: z15.number().optional().describe(
|
|
1545
2025
|
"Tempo m\xE1ximo em segundos para aguardar a chegada de um novo job da interface web do LinkeGringo (padr\xE3o: 60s, m\xE1x: 300s). Se j\xE1 houver um job na fila, retorna imediatamente."
|
|
1546
2026
|
),
|
|
1547
|
-
bridgeUrl:
|
|
2027
|
+
bridgeUrl: z15.string().optional().describe("URL base do bridge HTTP local do LinkeGringo (padr\xE3o: http://127.0.0.1:5174)")
|
|
1548
2028
|
});
|
|
1549
2029
|
async function waitForNextJob(bridgeUrl, timeoutMs) {
|
|
1550
2030
|
const localPending = bridgeJobStore.getPendingJob();
|
|
@@ -1675,129 +2155,551 @@ A interface web est\xE1 conectada ao Bridge. Assim que o usu\xE1rio clicar em **
|
|
|
1675
2155
|
return formatJobResponse(job);
|
|
1676
2156
|
}
|
|
1677
2157
|
|
|
1678
|
-
// src/
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
},
|
|
1720
|
-
async (args) => {
|
|
1721
|
-
return await handleGenerateHeadline(args);
|
|
1722
|
-
}
|
|
1723
|
-
);
|
|
1724
|
-
server.registerTool(
|
|
1725
|
-
"get_pending_job",
|
|
1726
|
-
{
|
|
1727
|
-
description: "Obt\xE9m os dados completos de um job pelo ID (ou o pr\xF3ximo da fila). Use APENAS quando for notificado de um job espec\xEDfico (ex: por watcher em background). N\xC3O use esta ferramenta para iniciar escuta ou checar jobs antes de iniciar o watch.",
|
|
1728
|
-
inputSchema: getPendingJobInputSchema.shape
|
|
1729
|
-
},
|
|
1730
|
-
async (args) => {
|
|
1731
|
-
return await handleGetPendingJob(args);
|
|
1732
|
-
}
|
|
1733
|
-
);
|
|
1734
|
-
server.registerTool(
|
|
1735
|
-
"submit_job_result",
|
|
1736
|
-
{
|
|
1737
|
-
description: "Envia o resultado estruturado do processamento de um job de volta para o navegador do LinkeGringo, desbloqueando a tela do usu\xE1rio instantaneamente. Sempre chame esta ferramenta imediatamente ap\xF3s processar um job recebido por watch_linkegringo ou get_pending_job.",
|
|
1738
|
-
inputSchema: submitJobResultInputSchema.shape
|
|
1739
|
-
},
|
|
1740
|
-
async (args) => {
|
|
1741
|
-
return await handleSubmitJobResult(args);
|
|
1742
|
-
}
|
|
1743
|
-
);
|
|
1744
|
-
server.registerTool(
|
|
1745
|
-
"watch_linkegringo",
|
|
1746
|
-
{
|
|
1747
|
-
description: 'Inicia a escuta ativa (watch) por jobs enviados pela interface web do LinkeGringo (http://127.0.0.1:5174). Use esta ferramenta SEMPRE que o usu\xE1rio disser "iniciar watch", "escutar", "conectar ao LinkeGringo" ou similar. N\xC3O chame get_pending_job antes desta ferramenta. Bloqueia aguardando e retorna o job assim que ele for submetido no navegador.',
|
|
1748
|
-
inputSchema: watchLinkeGringoInputSchema.shape
|
|
1749
|
-
},
|
|
1750
|
-
async (args) => {
|
|
1751
|
-
return await handleWatchLinkeGringo(args);
|
|
1752
|
-
}
|
|
1753
|
-
);
|
|
1754
|
-
server.registerResource(
|
|
1755
|
-
"guidelines",
|
|
1756
|
-
"linkegringo://guidelines",
|
|
1757
|
-
{
|
|
1758
|
-
title: "Diretrizes Oficiais do LinkeGringo para Vagas nos EUA",
|
|
1759
|
-
description: "Princ\xEDpios fundamentais: f\xF3rmula Google XYZ, headlines de at\xE9 160 caracteres, elimina\xE7\xE3o de red flags culturais brasileiras e maximiza\xE7\xE3o de Inbound Readiness.",
|
|
1760
|
-
mimeType: "text/markdown"
|
|
1761
|
-
},
|
|
1762
|
-
async () => {
|
|
1763
|
-
return {
|
|
1764
|
-
contents: [
|
|
1765
|
-
{
|
|
1766
|
-
uri: "linkegringo://guidelines",
|
|
1767
|
-
text: `
|
|
1768
|
-
# Diretrizes Oficiais LinkeGringo: Otimiza\xE7\xE3o de Perfil para Recrutadores dos EUA
|
|
1769
|
-
|
|
1770
|
-
1. **Headline $le$ 160 caracteres**:
|
|
1771
|
-
- Formato recomendado: \`[Cargo Espec\xEDfico] | [3-4 Tecnologias Core] | [Escala/Dom\xEDnio] | US Remote\`
|
|
1772
|
-
- Evite slogans vagos ("Apaixonado por tecnologia", "Resolvendo problemas complexos").
|
|
1773
|
-
- Headline tem peso 3x no algoritmo de busca do LinkedIn Recruiter.
|
|
1774
|
-
|
|
1775
|
-
2. **F\xF3rmula Google XYZ para Experi\xEAncias**:
|
|
1776
|
-
- Toda conquista deve responder: *"Accomplished [X], measured by [Y], by doing [Z]"*.
|
|
1777
|
-
- Exemplo: *"Architected distributed event-driven payment service in Go, reducing p99 latency by 42% and scaling to 15,000 requests/sec."*
|
|
2158
|
+
// src/tools/list-jobs.ts
|
|
2159
|
+
import { z as z16 } from "zod";
|
|
2160
|
+
var listJobsInputSchema = z16.object({
|
|
2161
|
+
status: jobStatusSchema.optional().describe("Filtra jobs pelo status atual (ex: waiting_for_agent, running, completed)"),
|
|
2162
|
+
type: jobTypeSchema.optional().describe("Filtra jobs pelo tipo (ex: diagnostic, interview, rewrite)")
|
|
2163
|
+
});
|
|
2164
|
+
async function handleListJobs(input = {}) {
|
|
2165
|
+
try {
|
|
2166
|
+
const jobs = await listRemoteJobs(input);
|
|
2167
|
+
return {
|
|
2168
|
+
content: [
|
|
2169
|
+
{
|
|
2170
|
+
type: "text",
|
|
2171
|
+
text: `Encontrados ${jobs.length} job(s)${input.status ? ` com status "${input.status}"` : ""}.`
|
|
2172
|
+
}
|
|
2173
|
+
],
|
|
2174
|
+
structuredData: {
|
|
2175
|
+
total: jobs.length,
|
|
2176
|
+
jobs: jobs.map((j) => ({
|
|
2177
|
+
id: j.id,
|
|
2178
|
+
type: j.type,
|
|
2179
|
+
status: j.status,
|
|
2180
|
+
targetRole: j.targetRole,
|
|
2181
|
+
fileHandleId: j.fileHandleId,
|
|
2182
|
+
createdAt: j.createdAt,
|
|
2183
|
+
leaseExpiresAt: j.leaseExpiresAt
|
|
2184
|
+
}))
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
} catch (err) {
|
|
2188
|
+
return {
|
|
2189
|
+
isError: true,
|
|
2190
|
+
content: [
|
|
2191
|
+
{
|
|
2192
|
+
type: "text",
|
|
2193
|
+
text: `Erro ao listar jobs: ${err.message}`
|
|
2194
|
+
}
|
|
2195
|
+
]
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
1778
2199
|
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
2200
|
+
// src/tools/claim-job.ts
|
|
2201
|
+
import { z as z17 } from "zod";
|
|
2202
|
+
var claimJobInputSchema = z17.object({
|
|
2203
|
+
jobId: z17.string().min(1).describe("ID \xFAnico do job a ser assumido (ex: job_123)"),
|
|
2204
|
+
agent: agentIdentitySchema.describe("Identidade do agente assumindo o job (agentId e sessionId)"),
|
|
2205
|
+
leaseDurationMs: z17.number().int().positive().optional().describe("Dura\xE7\xE3o do lease exclusivo em milissegundos (padr\xE3o: 60000ms / 60s)")
|
|
2206
|
+
});
|
|
2207
|
+
async function handleClaimJob(input) {
|
|
2208
|
+
try {
|
|
2209
|
+
const job = await claimRemoteJob(input.jobId, input.agent, input.leaseDurationMs);
|
|
2210
|
+
return {
|
|
2211
|
+
content: [
|
|
2212
|
+
{
|
|
2213
|
+
type: "text",
|
|
2214
|
+
text: `Job ${job.id} assumido com sucesso pelo agente ${input.agent.agentId}. Status atual: ${job.status}. Lease expira em: ${job.leaseExpiresAt}.`
|
|
2215
|
+
}
|
|
2216
|
+
],
|
|
2217
|
+
structuredData: {
|
|
2218
|
+
success: true,
|
|
2219
|
+
job
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
} catch (err) {
|
|
2223
|
+
return {
|
|
2224
|
+
isError: true,
|
|
2225
|
+
content: [
|
|
2226
|
+
{
|
|
2227
|
+
type: "text",
|
|
2228
|
+
text: `Falha ao dar claim no job ${input.jobId}: ${err.message}`
|
|
2229
|
+
}
|
|
2230
|
+
]
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
1789
2233
|
}
|
|
1790
2234
|
|
|
1791
|
-
// src/
|
|
1792
|
-
import
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
2235
|
+
// src/tools/report-progress.ts
|
|
2236
|
+
import { z as z18 } from "zod";
|
|
2237
|
+
var reportProgressInputSchema = z18.object({
|
|
2238
|
+
jobId: z18.string().min(1).describe("ID do job cujo progresso est\xE1 sendo atualizado"),
|
|
2239
|
+
agent: agentIdentitySchema.describe("Identidade do agente atual com lease no job"),
|
|
2240
|
+
phase: jobProgressPhaseSchema.optional().describe("Fase atual do trabalho (ex: loading_document, extracting_facts, diagnosing, writing_result)"),
|
|
2241
|
+
progress: z18.number().min(0).max(100).optional().describe("Porcentagem de progresso num\xE9rico de 0 a 100"),
|
|
2242
|
+
message: z18.string().optional().describe("Mensagem descritiva e concisa exibida para o usu\xE1rio na interface web"),
|
|
2243
|
+
leaseDurationMs: z18.number().int().positive().optional().describe("Tempo adicional de renova\xE7\xE3o do lease em ms (padr\xE3o: 60000ms)")
|
|
2244
|
+
});
|
|
2245
|
+
async function handleReportProgress(input) {
|
|
2246
|
+
try {
|
|
2247
|
+
const job = await reportRemoteProgress(input.jobId, input.agent, {
|
|
2248
|
+
phase: input.phase,
|
|
2249
|
+
progress: input.progress,
|
|
2250
|
+
message: input.message,
|
|
2251
|
+
leaseDurationMs: input.leaseDurationMs
|
|
2252
|
+
});
|
|
2253
|
+
return {
|
|
2254
|
+
content: [
|
|
2255
|
+
{
|
|
2256
|
+
type: "text",
|
|
2257
|
+
text: `Progresso do job ${job.id} atualizado: ${input.progress ?? job.progress}% (${input.phase ?? job.currentPhase ?? "em execu\xE7\xE3o"}). Lease renovado at\xE9 ${job.leaseExpiresAt}.`
|
|
2258
|
+
}
|
|
2259
|
+
],
|
|
2260
|
+
structuredData: {
|
|
2261
|
+
success: true,
|
|
2262
|
+
jobId: job.id,
|
|
2263
|
+
progress: job.progress,
|
|
2264
|
+
phase: job.currentPhase ?? input.phase,
|
|
2265
|
+
leaseExpiresAt: job.leaseExpiresAt
|
|
2266
|
+
}
|
|
2267
|
+
};
|
|
2268
|
+
} catch (err) {
|
|
2269
|
+
return {
|
|
2270
|
+
isError: true,
|
|
2271
|
+
content: [
|
|
2272
|
+
{
|
|
2273
|
+
type: "text",
|
|
2274
|
+
text: `Falha ao reportar progresso no job ${input.jobId}: ${err.message}`
|
|
2275
|
+
}
|
|
2276
|
+
]
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
// src/tools/inspect-document.ts
|
|
2282
|
+
import { z as z19 } from "zod";
|
|
2283
|
+
var inspectDocumentInputSchema = z19.object({
|
|
2284
|
+
fileId: z19.string().min(1).describe("Capability token opaco do documento registrado (ex: doc_123)")
|
|
2285
|
+
});
|
|
2286
|
+
async function handleInspectDocument(input) {
|
|
2287
|
+
try {
|
|
2288
|
+
const document = await inspectRemoteDocument(input.fileId);
|
|
2289
|
+
return {
|
|
2290
|
+
content: [
|
|
2291
|
+
{
|
|
2292
|
+
type: "text",
|
|
2293
|
+
text: `Documento inspecionado com sucesso: "${document.name}" (${(document.size / 1024).toFixed(1)} KB, ${document.mediaType}). Modo de acesso: ${document.access}. Origem: ${document.root}.`
|
|
2294
|
+
}
|
|
2295
|
+
],
|
|
2296
|
+
structuredData: {
|
|
2297
|
+
fileId: document.fileId,
|
|
2298
|
+
jobId: document.jobId,
|
|
2299
|
+
name: document.name,
|
|
2300
|
+
mediaType: document.mediaType,
|
|
2301
|
+
size: document.size,
|
|
2302
|
+
mtime: document.mtime,
|
|
2303
|
+
root: document.root,
|
|
2304
|
+
access: document.access,
|
|
2305
|
+
expiresAt: document.expiresAt
|
|
2306
|
+
}
|
|
2307
|
+
};
|
|
2308
|
+
} catch (err) {
|
|
2309
|
+
return {
|
|
2310
|
+
isError: true,
|
|
2311
|
+
content: [
|
|
2312
|
+
{
|
|
2313
|
+
type: "text",
|
|
2314
|
+
text: `Falha ao inspecionar documento ${input.fileId}: ${err.message}`
|
|
2315
|
+
}
|
|
2316
|
+
]
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
// src/tools/request-user-action.ts
|
|
2322
|
+
import { z as z20 } from "zod";
|
|
2323
|
+
var requestUserActionInputSchema = z20.object({
|
|
2324
|
+
jobId: z20.string().min(1).describe("ID do job que requer interven\xE7\xE3o humana"),
|
|
2325
|
+
agent: agentIdentitySchema.describe("Identidade do agente"),
|
|
2326
|
+
reason: z20.string().min(1).describe('Motivo da pausa (ex: "needs_fact_confirmation", "unclear_company_name")'),
|
|
2327
|
+
prompt: z20.string().optional().describe("Instru\xE7\xE3o ou pergunta clara exibida para o usu\xE1rio na interface web")
|
|
2328
|
+
});
|
|
2329
|
+
async function handleRequestUserAction(input) {
|
|
2330
|
+
try {
|
|
2331
|
+
const job = await requestRemoteUserAction(input.jobId, input.agent, {
|
|
2332
|
+
reason: input.reason,
|
|
2333
|
+
prompt: input.prompt
|
|
2334
|
+
});
|
|
2335
|
+
return {
|
|
2336
|
+
content: [
|
|
2337
|
+
{
|
|
2338
|
+
type: "text",
|
|
2339
|
+
text: `Job ${job.id} colocado em pausa aguardando a\xE7\xE3o do usu\xE1rio (status: waiting_for_user). Motivo: ${input.reason}. O usu\xE1rio responder\xE1 na interface web.`
|
|
2340
|
+
}
|
|
2341
|
+
],
|
|
2342
|
+
structuredData: {
|
|
2343
|
+
success: true,
|
|
2344
|
+
jobId: job.id,
|
|
2345
|
+
status: job.status,
|
|
2346
|
+
reason: input.reason
|
|
2347
|
+
}
|
|
2348
|
+
};
|
|
2349
|
+
} catch (err) {
|
|
2350
|
+
return {
|
|
2351
|
+
isError: true,
|
|
2352
|
+
content: [
|
|
2353
|
+
{
|
|
2354
|
+
type: "text",
|
|
2355
|
+
text: `Falha ao solicitar interven\xE7\xE3o do usu\xE1rio no job ${input.jobId}: ${err.message}`
|
|
2356
|
+
}
|
|
2357
|
+
]
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
// src/tools/submit-diagnostic.ts
|
|
2363
|
+
import { z as z21 } from "zod";
|
|
2364
|
+
var submitDiagnosticInputSchema = z21.object({
|
|
2365
|
+
jobId: z21.string().min(1).describe("ID do job de diagn\xF3stico a ser finalizado"),
|
|
2366
|
+
agent: agentIdentitySchema.describe("Identidade do agente"),
|
|
2367
|
+
submissionId: z21.string().optional().describe("ID de submiss\xE3o para deduplica\xE7\xE3o idempotente (opcional)"),
|
|
2368
|
+
payload: diagnosticResultPayloadSchema.describe(
|
|
2369
|
+
"Resultado estruturado com Profile extra\xEDdo e ProfileReview detalhado"
|
|
2370
|
+
)
|
|
2371
|
+
});
|
|
2372
|
+
async function handleSubmitDiagnostic(input) {
|
|
2373
|
+
try {
|
|
2374
|
+
const submissionId = input.submissionId || `sub_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2375
|
+
const { job, resultId } = await submitRemoteJobResult(input.jobId, input.agent, {
|
|
2376
|
+
submissionId,
|
|
2377
|
+
schemaVersion: "1.0",
|
|
2378
|
+
payload: input.payload
|
|
2379
|
+
});
|
|
2380
|
+
const score = input.payload.review.overallScore;
|
|
2381
|
+
return {
|
|
2382
|
+
content: [
|
|
2383
|
+
{
|
|
2384
|
+
type: "text",
|
|
2385
|
+
text: `Diagn\xF3stico do job ${job.id} submetido com sucesso! Result ID: ${resultId}. Score Geral: ${score}/100. A interface web foi atualizada instantaneamente.`
|
|
2386
|
+
}
|
|
2387
|
+
],
|
|
2388
|
+
structuredData: {
|
|
2389
|
+
success: true,
|
|
2390
|
+
jobId: job.id,
|
|
2391
|
+
resultId,
|
|
2392
|
+
score
|
|
2393
|
+
}
|
|
2394
|
+
};
|
|
2395
|
+
} catch (err) {
|
|
2396
|
+
return {
|
|
2397
|
+
isError: true,
|
|
2398
|
+
content: [
|
|
2399
|
+
{
|
|
2400
|
+
type: "text",
|
|
2401
|
+
text: `Falha ao submeter diagn\xF3stico para o job ${input.jobId}: ${err.message}`
|
|
2402
|
+
}
|
|
2403
|
+
]
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
// src/tools/submit-interview.ts
|
|
2409
|
+
import { z as z22 } from "zod";
|
|
2410
|
+
var submitInterviewInputSchema = z22.object({
|
|
2411
|
+
jobId: z22.string().min(1).describe("ID do job de entrevista a ser finalizado"),
|
|
2412
|
+
agent: agentIdentitySchema.describe("Identidade do agente"),
|
|
2413
|
+
submissionId: z22.string().optional().describe("ID de submiss\xE3o para deduplica\xE7\xE3o idempotente (opcional)"),
|
|
2414
|
+
payload: interviewResultPayloadSchema.describe(
|
|
2415
|
+
"Plano de entrevista estruturado contendo as perguntas estrat\xE9gicas para destravar o perfil"
|
|
2416
|
+
)
|
|
2417
|
+
});
|
|
2418
|
+
async function handleSubmitInterview(input) {
|
|
2419
|
+
try {
|
|
2420
|
+
const submissionId = input.submissionId || `sub_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2421
|
+
const { job, resultId } = await submitRemoteJobResult(input.jobId, input.agent, {
|
|
2422
|
+
submissionId,
|
|
2423
|
+
schemaVersion: "1.0",
|
|
2424
|
+
payload: input.payload
|
|
2425
|
+
});
|
|
2426
|
+
const questionCount = input.payload.interviewPlan.questions?.length ?? 0;
|
|
2427
|
+
return {
|
|
2428
|
+
content: [
|
|
2429
|
+
{
|
|
2430
|
+
type: "text",
|
|
2431
|
+
text: `Plano de entrevista para o job ${job.id} submetido com sucesso! Result ID: ${resultId}. Total de perguntas: ${questionCount}. O usu\xE1rio agora responder\xE1 as perguntas na interface web.`
|
|
2432
|
+
}
|
|
2433
|
+
],
|
|
2434
|
+
structuredData: {
|
|
2435
|
+
success: true,
|
|
2436
|
+
jobId: job.id,
|
|
2437
|
+
resultId,
|
|
2438
|
+
questionCount
|
|
2439
|
+
}
|
|
2440
|
+
};
|
|
2441
|
+
} catch (err) {
|
|
2442
|
+
return {
|
|
2443
|
+
isError: true,
|
|
2444
|
+
content: [
|
|
2445
|
+
{
|
|
2446
|
+
type: "text",
|
|
2447
|
+
text: `Falha ao submeter plano de entrevista para o job ${input.jobId}: ${err.message}`
|
|
2448
|
+
}
|
|
2449
|
+
]
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
// src/tools/submit-rewrite.ts
|
|
2455
|
+
import { z as z23 } from "zod";
|
|
2456
|
+
var submitRewriteInputSchema = z23.object({
|
|
2457
|
+
jobId: z23.string().min(1).describe("ID do job de reescrita a ser finalizado"),
|
|
2458
|
+
agent: agentIdentitySchema.describe("Identidade do agente"),
|
|
2459
|
+
submissionId: z23.string().optional().describe("ID de submiss\xE3o para deduplica\xE7\xE3o idempotente (opcional)"),
|
|
2460
|
+
payload: rewriteResultPayloadSchema.describe(
|
|
2461
|
+
"An\xE1lise completa e perfil reescrito (ProfileAnalysis) contendo headlines, about, experi\xEAncias expandidas em Google XYZ e compara\xE7\xE3o de notas"
|
|
2462
|
+
)
|
|
2463
|
+
});
|
|
2464
|
+
async function handleSubmitRewrite(input) {
|
|
2465
|
+
try {
|
|
2466
|
+
const submissionId = input.submissionId || `sub_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2467
|
+
const { job, resultId } = await submitRemoteJobResult(input.jobId, input.agent, {
|
|
2468
|
+
submissionId,
|
|
2469
|
+
schemaVersion: "1.0",
|
|
2470
|
+
payload: input.payload
|
|
2471
|
+
});
|
|
2472
|
+
const finalScore = input.payload.profileAnalysis.overallScore;
|
|
2473
|
+
return {
|
|
2474
|
+
content: [
|
|
2475
|
+
{
|
|
2476
|
+
type: "text",
|
|
2477
|
+
text: `Perfil reescrito do job ${job.id} submetido com sucesso! Result ID: ${resultId}. Novo Score Geral: ${finalScore}/100. A interface web desbloqueou a tela final de resultados.`
|
|
2478
|
+
}
|
|
2479
|
+
],
|
|
2480
|
+
structuredData: {
|
|
2481
|
+
success: true,
|
|
2482
|
+
jobId: job.id,
|
|
2483
|
+
resultId,
|
|
2484
|
+
finalScore
|
|
2485
|
+
}
|
|
2486
|
+
};
|
|
2487
|
+
} catch (err) {
|
|
2488
|
+
return {
|
|
2489
|
+
isError: true,
|
|
2490
|
+
content: [
|
|
2491
|
+
{
|
|
2492
|
+
type: "text",
|
|
2493
|
+
text: `Falha ao submeter perfil reescrito para o job ${input.jobId}: ${err.message}`
|
|
2494
|
+
}
|
|
2495
|
+
]
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
// src/server.ts
|
|
2501
|
+
function createLinkeGringoMcpServer() {
|
|
2502
|
+
const server = new McpServer({
|
|
2503
|
+
name: "linkegringo-mcp",
|
|
2504
|
+
version: "1.0.0"
|
|
2505
|
+
});
|
|
2506
|
+
server.registerTool(
|
|
2507
|
+
"list_jobs",
|
|
2508
|
+
{
|
|
2509
|
+
description: "Lista jobs cadastrados no LinkeGringo Bridge local (porta 5174). Permite filtrar por status (ex: waiting_for_agent, running, completed) ou tipo (diagnostic, interview, rewrite).",
|
|
2510
|
+
inputSchema: listJobsInputSchema.shape
|
|
2511
|
+
},
|
|
2512
|
+
async (args) => {
|
|
2513
|
+
return await handleListJobs(args);
|
|
2514
|
+
}
|
|
2515
|
+
);
|
|
2516
|
+
server.registerTool(
|
|
2517
|
+
"claim_job",
|
|
2518
|
+
{
|
|
2519
|
+
description: "Assume a execu\xE7\xE3o exclusiva (lease at\xF4mico) de um job pendente por 60 segundos. Garante que apenas um agente processe a tarefa por vez.",
|
|
2520
|
+
inputSchema: claimJobInputSchema.shape
|
|
2521
|
+
},
|
|
2522
|
+
async (args) => {
|
|
2523
|
+
return await handleClaimJob(args);
|
|
2524
|
+
}
|
|
2525
|
+
);
|
|
2526
|
+
server.registerTool(
|
|
2527
|
+
"report_progress",
|
|
2528
|
+
{
|
|
2529
|
+
description: "Atualiza a porcentagem (0-100), a fase atual e uma mensagem para o usu\xE1rio no navegador, renovando simultaneamente o lease exclusivo por mais 60s.",
|
|
2530
|
+
inputSchema: reportProgressInputSchema.shape
|
|
2531
|
+
},
|
|
2532
|
+
async (args) => {
|
|
2533
|
+
return await handleReportProgress(args);
|
|
2534
|
+
}
|
|
2535
|
+
);
|
|
2536
|
+
server.registerTool(
|
|
2537
|
+
"inspect_document",
|
|
2538
|
+
{
|
|
2539
|
+
description: "Consulta os metadados do documento PDF cadastrado via capability token (fileId). N\xE3o exp\xF5e paths locais arbitr\xE1rios do sistema de arquivos.",
|
|
2540
|
+
inputSchema: inspectDocumentInputSchema.shape
|
|
2541
|
+
},
|
|
2542
|
+
async (args) => {
|
|
2543
|
+
return await handleInspectDocument(args);
|
|
2544
|
+
}
|
|
2545
|
+
);
|
|
2546
|
+
server.registerTool(
|
|
2547
|
+
"request_user_action",
|
|
2548
|
+
{
|
|
2549
|
+
description: "Pausa o processamento do job e solicita confirma\xE7\xE3o ou dados do usu\xE1rio (Human-in-the-Loop) diretamente na interface web do LinkeGringo.",
|
|
2550
|
+
inputSchema: requestUserActionInputSchema.shape
|
|
2551
|
+
},
|
|
2552
|
+
async (args) => {
|
|
2553
|
+
return await handleRequestUserAction(args);
|
|
2554
|
+
}
|
|
2555
|
+
);
|
|
2556
|
+
server.registerTool(
|
|
2557
|
+
"submit_diagnostic",
|
|
2558
|
+
{
|
|
2559
|
+
description: "Submete o resultado final de um job de diagn\xF3stico estruturado com Profile e ProfileReview validados com Zod estrito.",
|
|
2560
|
+
inputSchema: submitDiagnosticInputSchema.shape
|
|
2561
|
+
},
|
|
2562
|
+
async (args) => {
|
|
2563
|
+
return await handleSubmitDiagnostic(args);
|
|
2564
|
+
}
|
|
2565
|
+
);
|
|
2566
|
+
server.registerTool(
|
|
2567
|
+
"submit_interview",
|
|
2568
|
+
{
|
|
2569
|
+
description: "Submete o resultado final de um job de entrevista com o InterviewPlan estruturado contendo perguntas estrat\xE9gicas.",
|
|
2570
|
+
inputSchema: submitInterviewInputSchema.shape
|
|
2571
|
+
},
|
|
2572
|
+
async (args) => {
|
|
2573
|
+
return await handleSubmitInterview(args);
|
|
2574
|
+
}
|
|
2575
|
+
);
|
|
2576
|
+
server.registerTool(
|
|
2577
|
+
"submit_rewrite",
|
|
2578
|
+
{
|
|
2579
|
+
description: "Submete o resultado final de um job de reescrita contendo ProfileAnalysis com Headlines calibradas, About e bullets Google XYZ.",
|
|
2580
|
+
inputSchema: submitRewriteInputSchema.shape
|
|
2581
|
+
},
|
|
2582
|
+
async (args) => {
|
|
2583
|
+
return await handleSubmitRewrite(args);
|
|
2584
|
+
}
|
|
2585
|
+
);
|
|
2586
|
+
server.registerTool(
|
|
2587
|
+
"audit_profile",
|
|
2588
|
+
{
|
|
2589
|
+
description: "Audita um perfil de LinkedIn (via caminho de PDF, base64 ou texto) contra os crit\xE9rios de contrata\xE7\xE3o de empresas tech dos EUA. Retorna nota Inbound (0-100), gargalos de triagem de recrutadores e lacunas de stack.",
|
|
2590
|
+
inputSchema: auditProfileInputSchema.shape
|
|
2591
|
+
},
|
|
2592
|
+
async (args) => {
|
|
2593
|
+
return await handleAuditProfile(args);
|
|
2594
|
+
}
|
|
2595
|
+
);
|
|
2596
|
+
server.registerTool(
|
|
2597
|
+
"simulate_recruiter_search",
|
|
2598
|
+
{
|
|
2599
|
+
description: "Simula buscas booleanas e algoritmos do LinkedIn Recruiter ATS. Avalia a presen\xE7a de palavras-chave com peso 3x em Headline/Skills e peso 1x em experi\xEAncias, calculando a probabilidade de indexa\xE7\xE3o.",
|
|
2600
|
+
inputSchema: simulateRecruiterSearchInputSchema.shape
|
|
2601
|
+
},
|
|
2602
|
+
async (args) => {
|
|
2603
|
+
return await handleSimulateRecruiterSearch(args);
|
|
2604
|
+
}
|
|
2605
|
+
);
|
|
2606
|
+
server.registerTool(
|
|
2607
|
+
"convert_to_xyz_bullet",
|
|
2608
|
+
{
|
|
2609
|
+
description: "Transforma descri\xE7\xF5es gen\xE9ricas de atividades em bullets de alto impacto seguindo a f\xF3rmula oficial do Google: Accomplished [X], measured by [Y], by doing [Z].",
|
|
2610
|
+
inputSchema: convertToXyzBulletInputSchema.shape
|
|
2611
|
+
},
|
|
2612
|
+
async (args) => {
|
|
2613
|
+
return await handleConvertToXyzBullet(args);
|
|
2614
|
+
}
|
|
2615
|
+
);
|
|
2616
|
+
server.registerTool(
|
|
2617
|
+
"generate_headline_proposals",
|
|
2618
|
+
{
|
|
2619
|
+
description: "Gera propostas de Headline (t\xEDtulo) no LinkedIn com at\xE9 160 caracteres, calibradas para visualiza\xE7\xE3o sem cortes no Desktop e Mobile e alta indexa\xE7\xE3o de busca por recrutadores gringos.",
|
|
2620
|
+
inputSchema: generateHeadlineInputSchema.shape
|
|
2621
|
+
},
|
|
2622
|
+
async (args) => {
|
|
2623
|
+
return await handleGenerateHeadline(args);
|
|
2624
|
+
}
|
|
2625
|
+
);
|
|
2626
|
+
server.registerTool(
|
|
2627
|
+
"get_pending_job",
|
|
2628
|
+
{
|
|
2629
|
+
description: "Obt\xE9m os dados completos de um job pelo ID (ou o pr\xF3ximo da fila). Suporta retrocompatibilidade.",
|
|
2630
|
+
inputSchema: getPendingJobInputSchema.shape
|
|
2631
|
+
},
|
|
2632
|
+
async (args) => {
|
|
2633
|
+
return await handleGetPendingJob(args);
|
|
2634
|
+
}
|
|
2635
|
+
);
|
|
2636
|
+
server.registerTool(
|
|
2637
|
+
"submit_job_result",
|
|
2638
|
+
{
|
|
2639
|
+
description: "Envia o resultado gen\xE9rico de um job de volta para o navegador do LinkeGringo (retrocompatibilidade).",
|
|
2640
|
+
inputSchema: submitJobResultInputSchema.shape
|
|
2641
|
+
},
|
|
2642
|
+
async (args) => {
|
|
2643
|
+
return await handleSubmitJobResult(args);
|
|
2644
|
+
}
|
|
2645
|
+
);
|
|
2646
|
+
server.registerTool(
|
|
2647
|
+
"watch_linkegringo",
|
|
2648
|
+
{
|
|
2649
|
+
description: "Inicia a escuta ativa (watch) por jobs enviados pela interface web do LinkeGringo (http://127.0.0.1:5174). Bloqueia aguardando e retorna o job assim que ele for submetido no navegador.",
|
|
2650
|
+
inputSchema: watchLinkeGringoInputSchema.shape
|
|
2651
|
+
},
|
|
2652
|
+
async (args) => {
|
|
2653
|
+
return await handleWatchLinkeGringo(args);
|
|
2654
|
+
}
|
|
2655
|
+
);
|
|
2656
|
+
server.registerResource(
|
|
2657
|
+
"guidelines",
|
|
2658
|
+
"linkegringo://guidelines",
|
|
2659
|
+
{
|
|
2660
|
+
title: "Diretrizes Oficiais do LinkeGringo para Vagas nos EUA",
|
|
2661
|
+
description: "Princ\xEDpios fundamentais: f\xF3rmula Google XYZ, headlines de at\xE9 160 caracteres, elimina\xE7\xE3o de red flags culturais brasileiras e maximiza\xE7\xE3o de Inbound Readiness.",
|
|
2662
|
+
mimeType: "text/markdown"
|
|
2663
|
+
},
|
|
2664
|
+
async () => {
|
|
2665
|
+
return {
|
|
2666
|
+
contents: [
|
|
2667
|
+
{
|
|
2668
|
+
uri: "linkegringo://guidelines",
|
|
2669
|
+
text: `
|
|
2670
|
+
# Diretrizes Oficiais LinkeGringo: Otimiza\xE7\xE3o de Perfil para Recrutadores dos EUA
|
|
2671
|
+
|
|
2672
|
+
1. **Headline $le$ 160 caracteres**:
|
|
2673
|
+
- Formato recomendado: \`[Cargo Espec\xEDfico] | [3-4 Tecnologias Core] | [Escala/Dom\xEDnio] | US Remote\`
|
|
2674
|
+
- Evite slogans vagos ("Apaixonado por tecnologia", "Resolvendo problemas complexos").
|
|
2675
|
+
- Headline tem peso 3x no algoritmo de busca do LinkedIn Recruiter.
|
|
2676
|
+
|
|
2677
|
+
2. **F\xF3rmula Google XYZ para Experi\xEAncias**:
|
|
2678
|
+
- Toda conquista deve responder: *"Accomplished [X], measured by [Y], by doing [Z]"*.
|
|
2679
|
+
- Exemplo: *"Architected distributed event-driven payment service in Go, reducing p99 latency by 42% and scaling to 15,000 requests/sec."*
|
|
2680
|
+
|
|
2681
|
+
3. **Incentivo a Inbound (Ser Descoberto)**:
|
|
2682
|
+
- Recrutadores usam filtros booleanos estritos. Se "Senior Software Engineer" e "Go" n\xE3o estiverem no t\xEDtulo da experi\xEAncia atual ou na headline, voc\xEA n\xE3o entra no funil inicial.
|
|
2683
|
+
- Elimine red flags de localiza\xE7\xE3o restrita e declare disponibilidade para contratos internacionais (W-8BEN / PJ Internacional / B2B).
|
|
2684
|
+
`.trim()
|
|
2685
|
+
}
|
|
2686
|
+
]
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
);
|
|
2690
|
+
return server;
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
// src/cli/installer.ts
|
|
2694
|
+
import fs from "fs";
|
|
2695
|
+
import path from "path";
|
|
2696
|
+
import os from "os";
|
|
2697
|
+
function getMcpConfigsForSystem() {
|
|
2698
|
+
const home = os.homedir();
|
|
2699
|
+
const platform = os.platform();
|
|
2700
|
+
const configs = [];
|
|
2701
|
+
const antigravityPath = path.join(home, ".gemini", "config", "mcp_config.json");
|
|
2702
|
+
configs.push({
|
|
1801
2703
|
id: "antigravity",
|
|
1802
2704
|
client: "Google Antigravity",
|
|
1803
2705
|
configPath: antigravityPath,
|
|
@@ -1916,63 +2818,877 @@ function runInstaller(args = process.argv) {
|
|
|
1916
2818
|
console.warn(`\u26A0\uFE0F [Workspace Local] Erro ao configurar: ${err.message}
|
|
1917
2819
|
`);
|
|
1918
2820
|
}
|
|
1919
|
-
return results;
|
|
2821
|
+
return results;
|
|
2822
|
+
}
|
|
2823
|
+
const detectedTargets = targets.filter((t) => t.detected);
|
|
2824
|
+
const shouldInstallAll = options.all || detectedTargets.length === 0;
|
|
2825
|
+
for (const target of targets) {
|
|
2826
|
+
if (options.client && !target.id.includes(options.client)) {
|
|
2827
|
+
continue;
|
|
2828
|
+
}
|
|
2829
|
+
if (!shouldInstallAll && !target.detected) {
|
|
2830
|
+
results.push({
|
|
2831
|
+
client: target.client,
|
|
2832
|
+
configPath: target.configPath,
|
|
2833
|
+
status: "skipped",
|
|
2834
|
+
message: "Cliente n\xE3o detectado nesta m\xE1quina (use --all para for\xE7ar)"
|
|
2835
|
+
});
|
|
2836
|
+
console.log(`\u23ED\uFE0F [${target.client}] N\xE3o detectado nesta m\xE1quina (pulado. Use --all para criar)`);
|
|
2837
|
+
continue;
|
|
2838
|
+
}
|
|
2839
|
+
try {
|
|
2840
|
+
const res = installMcpServerConfig(target.configPath);
|
|
2841
|
+
results.push({
|
|
2842
|
+
client: target.client,
|
|
2843
|
+
configPath: target.configPath,
|
|
2844
|
+
status: res.status
|
|
2845
|
+
});
|
|
2846
|
+
const tag = target.detected ? "(Detectado)" : "(Padr\xE3o)";
|
|
2847
|
+
console.log(`\u2705 [${target.client}] ${tag}`);
|
|
2848
|
+
console.log(` Arquivo: ${target.configPath} (${res.status === "created" ? "Criado" : "Atualizado"})
|
|
2849
|
+
`);
|
|
2850
|
+
} catch (err) {
|
|
2851
|
+
results.push({
|
|
2852
|
+
client: target.client,
|
|
2853
|
+
configPath: target.configPath,
|
|
2854
|
+
status: "error",
|
|
2855
|
+
message: err.message
|
|
2856
|
+
});
|
|
2857
|
+
console.warn(`\u26A0\uFE0F [${target.client}] N\xE3o foi poss\xEDvel atualizar: ${err.message}
|
|
2858
|
+
`);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
console.log("---");
|
|
2862
|
+
console.log("\u{1F4A1} Comandos One-Line diretos para agentes de linha de comando (CLI):");
|
|
2863
|
+
console.log(" \u2022 Antigravity CLI: agy mcp add linkegringo npx -y @linkegringo/mcp");
|
|
2864
|
+
console.log(" \u2022 Codex CLI: codex mcp add linkegringo -- npx -y @linkegringo/mcp");
|
|
2865
|
+
console.log(" \u2022 Claude Code CLI: claude mcp add linkegringo npx -y @linkegringo/mcp");
|
|
2866
|
+
console.log(' \u2022 Goose CLI: goose configure --add-extension "npx -y @linkegringo/mcp"');
|
|
2867
|
+
console.log("================================================");
|
|
2868
|
+
console.log("\u{1F389} Instala\xE7\xE3o conclu\xEDda! Reinicie o seu cliente de IA para ativar.\n");
|
|
2869
|
+
return results;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
// src/bridge/server.ts
|
|
2873
|
+
import http2 from "http";
|
|
2874
|
+
|
|
2875
|
+
// src/bridge/db.ts
|
|
2876
|
+
import { createRequire } from "module";
|
|
2877
|
+
import path2 from "path";
|
|
2878
|
+
import fs2 from "fs";
|
|
2879
|
+
import os2 from "os";
|
|
2880
|
+
var require2 = createRequire(import.meta.url);
|
|
2881
|
+
var { DatabaseSync } = require2("node:sqlite");
|
|
2882
|
+
function getDatabasePath(customPath) {
|
|
2883
|
+
if (customPath) return customPath;
|
|
2884
|
+
const dir = path2.join(os2.homedir(), ".linkegringo");
|
|
2885
|
+
if (!fs2.existsSync(dir)) {
|
|
2886
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
2887
|
+
}
|
|
2888
|
+
return path2.join(dir, "linkegringo.db");
|
|
2889
|
+
}
|
|
2890
|
+
function initDatabase(db) {
|
|
2891
|
+
try {
|
|
2892
|
+
db.exec("PRAGMA foreign_keys = ON;");
|
|
2893
|
+
db.exec("PRAGMA synchronous = NORMAL;");
|
|
2894
|
+
} catch {
|
|
2895
|
+
}
|
|
2896
|
+
db.exec(`
|
|
2897
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
2898
|
+
id TEXT PRIMARY KEY,
|
|
2899
|
+
type TEXT NOT NULL,
|
|
2900
|
+
status TEXT NOT NULL,
|
|
2901
|
+
attempt INTEGER NOT NULL DEFAULT 0,
|
|
2902
|
+
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
2903
|
+
claimed_by TEXT,
|
|
2904
|
+
lease_expires_at TEXT,
|
|
2905
|
+
expires_at TEXT NOT NULL,
|
|
2906
|
+
progress INTEGER NOT NULL DEFAULT 0,
|
|
2907
|
+
current_phase TEXT,
|
|
2908
|
+
progress_message TEXT,
|
|
2909
|
+
result_id TEXT,
|
|
2910
|
+
file_handle_id TEXT,
|
|
2911
|
+
target_role TEXT,
|
|
2912
|
+
created_at TEXT NOT NULL,
|
|
2913
|
+
updated_at TEXT NOT NULL,
|
|
2914
|
+
metadata TEXT
|
|
2915
|
+
);
|
|
2916
|
+
|
|
2917
|
+
CREATE TABLE IF NOT EXISTS job_events (
|
|
2918
|
+
id TEXT PRIMARY KEY,
|
|
2919
|
+
job_id TEXT NOT NULL,
|
|
2920
|
+
command_id TEXT,
|
|
2921
|
+
type TEXT NOT NULL,
|
|
2922
|
+
payload TEXT,
|
|
2923
|
+
created_at TEXT NOT NULL,
|
|
2924
|
+
FOREIGN KEY(job_id) REFERENCES jobs(id) ON DELETE CASCADE
|
|
2925
|
+
);
|
|
2926
|
+
|
|
2927
|
+
CREATE TABLE IF NOT EXISTS commands (
|
|
2928
|
+
command_id TEXT PRIMARY KEY,
|
|
2929
|
+
job_id TEXT NOT NULL,
|
|
2930
|
+
type TEXT NOT NULL,
|
|
2931
|
+
result TEXT,
|
|
2932
|
+
created_at TEXT NOT NULL
|
|
2933
|
+
);
|
|
2934
|
+
|
|
2935
|
+
CREATE TABLE IF NOT EXISTS job_results (
|
|
2936
|
+
id TEXT PRIMARY KEY,
|
|
2937
|
+
job_id TEXT NOT NULL,
|
|
2938
|
+
schema_version TEXT NOT NULL,
|
|
2939
|
+
submission_id TEXT NOT NULL UNIQUE,
|
|
2940
|
+
submitted_by TEXT NOT NULL,
|
|
2941
|
+
payload TEXT NOT NULL,
|
|
2942
|
+
created_at TEXT NOT NULL,
|
|
2943
|
+
FOREIGN KEY(job_id) REFERENCES jobs(id) ON DELETE CASCADE
|
|
2944
|
+
);
|
|
2945
|
+
|
|
2946
|
+
CREATE TABLE IF NOT EXISTS file_handles (
|
|
2947
|
+
id TEXT PRIMARY KEY,
|
|
2948
|
+
job_id TEXT NOT NULL,
|
|
2949
|
+
name TEXT NOT NULL,
|
|
2950
|
+
media_type TEXT NOT NULL,
|
|
2951
|
+
size INTEGER NOT NULL,
|
|
2952
|
+
mtime INTEGER NOT NULL,
|
|
2953
|
+
root TEXT NOT NULL,
|
|
2954
|
+
path TEXT NOT NULL,
|
|
2955
|
+
expires_at TEXT NOT NULL,
|
|
2956
|
+
created_at TEXT NOT NULL
|
|
2957
|
+
);
|
|
2958
|
+
|
|
2959
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
2960
|
+
agent_id TEXT PRIMARY KEY,
|
|
2961
|
+
session_id TEXT NOT NULL,
|
|
2962
|
+
client_name TEXT,
|
|
2963
|
+
client_version TEXT,
|
|
2964
|
+
capabilities TEXT,
|
|
2965
|
+
last_seen_at INTEGER NOT NULL,
|
|
2966
|
+
created_at INTEGER NOT NULL
|
|
2967
|
+
);
|
|
2968
|
+
|
|
2969
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
|
|
2970
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_status_lease ON jobs(status, lease_expires_at);
|
|
2971
|
+
CREATE INDEX IF NOT EXISTS idx_job_events_job_id ON job_events(job_id);
|
|
2972
|
+
CREATE INDEX IF NOT EXISTS idx_commands_job_id ON commands(job_id);
|
|
2973
|
+
CREATE INDEX IF NOT EXISTS idx_file_handles_job_id ON file_handles(job_id);
|
|
2974
|
+
CREATE INDEX IF NOT EXISTS idx_agents_last_seen ON agents(last_seen_at);
|
|
2975
|
+
`);
|
|
2976
|
+
}
|
|
2977
|
+
function createDatabase(dbPath) {
|
|
2978
|
+
const targetPath = dbPath || getDatabasePath();
|
|
2979
|
+
const db = new DatabaseSync(targetPath);
|
|
2980
|
+
initDatabase(db);
|
|
2981
|
+
return db;
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
// src/bridge/job-store.ts
|
|
2985
|
+
function parseJobRow(row) {
|
|
2986
|
+
return {
|
|
2987
|
+
id: row.id,
|
|
2988
|
+
type: row.type,
|
|
2989
|
+
status: row.status,
|
|
2990
|
+
attempt: row.attempt,
|
|
2991
|
+
maxAttempts: row.max_attempts,
|
|
2992
|
+
claimedBy: row.claimed_by ? JSON.parse(row.claimed_by) : null,
|
|
2993
|
+
leaseExpiresAt: row.lease_expires_at,
|
|
2994
|
+
expiresAt: row.expires_at,
|
|
2995
|
+
progress: row.progress,
|
|
2996
|
+
currentPhase: row.current_phase || null,
|
|
2997
|
+
progressMessage: row.progress_message,
|
|
2998
|
+
resultId: row.result_id,
|
|
2999
|
+
fileHandleId: row.file_handle_id,
|
|
3000
|
+
targetRole: row.target_role,
|
|
3001
|
+
createdAt: row.created_at,
|
|
3002
|
+
updatedAt: row.updated_at,
|
|
3003
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
3006
|
+
function parseEventRow(row) {
|
|
3007
|
+
return {
|
|
3008
|
+
id: row.id,
|
|
3009
|
+
jobId: row.job_id,
|
|
3010
|
+
commandId: row.command_id,
|
|
3011
|
+
type: row.type,
|
|
3012
|
+
timestamp: row.created_at,
|
|
3013
|
+
payload: row.payload ? JSON.parse(row.payload) : void 0
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
var JobStore = class {
|
|
3017
|
+
constructor(db, onEventRecorded) {
|
|
3018
|
+
this.db = db;
|
|
3019
|
+
this.onEventRecorded = onEventRecorded;
|
|
3020
|
+
}
|
|
3021
|
+
db;
|
|
3022
|
+
onEventRecorded;
|
|
3023
|
+
getRawDb() {
|
|
3024
|
+
return this.db;
|
|
3025
|
+
}
|
|
3026
|
+
// --- Transações e Idempotência ---
|
|
3027
|
+
getCachedCommand(commandId) {
|
|
3028
|
+
if (!commandId) return null;
|
|
3029
|
+
const stmt = this.db.prepare("SELECT result FROM commands WHERE command_id = ?");
|
|
3030
|
+
const row = stmt.get(commandId);
|
|
3031
|
+
if (row && row.result) {
|
|
3032
|
+
return JSON.parse(row.result);
|
|
3033
|
+
}
|
|
3034
|
+
return null;
|
|
3035
|
+
}
|
|
3036
|
+
saveCommand(commandId, jobId, type, result) {
|
|
3037
|
+
if (!commandId) return;
|
|
3038
|
+
const stmt = this.db.prepare(
|
|
3039
|
+
"INSERT OR REPLACE INTO commands (command_id, job_id, type, result, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
3040
|
+
);
|
|
3041
|
+
stmt.run(commandId, jobId, type, JSON.stringify(result), (/* @__PURE__ */ new Date()).toISOString());
|
|
3042
|
+
}
|
|
3043
|
+
// --- Operações de Job ---
|
|
3044
|
+
createJob(input) {
|
|
3045
|
+
if (input.commandId) {
|
|
3046
|
+
const cached = this.getCachedCommand(input.commandId);
|
|
3047
|
+
if (cached) return cached;
|
|
3048
|
+
}
|
|
3049
|
+
const now = /* @__PURE__ */ new Date();
|
|
3050
|
+
const nowIso = now.toISOString();
|
|
3051
|
+
const jobId = input.id || `job_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
3052
|
+
const expiresInMs = input.expiresInMs || 24 * 60 * 60 * 1e3;
|
|
3053
|
+
const expiresAt = new Date(now.getTime() + expiresInMs).toISOString();
|
|
3054
|
+
let job = {
|
|
3055
|
+
id: jobId,
|
|
3056
|
+
type: input.type,
|
|
3057
|
+
status: "created",
|
|
3058
|
+
attempt: 0,
|
|
3059
|
+
maxAttempts: 3,
|
|
3060
|
+
claimedBy: null,
|
|
3061
|
+
leaseExpiresAt: null,
|
|
3062
|
+
expiresAt,
|
|
3063
|
+
progress: 0,
|
|
3064
|
+
currentPhase: null,
|
|
3065
|
+
progressMessage: null,
|
|
3066
|
+
resultId: null,
|
|
3067
|
+
fileHandleId: input.fileHandleId || null,
|
|
3068
|
+
targetRole: input.targetRole || null,
|
|
3069
|
+
createdAt: nowIso,
|
|
3070
|
+
updatedAt: nowIso,
|
|
3071
|
+
metadata: input.metadata
|
|
3072
|
+
};
|
|
3073
|
+
const insertJobStmt = this.db.prepare(`
|
|
3074
|
+
INSERT INTO jobs (
|
|
3075
|
+
id, type, status, attempt, max_attempts, claimed_by, lease_expires_at,
|
|
3076
|
+
expires_at, progress, current_phase, progress_message, result_id,
|
|
3077
|
+
file_handle_id, target_role, created_at, updated_at, metadata
|
|
3078
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3079
|
+
`);
|
|
3080
|
+
insertJobStmt.run(
|
|
3081
|
+
job.id,
|
|
3082
|
+
job.type,
|
|
3083
|
+
job.status,
|
|
3084
|
+
job.attempt,
|
|
3085
|
+
job.maxAttempts,
|
|
3086
|
+
job.claimedBy ? JSON.stringify(job.claimedBy) : null,
|
|
3087
|
+
job.leaseExpiresAt ?? null,
|
|
3088
|
+
job.expiresAt,
|
|
3089
|
+
job.progress,
|
|
3090
|
+
job.currentPhase ?? null,
|
|
3091
|
+
job.progressMessage ?? null,
|
|
3092
|
+
job.resultId ?? null,
|
|
3093
|
+
job.fileHandleId ?? null,
|
|
3094
|
+
job.targetRole ?? null,
|
|
3095
|
+
job.createdAt,
|
|
3096
|
+
job.updatedAt,
|
|
3097
|
+
job.metadata ? JSON.stringify(job.metadata) : null
|
|
3098
|
+
);
|
|
3099
|
+
this.recordEvent({
|
|
3100
|
+
id: `evt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
3101
|
+
jobId: job.id,
|
|
3102
|
+
commandId: input.commandId,
|
|
3103
|
+
type: "job.created",
|
|
3104
|
+
timestamp: nowIso,
|
|
3105
|
+
payload: { type: job.type, targetRole: job.targetRole }
|
|
3106
|
+
});
|
|
3107
|
+
if (input.autoReady !== false) {
|
|
3108
|
+
const transition = transitionJob(job, { type: "ready_for_agent", commandId: input.commandId }, nowIso);
|
|
3109
|
+
job = transition.nextJob;
|
|
3110
|
+
this.updateJobRow(job);
|
|
3111
|
+
this.recordEvent(transition.event);
|
|
3112
|
+
}
|
|
3113
|
+
this.saveCommand(input.commandId, job.id, "create_job", job);
|
|
3114
|
+
return job;
|
|
3115
|
+
}
|
|
3116
|
+
getJob(id) {
|
|
3117
|
+
const stmt = this.db.prepare("SELECT * FROM jobs WHERE id = ?");
|
|
3118
|
+
const row = stmt.get(id);
|
|
3119
|
+
return row ? parseJobRow(row) : null;
|
|
3120
|
+
}
|
|
3121
|
+
listJobs(filter) {
|
|
3122
|
+
let query = "SELECT * FROM jobs WHERE 1=1";
|
|
3123
|
+
const params = [];
|
|
3124
|
+
if (filter?.status) {
|
|
3125
|
+
query += " AND status = ?";
|
|
3126
|
+
params.push(filter.status);
|
|
3127
|
+
}
|
|
3128
|
+
if (filter?.type) {
|
|
3129
|
+
query += " AND type = ?";
|
|
3130
|
+
params.push(filter.type);
|
|
3131
|
+
}
|
|
3132
|
+
query += " ORDER BY created_at DESC";
|
|
3133
|
+
const stmt = this.db.prepare(query);
|
|
3134
|
+
const rows = stmt.all(...params);
|
|
3135
|
+
return rows.map(parseJobRow);
|
|
3136
|
+
}
|
|
3137
|
+
claimJob(jobId, agent, options) {
|
|
3138
|
+
if (options?.commandId) {
|
|
3139
|
+
const cached = this.getCachedCommand(options.commandId);
|
|
3140
|
+
if (cached) return cached;
|
|
3141
|
+
}
|
|
3142
|
+
const job = this.getJob(jobId);
|
|
3143
|
+
if (!job) {
|
|
3144
|
+
throw new Error(`Job [${jobId}] n\xE3o encontrado para claim.`);
|
|
3145
|
+
}
|
|
3146
|
+
const transition = transitionJob(
|
|
3147
|
+
job,
|
|
3148
|
+
{
|
|
3149
|
+
type: "claim",
|
|
3150
|
+
agent,
|
|
3151
|
+
leaseDurationMs: options?.leaseDurationMs,
|
|
3152
|
+
commandId: options?.commandId
|
|
3153
|
+
}
|
|
3154
|
+
);
|
|
3155
|
+
this.updateJobRow(transition.nextJob);
|
|
3156
|
+
this.recordEvent(transition.event);
|
|
3157
|
+
this.registerAgent(agent);
|
|
3158
|
+
this.saveCommand(options?.commandId, jobId, "claim_job", transition.nextJob);
|
|
3159
|
+
return transition.nextJob;
|
|
3160
|
+
}
|
|
3161
|
+
reportProgress(jobId, agent, params) {
|
|
3162
|
+
if (params.commandId) {
|
|
3163
|
+
const cached = this.getCachedCommand(params.commandId);
|
|
3164
|
+
if (cached) return cached;
|
|
3165
|
+
}
|
|
3166
|
+
const job = this.getJob(jobId);
|
|
3167
|
+
if (!job) {
|
|
3168
|
+
throw new Error(`Job [${jobId}] n\xE3o encontrado para reportProgress.`);
|
|
3169
|
+
}
|
|
3170
|
+
const previousProgress = job.progress;
|
|
3171
|
+
const previousPhase = job.currentPhase;
|
|
3172
|
+
const previousMessage = job.progressMessage;
|
|
3173
|
+
const transition = transitionJob(
|
|
3174
|
+
job,
|
|
3175
|
+
{
|
|
3176
|
+
type: "report_progress",
|
|
3177
|
+
agent,
|
|
3178
|
+
phase: params.phase,
|
|
3179
|
+
progress: params.progress,
|
|
3180
|
+
message: params.message,
|
|
3181
|
+
leaseDurationMs: params.leaseDurationMs,
|
|
3182
|
+
commandId: params.commandId
|
|
3183
|
+
}
|
|
3184
|
+
);
|
|
3185
|
+
this.updateJobRow(transition.nextJob);
|
|
3186
|
+
const delta = Math.abs(transition.nextJob.progress - previousProgress);
|
|
3187
|
+
const phaseChanged = transition.nextJob.currentPhase !== previousPhase;
|
|
3188
|
+
const msgChanged = Boolean(params.message && params.message !== previousMessage);
|
|
3189
|
+
const shouldEmitEvent = delta >= 5 || phaseChanged || msgChanged;
|
|
3190
|
+
if (shouldEmitEvent) {
|
|
3191
|
+
this.recordEvent(transition.event);
|
|
3192
|
+
}
|
|
3193
|
+
this.registerAgent(agent);
|
|
3194
|
+
this.saveCommand(params.commandId, jobId, "report_progress", transition.nextJob);
|
|
3195
|
+
return transition.nextJob;
|
|
3196
|
+
}
|
|
3197
|
+
requestUserAction(jobId, agent, params) {
|
|
3198
|
+
if (params.commandId) {
|
|
3199
|
+
const cached = this.getCachedCommand(params.commandId);
|
|
3200
|
+
if (cached) return cached;
|
|
3201
|
+
}
|
|
3202
|
+
const job = this.getJob(jobId);
|
|
3203
|
+
if (!job) {
|
|
3204
|
+
throw new Error(`Job [${jobId}] n\xE3o encontrado para requestUserAction.`);
|
|
3205
|
+
}
|
|
3206
|
+
const transition = transitionJob(
|
|
3207
|
+
job,
|
|
3208
|
+
{
|
|
3209
|
+
type: "request_user_action",
|
|
3210
|
+
agent,
|
|
3211
|
+
reason: params.reason,
|
|
3212
|
+
prompt: params.prompt,
|
|
3213
|
+
commandId: params.commandId
|
|
3214
|
+
}
|
|
3215
|
+
);
|
|
3216
|
+
this.updateJobRow(transition.nextJob);
|
|
3217
|
+
this.recordEvent(transition.event);
|
|
3218
|
+
this.saveCommand(params.commandId, jobId, "request_user_action", transition.nextJob);
|
|
3219
|
+
return transition.nextJob;
|
|
3220
|
+
}
|
|
3221
|
+
resumeJob(jobId, params) {
|
|
3222
|
+
if (params.commandId) {
|
|
3223
|
+
const cached = this.getCachedCommand(params.commandId);
|
|
3224
|
+
if (cached) return cached;
|
|
3225
|
+
}
|
|
3226
|
+
const job = this.getJob(jobId);
|
|
3227
|
+
if (!job) {
|
|
3228
|
+
throw new Error(`Job [${jobId}] n\xE3o encontrado para resume.`);
|
|
3229
|
+
}
|
|
3230
|
+
const transition = transitionJob(
|
|
3231
|
+
job,
|
|
3232
|
+
{
|
|
3233
|
+
type: "user_resume",
|
|
3234
|
+
actor: "user",
|
|
3235
|
+
reason: params.reason,
|
|
3236
|
+
payload: params.payload,
|
|
3237
|
+
commandId: params.commandId
|
|
3238
|
+
}
|
|
3239
|
+
);
|
|
3240
|
+
this.updateJobRow(transition.nextJob);
|
|
3241
|
+
this.recordEvent(transition.event);
|
|
3242
|
+
this.saveCommand(params.commandId, jobId, "resume_job", transition.nextJob);
|
|
3243
|
+
return transition.nextJob;
|
|
3244
|
+
}
|
|
3245
|
+
completeJob(jobId, agent, result) {
|
|
3246
|
+
if (result.commandId) {
|
|
3247
|
+
const cached = this.getCachedCommand(result.commandId);
|
|
3248
|
+
if (cached) return cached;
|
|
3249
|
+
}
|
|
3250
|
+
const existingResultStmt = this.db.prepare("SELECT id, job_id FROM job_results WHERE submission_id = ?");
|
|
3251
|
+
const existing = existingResultStmt.get(result.submissionId);
|
|
3252
|
+
if (existing) {
|
|
3253
|
+
const existingJob = this.getJob(existing.job_id);
|
|
3254
|
+
return { job: existingJob, resultId: existing.id };
|
|
3255
|
+
}
|
|
3256
|
+
const job = this.getJob(jobId);
|
|
3257
|
+
if (!job) {
|
|
3258
|
+
throw new Error(`Job [${jobId}] n\xE3o encontrado para completeJob.`);
|
|
3259
|
+
}
|
|
3260
|
+
const resultId = `res_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
3261
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
3262
|
+
const transition = transitionJob(
|
|
3263
|
+
job,
|
|
3264
|
+
{
|
|
3265
|
+
type: "complete",
|
|
3266
|
+
agent,
|
|
3267
|
+
resultId,
|
|
3268
|
+
commandId: result.commandId
|
|
3269
|
+
},
|
|
3270
|
+
nowIso
|
|
3271
|
+
);
|
|
3272
|
+
const insertResultStmt = this.db.prepare(`
|
|
3273
|
+
INSERT INTO job_results (
|
|
3274
|
+
id, job_id, schema_version, submission_id, submitted_by, payload, created_at
|
|
3275
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
3276
|
+
`);
|
|
3277
|
+
insertResultStmt.run(
|
|
3278
|
+
resultId,
|
|
3279
|
+
job.id,
|
|
3280
|
+
result.schemaVersion || "1.0",
|
|
3281
|
+
result.submissionId,
|
|
3282
|
+
JSON.stringify(agent),
|
|
3283
|
+
JSON.stringify(result.payload),
|
|
3284
|
+
nowIso
|
|
3285
|
+
);
|
|
3286
|
+
this.updateJobRow(transition.nextJob);
|
|
3287
|
+
this.recordEvent(transition.event);
|
|
3288
|
+
const response = { job: transition.nextJob, resultId };
|
|
3289
|
+
this.saveCommand(result.commandId, jobId, "complete_job", response);
|
|
3290
|
+
return response;
|
|
3291
|
+
}
|
|
3292
|
+
failJob(jobId, error, actor, commandId) {
|
|
3293
|
+
const job = this.getJob(jobId);
|
|
3294
|
+
if (!job) {
|
|
3295
|
+
throw new Error(`Job [${jobId}] n\xE3o encontrado para failJob.`);
|
|
3296
|
+
}
|
|
3297
|
+
const transition = transitionJob(job, { type: "fail", error, actor, commandId });
|
|
3298
|
+
this.updateJobRow(transition.nextJob);
|
|
3299
|
+
this.recordEvent(transition.event);
|
|
3300
|
+
return transition.nextJob;
|
|
3301
|
+
}
|
|
3302
|
+
cancelJob(jobId, actor, reason, commandId) {
|
|
3303
|
+
const job = this.getJob(jobId);
|
|
3304
|
+
if (!job) {
|
|
3305
|
+
throw new Error(`Job [${jobId}] n\xE3o encontrado para cancelJob.`);
|
|
3306
|
+
}
|
|
3307
|
+
const transition = transitionJob(job, { type: "cancel", actor, reason, commandId });
|
|
3308
|
+
this.updateJobRow(transition.nextJob);
|
|
3309
|
+
this.recordEvent(transition.event);
|
|
3310
|
+
return transition.nextJob;
|
|
1920
3311
|
}
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
3312
|
+
// --- Recuperação de Falhas e Expiração ---
|
|
3313
|
+
expireStaleLeases() {
|
|
3314
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
3315
|
+
const stmt = this.db.prepare(`
|
|
3316
|
+
SELECT * FROM jobs
|
|
3317
|
+
WHERE status = 'running'
|
|
3318
|
+
AND lease_expires_at IS NOT NULL
|
|
3319
|
+
AND lease_expires_at < ?
|
|
3320
|
+
`);
|
|
3321
|
+
const rows = stmt.all(nowIso);
|
|
3322
|
+
let recovered = 0;
|
|
3323
|
+
for (const row of rows) {
|
|
3324
|
+
const job = parseJobRow(row);
|
|
3325
|
+
try {
|
|
3326
|
+
const transition = transitionJob(job, { type: "expire_lease", reason: "Lease expired" }, nowIso);
|
|
3327
|
+
this.updateJobRow(transition.nextJob);
|
|
3328
|
+
this.recordEvent(transition.event);
|
|
3329
|
+
recovered++;
|
|
3330
|
+
} catch (err) {
|
|
3331
|
+
console.error(`Erro ao expirar lease do job ${job.id}:`, err);
|
|
3332
|
+
}
|
|
1926
3333
|
}
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
3334
|
+
return recovered;
|
|
3335
|
+
}
|
|
3336
|
+
cleanupExpiredJobs() {
|
|
3337
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
3338
|
+
const stmt = this.db.prepare(`
|
|
3339
|
+
DELETE FROM jobs
|
|
3340
|
+
WHERE expires_at < ? AND status IN ('completed', 'failed', 'cancelled')
|
|
3341
|
+
`);
|
|
3342
|
+
const result = stmt.run(nowIso);
|
|
3343
|
+
return Number(result.changes);
|
|
3344
|
+
}
|
|
3345
|
+
// --- Event Log & SSE Replay ---
|
|
3346
|
+
recordEvent(event) {
|
|
3347
|
+
const stmt = this.db.prepare(`
|
|
3348
|
+
INSERT INTO job_events (id, job_id, command_id, type, payload, created_at)
|
|
3349
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
3350
|
+
`);
|
|
3351
|
+
stmt.run(
|
|
3352
|
+
event.id,
|
|
3353
|
+
event.jobId,
|
|
3354
|
+
event.commandId || null,
|
|
3355
|
+
event.type,
|
|
3356
|
+
event.payload ? JSON.stringify(event.payload) : null,
|
|
3357
|
+
event.timestamp
|
|
3358
|
+
);
|
|
3359
|
+
this.onEventRecorded?.(event);
|
|
3360
|
+
}
|
|
3361
|
+
getEventsSince(jobId, lastEventId) {
|
|
3362
|
+
if (!lastEventId) {
|
|
3363
|
+
const stmt2 = this.db.prepare(`
|
|
3364
|
+
SELECT * FROM job_events
|
|
3365
|
+
WHERE job_id = ?
|
|
3366
|
+
ORDER BY rowid ASC
|
|
3367
|
+
`);
|
|
3368
|
+
const rows2 = stmt2.all(jobId);
|
|
3369
|
+
return rows2.map(parseEventRow);
|
|
1936
3370
|
}
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
3371
|
+
const refStmt = this.db.prepare("SELECT rowid FROM job_events WHERE id = ?");
|
|
3372
|
+
const ref = refStmt.get(lastEventId);
|
|
3373
|
+
const refRowId = ref?.rowid ?? 0;
|
|
3374
|
+
const stmt = this.db.prepare(`
|
|
3375
|
+
SELECT * FROM job_events
|
|
3376
|
+
WHERE job_id = ? AND rowid > ?
|
|
3377
|
+
ORDER BY rowid ASC
|
|
3378
|
+
`);
|
|
3379
|
+
const rows = stmt.all(jobId, refRowId);
|
|
3380
|
+
return rows.map(parseEventRow);
|
|
3381
|
+
}
|
|
3382
|
+
getJobResult(resultId) {
|
|
3383
|
+
const stmt = this.db.prepare("SELECT * FROM job_results WHERE id = ?");
|
|
3384
|
+
const row = stmt.get(resultId);
|
|
3385
|
+
if (!row) return null;
|
|
3386
|
+
return {
|
|
3387
|
+
id: row.id,
|
|
3388
|
+
jobId: row.job_id,
|
|
3389
|
+
schemaVersion: row.schema_version,
|
|
3390
|
+
submissionId: row.submission_id,
|
|
3391
|
+
submittedBy: JSON.parse(row.submitted_by),
|
|
3392
|
+
createdAt: row.created_at,
|
|
3393
|
+
payload: JSON.parse(row.payload)
|
|
3394
|
+
};
|
|
3395
|
+
}
|
|
3396
|
+
// --- File Handles ---
|
|
3397
|
+
saveFileHandle(handle, realPath) {
|
|
3398
|
+
const stmt = this.db.prepare(`
|
|
3399
|
+
INSERT OR REPLACE INTO file_handles (
|
|
3400
|
+
id, job_id, name, media_type, size, mtime, root, path, expires_at, created_at
|
|
3401
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3402
|
+
`);
|
|
3403
|
+
stmt.run(
|
|
3404
|
+
handle.fileId,
|
|
3405
|
+
handle.jobId,
|
|
3406
|
+
handle.name,
|
|
3407
|
+
handle.mediaType,
|
|
3408
|
+
handle.size,
|
|
3409
|
+
handle.mtime,
|
|
3410
|
+
handle.root,
|
|
3411
|
+
realPath,
|
|
3412
|
+
handle.expiresAt,
|
|
3413
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
3414
|
+
);
|
|
3415
|
+
}
|
|
3416
|
+
getFileHandle(fileId) {
|
|
3417
|
+
const stmt = this.db.prepare("SELECT * FROM file_handles WHERE id = ?");
|
|
3418
|
+
const row = stmt.get(fileId);
|
|
3419
|
+
if (!row) return null;
|
|
3420
|
+
return {
|
|
3421
|
+
fileId: row.id,
|
|
3422
|
+
jobId: row.job_id,
|
|
3423
|
+
name: row.name,
|
|
3424
|
+
mediaType: row.media_type,
|
|
3425
|
+
size: row.size,
|
|
3426
|
+
mtime: row.mtime,
|
|
3427
|
+
root: row.root,
|
|
3428
|
+
access: "native-local-document",
|
|
3429
|
+
expiresAt: row.expires_at,
|
|
3430
|
+
path: row.path,
|
|
3431
|
+
localPath: row.path
|
|
3432
|
+
};
|
|
3433
|
+
}
|
|
3434
|
+
// --- Observabilidade de Agentes (Ponto 6 & 7) ---
|
|
3435
|
+
registerAgent(agent, capabilities) {
|
|
3436
|
+
const now = Date.now();
|
|
3437
|
+
const stmt = this.db.prepare(`
|
|
3438
|
+
INSERT INTO agents (
|
|
3439
|
+
agent_id, session_id, client_name, client_version, capabilities, last_seen_at, created_at
|
|
3440
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
3441
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
3442
|
+
session_id = excluded.session_id,
|
|
3443
|
+
client_name = excluded.client_name,
|
|
3444
|
+
client_version = excluded.client_version,
|
|
3445
|
+
capabilities = excluded.capabilities,
|
|
3446
|
+
last_seen_at = excluded.last_seen_at
|
|
3447
|
+
`);
|
|
3448
|
+
stmt.run(
|
|
3449
|
+
agent.agentId,
|
|
3450
|
+
agent.sessionId,
|
|
3451
|
+
agent.clientName || null,
|
|
3452
|
+
agent.clientVersion || null,
|
|
3453
|
+
capabilities ? JSON.stringify(capabilities) : null,
|
|
3454
|
+
now,
|
|
3455
|
+
now
|
|
3456
|
+
);
|
|
3457
|
+
}
|
|
3458
|
+
listActiveAgents(maxAgeMs = 5 * 60 * 1e3) {
|
|
3459
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
3460
|
+
const stmt = this.db.prepare("SELECT * FROM agents WHERE last_seen_at >= ? ORDER BY last_seen_at DESC");
|
|
3461
|
+
const rows = stmt.all(cutoff);
|
|
3462
|
+
return rows.map((r) => ({
|
|
3463
|
+
agentId: r.agent_id,
|
|
3464
|
+
sessionId: r.session_id,
|
|
3465
|
+
clientName: r.client_name || void 0,
|
|
3466
|
+
clientVersion: r.client_version || void 0,
|
|
3467
|
+
lastSeenAt: r.last_seen_at
|
|
3468
|
+
}));
|
|
3469
|
+
}
|
|
3470
|
+
// --- Auxiliares Internos ---
|
|
3471
|
+
updateJobRow(job) {
|
|
3472
|
+
const stmt = this.db.prepare(`
|
|
3473
|
+
UPDATE jobs SET
|
|
3474
|
+
type = ?,
|
|
3475
|
+
status = ?,
|
|
3476
|
+
attempt = ?,
|
|
3477
|
+
max_attempts = ?,
|
|
3478
|
+
claimed_by = ?,
|
|
3479
|
+
lease_expires_at = ?,
|
|
3480
|
+
expires_at = ?,
|
|
3481
|
+
progress = ?,
|
|
3482
|
+
current_phase = ?,
|
|
3483
|
+
progress_message = ?,
|
|
3484
|
+
result_id = ?,
|
|
3485
|
+
file_handle_id = ?,
|
|
3486
|
+
target_role = ?,
|
|
3487
|
+
updated_at = ?,
|
|
3488
|
+
metadata = ?
|
|
3489
|
+
WHERE id = ?
|
|
3490
|
+
`);
|
|
3491
|
+
stmt.run(
|
|
3492
|
+
job.type,
|
|
3493
|
+
job.status,
|
|
3494
|
+
job.attempt,
|
|
3495
|
+
job.maxAttempts,
|
|
3496
|
+
job.claimedBy ? JSON.stringify(job.claimedBy) : null,
|
|
3497
|
+
job.leaseExpiresAt ?? null,
|
|
3498
|
+
job.expiresAt,
|
|
3499
|
+
job.progress,
|
|
3500
|
+
job.currentPhase ?? null,
|
|
3501
|
+
job.progressMessage ?? null,
|
|
3502
|
+
job.resultId ?? null,
|
|
3503
|
+
job.fileHandleId ?? null,
|
|
3504
|
+
job.targetRole ?? null,
|
|
3505
|
+
job.updatedAt,
|
|
3506
|
+
job.metadata ? JSON.stringify(job.metadata) : null,
|
|
3507
|
+
job.id
|
|
3508
|
+
);
|
|
3509
|
+
}
|
|
3510
|
+
};
|
|
3511
|
+
|
|
3512
|
+
// src/bridge/sse.ts
|
|
3513
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
3514
|
+
var SseBroker = class extends EventEmitter2 {
|
|
3515
|
+
activeStreams = /* @__PURE__ */ new Map();
|
|
3516
|
+
getSubscriberCount(jobId) {
|
|
3517
|
+
if (!jobId) return this.activeStreams.size;
|
|
3518
|
+
let count = 0;
|
|
3519
|
+
for (const key of this.activeStreams.keys()) {
|
|
3520
|
+
if (key.startsWith(`${jobId}:`)) count++;
|
|
3521
|
+
}
|
|
3522
|
+
return count;
|
|
3523
|
+
}
|
|
3524
|
+
publish(jobId, event) {
|
|
3525
|
+
this.emit(`job:${jobId}`, event);
|
|
3526
|
+
this.emit("global", { jobId, event });
|
|
3527
|
+
}
|
|
3528
|
+
formatSseMessage(event) {
|
|
3529
|
+
const lines = [
|
|
3530
|
+
`id: ${event.id}`,
|
|
3531
|
+
`event: ${event.type}`,
|
|
3532
|
+
`data: ${JSON.stringify(event)}`,
|
|
3533
|
+
"",
|
|
3534
|
+
""
|
|
3535
|
+
];
|
|
3536
|
+
return lines.join("\n");
|
|
3537
|
+
}
|
|
3538
|
+
handleJobEvents(req, res, jobId, store) {
|
|
3539
|
+
const parsedUrl = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
3540
|
+
const lastEventId = req.headers["last-event-id"] || parsedUrl.searchParams.get("lastEventId") || void 0;
|
|
3541
|
+
res.writeHead(200, {
|
|
3542
|
+
"Content-Type": "text/event-stream",
|
|
3543
|
+
"Cache-Control": "no-cache, no-transform",
|
|
3544
|
+
"Connection": "keep-alive",
|
|
3545
|
+
"Access-Control-Allow-Origin": "*",
|
|
3546
|
+
"X-Accel-Buffering": "no"
|
|
3547
|
+
});
|
|
3548
|
+
res.flushHeaders?.();
|
|
3549
|
+
res.write(`: connected to job ${jobId}
|
|
3550
|
+
|
|
1947
3551
|
`);
|
|
3552
|
+
try {
|
|
3553
|
+
const pastEvents = store.getEventsSince(jobId, lastEventId);
|
|
3554
|
+
for (const evt of pastEvents) {
|
|
3555
|
+
res.write(this.formatSseMessage(evt));
|
|
3556
|
+
}
|
|
1948
3557
|
} catch (err) {
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
3558
|
+
console.error(`Erro ao buscar hist\xF3rico de eventos para o job ${jobId}:`, err);
|
|
3559
|
+
}
|
|
3560
|
+
const streamId = `${jobId}:${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
3561
|
+
const onEvent = (event) => {
|
|
3562
|
+
try {
|
|
3563
|
+
res.write(this.formatSseMessage(event));
|
|
3564
|
+
} catch (err) {
|
|
3565
|
+
cleanup();
|
|
3566
|
+
}
|
|
3567
|
+
};
|
|
3568
|
+
const heartbeat = setInterval(() => {
|
|
3569
|
+
try {
|
|
3570
|
+
res.write(`: heartbeat ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
3571
|
+
|
|
1956
3572
|
`);
|
|
3573
|
+
} catch {
|
|
3574
|
+
cleanup();
|
|
3575
|
+
}
|
|
3576
|
+
}, 15e3);
|
|
3577
|
+
const cleanup = () => {
|
|
3578
|
+
clearInterval(heartbeat);
|
|
3579
|
+
this.off(`job:${jobId}`, onEvent);
|
|
3580
|
+
this.activeStreams.delete(streamId);
|
|
3581
|
+
};
|
|
3582
|
+
this.on(`job:${jobId}`, onEvent);
|
|
3583
|
+
this.activeStreams.set(streamId, { res, cleanup });
|
|
3584
|
+
req.on("close", cleanup);
|
|
3585
|
+
}
|
|
3586
|
+
closeAll() {
|
|
3587
|
+
for (const [id, stream] of this.activeStreams.entries()) {
|
|
3588
|
+
try {
|
|
3589
|
+
stream.res.end();
|
|
3590
|
+
} catch {
|
|
3591
|
+
}
|
|
3592
|
+
stream.cleanup();
|
|
1957
3593
|
}
|
|
3594
|
+
this.activeStreams.clear();
|
|
1958
3595
|
}
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
3596
|
+
};
|
|
3597
|
+
var sseBroker = new SseBroker();
|
|
3598
|
+
|
|
3599
|
+
// src/bridge/file-resolver.ts
|
|
3600
|
+
import fs3 from "fs";
|
|
3601
|
+
import path3 from "path";
|
|
3602
|
+
import os3 from "os";
|
|
3603
|
+
function getDefaultScanRoots() {
|
|
3604
|
+
const home = os3.homedir();
|
|
3605
|
+
return [
|
|
3606
|
+
{ root: "downloads", dirPath: path3.join(home, "Downloads") },
|
|
3607
|
+
{ root: "desktop", dirPath: path3.join(home, "Desktop") },
|
|
3608
|
+
{ root: "documents", dirPath: path3.join(home, "Documents") }
|
|
3609
|
+
];
|
|
3610
|
+
}
|
|
3611
|
+
function scoreFileMatch(fileName, fileSize, fileMtime, fingerprint) {
|
|
3612
|
+
let score = 0;
|
|
3613
|
+
if (fileName === fingerprint.name) {
|
|
3614
|
+
score += 40;
|
|
3615
|
+
} else if (fileName.toLowerCase() === fingerprint.name.toLowerCase()) {
|
|
3616
|
+
score += 35;
|
|
3617
|
+
}
|
|
3618
|
+
if (fileSize === fingerprint.size) {
|
|
3619
|
+
score += 40;
|
|
3620
|
+
}
|
|
3621
|
+
if (Math.abs(fileMtime - fingerprint.lastModified) <= 3e3) {
|
|
3622
|
+
score += 20;
|
|
3623
|
+
}
|
|
3624
|
+
return score;
|
|
3625
|
+
}
|
|
3626
|
+
function scanForCandidate(fingerprint, roots = getDefaultScanRoots()) {
|
|
3627
|
+
const candidates = [];
|
|
3628
|
+
for (const { root, dirPath } of roots) {
|
|
3629
|
+
if (!fs3.existsSync(dirPath)) continue;
|
|
3630
|
+
try {
|
|
3631
|
+
const entries = fs3.readdirSync(dirPath, { withFileTypes: true });
|
|
3632
|
+
for (const entry of entries) {
|
|
3633
|
+
if (!entry.isFile()) continue;
|
|
3634
|
+
const isPdf = entry.name.toLowerCase().endsWith(".pdf");
|
|
3635
|
+
const isNameMatch = entry.name.toLowerCase() === fingerprint.name.toLowerCase();
|
|
3636
|
+
if (!isPdf && !isNameMatch) continue;
|
|
3637
|
+
const fullPath = path3.join(dirPath, entry.name);
|
|
3638
|
+
try {
|
|
3639
|
+
const stat = fs3.statSync(fullPath);
|
|
3640
|
+
const score = scoreFileMatch(entry.name, stat.size, stat.mtimeMs, fingerprint);
|
|
3641
|
+
if (score >= 60) {
|
|
3642
|
+
candidates.push({
|
|
3643
|
+
filePath: fullPath,
|
|
3644
|
+
name: entry.name,
|
|
3645
|
+
root,
|
|
3646
|
+
size: stat.size,
|
|
3647
|
+
mtime: stat.mtimeMs,
|
|
3648
|
+
score
|
|
3649
|
+
});
|
|
3650
|
+
}
|
|
3651
|
+
} catch {
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
} catch {
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
if (candidates.length === 0) {
|
|
3658
|
+
return null;
|
|
3659
|
+
}
|
|
3660
|
+
candidates.sort((a, b) => b.score - a.score || b.mtime - a.mtime);
|
|
3661
|
+
return candidates[0];
|
|
3662
|
+
}
|
|
3663
|
+
function resolveLocalDocument(store, jobId, fingerprint, customRoots) {
|
|
3664
|
+
const match = scanForCandidate(fingerprint, customRoots);
|
|
3665
|
+
if (!match) {
|
|
3666
|
+
throw new Error(
|
|
3667
|
+
`N\xE3o foi poss\xEDvel localizar o arquivo "${fingerprint.name}" (${fingerprint.size} bytes) nas pastas Downloads, Desktop ou Documents do usu\xE1rio local.`
|
|
3668
|
+
);
|
|
3669
|
+
}
|
|
3670
|
+
const fileId = `doc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
3671
|
+
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString();
|
|
3672
|
+
const handle = {
|
|
3673
|
+
fileId,
|
|
3674
|
+
jobId,
|
|
3675
|
+
name: match.name,
|
|
3676
|
+
mediaType: "application/pdf",
|
|
3677
|
+
size: match.size,
|
|
3678
|
+
mtime: match.mtime,
|
|
3679
|
+
root: match.root,
|
|
3680
|
+
access: "native-local-document",
|
|
3681
|
+
expiresAt
|
|
3682
|
+
};
|
|
3683
|
+
store.saveFileHandle(handle, match.filePath);
|
|
3684
|
+
return handle;
|
|
1968
3685
|
}
|
|
1969
3686
|
|
|
1970
3687
|
// src/bridge/server.ts
|
|
1971
|
-
import http2 from "http";
|
|
1972
3688
|
function setCorsHeaders(res) {
|
|
1973
3689
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1974
3690
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1975
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
3691
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Last-Event-ID");
|
|
1976
3692
|
res.setHeader("Access-Control-Max-Age", "86400");
|
|
1977
3693
|
}
|
|
1978
3694
|
function sendJson(res, statusCode, data) {
|
|
@@ -2001,7 +3717,17 @@ async function parseBody(req) {
|
|
|
2001
3717
|
});
|
|
2002
3718
|
}
|
|
2003
3719
|
function createBridgeHttpServer(options = {}) {
|
|
2004
|
-
const
|
|
3720
|
+
const store = options.jobStore || new JobStore(createDatabase(options.dbPath), (event) => {
|
|
3721
|
+
sseBroker.publish(event.jobId, event);
|
|
3722
|
+
});
|
|
3723
|
+
const leaseWatchTimer = setInterval(() => {
|
|
3724
|
+
try {
|
|
3725
|
+
store.expireStaleLeases();
|
|
3726
|
+
} catch (err) {
|
|
3727
|
+
console.error("[LinkeGringo Bridge] Erro ao expirar leases:", err);
|
|
3728
|
+
}
|
|
3729
|
+
}, 1e4);
|
|
3730
|
+
const activeSseWatchers = /* @__PURE__ */ new Map();
|
|
2005
3731
|
const server = http2.createServer(async (req, res) => {
|
|
2006
3732
|
setCorsHeaders(res);
|
|
2007
3733
|
if (req.method === "OPTIONS") {
|
|
@@ -2013,32 +3739,43 @@ function createBridgeHttpServer(options = {}) {
|
|
|
2013
3739
|
const pathname = parsedUrl.pathname;
|
|
2014
3740
|
try {
|
|
2015
3741
|
if (req.method === "GET" && (pathname === "/health" || pathname === "/api/health")) {
|
|
3742
|
+
const activeJobs = store.listJobs();
|
|
3743
|
+
const activeAgents = store.listActiveAgents();
|
|
2016
3744
|
sendJson(res, 200, {
|
|
2017
3745
|
status: "ok",
|
|
2018
3746
|
server: "linkegringo-mcp-bridge",
|
|
2019
|
-
watcherConnected: bridgeJobStore.hasActiveWatcher(),
|
|
3747
|
+
watcherConnected: bridgeJobStore.hasActiveWatcher() || activeAgents.length > 0,
|
|
2020
3748
|
watcherCount: bridgeJobStore.getWatcherCount(),
|
|
2021
|
-
pendingCount: bridgeJobStore.getPendingCount(),
|
|
2022
|
-
totalJobs: bridgeJobStore.getAllJobs().length
|
|
3749
|
+
pendingCount: bridgeJobStore.getPendingCount() + store.listJobs({ status: "waiting_for_agent" }).length,
|
|
3750
|
+
totalJobs: bridgeJobStore.getAllJobs().length + activeJobs.length,
|
|
3751
|
+
activeJobsCount: activeJobs.length,
|
|
3752
|
+
activeAgentsCount: activeAgents.length,
|
|
3753
|
+
subscribersCount: sseBroker.getSubscriberCount()
|
|
2023
3754
|
});
|
|
2024
3755
|
return;
|
|
2025
3756
|
}
|
|
2026
3757
|
if (req.method === "GET" && pathname === "/api/bridge/status") {
|
|
2027
|
-
const
|
|
3758
|
+
const pendingLegacy = bridgeJobStore.getPendingJob();
|
|
3759
|
+
const waitingJobs = store.listJobs({ status: "waiting_for_agent" });
|
|
3760
|
+
const runningJobs = store.listJobs({ status: "running" });
|
|
3761
|
+
const activeAgents = store.listActiveAgents();
|
|
2028
3762
|
sendJson(res, 200, {
|
|
2029
3763
|
ok: true,
|
|
2030
3764
|
status: "ready",
|
|
2031
3765
|
server: "linkegringo-mcp-bridge",
|
|
2032
|
-
watcherConnected: bridgeJobStore.hasActiveWatcher(),
|
|
3766
|
+
watcherConnected: bridgeJobStore.hasActiveWatcher() || activeAgents.length > 0,
|
|
2033
3767
|
watcherCount: bridgeJobStore.getWatcherCount(),
|
|
2034
|
-
pendingCount: bridgeJobStore.getPendingCount(),
|
|
2035
|
-
activeJobId:
|
|
2036
|
-
totalJobs: bridgeJobStore.getAllJobs().length
|
|
3768
|
+
pendingCount: bridgeJobStore.getPendingCount() + waitingJobs.length,
|
|
3769
|
+
activeJobId: pendingLegacy ? pendingLegacy.id : runningJobs[0]?.id || waitingJobs[0]?.id || null,
|
|
3770
|
+
totalJobs: bridgeJobStore.getAllJobs().length + store.listJobs().length,
|
|
3771
|
+
activeAgents,
|
|
3772
|
+
waitingCount: waitingJobs.length,
|
|
3773
|
+
runningCount: runningJobs.length
|
|
2037
3774
|
});
|
|
2038
3775
|
return;
|
|
2039
3776
|
}
|
|
2040
3777
|
if (req.method === "POST" && pathname === "/api/bridge/disconnect") {
|
|
2041
|
-
for (const [id, client] of
|
|
3778
|
+
for (const [id, client] of activeSseWatchers.entries()) {
|
|
2042
3779
|
try {
|
|
2043
3780
|
client.res.write(
|
|
2044
3781
|
`data: ${JSON.stringify({ type: "disconnect", message: "Desconectado pelo usu\xE1rio na interface web." })}
|
|
@@ -2050,16 +3787,31 @@ function createBridgeHttpServer(options = {}) {
|
|
|
2050
3787
|
}
|
|
2051
3788
|
client.cleanup();
|
|
2052
3789
|
}
|
|
2053
|
-
|
|
3790
|
+
activeSseWatchers.clear();
|
|
3791
|
+
sseBroker.closeAll();
|
|
3792
|
+
const runningJobs = store.listJobs({ status: "running" });
|
|
3793
|
+
const waitingJobs = store.listJobs({ status: "waiting_for_agent" });
|
|
3794
|
+
for (const j of [...runningJobs, ...waitingJobs]) {
|
|
3795
|
+
try {
|
|
3796
|
+
store.cancelJob(j.id, "user", "Cancelado via interface");
|
|
3797
|
+
} catch {
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
2054
3800
|
const { canceledCount, clearedCount } = bridgeJobStore.resetQueue();
|
|
2055
3801
|
sendJson(res, 200, {
|
|
2056
3802
|
ok: true,
|
|
2057
3803
|
message: "Conex\xE3o cancelada e fila de jobs limpa com sucesso.",
|
|
2058
|
-
canceledCount,
|
|
3804
|
+
canceledCount: canceledCount + runningJobs.length + waitingJobs.length,
|
|
2059
3805
|
clearedCount
|
|
2060
3806
|
});
|
|
2061
3807
|
return;
|
|
2062
3808
|
}
|
|
3809
|
+
const jobEventsMatch = pathname.match(/^\/api\/jobs\/([a-zA-Z0-9_-]+)\/events$/);
|
|
3810
|
+
if (req.method === "GET" && jobEventsMatch) {
|
|
3811
|
+
const jobId = jobEventsMatch[1];
|
|
3812
|
+
sseBroker.handleJobEvents(req, res, jobId, store);
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
2063
3815
|
if (req.method === "GET" && pathname === "/api/jobs/stream") {
|
|
2064
3816
|
res.writeHead(200, {
|
|
2065
3817
|
"Content-Type": "text/event-stream",
|
|
@@ -2091,20 +3843,25 @@ function createBridgeHttpServer(options = {}) {
|
|
|
2091
3843
|
bridgeJobStore.on("job:created", onJobCreated);
|
|
2092
3844
|
bridgeJobStore.on("job:completed", onJobCompleted);
|
|
2093
3845
|
bridgeJobStore.on("job:failed", onJobFailed);
|
|
2094
|
-
const
|
|
2095
|
-
res.write(
|
|
3846
|
+
const onGlobalEvent = ({ jobId, event }) => {
|
|
3847
|
+
res.write(`data: ${JSON.stringify({ type: event.type, jobId, event })}
|
|
2096
3848
|
|
|
2097
3849
|
`);
|
|
3850
|
+
};
|
|
3851
|
+
sseBroker.on("global", onGlobalEvent);
|
|
3852
|
+
const hb = setInterval(() => {
|
|
3853
|
+
res.write(": heartbeat\n\n");
|
|
2098
3854
|
}, 15e3);
|
|
2099
3855
|
const cleanup = () => {
|
|
2100
|
-
clearInterval(
|
|
3856
|
+
clearInterval(hb);
|
|
2101
3857
|
bridgeJobStore.unregisterWatcher(watcherId);
|
|
2102
3858
|
bridgeJobStore.off("job:created", onJobCreated);
|
|
2103
3859
|
bridgeJobStore.off("job:completed", onJobCompleted);
|
|
2104
3860
|
bridgeJobStore.off("job:failed", onJobFailed);
|
|
2105
|
-
|
|
3861
|
+
sseBroker.off("global", onGlobalEvent);
|
|
3862
|
+
activeSseWatchers.delete(watcherId);
|
|
2106
3863
|
};
|
|
2107
|
-
|
|
3864
|
+
activeSseWatchers.set(watcherId, { res, cleanup });
|
|
2108
3865
|
req.on("close", cleanup);
|
|
2109
3866
|
return;
|
|
2110
3867
|
}
|
|
@@ -2114,64 +3871,243 @@ function createBridgeHttpServer(options = {}) {
|
|
|
2114
3871
|
sendJson(res, 400, { ok: false, error: 'Campo "type" \xE9 obrigat\xF3rio.' });
|
|
2115
3872
|
return;
|
|
2116
3873
|
}
|
|
2117
|
-
|
|
2118
|
-
|
|
3874
|
+
if (body.payload !== void 0 || body.type.includes("_")) {
|
|
3875
|
+
const legacyJob = bridgeJobStore.createJob(body.type, body.payload || {});
|
|
3876
|
+
options.onJobCreated?.(legacyJob);
|
|
3877
|
+
sendJson(res, 201, { ok: true, job: legacyJob });
|
|
3878
|
+
return;
|
|
3879
|
+
}
|
|
3880
|
+
const job = store.createJob({
|
|
3881
|
+
id: body.id,
|
|
3882
|
+
type: body.type,
|
|
3883
|
+
targetRole: body.targetRole,
|
|
3884
|
+
fileHandleId: body.fileHandleId,
|
|
3885
|
+
metadata: body.metadata,
|
|
3886
|
+
commandId: body.commandId
|
|
3887
|
+
});
|
|
2119
3888
|
sendJson(res, 201, { ok: true, job });
|
|
2120
3889
|
return;
|
|
2121
3890
|
}
|
|
3891
|
+
if (req.method === "POST" && pathname === "/api/files/resolve") {
|
|
3892
|
+
const body = await parseBody(req);
|
|
3893
|
+
if (!body.fingerprint?.name || typeof body.fingerprint?.size !== "number") {
|
|
3894
|
+
sendJson(res, 400, {
|
|
3895
|
+
ok: false,
|
|
3896
|
+
error: 'Fingerprint inv\xE1lido. "name" e "size" num\xE9rico s\xE3o obrigat\xF3rios.'
|
|
3897
|
+
});
|
|
3898
|
+
return;
|
|
3899
|
+
}
|
|
3900
|
+
try {
|
|
3901
|
+
const handle = resolveLocalDocument(store, body.jobId || "temp_job", body.fingerprint);
|
|
3902
|
+
sendJson(res, 200, { ok: true, handle });
|
|
3903
|
+
} catch (err) {
|
|
3904
|
+
sendJson(res, 404, { ok: false, error: err.message });
|
|
3905
|
+
}
|
|
3906
|
+
return;
|
|
3907
|
+
}
|
|
3908
|
+
const docMatch = pathname.match(/^\/api\/documents\/([a-zA-Z0-9_-]+)$/);
|
|
3909
|
+
if (req.method === "GET" && docMatch) {
|
|
3910
|
+
const docId = docMatch[1];
|
|
3911
|
+
const record = store.getFileHandle(docId);
|
|
3912
|
+
if (!record) {
|
|
3913
|
+
sendJson(res, 404, { ok: false, error: `Documento n\xE3o encontrado: ${docId}` });
|
|
3914
|
+
return;
|
|
3915
|
+
}
|
|
3916
|
+
sendJson(res, 200, {
|
|
3917
|
+
ok: true,
|
|
3918
|
+
document: {
|
|
3919
|
+
fileId: record.fileId,
|
|
3920
|
+
jobId: record.jobId,
|
|
3921
|
+
name: record.name,
|
|
3922
|
+
mediaType: record.mediaType,
|
|
3923
|
+
size: record.size,
|
|
3924
|
+
mtime: record.mtime,
|
|
3925
|
+
root: record.root,
|
|
3926
|
+
access: "native-local-document",
|
|
3927
|
+
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString()
|
|
3928
|
+
}
|
|
3929
|
+
});
|
|
3930
|
+
return;
|
|
3931
|
+
}
|
|
2122
3932
|
if (req.method === "GET" && pathname === "/api/jobs") {
|
|
2123
|
-
|
|
3933
|
+
const status = parsedUrl.searchParams.get("status");
|
|
3934
|
+
const type = parsedUrl.searchParams.get("type");
|
|
3935
|
+
const jobs = store.listJobs({ status, type });
|
|
3936
|
+
sendJson(res, 200, { ok: true, jobs });
|
|
2124
3937
|
return;
|
|
2125
3938
|
}
|
|
2126
3939
|
if (req.method === "GET" && pathname === "/api/jobs/pending") {
|
|
2127
3940
|
const id = parsedUrl.searchParams.get("id") || void 0;
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
const timeout = parseInt(parsedUrl.searchParams.get("timeout") || "30000", 10);
|
|
2137
|
-
let job = bridgeJobStore.getJob(jobId);
|
|
2138
|
-
if (!job) {
|
|
2139
|
-
sendJson(res, 404, { ok: false, error: `Job n\xE3o encontrado: ${jobId}` });
|
|
3941
|
+
if (id) {
|
|
3942
|
+
const specificLegacy = bridgeJobStore.getJob(id);
|
|
3943
|
+
if (specificLegacy) {
|
|
3944
|
+
sendJson(res, 200, { ok: true, job: specificLegacy });
|
|
3945
|
+
return;
|
|
3946
|
+
}
|
|
3947
|
+
const specific = store.getJob(id);
|
|
3948
|
+
sendJson(res, 200, { ok: true, job: specific });
|
|
2140
3949
|
return;
|
|
2141
3950
|
}
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
job = bridgeJobStore.getJob(jobId) || job;
|
|
2147
|
-
}
|
|
3951
|
+
const legacyPending = bridgeJobStore.getPendingJob();
|
|
3952
|
+
if (legacyPending) {
|
|
3953
|
+
sendJson(res, 200, { ok: true, job: legacyPending });
|
|
3954
|
+
return;
|
|
2148
3955
|
}
|
|
2149
|
-
|
|
3956
|
+
const waiting = store.listJobs({ status: "waiting_for_agent" });
|
|
3957
|
+
sendJson(res, 200, { ok: true, job: waiting[0] || null });
|
|
2150
3958
|
return;
|
|
2151
3959
|
}
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
const body = await parseBody(req);
|
|
2156
|
-
try {
|
|
2157
|
-
const job = bridgeJobStore.completeJob(jobId, body.result);
|
|
2158
|
-
sendJson(res, 200, { ok: true, job });
|
|
2159
|
-
} catch (err) {
|
|
2160
|
-
sendJson(res, 404, { ok: false, error: err.message });
|
|
2161
|
-
}
|
|
3960
|
+
if (req.method === "GET" && pathname === "/api/agents") {
|
|
3961
|
+
const agents = store.listActiveAgents();
|
|
3962
|
+
sendJson(res, 200, { ok: true, agents });
|
|
2162
3963
|
return;
|
|
2163
3964
|
}
|
|
2164
|
-
const
|
|
2165
|
-
if (
|
|
2166
|
-
const jobId =
|
|
2167
|
-
const
|
|
2168
|
-
|
|
2169
|
-
const
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
3965
|
+
const jobRouteMatch = pathname.match(/^\/api\/jobs\/([a-zA-Z0-9_-]+)(?:\/([a-zA-Z0-9_-]+))?$/);
|
|
3966
|
+
if (jobRouteMatch) {
|
|
3967
|
+
const jobId = jobRouteMatch[1];
|
|
3968
|
+
const subAction = jobRouteMatch[2];
|
|
3969
|
+
if (req.method === "GET" && !subAction) {
|
|
3970
|
+
const wait = parsedUrl.searchParams.get("wait") === "true";
|
|
3971
|
+
const timeout = parseInt(parsedUrl.searchParams.get("timeout") || "30000", 10);
|
|
3972
|
+
const legacyJob = bridgeJobStore.getJob(jobId);
|
|
3973
|
+
if (legacyJob) {
|
|
3974
|
+
if (wait && (legacyJob.status === "pending" || legacyJob.status === "processing")) {
|
|
3975
|
+
try {
|
|
3976
|
+
const waited = await bridgeJobStore.waitForJob(jobId, timeout);
|
|
3977
|
+
sendJson(res, 200, { ok: true, job: waited });
|
|
3978
|
+
return;
|
|
3979
|
+
} catch {
|
|
3980
|
+
sendJson(res, 200, { ok: true, job: bridgeJobStore.getJob(jobId) || legacyJob });
|
|
3981
|
+
return;
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
sendJson(res, 200, { ok: true, job: legacyJob });
|
|
3985
|
+
return;
|
|
3986
|
+
}
|
|
3987
|
+
let job = store.getJob(jobId);
|
|
3988
|
+
if (!job) {
|
|
3989
|
+
sendJson(res, 404, { ok: false, error: `Job n\xE3o encontrado: ${jobId}` });
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
if (wait && (job.status === "waiting_for_agent" || job.status === "running")) {
|
|
3993
|
+
const startWait = Date.now();
|
|
3994
|
+
await new Promise((resolve) => {
|
|
3995
|
+
const checkInterval = setInterval(() => {
|
|
3996
|
+
const current = store.getJob(jobId);
|
|
3997
|
+
if (!current || current.status === "completed" || current.status === "failed" || current.status === "cancelled") {
|
|
3998
|
+
clearInterval(checkInterval);
|
|
3999
|
+
resolve();
|
|
4000
|
+
} else if (Date.now() - startWait >= timeout) {
|
|
4001
|
+
clearInterval(checkInterval);
|
|
4002
|
+
resolve();
|
|
4003
|
+
}
|
|
4004
|
+
}, 50);
|
|
4005
|
+
});
|
|
4006
|
+
job = store.getJob(jobId) || job;
|
|
4007
|
+
}
|
|
4008
|
+
const resultRecord = job.resultId ? store.getJobResult(job.resultId) : null;
|
|
4009
|
+
const fullJob = {
|
|
4010
|
+
...job,
|
|
4011
|
+
result: resultRecord?.payload ?? resultRecord
|
|
4012
|
+
};
|
|
4013
|
+
sendJson(res, 200, { ok: true, job: fullJob, result: resultRecord });
|
|
4014
|
+
return;
|
|
4015
|
+
}
|
|
4016
|
+
if (req.method === "POST" && subAction === "claim") {
|
|
4017
|
+
const body = await parseBody(req);
|
|
4018
|
+
if (!body.agent?.agentId || !body.agent?.sessionId) {
|
|
4019
|
+
sendJson(res, 400, { ok: false, error: "Identidade do agente (agentId e sessionId) \xE9 obrigat\xF3ria." });
|
|
4020
|
+
return;
|
|
4021
|
+
}
|
|
4022
|
+
const updated = store.claimJob(jobId, body.agent, {
|
|
4023
|
+
leaseDurationMs: body.leaseDurationMs,
|
|
4024
|
+
commandId: body.commandId
|
|
4025
|
+
});
|
|
4026
|
+
sendJson(res, 200, { ok: true, job: updated });
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
4029
|
+
if (req.method === "POST" && subAction === "progress") {
|
|
4030
|
+
const body = await parseBody(req);
|
|
4031
|
+
if (!body.agent) {
|
|
4032
|
+
sendJson(res, 400, { ok: false, error: "Identidade do agente \xE9 obrigat\xF3ria." });
|
|
4033
|
+
return;
|
|
4034
|
+
}
|
|
4035
|
+
const updated = store.reportProgress(jobId, body.agent, {
|
|
4036
|
+
phase: body.phase,
|
|
4037
|
+
progress: body.progress,
|
|
4038
|
+
message: body.message,
|
|
4039
|
+
leaseDurationMs: body.leaseDurationMs,
|
|
4040
|
+
commandId: body.commandId
|
|
4041
|
+
});
|
|
4042
|
+
sendJson(res, 200, { ok: true, job: updated });
|
|
4043
|
+
return;
|
|
4044
|
+
}
|
|
4045
|
+
if (req.method === "POST" && subAction === "user-action") {
|
|
4046
|
+
const body = await parseBody(req);
|
|
4047
|
+
const updated = store.requestUserAction(jobId, body.agent, {
|
|
4048
|
+
reason: body.reason,
|
|
4049
|
+
prompt: body.prompt,
|
|
4050
|
+
commandId: body.commandId
|
|
4051
|
+
});
|
|
4052
|
+
sendJson(res, 200, { ok: true, job: updated });
|
|
4053
|
+
return;
|
|
4054
|
+
}
|
|
4055
|
+
if (req.method === "POST" && subAction === "resume") {
|
|
4056
|
+
const body = await parseBody(req);
|
|
4057
|
+
const updated = store.resumeJob(jobId, {
|
|
4058
|
+
actor: "user",
|
|
4059
|
+
reason: body.reason || "user_confirmed",
|
|
4060
|
+
payload: body.payload,
|
|
4061
|
+
commandId: body.commandId
|
|
4062
|
+
});
|
|
4063
|
+
sendJson(res, 200, { ok: true, job: updated });
|
|
4064
|
+
return;
|
|
4065
|
+
}
|
|
4066
|
+
if (req.method === "POST" && subAction === "complete") {
|
|
4067
|
+
const body = await parseBody(req);
|
|
4068
|
+
const payload = body.result?.payload !== void 0 ? body.result.payload : body.result;
|
|
4069
|
+
const legacyJob = bridgeJobStore.getJob(jobId);
|
|
4070
|
+
if (legacyJob) {
|
|
4071
|
+
const completed2 = bridgeJobStore.completeJob(jobId, payload);
|
|
4072
|
+
sendJson(res, 200, { ok: true, job: completed2 });
|
|
4073
|
+
return;
|
|
4074
|
+
}
|
|
4075
|
+
const agent = body.agent || {
|
|
4076
|
+
agentId: "mcp-agent",
|
|
4077
|
+
sessionId: "session_mcp"
|
|
4078
|
+
};
|
|
4079
|
+
const submissionId = body.submissionId || `sub_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
4080
|
+
const completed = store.completeJob(jobId, agent, {
|
|
4081
|
+
submissionId,
|
|
4082
|
+
schemaVersion: body.schemaVersion || "1.0",
|
|
4083
|
+
payload,
|
|
4084
|
+
commandId: body.commandId
|
|
4085
|
+
});
|
|
4086
|
+
sendJson(res, 200, { ok: true, job: completed.job, resultId: completed.resultId });
|
|
4087
|
+
return;
|
|
4088
|
+
}
|
|
4089
|
+
if (req.method === "GET" && subAction === "result") {
|
|
4090
|
+
const job = store.getJob(jobId);
|
|
4091
|
+
if (!job || !job.resultId) {
|
|
4092
|
+
sendJson(res, 404, { ok: false, error: "Resultado do job n\xE3o encontrado ou ainda n\xE3o dispon\xEDvel." });
|
|
4093
|
+
return;
|
|
4094
|
+
}
|
|
4095
|
+
const resultEnvelope = store.getJobResult(job.resultId);
|
|
4096
|
+
sendJson(res, 200, { ok: true, result: resultEnvelope });
|
|
4097
|
+
return;
|
|
4098
|
+
}
|
|
4099
|
+
if (req.method === "POST" && subAction === "fail") {
|
|
4100
|
+
const body = await parseBody(req);
|
|
4101
|
+
const legacyJob = bridgeJobStore.getJob(jobId);
|
|
4102
|
+
if (legacyJob) {
|
|
4103
|
+
const failed2 = bridgeJobStore.failJob(jobId, body.error || "Erro desconhecido");
|
|
4104
|
+
sendJson(res, 200, { ok: true, job: failed2 });
|
|
4105
|
+
return;
|
|
4106
|
+
}
|
|
4107
|
+
const failed = store.failJob(jobId, body.error || "Erro desconhecido", body.actor, body.commandId);
|
|
4108
|
+
sendJson(res, 200, { ok: true, job: failed });
|
|
4109
|
+
return;
|
|
2173
4110
|
}
|
|
2174
|
-
return;
|
|
2175
4111
|
}
|
|
2176
4112
|
if (req.method === "POST" && pathname === "/api/jobs/clear") {
|
|
2177
4113
|
bridgeJobStore.clear();
|
|
@@ -2183,6 +4119,9 @@ function createBridgeHttpServer(options = {}) {
|
|
|
2183
4119
|
sendJson(res, 500, { ok: false, error: err?.message || "Erro interno no servidor bridge" });
|
|
2184
4120
|
}
|
|
2185
4121
|
});
|
|
4122
|
+
server.on("close", () => {
|
|
4123
|
+
clearInterval(leaseWatchTimer);
|
|
4124
|
+
});
|
|
2186
4125
|
return server;
|
|
2187
4126
|
}
|
|
2188
4127
|
var activeBridgeServer = null;
|
|
@@ -2365,8 +4304,236 @@ function startBridgeWatcher(options = {}) {
|
|
|
2365
4304
|
};
|
|
2366
4305
|
}
|
|
2367
4306
|
|
|
4307
|
+
// src/client/bridge-client.ts
|
|
4308
|
+
var BridgeClient = class {
|
|
4309
|
+
bridgeUrl;
|
|
4310
|
+
defaultTimeoutMs;
|
|
4311
|
+
constructor(config = {}) {
|
|
4312
|
+
this.bridgeUrl = (config.bridgeUrl || typeof process !== "undefined" && process.env?.LINKEGRINGO_BRIDGE_URL || "http://127.0.0.1:5174").replace(/\/$/, "");
|
|
4313
|
+
this.defaultTimeoutMs = config.defaultTimeoutMs || 3e5;
|
|
4314
|
+
}
|
|
4315
|
+
/**
|
|
4316
|
+
* Checa se o bridge HTTP local está online e responsivo.
|
|
4317
|
+
*/
|
|
4318
|
+
async checkHealth() {
|
|
4319
|
+
try {
|
|
4320
|
+
const controller = new AbortController();
|
|
4321
|
+
const timer = setTimeout(() => controller.abort(), 2e3);
|
|
4322
|
+
const res = await fetch(`${this.bridgeUrl}/health`, { signal: controller.signal });
|
|
4323
|
+
clearTimeout(timer);
|
|
4324
|
+
if (!res.ok) return false;
|
|
4325
|
+
const data = await res.json();
|
|
4326
|
+
return data.status === "ok";
|
|
4327
|
+
} catch {
|
|
4328
|
+
return false;
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
/**
|
|
4332
|
+
* Resolve um arquivo local por fingerprint determinístico sem copiá-lo.
|
|
4333
|
+
*/
|
|
4334
|
+
async resolveFile(fingerprint, jobId) {
|
|
4335
|
+
const res = await fetch(`${this.bridgeUrl}/api/files/resolve`, {
|
|
4336
|
+
method: "POST",
|
|
4337
|
+
headers: { "Content-Type": "application/json" },
|
|
4338
|
+
body: JSON.stringify({ fingerprint, jobId })
|
|
4339
|
+
});
|
|
4340
|
+
if (!res.ok) {
|
|
4341
|
+
const err = await res.json().catch(() => ({}));
|
|
4342
|
+
throw new Error(err.error || `Falha ao resolver arquivo local (${res.status})`);
|
|
4343
|
+
}
|
|
4344
|
+
const data = await res.json();
|
|
4345
|
+
return data.handle;
|
|
4346
|
+
}
|
|
4347
|
+
/**
|
|
4348
|
+
* Obtém detalhes públicos do DocumentHandle a partir do capability token opaco.
|
|
4349
|
+
*/
|
|
4350
|
+
async getDocument(fileId) {
|
|
4351
|
+
const res = await fetch(`${this.bridgeUrl}/api/documents/${encodeURIComponent(fileId)}`);
|
|
4352
|
+
if (!res.ok) {
|
|
4353
|
+
const err = await res.json().catch(() => ({}));
|
|
4354
|
+
throw new Error(err.error || `Documento n\xE3o encontrado: ${fileId}`);
|
|
4355
|
+
}
|
|
4356
|
+
const data = await res.json();
|
|
4357
|
+
return data.document;
|
|
4358
|
+
}
|
|
4359
|
+
/**
|
|
4360
|
+
* Cria um novo job na máquina de estados do Bridge.
|
|
4361
|
+
*/
|
|
4362
|
+
async createJob(params) {
|
|
4363
|
+
const res = await fetch(`${this.bridgeUrl}/api/jobs`, {
|
|
4364
|
+
method: "POST",
|
|
4365
|
+
headers: { "Content-Type": "application/json" },
|
|
4366
|
+
body: JSON.stringify(params)
|
|
4367
|
+
});
|
|
4368
|
+
if (!res.ok) {
|
|
4369
|
+
const err = await res.json().catch(() => ({}));
|
|
4370
|
+
throw new Error(err.error || `Falha ao criar job no Bridge (${res.status})`);
|
|
4371
|
+
}
|
|
4372
|
+
const data = await res.json();
|
|
4373
|
+
return data.job;
|
|
4374
|
+
}
|
|
4375
|
+
/**
|
|
4376
|
+
* Consulta o estado atual de um job.
|
|
4377
|
+
*/
|
|
4378
|
+
async getJob(jobId) {
|
|
4379
|
+
const res = await fetch(`${this.bridgeUrl}/api/jobs/${encodeURIComponent(jobId)}`);
|
|
4380
|
+
if (!res.ok) {
|
|
4381
|
+
const err = await res.json().catch(() => ({}));
|
|
4382
|
+
throw new Error(err.error || `Job n\xE3o encontrado: ${jobId}`);
|
|
4383
|
+
}
|
|
4384
|
+
const data = await res.json();
|
|
4385
|
+
if (data.result && !data.job.result) {
|
|
4386
|
+
data.job.result = data.result.payload ?? data.result;
|
|
4387
|
+
}
|
|
4388
|
+
return data.job;
|
|
4389
|
+
}
|
|
4390
|
+
/**
|
|
4391
|
+
* Retoma um job em espera de confirmação do usuário (Human-in-the-Loop).
|
|
4392
|
+
*/
|
|
4393
|
+
async resumeJob(jobId, paramsOrReason = "user_resumed", maybePayload) {
|
|
4394
|
+
const params = typeof paramsOrReason === "string" ? { actor: "user", reason: paramsOrReason, payload: maybePayload } : { actor: paramsOrReason.actor || "user", reason: paramsOrReason.reason, payload: paramsOrReason.payload ?? maybePayload };
|
|
4395
|
+
const res = await fetch(`${this.bridgeUrl}/api/jobs/${encodeURIComponent(jobId)}/resume`, {
|
|
4396
|
+
method: "POST",
|
|
4397
|
+
headers: { "Content-Type": "application/json" },
|
|
4398
|
+
body: JSON.stringify(params)
|
|
4399
|
+
});
|
|
4400
|
+
if (!res.ok) {
|
|
4401
|
+
const err = await res.json().catch(() => ({}));
|
|
4402
|
+
throw new Error(err.error || `Falha ao retomar job ${jobId}`);
|
|
4403
|
+
}
|
|
4404
|
+
const data = await res.json();
|
|
4405
|
+
return data.job;
|
|
4406
|
+
}
|
|
4407
|
+
/**
|
|
4408
|
+
* Subscreve ao stream SSE de eventos do job (`/api/jobs/:id/events`).
|
|
4409
|
+
* Suporta re-conexão automática com Last-Event-ID no browser nativo via EventSource.
|
|
4410
|
+
*/
|
|
4411
|
+
subscribeJobEvents(jobId, onEvent, onError) {
|
|
4412
|
+
if (typeof EventSource !== "undefined") {
|
|
4413
|
+
const url = `${this.bridgeUrl}/api/jobs/${encodeURIComponent(jobId)}/events`;
|
|
4414
|
+
const eventSource = new EventSource(url);
|
|
4415
|
+
eventSource.onmessage = (e) => {
|
|
4416
|
+
try {
|
|
4417
|
+
const parsed = JSON.parse(e.data);
|
|
4418
|
+
onEvent(parsed);
|
|
4419
|
+
} catch (err) {
|
|
4420
|
+
console.warn("[BridgeClient] Erro ao deserializar evento SSE:", err);
|
|
4421
|
+
}
|
|
4422
|
+
};
|
|
4423
|
+
eventSource.onerror = (e) => {
|
|
4424
|
+
if (onError) onError(e);
|
|
4425
|
+
};
|
|
4426
|
+
return () => {
|
|
4427
|
+
eventSource.close();
|
|
4428
|
+
};
|
|
4429
|
+
}
|
|
4430
|
+
let active = true;
|
|
4431
|
+
let lastEventSeq = 0;
|
|
4432
|
+
const poll = async () => {
|
|
4433
|
+
while (active) {
|
|
4434
|
+
try {
|
|
4435
|
+
const job = await this.getJob(jobId);
|
|
4436
|
+
if (job.status === "completed" || job.status === "failed" || job.status === "cancelled") {
|
|
4437
|
+
break;
|
|
4438
|
+
}
|
|
4439
|
+
} catch {
|
|
4440
|
+
}
|
|
4441
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
4442
|
+
}
|
|
4443
|
+
};
|
|
4444
|
+
poll();
|
|
4445
|
+
return () => {
|
|
4446
|
+
active = false;
|
|
4447
|
+
};
|
|
4448
|
+
}
|
|
4449
|
+
/**
|
|
4450
|
+
* Aguarda a resolução completa do job via streaming de eventos SSE,
|
|
4451
|
+
* despachando atualizações reativas de progresso e ações do usuário.
|
|
4452
|
+
*/
|
|
4453
|
+
async waitForJobResult(jobId, options = {}) {
|
|
4454
|
+
const timeoutMs = options.timeoutMs ?? this.defaultTimeoutMs;
|
|
4455
|
+
const startTime = Date.now();
|
|
4456
|
+
return new Promise((resolve, reject) => {
|
|
4457
|
+
let isSettled = false;
|
|
4458
|
+
const cleanup = () => {
|
|
4459
|
+
isSettled = true;
|
|
4460
|
+
unsubscribe();
|
|
4461
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
4462
|
+
};
|
|
4463
|
+
const timeoutTimer = setTimeout(() => {
|
|
4464
|
+
if (!isSettled) {
|
|
4465
|
+
cleanup();
|
|
4466
|
+
reject(
|
|
4467
|
+
new Error(
|
|
4468
|
+
`Tempo limite de ${Math.round(timeoutMs / 1e3)}s excedido aguardando resposta do Agente de IA para o job ${jobId}.`
|
|
4469
|
+
)
|
|
4470
|
+
);
|
|
4471
|
+
}
|
|
4472
|
+
}, timeoutMs);
|
|
4473
|
+
const handleEvent = async (event) => {
|
|
4474
|
+
if (isSettled) return;
|
|
4475
|
+
if (options.onEvent) {
|
|
4476
|
+
options.onEvent(event);
|
|
4477
|
+
}
|
|
4478
|
+
if (event.type === "job.progress") {
|
|
4479
|
+
const p = event.payload;
|
|
4480
|
+
if (options.onProgress) {
|
|
4481
|
+
options.onProgress(p.phase || "running", p.progress || 0, p.message);
|
|
4482
|
+
}
|
|
4483
|
+
} else if (event.type === "job.waiting_for_user") {
|
|
4484
|
+
const p = event.payload;
|
|
4485
|
+
if (options.onUserAction) {
|
|
4486
|
+
options.onUserAction(p.reason || "waiting_for_user", p.prompt);
|
|
4487
|
+
}
|
|
4488
|
+
} else if (event.type === "job.completed") {
|
|
4489
|
+
cleanup();
|
|
4490
|
+
try {
|
|
4491
|
+
const p = event.payload;
|
|
4492
|
+
if (p?.result?.payload) {
|
|
4493
|
+
resolve(p.result.payload);
|
|
4494
|
+
return;
|
|
4495
|
+
}
|
|
4496
|
+
const finishedJob = await this.getJob(jobId);
|
|
4497
|
+
if (finishedJob.result) {
|
|
4498
|
+
const res = finishedJob.result?.payload ?? finishedJob.result;
|
|
4499
|
+
resolve(res);
|
|
4500
|
+
} else if (finishedJob.resultId) {
|
|
4501
|
+
const res = await fetch(`${this.bridgeUrl}/api/jobs/${encodeURIComponent(jobId)}`);
|
|
4502
|
+
const data = await res.json();
|
|
4503
|
+
resolve(data.result?.payload ?? data.job?.result ?? data);
|
|
4504
|
+
} else {
|
|
4505
|
+
resolve(finishedJob);
|
|
4506
|
+
}
|
|
4507
|
+
} catch (err) {
|
|
4508
|
+
reject(err);
|
|
4509
|
+
}
|
|
4510
|
+
} else if (event.type === "job.failed") {
|
|
4511
|
+
cleanup();
|
|
4512
|
+
const p = event.payload;
|
|
4513
|
+
reject(new Error(p?.error || "O Agente de IA reportou uma falha ao processar o job."));
|
|
4514
|
+
}
|
|
4515
|
+
};
|
|
4516
|
+
const unsubscribe = this.subscribeJobEvents(jobId, handleEvent, (err) => {
|
|
4517
|
+
console.warn("[BridgeClient] Conex\xE3o SSE inst\xE1vel, aguardando reconex\xE3o...", err);
|
|
4518
|
+
});
|
|
4519
|
+
this.getJob(jobId).then((job) => {
|
|
4520
|
+
if (isSettled) return;
|
|
4521
|
+
if (job.status === "completed") {
|
|
4522
|
+
cleanup();
|
|
4523
|
+
const res = job.result?.payload ?? job.result;
|
|
4524
|
+
resolve(res);
|
|
4525
|
+
} else if (job.status === "failed") {
|
|
4526
|
+
cleanup();
|
|
4527
|
+
reject(new Error(job.error || "O job falhou."));
|
|
4528
|
+
}
|
|
4529
|
+
}).catch(() => {
|
|
4530
|
+
});
|
|
4531
|
+
});
|
|
4532
|
+
}
|
|
4533
|
+
};
|
|
4534
|
+
|
|
2368
4535
|
// src/index.ts
|
|
2369
|
-
import
|
|
4536
|
+
import fs4 from "fs";
|
|
2370
4537
|
import { fileURLToPath } from "url";
|
|
2371
4538
|
async function main() {
|
|
2372
4539
|
if (process.argv.includes("install") || process.argv.includes("setup") || process.argv.includes("--install")) {
|
|
@@ -2393,7 +4560,7 @@ function isDirectExecution() {
|
|
|
2393
4560
|
if (!process.argv[1]) return false;
|
|
2394
4561
|
try {
|
|
2395
4562
|
const currentFilePath = fileURLToPath(import.meta.url);
|
|
2396
|
-
const scriptPath =
|
|
4563
|
+
const scriptPath = fs4.existsSync(process.argv[1]) ? fs4.realpathSync(process.argv[1]) : process.argv[1];
|
|
2397
4564
|
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");
|
|
2398
4565
|
} catch {
|
|
2399
4566
|
return true;
|
|
@@ -2406,8 +4573,12 @@ if (isDirectExecution()) {
|
|
|
2406
4573
|
});
|
|
2407
4574
|
}
|
|
2408
4575
|
export {
|
|
4576
|
+
BridgeClient,
|
|
4577
|
+
DEFAULT_BRIDGE_URL,
|
|
2409
4578
|
auditProfileInputSchema,
|
|
2410
4579
|
bridgeJobStore,
|
|
4580
|
+
claimJobInputSchema,
|
|
4581
|
+
claimRemoteJob,
|
|
2411
4582
|
completeRemoteOrLocalJob,
|
|
2412
4583
|
convertToXyzBulletInputSchema,
|
|
2413
4584
|
createBridgeHttpServer,
|
|
@@ -2416,25 +4587,46 @@ export {
|
|
|
2416
4587
|
failRemoteOrLocalJob,
|
|
2417
4588
|
formatGoogleXyzBullet,
|
|
2418
4589
|
generateHeadlineInputSchema,
|
|
4590
|
+
getBridgeUrl,
|
|
2419
4591
|
getExperienceBulletCount,
|
|
2420
4592
|
getMcpConfigsForSystem,
|
|
2421
4593
|
getPendingJobInputSchema,
|
|
2422
4594
|
getRemoteOrLocalPendingJob,
|
|
2423
4595
|
handleAuditProfile,
|
|
4596
|
+
handleClaimJob,
|
|
2424
4597
|
handleConvertToXyzBullet,
|
|
2425
4598
|
handleGenerateHeadline,
|
|
2426
4599
|
handleGetPendingJob,
|
|
4600
|
+
handleInspectDocument,
|
|
4601
|
+
handleListJobs,
|
|
4602
|
+
handleReportProgress,
|
|
4603
|
+
handleRequestUserAction,
|
|
2427
4604
|
handleSimulateRecruiterSearch,
|
|
4605
|
+
handleSubmitDiagnostic,
|
|
4606
|
+
handleSubmitInterview,
|
|
2428
4607
|
handleSubmitJobResult,
|
|
4608
|
+
handleSubmitRewrite,
|
|
2429
4609
|
handleWatchLinkeGringo,
|
|
4610
|
+
inspectDocumentInputSchema,
|
|
4611
|
+
inspectRemoteDocument,
|
|
2430
4612
|
installMcpServerConfig,
|
|
4613
|
+
listJobsInputSchema,
|
|
4614
|
+
listRemoteJobs,
|
|
2431
4615
|
parseArgs,
|
|
4616
|
+
reportProgressInputSchema,
|
|
4617
|
+
reportRemoteProgress,
|
|
4618
|
+
requestRemoteUserAction,
|
|
4619
|
+
requestUserActionInputSchema,
|
|
2432
4620
|
runInstaller,
|
|
2433
4621
|
simulateRecruiterSearchInputSchema,
|
|
2434
4622
|
startBridgeServer,
|
|
2435
4623
|
startBridgeWatcher,
|
|
2436
4624
|
stopBridgeServer,
|
|
4625
|
+
submitDiagnosticInputSchema,
|
|
4626
|
+
submitInterviewInputSchema,
|
|
2437
4627
|
submitJobResultInputSchema,
|
|
4628
|
+
submitRemoteJobResult,
|
|
4629
|
+
submitRewriteInputSchema,
|
|
2438
4630
|
waitForNextJob,
|
|
2439
4631
|
watchLinkeGringoInputSchema
|
|
2440
4632
|
};
|