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.
- package/README.md +47 -252
- package/analytics.js +107 -0
- package/auth-store.js +148 -0
- package/auto-update.js +130 -0
- package/bridges/bridge-monitor.js +388 -0
- package/bridges/discord-bot.js +431 -0
- package/bridges/farcaster.js +299 -0
- package/bridges/telegram.js +261 -0
- package/bridges/webhook-health.js +420 -0
- package/bridges/webhook-server.js +437 -0
- package/bridges/whatsapp.js +441 -0
- package/bridges/x-webhook.js +423 -0
- package/config.js +27 -15
- package/games/arcade.js +406 -0
- package/games/chess.js +451 -0
- package/games/colorguess.js +343 -0
- package/games/crossword-words.js +171 -0
- package/games/crossword.js +461 -0
- package/games/drawing.js +347 -0
- package/games/gameroulette.js +300 -0
- package/games/gamerouter.js +336 -0
- package/games/gamestatus.js +337 -0
- package/games/guessnumber.js +209 -0
- package/games/hangman.js +279 -0
- package/games/memory.js +338 -0
- package/games/multiplayer-tictactoe.js +389 -0
- package/games/pixelart.js +399 -0
- package/games/quickduel.js +354 -0
- package/games/riddle.js +371 -0
- package/games/rockpaperscissors.js +291 -0
- package/games/snake.js +406 -0
- package/games/storybuilder.js +343 -0
- package/games/tictactoe.js +345 -0
- package/games/twentyquestions.js +286 -0
- package/games/twotruths.js +207 -0
- package/games/werewolf.js +508 -0
- package/games/wordassociation.js +247 -0
- package/games/wordchain.js +135 -0
- package/index.js +116 -159
- package/intelligence/index.js +9 -2
- package/intelligence/interests.js +369 -0
- package/notification-emitter.js +77 -0
- package/notify.js +5 -1
- package/package.json +21 -16
- package/prompts.js +1 -1
- package/protocol/index.js +73 -0
- package/setup.js +480 -0
- package/smart-inbox.js +276 -0
- package/store/api.js +536 -215
- package/store/profiles.js +160 -12
- package/tools/_actions.js +362 -21
- package/tools/_discovery.js +119 -26
- package/tools/_shared/index.js +64 -0
- package/tools/_shared.js +234 -0
- package/tools/_work-context.js +338 -0
- package/tools/_work-context.manual-test.js +199 -0
- package/tools/_work-context.test.js +260 -0
- package/tools/activity.js +220 -0
- package/tools/analytics.js +191 -0
- package/tools/approve.js +197 -0
- package/tools/artifact-create.js +14 -3
- package/tools/artifacts-price.js +107 -0
- package/tools/available.js +120 -0
- package/tools/broadcast.js +325 -0
- package/tools/chat.js +202 -0
- package/tools/collaborative-drawing.js +1 -1
- package/tools/connection-status.js +178 -0
- package/tools/discover.js +350 -34
- package/tools/dm.js +80 -8
- package/tools/earnings.js +126 -0
- package/tools/feed.js +35 -4
- package/tools/follow.js +224 -0
- package/tools/friends.js +207 -0
- package/tools/gig-browse.js +206 -0
- package/tools/gig-complete.js +144 -0
- package/tools/health.js +87 -0
- package/tools/help.js +3 -3
- package/tools/idea.js +9 -2
- package/tools/inbox.js +289 -105
- package/tools/init.js +131 -34
- package/tools/invite.js +15 -4
- package/tools/leaderboard.js +117 -0
- package/tools/lib/git-apply.js +206 -0
- package/tools/lib/git-bundle.js +407 -0
- package/tools/migrate.js +3 -3
- package/tools/multiplayer-game.js +1 -1
- package/tools/onboarding.js +7 -7
- package/tools/open.js +143 -12
- package/tools/party-game.js +1 -1
- package/tools/plan.js +225 -0
- package/tools/proof-of-work.js +144 -0
- package/tools/reply.js +166 -0
- package/tools/report.js +1 -1
- package/tools/request.js +17 -3
- package/tools/schedule.js +367 -0
- package/tools/search-messages.js +123 -0
- package/tools/session.js +467 -0
- package/tools/session_price.js +128 -0
- package/tools/settings.js +90 -2
- package/tools/ship.js +30 -7
- package/tools/smart-check.js +201 -0
- package/tools/start.js +147 -12
- package/tools/status.js +53 -6
- package/tools/streak.js +147 -0
- package/tools/stuck.js +297 -0
- package/tools/subscribe.js +148 -0
- package/tools/subscriptions.js +134 -0
- package/tools/suggest-tags.js +6 -8
- package/tools/tag-suggestions.js +1 -1
- package/tools/tip.js +150 -77
- package/tools/token.js +4 -4
- package/tools/update.js +1 -1
- package/tools/wallet.js +221 -79
- package/tools/watch.js +157 -0
- package/tools/who.js +30 -1
- package/tools/withdraw.js +145 -0
- package/tools/work-summary.js +96 -0
- package/version.json +10 -8
- package/LICENSE +0 -21
- package/store/sqlite.js +0 -347
- /package/tools/{auto-suggest-connections.js → _deprecated/auto-suggest-connections.js} +0 -0
- /package/tools/{away.js → _deprecated/away.js} +0 -0
- /package/tools/{back.js → _deprecated/back.js} +0 -0
- /package/tools/{bootstrap-skills.js → _deprecated/bootstrap-skills.js} +0 -0
- /package/tools/{bridge-dashboard.js → _deprecated/bridge-dashboard.js} +0 -0
- /package/tools/{bridge-health.js → _deprecated/bridge-health.js} +0 -0
- /package/tools/{bridge-live.js → _deprecated/bridge-live.js} +0 -0
- /package/tools/{bridges.js → _deprecated/bridges.js} +0 -0
- /package/tools/{colorguess.js → _deprecated/colorguess.js} +0 -0
- /package/tools/{discover-insights.js → _deprecated/discover-insights.js} +0 -0
- /package/tools/{discover-momentum.js → _deprecated/discover-momentum.js} +0 -0
- /package/tools/{discovery-analytics.js → _deprecated/discovery-analytics.js} +0 -0
- /package/tools/{discovery-auto-suggest.js → _deprecated/discovery-auto-suggest.js} +0 -0
- /package/tools/{discovery-bootstrap.js → _deprecated/discovery-bootstrap.js} +0 -0
- /package/tools/{discovery-daily.js → _deprecated/discovery-daily.js} +0 -0
- /package/tools/{discovery-dashboard.js → _deprecated/discovery-dashboard.js} +0 -0
- /package/tools/{discovery-digest.js → _deprecated/discovery-digest.js} +0 -0
- /package/tools/{discovery-hub.js → _deprecated/discovery-hub.js} +0 -0
- /package/tools/{discovery-insights.js → _deprecated/discovery-insights.js} +0 -0
- /package/tools/{discovery-momentum.js → _deprecated/discovery-momentum.js} +0 -0
- /package/tools/{discovery-monitor.js → _deprecated/discovery-monitor.js} +0 -0
- /package/tools/{discovery-proactive.js → _deprecated/discovery-proactive.js} +0 -0
- /package/tools/{draw.js → _deprecated/draw.js} +0 -0
- /package/tools/{farcaster.js → _deprecated/farcaster.js} +0 -0
- /package/tools/{forget.js → _deprecated/forget.js} +0 -0
- /package/tools/{games-catalog.js → _deprecated/games-catalog.js} +0 -0
- /package/tools/{games.js → _deprecated/games.js} +0 -0
- /package/tools/{guessnumber.js → _deprecated/guessnumber.js} +0 -0
- /package/tools/{hangman.js → _deprecated/hangman.js} +0 -0
- /package/tools/{multiplayer-tictactoe.js → _deprecated/multiplayer-tictactoe.js} +0 -0
- /package/tools/{mute.js → _deprecated/mute.js} +0 -0
- /package/tools/{recall.js → _deprecated/recall.js} +0 -0
- /package/tools/{remember.js → _deprecated/remember.js} +0 -0
- /package/tools/{riddle.js → _deprecated/riddle.js} +0 -0
- /package/tools/{run-bootstrap.js → _deprecated/run-bootstrap.js} +0 -0
- /package/tools/{skills-analytics.js → _deprecated/skills-analytics.js} +0 -0
- /package/tools/{skills-bootstrap.js → _deprecated/skills-bootstrap.js} +0 -0
- /package/tools/{skills-dashboard.js → _deprecated/skills-dashboard.js} +0 -0
- /package/tools/{skills-exchange.js → _deprecated/skills-exchange.js} +0 -0
- /package/tools/{skills.js → _deprecated/skills.js} +0 -0
- /package/tools/{smart-intro.js → _deprecated/smart-intro.js} +0 -0
- /package/tools/{storybuilder.js → _deprecated/storybuilder.js} +0 -0
- /package/tools/{telegram-bot.js → _deprecated/telegram-bot.js} +0 -0
- /package/tools/{telegram-setup.js → _deprecated/telegram-setup.js} +0 -0
- /package/tools/{tictactoe.js → _deprecated/tictactoe.js} +0 -0
- /package/tools/{twentyquestions.js → _deprecated/twentyquestions.js} +0 -0
- /package/tools/{wordassociation.js → _deprecated/wordassociation.js} +0 -0
package/tools/_discovery.js
CHANGED
|
@@ -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
|
-
//
|
|
124
|
-
if (building.includes('
|
|
125
|
-
|
|
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') ||
|
|
130
|
-
|
|
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
|
-
//
|
|
134
|
-
if (building.includes('
|
|
135
|
-
|
|
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') ||
|
|
140
|
-
|
|
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') ||
|
|
145
|
-
|
|
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') ||
|
|
150
|
-
|
|
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') ||
|
|
155
|
-
|
|
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
|
-
|
|
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
|
package/tools/_shared/index.js
CHANGED
|
@@ -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,
|
package/tools/_shared.js
ADDED
|
@@ -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
|
+
};
|