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.
- package/README.md +41 -58
- 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 +77 -53
- 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 +18 -6
- package/prompts.js +1 -1
- package/protocol/index.js +73 -0
- package/setup.js +402 -0
- package/store/api.js +436 -211
- 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 +286 -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/help.js +3 -3
- package/tools/idea.js +9 -2
- package/tools/inbox.js +289 -105
- package/tools/init.js +106 -27
- package/tools/invite.js +15 -4
- 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 +420 -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/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/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
|
|
9
|
-
const {
|
|
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
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
//
|
|
28
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
//
|
|
84
|
-
let cta =
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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:
|
|
98
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
//
|
|
138
|
-
|
|
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:
|
|
147
|
-
|
|
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
|
|
237
|
+
// Auto-open single unread thread inline (no second tool call needed)
|
|
159
238
|
if (totalUnread === 1 && unreadSenders.length === 1) {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
169
|
-
|
|
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 @
|
|
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.
|
|
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
|
-
//
|
|
278
|
-
|
|
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.
|
|
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
|
-
//
|
|
609
|
-
let
|
|
639
|
+
// Send personalized welcome and wait for it (2.5s timeout)
|
|
640
|
+
let welcomeResult = null;
|
|
610
641
|
try {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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,
|
|
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
|
-
|
|
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** ā @
|
|
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 @
|
|
728
|
-
[ ] Reply to @
|
|
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
|