slashvibe-mcp 0.2.9 → 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 (205) hide show
  1. package/README.md +1 -0
  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 +154 -5
  14. package/crypto.js +3 -8
  15. package/games/arcade.js +406 -0
  16. package/games/chess.js +451 -0
  17. package/games/colorguess.js +343 -0
  18. package/games/crossword-words.js +171 -0
  19. package/games/crossword.js +461 -0
  20. package/games/drawing.js +347 -0
  21. package/games/gameroulette.js +300 -0
  22. package/games/gamerouter.js +336 -0
  23. package/games/gamestatus.js +337 -0
  24. package/games/guessnumber.js +209 -0
  25. package/games/hangman.js +279 -0
  26. package/games/memory.js +338 -0
  27. package/games/multiplayer-tictactoe.js +389 -0
  28. package/games/pixelart.js +399 -0
  29. package/games/quickduel.js +354 -0
  30. package/games/riddle.js +371 -0
  31. package/games/rockpaperscissors.js +291 -0
  32. package/games/snake.js +406 -0
  33. package/games/storybuilder.js +343 -0
  34. package/games/tictactoe.js +345 -0
  35. package/games/twentyquestions.js +286 -0
  36. package/games/twotruths.js +207 -0
  37. package/games/werewolf.js +508 -0
  38. package/games/wordassociation.js +247 -0
  39. package/games/wordchain.js +135 -0
  40. package/index.js +105 -216
  41. package/intelligence/index.js +45 -0
  42. package/intelligence/infer.js +316 -0
  43. package/intelligence/interests.js +369 -0
  44. package/intelligence/patterns.js +651 -0
  45. package/intelligence/proactive.js +358 -0
  46. package/intelligence/serendipity.js +306 -0
  47. package/memory.js +166 -0
  48. package/notification-emitter.js +77 -0
  49. package/notify.js +102 -1
  50. package/package.json +21 -7
  51. package/prompts.js +1 -1
  52. package/protocol/index.js +161 -1
  53. package/setup.js +402 -0
  54. package/store/api.js +528 -82
  55. package/store/profiles.js +160 -12
  56. package/tools/_actions.js +463 -16
  57. package/tools/_deprecated/auto-suggest-connections.js +304 -0
  58. package/tools/_deprecated/bootstrap-skills.js +231 -0
  59. package/tools/_deprecated/bridge-dashboard.js +342 -0
  60. package/tools/_deprecated/bridge-health.js +400 -0
  61. package/tools/_deprecated/bridge-live.js +384 -0
  62. package/tools/_deprecated/bridges.js +383 -0
  63. package/tools/_deprecated/colorguess.js +281 -0
  64. package/tools/_deprecated/discover-insights.js +379 -0
  65. package/tools/_deprecated/discover-momentum.js +256 -0
  66. package/tools/_deprecated/discovery-analytics.js +345 -0
  67. package/tools/_deprecated/discovery-auto-suggest.js +275 -0
  68. package/tools/_deprecated/discovery-bootstrap.js +267 -0
  69. package/tools/_deprecated/discovery-daily.js +375 -0
  70. package/tools/_deprecated/discovery-dashboard.js +385 -0
  71. package/tools/_deprecated/discovery-digest.js +314 -0
  72. package/tools/_deprecated/discovery-hub.js +357 -0
  73. package/tools/_deprecated/discovery-insights.js +384 -0
  74. package/tools/_deprecated/discovery-momentum.js +281 -0
  75. package/tools/_deprecated/discovery-monitor.js +319 -0
  76. package/tools/_deprecated/discovery-proactive.js +300 -0
  77. package/tools/_deprecated/draw.js +317 -0
  78. package/tools/_deprecated/farcaster.js +307 -0
  79. package/tools/_deprecated/forget.js +119 -0
  80. package/tools/_deprecated/games-catalog.js +376 -0
  81. package/tools/_deprecated/games.js +313 -0
  82. package/tools/_deprecated/guessnumber.js +194 -0
  83. package/tools/_deprecated/hangman.js +129 -0
  84. package/tools/_deprecated/multiplayer-tictactoe.js +303 -0
  85. package/tools/_deprecated/recall.js +147 -0
  86. package/tools/_deprecated/remember.js +86 -0
  87. package/tools/_deprecated/riddle.js +240 -0
  88. package/tools/_deprecated/run-bootstrap.js +69 -0
  89. package/tools/_deprecated/skills-analytics.js +349 -0
  90. package/tools/_deprecated/skills-bootstrap.js +301 -0
  91. package/tools/_deprecated/skills-dashboard.js +268 -0
  92. package/tools/_deprecated/skills.js +380 -0
  93. package/tools/_deprecated/smart-intro.js +353 -0
  94. package/tools/_deprecated/storybuilder.js +331 -0
  95. package/tools/_deprecated/telegram-bot.js +183 -0
  96. package/tools/_deprecated/telegram-setup.js +214 -0
  97. package/tools/_deprecated/twentyquestions.js +143 -0
  98. package/tools/_discovery-enhanced.js +290 -0
  99. package/tools/_discovery.js +439 -0
  100. package/tools/_proactive-discovery.js +301 -0
  101. package/tools/_shared/index.js +64 -0
  102. package/tools/_shared.js +234 -0
  103. package/tools/_work-context.js +338 -0
  104. package/tools/_work-context.manual-test.js +199 -0
  105. package/tools/_work-context.test.js +260 -0
  106. package/tools/activity.js +220 -0
  107. package/tools/admin-inbox.js +218 -0
  108. package/tools/agent-treasury.js +288 -0
  109. package/tools/analytics.js +191 -0
  110. package/tools/approve.js +197 -0
  111. package/tools/arcade.js +173 -0
  112. package/tools/artifact-create.js +17 -11
  113. package/tools/artifact-view.js +31 -1
  114. package/tools/artifacts-price.js +47 -43
  115. package/tools/ask-expert.js +160 -0
  116. package/tools/available.js +120 -0
  117. package/tools/become-expert.js +150 -0
  118. package/tools/broadcast.js +286 -0
  119. package/tools/chat.js +202 -0
  120. package/tools/collaborative-drawing.js +286 -0
  121. package/tools/connection-status.js +178 -0
  122. package/tools/crossword.js +17 -1
  123. package/tools/discover.js +350 -34
  124. package/tools/dm.js +115 -73
  125. package/tools/drawing.js +1 -1
  126. package/tools/earnings.js +126 -0
  127. package/tools/echo.js +16 -0
  128. package/tools/feed.js +51 -7
  129. package/tools/follow.js +224 -0
  130. package/tools/friends.js +207 -0
  131. package/tools/game.js +2 -2
  132. package/tools/genesis.js +233 -0
  133. package/tools/gig-browse.js +206 -0
  134. package/tools/gig-complete.js +144 -0
  135. package/tools/handoff.js +7 -1
  136. package/tools/help.js +4 -4
  137. package/tools/idea.js +9 -2
  138. package/tools/inbox.js +334 -28
  139. package/tools/init.js +727 -36
  140. package/tools/invite.js +15 -4
  141. package/tools/l2-bridge.js +272 -0
  142. package/tools/l2-status.js +217 -0
  143. package/tools/l2.js +206 -0
  144. package/tools/migrate.js +156 -0
  145. package/tools/mint.js +377 -0
  146. package/tools/multiplayer-game.js +1 -1
  147. package/tools/notifications.js +415 -0
  148. package/tools/observe.js +200 -0
  149. package/tools/onboarding.js +147 -0
  150. package/tools/open.js +143 -12
  151. package/tools/party-game.js +2 -2
  152. package/tools/plan.js +225 -0
  153. package/tools/presence-agent.js +7 -0
  154. package/tools/proof-of-work.js +100 -104
  155. package/tools/pulse.js +218 -0
  156. package/tools/reply.js +166 -0
  157. package/tools/report.js +2 -2
  158. package/tools/reputation.js +175 -0
  159. package/tools/request.js +17 -3
  160. package/tools/schedule.js +367 -0
  161. package/tools/search-messages.js +123 -0
  162. package/tools/session.js +420 -0
  163. package/tools/session_price.js +128 -0
  164. package/tools/settings.js +126 -3
  165. package/tools/ship.js +31 -8
  166. package/tools/shipback.js +326 -0
  167. package/tools/smart-check.js +201 -0
  168. package/tools/social-processor.js +445 -0
  169. package/tools/solo-game.js +1 -1
  170. package/tools/start.js +335 -93
  171. package/tools/status.js +53 -6
  172. package/tools/stuck.js +297 -0
  173. package/tools/subscribe.js +148 -0
  174. package/tools/subscriptions.js +134 -0
  175. package/tools/suggest-tags.js +6 -8
  176. package/tools/tag-suggestions.js +257 -0
  177. package/tools/tip.js +193 -0
  178. package/tools/token.js +103 -0
  179. package/tools/update.js +1 -1
  180. package/tools/wallet.js +239 -186
  181. package/tools/watch.js +157 -0
  182. package/tools/webhook-test.js +388 -0
  183. package/tools/who.js +54 -3
  184. package/tools/withdraw.js +145 -0
  185. package/tools/work-summary.js +96 -0
  186. package/tools/workshop.js +327 -0
  187. package/tools/x-mentions.js +1 -1
  188. package/version.json +14 -3
  189. package/tools/artifacts-buy.js +0 -111
  190. package/tools/connect.js +0 -284
  191. package/tools/gigs-apply.js +0 -99
  192. package/tools/gigs-browse.js +0 -114
  193. package/tools/gigs-complete.js +0 -128
  194. package/tools/gigs-post.js +0 -140
  195. package/tools/live-off.js +0 -109
  196. package/tools/live-watch.js +0 -176
  197. package/tools/live.js +0 -128
  198. package/tools/sessions-browse.js +0 -105
  199. package/tools/whats-happening.js +0 -125
  200. /package/tools/{away.js → _deprecated/away.js} +0 -0
  201. /package/tools/{back.js → _deprecated/back.js} +0 -0
  202. /package/tools/{mute.js → _deprecated/mute.js} +0 -0
  203. /package/tools/{skills-exchange.js → _deprecated/skills-exchange.js} +0 -0
  204. /package/tools/{tictactoe.js → _deprecated/tictactoe.js} +0 -0
  205. /package/tools/{wordassociation.js → _deprecated/wordassociation.js} +0 -0
package/tools/start.js CHANGED
@@ -8,24 +8,164 @@
8
8
  * 4. Suggest someone to connect with
9
9
  */
10
10
 
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { execSync } = require('child_process');
11
14
  const config = require('../config');
12
15
  const store = require('../store');
13
16
  const memory = require('../memory');
14
17
  const notify = require('../notify');
15
18
  const patterns = require('../intelligence/patterns');
16
19
  const { actions, formatActions } = require('./_actions');
20
+ const init = require('./init');
21
+ const { gatherWithTimeout } = require('./_work-context');
17
22
 
18
- function formatTimeAgo(timestamp) {
19
- if (timestamp === undefined || timestamp === null || isNaN(timestamp)) return 'unknown';
23
+ const REPO_DIR = path.join(process.env.HOME, '.vibe', 'vibe-repo');
20
24
 
21
- const now = Date.now();
22
- const seconds = Math.floor((now - timestamp) / 1000);
25
+ /**
26
+ * Auto-update on session start
27
+ * Checks for updates and applies them automatically via git pull
28
+ * Returns update info if an update was applied, null otherwise
29
+ */
30
+ async function autoUpdate() {
31
+ try {
32
+ // Check if we're in a git repo
33
+ const gitDir = path.join(REPO_DIR, '.git');
34
+ if (!fs.existsSync(gitDir)) {
35
+ return null; // Not a git install, skip
36
+ }
37
+
38
+ // Check last update time (don't check more than once per hour)
39
+ const lastCheck = config.get('lastAutoUpdateCheck');
40
+ if (lastCheck && Date.now() - lastCheck < 60 * 60 * 1000) {
41
+ return null; // Checked recently
42
+ }
43
+
44
+ // Fetch latest from remote (without merging)
45
+ execSync('git fetch origin main', { cwd: REPO_DIR, stdio: 'ignore', timeout: 10000 });
46
+
47
+ // Check if we're behind
48
+ const localHead = execSync('git rev-parse HEAD', { cwd: REPO_DIR, encoding: 'utf8' }).trim();
49
+ const remoteHead = execSync('git rev-parse origin/main', { cwd: REPO_DIR, encoding: 'utf8' }).trim();
50
+
51
+ if (localHead === remoteHead) {
52
+ config.set('lastAutoUpdateCheck', Date.now());
53
+ return null; // Already up to date
54
+ }
55
+
56
+ // Get current version before update
57
+ const versionPath = path.join(REPO_DIR, 'mcp-server', 'version.json');
58
+ let oldVersion = 'unknown';
59
+ try {
60
+ oldVersion = JSON.parse(fs.readFileSync(versionPath, 'utf8')).version;
61
+ } catch (e) {}
62
+
63
+ // Pull the update
64
+ execSync('git pull origin main', { cwd: REPO_DIR, stdio: 'ignore', timeout: 30000 });
65
+
66
+ // Get new version
67
+ let newVersion = 'unknown';
68
+ try {
69
+ newVersion = JSON.parse(fs.readFileSync(versionPath, 'utf8')).version;
70
+ } catch (e) {}
71
+
72
+ config.set('lastAutoUpdateCheck', Date.now());
73
+
74
+ return {
75
+ updated: true,
76
+ from: oldVersion,
77
+ to: newVersion
78
+ };
79
+ } catch (e) {
80
+ // Silent fail - don't block startup
81
+ return null;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Check for version updates (cached for the session)
87
+ * Non-blocking - returns null on any error
88
+ */
89
+ async function getVersionInfo() {
90
+ // Check cache first (avoid repeated API calls)
91
+ const cached = config.get('versionInfo');
92
+ if (cached && cached.checkedAt > Date.now() - 5 * 60 * 1000) {
93
+ return cached;
94
+ }
95
+
96
+ try {
97
+ // Read local version
98
+ const versionPath = path.join(__dirname, '..', 'version.json');
99
+ const localVersion = JSON.parse(fs.readFileSync(versionPath, 'utf8'));
100
+ const current = localVersion.version;
101
+
102
+ // Fetch remote version (with timeout)
103
+ const controller = new AbortController();
104
+ const timeout = setTimeout(() => controller.abort(), 3000);
105
+
106
+ const response = await fetch(`${config.getApiUrl()}/api/version`, {
107
+ headers: { 'User-Agent': 'vibe-mcp-client' },
108
+ signal: controller.signal
109
+ });
110
+ clearTimeout(timeout);
111
+
112
+ if (!response.ok) {
113
+ // Cache failure to avoid retrying
114
+ const result = { current, latest: null, hasUpdate: false, checkedAt: Date.now() };
115
+ config.set('versionInfo', result);
116
+ return result;
117
+ }
118
+
119
+ const remoteVersion = await response.json();
120
+ const latest = remoteVersion.version;
121
+ const hasUpdate = compareVersions(latest, current) > 0;
122
+
123
+ const result = { current, latest, hasUpdate, checkedAt: Date.now() };
124
+ config.set('versionInfo', result);
125
+ return result;
126
+ } catch (e) {
127
+ // Silent fail - return local version only
128
+ try {
129
+ const versionPath = path.join(__dirname, '..', 'version.json');
130
+ const localVersion = JSON.parse(fs.readFileSync(versionPath, 'utf8'));
131
+ return { current: localVersion.version, latest: null, hasUpdate: false, checkedAt: Date.now() };
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+ }
23
137
 
24
- if (seconds < 0 || isNaN(seconds)) return 'unknown';
25
- if (seconds < 60) return 'just now';
26
- if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
27
- if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
28
- return `${Math.floor(seconds / 86400)}d ago`;
138
+ /**
139
+ * Compare semver versions: returns 1 if v1 > v2, -1 if v1 < v2, 0 if equal
140
+ */
141
+ function compareVersions(v1, v2) {
142
+ const parts1 = v1.split('.').map(Number);
143
+ const parts2 = v2.split('.').map(Number);
144
+ for (let i = 0; i < 3; i++) {
145
+ if (parts1[i] > parts2[i]) return 1;
146
+ if (parts1[i] < parts2[i]) return -1;
147
+ }
148
+ return 0;
149
+ }
150
+
151
+ /**
152
+ * Generate ASCII welcome card - matches init.js format
153
+ * Format: logo | handle + unread | tagline + online
154
+ */
155
+ function generateWelcomeCard({ handle, onlineCount, unreadCount, versionInfo }) {
156
+ // Match init.js generateAuthBanner format for consistency
157
+ const handleCol = `@${handle}`.padEnd(16);
158
+ const unreadCol = unreadCount > 0 ? `📬 ${unreadCount} unread`.padEnd(14) : `📬 0 messages`.padEnd(14);
159
+
160
+ // Add version badge if available
161
+ let versionSuffix = '';
162
+ if (versionInfo?.hasUpdate) {
163
+ versionSuffix = ' ⬆️';
164
+ }
165
+
166
+ return ` █░█ █ █▄▄ █▀▀ ${handleCol} 🚀 ship together
167
+ ▀▄▀ █ █▄█ ██▄ ${unreadCol} 🟢 ${onlineCount} online${versionSuffix}
168
+ ──────────────────────────────────────────────────`;
29
169
  }
30
170
 
31
171
  const definition = {
@@ -47,90 +187,78 @@ const definition = {
47
187
  };
48
188
 
49
189
  async function handler(args) {
50
- let display = '';
51
- let myHandle = config.getHandle();
52
-
53
- // Step 1: Initialize if needed
54
- if (!config.isInitialized()) {
55
- if (!args.handle) {
56
- return {
57
- display: `## Let's vibe
58
-
59
- First, tell me who you are:
60
- - **Handle**: Use your X handle (e.g., @davemorin)
61
- - **Building**: What are you working on?
62
-
63
- Example: "I'm @davemorin, working on social apps"`
64
- };
65
- }
66
-
67
- // Initialize with provided handle
68
- const h = args.handle.toLowerCase().replace('@', '').replace(/[^a-z0-9_-]/g, '');
69
- const oneLiner = args.building || 'building something';
70
-
71
- config.setSessionIdentity(h, oneLiner);
72
- const cfg = config.load();
73
- cfg.handle = h;
74
- cfg.one_liner = oneLiner;
75
- cfg.visible = true;
76
- config.save(cfg);
190
+ // Step 0: Auto-update check (runs git pull if behind)
191
+ const updateResult = await autoUpdate();
192
+
193
+ // Step 1: Check if properly authenticated with OAuth
194
+ // If not, redirect to init for GitHub auth flow (shows pre-auth banner + OAuth)
195
+ if (!config.hasOAuth()) {
196
+ return init.handler({
197
+ handle: args.handle,
198
+ one_liner: args.building
199
+ });
200
+ }
77
201
 
78
- const sessionId = config.getSessionId();
79
- await store.registerSession(sessionId, h);
80
- await store.heartbeat(h, oneLiner);
202
+ // Step 2: User is authenticated - show dashboard
203
+ const myHandle = config.getHandle();
204
+ let threads = [];
205
+ let updateNotice = '';
81
206
 
82
- myHandle = h;
83
- display += `**You're @${h}** — ${oneLiner}\n\n`;
207
+ // If we just updated, show a notice
208
+ if (updateResult?.updated) {
209
+ updateNotice = `\n\n⬆️ **Updated v${updateResult.from} → v${updateResult.to}** — restart Claude Code to apply`;
210
+ }
84
211
 
85
- // Log session start for patterns
86
- patterns.logSessionStart(h);
87
- } else {
88
- // ═══ RETURNING USER — Session Rehydration ═══
89
- const oneLiner = config.getOneLiner() || 'building something';
90
- display += `## Welcome back, @${myHandle}\n\n`;
91
- display += `_${oneLiner}_\n\n`;
212
+ // Fetch version info early (non-blocking, cached)
213
+ const versionInfo = await getVersionInfo().catch(() => null);
92
214
 
93
- // Log session start for patterns
94
- patterns.logSessionStart(myHandle);
215
+ // ═══════════════════════════════════════════════════════════════════════
216
+ // AMBIENT CONTEXT: Gather work context and auto-set presence
217
+ // ═══════════════════════════════════════════════════════════════════════
218
+ let workContext = null;
219
+ const autoContextEnabled = config.get('autoContext', true); // Opt-out via settings
95
220
 
96
- // Show who remembers you (threads with memories)
221
+ if (autoContextEnabled) {
97
222
  try {
98
- const threads = memory.listThreads();
99
- if (threads.length > 0) {
100
- const recentThreads = threads.slice(0, 3);
101
- const names = recentThreads.map(t => `@${t.handle}`).join(', ');
102
- display += `**${threads.length}** people in your memory · ${names}\n\n`;
223
+ // Gather context with timeout (won't block startup)
224
+ workContext = await gatherWithTimeout(2000);
225
+
226
+ // Auto-set presence so others see what we're working on (silent, non-blocking)
227
+ if (workContext?.suggestions?.brief) {
228
+ // Use store.heartbeat directly instead of importing context tool
229
+ // This avoids circular dependencies and is simpler
230
+ store.heartbeat(myHandle, config.getOneLiner(), {
231
+ note: workContext.suggestions.brief,
232
+ branch: workContext.git?.branch || null
233
+ }).catch(() => {}); // Silent fail - don't block
103
234
  }
104
- } catch (e) {}
235
+ } catch (e) {
236
+ // Silent fail - context is nice-to-have, not required
237
+ workContext = null;
238
+ }
105
239
  }
106
240
 
241
+ // Log session start for patterns
242
+ patterns.logSessionStart(myHandle);
243
+
244
+ // Get threads for memory context
245
+ try {
246
+ threads = memory.listThreads();
247
+ } catch (e) {}
248
+
107
249
  // Step 2: Get who's around
108
250
  const users = await store.getActiveUsers();
109
251
  const others = users.filter(u => u.handle !== myHandle);
110
252
 
111
- display += `## Who's Around\n\n`;
112
-
113
- if (others.length === 0) {
114
- display += `_No one else here yet._ Invite someone: slashvibe.dev\n\n`;
115
- } else {
116
- others.slice(0, 5).forEach(u => {
117
- const mood = u.mood ? ` ${u.mood}` : '';
118
- const xLink = `[x.com/${u.handle}](https://x.com/${u.handle})`;
119
- const context = u.note || u.one_liner || 'building';
120
- display += `● **@${u.handle}**${mood} — ${context}\n ${xLink} — _${formatTimeAgo(u.lastSeen)}_\n\n`;
121
- });
122
-
123
- if (others.length > 5) {
124
- display += `_...and ${others.length - 5} more_\n\n`;
125
- }
126
- }
127
-
128
- // Step 3: Check inbox + trigger notifications for old unread
253
+ // Step 3: Check inbox + trigger notifications
254
+ let unreadCount = 0;
255
+ let inboxThreads = [];
129
256
  try {
130
- const unreadCount = await store.getUnreadCount(myHandle);
131
- if (unreadCount > 0) {
132
- display += `---\n📬 **NEW MESSAGE ${unreadCount} UNREAD** say "check my messages"\n\n`;
257
+ // Fetch full inbox (not just count) so we can include summaries
258
+ inboxThreads = await store.getInbox(myHandle);
259
+ unreadCount = inboxThreads.reduce((sum, t) => sum + (t.unread || 0), 0);
133
260
 
261
+ if (unreadCount > 0) {
134
262
  // Check for messages needing desktop notification escalation
135
263
  const rawInbox = await store.getRawInbox(myHandle).catch(() => []);
136
264
  if (rawInbox.length > 0) {
@@ -139,31 +267,130 @@ Example: "I'm @davemorin, working on social apps"`
139
267
  }
140
268
  } catch (e) {}
141
269
 
142
- // Step 4: Suggest connection (if there are others)
270
+ // Step 4: Get connection suggestions from API
271
+ let suggestions = [];
272
+ try {
273
+ const apiUrl = config.getApiUrl();
274
+ const suggestionsResponse = await fetch(`${apiUrl}/api/suggestions?user=${myHandle}&limit=3`, {
275
+ headers: { 'User-Agent': 'vibe-mcp-client' }
276
+ });
277
+
278
+ if (suggestionsResponse.ok) {
279
+ const data = await suggestionsResponse.json();
280
+ if (data.success && data.suggestions) {
281
+ suggestions = data.suggestions;
282
+ }
283
+ }
284
+ } catch (e) {
285
+ // Silent fail - suggestions are nice-to-have
286
+ }
287
+
288
+ // Generate the ASCII welcome card (matches init.js format)
289
+ const welcomeCard = generateWelcomeCard({
290
+ handle: myHandle,
291
+ onlineCount: others.length,
292
+ unreadCount,
293
+ versionInfo
294
+ });
295
+
296
+ // Build display with card + any additional info
297
+ let display = welcomeCard;
298
+
299
+ // Add who's online section (top 5 with what they're building)
143
300
  if (others.length > 0) {
144
- const suggestion = others[Math.floor(Math.random() * Math.min(others.length, 3))];
145
- display += `---\n💬 _Say "message ${suggestion.handle}" to start a conversation_`;
301
+ const top5 = others.slice(0, 5);
302
+ display += `\n\n**🟢 Online now:**`;
303
+ top5.forEach(u => {
304
+ const status = u.status ? ` (${u.status})` : '';
305
+ const building = u.one_liner || u.note || '';
306
+ const truncated = building.length > 40 ? building.slice(0, 40) + '...' : building;
307
+ display += `\n• @${u.handle}${status}${truncated ? ' — ' + truncated : ''}`;
308
+ });
309
+ if (others.length > 5) {
310
+ display += `\n• _+${others.length - 5} more..._`;
311
+ }
146
312
  }
147
313
 
148
- // Step 5: Introduce @echo to new users
149
- if (args.handle) {
150
- // This was a new user init
151
- display += `\n\n📣 _Meet **@echo** — say "message @echo" to share feedback or ideas!_`;
314
+ // Add unread messages section (if any)
315
+ if (unreadCount > 0) {
316
+ const unreadSenders = inboxThreads.filter(t => t.unread > 0);
317
+ display += `\n\n**📬 Unread (${unreadCount}):**`;
318
+ unreadSenders.slice(0, 3).forEach(t => {
319
+ const preview = t.lastMessage ? t.lastMessage.slice(0, 50) : '';
320
+ const truncated = preview.length > 50 ? preview + '...' : preview;
321
+ display += `\n• @${t.handle} (${t.unread}) — "${truncated}"`;
322
+ });
323
+ if (unreadSenders.length > 3) {
324
+ display += `\n• _+${unreadSenders.length - 3} more threads..._`;
325
+ }
326
+ }
327
+
328
+ // Add connection suggestions (if any)
329
+ if (suggestions.length > 0) {
330
+ display += `\n\n**🤝 Suggested connections:**`;
331
+ suggestions.slice(0, 3).forEach(s => {
332
+ display += `\n• @${s.handle} — ${s.reason}`;
333
+ if (s.github_name) {
334
+ display += ` (${s.github_name})`;
335
+ }
336
+ });
152
337
  }
153
338
 
154
- // Step 6: Suggest background presence monitor (if not running)
155
- const presenceAgentEnabled = config.get('presenceAgentEnabled', true);
156
- const presenceAgentRunning = config.get('presenceAgentRunning');
339
+ // Add memory context for returning users
340
+ if (threads.length > 0) {
341
+ const recentThreads = threads.slice(0, 3);
342
+ const names = recentThreads.map(t => `@${t.handle}`).join(', ');
343
+ display += `\n\n💭 **${threads.length}** people in memory · ${names}`;
344
+ }
157
345
 
158
- if (presenceAgentEnabled && !presenceAgentRunning && others.length > 0) {
159
- display += `\n\n---\n💡 **Tip:** Say "start presence monitor" for real-time alerts when interesting people come online.`;
346
+ // Add update notice if we just auto-updated
347
+ if (updateNotice) {
348
+ display += updateNotice;
160
349
  }
161
350
 
351
+ // Step 6: Show rotating tips about features
352
+ const tips = [
353
+ '💡 **Tip:** Say "vibe stuck" when you need help — others can jump in and assist.',
354
+ '💡 **Tip:** Use "vibe available \'React, auth\'" to signal you\'re open to chat about topics.',
355
+ '💡 **Tip:** Use "vibe context --file auth.js" to share what you\'re working on.',
356
+ '💡 **Tip:** Say "start presence monitor" for real-time alerts when people come online.'
357
+ ];
358
+ const tipIndex = Math.floor(Date.now() / 60000) % tips.length; // Rotate every minute
359
+ display += `\n\n---\n${tips[tipIndex]}`;
360
+
162
361
  // Build response with hints for structured dashboard flow
163
362
  const response = { display };
164
363
 
364
+ // === ENRICHED DATA ===
365
+ // Include full online users list so Claude doesn't need to call vibe_who
366
+ response.onlineUsers = others.map(u => ({
367
+ handle: u.handle,
368
+ building: u.one_liner || u.note || null,
369
+ status: u.status || null,
370
+ lastActive: u.lastSeen ? new Date(u.lastSeen).toISOString() : null
371
+ }));
372
+
373
+ // Include unread thread summaries so Claude doesn't need to call vibe_inbox
374
+ const unreadSenders = inboxThreads.filter(t => t.unread > 0);
375
+ response.unreadThreads = unreadSenders.map(t => ({
376
+ handle: t.handle,
377
+ unread: t.unread,
378
+ preview: t.lastMessage ? t.lastMessage.slice(0, 80) : null,
379
+ isAgent: t.isAgent || false
380
+ }));
381
+
382
+ // Include connection suggestions for smart discovery
383
+ if (suggestions.length > 0) {
384
+ response.suggestions = suggestions.map(s => ({
385
+ handle: s.handle,
386
+ reason: s.reason,
387
+ githubName: s.github_name || null,
388
+ lastActive: s.last_active || null,
389
+ matchScore: s.score || null
390
+ }));
391
+ }
392
+
165
393
  // Determine session state and suggest appropriate flow
166
- const unreadCount = await store.getUnreadCount(myHandle).catch(() => 0);
167
394
  let suggestion = null;
168
395
 
169
396
  if (unreadCount >= 5) {
@@ -197,18 +424,33 @@ Example: "I'm @davemorin, working on social apps"`
197
424
 
198
425
  if (others.length === 0 && unreadCount === 0) {
199
426
  // Empty room
200
- actionList = actions.emptyRoom();
427
+ actionList = actions.emptyRoom({ workContext });
201
428
  } else {
202
429
  // Normal dashboard
203
430
  actionList = actions.dashboard({
204
431
  unreadCount,
205
432
  onlineUsers: onlineHandles,
206
- suggestion
433
+ suggestion,
434
+ workContext
207
435
  });
208
436
  }
209
437
 
210
438
  response.actions = formatActions(actionList);
211
439
 
440
+ // ═══════════════════════════════════════════════════════════════════════
441
+ // WORK CONTEXT: Include in response for Claude to use
442
+ // ═══════════════════════════════════════════════════════════════════════
443
+ if (workContext?.suggestions?.brief) {
444
+ response.workContext = {
445
+ summary: workContext.suggestions.brief,
446
+ detailed: workContext.suggestions.detailed,
447
+ project: workContext.project?.name,
448
+ branch: workContext.git?.branch,
449
+ recentCommit: workContext.git?.recentCommits?.[0]?.message || null,
450
+ hasUncommitted: workContext.git?.hasUncommitted || false
451
+ };
452
+ }
453
+
212
454
  return response;
213
455
  }
214
456
 
package/tools/status.js CHANGED
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * vibe status — Set your mood/status
3
+ *
4
+ * Now includes away/back functionality (previously vibe_away/vibe_back)
3
5
  */
4
6
 
5
7
  const config = require('../config');
6
8
  const store = require('../store');
7
9
  const discord = require('../discord');
8
10
  const { trackMood } = require('./summarize');
11
+ const { markAway, markBack, getProactiveSummary } = require('../intelligence/proactive');
9
12
 
10
13
  const MOODS = {
11
14
  'shipping': '🔥',
@@ -16,6 +19,8 @@ const MOODS = {
16
19
  'deep': '🎧',
17
20
  'celebrating': '🎉',
18
21
  'struggling': '😤',
22
+ 'away': '☕', // Merged from vibe_away
23
+ 'back': null, // Merged from vibe_back (clears status)
19
24
  'clear': null
20
25
  };
21
26
 
@@ -24,13 +29,17 @@ const SPECIAL_MODES = ['guided', 'freeform'];
24
29
 
25
30
  const definition = {
26
31
  name: 'vibe_status',
27
- description: 'Set your mood/status. Options: shipping, thinking, afk, debugging, pairing, deep, celebrating, struggling, clear',
32
+ description: 'Set your mood/status. Options: shipping, thinking, afk, debugging, pairing, deep, celebrating, struggling, away, back, clear',
28
33
  inputSchema: {
29
34
  type: 'object',
30
35
  properties: {
31
36
  mood: {
32
37
  type: 'string',
33
- description: 'Your mood (shipping, thinking, afk, debugging, pairing, deep, celebrating, struggling, clear)'
38
+ description: 'Your mood (shipping, thinking, afk, debugging, pairing, deep, celebrating, struggling, away, back, clear)'
39
+ },
40
+ message: {
41
+ type: 'string',
42
+ description: 'Optional away message (only used with away mood, e.g., "grabbing coffee")'
34
43
  }
35
44
  },
36
45
  required: ['mood']
@@ -44,8 +53,9 @@ async function handler(args) {
44
53
  };
45
54
  }
46
55
 
47
- const { mood } = args;
56
+ const { mood, message } = args;
48
57
  const moodKey = mood.toLowerCase().replace(/[^a-z]/g, '');
58
+ const handle = config.getHandle();
49
59
 
50
60
  // Handle special modes (guided/freeform)
51
61
  if (moodKey === 'guided') {
@@ -72,18 +82,55 @@ _Say "set status guided" to re-enable interactive menus._`
72
82
  };
73
83
  }
74
84
 
85
+ // Handle 'away' mood (merged from vibe_away)
86
+ if (moodKey === 'away') {
87
+ const awayMessage = message?.trim();
88
+
89
+ // Validate message length
90
+ if (awayMessage && awayMessage.length > 100) {
91
+ return { display: '⚠️ Away message too long (100 char max)' };
92
+ }
93
+
94
+ // Set away status with optional message
95
+ await store.setAwayStatus(handle, 'away', awayMessage || null);
96
+ markAway();
97
+
98
+ if (awayMessage) {
99
+ return { display: `☕ away — "${awayMessage}"` };
100
+ }
101
+ return { display: '☕ away' };
102
+ }
103
+
104
+ // Handle 'back' mood (merged from vibe_back)
105
+ if (moodKey === 'back') {
106
+ // Clear away status
107
+ await store.clearAwayStatus(handle);
108
+
109
+ let display = '👋 back';
110
+
111
+ // Check if anything happened while away
112
+ const unreadCount = await store.getUnreadCount(handle).catch(() => 0);
113
+ if (unreadCount > 0) {
114
+ display += ` — ${unreadCount} unread`;
115
+ }
116
+
117
+ // Mark as back for proactive tracking
118
+ markBack();
119
+
120
+ return { display };
121
+ }
122
+
75
123
  if (!MOODS.hasOwnProperty(moodKey)) {
76
124
  const options = Object.entries(MOODS)
77
- .filter(([k, v]) => v)
125
+ .filter(([k, v]) => v && k !== 'back') // Exclude 'back' from list (it's a clear action)
78
126
  .map(([k, v]) => `${v} ${k}`)
79
127
  .join(', ');
80
128
  return {
81
- display: `Unknown mood. Options: ${options}, or "clear" to remove`
129
+ display: `Unknown mood. Options: ${options}, or "clear"/"back" to remove`
82
130
  };
83
131
  }
84
132
 
85
133
  const emoji = MOODS[moodKey];
86
- const handle = config.getHandle();
87
134
 
88
135
  // Update presence with mood via context
89
136
  await store.heartbeat(handle, config.getOneLiner(), { mood: emoji });