slashvibe-mcp 0.2.9 → 0.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/README.md +1 -0
  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 +154 -5
  14. package/crypto.js +3 -8
  15. package/games/arcade.js +406 -0
  16. package/games/chess.js +451 -0
  17. package/games/colorguess.js +343 -0
  18. package/games/crossword-words.js +171 -0
  19. package/games/crossword.js +461 -0
  20. package/games/drawing.js +347 -0
  21. package/games/gameroulette.js +300 -0
  22. package/games/gamerouter.js +336 -0
  23. package/games/gamestatus.js +337 -0
  24. package/games/guessnumber.js +209 -0
  25. package/games/hangman.js +279 -0
  26. package/games/memory.js +338 -0
  27. package/games/multiplayer-tictactoe.js +389 -0
  28. package/games/pixelart.js +399 -0
  29. package/games/quickduel.js +354 -0
  30. package/games/riddle.js +371 -0
  31. package/games/rockpaperscissors.js +291 -0
  32. package/games/snake.js +406 -0
  33. package/games/storybuilder.js +343 -0
  34. package/games/tictactoe.js +345 -0
  35. package/games/twentyquestions.js +286 -0
  36. package/games/twotruths.js +207 -0
  37. package/games/werewolf.js +508 -0
  38. package/games/wordassociation.js +247 -0
  39. package/games/wordchain.js +135 -0
  40. package/index.js +105 -216
  41. package/intelligence/index.js +45 -0
  42. package/intelligence/infer.js +316 -0
  43. package/intelligence/interests.js +369 -0
  44. package/intelligence/patterns.js +651 -0
  45. package/intelligence/proactive.js +358 -0
  46. package/intelligence/serendipity.js +306 -0
  47. package/memory.js +166 -0
  48. package/notification-emitter.js +77 -0
  49. package/notify.js +102 -1
  50. package/package.json +21 -7
  51. package/prompts.js +1 -1
  52. package/protocol/index.js +161 -1
  53. package/setup.js +402 -0
  54. package/store/api.js +528 -82
  55. package/store/profiles.js +160 -12
  56. package/tools/_actions.js +463 -16
  57. package/tools/_deprecated/auto-suggest-connections.js +304 -0
  58. package/tools/_deprecated/bootstrap-skills.js +231 -0
  59. package/tools/_deprecated/bridge-dashboard.js +342 -0
  60. package/tools/_deprecated/bridge-health.js +400 -0
  61. package/tools/_deprecated/bridge-live.js +384 -0
  62. package/tools/_deprecated/bridges.js +383 -0
  63. package/tools/_deprecated/colorguess.js +281 -0
  64. package/tools/_deprecated/discover-insights.js +379 -0
  65. package/tools/_deprecated/discover-momentum.js +256 -0
  66. package/tools/_deprecated/discovery-analytics.js +345 -0
  67. package/tools/_deprecated/discovery-auto-suggest.js +275 -0
  68. package/tools/_deprecated/discovery-bootstrap.js +267 -0
  69. package/tools/_deprecated/discovery-daily.js +375 -0
  70. package/tools/_deprecated/discovery-dashboard.js +385 -0
  71. package/tools/_deprecated/discovery-digest.js +314 -0
  72. package/tools/_deprecated/discovery-hub.js +357 -0
  73. package/tools/_deprecated/discovery-insights.js +384 -0
  74. package/tools/_deprecated/discovery-momentum.js +281 -0
  75. package/tools/_deprecated/discovery-monitor.js +319 -0
  76. package/tools/_deprecated/discovery-proactive.js +300 -0
  77. package/tools/_deprecated/draw.js +317 -0
  78. package/tools/_deprecated/farcaster.js +307 -0
  79. package/tools/_deprecated/forget.js +119 -0
  80. package/tools/_deprecated/games-catalog.js +376 -0
  81. package/tools/_deprecated/games.js +313 -0
  82. package/tools/_deprecated/guessnumber.js +194 -0
  83. package/tools/_deprecated/hangman.js +129 -0
  84. package/tools/_deprecated/multiplayer-tictactoe.js +303 -0
  85. package/tools/_deprecated/recall.js +147 -0
  86. package/tools/_deprecated/remember.js +86 -0
  87. package/tools/_deprecated/riddle.js +240 -0
  88. package/tools/_deprecated/run-bootstrap.js +69 -0
  89. package/tools/_deprecated/skills-analytics.js +349 -0
  90. package/tools/_deprecated/skills-bootstrap.js +301 -0
  91. package/tools/_deprecated/skills-dashboard.js +268 -0
  92. package/tools/_deprecated/skills.js +380 -0
  93. package/tools/_deprecated/smart-intro.js +353 -0
  94. package/tools/_deprecated/storybuilder.js +331 -0
  95. package/tools/_deprecated/telegram-bot.js +183 -0
  96. package/tools/_deprecated/telegram-setup.js +214 -0
  97. package/tools/_deprecated/twentyquestions.js +143 -0
  98. package/tools/_discovery-enhanced.js +290 -0
  99. package/tools/_discovery.js +439 -0
  100. package/tools/_proactive-discovery.js +301 -0
  101. package/tools/_shared/index.js +64 -0
  102. package/tools/_shared.js +234 -0
  103. package/tools/_work-context.js +338 -0
  104. package/tools/_work-context.manual-test.js +199 -0
  105. package/tools/_work-context.test.js +260 -0
  106. package/tools/activity.js +220 -0
  107. package/tools/admin-inbox.js +218 -0
  108. package/tools/agent-treasury.js +288 -0
  109. package/tools/analytics.js +191 -0
  110. package/tools/approve.js +197 -0
  111. package/tools/arcade.js +173 -0
  112. package/tools/artifact-create.js +17 -11
  113. package/tools/artifact-view.js +31 -1
  114. package/tools/artifacts-price.js +47 -43
  115. package/tools/ask-expert.js +160 -0
  116. package/tools/available.js +120 -0
  117. package/tools/become-expert.js +150 -0
  118. package/tools/broadcast.js +286 -0
  119. package/tools/chat.js +202 -0
  120. package/tools/collaborative-drawing.js +286 -0
  121. package/tools/connection-status.js +178 -0
  122. package/tools/crossword.js +17 -1
  123. package/tools/discover.js +350 -34
  124. package/tools/dm.js +115 -73
  125. package/tools/drawing.js +1 -1
  126. package/tools/earnings.js +126 -0
  127. package/tools/echo.js +16 -0
  128. package/tools/feed.js +51 -7
  129. package/tools/follow.js +224 -0
  130. package/tools/friends.js +207 -0
  131. package/tools/game.js +2 -2
  132. package/tools/genesis.js +233 -0
  133. package/tools/gig-browse.js +206 -0
  134. package/tools/gig-complete.js +144 -0
  135. package/tools/handoff.js +7 -1
  136. package/tools/help.js +4 -4
  137. package/tools/idea.js +9 -2
  138. package/tools/inbox.js +334 -28
  139. package/tools/init.js +727 -36
  140. package/tools/invite.js +15 -4
  141. package/tools/l2-bridge.js +272 -0
  142. package/tools/l2-status.js +217 -0
  143. package/tools/l2.js +206 -0
  144. package/tools/migrate.js +156 -0
  145. package/tools/mint.js +377 -0
  146. package/tools/multiplayer-game.js +1 -1
  147. package/tools/notifications.js +415 -0
  148. package/tools/observe.js +200 -0
  149. package/tools/onboarding.js +147 -0
  150. package/tools/open.js +143 -12
  151. package/tools/party-game.js +2 -2
  152. package/tools/plan.js +225 -0
  153. package/tools/presence-agent.js +7 -0
  154. package/tools/proof-of-work.js +100 -104
  155. package/tools/pulse.js +218 -0
  156. package/tools/reply.js +166 -0
  157. package/tools/report.js +2 -2
  158. package/tools/reputation.js +175 -0
  159. package/tools/request.js +17 -3
  160. package/tools/schedule.js +367 -0
  161. package/tools/search-messages.js +123 -0
  162. package/tools/session.js +420 -0
  163. package/tools/session_price.js +128 -0
  164. package/tools/settings.js +126 -3
  165. package/tools/ship.js +31 -8
  166. package/tools/shipback.js +326 -0
  167. package/tools/smart-check.js +201 -0
  168. package/tools/social-processor.js +445 -0
  169. package/tools/solo-game.js +1 -1
  170. package/tools/start.js +335 -93
  171. package/tools/status.js +53 -6
  172. package/tools/stuck.js +297 -0
  173. package/tools/subscribe.js +148 -0
  174. package/tools/subscriptions.js +134 -0
  175. package/tools/suggest-tags.js +6 -8
  176. package/tools/tag-suggestions.js +257 -0
  177. package/tools/tip.js +193 -0
  178. package/tools/token.js +103 -0
  179. package/tools/update.js +1 -1
  180. package/tools/wallet.js +239 -186
  181. package/tools/watch.js +157 -0
  182. package/tools/webhook-test.js +388 -0
  183. package/tools/who.js +54 -3
  184. package/tools/withdraw.js +145 -0
  185. package/tools/work-summary.js +96 -0
  186. package/tools/workshop.js +327 -0
  187. package/tools/x-mentions.js +1 -1
  188. package/version.json +14 -3
  189. package/tools/artifacts-buy.js +0 -111
  190. package/tools/connect.js +0 -284
  191. package/tools/gigs-apply.js +0 -99
  192. package/tools/gigs-browse.js +0 -114
  193. package/tools/gigs-complete.js +0 -128
  194. package/tools/gigs-post.js +0 -140
  195. package/tools/live-off.js +0 -109
  196. package/tools/live-watch.js +0 -176
  197. package/tools/live.js +0 -128
  198. package/tools/sessions-browse.js +0 -105
  199. package/tools/whats-happening.js +0 -125
  200. /package/tools/{away.js → _deprecated/away.js} +0 -0
  201. /package/tools/{back.js → _deprecated/back.js} +0 -0
  202. /package/tools/{mute.js → _deprecated/mute.js} +0 -0
  203. /package/tools/{skills-exchange.js → _deprecated/skills-exchange.js} +0 -0
  204. /package/tools/{tictactoe.js → _deprecated/tictactoe.js} +0 -0
  205. /package/tools/{wordassociation.js → _deprecated/wordassociation.js} +0 -0
package/tools/init.js CHANGED
@@ -1,79 +1,756 @@
1
1
  /**
2
2
  * vibe init — Set your identity
3
3
  *
4
- * AIRC v0.1 compliant: Generates Ed25519 keypair for message signing
4
+ * Smooth browser-based OAuth flow:
5
+ * 1. Start local callback server on localhost:9876
6
+ * 2. Open browser to login page
7
+ * 3. User authenticates with GitHub/X
8
+ * 4. Browser redirects back to localhost with token
9
+ * 5. Tool WAITS for callback and returns success
5
10
  */
6
11
 
12
+ const http = require('http');
13
+ const { exec, execSync } = require('child_process');
14
+ const fs = require('fs');
15
+ const path = require('path');
7
16
  const config = require('../config');
8
17
  const store = require('../store');
9
- const crypto = require('../crypto');
10
18
  const discord = require('../discord');
19
+ const authStore = require('../auth-store');
20
+
21
+ const CALLBACK_PORT = 9876;
22
+ const API_BASE = 'https://www.slashvibe.dev';
23
+
24
+ /**
25
+ * Fetch online count from presence API
26
+ */
27
+ async function getOnlineCount() {
28
+ try {
29
+ const response = await fetch(`${API_BASE}/api/presence`);
30
+ if (!response.ok) return 0;
31
+ const data = await response.json();
32
+ return (data.active?.length || 0) + (data.away?.length || 0);
33
+ } catch (e) {
34
+ return 0;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Generate welcome banner for new users (pre-auth, no handle yet)
40
+ */
41
+ function generatePreAuthBanner(onlineCount) {
42
+ const onlineText = onlineCount > 0 ? `🟢 ${onlineCount} online now` : '🟢 join the crew';
43
+ return `
44
+ ā–ˆā–‘ā–ˆ ā–ˆ ā–ˆā–„ā–„ ā–ˆā–€ā–€ šŸš€ ship together
45
+ ▀▄▀ ā–ˆ ā–ˆā–„ā–ˆ ā–ˆā–ˆā–„ ${onlineText}
46
+ ──────────────────────────────────────────────────
47
+ `;
48
+ }
49
+
50
+ /**
51
+ * Generate welcome banner for authenticated users (with handle + unread)
52
+ */
53
+ function generateAuthBanner(handle, unreadCount, onlineCount) {
54
+ // Format: logo | handle + unread | tagline + online
55
+ // Keep alignment consistent with original banner
56
+ const handleCol = `@${handle}`.padEnd(16);
57
+ const unreadCol = unreadCount > 0 ? `šŸ“¬ ${unreadCount} unread`.padEnd(14) : `šŸ“¬ 0 messages`.padEnd(14);
58
+
59
+ return ` ā–ˆā–‘ā–ˆ ā–ˆ ā–ˆā–„ā–„ ā–ˆā–€ā–€ ${handleCol} šŸš€ ship together
60
+ ▀▄▀ ā–ˆ ā–ˆā–„ā–ˆ ā–ˆā–ˆā–„ ${unreadCol} 🟢 ${onlineCount} online
61
+ ──────────────────────────────────────────────────`;
62
+ }
63
+
64
+ /**
65
+ * Detect current git repository name
66
+ */
67
+ function detectRepoName() {
68
+ try {
69
+ const toplevel = execSync('git rev-parse --show-toplevel 2>/dev/null', {
70
+ encoding: 'utf8',
71
+ timeout: 1000
72
+ }).trim();
73
+ // Split on forward or back slash to get repo name
74
+ const parts = toplevel.replace(/\\/g, '/').split('/');
75
+ return parts[parts.length - 1];
76
+ } catch (e) {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Detect tech stack from package.json or file extensions
83
+ */
84
+ function detectTechStack() {
85
+ const techStack = new Set();
86
+
87
+ try {
88
+ // Find git root first, fallback to cwd
89
+ let cwd;
90
+ try {
91
+ cwd = execSync('git rev-parse --show-toplevel 2>/dev/null', {
92
+ encoding: 'utf8',
93
+ timeout: 1000
94
+ }).trim();
95
+ } catch (e) {
96
+ cwd = process.cwd();
97
+ }
98
+
99
+ // Try reading package.json
100
+ const pkgPath = path.join(cwd, 'package.json');
101
+ if (fs.existsSync(pkgPath)) {
102
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
103
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
104
+
105
+ // Map common packages to tech names
106
+ const techMap = {
107
+ 'react': 'React',
108
+ 'next': 'Next.js',
109
+ 'vue': 'Vue',
110
+ 'svelte': 'Svelte',
111
+ 'express': 'Express',
112
+ 'fastify': 'Fastify',
113
+ 'typescript': 'TypeScript',
114
+ '@anthropic-ai/sdk': 'Claude API',
115
+ 'openai': 'OpenAI',
116
+ 'langchain': 'LangChain',
117
+ 'prisma': 'Prisma',
118
+ '@vercel/kv': 'Vercel KV',
119
+ 'tailwindcss': 'Tailwind',
120
+ 'electron': 'Electron',
121
+ '@tauri-apps/api': 'Tauri'
122
+ };
123
+
124
+ for (const [pkg, tech] of Object.entries(techMap)) {
125
+ if (deps[pkg]) techStack.add(tech);
126
+ }
127
+ }
128
+
129
+ // Detect by file extensions in cwd
130
+ const files = fs.readdirSync(cwd).slice(0, 50); // Limit scan
131
+ for (const f of files) {
132
+ if (f.endsWith('.ts') || f.endsWith('.tsx')) techStack.add('TypeScript');
133
+ if (f.endsWith('.py')) techStack.add('Python');
134
+ if (f.endsWith('.rs')) techStack.add('Rust');
135
+ if (f.endsWith('.go')) techStack.add('Go');
136
+ if (f.endsWith('.sol')) techStack.add('Solidity');
137
+ }
138
+
139
+ } catch (e) {
140
+ // Non-fatal - continue without tech detection
141
+ }
142
+
143
+ return Array.from(techStack).slice(0, 8); // Limit to 8 techs
144
+ }
145
+
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
168
+ */
169
+ async function sendPersonalizedWelcome(handle, oneLiner) {
170
+ try {
171
+ const repoName = detectRepoName();
172
+ const techStack = detectTechStack();
173
+
174
+ const response = await fetch(`${API_BASE}/api/onboarding/personalized-welcome`, {
175
+ method: 'POST',
176
+ headers: { 'Content-Type': 'application/json' },
177
+ body: JSON.stringify({
178
+ handle,
179
+ oneLiner,
180
+ repoName,
181
+ techStack,
182
+ githubProfile: null
183
+ })
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;
193
+ } catch (e) {
194
+ console.error('[vibe_init] Personalized welcome failed:', e.message);
195
+ return null;
196
+ }
197
+ }
198
+
199
+ const LOGIN_URL = 'https://www.slashvibe.dev/login';
200
+ const API_URL = process.env.VIBE_API_URL || 'https://www.slashvibe.dev';
201
+ const AUTH_TIMEOUT_MS = 120000; // 2 minutes
202
+
203
+ /**
204
+ * Send welcome message from @seth (founder)
205
+ */
206
+ async function sendWelcomeMessage(handle, one_liner) {
207
+ try {
208
+ const response = await fetch(`${API_URL}/api/onboarding/welcome`, {
209
+ method: 'POST',
210
+ headers: { 'Content-Type': 'application/json' },
211
+ body: JSON.stringify({ handle, one_liner })
212
+ });
213
+ const result = await response.json();
214
+ return result.success;
215
+ } catch (e) {
216
+ console.error('[vibe_init] Welcome message failed:', e.message);
217
+ return false;
218
+ }
219
+ }
11
220
 
12
221
  const definition = {
13
222
  name: 'vibe_init',
14
- description: 'Set your identity for /vibe. Required before messaging.',
223
+ description: 'Set your identity for /vibe. Opens browser for GitHub auth and waits for completion. Returns when auth is done.',
15
224
  inputSchema: {
16
225
  type: 'object',
17
226
  properties: {
18
227
  handle: {
19
228
  type: 'string',
20
- description: 'Your handle (lowercase, no @)'
229
+ description: 'Custom handle (optional - defaults to your GitHub username)'
21
230
  },
22
231
  one_liner: {
23
232
  type: 'string',
24
233
  description: 'What are you building? (one line)'
25
234
  }
26
235
  },
27
- required: ['handle', 'one_liner']
236
+ required: []
28
237
  }
29
238
  };
30
239
 
240
+ /**
241
+ * Open URL in default browser
242
+ */
243
+ function openBrowser(url) {
244
+ const platform = process.platform;
245
+ let command;
246
+
247
+ if (platform === 'darwin') {
248
+ command = `open "${url}"`;
249
+ } else if (platform === 'win32') {
250
+ command = `start "" "${url}"`;
251
+ } else {
252
+ command = `xdg-open "${url}"`;
253
+ }
254
+
255
+ exec(command, (err) => {
256
+ if (err) {
257
+ console.error('[vibe_init] Failed to open browser:', err.message);
258
+ }
259
+ });
260
+ }
261
+
262
+ /**
263
+ * Wait for OAuth callback - returns Promise that resolves with handle when auth completes
264
+ */
265
+ function waitForCallback(requestedHandle, one_liner) {
266
+ return new Promise((resolve, reject) => {
267
+ let resolved = false;
268
+
269
+ const server = http.createServer(async (req, res) => {
270
+ const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
271
+
272
+ // Handle callback
273
+ if (url.pathname === '/callback') {
274
+ const token = url.searchParams.get('token');
275
+ const callbackHandle = url.searchParams.get('handle');
276
+
277
+ if (token && callbackHandle) {
278
+ // Save the token and handle
279
+ const finalHandle = requestedHandle || callbackHandle;
280
+
281
+ // Save to config (file persistence for restarts)
282
+ config.saveAuthToken(token);
283
+ config.setSessionIdentity(finalHandle, one_liner || '');
284
+
285
+ // PUSH to in-memory auth store (immediate propagation)
286
+ authStore.setToken(token);
287
+ authStore.setHandle(finalHandle);
288
+ authStore.setOneLiner(one_liner || '');
289
+
290
+ // Update shared config
291
+ const cfg = config.load();
292
+ cfg.handle = finalHandle;
293
+ cfg.one_liner = one_liner || '';
294
+ cfg.authMethod = 'browser';
295
+ cfg.pendingAuth = false;
296
+ config.save(cfg);
297
+
298
+ // Register session with API
299
+ const sessionId = config.getSessionId();
300
+ await store.registerSession(sessionId, finalHandle, one_liner);
301
+
302
+ // Send initial heartbeat
303
+ await store.heartbeat(finalHandle, one_liner);
304
+
305
+ // Post to Discord
306
+ discord.postJoin(finalHandle, one_liner);
307
+
308
+ // NOTE: Welcome message is sent in the main return path (awaited)
309
+ // to ensure it arrives before we show the unread count
310
+
311
+ // Send success response to browser - lightweight, no infinite animations
312
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
313
+ res.end(`<!DOCTYPE html>
314
+ <html lang="en">
315
+ <head>
316
+ <meta charset="UTF-8">
317
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
318
+ <title>Welcome to /vibe!</title>
319
+ <link rel="preconnect" href="https://fonts.googleapis.com">
320
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
321
+ <link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&display=swap" rel="stylesheet">
322
+ <style>
323
+ * { margin: 0; padding: 0; box-sizing: border-box; }
324
+
325
+ :root {
326
+ --neon-green: #00FF88;
327
+ --neon-cyan: #00FFFF;
328
+ --deep-black: #0A0A0A;
329
+ --glow-green: 0 0 15px #00FF88;
330
+ }
331
+
332
+ body {
333
+ font-family: 'VT323', monospace;
334
+ background: var(--deep-black);
335
+ min-height: 100vh;
336
+ display: flex;
337
+ align-items: center;
338
+ justify-content: center;
339
+ color: #fff;
340
+ overflow: hidden;
341
+ position: relative;
342
+ }
343
+
344
+ /* Static CRT Scanline Effect - no animation */
345
+ body::before {
346
+ content: '';
347
+ position: fixed;
348
+ top: 0; left: 0;
349
+ width: 100%; height: 100%;
350
+ background: repeating-linear-gradient(
351
+ 0deg,
352
+ rgba(0, 0, 0, 0.1),
353
+ rgba(0, 0, 0, 0.1) 1px,
354
+ transparent 1px,
355
+ transparent 2px
356
+ );
357
+ pointer-events: none;
358
+ z-index: 1000;
359
+ }
360
+
361
+ /* Static vignette */
362
+ body::after {
363
+ content: '';
364
+ position: fixed;
365
+ top: 0; left: 0;
366
+ width: 100%; height: 100%;
367
+ background: radial-gradient(ellipse at center, transparent 0%, rgba(0, 0, 0, 0.3) 100%);
368
+ pointer-events: none;
369
+ z-index: 999;
370
+ }
371
+
372
+ /* Static decorative symbols - NO animation */
373
+ .particles {
374
+ position: fixed;
375
+ top: 0; left: 0;
376
+ width: 100%; height: 100%;
377
+ pointer-events: none;
378
+ z-index: 1;
379
+ opacity: 0.15;
380
+ }
381
+
382
+ .particle {
383
+ position: absolute;
384
+ font-size: 20px;
385
+ color: var(--neon-green);
386
+ }
387
+
388
+ .container {
389
+ background: rgba(0, 255, 136, 0.03);
390
+ border: 2px solid var(--neon-green);
391
+ padding: 48px 64px;
392
+ max-width: 480px;
393
+ width: 90%;
394
+ text-align: center;
395
+ position: relative;
396
+ z-index: 10;
397
+ box-shadow: var(--glow-green);
398
+ animation: fadeIn 0.4s ease-out forwards;
399
+ }
400
+
401
+ @keyframes fadeIn {
402
+ from { opacity: 0; transform: scale(0.95); }
403
+ to { opacity: 1; transform: scale(1); }
404
+ }
405
+
406
+ /* Corner decorations */
407
+ .container::before, .container::after {
408
+ content: '+';
409
+ position: absolute;
410
+ font-family: 'VT323', monospace;
411
+ font-size: 24px;
412
+ color: var(--neon-green);
413
+ }
414
+ .container::before { top: 8px; left: 12px; }
415
+ .container::after { bottom: 8px; right: 12px; }
416
+
417
+ .logo {
418
+ font-family: 'Press Start 2P', cursive;
419
+ font-size: 28px;
420
+ color: var(--neon-green);
421
+ text-shadow: var(--glow-green);
422
+ margin-bottom: 24px;
423
+ }
424
+
425
+ .checkmark {
426
+ width: 80px;
427
+ height: 80px;
428
+ margin: 0 auto 24px;
429
+ }
430
+
431
+ .checkmark svg {
432
+ width: 100%;
433
+ height: 100%;
434
+ fill: none;
435
+ stroke: var(--neon-green);
436
+ stroke-width: 4;
437
+ stroke-linecap: round;
438
+ stroke-linejoin: round;
439
+ filter: drop-shadow(0 0 8px var(--neon-green));
440
+ animation: drawCheck 0.5s ease-out forwards;
441
+ }
442
+
443
+ @keyframes drawCheck {
444
+ 0% { stroke-dasharray: 100; stroke-dashoffset: 100; }
445
+ 100% { stroke-dasharray: 100; stroke-dashoffset: 0; }
446
+ }
447
+
448
+ .welcome {
449
+ font-size: 28px;
450
+ color: rgba(255, 255, 255, 0.9);
451
+ margin-bottom: 8px;
452
+ }
453
+
454
+ .handle {
455
+ color: var(--neon-green);
456
+ text-shadow: 0 0 10px var(--neon-green);
457
+ }
458
+
459
+ .status {
460
+ font-size: 20px;
461
+ color: var(--neon-cyan);
462
+ margin: 20px 0;
463
+ }
464
+
465
+ .close-msg {
466
+ font-size: 18px;
467
+ color: rgba(255, 255, 255, 0.6);
468
+ margin-top: 24px;
469
+ padding: 12px 20px;
470
+ border: 1px dashed rgba(255, 255, 255, 0.3);
471
+ }
472
+
473
+ .ascii {
474
+ font-size: 14px;
475
+ color: rgba(0, 255, 136, 0.3);
476
+ margin-top: 20px;
477
+ }
478
+
479
+ /* Respect reduced motion preference */
480
+ @media (prefers-reduced-motion: reduce) {
481
+ *, *::before, *::after {
482
+ animation: none !important;
483
+ transition: none !important;
484
+ }
485
+ }
486
+
487
+ /* Stop all animations after load */
488
+ .animations-stopped *,
489
+ .animations-stopped *::before,
490
+ .animations-stopped *::after {
491
+ animation: none !important;
492
+ }
493
+ </style>
494
+ </head>
495
+ <body>
496
+ <div class="particles" id="particles"></div>
497
+
498
+ <div class="container">
499
+ <div class="logo">/vibe</div>
500
+
501
+ <div class="checkmark">
502
+ <svg viewBox="0 0 52 52">
503
+ <circle cx="26" cy="26" r="22" stroke-opacity="0.3"/>
504
+ <path d="M14 27l8 8 16-16"/>
505
+ </svg>
506
+ </div>
507
+
508
+ <p class="welcome">Welcome, <span class="handle">@${finalHandle}</span></p>
509
+ <p class="status">Authentication successful</p>
510
+
511
+ <p class="close-msg">You can now close this window</p>
512
+
513
+ <div class="ascii">═══════════════════════════════</div>
514
+ </div>
515
+
516
+ <script>
517
+ // Create STATIC decorative symbols (no animation)
518
+ (function() {
519
+ var container = document.getElementById('particles');
520
+ var symbols = ['>', '<', '/', '*', '#', '@', '~'];
521
+ var positions = [
522
+ {x: 10, y: 15}, {x: 85, y: 20}, {x: 20, y: 75}, {x: 75, y: 80},
523
+ {x: 50, y: 10}, {x: 15, y: 45}, {x: 88, y: 55}, {x: 45, y: 85}
524
+ ];
525
+ positions.forEach(function(pos, i) {
526
+ var particle = document.createElement('div');
527
+ particle.className = 'particle';
528
+ particle.textContent = symbols[i % symbols.length];
529
+ particle.style.left = pos.x + '%';
530
+ particle.style.top = pos.y + '%';
531
+ container.appendChild(particle);
532
+ });
533
+ })();
534
+
535
+ // Stop any remaining animations after 2 seconds (safety net)
536
+ setTimeout(function() {
537
+ document.body.classList.add('animations-stopped');
538
+ }, 2000);
539
+ </script>
540
+ </body>
541
+ </html>`);
542
+
543
+ // Close server and resolve
544
+ resolved = true;
545
+ setTimeout(() => server.close(), 500);
546
+ resolve({ success: true, handle: finalHandle });
547
+ } else {
548
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
549
+ res.end('Missing token or handle');
550
+ }
551
+ } else {
552
+ res.writeHead(404);
553
+ res.end('Not found');
554
+ }
555
+ });
556
+
557
+ server.on('error', (err) => {
558
+ if (err.code === 'EADDRINUSE') {
559
+ reject(new Error('AUTH_IN_PROGRESS'));
560
+ } else {
561
+ reject(err);
562
+ }
563
+ });
564
+
565
+ // Start server
566
+ server.listen(CALLBACK_PORT, '127.0.0.1', () => {
567
+ console.log(`[vibe_init] Callback server listening on port ${CALLBACK_PORT}`);
568
+ });
569
+
570
+ // Timeout after 2 minutes
571
+ setTimeout(() => {
572
+ if (!resolved) {
573
+ server.close();
574
+ reject(new Error('AUTH_TIMEOUT'));
575
+ }
576
+ }, AUTH_TIMEOUT_MS);
577
+ });
578
+ }
579
+
31
580
  async function handler(args) {
32
- const { handle, one_liner } = args;
581
+ const { handle, one_liner, auth_method } = args;
33
582
 
34
- // Normalize handle
35
- const h = handle.toLowerCase().replace('@', '').replace(/[^a-z0-9_-]/g, '');
583
+ // Normalize handle if provided
584
+ const h = handle
585
+ ? handle.toLowerCase().replace('@', '').replace(/[^a-z0-9_-]/g, '')
586
+ : null;
36
587
 
37
- if (!h || h.length < 2) {
588
+ // Validate if custom handle provided
589
+ if (h && h.length < 2) {
38
590
  return {
39
591
  display: 'Handle must be at least 2 characters (letters, numbers, - or _)'
40
592
  };
41
593
  }
42
594
 
43
- // Hint about X handles for short/common names
44
- let xHandleHint = '';
45
- const commonShortNames = ['dave', 'dan', 'john', 'mike', 'chris', 'alex', 'sam', 'ben', 'tom', 'matt', 'nick', 'joe', 'max', 'ian', 'rob', 'bob', 'jim', 'tim', 'pat', 'ed'];
46
- if (h.length <= 5 && commonShortNames.includes(h)) {
47
- xHandleHint = `\n\nšŸ’” _Tip: Use your X handle (e.g., @${h}smith) so people can find you._`;
595
+ // Check if already authenticated
596
+ if (config.hasOAuth()) {
597
+ const existingHandle = config.getHandle();
598
+ if (existingHandle) {
599
+ return {
600
+ display: `## Already signed in as @${existingHandle}
601
+
602
+ To sign out and re-authenticate: \`vibe logout\`
603
+ To see who's online: \`vibe who\`
604
+ To check messages: \`vibe inbox\``
605
+ };
606
+ }
607
+ }
608
+
609
+ // ===========================================
610
+ // Show welcome banner (pre-auth)
611
+ // ===========================================
612
+ const onlineCount = await getOnlineCount();
613
+ const welcomeBanner = generatePreAuthBanner(onlineCount);
614
+
615
+ // ===========================================
616
+ // BROWSER AUTH (Default): GitHub OAuth
617
+ // ===========================================
618
+ if (auth_method === 'browser' || !auth_method) {
619
+ // Save one_liner for callback handler
620
+ const cfg = config.load();
621
+ if (h) cfg.handle = h;
622
+ cfg.one_liner = one_liner || '';
623
+ cfg.pendingAuth = true;
624
+ config.save(cfg);
625
+
626
+ // Build login URL with redirect to our local callback
627
+ const callbackUrl = `http://localhost:${CALLBACK_PORT}/callback`;
628
+ const loginUrl = h
629
+ ? `${LOGIN_URL}?redirect=${encodeURIComponent(callbackUrl)}&handle=${encodeURIComponent(h)}`
630
+ : `${LOGIN_URL}?redirect=${encodeURIComponent(callbackUrl)}`;
631
+
632
+ // Open browser BEFORE starting to wait
633
+ openBrowser(loginUrl);
634
+
635
+ try {
636
+ // Wait for callback (blocks until auth completes or times out)
637
+ const result = await waitForCallback(h, one_liner);
638
+
639
+ // Send personalized welcome and wait for it (2.5s timeout)
640
+ let welcomeResult = null;
641
+ try {
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
+ ]);
664
+ } catch (e) {}
665
+
666
+ // Generate authenticated banner with handle + unread (3 lines only - won't collapse)
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
+ }
685
+
686
+ return {
687
+ display: authBanner + friendsSection + welcomeSection,
688
+ onboarding: {
689
+ isNewUser: true,
690
+ handle: result.handle,
691
+ hint: 'show_onboarding_options',
692
+ hasWelcomeMessage: !!welcomeResult,
693
+ welcomeText: welcomeResult?.messageText || null,
694
+ friendsOnVibe: friendsData?.friendsOnVibe || [],
695
+ inviteSuggestions: friendsData?.inviteSuggestions || []
696
+ }
697
+ };
698
+
699
+ } catch (err) {
700
+ if (err.message === 'AUTH_IN_PROGRESS') {
701
+ return {
702
+ display: `## Auth already in progress
703
+
704
+ Another login flow is running. Complete it in your browser or wait a moment and try again.`
705
+ };
706
+ }
707
+
708
+ if (err.message === 'AUTH_TIMEOUT') {
709
+ return {
710
+ display: `## Auth timed out
711
+
712
+ The login flow wasn't completed within 2 minutes. Try again with \`vibe init\``
713
+ };
714
+ }
715
+
716
+ return {
717
+ display: `## Failed to authenticate
718
+
719
+ Error: ${err.message}
720
+
721
+ Try again or use legacy auth: \`vibe init --auth_method=legacy\``
722
+ };
723
+ }
48
724
  }
49
725
 
50
- // AIRC: Generate Ed25519 keypair if not already present
726
+ // ===========================================
727
+ // LEGACY AUTH: Local Ed25519 keypairs
728
+ // ===========================================
729
+ const crypto = require('../crypto');
730
+
731
+ // Generate Ed25519 keypair if not already present
51
732
  let keypair = config.getKeypair();
52
733
  let keypairNote = '';
53
734
  if (!keypair) {
54
735
  keypair = crypto.generateKeypair();
55
- // Save keypair to shared config (persists across MCP invocations)
56
736
  config.saveKeypair(keypair);
57
737
  keypairNote = '\nšŸ” _AIRC keypair generated for message signing_';
58
738
  }
59
739
 
60
- // Save identity to SESSION file (per-process isolation)
740
+ // Save identity
61
741
  config.setSessionIdentity(h, one_liner || '', keypair);
62
742
 
63
- // Also update shared config for backward compat
64
743
  const cfg = config.load();
65
744
  cfg.handle = h;
66
745
  cfg.one_liner = one_liner || '';
67
746
  cfg.visible = true;
747
+ cfg.authMethod = 'legacy';
68
748
  config.save(cfg);
69
749
 
70
- // Get session ID for this Claude Code process
750
+ // Register session with API
71
751
  const sessionId = config.getSessionId();
72
-
73
- // Register session with API (maps sessionId → handle)
74
- // Also registers user in users DB for @vibe welcome tracking
75
- // AIRC: Include public key for identity verification
76
752
  const registration = await store.registerSession(sessionId, h, one_liner, keypair.publicKey);
753
+
77
754
  if (!registration.success) {
78
755
  return {
79
756
  display: `## Identity Set (Local Only)
@@ -89,9 +766,21 @@ Local config saved. Heartbeats will use username fallback.`
89
766
  // Send initial heartbeat
90
767
  await store.heartbeat(h, one_liner);
91
768
 
92
- // Post to Discord if configured
769
+ // Post to Discord
93
770
  discord.postJoin(h, one_liner);
94
771
 
772
+ // Send personalized welcome from @vibe (non-blocking)
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
+ }
783
+
95
784
  // Check for unread messages
96
785
  let unreadNotice = '';
97
786
  try {
@@ -102,23 +791,25 @@ Local config saved. Heartbeats will use username fallback.`
102
791
  } catch (e) {}
103
792
 
104
793
  return {
105
- display: `## Welcome to /vibe!
794
+ display: `${welcomeBanner}
795
+ ## Welcome to /vibe! (Legacy Auth)
106
796
 
107
- **@${h}** — [x.com/${h}](https://x.com/${h})
108
- _${one_liner}_${unreadNotice}${xHandleHint}${keypairNote}
797
+ **@${h}**
798
+ _${one_liner}_${unreadNotice}${keypairNote}
109
799
 
110
- **What to do now:**
111
- 1. Say "who's around?" to see active builders
112
- 2. Say "message @sethgoldstein hello!" to connect
113
- 3. Say "I'm shipping" to set your status
800
+ šŸ“Ø **Check your messages** — @seth sent you a personalized welcome!
114
801
 
115
- **Reduce permission prompts** (recommended):
116
- \`/permission allow mcp__vibe__*\`
802
+ āš ļø **Using local keys** — consider upgrading to GitHub auth:
803
+ \`vibe init\` — Sign in with GitHub for verified identity
117
804
 
118
- This enables smart auto-approval for safe commands (inbox, pings to friends, etc.).
119
- Sensitive operations (DMs to new contacts, social posts) still require approval.
805
+ ### Onboarding Checklist
806
+ [ ] Read your welcome message from @seth
807
+ [ ] Reply to @seth
808
+ [ ] Message one recommended builder
809
+ [ ] Post your first ship
810
+ [ ] Leave some feedback
120
811
 
121
- _@vibe will DM you shortly with tips._`
812
+ _Say "vibe onboarding" anytime to check your progress_`
122
813
  };
123
814
  }
124
815