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/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)