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
package/tools/inbox.js CHANGED
@@ -5,47 +5,118 @@
5
5
  const config = require('../config');
6
6
  const store = require('../store');
7
7
  const notify = require('../notify');
8
- const analytics = require('../analytics');
9
- const { requireInit, header, emptyState, formatTimeAgo, truncate, divider } = require('./_shared');
8
+ const patterns = require('../intelligence/patterns');
9
+ const { formatPayload } = require('../protocol');
10
+ const { requireInit, header, emptyState, formatTimeAgo, truncate, divider, fetchRelevantUsers } = require('./_shared');
10
11
  const { actions, formatActions } = require('./_actions');
11
12
 
12
- // Helper: Fetch recent ships for social proof (FOMO)
13
- async function getRecentShips(limit = 2) {
14
- try {
15
- const apiUrl = config.getApiUrl();
16
- const response = await fetch(`${apiUrl}/api/board?limit=${limit}&category=shipped`);
17
- const data = await response.json();
18
- return (data.entries || []).map(e => ({
19
- author: e.author,
20
- content: e.content?.slice(0, 50)
21
- }));
22
- } catch (e) {
23
- return [];
24
- }
13
+ // Truncate message for preview (first 100 chars, clean break at word)
14
+ function summarizeMessage(text, maxLen = 100) {
15
+ if (!text || text.length <= maxLen) return text;
16
+ const truncated = text.slice(0, maxLen);
17
+ const lastSpace = truncated.lastIndexOf(' ');
18
+ return (lastSpace > maxLen * 0.7 ? truncated.slice(0, lastSpace) : truncated) + '...';
25
19
  }
26
20
 
27
- // Helper: Get next incomplete onboarding task
28
- async function getNextOnboardingTask(handle) {
21
+ // Get activity heat icon for a user (borrowed from who.js logic)
22
+ function getStatusIcon(user) {
23
+ if (!user) return 'ā—';
24
+
25
+ // Check mood/status
26
+ if (user.mood === 'šŸ”„' || user.mood === 'šŸš€' || user.builderMode === 'shipping') return 'šŸ”„';
27
+ if (user.mood === '🧠' || user.builderMode === 'deep-focus') return '🧠';
28
+ if (user.mood === 'šŸ›') return 'šŸ›';
29
+ if (user.mood === 'šŸŒ™') return 'šŸŒ™';
30
+
31
+ // Check recency
32
+ const lastSeenMs = user.lastSeen || Date.now();
33
+ const minutesAgo = (Date.now() - lastSeenMs) / 60000;
34
+ if (minutesAgo < 5) return '⚔';
35
+ if (minutesAgo < 30) return 'ā—';
36
+ return 'ā—‹';
37
+ }
38
+
39
+ // Get status label for a user
40
+ function getStatusLabel(user) {
41
+ if (!user) return null;
42
+
43
+ if (user.mood === 'šŸ”„' || user.mood === 'šŸš€' || user.builderMode === 'shipping') return 'shipping';
44
+ if (user.mood === '🧠' || user.builderMode === 'deep-focus') return 'deep focus';
45
+ if (user.mood === 'šŸ›') return 'debugging';
46
+ if (user.mood === 'šŸŒ™') return 'late night';
47
+ if (user.mood === 'šŸ’­') return 'thinking';
48
+
49
+ const lastSeenMs = user.lastSeen || Date.now();
50
+ const minutesAgo = (Date.now() - lastSeenMs) / 60000;
51
+ if (minutesAgo < 5) return 'active';
52
+ if (minutesAgo < 30) return 'online';
53
+ return 'away';
54
+ }
55
+
56
+ // Build recommended connections display for empty/caught-up inbox
57
+ async function buildRecommendationsDisplay(myHandle) {
58
+ // Fetch recommendations from relevancy API
59
+ const relevancy = await fetchRelevantUsers(myHandle, 'dm_suggest', 5);
60
+
61
+ if (!relevancy || !relevancy.matches || relevancy.matches.length === 0) {
62
+ return null; // Fallback to old behavior
63
+ }
64
+
65
+ const matches = relevancy.matches;
66
+
67
+ // Get presence info for status display
68
+ const presenceMap = new Map();
29
69
  try {
30
- const checklist = await store.getChecklistStatus(handle);
31
- if (checklist.success && checklist.tasks) {
32
- const nextTask = checklist.tasks.find(t => !t.done);
33
- if (nextTask) {
34
- // Map task IDs to user-friendly actions
35
- const taskActions = {
36
- 'read_welcome': { shortLabel: 'Read welcome', command: 'check my messages', description: 'See your welcome message' },
37
- 'reply_seth': { shortLabel: 'Reply to @vibe', command: 'message @vibe', description: 'Say hi back!' },
38
- 'message_builder': { shortLabel: 'Message a builder', command: 'discover suggest', description: 'Find someone to connect with' },
39
- 'post_ship': { shortLabel: 'Ship something', command: 'ship what I built', description: 'Share what you\'re building' },
40
- 'leave_feedback': { shortLabel: 'Give feedback', command: 'talk to @echo', description: 'Help improve /vibe' }
41
- };
42
- return taskActions[nextTask.id] || null;
43
- }
44
- }
45
- return null;
70
+ const presence = await store.getPresence();
71
+ const allUsers = [...(presence.active || []), ...(presence.away || [])];
72
+ allUsers.forEach(u => {
73
+ presenceMap.set(u.handle?.toLowerCase(), u);
74
+ });
46
75
  } catch (e) {
47
- return null;
76
+ // Continue without presence info
48
77
  }
78
+
79
+ // Build preview line (stays visible when collapsed)
80
+ const top3Handles = matches.slice(0, 3).map(m => `@${m.handle}`).join(', ');
81
+ let display = `šŸ“­ All caught up! Connect with: ${top3Handles}\n\n`;
82
+
83
+ // Build ranked list with statuses
84
+ display += `---\n`;
85
+
86
+ matches.forEach(match => {
87
+ const presenceUser = presenceMap.get(match.handle?.toLowerCase());
88
+ const icon = getStatusIcon(presenceUser);
89
+ const statusLabel = getStatusLabel(presenceUser);
90
+ const statusText = statusLabel ? ` — ${statusLabel}` : '';
91
+
92
+ display += `${icon} **@${match.handle}**${statusText}\n`;
93
+
94
+ // Show building + first reason
95
+ if (match.building) {
96
+ const reason = match.reasons?.[0] ? ` • ${match.reasons[0]}` : '';
97
+ display += ` "${summarizeMessage(match.building, 50)}"${reason}\n`;
98
+ } else if (match.reasons?.[0]) {
99
+ display += ` ${match.reasons[0]}\n`;
100
+ }
101
+
102
+ display += '\n';
103
+ });
104
+
105
+ // Enrich matches with status info for action descriptions
106
+ const enrichedMatches = matches.map(match => {
107
+ const presenceUser = presenceMap.get(match.handle?.toLowerCase());
108
+ return {
109
+ ...match,
110
+ statusIcon: getStatusIcon(presenceUser),
111
+ statusLabel: getStatusLabel(presenceUser)
112
+ };
113
+ });
114
+
115
+ return {
116
+ display,
117
+ matches: enrichedMatches,
118
+ actions: formatActions(actions.recommendedConnections(enrichedMatches))
119
+ };
49
120
  }
50
121
 
51
122
  const definition = {
@@ -68,49 +139,72 @@ async function handler(args) {
68
139
  notify.checkAll(store);
69
140
 
70
141
  if (!threads || threads.length === 0) {
71
- // Fetch context for retention-optimized actions (parallel for speed)
72
- const [recentShips, onboardingTask] = await Promise.all([
73
- getRecentShips(2),
74
- getNextOnboardingTask(myHandle)
75
- ]);
76
-
77
- // Build social proof line
78
- let socialProof = '';
79
- if (recentShips.length > 0) {
80
- socialProof = `\n šŸ’« @${recentShips[0].author} just shipped`;
142
+ // Try to show personalized recommendations
143
+ try {
144
+ const recommendations = await buildRecommendationsDisplay(myHandle);
145
+ if (recommendations) {
146
+ // Change preview line for empty inbox (no messages yet)
147
+ const display = recommendations.display.replace(
148
+ 'All caught up!',
149
+ 'No messages yet.'
150
+ );
151
+ return {
152
+ display,
153
+ actions: recommendations.actions
154
+ };
155
+ }
156
+ } catch (e) {
157
+ console.log('[inbox] recommendations error:', e.message);
81
158
  }
82
159
 
83
- // Build CTA based on onboarding state
84
- let cta = onboardingTask
85
- ? `→ ${onboardingTask.shortLabel}: "${onboardingTask.command}"`
86
- : 'Say "dm @someone" to start';
87
-
88
- // Track empty inbox state for retention analytics
89
- analytics.trackEmptyInbox('none', {
90
- recentThreads: [],
91
- recentShips,
92
- onboardingTask,
93
- state: 'no_messages'
94
- });
160
+ // Fallback: Check onboarding status to customize CTA
161
+ let cta = 'Say "dm @someone" to start';
162
+ try {
163
+ const checklist = await store.getChecklistStatus(myHandle);
164
+ if (checklist.success && checklist.tasks) {
165
+ const repliedToSeth = checklist.tasks.find(t => t.id === 'reply_seth')?.done;
166
+ if (!repliedToSeth) {
167
+ cta = 'Reply to @vibe to get started!';
168
+ }
169
+ }
170
+ } catch (e) {}
95
171
 
96
172
  return {
97
- display: `── šŸ“­ Inbox ──────────────────────────
98
- No messages yet${socialProof}
99
- ${cta}
100
- ──────────────────────────────────────`,
101
- hint: 'suggest_compose',
102
- actions: formatActions(actions.emptyInbox({
103
- recentThreads: [],
104
- recentShips,
105
- onboardingTask
106
- }))
173
+ display: `šŸ“­ No messages yet. ${cta}`,
174
+ actions: formatActions(actions.recommendedConnections([]))
107
175
  };
108
176
  }
109
177
 
110
- // Sort: unread first, then by most recent
178
+ // Fetch relevant users to prioritize notifications
179
+ // Messages from relevant users appear first within unread threads
180
+ const relevantHandles = new Map(); // handle -> relevancy score (position in matches)
181
+ try {
182
+ const relevancy = await fetchRelevantUsers(myHandle, 'notification', 20);
183
+ if (relevancy && relevancy.matches) {
184
+ relevancy.matches.forEach((m, idx) => {
185
+ // Higher score = more relevant (inverse of position)
186
+ relevantHandles.set(m.handle.toLowerCase(), relevancy.matches.length - idx);
187
+ });
188
+ }
189
+ } catch (e) {
190
+ // Don't fail inbox if relevancy fails
191
+ console.log('[inbox] relevancy fetch error:', e.message);
192
+ }
193
+
194
+ // Sort: unread first, then by relevancy within unread, then by most recent
111
195
  const sorted = threads.sort((a, b) => {
196
+ // Primary: unread vs read
112
197
  if (a.unread > 0 && b.unread === 0) return -1;
113
198
  if (b.unread > 0 && a.unread === 0) return 1;
199
+
200
+ // Secondary (within same read/unread status): relevancy
201
+ const aRelevancy = relevantHandles.get(a.handle.toLowerCase()) || 0;
202
+ const bRelevancy = relevantHandles.get(b.handle.toLowerCase()) || 0;
203
+ if (aRelevancy !== bRelevancy) {
204
+ return bRelevancy - aRelevancy; // Higher relevancy first
205
+ }
206
+
207
+ // Tertiary: timestamp (most recent first)
114
208
  return (b.lastTimestamp || 0) - (a.lastTimestamp || 0);
115
209
  });
116
210
 
@@ -119,51 +213,141 @@ async function handler(args) {
119
213
 
120
214
  // Handle case where all messages are read (no unread)
121
215
  if (totalUnread === 0) {
122
- const recentHandles = sorted.slice(0, 3).map(t => t.handle);
123
- const recentDisplay = recentHandles.map(h => `@${h}`).join(', ');
124
-
125
- // Fetch context for retention-optimized actions (parallel for speed)
126
- const [recentShips, onboardingTask] = await Promise.all([
127
- getRecentShips(2),
128
- getNextOnboardingTask(myHandle)
129
- ]);
130
-
131
- // Build social proof line
132
- let socialProof = '';
133
- if (recentShips.length > 0) {
134
- socialProof = `\n šŸ’« @${recentShips[0].author} just shipped`;
216
+ // Try to show personalized recommendations
217
+ try {
218
+ const recommendations = await buildRecommendationsDisplay(myHandle);
219
+ if (recommendations) {
220
+ return {
221
+ display: recommendations.display,
222
+ actions: recommendations.actions
223
+ };
224
+ }
225
+ } catch (e) {
226
+ console.log('[inbox] recommendations error:', e.message);
135
227
  }
136
228
 
137
- // Track empty inbox state for retention analytics
138
- analytics.trackEmptyInbox('none', {
139
- recentThreads: recentHandles,
140
- recentShips,
141
- onboardingTask,
142
- state: 'all_caught_up'
143
- });
144
-
229
+ // Fallback: show recent threads
230
+ const recentHandles = sorted.slice(0, 3).map(t => `@${t.handle}`).join(', ');
145
231
  return {
146
- display: `── šŸ“­ Inbox ──────────────────────────
147
- All caught up! Recent: ${recentDisplay}${socialProof}
148
- ──────────────────────────────────────`,
149
- hint: 'suggest_compose',
150
- actions: formatActions(actions.emptyInbox({
151
- recentThreads: recentHandles,
152
- recentShips,
153
- onboardingTask
154
- }))
232
+ display: `šŸ“­ All caught up! Recent: ${recentHandles}`,
233
+ actions: formatActions(actions.recommendedConnections([]))
155
234
  };
156
235
  }
157
236
 
158
- // Auto-open single unread message (skip inbox view to reduce friction)
237
+ // Auto-open single unread thread inline (no second tool call needed)
159
238
  if (totalUnread === 1 && unreadSenders.length === 1) {
160
- const singleSender = unreadSenders[0];
161
- return {
162
- hint: 'auto_open_single_thread',
163
- handle: singleSender.handle,
164
- preview: truncate(singleSender.lastMessage || '', 60),
165
- display: `šŸ“¬ Opening thread with @${singleSender.handle}...`
166
- };
239
+ const them = unreadSenders[0].handle;
240
+
241
+ // Fetch full thread and mark as read
242
+ const thread = await store.getThread(myHandle, them);
243
+ await store.markThreadRead(myHandle, them);
244
+
245
+ // Auto-track readWelcomeAt if viewing welcome from @seth
246
+ const isWelcomeThread = them.toLowerCase() === 'seth';
247
+ if (isWelcomeThread) {
248
+ try {
249
+ await store.trackChecklistCompletion(myHandle, 'read_welcome', {
250
+ source: 'inbox_auto_open',
251
+ timestamp: Date.now()
252
+ });
253
+ console.log('[inbox] Auto-tracked readWelcomeAt for', myHandle);
254
+ } catch (e) {
255
+ console.warn('[inbox] Failed to track readWelcomeAt:', e.message);
256
+ }
257
+ }
258
+
259
+ // Log received messages for patterns
260
+ const theirMessages = thread.filter(m => m.from === them);
261
+ if (theirMessages.length > 0) {
262
+ patterns.logMessageReceived(them);
263
+ }
264
+
265
+ // Check if they're typing
266
+ let typingNotice = '';
267
+ try {
268
+ const typingUsers = await store.getTypingUsers(myHandle);
269
+ if (typingUsers.includes(them)) {
270
+ typingNotice = `\n_@${them} is typing..._\n`;
271
+ }
272
+ } catch (e) {}
273
+
274
+ // Build thread display - summary first, newest-first thread
275
+ const latestFromThem = theirMessages.length > 0
276
+ ? theirMessages.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))[0]
277
+ : null;
278
+
279
+ let display = '';
280
+
281
+ if (latestFromThem) {
282
+ const agentBadge = latestFromThem.isAgent ? ' šŸ¤–' : '';
283
+ const time = store.formatTimeAgo(latestFromThem.timestamp);
284
+ const preview = latestFromThem.body
285
+ ? summarizeMessage(latestFromThem.body)
286
+ : (latestFromThem.payload ? '[attachment]' : '');
287
+
288
+ display = `šŸ’¬ @${them}${agentBadge} (${time}): "${preview}"\n\n`;
289
+ } else {
290
+ display = `šŸ’¬ @${them}: _Waiting for reply..._\n\n`;
291
+ }
292
+
293
+ // Thread section - sorted newest first
294
+ display += `---\nšŸ“œ Thread\n\n`;
295
+
296
+ const sortedThread = [...thread].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
297
+
298
+ sortedThread.forEach(m => {
299
+ const isMe = m.from === myHandle;
300
+ const agentBadge = m.isAgent && !isMe ? 'šŸ¤– ' : '';
301
+ const sender = isMe ? 'you' : `@${m.from}`;
302
+ const time = store.formatTimeAgo(m.timestamp);
303
+
304
+ display += `${agentBadge}**${sender}** — _${time}_\n`;
305
+
306
+ if (m.body) {
307
+ display += `${m.body}\n`;
308
+ }
309
+
310
+ if (m.payload) {
311
+ display += `${formatPayload(m.payload)}\n`;
312
+ }
313
+
314
+ display += '\n';
315
+ });
316
+
317
+ if (typingNotice) {
318
+ display += typingNotice + '\n';
319
+ }
320
+
321
+ display += `---\nJust type your reply to send it`;
322
+
323
+ // For @seth welcome thread, fetch recommended builders and add actions
324
+ if (isWelcomeThread) {
325
+ try {
326
+ const onboardingData = await store.getOnboardingData(myHandle);
327
+ if (onboardingData.success && onboardingData.recommendedUsers?.length > 0) {
328
+ // Build action options to message recommended builders
329
+ const recommendedActions = onboardingData.recommendedUsers.slice(0, 3).map(user => {
330
+ const description = user.workingOn
331
+ ? `Building: "${truncate(user.workingOn, 40)}"`
332
+ : 'Recommended for you';
333
+ return {
334
+ handle: user.handle,
335
+ building: user.workingOn,
336
+ reasons: ['Matched during your welcome']
337
+ };
338
+ });
339
+
340
+ return {
341
+ display,
342
+ actions: formatActions(actions.recommendedConnections(recommendedActions))
343
+ };
344
+ }
345
+ } catch (e) {
346
+ console.warn('[inbox] Failed to fetch recommended builders:', e.message);
347
+ }
348
+ }
349
+
350
+ return { display };
167
351
  }
168
352
 
169
353
  // Build compact display (3 lines above the fold)
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 {
@@ -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
 
@@ -700,7 +770,16 @@ Local config saved. Heartbeats will use username fallback.`
700
770
  discord.postJoin(h, one_liner);
701
771
 
702
772
  // Send personalized welcome from @vibe (non-blocking)
703
- sendPersonalizedWelcome(h, one_liner);
773
+ // Send personalized welcome and wait for it
774
+ let welcomeResult = null;
775
+ try {
776
+ welcomeResult = await Promise.race([
777
+ sendPersonalizedWelcome(h, one_liner),
778
+ new Promise(resolve => setTimeout(() => resolve(null), 2500))
779
+ ]);
780
+ } catch (e) {
781
+ console.error('[vibe_init] Welcome message failed:', e.message);
782
+ }
704
783
 
705
784
  // Check for unread messages
706
785
  let unreadNotice = '';
@@ -718,14 +797,14 @@ Local config saved. Heartbeats will use username fallback.`
718
797
  **@${h}**
719
798
  _${one_liner}_${unreadNotice}${keypairNote}
720
799
 
721
- šŸ“Ø **Check your messages** — @vibe sent you a personalized welcome!
800
+ šŸ“Ø **Check your messages** — @seth sent you a personalized welcome!
722
801
 
723
802
  āš ļø **Using local keys** — consider upgrading to GitHub auth:
724
803
  \`vibe init\` — Sign in with GitHub for verified identity
725
804
 
726
805
  ### Onboarding Checklist
727
- [ ] Read your welcome message from @vibe
728
- [ ] Reply to @vibe
806
+ [ ] Read your welcome message from @seth
807
+ [ ] Reply to @seth
729
808
  [ ] Message one recommended builder
730
809
  [ ] Post your first ship
731
810
  [ ] Leave some feedback