@vibevibes/mcp 0.6.0 → 0.7.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.
- package/dist/index.js +0 -26
- package/dist/server.js +1 -448
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -423,28 +423,6 @@ Call this first, then use act to interact. The stop hook keeps you present.`, {
|
|
|
423
423
|
}
|
|
424
424
|
throw stateErr;
|
|
425
425
|
}
|
|
426
|
-
// Fetch slot definitions to detect unfilled autoSpawn AI slots
|
|
427
|
-
let unfilledSlots = [];
|
|
428
|
-
try {
|
|
429
|
-
const slotsData = await fetchJSON("/slots", { timeoutMs: 5000 });
|
|
430
|
-
if (slotsData?.slots) {
|
|
431
|
-
const currentParticipants = slotsData.participantDetails || [];
|
|
432
|
-
for (const slot of slotsData.slots) {
|
|
433
|
-
if (slot.type !== "ai" && slot.type !== "any")
|
|
434
|
-
continue;
|
|
435
|
-
if (!slot.autoSpawn)
|
|
436
|
-
continue;
|
|
437
|
-
const max = slot.maxInstances ?? 1;
|
|
438
|
-
const filled = currentParticipants.filter(p => p.role === slot.role && p.type === "ai").length;
|
|
439
|
-
if (filled < max) {
|
|
440
|
-
unfilledSlots.push({ role: slot.role, autoSpawn: slot.autoSpawn, maxInstances: max });
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
catch {
|
|
446
|
-
// Slots endpoint not available — skip silently
|
|
447
|
-
}
|
|
448
426
|
const outputParts = [
|
|
449
427
|
`Connected as ${identity.actorId}`,
|
|
450
428
|
connections.size > 1
|
|
@@ -461,10 +439,6 @@ Call this first, then use act to interact. The stop hook keeps you present.`, {
|
|
|
461
439
|
outputParts.push(`Your instructions:`, systemPrompt, ``);
|
|
462
440
|
}
|
|
463
441
|
outputParts.push(formatState(state.sharedState, joinData?.observation || state.observation), `Participants: ${state.participants?.join(", ")}`, ``, `Tools:`, formatToolList(joinData?.tools || state.tools || []), ``);
|
|
464
|
-
// Surface unfilled autoSpawn slots
|
|
465
|
-
if (unfilledSlots.length > 0) {
|
|
466
|
-
outputParts.push(`UNFILLED AI SLOTS (autoSpawn):`, ...unfilledSlots.map(s => ` - ${s.role} (max ${s.maxInstances ?? 1})`), ``, `These roles need AI agents. Spawn independent teammates — each gets its own MCP session and identity.`, ` Agent(subagent_type="general-purpose", model="sonnet", run_in_background=true,`, ` prompt="Call connect with role='ROLE'. Then play your role.")`, ``);
|
|
467
|
-
}
|
|
468
442
|
outputParts.push(`Use act() to interact. The stop hook keeps you present.`);
|
|
469
443
|
// Register the stop hook so Claude Code wakes us on events
|
|
470
444
|
ensureStopHook();
|
package/dist/server.js
CHANGED
|
@@ -215,13 +215,8 @@ const DEFAULT_PORT = 4321;
|
|
|
215
215
|
const MAX_EVENTS = 200;
|
|
216
216
|
const JOIN_EVENT_HISTORY = 20;
|
|
217
217
|
const ROOM_STATE_EVENT_HISTORY = 50;
|
|
218
|
-
const HISTORY_DEFAULT_LIMIT = 50;
|
|
219
|
-
const HISTORY_MAX_LIMIT = 200;
|
|
220
|
-
const DEFAULT_STREAM_RATE_LIMIT = 60;
|
|
221
|
-
const STREAM_RATE_WINDOW_MS = 1000;
|
|
222
218
|
const EVENT_BATCH_DEBOUNCE_MS = 50;
|
|
223
219
|
const MAX_BATCH_CALLS = 10;
|
|
224
|
-
const LONG_POLL_MAX_TIMEOUT_MS = 55000;
|
|
225
220
|
const AGENT_CONTEXT_MAX_TIMEOUT_MS = 10000;
|
|
226
221
|
const WS_MAX_PAYLOAD_BYTES = 1024 * 1024;
|
|
227
222
|
const WS_EPHEMERAL_MAX_BYTES = 65536;
|
|
@@ -230,10 +225,7 @@ const HOT_RELOAD_DEBOUNCE_MS = 300;
|
|
|
230
225
|
const WS_CLOSE_GRACE_MS = 3000;
|
|
231
226
|
const JSON_BODY_LIMIT = "256kb";
|
|
232
227
|
const TOOL_HTTP_TIMEOUT_MS = 30_000;
|
|
233
|
-
const TOOL_REGEX_MAX_LENGTH = 100;
|
|
234
228
|
const ROOM_EVENTS_MAX_LISTENERS = 200;
|
|
235
|
-
const STREAM_RATE_LIMIT_STALE_MS = 5000;
|
|
236
|
-
const STREAM_RATE_LIMIT_CLEANUP_INTERVAL_MS = 10000;
|
|
237
229
|
const IDEMPOTENCY_CLEANUP_INTERVAL_MS = 60000;
|
|
238
230
|
// ── Default observe ────────────────────────────────────────
|
|
239
231
|
function defaultObserve(state, _event, _actorId) {
|
|
@@ -259,15 +251,6 @@ let experienceError = null;
|
|
|
259
251
|
// Hot-reload rebuild gate
|
|
260
252
|
let rebuildingResolve = null;
|
|
261
253
|
let rebuildingPromise = null;
|
|
262
|
-
// Stream rate limiting
|
|
263
|
-
const streamRateLimits = new Map();
|
|
264
|
-
const _streamRateCleanupTimer = setInterval(() => {
|
|
265
|
-
const now = Date.now();
|
|
266
|
-
for (const [key, entry] of streamRateLimits) {
|
|
267
|
-
if (now - entry.windowStart > STREAM_RATE_LIMIT_STALE_MS)
|
|
268
|
-
streamRateLimits.delete(key);
|
|
269
|
-
}
|
|
270
|
-
}, STREAM_RATE_LIMIT_CLEANUP_INTERVAL_MS);
|
|
271
254
|
export function setPublicUrl(url) {
|
|
272
255
|
publicUrl = url;
|
|
273
256
|
}
|
|
@@ -421,12 +404,6 @@ app.get("/state", (req, res) => {
|
|
|
421
404
|
observation,
|
|
422
405
|
});
|
|
423
406
|
});
|
|
424
|
-
// ── Slots endpoint ──────────────────────────────────────────
|
|
425
|
-
app.get("/slots", (_req, res) => {
|
|
426
|
-
const mod = getModule();
|
|
427
|
-
const slots = mod?.manifest?.participantSlots || mod?.participants || [];
|
|
428
|
-
res.json({ slots, participantDetails: room.participantDetails() });
|
|
429
|
-
});
|
|
430
407
|
// ── Participants endpoint ──────────────────────────────────
|
|
431
408
|
app.get("/participants", (_req, res) => res.json({ participants: room.participantDetails() }));
|
|
432
409
|
// ── Tools list endpoint ────────────────────────────────────
|
|
@@ -443,79 +420,10 @@ app.get("/tools-list", (req, res) => {
|
|
|
443
420
|
allowedTools = participant?.allowedTools;
|
|
444
421
|
}
|
|
445
422
|
const tools = getToolList(mod, allowedTools);
|
|
446
|
-
const streams = mod.streams
|
|
447
|
-
? mod.streams.map((s) => ({
|
|
448
|
-
name: s.name,
|
|
449
|
-
description: s.description || "",
|
|
450
|
-
rateLimit: s.rateLimit || DEFAULT_STREAM_RATE_LIMIT,
|
|
451
|
-
input_schema: s.input_schema ? zodToJsonSchema(s.input_schema) : {},
|
|
452
|
-
}))
|
|
453
|
-
: [];
|
|
454
423
|
res.json({
|
|
455
424
|
experienceId: mod.manifest?.id,
|
|
456
425
|
tools,
|
|
457
|
-
streams,
|
|
458
426
|
toolCount: tools.length,
|
|
459
|
-
streamCount: streams.length,
|
|
460
|
-
});
|
|
461
|
-
});
|
|
462
|
-
// ── History endpoint ───────────────────────────────────────
|
|
463
|
-
app.get("/history", (req, res) => {
|
|
464
|
-
const limit = Math.min(queryInt(req.query.limit) || HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT);
|
|
465
|
-
const since = queryInt(req.query.since);
|
|
466
|
-
const actor = queryString(req.query.actor);
|
|
467
|
-
const tool = queryString(req.query.tool);
|
|
468
|
-
const owner = queryString(req.query.owner);
|
|
469
|
-
let events = room.events;
|
|
470
|
-
if (since > 0)
|
|
471
|
-
events = events.filter(e => e.ts > since);
|
|
472
|
-
if (actor)
|
|
473
|
-
events = events.filter(e => e.actorId === actor);
|
|
474
|
-
if (owner)
|
|
475
|
-
events = events.filter(e => e.owner === owner);
|
|
476
|
-
if (tool) {
|
|
477
|
-
const SAFE_TOOL_REGEX = /^[a-zA-Z0-9._:\-]+$/;
|
|
478
|
-
if (tool.length <= TOOL_REGEX_MAX_LENGTH && SAFE_TOOL_REGEX.test(tool)) {
|
|
479
|
-
try {
|
|
480
|
-
const escaped = tool.replace(/\./g, '\\.');
|
|
481
|
-
const toolRegex = new RegExp('^' + escaped + '$');
|
|
482
|
-
events = events.filter(e => toolRegex.test(e.tool));
|
|
483
|
-
}
|
|
484
|
-
catch {
|
|
485
|
-
events = events.filter(e => e.tool === tool);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
else {
|
|
489
|
-
events = events.filter(e => e.tool === tool);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
const filteredTotal = events.length;
|
|
493
|
-
const result = events.slice(-limit);
|
|
494
|
-
res.json({
|
|
495
|
-
events: result,
|
|
496
|
-
total: room.events.length,
|
|
497
|
-
filtered: filteredTotal,
|
|
498
|
-
returned: result.length,
|
|
499
|
-
hasMore: filteredTotal > limit,
|
|
500
|
-
});
|
|
501
|
-
});
|
|
502
|
-
// ── Who endpoint ───────────────────────────────────────────
|
|
503
|
-
app.get("/who", (_req, res) => {
|
|
504
|
-
const participants = Array.from(room.participants.entries()).map(([actorId, p]) => {
|
|
505
|
-
let lastAction;
|
|
506
|
-
for (let i = room.events.length - 1; i >= 0; i--) {
|
|
507
|
-
if (room.events[i].actorId === actorId) {
|
|
508
|
-
lastAction = { tool: room.events[i].tool, ts: room.events[i].ts };
|
|
509
|
-
break;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
return { actorId, owner: p.owner, type: p.type, role: p.role, joinedAt: p.joinedAt, lastAction, allowedTools: p.allowedTools };
|
|
513
|
-
});
|
|
514
|
-
res.json({
|
|
515
|
-
participants,
|
|
516
|
-
count: participants.length,
|
|
517
|
-
humans: participants.filter(p => p.type === "human").length,
|
|
518
|
-
agents: participants.filter(p => p.type === "ai").length,
|
|
519
427
|
});
|
|
520
428
|
});
|
|
521
429
|
// ── Join ───────────────────────────────────────────────────
|
|
@@ -720,53 +628,6 @@ app.post("/leave", (req, res) => {
|
|
|
720
628
|
broadcastPresenceUpdate();
|
|
721
629
|
res.json({ left: true, actorId });
|
|
722
630
|
});
|
|
723
|
-
// ── Kick ───────────────────────────────────────────────────
|
|
724
|
-
app.post("/kick", (req, res) => {
|
|
725
|
-
const { kickerActorId, targetActorId } = req.body;
|
|
726
|
-
if (kickerActorId === targetActorId) {
|
|
727
|
-
res.status(400).json({ error: "Cannot kick yourself" });
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
const kicker = room.participants.get(kickerActorId);
|
|
731
|
-
if (!kicker || kicker.type !== "human") {
|
|
732
|
-
res.status(400).json({ error: "Only human participants can kick" });
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
if (!room.participants.has(targetActorId)) {
|
|
736
|
-
res.status(400).json({ error: "Participant not found" });
|
|
737
|
-
return;
|
|
738
|
-
}
|
|
739
|
-
const targetParticipant = room.participants.get(targetActorId);
|
|
740
|
-
room.participants.delete(targetActorId);
|
|
741
|
-
if (room.kickedActors.size >= 500) {
|
|
742
|
-
const oldest = room.kickedActors.values().next().value;
|
|
743
|
-
if (oldest)
|
|
744
|
-
room.kickedActors.delete(oldest);
|
|
745
|
-
}
|
|
746
|
-
room.kickedActors.add(targetActorId);
|
|
747
|
-
if (targetParticipant?.owner) {
|
|
748
|
-
if (room.kickedOwners.size >= 500) {
|
|
749
|
-
const oldest = room.kickedOwners.values().next().value;
|
|
750
|
-
if (oldest)
|
|
751
|
-
room.kickedOwners.delete(oldest);
|
|
752
|
-
}
|
|
753
|
-
room.kickedOwners.add(targetParticipant.owner);
|
|
754
|
-
}
|
|
755
|
-
for (const [targetWs, wsActorId] of room.wsConnections.entries()) {
|
|
756
|
-
if (wsActorId === targetActorId) {
|
|
757
|
-
try {
|
|
758
|
-
targetWs.send(JSON.stringify({ type: "kicked", by: kickerActorId }));
|
|
759
|
-
targetWs.close();
|
|
760
|
-
}
|
|
761
|
-
catch { }
|
|
762
|
-
room.wsConnections.delete(targetWs);
|
|
763
|
-
break;
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
broadcastPresenceUpdate();
|
|
767
|
-
roomEvents.emit("room");
|
|
768
|
-
res.json({ kicked: true, actorId: targetActorId });
|
|
769
|
-
});
|
|
770
631
|
// ── Idempotency cache ────────────────────────────────────────
|
|
771
632
|
const idempotencyCache = new Map();
|
|
772
633
|
const IDEMPOTENCY_TTL = 30000;
|
|
@@ -1021,80 +882,6 @@ app.post("/tools-batch", async (req, res) => {
|
|
|
1021
882
|
});
|
|
1022
883
|
res.status(hasError ? 207 : 200).json({ results, observation: lastObservation });
|
|
1023
884
|
});
|
|
1024
|
-
// ── Events (supports long-poll via ?timeout=N) ──────────────
|
|
1025
|
-
app.get("/events", (req, res) => {
|
|
1026
|
-
const since = queryInt(req.query.since);
|
|
1027
|
-
const timeout = Math.min(queryInt(req.query.timeout), LONG_POLL_MAX_TIMEOUT_MS);
|
|
1028
|
-
const requestingActorId = queryString(req.query.actorId);
|
|
1029
|
-
const computeObservation = (events) => {
|
|
1030
|
-
const mod = getModule();
|
|
1031
|
-
if (!requestingActorId)
|
|
1032
|
-
return undefined;
|
|
1033
|
-
const agentObserve = mod?.observe ?? defaultObserve;
|
|
1034
|
-
try {
|
|
1035
|
-
const lastEvent = events.length > 0 ? events[events.length - 1] : null;
|
|
1036
|
-
return agentObserve(room.sharedState, lastEvent, requestingActorId);
|
|
1037
|
-
}
|
|
1038
|
-
catch (e) {
|
|
1039
|
-
console.error(`[observe] Error:`, toErrorMessage(e));
|
|
1040
|
-
return undefined;
|
|
1041
|
-
}
|
|
1042
|
-
};
|
|
1043
|
-
const getNewEvents = () => room.events.filter((e) => e.ts > since && e.actorId !== requestingActorId);
|
|
1044
|
-
let newEvents = getNewEvents();
|
|
1045
|
-
if (newEvents.length > 0 || timeout === 0) {
|
|
1046
|
-
res.json({
|
|
1047
|
-
events: newEvents,
|
|
1048
|
-
sharedState: room.sharedState,
|
|
1049
|
-
participants: room.participantList(),
|
|
1050
|
-
observation: computeObservation(newEvents),
|
|
1051
|
-
});
|
|
1052
|
-
return;
|
|
1053
|
-
}
|
|
1054
|
-
let responded = false;
|
|
1055
|
-
let batchTimer = null;
|
|
1056
|
-
const respond = () => {
|
|
1057
|
-
if (responded)
|
|
1058
|
-
return;
|
|
1059
|
-
responded = true;
|
|
1060
|
-
clearTimeout(timer);
|
|
1061
|
-
if (batchTimer) {
|
|
1062
|
-
clearTimeout(batchTimer);
|
|
1063
|
-
batchTimer = null;
|
|
1064
|
-
}
|
|
1065
|
-
roomEvents.removeListener("room", onEvent);
|
|
1066
|
-
newEvents = getNewEvents();
|
|
1067
|
-
res.json({
|
|
1068
|
-
events: newEvents,
|
|
1069
|
-
sharedState: room.sharedState,
|
|
1070
|
-
participants: room.participantList(),
|
|
1071
|
-
observation: computeObservation(newEvents),
|
|
1072
|
-
});
|
|
1073
|
-
};
|
|
1074
|
-
const timer = setTimeout(respond, timeout);
|
|
1075
|
-
const onEvent = () => {
|
|
1076
|
-
if (responded)
|
|
1077
|
-
return;
|
|
1078
|
-
if (batchTimer)
|
|
1079
|
-
return;
|
|
1080
|
-
batchTimer = setTimeout(() => {
|
|
1081
|
-
batchTimer = null;
|
|
1082
|
-
if (responded)
|
|
1083
|
-
return;
|
|
1084
|
-
const pending = getNewEvents();
|
|
1085
|
-
if (pending.length > 0)
|
|
1086
|
-
respond();
|
|
1087
|
-
}, EVENT_BATCH_DEBOUNCE_MS);
|
|
1088
|
-
};
|
|
1089
|
-
roomEvents.on("room", onEvent);
|
|
1090
|
-
req.on("close", () => {
|
|
1091
|
-
responded = true;
|
|
1092
|
-
clearTimeout(timer);
|
|
1093
|
-
if (batchTimer)
|
|
1094
|
-
clearTimeout(batchTimer);
|
|
1095
|
-
roomEvents.removeListener("room", onEvent);
|
|
1096
|
-
});
|
|
1097
|
-
});
|
|
1098
885
|
// ── Browser error capture ──────────────────────────────────
|
|
1099
886
|
const browserErrors = [];
|
|
1100
887
|
const MAX_BROWSER_ERRORS = 20;
|
|
@@ -1239,140 +1026,10 @@ app.get("/bundle", async (_req, res) => {
|
|
|
1239
1026
|
setNoCacheHeaders(res);
|
|
1240
1027
|
res.send(loadedExperience?.clientBundle || "");
|
|
1241
1028
|
});
|
|
1242
|
-
// ── Stream endpoints ───────────────────────────────────────
|
|
1243
|
-
app.post("/streams/:streamName", (req, res) => {
|
|
1244
|
-
const mod = getModule();
|
|
1245
|
-
if (!mod?.streams) {
|
|
1246
|
-
res.status(404).json({ error: "No streams defined" });
|
|
1247
|
-
return;
|
|
1248
|
-
}
|
|
1249
|
-
const streamDef = mod.streams.find((s) => s.name === req.params.streamName);
|
|
1250
|
-
if (!streamDef) {
|
|
1251
|
-
res.status(404).json({ error: `Stream '${req.params.streamName}' not found` });
|
|
1252
|
-
return;
|
|
1253
|
-
}
|
|
1254
|
-
const { actorId, input } = req.body;
|
|
1255
|
-
if (!actorId) {
|
|
1256
|
-
res.status(400).json({ error: "actorId is required for stream updates" });
|
|
1257
|
-
return;
|
|
1258
|
-
}
|
|
1259
|
-
if (!room.participants.has(actorId)) {
|
|
1260
|
-
res.status(403).json({ error: `Actor '${actorId}' is not a participant. Call /join first.` });
|
|
1261
|
-
return;
|
|
1262
|
-
}
|
|
1263
|
-
const rateLimitKey = `${actorId}:${req.params.streamName}`;
|
|
1264
|
-
const now = Date.now();
|
|
1265
|
-
const rateLimit = streamDef.rateLimit || DEFAULT_STREAM_RATE_LIMIT;
|
|
1266
|
-
if (!streamRateLimits.has(rateLimitKey)) {
|
|
1267
|
-
streamRateLimits.set(rateLimitKey, { count: 0, windowStart: now });
|
|
1268
|
-
}
|
|
1269
|
-
const rl = streamRateLimits.get(rateLimitKey);
|
|
1270
|
-
if (now - rl.windowStart > STREAM_RATE_WINDOW_MS) {
|
|
1271
|
-
rl.count = 0;
|
|
1272
|
-
rl.windowStart = now;
|
|
1273
|
-
}
|
|
1274
|
-
if (rl.count >= rateLimit) {
|
|
1275
|
-
res.status(429).json({ error: `Rate limited: max ${rateLimit} updates/sec for stream '${req.params.streamName}'.` });
|
|
1276
|
-
return;
|
|
1277
|
-
}
|
|
1278
|
-
rl.count++;
|
|
1279
|
-
let validatedInput = input;
|
|
1280
|
-
if (streamDef.input_schema?.parse) {
|
|
1281
|
-
try {
|
|
1282
|
-
validatedInput = streamDef.input_schema.parse(input);
|
|
1283
|
-
}
|
|
1284
|
-
catch (err) {
|
|
1285
|
-
res.status(400).json({ error: toErrorMessage(err) });
|
|
1286
|
-
return;
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
try {
|
|
1290
|
-
room.sharedState = streamDef.merge(room.sharedState, validatedInput, actorId);
|
|
1291
|
-
}
|
|
1292
|
-
catch (err) {
|
|
1293
|
-
res.status(400).json({ error: toErrorMessage(err) });
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1296
|
-
room.broadcastStateUpdate({ changedBy: actorId, stream: req.params.streamName });
|
|
1297
|
-
roomEvents.emit("room");
|
|
1298
|
-
res.json({ ok: true });
|
|
1299
|
-
});
|
|
1300
|
-
// ── Reset ──────────────────────────────────────────────────
|
|
1301
|
-
app.post("/reset", (_req, res) => {
|
|
1302
|
-
const mod = getModule();
|
|
1303
|
-
if (!mod) {
|
|
1304
|
-
res.status(500).json({ error: experienceNotLoadedError() });
|
|
1305
|
-
return;
|
|
1306
|
-
}
|
|
1307
|
-
room.sharedState = resolveInitialState(mod);
|
|
1308
|
-
room.events.length = 0;
|
|
1309
|
-
for (const [, p] of room.participants) {
|
|
1310
|
-
p.eventCursor = 0;
|
|
1311
|
-
}
|
|
1312
|
-
const resetEvent = {
|
|
1313
|
-
id: `${Date.now()}-system-${Math.random().toString(36).slice(2, 6)}`,
|
|
1314
|
-
ts: Date.now(),
|
|
1315
|
-
actorId: "system",
|
|
1316
|
-
tool: "_reset",
|
|
1317
|
-
input: {},
|
|
1318
|
-
output: { reset: true },
|
|
1319
|
-
};
|
|
1320
|
-
room.appendEvent(resetEvent);
|
|
1321
|
-
roomEvents.emit("room");
|
|
1322
|
-
room.resetDeltaTracking();
|
|
1323
|
-
room.broadcastStateUpdate({ changedBy: "system", tool: "_reset" }, true);
|
|
1324
|
-
res.json({ ok: true });
|
|
1325
|
-
});
|
|
1326
|
-
// ── Sync (re-bundle) ──────────────────────────────────────
|
|
1327
|
-
app.post("/sync", async (_req, res) => {
|
|
1328
|
-
try {
|
|
1329
|
-
await loadExperience();
|
|
1330
|
-
room.broadcastToAll({ type: "experience_updated" });
|
|
1331
|
-
const mod = getModule();
|
|
1332
|
-
res.json({ synced: true, title: mod?.manifest?.title });
|
|
1333
|
-
}
|
|
1334
|
-
catch (err) {
|
|
1335
|
-
res.status(500).json({ error: toErrorMessage(err) });
|
|
1336
|
-
}
|
|
1337
|
-
});
|
|
1338
|
-
// ── Experiences endpoint ───────────────────────────────────
|
|
1339
|
-
app.get("/experiences", async (_req, res) => {
|
|
1340
|
-
const mod = getModule();
|
|
1341
|
-
if (!mod) {
|
|
1342
|
-
res.json([]);
|
|
1343
|
-
return;
|
|
1344
|
-
}
|
|
1345
|
-
res.json([{
|
|
1346
|
-
id: mod.manifest.id,
|
|
1347
|
-
title: mod.manifest.title,
|
|
1348
|
-
description: mod.manifest.description,
|
|
1349
|
-
version: mod.manifest.version,
|
|
1350
|
-
loaded: true,
|
|
1351
|
-
category: mod.manifest.category,
|
|
1352
|
-
tags: mod.manifest.tags,
|
|
1353
|
-
}]);
|
|
1354
|
-
});
|
|
1355
|
-
// ── MCP config ─────────────────────────────────────────────
|
|
1356
|
-
app.get("/mcp-config", (_req, res) => {
|
|
1357
|
-
const serverUrl = getBaseUrl();
|
|
1358
|
-
res.json({
|
|
1359
|
-
mcpServers: {
|
|
1360
|
-
"vibevibes-remote": {
|
|
1361
|
-
command: "npx",
|
|
1362
|
-
args: ["-y", "@vibevibes/mcp@latest"],
|
|
1363
|
-
env: { VIBEVIBES_SERVER_URL: serverUrl },
|
|
1364
|
-
},
|
|
1365
|
-
},
|
|
1366
|
-
instructions: [
|
|
1367
|
-
`Add the above to your .mcp.json to join this room.`,
|
|
1368
|
-
`Or run: npx @vibevibes/mcp@latest ${serverUrl}`,
|
|
1369
|
-
],
|
|
1370
|
-
});
|
|
1371
|
-
});
|
|
1372
1029
|
// ── Catch-all: serve viewer ─────────────────────────────────
|
|
1373
1030
|
app.get("*", (req, res, next) => {
|
|
1374
1031
|
if (req.path.startsWith("/tools/") || req.path.startsWith("/viewer/") ||
|
|
1375
|
-
req.path.
|
|
1032
|
+
req.path.endsWith(".js") ||
|
|
1376
1033
|
req.path.endsWith(".css") || req.path.endsWith(".map")) {
|
|
1377
1034
|
next();
|
|
1378
1035
|
return;
|
|
@@ -1562,109 +1219,6 @@ export async function startServer(config) {
|
|
|
1562
1219
|
}
|
|
1563
1220
|
}
|
|
1564
1221
|
}
|
|
1565
|
-
if (msg.type === "stream") {
|
|
1566
|
-
if (typeof msg.name !== "string" || !msg.name) {
|
|
1567
|
-
ws.send(JSON.stringify({ type: "stream_error", error: "stream.name must be a non-empty string" }));
|
|
1568
|
-
return;
|
|
1569
|
-
}
|
|
1570
|
-
const senderActorId = room.wsConnections.get(ws);
|
|
1571
|
-
if (!senderActorId)
|
|
1572
|
-
return;
|
|
1573
|
-
const mod = getModule();
|
|
1574
|
-
if (!mod?.streams)
|
|
1575
|
-
return;
|
|
1576
|
-
const streamDef = mod.streams.find((s) => s.name === msg.name);
|
|
1577
|
-
if (!streamDef) {
|
|
1578
|
-
ws.send(JSON.stringify({ type: "stream_error", error: `Stream '${msg.name}' not found` }));
|
|
1579
|
-
return;
|
|
1580
|
-
}
|
|
1581
|
-
const rateLimitKey = `${senderActorId}:${msg.name}`;
|
|
1582
|
-
const now = Date.now();
|
|
1583
|
-
const rateLimit = streamDef.rateLimit || DEFAULT_STREAM_RATE_LIMIT;
|
|
1584
|
-
if (!streamRateLimits.has(rateLimitKey)) {
|
|
1585
|
-
streamRateLimits.set(rateLimitKey, { count: 0, windowStart: now });
|
|
1586
|
-
}
|
|
1587
|
-
const rl = streamRateLimits.get(rateLimitKey);
|
|
1588
|
-
if (now - rl.windowStart > STREAM_RATE_WINDOW_MS) {
|
|
1589
|
-
rl.count = 0;
|
|
1590
|
-
rl.windowStart = now;
|
|
1591
|
-
}
|
|
1592
|
-
if (rl.count >= rateLimit) {
|
|
1593
|
-
ws.send(JSON.stringify({ type: "stream_error", error: `Rate limited: max ${rateLimit}/sec for '${msg.name}'` }));
|
|
1594
|
-
return;
|
|
1595
|
-
}
|
|
1596
|
-
rl.count++;
|
|
1597
|
-
let validatedInput = msg.input;
|
|
1598
|
-
if (streamDef.input_schema?.parse) {
|
|
1599
|
-
try {
|
|
1600
|
-
validatedInput = streamDef.input_schema.parse(msg.input);
|
|
1601
|
-
}
|
|
1602
|
-
catch (err) {
|
|
1603
|
-
ws.send(JSON.stringify({ type: "stream_error", error: `Invalid input for stream '${msg.name}': ${toErrorMessage(err)}` }));
|
|
1604
|
-
return;
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
try {
|
|
1608
|
-
room.sharedState = streamDef.merge(room.sharedState, validatedInput, senderActorId);
|
|
1609
|
-
}
|
|
1610
|
-
catch (err) {
|
|
1611
|
-
ws.send(JSON.stringify({ type: "stream_error", error: `Stream '${msg.name}' merge failed: ${toErrorMessage(err)}` }));
|
|
1612
|
-
return;
|
|
1613
|
-
}
|
|
1614
|
-
room.broadcastStateUpdate({ changedBy: senderActorId, stream: msg.name });
|
|
1615
|
-
roomEvents.emit("room");
|
|
1616
|
-
}
|
|
1617
|
-
if (msg.type === "kick") {
|
|
1618
|
-
const kickerActorId = room.wsConnections.get(ws);
|
|
1619
|
-
if (!kickerActorId) {
|
|
1620
|
-
ws.send(JSON.stringify({ type: "kick_error", error: "You are not in the room" }));
|
|
1621
|
-
return;
|
|
1622
|
-
}
|
|
1623
|
-
// Reuse kick logic inline
|
|
1624
|
-
if (kickerActorId === msg.targetActorId) {
|
|
1625
|
-
ws.send(JSON.stringify({ type: "kick_error", error: "Cannot kick yourself" }));
|
|
1626
|
-
return;
|
|
1627
|
-
}
|
|
1628
|
-
const kicker = room.participants.get(kickerActorId);
|
|
1629
|
-
if (!kicker || kicker.type !== "human") {
|
|
1630
|
-
ws.send(JSON.stringify({ type: "kick_error", error: "Only human participants can kick" }));
|
|
1631
|
-
return;
|
|
1632
|
-
}
|
|
1633
|
-
if (!room.participants.has(msg.targetActorId)) {
|
|
1634
|
-
ws.send(JSON.stringify({ type: "kick_error", error: "Participant not found" }));
|
|
1635
|
-
return;
|
|
1636
|
-
}
|
|
1637
|
-
const targetP = room.participants.get(msg.targetActorId);
|
|
1638
|
-
room.participants.delete(msg.targetActorId);
|
|
1639
|
-
if (room.kickedActors.size >= 500) {
|
|
1640
|
-
const oldest = room.kickedActors.values().next().value;
|
|
1641
|
-
if (oldest)
|
|
1642
|
-
room.kickedActors.delete(oldest);
|
|
1643
|
-
}
|
|
1644
|
-
room.kickedActors.add(msg.targetActorId);
|
|
1645
|
-
if (targetP?.owner) {
|
|
1646
|
-
if (room.kickedOwners.size >= 500) {
|
|
1647
|
-
const oldest = room.kickedOwners.values().next().value;
|
|
1648
|
-
if (oldest)
|
|
1649
|
-
room.kickedOwners.delete(oldest);
|
|
1650
|
-
}
|
|
1651
|
-
room.kickedOwners.add(targetP.owner);
|
|
1652
|
-
}
|
|
1653
|
-
for (const [targetWs, wsActorId] of room.wsConnections.entries()) {
|
|
1654
|
-
if (wsActorId === msg.targetActorId) {
|
|
1655
|
-
try {
|
|
1656
|
-
targetWs.send(JSON.stringify({ type: "kicked", by: kickerActorId }));
|
|
1657
|
-
targetWs.close();
|
|
1658
|
-
}
|
|
1659
|
-
catch { }
|
|
1660
|
-
room.wsConnections.delete(targetWs);
|
|
1661
|
-
break;
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
broadcastPresenceUpdate();
|
|
1665
|
-
roomEvents.emit("room");
|
|
1666
|
-
ws.send(JSON.stringify({ type: "kick_success", actorId: msg.targetActorId }));
|
|
1667
|
-
}
|
|
1668
1222
|
}
|
|
1669
1223
|
catch (err) {
|
|
1670
1224
|
if (!(err instanceof SyntaxError)) {
|
|
@@ -1817,7 +1371,6 @@ export async function startServer(config) {
|
|
|
1817
1371
|
server.on("close", () => {
|
|
1818
1372
|
clearInterval(heartbeatInterval);
|
|
1819
1373
|
clearInterval(aiHeartbeatInterval);
|
|
1820
|
-
clearInterval(_streamRateCleanupTimer);
|
|
1821
1374
|
clearInterval(_idempotencyCleanupTimer);
|
|
1822
1375
|
});
|
|
1823
1376
|
return server;
|