slashvibe-mcp 0.2.1 → 0.2.3

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 (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -40
  3. package/config.js +171 -3
  4. package/index.js +139 -16
  5. package/intelligence/index.js +38 -0
  6. package/intelligence/infer.js +316 -0
  7. package/intelligence/patterns.js +651 -0
  8. package/intelligence/proactive.js +358 -0
  9. package/intelligence/serendipity.js +306 -0
  10. package/notify.js +141 -18
  11. package/package.json +9 -4
  12. package/presence.js +5 -1
  13. package/prompts.js +141 -0
  14. package/protocol/index.js +88 -1
  15. package/protocol/telegram-commands.js +199 -0
  16. package/store/api.js +360 -25
  17. package/store/index.js +7 -7
  18. package/store/local.js +67 -11
  19. package/store/profiles.js +287 -0
  20. package/store/reservations.js +321 -0
  21. package/store/skills.js +378 -0
  22. package/tools/_actions.js +270 -14
  23. package/tools/_connection-queue.js +257 -0
  24. package/tools/_discovery-enhanced.js +290 -0
  25. package/tools/_discovery.js +346 -0
  26. package/tools/_proactive-discovery.js +301 -0
  27. package/tools/admin-inbox.js +218 -0
  28. package/tools/agent-treasury.js +288 -0
  29. package/tools/agents.js +122 -0
  30. package/tools/arcade.js +173 -0
  31. package/tools/artifact-create.js +236 -0
  32. package/tools/artifact-view.js +174 -0
  33. package/tools/ask-expert.js +160 -0
  34. package/tools/auto-suggest-connections.js +304 -0
  35. package/tools/away.js +68 -0
  36. package/tools/back.js +51 -0
  37. package/tools/become-expert.js +150 -0
  38. package/tools/bootstrap-skills.js +231 -0
  39. package/tools/bridge-dashboard.js +342 -0
  40. package/tools/bridge-health.js +400 -0
  41. package/tools/bridge-live.js +384 -0
  42. package/tools/bridges.js +383 -0
  43. package/tools/bye.js +4 -0
  44. package/tools/collaborative-drawing.js +286 -0
  45. package/tools/colorguess.js +281 -0
  46. package/tools/crossword.js +369 -0
  47. package/tools/discover-insights.js +379 -0
  48. package/tools/discover-momentum.js +256 -0
  49. package/tools/discover.js +395 -0
  50. package/tools/discovery-analytics.js +345 -0
  51. package/tools/discovery-auto-suggest.js +275 -0
  52. package/tools/discovery-bootstrap.js +267 -0
  53. package/tools/discovery-daily.js +375 -0
  54. package/tools/discovery-dashboard.js +385 -0
  55. package/tools/discovery-digest.js +314 -0
  56. package/tools/discovery-hub.js +357 -0
  57. package/tools/discovery-insights.js +384 -0
  58. package/tools/discovery-momentum.js +281 -0
  59. package/tools/discovery-monitor.js +319 -0
  60. package/tools/discovery-proactive.js +300 -0
  61. package/tools/dm.js +62 -9
  62. package/tools/draw.js +317 -0
  63. package/tools/drawing.js +310 -0
  64. package/tools/echo.js +16 -0
  65. package/tools/farcaster.js +307 -0
  66. package/tools/feed.js +196 -0
  67. package/tools/game.js +218 -110
  68. package/tools/games-catalog.js +376 -0
  69. package/tools/games.js +313 -0
  70. package/tools/genesis.js +233 -0
  71. package/tools/guessnumber.js +194 -0
  72. package/tools/handoff.js +239 -0
  73. package/tools/hangman.js +129 -0
  74. package/tools/help.js +269 -0
  75. package/tools/idea.js +210 -0
  76. package/tools/inbox.js +148 -25
  77. package/tools/init.js +651 -33
  78. package/tools/insights.js +123 -0
  79. package/tools/invite.js +142 -21
  80. package/tools/l2-bridge.js +272 -0
  81. package/tools/l2-status.js +217 -0
  82. package/tools/l2.js +206 -0
  83. package/tools/migrate.js +156 -0
  84. package/tools/mint.js +377 -0
  85. package/tools/multiplayer-game.js +275 -0
  86. package/tools/multiplayer-tictactoe.js +303 -0
  87. package/tools/mute.js +97 -0
  88. package/tools/notifications.js +415 -0
  89. package/tools/observe.js +200 -0
  90. package/tools/onboarding.js +147 -0
  91. package/tools/open.js +14 -2
  92. package/tools/party-game.js +314 -0
  93. package/tools/presence-agent.js +167 -0
  94. package/tools/profile.js +219 -0
  95. package/tools/pulse.js +218 -0
  96. package/tools/react.js +4 -0
  97. package/tools/release.js +83 -0
  98. package/tools/report.js +109 -0
  99. package/tools/reputation.js +175 -0
  100. package/tools/request.js +217 -0
  101. package/tools/reservations.js +116 -0
  102. package/tools/reserve.js +111 -0
  103. package/tools/riddle.js +240 -0
  104. package/tools/run-bootstrap.js +69 -0
  105. package/tools/settings.js +112 -0
  106. package/tools/ship.js +182 -0
  107. package/tools/shipback.js +326 -0
  108. package/tools/skills-analytics.js +349 -0
  109. package/tools/skills-bootstrap.js +301 -0
  110. package/tools/skills-dashboard.js +268 -0
  111. package/tools/skills-exchange.js +342 -0
  112. package/tools/skills.js +380 -0
  113. package/tools/smart-intro.js +353 -0
  114. package/tools/social-inbox.js +326 -69
  115. package/tools/social-post.js +251 -66
  116. package/tools/social-processor.js +445 -0
  117. package/tools/solo-game.js +390 -0
  118. package/tools/start.js +205 -83
  119. package/tools/storybuilder.js +331 -0
  120. package/tools/suggest-tags.js +186 -0
  121. package/tools/tag-suggestions.js +257 -0
  122. package/tools/telegram-bot.js +183 -0
  123. package/tools/telegram-setup.js +214 -0
  124. package/tools/tictactoe.js +155 -0
  125. package/tools/tip.js +120 -0
  126. package/tools/token.js +103 -0
  127. package/tools/twentyquestions.js +143 -0
  128. package/tools/wallet.js +127 -0
  129. package/tools/webhook-test.js +388 -0
  130. package/tools/who.js +118 -25
  131. package/tools/wordassociation.js +247 -0
  132. package/tools/workshop-buddy.js +394 -0
  133. package/tools/workshop.js +327 -0
  134. package/version.json +12 -3
  135. package/tools/board.js +0 -130
@@ -0,0 +1,143 @@
1
+ /**
2
+ * vibe twenty-questions — Play 20 questions guessing game
3
+ *
4
+ * I think of something, you ask yes/no questions to guess what it is!
5
+ */
6
+
7
+ const config = require('../config');
8
+ const store = require('../store');
9
+ const { createGamePayload } = require('../protocol');
10
+ const { requireInit } = require('./_shared');
11
+
12
+ // 20 Questions game implementation
13
+ const twentyQuestions = require('../games/twentyquestions');
14
+
15
+ const definition = {
16
+ name: 'vibe_twenty_questions',
17
+ description: 'Play 20 questions - I think of something, you guess it with yes/no questions',
18
+ inputSchema: {
19
+ type: 'object',
20
+ properties: {
21
+ question: {
22
+ type: 'string',
23
+ description: 'Yes/no question to ask, or "new" to start a new game'
24
+ },
25
+ category: {
26
+ type: 'string',
27
+ description: 'Category for new games (animals, objects, food, places, activities)',
28
+ enum: ['animals', 'objects', 'food', 'places', 'activities']
29
+ }
30
+ },
31
+ required: []
32
+ }
33
+ };
34
+
35
+ /**
36
+ * Get latest 20 questions game state for user
37
+ */
38
+ async function getLatestTwentyQuestionsState(handle) {
39
+ // Get user's thread with themselves (for single-player games)
40
+ const thread = await store.getThread(handle, handle);
41
+
42
+ // Find the most recent 20 questions game
43
+ for (let i = thread.length - 1; i >= 0; i--) {
44
+ const msg = thread[i];
45
+ if (msg.payload?.type === 'game' && msg.payload?.game === 'twentyquestions') {
46
+ return msg.payload.state;
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+
52
+ async function handler(args) {
53
+ const initCheck = requireInit();
54
+ if (initCheck) return initCheck;
55
+
56
+ const { question, category } = args;
57
+ const myHandle = config.getHandle();
58
+
59
+ // Get current game state
60
+ let gameState = await getLatestTwentyQuestionsState(myHandle);
61
+
62
+ // Start new game
63
+ if (!gameState || question === 'new' || (gameState.gameOver && !question)) {
64
+ gameState = twentyQuestions.createInitialTwentyQuestionsState('guess', null, category);
65
+ const payload = createGamePayload('twentyquestions', gameState);
66
+
67
+ const categoryText = category ? ` (${category} category)` : '';
68
+ await store.sendMessage(myHandle, myHandle, `New 20 questions game started${categoryText}! I'm thinking of something...`, 'dm', payload);
69
+
70
+ const display = twentyQuestions.formatTwentyQuestionsDisplay(gameState);
71
+ return {
72
+ display: `## New 20 Questions Game\n\n${display}\n\nAsk yes/no questions: \`vibe twenty-questions --question "Is it alive?"\``
73
+ };
74
+ }
75
+
76
+ // Show current game if no question
77
+ if (!question) {
78
+ const display = twentyQuestions.formatTwentyQuestionsDisplay(gameState);
79
+ const status = gameState.gameOver ?
80
+ '\nGame over! Use `vibe twenty-questions` to start a new game.' :
81
+ '\nAsk a question: `vibe twenty-questions --question "Is it big?"`';
82
+
83
+ return {
84
+ display: `## 20 Questions Game\n\n${display}${status}`
85
+ };
86
+ }
87
+
88
+ // Process question or guess
89
+ let result;
90
+
91
+ // Check if it's a guess (starts with "is it" or "it is")
92
+ const normalizedQuestion = question.toLowerCase().trim();
93
+ if (normalizedQuestion.startsWith('is it ') || normalizedQuestion.startsWith('it is ')) {
94
+ const guess = normalizedQuestion.startsWith('is it ') ?
95
+ normalizedQuestion.substring(6) : normalizedQuestion.substring(6);
96
+ result = twentyQuestions.processGuess(gameState, guess);
97
+ } else {
98
+ result = twentyQuestions.askQuestion(gameState, question);
99
+ }
100
+
101
+ if (result.error) {
102
+ const display = twentyQuestions.formatTwentyQuestionsDisplay(gameState);
103
+ return {
104
+ display: `## 20 Questions Game\n\n${display}\n\n❌ **${result.error}**\n\nTry again: \`vibe twenty-questions --question "Is it alive?"\``
105
+ };
106
+ }
107
+
108
+ // Update game state
109
+ const newGameState = result.gameState;
110
+ const payload = createGamePayload('twentyquestions', newGameState);
111
+
112
+ // Send message with updated state
113
+ let message;
114
+ if (newGameState.gameOver) {
115
+ if (newGameState.won) {
116
+ message = `🎉 Correct! It was "${newGameState.item}"! You guessed it in ${newGameState.moves} questions!`;
117
+ } else {
118
+ message = `💀 Sorry! It was "${newGameState.item}". Better luck next time!`;
119
+ }
120
+ } else if (newGameState.currentQuestion) {
121
+ const { answer } = newGameState.currentQuestion;
122
+ message = `Question ${newGameState.moves}: "${question}" → **${answer.toUpperCase()}**`;
123
+ } else if (newGameState.message) {
124
+ message = newGameState.message;
125
+ } else {
126
+ message = 'Question processed!';
127
+ }
128
+
129
+ await store.sendMessage(myHandle, myHandle, message, 'dm', payload);
130
+
131
+ const display = twentyQuestions.formatTwentyQuestionsDisplay(newGameState);
132
+ const status = newGameState.gameOver ?
133
+ '\n\n🎮 Use `vibe twenty-questions` to start a new game!' :
134
+ newGameState.questionsLeft === 0 ?
135
+ '\n\nNo questions left! Make your final guess: `vibe twenty-questions --question "Is it a cat?"`' :
136
+ '\n\nNext question: `vibe twenty-questions --question "Does it fly?"`';
137
+
138
+ return {
139
+ display: `## 20 Questions Game\n\n${display}${status}`
140
+ };
141
+ }
142
+
143
+ module.exports = { definition, handler };
@@ -0,0 +1,127 @@
1
+ /**
2
+ * vibe_wallet - Check wallet balance and transaction history
3
+ *
4
+ * View your economic state: balance, recent transactions, earnings
5
+ *
6
+ * Examples:
7
+ * - "vibe wallet"
8
+ * - "check my balance"
9
+ * - "show recent transactions"
10
+ */
11
+
12
+ const fetch = require('node-fetch');
13
+
14
+ module.exports = {
15
+ name: 'vibe_wallet',
16
+ description: 'Check your wallet balance and recent transactions. Shows tips sent/received, escrows, and current balance.',
17
+
18
+ inputSchema: {
19
+ type: 'object',
20
+ properties: {
21
+ limit: {
22
+ type: 'number',
23
+ description: 'Number of recent transactions to show (default 10, max 50)',
24
+ default: 10
25
+ },
26
+ show_balance: {
27
+ type: 'boolean',
28
+ description: 'Show current balance (default true)',
29
+ default: true
30
+ }
31
+ }
32
+ },
33
+
34
+ async execute({ limit = 10, show_balance = true }, context) {
35
+ try {
36
+ const handle = context.handle;
37
+
38
+ if (!handle) {
39
+ return {
40
+ success: false,
41
+ error: 'Not authenticated. Use vibe init first.'
42
+ };
43
+ }
44
+
45
+ const apiUrl = process.env.VIBE_API_URL || 'https://www.slashvibe.dev';
46
+
47
+ // Fetch transaction history
48
+ const historyResponse = await fetch(
49
+ `${apiUrl}/api/payments/history?handle=${handle}&limit=${Math.min(limit, 50)}`,
50
+ {
51
+ headers: {
52
+ 'Authorization': `Bearer ${context.token}`
53
+ }
54
+ }
55
+ );
56
+
57
+ const historyResult = await historyResponse.json();
58
+
59
+ if (!historyResponse.ok) {
60
+ return {
61
+ success: false,
62
+ error: 'Failed to fetch wallet data',
63
+ details: historyResult.error
64
+ };
65
+ }
66
+
67
+ const transactions = historyResult.transactions || [];
68
+
69
+ // Calculate balance from transactions (simplified)
70
+ let balance = 0;
71
+ let totalReceived = 0;
72
+ let totalSent = 0;
73
+
74
+ transactions.forEach(tx => {
75
+ if (tx.type?.includes('received') || tx.type === 'escrow_completed') {
76
+ totalReceived += tx.amount || 0;
77
+ } else if (tx.type?.includes('sent') || tx.type === 'escrow_created') {
78
+ totalSent += tx.amount || 0;
79
+ }
80
+ });
81
+
82
+ balance = totalReceived - totalSent;
83
+
84
+ // Format transactions
85
+ const formattedTxs = transactions.slice(0, limit).map(tx => {
86
+ const isReceived = tx.type?.includes('received') || tx.type === 'escrow_completed';
87
+ const icon = isReceived ? '↓' : '↑';
88
+ const peer = isReceived ? tx.from : tx.to;
89
+ const amountStr = isReceived
90
+ ? `+$${tx.amount?.toFixed(2) || '0.00'}`
91
+ : `-$${tx.amount?.toFixed(2) || '0.00'}`;
92
+
93
+ return `${icon} ${amountStr.padEnd(10)} ${peer || 'unknown'} (${tx.status})`;
94
+ });
95
+
96
+ return {
97
+ success: true,
98
+ wallet: {
99
+ handle,
100
+ balance: `$${balance.toFixed(2)}`,
101
+ total_received: `$${totalReceived.toFixed(2)}`,
102
+ total_sent: `$${totalSent.toFixed(2)}`,
103
+ transaction_count: transactions.length
104
+ },
105
+ formatted: `
106
+ 💰 Wallet: ${handle}
107
+
108
+ Balance: $${balance.toFixed(2)}
109
+ Received: $${totalReceived.toFixed(2)}
110
+ Sent: $${totalSent.toFixed(2)}
111
+
112
+ Recent Transactions (${Math.min(limit, transactions.length)}):
113
+ ${formattedTxs.length > 0 ? formattedTxs.join('\n') : ' No transactions yet'}
114
+
115
+ ${historyResult.has_more ? `\n(${transactions.length - limit} more transactions...)` : ''}
116
+ `.trim()
117
+ };
118
+
119
+ } catch (error) {
120
+ return {
121
+ success: false,
122
+ error: 'Failed to fetch wallet',
123
+ details: error.message
124
+ };
125
+ }
126
+ }
127
+ };
@@ -0,0 +1,388 @@
1
+ /**
2
+ * vibe webhook-test — Test webhook endpoints
3
+ *
4
+ * Send test events to webhook endpoints to verify they're working.
5
+ * Useful for debugging X, Telegram, Discord webhook integrations.
6
+ */
7
+
8
+ const { requireInit, header, divider, success, warning, error } = require('./_shared');
9
+
10
+ const definition = {
11
+ name: 'vibe_webhook_test',
12
+ description: 'Test webhook endpoints and view webhook activity',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ endpoint: {
17
+ type: 'string',
18
+ enum: ['x', 'telegram', 'discord', 'all'],
19
+ description: 'Which webhook to test (default: all)'
20
+ },
21
+ action: {
22
+ type: 'string',
23
+ enum: ['test', 'status', 'stats', 'events'],
24
+ description: 'Test webhook, check status, view stats, or list recent events'
25
+ },
26
+ mock_event: {
27
+ type: 'string',
28
+ enum: ['mention', 'dm', 'like', 'follow'],
29
+ description: 'Type of mock event to send (for testing)'
30
+ },
31
+ limit: {
32
+ type: 'number',
33
+ description: 'Number of recent events to show (default: 10)'
34
+ }
35
+ }
36
+ }
37
+ };
38
+
39
+ async function handler(args) {
40
+ const initCheck = requireInit();
41
+ if (initCheck) return initCheck;
42
+
43
+ const {
44
+ endpoint = 'all',
45
+ action = 'status',
46
+ mock_event = 'mention',
47
+ limit = 10
48
+ } = args;
49
+
50
+ try {
51
+ switch (action) {
52
+ case 'test':
53
+ return await testWebhook(endpoint, mock_event);
54
+ case 'status':
55
+ return await checkWebhookStatus(endpoint);
56
+ case 'stats':
57
+ return await getWebhookStats(endpoint);
58
+ case 'events':
59
+ return await getRecentEvents(endpoint, limit);
60
+ default:
61
+ return { display: error('Invalid action') };
62
+ }
63
+ } catch (e) {
64
+ return {
65
+ display: `${header('Webhook Test')}\n\n${error('Test failed: ' + e.message)}`
66
+ };
67
+ }
68
+ }
69
+
70
+ async function testWebhook(endpoint, mockEvent) {
71
+ let display = header('Webhook Test');
72
+ display += '\n\n';
73
+
74
+ if (endpoint === 'x' || endpoint === 'all') {
75
+ display += await testXWebhook(mockEvent);
76
+ display += '\n';
77
+ }
78
+
79
+ if (endpoint === 'telegram' || endpoint === 'all') {
80
+ display += '**Telegram Webhook:** Not yet implemented\n\n';
81
+ }
82
+
83
+ if (endpoint === 'discord' || endpoint === 'all') {
84
+ display += '**Discord Webhook:** Not yet implemented\n\n';
85
+ }
86
+
87
+ display += divider();
88
+ display += '**Next steps:**\n';
89
+ display += '• Check webhook events: `vibe webhook-test --action events`\n';
90
+ display += '• View social inbox: `vibe social-inbox --include_webhooks`\n';
91
+ display += '• Monitor bridge health: `vibe bridge-health`';
92
+
93
+ return { display };
94
+ }
95
+
96
+ async function testXWebhook(mockEvent) {
97
+ let result = '**X (Twitter) Webhook Test**\n';
98
+
99
+ // Check if endpoint exists
100
+ const webhookUrl = '/api/webhooks/x';
101
+ result += `Endpoint: ${webhookUrl}\n`;
102
+
103
+ // Check configuration
104
+ const secret = process.env.X_WEBHOOK_SECRET;
105
+ const configured = !!secret;
106
+ result += `Secret configured: ${configured ? '✅' : '❌'}\n`;
107
+
108
+ if (!configured) {
109
+ result += warning('Set X_WEBHOOK_SECRET environment variable\n');
110
+ return result;
111
+ }
112
+
113
+ // Create mock event
114
+ const mockEvents = {
115
+ mention: {
116
+ tweet_create_events: [{
117
+ id_str: 'test_' + Date.now(),
118
+ text: '@yourusername This is a test mention from webhook-test',
119
+ created_at: new Date().toISOString(),
120
+ user: {
121
+ id_str: 'test_user_123',
122
+ screen_name: 'testuser',
123
+ name: 'Test User',
124
+ profile_image_url_https: 'https://example.com/avatar.jpg'
125
+ },
126
+ entities: {
127
+ user_mentions: [{
128
+ id_str: 'your_user_id',
129
+ screen_name: 'yourusername'
130
+ }]
131
+ }
132
+ }]
133
+ },
134
+ dm: {
135
+ direct_message_events: [{
136
+ id: 'test_dm_' + Date.now(),
137
+ created_timestamp: Date.now().toString(),
138
+ message_create: {
139
+ sender_id: 'test_user_123',
140
+ target: { recipient_id: 'your_user_id' },
141
+ message_data: { text: 'Test DM from webhook-test tool' }
142
+ }
143
+ }]
144
+ },
145
+ like: {
146
+ favorite_events: [{
147
+ id_str: 'test_like_' + Date.now(),
148
+ created_at: new Date().toISOString(),
149
+ user: {
150
+ id_str: 'test_user_123',
151
+ screen_name: 'testuser',
152
+ name: 'Test User',
153
+ profile_image_url_https: 'https://example.com/avatar.jpg'
154
+ },
155
+ favorited_status: {
156
+ id_str: 'your_tweet_123',
157
+ text: 'Your tweet that got liked',
158
+ user: { id_str: 'your_user_id' }
159
+ }
160
+ }]
161
+ },
162
+ follow: {
163
+ follow_events: [{
164
+ source: {
165
+ id_str: 'test_user_123',
166
+ screen_name: 'testuser',
167
+ name: 'Test User',
168
+ profile_image_url_https: 'https://example.com/avatar.jpg',
169
+ description: 'A test user following you'
170
+ },
171
+ target: { id_str: 'your_user_id' },
172
+ created_at: new Date().toISOString()
173
+ }]
174
+ }
175
+ };
176
+
177
+ const testPayload = mockEvents[mockEvent] || mockEvents.mention;
178
+ result += `Mock event: ${mockEvent}\n`;
179
+
180
+ // Generate test signature
181
+ const crypto = require('crypto');
182
+ const body = JSON.stringify(testPayload);
183
+ const signature = crypto
184
+ .createHmac('sha256', secret)
185
+ .update(body)
186
+ .digest('base64');
187
+
188
+ result += `Event size: ${body.length} bytes\n`;
189
+ result += success('Mock event ready for webhook\n');
190
+
191
+ // Note: We don't actually send the request to avoid spamming
192
+ // In a real implementation, this could make an HTTP request to localhost
193
+ result += warning('Note: This creates a mock event structure.\n');
194
+ result += 'To test fully, send POST to /api/webhooks/x with:\n';
195
+ result += `• Header: x-twitter-webhooks-signature: sha256=${signature}\n`;
196
+ result += `• Body: ${body.slice(0, 100)}...\n`;
197
+
198
+ return result;
199
+ }
200
+
201
+ async function checkWebhookStatus(endpoint) {
202
+ let display = header('Webhook Status');
203
+ display += '\n\n';
204
+
205
+ // X webhook status
206
+ if (endpoint === 'x' || endpoint === 'all') {
207
+ const xSecret = process.env.X_WEBHOOK_SECRET;
208
+ const xBearer = process.env.X_BEARER_TOKEN;
209
+
210
+ display += '**X (Twitter) Webhook**\n';
211
+ display += `Endpoint: /api/webhooks/x\n`;
212
+ display += `Secret: ${xSecret ? '✅ Configured' : '❌ Missing'}\n`;
213
+ display += `Bearer Token: ${xBearer ? '✅ Configured' : '❌ Missing'}\n`;
214
+
215
+ if (xSecret && xBearer) {
216
+ display += success('Ready for CRC challenge and events\n');
217
+ } else {
218
+ display += warning('Set X_WEBHOOK_SECRET and X_BEARER_TOKEN\n');
219
+ }
220
+ display += '\n';
221
+ }
222
+
223
+ // General webhook system
224
+ const kvConfigured = !!(process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN);
225
+ display += '**Storage System**\n';
226
+ display += `KV Store: ${kvConfigured ? '✅ Available' : '❌ Not configured'}\n`;
227
+ display += `Social Inbox: ${kvConfigured ? '✅ Active' : '❌ Disabled'}\n`;
228
+
229
+ if (!kvConfigured) {
230
+ display += warning('Set KV_REST_API_URL and KV_REST_API_TOKEN for event storage\n');
231
+ }
232
+ display += '\n';
233
+
234
+ display += divider();
235
+ display += '**Webhook URLs (for X Developer Portal):**\n';
236
+ display += '• Development: `https://yourapp.vercel.app/api/webhooks/x`\n';
237
+ display += '• Production: `https://vibe.fyi/api/webhooks/x`\n\n';
238
+ display += '**Required X Webhook Events:**\n';
239
+ display += '• Tweet create events (mentions)\n';
240
+ display += '• Direct message events\n';
241
+ display += '• Favorite events (optional)\n';
242
+ display += '• Follow events (optional)';
243
+
244
+ return { display };
245
+ }
246
+
247
+ async function getWebhookStats(endpoint) {
248
+ let display = header('Webhook Statistics');
249
+ display += '\n\n';
250
+
251
+ try {
252
+ const KV_CONFIGURED = !!(process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN);
253
+ if (!KV_CONFIGURED) {
254
+ display += warning('KV storage not configured - no stats available\n');
255
+ return { display };
256
+ }
257
+
258
+ const { kv } = await import('@vercel/kv');
259
+
260
+ // X webhook stats
261
+ if (endpoint === 'x' || endpoint === 'all') {
262
+ const xStats = await kv.hgetall('vibe:x_webhook_stats') || {};
263
+
264
+ display += '**X Webhook Stats**\n';
265
+ display += `Total deliveries: ${xStats.total_deliveries || 0}\n`;
266
+ display += `Events processed: ${xStats.events_processed || 0}\n`;
267
+ display += `Last delivery: ${xStats.last_delivery ? new Date(xStats.last_delivery).toLocaleString() : 'Never'}\n`;
268
+
269
+ // Daily stats (last 7 days)
270
+ const today = new Date();
271
+ const dailyStats = [];
272
+ for (let i = 6; i >= 0; i--) {
273
+ const date = new Date(today);
274
+ date.setDate(date.getDate() - i);
275
+ const dateKey = date.toISOString().split('T')[0];
276
+ const count = parseInt(xStats[`deliveries_${dateKey}`]) || 0;
277
+ if (count > 0) {
278
+ dailyStats.push(`${dateKey}: ${count}`);
279
+ }
280
+ }
281
+
282
+ if (dailyStats.length > 0) {
283
+ display += `Daily activity (last 7d): ${dailyStats.join(', ')}\n`;
284
+ }
285
+
286
+ display += '\n';
287
+ }
288
+
289
+ display += divider();
290
+ display += '**Health Check:**\n';
291
+ display += '• Test webhook: `vibe webhook-test --action test --endpoint x`\n';
292
+ display += '• View recent events: `vibe webhook-test --action events --limit 5`\n';
293
+ display += '• Check social inbox: `vibe social-inbox --channel webhooks`';
294
+
295
+ } catch (e) {
296
+ display += error(`Stats unavailable: ${e.message}\n`);
297
+ }
298
+
299
+ return { display };
300
+ }
301
+
302
+ async function getRecentEvents(endpoint, limit) {
303
+ let display = header(`Recent Webhook Events (${limit})`);
304
+ display += '\n\n';
305
+
306
+ try {
307
+ const KV_CONFIGURED = !!(process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN);
308
+ if (!KV_CONFIGURED) {
309
+ display += warning('KV storage not configured - no events available\n');
310
+ return { display };
311
+ }
312
+
313
+ const { kv } = await import('@vercel/kv');
314
+ const inboxKey = 'vibe:social_inbox';
315
+
316
+ const rawEvents = await kv.lrange(inboxKey, 0, limit - 1);
317
+
318
+ if (rawEvents.length === 0) {
319
+ display += '_No recent webhook events._\n\n';
320
+ display += 'Events will appear here when webhooks are triggered.\n';
321
+ display += 'Test with: `vibe webhook-test --action test --endpoint x`';
322
+ return { display };
323
+ }
324
+
325
+ display += `Found ${rawEvents.length} recent events:\n`;
326
+ display += divider();
327
+ display += '\n';
328
+
329
+ for (const eventStr of rawEvents) {
330
+ try {
331
+ const event = JSON.parse(eventStr);
332
+ const timeAgo = formatTimeAgo(new Date(event.timestamp));
333
+
334
+ display += `**${event.platform.toUpperCase()}** ${getTypeIcon(event.type)} — _${timeAgo}_\n`;
335
+ display += `From: @${event.from.handle} (${event.from.name})\n`;
336
+ display += `${event.content.slice(0, 100)}${event.content.length > 100 ? '...' : ''}\n`;
337
+
338
+ if (event.metadata?.url) {
339
+ display += `Link: ${event.metadata.url}\n`;
340
+ }
341
+
342
+ display += `Status: ${event.processed ? '✅ Processed' : '⏳ Pending'}\n`;
343
+ display += `_[${event.id}]_\n\n`;
344
+
345
+ } catch (e) {
346
+ display += `_Invalid event data_\n\n`;
347
+ }
348
+ }
349
+
350
+ display += divider();
351
+ display += '**Actions:**\n';
352
+ display += '• View in social inbox: `vibe social-inbox --include_webhooks`\n';
353
+ display += '• Reply to event: `vibe social-post --content "reply"`\n';
354
+ display += '• Clear events: `vibe webhook-test --action clear` (not implemented)';
355
+
356
+ } catch (e) {
357
+ display += error(`Failed to fetch events: ${e.message}\n`);
358
+ }
359
+
360
+ return { display };
361
+ }
362
+
363
+ function formatTimeAgo(timestamp) {
364
+ const now = Date.now();
365
+ const diff = now - timestamp;
366
+ const minutes = Math.floor(diff / (1000 * 60));
367
+ const hours = Math.floor(minutes / 60);
368
+ const days = Math.floor(hours / 24);
369
+
370
+ if (days > 0) return `${days}d ago`;
371
+ if (hours > 0) return `${hours}h ago`;
372
+ if (minutes > 0) return `${minutes}m ago`;
373
+ return 'just now';
374
+ }
375
+
376
+ function getTypeIcon(type) {
377
+ const icons = {
378
+ mention: '@',
379
+ reply: '↩️',
380
+ dm: '✉️',
381
+ like: '❤️',
382
+ repost: '🔁',
383
+ follow: '👤'
384
+ };
385
+ return icons[type] || '📡';
386
+ }
387
+
388
+ module.exports = { definition, handler };