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
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 });
|
|
@@ -114,8 +169,8 @@ async function registerSession(sessionId, handle, building = null, publicKey = n
|
|
|
114
169
|
// Use server-issued sessionId and token (not client-generated)
|
|
115
170
|
currentSessionId = result.sessionId;
|
|
116
171
|
|
|
117
|
-
// Save token for future authenticated requests
|
|
118
|
-
config.
|
|
172
|
+
// Save token for future authenticated requests (persist to shared config)
|
|
173
|
+
config.savePrivyToken(result.token);
|
|
119
174
|
|
|
120
175
|
console.error(`[vibe] Registered @${handle} with session ${result.sessionId}`);
|
|
121
176
|
} else if (result.success) {
|
|
@@ -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(),
|
|
@@ -214,8 +271,29 @@ async function getActiveUsers() {
|
|
|
214
271
|
branch: u.context?.branch || null,
|
|
215
272
|
repo: u.context?.repo || null,
|
|
216
273
|
error: u.context?.error || null,
|
|
217
|
-
note: u.context?.note || null
|
|
274
|
+
note: u.context?.note || null,
|
|
275
|
+
// Away status
|
|
276
|
+
awayMessage: u.context?.awayMessage || null,
|
|
277
|
+
awayAt: u.context?.awayAt || null
|
|
218
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;
|
|
219
297
|
} catch (e) {
|
|
220
298
|
console.error('Who failed:', e.message);
|
|
221
299
|
return [];
|
|
@@ -230,26 +308,37 @@ async function setVisibility(handle, visible) {
|
|
|
230
308
|
|
|
231
309
|
async function sendMessage(from, to, body, type = 'dm', payload = null) {
|
|
232
310
|
try {
|
|
233
|
-
// AIRC: Create signed message if we have a keypair
|
|
234
|
-
const keypair = config.getKeypair();
|
|
235
|
-
|
|
236
311
|
let data;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// Also include 'text' for backward compat with current API
|
|
247
|
-
if (body) data.text = body;
|
|
312
|
+
|
|
313
|
+
// Check if using Privy auth (server-side signing)
|
|
314
|
+
if (config.hasPrivyAuth()) {
|
|
315
|
+
// NEW: Privy auth flow - server handles signing
|
|
316
|
+
// Just send message data, server signs it
|
|
317
|
+
data = { to, body: body || undefined, text: body };
|
|
318
|
+
if (payload) data.payload = payload;
|
|
319
|
+
|
|
320
|
+
console.error('[vibe] Sending message via Privy auth (server-side signing)');
|
|
248
321
|
} else {
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
322
|
+
// LEGACY: Create signed message if we have a keypair
|
|
323
|
+
const keypair = config.getKeypair();
|
|
324
|
+
|
|
325
|
+
if (keypair) {
|
|
326
|
+
// Full AIRC-compliant signed message
|
|
327
|
+
data = crypto.createSignedMessage({
|
|
328
|
+
from,
|
|
329
|
+
to,
|
|
330
|
+
body: body || undefined,
|
|
331
|
+
payload: payload || undefined
|
|
332
|
+
}, keypair.privateKey);
|
|
333
|
+
|
|
334
|
+
// Also include 'text' for backward compat with current API
|
|
335
|
+
if (body) data.text = body;
|
|
336
|
+
} else {
|
|
337
|
+
// No auth at all - legacy format (no signing)
|
|
338
|
+
data = { from, to, text: body };
|
|
339
|
+
if (payload) {
|
|
340
|
+
data.payload = payload;
|
|
341
|
+
}
|
|
253
342
|
}
|
|
254
343
|
}
|
|
255
344
|
|
|
@@ -258,7 +347,33 @@ async function sendMessage(from, to, body, type = 'dm', payload = null) {
|
|
|
258
347
|
// Handle auth errors
|
|
259
348
|
if (!result.success && result.error?.includes('Authentication')) {
|
|
260
349
|
console.error('[vibe] Auth failed for message. Try `vibe init` to re-register.');
|
|
261
|
-
return
|
|
350
|
+
return { error: 'auth_failed', message: 'Authentication failed. Try `vibe init` to re-register.' };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Handle expired token
|
|
354
|
+
if (result.statusCode === 401) {
|
|
355
|
+
console.error('[vibe] Auth expired. Run browser auth to refresh token.');
|
|
356
|
+
return { error: 'auth_expired', message: 'Auth expired. Run `vibe init` to refresh token.' };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Handle storage errors (KV write failed)
|
|
360
|
+
if (!result.success && result.error === 'storage_error') {
|
|
361
|
+
console.error('[vibe] Storage error:', result.details || result.message);
|
|
362
|
+
return { error: 'storage_error', message: result.message || 'Failed to save message. Please try again.' };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Handle other errors
|
|
366
|
+
if (!result.success && result.error) {
|
|
367
|
+
console.error('[vibe] Send error:', result.error, result.message);
|
|
368
|
+
return { error: result.error, message: result.message || 'Failed to send message.' };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Emit list_changed notification for successful message send
|
|
372
|
+
// This allows other Claude Code instances to see the new message instantly
|
|
373
|
+
if (result.success || result.message) {
|
|
374
|
+
if (global.vibeNotifier) {
|
|
375
|
+
global.vibeNotifier.emitImmediate(); // Immediate for DMs
|
|
376
|
+
}
|
|
262
377
|
}
|
|
263
378
|
|
|
264
379
|
return result.message;
|
|
@@ -270,8 +385,16 @@ async function sendMessage(from, to, body, type = 'dm', payload = null) {
|
|
|
270
385
|
|
|
271
386
|
async function getInbox(handle) {
|
|
272
387
|
try {
|
|
388
|
+
// Use unified messages endpoint - returns { inbox, unread, bySender }
|
|
273
389
|
const result = await request('GET', `/api/messages?user=${handle}`);
|
|
274
|
-
|
|
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
|
+
|
|
397
|
+
// Group messages by sender into thread format
|
|
275
398
|
const bySender = result.bySender || {};
|
|
276
399
|
return Object.entries(bySender).map(([sender, messages]) => ({
|
|
277
400
|
handle: sender,
|
|
@@ -293,6 +416,7 @@ async function getInbox(handle) {
|
|
|
293
416
|
|
|
294
417
|
async function getUnreadCount(handle) {
|
|
295
418
|
try {
|
|
419
|
+
// Use unified messages endpoint - returns { inbox, unread, bySender }
|
|
296
420
|
const result = await request('GET', `/api/messages?user=${handle}`);
|
|
297
421
|
return result.unread || 0;
|
|
298
422
|
} catch (e) {
|
|
@@ -303,6 +427,7 @@ async function getUnreadCount(handle) {
|
|
|
303
427
|
// Get raw inbox messages (for notification checks)
|
|
304
428
|
async function getRawInbox(handle) {
|
|
305
429
|
try {
|
|
430
|
+
// Use unified messages endpoint - returns { inbox, unread, bySender }
|
|
306
431
|
const result = await request('GET', `/api/messages?user=${handle}`);
|
|
307
432
|
return result.inbox || [];
|
|
308
433
|
} catch (e) {
|
|
@@ -315,6 +440,7 @@ async function getThread(myHandle, theirHandle) {
|
|
|
315
440
|
const result = await request('GET', `/api/messages?user=${myHandle}&with=${theirHandle}`);
|
|
316
441
|
return (result.thread || []).map(m => ({
|
|
317
442
|
from: m.from,
|
|
443
|
+
isAgent: m.isAgent || m.is_agent || false, // Support both naming conventions
|
|
318
444
|
body: m.text,
|
|
319
445
|
payload: m.payload || null,
|
|
320
446
|
timestamp: new Date(m.createdAt).getTime(),
|
|
@@ -327,7 +453,8 @@ async function getThread(myHandle, theirHandle) {
|
|
|
327
453
|
}
|
|
328
454
|
|
|
329
455
|
async function markThreadRead(myHandle, theirHandle) {
|
|
330
|
-
//
|
|
456
|
+
// No-op: Backend automatically marks messages as read when getThread() is called
|
|
457
|
+
// See: api/messages.js thread endpoint (GET /api/messages?user=X&with=Y)
|
|
331
458
|
}
|
|
332
459
|
|
|
333
460
|
// ============ CONSENT ============
|
|
@@ -380,6 +507,94 @@ async function blockUser(from, to) {
|
|
|
380
507
|
}
|
|
381
508
|
}
|
|
382
509
|
|
|
510
|
+
// ============ STATS ============
|
|
511
|
+
|
|
512
|
+
async function getStats() {
|
|
513
|
+
try {
|
|
514
|
+
const result = await request('GET', '/api/stats');
|
|
515
|
+
return result;
|
|
516
|
+
} catch (e) {
|
|
517
|
+
console.error('Stats failed:', e.message);
|
|
518
|
+
return { success: false };
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ============ INVITES ============
|
|
523
|
+
|
|
524
|
+
async function generateInviteCode(handle) {
|
|
525
|
+
try {
|
|
526
|
+
const result = await request('POST', '/api/invites', { handle });
|
|
527
|
+
return result;
|
|
528
|
+
} catch (e) {
|
|
529
|
+
console.error('Generate invite failed:', e.message);
|
|
530
|
+
return { success: false, error: e.message };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function getMyInvites(handle) {
|
|
535
|
+
try {
|
|
536
|
+
const result = await request('GET', `/api/invites/my?handle=${handle}`);
|
|
537
|
+
return result;
|
|
538
|
+
} catch (e) {
|
|
539
|
+
console.error('Get invites failed:', e.message);
|
|
540
|
+
return { success: false, error: e.message };
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async function submitReport({ reporter, reported, reason, message_id, details }) {
|
|
545
|
+
const result = await request('POST', '/api/report', {
|
|
546
|
+
reporter,
|
|
547
|
+
reported,
|
|
548
|
+
reason,
|
|
549
|
+
message_id,
|
|
550
|
+
details
|
|
551
|
+
});
|
|
552
|
+
return result;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function checkInviteCode(code) {
|
|
556
|
+
try {
|
|
557
|
+
const result = await request('GET', `/api/invites?code=${code}`);
|
|
558
|
+
return result;
|
|
559
|
+
} catch (e) {
|
|
560
|
+
console.error('Check invite failed:', e.message);
|
|
561
|
+
return { valid: false, error: e.message };
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ============ AUTH ============
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Verify a Privy token with the server
|
|
569
|
+
* @param {string} token - Privy JWT token
|
|
570
|
+
* @returns {Promise<{valid: boolean, handle?: string, error?: string}>}
|
|
571
|
+
*/
|
|
572
|
+
async function verifyPrivyToken(token) {
|
|
573
|
+
try {
|
|
574
|
+
const result = await request('POST', '/api/auth/verify', {}, { token, auth: true });
|
|
575
|
+
|
|
576
|
+
if (result.valid) {
|
|
577
|
+
return {
|
|
578
|
+
valid: true,
|
|
579
|
+
handle: result.handle,
|
|
580
|
+
userId: result.userId,
|
|
581
|
+
github: result.github,
|
|
582
|
+
expiresAt: result.expiresAt
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
valid: false,
|
|
588
|
+
error: result.error || 'Token verification failed'
|
|
589
|
+
};
|
|
590
|
+
} catch (e) {
|
|
591
|
+
return {
|
|
592
|
+
valid: false,
|
|
593
|
+
error: e.message
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
383
598
|
// ============ HELPERS ============
|
|
384
599
|
|
|
385
600
|
function formatTimeAgo(timestamp) {
|
|
@@ -395,6 +610,202 @@ function formatTimeAgo(timestamp) {
|
|
|
395
610
|
return `${Math.floor(seconds / 86400)}d ago`;
|
|
396
611
|
}
|
|
397
612
|
|
|
613
|
+
// ============ AWAY STATUS ============
|
|
614
|
+
|
|
615
|
+
// Local cache for away status (also sent to server via heartbeat)
|
|
616
|
+
let awayStatusCache = null;
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Set away status with optional message
|
|
620
|
+
* @param {string} handle - User handle
|
|
621
|
+
* @param {string} status - 'away' or 'online'
|
|
622
|
+
* @param {string|null} message - Custom away message
|
|
623
|
+
*/
|
|
624
|
+
async function setAwayStatus(handle, status, message = null) {
|
|
625
|
+
const awayAt = new Date().toISOString();
|
|
626
|
+
|
|
627
|
+
// Cache locally
|
|
628
|
+
awayStatusCache = {
|
|
629
|
+
status,
|
|
630
|
+
message,
|
|
631
|
+
awayAt
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
// Send via heartbeat to server
|
|
635
|
+
const one_liner = config.getBuildingMessage?.() || 'Building something';
|
|
636
|
+
await heartbeat(handle, one_liner, {
|
|
637
|
+
mood: 'ā', // AFK emoji
|
|
638
|
+
awayMessage: message,
|
|
639
|
+
awayAt: awayAt
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
return { success: true };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Get current away status
|
|
647
|
+
* @param {string} handle - User handle
|
|
648
|
+
*/
|
|
649
|
+
async function getAwayStatus(handle) {
|
|
650
|
+
return awayStatusCache;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Clear away status (user is back)
|
|
655
|
+
* @param {string} handle - User handle
|
|
656
|
+
*/
|
|
657
|
+
async function clearAwayStatus(handle) {
|
|
658
|
+
const wasAway = awayStatusCache;
|
|
659
|
+
awayStatusCache = null;
|
|
660
|
+
|
|
661
|
+
// Send heartbeat with cleared away status
|
|
662
|
+
const one_liner = config.getBuildingMessage?.() || 'Building something';
|
|
663
|
+
await heartbeat(handle, one_liner, {
|
|
664
|
+
mood: null, // Clear mood
|
|
665
|
+
awayMessage: null,
|
|
666
|
+
awayAt: null
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
return wasAway;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ============ ONBOARDING ============
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Get onboarding checklist status for a user
|
|
676
|
+
* @param {string} handle - User handle
|
|
677
|
+
* @returns {Promise<{success: boolean, tasks: Array, progress: Object}>}
|
|
678
|
+
*/
|
|
679
|
+
async function getChecklistStatus(handle) {
|
|
680
|
+
try {
|
|
681
|
+
const result = await request('GET', `/api/onboarding/checklist?handle=${encodeURIComponent(handle)}`);
|
|
682
|
+
return result;
|
|
683
|
+
} catch (e) {
|
|
684
|
+
console.error('Get checklist status failed:', e.message);
|
|
685
|
+
return { success: false, error: e.message };
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// ============ ARTIFACTS ============
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Create a new artifact
|
|
693
|
+
* @param {Object} artifact - Artifact object with all metadata
|
|
694
|
+
*/
|
|
695
|
+
async function createArtifact(artifact) {
|
|
696
|
+
try {
|
|
697
|
+
const result = await request('POST', '/api/artifacts', artifact);
|
|
698
|
+
|
|
699
|
+
if (result.success === false) {
|
|
700
|
+
return { success: false, error: result.error || 'API request failed' };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (!result.artifact_id) {
|
|
704
|
+
return { success: false, error: 'Invalid API response - missing artifact_id' };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return {
|
|
708
|
+
success: true,
|
|
709
|
+
artifact_id: result.artifact_id,
|
|
710
|
+
slug: result.slug,
|
|
711
|
+
url: result.url
|
|
712
|
+
};
|
|
713
|
+
} catch (e) {
|
|
714
|
+
console.error('Create artifact failed:', e.message);
|
|
715
|
+
return { success: false, error: e.message };
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Get an artifact by slug
|
|
721
|
+
* @param {string} slug - Artifact slug
|
|
722
|
+
*/
|
|
723
|
+
async function getArtifact(slug) {
|
|
724
|
+
try {
|
|
725
|
+
const result = await request('GET', `/api/artifacts/${slug}`);
|
|
726
|
+
|
|
727
|
+
if (result.success === false) {
|
|
728
|
+
return { success: false, error: result.error || 'Not found' };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return {
|
|
732
|
+
success: true,
|
|
733
|
+
artifact: result.artifact
|
|
734
|
+
};
|
|
735
|
+
} catch (e) {
|
|
736
|
+
console.error('Get artifact failed:', e.message);
|
|
737
|
+
return { success: false, error: e.message };
|
|
738
|
+
}
|
|
739
|
+
}
|
|
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
|
+
|
|
764
|
+
/**
|
|
765
|
+
* List artifacts
|
|
766
|
+
* @param {Object} options - { scope: 'mine'|'for-me'|'network', handle, limit }
|
|
767
|
+
*/
|
|
768
|
+
async function listArtifacts(options) {
|
|
769
|
+
try {
|
|
770
|
+
const { scope, handle, limit = 10 } = options;
|
|
771
|
+
const params = new URLSearchParams({ scope, handle, limit: limit.toString() });
|
|
772
|
+
const result = await request('GET', `/api/artifacts?${params}`);
|
|
773
|
+
|
|
774
|
+
if (result.success === false) {
|
|
775
|
+
return { success: false, error: result.error || 'List failed' };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return {
|
|
779
|
+
success: true,
|
|
780
|
+
artifacts: result.artifacts || [],
|
|
781
|
+
total: result.total || 0
|
|
782
|
+
};
|
|
783
|
+
} catch (e) {
|
|
784
|
+
console.error('List artifacts failed:', e.message);
|
|
785
|
+
return { success: false, error: e.message };
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Send artifact card via DM
|
|
791
|
+
* @param {string} to - Recipient handle
|
|
792
|
+
* @param {Object} card - Artifact card data
|
|
793
|
+
*/
|
|
794
|
+
async function sendArtifactCard(to, card) {
|
|
795
|
+
try {
|
|
796
|
+
const from = config.getHandle();
|
|
797
|
+
|
|
798
|
+
// Format as a rich message with artifact card embedded
|
|
799
|
+
const body = `š¦ ${card.preview.creator} shared an artifact with you:\n\n**${card.preview.title}**\n${card.preview.snippet}\n\nš ${card.url}\n\n_${card.context}_`;
|
|
800
|
+
|
|
801
|
+
const result = await sendMessage({ from, to, body });
|
|
802
|
+
return result;
|
|
803
|
+
} catch (e) {
|
|
804
|
+
console.error('Send artifact card failed:', e.message);
|
|
805
|
+
return { success: false, error: e.message };
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
398
809
|
module.exports = {
|
|
399
810
|
// Session
|
|
400
811
|
registerSession,
|
|
@@ -422,6 +833,35 @@ module.exports = {
|
|
|
422
833
|
acceptConsent,
|
|
423
834
|
blockUser,
|
|
424
835
|
|
|
836
|
+
// Stats
|
|
837
|
+
getStats,
|
|
838
|
+
|
|
839
|
+
// Invites
|
|
840
|
+
generateInviteCode,
|
|
841
|
+
getMyInvites,
|
|
842
|
+
checkInviteCode,
|
|
843
|
+
|
|
844
|
+
// Reports
|
|
845
|
+
submitReport,
|
|
846
|
+
|
|
425
847
|
// Helpers
|
|
426
|
-
formatTimeAgo
|
|
848
|
+
formatTimeAgo,
|
|
849
|
+
|
|
850
|
+
// Away Status
|
|
851
|
+
setAwayStatus,
|
|
852
|
+
getAwayStatus,
|
|
853
|
+
clearAwayStatus,
|
|
854
|
+
|
|
855
|
+
// Artifacts
|
|
856
|
+
createArtifact,
|
|
857
|
+
getArtifact,
|
|
858
|
+
updateArtifact,
|
|
859
|
+
listArtifacts,
|
|
860
|
+
sendArtifactCard,
|
|
861
|
+
|
|
862
|
+
// Onboarding
|
|
863
|
+
getChecklistStatus,
|
|
864
|
+
|
|
865
|
+
// Auth
|
|
866
|
+
verifyPrivyToken
|
|
427
867
|
};
|
package/store/index.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Store ā Chooses local or API based on environment
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Default: uses remote API (production behavior)
|
|
5
|
+
* Set VIBE_LOCAL=true to use local JSONL files
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const useLocal = process.env.VIBE_LOCAL === 'true';
|
|
9
9
|
|
|
10
|
-
if (
|
|
11
|
-
module.exports = require('./api');
|
|
12
|
-
module.exports.storage = 'api';
|
|
13
|
-
} else {
|
|
10
|
+
if (useLocal) {
|
|
14
11
|
module.exports = require('./local');
|
|
15
12
|
module.exports.storage = 'local';
|
|
13
|
+
} else {
|
|
14
|
+
module.exports = require('./api');
|
|
15
|
+
module.exports.storage = 'api';
|
|
16
16
|
}
|