@linkegringo/mcp 1.0.11 → 2.0.1

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