slashvibe-mcp 0.3.20 → 0.3.21

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 (167) hide show
  1. package/README.md +47 -252
  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 +116 -159
  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 +21 -16
  45. package/prompts.js +1 -1
  46. package/protocol/index.js +73 -0
  47. package/setup.js +480 -0
  48. package/smart-inbox.js +276 -0
  49. package/store/api.js +536 -215
  50. package/store/profiles.js +160 -12
  51. package/tools/_actions.js +362 -21
  52. package/tools/_discovery.js +119 -26
  53. package/tools/_shared/index.js +64 -0
  54. package/tools/_shared.js +234 -0
  55. package/tools/_work-context.js +338 -0
  56. package/tools/_work-context.manual-test.js +199 -0
  57. package/tools/_work-context.test.js +260 -0
  58. package/tools/activity.js +220 -0
  59. package/tools/analytics.js +191 -0
  60. package/tools/approve.js +197 -0
  61. package/tools/artifact-create.js +14 -3
  62. package/tools/artifacts-price.js +107 -0
  63. package/tools/available.js +120 -0
  64. package/tools/broadcast.js +325 -0
  65. package/tools/chat.js +202 -0
  66. package/tools/collaborative-drawing.js +1 -1
  67. package/tools/connection-status.js +178 -0
  68. package/tools/discover.js +350 -34
  69. package/tools/dm.js +80 -8
  70. package/tools/earnings.js +126 -0
  71. package/tools/feed.js +35 -4
  72. package/tools/follow.js +224 -0
  73. package/tools/friends.js +207 -0
  74. package/tools/gig-browse.js +206 -0
  75. package/tools/gig-complete.js +144 -0
  76. package/tools/health.js +87 -0
  77. package/tools/help.js +3 -3
  78. package/tools/idea.js +9 -2
  79. package/tools/inbox.js +289 -105
  80. package/tools/init.js +131 -34
  81. package/tools/invite.js +15 -4
  82. package/tools/leaderboard.js +117 -0
  83. package/tools/lib/git-apply.js +206 -0
  84. package/tools/lib/git-bundle.js +407 -0
  85. package/tools/migrate.js +3 -3
  86. package/tools/multiplayer-game.js +1 -1
  87. package/tools/onboarding.js +7 -7
  88. package/tools/open.js +143 -12
  89. package/tools/party-game.js +1 -1
  90. package/tools/plan.js +225 -0
  91. package/tools/proof-of-work.js +144 -0
  92. package/tools/reply.js +166 -0
  93. package/tools/report.js +1 -1
  94. package/tools/request.js +17 -3
  95. package/tools/schedule.js +367 -0
  96. package/tools/search-messages.js +123 -0
  97. package/tools/session.js +467 -0
  98. package/tools/session_price.js +128 -0
  99. package/tools/settings.js +90 -2
  100. package/tools/ship.js +30 -7
  101. package/tools/smart-check.js +201 -0
  102. package/tools/start.js +147 -12
  103. package/tools/status.js +53 -6
  104. package/tools/streak.js +147 -0
  105. package/tools/stuck.js +297 -0
  106. package/tools/subscribe.js +148 -0
  107. package/tools/subscriptions.js +134 -0
  108. package/tools/suggest-tags.js +6 -8
  109. package/tools/tag-suggestions.js +1 -1
  110. package/tools/tip.js +150 -77
  111. package/tools/token.js +4 -4
  112. package/tools/update.js +1 -1
  113. package/tools/wallet.js +221 -79
  114. package/tools/watch.js +157 -0
  115. package/tools/who.js +30 -1
  116. package/tools/withdraw.js +145 -0
  117. package/tools/work-summary.js +96 -0
  118. package/version.json +10 -8
  119. package/LICENSE +0 -21
  120. package/store/sqlite.js +0 -347
  121. /package/tools/{auto-suggest-connections.js → _deprecated/auto-suggest-connections.js} +0 -0
  122. /package/tools/{away.js → _deprecated/away.js} +0 -0
  123. /package/tools/{back.js → _deprecated/back.js} +0 -0
  124. /package/tools/{bootstrap-skills.js → _deprecated/bootstrap-skills.js} +0 -0
  125. /package/tools/{bridge-dashboard.js → _deprecated/bridge-dashboard.js} +0 -0
  126. /package/tools/{bridge-health.js → _deprecated/bridge-health.js} +0 -0
  127. /package/tools/{bridge-live.js → _deprecated/bridge-live.js} +0 -0
  128. /package/tools/{bridges.js → _deprecated/bridges.js} +0 -0
  129. /package/tools/{colorguess.js → _deprecated/colorguess.js} +0 -0
  130. /package/tools/{discover-insights.js → _deprecated/discover-insights.js} +0 -0
  131. /package/tools/{discover-momentum.js → _deprecated/discover-momentum.js} +0 -0
  132. /package/tools/{discovery-analytics.js → _deprecated/discovery-analytics.js} +0 -0
  133. /package/tools/{discovery-auto-suggest.js → _deprecated/discovery-auto-suggest.js} +0 -0
  134. /package/tools/{discovery-bootstrap.js → _deprecated/discovery-bootstrap.js} +0 -0
  135. /package/tools/{discovery-daily.js → _deprecated/discovery-daily.js} +0 -0
  136. /package/tools/{discovery-dashboard.js → _deprecated/discovery-dashboard.js} +0 -0
  137. /package/tools/{discovery-digest.js → _deprecated/discovery-digest.js} +0 -0
  138. /package/tools/{discovery-hub.js → _deprecated/discovery-hub.js} +0 -0
  139. /package/tools/{discovery-insights.js → _deprecated/discovery-insights.js} +0 -0
  140. /package/tools/{discovery-momentum.js → _deprecated/discovery-momentum.js} +0 -0
  141. /package/tools/{discovery-monitor.js → _deprecated/discovery-monitor.js} +0 -0
  142. /package/tools/{discovery-proactive.js → _deprecated/discovery-proactive.js} +0 -0
  143. /package/tools/{draw.js → _deprecated/draw.js} +0 -0
  144. /package/tools/{farcaster.js → _deprecated/farcaster.js} +0 -0
  145. /package/tools/{forget.js → _deprecated/forget.js} +0 -0
  146. /package/tools/{games-catalog.js → _deprecated/games-catalog.js} +0 -0
  147. /package/tools/{games.js → _deprecated/games.js} +0 -0
  148. /package/tools/{guessnumber.js → _deprecated/guessnumber.js} +0 -0
  149. /package/tools/{hangman.js → _deprecated/hangman.js} +0 -0
  150. /package/tools/{multiplayer-tictactoe.js → _deprecated/multiplayer-tictactoe.js} +0 -0
  151. /package/tools/{mute.js → _deprecated/mute.js} +0 -0
  152. /package/tools/{recall.js → _deprecated/recall.js} +0 -0
  153. /package/tools/{remember.js → _deprecated/remember.js} +0 -0
  154. /package/tools/{riddle.js → _deprecated/riddle.js} +0 -0
  155. /package/tools/{run-bootstrap.js → _deprecated/run-bootstrap.js} +0 -0
  156. /package/tools/{skills-analytics.js → _deprecated/skills-analytics.js} +0 -0
  157. /package/tools/{skills-bootstrap.js → _deprecated/skills-bootstrap.js} +0 -0
  158. /package/tools/{skills-dashboard.js → _deprecated/skills-dashboard.js} +0 -0
  159. /package/tools/{skills-exchange.js → _deprecated/skills-exchange.js} +0 -0
  160. /package/tools/{skills.js → _deprecated/skills.js} +0 -0
  161. /package/tools/{smart-intro.js → _deprecated/smart-intro.js} +0 -0
  162. /package/tools/{storybuilder.js → _deprecated/storybuilder.js} +0 -0
  163. /package/tools/{telegram-bot.js → _deprecated/telegram-bot.js} +0 -0
  164. /package/tools/{telegram-setup.js → _deprecated/telegram-setup.js} +0 -0
  165. /package/tools/{tictactoe.js → _deprecated/tictactoe.js} +0 -0
  166. /package/tools/{twentyquestions.js → _deprecated/twentyquestions.js} +0 -0
  167. /package/tools/{wordassociation.js → _deprecated/wordassociation.js} +0 -0
package/tools/settings.js CHANGED
@@ -6,13 +6,15 @@
6
6
  * - guided: on | off (dashboard mode)
7
7
  * - github_activity: on | off (show GitHub shipping status)
8
8
  * - github_activity_privacy: full | status_only | off
9
+ * - mute: 1h | 2h | 4h | forever | off (merged from vibe_mute)
10
+ * - auto_context: on | off (ambient work context gathering)
9
11
  */
10
12
 
11
13
  const config = require('../config');
12
14
 
13
15
  const definition = {
14
16
  name: 'vibe_settings',
15
- description: 'Configure /vibe preferences. Set notification level, toggle guided mode, or enable GitHub activity signals.',
17
+ description: 'Configure /vibe preferences. Set notification level, toggle guided mode, enable GitHub activity signals, or mute alerts.',
16
18
  inputSchema: {
17
19
  type: 'object',
18
20
  properties: {
@@ -33,6 +35,14 @@ const definition = {
33
35
  type: 'string',
34
36
  enum: ['full', 'status_only', 'off'],
35
37
  description: 'Privacy level: full (repos/commits), status_only (just badge), off'
38
+ },
39
+ mute: {
40
+ type: 'string',
41
+ description: 'Mute presence alerts: "1h", "2h", "4h", "forever", or "off" to unmute'
42
+ },
43
+ auto_context: {
44
+ type: 'boolean',
45
+ description: 'Enable/disable automatic work context gathering (git branch, recent commits shown in presence)'
36
46
  }
37
47
  }
38
48
  }
@@ -65,12 +75,68 @@ async function handler(args) {
65
75
  changes.push('github activity privacy → **' + args.github_activity_privacy + '**');
66
76
  }
67
77
 
78
+ // Handle mute setting (merged from vibe_mute)
79
+ if (args.mute) {
80
+ const duration = args.mute;
81
+
82
+ // UNMUTE
83
+ if (duration === 'off' || duration === 'unmute') {
84
+ config.set('mutedUntil', null);
85
+ config.set('focusMode', false);
86
+ changes.push('alerts → **unmuted**');
87
+ }
88
+ // MUTE FOREVER
89
+ else if (duration === 'forever' || duration === 'disable') {
90
+ config.set('presenceAgentEnabled', false);
91
+ config.set('mutedUntil', null);
92
+ changes.push('alerts → **disabled**');
93
+ }
94
+ // PARSE DURATION
95
+ else {
96
+ let ms = 0;
97
+ const match = duration.match(/^(\d+)(h|m|min|hour|hours|minutes)$/);
98
+
99
+ if (match) {
100
+ const amount = parseInt(match[1]);
101
+ const unit = match[2];
102
+
103
+ if (unit === 'h' || unit === 'hour' || unit === 'hours') {
104
+ ms = amount * 60 * 60 * 1000;
105
+ } else if (unit === 'm' || unit === 'min' || unit === 'minutes') {
106
+ ms = amount * 60 * 1000;
107
+ }
108
+ } else {
109
+ // Default to 1 hour if can't parse
110
+ ms = 60 * 60 * 1000;
111
+ }
112
+
113
+ const mutedUntil = Date.now() + ms;
114
+ config.set('mutedUntil', mutedUntil);
115
+
116
+ const until = new Date(mutedUntil).toLocaleTimeString();
117
+ const durationText = ms >= 3600000
118
+ ? `${Math.floor(ms / 3600000)} hour${Math.floor(ms / 3600000) > 1 ? 's' : ''}`
119
+ : `${Math.floor(ms / 60000)} minutes`;
120
+
121
+ changes.push(`alerts → **muted for ${durationText}** (until ${until})`);
122
+ }
123
+ }
124
+
125
+ // Update auto context if provided
126
+ if (args.auto_context !== undefined) {
127
+ config.set('autoContext', args.auto_context);
128
+ changes.push('auto context → **' + (args.auto_context ? 'on' : 'off') + '**');
129
+ }
130
+
68
131
  // If no args, show current settings
69
132
  if (changes.length === 0) {
70
133
  const notifications = config.getNotifications();
71
134
  const guided = config.getGuidedMode();
72
135
  const githubActivity = config.getGithubActivityEnabled();
73
136
  const githubPrivacy = config.getGithubActivityPrivacy();
137
+ const mutedUntil = config.get('mutedUntil');
138
+ const presenceEnabled = config.get('presenceAgentEnabled') !== false;
139
+ const autoContext = config.get('autoContext', true); // Default to true
74
140
 
75
141
  const notifyDesc = {
76
142
  all: 'All notifications (messages, mentions, presence)',
@@ -84,21 +150,43 @@ async function handler(args) {
84
150
  off: 'Disabled'
85
151
  };
86
152
 
153
+ // Determine mute status
154
+ let muteStatus = 'off';
155
+ let muteDesc = 'Alerts enabled';
156
+ if (!presenceEnabled) {
157
+ muteStatus = 'disabled';
158
+ muteDesc = 'Presence alerts permanently disabled';
159
+ } else if (mutedUntil) {
160
+ const remaining = mutedUntil - Date.now();
161
+ if (remaining > 0) {
162
+ const mins = Math.floor(remaining / 60000);
163
+ const hours = Math.floor(mins / 60);
164
+ muteStatus = hours > 0 ? `${hours}h ${mins % 60}m` : `${mins}m`;
165
+ muteDesc = `Muted until ${new Date(mutedUntil).toLocaleTimeString()}`;
166
+ }
167
+ }
168
+
87
169
  return {
88
170
  display: '## /vibe Settings\n\n' +
89
171
  '**Notifications:** ' + notifications + '\n' +
90
172
  '_' + notifyDesc[notifications] + '_\n\n' +
91
173
  '**Guided Mode:** ' + (guided ? 'on' : 'off') + '\n' +
92
174
  '_' + (guided ? 'Shows dashboard menus' : 'Freeform mode') + '_\n\n' +
175
+ '**Alert Mute:** ' + muteStatus + '\n' +
176
+ '_' + muteDesc + '_\n\n' +
93
177
  '**GitHub Activity:** ' + (githubActivity ? 'on' : 'off') + '\n' +
94
178
  '_' + (githubActivity ? 'Shows shipping status from your GitHub commits' : 'Not sharing GitHub activity') + '_\n\n' +
95
179
  (githubActivity ? '**GitHub Privacy:** ' + githubPrivacy + '\n_' + privacyDesc[githubPrivacy] + '_\n\n' : '') +
180
+ '**Auto Context:** ' + (autoContext ? 'on' : 'off') + '\n' +
181
+ '_' + (autoContext ? 'Git branch & work summary shared in presence' : 'Work context not auto-shared') + '_\n\n' +
96
182
  '---\n\n' +
97
183
  '**Change settings:**\n' +
98
184
  '• "set notifications to mentions" — less noisy\n' +
99
185
  '• "turn off guided mode" — no menus\n' +
186
+ '• "mute for 2h" — silence alerts temporarily\n' +
100
187
  '• "enable github activity" — show when you\'re shipping\n' +
101
- '• "set github privacy to status_only" — just badge, no details'
188
+ '• "set github privacy to status_only" — just badge, no details\n' +
189
+ '• "disable auto context" — stop sharing git branch/work'
102
190
  };
103
191
  }
104
192
 
package/tools/ship.js CHANGED
@@ -90,10 +90,16 @@ async function handler(args) {
90
90
  tags.push(`fulfills:${args.for_request}`);
91
91
  }
92
92
 
93
- // Post to board
93
+ // Post to board (with auth token)
94
+ const authToken = config.getAuthToken();
95
+ const headers = { 'Content-Type': 'application/json' };
96
+ if (authToken) {
97
+ headers['Authorization'] = `Bearer ${authToken}`;
98
+ }
99
+
94
100
  const response = await fetch(`${apiUrl}/api/board`, {
95
101
  method: 'POST',
96
- headers: { 'Content-Type': 'application/json' },
102
+ headers,
97
103
  body: JSON.stringify({
98
104
  author: myHandle,
99
105
  content,
@@ -114,21 +120,38 @@ async function handler(args) {
114
120
  patterns.logInspiredBy(args.inspired_by);
115
121
  }
116
122
 
117
- let display = `šŸš€ shipped\n\n${args.what}`;
123
+ let display = `šŸš€ **Shipped!**\n\n${args.what}`;
118
124
 
119
125
  if (args.url) {
120
- display += `\n${args.url}`;
126
+ display += `\nšŸ”— ${args.url}`;
121
127
  }
122
128
  if (args.inspired_by) {
123
- display += `\n_via @${args.inspired_by.replace('@', '')}_`;
129
+ display += `\n✨ _via @${args.inspired_by.replace('@', '')}_`;
130
+ }
131
+
132
+ // Add share URL for viral distribution
133
+ if (data.shareUrl) {
134
+ display += `\n\n**šŸ“£ Share your ship:**\n${data.shareUrl}`;
135
+
136
+ // Use the twitterShareUrl from API response (tracked for K-factor)
137
+ if (data.twitterShareUrl) {
138
+ display += `\n\n**🐦 Share on Twitter** (helps others discover /vibe!)`;
139
+ display += `\n${data.twitterShareUrl}`;
140
+ } else {
141
+ // Fallback to inline URL if API didn't return one
142
+ const shareText = encodeURIComponent(`šŸš€ Just shipped: ${args.what.substring(0, 80)}\n\nBuilding with /vibe`);
143
+ const encodedUrl = encodeURIComponent(data.shareUrl);
144
+ display += `\n\n**Quick share:**`;
145
+ display += `\n• Twitter: https://twitter.com/intent/tweet?text=${shareText}&url=${encodedUrl}&hashtags=buildinpublic,vibecoders`;
146
+ }
124
147
  }
125
148
 
126
149
  display += '\n';
127
-
150
+
128
151
  // Quiet awareness of similar builders
129
152
  const suggestions = await findSimilarShippers(myHandle, args.what);
130
153
  if (suggestions.length > 0) {
131
- display += `\n_similar: @${suggestions.slice(0, 2).map(s => s.handle).join(', @')}_`;
154
+ display += `\n_similar builders: @${suggestions.slice(0, 2).map(s => s.handle).join(', @')}_`;
132
155
  }
133
156
 
134
157
  return { display };
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Smart Inbox Check Tool
3
+ *
4
+ * MCP tool for intelligent inbox checking at natural breaks.
5
+ * Designed to be called by Claude Code after commits, tests, builds, or natural pauses.
6
+ *
7
+ * Usage:
8
+ * vibe check - Explicit check now
9
+ * vibe check --after-commit - Check after a commit
10
+ * vibe check --after-test - Check after test run
11
+ * vibe check --after-build - Check after build
12
+ * vibe check --natural - Check at natural break
13
+ * vibe check --stats - Show check statistics
14
+ */
15
+
16
+ const smartInbox = require('../smart-inbox');
17
+ const config = require('../config');
18
+
19
+ const TOOL_NAME = 'vibe_smart_check';
20
+
21
+ const toolDefinition = {
22
+ name: TOOL_NAME,
23
+ description: `Smart inbox check at natural breaks. Call this after commits, tests, builds, or between tasks. Returns unread messages and notifications. Debounced to avoid spam.
24
+
25
+ Example triggers:
26
+ - After git commit: trigger="commit", context={message: "feat: add auth"}
27
+ - After test: trigger="test_pass" or trigger="test_fail"
28
+ - After build: trigger="build_complete" or trigger="build_fail"
29
+ - Natural break: trigger="natural" (between tasks, after file saves)
30
+ - Explicit: trigger="explicit" or omit (check now regardless of timing)
31
+
32
+ The tool debounces checks to avoid notification spam (min 1 minute between checks for same trigger type).`,
33
+ inputSchema: {
34
+ type: 'object',
35
+ properties: {
36
+ trigger: {
37
+ type: 'string',
38
+ description: 'What triggered this check',
39
+ enum: ['explicit', 'commit', 'test_pass', 'test_fail', 'build_complete', 'build_fail', 'natural'],
40
+ default: 'explicit',
41
+ },
42
+ context: {
43
+ type: 'object',
44
+ description: 'Optional context about the trigger (commit message, test output, etc.)',
45
+ properties: {
46
+ message: { type: 'string', description: 'Commit message or summary' },
47
+ branch: { type: 'string', description: 'Git branch name' },
48
+ output: { type: 'string', description: 'Test/build output summary' },
49
+ file: { type: 'string', description: 'File being worked on' },
50
+ },
51
+ },
52
+ stats: {
53
+ type: 'boolean',
54
+ description: 'If true, return check statistics instead of checking inbox',
55
+ default: false,
56
+ },
57
+ },
58
+ required: [],
59
+ },
60
+ };
61
+
62
+ async function handler({ trigger = 'explicit', context = {}, stats = false }) {
63
+ // Check initialization
64
+ if (!config.isInitialized()) {
65
+ return {
66
+ content: [{
67
+ type: 'text',
68
+ text: 'āŒ Not initialized. Run `vibe init` first.',
69
+ }],
70
+ };
71
+ }
72
+
73
+ // Return stats if requested
74
+ if (stats) {
75
+ const checkStats = smartInbox.getStats();
76
+ const lines = [
77
+ 'šŸ“Š **Smart Check Statistics**',
78
+ '',
79
+ `Last check: ${checkStats.lastCheck || 'Never'}`,
80
+ `Last trigger: ${checkStats.lastTrigger || 'None'}`,
81
+ `Total checks: ${checkStats.checkCount}`,
82
+ `Time since last: ${checkStats.timeSinceLastCheck ? `${checkStats.timeSinceLastCheck}s ago` : 'N/A'}`,
83
+ '',
84
+ '**Today\'s triggers:**',
85
+ ];
86
+
87
+ const todayTriggers = checkStats.todayTriggers;
88
+ if (Object.keys(todayTriggers).length === 0) {
89
+ lines.push(' (none yet)');
90
+ } else {
91
+ for (const [trig, count] of Object.entries(todayTriggers)) {
92
+ lines.push(` ${trig}: ${count}`);
93
+ }
94
+ }
95
+
96
+ return {
97
+ content: [{
98
+ type: 'text',
99
+ text: lines.join('\n'),
100
+ }],
101
+ };
102
+ }
103
+
104
+ // Map trigger strings to constants
105
+ const triggerMap = {
106
+ explicit: smartInbox.TRIGGERS.EXPLICIT,
107
+ commit: smartInbox.TRIGGERS.COMMIT,
108
+ test_pass: smartInbox.TRIGGERS.TEST_PASS,
109
+ test_fail: smartInbox.TRIGGERS.TEST_FAIL,
110
+ build_complete: smartInbox.TRIGGERS.BUILD_COMPLETE,
111
+ build_fail: smartInbox.TRIGGERS.BUILD_FAIL,
112
+ natural: smartInbox.TRIGGERS.NATURAL_BREAK,
113
+ };
114
+
115
+ const triggerType = triggerMap[trigger] || smartInbox.TRIGGERS.EXPLICIT;
116
+
117
+ // Perform the check
118
+ const result = await smartInbox.check(triggerType, context);
119
+
120
+ // Build response
121
+ if (!result.checked) {
122
+ // Check was debounced or skipped
123
+ const waitSec = Math.ceil((result.waitMs || 0) / 1000);
124
+ return {
125
+ content: [{
126
+ type: 'text',
127
+ text: `ā³ Skipped (${result.reason})${waitSec > 0 ? ` — try again in ${waitSec}s` : ''}`,
128
+ }],
129
+ };
130
+ }
131
+
132
+ // Format the result
133
+ const lines = [];
134
+ const handle = config.getHandle();
135
+
136
+ // Show what triggered the check
137
+ const triggerEmoji = {
138
+ commit: 'šŸ“',
139
+ test_pass: 'āœ…',
140
+ test_fail: 'āŒ',
141
+ build_complete: 'šŸ—ļø',
142
+ build_fail: 'šŸ”“',
143
+ natural_break: 'ā˜•',
144
+ explicit: 'šŸ””',
145
+ session_start: 'šŸš€',
146
+ session_end: 'šŸ‘‹',
147
+ };
148
+
149
+ lines.push(`${triggerEmoji[result.trigger] || 'šŸ“¬'} **Inbox check** (${result.trigger})`);
150
+ lines.push('');
151
+
152
+ // Unread messages
153
+ if (result.unreadCount > 0) {
154
+ lines.push(`šŸ“Ø **${result.unreadCount} unread message${result.unreadCount > 1 ? 's' : ''}**`);
155
+ if (result.notified) {
156
+ lines.push(' → Notification sent');
157
+ }
158
+ lines.push(` → Open with \`vibe inbox\``);
159
+ } else {
160
+ lines.push('šŸ“­ No unread messages');
161
+ }
162
+
163
+ // New users online
164
+ if (result.newOnline && result.newOnline.length > 0) {
165
+ lines.push('');
166
+ lines.push(`🟢 **Just came online:** @${result.newOnline.join(', @')}`);
167
+ }
168
+
169
+ // New ships
170
+ if (result.newShips && result.newShips.length > 0) {
171
+ lines.push('');
172
+ lines.push(`šŸš€ **New ships from connections:** @${result.newShips.join(', @')}`);
173
+ }
174
+
175
+ // Show context if provided
176
+ if (context.message) {
177
+ lines.push('');
178
+ lines.push(`_Context: "${context.message.slice(0, 60)}${context.message.length > 60 ? '...' : ''}"_`);
179
+ }
180
+
181
+ return {
182
+ content: [{
183
+ type: 'text',
184
+ text: lines.join('\n'),
185
+ }],
186
+ // Include structured data for programmatic use
187
+ _meta: {
188
+ unreadCount: result.unreadCount,
189
+ notified: result.notified,
190
+ newOnline: result.newOnline,
191
+ newShips: result.newShips,
192
+ trigger: result.trigger,
193
+ },
194
+ };
195
+ }
196
+
197
+ module.exports = {
198
+ name: TOOL_NAME,
199
+ definition: toolDefinition,
200
+ handler,
201
+ };
package/tools/start.js CHANGED
@@ -18,6 +18,7 @@ const notify = require('../notify');
18
18
  const patterns = require('../intelligence/patterns');
19
19
  const { actions, formatActions } = require('./_actions');
20
20
  const init = require('./init');
21
+ const { gatherWithTimeout } = require('./_work-context');
21
22
 
22
23
  const REPO_DIR = path.join(process.env.HOME, '.vibe', 'vibe-repo');
23
24
 
@@ -191,7 +192,7 @@ async function handler(args) {
191
192
 
192
193
  // Step 1: Check if properly authenticated with OAuth
193
194
  // If not, redirect to init for GitHub auth flow (shows pre-auth banner + OAuth)
194
- if (!config.hasPrivyAuth()) {
195
+ if (!config.hasOAuth()) {
195
196
  return init.handler({
196
197
  handle: args.handle,
197
198
  one_liner: args.building
@@ -199,7 +200,7 @@ async function handler(args) {
199
200
  }
200
201
 
201
202
  // Step 2: User is authenticated - show dashboard
202
- let myHandle = config.getHandle();
203
+ const myHandle = config.getHandle();
203
204
  let threads = [];
204
205
  let updateNotice = '';
205
206
 
@@ -211,6 +212,32 @@ async function handler(args) {
211
212
  // Fetch version info early (non-blocking, cached)
212
213
  const versionInfo = await getVersionInfo().catch(() => null);
213
214
 
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
220
+
221
+ if (autoContextEnabled) {
222
+ try {
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
234
+ }
235
+ } catch (e) {
236
+ // Silent fail - context is nice-to-have, not required
237
+ workContext = null;
238
+ }
239
+ }
240
+
214
241
  // Log session start for patterns
215
242
  patterns.logSessionStart(myHandle);
216
243
 
@@ -225,8 +252,12 @@ async function handler(args) {
225
252
 
226
253
  // Step 3: Check inbox + trigger notifications
227
254
  let unreadCount = 0;
255
+ let inboxThreads = [];
228
256
  try {
229
- unreadCount = await store.getUnreadCount(myHandle);
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);
260
+
230
261
  if (unreadCount > 0) {
231
262
  // Check for messages needing desktop notification escalation
232
263
  const rawInbox = await store.getRawInbox(myHandle).catch(() => []);
@@ -236,6 +267,24 @@ async function handler(args) {
236
267
  }
237
268
  } catch (e) {}
238
269
 
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
+
239
288
  // Generate the ASCII welcome card (matches init.js format)
240
289
  const welcomeCard = generateWelcomeCard({
241
290
  handle: myHandle,
@@ -247,6 +296,46 @@ async function handler(args) {
247
296
  // Build display with card + any additional info
248
297
  let display = welcomeCard;
249
298
 
299
+ // Add who's online section (top 5 with what they're building)
300
+ if (others.length > 0) {
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
+ }
312
+ }
313
+
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
+ });
337
+ }
338
+
250
339
  // Add memory context for returning users
251
340
  if (threads.length > 0) {
252
341
  const recentThreads = threads.slice(0, 3);
@@ -259,17 +348,48 @@ async function handler(args) {
259
348
  display += updateNotice;
260
349
  }
261
350
 
262
- // Step 6: Suggest background presence monitor (if not running)
263
- const presenceAgentEnabled = config.get('presenceAgentEnabled', true);
264
- const presenceAgentRunning = config.get('presenceAgentRunning');
265
-
266
- if (presenceAgentEnabled && !presenceAgentRunning && others.length > 0) {
267
- display += `\n\n---\nšŸ’” **Tip:** Say "start presence monitor" for real-time alerts when interesting people come online.`;
268
- }
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]}`;
269
360
 
270
361
  // Build response with hints for structured dashboard flow
271
362
  const response = { display };
272
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
+
273
393
  // Determine session state and suggest appropriate flow
274
394
  let suggestion = null;
275
395
 
@@ -304,18 +424,33 @@ async function handler(args) {
304
424
 
305
425
  if (others.length === 0 && unreadCount === 0) {
306
426
  // Empty room
307
- actionList = actions.emptyRoom();
427
+ actionList = actions.emptyRoom({ workContext });
308
428
  } else {
309
429
  // Normal dashboard
310
430
  actionList = actions.dashboard({
311
431
  unreadCount,
312
432
  onlineUsers: onlineHandles,
313
- suggestion
433
+ suggestion,
434
+ workContext
314
435
  });
315
436
  }
316
437
 
317
438
  response.actions = formatActions(actionList);
318
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
+
319
454
  return response;
320
455
  }
321
456