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
@@ -0,0 +1,77 @@
1
+ /**
2
+ * MCP `list_changed` notification emitter
3
+ *
4
+ * Triggers Claude to refresh tool results without reconnection.
5
+ * Implements debouncing to prevent notification spam.
6
+ *
7
+ * This eliminates the need for 30-second polling loops,
8
+ * reducing API calls by ~90% and providing instant updates.
9
+ */
10
+
11
+ class NotificationEmitter {
12
+ constructor(server) {
13
+ this.server = server;
14
+ this.debounceTimers = {};
15
+ }
16
+
17
+ /**
18
+ * Emit list_changed notification with debouncing
19
+ * @param {string} reason - Why notification is being sent (for logging/debugging)
20
+ * @param {number} debounceMs - Debounce window in milliseconds (default: 1000ms)
21
+ */
22
+ emitChange(reason, debounceMs = 1000) {
23
+ // Debounce to prevent notification spam
24
+ // If we get multiple changes of the same type within the window,
25
+ // only emit one notification
26
+ if (this.debounceTimers[reason]) {
27
+ clearTimeout(this.debounceTimers[reason]);
28
+ }
29
+
30
+ this.debounceTimers[reason] = setTimeout(() => {
31
+ try {
32
+ this.server.notification({
33
+ method: "notifications/list_changed"
34
+ });
35
+ delete this.debounceTimers[reason];
36
+ } catch (e) {
37
+ // Silent fail - notifications are best-effort
38
+ // If notification fails, Claude will continue working normally
39
+ }
40
+ }, debounceMs);
41
+ }
42
+
43
+ /**
44
+ * Emit immediately without debouncing
45
+ * Use for urgent updates like direct mentions
46
+ */
47
+ emitImmediate() {
48
+ try {
49
+ this.server.notification({
50
+ method: "notifications/list_changed"
51
+ });
52
+ } catch (e) {
53
+ // Silent fail
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Cancel pending notifications for a specific reason
59
+ * Useful when shutting down or cleaning up
60
+ */
61
+ cancel(reason) {
62
+ if (this.debounceTimers[reason]) {
63
+ clearTimeout(this.debounceTimers[reason]);
64
+ delete this.debounceTimers[reason];
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Cancel all pending notifications
70
+ */
71
+ cancelAll() {
72
+ Object.values(this.debounceTimers).forEach(timer => clearTimeout(timer));
73
+ this.debounceTimers = {};
74
+ }
75
+ }
76
+
77
+ module.exports = NotificationEmitter;
package/notify.js CHANGED
@@ -34,15 +34,28 @@ function saveNotifyState(state) {
34
34
  } catch (e) {}
35
35
  }
36
36
 
37
+ /**
38
+ * Ring terminal bell (BEL character)
39
+ * Ambient, non-intrusive alert
40
+ */
41
+ function ringBell() {
42
+ process.stderr.write('\x07');
43
+ }
44
+
37
45
  /**
38
46
  * Show a macOS notification
39
47
  */
40
- function showNotification(title, message, sound = false) {
48
+ function showNotification(title, message, sound = false, bell = true) {
41
49
  if (os.platform() !== 'darwin') {
42
50
  // Only macOS supported for now
43
51
  return;
44
52
  }
45
53
 
54
+ // Ring terminal bell for all notifications
55
+ if (bell) {
56
+ ringBell();
57
+ }
58
+
46
59
  // Escape quotes for osascript
47
60
  const safeTitle = title.replace(/"/g, '\\"');
48
61
  const safeMessage = message.replace(/"/g, '\\"');
@@ -62,8 +75,14 @@ function showNotification(title, message, sound = false) {
62
75
  /**
63
76
  * Check for messages that need escalation
64
77
  * Called periodically or on inbox check
78
+ * Respects notification settings: all | mentions | off
65
79
  */
66
80
  async function checkAndNotify(inbox) {
81
+ const notifyLevel = config.getNotifications();
82
+
83
+ // If notifications are off, skip entirely
84
+ if (notifyLevel === 'off') return false;
85
+
67
86
  const state = loadNotifyState();
68
87
  const now = Date.now();
69
88
  const FIVE_MINUTES = 5 * 60 * 1000;
@@ -84,25 +103,25 @@ async function checkAndNotify(inbox) {
84
103
  let shouldNotify = false;
85
104
  let reason = '';
86
105
 
87
- // Rule 1: Unread > 5 minutes
88
- if (age > FIVE_MINUTES) {
89
- shouldNotify = true;
90
- reason = 'unread';
91
- }
92
-
93
- // Rule 2: Direct mention in message
106
+ // Rule 1: Direct mention in message (always if not "off")
94
107
  const myHandle = config.getHandle();
95
108
  if (myHandle && msg.text && msg.text.toLowerCase().includes(`@${myHandle}`)) {
96
109
  shouldNotify = true;
97
110
  reason = 'mention';
98
111
  }
99
112
 
100
- // Rule 3: Handshake/consent request
113
+ // Rule 2: Handshake/consent request (always if not "off")
101
114
  if (msg.payload?.type === 'handshake') {
102
115
  shouldNotify = true;
103
116
  reason = 'handshake';
104
117
  }
105
118
 
119
+ // Rule 3: Unread > 5 minutes (only in "all" mode)
120
+ if (notifyLevel === 'all' && age > FIVE_MINUTES) {
121
+ shouldNotify = true;
122
+ reason = reason || 'unread'; // Don't override mention/handshake
123
+ }
124
+
106
125
  if (shouldNotify) {
107
126
  const preview = msg.text?.slice(0, 50) || '(no preview)';
108
127
  showNotification(
@@ -157,8 +176,11 @@ function savePresenceState(state) {
157
176
  /**
158
177
  * Check for new online users and notify
159
178
  * Returns handles that just came online
179
+ * Only notifies in "all" mode (presence is lower priority)
160
180
  */
161
181
  function checkPresence(activeUsers) {
182
+ const notifyLevel = config.getNotifications();
183
+
162
184
  const state = loadPresenceState();
163
185
  const now = Date.now();
164
186
  const COOLDOWN = 30 * 60 * 1000; // Don't re-notify about same person for 30 min
@@ -183,17 +205,20 @@ function checkPresence(activeUsers) {
183
205
  if (userActive) {
184
206
  justJoined.push(user);
185
207
 
186
- // Show notification
187
- const context = user.note || user.one_liner || 'just joined';
188
- showNotification(
189
- `/vibe — @${user.handle} is here`,
190
- context,
191
- false
192
- );
208
+ // Only show notification in "all" mode (presence is lower priority)
209
+ if (notifyLevel === 'all') {
210
+ const context = user.note || user.one_liner || 'just joined';
211
+ showNotification(
212
+ `/vibe — @${user.handle} is here`,
213
+ context,
214
+ false, // no system sound
215
+ false // no terminal bell for presence (too noisy)
216
+ );
217
+ }
193
218
  }
194
219
  }
195
220
 
196
- // Update last seen
221
+ // Update last seen (always track, even if not notifying)
197
222
  state.seenHandles[user.handle] = now;
198
223
  }
199
224
 
@@ -211,6 +236,89 @@ function checkPresence(activeUsers) {
211
236
  return justJoined;
212
237
  }
213
238
 
239
+ /**
240
+ * Notify about new ships from connections (FOMO/retention driver)
241
+ */
242
+ const SHIPS_STATE_FILE = path.join(config.VIBE_DIR, '.ships_state.json');
243
+
244
+ function loadShipsState() {
245
+ try {
246
+ if (fs.existsSync(SHIPS_STATE_FILE)) {
247
+ return JSON.parse(fs.readFileSync(SHIPS_STATE_FILE, 'utf8'));
248
+ }
249
+ } catch (e) {}
250
+ return { seenShipIds: [], lastCheck: null };
251
+ }
252
+
253
+ function saveShipsState(state) {
254
+ try {
255
+ fs.writeFileSync(SHIPS_STATE_FILE, JSON.stringify(state, null, 2));
256
+ } catch (e) {}
257
+ }
258
+
259
+ /**
260
+ * Check for new ships and notify
261
+ * Only notifies about ships from people in memory (connections)
262
+ */
263
+ async function checkShips(memoryHandles = []) {
264
+ const notifyLevel = config.getNotifications();
265
+ if (notifyLevel === 'off') return [];
266
+
267
+ const state = loadShipsState();
268
+ const now = Date.now();
269
+ const myHandle = config.getHandle();
270
+ const newShips = [];
271
+
272
+ try {
273
+ // Fetch recent ships from board API
274
+ const apiUrl = config.getApiUrl();
275
+ const response = await fetch(`${apiUrl}/api/board?limit=10&category=shipped`);
276
+ const data = await response.json();
277
+ const ships = data.entries || [];
278
+
279
+ for (const ship of ships) {
280
+ // Skip if already seen
281
+ const shipId = ship.id || `${ship.author}-${ship.timestamp}`;
282
+ if (state.seenShipIds.includes(shipId)) continue;
283
+
284
+ // Skip our own ships
285
+ if (ship.author === myHandle) {
286
+ state.seenShipIds.push(shipId);
287
+ continue;
288
+ }
289
+
290
+ // Only notify about ships from connections (people in memory)
291
+ const isConnection = memoryHandles.includes(ship.author.toLowerCase());
292
+
293
+ if (isConnection && notifyLevel === 'all') {
294
+ const content = ship.content?.slice(0, 60) || 'something new';
295
+ showNotification(
296
+ `/vibe — @${ship.author} shipped! 🚀`,
297
+ content,
298
+ false, // no system sound
299
+ true // terminal bell (soft nudge)
300
+ );
301
+ newShips.push(ship);
302
+ }
303
+
304
+ state.seenShipIds.push(shipId);
305
+ }
306
+
307
+ // Trim old ship IDs (keep last 50)
308
+ if (state.seenShipIds.length > 50) {
309
+ state.seenShipIds = state.seenShipIds.slice(-50);
310
+ }
311
+
312
+ state.lastCheck = now;
313
+ saveShipsState(state);
314
+
315
+ } catch (e) {
316
+ // Silent fail - notifications are best-effort
317
+ }
318
+
319
+ return newShips;
320
+ }
321
+
214
322
  /**
215
323
  * Unified notification check - call from any tool
216
324
  */
@@ -230,6 +338,19 @@ async function checkAll(store) {
230
338
  if (users.length > 0) {
231
339
  checkPresence(users);
232
340
  }
341
+
342
+ // Check for ships (only from connections)
343
+ // Get memory handles from local store
344
+ try {
345
+ const localStore = require('./store/local');
346
+ const memories = localStore.getAllThreadMemories ? localStore.getAllThreadMemories() : {};
347
+ const memoryHandles = Object.keys(memories).map(h => h.toLowerCase());
348
+ if (memoryHandles.length > 0) {
349
+ await checkShips(memoryHandles);
350
+ }
351
+ } catch (e) {
352
+ // Memory check is optional
353
+ }
233
354
  } catch (e) {
234
355
  // Silent fail - notifications are best-effort
235
356
  }
@@ -239,6 +360,8 @@ module.exports = {
239
360
  showNotification,
240
361
  checkAndNotify,
241
362
  checkPresence,
363
+ checkShips,
242
364
  checkAll,
243
- notify
365
+ notify,
366
+ ringBell
244
367
  };
package/package.json CHANGED
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "name": "slashvibe-mcp",
3
- "version": "0.2.2",
4
- "mcpName": "io.github.brightseth/vibe",
3
+ "version": "0.2.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",
7
7
  "bin": {
8
8
  "slashvibe-mcp": "./index.js"
9
9
  },
10
10
  "scripts": {
11
- "start": "node index.js"
11
+ "start": "node index.js",
12
+ "test": "node --test tools/*.test.js",
13
+ "test:context": "node --test tools/_work-context.test.js"
12
14
  },
13
15
  "keywords": [
14
16
  "mcp",
@@ -23,11 +25,11 @@
23
25
  "license": "MIT",
24
26
  "repository": {
25
27
  "type": "git",
26
- "url": "git+https://github.com/spirit-protocol/vibe-mcp.git"
28
+ "url": "git+https://github.com/VibeCodingInc/vibe-mcp.git"
27
29
  },
28
30
  "homepage": "https://slashvibe.dev",
29
31
  "bugs": {
30
- "url": "https://github.com/spirit-protocol/vibe-mcp/issues"
32
+ "url": "https://github.com/VibeCodingInc/vibe-mcp/issues"
31
33
  },
32
34
  "engines": {
33
35
  "node": ">=18.0.0"
@@ -39,6 +41,8 @@
39
41
  "discord.js",
40
42
  "memory.js",
41
43
  "notify.js",
44
+ "notification-emitter.js",
45
+ "analytics.js",
42
46
  "presence.js",
43
47
  "prompts.js",
44
48
  "twitter.js",
@@ -46,6 +50,10 @@
46
50
  "tools/",
47
51
  "store/",
48
52
  "protocol/",
53
+ "intelligence/",
49
54
  "README.md"
50
- ]
55
+ ],
56
+ "dependencies": {
57
+ "crossword-layout-generator": "^0.1.1"
58
+ }
51
59
  }
package/presence.js CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  const config = require('./config');
10
10
  const store = require('./store');
11
+ const notify = require('./notify');
11
12
 
12
13
  let heartbeatInterval = null;
13
14
  let sessionInitialized = false;
@@ -52,7 +53,7 @@ async function initSession() {
52
53
  sendHeartbeat();
53
54
  }
54
55
 
55
- function sendHeartbeat() {
56
+ async function sendHeartbeat() {
56
57
  if (!config.isInitialized()) return;
57
58
 
58
59
  // Use session-aware getters (prefer session identity over shared config)
@@ -60,6 +61,9 @@ function sendHeartbeat() {
60
61
  const one_liner = config.getOneLiner();
61
62
  if (handle) {
62
63
  store.heartbeat(handle, one_liner || '');
64
+
65
+ // Check for notifications (runs in background, non-blocking)
66
+ notify.checkAll(store).catch(() => {});
63
67
  }
64
68
  }
65
69
 
package/protocol/index.js CHANGED
@@ -101,10 +101,49 @@ const ACK_SCHEMA = {
101
101
  }
102
102
  };
103
103
 
104
+ /**
105
+ * Artifact schema — For sharing artifacts in messages
106
+ *
107
+ * Example:
108
+ * {
109
+ * type: 'artifact',
110
+ * version: '0.1.0',
111
+ * artifactId: 'artifact_1768035804429_1651c037',
112
+ * slug: 'pizza-guide-abc123',
113
+ * title: 'Stan's North Beach Pizza Guide',
114
+ * template: 'guide',
115
+ * preview: 'Best pizza spots in North Beach...',
116
+ * url: 'https://slashvibe.dev/a/pizza-guide-abc123'
117
+ * }
118
+ */
119
+ const ARTIFACT_SCHEMA = {
120
+ type: 'artifact',
121
+ required: ['artifactId', 'slug', 'title', 'template', 'url'],
122
+ validate: (payload) => {
123
+ if (!payload.artifactId || typeof payload.artifactId !== 'string') {
124
+ return { valid: false, error: 'Missing or invalid artifactId' };
125
+ }
126
+ if (!payload.slug || typeof payload.slug !== 'string') {
127
+ return { valid: false, error: 'Missing or invalid slug' };
128
+ }
129
+ if (!payload.title || typeof payload.title !== 'string') {
130
+ return { valid: false, error: 'Missing or invalid title' };
131
+ }
132
+ if (!['guide', 'learning', 'workspace'].includes(payload.template)) {
133
+ return { valid: false, error: 'Invalid template (must be: guide, learning, workspace)' };
134
+ }
135
+ if (!payload.url || typeof payload.url !== 'string') {
136
+ return { valid: false, error: 'Missing or invalid url' };
137
+ }
138
+ return { valid: true };
139
+ }
140
+ };
141
+
104
142
  const SCHEMAS = {
105
143
  game: GAME_SCHEMA,
106
144
  handoff: HANDOFF_SCHEMA,
107
- ack: ACK_SCHEMA
145
+ ack: ACK_SCHEMA,
146
+ artifact: ARTIFACT_SCHEMA
108
147
  };
109
148
 
110
149
  // ============ PROTOCOL FUNCTIONS ============
@@ -266,6 +305,8 @@ function formatPayload(payload) {
266
305
  return formatHandoffPayload(payload);
267
306
  case 'ack':
268
307
  return formatAckPayload(payload);
308
+ case 'artifact':
309
+ return formatArtifactPayload(payload);
269
310
  default:
270
311
  return `📦 _${payload.type} payload_`;
271
312
  }
@@ -316,6 +357,49 @@ function formatAckPayload(payload) {
316
357
  return `${icon} Acknowledged: ${payload.replyTo} (${status})`;
317
358
  }
318
359
 
360
+ function formatArtifactPayload(payload) {
361
+ const template = payload.template || 'artifact';
362
+ const templateIcon = template === 'guide' ? '📘' : template === 'learning' ? '💡' : template === 'workspace' ? '🗂️' : '📦';
363
+
364
+ let display = `${templateIcon} **${payload.title}**\n`;
365
+
366
+ if (payload.preview) {
367
+ const preview = payload.preview.length > 120 ? payload.preview.substring(0, 120) + '...' : payload.preview;
368
+ display += `> ${preview}\n\n`;
369
+ }
370
+
371
+ display += `🔗 [View artifact](${payload.url})`;
372
+
373
+ return display;
374
+ }
375
+
376
+ // ============ ARTIFACT HELPERS ============
377
+
378
+ /**
379
+ * Create an artifact card payload
380
+ * @param {Object} artifact - Artifact object from API
381
+ * @returns {Object} - Artifact payload
382
+ */
383
+ function createArtifactPayload(artifact) {
384
+ // Extract preview from first paragraph block
385
+ let preview = '';
386
+ if (artifact.content && artifact.content.blocks) {
387
+ const firstPara = artifact.content.blocks.find(b => b.type === 'paragraph');
388
+ if (firstPara && firstPara.markdown) {
389
+ preview = firstPara.markdown.substring(0, 150);
390
+ }
391
+ }
392
+
393
+ return createPayload('artifact', {
394
+ artifactId: artifact.id,
395
+ slug: artifact.slug,
396
+ title: artifact.title,
397
+ template: artifact.template,
398
+ preview: preview || undefined,
399
+ url: `https://slashvibe.dev/a/${artifact.slug}`
400
+ });
401
+ }
402
+
319
403
  module.exports = {
320
404
  PROTOCOL_VERSION,
321
405
 
@@ -335,6 +419,9 @@ module.exports = {
335
419
  // Ack helpers
336
420
  createAckPayload,
337
421
 
422
+ // Artifact helpers
423
+ createArtifactPayload,
424
+
338
425
  // Schemas (for extension)
339
426
  SCHEMAS
340
427
  };