slashvibe-mcp 0.3.20 → 0.3.21
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/README.md +47 -252
- package/analytics.js +107 -0
- package/auth-store.js +148 -0
- package/auto-update.js +130 -0
- package/bridges/bridge-monitor.js +388 -0
- package/bridges/discord-bot.js +431 -0
- package/bridges/farcaster.js +299 -0
- package/bridges/telegram.js +261 -0
- package/bridges/webhook-health.js +420 -0
- package/bridges/webhook-server.js +437 -0
- package/bridges/whatsapp.js +441 -0
- package/bridges/x-webhook.js +423 -0
- package/config.js +27 -15
- package/games/arcade.js +406 -0
- package/games/chess.js +451 -0
- package/games/colorguess.js +343 -0
- package/games/crossword-words.js +171 -0
- package/games/crossword.js +461 -0
- package/games/drawing.js +347 -0
- package/games/gameroulette.js +300 -0
- package/games/gamerouter.js +336 -0
- package/games/gamestatus.js +337 -0
- package/games/guessnumber.js +209 -0
- package/games/hangman.js +279 -0
- package/games/memory.js +338 -0
- package/games/multiplayer-tictactoe.js +389 -0
- package/games/pixelart.js +399 -0
- package/games/quickduel.js +354 -0
- package/games/riddle.js +371 -0
- package/games/rockpaperscissors.js +291 -0
- package/games/snake.js +406 -0
- package/games/storybuilder.js +343 -0
- package/games/tictactoe.js +345 -0
- package/games/twentyquestions.js +286 -0
- package/games/twotruths.js +207 -0
- package/games/werewolf.js +508 -0
- package/games/wordassociation.js +247 -0
- package/games/wordchain.js +135 -0
- package/index.js +116 -159
- package/intelligence/index.js +9 -2
- package/intelligence/interests.js +369 -0
- package/notification-emitter.js +77 -0
- package/notify.js +5 -1
- package/package.json +21 -16
- package/prompts.js +1 -1
- package/protocol/index.js +73 -0
- package/setup.js +480 -0
- package/smart-inbox.js +276 -0
- package/store/api.js +536 -215
- package/store/profiles.js +160 -12
- package/tools/_actions.js +362 -21
- package/tools/_discovery.js +119 -26
- package/tools/_shared/index.js +64 -0
- package/tools/_shared.js +234 -0
- package/tools/_work-context.js +338 -0
- package/tools/_work-context.manual-test.js +199 -0
- package/tools/_work-context.test.js +260 -0
- package/tools/activity.js +220 -0
- package/tools/analytics.js +191 -0
- package/tools/approve.js +197 -0
- package/tools/artifact-create.js +14 -3
- package/tools/artifacts-price.js +107 -0
- package/tools/available.js +120 -0
- package/tools/broadcast.js +325 -0
- package/tools/chat.js +202 -0
- package/tools/collaborative-drawing.js +1 -1
- package/tools/connection-status.js +178 -0
- package/tools/discover.js +350 -34
- package/tools/dm.js +80 -8
- package/tools/earnings.js +126 -0
- package/tools/feed.js +35 -4
- package/tools/follow.js +224 -0
- package/tools/friends.js +207 -0
- package/tools/gig-browse.js +206 -0
- package/tools/gig-complete.js +144 -0
- package/tools/health.js +87 -0
- package/tools/help.js +3 -3
- package/tools/idea.js +9 -2
- package/tools/inbox.js +289 -105
- package/tools/init.js +131 -34
- package/tools/invite.js +15 -4
- package/tools/leaderboard.js +117 -0
- package/tools/lib/git-apply.js +206 -0
- package/tools/lib/git-bundle.js +407 -0
- package/tools/migrate.js +3 -3
- package/tools/multiplayer-game.js +1 -1
- package/tools/onboarding.js +7 -7
- package/tools/open.js +143 -12
- package/tools/party-game.js +1 -1
- package/tools/plan.js +225 -0
- package/tools/proof-of-work.js +144 -0
- package/tools/reply.js +166 -0
- package/tools/report.js +1 -1
- package/tools/request.js +17 -3
- package/tools/schedule.js +367 -0
- package/tools/search-messages.js +123 -0
- package/tools/session.js +467 -0
- package/tools/session_price.js +128 -0
- package/tools/settings.js +90 -2
- package/tools/ship.js +30 -7
- package/tools/smart-check.js +201 -0
- package/tools/start.js +147 -12
- package/tools/status.js +53 -6
- package/tools/streak.js +147 -0
- package/tools/stuck.js +297 -0
- package/tools/subscribe.js +148 -0
- package/tools/subscriptions.js +134 -0
- package/tools/suggest-tags.js +6 -8
- package/tools/tag-suggestions.js +1 -1
- package/tools/tip.js +150 -77
- package/tools/token.js +4 -4
- package/tools/update.js +1 -1
- package/tools/wallet.js +221 -79
- package/tools/watch.js +157 -0
- package/tools/who.js +30 -1
- package/tools/withdraw.js +145 -0
- package/tools/work-summary.js +96 -0
- package/version.json +10 -8
- package/LICENSE +0 -21
- package/store/sqlite.js +0 -347
- /package/tools/{auto-suggest-connections.js → _deprecated/auto-suggest-connections.js} +0 -0
- /package/tools/{away.js → _deprecated/away.js} +0 -0
- /package/tools/{back.js → _deprecated/back.js} +0 -0
- /package/tools/{bootstrap-skills.js → _deprecated/bootstrap-skills.js} +0 -0
- /package/tools/{bridge-dashboard.js → _deprecated/bridge-dashboard.js} +0 -0
- /package/tools/{bridge-health.js → _deprecated/bridge-health.js} +0 -0
- /package/tools/{bridge-live.js → _deprecated/bridge-live.js} +0 -0
- /package/tools/{bridges.js → _deprecated/bridges.js} +0 -0
- /package/tools/{colorguess.js → _deprecated/colorguess.js} +0 -0
- /package/tools/{discover-insights.js → _deprecated/discover-insights.js} +0 -0
- /package/tools/{discover-momentum.js → _deprecated/discover-momentum.js} +0 -0
- /package/tools/{discovery-analytics.js → _deprecated/discovery-analytics.js} +0 -0
- /package/tools/{discovery-auto-suggest.js → _deprecated/discovery-auto-suggest.js} +0 -0
- /package/tools/{discovery-bootstrap.js → _deprecated/discovery-bootstrap.js} +0 -0
- /package/tools/{discovery-daily.js → _deprecated/discovery-daily.js} +0 -0
- /package/tools/{discovery-dashboard.js → _deprecated/discovery-dashboard.js} +0 -0
- /package/tools/{discovery-digest.js → _deprecated/discovery-digest.js} +0 -0
- /package/tools/{discovery-hub.js → _deprecated/discovery-hub.js} +0 -0
- /package/tools/{discovery-insights.js → _deprecated/discovery-insights.js} +0 -0
- /package/tools/{discovery-momentum.js → _deprecated/discovery-momentum.js} +0 -0
- /package/tools/{discovery-monitor.js → _deprecated/discovery-monitor.js} +0 -0
- /package/tools/{discovery-proactive.js → _deprecated/discovery-proactive.js} +0 -0
- /package/tools/{draw.js → _deprecated/draw.js} +0 -0
- /package/tools/{farcaster.js → _deprecated/farcaster.js} +0 -0
- /package/tools/{forget.js → _deprecated/forget.js} +0 -0
- /package/tools/{games-catalog.js → _deprecated/games-catalog.js} +0 -0
- /package/tools/{games.js → _deprecated/games.js} +0 -0
- /package/tools/{guessnumber.js → _deprecated/guessnumber.js} +0 -0
- /package/tools/{hangman.js → _deprecated/hangman.js} +0 -0
- /package/tools/{multiplayer-tictactoe.js → _deprecated/multiplayer-tictactoe.js} +0 -0
- /package/tools/{mute.js → _deprecated/mute.js} +0 -0
- /package/tools/{recall.js → _deprecated/recall.js} +0 -0
- /package/tools/{remember.js → _deprecated/remember.js} +0 -0
- /package/tools/{riddle.js → _deprecated/riddle.js} +0 -0
- /package/tools/{run-bootstrap.js → _deprecated/run-bootstrap.js} +0 -0
- /package/tools/{skills-analytics.js → _deprecated/skills-analytics.js} +0 -0
- /package/tools/{skills-bootstrap.js → _deprecated/skills-bootstrap.js} +0 -0
- /package/tools/{skills-dashboard.js → _deprecated/skills-dashboard.js} +0 -0
- /package/tools/{skills-exchange.js → _deprecated/skills-exchange.js} +0 -0
- /package/tools/{skills.js → _deprecated/skills.js} +0 -0
- /package/tools/{smart-intro.js → _deprecated/smart-intro.js} +0 -0
- /package/tools/{storybuilder.js → _deprecated/storybuilder.js} +0 -0
- /package/tools/{telegram-bot.js → _deprecated/telegram-bot.js} +0 -0
- /package/tools/{telegram-setup.js → _deprecated/telegram-setup.js} +0 -0
- /package/tools/{tictactoe.js → _deprecated/tictactoe.js} +0 -0
- /package/tools/{twentyquestions.js → _deprecated/twentyquestions.js} +0 -0
- /package/tools/{wordassociation.js → _deprecated/wordassociation.js} +0 -0
package/tools/discover.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* vibe discover — Find your people
|
|
3
3
|
*
|
|
4
4
|
* Smart matchmaking based on:
|
|
5
|
+
* - Social connections (X/GitHub mutual follows)
|
|
5
6
|
* - What you're building (similar projects)
|
|
6
|
-
* - What you've shipped (complementary skills)
|
|
7
|
+
* - What you've shipped (complementary skills)
|
|
7
8
|
* - When you're active (timezone overlap)
|
|
8
9
|
* - Shared interests (tags)
|
|
9
10
|
*
|
|
@@ -12,23 +13,115 @@
|
|
|
12
13
|
* - discover search <query> — Find people building specific things
|
|
13
14
|
* - discover interests — Browse people by interest tags
|
|
14
15
|
* - discover active — Show who's building similar things right now
|
|
16
|
+
*
|
|
17
|
+
* Uses Universal Relevancy API for smart matching with social-first signals.
|
|
18
|
+
* Falls back to local matching if API unavailable.
|
|
15
19
|
*/
|
|
16
20
|
|
|
17
21
|
const config = require('../config');
|
|
18
22
|
const store = require('../store');
|
|
19
23
|
const userProfiles = require('../store/profiles');
|
|
20
24
|
const { formatTimeAgo, requireInit } = require('./_shared');
|
|
25
|
+
const { getTechTags, getTechOneLiner } = require('../lib/tech-detection');
|
|
26
|
+
|
|
27
|
+
// API configuration
|
|
28
|
+
const RELEVANCY_API = 'https://www.slashvibe.dev/api/relevancy';
|
|
29
|
+
const API_TIMEOUT = 5000; // 5 seconds
|
|
30
|
+
|
|
31
|
+
// Detect current project tech stack for better matching
|
|
32
|
+
let cachedTechStack = null;
|
|
33
|
+
let techStackCacheTime = 0;
|
|
34
|
+
const TECH_CACHE_TTL = 60000; // 1 minute cache
|
|
35
|
+
|
|
36
|
+
function getCurrentTechStack() {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
if (cachedTechStack && (now - techStackCacheTime) < TECH_CACHE_TTL) {
|
|
39
|
+
return cachedTechStack;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
cachedTechStack = {
|
|
43
|
+
tags: getTechTags(),
|
|
44
|
+
oneLiner: getTechOneLiner(),
|
|
45
|
+
};
|
|
46
|
+
techStackCacheTime = now;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
cachedTechStack = { tags: [], oneLiner: '' };
|
|
49
|
+
}
|
|
50
|
+
return cachedTechStack;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fetch suggestions from Universal Relevancy API
|
|
55
|
+
* Uses social signals (GitHub mutuals, etc.) for better matching
|
|
56
|
+
*
|
|
57
|
+
* @param {string} handle - User's vibe handle
|
|
58
|
+
* @param {number} limit - Max results (default 5)
|
|
59
|
+
* @returns {Promise<{ matches: object[], tier: string, fromApi: boolean } | null>}
|
|
60
|
+
*/
|
|
61
|
+
async function fetchApiSuggestions(handle, limit = 5) {
|
|
62
|
+
try {
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
const timeout = setTimeout(() => controller.abort(), API_TIMEOUT);
|
|
65
|
+
|
|
66
|
+
// Include current project tech stack for better matching
|
|
67
|
+
const techStack = getCurrentTechStack();
|
|
68
|
+
const techTags = techStack.tags.join(',');
|
|
69
|
+
|
|
70
|
+
let url = `${RELEVANCY_API}?handle=${encodeURIComponent(handle)}&context=discovery&limit=${limit}`;
|
|
71
|
+
if (techTags) {
|
|
72
|
+
url += `&tech_tags=${encodeURIComponent(techTags)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const response = await fetch(url, {
|
|
76
|
+
method: 'GET',
|
|
77
|
+
headers: {
|
|
78
|
+
'Accept': 'application/json',
|
|
79
|
+
'User-Agent': 'vibe-mcp-client'
|
|
80
|
+
},
|
|
81
|
+
signal: controller.signal
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
console.log(`[discover] API returned ${response.status}`);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const data = await response.json();
|
|
92
|
+
|
|
93
|
+
if (!data.success || !data.matches) {
|
|
94
|
+
console.log('[discover] API returned unsuccessful response');
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
matches: data.matches,
|
|
100
|
+
tier: data.tier,
|
|
101
|
+
socialStats: data.socialStats,
|
|
102
|
+
fromApi: true
|
|
103
|
+
};
|
|
104
|
+
} catch (e) {
|
|
105
|
+
// Log but don't fail - we'll fallback to local
|
|
106
|
+
if (e.name === 'AbortError') {
|
|
107
|
+
console.log('[discover] API timeout, falling back to local');
|
|
108
|
+
} else {
|
|
109
|
+
console.log('[discover] API error:', e.message);
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
21
114
|
|
|
22
115
|
const definition = {
|
|
23
116
|
name: 'vibe_discover',
|
|
24
|
-
description: 'Find interesting people to connect with
|
|
117
|
+
description: 'Find interesting people to connect with, see who\'s live, browse upcoming sessions.',
|
|
25
118
|
inputSchema: {
|
|
26
119
|
type: 'object',
|
|
27
120
|
properties: {
|
|
28
121
|
command: {
|
|
29
122
|
type: 'string',
|
|
30
|
-
enum: ['suggest', 'search', 'interests', 'active'],
|
|
31
|
-
description: 'Discovery command
|
|
123
|
+
enum: ['suggest', 'search', 'interests', 'active', 'live', 'upcoming', 'sessions', 'creators'],
|
|
124
|
+
description: 'Discovery command: suggest, search, interests, active, live, upcoming, sessions, creators'
|
|
32
125
|
},
|
|
33
126
|
query: {
|
|
34
127
|
type: 'string',
|
|
@@ -38,6 +131,34 @@ const definition = {
|
|
|
38
131
|
}
|
|
39
132
|
};
|
|
40
133
|
|
|
134
|
+
// Format duration in seconds to human readable
|
|
135
|
+
function formatDuration(seconds) {
|
|
136
|
+
if (!seconds || seconds < 60) return '<1m';
|
|
137
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
|
138
|
+
const hours = Math.floor(seconds / 3600);
|
|
139
|
+
const mins = Math.floor((seconds % 3600) / 60);
|
|
140
|
+
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Format scheduled time to relative string
|
|
144
|
+
function formatScheduledTime(isoString) {
|
|
145
|
+
const date = new Date(isoString);
|
|
146
|
+
const now = new Date();
|
|
147
|
+
const diffMs = date - now;
|
|
148
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
149
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
150
|
+
|
|
151
|
+
if (diffMs < 0) return 'started';
|
|
152
|
+
if (diffDays === 0) {
|
|
153
|
+
if (diffHours <= 1) return 'in < 1 hour';
|
|
154
|
+
return `in ${diffHours} hours`;
|
|
155
|
+
} else if (diffDays === 1) {
|
|
156
|
+
return 'tomorrow';
|
|
157
|
+
} else {
|
|
158
|
+
return `in ${diffDays} days`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
41
162
|
// Calculate match score between two users
|
|
42
163
|
function calculateMatchScore(user1, user2) {
|
|
43
164
|
let score = 0;
|
|
@@ -132,11 +253,12 @@ function findComplementaryTags(tags1, tags2) {
|
|
|
132
253
|
return complementary;
|
|
133
254
|
}
|
|
134
255
|
|
|
135
|
-
// Get personalized suggestions
|
|
136
|
-
|
|
256
|
+
// Get personalized suggestions (local fallback)
|
|
257
|
+
// This is used when API is unavailable
|
|
258
|
+
async function getLocalSuggestions(myHandle) {
|
|
137
259
|
const myProfile = await userProfiles.getProfile(myHandle);
|
|
138
260
|
const allProfiles = await userProfiles.getAllProfiles();
|
|
139
|
-
|
|
261
|
+
|
|
140
262
|
const candidates = allProfiles.filter(p => p.handle !== myHandle);
|
|
141
263
|
const matches = [];
|
|
142
264
|
|
|
@@ -158,6 +280,28 @@ async function getSuggestions(myHandle) {
|
|
|
158
280
|
return matches.sort((a, b) => b.score - a.score).slice(0, 5);
|
|
159
281
|
}
|
|
160
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Get suggestions - tries API first, falls back to local
|
|
285
|
+
* @param {string} myHandle - User's handle
|
|
286
|
+
* @returns {Promise<{ matches: object[], fromApi: boolean, tier?: string }>}
|
|
287
|
+
*/
|
|
288
|
+
async function getSuggestions(myHandle) {
|
|
289
|
+
// Try API first (has social signals, better matching)
|
|
290
|
+
const apiResult = await fetchApiSuggestions(myHandle, 5);
|
|
291
|
+
|
|
292
|
+
if (apiResult && apiResult.matches.length > 0) {
|
|
293
|
+
return apiResult;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Fallback to local matching
|
|
297
|
+
const localMatches = await getLocalSuggestions(myHandle);
|
|
298
|
+
return {
|
|
299
|
+
matches: localMatches,
|
|
300
|
+
fromApi: false,
|
|
301
|
+
tier: 'local'
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
161
305
|
// Search for people by query
|
|
162
306
|
async function searchPeople(query) {
|
|
163
307
|
const allProfiles = await userProfiles.getAllProfiles();
|
|
@@ -237,41 +381,68 @@ async function handler(args) {
|
|
|
237
381
|
try {
|
|
238
382
|
switch (command) {
|
|
239
383
|
case 'suggest': {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
384
|
+
const result = await getSuggestions(myHandle);
|
|
385
|
+
const { matches, fromApi, tier } = result;
|
|
386
|
+
|
|
387
|
+
if (!matches || matches.length === 0) {
|
|
243
388
|
display = `## No Matches Found
|
|
244
389
|
|
|
245
390
|
_Not enough people with profiles yet._
|
|
246
391
|
|
|
247
392
|
**Help us learn about you:**
|
|
248
|
-
- Set interests: \`vibe update interests "ai, startups, music"\`
|
|
393
|
+
- Set interests: \`vibe update interests "ai, startups, music"\`
|
|
249
394
|
- Tag your skills: \`vibe update tags "frontend, react, typescript"\`
|
|
250
395
|
- Share what you're building: \`vibe update building "AI chat app"\`
|
|
251
396
|
|
|
252
397
|
The more people share, the better our matches become!`;
|
|
253
398
|
} else {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
399
|
+
// Title varies by result tier
|
|
400
|
+
if (tier === 'contextual' || tier === 'mixed') {
|
|
401
|
+
display = `## 🔍 Suggested Connections\n\n`;
|
|
402
|
+
} else if (tier === 'trending') {
|
|
403
|
+
display = `## 🔥 Trending Builders\n\n`;
|
|
404
|
+
} else if (tier === 'new_joiners') {
|
|
405
|
+
display = `## 👋 New to /vibe\n\n`;
|
|
406
|
+
} else {
|
|
407
|
+
display = `## People You Should Meet\n\n`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Show detected tech stack if any
|
|
411
|
+
const techStack = getCurrentTechStack();
|
|
412
|
+
if (techStack.oneLiner) {
|
|
413
|
+
display += `_Matching for: ${techStack.oneLiner}_\n\n`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
for (const match of matches) {
|
|
417
|
+
// Handle name with optional tier badge
|
|
418
|
+
const tierBadge = match.tierEmoji ? ` ${match.tierEmoji}` : '';
|
|
419
|
+
display += `**@${match.handle}**${tierBadge}\n`;
|
|
258
420
|
display += `${match.building || 'Building something interesting'}\n`;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
421
|
+
|
|
422
|
+
// Show reasons as a tree structure (from API)
|
|
423
|
+
if (match.reasons && match.reasons.length > 0) {
|
|
424
|
+
for (let i = 0; i < match.reasons.length; i++) {
|
|
425
|
+
const prefix = i === match.reasons.length - 1 ? '└─' : '├─';
|
|
426
|
+
display += ` ${prefix} ${match.reasons[i]}\n`;
|
|
427
|
+
}
|
|
262
428
|
}
|
|
263
|
-
|
|
264
|
-
if (match.interests.length > 0) {
|
|
429
|
+
// Fallback for local matches (has interests array)
|
|
430
|
+
else if (match.interests && match.interests.length > 0) {
|
|
265
431
|
display += `💡 ${match.interests.slice(0, 3).join(', ')}\n`;
|
|
266
432
|
}
|
|
267
|
-
|
|
268
|
-
display +=
|
|
433
|
+
|
|
434
|
+
display += '\n';
|
|
269
435
|
}
|
|
270
|
-
|
|
436
|
+
|
|
271
437
|
display += `**Next steps:**\n`;
|
|
272
438
|
display += `- \`message @handle\` to reach out\n`;
|
|
273
439
|
display += `- \`discover active\` to see who's online now\n`;
|
|
274
440
|
display += `- \`discover search <topic>\` to find specific interests`;
|
|
441
|
+
|
|
442
|
+
// Debug info (only in dev)
|
|
443
|
+
if (process.env.VIBE_DEBUG === 'true') {
|
|
444
|
+
display += `\n\n_Source: ${fromApi ? 'API' : 'local'} | Tier: ${tier}_`;
|
|
445
|
+
}
|
|
275
446
|
}
|
|
276
447
|
break;
|
|
277
448
|
}
|
|
@@ -339,7 +510,7 @@ People haven't shared their interests yet.
|
|
|
339
510
|
|
|
340
511
|
case 'active': {
|
|
341
512
|
const similar = await getActiveSimilar(myHandle);
|
|
342
|
-
|
|
513
|
+
|
|
343
514
|
if (similar.length === 0) {
|
|
344
515
|
display = `## No Similar Builders Online
|
|
345
516
|
|
|
@@ -347,38 +518,183 @@ No one with similar interests is active right now.
|
|
|
347
518
|
|
|
348
519
|
**Try:**
|
|
349
520
|
- \`who\` to see who's around
|
|
350
|
-
- \`discover suggest\` for general recommendations
|
|
521
|
+
- \`discover suggest\` for general recommendations
|
|
351
522
|
- \`discover search <topic>\` to find people by interest`;
|
|
352
523
|
} else {
|
|
353
524
|
display = `## Similar Builders Online Now\n\n`;
|
|
354
|
-
|
|
525
|
+
|
|
355
526
|
for (const person of similar) {
|
|
356
527
|
display += `**@${person.handle}** _(${person.status})_\n`;
|
|
357
528
|
display += `${person.building || person.one_liner || 'Active now'}\n`;
|
|
358
|
-
|
|
529
|
+
|
|
359
530
|
if (person.reasons.length > 0) {
|
|
360
531
|
display += `🔗 ${person.reasons.join(' • ')}\n`;
|
|
361
532
|
}
|
|
362
|
-
|
|
533
|
+
|
|
363
534
|
display += `\n`;
|
|
364
535
|
}
|
|
365
|
-
|
|
536
|
+
|
|
366
537
|
display += `**Perfect time to connect!** 🎯`;
|
|
367
538
|
}
|
|
368
539
|
break;
|
|
369
540
|
}
|
|
370
541
|
|
|
542
|
+
case 'live': {
|
|
543
|
+
// Fetch live broadcasts from discover API
|
|
544
|
+
const apiUrl = config.getApiUrl();
|
|
545
|
+
const response = await fetch(`${apiUrl}/api/discover?section=live`);
|
|
546
|
+
const data = await response.json();
|
|
547
|
+
|
|
548
|
+
if (!data.success || !data.live || data.live.length === 0) {
|
|
549
|
+
display = `## No One Live Right Now
|
|
550
|
+
|
|
551
|
+
_Check back later or schedule your own session!_
|
|
552
|
+
|
|
553
|
+
**Go live:** \`vibe broadcast start "What you're building"\`
|
|
554
|
+
**See upcoming:** \`vibe discover upcoming\``;
|
|
555
|
+
} else {
|
|
556
|
+
display = `## 🔴 Live Now\n\n`;
|
|
557
|
+
|
|
558
|
+
for (const broadcast of data.live) {
|
|
559
|
+
const duration = formatDuration(broadcast.durationSeconds);
|
|
560
|
+
display += `**@${broadcast.handle}** — ${broadcast.title}\n`;
|
|
561
|
+
display += `👥 ${broadcast.viewerCount} watching · ⏱️ ${duration}`;
|
|
562
|
+
if (broadcast.reactionCount > 0) {
|
|
563
|
+
display += ` · 🔥 ${broadcast.reactionCount}`;
|
|
564
|
+
}
|
|
565
|
+
display += `\n`;
|
|
566
|
+
display += `[Watch](${broadcast.watchUrl})\n\n`;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
display += `---\n`;
|
|
570
|
+
display += `\`vibe watch @handle\` to tune in`;
|
|
571
|
+
}
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
case 'upcoming': {
|
|
576
|
+
const apiUrl = config.getApiUrl();
|
|
577
|
+
const response = await fetch(`${apiUrl}/api/discover?section=scheduled`);
|
|
578
|
+
const data = await response.json();
|
|
579
|
+
|
|
580
|
+
if (!data.success || !data.scheduled || data.scheduled.length === 0) {
|
|
581
|
+
display = `## No Scheduled Sessions
|
|
582
|
+
|
|
583
|
+
_No one has scheduled a session yet._
|
|
584
|
+
|
|
585
|
+
**Schedule yours:** \`vibe schedule "Topic" --time "tomorrow 3pm"\`
|
|
586
|
+
**See who's live:** \`vibe discover live\``;
|
|
587
|
+
} else {
|
|
588
|
+
display = `## 📅 Upcoming Sessions\n\n`;
|
|
589
|
+
|
|
590
|
+
for (const session of data.scheduled) {
|
|
591
|
+
const when = formatScheduledTime(session.scheduledFor);
|
|
592
|
+
display += `**${session.title}**\n`;
|
|
593
|
+
display += `@${session.handle} · ${when}`;
|
|
594
|
+
if (session.rsvpCount > 0) {
|
|
595
|
+
display += ` · ${session.rsvpCount} RSVPs`;
|
|
596
|
+
}
|
|
597
|
+
display += `\n`;
|
|
598
|
+
if (session.tags && session.tags.length > 0) {
|
|
599
|
+
display += `🏷️ ${session.tags.join(', ')}\n`;
|
|
600
|
+
}
|
|
601
|
+
display += `\n`;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
display += `---\n`;
|
|
605
|
+
display += `\`vibe schedule rsvp <id>\` to get notified`;
|
|
606
|
+
}
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
case 'sessions': {
|
|
611
|
+
const apiUrl = config.getApiUrl();
|
|
612
|
+
const response = await fetch(`${apiUrl}/api/discover?section=sessions`);
|
|
613
|
+
const data = await response.json();
|
|
614
|
+
|
|
615
|
+
if (!data.success || !data.recentSessions || data.recentSessions.length === 0) {
|
|
616
|
+
display = `## No Recent Sessions
|
|
617
|
+
|
|
618
|
+
_No recorded sessions yet._
|
|
619
|
+
|
|
620
|
+
**Record yours:** After broadcasting, use \`vibe session save\``;
|
|
621
|
+
} else {
|
|
622
|
+
display = `## 📼 Recent Sessions\n\n`;
|
|
623
|
+
|
|
624
|
+
for (const session of data.recentSessions) {
|
|
625
|
+
display += `**${session.title}**\n`;
|
|
626
|
+
display += `@${session.handle}`;
|
|
627
|
+
if (session.views > 0) {
|
|
628
|
+
display += ` · ${session.views} views`;
|
|
629
|
+
}
|
|
630
|
+
if (session.duration) {
|
|
631
|
+
display += ` · ${formatDuration(session.duration)}`;
|
|
632
|
+
}
|
|
633
|
+
display += `\n`;
|
|
634
|
+
if (session.tags && session.tags.length > 0) {
|
|
635
|
+
display += `🏷️ ${session.tags.join(', ')}\n`;
|
|
636
|
+
}
|
|
637
|
+
display += `\n`;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
display += `---\n`;
|
|
641
|
+
display += `\`vibe session view <id>\` to watch`;
|
|
642
|
+
}
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
case 'creators': {
|
|
647
|
+
const apiUrl = config.getApiUrl();
|
|
648
|
+
const response = await fetch(`${apiUrl}/api/discover?handle=${myHandle}§ion=recommended`);
|
|
649
|
+
const data = await response.json();
|
|
650
|
+
|
|
651
|
+
if (!data.success || !data.recommended || data.recommended.length === 0) {
|
|
652
|
+
display = `## Recommended Creators
|
|
653
|
+
|
|
654
|
+
_Not enough data yet to make recommendations._
|
|
655
|
+
|
|
656
|
+
**Try:**
|
|
657
|
+
- \`vibe discover suggest\` — Find people by interests
|
|
658
|
+
- \`vibe discover active\` — See who's online now`;
|
|
659
|
+
} else {
|
|
660
|
+
display = `## 👤 Recommended Creators\n\n`;
|
|
661
|
+
|
|
662
|
+
for (const creator of data.recommended) {
|
|
663
|
+
display += `**@${creator.handle}**`;
|
|
664
|
+
if (creator.followerCount > 0) {
|
|
665
|
+
display += ` (${creator.followerCount} followers)`;
|
|
666
|
+
}
|
|
667
|
+
display += `\n`;
|
|
668
|
+
if (creator.building) {
|
|
669
|
+
display += `${creator.building}\n`;
|
|
670
|
+
}
|
|
671
|
+
display += `_${creator.reason}_\n\n`;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
display += `---\n`;
|
|
675
|
+
display += `\`vibe follow @handle\` to follow`;
|
|
676
|
+
}
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
|
|
371
680
|
default:
|
|
372
681
|
display = `## Discovery Commands
|
|
373
682
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
683
|
+
**Live & Sessions:**
|
|
684
|
+
- \`discover live\` — See who's broadcasting now
|
|
685
|
+
- \`discover upcoming\` — Browse scheduled sessions
|
|
686
|
+
- \`discover sessions\` — Watch recent recorded sessions
|
|
687
|
+
|
|
688
|
+
**Find People:**
|
|
689
|
+
- \`discover suggest\` — Get personalized recommendations
|
|
690
|
+
- \`discover creators\` — Popular creators to follow
|
|
691
|
+
- \`discover search <query>\` — Find people building specific things
|
|
692
|
+
- \`discover interests\` — Browse people by interest tags
|
|
693
|
+
- \`discover active\` — Who's building similar things right now
|
|
378
694
|
|
|
379
695
|
**Set up your profile:**
|
|
380
696
|
- \`vibe update building "what you're working on"\`
|
|
381
|
-
- \`vibe update interests "ai, startups, music"\`
|
|
697
|
+
- \`vibe update interests "ai, startups, music"\`
|
|
382
698
|
- \`vibe update tags "frontend, react, typescript"\``;
|
|
383
699
|
}
|
|
384
700
|
} catch (error) {
|
package/tools/dm.js
CHANGED
|
@@ -8,12 +8,12 @@ const memory = require('../memory');
|
|
|
8
8
|
const userProfiles = require('../store/profiles');
|
|
9
9
|
const patterns = require('../intelligence/patterns');
|
|
10
10
|
const { trackMessage, checkBurst } = require('./summarize');
|
|
11
|
-
const { requireInit, normalizeHandle, truncate, warning } = require('./_shared');
|
|
11
|
+
const { requireInit, normalizeHandle, truncate, warning, fetchRelevantUsers } = require('./_shared');
|
|
12
12
|
const { actions, formatActions } = require('./_actions');
|
|
13
13
|
|
|
14
14
|
const definition = {
|
|
15
15
|
name: 'vibe_dm',
|
|
16
|
-
description: 'Send a direct message to someone. Can include structured payload for games, handoffs, or
|
|
16
|
+
description: 'Send a direct message to someone. Can include structured payload for games, handoffs, artifact cards, or an instant USDC tip.',
|
|
17
17
|
inputSchema: {
|
|
18
18
|
type: 'object',
|
|
19
19
|
properties: {
|
|
@@ -32,6 +32,14 @@ const definition = {
|
|
|
32
32
|
payload: {
|
|
33
33
|
type: 'object',
|
|
34
34
|
description: 'Optional structured data (game state, code review, handoff, etc.)'
|
|
35
|
+
},
|
|
36
|
+
reply_to: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'Optional: Message ID to reply to (creates a threaded reply)'
|
|
39
|
+
},
|
|
40
|
+
tip_amount_cents: {
|
|
41
|
+
type: 'number',
|
|
42
|
+
description: 'Optional: Attach an instant USDC tip (100 = $1, 500 = $5, 1000 = $10)'
|
|
35
43
|
}
|
|
36
44
|
},
|
|
37
45
|
required: ['handle']
|
|
@@ -42,7 +50,7 @@ async function handler(args) {
|
|
|
42
50
|
const initCheck = requireInit();
|
|
43
51
|
if (initCheck) return initCheck;
|
|
44
52
|
|
|
45
|
-
const { handle, message, artifact_slug, payload } = args;
|
|
53
|
+
const { handle, message, artifact_slug, payload, reply_to, tip_amount_cents } = args;
|
|
46
54
|
const myHandle = config.getHandle();
|
|
47
55
|
const them = normalizeHandle(handle);
|
|
48
56
|
|
|
@@ -87,7 +95,13 @@ async function handler(args) {
|
|
|
87
95
|
const wasTruncated = trimmed.length > MAX_LENGTH;
|
|
88
96
|
const finalMessage = wasTruncated ? trimmed.substring(0, MAX_LENGTH) : trimmed;
|
|
89
97
|
|
|
90
|
-
|
|
98
|
+
// Send typing indicator (shows "typing..." to recipient while message is being sent)
|
|
99
|
+
// Non-blocking - we don't wait for this
|
|
100
|
+
store.sendTypingIndicator(myHandle, them).catch(() => {});
|
|
101
|
+
|
|
102
|
+
const result = await store.sendMessage(myHandle, them, finalMessage || null, 'dm', finalPayload, {
|
|
103
|
+
replyTo: reply_to || null,
|
|
104
|
+
});
|
|
91
105
|
|
|
92
106
|
// Check for errors
|
|
93
107
|
if (result && result.error) {
|
|
@@ -121,10 +135,7 @@ async function handler(args) {
|
|
|
121
135
|
display += ` ${warning(`truncated to ${MAX_LENGTH} chars`)}`;
|
|
122
136
|
}
|
|
123
137
|
|
|
124
|
-
//
|
|
125
|
-
if (finalMessage) {
|
|
126
|
-
display += `\n\n"${truncate(finalMessage, 100)}"`;
|
|
127
|
-
}
|
|
138
|
+
// Only show payload type indicator (message already visible in tool call)
|
|
128
139
|
if (finalPayload) {
|
|
129
140
|
const payloadType = finalPayload.type || 'data';
|
|
130
141
|
if (payloadType === 'artifact') {
|
|
@@ -135,6 +146,47 @@ async function handler(args) {
|
|
|
135
146
|
}
|
|
136
147
|
}
|
|
137
148
|
|
|
149
|
+
// Execute attached tip if specified
|
|
150
|
+
let tipResult = null;
|
|
151
|
+
if (tip_amount_cents && tip_amount_cents > 0) {
|
|
152
|
+
const token = config.getToken();
|
|
153
|
+
if (token) {
|
|
154
|
+
try {
|
|
155
|
+
const apiUrl = config.getApiUrl();
|
|
156
|
+
// Generate idempotency key to prevent duplicate tips from retries
|
|
157
|
+
const timeBucket = Math.floor(Date.now() / 60000);
|
|
158
|
+
const tipIdempotencyKey = `dm:${myHandle}:${them}:${tip_amount_cents}:${timeBucket}`;
|
|
159
|
+
|
|
160
|
+
const tipResponse = await fetch(`${apiUrl}/api/tips/instant`, {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: {
|
|
163
|
+
'Content-Type': 'application/json',
|
|
164
|
+
'Authorization': `Bearer ${token}`,
|
|
165
|
+
'Idempotency-Key': tipIdempotencyKey
|
|
166
|
+
},
|
|
167
|
+
body: JSON.stringify({
|
|
168
|
+
to: them,
|
|
169
|
+
amount_cents: tip_amount_cents,
|
|
170
|
+
message: message ? `${message.substring(0, 50)}...` : null,
|
|
171
|
+
context: { type: 'dm_reply' }
|
|
172
|
+
})
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
tipResult = await tipResponse.json();
|
|
176
|
+
|
|
177
|
+
if (tipResult.success) {
|
|
178
|
+
const tipAmount = (tip_amount_cents / 100).toFixed(0);
|
|
179
|
+
display += `\n\n💸 _Tipped $${tipAmount} USDC — [view tx](${tipResult.explorer_url})_`;
|
|
180
|
+
} else {
|
|
181
|
+
display += `\n\n⚠️ _Tip failed: ${tipResult.message || 'Unknown error'}_`;
|
|
182
|
+
}
|
|
183
|
+
} catch (tipError) {
|
|
184
|
+
console.warn('[dm] Tip execution failed:', tipError.message);
|
|
185
|
+
display += `\n\n⚠️ _Tip failed: ${tipError.message}_`;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
138
190
|
// Burst notification (5+ messages in one thread)
|
|
139
191
|
if (burst.triggered && burst.thread === them) {
|
|
140
192
|
display += `\n\n💬 _${burst.count} messages with @${them} — say "summarize" when done_`;
|
|
@@ -162,6 +214,26 @@ async function handler(args) {
|
|
|
162
214
|
// Add guided mode actions
|
|
163
215
|
response.actions = formatActions(actions.afterDm(them));
|
|
164
216
|
|
|
217
|
+
// Fetch DM suggestions (async, non-blocking for response)
|
|
218
|
+
// This adds "You might want to message..." suggestions
|
|
219
|
+
try {
|
|
220
|
+
const suggestions = await fetchRelevantUsers(myHandle, 'dm_suggest', 3);
|
|
221
|
+
if (suggestions && suggestions.matches && suggestions.matches.length > 0) {
|
|
222
|
+
// Filter out the person we just messaged
|
|
223
|
+
const others = suggestions.matches.filter(m => m.handle !== them);
|
|
224
|
+
if (others.length > 0) {
|
|
225
|
+
response.dm_suggestions = others.map(m => ({
|
|
226
|
+
handle: m.handle,
|
|
227
|
+
building: m.building,
|
|
228
|
+
reasons: m.reasons?.slice(0, 2) || []
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} catch (e) {
|
|
233
|
+
// Don't fail DM if suggestions fail
|
|
234
|
+
console.log('[dm] dm_suggest fetch error:', e.message);
|
|
235
|
+
}
|
|
236
|
+
|
|
165
237
|
return response;
|
|
166
238
|
}
|
|
167
239
|
|