@linkegringo/mcp 1.0.6 → 1.0.8

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
@@ -72,6 +72,24 @@ declare function completeRemoteOrLocalJob(jobId: string, result: any, bridgeUrl?
72
72
  */
73
73
  declare function failRemoteOrLocalJob(jobId: string, error: string, bridgeUrl?: string): Promise<BridgeJob>;
74
74
 
75
+ interface WatcherOptions {
76
+ bridgeUrl?: string;
77
+ once?: boolean;
78
+ onJob?: (job: {
79
+ id: string;
80
+ type: string;
81
+ createdAt: number;
82
+ }) => void;
83
+ onConnect?: (info: {
84
+ watcherId: string;
85
+ pendingCount: number;
86
+ }) => void;
87
+ onError?: (err: Error) => void;
88
+ }
89
+ declare function startBridgeWatcher(options?: WatcherOptions): {
90
+ close: () => void;
91
+ };
92
+
75
93
  declare class BridgeJobStore extends EventEmitter {
76
94
  private jobs;
77
95
  private pendingQueue;
@@ -84,6 +102,16 @@ declare class BridgeJobStore extends EventEmitter {
84
102
  waitForJob(id: string, timeoutMs?: number): Promise<BridgeJob>;
85
103
  getAllJobs(): BridgeJob[];
86
104
  getPendingCount(): number;
105
+ private activeWatchers;
106
+ registerWatcher(id: string): void;
107
+ unregisterWatcher(id: string): void;
108
+ getWatcherCount(): number;
109
+ hasActiveWatcher(): boolean;
110
+ cancelAllPending(reason?: string): number;
111
+ resetQueue(reason?: string): {
112
+ canceledCount: number;
113
+ clearedCount: number;
114
+ };
87
115
  clear(): void;
88
116
  }
89
117
  declare const bridgeJobStore: BridgeJobStore;
@@ -439,4 +467,4 @@ declare function handleSubmitJobResult(input: SubmitJobResultInput): Promise<{
439
467
  };
440
468
  }>;
441
469
 
442
- export { type AuditIssue, type AuditProfileInput, type BridgeJob, type BridgeServerOptions, type ConvertToXyzBulletInput, type GenerateHeadlineInput, type GetPendingJobInput, type InstallResult, type InstallerOptions, type JobStatus, type JobType, type McpTarget, type SimulateRecruiterSearchInput, type SparseExperience, type SubmitJobResultInput, auditProfileInputSchema, bridgeJobStore, completeRemoteOrLocalJob, convertToXyzBulletInputSchema, createBridgeHttpServer, createLinkeGringoMcpServer, detectSparseExperiences, failRemoteOrLocalJob, formatGoogleXyzBullet, generateHeadlineInputSchema, getExperienceBulletCount, getMcpConfigsForSystem, getPendingJobInputSchema, getRemoteOrLocalPendingJob, handleAuditProfile, handleConvertToXyzBullet, handleGenerateHeadline, handleGetPendingJob, handleSimulateRecruiterSearch, handleSubmitJobResult, installMcpServerConfig, parseArgs, runInstaller, simulateRecruiterSearchInputSchema, startBridgeServer, stopBridgeServer, submitJobResultInputSchema };
470
+ export { type AuditIssue, type AuditProfileInput, type BridgeJob, type BridgeServerOptions, type ConvertToXyzBulletInput, type GenerateHeadlineInput, type GetPendingJobInput, type InstallResult, type InstallerOptions, type JobStatus, type JobType, type McpTarget, type SimulateRecruiterSearchInput, type SparseExperience, type SubmitJobResultInput, type WatcherOptions, auditProfileInputSchema, bridgeJobStore, completeRemoteOrLocalJob, convertToXyzBulletInputSchema, createBridgeHttpServer, createLinkeGringoMcpServer, detectSparseExperiences, failRemoteOrLocalJob, formatGoogleXyzBullet, generateHeadlineInputSchema, getExperienceBulletCount, getMcpConfigsForSystem, getPendingJobInputSchema, getRemoteOrLocalPendingJob, handleAuditProfile, handleConvertToXyzBullet, handleGenerateHeadline, handleGetPendingJob, handleSimulateRecruiterSearch, handleSubmitJobResult, installMcpServerConfig, parseArgs, runInstaller, simulateRecruiterSearchInputSchema, startBridgeServer, startBridgeWatcher, stopBridgeServer, submitJobResultInputSchema };
package/dist/index.js CHANGED
@@ -117,9 +117,51 @@ var BridgeJobStore = class extends EventEmitter {
117
117
  getPendingCount() {
118
118
  return this.jobs.size > 0 ? Array.from(this.jobs.values()).filter((j) => j.status === "pending" || j.status === "processing").length : 0;
119
119
  }
120
+ // Watcher tracking
121
+ activeWatchers = /* @__PURE__ */ new Set();
122
+ registerWatcher(id) {
123
+ this.activeWatchers.add(id);
124
+ this.emit("watcher:connected", id);
125
+ }
126
+ unregisterWatcher(id) {
127
+ this.activeWatchers.delete(id);
128
+ this.emit("watcher:disconnected", id);
129
+ }
130
+ getWatcherCount() {
131
+ return this.activeWatchers.size;
132
+ }
133
+ hasActiveWatcher() {
134
+ return this.activeWatchers.size > 0;
135
+ }
136
+ cancelAllPending(reason = "Cancelado pelo usu\xE1rio via interface web") {
137
+ let count = 0;
138
+ while (this.pendingQueue.length > 0) {
139
+ const id = this.pendingQueue.shift();
140
+ const job = this.jobs.get(id);
141
+ if (job && (job.status === "pending" || job.status === "processing")) {
142
+ job.status = "failed";
143
+ job.error = reason;
144
+ job.updatedAt = Date.now();
145
+ count++;
146
+ this.emit(`job:${id}`, job);
147
+ this.emit("job:failed", job);
148
+ }
149
+ }
150
+ this.emit("queue:cleared", count);
151
+ return count;
152
+ }
153
+ resetQueue(reason = "Cancelado pelo usu\xE1rio via interface web") {
154
+ const canceledCount = this.cancelAllPending(reason);
155
+ const clearedCount = this.jobs.size;
156
+ this.jobs.clear();
157
+ this.pendingQueue = [];
158
+ this.emit("queue:reset", { canceledCount, clearedCount });
159
+ return { canceledCount, clearedCount };
160
+ }
120
161
  clear() {
121
162
  this.jobs.clear();
122
163
  this.pendingQueue = [];
164
+ this.activeWatchers.clear();
123
165
  this.removeAllListeners();
124
166
  }
125
167
  };
@@ -1670,6 +1712,7 @@ async function parseBody(req) {
1670
1712
  });
1671
1713
  }
1672
1714
  function createBridgeHttpServer(options = {}) {
1715
+ const activeSseClients = /* @__PURE__ */ new Map();
1673
1716
  const server = http.createServer(async (req, res) => {
1674
1717
  setCorsHeaders(res);
1675
1718
  if (req.method === "OPTIONS") {
@@ -1684,11 +1727,98 @@ function createBridgeHttpServer(options = {}) {
1684
1727
  sendJson(res, 200, {
1685
1728
  status: "ok",
1686
1729
  server: "linkegringo-mcp-bridge",
1730
+ watcherConnected: bridgeJobStore.hasActiveWatcher(),
1731
+ watcherCount: bridgeJobStore.getWatcherCount(),
1732
+ pendingCount: bridgeJobStore.getPendingCount(),
1733
+ totalJobs: bridgeJobStore.getAllJobs().length
1734
+ });
1735
+ return;
1736
+ }
1737
+ if (req.method === "GET" && pathname === "/api/bridge/status") {
1738
+ const pendingJob = bridgeJobStore.getPendingJob();
1739
+ sendJson(res, 200, {
1740
+ ok: true,
1741
+ status: "ready",
1742
+ server: "linkegringo-mcp-bridge",
1743
+ watcherConnected: bridgeJobStore.hasActiveWatcher(),
1744
+ watcherCount: bridgeJobStore.getWatcherCount(),
1687
1745
  pendingCount: bridgeJobStore.getPendingCount(),
1746
+ activeJobId: pendingJob ? pendingJob.id : null,
1688
1747
  totalJobs: bridgeJobStore.getAllJobs().length
1689
1748
  });
1690
1749
  return;
1691
1750
  }
1751
+ if (req.method === "POST" && pathname === "/api/bridge/disconnect") {
1752
+ for (const [id, client] of activeSseClients.entries()) {
1753
+ try {
1754
+ client.res.write(
1755
+ `data: ${JSON.stringify({ type: "disconnect", message: "Desconectado pelo usu\xE1rio na interface web." })}
1756
+
1757
+ `
1758
+ );
1759
+ client.res.end();
1760
+ } catch {
1761
+ }
1762
+ client.cleanup();
1763
+ }
1764
+ activeSseClients.clear();
1765
+ const { canceledCount, clearedCount } = bridgeJobStore.resetQueue();
1766
+ sendJson(res, 200, {
1767
+ ok: true,
1768
+ message: "Conex\xE3o cancelada e fila de jobs limpa com sucesso.",
1769
+ canceledCount,
1770
+ clearedCount
1771
+ });
1772
+ return;
1773
+ }
1774
+ if (req.method === "GET" && pathname === "/api/jobs/stream") {
1775
+ res.writeHead(200, {
1776
+ "Content-Type": "text/event-stream",
1777
+ "Cache-Control": "no-cache, no-transform",
1778
+ "Connection": "keep-alive",
1779
+ "Access-Control-Allow-Origin": "*"
1780
+ });
1781
+ res.flushHeaders?.();
1782
+ const watcherId = `watcher_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
1783
+ bridgeJobStore.registerWatcher(watcherId);
1784
+ res.write(`data: ${JSON.stringify({ type: "connected", watcherId, pendingCount: bridgeJobStore.getPendingCount() })}
1785
+
1786
+ `);
1787
+ const onJobCreated = (job) => {
1788
+ res.write(`data: ${JSON.stringify({ type: "job_created", job: { id: job.id, type: job.type, createdAt: job.createdAt } })}
1789
+
1790
+ `);
1791
+ };
1792
+ const onJobCompleted = (job) => {
1793
+ res.write(`data: ${JSON.stringify({ type: "job_completed", job: { id: job.id, type: job.type } })}
1794
+
1795
+ `);
1796
+ };
1797
+ const onJobFailed = (job) => {
1798
+ res.write(`data: ${JSON.stringify({ type: "job_failed", job: { id: job.id, type: job.type, error: job.error } })}
1799
+
1800
+ `);
1801
+ };
1802
+ bridgeJobStore.on("job:created", onJobCreated);
1803
+ bridgeJobStore.on("job:completed", onJobCompleted);
1804
+ bridgeJobStore.on("job:failed", onJobFailed);
1805
+ const heartbeat = setInterval(() => {
1806
+ res.write(`: heartbeat
1807
+
1808
+ `);
1809
+ }, 15e3);
1810
+ const cleanup = () => {
1811
+ clearInterval(heartbeat);
1812
+ bridgeJobStore.unregisterWatcher(watcherId);
1813
+ bridgeJobStore.off("job:created", onJobCreated);
1814
+ bridgeJobStore.off("job:completed", onJobCompleted);
1815
+ bridgeJobStore.off("job:failed", onJobFailed);
1816
+ activeSseClients.delete(watcherId);
1817
+ };
1818
+ activeSseClients.set(watcherId, { res, cleanup });
1819
+ req.on("close", cleanup);
1820
+ return;
1821
+ }
1692
1822
  if (req.method === "POST" && pathname === "/api/jobs") {
1693
1823
  const body = await parseBody(req);
1694
1824
  if (!body.type) {
@@ -1700,6 +1830,10 @@ function createBridgeHttpServer(options = {}) {
1700
1830
  sendJson(res, 201, { ok: true, job });
1701
1831
  return;
1702
1832
  }
1833
+ if (req.method === "GET" && pathname === "/api/jobs") {
1834
+ sendJson(res, 200, { ok: true, jobs: bridgeJobStore.getAllJobs() });
1835
+ return;
1836
+ }
1703
1837
  if (req.method === "GET" && pathname === "/api/jobs/pending") {
1704
1838
  const id = parsedUrl.searchParams.get("id") || void 0;
1705
1839
  const job = bridgeJobStore.getPendingJob(id);
@@ -1810,6 +1944,137 @@ async function stopBridgeServer() {
1810
1944
  });
1811
1945
  }
1812
1946
 
1947
+ // src/bridge/watcher.ts
1948
+ import http2 from "http";
1949
+ function startBridgeWatcher(options = {}) {
1950
+ const bridgeUrl = options.bridgeUrl || process.env.LINKEGRINGO_BRIDGE_URL || "http://127.0.0.1:5174";
1951
+ let isClosed = false;
1952
+ let currentReq = null;
1953
+ let retryTimer = null;
1954
+ let retryDelay = 1e3;
1955
+ function connect() {
1956
+ if (isClosed) return;
1957
+ try {
1958
+ const url = new URL("/api/jobs/stream", bridgeUrl);
1959
+ const req = http2.get(
1960
+ url.toString(),
1961
+ {
1962
+ headers: {
1963
+ Accept: "text/event-stream",
1964
+ "Cache-Control": "no-cache"
1965
+ }
1966
+ },
1967
+ (res) => {
1968
+ if (res.statusCode !== 200) {
1969
+ options.onError?.(new Error(`Falha ao conectar no stream SSE: HTTP ${res.statusCode}`));
1970
+ scheduleRetry();
1971
+ return;
1972
+ }
1973
+ retryDelay = 1e3;
1974
+ let buffer = "";
1975
+ res.on("data", (chunk) => {
1976
+ buffer += chunk.toString("utf-8");
1977
+ const lines = buffer.split("\n\n");
1978
+ buffer = lines.pop() || "";
1979
+ for (const block of lines) {
1980
+ for (const line of block.split("\n")) {
1981
+ if (line.startsWith("data: ")) {
1982
+ try {
1983
+ const payload = JSON.parse(line.slice(6));
1984
+ if (payload.type === "connected") {
1985
+ options.onConnect?.(payload);
1986
+ console.log(
1987
+ `[LinkeGringo Watcher] \u{1F7E2} Conectado ao bridge (${bridgeUrl}). Watcher ativo: ${payload.watcherId} (Jobs pendentes: ${payload.pendingCount})`
1988
+ );
1989
+ if (options.once && payload.pendingCount > 0) {
1990
+ console.log(
1991
+ `[LinkeGringo Watcher] \u{1F680} JOB DETECTADO NA FILA (${payload.pendingCount} pendente(s)). Finalizando para ativar agente...`
1992
+ );
1993
+ setTimeout(() => process.exit(0), 50);
1994
+ }
1995
+ } else if (payload.type === "job_created") {
1996
+ console.log(
1997
+ `
1998
+ [LinkeGringo Watcher] \u{1F680} NOVO JOB DETECTADO: ${payload.job.id} | Tipo: ${payload.job.type}`
1999
+ );
2000
+ options.onJob?.(payload.job);
2001
+ if (options.once) {
2002
+ setTimeout(() => process.exit(0), 50);
2003
+ }
2004
+ } else if (payload.type === "job_completed") {
2005
+ console.log(`[LinkeGringo Watcher] \u2705 Job finalizado: ${payload.job.id}`);
2006
+ } else if (payload.type === "job_failed") {
2007
+ console.log(
2008
+ `[LinkeGringo Watcher] \u26A0\uFE0F Job cancelado ou com erro: ${payload.job.id} (${payload.job.error})`
2009
+ );
2010
+ } else if (payload.type === "disconnect") {
2011
+ console.log(
2012
+ `
2013
+ [LinkeGringo Watcher] \u{1F6D1} Conex\xE3o encerrada pelo servidor (desconectado via interface web).`
2014
+ );
2015
+ isClosed = true;
2016
+ if (currentReq) {
2017
+ currentReq.destroy();
2018
+ currentReq = null;
2019
+ }
2020
+ setTimeout(() => process.exit(0), 50);
2021
+ }
2022
+ } catch {
2023
+ }
2024
+ }
2025
+ }
2026
+ }
2027
+ });
2028
+ res.on("end", () => {
2029
+ if (!isClosed) {
2030
+ scheduleRetry();
2031
+ }
2032
+ });
2033
+ res.on("error", (err) => {
2034
+ options.onError?.(err);
2035
+ if (!isClosed) {
2036
+ scheduleRetry();
2037
+ }
2038
+ });
2039
+ }
2040
+ );
2041
+ req.on("error", (err) => {
2042
+ options.onError?.(err);
2043
+ if (!isClosed) {
2044
+ scheduleRetry();
2045
+ }
2046
+ });
2047
+ currentReq = req;
2048
+ } catch (err) {
2049
+ options.onError?.(err);
2050
+ scheduleRetry();
2051
+ }
2052
+ }
2053
+ function scheduleRetry() {
2054
+ if (isClosed || retryTimer) return;
2055
+ retryTimer = setTimeout(() => {
2056
+ retryTimer = null;
2057
+ retryDelay = Math.min(retryDelay * 1.5, 1e4);
2058
+ connect();
2059
+ }, retryDelay);
2060
+ }
2061
+ connect();
2062
+ return {
2063
+ close() {
2064
+ isClosed = true;
2065
+ if (retryTimer) {
2066
+ clearTimeout(retryTimer);
2067
+ retryTimer = null;
2068
+ }
2069
+ if (currentReq) {
2070
+ currentReq.destroy();
2071
+ currentReq = null;
2072
+ }
2073
+ console.log("[LinkeGringo Watcher] Modo watch desconectado.");
2074
+ }
2075
+ };
2076
+ }
2077
+
1813
2078
  // src/index.ts
1814
2079
  import fs2 from "fs";
1815
2080
  import { fileURLToPath } from "url";
@@ -1818,6 +2083,12 @@ async function main() {
1818
2083
  runInstaller(process.argv);
1819
2084
  return;
1820
2085
  }
2086
+ if (process.argv.includes("watch") || process.argv.includes("--watch") || process.argv.includes("listen")) {
2087
+ const once = process.argv.includes("--once") || process.argv.includes("-1");
2088
+ console.log(`[LinkeGringo Watcher] Iniciando modo escuta ultraleve${once ? " (once)" : ""}...`);
2089
+ startBridgeWatcher({ once });
2090
+ return;
2091
+ }
1821
2092
  try {
1822
2093
  await startBridgeServer();
1823
2094
  } catch (err) {
@@ -1870,6 +2141,7 @@ export {
1870
2141
  runInstaller,
1871
2142
  simulateRecruiterSearchInputSchema,
1872
2143
  startBridgeServer,
2144
+ startBridgeWatcher,
1873
2145
  stopBridgeServer,
1874
2146
  submitJobResultInputSchema
1875
2147
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linkegringo/mcp",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
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": {