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/init.js CHANGED
@@ -16,6 +16,7 @@ const path = require('path');
16
16
  const config = require('../config');
17
17
  const store = require('../store');
18
18
  const discord = require('../discord');
19
+ const authStore = require('../auth-store');
19
20
 
20
21
  const CALLBACK_PORT = 9876;
21
22
  const API_BASE = 'https://www.slashvibe.dev';
@@ -143,15 +144,34 @@ function detectTechStack() {
143
144
  }
144
145
 
145
146
  /**
146
- * Send personalized welcome from @vibe (non-blocking)
147
+ * Fetch GitHub friends who are on /vibe (non-blocking)
148
+ */
149
+ async function fetchGitHubFriends(handle) {
150
+ try {
151
+ const response = await fetch(`${API_BASE}/api/github/contacts?handle=${encodeURIComponent(handle)}`);
152
+ if (!response.ok) return null;
153
+ const data = await response.json();
154
+ if (!data.success) return null;
155
+ return {
156
+ friendsOnVibe: data.people_you_know?.slice(0, 5) || [],
157
+ inviteSuggestions: data.invite_suggestions?.slice(0, 10) || [],
158
+ totalContacts: data.stats?.total_contacts || 0
159
+ };
160
+ } catch (e) {
161
+ return null;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Send personalized welcome from @seth
167
+ * Returns the welcome message content so we can show it inline
147
168
  */
148
169
  async function sendPersonalizedWelcome(handle, oneLiner) {
149
170
  try {
150
171
  const repoName = detectRepoName();
151
172
  const techStack = detectTechStack();
152
173
 
153
- // Fire and forget - don't block init completion
154
- fetch(`${API_BASE}/api/onboarding/personalized-welcome`, {
174
+ const response = await fetch(`${API_BASE}/api/onboarding/personalized-welcome`, {
155
175
  method: 'POST',
156
176
  headers: { 'Content-Type': 'application/json' },
157
177
  body: JSON.stringify({
@@ -159,14 +179,20 @@ async function sendPersonalizedWelcome(handle, oneLiner) {
159
179
  oneLiner,
160
180
  repoName,
161
181
  techStack,
162
- githubProfile: null // Could be passed from callback if available
182
+ githubProfile: null
163
183
  })
164
- }).catch(e => {
165
- console.error('[vibe_init] Personalized welcome failed:', e.message);
166
184
  });
185
+
186
+ if (!response.ok) {
187
+ console.error('[vibe_init] Welcome API error:', response.status);
188
+ return null;
189
+ }
190
+
191
+ const result = await response.json();
192
+ return result.success ? result : null;
167
193
  } catch (e) {
168
- // Non-fatal - continue without personalized welcome
169
- console.error('[vibe_init] Context detection failed:', e.message);
194
+ console.error('[vibe_init] Personalized welcome failed:', e.message);
195
+ return null;
170
196
  }
171
197
  }
172
198
 
@@ -175,7 +201,7 @@ const API_URL = process.env.VIBE_API_URL || 'https://www.slashvibe.dev';
175
201
  const AUTH_TIMEOUT_MS = 120000; // 2 minutes
176
202
 
177
203
  /**
178
- * Send welcome message from @vibe
204
+ * Send welcome message from @seth (founder)
179
205
  */
180
206
  async function sendWelcomeMessage(handle, one_liner) {
181
207
  try {
@@ -194,17 +220,17 @@ async function sendWelcomeMessage(handle, one_liner) {
194
220
 
195
221
  const definition = {
196
222
  name: 'vibe_init',
197
- description: 'Set your identity for /vibe. Opens browser for GitHub auth and waits for completion. Returns when auth is done.',
223
+ description: 'Join /vibe social network. Opens browser for GitHub login - NO INPUT NEEDED. Your GitHub username becomes your handle automatically. Just run this and complete the browser auth.',
198
224
  inputSchema: {
199
225
  type: 'object',
200
226
  properties: {
201
227
  handle: {
202
228
  type: 'string',
203
- description: 'Custom handle (optional - defaults to your GitHub username)'
229
+ description: 'RARELY NEEDED - Override your handle (defaults to GitHub username, which is usually what you want)'
204
230
  },
205
231
  one_liner: {
206
232
  type: 'string',
207
- description: 'What are you building? (one line)'
233
+ description: 'OPTIONAL - What are you building? Can set this later.'
208
234
  }
209
235
  },
210
236
  required: []
@@ -252,10 +278,15 @@ function waitForCallback(requestedHandle, one_liner) {
252
278
  // Save the token and handle
253
279
  const finalHandle = requestedHandle || callbackHandle;
254
280
 
255
- // Save to config
256
- config.savePrivyToken(token);
281
+ // Save to config (file persistence for restarts)
282
+ config.saveAuthToken(token);
257
283
  config.setSessionIdentity(finalHandle, one_liner || '');
258
284
 
285
+ // PUSH to in-memory auth store (immediate propagation)
286
+ authStore.setToken(token);
287
+ authStore.setHandle(finalHandle);
288
+ authStore.setOneLiner(one_liner || '');
289
+
259
290
  // Update shared config
260
291
  const cfg = config.load();
261
292
  cfg.handle = finalHandle;
@@ -274,8 +305,8 @@ function waitForCallback(requestedHandle, one_liner) {
274
305
  // Post to Discord
275
306
  discord.postJoin(finalHandle, one_liner);
276
307
 
277
- // Send personalized welcome from @vibe (non-blocking)
278
- sendPersonalizedWelcome(finalHandle, one_liner);
308
+ // NOTE: Welcome message is sent in the main return path (awaited)
309
+ // to ensure it arrives before we show the unread count
279
310
 
280
311
  // Send success response to browser - lightweight, no infinite animations
281
312
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
@@ -562,7 +593,7 @@ async function handler(args) {
562
593
  }
563
594
 
564
595
  // Check if already authenticated
565
- if (config.hasPrivyAuth()) {
596
+ if (config.hasOAuth()) {
566
597
  const existingHandle = config.getHandle();
567
598
  if (existingHandle) {
568
599
  return {
@@ -605,24 +636,63 @@ To check messages: \`vibe inbox\``
605
636
  // Wait for callback (blocks until auth completes or times out)
606
637
  const result = await waitForCallback(h, one_liner);
607
638
 
608
- // Check for unread messages
609
- let unreadNotice = '';
639
+ // Send personalized welcome and wait for it (2.5s timeout)
640
+ let welcomeResult = null;
610
641
  try {
611
- const unreadCount = await store.getUnreadCount(result.handle);
612
- if (unreadCount > 0) {
613
- unreadNotice = `\n\n📬 **${unreadCount} unread messages** — say "check my messages"`;
614
- }
642
+ welcomeResult = await Promise.race([
643
+ sendPersonalizedWelcome(result.handle, one_liner),
644
+ new Promise(resolve => setTimeout(() => resolve(null), 2500))
645
+ ]);
646
+ } catch (e) {
647
+ console.error('[vibe_init] Welcome message failed:', e.message);
648
+ }
649
+
650
+ // Check for unread messages (includes the welcome we just sent)
651
+ let unreadCount = 0;
652
+ try {
653
+ unreadCount = await store.getUnreadCount(result.handle);
654
+ } catch (e) {}
655
+
656
+ // Fetch GitHub friends (non-blocking, 3s timeout)
657
+ let friendsData = null;
658
+ try {
659
+ const friendsPromise = fetchGitHubFriends(result.handle);
660
+ friendsData = await Promise.race([
661
+ friendsPromise,
662
+ new Promise(resolve => setTimeout(() => resolve(null), 3000))
663
+ ]);
615
664
  } catch (e) {}
616
665
 
617
666
  // Generate authenticated banner with handle + unread (3 lines only - won't collapse)
618
- const authBanner = generateAuthBanner(result.handle, 1, onlineCount);
667
+ const authBanner = generateAuthBanner(result.handle, unreadCount, onlineCount);
668
+
669
+ // Build friends section if we have data
670
+ let friendsSection = '';
671
+ if (friendsData?.friendsOnVibe?.length > 0) {
672
+ const friendHandles = friendsData.friendsOnVibe.map(f => `@${f.vibe_handle}`).join(', ');
673
+ friendsSection = `\n\n🤝 **${friendsData.friendsOnVibe.length} of your GitHub friends are here!**\n${friendHandles}`;
674
+ } else if (friendsData?.totalContacts > 0) {
675
+ friendsSection = `\n\n👋 None of your GitHub friends are on /vibe yet — say **"vibe invite"** to bring them in!`;
676
+ }
677
+
678
+ // Build welcome section - show inline if we have the message
679
+ let welcomeSection = '';
680
+ if (welcomeResult?.messageText) {
681
+ welcomeSection = `\n\n---\n**📨 Welcome message from @seth:**\n\n> ${welcomeResult.messageText.split('\n').join('\n> ')}\n\n_Reply with **"vibe dm @seth [message]"** to say hi!_`;
682
+ } else {
683
+ welcomeSection = '\n\n---\n**📨 @seth sent you a welcome message!**\n\nSay **"vibe inbox"** to read it and get started.';
684
+ }
619
685
 
620
686
  return {
621
- display: authBanner,
687
+ display: authBanner + friendsSection + welcomeSection,
622
688
  onboarding: {
623
689
  isNewUser: true,
624
690
  handle: result.handle,
625
- hint: 'show_onboarding_options'
691
+ hint: 'show_onboarding_options',
692
+ hasWelcomeMessage: !!welcomeResult,
693
+ welcomeText: welcomeResult?.messageText || null,
694
+ friendsOnVibe: friendsData?.friendsOnVibe || [],
695
+ inviteSuggestions: friendsData?.inviteSuggestions || []
626
696
  }
627
697
  };
628
698
 
@@ -631,7 +701,15 @@ To check messages: \`vibe inbox\``
631
701
  return {
632
702
  display: `## Auth already in progress
633
703
 
634
- Another login flow is running. Complete it in your browser or wait a moment and try again.`
704
+ Another login is running. Either:
705
+ 1. **Complete it** in your browser, or
706
+ 2. **Wait 30 seconds** and try again
707
+
708
+ If stuck, kill the auth server:
709
+ \`\`\`
710
+ lsof -ti:9876 | xargs kill
711
+ \`\`\`
712
+ Then run \`vibe init\` again.`
635
713
  };
636
714
  }
637
715
 
@@ -639,16 +717,26 @@ Another login flow is running. Complete it in your browser or wait a moment and
639
717
  return {
640
718
  display: `## Auth timed out
641
719
 
642
- The login flow wasn't completed within 2 minutes. Try again with \`vibe init\``
720
+ The browser login wasn't completed within 2 minutes.
721
+
722
+ **Try again:**
723
+ Just say "add the vibe mcp server" and complete the browser auth faster this time.
724
+
725
+ **Tip:** Keep Claude Code visible so you can see when auth completes.`
643
726
  };
644
727
  }
645
728
 
646
729
  return {
647
730
  display: `## Failed to authenticate
648
731
 
649
- Error: ${err.message}
732
+ **Error:** ${err.message}
733
+
734
+ **Quick fixes:**
735
+ 1. Check your internet connection
736
+ 2. Try again: just say "vibe init"
737
+ 3. Check status: slashvibe.dev/status
650
738
 
651
- Try again or use legacy auth: \`vibe init --auth_method=legacy\``
739
+ **Need help?** DM @seth on Twitter or email seth@slashvibe.dev`
652
740
  };
653
741
  }
654
742
  }
@@ -700,7 +788,16 @@ Local config saved. Heartbeats will use username fallback.`
700
788
  discord.postJoin(h, one_liner);
701
789
 
702
790
  // Send personalized welcome from @vibe (non-blocking)
703
- sendPersonalizedWelcome(h, one_liner);
791
+ // Send personalized welcome and wait for it
792
+ let welcomeResult = null;
793
+ try {
794
+ welcomeResult = await Promise.race([
795
+ sendPersonalizedWelcome(h, one_liner),
796
+ new Promise(resolve => setTimeout(() => resolve(null), 2500))
797
+ ]);
798
+ } catch (e) {
799
+ console.error('[vibe_init] Welcome message failed:', e.message);
800
+ }
704
801
 
705
802
  // Check for unread messages
706
803
  let unreadNotice = '';
@@ -718,14 +815,14 @@ Local config saved. Heartbeats will use username fallback.`
718
815
  **@${h}**
719
816
  _${one_liner}_${unreadNotice}${keypairNote}
720
817
 
721
- 📨 **Check your messages** — @vibe sent you a personalized welcome!
818
+ 📨 **Check your messages** — @seth sent you a personalized welcome!
722
819
 
723
820
  ⚠️ **Using local keys** — consider upgrading to GitHub auth:
724
821
  \`vibe init\` — Sign in with GitHub for verified identity
725
822
 
726
823
  ### Onboarding Checklist
727
- [ ] Read your welcome message from @vibe
728
- [ ] Reply to @vibe
824
+ [ ] Read your welcome message from @seth
825
+ [ ] Reply to @seth
729
826
  [ ] Message one recommended builder
730
827
  [ ] Post your first ship
731
828
  [ ] Leave some feedback
package/tools/invite.js CHANGED
@@ -203,14 +203,25 @@ _Copy the link above and send it._`
203
203
  const genNote = justGenerated ? ' (just generated)' : '';
204
204
  const randomMsg = INVITE_MESSAGES[Math.floor(Math.random() * INVITE_MESSAGES.length)];
205
205
 
206
+ // Build social share URLs
207
+ const tweetText = encodeURIComponent(`${randomMsg}\n\nhttps://${shareUrl}`);
208
+ const twitterUrl = `https://twitter.com/intent/tweet?text=${tweetText}`;
209
+
206
210
  return {
207
211
  display: `## Quick Invite${genNote}
208
212
 
209
- > ${randomMsg}
210
- >
211
- > **${shareUrl}**
213
+ **Copy this:**
214
+ \`\`\`
215
+ ${randomMsg}
216
+
217
+ https://${shareUrl}
218
+ \`\`\`
219
+
220
+ **Or share directly:**
221
+ - [Tweet it](${twitterUrl})
222
+ - Slack/Discord: Just paste the link above
212
223
 
213
- _Copy and send. ${shareCode ? `Bonus code earned when they join.` : ''}_`
224
+ ${shareCode ? `_When they join, you both get bonus codes._` : ''}`
214
225
  };
215
226
  }
216
227
 
@@ -0,0 +1,117 @@
1
+ /**
2
+ * vibe_leaderboard — See top builders on /vibe
3
+ *
4
+ * Shows the leaderboard ranked by Vibe Score:
5
+ * - Ships posted (building stuff)
6
+ * - Reactions received (quality signal)
7
+ * - Comments given (helping others)
8
+ * - Streak consistency (showing up)
9
+ * - Invites redeemed (growing community)
10
+ */
11
+
12
+ const config = require('../config');
13
+
14
+ const API_URL = process.env.VIBE_API_URL || 'https://www.slashvibe.dev';
15
+
16
+ const definition = {
17
+ name: 'vibe_leaderboard',
18
+ description: 'See top builders on /vibe ranked by Vibe Score',
19
+ inputSchema: {
20
+ type: 'object',
21
+ properties: {
22
+ category: {
23
+ type: 'string',
24
+ enum: ['overall', 'ships', 'streaks', 'reactions', 'helpful'],
25
+ description: 'Leaderboard category (default: overall)'
26
+ },
27
+ limit: {
28
+ type: 'number',
29
+ description: 'Number of entries to show (default: 10, max: 50)'
30
+ }
31
+ }
32
+ }
33
+ };
34
+
35
+ async function handler(args) {
36
+ const { category = 'overall', limit = 10 } = args;
37
+ const maxLimit = Math.min(limit, 50);
38
+
39
+ try {
40
+ const endpoint = `${API_URL}/api/growth/leaderboard?category=${category}&limit=${maxLimit}`;
41
+ const response = await fetch(endpoint);
42
+ const data = await response.json();
43
+
44
+ if (!data.success) {
45
+ return { display: `\u274c Failed to load leaderboard: ${data.error}` };
46
+ }
47
+
48
+ const myHandle = config.isInitialized() ? config.getHandle() : null;
49
+
50
+ let display = `## /vibe Leaderboard\n`;
51
+ display += `**Category:** ${category} | **Showing:** Top ${maxLimit}\n\n`;
52
+
53
+ if (data.leaderboard.length === 0) {
54
+ display += `_No entries yet. Be the first to ship!_\n`;
55
+ return { display };
56
+ }
57
+
58
+ // Table header
59
+ display += `| Rank | Builder | Vibe Score | Ships | Streak |\n`;
60
+ display += `|------|---------|------------|-------|--------|\n`;
61
+
62
+ for (const entry of data.leaderboard) {
63
+ const isMe = myHandle && entry.handle.toLowerCase() === myHandle.toLowerCase();
64
+ const highlight = isMe ? '**' : '';
65
+
66
+ // Position change indicator
67
+ let posIndicator = '';
68
+ if (entry.positionChange > 0) {
69
+ posIndicator = ` \u2191${entry.positionChange}`;
70
+ } else if (entry.positionChange < 0) {
71
+ posIndicator = ` \u2193${Math.abs(entry.positionChange)}`;
72
+ } else if (entry.isNew) {
73
+ posIndicator = ' \u2728';
74
+ }
75
+
76
+ // Tier emoji
77
+ const tierEmoji = entry.tierDisplay?.emoji || '';
78
+
79
+ display += `| ${highlight}#${entry.rank}${posIndicator}${highlight} | ${highlight}@${entry.handle}${highlight} ${tierEmoji} | ${entry.vibeScore} | ${entry.stats?.ships || 0} | ${entry.streak || 0}d |\n`;
80
+ }
81
+
82
+ // Stats summary
83
+ if (data.stats) {
84
+ display += `\n### Community Stats\n`;
85
+ display += `- **Total Builders:** ${data.stats.total}\n`;
86
+ display += `- **Ships Posted:** ${data.stats.shipsCount}\n`;
87
+ display += `- **Active Today:** ${data.stats.activeToday}\n`;
88
+ display += `- **Avg Vibe Score:** ${data.stats.averageVibeScore}\n`;
89
+ }
90
+
91
+ // Highlights
92
+ if (data.highlights?.movers?.length > 0) {
93
+ display += `\n### \ud83d\ude80 Biggest Movers\n`;
94
+ for (const mover of data.highlights.movers.slice(0, 3)) {
95
+ display += `- @${mover.handle} +${mover.change} (now #${mover.rank})\n`;
96
+ }
97
+ }
98
+
99
+ if (data.highlights?.newEntries?.length > 0) {
100
+ display += `\n### \u2728 New Entries\n`;
101
+ for (const entry of data.highlights.newEntries.slice(0, 3)) {
102
+ display += `- @${entry.handle} joined at #${entry.rank}\n`;
103
+ }
104
+ }
105
+
106
+ display += `\n_${data.note}_`;
107
+
108
+ return { display };
109
+
110
+ } catch (e) {
111
+ return {
112
+ display: `## /vibe Leaderboard\n\n\u274c **Failed to load**\n\nError: ${e.message}`
113
+ };
114
+ }
115
+ }
116
+
117
+ module.exports = { definition, handler };
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Git Apply Library
3
+ *
4
+ * Downloads and applies git bundles from session forks.
5
+ * Used by session.js fork command to get actual code.
6
+ *
7
+ * Key functions:
8
+ * - downloadBundle(url) - Download bundle from signed URL
9
+ * - applyBundleFromUrl(url, branchName) - Download and apply in one step
10
+ */
11
+
12
+ const { execSync } = require('child_process');
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const os = require('os');
16
+ const https = require('https');
17
+ const http = require('http');
18
+
19
+ const { isGitRepo, validateBundle, applyBundle } = require('./git-bundle');
20
+
21
+ /**
22
+ * Download a bundle from a URL
23
+ * @param {string} url - The signed download URL
24
+ * @returns {Promise<object>} - { success, buffer, error? }
25
+ */
26
+ async function downloadBundle(url) {
27
+ return new Promise((resolve) => {
28
+ const protocol = url.startsWith('https') ? https : http;
29
+
30
+ const request = protocol.get(url, (response) => {
31
+ // Handle redirects
32
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
33
+ downloadBundle(response.headers.location).then(resolve);
34
+ return;
35
+ }
36
+
37
+ if (response.statusCode !== 200) {
38
+ resolve({
39
+ success: false,
40
+ error: `HTTP ${response.statusCode}: ${response.statusMessage}`,
41
+ });
42
+ return;
43
+ }
44
+
45
+ const chunks = [];
46
+ response.on('data', (chunk) => chunks.push(chunk));
47
+ response.on('end', () => {
48
+ const buffer = Buffer.concat(chunks);
49
+ resolve({
50
+ success: true,
51
+ buffer,
52
+ size: buffer.length,
53
+ });
54
+ });
55
+ response.on('error', (err) => {
56
+ resolve({ success: false, error: err.message });
57
+ });
58
+ });
59
+
60
+ request.on('error', (err) => {
61
+ resolve({ success: false, error: err.message });
62
+ });
63
+
64
+ // Timeout after 60 seconds
65
+ request.setTimeout(60000, () => {
66
+ request.destroy();
67
+ resolve({ success: false, error: 'Download timeout (60s)' });
68
+ });
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Download and apply a bundle from a URL
74
+ * @param {string} url - The signed download URL
75
+ * @param {string} branchName - Name for the new branch (default: 'forked-session')
76
+ * @returns {Promise<object>} - { success, branch, commits?, error? }
77
+ */
78
+ async function applyBundleFromUrl(url, branchName = 'forked-session') {
79
+ // Check we're in a git repo
80
+ if (!isGitRepo()) {
81
+ return { success: false, error: 'Not in a git repository' };
82
+ }
83
+
84
+ // Download the bundle
85
+ console.log(`[git-apply] Downloading bundle...`);
86
+ const downloadResult = await downloadBundle(url);
87
+
88
+ if (!downloadResult.success) {
89
+ return { success: false, error: `Download failed: ${downloadResult.error}` };
90
+ }
91
+
92
+ console.log(`[git-apply] Downloaded ${downloadResult.size} bytes`);
93
+
94
+ // Validate the bundle
95
+ const validation = validateBundle(downloadResult.buffer);
96
+ if (!validation.success) {
97
+ return { success: false, error: `Invalid bundle: ${validation.error}` };
98
+ }
99
+
100
+ // Apply the bundle
101
+ console.log(`[git-apply] Applying bundle to branch: ${branchName}`);
102
+ const applyResult = applyBundle(downloadResult.buffer, branchName);
103
+
104
+ if (!applyResult.success) {
105
+ return { success: false, error: `Apply failed: ${applyResult.error}` };
106
+ }
107
+
108
+ return {
109
+ success: true,
110
+ branch: applyResult.branch,
111
+ refs: applyResult.refs,
112
+ size: downloadResult.size,
113
+ message: `Bundle applied to branch '${applyResult.branch}'. Use 'git checkout ${applyResult.branch}' to view.`,
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Check out the forked branch
119
+ * @param {string} branchName - Branch to checkout
120
+ * @returns {object} - { success, error? }
121
+ */
122
+ function checkoutBranch(branchName) {
123
+ try {
124
+ execSync(`git checkout ${branchName}`, {
125
+ stdio: 'pipe',
126
+ encoding: 'utf8',
127
+ });
128
+ return { success: true };
129
+ } catch (error) {
130
+ return { success: false, error: error.message };
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Get info about a branch
136
+ * @param {string} branchName - Branch name
137
+ * @returns {object} - { exists, current, commits? }
138
+ */
139
+ function getBranchInfo(branchName) {
140
+ try {
141
+ // Check if branch exists
142
+ const branches = execSync('git branch --list', {
143
+ stdio: 'pipe',
144
+ encoding: 'utf8',
145
+ });
146
+
147
+ const branchList = branches.split('\n').map((b) => b.replace(/^\*?\s*/, '').trim());
148
+ const exists = branchList.includes(branchName);
149
+
150
+ // Check current branch
151
+ const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
152
+ stdio: 'pipe',
153
+ encoding: 'utf8',
154
+ }).trim();
155
+
156
+ const result = {
157
+ exists,
158
+ current: currentBranch === branchName,
159
+ currentBranch,
160
+ };
161
+
162
+ // Get commit count if branch exists
163
+ if (exists) {
164
+ try {
165
+ const commitCount = execSync(`git rev-list --count ${branchName}`, {
166
+ stdio: 'pipe',
167
+ encoding: 'utf8',
168
+ });
169
+ result.commits = parseInt(commitCount.trim(), 10);
170
+ } catch (e) {
171
+ // Branch might not have commits yet
172
+ }
173
+ }
174
+
175
+ return result;
176
+ } catch (error) {
177
+ return { exists: false, current: false, error: error.message };
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Delete a branch
183
+ * @param {string} branchName - Branch to delete
184
+ * @param {boolean} force - Force delete even if not merged
185
+ * @returns {object} - { success, error? }
186
+ */
187
+ function deleteBranch(branchName, force = false) {
188
+ try {
189
+ const flag = force ? '-D' : '-d';
190
+ execSync(`git branch ${flag} ${branchName}`, {
191
+ stdio: 'pipe',
192
+ encoding: 'utf8',
193
+ });
194
+ return { success: true };
195
+ } catch (error) {
196
+ return { success: false, error: error.message };
197
+ }
198
+ }
199
+
200
+ module.exports = {
201
+ downloadBundle,
202
+ applyBundleFromUrl,
203
+ checkoutBranch,
204
+ getBranchInfo,
205
+ deleteBranch,
206
+ };