slashvibe-mcp 0.5.12 → 0.5.13

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
@@ -249,6 +249,9 @@ const coreTools = {
249
249
  vibe_inbox: require('./tools/inbox'),
250
250
  vibe_reply: require('./tools/reply'),
251
251
 
252
+ // Received collaboration — land the newcomer mid-conversation with a topical match
253
+ vibe_intro: require('./tools/intro'),
254
+
252
255
  // Return loop — get pinged about DMs you miss while away
253
256
  vibe_email: require('./tools/email'),
254
257
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slashvibe-mcp",
3
- "version": "0.5.12",
3
+ "version": "0.5.13",
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/_shared.js CHANGED
@@ -284,6 +284,120 @@ function firstDmNudge(others, threads) {
284
284
  );
285
285
  }
286
286
 
287
+ // ─────────────────────────────────────────────────────────────
288
+ // Received collaboration (topical intro on onboarding)
289
+ // ─────────────────────────────────────────────────────────────
290
+
291
+ // Words too generic to signal a shared topic. Kept aligned with the
292
+ // server-side building-keyword filter in api/lib/relevancy.js.
293
+ const INTRO_STOPWORDS = new Set([
294
+ 'the', 'and', 'for', 'with', 'that', 'this', 'from', 'will', 'app', 'apps',
295
+ 'using', 'your', 'you', 'building', 'build', 'built', 'working', 'work',
296
+ 'make', 'making', 'some', 'into', 'just', 'like', 'want', 'need', 'trying',
297
+ 'have', 'has', 'been', 'about', 'their', 'they', 'what', 'when', 'code',
298
+ 'coding', 'stuff', 'thing', 'things', 'project', 'projects', 'feature',
299
+ 'features', 'new', 'around', 'currently', 'right', 'now', 'today', 'session',
300
+ ]);
301
+
302
+ /**
303
+ * Extract meaningful, de-duped keywords from free text for topic matching.
304
+ * Mirrors the >3-char + common-word filter used server-side so client and
305
+ * server agree on what "related" means.
306
+ */
307
+ function introKeywords(text) {
308
+ return [
309
+ ...new Set(
310
+ String(text || '')
311
+ .toLowerCase()
312
+ .replace(/[^a-z0-9\s+#.-]/g, ' ')
313
+ .split(/\s+/)
314
+ .filter((w) => w.length > 3 && !INTRO_STOPWORDS.has(w))
315
+ ),
316
+ ];
317
+ }
318
+
319
+ // The topical text we know about an online human: what they're working on
320
+ // right now + their most recent ship. Both are already public (presence + board).
321
+ function introTopicText(u) {
322
+ const ship = u && u.lastShip && u.lastShip.what ? u.lastShip.what : '';
323
+ return `${(u && u.one_liner) || ''} ${ship}`.trim();
324
+ }
325
+
326
+ // Strip quotes/backslashes and clamp — the opener is pasted inside a
327
+ // double-quoted `vibe dm @x "..."` command, so an embedded quote would close
328
+ // it early. Same paste-safety rule as pickDormantTarget.
329
+ function sanitizeSnippet(text, max = 48) {
330
+ return String(text || '')
331
+ .replace(/["\\]/g, '')
332
+ .replace(/\s+/g, ' ')
333
+ .trim()
334
+ .slice(0, max);
335
+ }
336
+
337
+ /**
338
+ * Compose a two-sided warm opener: references what the newcomer is building AND
339
+ * what the target is on, anchored to the topic they share. This is the whole
340
+ * point of "received collaboration" — the newcomer arrives already in a
341
+ * relevant conversation instead of cold-DMing a stranger.
342
+ */
343
+ function composeIntro(building, target, shared) {
344
+ const mine = sanitizeSnippet(building, 60);
345
+ // For DISPLAY use one clean phrase — what they're doing now, else their last
346
+ // ship. (introTopicText concatenates both for MATCHING, which reads badly.)
347
+ const theirRaw =
348
+ (target && target.one_liner) ||
349
+ (target && target.lastShip && target.lastShip.what) ||
350
+ '';
351
+ const theirWork = sanitizeSnippet(theirRaw, 48);
352
+ const topic = shared && shared[0];
353
+ if (theirWork && topic) {
354
+ return `hey! i just jumped into /vibe — i'm working on ${mine} and saw you're on ${theirWork}. we're both deep in ${topic}, want to compare notes?`;
355
+ }
356
+ if (theirWork) {
357
+ return `hey! i just jumped into /vibe — i'm working on ${mine} and saw you're on ${theirWork}. what are you building?`;
358
+ }
359
+ return `hey! i just jumped into /vibe — i'm working on ${mine}. what are you building?`;
360
+ }
361
+
362
+ /**
363
+ * Pick the online human whose current work / recent ship most overlaps with what
364
+ * the newcomer is building this session. Returns a topically-matched target with
365
+ * a composed two-sided opener, or null when nothing lines up (caller should fall
366
+ * back to pickDormantTarget's recency pick). Agents are already excluded upstream
367
+ * (getActiveUsers only returns active+away, never the agents lane) but we guard
368
+ * anyway.
369
+ *
370
+ * @param {Array} others - active users (caller excludes self)
371
+ * @param {string} building - the agent's one-line summary of the user's current work
372
+ * @returns {{handle, opener, shared, related}|null}
373
+ */
374
+ function pickRelatedTarget(others, building) {
375
+ const myWords = introKeywords(building);
376
+ if (myWords.length === 0) return null;
377
+ const humans = (others || []).filter((u) => u && u.handle && !u.isAgent && !u.is_agent);
378
+ let best = null;
379
+ for (const u of humans) {
380
+ const theirWords = introKeywords(introTopicText(u));
381
+ const shared = myWords.filter((w) => theirWords.includes(w));
382
+ if (shared.length === 0) continue;
383
+ const recency = new Date(u.lastSeen || u.last_seen || 0).getTime() || 0;
384
+ if (
385
+ !best ||
386
+ shared.length > best.score ||
387
+ (shared.length === best.score && recency > best.recency)
388
+ ) {
389
+ best = { user: u, score: shared.length, shared, recency };
390
+ }
391
+ }
392
+ if (!best) return null;
393
+ return {
394
+ handle: best.user.handle,
395
+ shared: best.shared,
396
+ opener: composeIntro(building, best.user, best.shared),
397
+ related: true,
398
+ };
399
+ }
400
+
287
401
  // ─────────────────────────────────────────────────────────────
288
402
  // Exports
289
403
  // ─────────────────────────────────────────────────────────────
@@ -318,5 +432,10 @@ module.exports = {
318
432
  hasEverMessaged,
319
433
  markFirstDmSent,
320
434
  pickDormantTarget,
321
- firstDmNudge
435
+ firstDmNudge,
436
+
437
+ // Received collaboration (topical intro on onboarding)
438
+ introKeywords,
439
+ composeIntro,
440
+ pickRelatedTarget
322
441
  };
package/tools/init.js CHANGED
@@ -785,10 +785,18 @@ To check messages: \`vibe inbox\`${emailNudge}`
785
785
  const whoNudge = presence.humans > 1
786
786
  ? `**"vibe who"** — see the ${presence.humans} builders online right now`
787
787
  : `**"vibe who"** — see who's building right now`;
788
- const nextSteps = `\n\n---\n**Your first 60 seconds**\n`
789
- + `1. ${whoNudge}\n`
790
- + `2. **"vibe @someone hey"**your first DM lands right in their terminal\n`
791
- + `3. **"vibe ship <what you're building>"** put yourself on the board\n\n`
788
+ // Received collaboration: instead of handing the newcomer a cold to-do
789
+ // list ("go DM a stranger"), have their agent introduce them to someone
790
+ // online working on something related they land mid-conversation. The
791
+ // directive below tells the model to call vibe_intro with what this user
792
+ // is building THIS session (context only /vibe can see). vibe_intro
793
+ // handles the empty-room / no-match cases gracefully, so it's always safe
794
+ // to attempt. Draft-first: it proposes, the user says yes, then send.
795
+ const nextSteps = `\n\n---\n**You're in — let's land you in a real conversation, not an empty inbox.**\n`
796
+ + `\n→ _Agent: in one line, summarize what this user is building in the current session_`
797
+ + ` _(infer it from their open files, project, and recent work), then call the_ **\`vibe_intro\`** _tool with that as \`building\`._`
798
+ + ` _It finds someone online working on something related and drafts a warm intro. Show them the match, and if they're up for it, call \`vibe_intro\` again with \`send: true\`._\n`
799
+ + `\nPrefer to look around first? ${whoNudge}, or **"vibe ship <what you're building>"** to put yourself on the board.\n\n`
792
800
  + `_Stuck? Say **"vibe help"**._`;
793
801
 
794
802
  // One-time email nudge — only for users we can't otherwise reach (no
package/tools/intro.js ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * vibe intro — Received collaboration.
3
+ *
4
+ * The newcomer's agent knows what they're building this session (context only
5
+ * /vibe can see). This tool takes that one-line summary, finds a real human
6
+ * online whose current work / recent ship overlaps, and DRAFTS a two-sided warm
7
+ * intro so the newcomer arrives already in a relevant conversation instead of
8
+ * cold-DMing a stranger.
9
+ *
10
+ * Default is draft-only: the tool composes who + what-to-say (the hard part) and
11
+ * the user sends with one word. Pass send:true to actually deliver after they
12
+ * assent — the send routes through the normal authenticated vibe_dm path.
13
+ */
14
+
15
+ const config = require('../config');
16
+ const store = require('../store');
17
+ const {
18
+ requireInit,
19
+ normalizeHandle,
20
+ pickRelatedTarget,
21
+ pickDormantTarget,
22
+ } = require('./_shared');
23
+
24
+ const definition = {
25
+ name: 'vibe_intro',
26
+ description:
27
+ "Introduce this user to a real human online who's building something related, so they land mid-conversation instead of staring at an empty inbox. " +
28
+ 'Call this right after vibe_init succeeds (and any time a returning user hasn\'t connected yet). ' +
29
+ 'Pass `building`: a short, specific, first-person-free summary of what the user is actually working on in THIS session — infer it from the current files, project, and recent work (e.g. "an OAuth PKCE login flow in a Next.js app", "a Postgres migration for a chat schema", "a Tauri menubar app"). ' +
30
+ 'The tool finds the best-matched online human and drafts a warm two-sided opener. By default it only DRAFTS — show the user the match and the message, ask if they want to send it, then call again with send:true once they say yes.',
31
+ inputSchema: {
32
+ type: 'object',
33
+ properties: {
34
+ building: {
35
+ type: 'string',
36
+ description:
37
+ "One line describing what the user is working on this session, inferred from their actual context (files, project, recent work). Specific beats generic — 'realtime auth with WebSockets' matches better than 'a web app'.",
38
+ },
39
+ send: {
40
+ type: 'boolean',
41
+ description:
42
+ 'Set true ONLY after the user has agreed to send the drafted intro. Delivers the message via the normal DM path.',
43
+ },
44
+ handle: {
45
+ type: 'string',
46
+ description:
47
+ 'Optional: send to this specific person instead of the auto-matched one (used with send:true when the user picks someone else).',
48
+ },
49
+ message: {
50
+ type: 'string',
51
+ description:
52
+ 'Optional: override the drafted opener with custom text (used with send:true).',
53
+ },
54
+ },
55
+ required: [],
56
+ },
57
+ };
58
+
59
+ async function handler(args = {}) {
60
+ const initCheck = requireInit();
61
+ if (initCheck) return initCheck;
62
+
63
+ const myHandle = config.getHandle();
64
+ const { building, send } = args;
65
+
66
+ // Fetch who's around and drop self.
67
+ let others = [];
68
+ try {
69
+ const users = await store.getActiveUsers();
70
+ others = (users || []).filter((u) => u && u.handle && u.handle !== myHandle);
71
+ } catch (e) {
72
+ others = [];
73
+ }
74
+
75
+ // ── Send path: user has assented, deliver the intro ───────────────────────
76
+ if (send === true) {
77
+ let target = args.handle ? normalizeHandle(args.handle) : null;
78
+ let message = args.message || null;
79
+
80
+ // If no explicit target/message, re-derive the match so send is self-contained.
81
+ if (!target || !message) {
82
+ const match =
83
+ (building && pickRelatedTarget(others, building)) || pickDormantTarget(others);
84
+ if (!match) {
85
+ return {
86
+ display:
87
+ "No one who lines up is around this second. Try `vibe who` to see everyone, or `vibe ship <what you're building>` so the next person who arrives finds you.",
88
+ };
89
+ }
90
+ target = target || match.handle;
91
+ message = message || match.opener;
92
+ }
93
+
94
+ const dm = require('./dm');
95
+ const result = await dm.handler({ handle: target, message });
96
+ // dm.handler already returns a display + marks firstDmSent on success.
97
+ if (result && result.display && /failed/i.test(result.display)) return result;
98
+
99
+ return {
100
+ display:
101
+ `✨ **You're introduced.** Your note just landed in @${target}'s terminal — ` +
102
+ `you're mid-conversation now, no cold open required.\n\n` +
103
+ `_When they reply you'll see it here. Want to meet more people? \`vibe who\`._`,
104
+ _sent: true,
105
+ };
106
+ }
107
+
108
+ // ── Draft path (default): compose the intro, let the user decide ──────────
109
+ if (!others.length) {
110
+ return {
111
+ display:
112
+ "Nobody's online right this second to introduce you to. " +
113
+ "Run `vibe ship <what you're building>` so the next builder who arrives sees your work — " +
114
+ 'or `vibe who` in a bit to check again.',
115
+ };
116
+ }
117
+
118
+ const related = building ? pickRelatedTarget(others, building) : null;
119
+ const target = related || pickDormantTarget(others);
120
+
121
+ if (!target) {
122
+ return {
123
+ display:
124
+ "There are people around, but I couldn't line anyone up by topic yet. " +
125
+ '`vibe who` shows everyone online — say hi to whoever looks interesting.',
126
+ };
127
+ }
128
+
129
+ const sharedTopic =
130
+ related && related.shared && related.shared.length
131
+ ? related.shared.slice(0, 2).join(', ')
132
+ : null;
133
+
134
+ const lede = related
135
+ ? `**✨ Someone you should meet: @${target.handle} is online right now** — ` +
136
+ (sharedTopic
137
+ ? `you're both deep in ${sharedTopic}.`
138
+ : 'looks like a topical match.')
139
+ : `**👋 @${target.handle} is around right now** — a good first person to meet.`;
140
+
141
+ return {
142
+ display:
143
+ `${lede}\n\n` +
144
+ `I drafted an intro from you:\n` +
145
+ `> ${target.opener}\n\n` +
146
+ `Want me to send it? Say **yes** and I'll introduce you — or **\`vibe who\`** to browse everyone first.`,
147
+ _draft: { handle: target.handle, message: target.opener, related: !!related },
148
+ };
149
+ }
150
+
151
+ module.exports = { definition, handler };
package/version.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
- "version": "0.5.12",
3
- "updated": "2026-07-03",
4
- "changelog": "First-run just got un-losable. Saying `let's vibe` opens your browser for GitHub sign-in and now the tool tells you that's happening, hands you a clickable login link if the tab didn't pop, and if anything times out it says exactly what to do (`let's vibe` again) instead of an unrelated hint. No more staring at a silent prompt wondering if it worked.",
2
+ "version": "0.5.13",
3
+ "updated": "2026-07-04",
4
+ "changelog": "New: you arrive already in a conversation. Right after you sign in, /vibe looks at what you're building this session, finds a real person online working on something related, and drafts a warm two-sided intro — you send it with one word. No more `vibe who` then cold-DMing a stranger; you land mid-conversation with someone who gets what you're doing.",
5
5
  "features": [
6
- "Bulletproof first-run`let's vibe` now narrates the browser sign-in, gives you a clickable fallback login link, and every timeout/error path tells you the correct retry. You can't get stranded",
6
+ "Received collaboration (vibe_intro) after sign-in, your agent summarizes what you're building and /vibe matches you to a real human online whose current work or recent ship overlaps, then drafts a two-sided opener you send with one word. The signal only /vibe can see — what's inside two people's live coding sessions at once — becomes the introduction",
7
+ "Bulletproof first-run — `let's vibe` narrates the browser sign-in, gives you a clickable fallback login link, and every timeout/error path tells you the correct retry. You can't get stranded",
7
8
  "First-DM nudge — if you've never messaged anyone, vibe start/who surface the most-recently-active real human as a one-tap opener, warmed by their latest ship. Self-heals: disappears the moment you send your first DM",
8
9
  "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",
9
10
  "vibe_corpse — write an exquisite corpse: alternate lines blind to all but the last, then seal for the big reveal",