slashvibe-mcp 0.2.8 → 0.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/README.md +41 -58
  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 +77 -53
  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 +18 -6
  45. package/prompts.js +1 -1
  46. package/protocol/index.js +73 -0
  47. package/setup.js +402 -0
  48. package/store/api.js +436 -211
  49. package/store/profiles.js +160 -12
  50. package/tools/_actions.js +362 -21
  51. package/tools/_discovery.js +119 -26
  52. package/tools/_shared/index.js +64 -0
  53. package/tools/_shared.js +234 -0
  54. package/tools/_work-context.js +338 -0
  55. package/tools/_work-context.manual-test.js +199 -0
  56. package/tools/_work-context.test.js +260 -0
  57. package/tools/activity.js +220 -0
  58. package/tools/analytics.js +191 -0
  59. package/tools/approve.js +197 -0
  60. package/tools/artifact-create.js +14 -3
  61. package/tools/artifacts-price.js +107 -0
  62. package/tools/available.js +120 -0
  63. package/tools/broadcast.js +286 -0
  64. package/tools/chat.js +202 -0
  65. package/tools/collaborative-drawing.js +1 -1
  66. package/tools/connection-status.js +178 -0
  67. package/tools/discover.js +350 -34
  68. package/tools/dm.js +80 -8
  69. package/tools/earnings.js +126 -0
  70. package/tools/feed.js +35 -4
  71. package/tools/follow.js +224 -0
  72. package/tools/friends.js +207 -0
  73. package/tools/gig-browse.js +206 -0
  74. package/tools/gig-complete.js +144 -0
  75. package/tools/help.js +3 -3
  76. package/tools/idea.js +9 -2
  77. package/tools/inbox.js +289 -105
  78. package/tools/init.js +106 -27
  79. package/tools/invite.js +15 -4
  80. package/tools/migrate.js +3 -3
  81. package/tools/multiplayer-game.js +1 -1
  82. package/tools/onboarding.js +7 -7
  83. package/tools/open.js +143 -12
  84. package/tools/party-game.js +1 -1
  85. package/tools/plan.js +225 -0
  86. package/tools/proof-of-work.js +144 -0
  87. package/tools/reply.js +166 -0
  88. package/tools/report.js +1 -1
  89. package/tools/request.js +17 -3
  90. package/tools/schedule.js +367 -0
  91. package/tools/search-messages.js +123 -0
  92. package/tools/session.js +420 -0
  93. package/tools/session_price.js +128 -0
  94. package/tools/settings.js +90 -2
  95. package/tools/ship.js +30 -7
  96. package/tools/smart-check.js +201 -0
  97. package/tools/start.js +147 -12
  98. package/tools/status.js +53 -6
  99. package/tools/stuck.js +297 -0
  100. package/tools/subscribe.js +148 -0
  101. package/tools/subscriptions.js +134 -0
  102. package/tools/suggest-tags.js +6 -8
  103. package/tools/tag-suggestions.js +1 -1
  104. package/tools/tip.js +150 -77
  105. package/tools/token.js +4 -4
  106. package/tools/update.js +1 -1
  107. package/tools/wallet.js +221 -79
  108. package/tools/watch.js +157 -0
  109. package/tools/who.js +30 -1
  110. package/tools/withdraw.js +145 -0
  111. package/tools/work-summary.js +96 -0
  112. package/version.json +10 -8
  113. package/LICENSE +0 -21
  114. package/store/sqlite.js +0 -347
  115. /package/tools/{auto-suggest-connections.js → _deprecated/auto-suggest-connections.js} +0 -0
  116. /package/tools/{away.js → _deprecated/away.js} +0 -0
  117. /package/tools/{back.js → _deprecated/back.js} +0 -0
  118. /package/tools/{bootstrap-skills.js → _deprecated/bootstrap-skills.js} +0 -0
  119. /package/tools/{bridge-dashboard.js → _deprecated/bridge-dashboard.js} +0 -0
  120. /package/tools/{bridge-health.js → _deprecated/bridge-health.js} +0 -0
  121. /package/tools/{bridge-live.js → _deprecated/bridge-live.js} +0 -0
  122. /package/tools/{bridges.js → _deprecated/bridges.js} +0 -0
  123. /package/tools/{colorguess.js → _deprecated/colorguess.js} +0 -0
  124. /package/tools/{discover-insights.js → _deprecated/discover-insights.js} +0 -0
  125. /package/tools/{discover-momentum.js → _deprecated/discover-momentum.js} +0 -0
  126. /package/tools/{discovery-analytics.js → _deprecated/discovery-analytics.js} +0 -0
  127. /package/tools/{discovery-auto-suggest.js → _deprecated/discovery-auto-suggest.js} +0 -0
  128. /package/tools/{discovery-bootstrap.js → _deprecated/discovery-bootstrap.js} +0 -0
  129. /package/tools/{discovery-daily.js → _deprecated/discovery-daily.js} +0 -0
  130. /package/tools/{discovery-dashboard.js → _deprecated/discovery-dashboard.js} +0 -0
  131. /package/tools/{discovery-digest.js → _deprecated/discovery-digest.js} +0 -0
  132. /package/tools/{discovery-hub.js → _deprecated/discovery-hub.js} +0 -0
  133. /package/tools/{discovery-insights.js → _deprecated/discovery-insights.js} +0 -0
  134. /package/tools/{discovery-momentum.js → _deprecated/discovery-momentum.js} +0 -0
  135. /package/tools/{discovery-monitor.js → _deprecated/discovery-monitor.js} +0 -0
  136. /package/tools/{discovery-proactive.js → _deprecated/discovery-proactive.js} +0 -0
  137. /package/tools/{draw.js → _deprecated/draw.js} +0 -0
  138. /package/tools/{farcaster.js → _deprecated/farcaster.js} +0 -0
  139. /package/tools/{forget.js → _deprecated/forget.js} +0 -0
  140. /package/tools/{games-catalog.js → _deprecated/games-catalog.js} +0 -0
  141. /package/tools/{games.js → _deprecated/games.js} +0 -0
  142. /package/tools/{guessnumber.js → _deprecated/guessnumber.js} +0 -0
  143. /package/tools/{hangman.js → _deprecated/hangman.js} +0 -0
  144. /package/tools/{multiplayer-tictactoe.js → _deprecated/multiplayer-tictactoe.js} +0 -0
  145. /package/tools/{mute.js → _deprecated/mute.js} +0 -0
  146. /package/tools/{recall.js → _deprecated/recall.js} +0 -0
  147. /package/tools/{remember.js → _deprecated/remember.js} +0 -0
  148. /package/tools/{riddle.js → _deprecated/riddle.js} +0 -0
  149. /package/tools/{run-bootstrap.js → _deprecated/run-bootstrap.js} +0 -0
  150. /package/tools/{skills-analytics.js → _deprecated/skills-analytics.js} +0 -0
  151. /package/tools/{skills-bootstrap.js → _deprecated/skills-bootstrap.js} +0 -0
  152. /package/tools/{skills-dashboard.js → _deprecated/skills-dashboard.js} +0 -0
  153. /package/tools/{skills-exchange.js → _deprecated/skills-exchange.js} +0 -0
  154. /package/tools/{skills.js → _deprecated/skills.js} +0 -0
  155. /package/tools/{smart-intro.js → _deprecated/smart-intro.js} +0 -0
  156. /package/tools/{storybuilder.js → _deprecated/storybuilder.js} +0 -0
  157. /package/tools/{telegram-bot.js → _deprecated/telegram-bot.js} +0 -0
  158. /package/tools/{telegram-setup.js → _deprecated/telegram-setup.js} +0 -0
  159. /package/tools/{tictactoe.js → _deprecated/tictactoe.js} +0 -0
  160. /package/tools/{twentyquestions.js → _deprecated/twentyquestions.js} +0 -0
  161. /package/tools/{wordassociation.js → _deprecated/wordassociation.js} +0 -0
@@ -0,0 +1,126 @@
1
+ /**
2
+ * vibe_earnings - View unified earnings dashboard
3
+ *
4
+ * Shows all revenue sources aggregated:
5
+ * - Artifact sales
6
+ * - Session PPV revenue
7
+ * - Subscription revenue
8
+ * - Gig completions
9
+ * - Tips received
10
+ * - On-chain balance
11
+ *
12
+ * Examples:
13
+ * - "vibe earnings"
14
+ * - "show my earnings"
15
+ * - "how much have I made"
16
+ */
17
+
18
+ const fetch = require('node-fetch');
19
+ const config = require('../config');
20
+
21
+ const definition = {
22
+ name: 'vibe_earnings',
23
+ description: 'View your unified earnings dashboard - artifact sales, session PPV, subscriptions, gigs, tips, and on-chain balance.',
24
+ inputSchema: {
25
+ type: 'object',
26
+ properties: {
27
+ handle: {
28
+ type: 'string',
29
+ description: 'Handle to check earnings for (defaults to your handle)'
30
+ }
31
+ }
32
+ }
33
+ };
34
+
35
+ async function handler(args) {
36
+ const { handle } = args;
37
+
38
+ if (!config.isInitialized()) {
39
+ return {
40
+ display: 'Run `vibe init` first to set your identity.'
41
+ };
42
+ }
43
+
44
+ const targetHandle = handle || config.getHandle();
45
+ const apiUrl = process.env.VIBE_API_URL || 'https://www.slashvibe.dev';
46
+ const token = config.getToken();
47
+
48
+ try {
49
+ const response = await fetch(
50
+ `${apiUrl}/api/earnings?handle=${encodeURIComponent(targetHandle)}`,
51
+ {
52
+ headers: token ? {
53
+ 'Authorization': `Bearer ${token}`
54
+ } : {}
55
+ }
56
+ );
57
+
58
+ const result = await response.json();
59
+
60
+ if (!response.ok) {
61
+ return {
62
+ display: `❌ ${result.error || 'Failed to fetch earnings'}`
63
+ };
64
+ }
65
+
66
+ const { earnings, total_earned_display, pending_display, on_chain } = result;
67
+
68
+ let formatted = `
69
+ ┌────────────────────────────────────────────────────────────┐
70
+ │ 💰 EARNINGS DASHBOARD │
71
+ │ @${targetHandle.padEnd(33)}│
72
+ ├────────────────────────────────────────────────────────────┤
73
+ │ │
74
+ │ Total Earned: ${(total_earned_display || '$0').padEnd(40)} │
75
+ │ Pending (On-chain): ${(pending_display || '$0').padEnd(34)} │
76
+ │ │
77
+ ├────────────────────────────────────────────────────────────┤
78
+ │ 📊 Breakdown by Source: │
79
+ │ │
80
+ │ Artifacts: ${(earnings.artifacts.revenue_display || '$0').padEnd(15)} (${earnings.artifacts.sales || 0} sales)${' '.repeat(17)} │
81
+ │ Sessions: ${(earnings.sessions.revenue_display || '$0').padEnd(15)} (${earnings.sessions.ppv_sales || 0} PPV)${' '.repeat(18)} │
82
+ │ Gigs: ${(earnings.gigs.revenue_display || '$0').padEnd(15)} (${earnings.gigs.completed || 0} completed)${' '.repeat(12)} │
83
+ │ Tips: ${(earnings.tips.revenue_display || '$0').padEnd(15)} (${earnings.tips.received || 0} received)${' '.repeat(13)} │
84
+ │ │
85
+ `.trim();
86
+
87
+ if (on_chain) {
88
+ formatted += `
89
+ ├────────────────────────────────────────────────────────────┤
90
+ │ ⛓️ On-Chain Status: │`;
91
+
92
+ if (on_chain.registered) {
93
+ const walletDisplay = on_chain.wallet
94
+ ? `${on_chain.wallet.slice(0, 10)}...${on_chain.wallet.slice(-8)}`
95
+ : 'not set';
96
+ formatted += `
97
+ │ │
98
+ │ Registered: Yes │
99
+ │ Wallet: ${walletDisplay.padEnd(47)} │
100
+ │ Balance: ${(on_chain.balance_eth + ' ETH').padEnd(46)} │`;
101
+ if (on_chain.can_withdraw) {
102
+ formatted += `
103
+ │ ✅ Ready to withdraw │`;
104
+ }
105
+ } else {
106
+ formatted += `
107
+ │ │
108
+ │ Registered: No │
109
+ │ ⚠️ Register to receive payments: vibe register_onchain │`;
110
+ }
111
+ }
112
+
113
+ formatted += `
114
+ │ │
115
+ └────────────────────────────────────────────────────────────┘`;
116
+
117
+ return { display: formatted, data: result };
118
+
119
+ } catch (error) {
120
+ return {
121
+ display: `❌ Failed to fetch earnings: ${error.message}`
122
+ };
123
+ }
124
+ }
125
+
126
+ module.exports = { definition, handler };
package/tools/feed.js CHANGED
@@ -13,7 +13,8 @@
13
13
  */
14
14
 
15
15
  const config = require('../config');
16
- const { requireInit, header, emptyState, formatTimeAgo, divider } = require('./_shared');
16
+ const { requireInit, header, emptyState, formatTimeAgo, divider, fetchRelevantUsers } = require('./_shared');
17
+ const { actions, formatActions } = require('./_actions');
17
18
 
18
19
  const definition = {
19
20
  name: 'vibe_feed',
@@ -100,8 +101,27 @@ async function handler(args) {
100
101
  );
101
102
  }
102
103
 
103
- // Sort by timestamp (newest first)
104
- entries.sort((a, b) => b.timestamp - a.timestamp);
104
+ // Fetch relevant users to personalize feed
105
+ // Content from relevant users gets a time boost (appears more recent)
106
+ const relevantHandles = new Set();
107
+ try {
108
+ const relevancy = await fetchRelevantUsers(myHandle, 'feed', 20);
109
+ if (relevancy && relevancy.matches) {
110
+ relevancy.matches.forEach(m => relevantHandles.add(m.handle.toLowerCase()));
111
+ }
112
+ } catch (e) {
113
+ // Don't fail feed if relevancy fails
114
+ console.log('[feed] relevancy fetch error:', e.message);
115
+ }
116
+
117
+ // Sort by timestamp with relevancy boost
118
+ // Relevant users get a 6-hour boost (their content appears "more recent")
119
+ const RELEVANCY_BOOST_MS = 6 * 60 * 60 * 1000; // 6 hours in ms
120
+ entries.sort((a, b) => {
121
+ const aBoost = relevantHandles.has(a.author.toLowerCase()) ? RELEVANCY_BOOST_MS : 0;
122
+ const bBoost = relevantHandles.has(b.author.toLowerCase()) ? RELEVANCY_BOOST_MS : 0;
123
+ return (b.timestamp + bBoost) - (a.timestamp + aBoost);
124
+ });
105
125
 
106
126
  // Limit
107
127
  entries = entries.slice(0, limit);
@@ -186,7 +206,18 @@ async function handler(args) {
186
206
  display += ` _${timeAgo}_\n\n`;
187
207
  });
188
208
 
189
- return { display };
209
+ // Build response with tip actions if we have ships
210
+ const response = { display };
211
+
212
+ // Find the most recent ship to offer tip action for
213
+ const recentShip = entries.find(e => e.category === 'shipped');
214
+ if (recentShip && recentShip.author.toLowerCase() !== myHandle.toLowerCase()) {
215
+ response.actions = formatActions(actions.afterShipView(recentShip));
216
+ response.hint = 'tip_ship_available';
217
+ response.ship_author = recentShip.author;
218
+ }
219
+
220
+ return response;
190
221
 
191
222
  } catch (error) {
192
223
  return { display: `⚠️ Failed to load feed: ${error.message}` };
@@ -0,0 +1,224 @@
1
+ /**
2
+ * vibe follow — Follow/unfollow creators
3
+ *
4
+ * Commands:
5
+ * - follow @handle → Follow a creator
6
+ * - unfollow @handle → Unfollow a creator
7
+ * - following → See who you follow (+ their live status)
8
+ * - followers → See who follows you
9
+ */
10
+
11
+ const config = require('../config');
12
+ const { requireInit } = require('./_shared');
13
+
14
+ const definition = {
15
+ name: 'vibe_follow',
16
+ description: 'Follow creators to get notified when they go live. Unfollow, see who you follow, see your followers.',
17
+ inputSchema: {
18
+ type: 'object',
19
+ properties: {
20
+ action: {
21
+ type: 'string',
22
+ enum: ['follow', 'unfollow', 'following', 'followers'],
23
+ description: 'Action: follow, unfollow, following, or followers'
24
+ },
25
+ handle: {
26
+ type: 'string',
27
+ description: 'Handle to follow/unfollow (e.g., @creator)'
28
+ }
29
+ }
30
+ }
31
+ };
32
+
33
+ async function handler(args) {
34
+ const initCheck = requireInit();
35
+ if (initCheck) return initCheck;
36
+
37
+ const myHandle = config.getHandle();
38
+ const apiUrl = config.getApiUrl();
39
+ const action = args.action || 'following';
40
+ const targetHandle = args.handle?.replace('@', '').toLowerCase();
41
+
42
+ try {
43
+ // FOLLOW
44
+ if (action === 'follow') {
45
+ if (!targetHandle) {
46
+ return { display: '⚠️ Specify who to follow: `vibe follow @handle`' };
47
+ }
48
+
49
+ const response = await fetch(`${apiUrl}/api/follow`, {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/json' },
52
+ body: JSON.stringify({
53
+ follower: myHandle,
54
+ following: targetHandle
55
+ })
56
+ });
57
+
58
+ const data = await response.json();
59
+
60
+ if (!data.success) {
61
+ return { display: `⚠️ ${data.error}` };
62
+ }
63
+
64
+ if (data.action === 'followed') {
65
+ let display = `✅ Now following **@${targetHandle}**\n\n`;
66
+ display += `You'll be notified when they go live.\n\n`;
67
+ display += `_You follow ${data.counts.youAreFollowing} creators_`;
68
+ return { display };
69
+ } else {
70
+ return { display: `Already following @${targetHandle}` };
71
+ }
72
+ }
73
+
74
+ // UNFOLLOW
75
+ if (action === 'unfollow') {
76
+ if (!targetHandle) {
77
+ return { display: '⚠️ Specify who to unfollow: `vibe unfollow @handle`' };
78
+ }
79
+
80
+ const response = await fetch(`${apiUrl}/api/follow`, {
81
+ method: 'DELETE',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ body: JSON.stringify({
84
+ follower: myHandle,
85
+ following: targetHandle
86
+ })
87
+ });
88
+
89
+ const data = await response.json();
90
+
91
+ if (!data.success) {
92
+ return { display: `⚠️ ${data.error}` };
93
+ }
94
+
95
+ let display = `✅ Unfollowed **@${targetHandle}**\n\n`;
96
+ display += `_You follow ${data.counts.youAreFollowing} creators_`;
97
+ return { display };
98
+ }
99
+
100
+ // FOLLOWING (who I follow)
101
+ if (action === 'following') {
102
+ const response = await fetch(`${apiUrl}/api/following?handle=${myHandle}`);
103
+ const data = await response.json();
104
+
105
+ if (!data.success) {
106
+ return { display: `⚠️ ${data.error}` };
107
+ }
108
+
109
+ if (data.following.length === 0) {
110
+ let display = `## Following\n\n`;
111
+ display += `_You're not following anyone yet._\n\n`;
112
+ display += `**Discover creators:**\n`;
113
+ display += `- \`vibe discover\` — Find recommended creators\n`;
114
+ display += `- \`vibe follow @handle\` — Follow someone`;
115
+ return { display };
116
+ }
117
+
118
+ let display = `## Following (${data.count})\n\n`;
119
+
120
+ // Group by status
121
+ const live = data.following.filter(f => f.isLive);
122
+ const scheduled = data.following.filter(f => !f.isLive && f.nextScheduled);
123
+ const other = data.following.filter(f => !f.isLive && !f.nextScheduled);
124
+
125
+ if (live.length > 0) {
126
+ display += `**🔴 Live Now**\n`;
127
+ for (const f of live) {
128
+ display += `• **@${f.handle}** — [Watch](https://slashvibe.dev/watch/${f.roomId})\n`;
129
+ }
130
+ display += `\n`;
131
+ }
132
+
133
+ if (scheduled.length > 0) {
134
+ display += `**📅 Upcoming**\n`;
135
+ for (const f of scheduled) {
136
+ const when = formatScheduledTime(f.nextScheduled.scheduledFor);
137
+ display += `• **@${f.handle}** — ${f.nextScheduled.title} (${when})\n`;
138
+ }
139
+ display += `\n`;
140
+ }
141
+
142
+ if (other.length > 0) {
143
+ display += `**Following**\n`;
144
+ for (const f of other) {
145
+ const followers = f.followerCount > 0 ? ` (${f.followerCount} followers)` : '';
146
+ display += `• @${f.handle}${followers}\n`;
147
+ }
148
+ }
149
+
150
+ return { display };
151
+ }
152
+
153
+ // FOLLOWERS (who follows me)
154
+ if (action === 'followers') {
155
+ const response = await fetch(`${apiUrl}/api/followers?handle=${myHandle}`);
156
+ const data = await response.json();
157
+
158
+ if (!data.success) {
159
+ return { display: `⚠️ ${data.error}` };
160
+ }
161
+
162
+ if (data.followers.length === 0) {
163
+ let display = `## Followers\n\n`;
164
+ display += `_No followers yet._\n\n`;
165
+ display += `**Build your audience:**\n`;
166
+ display += `- Go live: \`vibe broadcast start\`\n`;
167
+ display += `- Share what you're building\n`;
168
+ display += `- Engage with other creators`;
169
+ return { display };
170
+ }
171
+
172
+ let display = `## Followers (${data.count})\n\n`;
173
+
174
+ // Show active vs inactive
175
+ const active = data.followers.filter(f => f.isActive);
176
+ const inactive = data.followers.filter(f => !f.isActive);
177
+
178
+ if (active.length > 0) {
179
+ display += `**🟢 Active Now**\n`;
180
+ for (const f of active) {
181
+ const building = f.building ? ` — ${f.building}` : '';
182
+ display += `• @${f.handle}${building}\n`;
183
+ }
184
+ display += `\n`;
185
+ }
186
+
187
+ if (inactive.length > 0) {
188
+ display += `**Followers**\n`;
189
+ for (const f of inactive.slice(0, 20)) {
190
+ display += `• @${f.handle}\n`;
191
+ }
192
+ if (inactive.length > 20) {
193
+ display += `_...and ${inactive.length - 20} more_\n`;
194
+ }
195
+ }
196
+
197
+ return { display };
198
+ }
199
+
200
+ return { display: 'Unknown action. Use: follow, unfollow, following, followers' };
201
+
202
+ } catch (error) {
203
+ return { display: `## Error\n\n${error.message}` };
204
+ }
205
+ }
206
+
207
+ function formatScheduledTime(isoString) {
208
+ const date = new Date(isoString);
209
+ const now = new Date();
210
+ const diffMs = date - now;
211
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
212
+ const diffDays = Math.floor(diffHours / 24);
213
+
214
+ if (diffDays === 0) {
215
+ if (diffHours <= 1) return 'in < 1 hour';
216
+ return `in ${diffHours} hours`;
217
+ } else if (diffDays === 1) {
218
+ return 'tomorrow';
219
+ } else {
220
+ return `in ${diffDays} days`;
221
+ }
222
+ }
223
+
224
+ module.exports = { definition, handler };
@@ -0,0 +1,207 @@
1
+ /**
2
+ * vibe friends — Find your GitHub friends on /vibe
3
+ *
4
+ * Commands:
5
+ * - vibe friends → Find GitHub connections on vibe
6
+ * - vibe friends follow → Follow all matched friends
7
+ * - vibe friends follow @user → Follow specific friend
8
+ *
9
+ * This is the Plaxo move — leverage existing social graphs
10
+ * to bootstrap the /vibe network.
11
+ */
12
+
13
+ const config = require('../config');
14
+ const { requireInit } = require('./_shared');
15
+
16
+ const definition = {
17
+ name: 'vibe_friends',
18
+ description: 'Find your GitHub friends on /vibe and follow them. "5 of your GitHub friends are here — follow them?"',
19
+ inputSchema: {
20
+ type: 'object',
21
+ properties: {
22
+ action: {
23
+ type: 'string',
24
+ enum: ['find', 'follow'],
25
+ description: 'Action: find (list GitHub friends on vibe) or follow (follow them)'
26
+ },
27
+ handle: {
28
+ type: 'string',
29
+ description: 'Specific handle to follow (optional, defaults to follow all)'
30
+ }
31
+ }
32
+ }
33
+ };
34
+
35
+ async function handler(args) {
36
+ const initCheck = requireInit();
37
+ if (initCheck) return initCheck;
38
+
39
+ const myHandle = config.getHandle();
40
+ const apiUrl = config.getApiUrl();
41
+ const action = args.action || 'find';
42
+
43
+ try {
44
+ // FIND — Discover GitHub friends on vibe
45
+ if (action === 'find') {
46
+ const response = await fetch(`${apiUrl}/api/github/friends?handle=${myHandle}`);
47
+ const data = await response.json();
48
+
49
+ if (!data.success) {
50
+ if (data.error?.includes('No GitHub connection')) {
51
+ return {
52
+ display: `## Connect Your GitHub
53
+
54
+ To find your GitHub friends on /vibe, you need to connect your GitHub account.
55
+
56
+ **Run:**
57
+ \`vibe init\`
58
+
59
+ This will authenticate with GitHub and unlock:
60
+ • Find friends who are already on /vibe
61
+ • One-click follow all
62
+ • Your GitHub username as your verified handle`
63
+ };
64
+ }
65
+ return { display: `⚠️ ${data.error}` };
66
+ }
67
+
68
+ const { github, friends, stats, cta } = data;
69
+
70
+ // Track onboarding completion (searched for GitHub friends)
71
+ try {
72
+ await fetch(`${apiUrl}/api/onboarding/checklist`, {
73
+ method: 'POST',
74
+ headers: { 'Content-Type': 'application/json' },
75
+ body: JSON.stringify({
76
+ handle: myHandle,
77
+ taskId: 'find_github_friends',
78
+ metadata: { friendsFound: friends.length }
79
+ })
80
+ });
81
+ } catch (e) {
82
+ // Non-critical, don't fail
83
+ }
84
+
85
+ if (friends.length === 0) {
86
+ let display = `## GitHub Friends on /vibe\n\n`;
87
+ display += `Connected as: **${github.login}** (${github.following} following, ${github.followers} followers)\n\n`;
88
+ display += `_None of your GitHub connections are on /vibe yet._\n\n`;
89
+ display += `**Be the connector!**\n`;
90
+ display += `Send your friends an invite: \`vibe invite\``;
91
+ return { display };
92
+ }
93
+
94
+ let display = `## 🎉 GitHub Friends on /vibe!\n\n`;
95
+ display += `Connected as: **${github.login}**\n`;
96
+
97
+ if (cta) {
98
+ display += `\n**${cta}**\n\n`;
99
+ }
100
+
101
+ display += `---\n\n`;
102
+
103
+ // Group by status
104
+ const toFollow = friends.filter(f => !f.alreadyFollowing);
105
+ const alreadyFollowing = friends.filter(f => f.alreadyFollowing);
106
+
107
+ if (toFollow.length > 0) {
108
+ display += `### Not Yet Following\n\n`;
109
+ for (const friend of toFollow.slice(0, 10)) {
110
+ const mutual = friend.isMutualOnGithub ? ' 🤝' : '';
111
+ const genesis = friend.genesis ? ' ✨' : '';
112
+ display += `**@${friend.vibeHandle}**${genesis}${mutual}`;
113
+ if (friend.oneLiner) {
114
+ display += ` — ${friend.oneLiner}`;
115
+ }
116
+ display += `\n`;
117
+ }
118
+ if (toFollow.length > 10) {
119
+ display += `_...and ${toFollow.length - 10} more_\n`;
120
+ }
121
+ display += `\n`;
122
+ }
123
+
124
+ if (alreadyFollowing.length > 0) {
125
+ display += `### Already Following ✓\n\n`;
126
+ for (const friend of alreadyFollowing.slice(0, 5)) {
127
+ display += `@${friend.vibeHandle}`;
128
+ if (friend.oneLiner) {
129
+ display += ` — ${friend.oneLiner}`;
130
+ }
131
+ display += `\n`;
132
+ }
133
+ if (alreadyFollowing.length > 5) {
134
+ display += `_...and ${alreadyFollowing.length - 5} more_\n`;
135
+ }
136
+ display += `\n`;
137
+ }
138
+
139
+ display += `---\n\n`;
140
+ display += `**Stats:** ${stats.total_matches} friends found · ${stats.mutuals} GitHub mutuals · ${stats.already_following} already following\n\n`;
141
+
142
+ if (toFollow.length > 0) {
143
+ display += `**Follow all ${toFollow.length} at once:**\n`;
144
+ display += `\`vibe friends follow\``;
145
+ }
146
+
147
+ return { display };
148
+ }
149
+
150
+ // FOLLOW — Follow GitHub friends
151
+ if (action === 'follow') {
152
+ // If specific handle provided, follow just that one
153
+ const handles = args.handle ? [args.handle.replace('@', '')] : null;
154
+
155
+ const response = await fetch(`${apiUrl}/api/github/friends`, {
156
+ method: 'POST',
157
+ headers: { 'Content-Type': 'application/json' },
158
+ body: JSON.stringify({
159
+ handle: myHandle,
160
+ handles
161
+ })
162
+ });
163
+
164
+ const data = await response.json();
165
+
166
+ if (!data.success) {
167
+ return { display: `⚠️ ${data.error}` };
168
+ }
169
+
170
+ if (data.followed.length === 0) {
171
+ return {
172
+ display: `## All Caught Up! ✓
173
+
174
+ You're already following all your GitHub friends on /vibe.
175
+
176
+ **Grow the network:**
177
+ • \`vibe invite\` — Generate invite link for friends
178
+ • \`vibe discover\` — Find new people to follow`
179
+ };
180
+ }
181
+
182
+ let display = `## 🎉 Followed ${data.count} GitHub Friend${data.count === 1 ? '' : 's'}!\n\n`;
183
+
184
+ for (const handle of data.followed.slice(0, 10)) {
185
+ display += `✓ @${handle}\n`;
186
+ }
187
+
188
+ if (data.followed.length > 10) {
189
+ display += `...and ${data.followed.length - 10} more\n`;
190
+ }
191
+
192
+ display += `\n---\n\n`;
193
+ display += `**${data.message}**\n\n`;
194
+ display += `They'll see you in their followers. Say hi!\n`;
195
+ display += `\`vibe dm @${data.followed[0]} "Hey! Saw you're on /vibe too 👋"\``;
196
+
197
+ return { display };
198
+ }
199
+
200
+ return { display: 'Unknown action. Use: find, follow' };
201
+
202
+ } catch (error) {
203
+ return { display: `## Error\n\n${error.message}` };
204
+ }
205
+ }
206
+
207
+ module.exports = { definition, handler };