slashvibe-mcp 0.5.3 → 0.5.5
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 +8 -1
- package/package.json +1 -1
- package/tools/email.js +117 -0
- package/tools/init.js +80 -15
- package/version.json +5 -5
package/index.js
CHANGED
|
@@ -248,6 +248,9 @@ const coreTools = {
|
|
|
248
248
|
vibe_inbox: require('./tools/inbox'),
|
|
249
249
|
vibe_reply: require('./tools/reply'),
|
|
250
250
|
|
|
251
|
+
// Return loop — get pinged about DMs you miss while away
|
|
252
|
+
vibe_email: require('./tools/email'),
|
|
253
|
+
|
|
251
254
|
// Ship — share what you built
|
|
252
255
|
vibe_ship: require('./tools/ship'),
|
|
253
256
|
vibe_feed: require('./tools/feed'),
|
|
@@ -356,7 +359,11 @@ class VibeMCPServer {
|
|
|
356
359
|
const actionName = params.name.replace('vibe_', '');
|
|
357
360
|
const initTool = tools['vibe_init'];
|
|
358
361
|
try {
|
|
359
|
-
|
|
362
|
+
// Forward the first command's args into init rather than dropping them.
|
|
363
|
+
// vibe_init reads only {handle, one_liner, auth_method} and ignores any
|
|
364
|
+
// extra keys, so this is safe for every command yet lets relevant
|
|
365
|
+
// context (e.g. a handle the caller already supplied) carry into auth.
|
|
366
|
+
const initResult = await initTool.handler(params.arguments || {});
|
|
360
367
|
const initDisplay = initResult.display || JSON.stringify(initResult, null, 2);
|
|
361
368
|
|
|
362
369
|
// After auth, emit tools/list_changed so Claude sees full toolset
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slashvibe-mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
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/email.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibe_email — Add (or view) the email where /vibe pings you about DMs you miss
|
|
3
|
+
*
|
|
4
|
+
* The return loop: when someone DMs you while you're away from Claude Code,
|
|
5
|
+
* /vibe emails you so you actually come back. That only works if we have your
|
|
6
|
+
* address — GitHub doesn't hand us a verified email, so this is the one-time
|
|
7
|
+
* opt-in that arms it. Offline-only, 6h cooldown, one-click unsubscribe.
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* - "vibe email me@example.com" → set it
|
|
11
|
+
* - "vibe email" → show what's on file
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const config = require('../config');
|
|
15
|
+
const { requireInit } = require('./_shared');
|
|
16
|
+
|
|
17
|
+
// Same shape the server validates with (api/profile/update.js isValidEmail)
|
|
18
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
19
|
+
|
|
20
|
+
const definition = {
|
|
21
|
+
name: 'vibe_email',
|
|
22
|
+
description: "Set the email where /vibe alerts you about DMs you miss while away. No address on file → no offline pings. Call with no args to see what's set.",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
email: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'Your email address. Omit to view the address currently on file.'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
async function handler(args) {
|
|
35
|
+
const initCheck = requireInit();
|
|
36
|
+
if (initCheck) return initCheck;
|
|
37
|
+
|
|
38
|
+
const token = config.getAuthToken();
|
|
39
|
+
if (!token) {
|
|
40
|
+
return {
|
|
41
|
+
display: '❌ **Not authenticated**\n\nRun `vibe init` to sign in with GitHub first.'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const apiUrl = config.getApiUrl();
|
|
46
|
+
const authHeaders = {
|
|
47
|
+
'Content-Type': 'application/json',
|
|
48
|
+
'Authorization': `Bearer ${token}`
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const { email } = args;
|
|
52
|
+
|
|
53
|
+
// ─── No arg: show what's on file ──────────────────────────────────────
|
|
54
|
+
if (!email || !String(email).trim()) {
|
|
55
|
+
try {
|
|
56
|
+
const r = await fetch(`${apiUrl}/api/profile/update`, { headers: authHeaders });
|
|
57
|
+
if (r.ok) {
|
|
58
|
+
const me = await r.json();
|
|
59
|
+
if (me.email) {
|
|
60
|
+
return {
|
|
61
|
+
display: `📧 **Email on file:** ${me.email}\n\n` +
|
|
62
|
+
"You'll get a heads-up here when someone DMs you while you're away from Claude Code.\n\n" +
|
|
63
|
+
'_Change it: `vibe email new@address.com` · stop alerts via the unsubscribe link in any email._'
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// fall through to the prompt
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
display: '📧 **No email on file yet**\n\n' +
|
|
72
|
+
"Add one and /vibe will ping you when someone DMs you while you're away — so you actually come back.\n\n" +
|
|
73
|
+
'`vibe email you@example.com`\n\n' +
|
|
74
|
+
'_Offline DMs only · max one every 6h · one-click unsubscribe in every email._'
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Arg given: validate + save ───────────────────────────────────────
|
|
79
|
+
const addr = String(email).trim();
|
|
80
|
+
if (!EMAIL_RE.test(addr)) {
|
|
81
|
+
return {
|
|
82
|
+
display: `❌ **That doesn't look like an email**\n\nGot: \`${addr}\`\n\nTry again: \`vibe email you@example.com\``
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const r = await fetch(`${apiUrl}/api/profile/update`, {
|
|
88
|
+
method: 'PATCH',
|
|
89
|
+
headers: authHeaders,
|
|
90
|
+
body: JSON.stringify({ email: addr })
|
|
91
|
+
});
|
|
92
|
+
const result = await r.json().catch(() => ({}));
|
|
93
|
+
|
|
94
|
+
if (!r.ok) {
|
|
95
|
+
if (r.status === 400 && result.error === 'Invalid email format') {
|
|
96
|
+
return { display: `❌ **Invalid email format**\n\nGot: \`${addr}\`` };
|
|
97
|
+
}
|
|
98
|
+
if (r.status === 401) {
|
|
99
|
+
return { display: '❌ **Session expired**\n\nRun `vibe init` to sign in again, then retry.' };
|
|
100
|
+
}
|
|
101
|
+
return { display: `❌ **Couldn't save email**\n\n${result.error || 'Please try again.'}` };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
display: `✅ **Email saved:** ${addr}\n\n` +
|
|
106
|
+
"Next time someone DMs you while you're away from Claude Code, you'll get an email so you don't miss it.\n\n" +
|
|
107
|
+
'_Offline DMs only · max one every 6h · one-click unsubscribe in every email._'
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('[email] Error:', error);
|
|
111
|
+
return {
|
|
112
|
+
display: `❌ **Couldn't save email**\n\nError: ${error.message}\n\nPlease try again.`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { definition, handler };
|
package/tools/init.js
CHANGED
|
@@ -22,24 +22,43 @@ const CALLBACK_PORT = 9876;
|
|
|
22
22
|
const API_BASE = 'https://www.slashvibe.dev';
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Fetch
|
|
25
|
+
* Fetch presence from the API, split into humans vs agents.
|
|
26
|
+
* The green dot should mean something — so we count real builders
|
|
27
|
+
* separately from agents and let callers frame each honestly.
|
|
26
28
|
*/
|
|
27
|
-
async function
|
|
29
|
+
async function getPresenceCounts() {
|
|
28
30
|
try {
|
|
29
31
|
const response = await fetch(`${API_BASE}/api/presence`);
|
|
30
|
-
if (!response.ok) return 0;
|
|
32
|
+
if (!response.ok) return { online: 0, humans: 0, agents: 0 };
|
|
31
33
|
const data = await response.json();
|
|
32
|
-
|
|
34
|
+
const everyone = [...(data.active || []), ...(data.away || [])];
|
|
35
|
+
const humans = everyone.filter(u => !u.isAgent).length;
|
|
36
|
+
const agents = everyone.filter(u => u.isAgent).length;
|
|
37
|
+
return { online: everyone.length, humans, agents };
|
|
33
38
|
} catch (e) {
|
|
34
|
-
return 0;
|
|
39
|
+
return { online: 0, humans: 0, agents: 0 };
|
|
35
40
|
}
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Short presence label that distinguishes humans from agents, e.g.
|
|
45
|
+
* "2 builders · 7 agents", "3 builders online", "8 agents online".
|
|
46
|
+
* Returns null when nobody is around (callers pick their own empty copy).
|
|
47
|
+
*/
|
|
48
|
+
function formatPresenceLabel({ humans = 0, agents = 0 } = {}) {
|
|
49
|
+
const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`;
|
|
50
|
+
if (humans > 0 && agents > 0) return `${plural(humans, 'builder')} · ${plural(agents, 'agent')}`;
|
|
51
|
+
if (humans > 0) return `${plural(humans, 'builder')} online`;
|
|
52
|
+
if (agents > 0) return `${plural(agents, 'agent')} online`;
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
38
56
|
/**
|
|
39
57
|
* Generate welcome banner for new users (pre-auth, no handle yet)
|
|
40
58
|
*/
|
|
41
|
-
function generatePreAuthBanner(
|
|
42
|
-
const
|
|
59
|
+
function generatePreAuthBanner(presence) {
|
|
60
|
+
const label = formatPresenceLabel(presence);
|
|
61
|
+
const onlineText = label ? `🟢 ${label}` : '🟢 join the crew';
|
|
43
62
|
return `
|
|
44
63
|
█░█ █ █▄▄ █▀▀ 🚀 ship together
|
|
45
64
|
▀▄▀ █ █▄█ ██▄ ${onlineText}
|
|
@@ -50,14 +69,15 @@ function generatePreAuthBanner(onlineCount) {
|
|
|
50
69
|
/**
|
|
51
70
|
* Generate welcome banner for authenticated users (with handle + unread)
|
|
52
71
|
*/
|
|
53
|
-
function generateAuthBanner(handle, unreadCount,
|
|
72
|
+
function generateAuthBanner(handle, unreadCount, presence) {
|
|
54
73
|
// Format: logo | handle + unread | tagline + online
|
|
55
74
|
// Keep alignment consistent with original banner
|
|
56
75
|
const handleCol = `@${handle}`.padEnd(16);
|
|
57
76
|
const unreadCol = unreadCount > 0 ? `📬 ${unreadCount} unread`.padEnd(14) : `📬 0 messages`.padEnd(14);
|
|
77
|
+
const onlineText = formatPresenceLabel(presence) || 'just you so far';
|
|
58
78
|
|
|
59
79
|
return ` █░█ █ █▄▄ █▀▀ ${handleCol} 🚀 ship together
|
|
60
|
-
▀▄▀ █ █▄█ ██▄ ${unreadCol} 🟢 ${
|
|
80
|
+
▀▄▀ █ █▄█ ██▄ ${unreadCol} 🟢 ${onlineText}
|
|
61
81
|
──────────────────────────────────────────────────`;
|
|
62
82
|
}
|
|
63
83
|
|
|
@@ -162,6 +182,31 @@ async function fetchGitHubFriends(handle) {
|
|
|
162
182
|
}
|
|
163
183
|
}
|
|
164
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Does this freshly-authed user have an email on file?
|
|
187
|
+
*
|
|
188
|
+
* GitHub email capture (callback.js) fills this for most users, but accounts
|
|
189
|
+
* with a private GitHub email come back NULL — and a null-email user is exactly
|
|
190
|
+
* the one we can't reach when a DM lands while they're away. We only nudge that
|
|
191
|
+
* gap population, so people who already gave us an address aren't nagged.
|
|
192
|
+
*
|
|
193
|
+
* Returns true (has email), false (no email → nudge), or null (unknown → stay
|
|
194
|
+
* quiet rather than guess).
|
|
195
|
+
*/
|
|
196
|
+
async function fetchHasEmail(token) {
|
|
197
|
+
if (!token) return null;
|
|
198
|
+
try {
|
|
199
|
+
const r = await fetch(`${API_BASE}/api/profile/update`, {
|
|
200
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
201
|
+
});
|
|
202
|
+
if (!r.ok) return null;
|
|
203
|
+
const me = await r.json();
|
|
204
|
+
return !!me.email;
|
|
205
|
+
} catch (e) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
165
210
|
/**
|
|
166
211
|
* Send personalized welcome from @seth
|
|
167
212
|
* Returns the welcome message content so we can show it inline
|
|
@@ -609,8 +654,8 @@ To check messages: \`vibe inbox\``
|
|
|
609
654
|
// ===========================================
|
|
610
655
|
// Show welcome banner (pre-auth)
|
|
611
656
|
// ===========================================
|
|
612
|
-
const
|
|
613
|
-
const welcomeBanner = generatePreAuthBanner(
|
|
657
|
+
const presence = await getPresenceCounts();
|
|
658
|
+
const welcomeBanner = generatePreAuthBanner(presence);
|
|
614
659
|
|
|
615
660
|
// ===========================================
|
|
616
661
|
// BROWSER AUTH (Default): GitHub OAuth
|
|
@@ -663,8 +708,18 @@ To check messages: \`vibe inbox\``
|
|
|
663
708
|
]);
|
|
664
709
|
} catch (e) {}
|
|
665
710
|
|
|
711
|
+
// Do we have an email to reach them at when a DM lands while away?
|
|
712
|
+
// (non-blocking, 2s timeout — never hold up onboarding for this)
|
|
713
|
+
let hasEmail = null;
|
|
714
|
+
try {
|
|
715
|
+
hasEmail = await Promise.race([
|
|
716
|
+
fetchHasEmail(config.getAuthToken()),
|
|
717
|
+
new Promise(resolve => setTimeout(() => resolve(null), 2000))
|
|
718
|
+
]);
|
|
719
|
+
} catch (e) {}
|
|
720
|
+
|
|
666
721
|
// Generate authenticated banner with handle + unread (3 lines only - won't collapse)
|
|
667
|
-
const authBanner = generateAuthBanner(result.handle, unreadCount,
|
|
722
|
+
const authBanner = generateAuthBanner(result.handle, unreadCount, presence);
|
|
668
723
|
|
|
669
724
|
// Build friends section if we have data
|
|
670
725
|
let friendsSection = '';
|
|
@@ -687,8 +742,11 @@ To check messages: \`vibe inbox\``
|
|
|
687
742
|
// landing in someone else's terminal. Only commands that exist (who /
|
|
688
743
|
// @handle / ship). Avoid pointing the reply at a specific founder handle
|
|
689
744
|
// here — it can misroute; reading the inbox is the safe path to reply.
|
|
690
|
-
|
|
691
|
-
|
|
745
|
+
// Only promise a crowd of *humans* when there's actually one to meet —
|
|
746
|
+
// presence skews agent-heavy, and overselling an empty room is how the
|
|
747
|
+
// green dot stops meaning anything. Agents still show in `vibe who`.
|
|
748
|
+
const whoNudge = presence.humans > 1
|
|
749
|
+
? `**"vibe who"** — see the ${presence.humans} builders online right now`
|
|
692
750
|
: `**"vibe who"** — see who's building right now`;
|
|
693
751
|
const nextSteps = `\n\n---\n**Your first 60 seconds**\n`
|
|
694
752
|
+ `1. ${whoNudge}\n`
|
|
@@ -696,8 +754,15 @@ To check messages: \`vibe inbox\``
|
|
|
696
754
|
+ `3. **"vibe ship <what you're building>"** — put yourself on the board\n\n`
|
|
697
755
|
+ `_Stuck? Say **"vibe help"**._`;
|
|
698
756
|
|
|
757
|
+
// One-time email nudge — only for users we can't otherwise reach (no
|
|
758
|
+
// address on file). This is the return loop: a DM that lands while you're
|
|
759
|
+
// away from Claude Code emails you so you actually come back.
|
|
760
|
+
const emailNudge = hasEmail === false
|
|
761
|
+
? `\n\n---\n📧 **Don't miss a DM** — add an email and /vibe pings you when someone messages you while you're away:\n**"vibe email you@example.com"** _(offline only · one-click unsubscribe)_`
|
|
762
|
+
: '';
|
|
763
|
+
|
|
699
764
|
return {
|
|
700
|
-
display: authBanner + friendsSection + welcomeSection + nextSteps,
|
|
765
|
+
display: authBanner + friendsSection + welcomeSection + nextSteps + emailNudge,
|
|
701
766
|
onboarding: {
|
|
702
767
|
isNewUser: true,
|
|
703
768
|
handle: result.handle,
|
package/version.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.5.
|
|
2
|
+
"version": "0.5.5",
|
|
3
3
|
"updated": "2026-06-29",
|
|
4
|
-
"changelog": "
|
|
4
|
+
"changelog": "Presence now tells the truth: builders and agents are counted separately, so the green dot means something. Onboarding only promises a room full of people when real humans are actually online.",
|
|
5
5
|
"features": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
6
|
+
"Presence banner shows the human/agent split (e.g. '2 builders · 7 agents') instead of one blended count",
|
|
7
|
+
"First-60s 'vibe who' nudge promises a crowd only when 2+ humans are online; agents still appear in vibe who",
|
|
8
|
+
"Honest empty-room copy when you're the first one in"
|
|
9
9
|
],
|
|
10
10
|
"deprecated": [
|
|
11
11
|
"vibe_pair — use Matrix room invites",
|