slashvibe-mcp 0.2.2 → 0.2.4

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 (162) hide show
  1. package/README.md +1 -0
  2. package/analytics.js +107 -0
  3. package/config.js +174 -3
  4. package/index.js +163 -34
  5. package/intelligence/index.js +45 -0
  6. package/intelligence/infer.js +316 -0
  7. package/intelligence/interests.js +369 -0
  8. package/intelligence/patterns.js +651 -0
  9. package/intelligence/proactive.js +358 -0
  10. package/intelligence/serendipity.js +306 -0
  11. package/notification-emitter.js +77 -0
  12. package/notify.js +141 -18
  13. package/package.json +14 -6
  14. package/presence.js +5 -1
  15. package/protocol/index.js +88 -1
  16. package/protocol/telegram-commands.js +199 -0
  17. package/store/api.js +469 -29
  18. package/store/index.js +7 -7
  19. package/store/local.js +67 -11
  20. package/store/profiles.js +435 -0
  21. package/store/reservations.js +321 -0
  22. package/store/skills.js +378 -0
  23. package/tools/_actions.js +491 -22
  24. package/tools/_connection-queue.js +257 -0
  25. package/tools/_discovery-enhanced.js +290 -0
  26. package/tools/_discovery.js +439 -0
  27. package/tools/_proactive-discovery.js +301 -0
  28. package/tools/_shared/index.js +64 -0
  29. package/tools/_work-context.js +338 -0
  30. package/tools/_work-context.manual-test.js +199 -0
  31. package/tools/_work-context.test.js +260 -0
  32. package/tools/admin-inbox.js +218 -0
  33. package/tools/agent-treasury.js +288 -0
  34. package/tools/agents.js +122 -0
  35. package/tools/analytics.js +191 -0
  36. package/tools/approve.js +197 -0
  37. package/tools/arcade.js +173 -0
  38. package/tools/artifact-create.js +247 -0
  39. package/tools/artifact-view.js +174 -0
  40. package/tools/artifacts-price.js +107 -0
  41. package/tools/ask-expert.js +160 -0
  42. package/tools/auto-suggest-connections.js +304 -0
  43. package/tools/away.js +68 -0
  44. package/tools/back.js +51 -0
  45. package/tools/become-expert.js +150 -0
  46. package/tools/bootstrap-skills.js +231 -0
  47. package/tools/bridge-dashboard.js +342 -0
  48. package/tools/bridge-health.js +400 -0
  49. package/tools/bridge-live.js +384 -0
  50. package/tools/bridges.js +383 -0
  51. package/tools/broadcast.js +286 -0
  52. package/tools/bye.js +4 -0
  53. package/tools/chat.js +202 -0
  54. package/tools/collaborative-drawing.js +286 -0
  55. package/tools/colorguess.js +281 -0
  56. package/tools/crossword.js +369 -0
  57. package/tools/discover-insights.js +379 -0
  58. package/tools/discover-momentum.js +256 -0
  59. package/tools/discover.js +675 -0
  60. package/tools/discovery-analytics.js +345 -0
  61. package/tools/discovery-auto-suggest.js +275 -0
  62. package/tools/discovery-bootstrap.js +267 -0
  63. package/tools/discovery-daily.js +375 -0
  64. package/tools/discovery-dashboard.js +385 -0
  65. package/tools/discovery-digest.js +314 -0
  66. package/tools/discovery-hub.js +357 -0
  67. package/tools/discovery-insights.js +384 -0
  68. package/tools/discovery-momentum.js +281 -0
  69. package/tools/discovery-monitor.js +319 -0
  70. package/tools/discovery-proactive.js +300 -0
  71. package/tools/dm.js +84 -14
  72. package/tools/draw.js +317 -0
  73. package/tools/drawing.js +310 -0
  74. package/tools/earnings.js +126 -0
  75. package/tools/echo.js +16 -0
  76. package/tools/farcaster.js +307 -0
  77. package/tools/feed.js +215 -0
  78. package/tools/follow.js +224 -0
  79. package/tools/friends.js +192 -0
  80. package/tools/game.js +218 -110
  81. package/tools/games-catalog.js +376 -0
  82. package/tools/games.js +313 -0
  83. package/tools/genesis.js +233 -0
  84. package/tools/gig-browse.js +206 -0
  85. package/tools/gig-complete.js +139 -0
  86. package/tools/guessnumber.js +194 -0
  87. package/tools/hangman.js +129 -0
  88. package/tools/help.js +269 -0
  89. package/tools/idea.js +217 -0
  90. package/tools/inbox.js +291 -25
  91. package/tools/init.js +657 -33
  92. package/tools/insights.js +123 -0
  93. package/tools/invite.js +142 -21
  94. package/tools/l2-bridge.js +272 -0
  95. package/tools/l2-status.js +217 -0
  96. package/tools/l2.js +206 -0
  97. package/tools/migrate.js +156 -0
  98. package/tools/mint.js +377 -0
  99. package/tools/multiplayer-game.js +275 -0
  100. package/tools/multiplayer-tictactoe.js +303 -0
  101. package/tools/mute.js +97 -0
  102. package/tools/notifications.js +415 -0
  103. package/tools/observe.js +200 -0
  104. package/tools/onboarding.js +147 -0
  105. package/tools/open.js +52 -3
  106. package/tools/party-game.js +314 -0
  107. package/tools/plan.js +225 -0
  108. package/tools/presence-agent.js +167 -0
  109. package/tools/profile.js +219 -0
  110. package/tools/proof-of-work.js +139 -0
  111. package/tools/pulse.js +218 -0
  112. package/tools/react.js +4 -0
  113. package/tools/release.js +83 -0
  114. package/tools/report.js +109 -0
  115. package/tools/reputation.js +175 -0
  116. package/tools/request.js +231 -0
  117. package/tools/reservations.js +116 -0
  118. package/tools/reserve.js +111 -0
  119. package/tools/riddle.js +240 -0
  120. package/tools/run-bootstrap.js +69 -0
  121. package/tools/schedule.js +367 -0
  122. package/tools/session.js +420 -0
  123. package/tools/session_price.js +128 -0
  124. package/tools/settings.js +200 -0
  125. package/tools/ship.js +188 -0
  126. package/tools/shipback.js +326 -0
  127. package/tools/skills-analytics.js +349 -0
  128. package/tools/skills-bootstrap.js +301 -0
  129. package/tools/skills-dashboard.js +268 -0
  130. package/tools/skills-exchange.js +342 -0
  131. package/tools/skills.js +380 -0
  132. package/tools/smart-intro.js +353 -0
  133. package/tools/social-inbox.js +326 -69
  134. package/tools/social-post.js +251 -66
  135. package/tools/social-processor.js +445 -0
  136. package/tools/solo-game.js +390 -0
  137. package/tools/start.js +296 -81
  138. package/tools/status.js +53 -6
  139. package/tools/storybuilder.js +331 -0
  140. package/tools/stuck.js +297 -0
  141. package/tools/subscribe.js +148 -0
  142. package/tools/subscriptions.js +134 -0
  143. package/tools/suggest-tags.js +184 -0
  144. package/tools/tag-suggestions.js +257 -0
  145. package/tools/telegram-bot.js +183 -0
  146. package/tools/telegram-setup.js +214 -0
  147. package/tools/tictactoe.js +155 -0
  148. package/tools/tip.js +120 -0
  149. package/tools/token.js +103 -0
  150. package/tools/twentyquestions.js +143 -0
  151. package/tools/update.js +1 -1
  152. package/tools/wallet.js +127 -0
  153. package/tools/watch.js +157 -0
  154. package/tools/webhook-test.js +388 -0
  155. package/tools/who.js +118 -25
  156. package/tools/withdraw.js +145 -0
  157. package/tools/wordassociation.js +247 -0
  158. package/tools/work-summary.js +96 -0
  159. package/tools/workshop-buddy.js +394 -0
  160. package/tools/workshop.js +327 -0
  161. package/version.json +12 -3
  162. package/tools/board.js +0 -130
package/tools/who.js CHANGED
@@ -12,6 +12,8 @@ const store = require('../store');
12
12
  const notify = require('../notify');
13
13
  const { formatTimeAgo, requireInit } = require('./_shared');
14
14
  const { actions, formatActions } = require('./_actions');
15
+ const { enhanceUsersWithInference } = require('../intelligence/infer');
16
+ const { getTopSerendipity, getAllSerendipity } = require('../intelligence/serendipity');
15
17
 
16
18
  const definition = {
17
19
  name: 'vibe_who',
@@ -22,7 +24,7 @@ const definition = {
22
24
  }
23
25
  };
24
26
 
25
- // Activity heat based on session signals
27
+ // Activity heat based on session signals + GitHub activity
26
28
  function getHeat(user) {
27
29
  const lastSeenMs = user.lastSeen;
28
30
  const now = Date.now();
@@ -32,40 +34,68 @@ function getHeat(user) {
32
34
  if (user.firstSeen) {
33
35
  const sessionDuration = (lastSeenMs - new Date(user.firstSeen).getTime()) / 60000;
34
36
  if (sessionDuration < 5 && minutesAgo < 2) {
35
- return { icon: '✨', label: 'just joined' };
37
+ return { icon: '✨', label: 'just joined', inferred: false };
36
38
  }
37
39
  }
38
40
 
41
+ // GitHub activity signals — real commits are high-confidence
42
+ if (user.github?.shipping_mode === 'hot') {
43
+ const commits = user.github.total_commits || 0;
44
+ const label = commits > 0 ? `shipping code (${commits} commits)` : 'shipping code';
45
+ return { icon: '🔥', label, inferred: false, source: 'github' };
46
+ }
47
+ if (user.github?.shipping_mode === 'active') {
48
+ return { icon: '⚡', label: 'pushing commits', inferred: false, source: 'github' };
49
+ }
50
+
51
+ // Check for inferred state from smart detection
52
+ if (user.mood_inferred && user.mood) {
53
+ const inferredLabel = user.inferred_state
54
+ ? `${user.inferred_state.replace('-', ' ')}`
55
+ : 'active';
56
+ return {
57
+ icon: user.mood,
58
+ label: inferredLabel,
59
+ inferred: true,
60
+ reason: user.mood_reason
61
+ };
62
+ }
63
+
39
64
  // Explicit mood takes priority
40
65
  if (user.mood === '🔥' || user.mood === '🚀') {
41
- return { icon: '🔥', label: 'shipping' };
66
+ return { icon: '🔥', label: 'shipping', inferred: false };
42
67
  }
43
68
  if (user.mood === '🐛') {
44
- return { icon: '🐛', label: 'debugging' };
69
+ return { icon: '🐛', label: 'debugging', inferred: false };
45
70
  }
46
71
  if (user.mood === '🌙') {
47
- return { icon: '🌙', label: 'late night' };
72
+ return { icon: '🌙', label: 'late night', inferred: false };
48
73
  }
49
74
  if (user.mood === '🧠') {
50
- return { icon: '🧠', label: 'deep work' };
75
+ return { icon: '🧠', label: 'deep work', inferred: false };
51
76
  }
52
77
 
53
78
  // Infer from builderMode
54
79
  if (user.builderMode === 'deep-focus') {
55
- return { icon: '🧠', label: 'deep focus' };
80
+ return { icon: '🧠', label: 'deep focus', inferred: false };
56
81
  }
57
82
  if (user.builderMode === 'shipping') {
58
- return { icon: '🔥', label: 'shipping' };
83
+ return { icon: '🔥', label: 'shipping', inferred: false };
84
+ }
85
+
86
+ // GitHub building mode (lower activity but still coding)
87
+ if (user.github?.shipping_mode === 'building') {
88
+ return { icon: '🔨', label: 'building', inferred: false, source: 'github' };
59
89
  }
60
90
 
61
91
  // Default based on recency
62
92
  if (minutesAgo < 2) {
63
- return { icon: '⚡', label: 'active' };
93
+ return { icon: '⚡', label: 'active', inferred: false };
64
94
  }
65
95
  if (minutesAgo < 10) {
66
- return { icon: '●', label: null };
96
+ return { icon: '●', label: null, inferred: false };
67
97
  }
68
- return { icon: '○', label: 'idle' };
98
+ return { icon: '○', label: 'idle', inferred: false };
69
99
  }
70
100
 
71
101
  // Format user's current activity
@@ -80,22 +110,34 @@ function formatActivity(user) {
80
110
  parts.push(`(${user.branch})`);
81
111
  }
82
112
 
83
- // Error they're stuck on
113
+ // Error they're stuck on (highest priority - they might need help)
84
114
  if (user.error) {
85
115
  const shortError = user.error.slice(0, 50) + (user.error.length > 50 ? '...' : '');
86
116
  return `⚠️ _stuck on: ${shortError}_`;
87
117
  }
88
118
 
89
- // Note about what they're doing
119
+ // Combine file + note if both present
120
+ if (user.note && parts.length > 0) {
121
+ return `${parts.join(' ')} — _"${user.note}"_`;
122
+ }
123
+
124
+ // Just note
90
125
  if (user.note) {
91
126
  return `_"${user.note}"_`;
92
127
  }
93
128
 
94
- // File context
129
+ // Just file context
95
130
  if (parts.length > 0) {
96
131
  return parts.join(' ');
97
132
  }
98
133
 
134
+ // GitHub active repos (if no other context and GitHub connected)
135
+ if (user.github?.active_repos?.length > 0) {
136
+ const repos = user.github.active_repos.slice(0, 2);
137
+ const repoNames = repos.map(r => r.split('/').pop()); // Get just repo name
138
+ return `pushing to ${repoNames.join(', ')}`;
139
+ }
140
+
99
141
  // Fall back to one_liner
100
142
  return user.one_liner || 'Building something';
101
143
  }
@@ -104,7 +146,9 @@ async function handler(args) {
104
146
  const initCheck = requireInit();
105
147
  if (initCheck) return initCheck;
106
148
 
107
- const users = await store.getActiveUsers();
149
+ const rawUsers = await store.getActiveUsers();
150
+ // Apply smart detection — infer states from context signals
151
+ const users = enhanceUsersWithInference(rawUsers);
108
152
  const myHandle = config.getHandle();
109
153
 
110
154
  // Check for notifications (presence + messages)
@@ -117,7 +161,7 @@ async function handler(args) {
117
161
  _You're the only one here right now..._
118
162
 
119
163
  🎮 **Challenge someone later**: "play tictactoe with @friend"
120
- 💬 **Ping @seth**: He's probably around somewhere
164
+ 💬 **Message @vibe**: Say hello to the platform!
121
165
  🔗 **Invite a friend**: Share \`slashvibe.dev\`
122
166
 
123
167
  _Check back in a bit — builders come and go._`
@@ -138,26 +182,53 @@ _Check back in a bit — builders come and go._`
138
182
  active.forEach(u => {
139
183
  const isMe = u.handle === myHandle;
140
184
  const tag = isMe ? ' _(you)_' : '';
185
+ const agentBadge = u.is_agent ? ' 🤖' : '';
186
+ const operatorTag = u.is_agent && u.operator ? ` _(op: @${u.operator})_` : '';
141
187
  const heat = getHeat(u);
188
+ // Keep it clean — state speaks for itself
142
189
  const heatLabel = heat.label ? ` ${heat.label}` : '';
143
190
  const activity = formatActivity(u);
144
191
  const timeAgo = formatTimeAgo(u.lastSeen);
145
192
 
146
- display += `${heat.icon} **@${u.handle}**${tag}${heatLabel}\n`;
193
+ display += `${heat.icon} **@${u.handle}**${agentBadge}${tag}${heatLabel}\n`;
194
+ if (operatorTag) {
195
+ display += ` ${operatorTag}\n`;
196
+ }
147
197
  display += ` ${activity}\n`;
148
198
  display += ` _${timeAgo}_\n\n`;
149
199
  });
150
200
  }
151
201
 
152
- // Away section (collapsed)
202
+ // Away section (expanded with messages if present)
153
203
  if (away.length > 0) {
154
- display += `---\n`;
155
- display += `**Away** (${away.length}): `;
156
- display += away.map(u => {
204
+ display += `---\n\n`;
205
+ display += `**Away:**\n`;
206
+
207
+ // Split into users with away messages and without
208
+ const withMessage = away.filter(u => u.awayMessage);
209
+ const withoutMessage = away.filter(u => !u.awayMessage);
210
+
211
+ // Show users with custom away messages (expanded)
212
+ withMessage.forEach(u => {
157
213
  const isMe = u.handle === myHandle;
158
- return `@${u.handle}${isMe ? ' (you)' : ''}`;
159
- }).join(', ');
160
- display += '\n\n';
214
+ const tag = isMe ? ' _(you)_' : '';
215
+ const timeAgo = formatTimeAgo(u.lastSeen);
216
+
217
+ display += `☕ **@${u.handle}**${tag} — _"${u.awayMessage}"_\n`;
218
+ display += ` _${timeAgo}_\n\n`;
219
+ });
220
+
221
+ // Show auto-away users (collapsed) with 💤
222
+ if (withoutMessage.length > 0) {
223
+ withoutMessage.forEach(u => {
224
+ const isMe = u.handle === myHandle;
225
+ const tag = isMe ? ' _(you)_' : '';
226
+ const timeAgo = formatTimeAgo(u.lastSeen);
227
+
228
+ display += `💤 **@${u.handle}**${tag} _(auto-away)_\n`;
229
+ display += ` _${timeAgo}_\n\n`;
230
+ });
231
+ }
161
232
  }
162
233
 
163
234
  // Fun quick actions - randomize suggestions
@@ -176,7 +247,7 @@ _Check back in a bit — builders come and go._`
176
247
  try {
177
248
  const unread = await store.getUnreadCount(myHandle);
178
249
  if (unread > 0) {
179
- display += `\n\n📬 **${unread} unread** — \`vibe inbox\``;
250
+ display += `\n\n📬 **NEW MESSAGE — ${unread} UNREAD** — \`vibe inbox\``;
180
251
  }
181
252
  } catch (e) {}
182
253
 
@@ -185,6 +256,18 @@ _Check back in a bit — builders come and go._`
185
256
  display += `\n\n_The room is lively today!_ ⚡`;
186
257
  }
187
258
 
259
+ // Genesis spots remaining
260
+ try {
261
+ const stats = await store.getStats();
262
+ if (stats.genesis && stats.genesis.genesis_remaining > 0) {
263
+ display += `\n\n🌱 **${stats.genesis.genesis_remaining} genesis spots left** of ${stats.genesis.genesis_cap}`;
264
+ } else if (stats.genesis && stats.genesis.genesis_remaining === 0) {
265
+ display += `\n\n_Genesis is full — ${stats.genesis.total} builders strong_`;
266
+ }
267
+ } catch (e) {
268
+ // Silent fail — genesis display is nice-to-have
269
+ }
270
+
188
271
  // Build response with optional hints for structured flows
189
272
  const response = { display };
190
273
 
@@ -233,6 +316,16 @@ _Check back in a bit — builders come and go._`
233
316
  response.suggestion = topSuggestion;
234
317
  }
235
318
 
319
+ // Serendipity detection — quiet awareness, not loud callouts
320
+ const myUser = users.find(u => u.handle === myHandle);
321
+ if (myUser && active.length > 1) {
322
+ const serendipity = getTopSerendipity(myUser, active);
323
+ if (serendipity && serendipity.relevance > 0.75) {
324
+ // Only surface high-confidence matches, and quietly
325
+ response.serendipity = serendipity;
326
+ }
327
+ }
328
+
236
329
  // Add guided mode actions
237
330
  const onlineHandles = active.filter(u => u.handle !== myHandle).map(u => u.handle);
238
331
  const unreadCount = await store.getUnreadCount(myHandle).catch(() => 0);
@@ -0,0 +1,145 @@
1
+ /**
2
+ * vibe_withdraw - Withdraw earnings from FeeRouter
3
+ *
4
+ * Generates transaction data to withdraw accumulated on-chain earnings.
5
+ * User must sign and submit the transaction with their wallet.
6
+ *
7
+ * Examples:
8
+ * - "vibe withdraw"
9
+ * - "withdraw my earnings"
10
+ * - "claim my on-chain balance"
11
+ */
12
+
13
+ const fetch = require('node-fetch');
14
+ const config = require('../config');
15
+
16
+ const definition = {
17
+ name: 'vibe_withdraw',
18
+ description: 'Withdraw accumulated earnings from the FeeRouter. Generates transaction data for you to sign.',
19
+ inputSchema: {
20
+ type: 'object',
21
+ properties: {
22
+ to_address: {
23
+ type: 'string',
24
+ description: 'Optional destination address (defaults to your registered payout wallet)'
25
+ },
26
+ check_only: {
27
+ type: 'boolean',
28
+ description: 'Only check balance without generating withdrawal transaction'
29
+ }
30
+ }
31
+ }
32
+ };
33
+
34
+ async function handler(args) {
35
+ const { to_address, check_only = false } = args;
36
+
37
+ if (!config.isInitialized()) {
38
+ return {
39
+ display: 'Run `vibe init` first to set your identity.'
40
+ };
41
+ }
42
+
43
+ const handle = config.getHandle();
44
+ const token = config.getToken();
45
+ const apiUrl = process.env.VIBE_API_URL || 'https://www.slashvibe.dev';
46
+
47
+ try {
48
+ // Check-only mode
49
+ if (check_only) {
50
+ const response = await fetch(
51
+ `${apiUrl}/api/earnings/withdraw?handle=${encodeURIComponent(handle)}`,
52
+ {
53
+ headers: token ? {
54
+ 'Authorization': `Bearer ${token}`
55
+ } : {}
56
+ }
57
+ );
58
+
59
+ const result = await response.json();
60
+
61
+ if (!response.ok) {
62
+ return {
63
+ display: `❌ ${result.error || 'Failed to check balance'}`
64
+ };
65
+ }
66
+
67
+ const statusEmoji = result.can_withdraw ? '🟢' : '🟡';
68
+ const action = result.can_withdraw
69
+ ? 'Run "vibe withdraw" to generate withdrawal transaction.'
70
+ : result.registered
71
+ ? 'No balance to withdraw yet.'
72
+ : 'Register on-chain first: vibe register_onchain';
73
+
74
+ return {
75
+ display: `
76
+ 💰 Withdrawal Status: @${handle}
77
+
78
+ Balance: ${result.balance_eth} ETH
79
+ Registered: ${result.registered ? 'Yes' : 'No'}
80
+ Can Withdraw: ${statusEmoji} ${result.can_withdraw ? 'Yes' : 'No'}
81
+
82
+ ${action}
83
+ `.trim(),
84
+ data: result
85
+ };
86
+ }
87
+
88
+ // Generate withdrawal transaction
89
+ const response = await fetch(
90
+ `${apiUrl}/api/earnings/withdraw`,
91
+ {
92
+ method: 'POST',
93
+ headers: {
94
+ 'Content-Type': 'application/json',
95
+ 'Authorization': `Bearer ${token}`
96
+ },
97
+ body: JSON.stringify({
98
+ to_address: to_address || undefined
99
+ })
100
+ }
101
+ );
102
+
103
+ const result = await response.json();
104
+
105
+ if (!response.ok) {
106
+ let display = `❌ ${result.error || 'Failed to generate withdrawal'}`;
107
+ if (result.action) {
108
+ display += `\n\nNext step: ${result.action}`;
109
+ }
110
+ return { display };
111
+ }
112
+
113
+ const destDisplay = result.destination
114
+ ? `${result.destination.slice(0, 10)}...${result.destination.slice(-8)}`
115
+ : 'your wallet';
116
+
117
+ return {
118
+ display: `
119
+ 💸 Withdrawal Ready: @${handle}
120
+
121
+ Amount: ${result.balance_display}
122
+ Destination: ${destDisplay}
123
+ Network: ${result.network}
124
+
125
+ Transaction to sign:
126
+ To: ${result.transaction.to}
127
+ Data: ${result.transaction.data?.slice(0, 40)}...
128
+ Gas: ${result.transaction.gas}
129
+
130
+ Instructions:
131
+ ${result.instructions?.map((i, idx) => ` ${idx + 1}. ${i}`).join('\n')}
132
+
133
+ Explorer: ${result.explorer}
134
+ `.trim(),
135
+ data: result
136
+ };
137
+
138
+ } catch (error) {
139
+ return {
140
+ display: `❌ Failed to process withdrawal: ${error.message}`
141
+ };
142
+ }
143
+ }
144
+
145
+ module.exports = { definition, handler };
@@ -0,0 +1,247 @@
1
+ /**
2
+ * vibe wordassociation — Start or play word association games
3
+ *
4
+ * A multiplayer game where players take turns saying words that associate with the previous word.
5
+ * Build creative chains of connected ideas!
6
+ */
7
+
8
+ const config = require('../config');
9
+ const store = require('../store');
10
+ const { createGamePayload } = require('../protocol');
11
+ const { requireInit, normalizeHandle } = require('./_shared');
12
+
13
+ // Word association game implementation
14
+ const wordassociation = require('../games/wordassociation');
15
+
16
+ const definition = {
17
+ name: 'vibe_wordassociation',
18
+ description: 'Start or play word association games. Players take turns saying related words.',
19
+ inputSchema: {
20
+ type: 'object',
21
+ properties: {
22
+ word: {
23
+ type: 'string',
24
+ description: 'Word to add to the association chain'
25
+ },
26
+ join: {
27
+ type: 'boolean',
28
+ description: 'Join an existing game'
29
+ },
30
+ new: {
31
+ type: 'boolean',
32
+ description: 'Start a new game'
33
+ },
34
+ invite: {
35
+ type: 'string',
36
+ description: 'Invite someone to the game (e.g., @username)'
37
+ }
38
+ },
39
+ required: []
40
+ }
41
+ };
42
+
43
+ /**
44
+ * Find the most recent word association game state in DMs or group context
45
+ */
46
+ async function getGameState(myHandle) {
47
+ // For now, we'll store game state in a special "game room" thread with myself
48
+ // This allows for multiplayer games that persist across sessions
49
+ const gameRoomHandle = 'wordassociation-room';
50
+ const thread = await store.getThread(myHandle, gameRoomHandle);
51
+
52
+ // Find the most recent game payload
53
+ for (let i = thread.length - 1; i >= 0; i--) {
54
+ const msg = thread[i];
55
+ if (msg.payload?.type === 'game' && msg.payload?.game === 'wordassociation') {
56
+ return { gameState: msg.payload.state, thread };
57
+ }
58
+ }
59
+
60
+ return { gameState: null, thread };
61
+ }
62
+
63
+ /**
64
+ * Save game state to the game room
65
+ */
66
+ async function saveGameState(myHandle, gameState, action) {
67
+ const gameRoomHandle = 'wordassociation-room';
68
+ const payload = createGamePayload('wordassociation', gameState);
69
+
70
+ let message;
71
+ if (action.type === 'new') {
72
+ message = 'Started a new word association game!';
73
+ } else if (action.type === 'join') {
74
+ message = `@${action.player} joined the game!`;
75
+ } else if (action.type === 'word') {
76
+ message = `@${action.player} said "${action.word}"`;
77
+ if (gameState.gameOver) {
78
+ message += ' - Game complete! 🎉';
79
+ }
80
+ } else {
81
+ message = 'Game updated';
82
+ }
83
+
84
+ await store.sendMessage(myHandle, gameRoomHandle, message, 'dm', payload);
85
+ }
86
+
87
+ /**
88
+ * Invite someone to play
89
+ */
90
+ async function invitePlayer(myHandle, targetHandle) {
91
+ const them = normalizeHandle(targetHandle);
92
+ const message = `Hey! I started a word association game. Want to join? Use \`vibe wordassociation --join\` to play!`;
93
+ await store.sendMessage(myHandle, them, message, 'dm');
94
+ }
95
+
96
+ async function handler(args) {
97
+ const initCheck = requireInit();
98
+ if (initCheck) return initCheck;
99
+
100
+ const { word, join, new: newGame, invite } = args;
101
+ const myHandle = config.getHandle();
102
+
103
+ // Get current game state
104
+ const { gameState, thread } = await getGameState(myHandle);
105
+
106
+ // Handle inviting someone
107
+ if (invite) {
108
+ await invitePlayer(myHandle, invite);
109
+ return {
110
+ display: `Invited @${normalizeHandle(invite)} to join the word association game! 🧠`
111
+ };
112
+ }
113
+
114
+ // Handle starting a new game
115
+ if (newGame) {
116
+ const newState = wordassociation.createInitialWordAssociationState();
117
+ const addResult = wordassociation.addPlayer(newState, myHandle);
118
+
119
+ if (addResult.error) {
120
+ return { display: addResult.error };
121
+ }
122
+
123
+ await saveGameState(myHandle, addResult.gameState, { type: 'new', player: myHandle });
124
+
125
+ return {
126
+ display: `🧠 **New Word Association Game!**\n\n${wordassociation.formatWordAssociationDisplay(addResult.gameState)}\n\nUse \`vibe wordassociation --word [word]\` to start the chain!\nInvite friends with \`vibe wordassociation --invite @username\``
127
+ };
128
+ }
129
+
130
+ // Handle joining existing game
131
+ if (join) {
132
+ if (!gameState) {
133
+ // Start a new game if none exists
134
+ const newState = wordassociation.createInitialWordAssociationState();
135
+ const addResult = wordassociation.addPlayer(newState, myHandle);
136
+
137
+ if (addResult.error) {
138
+ return { display: addResult.error };
139
+ }
140
+
141
+ await saveGameState(myHandle, addResult.gameState, { type: 'new', player: myHandle });
142
+
143
+ return {
144
+ display: `🧠 **Joined Word Association Game!**\n\n${wordassociation.formatWordAssociationDisplay(addResult.gameState)}\n\nUse \`vibe wordassociation --word [word]\` to start the chain!`
145
+ };
146
+ }
147
+
148
+ if (gameState.gameOver) {
149
+ return { display: 'The current game is over. Use `vibe wordassociation --new` to start a fresh game!' };
150
+ }
151
+
152
+ const addResult = wordassociation.addPlayer(gameState, myHandle);
153
+
154
+ if (addResult.error) {
155
+ return { display: addResult.error };
156
+ }
157
+
158
+ await saveGameState(myHandle, addResult.gameState, { type: 'join', player: myHandle });
159
+
160
+ return {
161
+ display: `🧠 **Joined Word Association Game!**\n\n${wordassociation.formatWordAssociationDisplay(addResult.gameState)}`
162
+ };
163
+ }
164
+
165
+ // Handle making a word move
166
+ if (word) {
167
+ if (!gameState) {
168
+ return { display: 'No active game! Use `vibe wordassociation --new` to start one, or `vibe wordassociation --join` to join.' };
169
+ }
170
+
171
+ if (gameState.gameOver) {
172
+ return { display: 'Game is over! Use `vibe wordassociation --new` to start a fresh game.' };
173
+ }
174
+
175
+ // Try to make the move
176
+ const moveResult = wordassociation.makeMove(gameState, word, myHandle);
177
+
178
+ if (moveResult.error) {
179
+ return { display: `❌ ${moveResult.error}` };
180
+ }
181
+
182
+ await saveGameState(myHandle, moveResult.gameState, {
183
+ type: 'word',
184
+ player: myHandle,
185
+ word: word
186
+ });
187
+
188
+ const display = wordassociation.formatWordAssociationDisplay(moveResult.gameState);
189
+
190
+ // Add fun themes if game is complete
191
+ if (moveResult.gameState.gameOver) {
192
+ const themes = wordassociation.findThemes(moveResult.gameState);
193
+ const stats = wordassociation.getGameStats(moveResult.gameState);
194
+
195
+ let extra = '\n\n**Final Stats:**\n';
196
+ if (stats) {
197
+ extra += `Total words: ${stats.totalWords}\n`;
198
+ extra += `Players: ${stats.contributors}\n`;
199
+ if (themes.length > 0) {
200
+ extra += `\n**Themes found:** ${themes.join(', ')}`;
201
+ }
202
+ }
203
+
204
+ return {
205
+ display: `🧠 **Word Association**\n\n${display}${extra}\n\nStart a new game with \`vibe wordassociation --new\`!`
206
+ };
207
+ }
208
+
209
+ return {
210
+ display: `🧠 **Word Association**\n\n${display}`
211
+ };
212
+ }
213
+
214
+ // Show current game state
215
+ if (gameState) {
216
+ const display = wordassociation.formatWordAssociationDisplay(gameState);
217
+
218
+ if (gameState.gameOver) {
219
+ const themes = wordassociation.findThemes(gameState);
220
+ const stats = wordassociation.getGameStats(gameState);
221
+
222
+ let extra = '\n\n**Final Stats:**\n';
223
+ if (stats) {
224
+ extra += `Total words: ${stats.totalWords}\n`;
225
+ extra += `Players: ${stats.contributors}\n`;
226
+ if (themes.length > 0) {
227
+ extra += `\n**Themes found:** ${themes.join(', ')}`;
228
+ }
229
+ }
230
+
231
+ return {
232
+ display: `🧠 **Word Association**\n\n${display}${extra}\n\nStart a new game with \`vibe wordassociation --new\`!`
233
+ };
234
+ }
235
+
236
+ return {
237
+ display: `🧠 **Word Association**\n\n${display}\n\nUse \`vibe wordassociation --word [word]\` to continue!\nInvite friends: \`vibe wordassociation --invite @username\``
238
+ };
239
+ }
240
+
241
+ // No active game
242
+ return {
243
+ display: `🧠 **Word Association**\n\nNo active game found.\n\n**Commands:**\n• \`vibe wordassociation --new\` - Start a new game\n• \`vibe wordassociation --join\` - Join existing game\n• \`vibe wordassociation --word [word]\` - Add a word\n• \`vibe wordassociation --invite @user\` - Invite someone\n\n**How to play:**\nPlayers take turns saying words that associate with the previous word. Let your creativity flow and see what interesting chains emerge!`
244
+ };
245
+ }
246
+
247
+ module.exports = { definition, handler };