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.
Files changed (167) hide show
  1. package/README.md +47 -252
  2. package/analytics.js +107 -0
  3. package/auth-store.js +148 -0
  4. package/auto-update.js +130 -0
  5. package/bridges/bridge-monitor.js +388 -0
  6. package/bridges/discord-bot.js +431 -0
  7. package/bridges/farcaster.js +299 -0
  8. package/bridges/telegram.js +261 -0
  9. package/bridges/webhook-health.js +420 -0
  10. package/bridges/webhook-server.js +437 -0
  11. package/bridges/whatsapp.js +441 -0
  12. package/bridges/x-webhook.js +423 -0
  13. package/config.js +27 -15
  14. package/games/arcade.js +406 -0
  15. package/games/chess.js +451 -0
  16. package/games/colorguess.js +343 -0
  17. package/games/crossword-words.js +171 -0
  18. package/games/crossword.js +461 -0
  19. package/games/drawing.js +347 -0
  20. package/games/gameroulette.js +300 -0
  21. package/games/gamerouter.js +336 -0
  22. package/games/gamestatus.js +337 -0
  23. package/games/guessnumber.js +209 -0
  24. package/games/hangman.js +279 -0
  25. package/games/memory.js +338 -0
  26. package/games/multiplayer-tictactoe.js +389 -0
  27. package/games/pixelart.js +399 -0
  28. package/games/quickduel.js +354 -0
  29. package/games/riddle.js +371 -0
  30. package/games/rockpaperscissors.js +291 -0
  31. package/games/snake.js +406 -0
  32. package/games/storybuilder.js +343 -0
  33. package/games/tictactoe.js +345 -0
  34. package/games/twentyquestions.js +286 -0
  35. package/games/twotruths.js +207 -0
  36. package/games/werewolf.js +508 -0
  37. package/games/wordassociation.js +247 -0
  38. package/games/wordchain.js +135 -0
  39. package/index.js +116 -159
  40. package/intelligence/index.js +9 -2
  41. package/intelligence/interests.js +369 -0
  42. package/notification-emitter.js +77 -0
  43. package/notify.js +5 -1
  44. package/package.json +21 -16
  45. package/prompts.js +1 -1
  46. package/protocol/index.js +73 -0
  47. package/setup.js +480 -0
  48. package/smart-inbox.js +276 -0
  49. package/store/api.js +536 -215
  50. package/store/profiles.js +160 -12
  51. package/tools/_actions.js +362 -21
  52. package/tools/_discovery.js +119 -26
  53. package/tools/_shared/index.js +64 -0
  54. package/tools/_shared.js +234 -0
  55. package/tools/_work-context.js +338 -0
  56. package/tools/_work-context.manual-test.js +199 -0
  57. package/tools/_work-context.test.js +260 -0
  58. package/tools/activity.js +220 -0
  59. package/tools/analytics.js +191 -0
  60. package/tools/approve.js +197 -0
  61. package/tools/artifact-create.js +14 -3
  62. package/tools/artifacts-price.js +107 -0
  63. package/tools/available.js +120 -0
  64. package/tools/broadcast.js +325 -0
  65. package/tools/chat.js +202 -0
  66. package/tools/collaborative-drawing.js +1 -1
  67. package/tools/connection-status.js +178 -0
  68. package/tools/discover.js +350 -34
  69. package/tools/dm.js +80 -8
  70. package/tools/earnings.js +126 -0
  71. package/tools/feed.js +35 -4
  72. package/tools/follow.js +224 -0
  73. package/tools/friends.js +207 -0
  74. package/tools/gig-browse.js +206 -0
  75. package/tools/gig-complete.js +144 -0
  76. package/tools/health.js +87 -0
  77. package/tools/help.js +3 -3
  78. package/tools/idea.js +9 -2
  79. package/tools/inbox.js +289 -105
  80. package/tools/init.js +131 -34
  81. package/tools/invite.js +15 -4
  82. package/tools/leaderboard.js +117 -0
  83. package/tools/lib/git-apply.js +206 -0
  84. package/tools/lib/git-bundle.js +407 -0
  85. package/tools/migrate.js +3 -3
  86. package/tools/multiplayer-game.js +1 -1
  87. package/tools/onboarding.js +7 -7
  88. package/tools/open.js +143 -12
  89. package/tools/party-game.js +1 -1
  90. package/tools/plan.js +225 -0
  91. package/tools/proof-of-work.js +144 -0
  92. package/tools/reply.js +166 -0
  93. package/tools/report.js +1 -1
  94. package/tools/request.js +17 -3
  95. package/tools/schedule.js +367 -0
  96. package/tools/search-messages.js +123 -0
  97. package/tools/session.js +467 -0
  98. package/tools/session_price.js +128 -0
  99. package/tools/settings.js +90 -2
  100. package/tools/ship.js +30 -7
  101. package/tools/smart-check.js +201 -0
  102. package/tools/start.js +147 -12
  103. package/tools/status.js +53 -6
  104. package/tools/streak.js +147 -0
  105. package/tools/stuck.js +297 -0
  106. package/tools/subscribe.js +148 -0
  107. package/tools/subscriptions.js +134 -0
  108. package/tools/suggest-tags.js +6 -8
  109. package/tools/tag-suggestions.js +1 -1
  110. package/tools/tip.js +150 -77
  111. package/tools/token.js +4 -4
  112. package/tools/update.js +1 -1
  113. package/tools/wallet.js +221 -79
  114. package/tools/watch.js +157 -0
  115. package/tools/who.js +30 -1
  116. package/tools/withdraw.js +145 -0
  117. package/tools/work-summary.js +96 -0
  118. package/version.json +10 -8
  119. package/LICENSE +0 -21
  120. package/store/sqlite.js +0 -347
  121. /package/tools/{auto-suggest-connections.js → _deprecated/auto-suggest-connections.js} +0 -0
  122. /package/tools/{away.js → _deprecated/away.js} +0 -0
  123. /package/tools/{back.js → _deprecated/back.js} +0 -0
  124. /package/tools/{bootstrap-skills.js → _deprecated/bootstrap-skills.js} +0 -0
  125. /package/tools/{bridge-dashboard.js → _deprecated/bridge-dashboard.js} +0 -0
  126. /package/tools/{bridge-health.js → _deprecated/bridge-health.js} +0 -0
  127. /package/tools/{bridge-live.js → _deprecated/bridge-live.js} +0 -0
  128. /package/tools/{bridges.js → _deprecated/bridges.js} +0 -0
  129. /package/tools/{colorguess.js → _deprecated/colorguess.js} +0 -0
  130. /package/tools/{discover-insights.js → _deprecated/discover-insights.js} +0 -0
  131. /package/tools/{discover-momentum.js → _deprecated/discover-momentum.js} +0 -0
  132. /package/tools/{discovery-analytics.js → _deprecated/discovery-analytics.js} +0 -0
  133. /package/tools/{discovery-auto-suggest.js → _deprecated/discovery-auto-suggest.js} +0 -0
  134. /package/tools/{discovery-bootstrap.js → _deprecated/discovery-bootstrap.js} +0 -0
  135. /package/tools/{discovery-daily.js → _deprecated/discovery-daily.js} +0 -0
  136. /package/tools/{discovery-dashboard.js → _deprecated/discovery-dashboard.js} +0 -0
  137. /package/tools/{discovery-digest.js → _deprecated/discovery-digest.js} +0 -0
  138. /package/tools/{discovery-hub.js → _deprecated/discovery-hub.js} +0 -0
  139. /package/tools/{discovery-insights.js → _deprecated/discovery-insights.js} +0 -0
  140. /package/tools/{discovery-momentum.js → _deprecated/discovery-momentum.js} +0 -0
  141. /package/tools/{discovery-monitor.js → _deprecated/discovery-monitor.js} +0 -0
  142. /package/tools/{discovery-proactive.js → _deprecated/discovery-proactive.js} +0 -0
  143. /package/tools/{draw.js → _deprecated/draw.js} +0 -0
  144. /package/tools/{farcaster.js → _deprecated/farcaster.js} +0 -0
  145. /package/tools/{forget.js → _deprecated/forget.js} +0 -0
  146. /package/tools/{games-catalog.js → _deprecated/games-catalog.js} +0 -0
  147. /package/tools/{games.js → _deprecated/games.js} +0 -0
  148. /package/tools/{guessnumber.js → _deprecated/guessnumber.js} +0 -0
  149. /package/tools/{hangman.js → _deprecated/hangman.js} +0 -0
  150. /package/tools/{multiplayer-tictactoe.js → _deprecated/multiplayer-tictactoe.js} +0 -0
  151. /package/tools/{mute.js → _deprecated/mute.js} +0 -0
  152. /package/tools/{recall.js → _deprecated/recall.js} +0 -0
  153. /package/tools/{remember.js → _deprecated/remember.js} +0 -0
  154. /package/tools/{riddle.js → _deprecated/riddle.js} +0 -0
  155. /package/tools/{run-bootstrap.js → _deprecated/run-bootstrap.js} +0 -0
  156. /package/tools/{skills-analytics.js → _deprecated/skills-analytics.js} +0 -0
  157. /package/tools/{skills-bootstrap.js → _deprecated/skills-bootstrap.js} +0 -0
  158. /package/tools/{skills-dashboard.js → _deprecated/skills-dashboard.js} +0 -0
  159. /package/tools/{skills-exchange.js → _deprecated/skills-exchange.js} +0 -0
  160. /package/tools/{skills.js → _deprecated/skills.js} +0 -0
  161. /package/tools/{smart-intro.js → _deprecated/smart-intro.js} +0 -0
  162. /package/tools/{storybuilder.js → _deprecated/storybuilder.js} +0 -0
  163. /package/tools/{telegram-bot.js → _deprecated/telegram-bot.js} +0 -0
  164. /package/tools/{telegram-setup.js → _deprecated/telegram-setup.js} +0 -0
  165. /package/tools/{tictactoe.js → _deprecated/tictactoe.js} +0 -0
  166. /package/tools/{twentyquestions.js → _deprecated/twentyquestions.js} +0 -0
  167. /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 based on what they\'re building.',
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 to run'
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
- async function getSuggestions(myHandle) {
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 suggestions = await getSuggestions(myHandle);
241
-
242
- if (suggestions.length === 0) {
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
- display = `## People You Should Meet\n\n`;
255
-
256
- for (const match of suggestions) {
257
- display += `**@${match.handle}** _(${match.score} match)_\n`;
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
- if (match.reasons.length > 0) {
261
- display += `🔗 ${match.reasons.join(' ')}\n`;
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 += `_Last seen: ${formatTimeAgo(match.lastSeen)}_\n\n`;
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}&section=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
- **\`discover suggest\`** — Get personalized recommendations
375
- **\`discover search <query>\`** Find people building specific things
376
- **\`discover interests\`** — Browse people by interest tags
377
- **\`discover active\`**Show who's building similar things right now
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 artifact cards.',
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
- const result = await store.sendMessage(myHandle, them, finalMessage || null, 'dm', finalPayload);
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
- // Show message preview or payload type
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