slashvibe-mcp 0.3.19 → 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.
package/README.md CHANGED
@@ -1,6 +1,19 @@
1
1
  # vibe-mcp
2
2
 
3
- Social layer for Claude Code. DMs, presence, and connection between AI-assisted developers.
3
+ Social layer for AI-assisted coding. DMs, presence, and connection between developers.
4
+
5
+ **Works in:** Claude Code • Cursor • Any MCP-compatible IDE
6
+
7
+ ## Quick Start
8
+
9
+ **Claude Code:**
10
+ ```bash
11
+ npx slashvibe-mcp setup
12
+ ```
13
+
14
+ **Cursor:** See [Cursor Setup Guide](docs/CURSOR_SETUP.md)
15
+
16
+ **Other IDEs:** Any editor supporting MCP protocol can use the manual config below.
4
17
 
5
18
  ## Installation
6
19
 
package/index.js CHANGED
@@ -99,10 +99,11 @@ async function getPresenceFooter() {
99
99
  const handle = config.getHandle();
100
100
  if (!handle) return '';
101
101
 
102
- // Fetch presence and unread in parallel
103
- const [users, unreadCount] = await Promise.all([
102
+ // Fetch presence, unread, and live broadcasts in parallel
103
+ const [users, unreadCount, liveCount] = await Promise.all([
104
104
  store.getActiveUsers().catch(() => []),
105
- store.getUnreadCount(handle).catch(() => 0)
105
+ store.getUnreadCount(handle).catch(() => 0),
106
+ store.getLiveBroadcastCount().catch(() => 0)
106
107
  ]);
107
108
 
108
109
  // Filter out self
@@ -125,7 +126,7 @@ async function getPresenceFooter() {
125
126
  // Build the visible footer
126
127
  let footer = '\n\n────────────────────────────────────────\n';
127
128
 
128
- // Line 1: vibe · X online · Y unread
129
+ // Line 1: vibe · X online · Y unread · Z live
129
130
  const parts = ['vibe'];
130
131
  if (onlineCount > 0) {
131
132
  parts.push(`${onlineCount} online`);
@@ -133,6 +134,9 @@ async function getPresenceFooter() {
133
134
  if (unreadCount > 0) {
134
135
  parts.push(`**${unreadCount} unread**`);
135
136
  }
137
+ if (liveCount > 0) {
138
+ parts.push(`🔴 ${liveCount} live`);
139
+ }
136
140
  footer += parts.join(' · ');
137
141
 
138
142
  // Line 2: Activity hints (if anyone is online)
@@ -154,8 +158,10 @@ async function getPresenceFooter() {
154
158
  }
155
159
  });
156
160
  footer += hints.join(' · ');
157
- } else if (unreadCount === 0) {
158
- footer += '\n_room is quiet_';
161
+ } else if (unreadCount === 0 && liveCount === 0) {
162
+ footer += '\n_room is quiet · `vibe broadcast start` to go live_';
163
+ } else if (unreadCount === 0 && liveCount > 0) {
164
+ footer += '\n_no messages · watch live broadcasts with `vibe watch`_';
159
165
  }
160
166
 
161
167
  footer += '\n────────────────────────────────────────';
@@ -281,7 +287,13 @@ const coreTools = {
281
287
  vibe_withdraw: require('./tools/withdraw'),
282
288
  vibe_session_price: require('./tools/session_price'),
283
289
  vibe_subscribe: require('./tools/subscribe'),
284
- vibe_subscriptions: require('./tools/subscriptions')
290
+ vibe_subscriptions: require('./tools/subscriptions'),
291
+ // NFT Minting — Mint artifacts to VIBE L2, Base, or Ethereum
292
+ vibe_mint: require('./tools/mint'),
293
+ // Platform Status — Health, leaderboard, streaks
294
+ vibe_health: require('./tools/health'),
295
+ vibe_leaderboard: require('./tools/leaderboard'),
296
+ vibe_streak: require('./tools/streak')
285
297
  };
286
298
 
287
299
  // Admin tools (only loaded when VIBE_ADMIN=true)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slashvibe-mcp",
3
- "version": "0.3.19",
3
+ "version": "0.3.21",
4
4
  "mcpName": "io.github.vibecodinginc/vibe",
5
5
  "description": "Social layer for Claude Code - DMs, presence, and connection between AI-assisted developers",
6
6
  "main": "index.js",
@@ -49,6 +49,7 @@
49
49
  "notify.js",
50
50
  "notification-emitter.js",
51
51
  "analytics.js",
52
+ "smart-inbox.js",
52
53
  "presence.js",
53
54
  "prompts.js",
54
55
  "twitter.js",
package/smart-inbox.js ADDED
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Smart Inbox Checking
3
+ *
4
+ * Intelligent notification checking that triggers at natural breaks:
5
+ * - After git commits
6
+ * - After test/build completion
7
+ * - After periods of activity (not idle)
8
+ * - On explicit check request
9
+ *
10
+ * Uses debouncing to avoid notification spam.
11
+ */
12
+
13
+ const config = require('./config');
14
+ const notify = require('./notify');
15
+ const store = require('./store');
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ // State file for tracking check timing
20
+ const SMART_STATE_FILE = path.join(config.VIBE_DIR, '.smart_inbox_state.json');
21
+
22
+ // Timing constants
23
+ const MIN_CHECK_INTERVAL = 60 * 1000; // Minimum 1 minute between checks
24
+ const ACTIVITY_COOLDOWN = 5 * 60 * 1000; // 5 min after activity burst
25
+ const COMMIT_DELAY = 3 * 1000; // 3 second delay after commit (let it settle)
26
+ const TEST_DELAY = 2 * 1000; // 2 second delay after test
27
+ const BUILD_DELAY = 5 * 1000; // 5 second delay after build
28
+
29
+ // Trigger types
30
+ const TRIGGERS = {
31
+ COMMIT: 'commit',
32
+ TEST_PASS: 'test_pass',
33
+ TEST_FAIL: 'test_fail',
34
+ BUILD_COMPLETE: 'build_complete',
35
+ BUILD_FAIL: 'build_fail',
36
+ NATURAL_BREAK: 'natural_break',
37
+ EXPLICIT: 'explicit',
38
+ SESSION_START: 'session_start',
39
+ SESSION_END: 'session_end',
40
+ };
41
+
42
+ function loadState() {
43
+ try {
44
+ if (fs.existsSync(SMART_STATE_FILE)) {
45
+ return JSON.parse(fs.readFileSync(SMART_STATE_FILE, 'utf8'));
46
+ }
47
+ } catch (e) {}
48
+ return {
49
+ lastCheck: null,
50
+ lastTrigger: null,
51
+ checkCount: 0,
52
+ triggersToday: {},
53
+ activityBurst: null,
54
+ };
55
+ }
56
+
57
+ function saveState(state) {
58
+ try {
59
+ fs.writeFileSync(SMART_STATE_FILE, JSON.stringify(state, null, 2));
60
+ } catch (e) {}
61
+ }
62
+
63
+ /**
64
+ * Check if we should actually run a check based on timing
65
+ */
66
+ function shouldCheck(trigger) {
67
+ const state = loadState();
68
+ const now = Date.now();
69
+
70
+ // Always allow explicit checks
71
+ if (trigger === TRIGGERS.EXPLICIT) {
72
+ return { should: true, reason: 'explicit_request' };
73
+ }
74
+
75
+ // Check minimum interval
76
+ if (state.lastCheck && (now - state.lastCheck) < MIN_CHECK_INTERVAL) {
77
+ return { should: false, reason: 'too_soon', wait: MIN_CHECK_INTERVAL - (now - state.lastCheck) };
78
+ }
79
+
80
+ // Session events always trigger
81
+ if (trigger === TRIGGERS.SESSION_START || trigger === TRIGGERS.SESSION_END) {
82
+ return { should: true, reason: 'session_event' };
83
+ }
84
+
85
+ // For build/test/commit, we want them but with debounce
86
+ if ([TRIGGERS.COMMIT, TRIGGERS.TEST_PASS, TRIGGERS.TEST_FAIL,
87
+ TRIGGERS.BUILD_COMPLETE, TRIGGERS.BUILD_FAIL].includes(trigger)) {
88
+ // If same trigger happened in last 30 seconds, skip
89
+ if (state.lastTrigger === trigger && state.lastCheck && (now - state.lastCheck) < 30000) {
90
+ return { should: false, reason: 'debounced', wait: 30000 - (now - state.lastCheck) };
91
+ }
92
+ return { should: true, reason: 'event_trigger' };
93
+ }
94
+
95
+ // Natural breaks - check if there's been activity recently
96
+ if (trigger === TRIGGERS.NATURAL_BREAK) {
97
+ // If we haven't checked in 5+ minutes and there's been activity, good time
98
+ if (!state.lastCheck || (now - state.lastCheck) > ACTIVITY_COOLDOWN) {
99
+ return { should: true, reason: 'natural_break' };
100
+ }
101
+ return { should: false, reason: 'not_time_yet' };
102
+ }
103
+
104
+ return { should: true, reason: 'default' };
105
+ }
106
+
107
+ /**
108
+ * Perform smart inbox check
109
+ * Returns notification info if any
110
+ */
111
+ async function check(trigger = TRIGGERS.EXPLICIT, context = {}) {
112
+ const decision = shouldCheck(trigger);
113
+
114
+ if (!decision.should) {
115
+ return {
116
+ checked: false,
117
+ reason: decision.reason,
118
+ waitMs: decision.wait || 0,
119
+ };
120
+ }
121
+
122
+ const handle = config.getHandle();
123
+ if (!handle) {
124
+ return { checked: false, reason: 'not_initialized' };
125
+ }
126
+
127
+ const state = loadState();
128
+ const now = Date.now();
129
+
130
+ // Apply appropriate delay based on trigger
131
+ let delay = 0;
132
+ switch (trigger) {
133
+ case TRIGGERS.COMMIT:
134
+ delay = COMMIT_DELAY;
135
+ break;
136
+ case TRIGGERS.TEST_PASS:
137
+ case TRIGGERS.TEST_FAIL:
138
+ delay = TEST_DELAY;
139
+ break;
140
+ case TRIGGERS.BUILD_COMPLETE:
141
+ case TRIGGERS.BUILD_FAIL:
142
+ delay = BUILD_DELAY;
143
+ break;
144
+ }
145
+
146
+ if (delay > 0) {
147
+ await new Promise(r => setTimeout(r, delay));
148
+ }
149
+
150
+ // Run the actual notification check
151
+ let unreadCount = 0;
152
+ let notified = false;
153
+ let newUsers = [];
154
+ let newShips = [];
155
+
156
+ try {
157
+ // Get unread messages
158
+ const inbox = await store.getRawInbox(handle).catch(() => []);
159
+ unreadCount = inbox.length;
160
+
161
+ if (inbox.length > 0) {
162
+ notified = await notify.checkAndNotify(inbox);
163
+ }
164
+
165
+ // Get presence for online users
166
+ const users = await store.getActiveUsers().catch(() => []);
167
+ if (users.length > 0) {
168
+ newUsers = notify.checkPresence(users);
169
+ }
170
+
171
+ // Get ships from connections
172
+ try {
173
+ const localStore = require('./store/local');
174
+ const memories = localStore.getAllThreadMemories ? localStore.getAllThreadMemories() : {};
175
+ const memoryHandles = Object.keys(memories).map(h => h.toLowerCase());
176
+ if (memoryHandles.length > 0) {
177
+ newShips = await notify.checkShips(memoryHandles);
178
+ }
179
+ } catch (e) {
180
+ // Memory check is optional
181
+ }
182
+ } catch (e) {
183
+ // Silent fail
184
+ }
185
+
186
+ // Update state
187
+ state.lastCheck = now;
188
+ state.lastTrigger = trigger;
189
+ state.checkCount++;
190
+
191
+ // Track triggers by day
192
+ const today = new Date().toISOString().split('T')[0];
193
+ if (!state.triggersToday || state.triggersToday.date !== today) {
194
+ state.triggersToday = { date: today, counts: {} };
195
+ }
196
+ state.triggersToday.counts[trigger] = (state.triggersToday.counts[trigger] || 0) + 1;
197
+
198
+ saveState(state);
199
+
200
+ return {
201
+ checked: true,
202
+ trigger,
203
+ reason: decision.reason,
204
+ unreadCount,
205
+ notified,
206
+ newOnline: newUsers.map(u => u.handle),
207
+ newShips: newShips.map(s => s.author),
208
+ context,
209
+ timestamp: new Date().toISOString(),
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Trigger after git commit
215
+ */
216
+ async function afterCommit(commitMessage = '', branch = '') {
217
+ return check(TRIGGERS.COMMIT, { commitMessage, branch });
218
+ }
219
+
220
+ /**
221
+ * Trigger after test completion
222
+ */
223
+ async function afterTest(passed = true, testOutput = '') {
224
+ const trigger = passed ? TRIGGERS.TEST_PASS : TRIGGERS.TEST_FAIL;
225
+ return check(trigger, { passed, testOutput: testOutput.slice(0, 200) });
226
+ }
227
+
228
+ /**
229
+ * Trigger after build completion
230
+ */
231
+ async function afterBuild(success = true, buildOutput = '') {
232
+ const trigger = success ? TRIGGERS.BUILD_COMPLETE : TRIGGERS.BUILD_FAIL;
233
+ return check(trigger, { success, buildOutput: buildOutput.slice(0, 200) });
234
+ }
235
+
236
+ /**
237
+ * Trigger on natural break (between tasks, file saves, etc.)
238
+ */
239
+ async function onNaturalBreak(activity = '') {
240
+ return check(TRIGGERS.NATURAL_BREAK, { activity });
241
+ }
242
+
243
+ /**
244
+ * Explicit check (user requested)
245
+ */
246
+ async function checkNow() {
247
+ return check(TRIGGERS.EXPLICIT);
248
+ }
249
+
250
+ /**
251
+ * Get check stats
252
+ */
253
+ function getStats() {
254
+ const state = loadState();
255
+ const now = Date.now();
256
+
257
+ return {
258
+ lastCheck: state.lastCheck ? new Date(state.lastCheck).toISOString() : null,
259
+ lastTrigger: state.lastTrigger,
260
+ checkCount: state.checkCount,
261
+ todayTriggers: state.triggersToday?.counts || {},
262
+ timeSinceLastCheck: state.lastCheck ? Math.floor((now - state.lastCheck) / 1000) : null,
263
+ };
264
+ }
265
+
266
+ module.exports = {
267
+ TRIGGERS,
268
+ check,
269
+ afterCommit,
270
+ afterTest,
271
+ afterBuild,
272
+ onNaturalBreak,
273
+ checkNow,
274
+ getStats,
275
+ shouldCheck,
276
+ };
package/store/api.js CHANGED
@@ -17,6 +17,30 @@ const API_URL = process.env.VIBE_API_URL || 'https://www.slashvibe.dev';
17
17
  // Default timeout for API requests (10 seconds)
18
18
  const REQUEST_TIMEOUT = 10000;
19
19
 
20
+ // Retry configuration
21
+ const MAX_RETRIES = 3;
22
+ const INITIAL_RETRY_DELAY = 500; // 500ms
23
+ const MAX_RETRY_DELAY = 5000; // 5 seconds
24
+
25
+ /**
26
+ * Calculate exponential backoff delay with jitter
27
+ * @param {number} attempt - Current attempt number (0-based)
28
+ * @returns {number} Delay in milliseconds
29
+ */
30
+ function getBackoffDelay(attempt) {
31
+ const exponentialDelay = INITIAL_RETRY_DELAY * Math.pow(2, attempt);
32
+ const jitter = Math.random() * 0.3 * exponentialDelay; // 0-30% jitter
33
+ return Math.min(exponentialDelay + jitter, MAX_RETRY_DELAY);
34
+ }
35
+
36
+ /**
37
+ * Sleep for a given duration
38
+ * @param {number} ms - Milliseconds to sleep
39
+ */
40
+ function sleep(ms) {
41
+ return new Promise(resolve => setTimeout(resolve, ms));
42
+ }
43
+
20
44
  /**
21
45
  * Force reload of config module to pick up auth changes
22
46
  * This clears the require cache and reloads the config from disk
@@ -34,7 +58,10 @@ function reloadConfig() {
34
58
  }
35
59
  }
36
60
 
37
- function request(method, path, data = null, options = {}) {
61
+ /**
62
+ * Internal single request (no retries)
63
+ */
64
+ function requestOnce(method, path, data = null, options = {}) {
38
65
  return new Promise((resolve, reject) => {
39
66
  const url = new URL(path, API_URL);
40
67
  const isHttps = url.protocol === 'https:';
@@ -124,11 +151,11 @@ function request(method, path, data = null, options = {}) {
124
151
  // Handle timeout
125
152
  req.on('timeout', () => {
126
153
  req.destroy();
127
- resolve({ success: false, error: 'Request timeout', timeout: true });
154
+ resolve({ success: false, error: 'Request timeout', timeout: true, retryable: true });
128
155
  });
129
156
 
130
157
  req.on('error', (e) => {
131
- resolve({ success: false, error: e.message, network: true });
158
+ resolve({ success: false, error: e.message, network: true, retryable: true });
132
159
  });
133
160
 
134
161
  if (data) {
@@ -138,6 +165,40 @@ function request(method, path, data = null, options = {}) {
138
165
  });
139
166
  }
140
167
 
168
+ /**
169
+ * Make HTTP request with exponential backoff retry for transient failures
170
+ */
171
+ async function request(method, path, data = null, options = {}) {
172
+ const maxRetries = options.maxRetries ?? MAX_RETRIES;
173
+ const skipRetry = options._skipRetry || false;
174
+
175
+ // Don't retry if explicitly disabled or already in a retry chain
176
+ if (skipRetry || maxRetries === 0) {
177
+ return requestOnce(method, path, data, options);
178
+ }
179
+
180
+ let lastResult;
181
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
182
+ lastResult = await requestOnce(method, path, data, { ...options, _skipRetry: true });
183
+
184
+ // Success or non-retryable error - return immediately
185
+ if (!lastResult.retryable) {
186
+ return lastResult;
187
+ }
188
+
189
+ // Don't retry on last attempt
190
+ if (attempt < maxRetries) {
191
+ const delay = getBackoffDelay(attempt);
192
+ console.error(`[api] Retrying ${method} ${path} in ${Math.round(delay)}ms (attempt ${attempt + 1}/${maxRetries})`);
193
+ await sleep(delay);
194
+ }
195
+ }
196
+
197
+ // All retries exhausted
198
+ console.error(`[api] All ${maxRetries} retries failed for ${method} ${path}`);
199
+ return lastResult;
200
+ }
201
+
141
202
  // ============ PRESENCE ============
142
203
 
143
204
  // Use v2 API by default (Postgres-backed)
@@ -318,7 +379,23 @@ async function getActiveUsers() {
318
379
  }
319
380
 
320
381
  async function setVisibility(handle, visible) {
321
- // TODO: implement visibility toggle API
382
+ try {
383
+ const endpoint = USE_V2_PRESENCE ? '/api/v2/presence' : '/api/presence';
384
+ const result = await request('POST', endpoint, {
385
+ action: 'visibility',
386
+ visible: visible
387
+ });
388
+
389
+ if (result.success === false) {
390
+ console.error('[setVisibility] API error:', result.error);
391
+ return { success: false, error: result.error };
392
+ }
393
+
394
+ return { success: true, visible };
395
+ } catch (e) {
396
+ console.error('[setVisibility] Error:', e.message);
397
+ return { success: false, error: e.message };
398
+ }
322
399
  }
323
400
 
324
401
  // ============ MESSAGES ============
@@ -512,6 +589,22 @@ async function getUnreadCount(handle) {
512
589
  }
513
590
  }
514
591
 
592
+ /**
593
+ * Get count of active live broadcasts
594
+ * Used in presence footer to show "X live now"
595
+ */
596
+ async function getLiveBroadcastCount() {
597
+ try {
598
+ const result = await request('GET', '/api/watch', null, { auth: false });
599
+ if (result.broadcasts) {
600
+ return Object.keys(result.broadcasts).length;
601
+ }
602
+ return 0;
603
+ } catch (e) {
604
+ return 0;
605
+ }
606
+ }
607
+
515
608
  // Get raw inbox messages (for notification checks)
516
609
  // V2: Fetches threads and constructs message objects from unread threads
517
610
  // Read state is synced via Postgres cursors, so if you read on iOS it's reflected here
@@ -1096,6 +1189,9 @@ module.exports = {
1096
1189
  markMessagesDelivered,
1097
1190
  searchMessages,
1098
1191
 
1192
+ // Watch Me Code
1193
+ getLiveBroadcastCount,
1194
+
1099
1195
  // Consent
1100
1196
  getConsentStatus,
1101
1197
  getPendingConsents,
@@ -6,12 +6,13 @@
6
6
  *
7
7
  * Commands:
8
8
  * - broadcast start "Building X" → Start streaming
9
- * - broadcast stop → End + show stats
9
+ * - broadcast stop → End + show stats + git bundle
10
10
  * - broadcast status → Check if broadcasting
11
11
  * - broadcast stream <data> → Push terminal data (internal)
12
12
  */
13
13
 
14
14
  const config = require('../config');
15
+ const gitBundle = require('./lib/git-bundle');
15
16
 
16
17
  const definition = {
17
18
  name: 'vibe_broadcast',
@@ -74,12 +75,18 @@ async function handler(args) {
74
75
  return { display: `⚠️ Failed to start broadcast: ${data.error}` };
75
76
  }
76
77
 
78
+ // Capture git state at session start
79
+ const gitState = gitBundle.captureSessionStart();
80
+
77
81
  // Store broadcast state
78
82
  activeBroadcast = {
79
83
  roomId: data.roomId,
80
84
  shareUrl: data.shareUrl,
81
85
  title,
82
- startedAt: Date.now()
86
+ startedAt: Date.now(),
87
+ // Git tracking for bundle generation on stop
88
+ gitInitialCommit: gitState.success ? gitState.commit : null,
89
+ gitBranch: gitState.success ? gitState.branch : null,
83
90
  };
84
91
 
85
92
  // Also store in config for persistence across tool calls
@@ -109,12 +116,29 @@ async function handler(args) {
109
116
  }
110
117
 
111
118
  try {
119
+ // Prepare git bundle if we have a starting commit
120
+ let bundlePayload = null;
121
+ if (broadcast.gitInitialCommit) {
122
+ const bundleResult = gitBundle.createBundle(broadcast.gitInitialCommit);
123
+ if (bundleResult.success) {
124
+ bundlePayload = {
125
+ base64: bundleResult.buffer.toString('base64'),
126
+ metadata: bundleResult.metadata,
127
+ };
128
+ } else {
129
+ // Log but don't fail - bundle is optional
130
+ console.log(`[broadcast] Git bundle skipped: ${bundleResult.error}`);
131
+ }
132
+ }
133
+
112
134
  const response = await fetch(`${apiUrl}/api/watch`, {
113
135
  method: 'POST',
114
136
  headers: { 'Content-Type': 'application/json' },
115
137
  body: JSON.stringify({
116
138
  action: 'stop',
117
- roomId: broadcast.roomId
139
+ roomId: broadcast.roomId,
140
+ // Include git bundle if available
141
+ gitBundle: bundlePayload,
118
142
  })
119
143
  });
120
144
 
@@ -136,10 +160,25 @@ async function handler(args) {
136
160
 
137
161
  let display = `📺 Broadcast ended\n\n`;
138
162
  display += `**Duration:** ${formatDuration(duration)}\n`;
139
- display += `**Room:** ${broadcast.roomId}\n\n`;
163
+ display += `**Room:** ${broadcast.roomId}\n`;
164
+
165
+ // Show git bundle status
166
+ if (bundlePayload) {
167
+ const commits = bundlePayload.metadata.commitCount || 0;
168
+ const files = bundlePayload.metadata.changedFiles?.length || 0;
169
+ display += `**Git:** ${commits} commit${commits !== 1 ? 's' : ''}, ${files} file${files !== 1 ? 's' : ''} bundled 📦\n`;
170
+ } else if (broadcast.gitInitialCommit) {
171
+ display += `**Git:** No new commits to bundle\n`;
172
+ }
173
+ display += `\n`;
140
174
 
141
- // Suggest saving as session
142
- if (data.suggestions) {
175
+ // Show auto-save info
176
+ if (data.autoSave?.saved) {
177
+ display += `✅ Session auto-saved: \`${data.autoSave.sessionId}\`\n`;
178
+ if (bundlePayload) {
179
+ display += `_Code bundle included - viewers can fork the full code!_\n`;
180
+ }
181
+ } else if (data.suggestions) {
143
182
  display += `_💡 Want to save this session? Use \`vibe session save\`_`;
144
183
  } else {
145
184
  display += `_💡 Save this broadcast as a replayable session:_\n`;