slashvibe-mcp 0.2.8 → 0.3.13

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 (161) hide show
  1. package/README.md +41 -58
  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 +77 -53
  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 +18 -6
  45. package/prompts.js +1 -1
  46. package/protocol/index.js +73 -0
  47. package/setup.js +402 -0
  48. package/store/api.js +436 -211
  49. package/store/profiles.js +160 -12
  50. package/tools/_actions.js +362 -21
  51. package/tools/_discovery.js +119 -26
  52. package/tools/_shared/index.js +64 -0
  53. package/tools/_shared.js +234 -0
  54. package/tools/_work-context.js +338 -0
  55. package/tools/_work-context.manual-test.js +199 -0
  56. package/tools/_work-context.test.js +260 -0
  57. package/tools/activity.js +220 -0
  58. package/tools/analytics.js +191 -0
  59. package/tools/approve.js +197 -0
  60. package/tools/artifact-create.js +14 -3
  61. package/tools/artifacts-price.js +107 -0
  62. package/tools/available.js +120 -0
  63. package/tools/broadcast.js +286 -0
  64. package/tools/chat.js +202 -0
  65. package/tools/collaborative-drawing.js +1 -1
  66. package/tools/connection-status.js +178 -0
  67. package/tools/discover.js +350 -34
  68. package/tools/dm.js +80 -8
  69. package/tools/earnings.js +126 -0
  70. package/tools/feed.js +35 -4
  71. package/tools/follow.js +224 -0
  72. package/tools/friends.js +207 -0
  73. package/tools/gig-browse.js +206 -0
  74. package/tools/gig-complete.js +144 -0
  75. package/tools/help.js +3 -3
  76. package/tools/idea.js +9 -2
  77. package/tools/inbox.js +289 -105
  78. package/tools/init.js +106 -27
  79. package/tools/invite.js +15 -4
  80. package/tools/migrate.js +3 -3
  81. package/tools/multiplayer-game.js +1 -1
  82. package/tools/onboarding.js +7 -7
  83. package/tools/open.js +143 -12
  84. package/tools/party-game.js +1 -1
  85. package/tools/plan.js +225 -0
  86. package/tools/proof-of-work.js +144 -0
  87. package/tools/reply.js +166 -0
  88. package/tools/report.js +1 -1
  89. package/tools/request.js +17 -3
  90. package/tools/schedule.js +367 -0
  91. package/tools/search-messages.js +123 -0
  92. package/tools/session.js +420 -0
  93. package/tools/session_price.js +128 -0
  94. package/tools/settings.js +90 -2
  95. package/tools/ship.js +30 -7
  96. package/tools/smart-check.js +201 -0
  97. package/tools/start.js +147 -12
  98. package/tools/status.js +53 -6
  99. package/tools/stuck.js +297 -0
  100. package/tools/subscribe.js +148 -0
  101. package/tools/subscriptions.js +134 -0
  102. package/tools/suggest-tags.js +6 -8
  103. package/tools/tag-suggestions.js +1 -1
  104. package/tools/tip.js +150 -77
  105. package/tools/token.js +4 -4
  106. package/tools/update.js +1 -1
  107. package/tools/wallet.js +221 -79
  108. package/tools/watch.js +157 -0
  109. package/tools/who.js +30 -1
  110. package/tools/withdraw.js +145 -0
  111. package/tools/work-summary.js +96 -0
  112. package/version.json +10 -8
  113. package/LICENSE +0 -21
  114. package/store/sqlite.js +0 -347
  115. /package/tools/{auto-suggest-connections.js → _deprecated/auto-suggest-connections.js} +0 -0
  116. /package/tools/{away.js → _deprecated/away.js} +0 -0
  117. /package/tools/{back.js → _deprecated/back.js} +0 -0
  118. /package/tools/{bootstrap-skills.js → _deprecated/bootstrap-skills.js} +0 -0
  119. /package/tools/{bridge-dashboard.js → _deprecated/bridge-dashboard.js} +0 -0
  120. /package/tools/{bridge-health.js → _deprecated/bridge-health.js} +0 -0
  121. /package/tools/{bridge-live.js → _deprecated/bridge-live.js} +0 -0
  122. /package/tools/{bridges.js → _deprecated/bridges.js} +0 -0
  123. /package/tools/{colorguess.js → _deprecated/colorguess.js} +0 -0
  124. /package/tools/{discover-insights.js → _deprecated/discover-insights.js} +0 -0
  125. /package/tools/{discover-momentum.js → _deprecated/discover-momentum.js} +0 -0
  126. /package/tools/{discovery-analytics.js → _deprecated/discovery-analytics.js} +0 -0
  127. /package/tools/{discovery-auto-suggest.js → _deprecated/discovery-auto-suggest.js} +0 -0
  128. /package/tools/{discovery-bootstrap.js → _deprecated/discovery-bootstrap.js} +0 -0
  129. /package/tools/{discovery-daily.js → _deprecated/discovery-daily.js} +0 -0
  130. /package/tools/{discovery-dashboard.js → _deprecated/discovery-dashboard.js} +0 -0
  131. /package/tools/{discovery-digest.js → _deprecated/discovery-digest.js} +0 -0
  132. /package/tools/{discovery-hub.js → _deprecated/discovery-hub.js} +0 -0
  133. /package/tools/{discovery-insights.js → _deprecated/discovery-insights.js} +0 -0
  134. /package/tools/{discovery-momentum.js → _deprecated/discovery-momentum.js} +0 -0
  135. /package/tools/{discovery-monitor.js → _deprecated/discovery-monitor.js} +0 -0
  136. /package/tools/{discovery-proactive.js → _deprecated/discovery-proactive.js} +0 -0
  137. /package/tools/{draw.js → _deprecated/draw.js} +0 -0
  138. /package/tools/{farcaster.js → _deprecated/farcaster.js} +0 -0
  139. /package/tools/{forget.js → _deprecated/forget.js} +0 -0
  140. /package/tools/{games-catalog.js → _deprecated/games-catalog.js} +0 -0
  141. /package/tools/{games.js → _deprecated/games.js} +0 -0
  142. /package/tools/{guessnumber.js → _deprecated/guessnumber.js} +0 -0
  143. /package/tools/{hangman.js → _deprecated/hangman.js} +0 -0
  144. /package/tools/{multiplayer-tictactoe.js → _deprecated/multiplayer-tictactoe.js} +0 -0
  145. /package/tools/{mute.js → _deprecated/mute.js} +0 -0
  146. /package/tools/{recall.js → _deprecated/recall.js} +0 -0
  147. /package/tools/{remember.js → _deprecated/remember.js} +0 -0
  148. /package/tools/{riddle.js → _deprecated/riddle.js} +0 -0
  149. /package/tools/{run-bootstrap.js → _deprecated/run-bootstrap.js} +0 -0
  150. /package/tools/{skills-analytics.js → _deprecated/skills-analytics.js} +0 -0
  151. /package/tools/{skills-bootstrap.js → _deprecated/skills-bootstrap.js} +0 -0
  152. /package/tools/{skills-dashboard.js → _deprecated/skills-dashboard.js} +0 -0
  153. /package/tools/{skills-exchange.js → _deprecated/skills-exchange.js} +0 -0
  154. /package/tools/{skills.js → _deprecated/skills.js} +0 -0
  155. /package/tools/{smart-intro.js → _deprecated/smart-intro.js} +0 -0
  156. /package/tools/{storybuilder.js → _deprecated/storybuilder.js} +0 -0
  157. /package/tools/{telegram-bot.js → _deprecated/telegram-bot.js} +0 -0
  158. /package/tools/{telegram-setup.js → _deprecated/telegram-setup.js} +0 -0
  159. /package/tools/{tictactoe.js → _deprecated/tictactoe.js} +0 -0
  160. /package/tools/{twentyquestions.js → _deprecated/twentyquestions.js} +0 -0
  161. /package/tools/{wordassociation.js → _deprecated/wordassociation.js} +0 -0
@@ -0,0 +1,197 @@
1
+ /**
2
+ * vibe_approve — Respond to a plan with approval, rejection, or request changes
3
+ *
4
+ * Only people in the plan's requested_from list can respond.
5
+ */
6
+
7
+ const config = require('../config');
8
+ const store = require('../store');
9
+ const { requireInit, normalizeHandle } = require('./_shared');
10
+
11
+ const definition = {
12
+ name: 'vibe_approve',
13
+ description: 'Approve, reject, or request changes on a plan. Only invited approvers can respond.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ plan_id: {
18
+ type: 'string',
19
+ description: 'Plan ID or slug (e.g., "claude-md-cleanup" or "plan_123...")'
20
+ },
21
+ decision: {
22
+ type: 'string',
23
+ enum: ['approve', 'needs_changes', 'reject'],
24
+ description: 'Your decision: approve, needs_changes, or reject'
25
+ },
26
+ note: {
27
+ type: 'string',
28
+ description: 'Optional feedback or reason for your decision'
29
+ }
30
+ },
31
+ required: ['plan_id', 'decision']
32
+ }
33
+ };
34
+
35
+ async function handler(args) {
36
+ const initCheck = requireInit();
37
+ if (initCheck) return initCheck;
38
+
39
+ const { plan_id, decision, note } = args;
40
+ const myHandle = config.getHandle();
41
+
42
+ // Fetch the plan
43
+ const planResult = await store.getArtifact(plan_id);
44
+
45
+ if (!planResult.success || !planResult.artifact) {
46
+ return {
47
+ display: `❌ Plan not found: ${plan_id}\n\nTip: Use the plan slug (e.g., "claude-md-cleanup") or full ID.`
48
+ };
49
+ }
50
+
51
+ const plan = planResult.artifact;
52
+
53
+ // Verify it's a plan template
54
+ if (plan.template !== 'plan') {
55
+ return {
56
+ display: `❌ "${plan_id}" is not a plan (it's a ${plan.template})`
57
+ };
58
+ }
59
+
60
+ // Verify it has approval workflow
61
+ if (!plan.approval) {
62
+ return {
63
+ display: `❌ This plan doesn't have an approval workflow`
64
+ };
65
+ }
66
+
67
+ // Check if user is in the requested_from list
68
+ if (!plan.approval.requested_from.includes(myHandle)) {
69
+ return {
70
+ display: `❌ You're not on the approver list for this plan.\n\n**Approvers:** ${plan.approval.requested_from.map(h => `@${h}`).join(', ')}`
71
+ };
72
+ }
73
+
74
+ // Check if plan is still pending
75
+ if (plan.approval.status !== 'pending') {
76
+ return {
77
+ display: `⚠️ This plan is already ${plan.approval.status}.\n\nNo changes made.`
78
+ };
79
+ }
80
+
81
+ // Check if user already responded
82
+ const existingResponse = plan.approval.responses.find(r => r.handle === myHandle);
83
+ if (existingResponse) {
84
+ return {
85
+ display: `⚠️ You already responded to this plan (${existingResponse.status}).\n\nTo change your response, the plan creator would need to request again.`
86
+ };
87
+ }
88
+
89
+ // Record the response
90
+ const response = {
91
+ handle: myHandle,
92
+ status: decision,
93
+ note: note || null,
94
+ responded_at: new Date().toISOString()
95
+ };
96
+
97
+ plan.approval.responses.push(response);
98
+ plan.updated_at = new Date().toISOString();
99
+
100
+ // Determine new status based on decision and requires mode
101
+ let newStatus = 'pending';
102
+
103
+ if (decision === 'approve') {
104
+ if (plan.approval.requires === 'any') {
105
+ // Any approval is enough
106
+ newStatus = 'approved';
107
+ } else {
108
+ // All must approve - check if everyone has approved
109
+ const allApproved = plan.approval.requested_from.every(h =>
110
+ plan.approval.responses.some(r => r.handle === h && r.status === 'approve')
111
+ );
112
+ newStatus = allApproved ? 'approved' : 'pending';
113
+ }
114
+ } else if (decision === 'reject') {
115
+ // Any rejection rejects the plan
116
+ newStatus = 'rejected';
117
+ } else if (decision === 'needs_changes') {
118
+ // Any needs_changes puts it in that state
119
+ newStatus = 'needs_changes';
120
+ }
121
+
122
+ plan.approval.status = newStatus;
123
+ if (newStatus !== 'pending') {
124
+ plan.approval.resolved_at = new Date().toISOString();
125
+ }
126
+
127
+ // Save the updated plan
128
+ const updateResult = await store.updateArtifact(plan.id, plan);
129
+
130
+ if (!updateResult.success) {
131
+ return {
132
+ display: `❌ Failed to save response: ${updateResult.error}`
133
+ };
134
+ }
135
+
136
+ // Notify the plan creator
137
+ const creator = plan.created_by;
138
+ const statusEmoji = {
139
+ approve: '✅',
140
+ needs_changes: '🔄',
141
+ reject: '❌'
142
+ }[decision];
143
+
144
+ const statusText = {
145
+ approve: 'approved',
146
+ needs_changes: 'requested changes on',
147
+ reject: 'rejected'
148
+ }[decision];
149
+
150
+ try {
151
+ await store.sendMessage(myHandle, creator,
152
+ `${statusEmoji} **@${myHandle} ${statusText} your plan**\n\n` +
153
+ `**Plan:** ${plan.title}\n` +
154
+ (note ? `**Note:** ${note}\n` : '') +
155
+ `**Status:** ${newStatus === 'pending' ? '⏳ Still pending (awaiting more approvers)' : `${statusEmoji} ${newStatus.charAt(0).toUpperCase() + newStatus.slice(1)}`}`,
156
+ {
157
+ type: 'plan_response',
158
+ plan_id: plan.id,
159
+ plan_slug: plan.slug,
160
+ decision,
161
+ new_status: newStatus
162
+ }
163
+ );
164
+ } catch (error) {
165
+ console.error('[APPROVE] Failed to notify creator:', error);
166
+ }
167
+
168
+ // Build response
169
+ let display = `${statusEmoji} **Response recorded: ${decision}**\n\n`;
170
+ display += `**Plan:** ${plan.title}\n`;
171
+ display += `**Your decision:** ${decision}${note ? ` — "${note}"` : ''}\n`;
172
+ display += `**Plan status:** ${newStatus}\n\n`;
173
+
174
+ if (newStatus === 'approved') {
175
+ display += `✓ Plan is now approved! @${creator} has been notified.`;
176
+ } else if (newStatus === 'rejected') {
177
+ display += `Plan has been rejected. @${creator} has been notified.`;
178
+ } else if (newStatus === 'needs_changes') {
179
+ display += `Plan needs changes. @${creator} has been notified to update.`;
180
+ } else {
181
+ const remaining = plan.approval.requested_from.filter(h =>
182
+ !plan.approval.responses.some(r => r.handle === h)
183
+ );
184
+ display += `⏳ Waiting for: ${remaining.map(h => `@${h}`).join(', ')}`;
185
+ }
186
+
187
+ return {
188
+ display,
189
+ plan_id: plan.id,
190
+ plan_slug: plan.slug,
191
+ decision,
192
+ new_status: newStatus,
193
+ creator_notified: true
194
+ };
195
+ }
196
+
197
+ module.exports = { definition, handler };
@@ -2,7 +2,7 @@
2
2
  * vibe_create_artifact - Create social artifacts from conversations
3
3
  *
4
4
  * Artifacts are first-class objects with provenance, permissions, and social context.
5
- * They can be guides, learnings, or workspaces - all shareable via DM.
5
+ * They can be guides, learnings, workspaces, or plans - all shareable via DM.
6
6
  */
7
7
 
8
8
  const config = require('../config');
@@ -11,7 +11,7 @@ const { requireInit } = require('./_shared');
11
11
 
12
12
  const definition = {
13
13
  name: 'vibe_create_artifact',
14
- description: 'Create a social artifact (guide, learning, workspace) from conversation or memory. Artifacts are first-class objects with provenance and can be shared via DM.',
14
+ description: 'Create a social artifact (guide, learning, workspace, plan) from conversation or memory. Artifacts are first-class objects with provenance and can be shared via DM.',
15
15
  inputSchema: {
16
16
  type: 'object',
17
17
  properties: {
@@ -25,7 +25,7 @@ const definition = {
25
25
  },
26
26
  template: {
27
27
  type: 'string',
28
- enum: ['guide', 'learning', 'workspace'],
28
+ enum: ['guide', 'learning', 'workspace', 'plan'],
29
29
  description: 'Structural template to use'
30
30
  },
31
31
  content: {
@@ -176,6 +176,17 @@ async function handler(args) {
176
176
  forked_from: null
177
177
  };
178
178
 
179
+ // Add approval workflow for plan templates
180
+ if (template === 'plan') {
181
+ artifact.approval = {
182
+ status: 'pending',
183
+ requested_from: recipient ? [recipient.replace('@', '')] : [],
184
+ requires: 'any', // 'any' or 'all'
185
+ responses: [],
186
+ resolved_at: null
187
+ };
188
+ }
189
+
179
190
  // Store artifact
180
191
  const storeResult = await store.createArtifact(artifact);
181
192
 
@@ -0,0 +1,107 @@
1
+ /**
2
+ * vibe_artifacts_price - Set pricing for an artifact
3
+ *
4
+ * Enables artifact commerce by setting a price.
5
+ * Supports one-time purchase, pay-what-you-want, and tip jar models.
6
+ */
7
+
8
+ const config = require('../config');
9
+ const { requireInit, success, error, header } = require('./_shared');
10
+
11
+ const BASE_URL = process.env.VIBE_API_URL || 'https://www.slashvibe.dev';
12
+
13
+ const definition = {
14
+ name: 'vibe_artifacts_price',
15
+ description: 'Set pricing for an artifact you created. Enable commerce for your artifacts.',
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: {
19
+ artifact_id: {
20
+ type: 'string',
21
+ description: 'The artifact ID to price (e.g., art_abc123)'
22
+ },
23
+ price: {
24
+ type: 'number',
25
+ description: 'Price in dollars (e.g., 5.00). Set to 0 to make free.'
26
+ },
27
+ sale_type: {
28
+ type: 'string',
29
+ enum: ['one_time', 'pay_what_you_want', 'tip'],
30
+ description: 'Sale type: one_time (fixed price), pay_what_you_want (price is minimum), tip (free with optional tip)'
31
+ }
32
+ },
33
+ required: ['artifact_id', 'price', 'sale_type']
34
+ }
35
+ };
36
+
37
+ async function handler(args) {
38
+ const initCheck = requireInit();
39
+ if (initCheck) return initCheck;
40
+
41
+ const { artifact_id, price, sale_type } = args;
42
+ const handle = config.getHandle();
43
+
44
+ // Validate price
45
+ if (price < 0) {
46
+ return { display: error('Price cannot be negative') };
47
+ }
48
+
49
+ if (price > 1000) {
50
+ return { display: error('Price cannot exceed $1000') };
51
+ }
52
+
53
+ try {
54
+ const response = await fetch(`${BASE_URL}/api/artifacts/price`, {
55
+ method: 'POST',
56
+ headers: { 'Content-Type': 'application/json' },
57
+ body: JSON.stringify({
58
+ artifact_id,
59
+ handle,
60
+ price_cents: Math.round(price * 100),
61
+ sale_type
62
+ })
63
+ });
64
+
65
+ const data = await response.json();
66
+
67
+ if (!data.success) {
68
+ return { display: error(data.error || 'Failed to set pricing') };
69
+ }
70
+
71
+ // Format display
72
+ let display = success('Artifact Pricing Updated') + '\n\n';
73
+
74
+ display += header('Pricing Details', 3) + '\n';
75
+ display += `**Artifact:** ${data.artifact?.title || artifact_id}\n`;
76
+ display += `**Price:** $${price.toFixed(2)}\n`;
77
+ display += `**Sale Type:** ${formatSaleType(sale_type)}\n`;
78
+ display += `**Status:** ${price > 0 ? 'For Sale' : 'Free'}\n\n`;
79
+
80
+ if (data.artifact?.url) {
81
+ display += `**Share Link:** ${data.artifact.url}\n`;
82
+ }
83
+
84
+ if (sale_type === 'pay_what_you_want') {
85
+ display += `\n_Buyers can pay $${price.toFixed(2)} or more_`;
86
+ } else if (sale_type === 'tip') {
87
+ display += `\n_Free to access, tips optional_`;
88
+ }
89
+
90
+ return { display };
91
+
92
+ } catch (e) {
93
+ console.error('[artifacts-price] Error:', e);
94
+ return { display: error(`Failed to set pricing: ${e.message}`) };
95
+ }
96
+ }
97
+
98
+ function formatSaleType(type) {
99
+ const types = {
100
+ one_time: 'One-Time Purchase',
101
+ pay_what_you_want: 'Pay What You Want',
102
+ tip: 'Free with Tips'
103
+ };
104
+ return types[type] || type;
105
+ }
106
+
107
+ module.exports = { definition, handler };
@@ -0,0 +1,120 @@
1
+ /**
2
+ * vibe available — Signal you're open to chat
3
+ *
4
+ * Lower the barrier to connection by explicitly signaling
5
+ * what topics you're happy to help with.
6
+ *
7
+ * Usage:
8
+ * - vibe available "React, auth, Postgres"
9
+ * - vibe available --clear
10
+ * - vibe available (show current)
11
+ */
12
+
13
+ const config = require('../config');
14
+ const store = require('../store');
15
+ const { requireInit } = require('./_shared');
16
+
17
+ const definition = {
18
+ name: 'vibe_available',
19
+ description: 'Signal you\'re open to chat about specific topics. Others see you\'re available and what you can help with.',
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {
23
+ topics: {
24
+ type: 'string',
25
+ description: 'Topics you\'re available to help with (e.g., "React, auth, Postgres")'
26
+ },
27
+ clear: {
28
+ type: 'boolean',
29
+ description: 'Clear your availability - stop signaling you\'re open'
30
+ }
31
+ }
32
+ }
33
+ };
34
+
35
+ async function handler(args) {
36
+ const initCheck = requireInit();
37
+ if (initCheck) return initCheck;
38
+
39
+ const myHandle = config.getHandle();
40
+ const { topics, clear } = args;
41
+
42
+ // Clear availability
43
+ if (clear) {
44
+ await store.heartbeat(myHandle, config.getOneLiner(), {
45
+ availableFor: null
46
+ });
47
+
48
+ return {
49
+ display: `## Availability Cleared
50
+
51
+ You're no longer signaling openness to chat.
52
+
53
+ Others won't see availability topics next to your name.
54
+
55
+ **To signal availability again:**
56
+ \`vibe available "React, auth, Postgres"\``
57
+ };
58
+ }
59
+
60
+ // Show current availability
61
+ if (!topics) {
62
+ return {
63
+ display: `## Signal Availability
64
+
65
+ Let others know you're open to chat about specific topics.
66
+
67
+ **Set your availability:**
68
+ \`vibe available "React, auth, Postgres"\`
69
+
70
+ **What happens:**
71
+ - Your presence shows: \`@${myHandle} — available for: React, auth, Postgres\`
72
+ - Others know they won't be bothering you
73
+ - Lower barrier to reaching out
74
+
75
+ **Clear availability:**
76
+ \`vibe available --clear\`
77
+
78
+ _Availability is ephemeral — clears when you go offline._`
79
+ };
80
+ }
81
+
82
+ // Parse topics (comma or space separated)
83
+ const topicList = topics
84
+ .split(/[,\s]+/)
85
+ .map(t => t.trim().toLowerCase())
86
+ .filter(t => t.length > 0 && t.length <= 30)
87
+ .slice(0, 5); // Max 5 topics
88
+
89
+ if (topicList.length === 0) {
90
+ return {
91
+ display: `Please provide at least one topic. Example:\n\`vibe available "React, auth, Postgres"\``
92
+ };
93
+ }
94
+
95
+ // Update presence with availability
96
+ await store.heartbeat(myHandle, config.getOneLiner(), {
97
+ availableFor: topicList
98
+ });
99
+
100
+ const topicsFormatted = topicList.join(', ');
101
+
102
+ return {
103
+ display: `## You're Available! 🤝
104
+
105
+ **Topics:** ${topicsFormatted}
106
+
107
+ Others now see:
108
+ \`@${myHandle} — available for: ${topicsFormatted}\`
109
+
110
+ This signals you're open to questions about these topics.
111
+ People are more likely to reach out when they know they won't be bothering you.
112
+
113
+ **Clear when busy:**
114
+ \`vibe available --clear\`
115
+
116
+ _Availability clears automatically when you go offline._`
117
+ };
118
+ }
119
+
120
+ module.exports = { definition, handler };