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
@@ -113,49 +113,142 @@ function calculateSkillComplementarity(tags1, tags2) {
113
113
  return { score, pairs: pairs.slice(0, 2) };
114
114
  }
115
115
 
116
+ // Helper: Check for word boundary match (avoids "said" matching "ai", "start" matching "art")
117
+ function hasWord(text, word) {
118
+ // Use word boundary regex for short keywords prone to false positives
119
+ const regex = new RegExp(`\\b${word}\\b`, 'i');
120
+ return regex.test(text);
121
+ }
122
+
116
123
  // Suggest interests based on what user is building
117
124
  function suggestInterestsFromBuilding(buildingDescription) {
118
125
  if (!buildingDescription) return [];
119
-
126
+
120
127
  const building = buildingDescription.toLowerCase();
121
128
  const suggestions = [];
122
-
123
- // AI/ML projects
124
- if (building.includes('ai') || building.includes('machine learning') || building.includes('llm')) {
125
- suggestions.push('ai', 'machine learning', 'deep learning');
129
+
130
+ // Agent/Automation projects (critical for /vibe users!)
131
+ if (building.includes('agent') || building.includes('automation') ||
132
+ hasWord(building, 'mcp') || building.includes('claude') ||
133
+ building.includes('orchestrat') || building.includes('workflow')) {
134
+ suggestions.push('agents', 'ai', 'automation');
126
135
  }
127
-
136
+
137
+ // AI/ML projects (use word boundary for short keywords)
138
+ if (hasWord(building, 'ai') || building.includes('machine learning') ||
139
+ hasWord(building, 'llm') || hasWord(building, 'gpt') ||
140
+ building.includes('neural') || hasWord(building, 'ml')) {
141
+ suggestions.push('ai', 'machine learning');
142
+ }
143
+
128
144
  // Web projects
129
- if (building.includes('web') || building.includes('website') || building.includes('app')) {
130
- suggestions.push('web development', 'frontend', 'fullstack');
145
+ if (building.includes('web') || building.includes('website') ||
146
+ building.includes('frontend') || building.includes('react') ||
147
+ building.includes('next') || building.includes('vue')) {
148
+ suggestions.push('web development', 'frontend');
131
149
  }
132
-
133
- // Mobile projects
134
- if (building.includes('mobile') || building.includes('ios') || building.includes('android')) {
135
- suggestions.push('mobile development', 'app development');
150
+
151
+ // Backend/API projects
152
+ if (building.includes('api') || building.includes('backend') ||
153
+ building.includes('server') || building.includes('database')) {
154
+ suggestions.push('backend', 'api');
136
155
  }
137
-
156
+
157
+ // Mobile projects
158
+ if (building.includes('mobile') || building.includes('ios') ||
159
+ building.includes('android') || building.includes('flutter') ||
160
+ building.includes('react native')) {
161
+ suggestions.push('mobile development');
162
+ }
163
+
164
+ // Platform/SaaS projects (NEW)
165
+ if (building.includes('platform') || building.includes('saas') ||
166
+ building.includes('.app') || building.includes('product')) {
167
+ suggestions.push('platform', 'saas', 'product');
168
+ }
169
+
138
170
  // Startup/business projects
139
- if (building.includes('startup') || building.includes('business') || building.includes('saas')) {
140
- suggestions.push('startups', 'entrepreneur', 'product management');
171
+ if (building.includes('startup') || building.includes('business') ||
172
+ building.includes('founder') || building.includes('company')) {
173
+ suggestions.push('startups', 'entrepreneur');
141
174
  }
142
-
175
+
143
176
  // Data projects
144
- if (building.includes('data') || building.includes('analytics') || building.includes('dashboard')) {
145
- suggestions.push('data science', 'analytics', 'visualization');
177
+ if (building.includes('data') || building.includes('analytics') ||
178
+ building.includes('dashboard') || building.includes('visualization')) {
179
+ suggestions.push('data science', 'analytics');
146
180
  }
147
-
181
+
182
+ // Developer tools (NEW)
183
+ if (building.includes('tool') || building.includes('cli') ||
184
+ building.includes('developer') || building.includes('devtool') ||
185
+ building.includes('editor') || building.includes('ide')) {
186
+ suggestions.push('developer tools', 'infrastructure');
187
+ }
188
+
189
+ // Creative/Generative projects (NEW)
190
+ if (hasWord(building, 'art') || building.includes('generative') ||
191
+ building.includes('creative') || building.includes('music') ||
192
+ building.includes('visual') || building.includes('design')) {
193
+ suggestions.push('generative art', 'creative');
194
+ }
195
+
148
196
  // Content/media projects
149
- if (building.includes('content') || building.includes('blog') || building.includes('media')) {
150
- suggestions.push('content creation', 'writing', 'marketing');
197
+ if (building.includes('content') || building.includes('blog') ||
198
+ building.includes('media') || building.includes('writing')) {
199
+ suggestions.push('content creation', 'writing');
151
200
  }
152
-
201
+
153
202
  // Gaming projects
154
- if (building.includes('game') || building.includes('unity') || building.includes('gaming')) {
155
- suggestions.push('game development', 'gaming', 'interactive media');
203
+ if (building.includes('game') || building.includes('unity') ||
204
+ building.includes('gaming') || building.includes('interactive')) {
205
+ suggestions.push('game development', 'gaming');
156
206
  }
157
-
158
- return [...new Set(suggestions)].slice(0, 4);
207
+
208
+ // Infrastructure/DevOps (NEW)
209
+ if (building.includes('infrastructure') || building.includes('devops') ||
210
+ building.includes('cloud') || building.includes('deploy') ||
211
+ building.includes('kubernetes') || building.includes('docker')) {
212
+ suggestions.push('devops', 'infrastructure');
213
+ }
214
+
215
+ // Social/Community (NEW)
216
+ if (building.includes('social') || building.includes('community') ||
217
+ building.includes('network') || building.includes('chat') ||
218
+ building.includes('messaging')) {
219
+ suggestions.push('social', 'community');
220
+ }
221
+
222
+ // Crypto/Web3 (NEW)
223
+ if (building.includes('crypto') || building.includes('web3') ||
224
+ building.includes('blockchain') || building.includes('defi') ||
225
+ building.includes('nft') || building.includes('token')) {
226
+ suggestions.push('crypto', 'web3');
227
+ }
228
+
229
+ // Education (NEW)
230
+ if (building.includes('education') || building.includes('learning') ||
231
+ building.includes('course') || building.includes('tutorial') ||
232
+ building.includes('teaching')) {
233
+ suggestions.push('education', 'learning');
234
+ }
235
+
236
+ // Fintech (NEW)
237
+ if (building.includes('fintech') || building.includes('payment') ||
238
+ building.includes('banking') || building.includes('finance') ||
239
+ building.includes('trading')) {
240
+ suggestions.push('fintech', 'finance');
241
+ }
242
+
243
+ // Health/Biotech (NEW)
244
+ if (building.includes('health') || building.includes('biotech') ||
245
+ building.includes('medical') || building.includes('fitness') ||
246
+ building.includes('wellness')) {
247
+ suggestions.push('healthtech', 'biotech');
248
+ }
249
+
250
+ // Return unique interests, limited to 5
251
+ return [...new Set(suggestions)].slice(0, 5);
159
252
  }
160
253
 
161
254
  // Suggest tags/skills based on interests and building
@@ -7,10 +7,71 @@
7
7
  * - Time formatting
8
8
  * - Display formatting
9
9
  * - Error handling
10
+ * - Relevancy API fetching
10
11
  */
11
12
 
12
13
  const config = require('../../config');
13
14
 
15
+ // ============ RELEVANCY API ============
16
+
17
+ const RELEVANCY_API_TIMEOUT = 5000; // 5 seconds
18
+
19
+ /**
20
+ * Fetch relevant users from the Universal Relevancy API
21
+ *
22
+ * @param {string} handle - The requesting user's handle
23
+ * @param {string} context - The context: 'discovery', 'dm_suggest', 'feed', 'notification'
24
+ * @param {number} limit - Max results (default 5)
25
+ * @returns {Promise<{ matches: object[], tier: string, fromApi: boolean } | null>}
26
+ */
27
+ async function fetchRelevantUsers(handle, context = 'discovery', limit = 5) {
28
+ try {
29
+ const apiUrl = config.getApiUrl();
30
+ const controller = new AbortController();
31
+ const timeout = setTimeout(() => controller.abort(), RELEVANCY_API_TIMEOUT);
32
+
33
+ const url = `${apiUrl}/api/relevancy?handle=${encodeURIComponent(handle)}&context=${context}&limit=${limit}`;
34
+
35
+ const response = await fetch(url, {
36
+ method: 'GET',
37
+ headers: {
38
+ 'Accept': 'application/json',
39
+ 'User-Agent': 'vibe-mcp-client'
40
+ },
41
+ signal: controller.signal
42
+ });
43
+
44
+ clearTimeout(timeout);
45
+
46
+ if (!response.ok) {
47
+ console.log(`[relevancy] API returned ${response.status}`);
48
+ return null;
49
+ }
50
+
51
+ const data = await response.json();
52
+
53
+ if (!data.success || !data.matches) {
54
+ console.log('[relevancy] API returned unsuccessful response');
55
+ return null;
56
+ }
57
+
58
+ return {
59
+ matches: data.matches,
60
+ tier: data.tier,
61
+ socialStats: data.socialStats,
62
+ fromApi: true
63
+ };
64
+ } catch (e) {
65
+ // Log but don't fail - caller should handle null gracefully
66
+ if (e.name === 'AbortError') {
67
+ console.log('[relevancy] API timeout');
68
+ } else {
69
+ console.log('[relevancy] API error:', e.message);
70
+ }
71
+ return null;
72
+ }
73
+ }
74
+
14
75
  // ============ INIT CHECK ============
15
76
 
16
77
  /**
@@ -231,6 +292,9 @@ function validateRequired(args, required) {
231
292
  }
232
293
 
233
294
  module.exports = {
295
+ // Relevancy
296
+ fetchRelevantUsers,
297
+
234
298
  // Init
235
299
  requireInit,
236
300
  withInit,
@@ -0,0 +1,234 @@
1
+ /**
2
+ * _shared.js — Common utilities for /vibe MCP tools
3
+ *
4
+ * This module provides shared helpers used across 90+ tools.
5
+ * Import only what you need to keep tool files clean.
6
+ */
7
+
8
+ const config = require('../config');
9
+ const store = require('../store');
10
+
11
+ // ─────────────────────────────────────────────────────────────
12
+ // Authentication & Initialization
13
+ // ─────────────────────────────────────────────────────────────
14
+
15
+ /**
16
+ * Check if user has initialized vibe. Returns error response if not.
17
+ * Use at the top of handler functions:
18
+ * const initCheck = requireInit();
19
+ * if (initCheck) return initCheck;
20
+ *
21
+ * @returns {Object|null} Error response object, or null if initialized
22
+ */
23
+ function requireInit() {
24
+ if (!config.isInitialized()) {
25
+ return {
26
+ display: '⚠️ Not initialized. Run `vibe init` first.',
27
+ error: 'not_initialized'
28
+ };
29
+ }
30
+ return null;
31
+ }
32
+
33
+ // ─────────────────────────────────────────────────────────────
34
+ // Handle Normalization
35
+ // ─────────────────────────────────────────────────────────────
36
+
37
+ /**
38
+ * Normalize a handle string (remove @, lowercase, trim)
39
+ * @param {string} handle - Raw handle input
40
+ * @returns {string} Normalized handle
41
+ */
42
+ function normalizeHandle(handle) {
43
+ if (!handle) return '';
44
+ return handle.toString().replace(/^@/, '').toLowerCase().trim();
45
+ }
46
+
47
+ /**
48
+ * Format handle for display (with @)
49
+ * @param {string} handle - Handle to format
50
+ * @returns {string} Display-formatted handle
51
+ */
52
+ function displayHandle(handle) {
53
+ if (!handle) return '@unknown';
54
+ const normalized = normalizeHandle(handle);
55
+ return `@${normalized}`;
56
+ }
57
+
58
+ // ─────────────────────────────────────────────────────────────
59
+ // Time Formatting
60
+ // ─────────────────────────────────────────────────────────────
61
+
62
+ /**
63
+ * Format timestamp as relative time (e.g., "5m ago", "2h ago")
64
+ * @param {number} timestamp - Unix timestamp in milliseconds
65
+ * @returns {string} Human-readable relative time
66
+ */
67
+ function formatTimeAgo(timestamp) {
68
+ if (timestamp === undefined || timestamp === null || isNaN(timestamp)) return 'unknown';
69
+
70
+ const now = Date.now();
71
+ const seconds = Math.floor((now - timestamp) / 1000);
72
+
73
+ if (seconds < 0 || isNaN(seconds)) return 'unknown';
74
+ if (seconds < 60) return 'just now';
75
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
76
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
77
+ return `${Math.floor(seconds / 86400)}d ago`;
78
+ }
79
+
80
+ /**
81
+ * Format duration in human-readable form
82
+ * @param {number} ms - Duration in milliseconds
83
+ * @returns {string} Human-readable duration (e.g., "5 minutes", "2 hours")
84
+ */
85
+ function formatDuration(ms) {
86
+ if (!ms || ms < 0) return 'unknown';
87
+
88
+ const seconds = Math.floor(ms / 1000);
89
+ if (seconds < 60) return `${seconds} second${seconds !== 1 ? 's' : ''}`;
90
+
91
+ const minutes = Math.floor(seconds / 60);
92
+ if (minutes < 60) return `${minutes} minute${minutes !== 1 ? 's' : ''}`;
93
+
94
+ const hours = Math.floor(minutes / 60);
95
+ if (hours < 24) return `${hours} hour${hours !== 1 ? 's' : ''}`;
96
+
97
+ const days = Math.floor(hours / 24);
98
+ return `${days} day${days !== 1 ? 's' : ''}`;
99
+ }
100
+
101
+ // ─────────────────────────────────────────────────────────────
102
+ // Text Formatting
103
+ // ─────────────────────────────────────────────────────────────
104
+
105
+ /**
106
+ * Truncate text to max length with ellipsis
107
+ * @param {string} text - Text to truncate
108
+ * @param {number} maxLength - Maximum length (default: 100)
109
+ * @returns {string} Truncated text
110
+ */
111
+ function truncate(text, maxLength = 100) {
112
+ if (!text) return '';
113
+ if (text.length <= maxLength) return text;
114
+ return text.slice(0, maxLength - 3) + '...';
115
+ }
116
+
117
+ // ─────────────────────────────────────────────────────────────
118
+ // Display Formatting (headers, dividers, status messages)
119
+ // ─────────────────────────────────────────────────────────────
120
+
121
+ /**
122
+ * Format a section header
123
+ * @param {string} text - Header text
124
+ * @returns {string} Formatted header
125
+ */
126
+ function header(text) {
127
+ return `\n## ${text}\n`;
128
+ }
129
+
130
+ /**
131
+ * Horizontal divider
132
+ * @returns {string} Divider string
133
+ */
134
+ function divider() {
135
+ return '\n---\n';
136
+ }
137
+
138
+ /**
139
+ * Format empty state message
140
+ * @param {string} message - Message to display
141
+ * @returns {string} Formatted empty state
142
+ */
143
+ function emptyState(message) {
144
+ return `\n_${message}_\n`;
145
+ }
146
+
147
+ /**
148
+ * Format success message
149
+ * @param {string} message - Success message
150
+ * @returns {string} Formatted success message
151
+ */
152
+ function success(message) {
153
+ return `✓ ${message}`;
154
+ }
155
+
156
+ /**
157
+ * Format warning message
158
+ * @param {string} message - Warning message
159
+ * @returns {string} Formatted warning message
160
+ */
161
+ function warning(message) {
162
+ return `⚠️ ${message}`;
163
+ }
164
+
165
+ /**
166
+ * Format error message
167
+ * @param {string} message - Error message
168
+ * @returns {string} Formatted error message
169
+ */
170
+ function error(message) {
171
+ return `❌ ${message}`;
172
+ }
173
+
174
+ // ─────────────────────────────────────────────────────────────
175
+ // API Helpers
176
+ // ─────────────────────────────────────────────────────────────
177
+
178
+ /**
179
+ * Fetch relevant users for a given context
180
+ * Uses the relevancy API to find people worth connecting with
181
+ *
182
+ * @param {string} handle - Current user's handle
183
+ * @param {string} context - Context type: 'dm_suggest', 'notification', etc.
184
+ * @param {number} limit - Max number of results (default: 5)
185
+ * @returns {Promise<{matches: Array}>} Relevant users with reasons
186
+ */
187
+ async function fetchRelevantUsers(handle, context = 'dm_suggest', limit = 5) {
188
+ try {
189
+ const apiUrl = config.getApiUrl();
190
+ const response = await fetch(
191
+ `${apiUrl}/api/relevancy?handle=${encodeURIComponent(handle)}&context=${context}&limit=${limit}`
192
+ );
193
+
194
+ if (!response.ok) {
195
+ return { matches: [] };
196
+ }
197
+
198
+ return await response.json();
199
+ } catch (e) {
200
+ console.warn('[_shared] fetchRelevantUsers error:', e.message);
201
+ return { matches: [] };
202
+ }
203
+ }
204
+
205
+ // ─────────────────────────────────────────────────────────────
206
+ // Exports
207
+ // ─────────────────────────────────────────────────────────────
208
+
209
+ module.exports = {
210
+ // Auth
211
+ requireInit,
212
+
213
+ // Handles
214
+ normalizeHandle,
215
+ displayHandle,
216
+
217
+ // Time
218
+ formatTimeAgo,
219
+ formatDuration,
220
+
221
+ // Text
222
+ truncate,
223
+
224
+ // Display
225
+ header,
226
+ divider,
227
+ emptyState,
228
+ success,
229
+ warning,
230
+ error,
231
+
232
+ // API
233
+ fetchRelevantUsers
234
+ };