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.
- package/README.md +1 -0
- package/analytics.js +107 -0
- package/config.js +174 -3
- package/index.js +163 -34
- package/intelligence/index.js +45 -0
- package/intelligence/infer.js +316 -0
- package/intelligence/interests.js +369 -0
- package/intelligence/patterns.js +651 -0
- package/intelligence/proactive.js +358 -0
- package/intelligence/serendipity.js +306 -0
- package/notification-emitter.js +77 -0
- package/notify.js +141 -18
- package/package.json +14 -6
- package/presence.js +5 -1
- package/protocol/index.js +88 -1
- package/protocol/telegram-commands.js +199 -0
- package/store/api.js +469 -29
- package/store/index.js +7 -7
- package/store/local.js +67 -11
- package/store/profiles.js +435 -0
- package/store/reservations.js +321 -0
- package/store/skills.js +378 -0
- package/tools/_actions.js +491 -22
- package/tools/_connection-queue.js +257 -0
- package/tools/_discovery-enhanced.js +290 -0
- package/tools/_discovery.js +439 -0
- package/tools/_proactive-discovery.js +301 -0
- package/tools/_shared/index.js +64 -0
- package/tools/_work-context.js +338 -0
- package/tools/_work-context.manual-test.js +199 -0
- package/tools/_work-context.test.js +260 -0
- package/tools/admin-inbox.js +218 -0
- package/tools/agent-treasury.js +288 -0
- package/tools/agents.js +122 -0
- package/tools/analytics.js +191 -0
- package/tools/approve.js +197 -0
- package/tools/arcade.js +173 -0
- package/tools/artifact-create.js +247 -0
- package/tools/artifact-view.js +174 -0
- package/tools/artifacts-price.js +107 -0
- package/tools/ask-expert.js +160 -0
- package/tools/auto-suggest-connections.js +304 -0
- package/tools/away.js +68 -0
- package/tools/back.js +51 -0
- package/tools/become-expert.js +150 -0
- package/tools/bootstrap-skills.js +231 -0
- package/tools/bridge-dashboard.js +342 -0
- package/tools/bridge-health.js +400 -0
- package/tools/bridge-live.js +384 -0
- package/tools/bridges.js +383 -0
- package/tools/broadcast.js +286 -0
- package/tools/bye.js +4 -0
- package/tools/chat.js +202 -0
- package/tools/collaborative-drawing.js +286 -0
- package/tools/colorguess.js +281 -0
- package/tools/crossword.js +369 -0
- package/tools/discover-insights.js +379 -0
- package/tools/discover-momentum.js +256 -0
- package/tools/discover.js +675 -0
- package/tools/discovery-analytics.js +345 -0
- package/tools/discovery-auto-suggest.js +275 -0
- package/tools/discovery-bootstrap.js +267 -0
- package/tools/discovery-daily.js +375 -0
- package/tools/discovery-dashboard.js +385 -0
- package/tools/discovery-digest.js +314 -0
- package/tools/discovery-hub.js +357 -0
- package/tools/discovery-insights.js +384 -0
- package/tools/discovery-momentum.js +281 -0
- package/tools/discovery-monitor.js +319 -0
- package/tools/discovery-proactive.js +300 -0
- package/tools/dm.js +84 -14
- package/tools/draw.js +317 -0
- package/tools/drawing.js +310 -0
- package/tools/earnings.js +126 -0
- package/tools/echo.js +16 -0
- package/tools/farcaster.js +307 -0
- package/tools/feed.js +215 -0
- package/tools/follow.js +224 -0
- package/tools/friends.js +192 -0
- package/tools/game.js +218 -110
- package/tools/games-catalog.js +376 -0
- package/tools/games.js +313 -0
- package/tools/genesis.js +233 -0
- package/tools/gig-browse.js +206 -0
- package/tools/gig-complete.js +139 -0
- package/tools/guessnumber.js +194 -0
- package/tools/hangman.js +129 -0
- package/tools/help.js +269 -0
- package/tools/idea.js +217 -0
- package/tools/inbox.js +291 -25
- package/tools/init.js +657 -33
- package/tools/insights.js +123 -0
- package/tools/invite.js +142 -21
- package/tools/l2-bridge.js +272 -0
- package/tools/l2-status.js +217 -0
- package/tools/l2.js +206 -0
- package/tools/migrate.js +156 -0
- package/tools/mint.js +377 -0
- package/tools/multiplayer-game.js +275 -0
- package/tools/multiplayer-tictactoe.js +303 -0
- package/tools/mute.js +97 -0
- package/tools/notifications.js +415 -0
- package/tools/observe.js +200 -0
- package/tools/onboarding.js +147 -0
- package/tools/open.js +52 -3
- package/tools/party-game.js +314 -0
- package/tools/plan.js +225 -0
- package/tools/presence-agent.js +167 -0
- package/tools/profile.js +219 -0
- package/tools/proof-of-work.js +139 -0
- package/tools/pulse.js +218 -0
- package/tools/react.js +4 -0
- package/tools/release.js +83 -0
- package/tools/report.js +109 -0
- package/tools/reputation.js +175 -0
- package/tools/request.js +231 -0
- package/tools/reservations.js +116 -0
- package/tools/reserve.js +111 -0
- package/tools/riddle.js +240 -0
- package/tools/run-bootstrap.js +69 -0
- package/tools/schedule.js +367 -0
- package/tools/session.js +420 -0
- package/tools/session_price.js +128 -0
- package/tools/settings.js +200 -0
- package/tools/ship.js +188 -0
- package/tools/shipback.js +326 -0
- package/tools/skills-analytics.js +349 -0
- package/tools/skills-bootstrap.js +301 -0
- package/tools/skills-dashboard.js +268 -0
- package/tools/skills-exchange.js +342 -0
- package/tools/skills.js +380 -0
- package/tools/smart-intro.js +353 -0
- package/tools/social-inbox.js +326 -69
- package/tools/social-post.js +251 -66
- package/tools/social-processor.js +445 -0
- package/tools/solo-game.js +390 -0
- package/tools/start.js +296 -81
- package/tools/status.js +53 -6
- package/tools/storybuilder.js +331 -0
- package/tools/stuck.js +297 -0
- package/tools/subscribe.js +148 -0
- package/tools/subscriptions.js +134 -0
- package/tools/suggest-tags.js +184 -0
- package/tools/tag-suggestions.js +257 -0
- package/tools/telegram-bot.js +183 -0
- package/tools/telegram-setup.js +214 -0
- package/tools/tictactoe.js +155 -0
- package/tools/tip.js +120 -0
- package/tools/token.js +103 -0
- package/tools/twentyquestions.js +143 -0
- package/tools/update.js +1 -1
- package/tools/wallet.js +127 -0
- package/tools/watch.js +157 -0
- package/tools/webhook-test.js +388 -0
- package/tools/who.js +118 -25
- package/tools/withdraw.js +145 -0
- package/tools/wordassociation.js +247 -0
- package/tools/work-summary.js +96 -0
- package/tools/workshop-buddy.js +394 -0
- package/tools/workshop.js +327 -0
- package/version.json +12 -3
- 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
|
+
};
|