@loupekit/mcp 0.10.23 → 0.10.24-next.71

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 (2) hide show
  1. package/dist/index.js +99 -2
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -164,6 +164,66 @@ var AgentRegistry = class {
164
164
  }
165
165
  };
166
166
 
167
+ // src/bridge/presence-registry.ts
168
+ import {
169
+ PEER_TTL_MS,
170
+ heartbeatPresence,
171
+ joinPresence,
172
+ leavePresence,
173
+ peerId,
174
+ sweepPresence
175
+ } from "@loupekit/shared";
176
+ var PresenceRegistry = class {
177
+ peers = [];
178
+ ttlMs;
179
+ now;
180
+ // Explicit fields, not constructor parameter properties — Node's strip-only
181
+ // TypeScript mode rejects those and this file runs unbundled from source.
182
+ constructor(ttlMs = PEER_TTL_MS, now = Date.now) {
183
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0) throw new Error("ttlMs must be positive");
184
+ this.ttlMs = ttlMs;
185
+ this.now = now;
186
+ }
187
+ /** Join or refresh. The same person on the same page is one peer, not two. */
188
+ join(input) {
189
+ this.peers = joinPresence(this.peers, input, this.now());
190
+ return this.peers.find((p) => p.id === peerId(input.url, input.userId));
191
+ }
192
+ heartbeat(id) {
193
+ const before = this.peers.length;
194
+ this.peers = heartbeatPresence(this.peers, id, this.now());
195
+ if (this.peers.length !== before) return null;
196
+ return this.peers.find((p) => p.id === id) ?? null;
197
+ }
198
+ leave(id) {
199
+ const had = this.peers.some((p) => p.id === id);
200
+ this.peers = leavePresence(this.peers, id);
201
+ return had;
202
+ }
203
+ /** Everyone on a page, the viewer excluded — "who else is here". */
204
+ list(url, viewerId) {
205
+ const live = this.peers;
206
+ if (!url) return live;
207
+ return live.filter((p) => p.url === url && p.id !== viewerId);
208
+ }
209
+ /**
210
+ * Drop anything silent for the TTL. Returns what went, so the caller can broadcast —
211
+ * a peer vanishing matters as much as one arriving.
212
+ */
213
+ sweep() {
214
+ const before = this.peers;
215
+ const after = sweepPresence(before, this.now(), this.ttlMs);
216
+ if (after.length === before.length) return [];
217
+ const kept = new Set(after.map((p) => p.id));
218
+ const gone = before.filter((p) => !kept.has(p.id));
219
+ this.peers = after;
220
+ return gone;
221
+ }
222
+ size() {
223
+ return this.peers.length;
224
+ }
225
+ };
226
+
167
227
  // src/bridge/events.ts
168
228
  var EventBus = class {
169
229
  subscribers = /* @__PURE__ */ new Set();
@@ -315,6 +375,7 @@ async function handle(req, res, deps) {
315
375
  ok: true,
316
376
  selections: deps.store.size(),
317
377
  agents: deps.registry.size(),
378
+ peers: deps.presence.size(),
318
379
  subscribers: deps.bus.count()
319
380
  }, origin);
320
381
  }
@@ -381,6 +442,40 @@ async function handle(req, res, deps) {
381
442
  const id = path.slice("/agents/".length);
382
443
  return send(res, 200, { ok: deps.registry.unregister(id) }, origin);
383
444
  }
445
+ if (method === "POST" && path === "/presence") {
446
+ const body = await readBody(req);
447
+ if (!body.ok) return send(res, 400, { error: body.error }, origin);
448
+ let parsed;
449
+ try {
450
+ parsed = JSON.parse(body.text);
451
+ } catch {
452
+ return send(res, 400, { error: "body is not valid JSON" }, origin);
453
+ }
454
+ const join2 = parsed;
455
+ if (!join2 || typeof join2 !== "object" || !join2.url || !join2.userId || !join2.name) {
456
+ return send(res, 400, { error: "url, userId and name are required" }, origin);
457
+ }
458
+ const peer = deps.presence.join({ url: join2.url, userId: join2.userId, name: join2.name, tab: join2.tab });
459
+ deps.bus.publish({ type: "presence", data: deps.presence.list(), at: (/* @__PURE__ */ new Date()).toISOString() });
460
+ return send(res, 201, { ok: true, peer, peers: deps.presence.list(join2.url, peer.id) }, origin);
461
+ }
462
+ if (method === "GET" && path === "/presence") {
463
+ const pageUrl = url.searchParams.get("url") ?? void 0;
464
+ const viewer = url.searchParams.get("viewer") ?? void 0;
465
+ return send(res, 200, { peers: deps.presence.list(pageUrl, viewer) }, origin);
466
+ }
467
+ if (method === "POST" && path.startsWith("/presence/") && path.endsWith("/heartbeat")) {
468
+ const id = path.slice("/presence/".length, -"/heartbeat".length);
469
+ const peer = deps.presence.heartbeat(id);
470
+ if (!peer) return send(res, 404, { error: "unknown peer" }, origin);
471
+ return send(res, 200, { ok: true, peer }, origin);
472
+ }
473
+ if (method === "DELETE" && path.startsWith("/presence/")) {
474
+ const id = path.slice("/presence/".length);
475
+ const gone = deps.presence.leave(id);
476
+ if (gone) deps.bus.publish({ type: "presence", data: deps.presence.list(), at: (/* @__PURE__ */ new Date()).toISOString() });
477
+ return send(res, 200, { ok: gone }, origin);
478
+ }
384
479
  if (method === "GET" && (path === "/events" || path === "/thread-updates")) {
385
480
  return streamEvents(req, res, deps, path === "/thread-updates" ? "thread" : null, origin);
386
481
  }
@@ -1463,6 +1558,7 @@ var PROJECT_KEY = process.env.LOUPE_PROJECT_KEY || "pk_demo_acme";
1463
1558
  var BRIDGE_PORT = Number(process.env.LOUPE_BRIDGE_PORT ?? 9800);
1464
1559
  var store = new SelectionStore(Number(process.env.LOUPE_SELECTION_CAP ?? 50));
1465
1560
  var registry = new AgentRegistry();
1561
+ var presence = new PresenceRegistry();
1466
1562
  var bus = new EventBus();
1467
1563
  var ADMIN = process.env.LOUPE_ADMIN_KEY || "";
1468
1564
  async function api(path, init) {
@@ -1612,7 +1708,7 @@ async function proposeChange({ id, html, css, notes }) {
1612
1708
  bus.publishThread(id, "preview_live", { author: proposal.author });
1613
1709
  return wrap(`Proposal saved for #${id}. The dev team can now review your modified HTML/CSS in the dashboard.`);
1614
1710
  }
1615
- var server = new McpServer({ name: "loupe", version: "0.10.23" });
1711
+ var server = new McpServer({ name: "loupe", version: "0.10.24" });
1616
1712
  server.tool(
1617
1713
  "list_comments",
1618
1714
  "List Loupe product-feedback comments for the project as a task backlog. Each item carries its board stage, priority and change type, so you can start with the most urgent. Use this to see what a PM has flagged, then work through the items.",
@@ -1799,7 +1895,7 @@ function isEntrypoint() {
1799
1895
  }
1800
1896
  }
1801
1897
  if (isEntrypoint()) {
1802
- const bridge = BRIDGE_PORT ? await startHttpBridge(BRIDGE_PORT, { store, registry, bus }) : null;
1898
+ const bridge = BRIDGE_PORT ? await startHttpBridge(BRIDGE_PORT, { store, registry, presence, bus }) : null;
1803
1899
  const self = registry.register({
1804
1900
  name: process.env.LOUPE_AGENT_NAME || "loupe-mcp",
1805
1901
  type: process.env.LOUPE_AGENT_TYPE || "claude-code",
@@ -1808,6 +1904,7 @@ if (isEntrypoint()) {
1808
1904
  });
1809
1905
  const sweep = setInterval(() => {
1810
1906
  if (registry.sweep().length) bus.publish({ type: "agents", data: registry.list(), at: (/* @__PURE__ */ new Date()).toISOString() });
1907
+ if (presence.sweep().length) bus.publish({ type: "presence", data: presence.list(), at: (/* @__PURE__ */ new Date()).toISOString() });
1811
1908
  registry.heartbeat(self.id);
1812
1909
  }, AGENT_SWEEP_MS);
1813
1910
  if (typeof sweep.unref === "function") sweep.unref();
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "email": "m.ashraf.saed@gmail.com",
6
6
  "url": "https://www.linkedin.com/in/mohamedashrafelsaed/"
7
7
  },
8
- "version": "0.10.23",
8
+ "version": "0.10.24-next.71",
9
9
  "description": "MCP server that exposes Loupe comments to Claude Code as an actionable, fully-contextualized backlog.",
10
10
  "keywords": [
11
11
  "loupe",
@@ -46,6 +46,6 @@
46
46
  "zod": "^3.24.1"
47
47
  },
48
48
  "devDependencies": {
49
- "@loupekit/shared": "0.10.23"
49
+ "@loupekit/shared": "0.10.24-next.71"
50
50
  }
51
51
  }