@vibevibes/mcp 0.4.1 → 0.5.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 +4 -23
- package/dist/server.js +1 -14
- package/hooks/logic.js +3 -21
- package/hooks/stop-hook.js +2 -72
- package/package.json +1 -1
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
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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
|
@@ -195,8 +195,6 @@ class Room {
|
|
|
195
195
|
const detail = {
|
|
196
196
|
actorId, type: p.type, role: p.role, owner: p.owner,
|
|
197
197
|
};
|
|
198
|
-
if (p.agentMode)
|
|
199
|
-
detail.agentMode = p.agentMode;
|
|
200
198
|
if (p.metadata && Object.keys(p.metadata).length > 0)
|
|
201
199
|
detail.metadata = p.metadata;
|
|
202
200
|
return detail;
|
|
@@ -581,11 +579,9 @@ app.post("/join", (req, res) => {
|
|
|
581
579
|
res.status(500).json({ error: experienceNotLoadedError() });
|
|
582
580
|
return;
|
|
583
581
|
}
|
|
584
|
-
const { username = "user", actorType: rawActorType = "human", owner, role: requestedRole,
|
|
582
|
+
const { username = "user", actorType: rawActorType = "human", owner, role: requestedRole, metadata: rawMetadata } = req.body;
|
|
585
583
|
const actorType = rawActorType === "ai" ? "ai" : "human";
|
|
586
584
|
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
585
|
let metadata;
|
|
590
586
|
if (rawMetadata && typeof rawMetadata === "object" && !Array.isArray(rawMetadata)) {
|
|
591
587
|
metadata = {};
|
|
@@ -719,8 +715,6 @@ app.post("/join", (req, res) => {
|
|
|
719
715
|
participant.systemPrompt = slotSystemPrompt;
|
|
720
716
|
if (!slotRole && requestedRole)
|
|
721
717
|
participant.role = requestedRole;
|
|
722
|
-
if (agentMode)
|
|
723
|
-
participant.agentMode = agentMode;
|
|
724
718
|
if (metadata)
|
|
725
719
|
participant.metadata = metadata;
|
|
726
720
|
room.participants.set(actorId, participant);
|
|
@@ -925,11 +919,6 @@ async function executeTool(toolName, actorId, input = {}, owner, expiredFlag) {
|
|
|
925
919
|
observation,
|
|
926
920
|
});
|
|
927
921
|
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
922
|
return { tool: toolName, output, observation };
|
|
934
923
|
}
|
|
935
924
|
// ── Single tool HTTP endpoint ───────────────────────────────
|
|
@@ -1819,8 +1808,6 @@ export async function startServer(config) {
|
|
|
1819
1808
|
for (const [actorId, p] of room.participants) {
|
|
1820
1809
|
if (p.type !== "ai")
|
|
1821
1810
|
continue;
|
|
1822
|
-
if (p.agentMode === "behavior")
|
|
1823
|
-
continue;
|
|
1824
1811
|
const lastSeen = p.lastPollAt || p.joinedAt;
|
|
1825
1812
|
if (now - lastSeen > AI_HEARTBEAT_TIMEOUT_MS)
|
|
1826
1813
|
toEvict.push(actorId);
|
package/hooks/logic.js
CHANGED
|
@@ -92,9 +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
|
-
|
|
96
|
-
const visibleEvents = ctx.events.filter((e) => !(e.tool || "").startsWith("_behavior.") &&
|
|
97
|
-
e.actorId !== "_tick-engine" &&
|
|
95
|
+
const visibleEvents = ctx.events.filter((e) => e.actorId !== "_tick-engine" &&
|
|
98
96
|
e.owner !== "_system");
|
|
99
97
|
if (visibleEvents.length > 0) {
|
|
100
98
|
for (const e of visibleEvents) {
|
|
@@ -115,16 +113,6 @@ export function formatPrompt(ctx) {
|
|
|
115
113
|
parts.push(` ${roomId} (${info.experience}, ${p} participant${p !== 1 ? "s" : ""})`);
|
|
116
114
|
}
|
|
117
115
|
}
|
|
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
116
|
// Participants (deduplicated)
|
|
129
117
|
if (ctx.participants && ctx.participants.length > 0) {
|
|
130
118
|
const unique = [...new Set(ctx.participants)];
|
|
@@ -139,16 +127,10 @@ export function formatPrompt(ctx) {
|
|
|
139
127
|
* Returns null to allow exit (no state file = not in agent mode).
|
|
140
128
|
* Returns a StopDecision to block exit and feed context to Claude.
|
|
141
129
|
*/
|
|
142
|
-
export function makeDecision(ctx, iteration
|
|
143
|
-
// No state file — allow normal exit
|
|
130
|
+
export function makeDecision(ctx, iteration) {
|
|
144
131
|
if (ctx === null)
|
|
145
132
|
return null;
|
|
146
|
-
|
|
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" &&
|
|
133
|
+
const realEvents = ctx.events?.filter((e) => e.actorId !== "_tick-engine" &&
|
|
152
134
|
e.owner !== "_system") || [];
|
|
153
135
|
const hasEvents = realEvents.length > 0;
|
|
154
136
|
const hasError = !!ctx.lastError;
|
package/hooks/stop-hook.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
}
|