@linkegringo/mcp 1.0.10 → 2.0.0
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 +2451 -260
- package/package.json +10 -10
- package/LICENSE +0 -21
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) {
|
|
@@ -1449,8 +1928,8 @@ ${instructions}`
|
|
|
1449
1928
|
}
|
|
1450
1929
|
|
|
1451
1930
|
// src/tools/get-pending-job.ts
|
|
1452
|
-
var getPendingJobInputSchema =
|
|
1453
|
-
jobId:
|
|
1931
|
+
var getPendingJobInputSchema = z13.object({
|
|
1932
|
+
jobId: z13.string().optional().describe("ID espec\xEDfico do job a ser buscado (opcional. Se omitido, pega o pr\xF3ximo da fila)")
|
|
1454
1933
|
});
|
|
1455
1934
|
async function handleGetPendingJob(input = {}) {
|
|
1456
1935
|
const job = await getRemoteOrLocalPendingJob(input.jobId);
|
|
@@ -1473,12 +1952,12 @@ async function handleGetPendingJob(input = {}) {
|
|
|
1473
1952
|
}
|
|
1474
1953
|
|
|
1475
1954
|
// src/tools/submit-job-result.ts
|
|
1476
|
-
import { z as
|
|
1477
|
-
var submitJobResultInputSchema =
|
|
1478
|
-
jobId:
|
|
1479
|
-
result:
|
|
1480
|
-
status:
|
|
1481
|
-
error:
|
|
1955
|
+
import { z as z14 } from "zod";
|
|
1956
|
+
var submitJobResultInputSchema = z14.object({
|
|
1957
|
+
jobId: z14.string().describe("ID do job a ser conclu\xEDdo"),
|
|
1958
|
+
result: z14.any().describe("JSON com os dados estruturados exigidos pelo front-end (ex: ParseAndDiagnoseResult, InterviewPlan, ProfileAnalysis)"),
|
|
1959
|
+
status: z14.enum(["completed", "failed"]).default("completed").describe("Status final do job"),
|
|
1960
|
+
error: z14.string().optional().describe('Mensagem de erro caso o status seja "failed"')
|
|
1482
1961
|
});
|
|
1483
1962
|
async function handleSubmitJobResult(input) {
|
|
1484
1963
|
try {
|
|
@@ -1538,13 +2017,13 @@ O front-end em \`localhost:5173\` acabou de receber a resposta formatada e atual
|
|
|
1538
2017
|
}
|
|
1539
2018
|
|
|
1540
2019
|
// src/tools/watch-linkegringo.ts
|
|
1541
|
-
import { z as
|
|
2020
|
+
import { z as z15 } from "zod";
|
|
1542
2021
|
import http from "http";
|
|
1543
|
-
var watchLinkeGringoInputSchema =
|
|
1544
|
-
timeoutSeconds:
|
|
2022
|
+
var watchLinkeGringoInputSchema = z15.object({
|
|
2023
|
+
timeoutSeconds: z15.number().optional().describe(
|
|
1545
2024
|
"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
2025
|
),
|
|
1547
|
-
bridgeUrl:
|
|
2026
|
+
bridgeUrl: z15.string().optional().describe("URL base do bridge HTTP local do LinkeGringo (padr\xE3o: http://127.0.0.1:5174)")
|
|
1548
2027
|
});
|
|
1549
2028
|
async function waitForNextJob(bridgeUrl, timeoutMs) {
|
|
1550
2029
|
const localPending = bridgeJobStore.getPendingJob();
|
|
@@ -1675,129 +2154,551 @@ A interface web est\xE1 conectada ao Bridge. Assim que o usu\xE1rio clicar em **
|
|
|
1675
2154
|
return formatJobResponse(job);
|
|
1676
2155
|
}
|
|
1677
2156
|
|
|
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."*
|
|
2157
|
+
// src/tools/list-jobs.ts
|
|
2158
|
+
import { z as z16 } from "zod";
|
|
2159
|
+
var listJobsInputSchema = z16.object({
|
|
2160
|
+
status: jobStatusSchema.optional().describe("Filtra jobs pelo status atual (ex: waiting_for_agent, running, completed)"),
|
|
2161
|
+
type: jobTypeSchema.optional().describe("Filtra jobs pelo tipo (ex: diagnostic, interview, rewrite)")
|
|
2162
|
+
});
|
|
2163
|
+
async function handleListJobs(input = {}) {
|
|
2164
|
+
try {
|
|
2165
|
+
const jobs = await listRemoteJobs(input);
|
|
2166
|
+
return {
|
|
2167
|
+
content: [
|
|
2168
|
+
{
|
|
2169
|
+
type: "text",
|
|
2170
|
+
text: `Encontrados ${jobs.length} job(s)${input.status ? ` com status "${input.status}"` : ""}.`
|
|
2171
|
+
}
|
|
2172
|
+
],
|
|
2173
|
+
structuredData: {
|
|
2174
|
+
total: jobs.length,
|
|
2175
|
+
jobs: jobs.map((j) => ({
|
|
2176
|
+
id: j.id,
|
|
2177
|
+
type: j.type,
|
|
2178
|
+
status: j.status,
|
|
2179
|
+
targetRole: j.targetRole,
|
|
2180
|
+
fileHandleId: j.fileHandleId,
|
|
2181
|
+
createdAt: j.createdAt,
|
|
2182
|
+
leaseExpiresAt: j.leaseExpiresAt
|
|
2183
|
+
}))
|
|
2184
|
+
}
|
|
2185
|
+
};
|
|
2186
|
+
} catch (err) {
|
|
2187
|
+
return {
|
|
2188
|
+
isError: true,
|
|
2189
|
+
content: [
|
|
2190
|
+
{
|
|
2191
|
+
type: "text",
|
|
2192
|
+
text: `Erro ao listar jobs: ${err.message}`
|
|
2193
|
+
}
|
|
2194
|
+
]
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
1778
2198
|
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
2199
|
+
// src/tools/claim-job.ts
|
|
2200
|
+
import { z as z17 } from "zod";
|
|
2201
|
+
var claimJobInputSchema = z17.object({
|
|
2202
|
+
jobId: z17.string().min(1).describe("ID \xFAnico do job a ser assumido (ex: job_123)"),
|
|
2203
|
+
agent: agentIdentitySchema.describe("Identidade do agente assumindo o job (agentId e sessionId)"),
|
|
2204
|
+
leaseDurationMs: z17.number().int().positive().optional().describe("Dura\xE7\xE3o do lease exclusivo em milissegundos (padr\xE3o: 60000ms / 60s)")
|
|
2205
|
+
});
|
|
2206
|
+
async function handleClaimJob(input) {
|
|
2207
|
+
try {
|
|
2208
|
+
const job = await claimRemoteJob(input.jobId, input.agent, input.leaseDurationMs);
|
|
2209
|
+
return {
|
|
2210
|
+
content: [
|
|
2211
|
+
{
|
|
2212
|
+
type: "text",
|
|
2213
|
+
text: `Job ${job.id} assumido com sucesso pelo agente ${input.agent.agentId}. Status atual: ${job.status}. Lease expira em: ${job.leaseExpiresAt}.`
|
|
2214
|
+
}
|
|
2215
|
+
],
|
|
2216
|
+
structuredData: {
|
|
2217
|
+
success: true,
|
|
2218
|
+
job
|
|
2219
|
+
}
|
|
2220
|
+
};
|
|
2221
|
+
} catch (err) {
|
|
2222
|
+
return {
|
|
2223
|
+
isError: true,
|
|
2224
|
+
content: [
|
|
2225
|
+
{
|
|
2226
|
+
type: "text",
|
|
2227
|
+
text: `Falha ao dar claim no job ${input.jobId}: ${err.message}`
|
|
2228
|
+
}
|
|
2229
|
+
]
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
1789
2232
|
}
|
|
1790
2233
|
|
|
1791
|
-
// src/
|
|
1792
|
-
import
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
2234
|
+
// src/tools/report-progress.ts
|
|
2235
|
+
import { z as z18 } from "zod";
|
|
2236
|
+
var reportProgressInputSchema = z18.object({
|
|
2237
|
+
jobId: z18.string().min(1).describe("ID do job cujo progresso est\xE1 sendo atualizado"),
|
|
2238
|
+
agent: agentIdentitySchema.describe("Identidade do agente atual com lease no job"),
|
|
2239
|
+
phase: jobProgressPhaseSchema.optional().describe("Fase atual do trabalho (ex: loading_document, extracting_facts, diagnosing, writing_result)"),
|
|
2240
|
+
progress: z18.number().min(0).max(100).optional().describe("Porcentagem de progresso num\xE9rico de 0 a 100"),
|
|
2241
|
+
message: z18.string().optional().describe("Mensagem descritiva e concisa exibida para o usu\xE1rio na interface web"),
|
|
2242
|
+
leaseDurationMs: z18.number().int().positive().optional().describe("Tempo adicional de renova\xE7\xE3o do lease em ms (padr\xE3o: 60000ms)")
|
|
2243
|
+
});
|
|
2244
|
+
async function handleReportProgress(input) {
|
|
2245
|
+
try {
|
|
2246
|
+
const job = await reportRemoteProgress(input.jobId, input.agent, {
|
|
2247
|
+
phase: input.phase,
|
|
2248
|
+
progress: input.progress,
|
|
2249
|
+
message: input.message,
|
|
2250
|
+
leaseDurationMs: input.leaseDurationMs
|
|
2251
|
+
});
|
|
2252
|
+
return {
|
|
2253
|
+
content: [
|
|
2254
|
+
{
|
|
2255
|
+
type: "text",
|
|
2256
|
+
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}.`
|
|
2257
|
+
}
|
|
2258
|
+
],
|
|
2259
|
+
structuredData: {
|
|
2260
|
+
success: true,
|
|
2261
|
+
jobId: job.id,
|
|
2262
|
+
progress: job.progress,
|
|
2263
|
+
phase: job.currentPhase ?? input.phase,
|
|
2264
|
+
leaseExpiresAt: job.leaseExpiresAt
|
|
2265
|
+
}
|
|
2266
|
+
};
|
|
2267
|
+
} catch (err) {
|
|
2268
|
+
return {
|
|
2269
|
+
isError: true,
|
|
2270
|
+
content: [
|
|
2271
|
+
{
|
|
2272
|
+
type: "text",
|
|
2273
|
+
text: `Falha ao reportar progresso no job ${input.jobId}: ${err.message}`
|
|
2274
|
+
}
|
|
2275
|
+
]
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
// src/tools/inspect-document.ts
|
|
2281
|
+
import { z as z19 } from "zod";
|
|
2282
|
+
var inspectDocumentInputSchema = z19.object({
|
|
2283
|
+
fileId: z19.string().min(1).describe("Capability token opaco do documento registrado (ex: doc_123)")
|
|
2284
|
+
});
|
|
2285
|
+
async function handleInspectDocument(input) {
|
|
2286
|
+
try {
|
|
2287
|
+
const document = await inspectRemoteDocument(input.fileId);
|
|
2288
|
+
return {
|
|
2289
|
+
content: [
|
|
2290
|
+
{
|
|
2291
|
+
type: "text",
|
|
2292
|
+
text: `Documento inspecionado com sucesso: "${document.name}" (${(document.size / 1024).toFixed(1)} KB, ${document.mediaType}). Modo de acesso: ${document.access}. Origem: ${document.root}.`
|
|
2293
|
+
}
|
|
2294
|
+
],
|
|
2295
|
+
structuredData: {
|
|
2296
|
+
fileId: document.fileId,
|
|
2297
|
+
jobId: document.jobId,
|
|
2298
|
+
name: document.name,
|
|
2299
|
+
mediaType: document.mediaType,
|
|
2300
|
+
size: document.size,
|
|
2301
|
+
mtime: document.mtime,
|
|
2302
|
+
root: document.root,
|
|
2303
|
+
access: document.access,
|
|
2304
|
+
expiresAt: document.expiresAt
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2307
|
+
} catch (err) {
|
|
2308
|
+
return {
|
|
2309
|
+
isError: true,
|
|
2310
|
+
content: [
|
|
2311
|
+
{
|
|
2312
|
+
type: "text",
|
|
2313
|
+
text: `Falha ao inspecionar documento ${input.fileId}: ${err.message}`
|
|
2314
|
+
}
|
|
2315
|
+
]
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
// src/tools/request-user-action.ts
|
|
2321
|
+
import { z as z20 } from "zod";
|
|
2322
|
+
var requestUserActionInputSchema = z20.object({
|
|
2323
|
+
jobId: z20.string().min(1).describe("ID do job que requer interven\xE7\xE3o humana"),
|
|
2324
|
+
agent: agentIdentitySchema.describe("Identidade do agente"),
|
|
2325
|
+
reason: z20.string().min(1).describe('Motivo da pausa (ex: "needs_fact_confirmation", "unclear_company_name")'),
|
|
2326
|
+
prompt: z20.string().optional().describe("Instru\xE7\xE3o ou pergunta clara exibida para o usu\xE1rio na interface web")
|
|
2327
|
+
});
|
|
2328
|
+
async function handleRequestUserAction(input) {
|
|
2329
|
+
try {
|
|
2330
|
+
const job = await requestRemoteUserAction(input.jobId, input.agent, {
|
|
2331
|
+
reason: input.reason,
|
|
2332
|
+
prompt: input.prompt
|
|
2333
|
+
});
|
|
2334
|
+
return {
|
|
2335
|
+
content: [
|
|
2336
|
+
{
|
|
2337
|
+
type: "text",
|
|
2338
|
+
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.`
|
|
2339
|
+
}
|
|
2340
|
+
],
|
|
2341
|
+
structuredData: {
|
|
2342
|
+
success: true,
|
|
2343
|
+
jobId: job.id,
|
|
2344
|
+
status: job.status,
|
|
2345
|
+
reason: input.reason
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
} catch (err) {
|
|
2349
|
+
return {
|
|
2350
|
+
isError: true,
|
|
2351
|
+
content: [
|
|
2352
|
+
{
|
|
2353
|
+
type: "text",
|
|
2354
|
+
text: `Falha ao solicitar interven\xE7\xE3o do usu\xE1rio no job ${input.jobId}: ${err.message}`
|
|
2355
|
+
}
|
|
2356
|
+
]
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
// src/tools/submit-diagnostic.ts
|
|
2362
|
+
import { z as z21 } from "zod";
|
|
2363
|
+
var submitDiagnosticInputSchema = z21.object({
|
|
2364
|
+
jobId: z21.string().min(1).describe("ID do job de diagn\xF3stico a ser finalizado"),
|
|
2365
|
+
agent: agentIdentitySchema.describe("Identidade do agente"),
|
|
2366
|
+
submissionId: z21.string().optional().describe("ID de submiss\xE3o para deduplica\xE7\xE3o idempotente (opcional)"),
|
|
2367
|
+
payload: diagnosticResultPayloadSchema.describe(
|
|
2368
|
+
"Resultado estruturado com Profile extra\xEDdo e ProfileReview detalhado"
|
|
2369
|
+
)
|
|
2370
|
+
});
|
|
2371
|
+
async function handleSubmitDiagnostic(input) {
|
|
2372
|
+
try {
|
|
2373
|
+
const submissionId = input.submissionId || `sub_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2374
|
+
const { job, resultId } = await submitRemoteJobResult(input.jobId, input.agent, {
|
|
2375
|
+
submissionId,
|
|
2376
|
+
schemaVersion: "1.0",
|
|
2377
|
+
payload: input.payload
|
|
2378
|
+
});
|
|
2379
|
+
const score = input.payload.review.overallScore;
|
|
2380
|
+
return {
|
|
2381
|
+
content: [
|
|
2382
|
+
{
|
|
2383
|
+
type: "text",
|
|
2384
|
+
text: `Diagn\xF3stico do job ${job.id} submetido com sucesso! Result ID: ${resultId}. Score Geral: ${score}/100. A interface web foi atualizada instantaneamente.`
|
|
2385
|
+
}
|
|
2386
|
+
],
|
|
2387
|
+
structuredData: {
|
|
2388
|
+
success: true,
|
|
2389
|
+
jobId: job.id,
|
|
2390
|
+
resultId,
|
|
2391
|
+
score
|
|
2392
|
+
}
|
|
2393
|
+
};
|
|
2394
|
+
} catch (err) {
|
|
2395
|
+
return {
|
|
2396
|
+
isError: true,
|
|
2397
|
+
content: [
|
|
2398
|
+
{
|
|
2399
|
+
type: "text",
|
|
2400
|
+
text: `Falha ao submeter diagn\xF3stico para o job ${input.jobId}: ${err.message}`
|
|
2401
|
+
}
|
|
2402
|
+
]
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
// src/tools/submit-interview.ts
|
|
2408
|
+
import { z as z22 } from "zod";
|
|
2409
|
+
var submitInterviewInputSchema = z22.object({
|
|
2410
|
+
jobId: z22.string().min(1).describe("ID do job de entrevista a ser finalizado"),
|
|
2411
|
+
agent: agentIdentitySchema.describe("Identidade do agente"),
|
|
2412
|
+
submissionId: z22.string().optional().describe("ID de submiss\xE3o para deduplica\xE7\xE3o idempotente (opcional)"),
|
|
2413
|
+
payload: interviewResultPayloadSchema.describe(
|
|
2414
|
+
"Plano de entrevista estruturado contendo as perguntas estrat\xE9gicas para destravar o perfil"
|
|
2415
|
+
)
|
|
2416
|
+
});
|
|
2417
|
+
async function handleSubmitInterview(input) {
|
|
2418
|
+
try {
|
|
2419
|
+
const submissionId = input.submissionId || `sub_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2420
|
+
const { job, resultId } = await submitRemoteJobResult(input.jobId, input.agent, {
|
|
2421
|
+
submissionId,
|
|
2422
|
+
schemaVersion: "1.0",
|
|
2423
|
+
payload: input.payload
|
|
2424
|
+
});
|
|
2425
|
+
const questionCount = input.payload.interviewPlan.questions?.length ?? 0;
|
|
2426
|
+
return {
|
|
2427
|
+
content: [
|
|
2428
|
+
{
|
|
2429
|
+
type: "text",
|
|
2430
|
+
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.`
|
|
2431
|
+
}
|
|
2432
|
+
],
|
|
2433
|
+
structuredData: {
|
|
2434
|
+
success: true,
|
|
2435
|
+
jobId: job.id,
|
|
2436
|
+
resultId,
|
|
2437
|
+
questionCount
|
|
2438
|
+
}
|
|
2439
|
+
};
|
|
2440
|
+
} catch (err) {
|
|
2441
|
+
return {
|
|
2442
|
+
isError: true,
|
|
2443
|
+
content: [
|
|
2444
|
+
{
|
|
2445
|
+
type: "text",
|
|
2446
|
+
text: `Falha ao submeter plano de entrevista para o job ${input.jobId}: ${err.message}`
|
|
2447
|
+
}
|
|
2448
|
+
]
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
// src/tools/submit-rewrite.ts
|
|
2454
|
+
import { z as z23 } from "zod";
|
|
2455
|
+
var submitRewriteInputSchema = z23.object({
|
|
2456
|
+
jobId: z23.string().min(1).describe("ID do job de reescrita a ser finalizado"),
|
|
2457
|
+
agent: agentIdentitySchema.describe("Identidade do agente"),
|
|
2458
|
+
submissionId: z23.string().optional().describe("ID de submiss\xE3o para deduplica\xE7\xE3o idempotente (opcional)"),
|
|
2459
|
+
payload: rewriteResultPayloadSchema.describe(
|
|
2460
|
+
"An\xE1lise completa e perfil reescrito (ProfileAnalysis) contendo headlines, about, experi\xEAncias expandidas em Google XYZ e compara\xE7\xE3o de notas"
|
|
2461
|
+
)
|
|
2462
|
+
});
|
|
2463
|
+
async function handleSubmitRewrite(input) {
|
|
2464
|
+
try {
|
|
2465
|
+
const submissionId = input.submissionId || `sub_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2466
|
+
const { job, resultId } = await submitRemoteJobResult(input.jobId, input.agent, {
|
|
2467
|
+
submissionId,
|
|
2468
|
+
schemaVersion: "1.0",
|
|
2469
|
+
payload: input.payload
|
|
2470
|
+
});
|
|
2471
|
+
const finalScore = input.payload.profileAnalysis.overallScore;
|
|
2472
|
+
return {
|
|
2473
|
+
content: [
|
|
2474
|
+
{
|
|
2475
|
+
type: "text",
|
|
2476
|
+
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.`
|
|
2477
|
+
}
|
|
2478
|
+
],
|
|
2479
|
+
structuredData: {
|
|
2480
|
+
success: true,
|
|
2481
|
+
jobId: job.id,
|
|
2482
|
+
resultId,
|
|
2483
|
+
finalScore
|
|
2484
|
+
}
|
|
2485
|
+
};
|
|
2486
|
+
} catch (err) {
|
|
2487
|
+
return {
|
|
2488
|
+
isError: true,
|
|
2489
|
+
content: [
|
|
2490
|
+
{
|
|
2491
|
+
type: "text",
|
|
2492
|
+
text: `Falha ao submeter perfil reescrito para o job ${input.jobId}: ${err.message}`
|
|
2493
|
+
}
|
|
2494
|
+
]
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
// src/server.ts
|
|
2500
|
+
function createLinkeGringoMcpServer() {
|
|
2501
|
+
const server = new McpServer({
|
|
2502
|
+
name: "linkegringo-mcp",
|
|
2503
|
+
version: "1.0.0"
|
|
2504
|
+
});
|
|
2505
|
+
server.registerTool(
|
|
2506
|
+
"list_jobs",
|
|
2507
|
+
{
|
|
2508
|
+
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).",
|
|
2509
|
+
inputSchema: listJobsInputSchema.shape
|
|
2510
|
+
},
|
|
2511
|
+
async (args) => {
|
|
2512
|
+
return await handleListJobs(args);
|
|
2513
|
+
}
|
|
2514
|
+
);
|
|
2515
|
+
server.registerTool(
|
|
2516
|
+
"claim_job",
|
|
2517
|
+
{
|
|
2518
|
+
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.",
|
|
2519
|
+
inputSchema: claimJobInputSchema.shape
|
|
2520
|
+
},
|
|
2521
|
+
async (args) => {
|
|
2522
|
+
return await handleClaimJob(args);
|
|
2523
|
+
}
|
|
2524
|
+
);
|
|
2525
|
+
server.registerTool(
|
|
2526
|
+
"report_progress",
|
|
2527
|
+
{
|
|
2528
|
+
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.",
|
|
2529
|
+
inputSchema: reportProgressInputSchema.shape
|
|
2530
|
+
},
|
|
2531
|
+
async (args) => {
|
|
2532
|
+
return await handleReportProgress(args);
|
|
2533
|
+
}
|
|
2534
|
+
);
|
|
2535
|
+
server.registerTool(
|
|
2536
|
+
"inspect_document",
|
|
2537
|
+
{
|
|
2538
|
+
description: "Consulta os metadados do documento PDF cadastrado via capability token (fileId). N\xE3o exp\xF5e paths locais arbitr\xE1rios do sistema de arquivos.",
|
|
2539
|
+
inputSchema: inspectDocumentInputSchema.shape
|
|
2540
|
+
},
|
|
2541
|
+
async (args) => {
|
|
2542
|
+
return await handleInspectDocument(args);
|
|
2543
|
+
}
|
|
2544
|
+
);
|
|
2545
|
+
server.registerTool(
|
|
2546
|
+
"request_user_action",
|
|
2547
|
+
{
|
|
2548
|
+
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.",
|
|
2549
|
+
inputSchema: requestUserActionInputSchema.shape
|
|
2550
|
+
},
|
|
2551
|
+
async (args) => {
|
|
2552
|
+
return await handleRequestUserAction(args);
|
|
2553
|
+
}
|
|
2554
|
+
);
|
|
2555
|
+
server.registerTool(
|
|
2556
|
+
"submit_diagnostic",
|
|
2557
|
+
{
|
|
2558
|
+
description: "Submete o resultado final de um job de diagn\xF3stico estruturado com Profile e ProfileReview validados com Zod estrito.",
|
|
2559
|
+
inputSchema: submitDiagnosticInputSchema.shape
|
|
2560
|
+
},
|
|
2561
|
+
async (args) => {
|
|
2562
|
+
return await handleSubmitDiagnostic(args);
|
|
2563
|
+
}
|
|
2564
|
+
);
|
|
2565
|
+
server.registerTool(
|
|
2566
|
+
"submit_interview",
|
|
2567
|
+
{
|
|
2568
|
+
description: "Submete o resultado final de um job de entrevista com o InterviewPlan estruturado contendo perguntas estrat\xE9gicas.",
|
|
2569
|
+
inputSchema: submitInterviewInputSchema.shape
|
|
2570
|
+
},
|
|
2571
|
+
async (args) => {
|
|
2572
|
+
return await handleSubmitInterview(args);
|
|
2573
|
+
}
|
|
2574
|
+
);
|
|
2575
|
+
server.registerTool(
|
|
2576
|
+
"submit_rewrite",
|
|
2577
|
+
{
|
|
2578
|
+
description: "Submete o resultado final de um job de reescrita contendo ProfileAnalysis com Headlines calibradas, About e bullets Google XYZ.",
|
|
2579
|
+
inputSchema: submitRewriteInputSchema.shape
|
|
2580
|
+
},
|
|
2581
|
+
async (args) => {
|
|
2582
|
+
return await handleSubmitRewrite(args);
|
|
2583
|
+
}
|
|
2584
|
+
);
|
|
2585
|
+
server.registerTool(
|
|
2586
|
+
"audit_profile",
|
|
2587
|
+
{
|
|
2588
|
+
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.",
|
|
2589
|
+
inputSchema: auditProfileInputSchema.shape
|
|
2590
|
+
},
|
|
2591
|
+
async (args) => {
|
|
2592
|
+
return await handleAuditProfile(args);
|
|
2593
|
+
}
|
|
2594
|
+
);
|
|
2595
|
+
server.registerTool(
|
|
2596
|
+
"simulate_recruiter_search",
|
|
2597
|
+
{
|
|
2598
|
+
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.",
|
|
2599
|
+
inputSchema: simulateRecruiterSearchInputSchema.shape
|
|
2600
|
+
},
|
|
2601
|
+
async (args) => {
|
|
2602
|
+
return await handleSimulateRecruiterSearch(args);
|
|
2603
|
+
}
|
|
2604
|
+
);
|
|
2605
|
+
server.registerTool(
|
|
2606
|
+
"convert_to_xyz_bullet",
|
|
2607
|
+
{
|
|
2608
|
+
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].",
|
|
2609
|
+
inputSchema: convertToXyzBulletInputSchema.shape
|
|
2610
|
+
},
|
|
2611
|
+
async (args) => {
|
|
2612
|
+
return await handleConvertToXyzBullet(args);
|
|
2613
|
+
}
|
|
2614
|
+
);
|
|
2615
|
+
server.registerTool(
|
|
2616
|
+
"generate_headline_proposals",
|
|
2617
|
+
{
|
|
2618
|
+
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.",
|
|
2619
|
+
inputSchema: generateHeadlineInputSchema.shape
|
|
2620
|
+
},
|
|
2621
|
+
async (args) => {
|
|
2622
|
+
return await handleGenerateHeadline(args);
|
|
2623
|
+
}
|
|
2624
|
+
);
|
|
2625
|
+
server.registerTool(
|
|
2626
|
+
"get_pending_job",
|
|
2627
|
+
{
|
|
2628
|
+
description: "Obt\xE9m os dados completos de um job pelo ID (ou o pr\xF3ximo da fila). Suporta retrocompatibilidade.",
|
|
2629
|
+
inputSchema: getPendingJobInputSchema.shape
|
|
2630
|
+
},
|
|
2631
|
+
async (args) => {
|
|
2632
|
+
return await handleGetPendingJob(args);
|
|
2633
|
+
}
|
|
2634
|
+
);
|
|
2635
|
+
server.registerTool(
|
|
2636
|
+
"submit_job_result",
|
|
2637
|
+
{
|
|
2638
|
+
description: "Envia o resultado gen\xE9rico de um job de volta para o navegador do LinkeGringo (retrocompatibilidade).",
|
|
2639
|
+
inputSchema: submitJobResultInputSchema.shape
|
|
2640
|
+
},
|
|
2641
|
+
async (args) => {
|
|
2642
|
+
return await handleSubmitJobResult(args);
|
|
2643
|
+
}
|
|
2644
|
+
);
|
|
2645
|
+
server.registerTool(
|
|
2646
|
+
"watch_linkegringo",
|
|
2647
|
+
{
|
|
2648
|
+
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.",
|
|
2649
|
+
inputSchema: watchLinkeGringoInputSchema.shape
|
|
2650
|
+
},
|
|
2651
|
+
async (args) => {
|
|
2652
|
+
return await handleWatchLinkeGringo(args);
|
|
2653
|
+
}
|
|
2654
|
+
);
|
|
2655
|
+
server.registerResource(
|
|
2656
|
+
"guidelines",
|
|
2657
|
+
"linkegringo://guidelines",
|
|
2658
|
+
{
|
|
2659
|
+
title: "Diretrizes Oficiais do LinkeGringo para Vagas nos EUA",
|
|
2660
|
+
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.",
|
|
2661
|
+
mimeType: "text/markdown"
|
|
2662
|
+
},
|
|
2663
|
+
async () => {
|
|
2664
|
+
return {
|
|
2665
|
+
contents: [
|
|
2666
|
+
{
|
|
2667
|
+
uri: "linkegringo://guidelines",
|
|
2668
|
+
text: `
|
|
2669
|
+
# Diretrizes Oficiais LinkeGringo: Otimiza\xE7\xE3o de Perfil para Recrutadores dos EUA
|
|
2670
|
+
|
|
2671
|
+
1. **Headline $le$ 160 caracteres**:
|
|
2672
|
+
- Formato recomendado: \`[Cargo Espec\xEDfico] | [3-4 Tecnologias Core] | [Escala/Dom\xEDnio] | US Remote\`
|
|
2673
|
+
- Evite slogans vagos ("Apaixonado por tecnologia", "Resolvendo problemas complexos").
|
|
2674
|
+
- Headline tem peso 3x no algoritmo de busca do LinkedIn Recruiter.
|
|
2675
|
+
|
|
2676
|
+
2. **F\xF3rmula Google XYZ para Experi\xEAncias**:
|
|
2677
|
+
- Toda conquista deve responder: *"Accomplished [X], measured by [Y], by doing [Z]"*.
|
|
2678
|
+
- Exemplo: *"Architected distributed event-driven payment service in Go, reducing p99 latency by 42% and scaling to 15,000 requests/sec."*
|
|
2679
|
+
|
|
2680
|
+
3. **Incentivo a Inbound (Ser Descoberto)**:
|
|
2681
|
+
- 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.
|
|
2682
|
+
- Elimine red flags de localiza\xE7\xE3o restrita e declare disponibilidade para contratos internacionais (W-8BEN / PJ Internacional / B2B).
|
|
2683
|
+
`.trim()
|
|
2684
|
+
}
|
|
2685
|
+
]
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
);
|
|
2689
|
+
return server;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
// src/cli/installer.ts
|
|
2693
|
+
import fs from "fs";
|
|
2694
|
+
import path from "path";
|
|
2695
|
+
import os from "os";
|
|
2696
|
+
function getMcpConfigsForSystem() {
|
|
2697
|
+
const home = os.homedir();
|
|
2698
|
+
const platform = os.platform();
|
|
2699
|
+
const configs = [];
|
|
2700
|
+
const antigravityPath = path.join(home, ".gemini", "config", "mcp_config.json");
|
|
2701
|
+
configs.push({
|
|
1801
2702
|
id: "antigravity",
|
|
1802
2703
|
client: "Google Antigravity",
|
|
1803
2704
|
configPath: antigravityPath,
|
|
@@ -1916,63 +2817,877 @@ function runInstaller(args = process.argv) {
|
|
|
1916
2817
|
console.warn(`\u26A0\uFE0F [Workspace Local] Erro ao configurar: ${err.message}
|
|
1917
2818
|
`);
|
|
1918
2819
|
}
|
|
1919
|
-
return results;
|
|
2820
|
+
return results;
|
|
2821
|
+
}
|
|
2822
|
+
const detectedTargets = targets.filter((t) => t.detected);
|
|
2823
|
+
const shouldInstallAll = options.all || detectedTargets.length === 0;
|
|
2824
|
+
for (const target of targets) {
|
|
2825
|
+
if (options.client && !target.id.includes(options.client)) {
|
|
2826
|
+
continue;
|
|
2827
|
+
}
|
|
2828
|
+
if (!shouldInstallAll && !target.detected) {
|
|
2829
|
+
results.push({
|
|
2830
|
+
client: target.client,
|
|
2831
|
+
configPath: target.configPath,
|
|
2832
|
+
status: "skipped",
|
|
2833
|
+
message: "Cliente n\xE3o detectado nesta m\xE1quina (use --all para for\xE7ar)"
|
|
2834
|
+
});
|
|
2835
|
+
console.log(`\u23ED\uFE0F [${target.client}] N\xE3o detectado nesta m\xE1quina (pulado. Use --all para criar)`);
|
|
2836
|
+
continue;
|
|
2837
|
+
}
|
|
2838
|
+
try {
|
|
2839
|
+
const res = installMcpServerConfig(target.configPath);
|
|
2840
|
+
results.push({
|
|
2841
|
+
client: target.client,
|
|
2842
|
+
configPath: target.configPath,
|
|
2843
|
+
status: res.status
|
|
2844
|
+
});
|
|
2845
|
+
const tag = target.detected ? "(Detectado)" : "(Padr\xE3o)";
|
|
2846
|
+
console.log(`\u2705 [${target.client}] ${tag}`);
|
|
2847
|
+
console.log(` Arquivo: ${target.configPath} (${res.status === "created" ? "Criado" : "Atualizado"})
|
|
2848
|
+
`);
|
|
2849
|
+
} catch (err) {
|
|
2850
|
+
results.push({
|
|
2851
|
+
client: target.client,
|
|
2852
|
+
configPath: target.configPath,
|
|
2853
|
+
status: "error",
|
|
2854
|
+
message: err.message
|
|
2855
|
+
});
|
|
2856
|
+
console.warn(`\u26A0\uFE0F [${target.client}] N\xE3o foi poss\xEDvel atualizar: ${err.message}
|
|
2857
|
+
`);
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
console.log("---");
|
|
2861
|
+
console.log("\u{1F4A1} Comandos One-Line diretos para agentes de linha de comando (CLI):");
|
|
2862
|
+
console.log(" \u2022 Antigravity CLI: agy mcp add linkegringo npx -y @linkegringo/mcp");
|
|
2863
|
+
console.log(" \u2022 Codex CLI: codex mcp add linkegringo -- npx -y @linkegringo/mcp");
|
|
2864
|
+
console.log(" \u2022 Claude Code CLI: claude mcp add linkegringo npx -y @linkegringo/mcp");
|
|
2865
|
+
console.log(' \u2022 Goose CLI: goose configure --add-extension "npx -y @linkegringo/mcp"');
|
|
2866
|
+
console.log("================================================");
|
|
2867
|
+
console.log("\u{1F389} Instala\xE7\xE3o conclu\xEDda! Reinicie o seu cliente de IA para ativar.\n");
|
|
2868
|
+
return results;
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
// src/bridge/server.ts
|
|
2872
|
+
import http2 from "http";
|
|
2873
|
+
|
|
2874
|
+
// src/bridge/db.ts
|
|
2875
|
+
import { createRequire } from "module";
|
|
2876
|
+
import path2 from "path";
|
|
2877
|
+
import fs2 from "fs";
|
|
2878
|
+
import os2 from "os";
|
|
2879
|
+
var require2 = createRequire(import.meta.url);
|
|
2880
|
+
var { DatabaseSync } = require2("node:sqlite");
|
|
2881
|
+
function getDatabasePath(customPath) {
|
|
2882
|
+
if (customPath) return customPath;
|
|
2883
|
+
const dir = path2.join(os2.homedir(), ".linkegringo");
|
|
2884
|
+
if (!fs2.existsSync(dir)) {
|
|
2885
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
2886
|
+
}
|
|
2887
|
+
return path2.join(dir, "linkegringo.db");
|
|
2888
|
+
}
|
|
2889
|
+
function initDatabase(db) {
|
|
2890
|
+
try {
|
|
2891
|
+
db.exec("PRAGMA foreign_keys = ON;");
|
|
2892
|
+
db.exec("PRAGMA synchronous = NORMAL;");
|
|
2893
|
+
} catch {
|
|
2894
|
+
}
|
|
2895
|
+
db.exec(`
|
|
2896
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
2897
|
+
id TEXT PRIMARY KEY,
|
|
2898
|
+
type TEXT NOT NULL,
|
|
2899
|
+
status TEXT NOT NULL,
|
|
2900
|
+
attempt INTEGER NOT NULL DEFAULT 0,
|
|
2901
|
+
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
2902
|
+
claimed_by TEXT,
|
|
2903
|
+
lease_expires_at TEXT,
|
|
2904
|
+
expires_at TEXT NOT NULL,
|
|
2905
|
+
progress INTEGER NOT NULL DEFAULT 0,
|
|
2906
|
+
current_phase TEXT,
|
|
2907
|
+
progress_message TEXT,
|
|
2908
|
+
result_id TEXT,
|
|
2909
|
+
file_handle_id TEXT,
|
|
2910
|
+
target_role TEXT,
|
|
2911
|
+
created_at TEXT NOT NULL,
|
|
2912
|
+
updated_at TEXT NOT NULL,
|
|
2913
|
+
metadata TEXT
|
|
2914
|
+
);
|
|
2915
|
+
|
|
2916
|
+
CREATE TABLE IF NOT EXISTS job_events (
|
|
2917
|
+
id TEXT PRIMARY KEY,
|
|
2918
|
+
job_id TEXT NOT NULL,
|
|
2919
|
+
command_id TEXT,
|
|
2920
|
+
type TEXT NOT NULL,
|
|
2921
|
+
payload TEXT,
|
|
2922
|
+
created_at TEXT NOT NULL,
|
|
2923
|
+
FOREIGN KEY(job_id) REFERENCES jobs(id) ON DELETE CASCADE
|
|
2924
|
+
);
|
|
2925
|
+
|
|
2926
|
+
CREATE TABLE IF NOT EXISTS commands (
|
|
2927
|
+
command_id TEXT PRIMARY KEY,
|
|
2928
|
+
job_id TEXT NOT NULL,
|
|
2929
|
+
type TEXT NOT NULL,
|
|
2930
|
+
result TEXT,
|
|
2931
|
+
created_at TEXT NOT NULL
|
|
2932
|
+
);
|
|
2933
|
+
|
|
2934
|
+
CREATE TABLE IF NOT EXISTS job_results (
|
|
2935
|
+
id TEXT PRIMARY KEY,
|
|
2936
|
+
job_id TEXT NOT NULL,
|
|
2937
|
+
schema_version TEXT NOT NULL,
|
|
2938
|
+
submission_id TEXT NOT NULL UNIQUE,
|
|
2939
|
+
submitted_by TEXT NOT NULL,
|
|
2940
|
+
payload TEXT NOT NULL,
|
|
2941
|
+
created_at TEXT NOT NULL,
|
|
2942
|
+
FOREIGN KEY(job_id) REFERENCES jobs(id) ON DELETE CASCADE
|
|
2943
|
+
);
|
|
2944
|
+
|
|
2945
|
+
CREATE TABLE IF NOT EXISTS file_handles (
|
|
2946
|
+
id TEXT PRIMARY KEY,
|
|
2947
|
+
job_id TEXT NOT NULL,
|
|
2948
|
+
name TEXT NOT NULL,
|
|
2949
|
+
media_type TEXT NOT NULL,
|
|
2950
|
+
size INTEGER NOT NULL,
|
|
2951
|
+
mtime INTEGER NOT NULL,
|
|
2952
|
+
root TEXT NOT NULL,
|
|
2953
|
+
path TEXT NOT NULL,
|
|
2954
|
+
expires_at TEXT NOT NULL,
|
|
2955
|
+
created_at TEXT NOT NULL
|
|
2956
|
+
);
|
|
2957
|
+
|
|
2958
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
2959
|
+
agent_id TEXT PRIMARY KEY,
|
|
2960
|
+
session_id TEXT NOT NULL,
|
|
2961
|
+
client_name TEXT,
|
|
2962
|
+
client_version TEXT,
|
|
2963
|
+
capabilities TEXT,
|
|
2964
|
+
last_seen_at INTEGER NOT NULL,
|
|
2965
|
+
created_at INTEGER NOT NULL
|
|
2966
|
+
);
|
|
2967
|
+
|
|
2968
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
|
|
2969
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_status_lease ON jobs(status, lease_expires_at);
|
|
2970
|
+
CREATE INDEX IF NOT EXISTS idx_job_events_job_id ON job_events(job_id);
|
|
2971
|
+
CREATE INDEX IF NOT EXISTS idx_commands_job_id ON commands(job_id);
|
|
2972
|
+
CREATE INDEX IF NOT EXISTS idx_file_handles_job_id ON file_handles(job_id);
|
|
2973
|
+
CREATE INDEX IF NOT EXISTS idx_agents_last_seen ON agents(last_seen_at);
|
|
2974
|
+
`);
|
|
2975
|
+
}
|
|
2976
|
+
function createDatabase(dbPath) {
|
|
2977
|
+
const targetPath = dbPath || getDatabasePath();
|
|
2978
|
+
const db = new DatabaseSync(targetPath);
|
|
2979
|
+
initDatabase(db);
|
|
2980
|
+
return db;
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
// src/bridge/job-store.ts
|
|
2984
|
+
function parseJobRow(row) {
|
|
2985
|
+
return {
|
|
2986
|
+
id: row.id,
|
|
2987
|
+
type: row.type,
|
|
2988
|
+
status: row.status,
|
|
2989
|
+
attempt: row.attempt,
|
|
2990
|
+
maxAttempts: row.max_attempts,
|
|
2991
|
+
claimedBy: row.claimed_by ? JSON.parse(row.claimed_by) : null,
|
|
2992
|
+
leaseExpiresAt: row.lease_expires_at,
|
|
2993
|
+
expiresAt: row.expires_at,
|
|
2994
|
+
progress: row.progress,
|
|
2995
|
+
currentPhase: row.current_phase || null,
|
|
2996
|
+
progressMessage: row.progress_message,
|
|
2997
|
+
resultId: row.result_id,
|
|
2998
|
+
fileHandleId: row.file_handle_id,
|
|
2999
|
+
targetRole: row.target_role,
|
|
3000
|
+
createdAt: row.created_at,
|
|
3001
|
+
updatedAt: row.updated_at,
|
|
3002
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
function parseEventRow(row) {
|
|
3006
|
+
return {
|
|
3007
|
+
id: row.id,
|
|
3008
|
+
jobId: row.job_id,
|
|
3009
|
+
commandId: row.command_id,
|
|
3010
|
+
type: row.type,
|
|
3011
|
+
timestamp: row.created_at,
|
|
3012
|
+
payload: row.payload ? JSON.parse(row.payload) : void 0
|
|
3013
|
+
};
|
|
3014
|
+
}
|
|
3015
|
+
var JobStore = class {
|
|
3016
|
+
constructor(db, onEventRecorded) {
|
|
3017
|
+
this.db = db;
|
|
3018
|
+
this.onEventRecorded = onEventRecorded;
|
|
3019
|
+
}
|
|
3020
|
+
db;
|
|
3021
|
+
onEventRecorded;
|
|
3022
|
+
getRawDb() {
|
|
3023
|
+
return this.db;
|
|
3024
|
+
}
|
|
3025
|
+
// --- Transações e Idempotência ---
|
|
3026
|
+
getCachedCommand(commandId) {
|
|
3027
|
+
if (!commandId) return null;
|
|
3028
|
+
const stmt = this.db.prepare("SELECT result FROM commands WHERE command_id = ?");
|
|
3029
|
+
const row = stmt.get(commandId);
|
|
3030
|
+
if (row && row.result) {
|
|
3031
|
+
return JSON.parse(row.result);
|
|
3032
|
+
}
|
|
3033
|
+
return null;
|
|
3034
|
+
}
|
|
3035
|
+
saveCommand(commandId, jobId, type, result) {
|
|
3036
|
+
if (!commandId) return;
|
|
3037
|
+
const stmt = this.db.prepare(
|
|
3038
|
+
"INSERT OR REPLACE INTO commands (command_id, job_id, type, result, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
3039
|
+
);
|
|
3040
|
+
stmt.run(commandId, jobId, type, JSON.stringify(result), (/* @__PURE__ */ new Date()).toISOString());
|
|
3041
|
+
}
|
|
3042
|
+
// --- Operações de Job ---
|
|
3043
|
+
createJob(input) {
|
|
3044
|
+
if (input.commandId) {
|
|
3045
|
+
const cached = this.getCachedCommand(input.commandId);
|
|
3046
|
+
if (cached) return cached;
|
|
3047
|
+
}
|
|
3048
|
+
const now = /* @__PURE__ */ new Date();
|
|
3049
|
+
const nowIso = now.toISOString();
|
|
3050
|
+
const jobId = input.id || `job_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
3051
|
+
const expiresInMs = input.expiresInMs || 24 * 60 * 60 * 1e3;
|
|
3052
|
+
const expiresAt = new Date(now.getTime() + expiresInMs).toISOString();
|
|
3053
|
+
let job = {
|
|
3054
|
+
id: jobId,
|
|
3055
|
+
type: input.type,
|
|
3056
|
+
status: "created",
|
|
3057
|
+
attempt: 0,
|
|
3058
|
+
maxAttempts: 3,
|
|
3059
|
+
claimedBy: null,
|
|
3060
|
+
leaseExpiresAt: null,
|
|
3061
|
+
expiresAt,
|
|
3062
|
+
progress: 0,
|
|
3063
|
+
currentPhase: null,
|
|
3064
|
+
progressMessage: null,
|
|
3065
|
+
resultId: null,
|
|
3066
|
+
fileHandleId: input.fileHandleId || null,
|
|
3067
|
+
targetRole: input.targetRole || null,
|
|
3068
|
+
createdAt: nowIso,
|
|
3069
|
+
updatedAt: nowIso,
|
|
3070
|
+
metadata: input.metadata
|
|
3071
|
+
};
|
|
3072
|
+
const insertJobStmt = this.db.prepare(`
|
|
3073
|
+
INSERT INTO jobs (
|
|
3074
|
+
id, type, status, attempt, max_attempts, claimed_by, lease_expires_at,
|
|
3075
|
+
expires_at, progress, current_phase, progress_message, result_id,
|
|
3076
|
+
file_handle_id, target_role, created_at, updated_at, metadata
|
|
3077
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3078
|
+
`);
|
|
3079
|
+
insertJobStmt.run(
|
|
3080
|
+
job.id,
|
|
3081
|
+
job.type,
|
|
3082
|
+
job.status,
|
|
3083
|
+
job.attempt,
|
|
3084
|
+
job.maxAttempts,
|
|
3085
|
+
job.claimedBy ? JSON.stringify(job.claimedBy) : null,
|
|
3086
|
+
job.leaseExpiresAt ?? null,
|
|
3087
|
+
job.expiresAt,
|
|
3088
|
+
job.progress,
|
|
3089
|
+
job.currentPhase ?? null,
|
|
3090
|
+
job.progressMessage ?? null,
|
|
3091
|
+
job.resultId ?? null,
|
|
3092
|
+
job.fileHandleId ?? null,
|
|
3093
|
+
job.targetRole ?? null,
|
|
3094
|
+
job.createdAt,
|
|
3095
|
+
job.updatedAt,
|
|
3096
|
+
job.metadata ? JSON.stringify(job.metadata) : null
|
|
3097
|
+
);
|
|
3098
|
+
this.recordEvent({
|
|
3099
|
+
id: `evt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
3100
|
+
jobId: job.id,
|
|
3101
|
+
commandId: input.commandId,
|
|
3102
|
+
type: "job.created",
|
|
3103
|
+
timestamp: nowIso,
|
|
3104
|
+
payload: { type: job.type, targetRole: job.targetRole }
|
|
3105
|
+
});
|
|
3106
|
+
if (input.autoReady !== false) {
|
|
3107
|
+
const transition = transitionJob(job, { type: "ready_for_agent", commandId: input.commandId }, nowIso);
|
|
3108
|
+
job = transition.nextJob;
|
|
3109
|
+
this.updateJobRow(job);
|
|
3110
|
+
this.recordEvent(transition.event);
|
|
3111
|
+
}
|
|
3112
|
+
this.saveCommand(input.commandId, job.id, "create_job", job);
|
|
3113
|
+
return job;
|
|
3114
|
+
}
|
|
3115
|
+
getJob(id) {
|
|
3116
|
+
const stmt = this.db.prepare("SELECT * FROM jobs WHERE id = ?");
|
|
3117
|
+
const row = stmt.get(id);
|
|
3118
|
+
return row ? parseJobRow(row) : null;
|
|
3119
|
+
}
|
|
3120
|
+
listJobs(filter) {
|
|
3121
|
+
let query = "SELECT * FROM jobs WHERE 1=1";
|
|
3122
|
+
const params = [];
|
|
3123
|
+
if (filter?.status) {
|
|
3124
|
+
query += " AND status = ?";
|
|
3125
|
+
params.push(filter.status);
|
|
3126
|
+
}
|
|
3127
|
+
if (filter?.type) {
|
|
3128
|
+
query += " AND type = ?";
|
|
3129
|
+
params.push(filter.type);
|
|
3130
|
+
}
|
|
3131
|
+
query += " ORDER BY created_at DESC";
|
|
3132
|
+
const stmt = this.db.prepare(query);
|
|
3133
|
+
const rows = stmt.all(...params);
|
|
3134
|
+
return rows.map(parseJobRow);
|
|
3135
|
+
}
|
|
3136
|
+
claimJob(jobId, agent, options) {
|
|
3137
|
+
if (options?.commandId) {
|
|
3138
|
+
const cached = this.getCachedCommand(options.commandId);
|
|
3139
|
+
if (cached) return cached;
|
|
3140
|
+
}
|
|
3141
|
+
const job = this.getJob(jobId);
|
|
3142
|
+
if (!job) {
|
|
3143
|
+
throw new Error(`Job [${jobId}] n\xE3o encontrado para claim.`);
|
|
3144
|
+
}
|
|
3145
|
+
const transition = transitionJob(
|
|
3146
|
+
job,
|
|
3147
|
+
{
|
|
3148
|
+
type: "claim",
|
|
3149
|
+
agent,
|
|
3150
|
+
leaseDurationMs: options?.leaseDurationMs,
|
|
3151
|
+
commandId: options?.commandId
|
|
3152
|
+
}
|
|
3153
|
+
);
|
|
3154
|
+
this.updateJobRow(transition.nextJob);
|
|
3155
|
+
this.recordEvent(transition.event);
|
|
3156
|
+
this.registerAgent(agent);
|
|
3157
|
+
this.saveCommand(options?.commandId, jobId, "claim_job", transition.nextJob);
|
|
3158
|
+
return transition.nextJob;
|
|
3159
|
+
}
|
|
3160
|
+
reportProgress(jobId, agent, params) {
|
|
3161
|
+
if (params.commandId) {
|
|
3162
|
+
const cached = this.getCachedCommand(params.commandId);
|
|
3163
|
+
if (cached) return cached;
|
|
3164
|
+
}
|
|
3165
|
+
const job = this.getJob(jobId);
|
|
3166
|
+
if (!job) {
|
|
3167
|
+
throw new Error(`Job [${jobId}] n\xE3o encontrado para reportProgress.`);
|
|
3168
|
+
}
|
|
3169
|
+
const previousProgress = job.progress;
|
|
3170
|
+
const previousPhase = job.currentPhase;
|
|
3171
|
+
const previousMessage = job.progressMessage;
|
|
3172
|
+
const transition = transitionJob(
|
|
3173
|
+
job,
|
|
3174
|
+
{
|
|
3175
|
+
type: "report_progress",
|
|
3176
|
+
agent,
|
|
3177
|
+
phase: params.phase,
|
|
3178
|
+
progress: params.progress,
|
|
3179
|
+
message: params.message,
|
|
3180
|
+
leaseDurationMs: params.leaseDurationMs,
|
|
3181
|
+
commandId: params.commandId
|
|
3182
|
+
}
|
|
3183
|
+
);
|
|
3184
|
+
this.updateJobRow(transition.nextJob);
|
|
3185
|
+
const delta = Math.abs(transition.nextJob.progress - previousProgress);
|
|
3186
|
+
const phaseChanged = transition.nextJob.currentPhase !== previousPhase;
|
|
3187
|
+
const msgChanged = Boolean(params.message && params.message !== previousMessage);
|
|
3188
|
+
const shouldEmitEvent = delta >= 5 || phaseChanged || msgChanged;
|
|
3189
|
+
if (shouldEmitEvent) {
|
|
3190
|
+
this.recordEvent(transition.event);
|
|
3191
|
+
}
|
|
3192
|
+
this.registerAgent(agent);
|
|
3193
|
+
this.saveCommand(params.commandId, jobId, "report_progress", transition.nextJob);
|
|
3194
|
+
return transition.nextJob;
|
|
3195
|
+
}
|
|
3196
|
+
requestUserAction(jobId, agent, params) {
|
|
3197
|
+
if (params.commandId) {
|
|
3198
|
+
const cached = this.getCachedCommand(params.commandId);
|
|
3199
|
+
if (cached) return cached;
|
|
3200
|
+
}
|
|
3201
|
+
const job = this.getJob(jobId);
|
|
3202
|
+
if (!job) {
|
|
3203
|
+
throw new Error(`Job [${jobId}] n\xE3o encontrado para requestUserAction.`);
|
|
3204
|
+
}
|
|
3205
|
+
const transition = transitionJob(
|
|
3206
|
+
job,
|
|
3207
|
+
{
|
|
3208
|
+
type: "request_user_action",
|
|
3209
|
+
agent,
|
|
3210
|
+
reason: params.reason,
|
|
3211
|
+
prompt: params.prompt,
|
|
3212
|
+
commandId: params.commandId
|
|
3213
|
+
}
|
|
3214
|
+
);
|
|
3215
|
+
this.updateJobRow(transition.nextJob);
|
|
3216
|
+
this.recordEvent(transition.event);
|
|
3217
|
+
this.saveCommand(params.commandId, jobId, "request_user_action", transition.nextJob);
|
|
3218
|
+
return transition.nextJob;
|
|
3219
|
+
}
|
|
3220
|
+
resumeJob(jobId, params) {
|
|
3221
|
+
if (params.commandId) {
|
|
3222
|
+
const cached = this.getCachedCommand(params.commandId);
|
|
3223
|
+
if (cached) return cached;
|
|
3224
|
+
}
|
|
3225
|
+
const job = this.getJob(jobId);
|
|
3226
|
+
if (!job) {
|
|
3227
|
+
throw new Error(`Job [${jobId}] n\xE3o encontrado para resume.`);
|
|
3228
|
+
}
|
|
3229
|
+
const transition = transitionJob(
|
|
3230
|
+
job,
|
|
3231
|
+
{
|
|
3232
|
+
type: "user_resume",
|
|
3233
|
+
actor: "user",
|
|
3234
|
+
reason: params.reason,
|
|
3235
|
+
payload: params.payload,
|
|
3236
|
+
commandId: params.commandId
|
|
3237
|
+
}
|
|
3238
|
+
);
|
|
3239
|
+
this.updateJobRow(transition.nextJob);
|
|
3240
|
+
this.recordEvent(transition.event);
|
|
3241
|
+
this.saveCommand(params.commandId, jobId, "resume_job", transition.nextJob);
|
|
3242
|
+
return transition.nextJob;
|
|
3243
|
+
}
|
|
3244
|
+
completeJob(jobId, agent, result) {
|
|
3245
|
+
if (result.commandId) {
|
|
3246
|
+
const cached = this.getCachedCommand(result.commandId);
|
|
3247
|
+
if (cached) return cached;
|
|
3248
|
+
}
|
|
3249
|
+
const existingResultStmt = this.db.prepare("SELECT id, job_id FROM job_results WHERE submission_id = ?");
|
|
3250
|
+
const existing = existingResultStmt.get(result.submissionId);
|
|
3251
|
+
if (existing) {
|
|
3252
|
+
const existingJob = this.getJob(existing.job_id);
|
|
3253
|
+
return { job: existingJob, resultId: existing.id };
|
|
3254
|
+
}
|
|
3255
|
+
const job = this.getJob(jobId);
|
|
3256
|
+
if (!job) {
|
|
3257
|
+
throw new Error(`Job [${jobId}] n\xE3o encontrado para completeJob.`);
|
|
3258
|
+
}
|
|
3259
|
+
const resultId = `res_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
3260
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
3261
|
+
const transition = transitionJob(
|
|
3262
|
+
job,
|
|
3263
|
+
{
|
|
3264
|
+
type: "complete",
|
|
3265
|
+
agent,
|
|
3266
|
+
resultId,
|
|
3267
|
+
commandId: result.commandId
|
|
3268
|
+
},
|
|
3269
|
+
nowIso
|
|
3270
|
+
);
|
|
3271
|
+
const insertResultStmt = this.db.prepare(`
|
|
3272
|
+
INSERT INTO job_results (
|
|
3273
|
+
id, job_id, schema_version, submission_id, submitted_by, payload, created_at
|
|
3274
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
3275
|
+
`);
|
|
3276
|
+
insertResultStmt.run(
|
|
3277
|
+
resultId,
|
|
3278
|
+
job.id,
|
|
3279
|
+
result.schemaVersion || "1.0",
|
|
3280
|
+
result.submissionId,
|
|
3281
|
+
JSON.stringify(agent),
|
|
3282
|
+
JSON.stringify(result.payload),
|
|
3283
|
+
nowIso
|
|
3284
|
+
);
|
|
3285
|
+
this.updateJobRow(transition.nextJob);
|
|
3286
|
+
this.recordEvent(transition.event);
|
|
3287
|
+
const response = { job: transition.nextJob, resultId };
|
|
3288
|
+
this.saveCommand(result.commandId, jobId, "complete_job", response);
|
|
3289
|
+
return response;
|
|
3290
|
+
}
|
|
3291
|
+
failJob(jobId, error, actor, commandId) {
|
|
3292
|
+
const job = this.getJob(jobId);
|
|
3293
|
+
if (!job) {
|
|
3294
|
+
throw new Error(`Job [${jobId}] n\xE3o encontrado para failJob.`);
|
|
3295
|
+
}
|
|
3296
|
+
const transition = transitionJob(job, { type: "fail", error, actor, commandId });
|
|
3297
|
+
this.updateJobRow(transition.nextJob);
|
|
3298
|
+
this.recordEvent(transition.event);
|
|
3299
|
+
return transition.nextJob;
|
|
3300
|
+
}
|
|
3301
|
+
cancelJob(jobId, actor, reason, commandId) {
|
|
3302
|
+
const job = this.getJob(jobId);
|
|
3303
|
+
if (!job) {
|
|
3304
|
+
throw new Error(`Job [${jobId}] n\xE3o encontrado para cancelJob.`);
|
|
3305
|
+
}
|
|
3306
|
+
const transition = transitionJob(job, { type: "cancel", actor, reason, commandId });
|
|
3307
|
+
this.updateJobRow(transition.nextJob);
|
|
3308
|
+
this.recordEvent(transition.event);
|
|
3309
|
+
return transition.nextJob;
|
|
1920
3310
|
}
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
3311
|
+
// --- Recuperação de Falhas e Expiração ---
|
|
3312
|
+
expireStaleLeases() {
|
|
3313
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
3314
|
+
const stmt = this.db.prepare(`
|
|
3315
|
+
SELECT * FROM jobs
|
|
3316
|
+
WHERE status = 'running'
|
|
3317
|
+
AND lease_expires_at IS NOT NULL
|
|
3318
|
+
AND lease_expires_at < ?
|
|
3319
|
+
`);
|
|
3320
|
+
const rows = stmt.all(nowIso);
|
|
3321
|
+
let recovered = 0;
|
|
3322
|
+
for (const row of rows) {
|
|
3323
|
+
const job = parseJobRow(row);
|
|
3324
|
+
try {
|
|
3325
|
+
const transition = transitionJob(job, { type: "expire_lease", reason: "Lease expired" }, nowIso);
|
|
3326
|
+
this.updateJobRow(transition.nextJob);
|
|
3327
|
+
this.recordEvent(transition.event);
|
|
3328
|
+
recovered++;
|
|
3329
|
+
} catch (err) {
|
|
3330
|
+
console.error(`Erro ao expirar lease do job ${job.id}:`, err);
|
|
3331
|
+
}
|
|
1926
3332
|
}
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
3333
|
+
return recovered;
|
|
3334
|
+
}
|
|
3335
|
+
cleanupExpiredJobs() {
|
|
3336
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
3337
|
+
const stmt = this.db.prepare(`
|
|
3338
|
+
DELETE FROM jobs
|
|
3339
|
+
WHERE expires_at < ? AND status IN ('completed', 'failed', 'cancelled')
|
|
3340
|
+
`);
|
|
3341
|
+
const result = stmt.run(nowIso);
|
|
3342
|
+
return Number(result.changes);
|
|
3343
|
+
}
|
|
3344
|
+
// --- Event Log & SSE Replay ---
|
|
3345
|
+
recordEvent(event) {
|
|
3346
|
+
const stmt = this.db.prepare(`
|
|
3347
|
+
INSERT INTO job_events (id, job_id, command_id, type, payload, created_at)
|
|
3348
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
3349
|
+
`);
|
|
3350
|
+
stmt.run(
|
|
3351
|
+
event.id,
|
|
3352
|
+
event.jobId,
|
|
3353
|
+
event.commandId || null,
|
|
3354
|
+
event.type,
|
|
3355
|
+
event.payload ? JSON.stringify(event.payload) : null,
|
|
3356
|
+
event.timestamp
|
|
3357
|
+
);
|
|
3358
|
+
this.onEventRecorded?.(event);
|
|
3359
|
+
}
|
|
3360
|
+
getEventsSince(jobId, lastEventId) {
|
|
3361
|
+
if (!lastEventId) {
|
|
3362
|
+
const stmt2 = this.db.prepare(`
|
|
3363
|
+
SELECT * FROM job_events
|
|
3364
|
+
WHERE job_id = ?
|
|
3365
|
+
ORDER BY rowid ASC
|
|
3366
|
+
`);
|
|
3367
|
+
const rows2 = stmt2.all(jobId);
|
|
3368
|
+
return rows2.map(parseEventRow);
|
|
1936
3369
|
}
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
3370
|
+
const refStmt = this.db.prepare("SELECT rowid FROM job_events WHERE id = ?");
|
|
3371
|
+
const ref = refStmt.get(lastEventId);
|
|
3372
|
+
const refRowId = ref?.rowid ?? 0;
|
|
3373
|
+
const stmt = this.db.prepare(`
|
|
3374
|
+
SELECT * FROM job_events
|
|
3375
|
+
WHERE job_id = ? AND rowid > ?
|
|
3376
|
+
ORDER BY rowid ASC
|
|
3377
|
+
`);
|
|
3378
|
+
const rows = stmt.all(jobId, refRowId);
|
|
3379
|
+
return rows.map(parseEventRow);
|
|
3380
|
+
}
|
|
3381
|
+
getJobResult(resultId) {
|
|
3382
|
+
const stmt = this.db.prepare("SELECT * FROM job_results WHERE id = ?");
|
|
3383
|
+
const row = stmt.get(resultId);
|
|
3384
|
+
if (!row) return null;
|
|
3385
|
+
return {
|
|
3386
|
+
id: row.id,
|
|
3387
|
+
jobId: row.job_id,
|
|
3388
|
+
schemaVersion: row.schema_version,
|
|
3389
|
+
submissionId: row.submission_id,
|
|
3390
|
+
submittedBy: JSON.parse(row.submitted_by),
|
|
3391
|
+
createdAt: row.created_at,
|
|
3392
|
+
payload: JSON.parse(row.payload)
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
// --- File Handles ---
|
|
3396
|
+
saveFileHandle(handle, realPath) {
|
|
3397
|
+
const stmt = this.db.prepare(`
|
|
3398
|
+
INSERT OR REPLACE INTO file_handles (
|
|
3399
|
+
id, job_id, name, media_type, size, mtime, root, path, expires_at, created_at
|
|
3400
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3401
|
+
`);
|
|
3402
|
+
stmt.run(
|
|
3403
|
+
handle.fileId,
|
|
3404
|
+
handle.jobId,
|
|
3405
|
+
handle.name,
|
|
3406
|
+
handle.mediaType,
|
|
3407
|
+
handle.size,
|
|
3408
|
+
handle.mtime,
|
|
3409
|
+
handle.root,
|
|
3410
|
+
realPath,
|
|
3411
|
+
handle.expiresAt,
|
|
3412
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
3413
|
+
);
|
|
3414
|
+
}
|
|
3415
|
+
getFileHandle(fileId) {
|
|
3416
|
+
const stmt = this.db.prepare("SELECT * FROM file_handles WHERE id = ?");
|
|
3417
|
+
const row = stmt.get(fileId);
|
|
3418
|
+
if (!row) return null;
|
|
3419
|
+
return {
|
|
3420
|
+
fileId: row.id,
|
|
3421
|
+
jobId: row.job_id,
|
|
3422
|
+
name: row.name,
|
|
3423
|
+
mediaType: row.media_type,
|
|
3424
|
+
size: row.size,
|
|
3425
|
+
mtime: row.mtime,
|
|
3426
|
+
root: row.root,
|
|
3427
|
+
access: "native-local-document",
|
|
3428
|
+
expiresAt: row.expires_at,
|
|
3429
|
+
path: row.path,
|
|
3430
|
+
localPath: row.path
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
3433
|
+
// --- Observabilidade de Agentes (Ponto 6 & 7) ---
|
|
3434
|
+
registerAgent(agent, capabilities) {
|
|
3435
|
+
const now = Date.now();
|
|
3436
|
+
const stmt = this.db.prepare(`
|
|
3437
|
+
INSERT INTO agents (
|
|
3438
|
+
agent_id, session_id, client_name, client_version, capabilities, last_seen_at, created_at
|
|
3439
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
3440
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
3441
|
+
session_id = excluded.session_id,
|
|
3442
|
+
client_name = excluded.client_name,
|
|
3443
|
+
client_version = excluded.client_version,
|
|
3444
|
+
capabilities = excluded.capabilities,
|
|
3445
|
+
last_seen_at = excluded.last_seen_at
|
|
3446
|
+
`);
|
|
3447
|
+
stmt.run(
|
|
3448
|
+
agent.agentId,
|
|
3449
|
+
agent.sessionId,
|
|
3450
|
+
agent.clientName || null,
|
|
3451
|
+
agent.clientVersion || null,
|
|
3452
|
+
capabilities ? JSON.stringify(capabilities) : null,
|
|
3453
|
+
now,
|
|
3454
|
+
now
|
|
3455
|
+
);
|
|
3456
|
+
}
|
|
3457
|
+
listActiveAgents(maxAgeMs = 5 * 60 * 1e3) {
|
|
3458
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
3459
|
+
const stmt = this.db.prepare("SELECT * FROM agents WHERE last_seen_at >= ? ORDER BY last_seen_at DESC");
|
|
3460
|
+
const rows = stmt.all(cutoff);
|
|
3461
|
+
return rows.map((r) => ({
|
|
3462
|
+
agentId: r.agent_id,
|
|
3463
|
+
sessionId: r.session_id,
|
|
3464
|
+
clientName: r.client_name || void 0,
|
|
3465
|
+
clientVersion: r.client_version || void 0,
|
|
3466
|
+
lastSeenAt: r.last_seen_at
|
|
3467
|
+
}));
|
|
3468
|
+
}
|
|
3469
|
+
// --- Auxiliares Internos ---
|
|
3470
|
+
updateJobRow(job) {
|
|
3471
|
+
const stmt = this.db.prepare(`
|
|
3472
|
+
UPDATE jobs SET
|
|
3473
|
+
type = ?,
|
|
3474
|
+
status = ?,
|
|
3475
|
+
attempt = ?,
|
|
3476
|
+
max_attempts = ?,
|
|
3477
|
+
claimed_by = ?,
|
|
3478
|
+
lease_expires_at = ?,
|
|
3479
|
+
expires_at = ?,
|
|
3480
|
+
progress = ?,
|
|
3481
|
+
current_phase = ?,
|
|
3482
|
+
progress_message = ?,
|
|
3483
|
+
result_id = ?,
|
|
3484
|
+
file_handle_id = ?,
|
|
3485
|
+
target_role = ?,
|
|
3486
|
+
updated_at = ?,
|
|
3487
|
+
metadata = ?
|
|
3488
|
+
WHERE id = ?
|
|
3489
|
+
`);
|
|
3490
|
+
stmt.run(
|
|
3491
|
+
job.type,
|
|
3492
|
+
job.status,
|
|
3493
|
+
job.attempt,
|
|
3494
|
+
job.maxAttempts,
|
|
3495
|
+
job.claimedBy ? JSON.stringify(job.claimedBy) : null,
|
|
3496
|
+
job.leaseExpiresAt ?? null,
|
|
3497
|
+
job.expiresAt,
|
|
3498
|
+
job.progress,
|
|
3499
|
+
job.currentPhase ?? null,
|
|
3500
|
+
job.progressMessage ?? null,
|
|
3501
|
+
job.resultId ?? null,
|
|
3502
|
+
job.fileHandleId ?? null,
|
|
3503
|
+
job.targetRole ?? null,
|
|
3504
|
+
job.updatedAt,
|
|
3505
|
+
job.metadata ? JSON.stringify(job.metadata) : null,
|
|
3506
|
+
job.id
|
|
3507
|
+
);
|
|
3508
|
+
}
|
|
3509
|
+
};
|
|
3510
|
+
|
|
3511
|
+
// src/bridge/sse.ts
|
|
3512
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
3513
|
+
var SseBroker = class extends EventEmitter2 {
|
|
3514
|
+
activeStreams = /* @__PURE__ */ new Map();
|
|
3515
|
+
getSubscriberCount(jobId) {
|
|
3516
|
+
if (!jobId) return this.activeStreams.size;
|
|
3517
|
+
let count = 0;
|
|
3518
|
+
for (const key of this.activeStreams.keys()) {
|
|
3519
|
+
if (key.startsWith(`${jobId}:`)) count++;
|
|
3520
|
+
}
|
|
3521
|
+
return count;
|
|
3522
|
+
}
|
|
3523
|
+
publish(jobId, event) {
|
|
3524
|
+
this.emit(`job:${jobId}`, event);
|
|
3525
|
+
this.emit("global", { jobId, event });
|
|
3526
|
+
}
|
|
3527
|
+
formatSseMessage(event) {
|
|
3528
|
+
const lines = [
|
|
3529
|
+
`id: ${event.id}`,
|
|
3530
|
+
`event: ${event.type}`,
|
|
3531
|
+
`data: ${JSON.stringify(event)}`,
|
|
3532
|
+
"",
|
|
3533
|
+
""
|
|
3534
|
+
];
|
|
3535
|
+
return lines.join("\n");
|
|
3536
|
+
}
|
|
3537
|
+
handleJobEvents(req, res, jobId, store) {
|
|
3538
|
+
const parsedUrl = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
3539
|
+
const lastEventId = req.headers["last-event-id"] || parsedUrl.searchParams.get("lastEventId") || void 0;
|
|
3540
|
+
res.writeHead(200, {
|
|
3541
|
+
"Content-Type": "text/event-stream",
|
|
3542
|
+
"Cache-Control": "no-cache, no-transform",
|
|
3543
|
+
"Connection": "keep-alive",
|
|
3544
|
+
"Access-Control-Allow-Origin": "*",
|
|
3545
|
+
"X-Accel-Buffering": "no"
|
|
3546
|
+
});
|
|
3547
|
+
res.flushHeaders?.();
|
|
3548
|
+
res.write(`: connected to job ${jobId}
|
|
3549
|
+
|
|
1947
3550
|
`);
|
|
3551
|
+
try {
|
|
3552
|
+
const pastEvents = store.getEventsSince(jobId, lastEventId);
|
|
3553
|
+
for (const evt of pastEvents) {
|
|
3554
|
+
res.write(this.formatSseMessage(evt));
|
|
3555
|
+
}
|
|
1948
3556
|
} catch (err) {
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
3557
|
+
console.error(`Erro ao buscar hist\xF3rico de eventos para o job ${jobId}:`, err);
|
|
3558
|
+
}
|
|
3559
|
+
const streamId = `${jobId}:${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
3560
|
+
const onEvent = (event) => {
|
|
3561
|
+
try {
|
|
3562
|
+
res.write(this.formatSseMessage(event));
|
|
3563
|
+
} catch (err) {
|
|
3564
|
+
cleanup();
|
|
3565
|
+
}
|
|
3566
|
+
};
|
|
3567
|
+
const heartbeat = setInterval(() => {
|
|
3568
|
+
try {
|
|
3569
|
+
res.write(`: heartbeat ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
3570
|
+
|
|
1956
3571
|
`);
|
|
3572
|
+
} catch {
|
|
3573
|
+
cleanup();
|
|
3574
|
+
}
|
|
3575
|
+
}, 15e3);
|
|
3576
|
+
const cleanup = () => {
|
|
3577
|
+
clearInterval(heartbeat);
|
|
3578
|
+
this.off(`job:${jobId}`, onEvent);
|
|
3579
|
+
this.activeStreams.delete(streamId);
|
|
3580
|
+
};
|
|
3581
|
+
this.on(`job:${jobId}`, onEvent);
|
|
3582
|
+
this.activeStreams.set(streamId, { res, cleanup });
|
|
3583
|
+
req.on("close", cleanup);
|
|
3584
|
+
}
|
|
3585
|
+
closeAll() {
|
|
3586
|
+
for (const [id, stream] of this.activeStreams.entries()) {
|
|
3587
|
+
try {
|
|
3588
|
+
stream.res.end();
|
|
3589
|
+
} catch {
|
|
3590
|
+
}
|
|
3591
|
+
stream.cleanup();
|
|
1957
3592
|
}
|
|
3593
|
+
this.activeStreams.clear();
|
|
1958
3594
|
}
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
3595
|
+
};
|
|
3596
|
+
var sseBroker = new SseBroker();
|
|
3597
|
+
|
|
3598
|
+
// src/bridge/file-resolver.ts
|
|
3599
|
+
import fs3 from "fs";
|
|
3600
|
+
import path3 from "path";
|
|
3601
|
+
import os3 from "os";
|
|
3602
|
+
function getDefaultScanRoots() {
|
|
3603
|
+
const home = os3.homedir();
|
|
3604
|
+
return [
|
|
3605
|
+
{ root: "downloads", dirPath: path3.join(home, "Downloads") },
|
|
3606
|
+
{ root: "desktop", dirPath: path3.join(home, "Desktop") },
|
|
3607
|
+
{ root: "documents", dirPath: path3.join(home, "Documents") }
|
|
3608
|
+
];
|
|
3609
|
+
}
|
|
3610
|
+
function scoreFileMatch(fileName, fileSize, fileMtime, fingerprint) {
|
|
3611
|
+
let score = 0;
|
|
3612
|
+
if (fileName === fingerprint.name) {
|
|
3613
|
+
score += 40;
|
|
3614
|
+
} else if (fileName.toLowerCase() === fingerprint.name.toLowerCase()) {
|
|
3615
|
+
score += 35;
|
|
3616
|
+
}
|
|
3617
|
+
if (fileSize === fingerprint.size) {
|
|
3618
|
+
score += 40;
|
|
3619
|
+
}
|
|
3620
|
+
if (Math.abs(fileMtime - fingerprint.lastModified) <= 3e3) {
|
|
3621
|
+
score += 20;
|
|
3622
|
+
}
|
|
3623
|
+
return score;
|
|
3624
|
+
}
|
|
3625
|
+
function scanForCandidate(fingerprint, roots = getDefaultScanRoots()) {
|
|
3626
|
+
const candidates = [];
|
|
3627
|
+
for (const { root, dirPath } of roots) {
|
|
3628
|
+
if (!fs3.existsSync(dirPath)) continue;
|
|
3629
|
+
try {
|
|
3630
|
+
const entries = fs3.readdirSync(dirPath, { withFileTypes: true });
|
|
3631
|
+
for (const entry of entries) {
|
|
3632
|
+
if (!entry.isFile()) continue;
|
|
3633
|
+
const isPdf = entry.name.toLowerCase().endsWith(".pdf");
|
|
3634
|
+
const isNameMatch = entry.name.toLowerCase() === fingerprint.name.toLowerCase();
|
|
3635
|
+
if (!isPdf && !isNameMatch) continue;
|
|
3636
|
+
const fullPath = path3.join(dirPath, entry.name);
|
|
3637
|
+
try {
|
|
3638
|
+
const stat = fs3.statSync(fullPath);
|
|
3639
|
+
const score = scoreFileMatch(entry.name, stat.size, stat.mtimeMs, fingerprint);
|
|
3640
|
+
if (score >= 60) {
|
|
3641
|
+
candidates.push({
|
|
3642
|
+
filePath: fullPath,
|
|
3643
|
+
name: entry.name,
|
|
3644
|
+
root,
|
|
3645
|
+
size: stat.size,
|
|
3646
|
+
mtime: stat.mtimeMs,
|
|
3647
|
+
score
|
|
3648
|
+
});
|
|
3649
|
+
}
|
|
3650
|
+
} catch {
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
} catch {
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
if (candidates.length === 0) {
|
|
3657
|
+
return null;
|
|
3658
|
+
}
|
|
3659
|
+
candidates.sort((a, b) => b.score - a.score || b.mtime - a.mtime);
|
|
3660
|
+
return candidates[0];
|
|
3661
|
+
}
|
|
3662
|
+
function resolveLocalDocument(store, jobId, fingerprint, customRoots) {
|
|
3663
|
+
const match = scanForCandidate(fingerprint, customRoots);
|
|
3664
|
+
if (!match) {
|
|
3665
|
+
throw new Error(
|
|
3666
|
+
`N\xE3o foi poss\xEDvel localizar o arquivo "${fingerprint.name}" (${fingerprint.size} bytes) nas pastas Downloads, Desktop ou Documents do usu\xE1rio local.`
|
|
3667
|
+
);
|
|
3668
|
+
}
|
|
3669
|
+
const fileId = `doc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
3670
|
+
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString();
|
|
3671
|
+
const handle = {
|
|
3672
|
+
fileId,
|
|
3673
|
+
jobId,
|
|
3674
|
+
name: match.name,
|
|
3675
|
+
mediaType: "application/pdf",
|
|
3676
|
+
size: match.size,
|
|
3677
|
+
mtime: match.mtime,
|
|
3678
|
+
root: match.root,
|
|
3679
|
+
access: "native-local-document",
|
|
3680
|
+
expiresAt
|
|
3681
|
+
};
|
|
3682
|
+
store.saveFileHandle(handle, match.filePath);
|
|
3683
|
+
return handle;
|
|
1968
3684
|
}
|
|
1969
3685
|
|
|
1970
3686
|
// src/bridge/server.ts
|
|
1971
|
-
import http2 from "http";
|
|
1972
3687
|
function setCorsHeaders(res) {
|
|
1973
3688
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1974
3689
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1975
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
3690
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Last-Event-ID");
|
|
1976
3691
|
res.setHeader("Access-Control-Max-Age", "86400");
|
|
1977
3692
|
}
|
|
1978
3693
|
function sendJson(res, statusCode, data) {
|
|
@@ -2001,7 +3716,17 @@ async function parseBody(req) {
|
|
|
2001
3716
|
});
|
|
2002
3717
|
}
|
|
2003
3718
|
function createBridgeHttpServer(options = {}) {
|
|
2004
|
-
const
|
|
3719
|
+
const store = options.jobStore || new JobStore(createDatabase(options.dbPath), (event) => {
|
|
3720
|
+
sseBroker.publish(event.jobId, event);
|
|
3721
|
+
});
|
|
3722
|
+
const leaseWatchTimer = setInterval(() => {
|
|
3723
|
+
try {
|
|
3724
|
+
store.expireStaleLeases();
|
|
3725
|
+
} catch (err) {
|
|
3726
|
+
console.error("[LinkeGringo Bridge] Erro ao expirar leases:", err);
|
|
3727
|
+
}
|
|
3728
|
+
}, 1e4);
|
|
3729
|
+
const activeSseWatchers = /* @__PURE__ */ new Map();
|
|
2005
3730
|
const server = http2.createServer(async (req, res) => {
|
|
2006
3731
|
setCorsHeaders(res);
|
|
2007
3732
|
if (req.method === "OPTIONS") {
|
|
@@ -2013,32 +3738,43 @@ function createBridgeHttpServer(options = {}) {
|
|
|
2013
3738
|
const pathname = parsedUrl.pathname;
|
|
2014
3739
|
try {
|
|
2015
3740
|
if (req.method === "GET" && (pathname === "/health" || pathname === "/api/health")) {
|
|
3741
|
+
const activeJobs = store.listJobs();
|
|
3742
|
+
const activeAgents = store.listActiveAgents();
|
|
2016
3743
|
sendJson(res, 200, {
|
|
2017
3744
|
status: "ok",
|
|
2018
3745
|
server: "linkegringo-mcp-bridge",
|
|
2019
|
-
watcherConnected: bridgeJobStore.hasActiveWatcher(),
|
|
3746
|
+
watcherConnected: bridgeJobStore.hasActiveWatcher() || activeAgents.length > 0,
|
|
2020
3747
|
watcherCount: bridgeJobStore.getWatcherCount(),
|
|
2021
|
-
pendingCount: bridgeJobStore.getPendingCount(),
|
|
2022
|
-
totalJobs: bridgeJobStore.getAllJobs().length
|
|
3748
|
+
pendingCount: bridgeJobStore.getPendingCount() + store.listJobs({ status: "waiting_for_agent" }).length,
|
|
3749
|
+
totalJobs: bridgeJobStore.getAllJobs().length + activeJobs.length,
|
|
3750
|
+
activeJobsCount: activeJobs.length,
|
|
3751
|
+
activeAgentsCount: activeAgents.length,
|
|
3752
|
+
subscribersCount: sseBroker.getSubscriberCount()
|
|
2023
3753
|
});
|
|
2024
3754
|
return;
|
|
2025
3755
|
}
|
|
2026
3756
|
if (req.method === "GET" && pathname === "/api/bridge/status") {
|
|
2027
|
-
const
|
|
3757
|
+
const pendingLegacy = bridgeJobStore.getPendingJob();
|
|
3758
|
+
const waitingJobs = store.listJobs({ status: "waiting_for_agent" });
|
|
3759
|
+
const runningJobs = store.listJobs({ status: "running" });
|
|
3760
|
+
const activeAgents = store.listActiveAgents();
|
|
2028
3761
|
sendJson(res, 200, {
|
|
2029
3762
|
ok: true,
|
|
2030
3763
|
status: "ready",
|
|
2031
3764
|
server: "linkegringo-mcp-bridge",
|
|
2032
|
-
watcherConnected: bridgeJobStore.hasActiveWatcher(),
|
|
3765
|
+
watcherConnected: bridgeJobStore.hasActiveWatcher() || activeAgents.length > 0,
|
|
2033
3766
|
watcherCount: bridgeJobStore.getWatcherCount(),
|
|
2034
|
-
pendingCount: bridgeJobStore.getPendingCount(),
|
|
2035
|
-
activeJobId:
|
|
2036
|
-
totalJobs: bridgeJobStore.getAllJobs().length
|
|
3767
|
+
pendingCount: bridgeJobStore.getPendingCount() + waitingJobs.length,
|
|
3768
|
+
activeJobId: pendingLegacy ? pendingLegacy.id : runningJobs[0]?.id || waitingJobs[0]?.id || null,
|
|
3769
|
+
totalJobs: bridgeJobStore.getAllJobs().length + store.listJobs().length,
|
|
3770
|
+
activeAgents,
|
|
3771
|
+
waitingCount: waitingJobs.length,
|
|
3772
|
+
runningCount: runningJobs.length
|
|
2037
3773
|
});
|
|
2038
3774
|
return;
|
|
2039
3775
|
}
|
|
2040
3776
|
if (req.method === "POST" && pathname === "/api/bridge/disconnect") {
|
|
2041
|
-
for (const [id, client] of
|
|
3777
|
+
for (const [id, client] of activeSseWatchers.entries()) {
|
|
2042
3778
|
try {
|
|
2043
3779
|
client.res.write(
|
|
2044
3780
|
`data: ${JSON.stringify({ type: "disconnect", message: "Desconectado pelo usu\xE1rio na interface web." })}
|
|
@@ -2050,16 +3786,31 @@ function createBridgeHttpServer(options = {}) {
|
|
|
2050
3786
|
}
|
|
2051
3787
|
client.cleanup();
|
|
2052
3788
|
}
|
|
2053
|
-
|
|
3789
|
+
activeSseWatchers.clear();
|
|
3790
|
+
sseBroker.closeAll();
|
|
3791
|
+
const runningJobs = store.listJobs({ status: "running" });
|
|
3792
|
+
const waitingJobs = store.listJobs({ status: "waiting_for_agent" });
|
|
3793
|
+
for (const j of [...runningJobs, ...waitingJobs]) {
|
|
3794
|
+
try {
|
|
3795
|
+
store.cancelJob(j.id, "user", "Cancelado via interface");
|
|
3796
|
+
} catch {
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
2054
3799
|
const { canceledCount, clearedCount } = bridgeJobStore.resetQueue();
|
|
2055
3800
|
sendJson(res, 200, {
|
|
2056
3801
|
ok: true,
|
|
2057
3802
|
message: "Conex\xE3o cancelada e fila de jobs limpa com sucesso.",
|
|
2058
|
-
canceledCount,
|
|
3803
|
+
canceledCount: canceledCount + runningJobs.length + waitingJobs.length,
|
|
2059
3804
|
clearedCount
|
|
2060
3805
|
});
|
|
2061
3806
|
return;
|
|
2062
3807
|
}
|
|
3808
|
+
const jobEventsMatch = pathname.match(/^\/api\/jobs\/([a-zA-Z0-9_-]+)\/events$/);
|
|
3809
|
+
if (req.method === "GET" && jobEventsMatch) {
|
|
3810
|
+
const jobId = jobEventsMatch[1];
|
|
3811
|
+
sseBroker.handleJobEvents(req, res, jobId, store);
|
|
3812
|
+
return;
|
|
3813
|
+
}
|
|
2063
3814
|
if (req.method === "GET" && pathname === "/api/jobs/stream") {
|
|
2064
3815
|
res.writeHead(200, {
|
|
2065
3816
|
"Content-Type": "text/event-stream",
|
|
@@ -2091,20 +3842,25 @@ function createBridgeHttpServer(options = {}) {
|
|
|
2091
3842
|
bridgeJobStore.on("job:created", onJobCreated);
|
|
2092
3843
|
bridgeJobStore.on("job:completed", onJobCompleted);
|
|
2093
3844
|
bridgeJobStore.on("job:failed", onJobFailed);
|
|
2094
|
-
const
|
|
2095
|
-
res.write(
|
|
3845
|
+
const onGlobalEvent = ({ jobId, event }) => {
|
|
3846
|
+
res.write(`data: ${JSON.stringify({ type: event.type, jobId, event })}
|
|
2096
3847
|
|
|
2097
3848
|
`);
|
|
3849
|
+
};
|
|
3850
|
+
sseBroker.on("global", onGlobalEvent);
|
|
3851
|
+
const hb = setInterval(() => {
|
|
3852
|
+
res.write(": heartbeat\n\n");
|
|
2098
3853
|
}, 15e3);
|
|
2099
3854
|
const cleanup = () => {
|
|
2100
|
-
clearInterval(
|
|
3855
|
+
clearInterval(hb);
|
|
2101
3856
|
bridgeJobStore.unregisterWatcher(watcherId);
|
|
2102
3857
|
bridgeJobStore.off("job:created", onJobCreated);
|
|
2103
3858
|
bridgeJobStore.off("job:completed", onJobCompleted);
|
|
2104
3859
|
bridgeJobStore.off("job:failed", onJobFailed);
|
|
2105
|
-
|
|
3860
|
+
sseBroker.off("global", onGlobalEvent);
|
|
3861
|
+
activeSseWatchers.delete(watcherId);
|
|
2106
3862
|
};
|
|
2107
|
-
|
|
3863
|
+
activeSseWatchers.set(watcherId, { res, cleanup });
|
|
2108
3864
|
req.on("close", cleanup);
|
|
2109
3865
|
return;
|
|
2110
3866
|
}
|
|
@@ -2114,64 +3870,243 @@ function createBridgeHttpServer(options = {}) {
|
|
|
2114
3870
|
sendJson(res, 400, { ok: false, error: 'Campo "type" \xE9 obrigat\xF3rio.' });
|
|
2115
3871
|
return;
|
|
2116
3872
|
}
|
|
2117
|
-
|
|
2118
|
-
|
|
3873
|
+
if (body.payload !== void 0 || body.type.includes("_")) {
|
|
3874
|
+
const legacyJob = bridgeJobStore.createJob(body.type, body.payload || {});
|
|
3875
|
+
options.onJobCreated?.(legacyJob);
|
|
3876
|
+
sendJson(res, 201, { ok: true, job: legacyJob });
|
|
3877
|
+
return;
|
|
3878
|
+
}
|
|
3879
|
+
const job = store.createJob({
|
|
3880
|
+
id: body.id,
|
|
3881
|
+
type: body.type,
|
|
3882
|
+
targetRole: body.targetRole,
|
|
3883
|
+
fileHandleId: body.fileHandleId,
|
|
3884
|
+
metadata: body.metadata,
|
|
3885
|
+
commandId: body.commandId
|
|
3886
|
+
});
|
|
2119
3887
|
sendJson(res, 201, { ok: true, job });
|
|
2120
3888
|
return;
|
|
2121
3889
|
}
|
|
3890
|
+
if (req.method === "POST" && pathname === "/api/files/resolve") {
|
|
3891
|
+
const body = await parseBody(req);
|
|
3892
|
+
if (!body.fingerprint?.name || typeof body.fingerprint?.size !== "number") {
|
|
3893
|
+
sendJson(res, 400, {
|
|
3894
|
+
ok: false,
|
|
3895
|
+
error: 'Fingerprint inv\xE1lido. "name" e "size" num\xE9rico s\xE3o obrigat\xF3rios.'
|
|
3896
|
+
});
|
|
3897
|
+
return;
|
|
3898
|
+
}
|
|
3899
|
+
try {
|
|
3900
|
+
const handle = resolveLocalDocument(store, body.jobId || "temp_job", body.fingerprint);
|
|
3901
|
+
sendJson(res, 200, { ok: true, handle });
|
|
3902
|
+
} catch (err) {
|
|
3903
|
+
sendJson(res, 404, { ok: false, error: err.message });
|
|
3904
|
+
}
|
|
3905
|
+
return;
|
|
3906
|
+
}
|
|
3907
|
+
const docMatch = pathname.match(/^\/api\/documents\/([a-zA-Z0-9_-]+)$/);
|
|
3908
|
+
if (req.method === "GET" && docMatch) {
|
|
3909
|
+
const docId = docMatch[1];
|
|
3910
|
+
const record = store.getFileHandle(docId);
|
|
3911
|
+
if (!record) {
|
|
3912
|
+
sendJson(res, 404, { ok: false, error: `Documento n\xE3o encontrado: ${docId}` });
|
|
3913
|
+
return;
|
|
3914
|
+
}
|
|
3915
|
+
sendJson(res, 200, {
|
|
3916
|
+
ok: true,
|
|
3917
|
+
document: {
|
|
3918
|
+
fileId: record.fileId,
|
|
3919
|
+
jobId: record.jobId,
|
|
3920
|
+
name: record.name,
|
|
3921
|
+
mediaType: record.mediaType,
|
|
3922
|
+
size: record.size,
|
|
3923
|
+
mtime: record.mtime,
|
|
3924
|
+
root: record.root,
|
|
3925
|
+
access: "native-local-document",
|
|
3926
|
+
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString()
|
|
3927
|
+
}
|
|
3928
|
+
});
|
|
3929
|
+
return;
|
|
3930
|
+
}
|
|
2122
3931
|
if (req.method === "GET" && pathname === "/api/jobs") {
|
|
2123
|
-
|
|
3932
|
+
const status = parsedUrl.searchParams.get("status");
|
|
3933
|
+
const type = parsedUrl.searchParams.get("type");
|
|
3934
|
+
const jobs = store.listJobs({ status, type });
|
|
3935
|
+
sendJson(res, 200, { ok: true, jobs });
|
|
2124
3936
|
return;
|
|
2125
3937
|
}
|
|
2126
3938
|
if (req.method === "GET" && pathname === "/api/jobs/pending") {
|
|
2127
3939
|
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}` });
|
|
3940
|
+
if (id) {
|
|
3941
|
+
const specificLegacy = bridgeJobStore.getJob(id);
|
|
3942
|
+
if (specificLegacy) {
|
|
3943
|
+
sendJson(res, 200, { ok: true, job: specificLegacy });
|
|
3944
|
+
return;
|
|
3945
|
+
}
|
|
3946
|
+
const specific = store.getJob(id);
|
|
3947
|
+
sendJson(res, 200, { ok: true, job: specific });
|
|
2140
3948
|
return;
|
|
2141
3949
|
}
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
job = bridgeJobStore.getJob(jobId) || job;
|
|
2147
|
-
}
|
|
3950
|
+
const legacyPending = bridgeJobStore.getPendingJob();
|
|
3951
|
+
if (legacyPending) {
|
|
3952
|
+
sendJson(res, 200, { ok: true, job: legacyPending });
|
|
3953
|
+
return;
|
|
2148
3954
|
}
|
|
2149
|
-
|
|
3955
|
+
const waiting = store.listJobs({ status: "waiting_for_agent" });
|
|
3956
|
+
sendJson(res, 200, { ok: true, job: waiting[0] || null });
|
|
2150
3957
|
return;
|
|
2151
3958
|
}
|
|
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
|
-
}
|
|
3959
|
+
if (req.method === "GET" && pathname === "/api/agents") {
|
|
3960
|
+
const agents = store.listActiveAgents();
|
|
3961
|
+
sendJson(res, 200, { ok: true, agents });
|
|
2162
3962
|
return;
|
|
2163
3963
|
}
|
|
2164
|
-
const
|
|
2165
|
-
if (
|
|
2166
|
-
const jobId =
|
|
2167
|
-
const
|
|
2168
|
-
|
|
2169
|
-
const
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
3964
|
+
const jobRouteMatch = pathname.match(/^\/api\/jobs\/([a-zA-Z0-9_-]+)(?:\/([a-zA-Z0-9_-]+))?$/);
|
|
3965
|
+
if (jobRouteMatch) {
|
|
3966
|
+
const jobId = jobRouteMatch[1];
|
|
3967
|
+
const subAction = jobRouteMatch[2];
|
|
3968
|
+
if (req.method === "GET" && !subAction) {
|
|
3969
|
+
const wait = parsedUrl.searchParams.get("wait") === "true";
|
|
3970
|
+
const timeout = parseInt(parsedUrl.searchParams.get("timeout") || "30000", 10);
|
|
3971
|
+
const legacyJob = bridgeJobStore.getJob(jobId);
|
|
3972
|
+
if (legacyJob) {
|
|
3973
|
+
if (wait && (legacyJob.status === "pending" || legacyJob.status === "processing")) {
|
|
3974
|
+
try {
|
|
3975
|
+
const waited = await bridgeJobStore.waitForJob(jobId, timeout);
|
|
3976
|
+
sendJson(res, 200, { ok: true, job: waited });
|
|
3977
|
+
return;
|
|
3978
|
+
} catch {
|
|
3979
|
+
sendJson(res, 200, { ok: true, job: bridgeJobStore.getJob(jobId) || legacyJob });
|
|
3980
|
+
return;
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
sendJson(res, 200, { ok: true, job: legacyJob });
|
|
3984
|
+
return;
|
|
3985
|
+
}
|
|
3986
|
+
let job = store.getJob(jobId);
|
|
3987
|
+
if (!job) {
|
|
3988
|
+
sendJson(res, 404, { ok: false, error: `Job n\xE3o encontrado: ${jobId}` });
|
|
3989
|
+
return;
|
|
3990
|
+
}
|
|
3991
|
+
if (wait && (job.status === "waiting_for_agent" || job.status === "running")) {
|
|
3992
|
+
const startWait = Date.now();
|
|
3993
|
+
await new Promise((resolve) => {
|
|
3994
|
+
const checkInterval = setInterval(() => {
|
|
3995
|
+
const current = store.getJob(jobId);
|
|
3996
|
+
if (!current || current.status === "completed" || current.status === "failed" || current.status === "cancelled") {
|
|
3997
|
+
clearInterval(checkInterval);
|
|
3998
|
+
resolve();
|
|
3999
|
+
} else if (Date.now() - startWait >= timeout) {
|
|
4000
|
+
clearInterval(checkInterval);
|
|
4001
|
+
resolve();
|
|
4002
|
+
}
|
|
4003
|
+
}, 50);
|
|
4004
|
+
});
|
|
4005
|
+
job = store.getJob(jobId) || job;
|
|
4006
|
+
}
|
|
4007
|
+
const resultRecord = job.resultId ? store.getJobResult(job.resultId) : null;
|
|
4008
|
+
const fullJob = {
|
|
4009
|
+
...job,
|
|
4010
|
+
result: resultRecord?.payload ?? resultRecord
|
|
4011
|
+
};
|
|
4012
|
+
sendJson(res, 200, { ok: true, job: fullJob, result: resultRecord });
|
|
4013
|
+
return;
|
|
4014
|
+
}
|
|
4015
|
+
if (req.method === "POST" && subAction === "claim") {
|
|
4016
|
+
const body = await parseBody(req);
|
|
4017
|
+
if (!body.agent?.agentId || !body.agent?.sessionId) {
|
|
4018
|
+
sendJson(res, 400, { ok: false, error: "Identidade do agente (agentId e sessionId) \xE9 obrigat\xF3ria." });
|
|
4019
|
+
return;
|
|
4020
|
+
}
|
|
4021
|
+
const updated = store.claimJob(jobId, body.agent, {
|
|
4022
|
+
leaseDurationMs: body.leaseDurationMs,
|
|
4023
|
+
commandId: body.commandId
|
|
4024
|
+
});
|
|
4025
|
+
sendJson(res, 200, { ok: true, job: updated });
|
|
4026
|
+
return;
|
|
4027
|
+
}
|
|
4028
|
+
if (req.method === "POST" && subAction === "progress") {
|
|
4029
|
+
const body = await parseBody(req);
|
|
4030
|
+
if (!body.agent) {
|
|
4031
|
+
sendJson(res, 400, { ok: false, error: "Identidade do agente \xE9 obrigat\xF3ria." });
|
|
4032
|
+
return;
|
|
4033
|
+
}
|
|
4034
|
+
const updated = store.reportProgress(jobId, body.agent, {
|
|
4035
|
+
phase: body.phase,
|
|
4036
|
+
progress: body.progress,
|
|
4037
|
+
message: body.message,
|
|
4038
|
+
leaseDurationMs: body.leaseDurationMs,
|
|
4039
|
+
commandId: body.commandId
|
|
4040
|
+
});
|
|
4041
|
+
sendJson(res, 200, { ok: true, job: updated });
|
|
4042
|
+
return;
|
|
4043
|
+
}
|
|
4044
|
+
if (req.method === "POST" && subAction === "user-action") {
|
|
4045
|
+
const body = await parseBody(req);
|
|
4046
|
+
const updated = store.requestUserAction(jobId, body.agent, {
|
|
4047
|
+
reason: body.reason,
|
|
4048
|
+
prompt: body.prompt,
|
|
4049
|
+
commandId: body.commandId
|
|
4050
|
+
});
|
|
4051
|
+
sendJson(res, 200, { ok: true, job: updated });
|
|
4052
|
+
return;
|
|
4053
|
+
}
|
|
4054
|
+
if (req.method === "POST" && subAction === "resume") {
|
|
4055
|
+
const body = await parseBody(req);
|
|
4056
|
+
const updated = store.resumeJob(jobId, {
|
|
4057
|
+
actor: "user",
|
|
4058
|
+
reason: body.reason || "user_confirmed",
|
|
4059
|
+
payload: body.payload,
|
|
4060
|
+
commandId: body.commandId
|
|
4061
|
+
});
|
|
4062
|
+
sendJson(res, 200, { ok: true, job: updated });
|
|
4063
|
+
return;
|
|
4064
|
+
}
|
|
4065
|
+
if (req.method === "POST" && subAction === "complete") {
|
|
4066
|
+
const body = await parseBody(req);
|
|
4067
|
+
const payload = body.result?.payload !== void 0 ? body.result.payload : body.result;
|
|
4068
|
+
const legacyJob = bridgeJobStore.getJob(jobId);
|
|
4069
|
+
if (legacyJob) {
|
|
4070
|
+
const completed2 = bridgeJobStore.completeJob(jobId, payload);
|
|
4071
|
+
sendJson(res, 200, { ok: true, job: completed2 });
|
|
4072
|
+
return;
|
|
4073
|
+
}
|
|
4074
|
+
const agent = body.agent || {
|
|
4075
|
+
agentId: "mcp-agent",
|
|
4076
|
+
sessionId: "session_mcp"
|
|
4077
|
+
};
|
|
4078
|
+
const submissionId = body.submissionId || `sub_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
4079
|
+
const completed = store.completeJob(jobId, agent, {
|
|
4080
|
+
submissionId,
|
|
4081
|
+
schemaVersion: body.schemaVersion || "1.0",
|
|
4082
|
+
payload,
|
|
4083
|
+
commandId: body.commandId
|
|
4084
|
+
});
|
|
4085
|
+
sendJson(res, 200, { ok: true, job: completed.job, resultId: completed.resultId });
|
|
4086
|
+
return;
|
|
4087
|
+
}
|
|
4088
|
+
if (req.method === "GET" && subAction === "result") {
|
|
4089
|
+
const job = store.getJob(jobId);
|
|
4090
|
+
if (!job || !job.resultId) {
|
|
4091
|
+
sendJson(res, 404, { ok: false, error: "Resultado do job n\xE3o encontrado ou ainda n\xE3o dispon\xEDvel." });
|
|
4092
|
+
return;
|
|
4093
|
+
}
|
|
4094
|
+
const resultEnvelope = store.getJobResult(job.resultId);
|
|
4095
|
+
sendJson(res, 200, { ok: true, result: resultEnvelope });
|
|
4096
|
+
return;
|
|
4097
|
+
}
|
|
4098
|
+
if (req.method === "POST" && subAction === "fail") {
|
|
4099
|
+
const body = await parseBody(req);
|
|
4100
|
+
const legacyJob = bridgeJobStore.getJob(jobId);
|
|
4101
|
+
if (legacyJob) {
|
|
4102
|
+
const failed2 = bridgeJobStore.failJob(jobId, body.error || "Erro desconhecido");
|
|
4103
|
+
sendJson(res, 200, { ok: true, job: failed2 });
|
|
4104
|
+
return;
|
|
4105
|
+
}
|
|
4106
|
+
const failed = store.failJob(jobId, body.error || "Erro desconhecido", body.actor, body.commandId);
|
|
4107
|
+
sendJson(res, 200, { ok: true, job: failed });
|
|
4108
|
+
return;
|
|
2173
4109
|
}
|
|
2174
|
-
return;
|
|
2175
4110
|
}
|
|
2176
4111
|
if (req.method === "POST" && pathname === "/api/jobs/clear") {
|
|
2177
4112
|
bridgeJobStore.clear();
|
|
@@ -2183,6 +4118,9 @@ function createBridgeHttpServer(options = {}) {
|
|
|
2183
4118
|
sendJson(res, 500, { ok: false, error: err?.message || "Erro interno no servidor bridge" });
|
|
2184
4119
|
}
|
|
2185
4120
|
});
|
|
4121
|
+
server.on("close", () => {
|
|
4122
|
+
clearInterval(leaseWatchTimer);
|
|
4123
|
+
});
|
|
2186
4124
|
return server;
|
|
2187
4125
|
}
|
|
2188
4126
|
var activeBridgeServer = null;
|
|
@@ -2365,8 +4303,236 @@ function startBridgeWatcher(options = {}) {
|
|
|
2365
4303
|
};
|
|
2366
4304
|
}
|
|
2367
4305
|
|
|
4306
|
+
// src/client/bridge-client.ts
|
|
4307
|
+
var BridgeClient = class {
|
|
4308
|
+
bridgeUrl;
|
|
4309
|
+
defaultTimeoutMs;
|
|
4310
|
+
constructor(config = {}) {
|
|
4311
|
+
this.bridgeUrl = (config.bridgeUrl || typeof process !== "undefined" && process.env?.LINKEGRINGO_BRIDGE_URL || "http://127.0.0.1:5174").replace(/\/$/, "");
|
|
4312
|
+
this.defaultTimeoutMs = config.defaultTimeoutMs || 3e5;
|
|
4313
|
+
}
|
|
4314
|
+
/**
|
|
4315
|
+
* Checa se o bridge HTTP local está online e responsivo.
|
|
4316
|
+
*/
|
|
4317
|
+
async checkHealth() {
|
|
4318
|
+
try {
|
|
4319
|
+
const controller = new AbortController();
|
|
4320
|
+
const timer = setTimeout(() => controller.abort(), 2e3);
|
|
4321
|
+
const res = await fetch(`${this.bridgeUrl}/health`, { signal: controller.signal });
|
|
4322
|
+
clearTimeout(timer);
|
|
4323
|
+
if (!res.ok) return false;
|
|
4324
|
+
const data = await res.json();
|
|
4325
|
+
return data.status === "ok";
|
|
4326
|
+
} catch {
|
|
4327
|
+
return false;
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
/**
|
|
4331
|
+
* Resolve um arquivo local por fingerprint determinístico sem copiá-lo.
|
|
4332
|
+
*/
|
|
4333
|
+
async resolveFile(fingerprint, jobId) {
|
|
4334
|
+
const res = await fetch(`${this.bridgeUrl}/api/files/resolve`, {
|
|
4335
|
+
method: "POST",
|
|
4336
|
+
headers: { "Content-Type": "application/json" },
|
|
4337
|
+
body: JSON.stringify({ fingerprint, jobId })
|
|
4338
|
+
});
|
|
4339
|
+
if (!res.ok) {
|
|
4340
|
+
const err = await res.json().catch(() => ({}));
|
|
4341
|
+
throw new Error(err.error || `Falha ao resolver arquivo local (${res.status})`);
|
|
4342
|
+
}
|
|
4343
|
+
const data = await res.json();
|
|
4344
|
+
return data.handle;
|
|
4345
|
+
}
|
|
4346
|
+
/**
|
|
4347
|
+
* Obtém detalhes públicos do DocumentHandle a partir do capability token opaco.
|
|
4348
|
+
*/
|
|
4349
|
+
async getDocument(fileId) {
|
|
4350
|
+
const res = await fetch(`${this.bridgeUrl}/api/documents/${encodeURIComponent(fileId)}`);
|
|
4351
|
+
if (!res.ok) {
|
|
4352
|
+
const err = await res.json().catch(() => ({}));
|
|
4353
|
+
throw new Error(err.error || `Documento n\xE3o encontrado: ${fileId}`);
|
|
4354
|
+
}
|
|
4355
|
+
const data = await res.json();
|
|
4356
|
+
return data.document;
|
|
4357
|
+
}
|
|
4358
|
+
/**
|
|
4359
|
+
* Cria um novo job na máquina de estados do Bridge.
|
|
4360
|
+
*/
|
|
4361
|
+
async createJob(params) {
|
|
4362
|
+
const res = await fetch(`${this.bridgeUrl}/api/jobs`, {
|
|
4363
|
+
method: "POST",
|
|
4364
|
+
headers: { "Content-Type": "application/json" },
|
|
4365
|
+
body: JSON.stringify(params)
|
|
4366
|
+
});
|
|
4367
|
+
if (!res.ok) {
|
|
4368
|
+
const err = await res.json().catch(() => ({}));
|
|
4369
|
+
throw new Error(err.error || `Falha ao criar job no Bridge (${res.status})`);
|
|
4370
|
+
}
|
|
4371
|
+
const data = await res.json();
|
|
4372
|
+
return data.job;
|
|
4373
|
+
}
|
|
4374
|
+
/**
|
|
4375
|
+
* Consulta o estado atual de um job.
|
|
4376
|
+
*/
|
|
4377
|
+
async getJob(jobId) {
|
|
4378
|
+
const res = await fetch(`${this.bridgeUrl}/api/jobs/${encodeURIComponent(jobId)}`);
|
|
4379
|
+
if (!res.ok) {
|
|
4380
|
+
const err = await res.json().catch(() => ({}));
|
|
4381
|
+
throw new Error(err.error || `Job n\xE3o encontrado: ${jobId}`);
|
|
4382
|
+
}
|
|
4383
|
+
const data = await res.json();
|
|
4384
|
+
if (data.result && !data.job.result) {
|
|
4385
|
+
data.job.result = data.result.payload ?? data.result;
|
|
4386
|
+
}
|
|
4387
|
+
return data.job;
|
|
4388
|
+
}
|
|
4389
|
+
/**
|
|
4390
|
+
* Retoma um job em espera de confirmação do usuário (Human-in-the-Loop).
|
|
4391
|
+
*/
|
|
4392
|
+
async resumeJob(jobId, paramsOrReason = "user_resumed", maybePayload) {
|
|
4393
|
+
const params = typeof paramsOrReason === "string" ? { actor: "user", reason: paramsOrReason, payload: maybePayload } : { actor: paramsOrReason.actor || "user", reason: paramsOrReason.reason, payload: paramsOrReason.payload ?? maybePayload };
|
|
4394
|
+
const res = await fetch(`${this.bridgeUrl}/api/jobs/${encodeURIComponent(jobId)}/resume`, {
|
|
4395
|
+
method: "POST",
|
|
4396
|
+
headers: { "Content-Type": "application/json" },
|
|
4397
|
+
body: JSON.stringify(params)
|
|
4398
|
+
});
|
|
4399
|
+
if (!res.ok) {
|
|
4400
|
+
const err = await res.json().catch(() => ({}));
|
|
4401
|
+
throw new Error(err.error || `Falha ao retomar job ${jobId}`);
|
|
4402
|
+
}
|
|
4403
|
+
const data = await res.json();
|
|
4404
|
+
return data.job;
|
|
4405
|
+
}
|
|
4406
|
+
/**
|
|
4407
|
+
* Subscreve ao stream SSE de eventos do job (`/api/jobs/:id/events`).
|
|
4408
|
+
* Suporta re-conexão automática com Last-Event-ID no browser nativo via EventSource.
|
|
4409
|
+
*/
|
|
4410
|
+
subscribeJobEvents(jobId, onEvent, onError) {
|
|
4411
|
+
if (typeof EventSource !== "undefined") {
|
|
4412
|
+
const url = `${this.bridgeUrl}/api/jobs/${encodeURIComponent(jobId)}/events`;
|
|
4413
|
+
const eventSource = new EventSource(url);
|
|
4414
|
+
eventSource.onmessage = (e) => {
|
|
4415
|
+
try {
|
|
4416
|
+
const parsed = JSON.parse(e.data);
|
|
4417
|
+
onEvent(parsed);
|
|
4418
|
+
} catch (err) {
|
|
4419
|
+
console.warn("[BridgeClient] Erro ao deserializar evento SSE:", err);
|
|
4420
|
+
}
|
|
4421
|
+
};
|
|
4422
|
+
eventSource.onerror = (e) => {
|
|
4423
|
+
if (onError) onError(e);
|
|
4424
|
+
};
|
|
4425
|
+
return () => {
|
|
4426
|
+
eventSource.close();
|
|
4427
|
+
};
|
|
4428
|
+
}
|
|
4429
|
+
let active = true;
|
|
4430
|
+
let lastEventSeq = 0;
|
|
4431
|
+
const poll = async () => {
|
|
4432
|
+
while (active) {
|
|
4433
|
+
try {
|
|
4434
|
+
const job = await this.getJob(jobId);
|
|
4435
|
+
if (job.status === "completed" || job.status === "failed" || job.status === "cancelled") {
|
|
4436
|
+
break;
|
|
4437
|
+
}
|
|
4438
|
+
} catch {
|
|
4439
|
+
}
|
|
4440
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
4441
|
+
}
|
|
4442
|
+
};
|
|
4443
|
+
poll();
|
|
4444
|
+
return () => {
|
|
4445
|
+
active = false;
|
|
4446
|
+
};
|
|
4447
|
+
}
|
|
4448
|
+
/**
|
|
4449
|
+
* Aguarda a resolução completa do job via streaming de eventos SSE,
|
|
4450
|
+
* despachando atualizações reativas de progresso e ações do usuário.
|
|
4451
|
+
*/
|
|
4452
|
+
async waitForJobResult(jobId, options = {}) {
|
|
4453
|
+
const timeoutMs = options.timeoutMs ?? this.defaultTimeoutMs;
|
|
4454
|
+
const startTime = Date.now();
|
|
4455
|
+
return new Promise((resolve, reject) => {
|
|
4456
|
+
let isSettled = false;
|
|
4457
|
+
const cleanup = () => {
|
|
4458
|
+
isSettled = true;
|
|
4459
|
+
unsubscribe();
|
|
4460
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
4461
|
+
};
|
|
4462
|
+
const timeoutTimer = setTimeout(() => {
|
|
4463
|
+
if (!isSettled) {
|
|
4464
|
+
cleanup();
|
|
4465
|
+
reject(
|
|
4466
|
+
new Error(
|
|
4467
|
+
`Tempo limite de ${Math.round(timeoutMs / 1e3)}s excedido aguardando resposta do Agente de IA para o job ${jobId}.`
|
|
4468
|
+
)
|
|
4469
|
+
);
|
|
4470
|
+
}
|
|
4471
|
+
}, timeoutMs);
|
|
4472
|
+
const handleEvent = async (event) => {
|
|
4473
|
+
if (isSettled) return;
|
|
4474
|
+
if (options.onEvent) {
|
|
4475
|
+
options.onEvent(event);
|
|
4476
|
+
}
|
|
4477
|
+
if (event.type === "job.progress") {
|
|
4478
|
+
const p = event.payload;
|
|
4479
|
+
if (options.onProgress) {
|
|
4480
|
+
options.onProgress(p.phase || "running", p.progress || 0, p.message);
|
|
4481
|
+
}
|
|
4482
|
+
} else if (event.type === "job.waiting_for_user") {
|
|
4483
|
+
const p = event.payload;
|
|
4484
|
+
if (options.onUserAction) {
|
|
4485
|
+
options.onUserAction(p.reason || "waiting_for_user", p.prompt);
|
|
4486
|
+
}
|
|
4487
|
+
} else if (event.type === "job.completed") {
|
|
4488
|
+
cleanup();
|
|
4489
|
+
try {
|
|
4490
|
+
const p = event.payload;
|
|
4491
|
+
if (p?.result?.payload) {
|
|
4492
|
+
resolve(p.result.payload);
|
|
4493
|
+
return;
|
|
4494
|
+
}
|
|
4495
|
+
const finishedJob = await this.getJob(jobId);
|
|
4496
|
+
if (finishedJob.result) {
|
|
4497
|
+
const res = finishedJob.result?.payload ?? finishedJob.result;
|
|
4498
|
+
resolve(res);
|
|
4499
|
+
} else if (finishedJob.resultId) {
|
|
4500
|
+
const res = await fetch(`${this.bridgeUrl}/api/jobs/${encodeURIComponent(jobId)}`);
|
|
4501
|
+
const data = await res.json();
|
|
4502
|
+
resolve(data.result?.payload ?? data.job?.result ?? data);
|
|
4503
|
+
} else {
|
|
4504
|
+
resolve(finishedJob);
|
|
4505
|
+
}
|
|
4506
|
+
} catch (err) {
|
|
4507
|
+
reject(err);
|
|
4508
|
+
}
|
|
4509
|
+
} else if (event.type === "job.failed") {
|
|
4510
|
+
cleanup();
|
|
4511
|
+
const p = event.payload;
|
|
4512
|
+
reject(new Error(p?.error || "O Agente de IA reportou uma falha ao processar o job."));
|
|
4513
|
+
}
|
|
4514
|
+
};
|
|
4515
|
+
const unsubscribe = this.subscribeJobEvents(jobId, handleEvent, (err) => {
|
|
4516
|
+
console.warn("[BridgeClient] Conex\xE3o SSE inst\xE1vel, aguardando reconex\xE3o...", err);
|
|
4517
|
+
});
|
|
4518
|
+
this.getJob(jobId).then((job) => {
|
|
4519
|
+
if (isSettled) return;
|
|
4520
|
+
if (job.status === "completed") {
|
|
4521
|
+
cleanup();
|
|
4522
|
+
const res = job.result?.payload ?? job.result;
|
|
4523
|
+
resolve(res);
|
|
4524
|
+
} else if (job.status === "failed") {
|
|
4525
|
+
cleanup();
|
|
4526
|
+
reject(new Error(job.error || "O job falhou."));
|
|
4527
|
+
}
|
|
4528
|
+
}).catch(() => {
|
|
4529
|
+
});
|
|
4530
|
+
});
|
|
4531
|
+
}
|
|
4532
|
+
};
|
|
4533
|
+
|
|
2368
4534
|
// src/index.ts
|
|
2369
|
-
import
|
|
4535
|
+
import fs4 from "fs";
|
|
2370
4536
|
import { fileURLToPath } from "url";
|
|
2371
4537
|
async function main() {
|
|
2372
4538
|
if (process.argv.includes("install") || process.argv.includes("setup") || process.argv.includes("--install")) {
|
|
@@ -2393,7 +4559,7 @@ function isDirectExecution() {
|
|
|
2393
4559
|
if (!process.argv[1]) return false;
|
|
2394
4560
|
try {
|
|
2395
4561
|
const currentFilePath = fileURLToPath(import.meta.url);
|
|
2396
|
-
const scriptPath =
|
|
4562
|
+
const scriptPath = fs4.existsSync(process.argv[1]) ? fs4.realpathSync(process.argv[1]) : process.argv[1];
|
|
2397
4563
|
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
4564
|
} catch {
|
|
2399
4565
|
return true;
|
|
@@ -2406,8 +4572,12 @@ if (isDirectExecution()) {
|
|
|
2406
4572
|
});
|
|
2407
4573
|
}
|
|
2408
4574
|
export {
|
|
4575
|
+
BridgeClient,
|
|
4576
|
+
DEFAULT_BRIDGE_URL,
|
|
2409
4577
|
auditProfileInputSchema,
|
|
2410
4578
|
bridgeJobStore,
|
|
4579
|
+
claimJobInputSchema,
|
|
4580
|
+
claimRemoteJob,
|
|
2411
4581
|
completeRemoteOrLocalJob,
|
|
2412
4582
|
convertToXyzBulletInputSchema,
|
|
2413
4583
|
createBridgeHttpServer,
|
|
@@ -2416,25 +4586,46 @@ export {
|
|
|
2416
4586
|
failRemoteOrLocalJob,
|
|
2417
4587
|
formatGoogleXyzBullet,
|
|
2418
4588
|
generateHeadlineInputSchema,
|
|
4589
|
+
getBridgeUrl,
|
|
2419
4590
|
getExperienceBulletCount,
|
|
2420
4591
|
getMcpConfigsForSystem,
|
|
2421
4592
|
getPendingJobInputSchema,
|
|
2422
4593
|
getRemoteOrLocalPendingJob,
|
|
2423
4594
|
handleAuditProfile,
|
|
4595
|
+
handleClaimJob,
|
|
2424
4596
|
handleConvertToXyzBullet,
|
|
2425
4597
|
handleGenerateHeadline,
|
|
2426
4598
|
handleGetPendingJob,
|
|
4599
|
+
handleInspectDocument,
|
|
4600
|
+
handleListJobs,
|
|
4601
|
+
handleReportProgress,
|
|
4602
|
+
handleRequestUserAction,
|
|
2427
4603
|
handleSimulateRecruiterSearch,
|
|
4604
|
+
handleSubmitDiagnostic,
|
|
4605
|
+
handleSubmitInterview,
|
|
2428
4606
|
handleSubmitJobResult,
|
|
4607
|
+
handleSubmitRewrite,
|
|
2429
4608
|
handleWatchLinkeGringo,
|
|
4609
|
+
inspectDocumentInputSchema,
|
|
4610
|
+
inspectRemoteDocument,
|
|
2430
4611
|
installMcpServerConfig,
|
|
4612
|
+
listJobsInputSchema,
|
|
4613
|
+
listRemoteJobs,
|
|
2431
4614
|
parseArgs,
|
|
4615
|
+
reportProgressInputSchema,
|
|
4616
|
+
reportRemoteProgress,
|
|
4617
|
+
requestRemoteUserAction,
|
|
4618
|
+
requestUserActionInputSchema,
|
|
2432
4619
|
runInstaller,
|
|
2433
4620
|
simulateRecruiterSearchInputSchema,
|
|
2434
4621
|
startBridgeServer,
|
|
2435
4622
|
startBridgeWatcher,
|
|
2436
4623
|
stopBridgeServer,
|
|
4624
|
+
submitDiagnosticInputSchema,
|
|
4625
|
+
submitInterviewInputSchema,
|
|
2437
4626
|
submitJobResultInputSchema,
|
|
4627
|
+
submitRemoteJobResult,
|
|
4628
|
+
submitRewriteInputSchema,
|
|
2438
4629
|
waitForNextJob,
|
|
2439
4630
|
watchLinkeGringoInputSchema
|
|
2440
4631
|
};
|