@vibevibes/mcp 0.9.3 → 0.10.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.d.ts +1 -1
- package/dist/index.js +1 -30
- package/hooks/stop-hook.js +51 -168
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* vibevibes MCP server — connects Claude to a running vibevibes experience.
|
|
3
3
|
*
|
|
4
4
|
* The dev server (vibevibes-dev from @vibevibes/sdk) must be running first.
|
|
5
|
-
*
|
|
5
|
+
* 4 tools: connect, act, look, disconnect
|
|
6
6
|
*/
|
|
7
7
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* vibevibes MCP server — connects Claude to a running vibevibes experience.
|
|
3
3
|
*
|
|
4
4
|
* The dev server (vibevibes-dev from @vibevibes/sdk) must be running first.
|
|
5
|
-
*
|
|
5
|
+
* 4 tools: connect, act, look, disconnect
|
|
6
6
|
*/
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -583,35 +583,6 @@ Use this to orient yourself before deciding whether to act.`, {
|
|
|
583
583
|
return { content: [{ type: "text", text: `Look failed: ${toErrorMessage(err)}` }] };
|
|
584
584
|
}
|
|
585
585
|
});
|
|
586
|
-
// ── Tool: screenshot ───────────────────────────────────────
|
|
587
|
-
server.tool("screenshot", `Capture a screenshot of the experience as seen in the browser.
|
|
588
|
-
|
|
589
|
-
Returns the current visual state as a PNG image. Use this to:
|
|
590
|
-
- See what the UI looks like
|
|
591
|
-
- Debug visual/layout issues
|
|
592
|
-
- Verify your changes rendered correctly
|
|
593
|
-
|
|
594
|
-
Requires at least one browser viewer to be connected.`, {}, async () => {
|
|
595
|
-
try {
|
|
596
|
-
const res = await fetchJSON("/screenshot", { timeoutMs: 15000 });
|
|
597
|
-
if (res.error) {
|
|
598
|
-
return { content: [{ type: "text", text: `Screenshot failed: ${res.error}` }] };
|
|
599
|
-
}
|
|
600
|
-
if (!res.dataUrl) {
|
|
601
|
-
return { content: [{ type: "text", text: "Screenshot returned empty" }] };
|
|
602
|
-
}
|
|
603
|
-
// dataUrl is "data:image/png;base64,..."
|
|
604
|
-
const base64 = res.dataUrl.replace(/^data:image\/\w+;base64,/, "");
|
|
605
|
-
return {
|
|
606
|
-
content: [
|
|
607
|
-
{ type: "image", data: base64, mimeType: "image/png" },
|
|
608
|
-
],
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
catch (err) {
|
|
612
|
-
return { content: [{ type: "text", text: `Screenshot failed: ${toErrorMessage(err)}` }] };
|
|
613
|
-
}
|
|
614
|
-
});
|
|
615
586
|
// ── Tool: disconnect ────────────────────────────────────────
|
|
616
587
|
server.tool("disconnect", `Disconnect from the current experience.
|
|
617
588
|
|
package/hooks/stop-hook.js
CHANGED
|
@@ -2,20 +2,15 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* vibevibes Stop Hook
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Self-healing: reconciles agent files against server, cleans stale files,
|
|
9
|
-
* and exits gracefully when no AI participants remain.
|
|
10
|
-
*
|
|
11
|
-
* Shipped with @vibevibes/mcp.
|
|
5
|
+
* Checks if the agent is connected to an experience.
|
|
6
|
+
* If connected, checks for events ONCE (no polling, no timeout).
|
|
7
|
+
* Returns immediately so user input is never blocked.
|
|
12
8
|
*/
|
|
13
9
|
import { readFileSync, writeFileSync, existsSync, unlinkSync, readdirSync } from "node:fs";
|
|
14
10
|
import { resolve } from "node:path";
|
|
15
11
|
import { execSync } from "node:child_process";
|
|
16
12
|
import { makeDecision } from "./logic.js";
|
|
17
13
|
// ── Helpers ────────────────────────────────────────────────
|
|
18
|
-
/** Resolve project root: git root or cwd */
|
|
19
14
|
function resolveProjectRoot() {
|
|
20
15
|
try {
|
|
21
16
|
return execSync("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3000 }).trim();
|
|
@@ -26,8 +21,6 @@ function resolveProjectRoot() {
|
|
|
26
21
|
}
|
|
27
22
|
const PROJECT_ROOT = resolveProjectRoot();
|
|
28
23
|
const AGENTS_DIR = process.env.VIBEVIBES_AGENTS_DIR || resolve(PROJECT_ROOT, ".claude", "vibevibes-agents");
|
|
29
|
-
const DEFAULT_TIMEOUT = 5000;
|
|
30
|
-
/** Read JSON from stdin (Claude Code passes hook context here) */
|
|
31
24
|
function readStdin() {
|
|
32
25
|
return new Promise((res) => {
|
|
33
26
|
let data = "";
|
|
@@ -45,7 +38,6 @@ function readStdin() {
|
|
|
45
38
|
res({});
|
|
46
39
|
});
|
|
47
40
|
}
|
|
48
|
-
/** Load all agent files from AGENTS_DIR. Returns array of session objects. */
|
|
49
41
|
function loadAgentFiles() {
|
|
50
42
|
if (!existsSync(AGENTS_DIR))
|
|
51
43
|
return [];
|
|
@@ -55,7 +47,7 @@ function loadAgentFiles() {
|
|
|
55
47
|
.map((f) => {
|
|
56
48
|
try {
|
|
57
49
|
const data = JSON.parse(readFileSync(resolve(AGENTS_DIR, f), "utf-8"));
|
|
58
|
-
data._filename = f;
|
|
50
|
+
data._filename = f;
|
|
59
51
|
return data;
|
|
60
52
|
}
|
|
61
53
|
catch {
|
|
@@ -68,83 +60,39 @@ function loadAgentFiles() {
|
|
|
68
60
|
return [];
|
|
69
61
|
}
|
|
70
62
|
}
|
|
71
|
-
/** Increment and persist the iteration counter in an agent file */
|
|
72
63
|
function bumpIteration(agent) {
|
|
73
64
|
const iter = (agent.iteration || 0) + 1;
|
|
74
65
|
try {
|
|
75
66
|
const filePath = resolve(AGENTS_DIR, agent._filename || `${agent.owner}.json`);
|
|
76
|
-
// Use in-memory agent data instead of re-reading from disk to avoid
|
|
77
|
-
// race conditions with concurrent stop-hook processes.
|
|
78
67
|
const data = { ...agent };
|
|
79
|
-
delete data._filename;
|
|
68
|
+
delete data._filename;
|
|
80
69
|
data.iteration = iter;
|
|
81
70
|
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
82
71
|
}
|
|
83
|
-
catch {
|
|
84
|
-
// Non-fatal — iteration tracking is best-effort
|
|
85
|
-
}
|
|
72
|
+
catch { }
|
|
86
73
|
return iter;
|
|
87
74
|
}
|
|
88
|
-
/** Delete owned agent files and exit.
|
|
89
|
-
* If no filenames specified, deletes all agent files (fallback for SIGINT/SIGTERM). */
|
|
90
|
-
let _cleaningUp = false;
|
|
91
|
-
function cleanupAndExit(ownedFilenames) {
|
|
92
|
-
if (_cleaningUp)
|
|
93
|
-
process.exit(0);
|
|
94
|
-
_cleaningUp = true;
|
|
95
|
-
if (existsSync(AGENTS_DIR)) {
|
|
96
|
-
try {
|
|
97
|
-
if (ownedFilenames && ownedFilenames.length > 0) {
|
|
98
|
-
// Only delete agent files owned by this stop-hook invocation
|
|
99
|
-
for (const f of ownedFilenames) {
|
|
100
|
-
try {
|
|
101
|
-
unlinkSync(resolve(AGENTS_DIR, f));
|
|
102
|
-
}
|
|
103
|
-
catch { /* ignore */ }
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
// Fallback: delete all (SIGINT/SIGTERM where ourAgents is unknown)
|
|
108
|
-
for (const f of readdirSync(AGENTS_DIR).filter((f) => f.endsWith(".json"))) {
|
|
109
|
-
try {
|
|
110
|
-
unlinkSync(resolve(AGENTS_DIR, f));
|
|
111
|
-
}
|
|
112
|
-
catch { /* ignore */ }
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
catch { /* ignore */ }
|
|
117
|
-
}
|
|
118
|
-
process.exit(0);
|
|
119
|
-
}
|
|
120
|
-
/** Delete a specific agent file by owner name */
|
|
121
75
|
function deleteAgentFile(owner) {
|
|
122
76
|
try {
|
|
123
77
|
unlinkSync(resolve(AGENTS_DIR, `${owner}.json`));
|
|
124
78
|
}
|
|
125
|
-
catch {
|
|
79
|
+
catch { }
|
|
126
80
|
}
|
|
127
|
-
// Session end cleanup — SIGINT (Ctrl+C), SIGTERM (process kill)
|
|
128
|
-
process.on("SIGINT", () => cleanupAndExit());
|
|
129
|
-
process.on("SIGTERM", () => cleanupAndExit());
|
|
130
81
|
// ── Main ───────────────────────────────────────────────────
|
|
131
82
|
async function main() {
|
|
132
83
|
await readStdin();
|
|
133
|
-
//
|
|
84
|
+
// Load agent files — if none, we're not connected, allow exit
|
|
134
85
|
const agents = loadAgentFiles();
|
|
135
86
|
if (agents.length === 0) {
|
|
136
87
|
process.exit(0);
|
|
137
88
|
}
|
|
138
|
-
// Use the first agent file for server/room info (all should be same server)
|
|
139
89
|
const session = agents[0];
|
|
140
|
-
//
|
|
90
|
+
// Quick reconciliation — check if our agents still exist on server
|
|
141
91
|
try {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
: `${sUrl}/participants`;
|
|
147
|
-
const res = await fetch(participantsUrl, { signal: AbortSignal.timeout(3000) });
|
|
92
|
+
const participantsUrl = session.roomId
|
|
93
|
+
? `${session.serverUrl}/rooms/${session.roomId}/participants`
|
|
94
|
+
: `${session.serverUrl}/participants`;
|
|
95
|
+
const res = await fetch(participantsUrl, { signal: AbortSignal.timeout(2000) });
|
|
148
96
|
if (res.ok) {
|
|
149
97
|
const data = await res.json();
|
|
150
98
|
if (Array.isArray(data?.participants)) {
|
|
@@ -155,127 +103,62 @@ async function main() {
|
|
|
155
103
|
}
|
|
156
104
|
}
|
|
157
105
|
if (!data.participants.some((p) => p.type === "ai")) {
|
|
158
|
-
|
|
106
|
+
// No AI participants on server — clean up and allow exit
|
|
107
|
+
for (const a of agents)
|
|
108
|
+
deleteAgentFile(a.owner);
|
|
109
|
+
process.exit(0);
|
|
159
110
|
}
|
|
160
111
|
}
|
|
161
112
|
}
|
|
162
113
|
}
|
|
163
114
|
catch {
|
|
164
|
-
// Server unreachable —
|
|
115
|
+
// Server unreachable — still block to keep agent alive
|
|
165
116
|
}
|
|
166
|
-
//
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
.
|
|
171
|
-
.
|
|
172
|
-
actorId: a.actorId,
|
|
173
|
-
owner: a.owner,
|
|
174
|
-
role: a.role || undefined,
|
|
175
|
-
iteration: a.iteration || 0,
|
|
176
|
-
_filename: a._filename,
|
|
177
|
-
serverUrl: a.serverUrl || session.serverUrl,
|
|
178
|
-
roomId: a.roomId || undefined,
|
|
179
|
-
}));
|
|
180
|
-
if (ourAgents.length === 0) {
|
|
181
|
-
cleanupAndExit(agents.map(a => a._filename || `${a.owner}.json`));
|
|
182
|
-
return;
|
|
117
|
+
// Check for events ONCE — no polling, no timeout, instant return
|
|
118
|
+
const agent = agents[0];
|
|
119
|
+
if (!agent.actorId) {
|
|
120
|
+
// Connected but no actorId — block exit silently
|
|
121
|
+
process.stdout.write(JSON.stringify({ decision: "block" }));
|
|
122
|
+
process.exit(0);
|
|
183
123
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const currentAgents = loadAgentFiles();
|
|
194
|
-
if (currentAgents.length === 0) {
|
|
195
|
-
process.exit(0);
|
|
196
|
-
}
|
|
197
|
-
// Still connected — block exit
|
|
198
|
-
process.stdout.write(JSON.stringify({
|
|
199
|
-
decision: "block",
|
|
200
|
-
reason: "Still connected to the experience. Waiting for activity.",
|
|
201
|
-
}));
|
|
124
|
+
try {
|
|
125
|
+
const ownerParam = agent.owner ? `&owner=${encodeURIComponent(agent.owner)}` : "";
|
|
126
|
+
const roomP = agent.roomId ? `&roomId=${encodeURIComponent(agent.roomId)}` : "";
|
|
127
|
+
// timeout=0 means don't long-poll, just return immediately
|
|
128
|
+
const url = `${agent.serverUrl}/agent-context?since=0&actorId=${encodeURIComponent(agent.actorId)}&timeout=0${ownerParam}${roomP}`;
|
|
129
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
|
|
130
|
+
if (!res.ok) {
|
|
131
|
+
// Server error — block exit silently
|
|
132
|
+
process.stdout.write(JSON.stringify({ decision: "block" }));
|
|
202
133
|
process.exit(0);
|
|
203
134
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const results = await Promise.all(ourAgents.map(async (agent) => {
|
|
209
|
-
const ctx = await pollAgent(agent.serverUrl, agent.roomId, agent, cursors, pollTimeout);
|
|
210
|
-
return { agent, ctx };
|
|
211
|
-
}));
|
|
212
|
-
// Update cursors from server responses
|
|
213
|
-
for (const { agent, ctx } of results) {
|
|
214
|
-
if (ctx?.eventCursor != null && agent.actorId) {
|
|
215
|
-
cursors.set(agent.actorId, ctx.eventCursor);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
// Handle done signals — clean up agent file if experience says done
|
|
219
|
-
const doneOwners = new Set();
|
|
220
|
-
for (const { agent, ctx } of results) {
|
|
221
|
-
if (!ctx)
|
|
222
|
-
continue;
|
|
223
|
-
if (ctx.observation?.done) {
|
|
224
|
-
deleteAgentFile(agent.owner);
|
|
225
|
-
doneOwners.add(agent.owner);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
// Check if any agent got null context (possibly evicted from server or network blip)
|
|
229
|
-
const stillAlive = results.filter((r) => r.ctx !== null);
|
|
230
|
-
if (stillAlive.length === 0) {
|
|
231
|
-
// Don't delete agent files on transient failure — just exit quietly so retry can succeed
|
|
135
|
+
const ctx = await res.json();
|
|
136
|
+
// Check for done signal
|
|
137
|
+
if (ctx.observation?.done) {
|
|
138
|
+
deleteAgentFile(agent.owner);
|
|
232
139
|
process.exit(0);
|
|
233
|
-
return;
|
|
234
140
|
}
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
(
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
process.stderr.write(`[vibevibes] Warning: ${live.length} agent files found but expected 1. Using first agent only.\n`);
|
|
243
|
-
}
|
|
244
|
-
const { agent, ctx } = live[0];
|
|
245
|
-
const iteration = doneOwners.has(agent.owner) ? (agent.iteration || 0) + 1 : bumpIteration(agent);
|
|
141
|
+
// Check for actionable events
|
|
142
|
+
const hasEvents = (ctx.events && ctx.events.length > 0) ||
|
|
143
|
+
ctx.lastError ||
|
|
144
|
+
ctx.observeError ||
|
|
145
|
+
(ctx.browserErrors != null && ctx.browserErrors.length > 0);
|
|
146
|
+
if (hasEvents) {
|
|
147
|
+
const iteration = bumpIteration(agent);
|
|
246
148
|
const decision = makeDecision(ctx, iteration);
|
|
247
149
|
if (decision) {
|
|
248
150
|
process.stdout.write(JSON.stringify(decision));
|
|
249
151
|
}
|
|
250
152
|
process.exit(0);
|
|
251
153
|
}
|
|
252
|
-
// No events —
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
/** Poll a single agent's server for events */
|
|
256
|
-
async function pollAgent(serverUrl, roomId, agent, cursors, timeout = DEFAULT_TIMEOUT) {
|
|
257
|
-
const { actorId, owner } = agent;
|
|
258
|
-
if (!actorId)
|
|
259
|
-
return null;
|
|
260
|
-
const since = cursors.get(actorId) || 0;
|
|
261
|
-
const ownerParam = owner ? `&owner=${encodeURIComponent(owner)}` : "";
|
|
262
|
-
const roomP = roomId ? `&roomId=${encodeURIComponent(roomId)}` : "";
|
|
263
|
-
const controller = new AbortController();
|
|
264
|
-
const timer = setTimeout(() => controller.abort(), timeout + 1000);
|
|
265
|
-
try {
|
|
266
|
-
const url = `${serverUrl}/agent-context?since=${since}&actorId=${encodeURIComponent(actorId)}&timeout=${timeout}${ownerParam}${roomP}`;
|
|
267
|
-
const res = await fetch(url, { signal: controller.signal });
|
|
268
|
-
if (!res.ok)
|
|
269
|
-
return null;
|
|
270
|
-
return await res.json();
|
|
154
|
+
// No events — still connected, block exit silently
|
|
155
|
+
process.stdout.write(JSON.stringify({ decision: "block" }));
|
|
156
|
+
process.exit(0);
|
|
271
157
|
}
|
|
272
158
|
catch {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
clearTimeout(timer);
|
|
159
|
+
// Network error — block exit silently
|
|
160
|
+
process.stdout.write(JSON.stringify({ decision: "block" }));
|
|
161
|
+
process.exit(0);
|
|
277
162
|
}
|
|
278
163
|
}
|
|
279
|
-
main().catch(() =>
|
|
280
|
-
process.exit(0);
|
|
281
|
-
});
|
|
164
|
+
main().catch(() => process.exit(0));
|