slashvibe-mcp 0.2.2 → 0.2.4

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 (162) hide show
  1. package/README.md +1 -0
  2. package/analytics.js +107 -0
  3. package/config.js +174 -3
  4. package/index.js +163 -34
  5. package/intelligence/index.js +45 -0
  6. package/intelligence/infer.js +316 -0
  7. package/intelligence/interests.js +369 -0
  8. package/intelligence/patterns.js +651 -0
  9. package/intelligence/proactive.js +358 -0
  10. package/intelligence/serendipity.js +306 -0
  11. package/notification-emitter.js +77 -0
  12. package/notify.js +141 -18
  13. package/package.json +14 -6
  14. package/presence.js +5 -1
  15. package/protocol/index.js +88 -1
  16. package/protocol/telegram-commands.js +199 -0
  17. package/store/api.js +469 -29
  18. package/store/index.js +7 -7
  19. package/store/local.js +67 -11
  20. package/store/profiles.js +435 -0
  21. package/store/reservations.js +321 -0
  22. package/store/skills.js +378 -0
  23. package/tools/_actions.js +491 -22
  24. package/tools/_connection-queue.js +257 -0
  25. package/tools/_discovery-enhanced.js +290 -0
  26. package/tools/_discovery.js +439 -0
  27. package/tools/_proactive-discovery.js +301 -0
  28. package/tools/_shared/index.js +64 -0
  29. package/tools/_work-context.js +338 -0
  30. package/tools/_work-context.manual-test.js +199 -0
  31. package/tools/_work-context.test.js +260 -0
  32. package/tools/admin-inbox.js +218 -0
  33. package/tools/agent-treasury.js +288 -0
  34. package/tools/agents.js +122 -0
  35. package/tools/analytics.js +191 -0
  36. package/tools/approve.js +197 -0
  37. package/tools/arcade.js +173 -0
  38. package/tools/artifact-create.js +247 -0
  39. package/tools/artifact-view.js +174 -0
  40. package/tools/artifacts-price.js +107 -0
  41. package/tools/ask-expert.js +160 -0
  42. package/tools/auto-suggest-connections.js +304 -0
  43. package/tools/away.js +68 -0
  44. package/tools/back.js +51 -0
  45. package/tools/become-expert.js +150 -0
  46. package/tools/bootstrap-skills.js +231 -0
  47. package/tools/bridge-dashboard.js +342 -0
  48. package/tools/bridge-health.js +400 -0
  49. package/tools/bridge-live.js +384 -0
  50. package/tools/bridges.js +383 -0
  51. package/tools/broadcast.js +286 -0
  52. package/tools/bye.js +4 -0
  53. package/tools/chat.js +202 -0
  54. package/tools/collaborative-drawing.js +286 -0
  55. package/tools/colorguess.js +281 -0
  56. package/tools/crossword.js +369 -0
  57. package/tools/discover-insights.js +379 -0
  58. package/tools/discover-momentum.js +256 -0
  59. package/tools/discover.js +675 -0
  60. package/tools/discovery-analytics.js +345 -0
  61. package/tools/discovery-auto-suggest.js +275 -0
  62. package/tools/discovery-bootstrap.js +267 -0
  63. package/tools/discovery-daily.js +375 -0
  64. package/tools/discovery-dashboard.js +385 -0
  65. package/tools/discovery-digest.js +314 -0
  66. package/tools/discovery-hub.js +357 -0
  67. package/tools/discovery-insights.js +384 -0
  68. package/tools/discovery-momentum.js +281 -0
  69. package/tools/discovery-monitor.js +319 -0
  70. package/tools/discovery-proactive.js +300 -0
  71. package/tools/dm.js +84 -14
  72. package/tools/draw.js +317 -0
  73. package/tools/drawing.js +310 -0
  74. package/tools/earnings.js +126 -0
  75. package/tools/echo.js +16 -0
  76. package/tools/farcaster.js +307 -0
  77. package/tools/feed.js +215 -0
  78. package/tools/follow.js +224 -0
  79. package/tools/friends.js +192 -0
  80. package/tools/game.js +218 -110
  81. package/tools/games-catalog.js +376 -0
  82. package/tools/games.js +313 -0
  83. package/tools/genesis.js +233 -0
  84. package/tools/gig-browse.js +206 -0
  85. package/tools/gig-complete.js +139 -0
  86. package/tools/guessnumber.js +194 -0
  87. package/tools/hangman.js +129 -0
  88. package/tools/help.js +269 -0
  89. package/tools/idea.js +217 -0
  90. package/tools/inbox.js +291 -25
  91. package/tools/init.js +657 -33
  92. package/tools/insights.js +123 -0
  93. package/tools/invite.js +142 -21
  94. package/tools/l2-bridge.js +272 -0
  95. package/tools/l2-status.js +217 -0
  96. package/tools/l2.js +206 -0
  97. package/tools/migrate.js +156 -0
  98. package/tools/mint.js +377 -0
  99. package/tools/multiplayer-game.js +275 -0
  100. package/tools/multiplayer-tictactoe.js +303 -0
  101. package/tools/mute.js +97 -0
  102. package/tools/notifications.js +415 -0
  103. package/tools/observe.js +200 -0
  104. package/tools/onboarding.js +147 -0
  105. package/tools/open.js +52 -3
  106. package/tools/party-game.js +314 -0
  107. package/tools/plan.js +225 -0
  108. package/tools/presence-agent.js +167 -0
  109. package/tools/profile.js +219 -0
  110. package/tools/proof-of-work.js +139 -0
  111. package/tools/pulse.js +218 -0
  112. package/tools/react.js +4 -0
  113. package/tools/release.js +83 -0
  114. package/tools/report.js +109 -0
  115. package/tools/reputation.js +175 -0
  116. package/tools/request.js +231 -0
  117. package/tools/reservations.js +116 -0
  118. package/tools/reserve.js +111 -0
  119. package/tools/riddle.js +240 -0
  120. package/tools/run-bootstrap.js +69 -0
  121. package/tools/schedule.js +367 -0
  122. package/tools/session.js +420 -0
  123. package/tools/session_price.js +128 -0
  124. package/tools/settings.js +200 -0
  125. package/tools/ship.js +188 -0
  126. package/tools/shipback.js +326 -0
  127. package/tools/skills-analytics.js +349 -0
  128. package/tools/skills-bootstrap.js +301 -0
  129. package/tools/skills-dashboard.js +268 -0
  130. package/tools/skills-exchange.js +342 -0
  131. package/tools/skills.js +380 -0
  132. package/tools/smart-intro.js +353 -0
  133. package/tools/social-inbox.js +326 -69
  134. package/tools/social-post.js +251 -66
  135. package/tools/social-processor.js +445 -0
  136. package/tools/solo-game.js +390 -0
  137. package/tools/start.js +296 -81
  138. package/tools/status.js +53 -6
  139. package/tools/storybuilder.js +331 -0
  140. package/tools/stuck.js +297 -0
  141. package/tools/subscribe.js +148 -0
  142. package/tools/subscriptions.js +134 -0
  143. package/tools/suggest-tags.js +184 -0
  144. package/tools/tag-suggestions.js +257 -0
  145. package/tools/telegram-bot.js +183 -0
  146. package/tools/telegram-setup.js +214 -0
  147. package/tools/tictactoe.js +155 -0
  148. package/tools/tip.js +120 -0
  149. package/tools/token.js +103 -0
  150. package/tools/twentyquestions.js +143 -0
  151. package/tools/update.js +1 -1
  152. package/tools/wallet.js +127 -0
  153. package/tools/watch.js +157 -0
  154. package/tools/webhook-test.js +388 -0
  155. package/tools/who.js +118 -25
  156. package/tools/withdraw.js +145 -0
  157. package/tools/wordassociation.js +247 -0
  158. package/tools/work-summary.js +96 -0
  159. package/tools/workshop-buddy.js +394 -0
  160. package/tools/workshop.js +327 -0
  161. package/version.json +12 -3
  162. package/tools/board.js +0 -130
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Smart Detection — Ambient Intelligence for /vibe
3
+ *
4
+ * Automatically infers user state from context signals:
5
+ * - File activity (what they're editing)
6
+ * - Branch names (fix-, feat-, debug-)
7
+ * - Error patterns (debugging)
8
+ * - Session length (deep focus)
9
+ * - Message activity (social vs focused)
10
+ *
11
+ * Inferred states displayed in `vibe who` with (inferred) label
12
+ */
13
+
14
+ // State definitions with detection rules
15
+ const STATES = {
16
+ 'github-shipping': {
17
+ emoji: '🔥',
18
+ label: 'shipping code',
19
+ priority: 0, // Highest priority — real GitHub commits
20
+ description: 'Active GitHub commits detected'
21
+ },
22
+ 'deep-focus': {
23
+ emoji: '🧠',
24
+ label: 'deep focus',
25
+ priority: 1,
26
+ description: 'Long session, minimal messaging'
27
+ },
28
+ 'shipping': {
29
+ emoji: '🚀',
30
+ label: 'shipping',
31
+ priority: 2,
32
+ description: 'Active commits, on main/master branch'
33
+ },
34
+ 'debugging': {
35
+ emoji: '🐛',
36
+ label: 'debugging',
37
+ priority: 3,
38
+ description: 'Errors present or fix- branch'
39
+ },
40
+ 'exploring': {
41
+ emoji: '🔍',
42
+ label: 'exploring',
43
+ priority: 4,
44
+ description: 'Many file switches, few edits'
45
+ },
46
+ 'stuck': {
47
+ emoji: '🤔',
48
+ label: 'might be stuck',
49
+ priority: 5,
50
+ description: 'Same file for a while, no commits'
51
+ },
52
+ 'pairing': {
53
+ emoji: '👥',
54
+ label: 'pairing',
55
+ priority: 6,
56
+ description: 'Active messaging with one person'
57
+ },
58
+ 'late-night': {
59
+ emoji: '🌙',
60
+ label: 'late night',
61
+ priority: 7,
62
+ description: 'Coding after midnight local time'
63
+ }
64
+ };
65
+
66
+ /**
67
+ * Infer user state from context signals
68
+ *
69
+ * @param {Object} context - User's context from presence
70
+ * @param {string} context.file - Current file being edited
71
+ * @param {string} context.branch - Current git branch
72
+ * @param {string} context.error - Recent error message
73
+ * @param {string} context.note - User's note
74
+ * @param {string} context.mood - Explicit mood (if set)
75
+ * @param {number} context.sessionStart - Session start timestamp
76
+ * @param {number} context.lastMessage - Last message timestamp
77
+ * @param {number} context.messageCount - Messages in last hour
78
+ * @param {number} context.fileChangeCount - File changes in last 30min
79
+ * @param {string} context.lastCommit - Last commit timestamp
80
+ * @returns {Object|null} Inferred state or null if no strong signal
81
+ */
82
+ function inferState(context = {}) {
83
+ // If user has explicit mood set, respect it (no inference)
84
+ if (context.mood && !context.mood_inferred) {
85
+ return null;
86
+ }
87
+
88
+ const signals = analyzeSignals(context);
89
+ const candidates = [];
90
+
91
+ // Rule: GitHub Shipping (highest priority - real commits are undeniable)
92
+ // Active GitHub activity detected from cached activity data
93
+ if (signals.githubShippingMode === 'hot') {
94
+ candidates.push({
95
+ state: 'github-shipping',
96
+ confidence: 0.95, // Very high - based on actual commits
97
+ reason: signals.githubCommits > 0
98
+ ? `${signals.githubCommits} commits recently`
99
+ : 'active on GitHub'
100
+ });
101
+ } else if (signals.githubShippingMode === 'active') {
102
+ candidates.push({
103
+ state: 'github-shipping',
104
+ confidence: 0.85,
105
+ reason: 'pushing commits'
106
+ });
107
+ }
108
+
109
+ // Rule: Deep Focus
110
+ // Long session (2h+), minimal messaging in last hour
111
+ if (signals.sessionHours >= 2 && signals.messageCount < 2) {
112
+ candidates.push({
113
+ state: 'deep-focus',
114
+ confidence: Math.min(0.9, 0.5 + (signals.sessionHours - 2) * 0.1),
115
+ reason: `${signals.sessionHours}h session, focused`
116
+ });
117
+ }
118
+
119
+ // Rule: Debugging
120
+ // Has error OR on a fix/debug/bug branch
121
+ if (signals.hasError || signals.isDebugBranch) {
122
+ const confidence = signals.hasError ? 0.85 : 0.7;
123
+ const reason = signals.hasError
124
+ ? 'working through an error'
125
+ : `on ${context.branch}`;
126
+ candidates.push({
127
+ state: 'debugging',
128
+ confidence,
129
+ reason
130
+ });
131
+ }
132
+
133
+ // Rule: Shipping
134
+ // On main/master, recent commit activity
135
+ if (signals.isMainBranch && signals.recentCommit) {
136
+ candidates.push({
137
+ state: 'shipping',
138
+ confidence: 0.8,
139
+ reason: 'deploying to main'
140
+ });
141
+ }
142
+
143
+ // Rule: Stuck
144
+ // Same file for 30+ min, no commits
145
+ if (signals.sameFileDuration >= 30 && !signals.recentCommit) {
146
+ candidates.push({
147
+ state: 'stuck',
148
+ confidence: Math.min(0.7, 0.4 + (signals.sameFileDuration - 30) * 0.01),
149
+ reason: `${signals.sameFileDuration}min on same file`
150
+ });
151
+ }
152
+
153
+ // Rule: Exploring
154
+ // Many file changes, few edits (high switch rate)
155
+ if (signals.fileChangeCount >= 5 && signals.sessionHours < 1) {
156
+ candidates.push({
157
+ state: 'exploring',
158
+ confidence: 0.65,
159
+ reason: 'browsing codebase'
160
+ });
161
+ }
162
+
163
+ // Rule: Late Night
164
+ // After midnight, before 5am
165
+ if (signals.isLateNight) {
166
+ candidates.push({
167
+ state: 'late-night',
168
+ confidence: 0.75,
169
+ reason: 'burning midnight oil'
170
+ });
171
+ }
172
+
173
+ // Return highest confidence state above threshold
174
+ if (candidates.length === 0) return null;
175
+
176
+ candidates.sort((a, b) => b.confidence - a.confidence);
177
+ const best = candidates[0];
178
+
179
+ // Only return if confidence is high enough
180
+ if (best.confidence < 0.6) return null;
181
+
182
+ const stateInfo = STATES[best.state];
183
+ return {
184
+ state: best.state,
185
+ emoji: stateInfo.emoji,
186
+ label: stateInfo.label,
187
+ confidence: best.confidence,
188
+ reason: best.reason,
189
+ inferred: true
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Analyze raw context into normalized signals
195
+ */
196
+ function analyzeSignals(context) {
197
+ const now = Date.now();
198
+
199
+ // Session duration in hours
200
+ const sessionStart = context.sessionStart || context.firstSeen || now;
201
+ const sessionHours = (now - sessionStart) / (1000 * 60 * 60);
202
+
203
+ // Branch analysis
204
+ const branch = (context.branch || '').toLowerCase();
205
+ const isDebugBranch = /^(fix|debug|bug|hotfix)[-_\/]/.test(branch);
206
+ const isMainBranch = ['main', 'master', 'production', 'prod'].includes(branch);
207
+
208
+ // Error present
209
+ const hasError = Boolean(context.error && context.error.length > 0);
210
+
211
+ // Time since last commit (in minutes)
212
+ const lastCommitTime = context.lastCommit ? new Date(context.lastCommit).getTime() : 0;
213
+ const minutesSinceCommit = lastCommitTime ? (now - lastCommitTime) / (1000 * 60) : Infinity;
214
+ const recentCommit = minutesSinceCommit < 30;
215
+
216
+ // File stickiness (how long on same file)
217
+ const sameFileDuration = context.sameFileSince
218
+ ? (now - context.sameFileSince) / (1000 * 60)
219
+ : 0;
220
+
221
+ // Message activity
222
+ const messageCount = context.messageCount || 0;
223
+
224
+ // File change frequency
225
+ const fileChangeCount = context.fileChangeCount || 0;
226
+
227
+ // Time of day check (local time)
228
+ const hour = new Date().getHours();
229
+ const isLateNight = hour >= 0 && hour < 5;
230
+
231
+ // GitHub activity signals (from presence enrichment)
232
+ const github = context.github || {};
233
+ const githubShippingMode = github.shipping_mode || null;
234
+ const githubCommits = github.total_commits || 0;
235
+
236
+ return {
237
+ sessionHours: Math.round(sessionHours * 10) / 10,
238
+ isDebugBranch,
239
+ isMainBranch,
240
+ hasError,
241
+ recentCommit,
242
+ sameFileDuration: Math.round(sameFileDuration),
243
+ messageCount,
244
+ fileChangeCount,
245
+ isLateNight,
246
+ // GitHub signals
247
+ githubShippingMode,
248
+ githubCommits
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Get display text for inferred state
254
+ */
255
+ function formatInferredState(inference) {
256
+ if (!inference) return null;
257
+ return {
258
+ display: `${inference.emoji} ${inference.label} _(inferred)_`,
259
+ short: `${inference.emoji} ${inference.label}`,
260
+ reason: inference.reason
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Enhance user data with inferred state
266
+ * Called when building presence list
267
+ */
268
+ function enhanceUserWithInference(user) {
269
+ // Build context from user data
270
+ const context = {
271
+ file: user.file,
272
+ branch: user.branch,
273
+ error: user.error,
274
+ note: user.note,
275
+ mood: user.mood,
276
+ mood_inferred: user.mood_inferred,
277
+ firstSeen: user.firstSeen,
278
+ sessionStart: user.firstSeen,
279
+ messageCount: user.messageCount || 0,
280
+ fileChangeCount: user.fileChangeCount || 0,
281
+ sameFileSince: user.sameFileSince,
282
+ lastCommit: user.lastCommit,
283
+ // GitHub activity data (from presence enrichment)
284
+ github: user.github || null
285
+ };
286
+
287
+ const inference = inferState(context);
288
+
289
+ if (inference) {
290
+ return {
291
+ ...user,
292
+ mood: inference.emoji,
293
+ mood_inferred: true,
294
+ mood_reason: inference.reason,
295
+ inferred_state: inference.state
296
+ };
297
+ }
298
+
299
+ return user;
300
+ }
301
+
302
+ /**
303
+ * Batch enhance multiple users
304
+ */
305
+ function enhanceUsersWithInference(users) {
306
+ return users.map(enhanceUserWithInference);
307
+ }
308
+
309
+ module.exports = {
310
+ STATES,
311
+ inferState,
312
+ analyzeSignals,
313
+ formatInferredState,
314
+ enhanceUserWithInference,
315
+ enhanceUsersWithInference
316
+ };
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Interest Inference — Derive interests from live context signals
3
+ *
4
+ * Complements static inference (from building descriptions) with DYNAMIC
5
+ * interests inferred from what users are ACTUALLY doing:
6
+ * - Current file/module they're editing
7
+ * - Branch name they're working on
8
+ * - Activity patterns (shipping, debugging, etc.)
9
+ *
10
+ * This creates a more accurate picture of interests that updates in real-time.
11
+ */
12
+
13
+ const { getModule, extractBranchTopic } = require('./serendipity');
14
+
15
+ // Map file modules to interest categories
16
+ const MODULE_TO_INTERESTS = {
17
+ // Code organization
18
+ 'agents': ['agents', 'ai', 'automation'],
19
+ 'agent': ['agents', 'ai', 'automation'],
20
+ 'mcp': ['agents', 'mcp', 'ai'],
21
+ 'ai': ['ai', 'machine learning'],
22
+ 'ml': ['machine learning', 'ai', 'data science'],
23
+ 'llm': ['ai', 'machine learning'],
24
+
25
+ // Web development
26
+ 'api': ['backend', 'api'],
27
+ 'components': ['frontend', 'web development'],
28
+ 'pages': ['frontend', 'web development'],
29
+ 'views': ['frontend', 'web development'],
30
+ 'hooks': ['frontend', 'react'],
31
+ 'store': ['frontend', 'state management'],
32
+ 'redux': ['frontend', 'state management'],
33
+
34
+ // Backend
35
+ 'server': ['backend', 'api'],
36
+ 'services': ['backend', 'microservices'],
37
+ 'controllers': ['backend', 'api'],
38
+ 'models': ['backend', 'database'],
39
+ 'db': ['database', 'backend'],
40
+ 'database': ['database', 'backend'],
41
+
42
+ // Infrastructure
43
+ 'infra': ['infrastructure', 'devops'],
44
+ 'deploy': ['devops', 'infrastructure'],
45
+ 'docker': ['devops', 'containers'],
46
+ 'k8s': ['devops', 'kubernetes'],
47
+ 'terraform': ['infrastructure', 'devops'],
48
+ 'ci': ['devops', 'automation'],
49
+
50
+ // Testing
51
+ 'tests': ['testing', 'quality'],
52
+ 'test': ['testing', 'quality'],
53
+ '__tests__': ['testing', 'quality'],
54
+ 'spec': ['testing', 'quality'],
55
+
56
+ // Mobile
57
+ 'ios': ['mobile development', 'ios'],
58
+ 'android': ['mobile development', 'android'],
59
+ 'mobile': ['mobile development'],
60
+
61
+ // Data
62
+ 'data': ['data science', 'analytics'],
63
+ 'analytics': ['analytics', 'data science'],
64
+
65
+ // Security
66
+ 'auth': ['security', 'authentication'],
67
+ 'security': ['security'],
68
+
69
+ // Tools
70
+ 'tools': ['developer tools'],
71
+ 'cli': ['developer tools', 'cli'],
72
+ 'scripts': ['developer tools', 'automation'],
73
+
74
+ // Content
75
+ 'docs': ['documentation', 'writing'],
76
+ 'content': ['content creation'],
77
+
78
+ // Games
79
+ 'game': ['game development', 'gaming'],
80
+ 'games': ['game development', 'gaming'],
81
+
82
+ // Design
83
+ 'design': ['design', 'creative'],
84
+ 'styles': ['frontend', 'design'],
85
+ 'ui': ['design', 'frontend']
86
+ };
87
+
88
+ // Map branch topics to interest categories
89
+ const TOPIC_TO_INTERESTS = {
90
+ 'auth': ['security', 'authentication'],
91
+ 'login': ['security', 'authentication'],
92
+ 'signup': ['security', 'authentication'],
93
+ 'user': ['user experience', 'product'],
94
+ 'ui': ['frontend', 'design'],
95
+ 'api': ['backend', 'api'],
96
+ 'db': ['database', 'backend'],
97
+ 'database': ['database', 'backend'],
98
+ 'ml': ['machine learning', 'ai'],
99
+ 'ai': ['ai', 'machine learning'],
100
+ 'agent': ['agents', 'ai', 'automation'],
101
+ 'test': ['testing', 'quality'],
102
+ 'config': ['infrastructure', 'devops'],
103
+ 'payment': ['fintech', 'product'],
104
+ 'email': ['communication', 'backend'],
105
+ 'notification': ['communication', 'product'],
106
+ 'search': ['search', 'backend'],
107
+ 'cache': ['performance', 'backend'],
108
+ 'session': ['backend', 'security'],
109
+ 'deploy': ['devops', 'infrastructure'],
110
+ 'perf': ['performance', 'optimization'],
111
+ 'mobile': ['mobile development'],
112
+ 'social': ['social', 'community']
113
+ };
114
+
115
+ // Map file extensions to interests
116
+ const EXTENSION_TO_INTERESTS = {
117
+ '.py': ['python'],
118
+ '.js': ['javascript'],
119
+ '.ts': ['typescript'],
120
+ '.tsx': ['react', 'typescript'],
121
+ '.jsx': ['react', 'javascript'],
122
+ '.go': ['go'],
123
+ '.rs': ['rust'],
124
+ '.rb': ['ruby'],
125
+ '.java': ['java'],
126
+ '.swift': ['swift', 'ios'],
127
+ '.kt': ['kotlin', 'android'],
128
+ '.sol': ['solidity', 'web3'],
129
+ '.vue': ['vue', 'frontend'],
130
+ '.svelte': ['svelte', 'frontend'],
131
+ '.css': ['css', 'frontend'],
132
+ '.scss': ['css', 'frontend'],
133
+ '.sql': ['sql', 'database'],
134
+ '.prisma': ['database', 'backend'],
135
+ '.graphql': ['graphql', 'api'],
136
+ '.proto': ['grpc', 'backend'],
137
+ '.yml': ['devops', 'infrastructure'],
138
+ '.yaml': ['devops', 'infrastructure'],
139
+ '.tf': ['terraform', 'infrastructure'],
140
+ '.md': ['documentation', 'writing']
141
+ };
142
+
143
+ /**
144
+ * Infer interests from user's current context
145
+ * Returns array of interests with confidence scores and sources
146
+ *
147
+ * @param {Object} context - User's current context
148
+ * @param {string} context.file - Current file path
149
+ * @param {string} context.branch - Current branch name
150
+ * @param {string} context.mood - Current mood/state
151
+ * @param {string} context.error - Current error (if any)
152
+ * @returns {Array} Array of { interest, source, confidence }
153
+ */
154
+ function inferLiveInterests(context) {
155
+ if (!context) return [];
156
+
157
+ const interests = [];
158
+
159
+ // 1. Infer from current file/module
160
+ if (context.file) {
161
+ const module = getModule(context.file);
162
+ if (module && MODULE_TO_INTERESTS[module.toLowerCase()]) {
163
+ const moduleInterests = MODULE_TO_INTERESTS[module.toLowerCase()];
164
+ interests.push(...moduleInterests.map(i => ({
165
+ interest: i,
166
+ source: 'file',
167
+ confidence: 0.7,
168
+ detail: `editing ${module}/`
169
+ })));
170
+ }
171
+
172
+ // Also check file extension
173
+ const ext = getFileExtension(context.file);
174
+ if (ext && EXTENSION_TO_INTERESTS[ext]) {
175
+ interests.push(...EXTENSION_TO_INTERESTS[ext].map(i => ({
176
+ interest: i,
177
+ source: 'extension',
178
+ confidence: 0.5,
179
+ detail: `working with ${ext} files`
180
+ })));
181
+ }
182
+ }
183
+
184
+ // 2. Infer from branch name
185
+ if (context.branch) {
186
+ const topic = extractBranchTopic(context.branch);
187
+ if (topic && TOPIC_TO_INTERESTS[topic]) {
188
+ interests.push(...TOPIC_TO_INTERESTS[topic].map(i => ({
189
+ interest: i,
190
+ source: 'branch',
191
+ confidence: 0.8,
192
+ detail: `on ${context.branch}`
193
+ })));
194
+ }
195
+
196
+ // Special branch patterns
197
+ if (/agent|mcp|claude/i.test(context.branch)) {
198
+ interests.push({
199
+ interest: 'agents',
200
+ source: 'branch',
201
+ confidence: 0.9,
202
+ detail: `agent-related branch`
203
+ });
204
+ }
205
+ }
206
+
207
+ // 3. Infer from activity state (mood)
208
+ if (context.mood) {
209
+ const moodInterests = inferFromMood(context.mood);
210
+ interests.push(...moodInterests);
211
+ }
212
+
213
+ // 4. Infer from error (if debugging)
214
+ if (context.error) {
215
+ const errorInterests = inferFromError(context.error);
216
+ interests.push(...errorInterests);
217
+ }
218
+
219
+ // Dedupe and return top interests by confidence
220
+ return dedupeByConfidence(interests).slice(0, 5);
221
+ }
222
+
223
+ /**
224
+ * Infer interests from mood/state
225
+ */
226
+ function inferFromMood(mood) {
227
+ const interests = [];
228
+ const moodLower = (mood || '').toLowerCase();
229
+
230
+ if (moodLower.includes('shipping') || moodLower.includes('deploy')) {
231
+ interests.push({
232
+ interest: 'shipping',
233
+ source: 'mood',
234
+ confidence: 0.6,
235
+ detail: 'actively shipping'
236
+ });
237
+ }
238
+
239
+ if (moodLower.includes('debug') || moodLower.includes('fix')) {
240
+ interests.push({
241
+ interest: 'debugging',
242
+ source: 'mood',
243
+ confidence: 0.6,
244
+ detail: 'debugging'
245
+ });
246
+ }
247
+
248
+ return interests;
249
+ }
250
+
251
+ /**
252
+ * Infer interests from error messages
253
+ */
254
+ function inferFromError(error) {
255
+ const interests = [];
256
+ const errorLower = (error || '').toLowerCase();
257
+
258
+ // Database errors
259
+ if (/sql|postgres|mysql|mongo|prisma|database/i.test(errorLower)) {
260
+ interests.push({
261
+ interest: 'database',
262
+ source: 'error',
263
+ confidence: 0.5,
264
+ detail: 'debugging database issue'
265
+ });
266
+ }
267
+
268
+ // API errors
269
+ if (/fetch|axios|api|endpoint|401|403|404|500/i.test(errorLower)) {
270
+ interests.push({
271
+ interest: 'api',
272
+ source: 'error',
273
+ confidence: 0.5,
274
+ detail: 'debugging API issue'
275
+ });
276
+ }
277
+
278
+ // Type errors (TypeScript)
279
+ if (/type.*error|typescript|cannot assign/i.test(errorLower)) {
280
+ interests.push({
281
+ interest: 'typescript',
282
+ source: 'error',
283
+ confidence: 0.5,
284
+ detail: 'debugging type error'
285
+ });
286
+ }
287
+
288
+ return interests;
289
+ }
290
+
291
+ /**
292
+ * Get file extension from path
293
+ */
294
+ function getFileExtension(filePath) {
295
+ if (!filePath) return null;
296
+ const match = filePath.match(/\.[^./]+$/);
297
+ return match ? match[0].toLowerCase() : null;
298
+ }
299
+
300
+ /**
301
+ * Deduplicate interests, keeping highest confidence for each
302
+ */
303
+ function dedupeByConfidence(interests) {
304
+ const byInterest = new Map();
305
+
306
+ for (const item of interests) {
307
+ const existing = byInterest.get(item.interest);
308
+ if (!existing || item.confidence > existing.confidence) {
309
+ byInterest.set(item.interest, item);
310
+ }
311
+ }
312
+
313
+ return Array.from(byInterest.values())
314
+ .sort((a, b) => b.confidence - a.confidence);
315
+ }
316
+
317
+ /**
318
+ * Merge live interests with static interests
319
+ * Prioritizes explicit user-set interests, then adds live context
320
+ *
321
+ * @param {Array} staticInterests - User's profile interests
322
+ * @param {Array} liveInterests - Inferred from current context
323
+ * @returns {Array} Combined interests with sources
324
+ */
325
+ function mergeInterests(staticInterests = [], liveInterests = []) {
326
+ const merged = new Map();
327
+
328
+ // Add static interests first (higher priority)
329
+ for (const interest of staticInterests) {
330
+ merged.set(interest, {
331
+ interest,
332
+ source: 'profile',
333
+ confidence: 1.0
334
+ });
335
+ }
336
+
337
+ // Add live interests if not already present
338
+ for (const item of liveInterests) {
339
+ if (!merged.has(item.interest)) {
340
+ merged.set(item.interest, item);
341
+ }
342
+ }
343
+
344
+ return Array.from(merged.values())
345
+ .sort((a, b) => b.confidence - a.confidence);
346
+ }
347
+
348
+ /**
349
+ * Format live interests for display
350
+ */
351
+ function formatLiveInterests(interests) {
352
+ if (!interests || interests.length === 0) return null;
353
+
354
+ const live = interests.filter(i => i.source !== 'profile');
355
+ if (live.length === 0) return null;
356
+
357
+ return live.map(i => i.interest).join(', ');
358
+ }
359
+
360
+ module.exports = {
361
+ inferLiveInterests,
362
+ mergeInterests,
363
+ formatLiveInterests,
364
+ dedupeByConfidence,
365
+ // Expose mappings for testing/extension
366
+ MODULE_TO_INTERESTS,
367
+ TOPIC_TO_INTERESTS,
368
+ EXTENSION_TO_INTERESTS
369
+ };