slashvibe-mcp 0.5.6 → 0.5.8
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 +5 -0
- package/notify.js +7 -5
- package/package.json +1 -1
- package/store/api.js +12 -5
- package/tools/poem.js +208 -0
- package/version.json +6 -6
package/index.js
CHANGED
|
@@ -71,6 +71,7 @@ function inferPromptFromArgs(toolName, args) {
|
|
|
71
71
|
case 'reservations': return 'list reservations';
|
|
72
72
|
case 'solo_game': return `play ${args.game || 'game'}`;
|
|
73
73
|
case 'game': return `play ${args.game || 'game'} with ${handle}`;
|
|
74
|
+
case 'poem': return args.line ? `add a line to poem with ${handle}` : `emoji poem with ${handle}`;
|
|
74
75
|
case 'multiplayer_game': return `multiplayer ${args.game || 'game'}`;
|
|
75
76
|
case 'drawing': return args.action ? `drawing ${args.action}` : 'collaborative drawing';
|
|
76
77
|
case 'crossword': return `crossword ${args.action || 'daily'}`;
|
|
@@ -255,6 +256,10 @@ const coreTools = {
|
|
|
255
256
|
vibe_ship: require('./tools/ship'),
|
|
256
257
|
vibe_feed: require('./tools/feed'),
|
|
257
258
|
|
|
259
|
+
// Play — the original January magic: shared experiences over the DM transport
|
|
260
|
+
vibe_game: require('./tools/game'),
|
|
261
|
+
vibe_poem: require('./tools/poem'),
|
|
262
|
+
|
|
258
263
|
// Matrix rooms — multiplayer
|
|
259
264
|
vibe_matrix_rooms: require('./tools/matrix-rooms'),
|
|
260
265
|
vibe_matrix_read: require('./tools/matrix-read'),
|
package/notify.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* This is escalation, not baseline.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
const {
|
|
13
|
+
const { execFile } = require('child_process');
|
|
14
14
|
const os = require('os');
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
@@ -56,16 +56,18 @@ function showNotification(title, message, sound = false, bell = true) {
|
|
|
56
56
|
ringBell();
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
// Escape quotes
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
// Escape double quotes so title/message stay inside their AppleScript string
|
|
60
|
+
// literals. Everything is passed to osascript via execFile (no shell), so
|
|
61
|
+
// sender-controlled single quotes can no longer break out into a shell command.
|
|
62
|
+
const safeTitle = String(title).replace(/"/g, '\\"');
|
|
63
|
+
const safeMessage = String(message).replace(/"/g, '\\"');
|
|
62
64
|
|
|
63
65
|
let script = `display notification "${safeMessage}" with title "${safeTitle}"`;
|
|
64
66
|
if (sound) {
|
|
65
67
|
script += ` sound name "Ping"`;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
execFile('osascript', ['-e', script], (err) => {
|
|
69
71
|
if (err) {
|
|
70
72
|
// Silently fail - notifications are best-effort
|
|
71
73
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slashvibe-mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
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/store/api.js
CHANGED
|
@@ -694,15 +694,22 @@ async function getThread(myHandle, theirHandle) {
|
|
|
694
694
|
console.error('[getThread] V2 failed, falling back to v1');
|
|
695
695
|
}
|
|
696
696
|
|
|
697
|
-
// V1 fallback
|
|
697
|
+
// V1 fallback — live /api/messages returns { messages: [...] } with snake_case
|
|
698
|
+
// fields (body, created_at, source_client). Older builds returned { thread: [...] }
|
|
699
|
+
// with (text, createdAt). Accept BOTH shapes so the read path survives whichever
|
|
700
|
+
// the server sends — otherwise payload-bearing DMs (games, poems) silently vanish
|
|
701
|
+
// and every turn reads as a fresh game. This is the path the game/poem tools rely
|
|
702
|
+
// on while the v2 /threads/:id detail route is unavailable.
|
|
698
703
|
const result = await request('GET', `/api/messages?user=${myHandle}&with=${theirHandle}`);
|
|
699
|
-
|
|
704
|
+
const rows = result.messages || result.thread || [];
|
|
705
|
+
return rows.map(m => ({
|
|
700
706
|
from: m.from,
|
|
701
707
|
isAgent: m.isAgent || m.is_agent || false, // Support both naming conventions
|
|
702
|
-
body: m.text,
|
|
708
|
+
body: m.body ?? m.text,
|
|
703
709
|
payload: m.payload || null,
|
|
704
|
-
|
|
705
|
-
|
|
710
|
+
source_client: m.source_client || null,
|
|
711
|
+
timestamp: new Date(m.created_at || m.createdAt).getTime(),
|
|
712
|
+
direction: m.direction || (m.from === myHandle ? 'sent' : 'received'),
|
|
706
713
|
}));
|
|
707
714
|
} catch (e) {
|
|
708
715
|
console.error('Thread failed:', e.message);
|
package/tools/poem.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibe poem — Write an emoji poem together, one line at a time
|
|
3
|
+
*
|
|
4
|
+
* The original January /vibe magic: two people co-authoring something small
|
|
5
|
+
* and playful inside Claude Code, passed back and forth over the DM transport.
|
|
6
|
+
* Each turn adds a line; the poem state travels in the message payload (durable
|
|
7
|
+
* via the Postgres payload column), exactly like `vibe game`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const config = require('../config');
|
|
11
|
+
const store = require('../store');
|
|
12
|
+
const { requireInit, normalizeHandle } = require('./_shared');
|
|
13
|
+
|
|
14
|
+
// Poems seal automatically at this many lines (either player can seal earlier).
|
|
15
|
+
const MAX_LINES = 8;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Find the most recent poem state in a DM thread.
|
|
19
|
+
*/
|
|
20
|
+
function getPoemState(thread) {
|
|
21
|
+
for (let i = thread.length - 1; i >= 0; i--) {
|
|
22
|
+
const p = thread[i].payload;
|
|
23
|
+
if (p && p.type === 'poem' && p.state) return p.state;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Render a poem for the terminal.
|
|
30
|
+
*/
|
|
31
|
+
function renderPoem(state, { withMe, them } = {}) {
|
|
32
|
+
const lines = state.lines || [];
|
|
33
|
+
let out = '📜 **emoji poem';
|
|
34
|
+
if (state.title) out += ` — ${state.title}`;
|
|
35
|
+
out += '**\n\n';
|
|
36
|
+
if (!lines.length) {
|
|
37
|
+
out += '_(blank page — add the first line)_\n';
|
|
38
|
+
} else {
|
|
39
|
+
out += '```\n';
|
|
40
|
+
for (const l of lines) out += `${l.text}\n`;
|
|
41
|
+
out += '```\n';
|
|
42
|
+
out += `\n_by ${uniqueAuthors(lines).map((a) => '@' + a).join(' & ')}_\n`;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function uniqueAuthors(lines) {
|
|
48
|
+
const seen = [];
|
|
49
|
+
for (const l of lines) if (!seen.includes(l.author)) seen.push(l.author);
|
|
50
|
+
return seen;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whose turn is it? Alternate off the last line's author. If the last line was
|
|
55
|
+
* mine, it's their turn; if theirs (or empty), it's mine.
|
|
56
|
+
*/
|
|
57
|
+
function isMyTurn(state, me) {
|
|
58
|
+
const lines = state.lines || [];
|
|
59
|
+
if (!lines.length) return true; // whoever opens goes first
|
|
60
|
+
return lines[lines.length - 1].author !== me;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function postPoemToBoard(state, authors) {
|
|
64
|
+
const API_URL = process.env.VIBE_API_URL || 'https://www.slashvibe.dev';
|
|
65
|
+
const body = (state.lines || []).map((l) => l.text).join(' / ');
|
|
66
|
+
try {
|
|
67
|
+
await fetch(`${API_URL}/api/board`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
author: 'echo',
|
|
72
|
+
content: `${authors.map((a) => '@' + a).join(' & ')} finished an emoji poem: ${body}`,
|
|
73
|
+
category: 'general',
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.error('[poem] Failed to post to board:', e.message);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const definition = {
|
|
82
|
+
name: 'vibe_poem',
|
|
83
|
+
description:
|
|
84
|
+
'Write an emoji poem together, one line at a time. Pass it back and forth with someone — each turn adds a line. `vibe poem @handle --line "🌊🌙 the tide remembers"`',
|
|
85
|
+
inputSchema: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
handle: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: 'Who to write with (e.g., @solienne)',
|
|
91
|
+
},
|
|
92
|
+
line: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
description: 'The line to add (emoji-forward, but any text welcome)',
|
|
95
|
+
},
|
|
96
|
+
title: {
|
|
97
|
+
type: 'string',
|
|
98
|
+
description: 'Optional title, only used when starting a new poem',
|
|
99
|
+
},
|
|
100
|
+
done: {
|
|
101
|
+
type: 'boolean',
|
|
102
|
+
description: 'Seal the poem as finished after adding your line',
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
required: ['handle'],
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
async function handler(args) {
|
|
110
|
+
const initCheck = requireInit();
|
|
111
|
+
if (initCheck) return initCheck;
|
|
112
|
+
|
|
113
|
+
const { line, title, done } = args;
|
|
114
|
+
const me = config.getHandle();
|
|
115
|
+
const them = normalizeHandle(args.handle);
|
|
116
|
+
|
|
117
|
+
if (them === me) {
|
|
118
|
+
return { display: "You can't write a poem with yourself. Invite someone: `vibe poem @handle`." };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const thread = await store.getThread(me, them);
|
|
122
|
+
let state = getPoemState(thread);
|
|
123
|
+
|
|
124
|
+
// No line provided → show the current poem (or start a fresh one).
|
|
125
|
+
if (!line) {
|
|
126
|
+
if (!state) {
|
|
127
|
+
const newState = { lines: [], title: title || null, done: false, startedBy: me };
|
|
128
|
+
await store.sendMessage(
|
|
129
|
+
me,
|
|
130
|
+
them,
|
|
131
|
+
`Let's write an emoji poem together 📜 Add a line: \`vibe poem @${me} --line "…"\``,
|
|
132
|
+
'dm',
|
|
133
|
+
{ type: 'poem', state: newState }
|
|
134
|
+
);
|
|
135
|
+
return {
|
|
136
|
+
display:
|
|
137
|
+
`## New emoji poem with @${them}\n\n${renderPoem(newState)}\n` +
|
|
138
|
+
`Add the first line: \`vibe poem @${them} --line "🌱 a small green start"\``,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Show existing poem.
|
|
143
|
+
const authors = uniqueAuthors(state.lines);
|
|
144
|
+
let display = `## emoji poem with @${them}\n\n${renderPoem(state)}\n`;
|
|
145
|
+
if (state.done) {
|
|
146
|
+
display += `\n✨ Finished. Start another: \`vibe poem @${them} --line "…"\``;
|
|
147
|
+
} else if (isMyTurn(state, me)) {
|
|
148
|
+
display += `\nYour turn — add a line: \`vibe poem @${them} --line "…"\``;
|
|
149
|
+
} else {
|
|
150
|
+
display += `\nWaiting for @${them} to add the next line…`;
|
|
151
|
+
}
|
|
152
|
+
return { display };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Adding a line.
|
|
156
|
+
if (!state) {
|
|
157
|
+
state = { lines: [], title: title || null, done: false, startedBy: me };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (state.done) {
|
|
161
|
+
return {
|
|
162
|
+
display: `This poem is finished. Start a new one: \`vibe poem @${them} --line "…"\``,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!isMyTurn(state, me)) {
|
|
167
|
+
return {
|
|
168
|
+
display:
|
|
169
|
+
`It's @${them}'s turn to add a line.\n\n${renderPoem(state)}\n` +
|
|
170
|
+
`Nudge them, or wait for their line.`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const text = String(line).trim();
|
|
175
|
+
if (!text) return { display: 'Give me a line to add: `vibe poem @handle --line "…"`' };
|
|
176
|
+
if (text.length > 200) return { display: 'Keep each line under 200 characters.' };
|
|
177
|
+
|
|
178
|
+
const newLines = [...(state.lines || []), { author: me, text }];
|
|
179
|
+
const shouldSeal = done === true || newLines.length >= MAX_LINES;
|
|
180
|
+
const newState = {
|
|
181
|
+
...state,
|
|
182
|
+
lines: newLines,
|
|
183
|
+
done: shouldSeal,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const authors = uniqueAuthors(newLines);
|
|
187
|
+
let message;
|
|
188
|
+
if (shouldSeal) {
|
|
189
|
+
message = `Added a line and sealed our poem 📜✨\n\n${newLines.map((l) => l.text).join('\n')}`;
|
|
190
|
+
if (authors.length > 1) postPoemToBoard(newState, authors);
|
|
191
|
+
} else if (isMyTurn(newState, them)) {
|
|
192
|
+
message = `Added a line — your turn 📜\n"${text}"`;
|
|
193
|
+
} else {
|
|
194
|
+
message = `Added a line 📜\n"${text}"`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await store.sendMessage(me, them, message, 'dm', { type: 'poem', state: newState });
|
|
198
|
+
|
|
199
|
+
let display = `## emoji poem with @${them}\n\n${renderPoem(newState)}\n`;
|
|
200
|
+
if (shouldSeal) {
|
|
201
|
+
display += `\n✨ Poem sealed${newLines.length >= MAX_LINES ? ` (${MAX_LINES} lines)` : ''}. Start another anytime.`;
|
|
202
|
+
} else {
|
|
203
|
+
display += `\nSent to @${them}. Waiting for their line…`;
|
|
204
|
+
}
|
|
205
|
+
return { display };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = { definition, handler };
|
package/version.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.5.
|
|
3
|
-
"updated": "2026-
|
|
4
|
-
"changelog": "
|
|
2
|
+
"version": "0.5.8",
|
|
3
|
+
"updated": "2026-07-03",
|
|
4
|
+
"changelog": "The January magic is back. Play a game or write a poem with another builder — right inside your terminal, turn by turn, over your DM thread. `vibe_game` for tic-tac-toe, `vibe_poem` for a collaborative emoji poem. Ship together, play together.",
|
|
5
5
|
"features": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
6
|
+
"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"
|
|
9
9
|
],
|
|
10
10
|
"deprecated": [
|
|
11
11
|
"vibe_pair — use Matrix room invites",
|