@linkegringo/mcp 1.0.11 → 2.0.0

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