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
package/tools/invite.js CHANGED
@@ -203,14 +203,25 @@ _Copy the link above and send it._`
203
203
  const genNote = justGenerated ? ' (just generated)' : '';
204
204
  const randomMsg = INVITE_MESSAGES[Math.floor(Math.random() * INVITE_MESSAGES.length)];
205
205
 
206
+ // Build social share URLs
207
+ const tweetText = encodeURIComponent(`${randomMsg}\n\nhttps://${shareUrl}`);
208
+ const twitterUrl = `https://twitter.com/intent/tweet?text=${tweetText}`;
209
+
206
210
  return {
207
211
  display: `## Quick Invite${genNote}
208
212
 
209
- > ${randomMsg}
210
- >
211
- > **${shareUrl}**
213
+ **Copy this:**
214
+ \`\`\`
215
+ ${randomMsg}
216
+
217
+ https://${shareUrl}
218
+ \`\`\`
219
+
220
+ **Or share directly:**
221
+ - [Tweet it](${twitterUrl})
222
+ - Slack/Discord: Just paste the link above
212
223
 
213
- _Copy and send. ${shareCode ? `Bonus code earned when they join.` : ''}_`
224
+ ${shareCode ? `_When they join, you both get bonus codes._` : ''}`
214
225
  };
215
226
  }
216
227
 
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);
package/tools/plan.js ADDED
@@ -0,0 +1,225 @@
1
+ /**
2
+ * vibe_plan — Create a plan and request approval
3
+ *
4
+ * Plans are structured proposals that can be sent for async approval.
5
+ * They're artifacts with an approval workflow built in.
6
+ */
7
+
8
+ const config = require('../config');
9
+ const store = require('../store');
10
+ const { requireInit, normalizeHandle } = require('./_shared');
11
+
12
+ const definition = {
13
+ name: 'vibe_plan',
14
+ description: 'Create a plan and request approval from collaborators. Plans are structured proposals with summary, problem, changes, and impact.',
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: {
18
+ title: {
19
+ type: 'string',
20
+ description: 'Plan title (e.g., "Refactor auth system", "CLAUDE.md cleanup")'
21
+ },
22
+ summary: {
23
+ type: 'string',
24
+ description: 'One-line summary of what this plan does'
25
+ },
26
+ problem: {
27
+ type: 'string',
28
+ description: 'What problem does this solve? (optional)'
29
+ },
30
+ changes: {
31
+ type: 'array',
32
+ items: { type: 'string' },
33
+ description: 'List of proposed changes'
34
+ },
35
+ impact: {
36
+ type: 'string',
37
+ description: 'What will be affected by this change? (optional)'
38
+ },
39
+ request_approval_from: {
40
+ type: 'array',
41
+ items: { type: 'string' },
42
+ description: 'Handles to request approval from (e.g., ["@brightseth", "@alice"])'
43
+ },
44
+ requires: {
45
+ type: 'string',
46
+ enum: ['any', 'all'],
47
+ description: 'Require approval from any one person (default) or all of them'
48
+ }
49
+ },
50
+ required: ['title', 'summary', 'changes', 'request_approval_from']
51
+ }
52
+ };
53
+
54
+ // Generate artifact ID and slug
55
+ function generateArtifactId() {
56
+ return `plan_${Date.now()}_${Math.random().toString(36).substring(7)}`;
57
+ }
58
+
59
+ function generateSlug(title) {
60
+ return title
61
+ .toLowerCase()
62
+ .replace(/[^a-z0-9]+/g, '-')
63
+ .replace(/^-|-$/g, '')
64
+ .substring(0, 60);
65
+ }
66
+
67
+ async function handler(args) {
68
+ const initCheck = requireInit();
69
+ if (initCheck) return initCheck;
70
+
71
+ const {
72
+ title,
73
+ summary,
74
+ problem,
75
+ changes,
76
+ impact,
77
+ request_approval_from,
78
+ requires = 'any'
79
+ } = args;
80
+
81
+ const creator = config.getHandle();
82
+
83
+ // Validate approvers
84
+ if (!request_approval_from || request_approval_from.length === 0) {
85
+ return {
86
+ display: '❌ Must specify at least one approver in request_approval_from'
87
+ };
88
+ }
89
+
90
+ // Normalize approver handles
91
+ const approvers = request_approval_from.map(h => normalizeHandle(h));
92
+
93
+ // Build content blocks for the plan
94
+ const blocks = [
95
+ { type: 'summary', data: { text: summary } }
96
+ ];
97
+
98
+ if (problem) {
99
+ blocks.push({ type: 'problem', data: { text: problem } });
100
+ }
101
+
102
+ blocks.push({
103
+ type: 'proposed_changes',
104
+ data: { items: changes }
105
+ });
106
+
107
+ if (impact) {
108
+ blocks.push({ type: 'impact', data: { text: impact } });
109
+ }
110
+
111
+ // Build the plan artifact
112
+ const planId = generateArtifactId();
113
+ const slug = generateSlug(title);
114
+
115
+ // Build audience list (creator + all approvers)
116
+ const audience = new Set([creator, ...approvers]);
117
+
118
+ const plan = {
119
+ id: planId,
120
+ slug,
121
+ title,
122
+ template: 'plan',
123
+ content: { blocks },
124
+
125
+ // Social metadata
126
+ created_by: creator,
127
+ created_for: approvers[0], // Primary approver
128
+ thread_id: null,
129
+
130
+ // Privacy - plans are unlisted by default
131
+ visibility: 'unlisted',
132
+ audience: Array.from(audience),
133
+
134
+ // Provenance
135
+ provenance: {
136
+ source_type: 'manual',
137
+ personalized_for: null,
138
+ notes: null
139
+ },
140
+
141
+ // Lifecycle
142
+ created_at: new Date().toISOString(),
143
+ updated_at: new Date().toISOString(),
144
+ expires_at: null,
145
+
146
+ // Evolution
147
+ revision: 1,
148
+ forked_from: null,
149
+
150
+ // Approval workflow
151
+ approval: {
152
+ status: 'pending',
153
+ requested_from: approvers,
154
+ requires,
155
+ responses: [],
156
+ resolved_at: null
157
+ }
158
+ };
159
+
160
+ // Store the plan
161
+ const storeResult = await store.createArtifact(plan);
162
+
163
+ if (!storeResult.success) {
164
+ return {
165
+ display: `❌ Failed to create plan: ${storeResult.error}`
166
+ };
167
+ }
168
+
169
+ const planUrl = `https://slashvibe.dev/a/${slug}`;
170
+
171
+ // Send DM to each approver
172
+ const dmResults = [];
173
+ for (const approver of approvers) {
174
+ try {
175
+ const dmResult = await store.sendMessage(creator, approver,
176
+ `📋 **Plan: ${title}**\n\n` +
177
+ `**Summary:** ${summary}\n\n` +
178
+ (problem ? `**Problem:** ${problem}\n\n` : '') +
179
+ `**Changes:**\n${changes.map(c => `• ${c}`).join('\n')}\n\n` +
180
+ (impact ? `**Impact:** ${impact}\n\n` : '') +
181
+ `---\n` +
182
+ `👍 Approve · 🔄 Request changes · ❌ Reject\n` +
183
+ `Use: \`vibe approve ${slug}\` or \`vibe approve ${slug} --reject\``,
184
+ {
185
+ type: 'plan_card',
186
+ plan_id: planId,
187
+ plan_slug: slug,
188
+ plan_url: planUrl
189
+ }
190
+ );
191
+ dmResults.push({ approver, success: dmResult.success });
192
+ } catch (error) {
193
+ console.error(`[PLAN] Failed to DM ${approver}:`, error);
194
+ dmResults.push({ approver, success: false, error: error.message });
195
+ }
196
+ }
197
+
198
+ // Build response
199
+ const successfulDMs = dmResults.filter(r => r.success).length;
200
+ const approverList = approvers.map(a => `@${a}`).join(', ');
201
+
202
+ let display = `📋 **Plan created: ${title}**\n\n`;
203
+ display += `**Status:** ⏳ Pending approval\n`;
204
+ display += `**Requested from:** ${approverList}\n`;
205
+ display += `**Requires:** ${requires === 'all' ? 'All approvers' : 'Any approver'}\n\n`;
206
+
207
+ if (successfulDMs === approvers.length) {
208
+ display += `✓ Sent to ${successfulDMs} approver${successfulDMs > 1 ? 's' : ''}\n`;
209
+ } else {
210
+ display += `⚠️ Sent to ${successfulDMs}/${approvers.length} approvers\n`;
211
+ }
212
+
213
+ display += `\n**Plan ID:** \`${slug}\``;
214
+
215
+ return {
216
+ display,
217
+ plan_id: planId,
218
+ plan_slug: slug,
219
+ plan_url: planUrl,
220
+ approval_status: 'pending',
221
+ approvers
222
+ };
223
+ }
224
+
225
+ module.exports = { definition, handler };
@@ -0,0 +1,144 @@
1
+ /**
2
+ * vibe_proof_of_work - Get unified proof-of-work summary for a user
3
+ *
4
+ * Shows the complete "proof of work" picture:
5
+ * - PAST: Artifacts created/sold, sessions shared
6
+ * - PRESENT: Live presence, current work
7
+ * - FUTURE: Gigs completed/posted, hire availability
8
+ * - UNIFIED: Vibe score, tier, proven skills
9
+ */
10
+
11
+ const config = require('../config');
12
+ const { requireInit, normalizeHandle, displayHandle, header, divider, emptyState, formatTimeAgo } = require('./_shared');
13
+ const { actions, formatActions } = require('./_actions');
14
+
15
+ const BASE_URL = process.env.VIBE_API_URL || 'https://www.slashvibe.dev';
16
+
17
+ const definition = {
18
+ name: 'vibe_proof_of_work',
19
+ description: 'Get proof-of-work summary for a user. Shows artifacts, sessions, gigs, vibe score, and proven skills.',
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {
23
+ handle: {
24
+ type: 'string',
25
+ description: 'User handle to look up (default: yourself)'
26
+ }
27
+ }
28
+ }
29
+ };
30
+
31
+ async function handler(args) {
32
+ const initCheck = requireInit();
33
+ if (initCheck) return initCheck;
34
+
35
+ const targetHandle = normalizeHandle(args.handle) || config.getHandle();
36
+
37
+ try {
38
+ const response = await fetch(`${BASE_URL}/api/proof-of-work?handle=${encodeURIComponent(targetHandle)}`);
39
+ const data = await response.json();
40
+
41
+ if (!data.success) {
42
+ return { display: `❌ ${data.error || 'Failed to fetch proof-of-work'}` };
43
+ }
44
+
45
+ const pow = data.proof_of_work;
46
+ const isSelf = targetHandle === config.getHandle();
47
+
48
+ // Build display
49
+ let display = header(`Proof of Work: ${displayHandle(targetHandle)}`, 2) + '\n\n';
50
+
51
+ // Vibe Score & Tier
52
+ display += `**Vibe Score:** ${pow.unified?.vibe_score || 0}\n`;
53
+ display += `**Tier:** ${formatTier(pow.unified?.tier)}\n\n`;
54
+
55
+ // ━━━ PAST ━━━
56
+ display += header('📜 PAST (Artifacts & Sessions)', 3) + '\n';
57
+ const past = pow.past || {};
58
+ display += `• **Artifacts Created:** ${past.artifacts_created || 0}\n`;
59
+ display += `• **Artifacts Sold:** ${past.artifacts_sold || 0}\n`;
60
+ display += `• **Revenue Earned:** $${((past.revenue_cents || 0) / 100).toFixed(2)}\n`;
61
+ display += `• **Sessions Shared:** ${past.sessions_shared || 0}\n\n`;
62
+
63
+ // ━━━ PRESENT ━━━
64
+ display += header('🟢 PRESENT (Live)', 3) + '\n';
65
+ const present = pow.present || {};
66
+ if (present.is_online) {
67
+ display += `• **Status:** Online\n`;
68
+ if (present.working_on) {
69
+ display += `• **Working on:** ${present.working_on}\n`;
70
+ }
71
+ if (present.project) {
72
+ display += `• **Project:** ${present.project}\n`;
73
+ }
74
+ } else {
75
+ display += `• **Status:** Offline\n`;
76
+ if (present.last_seen) {
77
+ display += `• **Last seen:** ${formatTimeAgo(present.last_seen)}\n`;
78
+ }
79
+ }
80
+ display += '\n';
81
+
82
+ // ━━━ FUTURE ━━━
83
+ display += header('🚀 FUTURE (Gigs & Availability)', 3) + '\n';
84
+ const future = pow.future || {};
85
+ display += `• **Gigs Completed:** ${future.gigs_completed || 0}\n`;
86
+ display += `• **Gigs Posted:** ${future.gigs_posted || 0}\n`;
87
+ display += `• **Avg Rating:** ${future.avg_rating ? `${future.avg_rating.toFixed(1)}⭐` : 'No ratings yet'}\n`;
88
+ display += `• **Available for Hire:** ${future.available_for_hire ? 'Yes' : 'No'}\n\n`;
89
+
90
+ // ━━━ PROVEN SKILLS ━━━
91
+ const skills = pow.unified?.proven_skills || [];
92
+ if (skills.length > 0) {
93
+ display += header('🛠️ Proven Skills', 3) + '\n';
94
+ skills.slice(0, 10).forEach(skill => {
95
+ display += `• ${skill.name} (${skill.count} gig${skill.count !== 1 ? 's' : ''})\n`;
96
+ });
97
+ display += '\n';
98
+ }
99
+
100
+ // ━━━ SCORE BREAKDOWN ━━━
101
+ const breakdown = pow.unified?.score_breakdown;
102
+ if (breakdown) {
103
+ display += header('📊 Score Breakdown', 3) + '\n';
104
+ display += `• Gigs: ${breakdown.gigs || 0}\n`;
105
+ display += `• Artifacts: ${breakdown.artifacts || 0}\n`;
106
+ display += `• Sessions: ${breakdown.sessions || 0}\n`;
107
+ display += `• Invites: ${breakdown.invites || 0}\n`;
108
+ display += `• Ships: ${breakdown.ships || 0}\n`;
109
+ }
110
+
111
+ // CTA for self
112
+ if (isSelf) {
113
+ display += divider();
114
+ display += '💡 **Boost your score:** Complete gigs (+50), sell artifacts (+30), share sessions (+10)\n';
115
+ }
116
+
117
+ // Build result with tip actions
118
+ const result = { display };
119
+ result.actions = formatActions(actions.afterProofOfWork(targetHandle, isSelf));
120
+
121
+ return result;
122
+
123
+ } catch (e) {
124
+ console.error('[proof-of-work] Error:', e);
125
+ return { display: `❌ Failed to fetch proof-of-work: ${e.message}` };
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Format tier with emoji
131
+ */
132
+ function formatTier(tier) {
133
+ const tiers = {
134
+ newcomer: '🌱 Newcomer',
135
+ rising: '📈 Rising',
136
+ proven: '✅ Proven',
137
+ established: '🏆 Established',
138
+ expert: '⭐ Expert',
139
+ legendary: '👑 Legendary'
140
+ };
141
+ return tiers[tier] || tier || 'Unknown';
142
+ }
143
+
144
+ module.exports = { definition, handler };