slashvibe-mcp 0.5.4 → 0.5.6

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 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
- const initResult = await initTool.handler({});
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.4",
3
+ "version": "0.5.6",
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
@@ -182,6 +182,31 @@ async function fetchGitHubFriends(handle) {
182
182
  }
183
183
  }
184
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
+
185
210
  /**
186
211
  * Send personalized welcome from @seth
187
212
  * Returns the welcome message content so we can show it inline
@@ -616,12 +641,45 @@ async function handler(args) {
616
641
  if (config.hasOAuth()) {
617
642
  const existingHandle = config.getHandle();
618
643
  if (existingHandle) {
644
+ // Enrich the returning-user surface — this fires on every `vibe` for an
645
+ // already-authed user, so it's our highest-frequency touchpoint. Surface
646
+ // unread (the reason to come back) and, if we have no email on file, nudge
647
+ // the return loop right at the teachable moment. Both are best-effort with
648
+ // tight timeouts — never hold up the response; degrade to the plain message.
649
+ let unreadCount = 0;
650
+ let hasEmail = null;
651
+ try {
652
+ [unreadCount, hasEmail] = await Promise.all([
653
+ Promise.race([
654
+ store.getUnreadCount(existingHandle),
655
+ new Promise(resolve => setTimeout(() => resolve(0), 1500))
656
+ ]).catch(() => 0),
657
+ Promise.race([
658
+ fetchHasEmail(config.getAuthToken()),
659
+ new Promise(resolve => setTimeout(() => resolve(null), 1500))
660
+ ]).catch(() => null)
661
+ ]);
662
+ } catch (e) {}
663
+
664
+ const unreadLine = unreadCount > 0
665
+ ? `\n\n📬 **${unreadCount} unread** — say \`vibe inbox\` to read ${unreadCount > 1 ? 'them' : 'it'}.`
666
+ : '';
667
+
668
+ // Only nudge when we genuinely can't reach them out-of-band. If they have
669
+ // unread right now, the value is concrete: "these landed while you were
670
+ // away — next time we'll email you." Otherwise it's a gentle one-liner.
671
+ const emailNudge = hasEmail === false
672
+ ? (unreadCount > 0
673
+ ? `\n\n📧 These landed while you were away. Add an email and /vibe pings you next time — say **"vibe email you@example.com"** _(offline only · one-click unsubscribe)_.`
674
+ : `\n\n📧 **Don't miss a DM** — add an email and /vibe pings you when someone messages you while you're away: **"vibe email you@example.com"** _(offline only · one-click unsubscribe)_.`)
675
+ : '';
676
+
619
677
  return {
620
- display: `## Already signed in as @${existingHandle}
678
+ display: `## Already signed in as @${existingHandle}${unreadLine}
621
679
 
622
680
  To sign out and re-authenticate: \`vibe logout\`
623
681
  To see who's online: \`vibe who\`
624
- To check messages: \`vibe inbox\``
682
+ To check messages: \`vibe inbox\`${emailNudge}`
625
683
  };
626
684
  }
627
685
  }
@@ -683,6 +741,16 @@ To check messages: \`vibe inbox\``
683
741
  ]);
684
742
  } catch (e) {}
685
743
 
744
+ // Do we have an email to reach them at when a DM lands while away?
745
+ // (non-blocking, 2s timeout — never hold up onboarding for this)
746
+ let hasEmail = null;
747
+ try {
748
+ hasEmail = await Promise.race([
749
+ fetchHasEmail(config.getAuthToken()),
750
+ new Promise(resolve => setTimeout(() => resolve(null), 2000))
751
+ ]);
752
+ } catch (e) {}
753
+
686
754
  // Generate authenticated banner with handle + unread (3 lines only - won't collapse)
687
755
  const authBanner = generateAuthBanner(result.handle, unreadCount, presence);
688
756
 
@@ -719,8 +787,15 @@ To check messages: \`vibe inbox\``
719
787
  + `3. **"vibe ship <what you're building>"** — put yourself on the board\n\n`
720
788
  + `_Stuck? Say **"vibe help"**._`;
721
789
 
790
+ // One-time email nudge — only for users we can't otherwise reach (no
791
+ // address on file). This is the return loop: a DM that lands while you're
792
+ // away from Claude Code emails you so you actually come back.
793
+ const emailNudge = hasEmail === false
794
+ ? `\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)_`
795
+ : '';
796
+
722
797
  return {
723
- display: authBanner + friendsSection + welcomeSection + nextSteps,
798
+ display: authBanner + friendsSection + welcomeSection + nextSteps + emailNudge,
724
799
  onboarding: {
725
800
  isNewUser: true,
726
801
  handle: result.handle,
package/version.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
- "version": "0.5.4",
3
- "updated": "2026-06-29",
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.",
2
+ "version": "0.5.6",
3
+ "updated": "2026-06-30",
4
+ "changelog": "Come back to what you missed. Returning to /vibe now shows your unread count right away, and if we don't have an email to reach you at, we offer to arm the return-loop ping at the moment it matters when a DM just landed while you were away.",
5
5
  "features": [
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"
6
+ "Returning 'already signed in' surface now shows your unread count instead of a static line",
7
+ "Contextual email nudge on return: if a DM landed while you were away and we can't reach you, we offer 'vibe email' right then",
8
+ "One-line, offline-only, one-click unsubscribe — the return loop only pings when you're actually gone"
9
9
  ],
10
10
  "deprecated": [
11
11
  "vibe_pair — use Matrix room invites",