slashvibe-mcp 0.2.3 → 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 +41 -58
- package/analytics.js +107 -0
- package/config.js +5 -2
- package/index.js +60 -51
- package/intelligence/index.js +9 -2
- package/intelligence/interests.js +369 -0
- package/notification-emitter.js +77 -0
- package/package.json +7 -3
- package/store/api.js +109 -4
- package/store/profiles.js +160 -12
- package/tools/_actions.js +230 -17
- package/tools/_discovery.js +119 -26
- 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/analytics.js +191 -0
- package/tools/approve.js +197 -0
- package/tools/artifact-create.js +14 -3
- package/tools/artifacts-price.js +107 -0
- package/tools/broadcast.js +286 -0
- package/tools/chat.js +202 -0
- package/tools/discover.js +314 -34
- package/tools/dm.js +22 -5
- package/tools/earnings.js +126 -0
- package/tools/feed.js +22 -3
- package/tools/follow.js +224 -0
- package/tools/friends.js +192 -0
- package/tools/gig-browse.js +206 -0
- package/tools/gig-complete.js +139 -0
- package/tools/help.js +2 -2
- package/tools/idea.js +8 -1
- package/tools/inbox.js +248 -105
- package/tools/init.js +7 -1
- package/tools/open.js +42 -5
- package/tools/plan.js +225 -0
- package/tools/proof-of-work.js +139 -0
- package/tools/request.js +16 -2
- package/tools/schedule.js +367 -0
- package/tools/session.js +420 -0
- package/tools/session_price.js +128 -0
- package/tools/settings.js +90 -2
- package/tools/ship.js +8 -2
- package/tools/start.js +96 -3
- package/tools/status.js +53 -6
- package/tools/stuck.js +297 -0
- package/tools/subscribe.js +148 -0
- package/tools/subscriptions.js +134 -0
- package/tools/suggest-tags.js +6 -8
- package/tools/update.js +1 -1
- package/tools/watch.js +157 -0
- package/tools/withdraw.js +145 -0
- package/tools/work-summary.js +96 -0
- package/LICENSE +0 -21
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP `list_changed` notification emitter
|
|
3
|
+
*
|
|
4
|
+
* Triggers Claude to refresh tool results without reconnection.
|
|
5
|
+
* Implements debouncing to prevent notification spam.
|
|
6
|
+
*
|
|
7
|
+
* This eliminates the need for 30-second polling loops,
|
|
8
|
+
* reducing API calls by ~90% and providing instant updates.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
class NotificationEmitter {
|
|
12
|
+
constructor(server) {
|
|
13
|
+
this.server = server;
|
|
14
|
+
this.debounceTimers = {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Emit list_changed notification with debouncing
|
|
19
|
+
* @param {string} reason - Why notification is being sent (for logging/debugging)
|
|
20
|
+
* @param {number} debounceMs - Debounce window in milliseconds (default: 1000ms)
|
|
21
|
+
*/
|
|
22
|
+
emitChange(reason, debounceMs = 1000) {
|
|
23
|
+
// Debounce to prevent notification spam
|
|
24
|
+
// If we get multiple changes of the same type within the window,
|
|
25
|
+
// only emit one notification
|
|
26
|
+
if (this.debounceTimers[reason]) {
|
|
27
|
+
clearTimeout(this.debounceTimers[reason]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.debounceTimers[reason] = setTimeout(() => {
|
|
31
|
+
try {
|
|
32
|
+
this.server.notification({
|
|
33
|
+
method: "notifications/list_changed"
|
|
34
|
+
});
|
|
35
|
+
delete this.debounceTimers[reason];
|
|
36
|
+
} catch (e) {
|
|
37
|
+
// Silent fail - notifications are best-effort
|
|
38
|
+
// If notification fails, Claude will continue working normally
|
|
39
|
+
}
|
|
40
|
+
}, debounceMs);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Emit immediately without debouncing
|
|
45
|
+
* Use for urgent updates like direct mentions
|
|
46
|
+
*/
|
|
47
|
+
emitImmediate() {
|
|
48
|
+
try {
|
|
49
|
+
this.server.notification({
|
|
50
|
+
method: "notifications/list_changed"
|
|
51
|
+
});
|
|
52
|
+
} catch (e) {
|
|
53
|
+
// Silent fail
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Cancel pending notifications for a specific reason
|
|
59
|
+
* Useful when shutting down or cleaning up
|
|
60
|
+
*/
|
|
61
|
+
cancel(reason) {
|
|
62
|
+
if (this.debounceTimers[reason]) {
|
|
63
|
+
clearTimeout(this.debounceTimers[reason]);
|
|
64
|
+
delete this.debounceTimers[reason];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Cancel all pending notifications
|
|
70
|
+
*/
|
|
71
|
+
cancelAll() {
|
|
72
|
+
Object.values(this.debounceTimers).forEach(timer => clearTimeout(timer));
|
|
73
|
+
this.debounceTimers = {};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = NotificationEmitter;
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slashvibe-mcp",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"mcpName": "io.github.
|
|
3
|
+
"version": "0.2.4",
|
|
4
|
+
"mcpName": "io.github.vibecodinginc/vibe",
|
|
5
5
|
"description": "Social layer for Claude Code - DMs, presence, and connection between AI-assisted developers",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"slashvibe-mcp": "./index.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"start": "node index.js"
|
|
11
|
+
"start": "node index.js",
|
|
12
|
+
"test": "node --test tools/*.test.js",
|
|
13
|
+
"test:context": "node --test tools/_work-context.test.js"
|
|
12
14
|
},
|
|
13
15
|
"keywords": [
|
|
14
16
|
"mcp",
|
|
@@ -39,6 +41,8 @@
|
|
|
39
41
|
"discord.js",
|
|
40
42
|
"memory.js",
|
|
41
43
|
"notify.js",
|
|
44
|
+
"notification-emitter.js",
|
|
45
|
+
"analytics.js",
|
|
42
46
|
"presence.js",
|
|
43
47
|
"prompts.js",
|
|
44
48
|
"twitter.js",
|
package/store/api.js
CHANGED
|
@@ -10,12 +10,30 @@ const https = require('https');
|
|
|
10
10
|
const http = require('http');
|
|
11
11
|
const config = require('../config');
|
|
12
12
|
const crypto = require('../crypto');
|
|
13
|
+
const authStore = require('../auth-store');
|
|
13
14
|
|
|
14
15
|
const API_URL = process.env.VIBE_API_URL || 'https://www.slashvibe.dev';
|
|
15
16
|
|
|
16
17
|
// Default timeout for API requests (10 seconds)
|
|
17
18
|
const REQUEST_TIMEOUT = 10000;
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Force reload of config module to pick up auth changes
|
|
22
|
+
* This clears the require cache and reloads the config from disk
|
|
23
|
+
*/
|
|
24
|
+
function reloadConfig() {
|
|
25
|
+
try {
|
|
26
|
+
// Clear the config module from require cache
|
|
27
|
+
const configPath = require.resolve('../config');
|
|
28
|
+
delete require.cache[configPath];
|
|
29
|
+
// Re-require to get fresh module
|
|
30
|
+
return require('../config');
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error('[api] Failed to reload config:', e.message);
|
|
33
|
+
return config;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
19
37
|
function request(method, path, data = null, options = {}) {
|
|
20
38
|
return new Promise((resolve, reject) => {
|
|
21
39
|
const url = new URL(path, API_URL);
|
|
@@ -28,8 +46,9 @@ function request(method, path, data = null, options = {}) {
|
|
|
28
46
|
'User-Agent': 'vibe-mcp/1.0'
|
|
29
47
|
};
|
|
30
48
|
|
|
31
|
-
// Add auth token
|
|
32
|
-
|
|
49
|
+
// Add auth token: priority is explicit option > in-memory store > config file
|
|
50
|
+
// authStore is the SOURCE OF TRUTH during runtime (immediate updates from OAuth)
|
|
51
|
+
const token = options.token || authStore.getToken() || config.getAuthToken();
|
|
33
52
|
if (token && options.auth !== false) {
|
|
34
53
|
headers['Authorization'] = `Bearer ${token}`;
|
|
35
54
|
}
|
|
@@ -46,9 +65,45 @@ function request(method, path, data = null, options = {}) {
|
|
|
46
65
|
const req = client.request(reqOptions, (res) => {
|
|
47
66
|
let body = '';
|
|
48
67
|
res.on('data', chunk => body += chunk);
|
|
49
|
-
res.on('end', () => {
|
|
68
|
+
res.on('end', async () => {
|
|
50
69
|
// Handle non-2xx responses
|
|
51
70
|
if (res.statusCode >= 400) {
|
|
71
|
+
// 401 REFRESH: If unauthorized and haven't retried, try to recover
|
|
72
|
+
if (res.statusCode === 401 && !options._retried && options.auth !== false) {
|
|
73
|
+
console.error('[api] 401 received, attempting token refresh...');
|
|
74
|
+
|
|
75
|
+
// First, reload config from disk (in case token was saved but not pushed to store)
|
|
76
|
+
const freshConfig = reloadConfig();
|
|
77
|
+
const diskToken = freshConfig.getAuthToken();
|
|
78
|
+
|
|
79
|
+
// If disk has a different token, sync it to the auth store
|
|
80
|
+
if (diskToken && diskToken !== authStore.getToken()) {
|
|
81
|
+
console.error('[api] Found newer token on disk, syncing to auth store...');
|
|
82
|
+
authStore.setToken(diskToken);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Now check if we have a fresh token to retry with
|
|
86
|
+
const freshToken = authStore.getToken() || diskToken;
|
|
87
|
+
|
|
88
|
+
// Only retry if we got a different/new token
|
|
89
|
+
if (freshToken && freshToken !== token) {
|
|
90
|
+
console.error('[api] Found fresh token, retrying request...');
|
|
91
|
+
try {
|
|
92
|
+
const retryResult = await request(method, path, data, {
|
|
93
|
+
...options,
|
|
94
|
+
token: freshToken,
|
|
95
|
+
_retried: true
|
|
96
|
+
});
|
|
97
|
+
resolve(retryResult);
|
|
98
|
+
return;
|
|
99
|
+
} catch (retryError) {
|
|
100
|
+
console.error('[api] Retry failed:', retryError.message);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
console.error('[api] No fresh token found, not retrying');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
52
107
|
try {
|
|
53
108
|
const parsed = JSON.parse(body);
|
|
54
109
|
resolve({ success: false, error: parsed.error || `HTTP ${res.statusCode}`, statusCode: res.statusCode });
|
|
@@ -198,7 +253,9 @@ async function getActiveUsers() {
|
|
|
198
253
|
const result = await request('GET', '/api/presence');
|
|
199
254
|
// Combine active and away users
|
|
200
255
|
const users = [...(result.active || []), ...(result.away || [])];
|
|
201
|
-
|
|
256
|
+
|
|
257
|
+
// Map to normalized format
|
|
258
|
+
const mappedUsers = users.map(u => ({
|
|
202
259
|
handle: u.username,
|
|
203
260
|
one_liner: u.workingOn,
|
|
204
261
|
lastSeen: new Date(u.lastSeen).getTime(),
|
|
@@ -219,6 +276,24 @@ async function getActiveUsers() {
|
|
|
219
276
|
awayMessage: u.context?.awayMessage || null,
|
|
220
277
|
awayAt: u.context?.awayAt || null
|
|
221
278
|
}));
|
|
279
|
+
|
|
280
|
+
// Sync presence data to local profiles (non-blocking)
|
|
281
|
+
// This enables discovery to find users by what they're building
|
|
282
|
+
try {
|
|
283
|
+
const profiles = require('./profiles');
|
|
284
|
+
profiles.syncFromPresence(mappedUsers).then(synced => {
|
|
285
|
+
if (synced > 0) {
|
|
286
|
+
// Auto-infer interests for new/updated profiles
|
|
287
|
+
profiles.inferMissingInterests().catch(e =>
|
|
288
|
+
console.error('[presence] interest inference failed:', e.message)
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}).catch(e => console.error('[presence] sync failed:', e.message));
|
|
292
|
+
} catch (e) {
|
|
293
|
+
// Non-fatal: profiles module may not be available in some contexts
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return mappedUsers;
|
|
222
297
|
} catch (e) {
|
|
223
298
|
console.error('Who failed:', e.message);
|
|
224
299
|
return [];
|
|
@@ -313,6 +388,12 @@ async function getInbox(handle) {
|
|
|
313
388
|
// Use unified messages endpoint - returns { inbox, unread, bySender }
|
|
314
389
|
const result = await request('GET', `/api/messages?user=${handle}`);
|
|
315
390
|
|
|
391
|
+
// Check for API errors (auth failures, etc.)
|
|
392
|
+
if (result.success === false) {
|
|
393
|
+
console.error('[getInbox] API error:', result.error, result.message);
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
|
|
316
397
|
// Group messages by sender into thread format
|
|
317
398
|
const bySender = result.bySender || {};
|
|
318
399
|
return Object.entries(bySender).map(([sender, messages]) => ({
|
|
@@ -657,6 +738,29 @@ async function getArtifact(slug) {
|
|
|
657
738
|
}
|
|
658
739
|
}
|
|
659
740
|
|
|
741
|
+
/**
|
|
742
|
+
* Update an artifact by ID or slug
|
|
743
|
+
* @param {string} idOrSlug - Artifact ID or slug
|
|
744
|
+
* @param {Object} artifact - Updated artifact data
|
|
745
|
+
*/
|
|
746
|
+
async function updateArtifact(idOrSlug, artifact) {
|
|
747
|
+
try {
|
|
748
|
+
const result = await request('PUT', `/api/artifacts/${idOrSlug}`, artifact);
|
|
749
|
+
|
|
750
|
+
if (result.success === false) {
|
|
751
|
+
return { success: false, error: result.error || 'Update failed' };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return {
|
|
755
|
+
success: true,
|
|
756
|
+
artifact: result.artifact
|
|
757
|
+
};
|
|
758
|
+
} catch (e) {
|
|
759
|
+
console.error('Update artifact failed:', e.message);
|
|
760
|
+
return { success: false, error: e.message };
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
660
764
|
/**
|
|
661
765
|
* List artifacts
|
|
662
766
|
* @param {Object} options - { scope: 'mine'|'for-me'|'network', handle, limit }
|
|
@@ -751,6 +855,7 @@ module.exports = {
|
|
|
751
855
|
// Artifacts
|
|
752
856
|
createArtifact,
|
|
753
857
|
getArtifact,
|
|
858
|
+
updateArtifact,
|
|
754
859
|
listArtifacts,
|
|
755
860
|
sendArtifactCard,
|
|
756
861
|
|