@vibevibes/mcp 0.4.1 → 0.6.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 CHANGED
@@ -316,11 +316,9 @@ and returns available tools, current state, participants, and the browser URL.
316
316
 
317
317
  Call this first, then use act to interact. The stop hook keeps you present.`, {
318
318
  url: z.string().optional().describe("Server URL to connect to. Defaults to the MCP server's configured URL."),
319
- role: z.string().optional().describe("Preferred participant slot role to request (e.g. 'Blue Tank', 'Red ADC'). Server assigns the first available slot if omitted."),
320
- subscription: z.enum(["all", "phase"]).optional().describe("Event subscription mode. 'all' (default) wakes on every event. 'phase' wakes only on phase changes — use for orchestrator agents."),
321
- agentMode: z.enum(["behavior", "manual", "hybrid"]).optional().describe("Agent operating mode. 'behavior': autonomous via tick engine (no stop hook). 'manual': all decisions via act(). 'hybrid' (default): both behaviors and act()."),
322
- metadata: z.record(z.string()).optional().describe("Arbitrary metadata (model, team, tags). Flows to server and viewer. E.g. { model: 'haiku', team: 'blue' }"),
323
- }, async ({ url, role: requestedRole, subscription, agentMode, metadata }) => {
319
+ role: z.string().optional().describe("Preferred participant slot role to request."),
320
+ metadata: z.record(z.string()).optional().describe("Arbitrary metadata. Flows to server and viewer."),
321
+ }, async ({ url, role: requestedRole, metadata }) => {
324
322
  try {
325
323
  if (url) {
326
324
  SERVER_URL = url.replace(/\/$/, "");
@@ -381,8 +379,6 @@ Call this first, then use act to interact. The stop hook keeps you present.`, {
381
379
  const joinBody = { username: id, actorType: "ai", owner: id };
382
380
  if (requestedRole)
383
381
  joinBody.role = requestedRole;
384
- if (agentMode)
385
- joinBody.agentMode = agentMode;
386
382
  if (metadata && Object.keys(metadata).length > 0)
387
383
  joinBody.metadata = metadata;
388
384
  joinData = await fetchJSON("/join", {
@@ -403,12 +399,6 @@ Call this first, then use act to interact. The stop hook keeps you present.`, {
403
399
  actorId: joinData.actorId,
404
400
  role: joinData.role || undefined,
405
401
  };
406
- if (subscription && subscription !== "all") {
407
- sessionData.subscription = subscription;
408
- }
409
- if (agentMode) {
410
- sessionData.agentMode = agentMode;
411
- }
412
402
  writeFileSync(resolve(AGENTS_DIR, `${id}.json`), JSON.stringify(sessionData, null, 2));
413
403
  identity = { actorId: joinData.actorId, owner: id, id };
414
404
  currentActorId = joinData.actorId;
@@ -475,16 +465,7 @@ Call this first, then use act to interact. The stop hook keeps you present.`, {
475
465
  if (unfilledSlots.length > 0) {
476
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.")`, ``);
477
467
  }
478
- // Mode-specific instructions
479
- if (agentMode === "behavior") {
480
- outputParts.push(`BEHAVIOR MODE:`, ` You are a behavior-only agent. All actions run autonomously via the tick engine.`, ` No stop hook — you will NOT receive wake-up events.`, ` Set up behaviors now, then disconnect. The tick engine runs them automatically.`, ``);
481
- }
482
- else if (agentMode === "manual") {
483
- outputParts.push(`MANUAL MODE:`, ` You are a manual agent. Use act() for all decisions. No behaviors.`, ` The stop hook keeps you present — use act to interact.`, ` Use act() to interact. The stop hook keeps you present.`, ``);
484
- }
485
- else {
486
- outputParts.push(`FAST BRAIN / SLOW BRAIN:`, ` Fast brain: Register behaviors via _behavior.set for reactive, per-tick actions that run automatically.`, ` Slow brain: Use act() for strategic decisions and adapting to new situations.`, ` Set up your fast brain FIRST, then use your slow brain to observe and adapt.`, ` Use act() to interact. The stop hook keeps you present.`, ``, `You are now a live participant. The stop hook keeps you present — use act to interact.`);
487
- }
468
+ outputParts.push(`Use act() to interact. The stop hook keeps you present.`);
488
469
  // Register the stop hook so Claude Code wakes us on events
489
470
  ensureStopHook();
490
471
  return { content: [{ type: "text", text: outputParts.filter(Boolean).join("\n") }] };
package/dist/server.js CHANGED
@@ -15,8 +15,6 @@ import { ZodError } from "zod";
15
15
  import { EventEmitter } from "events";
16
16
  import { bundleForServer, bundleForClient, evalServerBundle, validateClientBundle } from "./bundler.js";
17
17
  import { zodToJsonSchema } from "zod-to-json-schema";
18
- import { TickEngine } from "./tick-engine.js";
19
- import { isProtocolExperience, loadProtocolManifest, createProtocolModule, SubprocessExecutor } from "./protocol.js";
20
18
  function formatZodError(err, toolName, tool) {
21
19
  const issues = err.issues.map((issue) => {
22
20
  const path = issue.path.length > 0 ? `'${issue.path.join(".")}'` : "input";
@@ -195,8 +193,6 @@ class Room {
195
193
  const detail = {
196
194
  actorId, type: p.type, role: p.role, owner: p.owner,
197
195
  };
198
- if (p.agentMode)
199
- detail.agentMode = p.agentMode;
200
196
  if (p.metadata && Object.keys(p.metadata).length > 0)
201
197
  detail.metadata = p.metadata;
202
198
  return detail;
@@ -224,7 +220,6 @@ const HISTORY_MAX_LIMIT = 200;
224
220
  const DEFAULT_STREAM_RATE_LIMIT = 60;
225
221
  const STREAM_RATE_WINDOW_MS = 1000;
226
222
  const EVENT_BATCH_DEBOUNCE_MS = 50;
227
- const DEFAULT_TICK_RATE_MS = 50;
228
223
  const MAX_BATCH_CALLS = 10;
229
224
  const LONG_POLL_MAX_TIMEOUT_MS = 55000;
230
225
  const AGENT_CONTEXT_MAX_TIMEOUT_MS = 10000;
@@ -255,7 +250,6 @@ function defaultObserve(state, _event, _actorId) {
255
250
  let PORT = parseInt(process.env.PORT || String(DEFAULT_PORT), 10);
256
251
  let publicUrl = null;
257
252
  let room;
258
- let tickEngine = null;
259
253
  let _actorCounter = 0;
260
254
  const roomEvents = new EventEmitter();
261
255
  roomEvents.setMaxListeners(ROOM_EVENTS_MAX_LISTENERS);
@@ -347,9 +341,6 @@ function experienceNotLoadedError() {
347
341
  }
348
342
  // ── Experience discovery & loading ──────────────────────────
349
343
  function discoverEntryPath() {
350
- const manifestPath = path.join(PROJECT_ROOT, "manifest.json");
351
- if (fs.existsSync(manifestPath))
352
- return manifestPath;
353
344
  const tsxPath = path.join(PROJECT_ROOT, "src", "index.tsx");
354
345
  if (fs.existsSync(tsxPath))
355
346
  return tsxPath;
@@ -357,15 +348,10 @@ function discoverEntryPath() {
357
348
  if (fs.existsSync(rootTsx))
358
349
  return rootTsx;
359
350
  throw new Error(`No experience found in ${PROJECT_ROOT}. ` +
360
- `Create a manifest.json (protocol) or src/index.tsx (TypeScript).`);
351
+ `Create src/index.tsx (TypeScript).`);
361
352
  }
362
- const protocolExecutors = new Map();
363
353
  async function loadExperience() {
364
354
  const entryPath = discoverEntryPath();
365
- if (isProtocolExperience(entryPath)) {
366
- await loadProtocolExperience(entryPath);
367
- return;
368
- }
369
355
  const [sCode, cCode] = await Promise.all([
370
356
  bundleForServer(entryPath),
371
357
  bundleForClient(entryPath),
@@ -387,48 +373,6 @@ async function loadExperience() {
387
373
  };
388
374
  experienceError = null;
389
375
  }
390
- async function loadProtocolExperience(manifestPath) {
391
- const manifestDir = path.dirname(manifestPath);
392
- const manifest = loadProtocolManifest(manifestPath);
393
- const existing = protocolExecutors.get(manifest.id);
394
- if (existing)
395
- existing.stop();
396
- const executor = new SubprocessExecutor(manifest.toolProcess.command, manifest.toolProcess.args || [], manifestDir);
397
- executor.start();
398
- protocolExecutors.set(manifest.id, executor);
399
- try {
400
- await executor.send("init", { experienceId: manifest.id }, 5000);
401
- }
402
- catch { }
403
- const mod = createProtocolModule(manifest, executor, manifestDir);
404
- loadedExperience = {
405
- module: mod,
406
- clientBundle: "",
407
- serverCode: JSON.stringify(manifest),
408
- loadedAt: Date.now(),
409
- sourcePath: manifestPath,
410
- };
411
- experienceError = null;
412
- console.log(`[protocol] Loaded ${manifest.title} (${manifest.id}) — ${manifest.tools.length} tools`);
413
- }
414
- // ── Tick Engine lifecycle ──────────────────────────────────
415
- function maybeStartTickEngine() {
416
- if (!loadedExperience)
417
- return;
418
- const manifest = loadedExperience.module?.manifest;
419
- if (manifest?.netcode !== "tick")
420
- return;
421
- stopTickEngine();
422
- const tickRateMs = manifest.tickRateMs || DEFAULT_TICK_RATE_MS;
423
- tickEngine = new TickEngine(room, loadedExperience, roomEvents, tickRateMs);
424
- tickEngine.start();
425
- }
426
- function stopTickEngine() {
427
- if (tickEngine) {
428
- tickEngine.stop();
429
- tickEngine = null;
430
- }
431
- }
432
376
  // ── Express app ────────────────────────────────────────────
433
377
  const app = express();
434
378
  app.use(express.json({ limit: JSON_BODY_LIMIT }));
@@ -581,11 +525,9 @@ app.post("/join", (req, res) => {
581
525
  res.status(500).json({ error: experienceNotLoadedError() });
582
526
  return;
583
527
  }
584
- const { username = "user", actorType: rawActorType = "human", owner, role: requestedRole, agentMode: rawAgentMode, metadata: rawMetadata } = req.body;
528
+ const { username = "user", actorType: rawActorType = "human", owner, role: requestedRole, metadata: rawMetadata } = req.body;
585
529
  const actorType = rawActorType === "ai" ? "ai" : "human";
586
530
  const resolvedOwner = owner || username;
587
- const VALID_AGENT_MODES = ["behavior", "manual", "hybrid"];
588
- const agentMode = actorType === "ai" && typeof rawAgentMode === "string" && VALID_AGENT_MODES.includes(rawAgentMode) ? rawAgentMode : undefined;
589
531
  let metadata;
590
532
  if (rawMetadata && typeof rawMetadata === "object" && !Array.isArray(rawMetadata)) {
591
533
  metadata = {};
@@ -719,8 +661,6 @@ app.post("/join", (req, res) => {
719
661
  participant.systemPrompt = slotSystemPrompt;
720
662
  if (!slotRole && requestedRole)
721
663
  participant.role = requestedRole;
722
- if (agentMode)
723
- participant.agentMode = agentMode;
724
664
  if (metadata)
725
665
  participant.metadata = metadata;
726
666
  room.participants.set(actorId, participant);
@@ -925,11 +865,6 @@ async function executeTool(toolName, actorId, input = {}, owner, expiredFlag) {
925
865
  observation,
926
866
  });
927
867
  roomEvents.emit("room");
928
- const baseTool = toolName.includes(':') ? toolName.split(':').pop() : toolName;
929
- if (baseTool.startsWith("_behavior.")) {
930
- if (tickEngine)
931
- tickEngine.markDirty();
932
- }
933
868
  return { tool: toolName, output, observation };
934
869
  }
935
870
  // ── Single tool HTTP endpoint ───────────────────────────────
@@ -1197,8 +1132,6 @@ app.get("/agent-context", (req, res) => {
1197
1132
  return room.events.filter(e => {
1198
1133
  if (requestingOwner && e.owner === requestingOwner)
1199
1134
  return false;
1200
- if (e.actorId === "_tick-engine" || e.owner === "_system")
1201
- return false;
1202
1135
  return e.ts > since;
1203
1136
  }).sort((a, b) => a.ts - b.ts);
1204
1137
  };
@@ -1250,7 +1183,6 @@ app.get("/agent-context", (req, res) => {
1250
1183
  lastError,
1251
1184
  browserErrors: recentBrowserErrors.length > 0 ? recentBrowserErrors : undefined,
1252
1185
  participants: room.participantList(),
1253
- tickEngine: tickEngine ? { enabled: true, tickCount: tickEngine.getStatus().tickCount } : undefined,
1254
1186
  eventCursor,
1255
1187
  };
1256
1188
  };
@@ -1394,10 +1326,8 @@ app.post("/reset", (_req, res) => {
1394
1326
  // ── Sync (re-bundle) ──────────────────────────────────────
1395
1327
  app.post("/sync", async (_req, res) => {
1396
1328
  try {
1397
- stopTickEngine();
1398
1329
  await loadExperience();
1399
1330
  room.broadcastToAll({ type: "experience_updated" });
1400
- maybeStartTickEngine();
1401
1331
  const mod = getModule();
1402
1332
  res.json({ synced: true, title: mod?.manifest?.title });
1403
1333
  }
@@ -1422,14 +1352,6 @@ app.get("/experiences", async (_req, res) => {
1422
1352
  tags: mod.manifest.tags,
1423
1353
  }]);
1424
1354
  });
1425
- // ── Tick status ────────────────────────────────────────────
1426
- app.get("/tick-status", (_req, res) => {
1427
- if (!tickEngine) {
1428
- res.json({ enabled: false, tickRateMs: 0, tickCount: 0, behaviorsTotal: 0, behaviorsEnabled: 0, lastTick: null });
1429
- return;
1430
- }
1431
- res.json(tickEngine.getStatus());
1432
- });
1433
1355
  // ── MCP config ─────────────────────────────────────────────
1434
1356
  app.get("/mcp-config", (_req, res) => {
1435
1357
  const serverUrl = getBaseUrl();
@@ -1447,21 +1369,6 @@ app.get("/mcp-config", (_req, res) => {
1447
1369
  ],
1448
1370
  });
1449
1371
  });
1450
- // ── Protocol experience HTML canvas ───────────────────────
1451
- app.get("/canvas", async (_req, res) => {
1452
- if (!loadedExperience) {
1453
- res.status(500).json({ error: experienceNotLoadedError() });
1454
- return;
1455
- }
1456
- const canvasPath = loadedExperience.module?._canvasPath;
1457
- if (!canvasPath) {
1458
- res.status(404).json({ error: "This experience has no HTML canvas" });
1459
- return;
1460
- }
1461
- res.setHeader("Content-Type", "text/html");
1462
- setNoCacheHeaders(res);
1463
- res.sendFile(canvasPath);
1464
- });
1465
1372
  // ── Catch-all: serve viewer ─────────────────────────────────
1466
1373
  app.get("*", (req, res, next) => {
1467
1374
  if (req.path.startsWith("/tools/") || req.path.startsWith("/viewer/") ||
@@ -1508,7 +1415,6 @@ export async function startServer(config) {
1508
1415
  const mod = getModule();
1509
1416
  const initialState = resolveInitialState(mod);
1510
1417
  room = new Room(mod.manifest.id, initialState);
1511
- maybeStartTickEngine();
1512
1418
  console.log(` Experience: ${mod.manifest.id}`);
1513
1419
  const server = http.createServer(app);
1514
1420
  const wss = new WebSocketServer({ server, maxPayload: WS_MAX_PAYLOAD_BYTES });
@@ -1819,8 +1725,6 @@ export async function startServer(config) {
1819
1725
  for (const [actorId, p] of room.participants) {
1820
1726
  if (p.type !== "ai")
1821
1727
  continue;
1822
- if (p.agentMode === "behavior")
1823
- continue;
1824
1728
  const lastSeen = p.lastPollAt || p.joinedAt;
1825
1729
  if (now - lastSeen > AI_HEARTBEAT_TIMEOUT_MS)
1826
1730
  toEvict.push(actorId);
@@ -1848,11 +1752,9 @@ export async function startServer(config) {
1848
1752
  }
1849
1753
  debounceTimer = setTimeout(async () => {
1850
1754
  console.log(`\nFile changed${filename ? ` (${filename})` : ""}, rebuilding...`);
1851
- stopTickEngine();
1852
1755
  try {
1853
1756
  await loadExperience();
1854
1757
  room.broadcastToAll({ type: "experience_updated" });
1855
- maybeStartTickEngine();
1856
1758
  smokeTestClientBundle(PORT);
1857
1759
  console.log("Hot reload complete.");
1858
1760
  }
@@ -1860,7 +1762,6 @@ export async function startServer(config) {
1860
1762
  experienceError = toErrorMessage(err);
1861
1763
  console.error("Hot reload failed:", toErrorMessage(err));
1862
1764
  room.broadcastToAll({ type: "build_error", error: toErrorMessage(err) });
1863
- maybeStartTickEngine();
1864
1765
  }
1865
1766
  finally {
1866
1767
  if (rebuildingResolve) {
package/hooks/logic.js CHANGED
@@ -92,10 +92,7 @@ export function formatPrompt(ctx) {
92
92
  }
93
93
  // ── 3. Events (what happened since last wake-up) ──────
94
94
  if (ctx.events && ctx.events.length > 0) {
95
- // Filter out noise: _behavior.* (setup), tick engine events (_tick-engine / _system)
96
- const visibleEvents = ctx.events.filter((e) => !(e.tool || "").startsWith("_behavior.") &&
97
- e.actorId !== "_tick-engine" &&
98
- e.owner !== "_system");
95
+ const visibleEvents = ctx.events;
99
96
  if (visibleEvents.length > 0) {
100
97
  for (const e of visibleEvents) {
101
98
  const actor = e.role || e.owner || (e.actorId ? e.actorId.split("-")[0] : "?");
@@ -115,16 +112,6 @@ export function formatPrompt(ctx) {
115
112
  parts.push(` ${roomId} (${info.experience}, ${p} participant${p !== 1 ? "s" : ""})`);
116
113
  }
117
114
  }
118
- // Tick engine status — only show if something interesting
119
- if (ctx.tickEngines) {
120
- const activeEngines = Object.entries(ctx.tickEngines).filter(([, status]) => status.enabled && status.behaviorsActive > 0);
121
- if (activeEngines.length > 0) {
122
- parts.push("");
123
- for (const [roomId, status] of activeEngines) {
124
- parts.push(`Tick engine [${roomId}]: ${status.behaviorsActive} behavior(s) active, ${status.tickCount} ticks`);
125
- }
126
- }
127
- }
128
115
  // Participants (deduplicated)
129
116
  if (ctx.participants && ctx.participants.length > 0) {
130
117
  const unique = [...new Set(ctx.participants)];
@@ -139,18 +126,10 @@ export function formatPrompt(ctx) {
139
126
  * Returns null to allow exit (no state file = not in agent mode).
140
127
  * Returns a StopDecision to block exit and feed context to Claude.
141
128
  */
142
- export function makeDecision(ctx, iteration, agentState) {
143
- // No state file — allow normal exit
129
+ export function makeDecision(ctx, iteration) {
144
130
  if (ctx === null)
145
131
  return null;
146
- // Behavior-only agents never produce stop decisions — they run autonomously via tick engine
147
- if (agentState?.agentMode === "behavior")
148
- return null;
149
- // Only count non-system events (tick engine events are noise)
150
- const realEvents = ctx.events?.filter((e) => !(e.tool || "").startsWith("_behavior.") &&
151
- e.actorId !== "_tick-engine" &&
152
- e.owner !== "_system") || [];
153
- const hasEvents = realEvents.length > 0;
132
+ const hasEvents = (ctx.events?.length || 0) > 0;
154
133
  const hasError = !!ctx.lastError;
155
134
  const hasBrowserErrors = ctx.browserErrors != null && ctx.browserErrors.length > 0;
156
135
  const hasObserveError = !!ctx.observeError;
@@ -165,13 +165,7 @@ async function main() {
165
165
  }
166
166
  // In-memory cursor per agent (not written to disk)
167
167
  const cursors = new Map(); // actorId → lastEventCursor
168
- // Track last-seen phase per agent for subscription: "phase" filtering
169
- const lastPhase = new Map(); // owner → last phase value
170
- // Build the list of OUR agents from agent files (not all AI agents on server).
171
- // Each Claude Code workspace only polls its own agents — prevents cross-terminal
172
- // observation merging that causes identity confusion (wrong myClaimedHero, wrong role).
173
- // Reuse the already-loaded agents array — reconciliation may have deleted stale files,
174
- // so filter to only those whose files still exist on disk.
168
+ // Build the list of OUR agents from agent files.
175
169
  const ourAgents = agents
176
170
  .filter(a => existsSync(resolve(AGENTS_DIR, a._filename || `${a.owner}.json`)))
177
171
  .map((a) => ({
@@ -182,16 +176,7 @@ async function main() {
182
176
  _filename: a._filename,
183
177
  serverUrl: a.serverUrl || session.serverUrl,
184
178
  roomId: a.roomId || undefined,
185
- subscription: a.subscription || "all",
186
- agentMode: a.agentMode || undefined,
187
- lastPhase: a.lastPhase,
188
179
  }));
189
- // Initialize lastPhase from persisted agent files (survives across invocations)
190
- for (const agent of ourAgents) {
191
- if (agent.subscription === "phase" && agent.lastPhase !== undefined) {
192
- lastPhase.set(agent.owner, agent.lastPhase ?? null);
193
- }
194
- }
195
180
  if (ourAgents.length === 0) {
196
181
  cleanupAndExit(agents.map(a => a._filename || `${a.owner}.json`));
197
182
  return;
@@ -242,68 +227,13 @@ async function main() {
242
227
  r.ctx.lastError ||
243
228
  r.ctx.observeError ||
244
229
  (r.ctx.browserErrors != null && r.ctx.browserErrors.length > 0)));
245
- // Filter out behavior-only agents — they run autonomously via tick engine, never need waking
246
- live = live.filter((r) => r.agent.agentMode !== "behavior");
247
- // Apply subscription filtering: "phase" agents only wake on phase changes or errors
248
- live = live.filter((r) => {
249
- if (r.agent.subscription !== "phase")
250
- return true; // "all" passes through
251
- // Always extract and persist current phase (even on error wakeups)
252
- const currentPhase = typeof r.ctx?.observation?.phase === "string"
253
- ? r.ctx.observation.phase : null;
254
- const prevPhase = lastPhase.get(r.agent.owner) ?? null;
255
- lastPhase.set(r.agent.owner, currentPhase);
256
- // Always wake on errors
257
- if (r.ctx?.lastError || r.ctx?.observeError ||
258
- (r.ctx?.browserErrors != null && r.ctx.browserErrors.length > 0))
259
- return true;
260
- // Phase changed — wake
261
- if (currentPhase !== prevPhase)
262
- return true;
263
- // Check for phase-related events from agent's own room only
264
- const phaseEvents = r.ctx?.events?.filter((e) => {
265
- if (e.roomId && r.agent.roomId && e.roomId !== r.agent.roomId)
266
- return false;
267
- return /phase|start|reset|end|finish/i.test(e.tool || "");
268
- }) || [];
269
- if (phaseEvents.length > 0)
270
- return true;
271
- return false; // Not a phase change — skip
272
- });
273
- // Persist lastPhase for phase-subscription agents across invocations.
274
- // Update agent.lastPhase in memory (bumpIteration writes it for woken agents).
275
- // For filtered-out agents, write to disk here so next invocation sees the correct phase.
276
- const liveOwners = new Set(live.map((r) => r.agent.owner));
277
- for (const { agent } of results) {
278
- if (agent.subscription !== "phase")
279
- continue;
280
- if (!lastPhase.has(agent.owner))
281
- continue;
282
- const phaseVal = lastPhase.get(agent.owner) ?? null;
283
- agent.lastPhase = phaseVal;
284
- if (!liveOwners.has(agent.owner)) {
285
- try {
286
- const filePath = resolve(AGENTS_DIR, agent._filename || `${agent.owner}.json`);
287
- const data = { ...agent };
288
- delete data._filename;
289
- writeFileSync(filePath, JSON.stringify(data, null, 2));
290
- }
291
- catch { /* Non-fatal — best-effort persistence */ }
292
- }
293
- }
294
230
  if (live.length > 0) {
295
231
  if (live.length > 1) {
296
232
  process.stderr.write(`[vibevibes] Warning: ${live.length} agent files found but expected 1. Using first agent only.\n`);
297
233
  }
298
234
  const { agent, ctx } = live[0];
299
235
  const iteration = doneOwners.has(agent.owner) ? (agent.iteration || 0) + 1 : bumpIteration(agent);
300
- const agentState = {
301
- roomId: agent.roomId || session.roomId,
302
- role: agent.role,
303
- iteration,
304
- agentMode: agent.agentMode,
305
- };
306
- const decision = makeDecision(ctx, iteration, agentState);
236
+ const decision = makeDecision(ctx, iteration);
307
237
  if (decision) {
308
238
  process.stdout.write(JSON.stringify(decision));
309
239
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibevibes/mcp",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "MCP server + runtime engine for vibevibes experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,85 +0,0 @@
1
- /**
2
- * Protocol experience support.
3
- *
4
- * Loads experiences defined as manifest.json + subprocess tool handler.
5
- * The subprocess communicates via newline-delimited JSON over stdin/stdout.
6
- */
7
- import type { ExperienceModule } from "@vibevibes/sdk";
8
- export interface ProtocolManifest {
9
- id: string;
10
- version: string;
11
- title: string;
12
- description?: string;
13
- initialState?: Record<string, any>;
14
- tools: ProtocolToolDef[];
15
- toolProcess: {
16
- command: string;
17
- args?: string[];
18
- };
19
- agents?: Array<{
20
- role: string;
21
- systemPrompt: string;
22
- allowedTools?: string[];
23
- autoSpawn?: boolean;
24
- maxInstances?: number;
25
- }>;
26
- observe?: {
27
- exclude?: string[];
28
- include?: string[];
29
- };
30
- canvas?: string;
31
- netcode?: "default" | "tick";
32
- tickRateMs?: number;
33
- streams?: Array<{
34
- name: string;
35
- input_schema: Record<string, any>;
36
- rateLimit?: number;
37
- }>;
38
- }
39
- interface ProtocolToolDef {
40
- name: string;
41
- description: string;
42
- input_schema: Record<string, any>;
43
- risk?: "low" | "medium" | "high";
44
- }
45
- interface RpcRequest {
46
- id: string;
47
- method: "tool" | "observe" | "stream" | "init" | "ping";
48
- params: Record<string, any>;
49
- }
50
- interface RpcResponse {
51
- id: string;
52
- result?: Record<string, any>;
53
- error?: {
54
- message: string;
55
- code?: string;
56
- };
57
- }
58
- export declare class SubprocessExecutor {
59
- private proc;
60
- private pending;
61
- private buffer;
62
- private seq;
63
- private command;
64
- private args;
65
- private cwd;
66
- private restarting;
67
- constructor(command: string, args: string[], cwd: string);
68
- start(): void;
69
- stop(): void;
70
- private processBuffer;
71
- send(method: RpcRequest["method"], params: Record<string, any>, timeoutMs?: number): Promise<RpcResponse>;
72
- get running(): boolean;
73
- }
74
- export declare function isProtocolExperience(entryPath: string): boolean;
75
- export declare function loadProtocolManifest(manifestPath: string): ProtocolManifest;
76
- /**
77
- * Create a synthetic ExperienceModule from a protocol manifest.
78
- * Tool handlers proxy calls to the subprocess executor.
79
- */
80
- export declare function createProtocolModule(manifest: ProtocolManifest, executor: SubprocessExecutor, manifestDir: string): ExperienceModule & {
81
- initialState?: Record<string, any>;
82
- _executor: SubprocessExecutor;
83
- _canvasPath?: string;
84
- };
85
- export {};
package/dist/protocol.js DELETED
@@ -1,240 +0,0 @@
1
- /**
2
- * Protocol experience support.
3
- *
4
- * Loads experiences defined as manifest.json + subprocess tool handler.
5
- * The subprocess communicates via newline-delimited JSON over stdin/stdout.
6
- */
7
- import { spawn } from "child_process";
8
- import * as path from "path";
9
- import * as fs from "fs";
10
- // ── Subprocess executor ─────────────────────────────────────────────────
11
- export class SubprocessExecutor {
12
- proc = null;
13
- pending = new Map();
14
- buffer = "";
15
- seq = 0;
16
- command;
17
- args;
18
- cwd;
19
- restarting = false;
20
- constructor(command, args, cwd) {
21
- this.command = command;
22
- this.args = args;
23
- this.cwd = cwd;
24
- }
25
- start() {
26
- if (this.proc)
27
- return;
28
- this.proc = spawn(this.command, this.args, {
29
- cwd: this.cwd,
30
- stdio: ["pipe", "pipe", "pipe"],
31
- env: { ...process.env },
32
- });
33
- this.proc.stdout.on("data", (chunk) => {
34
- this.buffer += chunk.toString();
35
- this.processBuffer();
36
- });
37
- this.proc.stderr.on("data", (chunk) => {
38
- const msg = chunk.toString().trim();
39
- if (msg)
40
- console.error(`[protocol:${this.command}] ${msg}`);
41
- });
42
- this.proc.on("exit", (code) => {
43
- console.warn(`[protocol] Tool process exited with code ${code}`);
44
- this.proc = null;
45
- // Reject all pending requests
46
- for (const [id, p] of this.pending) {
47
- p.reject(new Error(`Tool process exited (code ${code})`));
48
- clearTimeout(p.timer);
49
- }
50
- this.pending.clear();
51
- this.buffer = "";
52
- // Auto-restart after a short delay
53
- if (!this.restarting) {
54
- this.restarting = true;
55
- setTimeout(() => {
56
- this.restarting = false;
57
- try {
58
- this.start();
59
- }
60
- catch { }
61
- }, 1000);
62
- }
63
- });
64
- this.proc.on("error", (err) => {
65
- console.error(`[protocol] Failed to spawn tool process: ${err.message}`);
66
- });
67
- }
68
- stop() {
69
- this.restarting = true; // Prevent auto-restart
70
- if (this.proc) {
71
- this.proc.kill();
72
- this.proc = null;
73
- }
74
- for (const [, p] of this.pending) {
75
- p.reject(new Error("Executor stopped"));
76
- clearTimeout(p.timer);
77
- }
78
- this.pending.clear();
79
- }
80
- processBuffer() {
81
- const lines = this.buffer.split("\n");
82
- this.buffer = lines.pop() || "";
83
- for (const line of lines) {
84
- const trimmed = line.trim();
85
- if (!trimmed)
86
- continue;
87
- try {
88
- const response = JSON.parse(trimmed);
89
- const pending = this.pending.get(response.id);
90
- if (pending) {
91
- clearTimeout(pending.timer);
92
- this.pending.delete(response.id);
93
- pending.resolve(response);
94
- }
95
- }
96
- catch {
97
- console.warn(`[protocol] Invalid JSON from tool process: ${trimmed.slice(0, 200)}`);
98
- }
99
- }
100
- }
101
- async send(method, params, timeoutMs = 10000) {
102
- if (!this.proc?.stdin?.writable) {
103
- throw new Error("Tool process not running");
104
- }
105
- const id = `req-${++this.seq}`;
106
- const request = { id, method, params };
107
- return new Promise((resolve, reject) => {
108
- const timer = setTimeout(() => {
109
- this.pending.delete(id);
110
- reject(new Error(`Tool process timeout after ${timeoutMs}ms`));
111
- }, timeoutMs);
112
- this.pending.set(id, { resolve, reject, timer });
113
- try {
114
- this.proc.stdin.write(JSON.stringify(request) + "\n");
115
- }
116
- catch (err) {
117
- clearTimeout(timer);
118
- this.pending.delete(id);
119
- reject(err instanceof Error ? err : new Error(String(err)));
120
- }
121
- });
122
- }
123
- get running() {
124
- return this.proc !== null && !this.proc.killed;
125
- }
126
- }
127
- // ── Load a protocol experience ──────────────────────────────────────────
128
- export function isProtocolExperience(entryPath) {
129
- return entryPath.endsWith("manifest.json");
130
- }
131
- export function loadProtocolManifest(manifestPath) {
132
- const raw = fs.readFileSync(manifestPath, "utf-8");
133
- const manifest = JSON.parse(raw);
134
- if (!manifest.id)
135
- throw new Error("manifest.json: id is required");
136
- if (!manifest.version)
137
- throw new Error("manifest.json: version is required");
138
- if (!manifest.title)
139
- throw new Error("manifest.json: title is required");
140
- if (!manifest.toolProcess)
141
- throw new Error("manifest.json: toolProcess is required");
142
- if (!Array.isArray(manifest.tools))
143
- throw new Error("manifest.json: tools must be an array");
144
- return manifest;
145
- }
146
- /**
147
- * Create a synthetic ExperienceModule from a protocol manifest.
148
- * Tool handlers proxy calls to the subprocess executor.
149
- */
150
- export function createProtocolModule(manifest, executor, manifestDir) {
151
- // Create tool defs with handlers that proxy to the subprocess
152
- const tools = manifest.tools.map((t) => ({
153
- name: t.name,
154
- description: t.description,
155
- // Use a plain object as input_schema with a parse method for compatibility
156
- input_schema: createJsonSchemaValidator(t.input_schema),
157
- risk: (t.risk || "low"),
158
- capabilities_required: ["state.write"],
159
- handler: async (ctx, input) => {
160
- const response = await executor.send("tool", {
161
- tool: t.name,
162
- input,
163
- state: ctx.state,
164
- actorId: ctx.actorId,
165
- roomId: ctx.roomId,
166
- timestamp: ctx.timestamp,
167
- });
168
- if (response.error) {
169
- throw new Error(response.error.message);
170
- }
171
- if (response.result?.state) {
172
- ctx.setState(response.result.state);
173
- }
174
- return response.result?.output ?? {};
175
- },
176
- }));
177
- // Build observe function if manifest specifies observation config
178
- let observe;
179
- if (manifest.observe) {
180
- const { exclude, include } = manifest.observe;
181
- observe = (state, _event, _actorId) => {
182
- if (include) {
183
- const filtered = {};
184
- for (const key of include) {
185
- if (key in state)
186
- filtered[key] = state[key];
187
- }
188
- return filtered;
189
- }
190
- if (exclude) {
191
- const filtered = { ...state };
192
- for (const key of exclude) {
193
- delete filtered[key];
194
- }
195
- return filtered;
196
- }
197
- return state;
198
- };
199
- }
200
- // Resolve canvas path
201
- const canvasFile = manifest.canvas || "index.html";
202
- const canvasPath = path.resolve(manifestDir, canvasFile);
203
- const hasCanvas = fs.existsSync(canvasPath);
204
- return {
205
- manifest: {
206
- id: manifest.id,
207
- version: manifest.version,
208
- title: manifest.title,
209
- description: manifest.description || "",
210
- requested_capabilities: ["state.write"],
211
- agentSlots: manifest.agents,
212
- participantSlots: manifest.agents?.map((a) => ({ ...a, type: "ai" })),
213
- netcode: manifest.netcode,
214
- tickRateMs: manifest.tickRateMs,
215
- },
216
- Canvas: (() => null), // Protocol experiences use HTML canvas, not React
217
- tools,
218
- observe,
219
- initialState: manifest.initialState ?? {},
220
- _executor: executor,
221
- _canvasPath: hasCanvas ? canvasPath : undefined,
222
- };
223
- }
224
- /**
225
- * Create a minimal Zod-compatible validator from a JSON Schema.
226
- * Only needs .parse() for the server's input validation.
227
- */
228
- function createJsonSchemaValidator(schema) {
229
- return {
230
- parse: (input) => {
231
- // Basic type validation for protocol experiences
232
- if (schema.type === "object" && typeof input !== "object") {
233
- throw new Error("Expected object input");
234
- }
235
- return input;
236
- },
237
- // Expose the raw JSON Schema for tool listing
238
- _jsonSchema: schema,
239
- };
240
- }
@@ -1,81 +0,0 @@
1
- /**
2
- * Tick Engine — server-side tick loop for experiences with netcode: "tick".
3
- *
4
- * Runs at a fixed tick rate, executing tool calls from the tick context.
5
- * The tick engine provides the timing infrastructure; experiences define
6
- * what happens each tick via their tool handlers.
7
- */
8
- import type { ToolCtx, ToolEvent } from "@vibevibes/sdk";
9
- import type { EventEmitter } from "events";
10
- /** ToolEvent extended with observation field (computed server-side after tool execution). */
11
- interface TickToolEvent extends ToolEvent {
12
- observation?: Record<string, unknown>;
13
- }
14
- /** Minimal Room interface — what the tick engine needs from a room. */
15
- export interface TickRoom {
16
- readonly id: string;
17
- readonly experienceId: string;
18
- readonly config: Record<string, unknown>;
19
- sharedState: Record<string, unknown>;
20
- participantList(): string[];
21
- broadcastToAll(message: Record<string, unknown>): void;
22
- broadcastStateUpdate?(extra: {
23
- changedBy: string;
24
- tick?: unknown;
25
- }, forceFullState?: boolean): void;
26
- appendEvent(event: TickToolEvent): void;
27
- enqueueExecution<T>(fn: () => Promise<T>): Promise<T>;
28
- }
29
- /** Minimal experience interface — what the tick engine needs. */
30
- export interface TickExperience {
31
- module: {
32
- manifest: {
33
- id: string;
34
- };
35
- tools: Array<{
36
- name: string;
37
- input_schema?: {
38
- parse?: (input: unknown) => unknown;
39
- };
40
- handler: (ctx: ToolCtx, input: unknown) => Promise<unknown>;
41
- }>;
42
- observe?: (state: Record<string, unknown>, event: TickToolEvent, actorId: string) => Record<string, unknown>;
43
- };
44
- }
45
- export declare class TickEngine {
46
- private room;
47
- private experience;
48
- private roomEvents;
49
- private interval;
50
- private tickCount;
51
- private tickRateMs;
52
- private startedAt;
53
- private _running;
54
- private _stopped;
55
- private _stateSetThisTick;
56
- constructor(room: TickRoom, experience: TickExperience, roomEvents: EventEmitter, tickRateMs?: number);
57
- /** Start the tick loop. Uses self-correcting setTimeout to prevent timing drift. */
58
- start(): void;
59
- /** Schedule the next tick with drift correction. */
60
- private scheduleNext;
61
- /** Stop the tick loop. */
62
- stop(): void;
63
- /** Mark dirty — no-op in simplified engine, kept for API compat. */
64
- markDirty(): void;
65
- /** Get current tick status. */
66
- getStatus(): {
67
- enabled: boolean;
68
- tickRateMs: number;
69
- tickCount: number;
70
- };
71
- /** Execute one tick cycle. */
72
- private tick;
73
- /** Execute a tool call internally (no HTTP round-trip). */
74
- executeTool(toolName: string, input: Record<string, unknown>, callerActorId?: string, expiredFlag?: {
75
- value: boolean;
76
- }): Promise<{
77
- output?: unknown;
78
- error?: string;
79
- }>;
80
- }
81
- export {};
@@ -1,151 +0,0 @@
1
- /**
2
- * Tick Engine — server-side tick loop for experiences with netcode: "tick".
3
- *
4
- * Runs at a fixed tick rate, executing tool calls from the tick context.
5
- * The tick engine provides the timing infrastructure; experiences define
6
- * what happens each tick via their tool handlers.
7
- */
8
- // ── Tick Engine ───────────────────────────────────────────────────────────────
9
- const TICK_ACTOR_ID = "_tick-engine";
10
- const TICK_OWNER = "_system";
11
- export class TickEngine {
12
- room;
13
- experience;
14
- roomEvents;
15
- interval = null;
16
- tickCount = 0;
17
- tickRateMs;
18
- startedAt = 0;
19
- _running = false;
20
- _stopped = false;
21
- _stateSetThisTick = false;
22
- constructor(room, experience, roomEvents, tickRateMs = 50) {
23
- this.room = room;
24
- this.experience = experience;
25
- this.roomEvents = roomEvents;
26
- this.tickRateMs = tickRateMs;
27
- }
28
- /** Start the tick loop. Uses self-correcting setTimeout to prevent timing drift. */
29
- start() {
30
- if (this.interval)
31
- return;
32
- this._stopped = false;
33
- this.tickCount = 0;
34
- this.startedAt = Date.now();
35
- this.scheduleNext();
36
- }
37
- /** Schedule the next tick with drift correction. */
38
- scheduleNext() {
39
- if (this._stopped)
40
- return;
41
- const expected = this.startedAt + (this.tickCount + 1) * this.tickRateMs;
42
- const delay = Math.max(0, expected - Date.now());
43
- this.interval = setTimeout(() => {
44
- this.tick().then(() => this.scheduleNext(), () => this.scheduleNext());
45
- }, delay);
46
- }
47
- /** Stop the tick loop. */
48
- stop() {
49
- this._stopped = true;
50
- this._running = false;
51
- if (this.interval) {
52
- clearTimeout(this.interval);
53
- this.interval = null;
54
- }
55
- }
56
- /** Mark dirty — no-op in simplified engine, kept for API compat. */
57
- markDirty() { }
58
- /** Get current tick status. */
59
- getStatus() {
60
- return {
61
- enabled: this.interval !== null,
62
- tickRateMs: this.tickRateMs,
63
- tickCount: this.tickCount,
64
- };
65
- }
66
- /** Execute one tick cycle. */
67
- async tick() {
68
- if (this._running)
69
- return;
70
- this._running = true;
71
- try {
72
- this.tickCount++;
73
- }
74
- finally {
75
- this._running = false;
76
- }
77
- }
78
- /** Execute a tool call internally (no HTTP round-trip). */
79
- async executeTool(toolName, input, callerActorId, expiredFlag) {
80
- const tool = this.experience.module.tools.find((t) => t.name === toolName);
81
- if (!tool) {
82
- return { error: `Tool '${toolName}' not found` };
83
- }
84
- const actorId = callerActorId || TICK_ACTOR_ID;
85
- const owner = callerActorId || TICK_OWNER;
86
- try {
87
- let validatedInput = input;
88
- if (tool.input_schema?.parse) {
89
- validatedInput = tool.input_schema.parse(input);
90
- }
91
- const self = this;
92
- const ctxBase = {
93
- roomId: this.room.id,
94
- actorId,
95
- owner,
96
- state: null,
97
- setState: (newState) => {
98
- if (expiredFlag?.value)
99
- return;
100
- self.room.sharedState = newState;
101
- self._stateSetThisTick = true;
102
- },
103
- timestamp: Date.now(),
104
- memory: {},
105
- setMemory: () => { },
106
- };
107
- Object.defineProperty(ctxBase, 'state', {
108
- get() { return self.room.sharedState; },
109
- enumerable: true,
110
- configurable: true,
111
- });
112
- const ctx = ctxBase;
113
- const output = await tool.handler(ctx, validatedInput);
114
- const event = {
115
- id: `tick-${this.tickCount}-${toolName}-${Math.random().toString(36).slice(2, 6)}`,
116
- ts: Date.now(),
117
- actorId: TICK_ACTOR_ID,
118
- owner: TICK_OWNER,
119
- tool: toolName,
120
- input: validatedInput,
121
- output,
122
- };
123
- if (this.experience.module.observe) {
124
- try {
125
- event.observation = this.experience.module.observe(this.room.sharedState, event, actorId);
126
- }
127
- catch {
128
- // Don't fail tick if observe throws
129
- }
130
- }
131
- this.room.appendEvent(event);
132
- this.roomEvents.emit(`room:${this.room.id}`);
133
- return { output };
134
- }
135
- catch (err) {
136
- const errorMsg = err instanceof Error ? err.message : String(err);
137
- const event = {
138
- id: `tick-${this.tickCount}-${toolName}-${Math.random().toString(36).slice(2, 6)}`,
139
- ts: Date.now(),
140
- actorId: TICK_ACTOR_ID,
141
- owner: TICK_OWNER,
142
- tool: toolName,
143
- input,
144
- error: errorMsg,
145
- };
146
- this.room.appendEvent(event);
147
- this.roomEvents.emit(`room:${this.room.id}`);
148
- return { error: errorMsg };
149
- }
150
- }
151
- }