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 +3 -0
- package/package.json +1 -1
- package/tools/_shared.js +120 -1
- package/tools/init.js +12 -4
- package/tools/intro.js +151 -0
- package/version.json +5 -4
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.
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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.
|
|
3
|
-
"updated": "2026-07-
|
|
4
|
-
"changelog": "
|
|
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
|
-
"
|
|
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",
|