slashvibe-mcp 0.5.8 → 0.5.9
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/index.js +1 -0
- package/package.json +1 -1
- package/tools/corpse.js +269 -0
- package/version.json +4 -4
package/index.js
CHANGED
|
@@ -259,6 +259,7 @@ const coreTools = {
|
|
|
259
259
|
// Play — the original January magic: shared experiences over the DM transport
|
|
260
260
|
vibe_game: require('./tools/game'),
|
|
261
261
|
vibe_poem: require('./tools/poem'),
|
|
262
|
+
vibe_corpse: require('./tools/corpse'),
|
|
262
263
|
|
|
263
264
|
// Matrix rooms — multiplayer
|
|
264
265
|
vibe_matrix_rooms: require('./tools/matrix-rooms'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slashvibe-mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"mcpName": "io.github.vibecodinginc/vibe",
|
|
5
5
|
"description": "Social layer for Claude Code - DMs, presence, Matrix multiplayer rooms, and connection between AI-assisted developers",
|
|
6
6
|
"main": "index.js",
|
package/tools/corpse.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibe corpse — Write an exquisite corpse together, one line at a time
|
|
3
|
+
*
|
|
4
|
+
* The surrealist parlor game, ported to the terminal. Two people build a story
|
|
5
|
+
* (or a poem) by alternating lines — but you only ever see the LAST line the
|
|
6
|
+
* other person wrote, never the whole thing. When someone seals it, the full
|
|
7
|
+
* corpse is revealed to both, usually gloriously nonsensical.
|
|
8
|
+
*
|
|
9
|
+
* Same transport as `vibe poem` / `vibe game`: the story state travels in the
|
|
10
|
+
* DM message payload (durable via the Postgres payload column), passed back and
|
|
11
|
+
* forth over the DM thread. The only new idea here is that render hides every
|
|
12
|
+
* line but the last until the story is sealed.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const config = require('../config');
|
|
16
|
+
const store = require('../store');
|
|
17
|
+
const { requireInit, normalizeHandle } = require('./_shared');
|
|
18
|
+
|
|
19
|
+
// Corpses seal automatically at this many lines (either player can seal earlier).
|
|
20
|
+
const MAX_LINES = 10;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Find the most recent corpse state in a DM thread.
|
|
24
|
+
*/
|
|
25
|
+
function getCorpseState(thread) {
|
|
26
|
+
for (let i = thread.length - 1; i >= 0; i--) {
|
|
27
|
+
const p = thread[i].payload;
|
|
28
|
+
if (p && p.type === 'corpse' && p.state) return p.state;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function uniqueAuthors(lines) {
|
|
34
|
+
const seen = [];
|
|
35
|
+
for (const l of lines) if (!seen.includes(l.author)) seen.push(l.author);
|
|
36
|
+
return seen;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Render the full, revealed corpse (only shown once sealed).
|
|
41
|
+
*/
|
|
42
|
+
function renderReveal(state) {
|
|
43
|
+
const lines = state.lines || [];
|
|
44
|
+
let out = '💀 **exquisite corpse';
|
|
45
|
+
if (state.title) out += ` — ${state.title}`;
|
|
46
|
+
out += '**\n\n';
|
|
47
|
+
if (!lines.length) {
|
|
48
|
+
out += '_(empty — nothing was written)_\n';
|
|
49
|
+
} else {
|
|
50
|
+
out += '```\n';
|
|
51
|
+
for (const l of lines) out += `${l.text}\n`;
|
|
52
|
+
out += '```\n';
|
|
53
|
+
out += `\n_by ${uniqueAuthors(lines).map((a) => '@' + a).join(' & ')} — ${lines.length} lines_\n`;
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Render the *in-progress* corpse: the whole story stays folded, you only see
|
|
60
|
+
* the last line to build from. This is the whole point of the game.
|
|
61
|
+
*/
|
|
62
|
+
function renderFolded(state, me) {
|
|
63
|
+
const lines = state.lines || [];
|
|
64
|
+
let out = '💀 **exquisite corpse';
|
|
65
|
+
if (state.title) out += ` — ${state.title}`;
|
|
66
|
+
out += '**\n\n';
|
|
67
|
+
if (!lines.length) {
|
|
68
|
+
out += '_(blank page — write the opening line; the rest stays hidden)_\n';
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
const last = lines[lines.length - 1];
|
|
72
|
+
const hiddenCount = lines.length - 1;
|
|
73
|
+
out += '```\n';
|
|
74
|
+
if (hiddenCount > 0) {
|
|
75
|
+
out += `… ${hiddenCount} line${hiddenCount === 1 ? '' : 's'} folded out of sight …\n`;
|
|
76
|
+
}
|
|
77
|
+
out += `${last.text}\n`;
|
|
78
|
+
out += '```\n';
|
|
79
|
+
out += `\n_only the last line shows — continue from it, blind to the rest_\n`;
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whose turn is it? Alternate off the last line's author. If the last line was
|
|
85
|
+
* mine, it's their turn; if theirs (or empty), it's mine.
|
|
86
|
+
*/
|
|
87
|
+
function isMyTurn(state, me) {
|
|
88
|
+
const lines = state.lines || [];
|
|
89
|
+
if (!lines.length) return true; // whoever opens goes first
|
|
90
|
+
return lines[lines.length - 1].author !== me;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function postCorpseToBoard(state, authors) {
|
|
94
|
+
const API_URL = process.env.VIBE_API_URL || 'https://www.slashvibe.dev';
|
|
95
|
+
const body = (state.lines || []).map((l) => l.text).join(' / ');
|
|
96
|
+
try {
|
|
97
|
+
await fetch(`${API_URL}/api/board`, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: { 'Content-Type': 'application/json' },
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
author: 'echo',
|
|
102
|
+
content: `${authors.map((a) => '@' + a).join(' & ')} unfolded an exquisite corpse: ${body}`,
|
|
103
|
+
category: 'general',
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
} catch (e) {
|
|
107
|
+
console.error('[corpse] Failed to post to board:', e.message);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const definition = {
|
|
112
|
+
name: 'vibe_corpse',
|
|
113
|
+
description:
|
|
114
|
+
'Write an exquisite corpse together — a surrealist story where you only ever see the LAST line, never the whole thing, until it is sealed and revealed. Pass it back and forth: each turn adds a line, blind to what came before. `vibe corpse @handle --line "the clock swallowed its own hands"`',
|
|
115
|
+
inputSchema: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
handle: {
|
|
119
|
+
type: 'string',
|
|
120
|
+
description: 'Who to write with (e.g., @solienne)',
|
|
121
|
+
},
|
|
122
|
+
line: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
description: 'The line to add — builds from the last visible line only',
|
|
125
|
+
},
|
|
126
|
+
title: {
|
|
127
|
+
type: 'string',
|
|
128
|
+
description: 'Optional title, only used when starting a new corpse',
|
|
129
|
+
},
|
|
130
|
+
done: {
|
|
131
|
+
type: 'boolean',
|
|
132
|
+
description: 'Seal the corpse after adding your line, revealing the whole thing to both of you',
|
|
133
|
+
},
|
|
134
|
+
reveal: {
|
|
135
|
+
type: 'boolean',
|
|
136
|
+
description: 'Show the full revealed corpse (only works once it has been sealed)',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
required: ['handle'],
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
async function handler(args) {
|
|
144
|
+
const initCheck = requireInit();
|
|
145
|
+
if (initCheck) return initCheck;
|
|
146
|
+
|
|
147
|
+
const { line, title, done, reveal } = args;
|
|
148
|
+
const me = config.getHandle();
|
|
149
|
+
const them = normalizeHandle(args.handle);
|
|
150
|
+
|
|
151
|
+
if (them === me) {
|
|
152
|
+
return { display: "You can't play exquisite corpse with yourself — half the fun is the surprise. Invite someone: `vibe corpse @handle`." };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const thread = await store.getThread(me, them);
|
|
156
|
+
let state = getCorpseState(thread);
|
|
157
|
+
|
|
158
|
+
// Explicit reveal request.
|
|
159
|
+
if (reveal) {
|
|
160
|
+
if (!state) {
|
|
161
|
+
return { display: `No corpse with @${them} yet. Start one: \`vibe corpse @${them} --line "…"\`` };
|
|
162
|
+
}
|
|
163
|
+
if (!state.done) {
|
|
164
|
+
return {
|
|
165
|
+
display:
|
|
166
|
+
`That corpse isn't sealed yet — no peeking! 💀\n\n${renderFolded(state, me)}\n` +
|
|
167
|
+
(isMyTurn(state, me)
|
|
168
|
+
? `Your turn — add a line: \`vibe corpse @${them} --line "…"\``
|
|
169
|
+
: `Waiting for @${them} to add the next line…`),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return { display: `## exquisite corpse with @${them}\n\n${renderReveal(state)}` };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// No line provided → show current status (folded), or start a fresh corpse.
|
|
176
|
+
if (!line) {
|
|
177
|
+
if (!state) {
|
|
178
|
+
const newState = { lines: [], title: title || null, done: false, startedBy: me };
|
|
179
|
+
await store.sendMessage(
|
|
180
|
+
me,
|
|
181
|
+
them,
|
|
182
|
+
`Let's play exquisite corpse 💀 A surrealist story where you only see my last line. Add yours: \`vibe corpse @${me} --line "…"\``,
|
|
183
|
+
'dm',
|
|
184
|
+
{ type: 'corpse', state: newState }
|
|
185
|
+
);
|
|
186
|
+
return {
|
|
187
|
+
display:
|
|
188
|
+
`## New exquisite corpse with @${them}\n\n${renderFolded(newState, me)}\n` +
|
|
189
|
+
`Write the opening line: \`vibe corpse @${them} --line "the moon filed a complaint"\``,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Show existing corpse (folded unless sealed).
|
|
194
|
+
if (state.done) {
|
|
195
|
+
return {
|
|
196
|
+
display:
|
|
197
|
+
`## exquisite corpse with @${them}\n\n${renderReveal(state)}\n` +
|
|
198
|
+
`✨ Sealed. Start another: \`vibe corpse @${them} --line "…"\``,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
let display = `## exquisite corpse with @${them}\n\n${renderFolded(state, me)}\n`;
|
|
202
|
+
if (isMyTurn(state, me)) {
|
|
203
|
+
display += `\nYour turn — add a line (blind to the rest): \`vibe corpse @${them} --line "…"\``;
|
|
204
|
+
} else {
|
|
205
|
+
display += `\nWaiting for @${them} to add the next line…`;
|
|
206
|
+
}
|
|
207
|
+
return { display };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Adding a line.
|
|
211
|
+
if (!state) {
|
|
212
|
+
state = { lines: [], title: title || null, done: false, startedBy: me };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (state.done) {
|
|
216
|
+
return {
|
|
217
|
+
display:
|
|
218
|
+
`This corpse is already sealed 💀\n\n${renderReveal(state)}\n` +
|
|
219
|
+
`Start a new one: \`vibe corpse @${them} --line "…"\``,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!isMyTurn(state, me)) {
|
|
224
|
+
return {
|
|
225
|
+
display:
|
|
226
|
+
`It's @${them}'s turn to add a line.\n\n${renderFolded(state, me)}\n` +
|
|
227
|
+
`Nudge them, or wait for their line.`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const text = String(line).trim();
|
|
232
|
+
if (!text) return { display: 'Give me a line to add: `vibe corpse @handle --line "…"`' };
|
|
233
|
+
if (text.length > 200) return { display: 'Keep each line under 200 characters.' };
|
|
234
|
+
|
|
235
|
+
const newLines = [...(state.lines || []), { author: me, text }];
|
|
236
|
+
const shouldSeal = done === true || newLines.length >= MAX_LINES;
|
|
237
|
+
const newState = {
|
|
238
|
+
...state,
|
|
239
|
+
lines: newLines,
|
|
240
|
+
done: shouldSeal,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const authors = uniqueAuthors(newLines);
|
|
244
|
+
let message;
|
|
245
|
+
if (shouldSeal) {
|
|
246
|
+
// On seal, the full corpse is revealed — send it in the DM so both sides see it.
|
|
247
|
+
message =
|
|
248
|
+
`Sealed our exquisite corpse 💀✨ Here's the whole thing, unfolded:\n\n` +
|
|
249
|
+
newLines.map((l) => l.text).join('\n');
|
|
250
|
+
if (authors.length > 1) postCorpseToBoard(newState, authors);
|
|
251
|
+
} else {
|
|
252
|
+
// Mid-game: only tell them it's their turn. Do NOT echo the story — keep it folded.
|
|
253
|
+
message = `Added a line to our corpse — your turn 💀 You'll only see my last line:\n"${text}"`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await store.sendMessage(me, them, message, 'dm', { type: 'corpse', state: newState });
|
|
257
|
+
|
|
258
|
+
if (shouldSeal) {
|
|
259
|
+
let display = `## exquisite corpse with @${them}\n\n${renderReveal(newState)}\n`;
|
|
260
|
+
display += `\n✨ Corpse sealed${newLines.length >= MAX_LINES ? ` (${MAX_LINES} lines)` : ''} and revealed to you both. Start another anytime.`;
|
|
261
|
+
return { display };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let display = `## exquisite corpse with @${them}\n\n${renderFolded(newState, me)}\n`;
|
|
265
|
+
display += `\nYour line is in. Sent to @${them}, folded away — waiting for theirs…`;
|
|
266
|
+
return { display };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = { definition, handler };
|
package/version.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.5.
|
|
2
|
+
"version": "0.5.9",
|
|
3
3
|
"updated": "2026-07-03",
|
|
4
|
-
"changelog": "
|
|
4
|
+
"changelog": "New way to play together: the exquisite corpse. `vibe_corpse @handle` starts a surrealist story you build one line at a time — but you only ever see the LAST line, never the whole thing, until someone seals it and the full corpse is revealed. Joins `vibe_game` (tic-tac-toe) and `vibe_poem` (emoji poem) as shared experiences carried over your DM thread. Ship together, play together.",
|
|
5
5
|
"features": [
|
|
6
|
+
"vibe_corpse — write an exquisite corpse with another viber: alternate lines blind to all but the last, then seal for the big reveal",
|
|
6
7
|
"vibe_game — play tic-tac-toe with another viber, move by move, carried over your DM thread",
|
|
7
|
-
"vibe_poem — build a collaborative emoji poem together, one line each, until someone seals it"
|
|
8
|
-
"Fixed DM thread reads so game and poem state reliably round-trips between two people"
|
|
8
|
+
"vibe_poem — build a collaborative emoji poem together, one line each, until someone seals it"
|
|
9
9
|
],
|
|
10
10
|
"deprecated": [
|
|
11
11
|
"vibe_pair — use Matrix room invites",
|