slashvibe-mcp 0.3.20 โ†’ 0.3.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +47 -252
  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 +27 -15
  14. package/games/arcade.js +406 -0
  15. package/games/chess.js +451 -0
  16. package/games/colorguess.js +343 -0
  17. package/games/crossword-words.js +171 -0
  18. package/games/crossword.js +461 -0
  19. package/games/drawing.js +347 -0
  20. package/games/gameroulette.js +300 -0
  21. package/games/gamerouter.js +336 -0
  22. package/games/gamestatus.js +337 -0
  23. package/games/guessnumber.js +209 -0
  24. package/games/hangman.js +279 -0
  25. package/games/memory.js +338 -0
  26. package/games/multiplayer-tictactoe.js +389 -0
  27. package/games/pixelart.js +399 -0
  28. package/games/quickduel.js +354 -0
  29. package/games/riddle.js +371 -0
  30. package/games/rockpaperscissors.js +291 -0
  31. package/games/snake.js +406 -0
  32. package/games/storybuilder.js +343 -0
  33. package/games/tictactoe.js +345 -0
  34. package/games/twentyquestions.js +286 -0
  35. package/games/twotruths.js +207 -0
  36. package/games/werewolf.js +508 -0
  37. package/games/wordassociation.js +247 -0
  38. package/games/wordchain.js +135 -0
  39. package/index.js +116 -159
  40. package/intelligence/index.js +9 -2
  41. package/intelligence/interests.js +369 -0
  42. package/notification-emitter.js +77 -0
  43. package/notify.js +5 -1
  44. package/package.json +21 -16
  45. package/prompts.js +1 -1
  46. package/protocol/index.js +73 -0
  47. package/setup.js +480 -0
  48. package/smart-inbox.js +276 -0
  49. package/store/api.js +536 -215
  50. package/store/profiles.js +160 -12
  51. package/tools/_actions.js +362 -21
  52. package/tools/_discovery.js +119 -26
  53. package/tools/_shared/index.js +64 -0
  54. package/tools/_shared.js +234 -0
  55. package/tools/_work-context.js +338 -0
  56. package/tools/_work-context.manual-test.js +199 -0
  57. package/tools/_work-context.test.js +260 -0
  58. package/tools/activity.js +220 -0
  59. package/tools/analytics.js +191 -0
  60. package/tools/approve.js +197 -0
  61. package/tools/artifact-create.js +14 -3
  62. package/tools/artifacts-price.js +107 -0
  63. package/tools/available.js +120 -0
  64. package/tools/broadcast.js +325 -0
  65. package/tools/chat.js +202 -0
  66. package/tools/collaborative-drawing.js +1 -1
  67. package/tools/connection-status.js +178 -0
  68. package/tools/discover.js +350 -34
  69. package/tools/dm.js +80 -8
  70. package/tools/earnings.js +126 -0
  71. package/tools/feed.js +35 -4
  72. package/tools/follow.js +224 -0
  73. package/tools/friends.js +207 -0
  74. package/tools/gig-browse.js +206 -0
  75. package/tools/gig-complete.js +144 -0
  76. package/tools/health.js +87 -0
  77. package/tools/help.js +3 -3
  78. package/tools/idea.js +9 -2
  79. package/tools/inbox.js +289 -105
  80. package/tools/init.js +131 -34
  81. package/tools/invite.js +15 -4
  82. package/tools/leaderboard.js +117 -0
  83. package/tools/lib/git-apply.js +206 -0
  84. package/tools/lib/git-bundle.js +407 -0
  85. package/tools/migrate.js +3 -3
  86. package/tools/multiplayer-game.js +1 -1
  87. package/tools/onboarding.js +7 -7
  88. package/tools/open.js +143 -12
  89. package/tools/party-game.js +1 -1
  90. package/tools/plan.js +225 -0
  91. package/tools/proof-of-work.js +144 -0
  92. package/tools/reply.js +166 -0
  93. package/tools/report.js +1 -1
  94. package/tools/request.js +17 -3
  95. package/tools/schedule.js +367 -0
  96. package/tools/search-messages.js +123 -0
  97. package/tools/session.js +467 -0
  98. package/tools/session_price.js +128 -0
  99. package/tools/settings.js +90 -2
  100. package/tools/ship.js +30 -7
  101. package/tools/smart-check.js +201 -0
  102. package/tools/start.js +147 -12
  103. package/tools/status.js +53 -6
  104. package/tools/streak.js +147 -0
  105. package/tools/stuck.js +297 -0
  106. package/tools/subscribe.js +148 -0
  107. package/tools/subscriptions.js +134 -0
  108. package/tools/suggest-tags.js +6 -8
  109. package/tools/tag-suggestions.js +1 -1
  110. package/tools/tip.js +150 -77
  111. package/tools/token.js +4 -4
  112. package/tools/update.js +1 -1
  113. package/tools/wallet.js +221 -79
  114. package/tools/watch.js +157 -0
  115. package/tools/who.js +30 -1
  116. package/tools/withdraw.js +145 -0
  117. package/tools/work-summary.js +96 -0
  118. package/version.json +10 -8
  119. package/LICENSE +0 -21
  120. package/store/sqlite.js +0 -347
  121. /package/tools/{auto-suggest-connections.js โ†’ _deprecated/auto-suggest-connections.js} +0 -0
  122. /package/tools/{away.js โ†’ _deprecated/away.js} +0 -0
  123. /package/tools/{back.js โ†’ _deprecated/back.js} +0 -0
  124. /package/tools/{bootstrap-skills.js โ†’ _deprecated/bootstrap-skills.js} +0 -0
  125. /package/tools/{bridge-dashboard.js โ†’ _deprecated/bridge-dashboard.js} +0 -0
  126. /package/tools/{bridge-health.js โ†’ _deprecated/bridge-health.js} +0 -0
  127. /package/tools/{bridge-live.js โ†’ _deprecated/bridge-live.js} +0 -0
  128. /package/tools/{bridges.js โ†’ _deprecated/bridges.js} +0 -0
  129. /package/tools/{colorguess.js โ†’ _deprecated/colorguess.js} +0 -0
  130. /package/tools/{discover-insights.js โ†’ _deprecated/discover-insights.js} +0 -0
  131. /package/tools/{discover-momentum.js โ†’ _deprecated/discover-momentum.js} +0 -0
  132. /package/tools/{discovery-analytics.js โ†’ _deprecated/discovery-analytics.js} +0 -0
  133. /package/tools/{discovery-auto-suggest.js โ†’ _deprecated/discovery-auto-suggest.js} +0 -0
  134. /package/tools/{discovery-bootstrap.js โ†’ _deprecated/discovery-bootstrap.js} +0 -0
  135. /package/tools/{discovery-daily.js โ†’ _deprecated/discovery-daily.js} +0 -0
  136. /package/tools/{discovery-dashboard.js โ†’ _deprecated/discovery-dashboard.js} +0 -0
  137. /package/tools/{discovery-digest.js โ†’ _deprecated/discovery-digest.js} +0 -0
  138. /package/tools/{discovery-hub.js โ†’ _deprecated/discovery-hub.js} +0 -0
  139. /package/tools/{discovery-insights.js โ†’ _deprecated/discovery-insights.js} +0 -0
  140. /package/tools/{discovery-momentum.js โ†’ _deprecated/discovery-momentum.js} +0 -0
  141. /package/tools/{discovery-monitor.js โ†’ _deprecated/discovery-monitor.js} +0 -0
  142. /package/tools/{discovery-proactive.js โ†’ _deprecated/discovery-proactive.js} +0 -0
  143. /package/tools/{draw.js โ†’ _deprecated/draw.js} +0 -0
  144. /package/tools/{farcaster.js โ†’ _deprecated/farcaster.js} +0 -0
  145. /package/tools/{forget.js โ†’ _deprecated/forget.js} +0 -0
  146. /package/tools/{games-catalog.js โ†’ _deprecated/games-catalog.js} +0 -0
  147. /package/tools/{games.js โ†’ _deprecated/games.js} +0 -0
  148. /package/tools/{guessnumber.js โ†’ _deprecated/guessnumber.js} +0 -0
  149. /package/tools/{hangman.js โ†’ _deprecated/hangman.js} +0 -0
  150. /package/tools/{multiplayer-tictactoe.js โ†’ _deprecated/multiplayer-tictactoe.js} +0 -0
  151. /package/tools/{mute.js โ†’ _deprecated/mute.js} +0 -0
  152. /package/tools/{recall.js โ†’ _deprecated/recall.js} +0 -0
  153. /package/tools/{remember.js โ†’ _deprecated/remember.js} +0 -0
  154. /package/tools/{riddle.js โ†’ _deprecated/riddle.js} +0 -0
  155. /package/tools/{run-bootstrap.js โ†’ _deprecated/run-bootstrap.js} +0 -0
  156. /package/tools/{skills-analytics.js โ†’ _deprecated/skills-analytics.js} +0 -0
  157. /package/tools/{skills-bootstrap.js โ†’ _deprecated/skills-bootstrap.js} +0 -0
  158. /package/tools/{skills-dashboard.js โ†’ _deprecated/skills-dashboard.js} +0 -0
  159. /package/tools/{skills-exchange.js โ†’ _deprecated/skills-exchange.js} +0 -0
  160. /package/tools/{skills.js โ†’ _deprecated/skills.js} +0 -0
  161. /package/tools/{smart-intro.js โ†’ _deprecated/smart-intro.js} +0 -0
  162. /package/tools/{storybuilder.js โ†’ _deprecated/storybuilder.js} +0 -0
  163. /package/tools/{telegram-bot.js โ†’ _deprecated/telegram-bot.js} +0 -0
  164. /package/tools/{telegram-setup.js โ†’ _deprecated/telegram-setup.js} +0 -0
  165. /package/tools/{tictactoe.js โ†’ _deprecated/tictactoe.js} +0 -0
  166. /package/tools/{twentyquestions.js โ†’ _deprecated/twentyquestions.js} +0 -0
  167. /package/tools/{wordassociation.js โ†’ _deprecated/wordassociation.js} +0 -0
@@ -0,0 +1,407 @@
1
+ /**
2
+ * Git Bundle Library
3
+ *
4
+ * Creates and validates git bundles for session sharing.
5
+ * Used by broadcast.js to capture code changes during a broadcast.
6
+ *
7
+ * Key functions:
8
+ * - captureSessionStart() - Returns current HEAD commit
9
+ * - createBundle(startCommit) - Creates bundle from startCommit to HEAD
10
+ * - validateBundle(buffer) - Validates bundle integrity
11
+ */
12
+
13
+ const { execSync, spawn } = require('child_process');
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const os = require('os');
17
+
18
+ // Max bundle size (10MB)
19
+ const MAX_BUNDLE_SIZE = 10 * 1024 * 1024;
20
+
21
+ // Files to exclude from bundles (security)
22
+ const EXCLUDED_PATTERNS = [
23
+ '.env',
24
+ '.env.*',
25
+ '*.pem',
26
+ '*.key',
27
+ '*.p12',
28
+ '*.pfx',
29
+ '.npmrc',
30
+ '.netrc',
31
+ 'credentials.json',
32
+ 'secrets.json',
33
+ '*_rsa',
34
+ '*_dsa',
35
+ '*_ed25519',
36
+ '*_ecdsa',
37
+ 'id_rsa*',
38
+ 'id_dsa*',
39
+ '*.keystore',
40
+ ];
41
+
42
+ /**
43
+ * Check if we're in a git repository
44
+ */
45
+ function isGitRepo() {
46
+ try {
47
+ execSync('git rev-parse --is-inside-work-tree', {
48
+ stdio: 'pipe',
49
+ encoding: 'utf8',
50
+ });
51
+ return true;
52
+ } catch (e) {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Get current git branch
59
+ */
60
+ function getCurrentBranch() {
61
+ try {
62
+ return execSync('git rev-parse --abbrev-ref HEAD', {
63
+ stdio: 'pipe',
64
+ encoding: 'utf8',
65
+ }).trim();
66
+ } catch (e) {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Get current HEAD commit hash
73
+ */
74
+ function getHeadCommit() {
75
+ try {
76
+ return execSync('git rev-parse HEAD', {
77
+ stdio: 'pipe',
78
+ encoding: 'utf8',
79
+ }).trim();
80
+ } catch (e) {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Get commit count between two commits
87
+ */
88
+ function getCommitCount(startCommit, endCommit = 'HEAD') {
89
+ try {
90
+ const output = execSync(`git rev-list --count ${startCommit}..${endCommit}`, {
91
+ stdio: 'pipe',
92
+ encoding: 'utf8',
93
+ });
94
+ return parseInt(output.trim(), 10);
95
+ } catch (e) {
96
+ return 0;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Get list of files changed between commits
102
+ */
103
+ function getChangedFiles(startCommit, endCommit = 'HEAD') {
104
+ try {
105
+ const output = execSync(`git diff --name-only ${startCommit}..${endCommit}`, {
106
+ stdio: 'pipe',
107
+ encoding: 'utf8',
108
+ });
109
+ return output.trim().split('\n').filter(Boolean);
110
+ } catch (e) {
111
+ return [];
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Check if any excluded files would be in the bundle
117
+ */
118
+ function hasExcludedFiles(changedFiles) {
119
+ const excluded = [];
120
+
121
+ for (const file of changedFiles) {
122
+ const basename = path.basename(file);
123
+
124
+ for (const pattern of EXCLUDED_PATTERNS) {
125
+ // Simple glob matching
126
+ if (pattern.startsWith('*')) {
127
+ const suffix = pattern.slice(1);
128
+ if (basename.endsWith(suffix)) {
129
+ excluded.push(file);
130
+ break;
131
+ }
132
+ } else if (pattern.endsWith('*')) {
133
+ const prefix = pattern.slice(0, -1);
134
+ if (basename.startsWith(prefix)) {
135
+ excluded.push(file);
136
+ break;
137
+ }
138
+ } else if (pattern.includes('*')) {
139
+ // Pattern like ".env.*"
140
+ const [prefix, suffix] = pattern.split('*');
141
+ if (basename.startsWith(prefix) && (suffix === '' || basename.endsWith(suffix))) {
142
+ excluded.push(file);
143
+ break;
144
+ }
145
+ } else if (basename === pattern || file === pattern) {
146
+ excluded.push(file);
147
+ break;
148
+ }
149
+ }
150
+ }
151
+
152
+ return excluded;
153
+ }
154
+
155
+ /**
156
+ * Capture git state at session start
157
+ * @returns {object} - { success, commit, branch, error? }
158
+ */
159
+ function captureSessionStart() {
160
+ if (!isGitRepo()) {
161
+ return { success: false, error: 'Not a git repository' };
162
+ }
163
+
164
+ const commit = getHeadCommit();
165
+ const branch = getCurrentBranch();
166
+
167
+ if (!commit) {
168
+ return { success: false, error: 'Could not get HEAD commit' };
169
+ }
170
+
171
+ return {
172
+ success: true,
173
+ commit,
174
+ branch,
175
+ capturedAt: new Date().toISOString(),
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Create a git bundle from startCommit to HEAD
181
+ * @param {string} startCommit - Starting commit hash
182
+ * @returns {object} - { success, buffer, metadata, error? }
183
+ */
184
+ function createBundle(startCommit) {
185
+ if (!isGitRepo()) {
186
+ return { success: false, error: 'Not a git repository' };
187
+ }
188
+
189
+ if (!startCommit) {
190
+ return { success: false, error: 'Start commit required' };
191
+ }
192
+
193
+ const endCommit = getHeadCommit();
194
+ const branch = getCurrentBranch();
195
+
196
+ if (!endCommit) {
197
+ return { success: false, error: 'Could not get HEAD commit' };
198
+ }
199
+
200
+ // Check if there are any new commits
201
+ const commitCount = getCommitCount(startCommit, endCommit);
202
+ if (commitCount === 0) {
203
+ return {
204
+ success: false,
205
+ error: 'No new commits since session start',
206
+ metadata: {
207
+ startCommit,
208
+ endCommit,
209
+ branch,
210
+ },
211
+ };
212
+ }
213
+
214
+ // Check for excluded files
215
+ const changedFiles = getChangedFiles(startCommit, endCommit);
216
+ const excludedFiles = hasExcludedFiles(changedFiles);
217
+
218
+ if (excludedFiles.length > 0) {
219
+ console.log(`[git-bundle] Warning: Excluding sensitive files: ${excludedFiles.join(', ')}`);
220
+ }
221
+
222
+ // Create temp file for bundle
223
+ const tmpDir = os.tmpdir();
224
+ const bundlePath = path.join(tmpDir, `vibe-bundle-${Date.now()}.bundle`);
225
+
226
+ try {
227
+ // Create the bundle
228
+ // Format: git bundle create <file> <range>
229
+ const bundleRange = `${startCommit}..HEAD`;
230
+
231
+ execSync(`git bundle create "${bundlePath}" ${bundleRange}`, {
232
+ stdio: 'pipe',
233
+ encoding: 'utf8',
234
+ });
235
+
236
+ // Read the bundle
237
+ const buffer = fs.readFileSync(bundlePath);
238
+
239
+ // Check size
240
+ if (buffer.length > MAX_BUNDLE_SIZE) {
241
+ fs.unlinkSync(bundlePath);
242
+ return {
243
+ success: false,
244
+ error: `Bundle too large (${Math.round(buffer.length / 1024 / 1024)}MB > ${MAX_BUNDLE_SIZE / 1024 / 1024}MB limit)`,
245
+ };
246
+ }
247
+
248
+ // Clean up temp file
249
+ fs.unlinkSync(bundlePath);
250
+
251
+ const metadata = {
252
+ initialCommit: startCommit,
253
+ finalCommit: endCommit,
254
+ branch,
255
+ commitCount,
256
+ changedFiles: changedFiles.filter((f) => !excludedFiles.includes(f)),
257
+ excludedFiles,
258
+ size: buffer.length,
259
+ createdAt: new Date().toISOString(),
260
+ };
261
+
262
+ console.log(`[git-bundle] Created bundle: ${commitCount} commits, ${changedFiles.length} files, ${buffer.length} bytes`);
263
+
264
+ return {
265
+ success: true,
266
+ buffer,
267
+ metadata,
268
+ };
269
+ } catch (error) {
270
+ // Clean up on error
271
+ try {
272
+ if (fs.existsSync(bundlePath)) {
273
+ fs.unlinkSync(bundlePath);
274
+ }
275
+ } catch (e) {
276
+ // Ignore cleanup errors
277
+ }
278
+
279
+ console.error('[git-bundle] Failed to create bundle:', error.message);
280
+ return { success: false, error: error.message };
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Validate a git bundle
286
+ * @param {Buffer} buffer - The bundle data
287
+ * @returns {object} - { success, info, error? }
288
+ */
289
+ function validateBundle(buffer) {
290
+ if (!buffer || buffer.length === 0) {
291
+ return { success: false, error: 'Empty buffer' };
292
+ }
293
+
294
+ // Check magic bytes (git bundles start with "# v2 git bundle" or "# v3 git bundle")
295
+ const header = buffer.slice(0, 20).toString('utf8');
296
+ if (!header.startsWith('# v2 git bundle') && !header.startsWith('# v3 git bundle')) {
297
+ return { success: false, error: 'Invalid bundle format' };
298
+ }
299
+
300
+ // Write to temp and verify
301
+ const tmpDir = os.tmpdir();
302
+ const bundlePath = path.join(tmpDir, `vibe-verify-${Date.now()}.bundle`);
303
+
304
+ try {
305
+ fs.writeFileSync(bundlePath, buffer);
306
+
307
+ // Verify the bundle
308
+ const output = execSync(`git bundle verify "${bundlePath}"`, {
309
+ stdio: 'pipe',
310
+ encoding: 'utf8',
311
+ });
312
+
313
+ fs.unlinkSync(bundlePath);
314
+
315
+ return {
316
+ success: true,
317
+ info: output.trim(),
318
+ size: buffer.length,
319
+ };
320
+ } catch (error) {
321
+ try {
322
+ if (fs.existsSync(bundlePath)) {
323
+ fs.unlinkSync(bundlePath);
324
+ }
325
+ } catch (e) {
326
+ // Ignore cleanup errors
327
+ }
328
+
329
+ return { success: false, error: error.message };
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Apply a bundle to create a new branch (for forking)
335
+ * @param {Buffer} buffer - The bundle data
336
+ * @param {string} branchName - Name for the new branch
337
+ * @returns {object} - { success, branch, error? }
338
+ */
339
+ function applyBundle(buffer, branchName = 'forked-session') {
340
+ if (!isGitRepo()) {
341
+ return { success: false, error: 'Not a git repository' };
342
+ }
343
+
344
+ const validation = validateBundle(buffer);
345
+ if (!validation.success) {
346
+ return { success: false, error: `Invalid bundle: ${validation.error}` };
347
+ }
348
+
349
+ const tmpDir = os.tmpdir();
350
+ const bundlePath = path.join(tmpDir, `vibe-apply-${Date.now()}.bundle`);
351
+
352
+ try {
353
+ fs.writeFileSync(bundlePath, buffer);
354
+
355
+ // Get the refs in the bundle
356
+ const listOutput = execSync(`git bundle list-heads "${bundlePath}"`, {
357
+ stdio: 'pipe',
358
+ encoding: 'utf8',
359
+ });
360
+
361
+ const refs = listOutput.trim().split('\n');
362
+ if (refs.length === 0) {
363
+ throw new Error('No refs in bundle');
364
+ }
365
+
366
+ // Get the first commit hash from the bundle
367
+ const firstRef = refs[0].split(/\s+/)[0];
368
+
369
+ // Unbundle (fetch from bundle)
370
+ execSync(`git fetch "${bundlePath}" ${firstRef}:${branchName}`, {
371
+ stdio: 'pipe',
372
+ });
373
+
374
+ fs.unlinkSync(bundlePath);
375
+
376
+ console.log(`[git-bundle] Applied bundle to branch: ${branchName}`);
377
+
378
+ return {
379
+ success: true,
380
+ branch: branchName,
381
+ refs: refs.map((r) => r.split(/\s+/)),
382
+ };
383
+ } catch (error) {
384
+ try {
385
+ if (fs.existsSync(bundlePath)) {
386
+ fs.unlinkSync(bundlePath);
387
+ }
388
+ } catch (e) {
389
+ // Ignore cleanup errors
390
+ }
391
+
392
+ console.error('[git-bundle] Failed to apply bundle:', error.message);
393
+ return { success: false, error: error.message };
394
+ }
395
+ }
396
+
397
+ module.exports = {
398
+ isGitRepo,
399
+ getCurrentBranch,
400
+ getHeadCommit,
401
+ captureSessionStart,
402
+ createBundle,
403
+ validateBundle,
404
+ applyBundle,
405
+ MAX_BUNDLE_SIZE,
406
+ EXCLUDED_PATTERNS,
407
+ };
package/tools/migrate.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * vibe migrate โ€” Migrate existing handle to GitHub auth
3
3
  *
4
4
  * For users who have existing handles with local keypairs,
5
- * this command helps them migrate to the new Privy/GitHub auth.
5
+ * this command helps them migrate to GitHub OAuth auth.
6
6
  *
7
7
  * Flow:
8
8
  * 1. Check if user has existing handle and keys
@@ -36,8 +36,8 @@ If you're new, use \`vibe init @yourhandle "what you're building"\` instead.`
36
36
  };
37
37
  }
38
38
 
39
- // Check if already using Privy auth
40
- if (config.hasPrivyAuth()) {
39
+ // Check if already using GitHub auth
40
+ if (config.hasOAuth()) {
41
41
  return {
42
42
  display: `โœ… **Already using GitHub auth**
43
43
 
@@ -105,7 +105,7 @@ async function handler(args) {
105
105
  const roomId = roomName || 'default';
106
106
 
107
107
  // Get current game state
108
- let gameState = getGameRoom(game, roomId);
108
+ const gameState = getGameRoom(game, roomId);
109
109
 
110
110
  try {
111
111
  if (game === 'drawing') {
@@ -110,23 +110,23 @@ Error: ${response.error || 'Unknown error'}`
110
110
  if (nextTask) {
111
111
  switch (nextTask.id) {
112
112
  case 'read_welcome':
113
- display += 'โ†’ Say **"check my messages"** to read @vibe\'s welcome\n';
113
+ display += 'โ†’ Say **"check my messages"** to read @seth\'s welcome\n';
114
114
  break;
115
115
  case 'reply_seth':
116
- display += 'โ†’ Say **"reply to vibe"** or **"dm @vibe hi!"**\n';
116
+ display += 'โ†’ Say **"reply to seth"** or **"dm @seth hi!"**\n';
117
117
  break;
118
- case 'message_builder':
119
- display += 'โ†’ Say **"who\'s around?"** then message someone\n';
118
+ case 'find_github_friends':
119
+ display += 'โ†’ Say **"find my github friends"** to see who you know! ๐Ÿ”ฅ\n';
120
120
  break;
121
121
  case 'first_ship':
122
122
  display += 'โ†’ Say **"ship something"** or **"I shipped X"**\n';
123
123
  break;
124
+ case 'share_ship':
125
+ display += 'โ†’ After shipping, click the Twitter link to share! ๐Ÿฆ\n';
126
+ break;
124
127
  case 'invite_friend':
125
128
  display += 'โ†’ Say **"invite a friend"** or **"share my invite link"**\n';
126
129
  break;
127
- case 'leave_feedback':
128
- display += 'โ†’ Say **"echo feedback about X"** or **"give feedback"**\n';
129
- break;
130
130
  }
131
131
  }
132
132
  }
package/tools/open.js CHANGED
@@ -8,6 +8,15 @@ const memory = require('../memory');
8
8
  const patterns = require('../intelligence/patterns');
9
9
  const { formatPayload } = require('../protocol');
10
10
  const { requireInit, normalizeHandle } = require('./_shared');
11
+ const { actions, formatActions } = require('./_actions');
12
+
13
+ // Truncate message for preview (first 100 chars, clean break at word)
14
+ function summarizeMessage(text, maxLen = 100) {
15
+ if (!text || text.length <= maxLen) return text;
16
+ const truncated = text.slice(0, maxLen);
17
+ const lastSpace = truncated.lastIndexOf(' ');
18
+ return (lastSpace > maxLen * 0.7 ? truncated.slice(0, lastSpace) : truncated) + '...';
19
+ }
11
20
 
12
21
  const definition = {
13
22
  name: 'vibe_open',
@@ -46,38 +55,152 @@ async function handler(args) {
46
55
  patterns.logMessageReceived(them);
47
56
  }
48
57
 
49
- // Check if they're typing
58
+ // Check if they're typing (cross-platform: synced via KV)
50
59
  let typingNotice = '';
51
60
  try {
52
61
  const typingUsers = await store.getTypingUsers(myHandle);
53
- if (typingUsers.includes(them)) {
62
+ const isTyping = typingUsers.some(t => t.handle?.toLowerCase() === them.toLowerCase());
63
+ if (isTyping) {
54
64
  typingNotice = `\n_@${them} is typing..._\n`;
55
65
  }
56
66
  } catch (e) {}
57
67
 
68
+ // Get their presence status (for "last seen" display)
69
+ let presenceStatus = '';
70
+ try {
71
+ const activeUsers = await store.getActiveUsers();
72
+ const theirPresence = activeUsers.find(u => u.handle?.toLowerCase() === them.toLowerCase());
73
+
74
+ if (theirPresence) {
75
+ if (theirPresence.status === 'active') {
76
+ presenceStatus = '๐ŸŸข online now';
77
+ } else if (theirPresence.status === 'away') {
78
+ const ago = store.formatTimeAgo(theirPresence.lastSeen);
79
+ presenceStatus = `โ˜• away ยท last seen ${ago}`;
80
+ } else {
81
+ const ago = store.formatTimeAgo(theirPresence.lastSeen);
82
+ presenceStatus = `last seen ${ago}`;
83
+ }
84
+ }
85
+ } catch (e) {}
86
+
58
87
  if (thread.length === 0) {
59
- return {
60
- display: `## @${them}
88
+ const statusLine = presenceStatus ? `\n_${presenceStatus}_` : '';
89
+
90
+ // Fetch conversation starters to help break the ice
91
+ let starters = [];
92
+ let starterContext = {};
93
+ try {
94
+ const apiUrl = config.getApiUrl();
95
+ const promptsResponse = await fetch(`${apiUrl}/api/prompts?from=${myHandle}&to=${them}`, {
96
+ headers: { 'User-Agent': 'vibe-mcp-client' }
97
+ });
98
+ if (promptsResponse.ok) {
99
+ const data = await promptsResponse.json();
100
+ if (data.success && data.prompts) {
101
+ starters = data.prompts.slice(0, 3);
102
+ starterContext = data.context || {};
103
+ }
104
+ }
105
+ } catch (e) {}
106
+
107
+ // Build display with conversation starters
108
+ let emptyDisplay = `## @${them}${statusLine}
61
109
 
62
110
  _No messages yet._${typingNotice}
111
+ `;
112
+
113
+ if (starters.length > 0) {
114
+ emptyDisplay += `\n**๐Ÿ’ฌ Conversation starters:**\n`;
115
+ starters.forEach((starter, i) => {
116
+ emptyDisplay += `${i + 1}. "${starter}"\n`;
117
+ });
118
+ emptyDisplay += `\n_Copy one or say your own!_`;
119
+ } else {
120
+ emptyDisplay += `\nSay "message ${them} hello" to start`;
121
+ }
122
+
123
+ // Build response with actions for quick-send
124
+ const response = { display: emptyDisplay };
125
+
126
+ if (starters.length > 0) {
127
+ response.actions = formatActions(starters.map((starter, i) => ({
128
+ label: `Send #${i + 1}`,
129
+ action: `vibe dm @${them} ${starter}`,
130
+ description: starter.slice(0, 40) + (starter.length > 40 ? '...' : '')
131
+ })));
132
+ response.conversationStarters = starters;
133
+ response.context = starterContext;
134
+ }
63
135
 
64
- Say "message ${them} hello" to start`
65
- };
136
+ return response;
66
137
  }
67
138
 
68
- // Check if they're an agent (from first message if available)
69
- const theirMessage = thread.find(m => m.from === them);
70
- const agentIndicator = theirMessage?.isAgent ? ' ๐Ÿค–' : '';
139
+ // Get latest message from them for the summary preview
140
+ const latestFromThem = theirMessages.length > 0
141
+ ? theirMessages.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))[0]
142
+ : null;
143
+
144
+ // Build the preview summary line (what appears in collapsed view)
145
+ let display = '';
146
+
147
+ // Header with presence status
148
+ const statusSuffix = presenceStatus ? ` ยท _${presenceStatus}_` : '';
71
149
 
72
- let display = `## Thread with @${them}${agentIndicator}\n\n`;
150
+ if (latestFromThem) {
151
+ const agentBadge = latestFromThem.isAgent ? ' ๐Ÿค–' : '';
152
+ const time = store.formatTimeAgo(latestFromThem.timestamp);
153
+ const preview = latestFromThem.body
154
+ ? summarizeMessage(latestFromThem.body)
155
+ : (latestFromThem.payload ? '[attachment]' : '');
73
156
 
74
- thread.forEach(m => {
157
+ display = `๐Ÿ’ฌ @${them}${agentBadge}${statusSuffix}\n> (${time}): "${preview}"\n\n`;
158
+ } else {
159
+ // No messages from them yet - you sent first
160
+ display = `๐Ÿ’ฌ @${them}${statusSuffix}\n> _Waiting for reply..._\n\n`;
161
+ }
162
+
163
+ // Thread section - sorted newest first so their latest message appears at top
164
+ display += `---\n๐Ÿ“œ Thread\n\n`;
165
+
166
+ const sortedThread = [...thread].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
167
+
168
+ // Build a map for reply lookups
169
+ const messageMap = {};
170
+ thread.forEach(m => { messageMap[m.id] = m; });
171
+
172
+ sortedThread.forEach(m => {
75
173
  const isMe = m.from === myHandle;
76
174
  const agentBadge = m.isAgent && !isMe ? '๐Ÿค– ' : '';
77
175
  const sender = isMe ? 'you' : `@${m.from}`;
78
176
  const time = store.formatTimeAgo(m.timestamp);
79
177
 
80
- display += `${agentBadge}**${sender}** โ€” _${time}_\n`;
178
+ // Status indicator for sent messages: โœ“ sent, โœ“โœ“ delivered, โœ“โœ“ read
179
+ let statusIndicator = '';
180
+ if (isMe) {
181
+ if (m.status === 'read' || m.readByThem) {
182
+ statusIndicator = ' โœ“โœ“ read';
183
+ } else if (m.status === 'delivered' || m.delivered) {
184
+ statusIndicator = ' โœ“โœ“';
185
+ } else {
186
+ statusIndicator = ' โœ“';
187
+ }
188
+ }
189
+
190
+ display += `${agentBadge}**${sender}** โ€” _${time}${statusIndicator}_\n`;
191
+
192
+ // Show reply context if this is a threaded reply
193
+ if (m.reply_to_id || m.replyToId) {
194
+ const replyToId = m.reply_to_id || m.replyToId;
195
+ const originalMsg = messageMap[replyToId];
196
+ if (originalMsg) {
197
+ const originalSender = originalMsg.from === myHandle ? 'you' : `@${originalMsg.from}`;
198
+ const preview = (originalMsg.body || '').substring(0, 50);
199
+ display += `> โ†ฉ replying to ${originalSender}: "${preview}${preview.length >= 50 ? '...' : ''}"\n`;
200
+ } else {
201
+ display += `> โ†ฉ _replying to earlier message_\n`;
202
+ }
203
+ }
81
204
 
82
205
  // Show text if present
83
206
  if (m.body) {
@@ -114,6 +237,14 @@ Say "message ${them} hello" to start`
114
237
  response.reason = 'long_thread';
115
238
  }
116
239
 
240
+ // Add smart reply actions based on their last message
241
+ if (latestFromThem && latestFromThem.body) {
242
+ response.actions = formatActions(actions.afterOpenThread(them, latestFromThem.body));
243
+ } else {
244
+ // No message from them yet - offer generic options
245
+ response.actions = formatActions(actions.afterOpenThread(them, ''));
246
+ }
247
+
117
248
  return response;
118
249
  }
119
250
 
@@ -103,7 +103,7 @@ async function handler(args) {
103
103
  const gameKey = getGameKey(game, room || myHandle);
104
104
 
105
105
  // Get or create game state
106
- let gameState = activeGames[gameKey];
106
+ const gameState = activeGames[gameKey];
107
107
 
108
108
  if (game === 'twotruths') {
109
109
  return handleTwoTruths(args, myHandle, gameKey, gameState);