@linkegringo/mcp 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -392,8 +392,7 @@ declare function handleGetPendingJob(input?: GetPendingJobInput): Promise<{
392
392
  }[];
393
393
  structuredData: {
394
394
  jobFound: boolean;
395
- pendingCount: number;
396
- job?: undefined;
395
+ job: BridgeJob<any, any>;
397
396
  };
398
397
  } | {
399
398
  content: {
@@ -402,13 +401,7 @@ declare function handleGetPendingJob(input?: GetPendingJobInput): Promise<{
402
401
  }[];
403
402
  structuredData: {
404
403
  jobFound: boolean;
405
- job: {
406
- id: string;
407
- type: JobType;
408
- payload: any;
409
- createdAt: number;
410
- };
411
- pendingCount?: undefined;
404
+ pendingCount: number;
412
405
  };
413
406
  }>;
414
407
 
package/dist/index.js CHANGED
@@ -172,9 +172,9 @@ var DEFAULT_BRIDGE_URL = process.env.LINKEGRINGO_BRIDGE_URL || (process.env.NODE
172
172
  async function getRemoteOrLocalPendingJob(jobId, bridgeUrl = DEFAULT_BRIDGE_URL) {
173
173
  if (bridgeUrl) {
174
174
  try {
175
- const url = jobId ? `${bridgeUrl}/api/jobs/pending?id=${encodeURIComponent(jobId)}` : `${bridgeUrl}/api/jobs/pending`;
175
+ const url = jobId ? `${bridgeUrl}/api/jobs/${encodeURIComponent(jobId)}` : `${bridgeUrl}/api/jobs/pending`;
176
176
  const controller = new AbortController();
177
- const timer = setTimeout(() => controller.abort(), 2e3);
177
+ const timer = setTimeout(() => controller.abort(), 15e3);
178
178
  const res = await fetch(url, { signal: controller.signal });
179
179
  clearTimeout(timer);
180
180
  if (res.ok) {
@@ -192,7 +192,7 @@ async function completeRemoteOrLocalJob(jobId, result, bridgeUrl = DEFAULT_BRIDG
192
192
  if (bridgeUrl) {
193
193
  try {
194
194
  const controller = new AbortController();
195
- const timer = setTimeout(() => controller.abort(), 5e3);
195
+ const timer = setTimeout(() => controller.abort(), 15e3);
196
196
  const res = await fetch(`${bridgeUrl}/api/jobs/${encodeURIComponent(jobId)}/complete`, {
197
197
  method: "POST",
198
198
  headers: { "Content-Type": "application/json" },
@@ -1256,26 +1256,10 @@ ${proposals.map(
1256
1256
 
1257
1257
  // src/tools/get-pending-job.ts
1258
1258
  import { z as z11 } from "zod";
1259
- var getPendingJobInputSchema = z11.object({
1260
- jobId: z11.string().optional().describe("ID espec\xEDfico do job a ser buscado (opcional. Se omitido, pega o pr\xF3ximo da fila)")
1261
- });
1262
- async function handleGetPendingJob(input = {}) {
1263
- const job = await getRemoteOrLocalPendingJob(input.jobId);
1264
- if (!job) {
1265
- return {
1266
- content: [
1267
- {
1268
- type: "text",
1269
- text: "Nenhum job pendente no momento na interface web do LinkeGringo."
1270
- }
1271
- ],
1272
- structuredData: {
1273
- jobFound: false,
1274
- pendingCount: bridgeJobStore.getPendingCount()
1275
- }
1276
- };
1277
- }
1278
- bridgeJobStore.markProcessing(job.id);
1259
+
1260
+ // src/tools/format-job.ts
1261
+ function formatJobResponse(job) {
1262
+ let instructions = "";
1279
1263
  let details = "";
1280
1264
  if (job.type === "parse_and_diagnose") {
1281
1265
  const p = job.payload || {};
@@ -1284,53 +1268,210 @@ async function handleGetPendingJob(input = {}) {
1284
1268
  - **Cargo Almejado**: ${p.targetRole || "N\xE3o especificado"}
1285
1269
  - **Possui PDF Base64**: ${Boolean(p.pdfBase64)} (${p.pdfBase64 ? Math.round(p.pdfBase64.length / 1024) + " KB base64" : "N/A"})
1286
1270
  - **Possui Texto**: ${Boolean(p.pdfText)}
1271
+ `.trim();
1272
+ instructions = `
1273
+ ### \u26A0\uFE0F DIRETRIZES OBRIGAT\xD3RIAS PARA O AGENTE DE IA:
1274
+ 1. **Zero Scripts**: N\xC3O execute scripts Python/Node no disco. N\xC3O salve arquivos no sistema de arquivos. N\xC3O inspecione arquivos locais de c\xF3digo.
1275
+ 2. **Processamento Multimodal**: Analise o PDF/dados recebidos diretamente no seu racioc\xEDnio em 1 \xFAnico turno.
1276
+ 3. **Submiss\xE3o Imediata**: Chame a ferramenta \`submit_job_result\` com o JSON estruturado no campo \`result\`.
1277
+
1278
+ ### \u{1F4CB} Estrutura JSON Exata para \`submit_job_result\`:
1279
+ \`\`\`json
1280
+ {
1281
+ "jobId": "${job.id}",
1282
+ "result": {
1283
+ "profile": {
1284
+ "firstName": "Nome",
1285
+ "lastName": "Sobrenome",
1286
+ "headline": "Role Anchor | Core Techs | Scale/Impact | US Remote",
1287
+ "location": "Cidade, Pa\xEDs",
1288
+ "summary": "Resumo profissional com foco em impacto t\xE9cnico...",
1289
+ "experiences": [
1290
+ {
1291
+ "title": "Senior Software Engineer",
1292
+ "companyName": "Nome da Empresa",
1293
+ "workplaceType": "remote",
1294
+ "location": "Local",
1295
+ "current": true,
1296
+ "startDate": { "year": 2022, "month": 1 },
1297
+ "endDate": undefined,
1298
+ "description": "Responsabilidades e impacto..."
1299
+ }
1300
+ ],
1301
+ "education": [
1302
+ {
1303
+ "schoolName": "Universidade",
1304
+ "degreeName": "Bacharelado",
1305
+ "fieldOfStudy": "Ci\xEAncia da Computa\xE7\xE3o"
1306
+ }
1307
+ ],
1308
+ "skills": [
1309
+ { "name": "Node.js" },
1310
+ { "name": "TypeScript" }
1311
+ ],
1312
+ "languages": [
1313
+ { "name": "English", "proficiency": "professional" }
1314
+ ]
1315
+ },
1316
+ "review": {
1317
+ "targetMarket": "United States Remote",
1318
+ "language": "en",
1319
+ "overallScore": 82,
1320
+ "scores": {
1321
+ "searchRelevance": 85,
1322
+ "humanVoice": 80,
1323
+ "credibility": 80,
1324
+ "positioningClarity": 85,
1325
+ "evidenceCoverage": 80
1326
+ },
1327
+ "scoreExplanations": {
1328
+ "searchRelevance": "Avalia\xE7\xE3o de indexa\xE7\xE3o ATS...",
1329
+ "humanVoice": "Tom profissional e aut\xEAntico...",
1330
+ "credibility": "Evid\xEAncias t\xE9cnicas e consist\xEAncia...",
1331
+ "positioningClarity": "Clareza do papel s\xEAnior...",
1332
+ "evidenceCoverage": "Densidade de m\xE9tricas quantific\xE1veis..."
1333
+ },
1334
+ "executiveSummary": "Resumo executivo do diagn\xF3stico e posicionamento para vagas nos EUA...",
1335
+ "profileDirection": {
1336
+ "positioning": "Senior Full-Stack Engineer",
1337
+ "primaryRole": "${p.targetRole || "Senior Full-Stack Engineer"}",
1338
+ "openToWorkTitles": [
1339
+ "Senior Full Stack Engineer",
1340
+ "Senior Backend Engineer",
1341
+ "Staff Software Engineer",
1342
+ "Lead Software Engineer",
1343
+ "Full Stack Developer"
1344
+ ],
1345
+ "rationale": "Justificativa do posicionamento estrat\xE9gico para o mercado americano..."
1346
+ },
1347
+ "triageBottlenecks": [
1348
+ "Gargalo 1: Descri\xE7\xF5es de cargos com poucos bullets quantific\xE1veis",
1349
+ "Gargalo 2: Headline precisa de palavras-chave de indexa\xE7\xE3o booleana"
1350
+ ],
1351
+ "critique": [
1352
+ {
1353
+ "section": "Headline",
1354
+ "assessment": "Avalia\xE7\xE3o da headline atual",
1355
+ "strengths": ["Clareza do cargo principal"],
1356
+ "issues": ["Falta men\xE7\xE3o a escala e especializa\xE7\xE3o"],
1357
+ "severity": "medium"
1358
+ }
1359
+ ]
1360
+ }
1361
+ }
1362
+ }
1363
+ \`\`\`
1287
1364
  `.trim();
1288
1365
  } else if (job.type === "generate_interview") {
1366
+ const p = job.payload || {};
1289
1367
  details = `
1290
1368
  - **Tipo de A\xE7\xE3o**: Gera\xE7\xE3o de Perguntas de Entrevista T\xE9cnica
1291
- - **Candidato**: ${job.payload?.profile?.name || "N\xE3o informado"}
1292
- - **Cargo Almejado**: ${job.payload?.objective?.primaryRole || "N\xE3o informado"}
1369
+ - **Candidato**: ${p.profile?.firstName || ""} ${p.profile?.lastName || ""}
1370
+ - **Cargo Almejado**: ${p.objective?.primaryRole || "N\xE3o informado"}
1371
+ `.trim();
1372
+ instructions = `
1373
+ ### \u26A0\uFE0F DIRETRIZES OBRIGAT\xD3RIAS PARA O AGENTE DE IA:
1374
+ 1. Gere perguntas t\xE9cnicas focando nos gargalos e lacunas identificados no perfil.
1375
+ 2. N\xC3O execute scripts nem inspecione arquivos.
1376
+ 3. Chame a ferramenta \`submit_job_result\` com a estrutura:
1377
+
1378
+ \`\`\`json
1379
+ {
1380
+ "jobId": "${job.id}",
1381
+ "result": {
1382
+ "questions": [
1383
+ {
1384
+ "id": "q1",
1385
+ "category": "scale",
1386
+ "question": "Texto claro da pergunta para investigar o escopo e impacto...",
1387
+ "rationale": "Por que esta pergunta \xE9 importante para o recrutador gringo...",
1388
+ "targetSignal": "M\xE9tricas de escala, throughput ou lat\xEAncia que queremos capturar"
1389
+ }
1390
+ ]
1391
+ }
1392
+ }
1393
+ \`\`\`
1293
1394
  `.trim();
1294
1395
  } else if (job.type === "generate_rewritten_profile") {
1396
+ const p = job.payload || {};
1295
1397
  details = `
1296
- - **Tipo de A\xE7\xE3o**: Reescrita do Perfil com Bullets Google XYZ
1297
- - **Candidato**: ${job.payload?.profile?.name || "N\xE3o informado"}
1298
- - **Fatos Confirmados**: ${job.payload?.confirmedFacts?.length || 0} fatos
1398
+ - **Tipo de A\xE7\xE3o**: Reescrita Completa de Perfil com Bullets Google XYZ
1399
+ - **Candidato**: ${p.profile?.firstName || ""} ${p.profile?.lastName || ""}
1400
+ - **Fatos Confirmados**: ${p.confirmedFacts?.length || 0}
1401
+ `.trim();
1402
+ instructions = `
1403
+ ### \u26A0\uFE0F DIRETRIZES OBRIGAT\xD3RIAS PARA O AGENTE DE IA:
1404
+ 1. Reescreva o perfil aplicando a f\xF3rmula Google XYZ (*Accomplished [X], measured by [Y], by doing [Z]*) e Headline $\\le$ 160 caracteres.
1405
+ 2. N\xC3O execute scripts nem inspecione arquivos.
1406
+ 3. Chame a ferramenta \`submit_job_result\` com a estrutura:
1407
+
1408
+ \`\`\`json
1409
+ {
1410
+ "jobId": "${job.id}",
1411
+ "result": {
1412
+ "rewritten": {
1413
+ "headline": "Role Anchor | 3-4 Core Techs | Scale/Impact | US Remote",
1414
+ "summary": "Resumo About reformulado com gancho forte nos primeiros 250 caracteres...",
1415
+ "experiences": [
1416
+ {
1417
+ "title": "Senior Software Engineer",
1418
+ "companyName": "Nome da Empresa",
1419
+ "workplaceType": "remote",
1420
+ "location": "Local",
1421
+ "current": true,
1422
+ "startDate": { "year": 2022, "month": 1 },
1423
+ "endDate": undefined,
1424
+ "description": "Accomplished [X], measured by [Y], by doing [Z]...\\nAccomplished [X], measured by [Y], by doing [Z]..."
1425
+ }
1426
+ ]
1427
+ }
1428
+ }
1429
+ }
1430
+ \`\`\`
1299
1431
  `.trim();
1300
1432
  }
1301
1433
  return {
1302
1434
  content: [
1303
1435
  {
1304
1436
  type: "text",
1305
- text: `
1306
- # \u{1F4E5} Job da Interface Web Encontrado!
1307
-
1308
- - **Job ID**: \`${job.id}\`
1309
- - **Tipo**: \`${job.type}\`
1310
- - **Criado em**: ${new Date(job.createdAt).toLocaleTimeString()}
1437
+ text: `\u{1F680} **NOVO JOB RECEBIDO DO LINKEGRINGO**: \`${job.id}\`
1311
1438
 
1312
1439
  ${details}
1313
1440
 
1314
- ---
1315
- \u{1F4A1} **Instru\xE7\xF5es para o Agente de IA**:
1316
- 1. Processe a intelig\xEAncia requerida para este job com base nas diretrizes do LinkeGringo.
1317
- 2. Ao concluir, chame a ferramenta \`submit_job_result\` passando \`jobId: "${job.id}"\` e o JSON estruturado no campo \`result\`.
1318
- 3. A interface web em \`localhost:5173\` atualizar\xE1 a tela no mesmo instante!
1319
- `.trim()
1441
+ ${instructions}`
1320
1442
  }
1321
1443
  ],
1322
1444
  structuredData: {
1323
1445
  jobFound: true,
1324
- job: {
1325
- id: job.id,
1326
- type: job.type,
1327
- payload: job.payload,
1328
- createdAt: job.createdAt
1329
- }
1446
+ job
1330
1447
  }
1331
1448
  };
1332
1449
  }
1333
1450
 
1451
+ // 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)")
1454
+ });
1455
+ async function handleGetPendingJob(input = {}) {
1456
+ const job = await getRemoteOrLocalPendingJob(input.jobId);
1457
+ if (!job) {
1458
+ return {
1459
+ content: [
1460
+ {
1461
+ type: "text",
1462
+ text: "Nenhum job pendente no momento na interface web do LinkeGringo."
1463
+ }
1464
+ ],
1465
+ structuredData: {
1466
+ jobFound: false,
1467
+ pendingCount: bridgeJobStore.getPendingCount()
1468
+ }
1469
+ };
1470
+ }
1471
+ bridgeJobStore.markProcessing(job.id);
1472
+ return formatJobResponse(job);
1473
+ }
1474
+
1334
1475
  // src/tools/submit-job-result.ts
1335
1476
  import { z as z12 } from "zod";
1336
1477
  var submitJobResultInputSchema = z12.object({
@@ -1501,76 +1642,6 @@ async function waitForNextJob(bridgeUrl, timeoutMs) {
1501
1642
  }
1502
1643
  });
1503
1644
  }
1504
- function formatJobResponse(job) {
1505
- let instructions = "";
1506
- let details = "";
1507
- if (job.type === "parse_and_diagnose") {
1508
- const p = job.payload || {};
1509
- details = `
1510
- - **Tipo de A\xE7\xE3o**: Extra\xE7\xE3o Multimodal e Diagn\xF3stico de Perfil (parseAndDiagnose)
1511
- - **Cargo Almejado**: ${p.targetRole || "N\xE3o especificado"}
1512
- - **Possui PDF Base64**: ${Boolean(p.pdfBase64)} (${p.pdfBase64 ? Math.round(p.pdfBase64.length / 1024) + " KB" : "N/A"})
1513
- - **Possui Texto**: ${Boolean(p.pdfText)}
1514
- `.trim();
1515
- instructions = `
1516
- ### Pr\xF3ximos Passos Obrigat\xF3rios:
1517
- 1. Extraia e analise o perfil estruturado (\`Profile\`) e o diagn\xF3stico (\`ProfileReview\`) com notas (0-100), gargalos de triagem de recrutadores e lacunas de stack.
1518
- 2. Envie o resultado de volta para o navegador chamando a tool \`submit_job_result\`:
1519
- \`\`\`json
1520
- {
1521
- "jobId": "${job.id}",
1522
- "result": {
1523
- "profile": { ... },
1524
- "review": { ... }
1525
- }
1526
- }
1527
- \`\`\`
1528
- 3. Ap\xF3s enviar o resultado, execute a tool \`watch_linkegringo\` novamente para aguardar a pr\xF3xima a\xE7\xE3o do usu\xE1rio (entrevista t\xE9cnica ou reescrita).
1529
- `.trim();
1530
- } else if (job.type === "generate_interview") {
1531
- const p = job.payload || {};
1532
- details = `
1533
- - **Tipo de A\xE7\xE3o**: Gera\xE7\xE3o de Perguntas de Entrevista T\xE9cnica
1534
- - **Candidato**: ${p.profile?.name || "N\xE3o informado"}
1535
- - **Cargo Almejado**: ${p.objective?.primaryRole || "N\xE3o informado"}
1536
- `.trim();
1537
- instructions = `
1538
- ### Pr\xF3ximos Passos Obrigat\xF3rios:
1539
- 1. Gere as perguntas t\xE9cnicas focando nos gargalos e lacunas identificados.
1540
- 2. Envie o resultado chamando \`submit_job_result({ jobId: "${job.id}", result: { questions: [...] } })\`.
1541
- 3. Rearme a escuta chamando \`watch_linkegringo\` para aguardar as respostas do candidato.
1542
- `.trim();
1543
- } else if (job.type === "generate_rewritten_profile") {
1544
- const p = job.payload || {};
1545
- details = `
1546
- - **Tipo de A\xE7\xE3o**: Reescrita Completa de Perfil com Bullets Google XYZ
1547
- - **Candidato**: ${p.profile?.name || "N\xE3o informado"}
1548
- - **Fatos Confirmados na Entrevista**: ${p.confirmedFacts?.length || 0}
1549
- `.trim();
1550
- instructions = `
1551
- ### Pr\xF3ximos Passos Obrigat\xF3rios:
1552
- 1. Reescreva o perfil aplicando a f\xF3rmula Google XYZ (Accomplished [X], measured by [Y], by doing [Z]) e Headline $\\le$ 160 caracteres.
1553
- 2. Envie o resultado chamando \`submit_job_result({ jobId: "${job.id}", result: { rewritten: { ... } } })\`.
1554
- 3. Rearme a escuta chamando \`watch_linkegringo\`.
1555
- `.trim();
1556
- }
1557
- return {
1558
- content: [
1559
- {
1560
- type: "text",
1561
- text: `\u{1F680} **NOVO JOB RECEBIDO DO LINKEGRINGO**: \`${job.id}\`
1562
-
1563
- ${details}
1564
-
1565
- ${instructions}`
1566
- }
1567
- ],
1568
- structuredData: {
1569
- jobFound: true,
1570
- job
1571
- }
1572
- };
1573
- }
1574
1645
  async function handleWatchLinkeGringo(input = {}) {
1575
1646
  const timeoutSeconds = Math.min(Math.max(input.timeoutSeconds ?? 60, 0.1), 300);
1576
1647
  const timeoutMs = timeoutSeconds * 1e3;
@@ -1653,7 +1724,7 @@ function createLinkeGringoMcpServer() {
1653
1724
  server.registerTool(
1654
1725
  "get_pending_job",
1655
1726
  {
1656
- description: "Busca o pr\xF3ximo job pendente enviado pela interface gr\xE1fica web do LinkeGringo (ex: upload de perfil para diagn\xF3stico, gera\xE7\xE3o de entrevista, reescrita de perfil).",
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.",
1657
1728
  inputSchema: getPendingJobInputSchema.shape
1658
1729
  },
1659
1730
  async (args) => {
@@ -1663,7 +1734,7 @@ function createLinkeGringoMcpServer() {
1663
1734
  server.registerTool(
1664
1735
  "submit_job_result",
1665
1736
  {
1666
- description: "Envia o resultado processado pelo agente de IA de volta para a interface gr\xE1fica web do LinkeGringo, desbloqueando o front-end e atualizando a tela imediatamente.",
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.",
1667
1738
  inputSchema: submitJobResultInputSchema.shape
1668
1739
  },
1669
1740
  async (args) => {
@@ -1673,7 +1744,7 @@ function createLinkeGringoMcpServer() {
1673
1744
  server.registerTool(
1674
1745
  "watch_linkegringo",
1675
1746
  {
1676
- description: "Inicia a escuta ativa por jobs enviados pela interface web do LinkeGringo (upload de perfil para diagn\xF3stico, entrevista t\xE9cnica, reescrita de perfil). Aguarda o job e o retorna imediatamente para processamento.",
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.',
1677
1748
  inputSchema: watchLinkeGringoInputSchema.shape
1678
1749
  },
1679
1750
  async (args) => {
@@ -2204,16 +2275,17 @@ function startBridgeWatcher(options = {}) {
2204
2275
  console.log(
2205
2276
  `[LinkeGringo Watcher] \u{1F7E2} Conectado ao bridge (${bridgeUrl}). Watcher ativo: ${payload.watcherId} (Jobs pendentes: ${payload.pendingCount})`
2206
2277
  );
2207
- if (options.once && payload.pendingCount > 0) {
2278
+ if (payload.pendingCount > 0 && options.once) {
2208
2279
  console.log(
2209
- `[LinkeGringo Watcher] \u{1F680} JOB DETECTADO NA FILA (${payload.pendingCount} pendente(s)). Finalizando para ativar agente...`
2280
+ `[LinkeGringo Watcher] \u{1F680} JOB DETECTADO NA FILA (${payload.pendingCount} pendente(s)). Chame get_pending_job para processar e submit_job_result para responder.`
2210
2281
  );
2211
2282
  setTimeout(() => process.exit(0), 50);
2212
2283
  }
2213
2284
  } else if (payload.type === "job_created") {
2214
2285
  console.log(
2215
2286
  `
2216
- [LinkeGringo Watcher] \u{1F680} NOVO JOB DETECTADO: ${payload.job.id} | Tipo: ${payload.job.type}`
2287
+ [LinkeGringo Watcher] \u{1F680} NOVO JOB DETECTADO: ${payload.job.id} | Tipo: ${payload.job.type}
2288
+ [LinkeGringo Watcher] \u{1F449} Chame get_pending_job({ jobId: '${payload.job.id}' }) e envie o resultado direto com submit_job_result.`
2217
2289
  );
2218
2290
  options.onJob?.(payload.job);
2219
2291
  if (options.once) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linkegringo/mcp",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Servidor MCP oficial do LinkeGringo para auditoria e otimização de perfis para o mercado internacional",
5
5
  "type": "module",
6
6
  "bin": {