slashvibe-mcp 0.3.21 → 0.3.23

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 (235) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +280 -47
  3. package/auto-update.js +10 -15
  4. package/config.js +36 -31
  5. package/crypto.js +1 -6
  6. package/debug.js +12 -0
  7. package/discord.js +19 -19
  8. package/eslint.config.js +54 -0
  9. package/index.js +217 -207
  10. package/intelligence/index.js +2 -9
  11. package/intelligence/infer.js +10 -16
  12. package/intelligence/patterns.js +23 -18
  13. package/intelligence/proactive.js +16 -15
  14. package/intelligence/serendipity.js +57 -20
  15. package/memory.js +13 -8
  16. package/migrate-v2.js +72 -0
  17. package/notification-emitter.js +2 -2
  18. package/notify.js +39 -14
  19. package/package.json +28 -29
  20. package/post-install.js +141 -0
  21. package/presence.js +2 -2
  22. package/prompts.js +5 -9
  23. package/protocol/index.js +123 -87
  24. package/protocol/telegram-commands.js +36 -37
  25. package/store/api.js +358 -529
  26. package/store/local.js +9 -10
  27. package/store/profiles.js +48 -192
  28. package/store/reservations.js +2 -9
  29. package/store/skills.js +69 -71
  30. package/store/sqlite.js +355 -0
  31. package/test-skills-bootstrap.js +20 -0
  32. package/test-v2-integration.js +385 -0
  33. package/tools/_actions.js +48 -387
  34. package/tools/_connection-queue.js +45 -56
  35. package/tools/_discovery-enhanced.js +52 -57
  36. package/tools/_discovery.js +87 -185
  37. package/tools/{l2-status.js → _experimental/l2-status.js} +68 -70
  38. package/tools/{shipback.js → _experimental/shipback.js} +4 -3
  39. package/tools/_proactive-discovery.js +60 -73
  40. package/tools/_shared/index.js +41 -64
  41. package/tools/admin-inbox.js +10 -15
  42. package/tools/agents.js +1 -1
  43. package/tools/artifact-create.js +13 -23
  44. package/tools/artifact-view.js +4 -4
  45. package/tools/{_deprecated/back.js → back.js} +1 -1
  46. package/tools/bye.js +3 -5
  47. package/tools/consent.js +2 -2
  48. package/tools/context.js +9 -10
  49. package/tools/crossword.js +3 -2
  50. package/tools/discover.js +94 -356
  51. package/tools/dm.js +27 -86
  52. package/tools/doctor.js +12 -41
  53. package/tools/drawing.js +34 -20
  54. package/tools/echo.js +11 -11
  55. package/tools/feed.js +30 -58
  56. package/tools/follow.js +64 -187
  57. package/tools/{_deprecated/forget.js → forget.js} +4 -7
  58. package/tools/game.js +144 -48
  59. package/tools/handoff.js +6 -8
  60. package/tools/help.js +3 -3
  61. package/tools/idea.js +15 -27
  62. package/tools/inbox.js +121 -293
  63. package/tools/init.js +54 -151
  64. package/tools/invite.js +8 -21
  65. package/tools/migrate.js +27 -24
  66. package/tools/multiplayer-game.js +50 -40
  67. package/tools/{_deprecated/mute.js → mute.js} +4 -3
  68. package/tools/notifications.js +58 -48
  69. package/tools/observe.js +12 -15
  70. package/tools/onboarding.js +8 -11
  71. package/tools/open.js +13 -144
  72. package/tools/party-game.js +23 -12
  73. package/tools/patterns.js +2 -1
  74. package/tools/ping.js +5 -7
  75. package/tools/react.js +28 -30
  76. package/tools/{_deprecated/recall.js → recall.js} +5 -10
  77. package/tools/release.js +4 -2
  78. package/tools/{_deprecated/remember.js → remember.js} +4 -6
  79. package/tools/report.js +2 -2
  80. package/tools/request.js +6 -26
  81. package/tools/reserve.js +1 -1
  82. package/tools/session-fork.js +97 -0
  83. package/tools/session-save.js +109 -0
  84. package/tools/settings.js +30 -99
  85. package/tools/ship.js +74 -56
  86. package/tools/{_deprecated/skills-exchange.js → skills-exchange.js} +38 -39
  87. package/tools/social-inbox.js +22 -28
  88. package/tools/social-post.js +24 -27
  89. package/tools/solo-game.js +54 -46
  90. package/tools/start.js +14 -148
  91. package/tools/status.js +21 -68
  92. package/tools/submit.js +4 -2
  93. package/tools/suggest-tags.js +36 -33
  94. package/tools/summarize.js +19 -16
  95. package/tools/tag-suggestions.js +72 -73
  96. package/tools/test.js +1 -1
  97. package/tools/{_deprecated/tictactoe.js → tictactoe.js} +26 -26
  98. package/tools/token.js +4 -4
  99. package/tools/update.js +1 -2
  100. package/tools/watch.js +132 -112
  101. package/tools/who.js +20 -40
  102. package/tools/{_deprecated/wordassociation.js → wordassociation.js} +23 -20
  103. package/tools/workshop-buddy.js +52 -53
  104. package/tools/x-mentions.js +0 -1
  105. package/tools/x-reply.js +0 -1
  106. package/twitter.js +14 -20
  107. package/version.json +8 -10
  108. package/webhook-runner.js +132 -0
  109. package/auth-store.js +0 -148
  110. package/bridges/bridge-monitor.js +0 -388
  111. package/bridges/discord-bot.js +0 -431
  112. package/bridges/farcaster.js +0 -299
  113. package/bridges/telegram.js +0 -261
  114. package/bridges/webhook-health.js +0 -420
  115. package/bridges/webhook-server.js +0 -437
  116. package/bridges/whatsapp.js +0 -441
  117. package/bridges/x-webhook.js +0 -423
  118. package/games/arcade.js +0 -406
  119. package/games/chess.js +0 -451
  120. package/games/colorguess.js +0 -343
  121. package/games/crossword-words.js +0 -171
  122. package/games/crossword.js +0 -461
  123. package/games/drawing.js +0 -347
  124. package/games/gameroulette.js +0 -300
  125. package/games/gamerouter.js +0 -336
  126. package/games/gamestatus.js +0 -337
  127. package/games/guessnumber.js +0 -209
  128. package/games/hangman.js +0 -279
  129. package/games/memory.js +0 -338
  130. package/games/multiplayer-tictactoe.js +0 -389
  131. package/games/pixelart.js +0 -399
  132. package/games/quickduel.js +0 -354
  133. package/games/riddle.js +0 -371
  134. package/games/rockpaperscissors.js +0 -291
  135. package/games/snake.js +0 -406
  136. package/games/storybuilder.js +0 -343
  137. package/games/tictactoe.js +0 -345
  138. package/games/twentyquestions.js +0 -286
  139. package/games/twotruths.js +0 -207
  140. package/games/werewolf.js +0 -508
  141. package/games/wordassociation.js +0 -247
  142. package/games/wordchain.js +0 -135
  143. package/intelligence/interests.js +0 -369
  144. package/setup.js +0 -480
  145. package/smart-inbox.js +0 -276
  146. package/tools/_deprecated/auto-suggest-connections.js +0 -304
  147. package/tools/_deprecated/bootstrap-skills.js +0 -231
  148. package/tools/_deprecated/bridge-dashboard.js +0 -342
  149. package/tools/_deprecated/bridge-health.js +0 -400
  150. package/tools/_deprecated/bridge-live.js +0 -384
  151. package/tools/_deprecated/bridges.js +0 -383
  152. package/tools/_deprecated/colorguess.js +0 -281
  153. package/tools/_deprecated/discover-insights.js +0 -379
  154. package/tools/_deprecated/discover-momentum.js +0 -256
  155. package/tools/_deprecated/discovery-analytics.js +0 -345
  156. package/tools/_deprecated/discovery-auto-suggest.js +0 -275
  157. package/tools/_deprecated/discovery-bootstrap.js +0 -267
  158. package/tools/_deprecated/discovery-daily.js +0 -375
  159. package/tools/_deprecated/discovery-dashboard.js +0 -385
  160. package/tools/_deprecated/discovery-digest.js +0 -314
  161. package/tools/_deprecated/discovery-hub.js +0 -357
  162. package/tools/_deprecated/discovery-insights.js +0 -384
  163. package/tools/_deprecated/discovery-momentum.js +0 -281
  164. package/tools/_deprecated/discovery-monitor.js +0 -319
  165. package/tools/_deprecated/discovery-proactive.js +0 -300
  166. package/tools/_deprecated/draw.js +0 -317
  167. package/tools/_deprecated/farcaster.js +0 -307
  168. package/tools/_deprecated/games-catalog.js +0 -376
  169. package/tools/_deprecated/games.js +0 -313
  170. package/tools/_deprecated/guessnumber.js +0 -194
  171. package/tools/_deprecated/hangman.js +0 -129
  172. package/tools/_deprecated/multiplayer-tictactoe.js +0 -303
  173. package/tools/_deprecated/riddle.js +0 -240
  174. package/tools/_deprecated/run-bootstrap.js +0 -69
  175. package/tools/_deprecated/skills-analytics.js +0 -349
  176. package/tools/_deprecated/skills-bootstrap.js +0 -301
  177. package/tools/_deprecated/skills-dashboard.js +0 -268
  178. package/tools/_deprecated/skills.js +0 -380
  179. package/tools/_deprecated/smart-intro.js +0 -353
  180. package/tools/_deprecated/storybuilder.js +0 -331
  181. package/tools/_deprecated/telegram-bot.js +0 -183
  182. package/tools/_deprecated/telegram-setup.js +0 -214
  183. package/tools/_deprecated/twentyquestions.js +0 -143
  184. package/tools/_shared.js +0 -234
  185. package/tools/_work-context.js +0 -338
  186. package/tools/_work-context.manual-test.js +0 -199
  187. package/tools/_work-context.test.js +0 -260
  188. package/tools/activity.js +0 -220
  189. package/tools/agent-treasury.js +0 -288
  190. package/tools/analytics.js +0 -191
  191. package/tools/approve.js +0 -197
  192. package/tools/arcade.js +0 -173
  193. package/tools/artifacts-price.js +0 -107
  194. package/tools/ask-expert.js +0 -160
  195. package/tools/available.js +0 -120
  196. package/tools/become-expert.js +0 -150
  197. package/tools/broadcast.js +0 -325
  198. package/tools/chat.js +0 -202
  199. package/tools/collaborative-drawing.js +0 -286
  200. package/tools/connection-status.js +0 -178
  201. package/tools/earnings.js +0 -126
  202. package/tools/friends.js +0 -207
  203. package/tools/genesis.js +0 -233
  204. package/tools/gig-browse.js +0 -206
  205. package/tools/gig-complete.js +0 -144
  206. package/tools/health.js +0 -87
  207. package/tools/leaderboard.js +0 -117
  208. package/tools/lib/git-apply.js +0 -206
  209. package/tools/lib/git-bundle.js +0 -407
  210. package/tools/mint.js +0 -377
  211. package/tools/plan.js +0 -225
  212. package/tools/profile.js +0 -219
  213. package/tools/proof-of-work.js +0 -144
  214. package/tools/pulse.js +0 -218
  215. package/tools/reply.js +0 -166
  216. package/tools/reputation.js +0 -175
  217. package/tools/schedule.js +0 -367
  218. package/tools/search-messages.js +0 -123
  219. package/tools/session.js +0 -467
  220. package/tools/session_price.js +0 -128
  221. package/tools/smart-check.js +0 -201
  222. package/tools/social-processor.js +0 -445
  223. package/tools/streak.js +0 -147
  224. package/tools/stuck.js +0 -297
  225. package/tools/subscribe.js +0 -148
  226. package/tools/subscriptions.js +0 -134
  227. package/tools/tip.js +0 -193
  228. package/tools/wallet.js +0 -269
  229. package/tools/webhook-test.js +0 -388
  230. package/tools/withdraw.js +0 -145
  231. package/tools/work-summary.js +0 -96
  232. package/tools/workshop.js +0 -327
  233. /package/tools/{l2-bridge.js → _experimental/l2-bridge.js} +0 -0
  234. /package/tools/{l2.js → _experimental/l2.js} +0 -0
  235. /package/tools/{_deprecated/away.js → away.js} +0 -0
package/tools/feed.js CHANGED
@@ -13,8 +13,7 @@
13
13
  */
14
14
 
15
15
  const config = require('../config');
16
- const { requireInit, header, emptyState, formatTimeAgo, divider, fetchRelevantUsers } = require('./_shared');
17
- const { actions, formatActions } = require('./_actions');
16
+ const { requireInit, normalizeHandle, header, emptyState, formatTimeAgo, divider } = require('./_shared');
18
17
 
19
18
  const definition = {
20
19
  name: 'vibe_feed',
@@ -44,21 +43,21 @@ const definition = {
44
43
  };
45
44
 
46
45
  const TYPE_CONFIG = {
47
- 'idea': { emoji: '💡', label: 'idea', verb: 'had an idea' },
48
- 'riff': { emoji: '↳', label: 'riff', verb: 'riffed' },
49
- 'shipped': { emoji: '🚀', label: 'ship', verb: 'shipped' },
50
- 'request': { emoji: '🔓', label: 'request', verb: 'requested' },
51
- 'claim': { emoji: '🔨', label: 'claim', verb: 'claimed' },
52
- 'observation': { emoji: '👁️', label: 'observation', verb: 'observed' },
53
- 'general': { emoji: '📝', label: 'post', verb: 'posted' }
46
+ idea: { emoji: '💡', label: 'idea', verb: 'had an idea' },
47
+ riff: { emoji: '↳', label: 'riff', verb: 'riffed' },
48
+ shipped: { emoji: '🚀', label: 'ship', verb: 'shipped' },
49
+ request: { emoji: '🔓', label: 'request', verb: 'requested' },
50
+ claim: { emoji: '🔨', label: 'claim', verb: 'claimed' },
51
+ observation: { emoji: '👁️', label: 'observation', verb: 'observed' },
52
+ general: { emoji: '📝', label: 'post', verb: 'posted' }
54
53
  };
55
54
 
56
55
  const CATEGORY_MAP = {
57
- 'ideas': ['idea', 'riff'],
58
- 'ships': ['shipped'],
59
- 'requests': ['request', 'claim'],
60
- 'riffs': ['riff'],
61
- 'observations': ['observation']
56
+ ideas: ['idea', 'riff'],
57
+ ships: ['shipped'],
58
+ requests: ['request', 'claim'],
59
+ riffs: ['riff'],
60
+ observations: ['observation']
62
61
  };
63
62
 
64
63
  async function handler(args) {
@@ -89,39 +88,18 @@ async function handler(args) {
89
88
 
90
89
  // Filter by author if specified
91
90
  if (args.from) {
92
- const targetHandle = args.from.replace('@', '').toLowerCase();
91
+ const targetHandle = normalizeHandle(args.from);
93
92
  entries = entries.filter(e => e.author.toLowerCase() === targetHandle);
94
93
  }
95
94
 
96
95
  // Filter by tag if specified
97
96
  if (args.tag) {
98
97
  const tag = args.tag.toLowerCase();
99
- entries = entries.filter(e =>
100
- e.tags && e.tags.some(t => t.toLowerCase().includes(tag))
101
- );
98
+ entries = entries.filter(e => e.tags && e.tags.some(t => t.toLowerCase().includes(tag)));
102
99
  }
103
100
 
104
- // Fetch relevant users to personalize feed
105
- // Content from relevant users gets a time boost (appears more recent)
106
- const relevantHandles = new Set();
107
- try {
108
- const relevancy = await fetchRelevantUsers(myHandle, 'feed', 20);
109
- if (relevancy && relevancy.matches) {
110
- relevancy.matches.forEach(m => relevantHandles.add(m.handle.toLowerCase()));
111
- }
112
- } catch (e) {
113
- // Don't fail feed if relevancy fails
114
- console.log('[feed] relevancy fetch error:', e.message);
115
- }
116
-
117
- // Sort by timestamp with relevancy boost
118
- // Relevant users get a 6-hour boost (their content appears "more recent")
119
- const RELEVANCY_BOOST_MS = 6 * 60 * 60 * 1000; // 6 hours in ms
120
- entries.sort((a, b) => {
121
- const aBoost = relevantHandles.has(a.author.toLowerCase()) ? RELEVANCY_BOOST_MS : 0;
122
- const bBoost = relevantHandles.has(b.author.toLowerCase()) ? RELEVANCY_BOOST_MS : 0;
123
- return (b.timestamp + bBoost) - (a.timestamp + aBoost);
124
- });
101
+ // Sort by timestamp (newest first)
102
+ entries.sort((a, b) => b.timestamp - a.timestamp);
125
103
 
126
104
  // Limit
127
105
  entries = entries.slice(0, limit);
@@ -131,7 +109,7 @@ async function handler(args) {
131
109
  let suggestion = 'Start something! "idea: [your idea]"';
132
110
 
133
111
  if (args.from) {
134
- emptyMsg = `No activity from @${args.from.replace('@', '')} yet`;
112
+ emptyMsg = `No activity from @${normalizeHandle(args.from)} yet`;
135
113
  suggestion = `Try "feed" to see all activity`;
136
114
  }
137
115
 
@@ -146,7 +124,7 @@ async function handler(args) {
146
124
  display += ` (${args.filter})`;
147
125
  }
148
126
  if (args.from) {
149
- display += ` @${args.from.replace('@', '')}`;
127
+ display += ` @${normalizeHandle(args.from)}`;
150
128
  }
151
129
  if (args.tag) {
152
130
  display += ` #${args.tag}`;
@@ -168,9 +146,7 @@ async function handler(args) {
168
146
  display += `${typeInfo.emoji} **@${entry.author}**${meTag} ${typeInfo.verb}\n`;
169
147
 
170
148
  // Content (truncated if long)
171
- const truncatedContent = mainContent.length > 100
172
- ? mainContent.slice(0, 97) + '...'
173
- : mainContent;
149
+ const truncatedContent = mainContent.length > 100 ? mainContent.slice(0, 97) + '...' : mainContent;
174
150
  display += ` "${truncatedContent}"\n`;
175
151
 
176
152
  // Metadata (URL, inspired by, etc.)
@@ -185,7 +161,15 @@ async function handler(args) {
185
161
  // Tags (non-system ones)
186
162
  if (entry.tags && entry.tags.length > 0) {
187
163
  const visibleTags = entry.tags
188
- .filter(t => !t.startsWith('inspired:') && !t.startsWith('fulfills:') && !t.startsWith('riff:') && !t.startsWith('claim:') && !t.startsWith('observation:') && !t.startsWith('type:'))
164
+ .filter(
165
+ t =>
166
+ !t.startsWith('inspired:') &&
167
+ !t.startsWith('fulfills:') &&
168
+ !t.startsWith('riff:') &&
169
+ !t.startsWith('claim:') &&
170
+ !t.startsWith('observation:') &&
171
+ !t.startsWith('type:')
172
+ )
189
173
  .slice(0, 3);
190
174
  if (visibleTags.length > 0) {
191
175
  display += ` ${visibleTags.map(t => `#${t}`).join(' ')}\n`;
@@ -206,19 +190,7 @@ async function handler(args) {
206
190
  display += ` _${timeAgo}_\n\n`;
207
191
  });
208
192
 
209
- // Build response with tip actions if we have ships
210
- const response = { display };
211
-
212
- // Find the most recent ship to offer tip action for
213
- const recentShip = entries.find(e => e.category === 'shipped');
214
- if (recentShip && recentShip.author.toLowerCase() !== myHandle.toLowerCase()) {
215
- response.actions = formatActions(actions.afterShipView(recentShip));
216
- response.hint = 'tip_ship_available';
217
- response.ship_author = recentShip.author;
218
- }
219
-
220
- return response;
221
-
193
+ return { display };
222
194
  } catch (error) {
223
195
  return { display: `⚠️ Failed to load feed: ${error.message}` };
224
196
  }
package/tools/follow.js CHANGED
@@ -1,224 +1,101 @@
1
1
  /**
2
- * vibe follow — Follow/unfollow creators
2
+ * vibe follow/unfollow — Follow and unfollow creators
3
3
  *
4
- * Commands:
5
- * - follow @handle → Follow a creator
6
- * - unfollow @handle → Unfollow a creator
7
- * - following → See who you follow (+ their live status)
8
- * - followers → See who follows you
4
+ * vibe_follow: Follow a creator to see their updates
5
+ * vibe_unfollow: Unfollow a creator
9
6
  */
10
7
 
8
+ const { requireInit, normalizeHandle } = require('./_shared');
11
9
  const config = require('../config');
12
- const { requireInit } = require('./_shared');
10
+ const store = require('../store');
13
11
 
14
- const definition = {
12
+ const followDefinition = {
15
13
  name: 'vibe_follow',
16
- description: 'Follow creators to get notified when they go live. Unfollow, see who you follow, see your followers.',
14
+ description: 'Follow a creator to see their updates, ships, and sessions.',
17
15
  inputSchema: {
18
16
  type: 'object',
19
17
  properties: {
20
- action: {
18
+ handle: {
21
19
  type: 'string',
22
- enum: ['follow', 'unfollow', 'following', 'followers'],
23
- description: 'Action: follow, unfollow, following, or followers'
24
- },
20
+ description: 'Who to follow (e.g., @stan)'
21
+ }
22
+ },
23
+ required: ['handle']
24
+ }
25
+ };
26
+
27
+ const unfollowDefinition = {
28
+ name: 'vibe_unfollow',
29
+ description: 'Unfollow a creator.',
30
+ inputSchema: {
31
+ type: 'object',
32
+ properties: {
25
33
  handle: {
26
34
  type: 'string',
27
- description: 'Handle to follow/unfollow (e.g., @creator)'
35
+ description: 'Who to unfollow (e.g., @stan)'
28
36
  }
29
- }
37
+ },
38
+ required: ['handle']
30
39
  }
31
40
  };
32
41
 
33
- async function handler(args) {
42
+ async function followHandler(args) {
34
43
  const initCheck = requireInit();
35
44
  if (initCheck) return initCheck;
36
45
 
37
46
  const myHandle = config.getHandle();
38
- const apiUrl = config.getApiUrl();
39
- const action = args.action || 'following';
40
- const targetHandle = args.handle?.replace('@', '').toLowerCase();
41
-
42
- try {
43
- // FOLLOW
44
- if (action === 'follow') {
45
- if (!targetHandle) {
46
- return { display: '⚠️ Specify who to follow: `vibe follow @handle`' };
47
- }
48
-
49
- const response = await fetch(`${apiUrl}/api/follow`, {
50
- method: 'POST',
51
- headers: { 'Content-Type': 'application/json' },
52
- body: JSON.stringify({
53
- follower: myHandle,
54
- following: targetHandle
55
- })
56
- });
57
-
58
- const data = await response.json();
59
-
60
- if (!data.success) {
61
- return { display: `⚠️ ${data.error}` };
62
- }
47
+ const them = normalizeHandle(args.handle);
63
48
 
64
- if (data.action === 'followed') {
65
- let display = `✅ Now following **@${targetHandle}**\n\n`;
66
- display += `You'll be notified when they go live.\n\n`;
67
- display += `_You follow ${data.counts.youAreFollowing} creators_`;
68
- return { display };
69
- } else {
70
- return { display: `Already following @${targetHandle}` };
71
- }
72
- }
73
-
74
- // UNFOLLOW
75
- if (action === 'unfollow') {
76
- if (!targetHandle) {
77
- return { display: '⚠️ Specify who to unfollow: `vibe unfollow @handle`' };
78
- }
79
-
80
- const response = await fetch(`${apiUrl}/api/follow`, {
81
- method: 'DELETE',
82
- headers: { 'Content-Type': 'application/json' },
83
- body: JSON.stringify({
84
- follower: myHandle,
85
- following: targetHandle
86
- })
87
- });
88
-
89
- const data = await response.json();
90
-
91
- if (!data.success) {
92
- return { display: `⚠️ ${data.error}` };
93
- }
94
-
95
- let display = `✅ Unfollowed **@${targetHandle}**\n\n`;
96
- display += `_You follow ${data.counts.youAreFollowing} creators_`;
97
- return { display };
98
- }
99
-
100
- // FOLLOWING (who I follow)
101
- if (action === 'following') {
102
- const response = await fetch(`${apiUrl}/api/following?handle=${myHandle}`);
103
- const data = await response.json();
104
-
105
- if (!data.success) {
106
- return { display: `⚠️ ${data.error}` };
107
- }
108
-
109
- if (data.following.length === 0) {
110
- let display = `## Following\n\n`;
111
- display += `_You're not following anyone yet._\n\n`;
112
- display += `**Discover creators:**\n`;
113
- display += `- \`vibe discover\` — Find recommended creators\n`;
114
- display += `- \`vibe follow @handle\` — Follow someone`;
115
- return { display };
116
- }
117
-
118
- let display = `## Following (${data.count})\n\n`;
119
-
120
- // Group by status
121
- const live = data.following.filter(f => f.isLive);
122
- const scheduled = data.following.filter(f => !f.isLive && f.nextScheduled);
123
- const other = data.following.filter(f => !f.isLive && !f.nextScheduled);
124
-
125
- if (live.length > 0) {
126
- display += `**🔴 Live Now**\n`;
127
- for (const f of live) {
128
- display += `• **@${f.handle}** — [Watch](https://slashvibe.dev/watch/${f.roomId})\n`;
129
- }
130
- display += `\n`;
131
- }
132
-
133
- if (scheduled.length > 0) {
134
- display += `**📅 Upcoming**\n`;
135
- for (const f of scheduled) {
136
- const when = formatScheduledTime(f.nextScheduled.scheduledFor);
137
- display += `• **@${f.handle}** — ${f.nextScheduled.title} (${when})\n`;
138
- }
139
- display += `\n`;
140
- }
141
-
142
- if (other.length > 0) {
143
- display += `**Following**\n`;
144
- for (const f of other) {
145
- const followers = f.followerCount > 0 ? ` (${f.followerCount} followers)` : '';
146
- display += `• @${f.handle}${followers}\n`;
147
- }
148
- }
149
-
150
- return { display };
151
- }
49
+ if (them === myHandle) {
50
+ return { display: "You can't follow yourself." };
51
+ }
152
52
 
153
- // FOLLOWERS (who follows me)
154
- if (action === 'followers') {
155
- const response = await fetch(`${apiUrl}/api/followers?handle=${myHandle}`);
156
- const data = await response.json();
53
+ const result = await store.followUser(myHandle, them);
157
54
 
158
- if (!data.success) {
159
- return { display: `⚠️ ${data.error}` };
160
- }
55
+ if (result.success === false) {
56
+ return { display: `Failed to follow @${them}: ${result.error || 'Unknown error'}` };
57
+ }
161
58
 
162
- if (data.followers.length === 0) {
163
- let display = `## Followers\n\n`;
164
- display += `_No followers yet._\n\n`;
165
- display += `**Build your audience:**\n`;
166
- display += `- Go live: \`vibe broadcast start\`\n`;
167
- display += `- Share what you're building\n`;
168
- display += `- Engage with other creators`;
169
- return { display };
170
- }
59
+ if (result.message === 'Already following') {
60
+ return { display: `You're already following **@${them}**.` };
61
+ }
171
62
 
172
- let display = `## Followers (${data.count})\n\n`;
63
+ const counts = result.counts || {};
64
+ let display = `Now following **@${them}**`;
65
+ if (counts.youAreFollowing) {
66
+ display += ` (following ${counts.youAreFollowing} people)`;
67
+ }
173
68
 
174
- // Show active vs inactive
175
- const active = data.followers.filter(f => f.isActive);
176
- const inactive = data.followers.filter(f => !f.isActive);
69
+ return { display };
70
+ }
177
71
 
178
- if (active.length > 0) {
179
- display += `**🟢 Active Now**\n`;
180
- for (const f of active) {
181
- const building = f.building ? ` — ${f.building}` : '';
182
- display += `• @${f.handle}${building}\n`;
183
- }
184
- display += `\n`;
185
- }
72
+ async function unfollowHandler(args) {
73
+ const initCheck = requireInit();
74
+ if (initCheck) return initCheck;
186
75
 
187
- if (inactive.length > 0) {
188
- display += `**Followers**\n`;
189
- for (const f of inactive.slice(0, 20)) {
190
- display += `• @${f.handle}\n`;
191
- }
192
- if (inactive.length > 20) {
193
- display += `_...and ${inactive.length - 20} more_\n`;
194
- }
195
- }
76
+ const myHandle = config.getHandle();
77
+ const them = normalizeHandle(args.handle);
196
78
 
197
- return { display };
198
- }
79
+ if (them === myHandle) {
80
+ return { display: "You can't unfollow yourself." };
81
+ }
199
82
 
200
- return { display: 'Unknown action. Use: follow, unfollow, following, followers' };
83
+ const result = await store.unfollowUser(myHandle, them);
201
84
 
202
- } catch (error) {
203
- return { display: `## Error\n\n${error.message}` };
85
+ if (result.success === false) {
86
+ return { display: `Failed to unfollow @${them}: ${result.error || 'Unknown error'}` };
204
87
  }
205
- }
206
88
 
207
- function formatScheduledTime(isoString) {
208
- const date = new Date(isoString);
209
- const now = new Date();
210
- const diffMs = date - now;
211
- const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
212
- const diffDays = Math.floor(diffHours / 24);
213
-
214
- if (diffDays === 0) {
215
- if (diffHours <= 1) return 'in < 1 hour';
216
- return `in ${diffHours} hours`;
217
- } else if (diffDays === 1) {
218
- return 'tomorrow';
219
- } else {
220
- return `in ${diffDays} days`;
89
+ const counts = result.counts || {};
90
+ let display = `Unfollowed **@${them}**`;
91
+ if (counts.youAreFollowing != null) {
92
+ display += ` (following ${counts.youAreFollowing} people)`;
221
93
  }
94
+
95
+ return { display };
222
96
  }
223
97
 
224
- module.exports = { definition, handler };
98
+ module.exports = {
99
+ follow: { definition: followDefinition, handler: followHandler },
100
+ unfollow: { definition: unfollowDefinition, handler: unfollowHandler }
101
+ };
@@ -9,7 +9,7 @@
9
9
  * vibe forget --all — Delete all memories (requires confirmation)
10
10
  */
11
11
 
12
- const config = require('../config');
12
+ const { requireInit, normalizeHandle } = require('./_shared');
13
13
  const memory = require('../memory');
14
14
  const fs = require('fs');
15
15
 
@@ -36,11 +36,8 @@ const definition = {
36
36
  };
37
37
 
38
38
  async function handler(args) {
39
- if (!config.isInitialized()) {
40
- return {
41
- display: 'Run `vibe init` first to set your identity.'
42
- };
43
- }
39
+ const initCheck = requireInit();
40
+ if (initCheck) return initCheck;
44
41
 
45
42
  const { all, confirm } = args;
46
43
  let { handle } = args;
@@ -93,7 +90,7 @@ async function handler(args) {
93
90
  }
94
91
 
95
92
  // Clean handle
96
- handle = handle.replace(/^@/, '').toLowerCase();
93
+ handle = normalizeHandle(handle);
97
94
 
98
95
  // Check if thread exists
99
96
  const countBefore = memory.count(handle);