@loupekit/mcp 0.10.23 → 0.10.24-next.70
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.js +99 -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.
|
|
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.
|
|
8
|
+
"version": "0.10.24-next.70",
|
|
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.
|
|
49
|
+
"@loupekit/shared": "0.10.24-next.70"
|
|
50
50
|
}
|
|
51
51
|
}
|