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
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 if provided or if we have one stored
32
- const token = options.token || config.getAuthToken();
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.setAuthToken(result.token, result.sessionId);
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
- return users.map(u => ({
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
- if (keypair) {
238
- // Full AIRC-compliant signed message
239
- data = crypto.createSignedMessage({
240
- from,
241
- to,
242
- body: body || undefined,
243
- payload: payload || undefined
244
- }, keypair.privateKey);
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
- // Legacy format (no signing)
250
- data = { from, to, text: body };
251
- if (payload) {
252
- data.payload = payload;
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 null;
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
- // Group messages by sender into threads
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
- // Reading thread via API already marks as read
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
- * If VIBE_API_URL is set, uses remote API
5
- * Otherwise uses local JSONL files
4
+ * Default: uses remote API (production behavior)
5
+ * Set VIBE_LOCAL=true to use local JSONL files
6
6
  */
7
7
 
8
- const useApi = !!process.env.VIBE_API_URL;
8
+ const useLocal = process.env.VIBE_LOCAL === 'true';
9
9
 
10
- if (useApi) {
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
  }