slashvibe-mcp 0.5.9 → 0.5.10
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 +3 -1
- package/package.json +1 -1
- package/tools/play.js +208 -0
- package/version.json +6 -5
package/index.js
CHANGED
|
@@ -256,7 +256,9 @@ const coreTools = {
|
|
|
256
256
|
vibe_ship: require('./tools/ship'),
|
|
257
257
|
vibe_feed: require('./tools/feed'),
|
|
258
258
|
|
|
259
|
-
// Play — the original January magic: shared experiences over the DM transport
|
|
259
|
+
// Play — the original January magic: shared experiences over the DM transport.
|
|
260
|
+
// vibe_play is the open primitive; game/poem/corpse are hardcoded on-ramps.
|
|
261
|
+
vibe_play: require('./tools/play'),
|
|
260
262
|
vibe_game: require('./tools/game'),
|
|
261
263
|
vibe_poem: require('./tools/poem'),
|
|
262
264
|
vibe_corpse: require('./tools/corpse'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slashvibe-mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
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/play.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibe play — the open channel for emergent, AI-improvised social play
|
|
3
|
+
*
|
|
4
|
+
* game / poem / corpse each hardcode one activity's rules. This tool hardcodes
|
|
5
|
+
* NONE. It is the bare primitive underneath all of them: carry an arbitrary
|
|
6
|
+
* `state` object back and forth over the DM thread, with a freeform `note`, and
|
|
7
|
+
* let the two Claude sessions invent whatever they want on top of it — chess,
|
|
8
|
+
* 20 questions, a co-written story, a shared drawing, a debate, a game neither
|
|
9
|
+
* player has a name for yet.
|
|
10
|
+
*
|
|
11
|
+
* The rules live in the conversation, not in this file. That's the whole point:
|
|
12
|
+
* the platform supplies the presence + the channel; the creativity is emergent
|
|
13
|
+
* from the humans and their Claudes. game/poem/corpse are now just friendly
|
|
14
|
+
* on-ramps that teach the pattern; `vibe play` is the pattern itself.
|
|
15
|
+
*
|
|
16
|
+
* Transport is identical to the others: `payload: { type:'play', ... }` on a DM,
|
|
17
|
+
* durable via the Postgres payload column, read back with store.getThread().
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const config = require('../config');
|
|
21
|
+
const store = require('../store');
|
|
22
|
+
const { requireInit, normalizeHandle } = require('./_shared');
|
|
23
|
+
|
|
24
|
+
// Guardrail so a runaway state blob can't wedge the DM store. Generous — a
|
|
25
|
+
// chess board, a long story-so-far, or a 100-cell canvas all fit easily.
|
|
26
|
+
const MAX_STATE_BYTES = 24000;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Find the most recent play session in a DM thread.
|
|
30
|
+
*/
|
|
31
|
+
function getPlaySession(thread) {
|
|
32
|
+
for (let i = thread.length - 1; i >= 0; i--) {
|
|
33
|
+
const p = thread[i].payload;
|
|
34
|
+
if (p && p.type === 'play') return p;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Accept state as a structured object/array/primitive, or a JSON string, or a
|
|
41
|
+
* plain string. We never force a shape — whatever the two Claudes agree on.
|
|
42
|
+
*/
|
|
43
|
+
function coerceState(raw) {
|
|
44
|
+
if (raw === undefined || raw === null) return undefined;
|
|
45
|
+
if (typeof raw === 'string') {
|
|
46
|
+
const s = raw.trim();
|
|
47
|
+
if (!s) return undefined;
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(s);
|
|
50
|
+
} catch {
|
|
51
|
+
return s; // not JSON — keep the raw string, it's a valid state too
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return raw;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function stateBytes(state) {
|
|
58
|
+
if (state === undefined) return 0;
|
|
59
|
+
try {
|
|
60
|
+
return Buffer.byteLength(typeof state === 'string' ? state : JSON.stringify(state), 'utf8');
|
|
61
|
+
} catch {
|
|
62
|
+
return Infinity;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Render whatever state is, legibly, for the local Claude to reason about.
|
|
68
|
+
*/
|
|
69
|
+
function renderState(state) {
|
|
70
|
+
if (state === undefined) return '_(no state yet — you define it)_';
|
|
71
|
+
if (typeof state === 'string') return '```\n' + state + '\n```';
|
|
72
|
+
try {
|
|
73
|
+
return '```json\n' + JSON.stringify(state, null, 2) + '\n```';
|
|
74
|
+
} catch {
|
|
75
|
+
return '_(unrenderable state)_';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const definition = {
|
|
80
|
+
name: 'vibe_play',
|
|
81
|
+
description:
|
|
82
|
+
'The open channel for playing ANY shared activity with another viber — you and their Claude improvise the rules, this tool just carries the state. No built-in game logic. Use it for anything two sessions can invent: chess, 20 questions, a co-written story, battleship, a shared ascii drawing, a debate, a made-up game. ' +
|
|
83
|
+
'Pass a freeform `state` (any JSON — the current board/story/whatever) and a `note` (a plain-language message to the other player and their Claude). ' +
|
|
84
|
+
'Call it with no state to READ the current session so you can interpret it and take your turn. This is the primitive under vibe_game / vibe_poem / vibe_corpse — reach for it when the activity is anything else.',
|
|
85
|
+
inputSchema: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
handle: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: 'Who to play with (e.g., @solienne)',
|
|
91
|
+
},
|
|
92
|
+
note: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
description:
|
|
95
|
+
"A plain-language message to the other player and their Claude — what you did, whose turn it is, the rules if you're starting. This is what shows in their DM.",
|
|
96
|
+
},
|
|
97
|
+
state: {
|
|
98
|
+
description:
|
|
99
|
+
'The current activity state, as any JSON value (object, array, or string). Whatever shape you and the other Claude agree on — a board, a story-so-far, a score, moves. Omit to just read the current state.',
|
|
100
|
+
},
|
|
101
|
+
kind: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
description:
|
|
104
|
+
"A freeform label for the activity, e.g. 'chess', '20-questions', 'story', 'battleship'. You coin it — it's just a hint so both sides know what game this is.",
|
|
105
|
+
},
|
|
106
|
+
done: {
|
|
107
|
+
type: 'boolean',
|
|
108
|
+
description: 'Mark the activity finished after this turn.',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
required: ['handle'],
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
async function handler(args) {
|
|
116
|
+
const initCheck = requireInit();
|
|
117
|
+
if (initCheck) return initCheck;
|
|
118
|
+
|
|
119
|
+
const me = config.getHandle();
|
|
120
|
+
const them = normalizeHandle(args.handle);
|
|
121
|
+
|
|
122
|
+
if (them === me) {
|
|
123
|
+
return { display: "You can't play with yourself here — invite someone: `vibe play @handle`." };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const thread = await store.getThread(me, them);
|
|
127
|
+
const last = getPlaySession(thread);
|
|
128
|
+
|
|
129
|
+
const incomingState = coerceState(args.state);
|
|
130
|
+
const note = args.note ? String(args.note).trim() : '';
|
|
131
|
+
const kind = (args.kind ? String(args.kind).trim() : '') || (last && last.kind) || null;
|
|
132
|
+
const done = args.done === true;
|
|
133
|
+
|
|
134
|
+
const hasWrite = incomingState !== undefined || note || done;
|
|
135
|
+
|
|
136
|
+
// ── READ: no write args → surface the current session for this Claude to act on.
|
|
137
|
+
if (!hasWrite) {
|
|
138
|
+
if (!last) {
|
|
139
|
+
return {
|
|
140
|
+
display:
|
|
141
|
+
`## play with @${them}\n\n` +
|
|
142
|
+
`_No shared activity going yet._\n\n` +
|
|
143
|
+
`Start anything — you and @${them}'s Claude make up the rules. Examples:\n` +
|
|
144
|
+
`\`vibe play @${them} --kind "chess" --note "you're white, your move" --state '{"board":"start"}'\`\n` +
|
|
145
|
+
`\`vibe play @${them} --kind "20-questions" --note "I'm thinking of something. ask away."\`\n` +
|
|
146
|
+
`\`vibe play @${them} --kind "story" --note "once upon a time..." --state "once upon a time"\`\n\n` +
|
|
147
|
+
`(For the classics there are shortcuts: \`vibe game\`, \`vibe poem\`, \`vibe corpse\`.)`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const who = last.by === me ? 'you' : `@${last.by}`;
|
|
152
|
+
let display = `## play with @${them}${last.kind ? ` — ${last.kind}` : ''}\n\n`;
|
|
153
|
+
if (last.note) display += `last from ${who}: "${last.note}"\n\n`;
|
|
154
|
+
display += `state:\n${renderState(last.state)}\n`;
|
|
155
|
+
if (last.done) {
|
|
156
|
+
display += `\n✨ This one's finished. Start another with \`vibe play @${them} …\``;
|
|
157
|
+
} else if (last.by === me) {
|
|
158
|
+
display += `\n_Waiting on @${them} — they play next._`;
|
|
159
|
+
} else {
|
|
160
|
+
display +=
|
|
161
|
+
`\n**Your move.** Interpret the state above, decide what happens next, and send it back:\n` +
|
|
162
|
+
`\`vibe play @${them}${last.kind ? ` --kind "${last.kind}"` : ''} --note "…" --state '{…}'\` (add \`--done\` to finish)`;
|
|
163
|
+
}
|
|
164
|
+
return { display };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── WRITE: take a turn.
|
|
168
|
+
// Carry the previous state forward if the caller didn't supply a new one.
|
|
169
|
+
const newState = incomingState !== undefined ? incomingState : last ? last.state : undefined;
|
|
170
|
+
|
|
171
|
+
if (stateBytes(newState) > MAX_STATE_BYTES) {
|
|
172
|
+
return {
|
|
173
|
+
display: `That state is too big (>${Math.round(MAX_STATE_BYTES / 1000)}KB). Keep the shared state compact — summarize instead of accumulating raw history.`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const payload = {
|
|
178
|
+
type: 'play',
|
|
179
|
+
kind: kind || null,
|
|
180
|
+
state: newState,
|
|
181
|
+
note: note || null,
|
|
182
|
+
by: me,
|
|
183
|
+
done,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// The DM body is what lands in their inbox. Lead with the note; add a light
|
|
187
|
+
// hint so their Claude knows to open the session and respond.
|
|
188
|
+
let body;
|
|
189
|
+
if (done) {
|
|
190
|
+
body = note ? `${note}\n\n(sealed our ${kind || 'game'} 🎲)` : `Sealed our ${kind || 'game'} 🎲`;
|
|
191
|
+
} else {
|
|
192
|
+
body = note || `Your move 🎲`;
|
|
193
|
+
body += `\n\n▸ open with \`vibe play @${me}\` to see the state and reply`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await store.sendMessage(me, them, body, 'dm', payload);
|
|
197
|
+
|
|
198
|
+
let display = `## play with @${them}${kind ? ` — ${kind}` : ''}\n\n`;
|
|
199
|
+
display += `state:\n${renderState(newState)}\n`;
|
|
200
|
+
if (done) {
|
|
201
|
+
display += `\n✨ Sealed and sent to @${them}. Start another anytime.`;
|
|
202
|
+
} else {
|
|
203
|
+
display += `\nSent to @${them}. Waiting for their move…`;
|
|
204
|
+
}
|
|
205
|
+
return { display };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = { definition, handler };
|
package/version.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.5.
|
|
2
|
+
"version": "0.5.10",
|
|
3
3
|
"updated": "2026-07-03",
|
|
4
|
-
"changelog": "
|
|
4
|
+
"changelog": "Play anything. `vibe_play @handle` is the open channel underneath the games — it carries an arbitrary state object over your DM thread and lets you and the other person's Claude improvise the rules to whatever you invent: chess, 20 questions, a co-written story, battleship, a shared drawing, a debate. No built-in game logic — the creativity is yours. `vibe_game`, `vibe_poem`, and `vibe_corpse` are still here as quick on-ramps. Ship together, play together.",
|
|
5
5
|
"features": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
6
|
+
"vibe_play — the open primitive: pass any JSON state + a note to another viber, read it back to take your turn, and improvise any activity two sessions can dream up",
|
|
7
|
+
"vibe_corpse — write an exquisite corpse: alternate lines blind to all but the last, then seal for the big reveal",
|
|
8
|
+
"vibe_game — play tic-tac-toe with another viber, move by move, over your DM thread",
|
|
9
|
+
"vibe_poem — build a collaborative emoji poem together, one line each"
|
|
9
10
|
],
|
|
10
11
|
"deprecated": [
|
|
11
12
|
"vibe_pair — use Matrix room invites",
|